Merge branch 'master' into badge-in-app-checkout

This commit is contained in:
spaced4ndy committed 2026-10-09 15:00:36 +04:00
commit 57ab333a2b
94 files changed
+2255 -405

No files matched your search

+5
View File
@@ -1972,10 +1972,13 @@ enum UserServersError: Decodable {
case storageMissing(protocol: ServerProtocol, user: UserRef?)
case proxyMissing(protocol: ServerProtocol, user: UserRef?)
case duplicateServer(protocol: ServerProtocol, duplicateServer: String, duplicateHost: String)
case tooManyHosts(protocol: ServerProtocol, tooManyHostsServer: String)
case duplicateChatRelayAddress(duplicateChatRelay: String, duplicateAddress: String)
var globalError: String? {
switch self {
case .tooManyHosts:
return globalSMPError
case let .noServers(`protocol`, _):
switch `protocol` {
case .smp: return globalSMPError
@@ -2018,6 +2021,8 @@ enum UserServersError: Decodable {
} else {
return text
}
case let .tooManyHosts(.smp, server):
return String.localizedStringWithFormat(NSLocalizedString("Server %@ has more than two host names.", comment: "servers error"), serverHostname(server))
default:
return nil
}
+2
View File
@@ -935,6 +935,8 @@ final class ChatModel: ObservableObject {
func updateCurrentUser(_ newProfile: Profile, _ preferences: FullPreferences? = nil) {
if let current = currentUser {
currentUser?.profile = toLocalProfile(current.profile.profileId, newProfile, "")
currentUser?.profile.localBadge = current.profile.localBadge
currentUser?.profile.contactDomainVerified = current.profile.contactDomainVerified
if let preferences = preferences {
currentUser?.fullPreferences = preferences
}
+6 -2
View File
@@ -1359,8 +1359,12 @@ func apiUpdateProfile(profile: Profile) async throws -> (Profile, [Contact])? {
switch r {
case .result(.userProfileNoChange): return (profile, [])
case let .result(.userProfileUpdated(_, _, toProfile, updateSummary)): return (toProfile, updateSummary.changedContacts)
case .error(.errorStore(.duplicateName)): return nil;
default: throw r.unexpected
case .error(.errorStore(.duplicateName)), .error(.error(.userExists)): return nil;
default:
await MainActor.run {
showErrorAlert(r.unexpected, NSLocalizedString("Error saving profile", comment: "alert title"))
}
throw r.unexpected
}
}
@@ -767,6 +767,7 @@ struct ComposeView: View {
let connectedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connStatus == .ready && $0.wrapped.activeConn?.connFailedErr == nil }.count
let failedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connFailedErr != nil }.count
let total = relayMembers.count > 0 ? relayMembers.count : hostnames.count
guard total > 0 || chatModel.membersLoaded else { return nil }
let noActiveRelays = connectedCount == 0 && (removedCount + failedCount) == total
return (hostnames, relayMembers, connectedCount, removedCount, failedCount, total, noActiveRelays)
}
@@ -139,7 +139,7 @@ struct UserAddressView: View {
title: Text("Share address with SimpleX contacts?"),
message: Text("Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts."),
primaryButton: .default(Text("Share")) {
setProfileAddress($progressIndicator, true)
setProfileAddress($progressIndicator, true) { shareViaProfile = false }
shareViaProfile = true
}, secondaryButton: .cancel()
)
@@ -318,7 +318,7 @@ struct UserAddressView: View {
alert = .shareOnCreate
progressIndicator = false
} else {
setProfileAddress($progressIndicator, true)
setProfileAddress($progressIndicator, true) { shareViaProfile = false }
shareViaProfile = true
}
} else {
@@ -601,7 +601,7 @@ struct AddressSettingsState: Equatable {
}
}
private func setProfileAddress(_ progressIndicator: Binding<Bool>, _ on: Bool) {
private func setProfileAddress(_ progressIndicator: Binding<Bool>, _ on: Bool, revert: @escaping () -> Void) {
progressIndicator.wrappedValue = true
Task {
do {
@@ -613,7 +613,11 @@ private func setProfileAddress(_ progressIndicator: Binding<Bool>, _ on: Bool) {
await MainActor.run { progressIndicator.wrappedValue = false }
} catch let error {
logger.error("apiSetProfileAddress: \(responseError(error))")
await MainActor.run { progressIndicator.wrappedValue = false }
await MainActor.run {
progressIndicator.wrappedValue = false
revert()
showErrorAlert(error, NSLocalizedString("Error saving profile", comment: "alert title"))
}
}
}
}
@@ -709,7 +713,10 @@ struct UserAddressSettingsView: View {
title: NSLocalizedString("Share", comment: "alert action"),
style: .default,
handler: { _ in
setProfileAddress($progressIndicator, on)
setProfileAddress($progressIndicator, on) {
ignoreShareViaProfileChange = true
shareViaProfile = !on
}
}
)
]}
@@ -731,7 +738,10 @@ struct UserAddressSettingsView: View {
title: NSLocalizedString("Stop sharing", comment: "alert action"),
style: .default,
handler: { _ in
setProfileAddress($progressIndicator, on)
setProfileAddress($progressIndicator, on) {
ignoreShareViaProfileChange = true
shareViaProfile = !on
}
}
)
]}
@@ -36,7 +36,7 @@ struct UserProfile: View {
HStack {
TextField("Enter your name…", text: $profile.displayName)
.focused($focusDisplayName)
if !validDisplayName(profile.displayName) {
if !validNewProfileName {
Button {
alert = .invalidNameError(validName: mkValidName(profile.displayName))
} label: {
@@ -147,10 +147,15 @@ struct UserProfile: View {
(chatModel.currentUser?.profile.description ?? "") != description.trimmingCharacters(in: .whitespacesAndNewlines)
) &&
profile.displayName.trimmingCharacters(in: .whitespaces) != "" &&
validDisplayName(profile.displayName) &&
validNewProfileName &&
bioFitsLimit()
}
private var validNewProfileName: Bool {
profile.displayName == chatModel.currentUser?.profile.displayName
|| validDisplayName(profile.displayName)
}
private func saveProfile() {
focusDisplayName = false
Task {
+1
View File
@@ -598,6 +598,7 @@ public enum RcvSwitchStatus: String, Decodable, Hashable {
case sendingQADD = "sending_qadd"
case sendingQUSE = "sending_quse"
case receivedMessage = "received_message"
case receivedQEND = "received_qend"
}
public struct SndQueueInfo: Decodable, Hashable {
+3 -1
View File
@@ -133,6 +133,7 @@ public struct Profile: Codable, NamedChat, Hashable {
self.image = image
self.contactLink = contactLink
self.preferences = preferences
self.peerType = peerType
self.contactDomain = contactDomain
}
@@ -593,7 +594,8 @@ public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias
contactLink: profile.contactLink,
preferences: profile.preferences,
peerType: profile.peerType,
localAlias: localAlias
localAlias: localAlias,
contactDomain: profile.contactDomain
)
}
@@ -1115,7 +1115,7 @@ object ChatModel {
fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) {
val current = currentUser.value ?: return
val updated = current.copy(
profile = newProfile.toLocalProfile(current.profile.profileId),
profile = newProfile.toLocalProfile(current.profile.profileId).copy(localBadge = current.profile.localBadge, contactDomainVerified = current.profile.contactDomainVerified),
fullPreferences = preferences ?: current.fullPreferences
)
val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId }
@@ -1959,8 +1959,10 @@ object ChatController {
val r = sendCmd(rh, CC.ApiUpdateProfile(userId, profile))
if (r is API.Result && r.res is CR.UserProfileNoChange) return profile to emptyList()
if (r is API.Result && r.res is CR.UserProfileUpdated) return r.res.toProfile to r.res.updateSummary.changedContacts
if (r is API.Error && r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.DuplicateName) {
if (r is API.Error && (r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.DuplicateName || r.err is ChatError.ChatErrorChat && r.err.errorType is ChatErrorType.UserExists)) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc))
} else if (!(networkErrorAlert(r))) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_profile), "${r.responseType}: ${r.details}")
}
Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}")
return null
@@ -1972,7 +1974,12 @@ object ChatController {
return when {
r is API.Result && r.res is CR.UserProfileNoChange -> null
r is API.Result && r.res is CR.UserProfileUpdated -> r.res.user.updateRemoteHostId(rh)
else -> throw Exception("failed to set profile address: ${r.responseType} ${r.details}")
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiSetProfileAddress", generalGetString(MR.strings.error_saving_profile), r)
}
throw Exception("failed to set profile address: ${r.responseType} ${r.details}")
}
}
}
@@ -4948,6 +4955,7 @@ sealed class UserServersError {
@Serializable @SerialName("storageMissing") data class StorageMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
@Serializable @SerialName("proxyMissing") data class ProxyMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
@Serializable @SerialName("duplicateServer") data class DuplicateServer(val protocol: ServerProtocol, val duplicateServer: String, val duplicateHost: String): UserServersError()
@Serializable @SerialName("tooManyHosts") data class TooManyHosts(val protocol: ServerProtocol, val tooManyHostsServer: String): UserServersError()
@Serializable @SerialName("duplicateChatRelayAddress") data class DuplicateChatRelayAddress(val duplicateChatRelay: String, val duplicateAddress: String): UserServersError()
val globalError: String?
@@ -4963,6 +4971,7 @@ sealed class UserServersError {
is StorageMissing -> this.protocol
is ProxyMissing -> this.protocol
is DuplicateServer -> this.protocol
is TooManyHosts -> this.protocol
is DuplicateChatRelayAddress -> null
}
@@ -4978,6 +4987,8 @@ sealed class UserServersError {
is ProxyMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured_for_private_routing)}" }
?: generalGetString(MR.strings.no_message_servers_configured_for_private_routing)
is TooManyHosts -> String.format(generalGetString(MR.strings.server_has_too_many_hosts), ServerAddress.parseServerAddress(this.tooManyHostsServer)?.hostnames?.firstOrNull() ?: this.tooManyHostsServer)
else -> null
}
} else {
@@ -7651,7 +7662,8 @@ enum class RcvSwitchStatus {
@SerialName("switch_started") SwitchStarted,
@SerialName("sending_qadd") SendingQADD,
@SerialName("sending_quse") SendingQUSE,
@SerialName("received_message") ReceivedMessage
@SerialName("received_message") ReceivedMessage,
@SerialName("received_qend") ReceivedQEND
}
@Serializable
@@ -119,6 +119,20 @@ fun ChatView(
}
chat
} }
if (chatsCtx.secondaryContextFilter == null) {
// cleared when the view is gone rather than on back: Android keeps it on screen while it slides out,
// and by then a channel being created may already be using this state
DisposableEffect(Unit) {
onDispose {
if (chatModel.chatId.value == null && chatModel.creatingChannelId.value == null) {
chatModel.groupMembers.value = emptyList()
chatModel.groupMembersIndexes.value = emptyMap()
chatModel.membersLoaded.value = false
ChannelRelaysModel.reset()
}
}
}
}
val user = chatModel.currentUser.value
val chatInfo = activeChat.value?.chatInfo
if (chat == null || chatInfo == null || user == null) {
@@ -382,10 +396,6 @@ fun ChatView(
hideKeyboard(view)
AudioPlayer.stop()
chatModel.chatId.value = null
chatModel.groupMembers.value = emptyList()
chatModel.groupMembersIndexes.value = emptyMap()
chatModel.membersLoaded.value = false
ChannelRelaysModel.reset()
},
info = {
if (ModalManager.end.hasModalsOpen()) {
@@ -2153,7 +2153,8 @@ private fun ownerRelayState(chat: Chat, chatModel: ChatModel): OwnerRelayState?
if (!gInfo.useRelays || gInfo.membership.memberRole != GroupMemberRole.Owner ||
gInfo.membership.memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted)
) return null
val relays = if (ChannelRelaysModel.groupId.value == gInfo.groupId) ChannelRelaysModel.groupRelays.toList() else emptyList()
if (ChannelRelaysModel.groupId.value != gInfo.groupId) return null
val relays = ChannelRelaysModel.groupRelays.toList()
if (relays.isEmpty()) return OwnerRelayState(emptyList(), 0, 0, 0, true)
val relayMembers = relays.map { relay ->
relay to chatModel.groupMembers.value.firstOrNull { it.groupMemberId == relay.groupMemberId }
@@ -2186,6 +2187,7 @@ private fun subscriberRelayState(chat: Chat, chatModel: ChatModel): SubscriberRe
val connectedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connStatus == ConnStatus.Ready && it.activeConn?.connFailedErr == null }
val failedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connFailedErr != null }
val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size
if (total == 0 && !chatModel.membersLoaded.value) return null
val noActiveRelays = connectedCount == 0 && (removedCount + failedCount) == total
return SubscriberRelayState(hostnames, relayMembers, connectedCount, removedCount, failedCount, total, noActiveRelays)
}
@@ -265,10 +265,13 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo
newMember
}
}
chatModel.groupMembersIndexes.value = emptyMap()
chatModel.groupMembers.value = newMembers
chatModel.membersLoaded.value = true
chatModel.populateGroupMembersIndexes()
withContext(Dispatchers.Main) {
if (chatModel.chatId.value != groupInfo.id) return@withContext
chatModel.groupMembersIndexes.value = emptyMap()
chatModel.groupMembers.value = newMembers
chatModel.membersLoaded.value = true
chatModel.populateGroupMembersIndexes()
}
}
@Composable
@@ -63,6 +63,7 @@ fun UserAddressView(
chatModel.updateUser(u)
}
} catch (e: Exception) {
shareViaProfile.value = !on
Log.e(TAG, "UserAddressView apiSetProfileAddress: ${e.stackTraceToString()}")
} finally {
progressIndicator.value = false
@@ -141,6 +141,7 @@
<string name="no_message_servers_configured">No message servers.</string>
<string name="no_message_servers_configured_for_receiving">No servers to receive messages.</string>
<string name="no_message_servers_configured_for_private_routing">No servers for private message routing.</string>
<string name="server_has_too_many_hosts">Server %1$s has more than two host names.</string>
<string name="no_media_servers_configured">No media &amp; file servers.</string>
<string name="no_media_servers_configured_for_sending">No servers to send files.</string>
<string name="no_media_servers_configured_for_private_routing">No servers to receive files.</string>
@@ -255,6 +256,7 @@
<string name="error_preparing_contact">Error opening chat</string>
<string name="error_preparing_group">Error opening group</string>
<string name="error_changing_user">Error changing profile</string>
<string name="error_saving_profile">Error saving profile</string>
<!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Instant notifications</string>