](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) [
](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) [
](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
+[
](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) [
](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) [
](https://www.whonix.org/wiki/Chat#Recommendation) [
](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
## Welcome to SimpleX Chat!
@@ -72,7 +72,7 @@ You must:
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
-You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
+You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FiBkJE72asZX1NUZaYFIeKRVk6oVjb-iv%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAinqu3j74AMjODLoIRR487ZW6ysip_dlpD6Zxk18SPFY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22groupLinkId%22%3A%223wAFGCLygQHR5AwynZOHlQ%3D%3D%22%7D)
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform:
@@ -110,6 +110,15 @@ After you connect, you can [verify connection security code](./blog/20230103-sim
Read about the app features and settings in the new [User guide](./docs/guide/README.md).
+## Contribute
+
+We would love to have you join the development! You can help us with:
+
+- [share the color theme](./docs/THEMES.md) you use in Android app!
+- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
+- contributing to SimpleX Chat knowledge-base.
+- developing features - please connect to us via chat so we can help you get started.
+
## Help translating SimpleX Chat
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps, website and documents are translated to many other languages.
@@ -141,15 +150,6 @@ Join our translators to help SimpleX grow!
Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed – please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
-## Contribute
-
-We would love to have you join the development! You can help us with:
-
-- [share the color theme](./docs/THEMES.md) you use in Android app!
-- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
-- contributing to SimpleX Chat knowledge-base.
-- developing features - please connect to us via chat so we can help you get started.
-
## Please support us with your donations
Huge thank you to everybody who donated to SimpleX Chat!
@@ -169,6 +169,7 @@ It is possible to donate via:
- ETH: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
- USDT (Ethereum): 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
- ZEC: t1fwjQW5gpFhDqXNhxqDWyF9j9WeKvVS5Jg
+- ZEC shielded: u16rnvkflumf5uw9frngc2lymvmzgdr2mmc9unyu0l44unwfmdcpfm0axujd2w34ct3ye709azxsqge45705lpvvqu264ltzvfay55ygyq
- DOGE: D99pV4n9TrPxBPCkQGx4w4SMSa6QjRBxPf
- SOL: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu
- please ask if you want to donate any other coins.
@@ -234,6 +235,10 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent and important updates:
+[Mar 8, 2025. SimpleX Chat v6.3: new user experience and safety in public groups](./blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md)
+
+[Jan 14, 2025. SimpleX network: large groups and privacy-preserving content moderation](./blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md)
+
[Dec 10, 2024. SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md)
[Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md)
@@ -305,12 +310,13 @@ What is already implemented:
15. Manual messaging queue rotations to move conversation to another SMP relay.
16. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
17. Local files encryption.
+18. [Reproducible server builds](./docs/SERVER.md#reproduce-builds).
We plan to add:
1. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
2. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
-3. Reproducible builds – this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code.
+3. Reproducible clients builds – this is a complex problem, but we are aiming to have it in 2025 at least partially.
4. Recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party.
## For developers
diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift
index ad8c661e1c..3f6998c9ec 100644
--- a/apps/ios/Shared/AppDelegate.swift
+++ b/apps/ios/Shared/AppDelegate.swift
@@ -54,7 +54,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
try await apiVerifyToken(token: token, nonce: nonce, code: verification)
m.tokenStatus = .active
} catch {
- if let cr = error as? ChatResponse, case .chatCmdError(_, .errorAgent(.NTF(.AUTH))) = cr {
+ if let cr = error as? ChatError, case .errorAgent(.NTF(.AUTH)) = cr {
m.tokenStatus = .expired
}
logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))")
diff --git a/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/Contents.json
new file mode 100644
index 0000000000..cb29f09fe1
--- /dev/null
+++ b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "vertical_logo_x1.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "vertical_logo_x2.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "vertical_logo_x3.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x1.png b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x1.png
new file mode 100644
index 0000000000..f916e43ea9
Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x1.png differ
diff --git a/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x2.png b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x2.png
new file mode 100644
index 0000000000..bb35878f0c
Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x2.png differ
diff --git a/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x3.png b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x3.png
new file mode 100644
index 0000000000..c55f481b36
Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x3.png differ
diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift
index 652258415e..2ad8d546f2 100644
--- a/apps/ios/Shared/ContentView.swift
+++ b/apps/ios/Shared/ContentView.swift
@@ -11,12 +11,10 @@ import SimpleXChat
private enum NoticesSheet: Identifiable {
case whatsNew(updatedConditions: Bool)
- case updatedConditions
var id: String {
switch self {
case .whatsNew: return "whatsNew"
- case .updatedConditions: return "updatedConditions"
}
}
}
@@ -76,7 +74,7 @@ struct ContentView: View {
}
}
- @ViewBuilder func allViews() -> some View {
+ func allViews() -> some View {
ZStack {
let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
@@ -211,7 +209,7 @@ struct ContentView: View {
}
}
- @ViewBuilder private func activeCallInteractiveArea(_ call: Call) -> some View {
+ private func activeCallInteractiveArea(_ call: Call) -> some View {
HStack {
Text(call.contact.displayName).font(.body).foregroundColor(.white)
Spacer()
@@ -278,18 +276,18 @@ struct ContentView: View {
let showWhatsNew = shouldShowWhatsNew()
let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false
noticesShown = showWhatsNew || showUpdatedConditions
- if showWhatsNew {
+ if showWhatsNew || showUpdatedConditions {
noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions)
- } else if showUpdatedConditions {
- noticesSheetItem = .updatedConditions
}
}
}
}
prefShowLANotice = true
connectViaUrl()
+ showReRegisterTokenAlert()
}
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
+ .onChange(of: chatModel.reRegisterTknStatus) { _ in showReRegisterTokenAlert() }
.sheet(item: $noticesSheetItem) { item in
switch item {
case let .whatsNew(updatedConditions):
@@ -298,13 +296,6 @@ struct ContentView: View {
.if(updatedConditions) { v in
v.task { await setConditionsNotified_() }
}
- case .updatedConditions:
- UsageConditionsView(
- currUserServers: Binding.constant([]),
- userServers: Binding.constant([])
- )
- .modifier(ThemedBackground(grouped: true))
- .task { await setConditionsNotified_() }
}
}
if chatModel.setDeliveryReceipts {
@@ -315,6 +306,12 @@ struct ContentView: View {
.onContinueUserActivity("INStartCallIntent", perform: processUserActivity)
.onContinueUserActivity("INStartAudioCallIntent", perform: processUserActivity)
.onContinueUserActivity("INStartVideoCallIntent", perform: processUserActivity)
+ .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
+ if let url = userActivity.webpageURL {
+ logger.debug("onContinueUserActivity.NSUserActivityTypeBrowsingWeb: \(url)")
+ chatModel.appOpenUrl = url
+ }
+ }
}
private func setConditionsNotified_() async {
@@ -446,12 +443,12 @@ struct ContentView: View {
}
func connectViaUrl() {
- dismissAllSheets() {
- let m = ChatModel.shared
- if let url = m.appOpenUrl {
- m.appOpenUrl = nil
+ let m = ChatModel.shared
+ if let url = m.appOpenUrl {
+ m.appOpenUrl = nil
+ dismissAllSheets() {
var path = url.path
- if (path == "/contact" || path == "/invitation") {
+ if (path == "/contact" || path == "/invitation" || path == "/a" || path == "/c" || path == "/g" || path == "/i") {
path.removeFirst()
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
planAndConnect(
@@ -468,6 +465,21 @@ struct ContentView: View {
}
}
+ func showReRegisterTokenAlert() {
+ dismissAllSheets() {
+ let m = ChatModel.shared
+ if let errorTknStatus = m.reRegisterTknStatus, let token = chatModel.deviceToken {
+ chatModel.reRegisterTknStatus = nil
+ AlertManager.shared.showAlert(Alert(
+ title: Text("Notifications error"),
+ message: Text(tokenStatusInfo(errorTknStatus, register: true)),
+ primaryButton: .default(Text("Register")) { reRegisterToken(token: token) },
+ secondaryButton: .cancel()
+ ))
+ }
+ }
+ }
+
private func showPlanAndConnectAlert(_ alert: PlanAndConnectAlert) {
AlertManager.shared.showAlert(planAndConnectAlert(alert, dismiss: false))
}
diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift
new file mode 100644
index 0000000000..3bf4cb7b56
--- /dev/null
+++ b/apps/ios/Shared/Model/AppAPITypes.swift
@@ -0,0 +1,2281 @@
+//
+// APITypes.swift
+// SimpleX
+//
+// Created by EP on 01/05/2025.
+// Copyright © 2025 SimpleX Chat. All rights reserved.
+//
+
+import SimpleXChat
+import SwiftUI
+
+// some constructors are used in SEChatCommand or NSEChatCommand types as well - they must be syncronised
+enum ChatCommand: ChatCmdProtocol {
+ case showActiveUser
+ case createActiveUser(profile: Profile?, pastTimestamp: Bool)
+ case listUsers
+ case apiSetActiveUser(userId: Int64, viewPwd: String?)
+ case setAllContactReceipts(enable: Bool)
+ case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings)
+ case apiSetUserGroupReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings)
+ case apiHideUser(userId: Int64, viewPwd: String)
+ case apiUnhideUser(userId: Int64, viewPwd: String)
+ case apiMuteUser(userId: Int64)
+ case apiUnmuteUser(userId: Int64)
+ case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?)
+ case startChat(mainApp: Bool, enableSndFiles: Bool)
+ case checkChatRunning
+ case apiStopChat
+ case apiActivateChat(restoreChat: Bool)
+ case apiSuspendChat(timeoutMicroseconds: Int)
+ case apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String)
+ case apiSetEncryptLocalFiles(enable: Bool)
+ case apiExportArchive(config: ArchiveConfig)
+ case apiImportArchive(config: ArchiveConfig)
+ case apiDeleteStorage
+ case apiStorageEncryption(config: DBEncryptionConfig)
+ case testStorageEncryption(key: String)
+ case apiSaveSettings(settings: AppSettings)
+ case apiGetSettings(settings: AppSettings)
+ case apiGetChatTags(userId: Int64)
+ case apiGetChats(userId: Int64)
+ case apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String)
+ case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
+ case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
+ case apiCreateChatTag(tag: ChatTagData)
+ case apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64])
+ case apiDeleteChatTag(tagId: Int64)
+ case apiUpdateChatTag(tagId: Int64, tagData: ChatTagData)
+ case apiReorderChatTags(tagIds: [Int64])
+ case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage])
+ case apiReportMessage(groupId: Int64, chatItemId: Int64, reportReason: ReportReason, reportText: String)
+ case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool)
+ case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode)
+ case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64])
+ case apiArchiveReceivedReports(groupId: Int64)
+ case apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode)
+ case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
+ case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction)
+ case apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64])
+ case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?)
+ case apiGetNtfToken
+ case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
+ case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
+ case apiCheckToken(token: DeviceToken)
+ case apiDeleteToken(token: DeviceToken)
+ case apiGetNtfConns(nonce: String, encNtfInfo: String)
+ case apiGetConnNtfMessages(connMsgReqs: [ConnMsgReq])
+ case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile)
+ case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole)
+ case apiJoinGroup(groupId: Int64)
+ case apiMembersRole(groupId: Int64, memberIds: [Int64], memberRole: GroupMemberRole)
+ case apiBlockMembersForAll(groupId: Int64, memberIds: [Int64], blocked: Bool)
+ case apiRemoveMembers(groupId: Int64, memberIds: [Int64], withMessages: Bool)
+ case apiLeaveGroup(groupId: Int64)
+ case apiListMembers(groupId: Int64)
+ case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile)
+ case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole, short: Bool)
+ case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole)
+ case apiDeleteGroupLink(groupId: Int64)
+ case apiGetGroupLink(groupId: Int64)
+ case apiCreateMemberContact(groupId: Int64, groupMemberId: Int64)
+ case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent)
+ case apiTestProtoServer(userId: Int64, server: String)
+ case apiGetServerOperators
+ case apiSetServerOperators(operators: [ServerOperator])
+ case apiGetUserServers(userId: Int64)
+ case apiSetUserServers(userId: Int64, userServers: [UserOperatorServers])
+ case apiValidateServers(userId: Int64, userServers: [UserOperatorServers])
+ case apiGetUsageConditions
+ case apiSetConditionsNotified(conditionsId: Int64)
+ case apiAcceptConditions(conditionsId: Int64, operatorIds: [Int64])
+ case apiSetChatItemTTL(userId: Int64, seconds: Int64)
+ case apiGetChatItemTTL(userId: Int64)
+ case apiSetChatTTL(userId: Int64, type: ChatType, id: Int64, seconds: Int64?)
+ case apiSetNetworkConfig(networkConfig: NetCfg)
+ case apiGetNetworkConfig
+ case apiSetNetworkInfo(networkInfo: UserNetworkInfo)
+ case reconnectAllServers
+ case reconnectServer(userId: Int64, smpServer: String)
+ case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
+ case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings)
+ case apiContactInfo(contactId: Int64)
+ case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64)
+ case apiContactQueueInfo(contactId: Int64)
+ case apiGroupMemberQueueInfo(groupId: Int64, groupMemberId: Int64)
+ case apiSwitchContact(contactId: Int64)
+ case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
+ case apiAbortSwitchContact(contactId: Int64)
+ case apiAbortSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
+ case apiSyncContactRatchet(contactId: Int64, force: Bool)
+ case apiSyncGroupMemberRatchet(groupId: Int64, groupMemberId: Int64, force: Bool)
+ case apiGetContactCode(contactId: Int64)
+ case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64)
+ case apiVerifyContact(contactId: Int64, connectionCode: String?)
+ case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?)
+ case apiAddContact(userId: Int64, short: Bool, incognito: Bool)
+ case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
+ case apiChangeConnectionUser(connId: Int64, userId: Int64)
+ case apiConnectPlan(userId: Int64, connLink: String)
+ case apiConnect(userId: Int64, incognito: Bool, connLink: CreatedConnLink)
+ case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64)
+ case apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode)
+ case apiClearChat(type: ChatType, id: Int64)
+ case apiListContacts(userId: Int64)
+ case apiUpdateProfile(userId: Int64, profile: Profile)
+ case apiSetContactPrefs(contactId: Int64, preferences: Preferences)
+ case apiSetContactAlias(contactId: Int64, localAlias: String)
+ case apiSetGroupAlias(groupId: Int64, localAlias: String)
+ case apiSetConnectionAlias(connId: Int64, localAlias: String)
+ case apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?)
+ case apiSetChatUIThemes(chatId: String, themes: ThemeModeOverrides?)
+ case apiCreateMyAddress(userId: Int64, short: Bool)
+ case apiDeleteMyAddress(userId: Int64)
+ case apiShowMyAddress(userId: Int64)
+ case apiSetProfileAddress(userId: Int64, on: Bool)
+ case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?)
+ case apiAcceptContact(incognito: Bool, contactReqId: Int64)
+ case apiRejectContact(contactReqId: Int64)
+ // WebRTC calls
+ case apiSendCallInvitation(contact: Contact, callType: CallType)
+ case apiRejectCall(contact: Contact)
+ case apiSendCallOffer(contact: Contact, callOffer: WebRTCCallOffer)
+ case apiSendCallAnswer(contact: Contact, answer: WebRTCSession)
+ case apiSendCallExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
+ case apiEndCall(contact: Contact)
+ case apiGetCallInvitations
+ case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
+ // WebRTC calls /
+ case apiGetNetworkStatuses
+ case apiChatRead(type: ChatType, id: Int64)
+ case apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64])
+ case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
+ case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?)
+ case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?)
+ case cancelFile(fileId: Int64)
+ // remote desktop commands
+ case setLocalDeviceName(displayName: String)
+ case connectRemoteCtrl(xrcpInvitation: String)
+ case findKnownRemoteCtrl
+ case confirmRemoteCtrl(remoteCtrlId: Int64)
+ case verifyRemoteCtrlSession(sessionCode: String)
+ case listRemoteCtrls
+ case stopRemoteCtrl
+ case deleteRemoteCtrl(remoteCtrlId: Int64)
+ case apiUploadStandaloneFile(userId: Int64, file: CryptoFile)
+ case apiDownloadStandaloneFile(userId: Int64, url: String, file: CryptoFile)
+ case apiStandaloneFileInfo(url: String)
+ // misc
+ case showVersion
+ case getAgentSubsTotal(userId: Int64)
+ case getAgentServersSummary(userId: Int64)
+ case resetAgentServersStats
+ case string(String)
+
+ var cmdString: String {
+ get {
+ switch self {
+ case .showActiveUser: return "/u"
+ case let .createActiveUser(profile, pastTimestamp):
+ let user = NewUser(profile: profile, pastTimestamp: pastTimestamp)
+ return "/_create user \(encodeJSON(user))"
+ case .listUsers: return "/users"
+ case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))"
+ case let .setAllContactReceipts(enable): return "/set receipts all \(onOff(enable))"
+ case let .apiSetUserContactReceipts(userId, userMsgReceiptSettings):
+ let umrs = userMsgReceiptSettings
+ return "/_set receipts contacts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))"
+ case let .apiSetUserGroupReceipts(userId, userMsgReceiptSettings):
+ let umrs = userMsgReceiptSettings
+ return "/_set receipts groups \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))"
+ case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))"
+ case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))"
+ case let .apiMuteUser(userId): return "/_mute user \(userId)"
+ case let .apiUnmuteUser(userId): return "/_unmute user \(userId)"
+ case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))"
+ case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))"
+ case .checkChatRunning: return "/_check running"
+ case .apiStopChat: return "/_stop"
+ case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
+ case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
+ case let .apiSetAppFilePaths(filesFolder, tempFolder, assetsFolder): return "/set file paths \(encodeJSON(AppFilePaths(appFilesFolder: filesFolder, appTempFolder: tempFolder, appAssetsFolder: assetsFolder)))"
+ case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))"
+ case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
+ case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
+ case .apiDeleteStorage: return "/_db delete"
+ case let .apiStorageEncryption(cfg): return "/_db encryption \(encodeJSON(cfg))"
+ case let .testStorageEncryption(key): return "/db test key \(key)"
+ case let .apiSaveSettings(settings): return "/_save app settings \(encodeJSON(settings))"
+ case let .apiGetSettings(settings): return "/_get app settings \(encodeJSON(settings))"
+ case let .apiGetChatTags(userId): return "/_get tags \(userId)"
+ case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
+ case let .apiGetChat(chatId, pagination, search): return "/_get chat \(chatId) \(pagination.cmdString)" +
+ (search == "" ? "" : " search=\(search)")
+ case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)"
+ case let .apiSendMessages(type, id, live, ttl, composedMessages):
+ let msgs = encodeJSON(composedMessages)
+ let ttlStr = ttl != nil ? "\(ttl!)" : "default"
+ return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
+ case let .apiCreateChatTag(tag): return "/_create tag \(encodeJSON(tag))"
+ case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id)) \(tagIds.map({ "\($0)" }).joined(separator: ","))"
+ case let .apiDeleteChatTag(tagId): return "/_delete tag \(tagId)"
+ case let .apiUpdateChatTag(tagId, tagData): return "/_update tag \(tagId) \(encodeJSON(tagData))"
+ case let .apiReorderChatTags(tagIds): return "/_reorder tags \(tagIds.map({ "\($0)" }).joined(separator: ","))"
+ case let .apiCreateChatItems(noteFolderId, composedMessages):
+ let msgs = encodeJSON(composedMessages)
+ return "/_create *\(noteFolderId) json \(msgs)"
+ case let .apiReportMessage(groupId, chatItemId, reportReason, reportText):
+ return "/_report #\(groupId) \(chatItemId) reason=\(reportReason) \(reportText)"
+ case let .apiUpdateChatItem(type, id, itemId, um, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(um.cmdString)"
+ case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
+ case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
+ case let .apiArchiveReceivedReports(groupId): return "/_archive reports #\(groupId)"
+ case let .apiDeleteReceivedReports(groupId, itemIds, mode): return "/_delete reports #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
+ case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
+ case let .apiGetReactionMembers(userId, groupId, itemId, reaction): return "/_reaction members \(userId) #\(groupId) \(itemId) \(encodeJSON(reaction))"
+ case let .apiPlanForwardChatItems(type, id, itemIds): return "/_forward plan \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
+ case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl):
+ let ttlStr = ttl != nil ? "\(ttl!)" : "default"
+ return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)"
+ case .apiGetNtfToken: return "/_ntf get "
+ case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
+ case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
+ case let .apiCheckToken(token): return "/_ntf check \(token.cmdString)"
+ case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)"
+ case let .apiGetNtfConns(nonce, encNtfInfo): return "/_ntf conns \(nonce) \(encNtfInfo)"
+ case let .apiGetConnNtfMessages(connMsgReqs): return "/_ntf conn messages \(connMsgReqs.map { $0.cmdString }.joined(separator: ","))"
+ case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))"
+ case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)"
+ case let .apiJoinGroup(groupId): return "/_join #\(groupId)"
+ case let .apiMembersRole(groupId, memberIds, memberRole): return "/_member role #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) \(memberRole.rawValue)"
+ case let .apiBlockMembersForAll(groupId, memberIds, blocked): return "/_block #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) blocked=\(onOff(blocked))"
+ case let .apiRemoveMembers(groupId, memberIds, withMessages): return "/_remove #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) messages=\(onOff(withMessages))"
+ case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)"
+ case let .apiListMembers(groupId): return "/_members #\(groupId)"
+ case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))"
+ case let .apiCreateGroupLink(groupId, memberRole, short): return "/_create link #\(groupId) \(memberRole) short=\(onOff(short))"
+ case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)"
+ case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
+ case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
+ case let .apiCreateMemberContact(groupId, groupMemberId): return "/_create member contact #\(groupId) \(groupMemberId)"
+ case let .apiSendMemberContactInvitation(contactId, mc): return "/_invite member contact @\(contactId) \(mc.cmdString)"
+ case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)"
+ case .apiGetServerOperators: return "/_operators"
+ case let .apiSetServerOperators(operators): return "/_operators \(encodeJSON(operators))"
+ case let .apiGetUserServers(userId): return "/_servers \(userId)"
+ case let .apiSetUserServers(userId, userServers): return "/_servers \(userId) \(encodeJSON(userServers))"
+ case let .apiValidateServers(userId, userServers): return "/_validate_servers \(userId) \(encodeJSON(userServers))"
+ case .apiGetUsageConditions: return "/_conditions"
+ case let .apiSetConditionsNotified(conditionsId): return "/_conditions_notified \(conditionsId)"
+ case let .apiAcceptConditions(conditionsId, operatorIds): return "/_accept_conditions \(conditionsId) \(joinedIds(operatorIds))"
+ case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))"
+ case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
+ case let .apiSetChatTTL(userId, type, id, seconds): return "/_ttl \(userId) \(ref(type, id)) \(chatItemTTLStr(seconds: seconds))"
+ case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
+ case .apiGetNetworkConfig: return "/network"
+ case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))"
+ case .reconnectAllServers: return "/reconnect"
+ case let .reconnectServer(userId, smpServer): return "/reconnect \(userId) \(smpServer)"
+ case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
+ case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))"
+ case let .apiContactInfo(contactId): return "/_info @\(contactId)"
+ case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)"
+ case let .apiContactQueueInfo(contactId): return "/_queue info @\(contactId)"
+ case let .apiGroupMemberQueueInfo(groupId, groupMemberId): return "/_queue info #\(groupId) \(groupMemberId)"
+ case let .apiSwitchContact(contactId): return "/_switch @\(contactId)"
+ case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)"
+ case let .apiAbortSwitchContact(contactId): return "/_abort switch @\(contactId)"
+ case let .apiAbortSwitchGroupMember(groupId, groupMemberId): return "/_abort switch #\(groupId) \(groupMemberId)"
+ case let .apiSyncContactRatchet(contactId, force): if force {
+ return "/_sync @\(contactId) force=on"
+ } else {
+ return "/_sync @\(contactId)"
+ }
+ case let .apiSyncGroupMemberRatchet(groupId, groupMemberId, force): if force {
+ return "/_sync #\(groupId) \(groupMemberId) force=on"
+ } else {
+ return "/_sync #\(groupId) \(groupMemberId)"
+ }
+ case let .apiGetContactCode(contactId): return "/_get code @\(contactId)"
+ case let .apiGetGroupMemberCode(groupId, groupMemberId): return "/_get code #\(groupId) \(groupMemberId)"
+ case let .apiVerifyContact(contactId, .some(connectionCode)): return "/_verify code @\(contactId) \(connectionCode)"
+ case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)"
+ case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)"
+ case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)"
+ case let .apiAddContact(userId, short, incognito): return "/_connect \(userId) short=\(onOff(short)) incognito=\(onOff(incognito))"
+ case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))"
+ case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)"
+ case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)"
+ case let .apiConnect(userId, incognito, connLink): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connLink.connFullLink) \(connLink.connShortLink ?? "")"
+ case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)"
+ case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)"
+ case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
+ case let .apiListContacts(userId): return "/_contacts \(userId)"
+ case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))"
+ case let .apiSetContactPrefs(contactId, preferences): return "/_set prefs @\(contactId) \(encodeJSON(preferences))"
+ case let .apiSetContactAlias(contactId, localAlias): return "/_set alias @\(contactId) \(localAlias.trimmingCharacters(in: .whitespaces))"
+ case let .apiSetGroupAlias(groupId, localAlias): return "/_set alias #\(groupId) \(localAlias.trimmingCharacters(in: .whitespaces))"
+ case let .apiSetConnectionAlias(connId, localAlias): return "/_set alias :\(connId) \(localAlias.trimmingCharacters(in: .whitespaces))"
+ case let .apiSetUserUIThemes(userId, themes): return "/_set theme user \(userId) \(themes != nil ? encodeJSON(themes) : "")"
+ case let .apiSetChatUIThemes(chatId, themes): return "/_set theme \(chatId) \(themes != nil ? encodeJSON(themes) : "")"
+ case let .apiCreateMyAddress(userId, short): return "/_address \(userId) short=\(onOff(short))"
+ case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)"
+ case let .apiShowMyAddress(userId): return "/_show_address \(userId)"
+ case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))"
+ case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))"
+ case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)"
+ case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
+ case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))"
+ case let .apiRejectCall(contact): return "/_call reject @\(contact.apiId)"
+ case let .apiSendCallOffer(contact, callOffer): return "/_call offer @\(contact.apiId) \(encodeJSON(callOffer))"
+ case let .apiSendCallAnswer(contact, answer): return "/_call answer @\(contact.apiId) \(encodeJSON(answer))"
+ case let .apiSendCallExtraInfo(contact, extraInfo): return "/_call extra @\(contact.apiId) \(encodeJSON(extraInfo))"
+ case let .apiEndCall(contact): return "/_call end @\(contact.apiId)"
+ case .apiGetCallInvitations: return "/_call get"
+ case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
+ case .apiGetNetworkStatuses: return "/_network_statuses"
+ case let .apiChatRead(type, id): return "/_read chat \(ref(type, id))"
+ case let .apiChatItemsRead(type, id, itemIds): return "/_read chat items \(ref(type, id)) \(joinedIds(itemIds))"
+ case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
+ case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
+ case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))"
+ case let .cancelFile(fileId): return "/fcancel \(fileId)"
+ case let .setLocalDeviceName(displayName): return "/set device name \(displayName)"
+ case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)"
+ case .findKnownRemoteCtrl: return "/find remote ctrl"
+ case let .confirmRemoteCtrl(rcId): return "/confirm remote ctrl \(rcId)"
+ case let .verifyRemoteCtrlSession(sessCode): return "/verify remote ctrl \(sessCode)"
+ case .listRemoteCtrls: return "/list remote ctrls"
+ case .stopRemoteCtrl: return "/stop remote ctrl"
+ case let .deleteRemoteCtrl(rcId): return "/delete remote ctrl \(rcId)"
+ case let .apiUploadStandaloneFile(userId, file): return "/_upload \(userId) \(file.filePath)"
+ case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)"
+ case let .apiStandaloneFileInfo(link): return "/_download info \(link)"
+ case .showVersion: return "/version"
+ case let .getAgentSubsTotal(userId): return "/get subs total \(userId)"
+ case let .getAgentServersSummary(userId): return "/get servers summary \(userId)"
+ case .resetAgentServersStats: return "/reset servers stats"
+ case let .string(str): return str
+ }
+ }
+ }
+
+ var cmdType: String {
+ get {
+ switch self {
+ case .showActiveUser: return "showActiveUser"
+ case .createActiveUser: return "createActiveUser"
+ case .listUsers: return "listUsers"
+ case .apiSetActiveUser: return "apiSetActiveUser"
+ case .setAllContactReceipts: return "setAllContactReceipts"
+ case .apiSetUserContactReceipts: return "apiSetUserContactReceipts"
+ case .apiSetUserGroupReceipts: return "apiSetUserGroupReceipts"
+ case .apiHideUser: return "apiHideUser"
+ case .apiUnhideUser: return "apiUnhideUser"
+ case .apiMuteUser: return "apiMuteUser"
+ case .apiUnmuteUser: return "apiUnmuteUser"
+ case .apiDeleteUser: return "apiDeleteUser"
+ case .startChat: return "startChat"
+ case .checkChatRunning: return "checkChatRunning"
+ case .apiStopChat: return "apiStopChat"
+ case .apiActivateChat: return "apiActivateChat"
+ case .apiSuspendChat: return "apiSuspendChat"
+ case .apiSetAppFilePaths: return "apiSetAppFilePaths"
+ case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles"
+ case .apiExportArchive: return "apiExportArchive"
+ case .apiImportArchive: return "apiImportArchive"
+ case .apiDeleteStorage: return "apiDeleteStorage"
+ case .apiStorageEncryption: return "apiStorageEncryption"
+ case .testStorageEncryption: return "testStorageEncryption"
+ case .apiSaveSettings: return "apiSaveSettings"
+ case .apiGetSettings: return "apiGetSettings"
+ case .apiGetChatTags: return "apiGetChatTags"
+ case .apiGetChats: return "apiGetChats"
+ case .apiGetChat: return "apiGetChat"
+ case .apiGetChatItemInfo: return "apiGetChatItemInfo"
+ case .apiSendMessages: return "apiSendMessages"
+ case .apiCreateChatTag: return "apiCreateChatTag"
+ case .apiSetChatTags: return "apiSetChatTags"
+ case .apiDeleteChatTag: return "apiDeleteChatTag"
+ case .apiUpdateChatTag: return "apiUpdateChatTag"
+ case .apiReorderChatTags: return "apiReorderChatTags"
+ case .apiCreateChatItems: return "apiCreateChatItems"
+ case .apiReportMessage: return "apiReportMessage"
+ case .apiUpdateChatItem: return "apiUpdateChatItem"
+ case .apiDeleteChatItem: return "apiDeleteChatItem"
+ case .apiConnectContactViaAddress: return "apiConnectContactViaAddress"
+ case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem"
+ case .apiArchiveReceivedReports: return "apiArchiveReceivedReports"
+ case .apiDeleteReceivedReports: return "apiDeleteReceivedReports"
+ case .apiChatItemReaction: return "apiChatItemReaction"
+ case .apiGetReactionMembers: return "apiGetReactionMembers"
+ case .apiPlanForwardChatItems: return "apiPlanForwardChatItems"
+ case .apiForwardChatItems: return "apiForwardChatItems"
+ case .apiGetNtfToken: return "apiGetNtfToken"
+ case .apiRegisterToken: return "apiRegisterToken"
+ case .apiVerifyToken: return "apiVerifyToken"
+ case .apiCheckToken: return "apiCheckToken"
+ case .apiDeleteToken: return "apiDeleteToken"
+ case .apiGetNtfConns: return "apiGetNtfConns"
+ case .apiGetConnNtfMessages: return "apiGetConnNtfMessages"
+ case .apiNewGroup: return "apiNewGroup"
+ case .apiAddMember: return "apiAddMember"
+ case .apiJoinGroup: return "apiJoinGroup"
+ case .apiMembersRole: return "apiMembersRole"
+ case .apiBlockMembersForAll: return "apiBlockMembersForAll"
+ case .apiRemoveMembers: return "apiRemoveMembers"
+ case .apiLeaveGroup: return "apiLeaveGroup"
+ case .apiListMembers: return "apiListMembers"
+ case .apiUpdateGroupProfile: return "apiUpdateGroupProfile"
+ case .apiCreateGroupLink: return "apiCreateGroupLink"
+ case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole"
+ case .apiDeleteGroupLink: return "apiDeleteGroupLink"
+ case .apiGetGroupLink: return "apiGetGroupLink"
+ case .apiCreateMemberContact: return "apiCreateMemberContact"
+ case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation"
+ case .apiTestProtoServer: return "apiTestProtoServer"
+ case .apiGetServerOperators: return "apiGetServerOperators"
+ case .apiSetServerOperators: return "apiSetServerOperators"
+ case .apiGetUserServers: return "apiGetUserServers"
+ case .apiSetUserServers: return "apiSetUserServers"
+ case .apiValidateServers: return "apiValidateServers"
+ case .apiGetUsageConditions: return "apiGetUsageConditions"
+ case .apiSetConditionsNotified: return "apiSetConditionsNotified"
+ case .apiAcceptConditions: return "apiAcceptConditions"
+ case .apiSetChatItemTTL: return "apiSetChatItemTTL"
+ case .apiGetChatItemTTL: return "apiGetChatItemTTL"
+ case .apiSetChatTTL: return "apiSetChatTTL"
+ case .apiSetNetworkConfig: return "apiSetNetworkConfig"
+ case .apiGetNetworkConfig: return "apiGetNetworkConfig"
+ case .apiSetNetworkInfo: return "apiSetNetworkInfo"
+ case .reconnectAllServers: return "reconnectAllServers"
+ case .reconnectServer: return "reconnectServer"
+ case .apiSetChatSettings: return "apiSetChatSettings"
+ case .apiSetMemberSettings: return "apiSetMemberSettings"
+ case .apiContactInfo: return "apiContactInfo"
+ case .apiGroupMemberInfo: return "apiGroupMemberInfo"
+ case .apiContactQueueInfo: return "apiContactQueueInfo"
+ case .apiGroupMemberQueueInfo: return "apiGroupMemberQueueInfo"
+ case .apiSwitchContact: return "apiSwitchContact"
+ case .apiSwitchGroupMember: return "apiSwitchGroupMember"
+ case .apiAbortSwitchContact: return "apiAbortSwitchContact"
+ case .apiAbortSwitchGroupMember: return "apiAbortSwitchGroupMember"
+ case .apiSyncContactRatchet: return "apiSyncContactRatchet"
+ case .apiSyncGroupMemberRatchet: return "apiSyncGroupMemberRatchet"
+ case .apiGetContactCode: return "apiGetContactCode"
+ case .apiGetGroupMemberCode: return "apiGetGroupMemberCode"
+ case .apiVerifyContact: return "apiVerifyContact"
+ case .apiVerifyGroupMember: return "apiVerifyGroupMember"
+ case .apiAddContact: return "apiAddContact"
+ case .apiSetConnectionIncognito: return "apiSetConnectionIncognito"
+ case .apiChangeConnectionUser: return "apiChangeConnectionUser"
+ case .apiConnectPlan: return "apiConnectPlan"
+ case .apiConnect: return "apiConnect"
+ case .apiDeleteChat: return "apiDeleteChat"
+ case .apiClearChat: return "apiClearChat"
+ case .apiListContacts: return "apiListContacts"
+ case .apiUpdateProfile: return "apiUpdateProfile"
+ case .apiSetContactPrefs: return "apiSetContactPrefs"
+ case .apiSetContactAlias: return "apiSetContactAlias"
+ case .apiSetGroupAlias: return "apiSetGroupAlias"
+ case .apiSetConnectionAlias: return "apiSetConnectionAlias"
+ case .apiSetUserUIThemes: return "apiSetUserUIThemes"
+ case .apiSetChatUIThemes: return "apiSetChatUIThemes"
+ case .apiCreateMyAddress: return "apiCreateMyAddress"
+ case .apiDeleteMyAddress: return "apiDeleteMyAddress"
+ case .apiShowMyAddress: return "apiShowMyAddress"
+ case .apiSetProfileAddress: return "apiSetProfileAddress"
+ case .apiAddressAutoAccept: return "apiAddressAutoAccept"
+ case .apiAcceptContact: return "apiAcceptContact"
+ case .apiRejectContact: return "apiRejectContact"
+ case .apiSendCallInvitation: return "apiSendCallInvitation"
+ case .apiRejectCall: return "apiRejectCall"
+ case .apiSendCallOffer: return "apiSendCallOffer"
+ case .apiSendCallAnswer: return "apiSendCallAnswer"
+ case .apiSendCallExtraInfo: return "apiSendCallExtraInfo"
+ case .apiEndCall: return "apiEndCall"
+ case .apiGetCallInvitations: return "apiGetCallInvitations"
+ case .apiCallStatus: return "apiCallStatus"
+ case .apiGetNetworkStatuses: return "apiGetNetworkStatuses"
+ case .apiChatRead: return "apiChatRead"
+ case .apiChatItemsRead: return "apiChatItemsRead"
+ case .apiChatUnread: return "apiChatUnread"
+ case .receiveFile: return "receiveFile"
+ case .setFileToReceive: return "setFileToReceive"
+ case .cancelFile: return "cancelFile"
+ case .setLocalDeviceName: return "setLocalDeviceName"
+ case .connectRemoteCtrl: return "connectRemoteCtrl"
+ case .findKnownRemoteCtrl: return "findKnownRemoteCtrl"
+ case .confirmRemoteCtrl: return "confirmRemoteCtrl"
+ case .verifyRemoteCtrlSession: return "verifyRemoteCtrlSession"
+ case .listRemoteCtrls: return "listRemoteCtrls"
+ case .stopRemoteCtrl: return "stopRemoteCtrl"
+ case .deleteRemoteCtrl: return "deleteRemoteCtrl"
+ case .apiUploadStandaloneFile: return "apiUploadStandaloneFile"
+ case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
+ case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
+ case .showVersion: return "showVersion"
+ case .getAgentSubsTotal: return "getAgentSubsTotal"
+ case .getAgentServersSummary: return "getAgentServersSummary"
+ case .resetAgentServersStats: return "resetAgentServersStats"
+ case .string: return "console command"
+ }
+ }
+ }
+
+ func ref(_ type: ChatType, _ id: Int64) -> String {
+ "\(type.rawValue)\(id)"
+ }
+
+ func joinedIds(_ ids: [Int64]) -> String {
+ ids.map { "\($0)" }.joined(separator: ",")
+ }
+
+ func chatItemTTLStr(seconds: Int64?) -> String {
+ if let seconds = seconds {
+ return String(seconds)
+ } else {
+ return "default"
+ }
+ }
+
+ var obfuscated: ChatCommand {
+ switch self {
+ case let .apiStorageEncryption(cfg):
+ return .apiStorageEncryption(config: DBEncryptionConfig(currentKey: obfuscate(cfg.currentKey), newKey: obfuscate(cfg.newKey)))
+ case let .apiSetActiveUser(userId, viewPwd):
+ return .apiSetActiveUser(userId: userId, viewPwd: obfuscate(viewPwd))
+ case let .apiHideUser(userId, viewPwd):
+ return .apiHideUser(userId: userId, viewPwd: obfuscate(viewPwd))
+ case let .apiUnhideUser(userId, viewPwd):
+ return .apiUnhideUser(userId: userId, viewPwd: obfuscate(viewPwd))
+ case let .apiDeleteUser(userId, delSMPQueues, viewPwd):
+ return .apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: obfuscate(viewPwd))
+ case let .testStorageEncryption(key):
+ return .testStorageEncryption(key: obfuscate(key))
+ default: return self
+ }
+ }
+
+ private func obfuscate(_ s: String) -> String {
+ s == "" ? "" : "***"
+ }
+
+ private func obfuscate(_ s: String?) -> String? {
+ if let s = s {
+ return obfuscate(s)
+ }
+ return nil
+ }
+
+ private func onOffParam(_ param: String, _ b: Bool?) -> String {
+ if let b = b {
+ return " \(param)=\(onOff(b))"
+ }
+ return ""
+ }
+
+ private func maybePwd(_ pwd: String?) -> String {
+ pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd)
+ }
+}
+
+// ChatResponse is split to three enums to reduce stack size used when parsing it, parsing large enums is very inefficient.
+enum ChatResponse0: Decodable, ChatAPIResult {
+ case activeUser(user: User)
+ case usersList(users: [UserInfo])
+ case chatStarted
+ case chatRunning
+ case chatStopped
+ case apiChats(user: UserRef, chats: [ChatData])
+ case apiChat(user: UserRef, chat: ChatData, navInfo: NavigationInfo?)
+ case chatTags(user: UserRef, userTags: [ChatTag])
+ case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo)
+ case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?)
+ case serverOperatorConditions(conditions: ServerOperatorConditions)
+ case userServers(user: UserRef, userServers: [UserOperatorServers])
+ case userServersValidation(user: UserRef, serverErrors: [UserServersError])
+ case usageConditions(usageConditions: UsageConditions, conditionsText: String, acceptedConditions: UsageConditions?)
+ case chatItemTTL(user: UserRef, chatItemTTL: Int64?)
+ case networkConfig(networkConfig: NetCfg)
+ case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?)
+ case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
+ case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: ServerQueueInfo)
+ case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
+ case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
+ case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
+ case groupMemberSwitchAborted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
+ case contactRatchetSyncStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
+ case groupMemberRatchetSyncStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
+ case contactCode(user: UserRef, contact: Contact, connectionCode: String)
+ case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
+ case connectionVerified(user: UserRef, verified: Bool, expectedCode: String)
+ case tagsUpdated(user: UserRef, userTags: [ChatTag], chatTags: [Int64])
+
+ var responseType: String {
+ switch self {
+ case .activeUser: "activeUser"
+ case .usersList: "usersList"
+ case .chatStarted: "chatStarted"
+ case .chatRunning: "chatRunning"
+ case .chatStopped: "chatStopped"
+ case .apiChats: "apiChats"
+ case .apiChat: "apiChat"
+ case .chatTags: "chatTags"
+ case .chatItemInfo: "chatItemInfo"
+ case .serverTestResult: "serverTestResult"
+ case .serverOperatorConditions: "serverOperators"
+ case .userServers: "userServers"
+ case .userServersValidation: "userServersValidation"
+ case .usageConditions: "usageConditions"
+ case .chatItemTTL: "chatItemTTL"
+ case .networkConfig: "networkConfig"
+ case .contactInfo: "contactInfo"
+ case .groupMemberInfo: "groupMemberInfo"
+ case .queueInfo: "queueInfo"
+ case .contactSwitchStarted: "contactSwitchStarted"
+ case .groupMemberSwitchStarted: "groupMemberSwitchStarted"
+ case .contactSwitchAborted: "contactSwitchAborted"
+ case .groupMemberSwitchAborted: "groupMemberSwitchAborted"
+ case .contactRatchetSyncStarted: "contactRatchetSyncStarted"
+ case .groupMemberRatchetSyncStarted: "groupMemberRatchetSyncStarted"
+ case .contactCode: "contactCode"
+ case .groupMemberCode: "groupMemberCode"
+ case .connectionVerified: "connectionVerified"
+ case .tagsUpdated: "tagsUpdated"
+ }
+ }
+
+ var details: String {
+ switch self {
+ case let .activeUser(user): return String(describing: user)
+ case let .usersList(users): return String(describing: users)
+ case .chatStarted: return noDetails
+ case .chatRunning: return noDetails
+ case .chatStopped: return noDetails
+ case let .apiChats(u, chats): return withUser(u, String(describing: chats))
+ case let .apiChat(u, chat, navInfo): return withUser(u, "chat: \(String(describing: chat))\nnavInfo: \(String(describing: navInfo))")
+ case let .chatTags(u, userTags): return withUser(u, "userTags: \(String(describing: userTags))")
+ case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))")
+ case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))")
+ case let .serverOperatorConditions(conditions): return "conditions: \(String(describing: conditions))"
+ case let .userServers(u, userServers): return withUser(u, "userServers: \(String(describing: userServers))")
+ case let .userServersValidation(u, serverErrors): return withUser(u, "serverErrors: \(String(describing: serverErrors))")
+ case let .usageConditions(usageConditions, _, acceptedConditions): return "usageConditions: \(String(describing: usageConditions))\nacceptedConditions: \(String(describing: acceptedConditions))"
+ case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL))
+ case let .networkConfig(networkConfig): return String(describing: networkConfig)
+ case let .contactInfo(u, contact, connectionStats_, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats_: \(String(describing: connectionStats_))\ncustomUserProfile: \(String(describing: customUserProfile))")
+ case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))")
+ case let .queueInfo(u, rcvMsgInfo, queueInfo):
+ let msgInfo = if let info = rcvMsgInfo { encodeJSON(info) } else { "none" }
+ return withUser(u, "rcvMsgInfo: \(msgInfo)\nqueueInfo: \(encodeJSON(queueInfo))")
+ case let .contactSwitchStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
+ case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
+ case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
+ case let .groupMemberSwitchAborted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
+ case let .contactRatchetSyncStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
+ case let .groupMemberRatchetSyncStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
+ case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
+ case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
+ case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
+ case let .tagsUpdated(u, userTags, chatTags): return withUser(u, "userTags: \(String(describing: userTags))\nchatTags: \(String(describing: chatTags))")
+ }
+ }
+
+ static func fallbackResult(_ type: String, _ json: NSDictionary) -> ChatResponse0? {
+ if type == "apiChats" {
+ if let r = parseApiChats(json) {
+ return .apiChats(user: r.user, chats: r.chats)
+ }
+ } else if type == "apiChat" {
+ if let jApiChat = json["apiChat"] as? NSDictionary,
+ let user: UserRef = try? decodeObject(jApiChat["user"] as Any),
+ let jChat = jApiChat["chat"] as? NSDictionary,
+ let (chat, navInfo) = try? parseChatData(jChat, jApiChat["navInfo"] as? NSDictionary) {
+ return .apiChat(user: user, chat: chat, navInfo: navInfo)
+ }
+ }
+ return nil
+ }
+}
+
+enum ChatResponse1: Decodable, ChatAPIResult {
+ case invitation(user: UserRef, connLinkInvitation: CreatedConnLink, connection: PendingContactConnection)
+ case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
+ case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef)
+ case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan)
+ case sentConfirmation(user: UserRef, connection: PendingContactConnection)
+ case sentInvitation(user: UserRef, connection: PendingContactConnection)
+ case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?)
+ case contactAlreadyExists(user: UserRef, contact: Contact)
+ case contactDeleted(user: UserRef, contact: Contact)
+ case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
+ case groupDeletedUser(user: UserRef, groupInfo: GroupInfo)
+ case chatCleared(user: UserRef, chatInfo: ChatInfo)
+ case userProfileNoChange(user: User)
+ case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary)
+ case userPrivacy(user: User, updatedUser: User)
+ case contactAliasUpdated(user: UserRef, toContact: Contact)
+ case groupAliasUpdated(user: UserRef, toGroup: GroupInfo)
+ case connectionAliasUpdated(user: UserRef, toConnection: PendingContactConnection)
+ case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact)
+ case userContactLink(user: User, contactLink: UserContactLink)
+ case userContactLinkUpdated(user: User, contactLink: UserContactLink)
+ case userContactLinkCreated(user: User, connLinkContact: CreatedConnLink)
+ case userContactLinkDeleted(user: User)
+ case acceptingContactRequest(user: UserRef, contact: Contact)
+ case contactRequestRejected(user: UserRef)
+ case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
+ case newChatItems(user: UserRef, chatItems: [AChatItem])
+ case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: SetHi!
Connect to me via SimpleX Chat
- """, comment: "email text"), simplexChatLink(userAddress.connReqContact)) + """, comment: "email text"), simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: false))) MailView( isShowing: self.$showMailView, result: $mailViewResult, diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index b2b1b8fa68..8f448dc508 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -23,7 +23,7 @@ struct OnboardingView: View { case .step3_CreateSimpleXAddress: // deprecated CreateSimpleXAddress() case .step3_ChooseServerOperators: - ChooseServerOperators(onboarding: true) + OnboardingConditionsView() .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) case .step4_SetNotificationsMode: @@ -44,7 +44,7 @@ enum OnboardingStage: String, Identifiable { case step1_SimpleXInfo case step2_CreateProfile // deprecated case step3_CreateSimpleXAddress // deprecated - case step3_ChooseServerOperators + case step3_ChooseServerOperators // changed to simplified conditions case step4_SetNotificationsMode case onboardingComplete diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 97e1f49382..31865e7af9 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -17,7 +17,7 @@ struct SetNotificationsMode: View { var body: some View { GeometryReader { g in - ScrollView { + let v = ScrollView { VStack(alignment: .center, spacing: 20) { Text("Push notifications") .font(.largeTitle) @@ -57,11 +57,17 @@ struct SetNotificationsMode: View { .padding(25) .frame(minHeight: g.size.height) } + if #available(iOS 16.4, *) { + v.scrollBounceBehavior(.basedOnSize) + } else { + v + } } .frame(maxHeight: .infinity) .sheet(isPresented: $showInfo) { NotificationsInfoView() } + .navigationBarHidden(true) // necessary on iOS 15 } private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) { diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index a8704e964b..9f41a37b1d 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -18,7 +18,7 @@ struct SimpleXInfo: View { var body: some View { GeometryReader { g in - ScrollView { + let v = ScrollView { VStack(alignment: .leading) { VStack(alignment: .center, spacing: 10) { Image(colorScheme == .light ? "logo" : "logo-light") @@ -36,7 +36,7 @@ struct SimpleXInfo: View { .font(.headline) } } - + Spacer() VStack(alignment: .leading) { @@ -66,6 +66,9 @@ struct SimpleXInfo: View { } } } + .padding(.horizontal, 25) + .padding(.top, 75) + .padding(.bottom, 25) .frame(minHeight: g.size.height) } .sheet(isPresented: Binding( @@ -88,11 +91,17 @@ struct SimpleXInfo: View { createProfileNavLinkActive: $createProfileNavLinkActive ) } + if #available(iOS 16.4, *) { + v.scrollBounceBehavior(.basedOnSize) + } else { + v + } + } + .onAppear() { + setLastVersionDefault() } .frame(maxHeight: .infinity) - .padding(.horizontal, 25) - .padding(.top, 75) - .padding(.bottom, 25) + .navigationBarHidden(true) // necessary on iOS 15 } private func onboardingInfoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View { @@ -126,6 +135,7 @@ struct SimpleXInfo: View { NavigationLink(isActive: $createProfileNavLinkActive) { CreateFirstProfile() + .modifier(ThemedBackground()) } label: { EmptyView() } @@ -137,6 +147,8 @@ struct SimpleXInfo: View { let textSpace = Text(verbatim: " ") +let textNewLine = Text(verbatim: "\n") + struct SimpleXInfo_Previews: PreviewProvider { static var previews: some View { SimpleXInfo(onboarding: true) diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 182c5652d7..f65a21623a 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -539,7 +539,46 @@ private let versionDescriptions: [VersionDescription] = [ description: "Delivered even when Apple drops them." )), ] - ) + ), + VersionDescription( + version: "v6.3", + post: URL(string: "https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html"), + features: [ + .feature(Description( + icon: "at", + title: "Mention members 👋", + description: "Get notified when mentioned." + )), + .feature(Description( + icon: "flag", + title: "Send private reports", + description: "Help admins moderating their groups." + )), + .feature(Description( + icon: "list.bullet", + title: "Organize chats into lists", + description: "Don't miss important messages." + )), + .feature(Description( + icon: nil, + title: "Better privacy and security", + description: nil, + subfeatures: [ + ("eye.slash", "Private media file names."), + ("trash", "Set message expiration in chats.") + ] + )), + .feature(Description( + icon: nil, + title: "Better groups performance", + description: nil, + subfeatures: [ + ("bolt", "Faster sending messages."), + ("person.2.slash", "Faster deletion of groups.") + ] + )), + ] + ), ] private let lastVersion = versionDescriptions.last!.version @@ -555,8 +594,6 @@ func shouldShowWhatsNew() -> Bool { } fileprivate struct NewOperatorsView: View { - @State private var showOperatorsSheet = false - var body: some View { VStack(alignment: .leading) { Image((operatorsInfo[.flux] ?? ServerOperator.dummyOperatorInfo).largeLogo) @@ -567,16 +604,7 @@ fileprivate struct NewOperatorsView: View { .multilineTextAlignment(.leading) .lineLimit(10) HStack { - Button("Enable Flux") { - showOperatorsSheet = true - } - Text("for better metadata privacy.") - } - } - .sheet(isPresented: $showOperatorsSheet) { - NavigationView { - ChooseServerOperators(onboarding: false) - .modifier(ThemedBackground()) + Text("Enable Flux in Network & servers settings for better metadata privacy.") } } } diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 67020e09e7..01b25baed8 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -456,12 +456,12 @@ struct ConnectDesktopView: View { } } catch let e { await MainActor.run { - switch e as? ChatResponse { - case .chatCmdError(_, .errorRemoteCtrl(.badInvitation)): alert = .badInvitationError - case .chatCmdError(_, .error(.commandError)): alert = .badInvitationError - case let .chatCmdError(_, .errorRemoteCtrl(.badVersion(v))): alert = .badVersionError(version: v) - case .chatCmdError(_, .errorAgent(.RCP(.version))): alert = .badVersionError(version: nil) - case .chatCmdError(_, .errorAgent(.RCP(.ctrlAuth))): alert = .desktopDisconnectedError + switch e as? ChatError { + case .errorRemoteCtrl(.badInvitation): alert = .badInvitationError + case .error(.commandError): alert = .badInvitationError + case let .errorRemoteCtrl(.badVersion(v)): alert = .badVersionError(version: v) + case .errorAgent(.RCP(.version)): alert = .badVersionError(version: nil) + case .errorAgent(.RCP(.ctrlAuth)): alert = .desktopDisconnectedError default: errorAlert(e) } } diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 23e1f783f7..554219eb69 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -18,7 +18,9 @@ struct TerminalView: View { @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State var composeState: ComposeState = ComposeState() + @State var selectedRange = NSRange() @State private var keyboardVisible = false + @State private var keyboardHiddenDate = Date.now @State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) @State private var terminalItem: TerminalItem? @State private var scrolled = false @@ -96,10 +98,12 @@ struct TerminalView: View { SendMessageView( composeState: $composeState, + selectedRange: $selectedRange, sendMessage: { _ in consoleSendMessage() }, showVoiceMessageButton: false, onMediaAdded: { _ in }, - keyboardVisible: $keyboardVisible + keyboardVisible: $keyboardVisible, + keyboardHiddenDate: $keyboardHiddenDate ) .padding(.horizontal, 12) } @@ -141,18 +145,18 @@ struct TerminalView: View { } func consoleSendMessage() { - let cmd = ChatCommand.string(composeState.message) if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) { - let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty"))) + let resp: APIResult