Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f8eb1d638 | |||
| 5a016378a8 | |||
| bac4e61997 | |||
| f3eeb9dcc2 | |||
| 240ca30f91 | |||
| 835944ab24 | |||
| d53ef24bf1 | |||
| e75be71d9a | |||
| 7fa2f2f72e | |||
| 5fd8e6e4fe | |||
| d3b255b7cb | |||
| 96fba950ff | |||
| 4a404f14d9 | |||
| 4f893d9502 | |||
| 01447716fa | |||
| c9df591e52 | |||
| a56bc6760b | |||
| 80690326cb | |||
| 3f6c74f975 | |||
| 8b8846c7b7 | |||
| 6c78bbc178 | |||
| eaf720a0f2 | |||
| 0e7d81681f | |||
| 56fcaf514e | |||
| 49bd866c4b | |||
| 8660bf420a | |||
| 60a73a539e | |||
| 93d56a25bc | |||
| 7fb3c4abdb | |||
| 191d833947 | |||
| 19ca4f7447 | |||
| b86b5578de | |||
| 435ea9a453 | |||
| 1f93d91af5 | |||
| 405348732b | |||
| 109b6e0cff | |||
| b403201310 | |||
| 9ff11f886e |
@@ -270,7 +270,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Unix test
|
- name: Unix test
|
||||||
if: matrix.os != 'windows-latest'
|
if: matrix.os != 'windows-latest'
|
||||||
timeout-minutes: 30
|
timeout-minutes: 40
|
||||||
shell: bash
|
shell: bash
|
||||||
run: cabal test --test-show-details=direct
|
run: cabal test --test-show-details=direct
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ You can use SimpleX with your own servers and still communicate with people usin
|
|||||||
|
|
||||||
Recent and important updates:
|
Recent and important updates:
|
||||||
|
|
||||||
|
[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
|
||||||
|
|
||||||
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
||||||
|
|
||||||
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
|
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ final class ChatModel: ObservableObject {
|
|||||||
@Published var remoteCtrlSession: RemoteCtrlSession?
|
@Published var remoteCtrlSession: RemoteCtrlSession?
|
||||||
// currently showing invitation
|
// currently showing invitation
|
||||||
@Published var showingInvitation: ShowingInvitation?
|
@Published var showingInvitation: ShowingInvitation?
|
||||||
|
@Published var migrationState: MigrationToState? = MigrationToDeviceState.makeMigrationState()
|
||||||
// audio recording and playback
|
// audio recording and playback
|
||||||
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
|
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
|
||||||
@Published var draft: ComposeState?
|
@Published var draft: ComposeState?
|
||||||
|
|||||||
@@ -90,12 +90,12 @@ private func withBGTask<T>(bgDelay: Double? = nil, f: @escaping () -> T) -> T {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) -> ChatResponse {
|
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) -> ChatResponse {
|
||||||
logger.debug("chatSendCmd \(cmd.cmdType)")
|
logger.debug("chatSendCmd \(cmd.cmdType)")
|
||||||
let start = Date.now
|
let start = Date.now
|
||||||
let resp = bgTask
|
let resp = bgTask
|
||||||
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd) }
|
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) }
|
||||||
: sendSimpleXCmd(cmd)
|
: sendSimpleXCmd(cmd, ctrl)
|
||||||
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
||||||
if case let .response(_, json) = resp {
|
if case let .response(_, json) = resp {
|
||||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||||
@@ -106,24 +106,24 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) async -> ChatResponse {
|
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) async -> ChatResponse {
|
||||||
await withCheckedContinuation { cont in
|
await withCheckedContinuation { cont in
|
||||||
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay))
|
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatRecvMsg() async -> ChatResponse? {
|
func chatRecvMsg(_ ctrl: chat_ctrl? = nil) async -> ChatResponse? {
|
||||||
await withCheckedContinuation { cont in
|
await withCheckedContinuation { cont in
|
||||||
_ = withBGTask(bgDelay: msgDelay) { () -> ChatResponse? in
|
_ = withBGTask(bgDelay: msgDelay) { () -> ChatResponse? in
|
||||||
let resp = recvSimpleXMsg()
|
let resp = recvSimpleXMsg(ctrl)
|
||||||
cont.resume(returning: resp)
|
cont.resume(returning: resp)
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGetActiveUser() throws -> User? {
|
func apiGetActiveUser(ctrl: chat_ctrl? = nil) throws -> User? {
|
||||||
let r = chatSendCmdSync(.showActiveUser)
|
let r = chatSendCmdSync(.showActiveUser, ctrl)
|
||||||
switch r {
|
switch r {
|
||||||
case let .activeUser(user): return user
|
case let .activeUser(user): return user
|
||||||
case .chatCmdError(_, .error(.noActiveUser)): return nil
|
case .chatCmdError(_, .error(.noActiveUser)): return nil
|
||||||
@@ -131,8 +131,8 @@ func apiGetActiveUser() throws -> User? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiCreateActiveUser(_ p: Profile?, sameServers: Bool = false, pastTimestamp: Bool = false) throws -> User {
|
func apiCreateActiveUser(_ p: Profile?, sameServers: Bool = false, pastTimestamp: Bool = false, ctrl: chat_ctrl? = nil) throws -> User {
|
||||||
let r = chatSendCmdSync(.createActiveUser(profile: p, sameServers: sameServers, pastTimestamp: pastTimestamp))
|
let r = chatSendCmdSync(.createActiveUser(profile: p, sameServers: sameServers, pastTimestamp: pastTimestamp), ctrl)
|
||||||
if case let .activeUser(user) = r { return user }
|
if case let .activeUser(user) = r { return user }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
@@ -210,8 +210,8 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiStartChat() throws -> Bool {
|
func apiStartChat(ctrl: chat_ctrl? = nil) throws -> Bool {
|
||||||
let r = chatSendCmdSync(.startChat(mainApp: true))
|
let r = chatSendCmdSync(.startChat(mainApp: true), ctrl)
|
||||||
switch r {
|
switch r {
|
||||||
case .chatStarted: return true
|
case .chatStarted: return true
|
||||||
case .chatRunning: return false
|
case .chatRunning: return false
|
||||||
@@ -240,14 +240,14 @@ func apiSuspendChat(timeoutMicroseconds: Int) {
|
|||||||
logger.error("apiSuspendChat error: \(String(describing: r))")
|
logger.error("apiSuspendChat error: \(String(describing: r))")
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiSetTempFolder(tempFolder: String) throws {
|
func apiSetTempFolder(tempFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||||
let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder))
|
let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder), ctrl)
|
||||||
if case .cmdOk = r { return }
|
if case .cmdOk = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiSetFilesFolder(filesFolder: String) throws {
|
func apiSetFilesFolder(filesFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||||
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder))
|
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder), ctrl)
|
||||||
if case .cmdOk = r { return }
|
if case .cmdOk = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
@@ -258,15 +258,27 @@ func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiSetPQEnabled(_ enable: Bool) throws {
|
func apiSaveAppSettings(settings: AppSettings) throws {
|
||||||
let r = chatSendCmdSync(.apiSetPQEnabled(enable: enable))
|
let r = chatSendCmdSync(.apiSaveSettings(settings: settings))
|
||||||
if case .cmdOk = r { return }
|
if case .cmdOk = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiAllowContactPQ(_ contactId: Int64) async throws -> Contact {
|
func apiGetAppSettings(settings: AppSettings) throws -> AppSettings {
|
||||||
let r = await chatSendCmd(.apiAllowContactPQ(contactId: contactId))
|
let r = chatSendCmdSync(.apiGetSettings(settings: settings))
|
||||||
if case let .contactPQAllowed(_, contact) = r { return contact }
|
if case let .appSettings(settings) = r { return settings }
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiSetPQEncryption(_ enable: Bool) throws {
|
||||||
|
let r = chatSendCmdSync(.apiSetPQEncryption(enable: enable))
|
||||||
|
if case .cmdOk = r { return }
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiSetContactPQ(_ contactId: Int64, _ enable: Bool) async throws -> Contact {
|
||||||
|
let r = await chatSendCmd(.apiSetContactPQ(contactId: contactId, enable: enable))
|
||||||
|
if case let .contactPQAllowed(_, contact, _) = r { return contact }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +300,10 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th
|
|||||||
try await sendCommandOkResp(.apiStorageEncryption(config: DBEncryptionConfig(currentKey: currentKey, newKey: newKey)))
|
try await sendCommandOkResp(.apiStorageEncryption(config: DBEncryptionConfig(currentKey: currentKey, newKey: newKey)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testStorageEncryption(key: String, _ ctrl: chat_ctrl? = nil) async throws {
|
||||||
|
try await sendCommandOkResp(.testStorageEncryption(key: key), ctrl)
|
||||||
|
}
|
||||||
|
|
||||||
func apiGetChats() throws -> [ChatData] {
|
func apiGetChats() throws -> [ChatData] {
|
||||||
let userId = try currentUserId("apiGetChats")
|
let userId = try currentUserId("apiGetChats")
|
||||||
return try apiChatsResponse(chatSendCmdSync(.apiGetChats(userId: userId)))
|
return try apiChatsResponse(chatSendCmdSync(.apiGetChats(userId: userId)))
|
||||||
@@ -510,8 +526,8 @@ func getNetworkConfig() async throws -> NetCfg? {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func setNetworkConfig(_ cfg: NetCfg) throws {
|
func setNetworkConfig(_ cfg: NetCfg, ctrl: chat_ctrl? = nil) throws {
|
||||||
let r = chatSendCmdSync(.apiSetNetworkConfig(networkConfig: cfg))
|
let r = chatSendCmdSync(.apiSetNetworkConfig(networkConfig: cfg), ctrl)
|
||||||
if case .cmdOk = r { return }
|
if case .cmdOk = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
@@ -876,6 +892,36 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
|
|||||||
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
|
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func uploadStandaloneFile(user: any UserLike, file: CryptoFile, ctrl: chat_ctrl? = nil) async -> (FileTransferMeta?, String?) {
|
||||||
|
let r = await chatSendCmd(.apiUploadStandaloneFile(userId: user.userId, file: file), ctrl)
|
||||||
|
if case let .sndStandaloneFileCreated(_, fileTransferMeta) = r {
|
||||||
|
return (fileTransferMeta, nil)
|
||||||
|
} else {
|
||||||
|
logger.error("uploadStandaloneFile error: \(String(describing: r))")
|
||||||
|
return (nil, String(describing: r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadStandaloneFile(user: any UserLike, url: String, file: CryptoFile, ctrl: chat_ctrl? = nil) async -> (RcvFileTransfer?, String?) {
|
||||||
|
let r = await chatSendCmd(.apiDownloadStandaloneFile(userId: user.userId, url: url, file: file), ctrl)
|
||||||
|
if case let .rcvStandaloneFileCreated(_, rcvFileTransfer) = r {
|
||||||
|
return (rcvFileTransfer, nil)
|
||||||
|
} else {
|
||||||
|
logger.error("downloadStandaloneFile error: \(String(describing: r))")
|
||||||
|
return (nil, String(describing: r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationFileLinkData? {
|
||||||
|
let r = await chatSendCmd(.apiStandaloneFileInfo(url: url), ctrl)
|
||||||
|
if case let .standaloneFileInfo(fileMeta) = r {
|
||||||
|
return fileMeta
|
||||||
|
} else {
|
||||||
|
logger.error("standaloneFileInfo error: \(String(describing: r))")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async {
|
func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async {
|
||||||
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) {
|
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) {
|
||||||
await chatItemSimpleUpdate(user, chatItem)
|
await chatItemSimpleUpdate(user, chatItem)
|
||||||
@@ -921,8 +967,8 @@ func cancelFile(user: User, fileId: Int64) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiCancelFile(fileId: Int64) async -> AChatItem? {
|
func apiCancelFile(fileId: Int64, ctrl: chat_ctrl? = nil) async -> AChatItem? {
|
||||||
let r = await chatSendCmd(.cancelFile(fileId: fileId))
|
let r = await chatSendCmd(.cancelFile(fileId: fileId), ctrl)
|
||||||
switch r {
|
switch r {
|
||||||
case let .sndFileCancelled(_, chatItem, _, _) : return chatItem
|
case let .sndFileCancelled(_, chatItem, _, _) : return chatItem
|
||||||
case let .rcvFileCancelled(_, chatItem, _) : return chatItem
|
case let .rcvFileCancelled(_, chatItem, _) : return chatItem
|
||||||
@@ -1094,8 +1140,8 @@ func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
|
private func sendCommandOkResp(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) async throws {
|
||||||
let r = await chatSendCmd(cmd)
|
let r = await chatSendCmd(cmd, ctrl)
|
||||||
if case .cmdOk = r { return }
|
if case .cmdOk = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
@@ -1256,7 +1302,7 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
|
|||||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||||
try apiSetPQEnabled(pqExperimentalEnabledDefault.get())
|
try apiSetPQEncryption(pqExperimentalEnabledDefault.get())
|
||||||
m.chatInitialized = true
|
m.chatInitialized = true
|
||||||
m.currentUser = try apiGetActiveUser()
|
m.currentUser = try apiGetActiveUser()
|
||||||
if m.currentUser == nil {
|
if m.currentUser == nil {
|
||||||
@@ -1336,6 +1382,16 @@ func startChat(refreshInvitations: Bool = true) throws {
|
|||||||
chatLastStartGroupDefault.set(Date.now)
|
chatLastStartGroupDefault.set(Date.now)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startChatWithTemporaryDatabase(ctrl: chat_ctrl) throws -> User? {
|
||||||
|
logger.debug("startChatWithTemporaryDatabase")
|
||||||
|
let migrationActiveUser = try? apiGetActiveUser(ctrl: ctrl) ?? apiCreateActiveUser(Profile(displayName: "Temp", fullName: ""), ctrl: ctrl)
|
||||||
|
try setNetworkConfig(getNetCfg(), ctrl: ctrl)
|
||||||
|
try apiSetTempFolder(tempFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
|
||||||
|
try apiSetFilesFolder(filesFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
|
||||||
|
_ = try apiStartChat(ctrl: ctrl)
|
||||||
|
return migrationActiveUser
|
||||||
|
}
|
||||||
|
|
||||||
func changeActiveUser(_ userId: Int64, viewPwd: String?) {
|
func changeActiveUser(_ userId: Int64, viewPwd: String?) {
|
||||||
do {
|
do {
|
||||||
try changeActiveUser_(userId, viewPwd: viewPwd)
|
try changeActiveUser_(userId, viewPwd: viewPwd)
|
||||||
@@ -1714,27 +1770,37 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
case let .rcvFileSndCancelled(user, aChatItem, _):
|
case let .rcvFileSndCancelled(user, aChatItem, _):
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
Task { cleanupFile(aChatItem) }
|
Task { cleanupFile(aChatItem) }
|
||||||
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
|
case let .rcvFileProgressXFTP(user, aChatItem, _, _, _):
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
if let aChatItem = aChatItem {
|
||||||
case let .rcvFileError(user, aChatItem):
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
}
|
||||||
Task { cleanupFile(aChatItem) }
|
case let .rcvFileError(user, aChatItem, _):
|
||||||
|
if let aChatItem = aChatItem {
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
Task { cleanupFile(aChatItem) }
|
||||||
|
}
|
||||||
case let .sndFileStart(user, aChatItem, _):
|
case let .sndFileStart(user, aChatItem, _):
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
case let .sndFileComplete(user, aChatItem, _):
|
case let .sndFileComplete(user, aChatItem, _):
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
Task { cleanupDirectFile(aChatItem) }
|
Task { cleanupDirectFile(aChatItem) }
|
||||||
case let .sndFileRcvCancelled(user, aChatItem, _):
|
case let .sndFileRcvCancelled(user, aChatItem, _):
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
if let aChatItem = aChatItem {
|
||||||
Task { cleanupDirectFile(aChatItem) }
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
Task { cleanupDirectFile(aChatItem) }
|
||||||
|
}
|
||||||
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
|
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
if let aChatItem = aChatItem {
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
}
|
||||||
case let .sndFileCompleteXFTP(user, aChatItem, _):
|
case let .sndFileCompleteXFTP(user, aChatItem, _):
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
Task { cleanupFile(aChatItem) }
|
Task { cleanupFile(aChatItem) }
|
||||||
case let .sndFileError(user, aChatItem):
|
case let .sndFileError(user, aChatItem, _):
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
if let aChatItem = aChatItem {
|
||||||
Task { cleanupFile(aChatItem) }
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
Task { cleanupFile(aChatItem) }
|
||||||
|
}
|
||||||
case let .callInvitation(invitation):
|
case let .callInvitation(invitation):
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
m.callInvitations[invitation.contact.id] = invitation
|
m.callInvitations[invitation.contact.id] = invitation
|
||||||
@@ -1834,7 +1900,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
case let .contactPQEnabled(user, contact, _):
|
case let .contactPQEnabled(user, contact, _):
|
||||||
if active(user) {
|
if active(user) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
m.updateContact(contact) // or updateContactConnectionStats?
|
m.updateContact(contact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ struct SimpleXApp: App {
|
|||||||
chatModel.appOpenUrl = url
|
chatModel.appOpenUrl = url
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
|
// Present screen for continue migration if it wasn't finished yet
|
||||||
|
if chatModel.migrationState != nil {
|
||||||
|
// It's important, otherwise, user may be locked in undefined state
|
||||||
|
onboardingStageDefault.set(.step1_SimpleXInfo)
|
||||||
|
chatModel.onboardingStage = onboardingStageDefault.get()
|
||||||
|
} else if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||||||
initChatAndMigrate()
|
initChatAndMigrate()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,15 +171,15 @@ struct ChatInfoView: View {
|
|||||||
if pqExperimentalEnabled,
|
if pqExperimentalEnabled,
|
||||||
let conn = contact.activeConn {
|
let conn = contact.activeConn {
|
||||||
Section {
|
Section {
|
||||||
infoRow(Text(String("PQ E2E encryption")), conn.connPQEnabled ? "Enabled" : "Disabled")
|
infoRow(Text(String("E2E encryption")), conn.connPQEnabled ? "Quantum resistant" : "Standard")
|
||||||
if !conn.enablePQ {
|
if !conn.pqEncryption {
|
||||||
allowPQButton()
|
allowPQButton()
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text(String("Post-quantum E2E encryption"))
|
Text(String("Quantum resistant E2E encryption"))
|
||||||
} footer: {
|
} footer: {
|
||||||
if !conn.enablePQ {
|
if !conn.pqEncryption {
|
||||||
Text(String("After allowing post-quantum encryption, it will be enabled after several messages if your contact also allows it."))
|
Text(String("After allowing quantum resistant encryption, it will be enabled after several messages if your contact also allows it."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,14 +576,14 @@ struct ChatInfoView: View {
|
|||||||
private func allowContactPQEncryption() {
|
private func allowContactPQEncryption() {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let ct = try await apiAllowContactPQ(contact.apiId)
|
let ct = try await apiSetContactPQ(contact.apiId, true)
|
||||||
contact = ct
|
contact = ct
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
chatModel.updateContact(contact)
|
chatModel.updateContact(contact)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("allowContactPQEncryption apiAllowContactPQ error: \(responseError(error))")
|
logger.error("allowContactPQEncryption apiSetContactPQ error: \(responseError(error))")
|
||||||
let a = getErrorAlert(error, "Error allowing contact PQ encryption")
|
let a = getErrorAlert(error, "Error allowing contact PQ encryption")
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
alert = .error(title: a.title, error: a.message)
|
alert = .error(title: a.title, error: a.message)
|
||||||
@@ -594,8 +594,8 @@ struct ChatInfoView: View {
|
|||||||
|
|
||||||
func allowContactPQEncryptionAlert() -> Alert {
|
func allowContactPQEncryptionAlert() -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text(String("Allow post-quantum encryption?")),
|
title: Text(String("Allow quantum resistant encryption?")),
|
||||||
message: Text(String("This is an experimental feature, it is not recommended to enable it for high importance communications. It may result in connection errors!")),
|
message: Text(String("This is an experimental feature, it is not recommended to enable it for important chats.")),
|
||||||
primaryButton: .destructive(Text(String("Allow")), action: allowContactPQEncryption),
|
primaryButton: .destructive(Text(String("Allow")), action: allowContactPQEncryption),
|
||||||
secondaryButton: .cancel()
|
secondaryButton: .cancel()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -110,12 +110,11 @@ struct ChatItemContentView<Content: View>: View {
|
|||||||
case .sndModerated: deletedItemView()
|
case .sndModerated: deletedItemView()
|
||||||
case .rcvModerated: deletedItemView()
|
case .rcvModerated: deletedItemView()
|
||||||
case .rcvBlocked: deletedItemView()
|
case .rcvBlocked: deletedItemView()
|
||||||
|
case let .sndDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo))
|
||||||
|
case let .rcvDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo))
|
||||||
|
case .sndGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText())
|
||||||
|
case .rcvGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText())
|
||||||
case let .invalidJSON(json): CIInvalidJSONView(json: json)
|
case let .invalidJSON(json): CIInvalidJSONView(json: json)
|
||||||
// TODO proper items
|
|
||||||
case .sndDirectE2EEInfo: CIEventView(eventText: Text(chatItem.content.text))
|
|
||||||
case .rcvDirectE2EEInfo: CIEventView(eventText: Text(chatItem.content.text))
|
|
||||||
case .sndGroupE2EEInfo: CIEventView(eventText: Text(chatItem.content.text))
|
|
||||||
case .rcvGroupE2EEInfo: CIEventView(eventText: Text(chatItem.content.text))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +174,22 @@ struct ChatItemContentView<Content: View>: View {
|
|||||||
Text(members)
|
Text(members)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func directE2EEInfoText(_ info: E2EEInfo) -> Text {
|
||||||
|
info.pqEnabled
|
||||||
|
? Text("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.fontWeight(.light)
|
||||||
|
: e2eeInfoNoPQText()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func e2eeInfoNoPQText() -> Text {
|
||||||
|
Text("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.fontWeight(.light)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatEventText(_ text: Text) -> Text {
|
func chatEventText(_ text: Text) -> Text {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ struct ContactPreferencesView: View {
|
|||||||
.disabled(currentFeaturesAllowed == featuresAllowed)
|
.disabled(currentFeaturesAllowed == featuresAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modifier(BackButton {
|
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||||
if currentFeaturesAllowed == featuresAllowed {
|
if currentFeaturesAllowed == featuresAllowed {
|
||||||
dismiss()
|
dismiss()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ struct GroupPreferencesView: View {
|
|||||||
preferences.timedMessages.ttl = currentPreferences.timedMessages.ttl
|
preferences.timedMessages.ttl = currentPreferences.timedMessages.ttl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modifier(BackButton {
|
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||||
if currentPreferences == preferences {
|
if currentPreferences == preferences {
|
||||||
dismiss()
|
dismiss()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct GroupWelcomeView: View {
|
|||||||
VStack {
|
VStack {
|
||||||
if groupInfo.canEdit {
|
if groupInfo.canEdit {
|
||||||
editorView()
|
editorView()
|
||||||
.modifier(BackButton {
|
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||||
if welcomeTextUnchanged() {
|
if welcomeTextUnchanged() {
|
||||||
dismiss()
|
dismiss()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -264,7 +264,9 @@ struct ChatListView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func filtered(_ chat: Chat) -> Bool {
|
func filtered(_ chat: Chat) -> Bool {
|
||||||
(chat.chatInfo.chatSettings?.favorite ?? false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
|
(chat.chatInfo.chatSettings?.favorite ?? false) ||
|
||||||
|
chat.chatStats.unreadChat ||
|
||||||
|
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool {
|
func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ enum DatabaseEncryptionAlert: Identifiable {
|
|||||||
struct DatabaseEncryptionView: View {
|
struct DatabaseEncryptionView: View {
|
||||||
@EnvironmentObject private var m: ChatModel
|
@EnvironmentObject private var m: ChatModel
|
||||||
@Binding var useKeychain: Bool
|
@Binding var useKeychain: Bool
|
||||||
|
var migration: Bool
|
||||||
@State private var alert: DatabaseEncryptionAlert? = nil
|
@State private var alert: DatabaseEncryptionAlert? = nil
|
||||||
@State private var progressIndicator = false
|
@State private var progressIndicator = false
|
||||||
@State private var useKeychainToggle = storeDBPassphraseGroupDefault.get()
|
@State private var useKeychainToggle = storeDBPassphraseGroupDefault.get()
|
||||||
@@ -48,7 +49,12 @@ struct DatabaseEncryptionView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
databaseEncryptionView()
|
List {
|
||||||
|
if migration {
|
||||||
|
chatStoppedView()
|
||||||
|
}
|
||||||
|
databaseEncryptionView()
|
||||||
|
}
|
||||||
if progressIndicator {
|
if progressIndicator {
|
||||||
ProgressView().scaleEffect(2)
|
ProgressView().scaleEffect(2)
|
||||||
}
|
}
|
||||||
@@ -56,72 +62,71 @@ struct DatabaseEncryptionView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func databaseEncryptionView() -> some View {
|
private func databaseEncryptionView() -> some View {
|
||||||
List {
|
Section {
|
||||||
Section {
|
settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) {
|
||||||
settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) {
|
Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle)
|
||||||
Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle)
|
|
||||||
.onChange(of: useKeychainToggle) { _ in
|
.onChange(of: useKeychainToggle) { _ in
|
||||||
if useKeychainToggle {
|
if useKeychainToggle {
|
||||||
setUseKeychain(true)
|
setUseKeychain(true)
|
||||||
} else if storedKey {
|
} else if storedKey && !migration {
|
||||||
|
// Don't show in migration process since it will remove the key after successfull encryption
|
||||||
alert = .keychainRemoveKey
|
alert = .keychainRemoveKey
|
||||||
} else {
|
} else {
|
||||||
setUseKeychain(false)
|
setUseKeychain(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(initialRandomDBPassphrase)
|
.disabled(initialRandomDBPassphrase && !migration)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !initialRandomDBPassphrase && m.chatDbEncrypted == true {
|
if !initialRandomDBPassphrase && m.chatDbEncrypted == true {
|
||||||
PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
|
PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
PassphraseField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true)
|
PassphraseField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true)
|
||||||
PassphraseField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey)
|
PassphraseField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey)
|
||||||
|
|
||||||
settingsRow("lock.rotation") {
|
settingsRow("lock.rotation") {
|
||||||
Button("Update database passphrase") {
|
Button(migration ? "Set passphrase" : "Update database passphrase") {
|
||||||
alert = currentKey == ""
|
alert = currentKey == ""
|
||||||
? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase)
|
? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase)
|
||||||
: (useKeychain ? .changeDatabaseKeySaved : .changeDatabaseKey)
|
: (useKeychain ? .changeDatabaseKeySaved : .changeDatabaseKey)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.disabled(
|
}
|
||||||
(m.chatDbEncrypted == true && currentKey == "") ||
|
.disabled(
|
||||||
currentKey == newKey ||
|
(m.chatDbEncrypted == true && currentKey == "") ||
|
||||||
newKey != confirmNewKey ||
|
currentKey == newKey ||
|
||||||
newKey == "" ||
|
newKey != confirmNewKey ||
|
||||||
!validKey(currentKey) ||
|
newKey == "" ||
|
||||||
!validKey(newKey)
|
!validKey(currentKey) ||
|
||||||
)
|
!validKey(newKey)
|
||||||
} header: {
|
)
|
||||||
Text("")
|
} header: {
|
||||||
} footer: {
|
Text(migration ? "Database passphrase" : "")
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
} footer: {
|
||||||
if m.chatDbEncrypted == false {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
Text("Your chat database is not encrypted - set passphrase to encrypt it.")
|
if m.chatDbEncrypted == false {
|
||||||
} else if useKeychain {
|
Text("Your chat database is not encrypted - set passphrase to encrypt it.")
|
||||||
if storedKey {
|
} else if useKeychain {
|
||||||
Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.")
|
if storedKey {
|
||||||
if initialRandomDBPassphrase {
|
Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.")
|
||||||
Text("Database is encrypted using a random passphrase, you can change it.")
|
if initialRandomDBPassphrase && !migration {
|
||||||
} else {
|
Text("Database is encrypted using a random passphrase, you can change it.")
|
||||||
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Text("iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.")
|
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text("You have to enter passphrase every time the app starts - it is not stored on the device.")
|
Text("iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.")
|
||||||
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
}
|
||||||
if m.notificationMode == .instant && m.notificationPreview != .hidden {
|
} else {
|
||||||
Text("**Warning**: Instant push notifications require passphrase saved in Keychain.")
|
Text("You have to enter passphrase every time the app starts - it is not stored on the device.")
|
||||||
}
|
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
||||||
|
if m.notificationMode == .instant && m.notificationPreview != .hidden && !migration {
|
||||||
|
Text("**Warning**: Instant push notifications require passphrase saved in Keychain.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, 1)
|
|
||||||
.font(.callout)
|
|
||||||
}
|
}
|
||||||
|
.padding(.top, 1)
|
||||||
|
.font(.callout)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" }
|
if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" }
|
||||||
@@ -136,9 +141,15 @@ struct DatabaseEncryptionView: View {
|
|||||||
do {
|
do {
|
||||||
encryptionStartedDefault.set(true)
|
encryptionStartedDefault.set(true)
|
||||||
encryptionStartedAtDefault.set(Date.now)
|
encryptionStartedAtDefault.set(Date.now)
|
||||||
|
if !m.chatDbChanged {
|
||||||
|
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||||
|
}
|
||||||
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
||||||
encryptionStartedDefault.set(false)
|
encryptionStartedDefault.set(false)
|
||||||
initialRandomDBPassphraseGroupDefault.set(false)
|
initialRandomDBPassphraseGroupDefault.set(false)
|
||||||
|
if migration {
|
||||||
|
storeDBPassphraseGroupDefault.set(useKeychain)
|
||||||
|
}
|
||||||
if useKeychain {
|
if useKeychain {
|
||||||
if kcDatabasePassword.set(newKey) {
|
if kcDatabasePassword.set(newKey) {
|
||||||
await resetFormAfterEncryption(true)
|
await resetFormAfterEncryption(true)
|
||||||
@@ -148,6 +159,9 @@ struct DatabaseEncryptionView: View {
|
|||||||
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
|
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if migration {
|
||||||
|
removePassphraseFromKeyChain()
|
||||||
|
}
|
||||||
await resetFormAfterEncryption()
|
await resetFormAfterEncryption()
|
||||||
await operationEnded(.databaseEncrypted)
|
await operationEnded(.databaseEncrypted)
|
||||||
}
|
}
|
||||||
@@ -174,7 +188,10 @@ struct DatabaseEncryptionView: View {
|
|||||||
|
|
||||||
private func setUseKeychain(_ value: Bool) {
|
private func setUseKeychain(_ value: Bool) {
|
||||||
useKeychain = value
|
useKeychain = value
|
||||||
storeDBPassphraseGroupDefault.set(value)
|
// Postpone it when migrating to the end of encryption process
|
||||||
|
if !migration {
|
||||||
|
storeDBPassphraseGroupDefault.set(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func databaseEncryptionAlert(_ alertItem: DatabaseEncryptionAlert) -> Alert {
|
private func databaseEncryptionAlert(_ alertItem: DatabaseEncryptionAlert) -> Alert {
|
||||||
@@ -184,13 +201,7 @@ struct DatabaseEncryptionView: View {
|
|||||||
title: Text("Remove passphrase from keychain?"),
|
title: Text("Remove passphrase from keychain?"),
|
||||||
message: Text("Instant push notifications will be hidden!\n") + storeSecurelyDanger(),
|
message: Text("Instant push notifications will be hidden!\n") + storeSecurelyDanger(),
|
||||||
primaryButton: .destructive(Text("Remove")) {
|
primaryButton: .destructive(Text("Remove")) {
|
||||||
if kcDatabasePassword.remove() {
|
removePassphraseFromKeyChain()
|
||||||
logger.debug("passphrase removed from keychain")
|
|
||||||
setUseKeychain(false)
|
|
||||||
storedKey = false
|
|
||||||
} else {
|
|
||||||
alert = .error(title: "Keychain error", error: "Failed to remove passphrase")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
secondaryButton: .cancel() {
|
secondaryButton: .cancel() {
|
||||||
withAnimation { useKeychainToggle = true }
|
withAnimation { useKeychainToggle = true }
|
||||||
@@ -236,6 +247,16 @@ struct DatabaseEncryptionView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func removePassphraseFromKeyChain() {
|
||||||
|
if kcDatabasePassword.remove() {
|
||||||
|
logger.debug("passphrase removed from keychain")
|
||||||
|
setUseKeychain(false)
|
||||||
|
storedKey = false
|
||||||
|
} else {
|
||||||
|
alert = .error(title: "Keychain error", error: "Failed to remove passphrase")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func storeSecurelySaved() -> Text {
|
private func storeSecurelySaved() -> Text {
|
||||||
Text("Please store passphrase securely, you will NOT be able to change it if you lose it.")
|
Text("Please store passphrase securely, you will NOT be able to change it if you lose it.")
|
||||||
}
|
}
|
||||||
@@ -346,6 +367,6 @@ func validKey(_ s: String) -> Bool {
|
|||||||
|
|
||||||
struct DatabaseEncryptionView_Previews: PreviewProvider {
|
struct DatabaseEncryptionView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
DatabaseEncryptionView(useKeychain: Binding.constant(true))
|
DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ struct DatabaseErrorView: View {
|
|||||||
case let .migrationError(mtrError):
|
case let .migrationError(mtrError):
|
||||||
titleText("Incompatible database version")
|
titleText("Incompatible database version")
|
||||||
fileNameText(dbFile)
|
fileNameText(dbFile)
|
||||||
Text("Error: ") + Text(mtrErrorDescription(mtrError))
|
Text("Error: ") + Text(DatabaseErrorView.mtrErrorDescription(mtrError))
|
||||||
}
|
}
|
||||||
case let .errorSQL(dbFile, migrationSQLError):
|
case let .errorSQL(dbFile, migrationSQLError):
|
||||||
titleText("Database error")
|
titleText("Database error")
|
||||||
@@ -105,7 +105,7 @@ struct DatabaseErrorView: View {
|
|||||||
Text("Migrations: \(ms.joined(separator: ", "))")
|
Text("Migrations: \(ms.joined(separator: ", "))")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey {
|
static func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey {
|
||||||
switch err {
|
switch err {
|
||||||
case let .noDown(dbMigrations):
|
case let .noDown(dbMigrations):
|
||||||
return "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))"
|
return "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))"
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ struct DatabaseView: View {
|
|||||||
let color: Color = unencrypted ? .orange : .secondary
|
let color: Color = unencrypted ? .orange : .secondary
|
||||||
settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) {
|
settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
DatabaseEncryptionView(useKeychain: $useKeychain)
|
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
|
||||||
.navigationTitle("Database passphrase")
|
.navigationTitle("Database passphrase")
|
||||||
} label: {
|
} label: {
|
||||||
Text("Database passphrase")
|
Text("Database passphrase")
|
||||||
@@ -485,6 +485,10 @@ func deleteChatAsync() async throws {
|
|||||||
_ = kcDatabasePassword.remove()
|
_ = kcDatabasePassword.remove()
|
||||||
storeDBPassphraseGroupDefault.set(true)
|
storeDBPassphraseGroupDefault.set(true)
|
||||||
deleteAppDatabaseAndFiles()
|
deleteAppDatabaseAndFiles()
|
||||||
|
// Clean state so when creating new user the app will start chat automatically (see CreateProfile:createProfile())
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
ChatModel.shared.users = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DatabaseView_Previews: PreviewProvider {
|
struct DatabaseView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ struct MigrateToAppGroupView: View {
|
|||||||
let config = ArchiveConfig(archivePath: getDocumentsDirectory().appendingPathComponent(archiveName).path)
|
let config = ArchiveConfig(archivePath: getDocumentsDirectory().appendingPathComponent(archiveName).path)
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
|
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||||
try await apiExportArchive(config: config)
|
try await apiExportArchive(config: config)
|
||||||
await MainActor.run { setV3DBMigration(.exported) }
|
await MainActor.run { setV3DBMigration(.exported) }
|
||||||
} catch let error {
|
} catch let error {
|
||||||
@@ -204,7 +205,11 @@ struct MigrateToAppGroupView: View {
|
|||||||
resetChatCtrl()
|
resetChatCtrl()
|
||||||
try await MainActor.run { try initializeChat(start: false) }
|
try await MainActor.run { try initializeChat(start: false) }
|
||||||
let _ = try await apiImportArchive(config: config)
|
let _ = try await apiImportArchive(config: config)
|
||||||
await MainActor.run { setV3DBMigration(.migrated) }
|
let appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport())
|
||||||
|
await MainActor.run {
|
||||||
|
appSettings.importIntoApp()
|
||||||
|
setV3DBMigration(.migrated)
|
||||||
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
dbContainerGroupDefault.set(.documents)
|
dbContainerGroupDefault.set(.documents)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
@@ -216,16 +221,22 @@ struct MigrateToAppGroupView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func exportChatArchive() async throws -> URL {
|
func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL {
|
||||||
let archiveTime = Date.now
|
let archiveTime = Date.now
|
||||||
let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted))
|
let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted))
|
||||||
let archiveName = "simplex-chat.\(ts).zip"
|
let archiveName = "simplex-chat.\(ts).zip"
|
||||||
let archivePath = getDocumentsDirectory().appendingPathComponent(archiveName)
|
let archivePath = (storagePath ?? getDocumentsDirectory()).appendingPathComponent(archiveName)
|
||||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||||
|
// Settings should be saved before changing a passphrase, otherwise the database needs to be migrated first
|
||||||
|
if !ChatModel.shared.chatDbChanged {
|
||||||
|
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||||
|
}
|
||||||
try await apiExportArchive(config: config)
|
try await apiExportArchive(config: config)
|
||||||
deleteOldArchive()
|
if storagePath == nil {
|
||||||
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
deleteOldArchive()
|
||||||
chatArchiveTimeDefault.set(archiveTime)
|
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
||||||
|
chatArchiveTimeDefault.set(archiveTime)
|
||||||
|
}
|
||||||
return archivePath
|
return archivePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,734 @@
|
|||||||
|
//
|
||||||
|
// MigrateFromDevice.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by Avently on 14.02.2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
private enum MigrationFromState: Equatable {
|
||||||
|
case chatStopInProgress
|
||||||
|
case chatStopFailed(reason: String)
|
||||||
|
case passphraseNotSet
|
||||||
|
case passphraseConfirmation
|
||||||
|
case uploadConfirmation
|
||||||
|
case archiving
|
||||||
|
case uploadProgress(uploadedBytes: Int64, totalBytes: Int64, fileId: Int64, archivePath: URL, ctrl: chat_ctrl?)
|
||||||
|
case uploadFailed(totalBytes: Int64, archivePath: URL)
|
||||||
|
case linkCreation
|
||||||
|
case linkShown(fileId: Int64, link: String, archivePath: URL, ctrl: chat_ctrl)
|
||||||
|
case finished(chatDeletion: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum MigrateFromDeviceViewAlert: Identifiable {
|
||||||
|
case deleteChat(_ title: LocalizedStringKey = "Delete chat profile?", _ text: LocalizedStringKey = "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.")
|
||||||
|
case startChat(_ title: LocalizedStringKey = "Start chat?", _ text: LocalizedStringKey = "Warning: starting chat on multiple devices is not supported and will cause message delivery failures")
|
||||||
|
|
||||||
|
case wrongPassphrase(title: LocalizedStringKey = "Wrong passphrase!", message: LocalizedStringKey = "Enter correct passphrase.")
|
||||||
|
case invalidConfirmation(title: LocalizedStringKey = "Invalid migration confirmation")
|
||||||
|
case keychainError(_ title: LocalizedStringKey = "Keychain error")
|
||||||
|
case databaseError(_ title: LocalizedStringKey = "Database error", message: String)
|
||||||
|
case unknownError(_ title: LocalizedStringKey = "Unknown error", message: String)
|
||||||
|
|
||||||
|
case error(title: LocalizedStringKey, error: String = "")
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case let .deleteChat(title, text): return "\(title) \(text)"
|
||||||
|
case let .startChat(title, text): return "\(title) \(text)"
|
||||||
|
|
||||||
|
case .wrongPassphrase: return "wrongPassphrase"
|
||||||
|
case .invalidConfirmation: return "invalidConfirmation"
|
||||||
|
case .keychainError: return "keychainError"
|
||||||
|
case let .databaseError(title, message): return "\(title) \(message)"
|
||||||
|
case let .unknownError(title, message): return "\(title) \(message)"
|
||||||
|
|
||||||
|
case let .error(title, _): return "error \(title)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MigrateFromDevice: View {
|
||||||
|
@EnvironmentObject var m: ChatModel
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@Binding var showSettings: Bool
|
||||||
|
@Binding var showProgressOnSettings: Bool
|
||||||
|
@State private var migrationState: MigrationFromState = .chatStopInProgress
|
||||||
|
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||||
|
@AppStorage(GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE, store: groupDefaults) private var initialRandomDBPassphrase: Bool = false
|
||||||
|
@State private var alert: MigrateFromDeviceViewAlert?
|
||||||
|
@State private var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
|
||||||
|
private let tempDatabaseUrl = urlForTemporaryDatabase()
|
||||||
|
@State private var chatReceiver: MigrationChatReceiver? = nil
|
||||||
|
@State private var backDisabled: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if authorized {
|
||||||
|
migrateView()
|
||||||
|
} else {
|
||||||
|
Button(action: runAuth) { Label("Unlock", systemImage: "lock") }
|
||||||
|
.onAppear(perform: runAuth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runAuth() { authorize(NSLocalizedString("Open migration to another device", comment: "authentication reason"), $authorized) }
|
||||||
|
|
||||||
|
func migrateView() -> some View {
|
||||||
|
VStack {
|
||||||
|
switch migrationState {
|
||||||
|
case .chatStopInProgress:
|
||||||
|
chatStopInProgressView()
|
||||||
|
case let .chatStopFailed(reason):
|
||||||
|
chatStopFailedView(reason)
|
||||||
|
case .passphraseNotSet:
|
||||||
|
passphraseNotSetView()
|
||||||
|
case .passphraseConfirmation:
|
||||||
|
PassphraseConfirmationView(migrationState: $migrationState, alert: $alert)
|
||||||
|
case .uploadConfirmation:
|
||||||
|
uploadConfirmationView()
|
||||||
|
case .archiving:
|
||||||
|
archivingView()
|
||||||
|
case let .uploadProgress(uploaded, total, _, archivePath, _):
|
||||||
|
uploadProgressView(uploaded, totalBytes: total, archivePath)
|
||||||
|
case let .uploadFailed(total, archivePath):
|
||||||
|
uploadFailedView(totalBytes: total, archivePath)
|
||||||
|
case .linkCreation:
|
||||||
|
linkCreationView()
|
||||||
|
case let .linkShown(fileId, link, archivePath, ctrl):
|
||||||
|
linkShownView(fileId, link, archivePath, ctrl)
|
||||||
|
case let .finished(chatDeletion):
|
||||||
|
finishedView(chatDeletion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modifier(BackButton(label: "Back", disabled: $backDisabled) {
|
||||||
|
dismiss()
|
||||||
|
})
|
||||||
|
.onChange(of: migrationState) { state in
|
||||||
|
backDisabled = switch migrationState {
|
||||||
|
case .chatStopInProgress, .archiving, .linkShown, .finished: true
|
||||||
|
case .chatStopFailed, .passphraseNotSet, .passphraseConfirmation, .uploadConfirmation, .uploadProgress, .uploadFailed, .linkCreation: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
stopChat()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
Task {
|
||||||
|
if !backDisabled {
|
||||||
|
await MainActor.run {
|
||||||
|
showProgressOnSettings = true
|
||||||
|
}
|
||||||
|
await startChatAndDismiss(false)
|
||||||
|
await MainActor.run {
|
||||||
|
showProgressOnSettings = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if case let .uploadProgress(_, _, fileId, _, ctrl) = migrationState, let ctrl {
|
||||||
|
await cancelUploadedArchive(fileId, ctrl)
|
||||||
|
}
|
||||||
|
chatReceiver?.stopAndCleanUp()
|
||||||
|
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(item: $alert) { alert in
|
||||||
|
switch alert {
|
||||||
|
case let .startChat(title, text):
|
||||||
|
return Alert(
|
||||||
|
title: Text(title),
|
||||||
|
message: Text(text),
|
||||||
|
primaryButton: .destructive(Text("Start chat")) {
|
||||||
|
Task {
|
||||||
|
await startChatAndDismiss()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
case let .deleteChat(title, text):
|
||||||
|
return Alert(
|
||||||
|
title: Text(title),
|
||||||
|
message: Text(text),
|
||||||
|
primaryButton: .destructive(Text("Delete")) {
|
||||||
|
deleteChatAndDismiss()
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
case let .wrongPassphrase(title, message):
|
||||||
|
return Alert(title: Text(title), message: Text(message))
|
||||||
|
case let .invalidConfirmation(title):
|
||||||
|
return Alert(title: Text(title))
|
||||||
|
case let .keychainError(title):
|
||||||
|
return Alert(title: Text(title))
|
||||||
|
case let .databaseError(title, message):
|
||||||
|
return Alert(title: Text(title), message: Text(message))
|
||||||
|
case let .unknownError(title, message):
|
||||||
|
return Alert(title: Text(title), message: Text(message))
|
||||||
|
case let .error(title, error):
|
||||||
|
return Alert(title: Text(title), message: Text(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.interactiveDismissDisabled(backDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func chatStopInProgressView() -> some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section {} header: {
|
||||||
|
Text("Stopping chat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func chatStopFailedView(_ reason: String) -> some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
Text(reason)
|
||||||
|
Button(action: stopChat) {
|
||||||
|
settingsRow("stop.fill") {
|
||||||
|
Text("Stop chat").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Error stopping chat")
|
||||||
|
} footer: {
|
||||||
|
Text("In order to continue, chat should be stopped.")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func passphraseNotSetView() -> some View {
|
||||||
|
DatabaseEncryptionView(useKeychain: $useKeychain, migration: true)
|
||||||
|
.onChange(of: initialRandomDBPassphrase) { initial in
|
||||||
|
if !initial {
|
||||||
|
migrationState = .uploadConfirmation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadConfirmationView() -> some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
Button(action: { migrationState = .archiving }) {
|
||||||
|
settingsRow("tray.and.arrow.up") {
|
||||||
|
Text("Archive and upload").foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Confirm upload")
|
||||||
|
} footer: {
|
||||||
|
Text("All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func archivingView() -> some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section {} header: {
|
||||||
|
Text("Archiving database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progressView()
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
exportArchive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadProgressView(_ uploadedBytes: Int64, totalBytes: Int64, _ archivePath: URL) -> some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section {} header: {
|
||||||
|
Text("Uploading archive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ratio = Float(uploadedBytes) / Float(totalBytes)
|
||||||
|
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded")
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
startUploading(totalBytes, archivePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadFailedView(totalBytes: Int64, _ archivePath: URL) -> some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
Button(action: {
|
||||||
|
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil)
|
||||||
|
}) {
|
||||||
|
settingsRow("tray.and.arrow.up") {
|
||||||
|
Text("Repeat upload").foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Upload failed")
|
||||||
|
} footer: {
|
||||||
|
Text("You can give another try.")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
chatReceiver?.stopAndCleanUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func linkCreationView() -> some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section {} header: {
|
||||||
|
Text("Creating archive link")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func linkShownView(_ fileId: Int64, _ link: String, _ archivePath: URL, _ ctrl: chat_ctrl) -> some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
Button(action: { cancelMigration(fileId, ctrl) }) {
|
||||||
|
settingsRow("multiply") {
|
||||||
|
Text("Cancel migration").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(action: { finishMigration(fileId, ctrl) }) {
|
||||||
|
settingsRow("checkmark") {
|
||||||
|
Text("Finalize migration").foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text("**Warning**: the archive will be removed.")
|
||||||
|
Text("Choose _Migrate from another device_ on the new device and scan QR code.")
|
||||||
|
}
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
Section("Show QR code") {
|
||||||
|
SimpleXLinkQRCode(uri: link)
|
||||||
|
.padding()
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.fill(Color(uiColor: .secondarySystemGroupedBackground))
|
||||||
|
)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Or securely share this file link") {
|
||||||
|
shareLinkView(link)
|
||||||
|
}
|
||||||
|
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finishedView(_ chatDeletion: Bool) -> some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
Button(action: { alert = .deleteChat() }) {
|
||||||
|
settingsRow("trash.fill") {
|
||||||
|
Text("Delete database from this device").foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(action: { alert = .startChat() }) {
|
||||||
|
settingsRow("play.fill") {
|
||||||
|
Text("Start chat").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Migration complete")
|
||||||
|
} footer: {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text("You **must not** use the same database on two devices.")
|
||||||
|
Text("**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.")
|
||||||
|
}
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if chatDeletion {
|
||||||
|
progressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func shareLinkView(_ link: String) -> some View {
|
||||||
|
HStack {
|
||||||
|
linkTextView(link)
|
||||||
|
Button {
|
||||||
|
showShareSheet(items: [link])
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "square.and.arrow.up")
|
||||||
|
.padding(.top, -7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func linkTextView(_ link: String) -> some View {
|
||||||
|
Text(link)
|
||||||
|
.lineLimit(1)
|
||||||
|
.font(.caption)
|
||||||
|
.truncationMode(.middle)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey) -> some View {
|
||||||
|
ZStack {
|
||||||
|
VStack {
|
||||||
|
Text(description)
|
||||||
|
.font(.title3)
|
||||||
|
.hidden()
|
||||||
|
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 54))
|
||||||
|
.bold()
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
|
Text(description)
|
||||||
|
.font(.title3)
|
||||||
|
}
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.trim(from: 0, to: CGFloat(value))
|
||||||
|
.stroke(
|
||||||
|
Color.accentColor,
|
||||||
|
style: StrokeStyle(lineWidth: 27)
|
||||||
|
)
|
||||||
|
.rotationEffect(.degrees(180))
|
||||||
|
.animation(.linear, value: value)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopChat() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await stopChatAsync()
|
||||||
|
do {
|
||||||
|
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = initialRandomDBPassphraseGroupDefault.get() ? .passphraseNotSet : .passphraseConfirmation
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
alert = .error(title: "Error saving settings", error: error.localizedDescription)
|
||||||
|
migrationState = .chatStopFailed(reason: NSLocalizedString("Error saving settings", comment: "when migrating"))
|
||||||
|
}
|
||||||
|
} catch let e {
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = .chatStopFailed(reason: e.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func exportArchive() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try? FileManager.default.createDirectory(at: getMigrationTempFilesDirectory(), withIntermediateDirectories: true)
|
||||||
|
let archivePath = try await exportChatArchive(getMigrationTempFilesDirectory())
|
||||||
|
if let attrs = try? FileManager.default.attributesOfItem(atPath: archivePath.path),
|
||||||
|
let totalBytes = attrs[.size] as? Int64 {
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await MainActor.run {
|
||||||
|
alert = .error(title: "Exported file doesn't exist")
|
||||||
|
migrationState = .uploadConfirmation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
await MainActor.run {
|
||||||
|
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
||||||
|
migrationState = .uploadConfirmation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initTemporaryDatabase() -> (chat_ctrl, User)? {
|
||||||
|
let (status, ctrl) = chatInitTemporaryDatabase(url: tempDatabaseUrl)
|
||||||
|
showErrorOnMigrationIfNeeded(status, $alert)
|
||||||
|
do {
|
||||||
|
if let ctrl, let user = try startChatWithTemporaryDatabase(ctrl: ctrl) {
|
||||||
|
return (ctrl, user)
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("Error while starting chat in temporary database: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startUploading(_ totalBytes: Int64, _ archivePath: URL) {
|
||||||
|
Task {
|
||||||
|
guard let ctrlAndUser = initTemporaryDatabase() else {
|
||||||
|
return migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath)
|
||||||
|
}
|
||||||
|
let (ctrl, user) = ctrlAndUser
|
||||||
|
chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in
|
||||||
|
await MainActor.run {
|
||||||
|
switch msg {
|
||||||
|
case let .sndFileProgressXFTP(_, _, fileTransferMeta, sentSize, totalSize):
|
||||||
|
if case let .uploadProgress(uploaded, total, _, _, _) = migrationState, uploaded != total {
|
||||||
|
migrationState = .uploadProgress(uploadedBytes: sentSize, totalBytes: totalSize, fileId: fileTransferMeta.fileId, archivePath: archivePath, ctrl: ctrl)
|
||||||
|
}
|
||||||
|
case .sndFileRedirectStartXFTP:
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
migrationState = .linkCreation
|
||||||
|
}
|
||||||
|
case let .sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs):
|
||||||
|
let cfg = getNetCfg()
|
||||||
|
let data = MigrationFileLinkData.init(
|
||||||
|
networkConfig: MigrationFileLinkData.NetworkConfig(
|
||||||
|
socksProxy: cfg.socksProxy,
|
||||||
|
hostMode: cfg.hostMode,
|
||||||
|
requiredHostMode: cfg.requiredHostMode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
migrationState = .linkShown(fileId: fileTransferMeta.fileId, link: data.addToLink(link: rcvURIs[0]), archivePath: archivePath, ctrl: ctrl)
|
||||||
|
}
|
||||||
|
case .sndFileError:
|
||||||
|
alert = .error(title: "Upload failed", error: "Check your internet connection and try again")
|
||||||
|
migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath)
|
||||||
|
default:
|
||||||
|
logger.debug("unsupported event: \(msg.responseType)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chatReceiver?.start()
|
||||||
|
|
||||||
|
let (res, error) = await uploadStandaloneFile(user: user, file: CryptoFile.plain(archivePath.lastPathComponent), ctrl: ctrl)
|
||||||
|
await MainActor.run {
|
||||||
|
guard let res = res else {
|
||||||
|
migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath)
|
||||||
|
return alert = .error(title: "Error uploading the archive", error: error ?? "")
|
||||||
|
}
|
||||||
|
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: res.fileSize, fileId: res.fileId, archivePath: archivePath, ctrl: ctrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelUploadedArchive(_ fileId: Int64, _ ctrl: chat_ctrl) async {
|
||||||
|
_ = await apiCancelFile(fileId: fileId, ctrl: ctrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelMigration(_ fileId: Int64, _ ctrl: chat_ctrl) {
|
||||||
|
Task {
|
||||||
|
await cancelUploadedArchive(fileId, ctrl)
|
||||||
|
await startChatAndDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finishMigration(_ fileId: Int64, _ ctrl: chat_ctrl) {
|
||||||
|
Task {
|
||||||
|
await cancelUploadedArchive(fileId, ctrl)
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = .finished(chatDeletion: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteChatAndDismiss() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await deleteChatAsync()
|
||||||
|
m.chatDbChanged = true
|
||||||
|
m.chatInitialized = false
|
||||||
|
migrationState = .finished(chatDeletion: true)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
||||||
|
resetChatCtrl()
|
||||||
|
do {
|
||||||
|
try initializeChat(start: false)
|
||||||
|
m.chatDbChanged = false
|
||||||
|
AppChatState.shared.set(.active)
|
||||||
|
} catch let error {
|
||||||
|
fatalError("Error starting chat \(responseError(error))")
|
||||||
|
}
|
||||||
|
showSettings = false
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
alert = .error(title: "Error deleting database", error: responseError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startChatAndDismiss(_ dismiss: Bool = true) async {
|
||||||
|
AppChatState.shared.set(.active)
|
||||||
|
do {
|
||||||
|
if m.chatDbChanged {
|
||||||
|
resetChatCtrl()
|
||||||
|
try initializeChat(start: true)
|
||||||
|
m.chatDbChanged = false
|
||||||
|
} else {
|
||||||
|
try startChat(refreshInvitations: true)
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
alert = .error(title: "Error starting chat", error: responseError(error))
|
||||||
|
}
|
||||||
|
// Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered
|
||||||
|
if dismiss || m.chatDbStatus != .ok {
|
||||||
|
await MainActor.run {
|
||||||
|
showSettings = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func urlForTemporaryDatabase() -> URL {
|
||||||
|
URL(fileURLWithPath: generateNewFileName(getMigrationTempFilesDirectory().path + "/" + "migration", "db", fullPath: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PassphraseConfirmationView: View {
|
||||||
|
@Binding var migrationState: MigrationFromState
|
||||||
|
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||||
|
@State private var currentKey: String = ""
|
||||||
|
@State private var verifyingPassphrase: Bool = false
|
||||||
|
@FocusState private var keyboardVisible: Bool
|
||||||
|
@Binding var alert: MigrateFromDeviceViewAlert?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
chatStoppedView()
|
||||||
|
Section {
|
||||||
|
PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
|
||||||
|
.focused($keyboardVisible)
|
||||||
|
Button(action: {
|
||||||
|
verifyingPassphrase = true
|
||||||
|
hideKeyboard()
|
||||||
|
Task {
|
||||||
|
await verifyDatabasePassphrase(currentKey)
|
||||||
|
verifyingPassphrase = false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
settingsRow(useKeychain ? "key" : "lock", color: .secondary) {
|
||||||
|
Text("Verify passphrase")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(verifyingPassphrase || currentKey.isEmpty)
|
||||||
|
} header: {
|
||||||
|
Text("Verify database passphrase")
|
||||||
|
} footer: {
|
||||||
|
Text("Confirm that you remember database passphrase to migrate it.")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
|
keyboardVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if verifyingPassphrase {
|
||||||
|
progressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func verifyDatabasePassphrase(_ dbKey: String) async {
|
||||||
|
do {
|
||||||
|
try await testStorageEncryption(key: dbKey)
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = .uploadConfirmation
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
if case .chatCmdError(_, .errorDatabase(.errorOpen(.errorNotADatabase))) = error as? ChatResponse {
|
||||||
|
showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert)
|
||||||
|
} else {
|
||||||
|
alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(String(describing: error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateFromDeviceViewAlert?>) {
|
||||||
|
switch status {
|
||||||
|
case .invalidConfirmation:
|
||||||
|
alert.wrappedValue = .invalidConfirmation()
|
||||||
|
case .errorNotADatabase:
|
||||||
|
alert.wrappedValue = .wrongPassphrase()
|
||||||
|
case .errorKeychain:
|
||||||
|
alert.wrappedValue = .keychainError()
|
||||||
|
case let .errorSQL(_, error):
|
||||||
|
alert.wrappedValue = .databaseError(message: error)
|
||||||
|
case let .unknown(error):
|
||||||
|
alert.wrappedValue = .unknownError(message: error)
|
||||||
|
case .errorMigration: ()
|
||||||
|
case .ok: ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func progressView() -> some View {
|
||||||
|
VStack {
|
||||||
|
ProgressView().scaleEffect(2)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity )
|
||||||
|
}
|
||||||
|
|
||||||
|
func chatStoppedView() -> some View {
|
||||||
|
settingsRow("exclamationmark.octagon.fill", color: .red) {
|
||||||
|
Text("Chat is stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MigrationChatReceiver {
|
||||||
|
let ctrl: chat_ctrl
|
||||||
|
let databaseUrl: URL
|
||||||
|
let processReceivedMsg: (ChatResponse) async -> Void
|
||||||
|
private var receiveLoop: Task<Void, Never>?
|
||||||
|
private var receiveMessages = true
|
||||||
|
|
||||||
|
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (ChatResponse) async -> Void) {
|
||||||
|
self.ctrl = ctrl
|
||||||
|
self.databaseUrl = databaseUrl
|
||||||
|
self.processReceivedMsg = processReceivedMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
logger.debug("MigrationChatReceiver.start")
|
||||||
|
receiveMessages = true
|
||||||
|
if receiveLoop != nil { return }
|
||||||
|
receiveLoop = Task { await receiveMsgLoop() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveMsgLoop() async {
|
||||||
|
// TODO use function that has timeout
|
||||||
|
if let msg = await chatRecvMsg(ctrl) {
|
||||||
|
Task {
|
||||||
|
await TerminalItems.shared.add(.resp(.now, msg))
|
||||||
|
}
|
||||||
|
logger.debug("processReceivedMsg: \(msg.responseType)")
|
||||||
|
await processReceivedMsg(msg)
|
||||||
|
}
|
||||||
|
if self.receiveMessages {
|
||||||
|
_ = try? await Task.sleep(nanoseconds: 7_500_000)
|
||||||
|
await receiveMsgLoop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopAndCleanUp() {
|
||||||
|
logger.debug("MigrationChatReceiver.stop")
|
||||||
|
receiveMessages = false
|
||||||
|
receiveLoop?.cancel()
|
||||||
|
receiveLoop = nil
|
||||||
|
chat_close_store(ctrl)
|
||||||
|
try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_chat.db")
|
||||||
|
try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_agent.db")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MigrateFromDevice_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
MigrateFromDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,714 @@
|
|||||||
|
//
|
||||||
|
// MigrateToDevice.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by Avently on 23.02.2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
enum MigrationToDeviceState: Codable, Equatable {
|
||||||
|
case downloadProgress(link: String, archiveName: String)
|
||||||
|
case archiveImport(archiveName: String)
|
||||||
|
case passphrase
|
||||||
|
|
||||||
|
// Here we check whether it's needed to show migration process after app restart or not
|
||||||
|
// It's important to NOT show the process when archive was corrupted/not fully downloaded
|
||||||
|
static func makeMigrationState() -> MigrationToState? {
|
||||||
|
let state: MigrationToDeviceState? = UserDefaults.standard.string(forKey: DEFAULT_MIGRATION_TO_STAGE) != nil ? decodeJSON(UserDefaults.standard.string(forKey: DEFAULT_MIGRATION_TO_STAGE)!) : nil
|
||||||
|
var initial: MigrationToState? = .pasteOrScanLink
|
||||||
|
//logger.debug("Inited with migrationState: \(String(describing: state))")
|
||||||
|
switch state {
|
||||||
|
case nil:
|
||||||
|
initial = nil
|
||||||
|
case .downloadProgress:
|
||||||
|
// No migration happens at the moment actually since archive were not downloaded fully
|
||||||
|
logger.debug("MigrateToDevice: archive wasn't fully downloaded, removed broken file")
|
||||||
|
initial = nil
|
||||||
|
case let .archiveImport(archiveName):
|
||||||
|
let archivePath = getMigrationTempFilesDirectory().path + "/" + archiveName
|
||||||
|
initial = .archiveImportFailed(archivePath: archivePath)
|
||||||
|
case .passphrase:
|
||||||
|
initial = .passphrase(passphrase: "")
|
||||||
|
}
|
||||||
|
if initial == nil {
|
||||||
|
UserDefaults.standard.removeObject(forKey: DEFAULT_MIGRATION_TO_STAGE)
|
||||||
|
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
|
||||||
|
}
|
||||||
|
return initial
|
||||||
|
}
|
||||||
|
|
||||||
|
static func save(_ state: MigrationToDeviceState?) {
|
||||||
|
if let state {
|
||||||
|
UserDefaults.standard.setValue(encodeJSON(state), forKey: DEFAULT_MIGRATION_TO_STAGE)
|
||||||
|
} else {
|
||||||
|
UserDefaults.standard.removeObject(forKey: DEFAULT_MIGRATION_TO_STAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MigrationToState: Equatable {
|
||||||
|
case pasteOrScanLink
|
||||||
|
case linkDownloading(link: String)
|
||||||
|
case downloadProgress(downloadedBytes: Int64, totalBytes: Int64, fileId: Int64, link: String, archivePath: String, ctrl: chat_ctrl?)
|
||||||
|
case downloadFailed(totalBytes: Int64, link: String, archivePath: String)
|
||||||
|
case archiveImport(archivePath: String)
|
||||||
|
case archiveImportFailed(archivePath: String)
|
||||||
|
case passphrase(passphrase: String)
|
||||||
|
case migrationConfirmation(status: DBMigrationResult, passphrase: String, useKeychain: Bool)
|
||||||
|
case migration(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Bool)
|
||||||
|
case onion(appSettings: AppSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum MigrateToDeviceViewAlert: Identifiable {
|
||||||
|
case chatImportedWithErrors(title: LocalizedStringKey = "Chat database imported",
|
||||||
|
text: LocalizedStringKey = "Some non-fatal errors occurred during import - you may see Chat console for more details.")
|
||||||
|
|
||||||
|
case wrongPassphrase(title: LocalizedStringKey = "Wrong passphrase!", message: LocalizedStringKey = "Enter correct passphrase.")
|
||||||
|
case invalidConfirmation(title: LocalizedStringKey = "Invalid migration confirmation")
|
||||||
|
case keychainError(_ title: LocalizedStringKey = "Keychain error")
|
||||||
|
case databaseError(_ title: LocalizedStringKey = "Database error", message: String)
|
||||||
|
case unknownError(_ title: LocalizedStringKey = "Unknown error", message: String)
|
||||||
|
|
||||||
|
case error(title: LocalizedStringKey, error: String = "")
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .chatImportedWithErrors: return "chatImportedWithErrors"
|
||||||
|
|
||||||
|
case .wrongPassphrase: return "wrongPassphrase"
|
||||||
|
case .invalidConfirmation: return "invalidConfirmation"
|
||||||
|
case .keychainError: return "keychainError"
|
||||||
|
case let .databaseError(title, message): return "\(title) \(message)"
|
||||||
|
case let .unknownError(title, message): return "\(title) \(message)"
|
||||||
|
|
||||||
|
case let .error(title, _): return "error \(title)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MigrateToDevice: View {
|
||||||
|
@EnvironmentObject var m: ChatModel
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
|
@Binding var migrationState: MigrationToState?
|
||||||
|
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||||
|
@State private var alert: MigrateToDeviceViewAlert?
|
||||||
|
private let tempDatabaseUrl = urlForTemporaryDatabase()
|
||||||
|
@State private var chatReceiver: MigrationChatReceiver? = nil
|
||||||
|
// Prevent from hiding the view until migration is finished or app deleted
|
||||||
|
@State private var backDisabled: Bool = false
|
||||||
|
@State private var showQRCodeScanner: Bool = true
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
switch migrationState {
|
||||||
|
case nil: EmptyView()
|
||||||
|
case .pasteOrScanLink:
|
||||||
|
pasteOrScanLinkView()
|
||||||
|
case let .linkDownloading(link):
|
||||||
|
linkDownloadingView(link)
|
||||||
|
case let .downloadProgress(downloaded, total, _, _, _, _):
|
||||||
|
downloadProgressView(downloaded, totalBytes: total)
|
||||||
|
case let .downloadFailed(total, link, archivePath):
|
||||||
|
downloadFailedView(totalBytes: total, link, archivePath)
|
||||||
|
case let .archiveImport(archivePath):
|
||||||
|
archiveImportView(archivePath)
|
||||||
|
case let .archiveImportFailed(archivePath):
|
||||||
|
archiveImportFailedView(archivePath)
|
||||||
|
case let .passphrase(passphrase):
|
||||||
|
PassphraseEnteringView(migrationState: $migrationState, currentKey: passphrase, alert: $alert)
|
||||||
|
case let .migrationConfirmation(status, passphrase, useKeychain):
|
||||||
|
migrationConfirmationView(status, passphrase, useKeychain)
|
||||||
|
case let .migration(passphrase, confirmation, useKeychain):
|
||||||
|
migrationView(passphrase, confirmation, useKeychain)
|
||||||
|
case let .onion(appSettings):
|
||||||
|
OnionView(appSettings: appSettings, finishMigration: finishMigration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
backDisabled = switch migrationState {
|
||||||
|
case nil, .pasteOrScanLink, .linkDownloading, .downloadProgress, .downloadFailed, .archiveImportFailed: false
|
||||||
|
case .archiveImport, .passphrase, .migrationConfirmation, .migration, .onion: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: migrationState) { state in
|
||||||
|
backDisabled = switch state {
|
||||||
|
case nil, .pasteOrScanLink, .linkDownloading, .downloadProgress, .downloadFailed, .archiveImportFailed: false
|
||||||
|
case .archiveImport, .passphrase, .migrationConfirmation, .migration, .onion: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
Task {
|
||||||
|
if case .archiveImportFailed = migrationState {
|
||||||
|
// Original database is not exist, nothing is setup correctly for showing to a user yet. Return to clean state
|
||||||
|
deleteAppDatabaseAndFiles()
|
||||||
|
initChatAndMigrate()
|
||||||
|
} else if case let .downloadProgress(_, _, fileId, _, _, ctrl) = migrationState, let ctrl {
|
||||||
|
await stopArchiveDownloading(fileId, ctrl)
|
||||||
|
}
|
||||||
|
chatReceiver?.stopAndCleanUp()
|
||||||
|
if !backDisabled {
|
||||||
|
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
|
||||||
|
MigrationToDeviceState.save(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(item: $alert) { alert in
|
||||||
|
switch alert {
|
||||||
|
case let .chatImportedWithErrors(title, text):
|
||||||
|
return Alert(title: Text(title), message: Text(text))
|
||||||
|
case let .wrongPassphrase(title, message):
|
||||||
|
return Alert(title: Text(title), message: Text(message))
|
||||||
|
case let .invalidConfirmation(title):
|
||||||
|
return Alert(title: Text(title))
|
||||||
|
case let .keychainError(title):
|
||||||
|
return Alert(title: Text(title))
|
||||||
|
case let .databaseError(title, message):
|
||||||
|
return Alert(title: Text(title), message: Text(message))
|
||||||
|
case let .unknownError(title, message):
|
||||||
|
return Alert(title: Text(title), message: Text(message))
|
||||||
|
case let .error(title, error):
|
||||||
|
return Alert(title: Text(title), message: Text(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.interactiveDismissDisabled(backDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pasteOrScanLinkView() -> some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section("Scan QR code") {
|
||||||
|
ScannerInView(showQRCodeScanner: $showQRCodeScanner) { resp in
|
||||||
|
switch resp {
|
||||||
|
case let .success(r):
|
||||||
|
let link = r.string
|
||||||
|
if strHasSimplexFileLink(link.trimmingCharacters(in: .whitespaces)) {
|
||||||
|
migrationState = .linkDownloading(link: link.trimmingCharacters(in: .whitespaces))
|
||||||
|
} else {
|
||||||
|
alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.")
|
||||||
|
}
|
||||||
|
case let .failure(e):
|
||||||
|
logger.error("processQRCode QR code error: \(e.localizedDescription)")
|
||||||
|
alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if developerTools {
|
||||||
|
Section("Or paste archive link") {
|
||||||
|
pasteLinkView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pasteLinkView() -> some View {
|
||||||
|
Button {
|
||||||
|
if let str = UIPasteboard.general.string {
|
||||||
|
if strHasSimplexFileLink(str.trimmingCharacters(in: .whitespaces)) {
|
||||||
|
migrationState = .linkDownloading(link: str.trimmingCharacters(in: .whitespaces))
|
||||||
|
} else {
|
||||||
|
alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Tap to paste link")
|
||||||
|
}
|
||||||
|
.disabled(!ChatModel.shared.pasteboardHasStrings)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func linkDownloadingView(_ link: String) -> some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section {} header: {
|
||||||
|
Text("Downloading link details")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progressView()
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
downloadLinkDetails(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func downloadProgressView(_ downloadedBytes: Int64, totalBytes: Int64) -> some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section {} header: {
|
||||||
|
Text("Downloading archive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ratio = Float(downloadedBytes) / Float(max(totalBytes, 1))
|
||||||
|
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func downloadFailedView(totalBytes: Int64, _ link: String, _ archivePath: String) -> some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
Button(action: {
|
||||||
|
try? FileManager.default.removeItem(atPath: archivePath)
|
||||||
|
migrationState = .linkDownloading(link: link)
|
||||||
|
}) {
|
||||||
|
settingsRow("tray.and.arrow.down") {
|
||||||
|
Text("Repeat download").foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Download failed")
|
||||||
|
} footer: {
|
||||||
|
Text("You can give another try.")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
chatReceiver?.stopAndCleanUp()
|
||||||
|
try? FileManager.default.removeItem(atPath: archivePath)
|
||||||
|
MigrationToDeviceState.save(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func archiveImportView(_ archivePath: String) -> some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section {} header: {
|
||||||
|
Text("Importing archive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progressView()
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
importArchive(archivePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func archiveImportFailedView(_ archivePath: String) -> some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
Button(action: {
|
||||||
|
migrationState = .archiveImport(archivePath: archivePath)
|
||||||
|
}) {
|
||||||
|
settingsRow("square.and.arrow.down") {
|
||||||
|
Text("Repeat import").foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Import failed")
|
||||||
|
} footer: {
|
||||||
|
Text("You can give another try.")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func migrationConfirmationView(_ status: DBMigrationResult, _ passphrase: String, _ useKeychain: Bool) -> some View {
|
||||||
|
List {
|
||||||
|
let (header, button, footer, confirmation): (LocalizedStringKey, LocalizedStringKey?, String, MigrationConfirmation?) = switch status {
|
||||||
|
case let .errorMigration(_, migrationError):
|
||||||
|
switch migrationError {
|
||||||
|
case .upgrade:
|
||||||
|
("Database upgrade",
|
||||||
|
"Upgrade and open chat",
|
||||||
|
"",
|
||||||
|
.yesUp)
|
||||||
|
case .downgrade:
|
||||||
|
("Database downgrade",
|
||||||
|
"Downgrade and open chat",
|
||||||
|
NSLocalizedString("Warning: you may lose some data!", comment: ""),
|
||||||
|
.yesUpDown)
|
||||||
|
case let .migrationError(mtrError):
|
||||||
|
("Incompatible database version",
|
||||||
|
nil,
|
||||||
|
"\(NSLocalizedString("Error: ", comment: "")) \(DatabaseErrorView.mtrErrorDescription(mtrError))",
|
||||||
|
nil)
|
||||||
|
}
|
||||||
|
default: ("Error", nil, "Unknown error", nil)
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
if let button, let confirmation {
|
||||||
|
Button(action: {
|
||||||
|
migrationState = .migration(passphrase: passphrase, confirmation: confirmation, useKeychain: useKeychain)
|
||||||
|
}) {
|
||||||
|
settingsRow("square.and.arrow.down") {
|
||||||
|
Text(button).foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text(header)
|
||||||
|
} footer: {
|
||||||
|
Text(footer)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func migrationView(_ passphrase: String, _ confirmation: MigrationConfirmation, _ useKeychain: Bool) -> some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section {} header: {
|
||||||
|
Text("Migrating")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progressView()
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
startChat(passphrase, confirmation, useKeychain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OnionView: View {
|
||||||
|
@State var appSettings: AppSettings
|
||||||
|
@State private var onionHosts: OnionHosts = .no
|
||||||
|
var finishMigration: (AppSettings) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
Button(action: {
|
||||||
|
var updated = appSettings.networkConfig!
|
||||||
|
let (hostMode, requiredHostMode) = onionHosts.hostMode
|
||||||
|
updated.hostMode = hostMode
|
||||||
|
updated.requiredHostMode = requiredHostMode
|
||||||
|
updated.socksProxy = nil
|
||||||
|
appSettings.networkConfig = updated
|
||||||
|
finishMigration(appSettings)
|
||||||
|
}) {
|
||||||
|
settingsRow("checkmark") {
|
||||||
|
Text("Apply").foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Confirm network settings")
|
||||||
|
} footer: {
|
||||||
|
Text("Please confirm that network settings are correct for this device.")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Network settings") {
|
||||||
|
Picker("Use .onion hosts", selection: $onionHosts) {
|
||||||
|
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
|
||||||
|
}
|
||||||
|
.frame(height: 36)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func downloadLinkDetails(_ link: String) {
|
||||||
|
let archiveTime = Date.now
|
||||||
|
let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted))
|
||||||
|
let archiveName = "simplex-chat.\(ts).zip"
|
||||||
|
let archivePath = getMigrationTempFilesDirectory().appendingPathComponent(archiveName)
|
||||||
|
|
||||||
|
startDownloading(0, link, archivePath.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initTemporaryDatabase() -> (chat_ctrl, User)? {
|
||||||
|
let (status, ctrl) = chatInitTemporaryDatabase(url: tempDatabaseUrl)
|
||||||
|
showErrorOnMigrationIfNeeded(status, $alert)
|
||||||
|
do {
|
||||||
|
if let ctrl, let user = try startChatWithTemporaryDatabase(ctrl: ctrl) {
|
||||||
|
return (ctrl, user)
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("Error while starting chat in temporary database: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startDownloading(_ totalBytes: Int64, _ link: String, _ archivePath: String) {
|
||||||
|
Task {
|
||||||
|
guard let ctrlAndUser = initTemporaryDatabase() else {
|
||||||
|
return migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||||
|
}
|
||||||
|
let (ctrl, user) = ctrlAndUser
|
||||||
|
chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in
|
||||||
|
await MainActor.run {
|
||||||
|
switch msg {
|
||||||
|
case let .rcvFileProgressXFTP(_, _, receivedSize, totalSize, rcvFileTransfer):
|
||||||
|
migrationState = .downloadProgress(downloadedBytes: receivedSize, totalBytes: totalSize, fileId: rcvFileTransfer.fileId, link: link, archivePath: archivePath, ctrl: ctrl)
|
||||||
|
MigrationToDeviceState.save(.downloadProgress(link: link, archiveName: URL(fileURLWithPath: archivePath).lastPathComponent))
|
||||||
|
case .rcvStandaloneFileComplete:
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
// User closed the whole screen before new state was saved
|
||||||
|
if migrationState == nil {
|
||||||
|
MigrationToDeviceState.save(nil)
|
||||||
|
} else {
|
||||||
|
migrationState = .archiveImport(archivePath: archivePath)
|
||||||
|
MigrationToDeviceState.save(.archiveImport(archiveName: URL(fileURLWithPath: archivePath).lastPathComponent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .rcvFileError:
|
||||||
|
alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
|
||||||
|
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||||
|
default:
|
||||||
|
logger.debug("unsupported event: \(msg.responseType)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chatReceiver?.start()
|
||||||
|
|
||||||
|
let (res, error) = await downloadStandaloneFile(user: user, url: link, file: CryptoFile.plain(URL(fileURLWithPath: archivePath).lastPathComponent), ctrl: ctrl)
|
||||||
|
if res == nil {
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||||
|
}
|
||||||
|
return alert = .error(title: "Error downloading the archive", error: error ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func importArchive(_ archivePath: String) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
if !hasChatCtrl() {
|
||||||
|
chatInitControllerRemovingDatabases()
|
||||||
|
}
|
||||||
|
try await apiDeleteStorage()
|
||||||
|
do {
|
||||||
|
let config = ArchiveConfig(archivePath: archivePath)
|
||||||
|
let archiveErrors = try await apiImportArchive(config: config)
|
||||||
|
if !archiveErrors.isEmpty {
|
||||||
|
alert = .chatImportedWithErrors()
|
||||||
|
}
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = .passphrase(passphrase: "")
|
||||||
|
MigrationToDeviceState.save(.passphrase)
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = .archiveImportFailed(archivePath: archivePath)
|
||||||
|
}
|
||||||
|
alert = .error(title: "Error importing chat database", error: responseError(error))
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = .archiveImportFailed(archivePath: archivePath)
|
||||||
|
}
|
||||||
|
alert = .error(title: "Error deleting chat database", error: responseError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func stopArchiveDownloading(_ fileId: Int64, _ ctrl: chat_ctrl) async {
|
||||||
|
_ = await apiCancelFile(fileId: fileId, ctrl: ctrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startChat(_ passphrase: String, _ confirmation: MigrationConfirmation, _ useKeychain: Bool) {
|
||||||
|
if useKeychain {
|
||||||
|
_ = kcDatabasePassword.set(passphrase)
|
||||||
|
} else {
|
||||||
|
_ = kcDatabasePassword.remove()
|
||||||
|
}
|
||||||
|
storeDBPassphraseGroupDefault.set(useKeychain)
|
||||||
|
initialRandomDBPassphraseGroupDefault.set(false)
|
||||||
|
AppChatState.shared.set(.active)
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
resetChatCtrl()
|
||||||
|
try initializeChat(start: false, confirmStart: false, dbKey: passphrase, refreshInvitations: true, confirmMigrations: confirmation)
|
||||||
|
var appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport())
|
||||||
|
let hasOnionConfigured = appSettings.networkConfig?.socksProxy != nil || appSettings.networkConfig?.hostMode == .onionHost
|
||||||
|
appSettings.networkConfig?.socksProxy = nil
|
||||||
|
appSettings.networkConfig?.hostMode = .publicHost
|
||||||
|
appSettings.networkConfig?.requiredHostMode = true
|
||||||
|
await MainActor.run {
|
||||||
|
if hasOnionConfigured {
|
||||||
|
migrationState = .onion(appSettings: appSettings)
|
||||||
|
} else {
|
||||||
|
finishMigration(appSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
hideView()
|
||||||
|
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finishMigration(_ appSettings: AppSettings) {
|
||||||
|
do {
|
||||||
|
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
|
||||||
|
MigrationToDeviceState.save(nil)
|
||||||
|
appSettings.importIntoApp()
|
||||||
|
try SimpleX.startChat(refreshInvitations: true)
|
||||||
|
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
|
||||||
|
} catch let error {
|
||||||
|
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
|
||||||
|
}
|
||||||
|
hideView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideView() {
|
||||||
|
onboardingStageDefault.set(.onboardingComplete)
|
||||||
|
m.onboardingStage = .onboardingComplete
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func strHasSimplexFileLink(_ text: String) -> Bool {
|
||||||
|
text.starts(with: "simplex:/file") || text.starts(with: "https://simplex.chat/file")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func urlForTemporaryDatabase() -> URL {
|
||||||
|
URL(fileURLWithPath: generateNewFileName(getMigrationTempFilesDirectory().path + "/" + "migration", "db", fullPath: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PassphraseEnteringView: View {
|
||||||
|
@Binding var migrationState: MigrationToState?
|
||||||
|
@State private var useKeychain = true
|
||||||
|
@State var currentKey: String
|
||||||
|
@State private var verifyingPassphrase: Bool = false
|
||||||
|
@FocusState private var keyboardVisible: Bool
|
||||||
|
@Binding var alert: MigrateToDeviceViewAlert?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
settingsRow("key", color: .secondary) {
|
||||||
|
Toggle("Save passphrase in Keychain", isOn: $useKeychain)
|
||||||
|
}
|
||||||
|
|
||||||
|
PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
|
||||||
|
.focused($keyboardVisible)
|
||||||
|
Button(action: {
|
||||||
|
verifyingPassphrase = true
|
||||||
|
hideKeyboard()
|
||||||
|
Task {
|
||||||
|
let (status, _) = chatInitTemporaryDatabase(url: getAppDatabasePath(), key: currentKey, confirmation: .yesUp)
|
||||||
|
let success = switch status {
|
||||||
|
case .ok, .invalidConfirmation: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
if success {
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = .migration(passphrase: currentKey, confirmation: .yesUp, useKeychain: useKeychain)
|
||||||
|
}
|
||||||
|
} else if case .errorMigration = status {
|
||||||
|
await MainActor.run {
|
||||||
|
migrationState = .migrationConfirmation(status: status, passphrase: currentKey, useKeychain: useKeychain)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showErrorOnMigrationIfNeeded(status, $alert)
|
||||||
|
}
|
||||||
|
verifyingPassphrase = false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
settingsRow("key", color: .secondary) {
|
||||||
|
Text("Open chat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(verifyingPassphrase || currentKey.isEmpty)
|
||||||
|
} header: {
|
||||||
|
Text("Enter passphrase")
|
||||||
|
} footer: {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
if useKeychain {
|
||||||
|
Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.")
|
||||||
|
} else {
|
||||||
|
Text("You have to enter passphrase every time the app starts - it is not stored on the device.")
|
||||||
|
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
||||||
|
Text("**Warning**: Instant push notifications require passphrase saved in Keychain.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.callout)
|
||||||
|
.padding(.top, 1)
|
||||||
|
.onTapGesture { keyboardVisible = false }
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
|
keyboardVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if verifyingPassphrase {
|
||||||
|
progressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateToDeviceViewAlert?>) {
|
||||||
|
switch status {
|
||||||
|
case .invalidConfirmation:
|
||||||
|
alert.wrappedValue = .invalidConfirmation()
|
||||||
|
case .errorNotADatabase:
|
||||||
|
alert.wrappedValue = .wrongPassphrase()
|
||||||
|
case .errorKeychain:
|
||||||
|
alert.wrappedValue = .keychainError()
|
||||||
|
case let .errorSQL(_, error):
|
||||||
|
alert.wrappedValue = .databaseError(message: error)
|
||||||
|
case let .unknown(error):
|
||||||
|
alert.wrappedValue = .unknownError(message: error)
|
||||||
|
case .errorMigration: ()
|
||||||
|
case .ok: ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func progressView() -> some View {
|
||||||
|
VStack {
|
||||||
|
ProgressView().scaleEffect(2)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity )
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MigrationChatReceiver {
|
||||||
|
let ctrl: chat_ctrl
|
||||||
|
let databaseUrl: URL
|
||||||
|
let processReceivedMsg: (ChatResponse) async -> Void
|
||||||
|
private var receiveLoop: Task<Void, Never>?
|
||||||
|
private var receiveMessages = true
|
||||||
|
|
||||||
|
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (ChatResponse) async -> Void) {
|
||||||
|
self.ctrl = ctrl
|
||||||
|
self.databaseUrl = databaseUrl
|
||||||
|
self.processReceivedMsg = processReceivedMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
logger.debug("MigrationChatReceiver.start")
|
||||||
|
receiveMessages = true
|
||||||
|
if receiveLoop != nil { return }
|
||||||
|
receiveLoop = Task { await receiveMsgLoop() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveMsgLoop() async {
|
||||||
|
// TODO use function that has timeout
|
||||||
|
if let msg = await chatRecvMsg(ctrl) {
|
||||||
|
Task {
|
||||||
|
await TerminalItems.shared.add(.resp(.now, msg))
|
||||||
|
}
|
||||||
|
logger.debug("processReceivedMsg: \(msg.responseType)")
|
||||||
|
await processReceivedMsg(msg)
|
||||||
|
}
|
||||||
|
if self.receiveMessages {
|
||||||
|
_ = try? await Task.sleep(nanoseconds: 7_500_000)
|
||||||
|
await receiveMsgLoop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopAndCleanUp() {
|
||||||
|
logger.debug("MigrationChatReceiver.stop")
|
||||||
|
receiveMessages = false
|
||||||
|
receiveLoop?.cancel()
|
||||||
|
receiveLoop = nil
|
||||||
|
chat_close_store(ctrl)
|
||||||
|
try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_chat.db")
|
||||||
|
try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_agent.db")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MigrateToDevice_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
MigrateToDevice(migrationState: Binding.constant(.pasteOrScanLink))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,7 +86,7 @@ struct NewChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if case .connect = selection {
|
if case .connect = selection {
|
||||||
ConnectView(showQRCodeScanner: showQRCodeScanner, pastedLink: $pastedLink, alert: $alert)
|
ConnectView(showQRCodeScanner: $showQRCodeScanner, pastedLink: $pastedLink, alert: $alert)
|
||||||
.transition(.move(edge: .trailing))
|
.transition(.move(edge: .trailing))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,8 +284,7 @@ private struct InviteView: View {
|
|||||||
|
|
||||||
private struct ConnectView: View {
|
private struct ConnectView: View {
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
@State var showQRCodeScanner = false
|
@Binding var showQRCodeScanner: Bool
|
||||||
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
|
|
||||||
@Binding var pastedLink: String
|
@Binding var pastedLink: String
|
||||||
@Binding var alert: NewChatViewAlert?
|
@Binding var alert: NewChatViewAlert?
|
||||||
@State private var sheet: PlanAndConnectActionSheet?
|
@State private var sheet: PlanAndConnectActionSheet?
|
||||||
@@ -295,32 +294,13 @@ private struct ConnectView: View {
|
|||||||
Section("Paste the link you received") {
|
Section("Paste the link you received") {
|
||||||
pasteLinkView()
|
pasteLinkView()
|
||||||
}
|
}
|
||||||
|
Section("Or scan QR code") {
|
||||||
scanCodeView()
|
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.actionSheet(item: $sheet) { s in
|
.actionSheet(item: $sheet) { s in
|
||||||
planAndConnectActionSheet(s, dismiss: true, cleanup: { pastedLink = "" })
|
planAndConnectActionSheet(s, dismiss: true, cleanup: { pastedLink = "" })
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
||||||
cameraAuthorizationStatus = status
|
|
||||||
if showQRCodeScanner {
|
|
||||||
switch status {
|
|
||||||
case .notDetermined: askCameraAuthorization()
|
|
||||||
case .restricted: showQRCodeScanner = false
|
|
||||||
case .denied: showQRCodeScanner = false
|
|
||||||
case .authorized: ()
|
|
||||||
@unknown default: askCameraAuthorization()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
|
|
||||||
AVCaptureDevice.requestAccess(for: .video) { allowed in
|
|
||||||
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
|
|
||||||
if allowed { cb?() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func pasteLinkView() -> some View {
|
@ViewBuilder private func pasteLinkView() -> some View {
|
||||||
@@ -351,8 +331,45 @@ private struct ConnectView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func scanCodeView() -> some View {
|
private func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||||
Section("Or scan QR code") {
|
switch resp {
|
||||||
|
case let .success(r):
|
||||||
|
let link = r.string
|
||||||
|
if strIsSimplexLink(r.string) {
|
||||||
|
connect(link)
|
||||||
|
} else {
|
||||||
|
alert = .newChatSomeAlert(alert: .someAlert(
|
||||||
|
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
|
||||||
|
id: "processQRCode: code is not a SimpleX link"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
case let .failure(e):
|
||||||
|
logger.error("processQRCode QR code error: \(e.localizedDescription)")
|
||||||
|
alert = .newChatSomeAlert(alert: .someAlert(
|
||||||
|
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
|
||||||
|
id: "processQRCode: failure"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func connect(_ link: String) {
|
||||||
|
planAndConnect(
|
||||||
|
link,
|
||||||
|
showAlert: { alert = .planAndConnectAlert(alert: $0) },
|
||||||
|
showActionSheet: { sheet = $0 },
|
||||||
|
dismiss: true,
|
||||||
|
incognito: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScannerInView: View {
|
||||||
|
@Binding var showQRCodeScanner: Bool
|
||||||
|
let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void
|
||||||
|
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
|
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
|
||||||
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
|
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
|
||||||
.aspectRatio(1, contentMode: .fit)
|
.aspectRatio(1, contentMode: .fit)
|
||||||
@@ -396,37 +413,26 @@ private struct ConnectView: View {
|
|||||||
.disabled(cameraAuthorizationStatus == .restricted)
|
.disabled(cameraAuthorizationStatus == .restricted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.onAppear {
|
||||||
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||||
private func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
cameraAuthorizationStatus = status
|
||||||
switch resp {
|
if showQRCodeScanner {
|
||||||
case let .success(r):
|
switch status {
|
||||||
let link = r.string
|
case .notDetermined: askCameraAuthorization()
|
||||||
if strIsSimplexLink(r.string) {
|
case .restricted: showQRCodeScanner = false
|
||||||
connect(link)
|
case .denied: showQRCodeScanner = false
|
||||||
} else {
|
case .authorized: ()
|
||||||
alert = .newChatSomeAlert(alert: .someAlert(
|
@unknown default: askCameraAuthorization()
|
||||||
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
|
}
|
||||||
id: "processQRCode: code is not a SimpleX link"
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
case let .failure(e):
|
|
||||||
logger.error("processQRCode QR code error: \(e.localizedDescription)")
|
|
||||||
alert = .newChatSomeAlert(alert: .someAlert(
|
|
||||||
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
|
|
||||||
id: "processQRCode: failure"
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func connect(_ link: String) {
|
func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
|
||||||
planAndConnect(
|
AVCaptureDevice.requestAccess(for: .video) { allowed in
|
||||||
link,
|
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
|
||||||
showAlert: { alert = .planAndConnectAlert(alert: $0) },
|
if allowed { cb?() }
|
||||||
showActionSheet: { sheet = $0 },
|
}
|
||||||
dismiss: true,
|
|
||||||
incognito: nil
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
struct SimpleXInfo: View {
|
struct SimpleXInfo: View {
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
@@ -44,6 +45,15 @@ struct SimpleXInfo: View {
|
|||||||
if onboarding {
|
if onboarding {
|
||||||
OnboardingActionButton()
|
OnboardingActionButton()
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
Button {
|
||||||
|
m.migrationState = .pasteOrScanLink
|
||||||
|
} label: {
|
||||||
|
Label("Migrate from another device", systemImage: "tray.and.arrow.down")
|
||||||
|
.font(.subheadline)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@@ -54,9 +64,24 @@ struct SimpleXInfo: View {
|
|||||||
}
|
}
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
}
|
}
|
||||||
.frame(minHeight: g.size.height)
|
.frame(minHeight: g.size.height)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: Binding(
|
||||||
|
get: { m.migrationState != nil },
|
||||||
|
set: { _ in
|
||||||
|
m.migrationState = nil
|
||||||
|
MigrationToDeviceState.save(nil) }
|
||||||
|
)) {
|
||||||
|
NavigationView {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
MigrateToDevice(migrationState: $m.migrationState)
|
||||||
|
}
|
||||||
|
.navigationTitle("Migrate here")
|
||||||
|
.background(colorScheme == .light ? Color(uiColor: .tertiarySystemGroupedBackground) : .clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
.sheet(isPresented: $showHowItWorks) {
|
.sheet(isPresented: $showHowItWorks) {
|
||||||
HowItWorks(onboarding: onboarding)
|
HowItWorks(onboarding: onboarding)
|
||||||
}
|
}
|
||||||
@@ -87,6 +112,7 @@ struct SimpleXInfo: View {
|
|||||||
|
|
||||||
struct OnboardingActionButton: View {
|
struct OnboardingActionButton: View {
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if m.currentUser == nil {
|
if m.currentUser == nil {
|
||||||
@@ -111,6 +137,21 @@ struct OnboardingActionButton: View {
|
|||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func actionButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View {
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(label).font(.title2)
|
||||||
|
Image(systemName: "greaterthan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SimpleXInfo_Previews: PreviewProvider {
|
struct SimpleXInfo_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ struct ConnectDesktopView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
if viaSettings {
|
if viaSettings {
|
||||||
viewBody
|
viewBody
|
||||||
.modifier(BackButton(label: "Back") {
|
.modifier(BackButton(label: "Back", disabled: Binding.constant(false)) {
|
||||||
if m.activeRemoteCtrl {
|
if m.activeRemoteCtrl {
|
||||||
alert = .disconnectDesktop(action: .back)
|
alert = .disconnectDesktop(action: .back)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
//
|
||||||
|
// AppSettings.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by Avently on 26.02.2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SimpleXChat
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension AppSettings {
|
||||||
|
public func importIntoApp() {
|
||||||
|
let def = UserDefaults.standard
|
||||||
|
if var val = networkConfig {
|
||||||
|
// migrating from Android/desktop BUT shouldn't be here ever because it should be changed in migration stage
|
||||||
|
if case .onionViaSocks = val.hostMode {
|
||||||
|
val.hostMode = .publicHost
|
||||||
|
val.requiredHostMode = true
|
||||||
|
}
|
||||||
|
val.socksProxy = nil
|
||||||
|
setNetCfg(val)
|
||||||
|
}
|
||||||
|
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
|
||||||
|
if let val = privacyAcceptImages {
|
||||||
|
privacyAcceptImagesGroupDefault.set(val)
|
||||||
|
def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||||
|
}
|
||||||
|
if let val = privacyLinkPreviews { def.setValue(val, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) }
|
||||||
|
if let val = privacyShowChatPreviews { def.setValue(val, forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) }
|
||||||
|
if let val = privacySaveLastDraft { def.setValue(val, forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) }
|
||||||
|
if let val = privacyProtectScreen { def.setValue(val, forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) }
|
||||||
|
if let val = notificationMode { ChatModel.shared.notificationMode = val.toNotificationsMode() }
|
||||||
|
if let val = notificationPreviewMode { ntfPreviewModeGroupDefault.set(val) }
|
||||||
|
if let val = webrtcPolicyRelay { def.setValue(val, forKey: DEFAULT_WEBRTC_POLICY_RELAY) }
|
||||||
|
if let val = webrtcICEServers { def.setValue(val, forKey: DEFAULT_WEBRTC_ICE_SERVERS) }
|
||||||
|
if let val = confirmRemoteSessions { def.setValue(val, forKey: DEFAULT_CONFIRM_REMOTE_SESSIONS) }
|
||||||
|
if let val = connectRemoteViaMulticast { def.setValue(val, forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) }
|
||||||
|
if let val = connectRemoteViaMulticastAuto { def.setValue(val, forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO) }
|
||||||
|
if let val = developerTools { def.setValue(val, forKey: DEFAULT_DEVELOPER_TOOLS) }
|
||||||
|
if let val = confirmDBUpgrades { confirmDBUpgradesGroupDefault.set(val) }
|
||||||
|
if let val = androidCallOnLockScreen { def.setValue(val.rawValue, forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN) }
|
||||||
|
if let val = iosCallKitEnabled { callKitEnabledGroupDefault.set(val) }
|
||||||
|
if let val = iosCallKitCallsInRecents { def.setValue(val, forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var current: AppSettings {
|
||||||
|
let def = UserDefaults.standard
|
||||||
|
var c = AppSettings.defaults
|
||||||
|
c.networkConfig = getNetCfg()
|
||||||
|
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
|
||||||
|
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
|
||||||
|
c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
||||||
|
c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS)
|
||||||
|
c.privacySaveLastDraft = def.bool(forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT)
|
||||||
|
c.privacyProtectScreen = def.bool(forKey: DEFAULT_PRIVACY_PROTECT_SCREEN)
|
||||||
|
c.notificationMode = AppSettingsNotificationMode.from(ChatModel.shared.notificationMode)
|
||||||
|
c.notificationPreviewMode = ntfPreviewModeGroupDefault.get()
|
||||||
|
c.webrtcPolicyRelay = def.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
||||||
|
c.webrtcICEServers = def.stringArray(forKey: DEFAULT_WEBRTC_ICE_SERVERS)
|
||||||
|
c.confirmRemoteSessions = def.bool(forKey: DEFAULT_CONFIRM_REMOTE_SESSIONS)
|
||||||
|
c.connectRemoteViaMulticast = def.bool(forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST)
|
||||||
|
c.connectRemoteViaMulticastAuto = def.bool(forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO)
|
||||||
|
c.developerTools = def.bool(forKey: DEFAULT_DEVELOPER_TOOLS)
|
||||||
|
c.confirmDBUpgrades = confirmDBUpgradesGroupDefault.get()
|
||||||
|
c.androidCallOnLockScreen = AppSettingsLockScreenCalls(rawValue: def.string(forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN)!)
|
||||||
|
c.iosCallKitEnabled = callKitEnabledGroupDefault.get()
|
||||||
|
c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,10 +64,10 @@ struct DeveloperView: View {
|
|||||||
|
|
||||||
private func setPQExperimentalEnabled(_ enable: Bool) {
|
private func setPQExperimentalEnabled(_ enable: Bool) {
|
||||||
do {
|
do {
|
||||||
try apiSetPQEnabled(enable)
|
try apiSetPQEncryption(enable)
|
||||||
} catch let error {
|
} catch let error {
|
||||||
let err = responseError(error)
|
let err = responseError(error)
|
||||||
logger.error("apiSetPQEnabled \(err)")
|
logger.error("apiSetPQEncryption \(err)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct ProtocolServerView: View {
|
|||||||
ProgressView().scaleEffect(2)
|
ProgressView().scaleEffect(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modifier(BackButton(label: "Your \(proto) servers") {
|
.modifier(BackButton(label: "Your \(proto) servers", disabled: Binding.constant(false)) {
|
||||||
server = serverToEdit
|
server = serverToEdit
|
||||||
dismiss()
|
dismiss()
|
||||||
})
|
})
|
||||||
@@ -117,6 +117,7 @@ struct ProtocolServerView: View {
|
|||||||
|
|
||||||
struct BackButton: ViewModifier {
|
struct BackButton: ViewModifier {
|
||||||
var label: LocalizedStringKey = "Back"
|
var label: LocalizedStringKey = "Back"
|
||||||
|
@Binding var disabled: Bool
|
||||||
var action: () -> Void
|
var action: () -> Void
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
@@ -130,6 +131,7 @@ struct BackButton: ViewModifier {
|
|||||||
Text(label)
|
Text(label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.disabled(disabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ struct ProtocolServersView: View {
|
|||||||
.sheet(isPresented: $showScanProtoServer) {
|
.sheet(isPresented: $showScanProtoServer) {
|
||||||
ScanProtocolServer(servers: $servers)
|
ScanProtocolServer(servers: $servers)
|
||||||
}
|
}
|
||||||
.modifier(BackButton {
|
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||||
if saveDisabled {
|
if saveDisabled {
|
||||||
dismiss()
|
dismiss()
|
||||||
justOpened = false
|
justOpened = false
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ let DEFAULT_NOTIFICATION_ALERT_SHOWN = "notificationAlertShown"
|
|||||||
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
|
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
|
||||||
let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers"
|
let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers"
|
||||||
let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents"
|
let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents"
|
||||||
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" // unused. Use GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES instead
|
||||||
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
|
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
|
||||||
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
|
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
|
||||||
let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
|
let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
|
||||||
@@ -51,6 +51,8 @@ let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice"
|
|||||||
let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert"
|
let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert"
|
||||||
let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
|
let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
|
||||||
let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
|
let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
|
||||||
|
let DEFAULT_MIGRATION_TO_STAGE = "migrationToStage"
|
||||||
|
let DEFAULT_MIGRATION_FROM_STAGE = "migrationFromStage"
|
||||||
let DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME = "customDisappearingMessageTime"
|
let DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME = "customDisappearingMessageTime"
|
||||||
let DEFAULT_SHOW_UNREAD_AND_FAVORITES = "showUnreadAndFavorites"
|
let DEFAULT_SHOW_UNREAD_AND_FAVORITES = "showUnreadAndFavorites"
|
||||||
let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess"
|
let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess"
|
||||||
@@ -58,6 +60,8 @@ let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions"
|
|||||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
||||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
||||||
|
|
||||||
|
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||||
|
|
||||||
let appDefaults: [String: Any] = [
|
let appDefaults: [String: Any] = [
|
||||||
DEFAULT_SHOW_LA_NOTICE: false,
|
DEFAULT_SHOW_LA_NOTICE: false,
|
||||||
DEFAULT_LA_NOTICE_SHOWN: false,
|
DEFAULT_LA_NOTICE_SHOWN: false,
|
||||||
@@ -93,6 +97,7 @@ let appDefaults: [String: Any] = [
|
|||||||
DEFAULT_CONFIRM_REMOTE_SESSIONS: false,
|
DEFAULT_CONFIRM_REMOTE_SESSIONS: false,
|
||||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
|
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
|
||||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
||||||
|
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
|
||||||
]
|
]
|
||||||
|
|
||||||
// not used anymore
|
// not used anymore
|
||||||
@@ -148,10 +153,14 @@ struct SettingsView: View {
|
|||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||||
@Binding var showSettings: Bool
|
@Binding var showSettings: Bool
|
||||||
|
@State private var showProgress: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
settingsView()
|
settingsView()
|
||||||
|
if showProgress {
|
||||||
|
progressView()
|
||||||
|
}
|
||||||
if let la = chatModel.laRequest {
|
if let la = chatModel.laRequest {
|
||||||
LocalAuthView(authRequest: la)
|
LocalAuthView(authRequest: la)
|
||||||
}
|
}
|
||||||
@@ -202,9 +211,17 @@ struct SettingsView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
settingsRow("desktopcomputer") { Text("Use from desktop") }
|
settingsRow("desktopcomputer") { Text("Use from desktop") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
|
||||||
|
.navigationTitle("Migrate device")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
} label: {
|
||||||
|
settingsRow("tray.and.arrow.up") { Text("Migrate to another device") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.disabled(chatModel.chatRunning != true)
|
.disabled(chatModel.chatRunning != true)
|
||||||
|
|
||||||
Section("Settings") {
|
Section("Settings") {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
NotificationsView()
|
NotificationsView()
|
||||||
@@ -349,6 +366,13 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func progressView() -> some View {
|
||||||
|
VStack {
|
||||||
|
ProgressView().scaleEffect(2)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity )
|
||||||
|
}
|
||||||
|
|
||||||
private enum NotificationAlert {
|
private enum NotificationAlert {
|
||||||
case enable
|
case enable
|
||||||
case error(LocalizedStringKey, String)
|
case error(LocalizedStringKey, String)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ struct UserAddressView: View {
|
|||||||
userAddressScrollView()
|
userAddressScrollView()
|
||||||
} else {
|
} else {
|
||||||
userAddressScrollView()
|
userAddressScrollView()
|
||||||
.modifier(BackButton {
|
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||||
if savedAAS == aas {
|
if savedAAS == aas {
|
||||||
dismiss()
|
dismiss()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -640,7 +640,9 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
|||||||
cleanupDirectFile(aChatItem)
|
cleanupDirectFile(aChatItem)
|
||||||
return nil
|
return nil
|
||||||
case let .sndFileRcvCancelled(_, aChatItem, _):
|
case let .sndFileRcvCancelled(_, aChatItem, _):
|
||||||
cleanupDirectFile(aChatItem)
|
if let aChatItem = aChatItem {
|
||||||
|
cleanupDirectFile(aChatItem)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
case let .sndFileCompleteXFTP(_, aChatItem, _):
|
case let .sndFileCompleteXFTP(_, aChatItem, _):
|
||||||
cleanupFile(aChatItem)
|
cleanupFile(aChatItem)
|
||||||
|
|||||||
@@ -57,15 +57,15 @@
|
|||||||
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65F341297D3F3600B67AF3 /* VersionView.swift */; };
|
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65F341297D3F3600B67AF3 /* VersionView.swift */; };
|
||||||
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6BA666289BD954009B8ECC /* DismissSheets.swift */; };
|
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6BA666289BD954009B8ECC /* DismissSheets.swift */; };
|
||||||
5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */; };
|
5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */; };
|
||||||
|
5C746DB82BA0DA920049D734 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C746DB32BA0DA920049D734 /* libffi.a */; };
|
||||||
|
5C746DB92BA0DA920049D734 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C746DB42BA0DA920049D734 /* libgmpxx.a */; };
|
||||||
|
5C746DBA2BA0DA920049D734 /* libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C746DB52BA0DA920049D734 /* libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo.a */; };
|
||||||
|
5C746DBB2BA0DA920049D734 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C746DB62BA0DA920049D734 /* libgmp.a */; };
|
||||||
|
5C746DBC2BA0DA920049D734 /* libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C746DB72BA0DA920049D734 /* libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo-ghc9.6.3.a */; };
|
||||||
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
|
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
|
||||||
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
||||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
|
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
|
||||||
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
|
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
|
||||||
5C777BD82B99B38B00C72EFF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C777BD32B99B38B00C72EFF /* libgmp.a */; };
|
|
||||||
5C777BD92B99B38B00C72EFF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C777BD42B99B38B00C72EFF /* libgmpxx.a */; };
|
|
||||||
5C777BDA2B99B38B00C72EFF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C777BD52B99B38B00C72EFF /* libffi.a */; };
|
|
||||||
5C777BDB2B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C777BD62B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a */; };
|
|
||||||
5C777BDC2B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C777BD72B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a */; };
|
|
||||||
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
|
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
|
||||||
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; };
|
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; };
|
||||||
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; };
|
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; };
|
||||||
@@ -185,6 +185,9 @@
|
|||||||
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
|
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
|
||||||
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
|
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
|
||||||
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; };
|
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; };
|
||||||
|
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; };
|
||||||
|
8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */; };
|
||||||
|
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */; };
|
||||||
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
|
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
|
||||||
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
|
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
|
||||||
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; };
|
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; };
|
||||||
@@ -321,15 +324,15 @@
|
|||||||
5C6D183229E93FBA00D430B3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = "pl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
5C6D183229E93FBA00D430B3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = "pl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
5C6D183329E93FBA00D430B3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
5C6D183329E93FBA00D430B3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFeaturePreferenceView.swift; sourceTree = "<group>"; };
|
5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFeaturePreferenceView.swift; sourceTree = "<group>"; };
|
||||||
|
5C746DB32BA0DA920049D734 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
|
5C746DB42BA0DA920049D734 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
|
5C746DB52BA0DA920049D734 /* libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo.a"; sourceTree = "<group>"; };
|
||||||
|
5C746DB62BA0DA920049D734 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
|
5C746DB72BA0DA920049D734 /* libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||||
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
|
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
|
||||||
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
||||||
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
||||||
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
|
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
|
||||||
5C777BD32B99B38B00C72EFF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
|
||||||
5C777BD42B99B38B00C72EFF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
|
||||||
5C777BD52B99B38B00C72EFF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
|
||||||
5C777BD62B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a"; sourceTree = "<group>"; };
|
|
||||||
5C777BD72B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a"; sourceTree = "<group>"; };
|
|
||||||
5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
5C84FE9429A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
5C84FE9429A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
@@ -473,6 +476,9 @@
|
|||||||
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
|
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
|
||||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
|
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
|
||||||
8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = "<group>"; };
|
8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = "<group>"; };
|
||||||
|
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||||
|
8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToDevice.swift; sourceTree = "<group>"; };
|
||||||
|
8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromDevice.swift; sourceTree = "<group>"; };
|
||||||
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
|
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
|
||||||
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
|
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
@@ -514,13 +520,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5C777BD92B99B38B00C72EFF /* libgmpxx.a in Frameworks */,
|
5C746DB92BA0DA920049D734 /* libgmpxx.a in Frameworks */,
|
||||||
5C777BDB2B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a in Frameworks */,
|
5C746DBA2BA0DA920049D734 /* libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo.a in Frameworks */,
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
5C777BD82B99B38B00C72EFF /* libgmp.a in Frameworks */,
|
5C746DB82BA0DA920049D734 /* libffi.a in Frameworks */,
|
||||||
5C777BDA2B99B38B00C72EFF /* libffi.a in Frameworks */,
|
5C746DBC2BA0DA920049D734 /* libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo-ghc9.6.3.a in Frameworks */,
|
||||||
|
5C746DBB2BA0DA920049D734 /* libgmp.a in Frameworks */,
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
5C777BDC2B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a in Frameworks */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -553,6 +559,7 @@
|
|||||||
5CB924DD27A8622200ACCCDD /* NewChat */,
|
5CB924DD27A8622200ACCCDD /* NewChat */,
|
||||||
5CFA59C22860B04D00863A68 /* Database */,
|
5CFA59C22860B04D00863A68 /* Database */,
|
||||||
5CB634AB29E46CDB0066AD6B /* LocalAuth */,
|
5CB634AB29E46CDB0066AD6B /* LocalAuth */,
|
||||||
|
8C7D94982B8894D300B7B9E1 /* Migration */,
|
||||||
5CA8D01B2AD9B076001FD661 /* RemoteAccess */,
|
5CA8D01B2AD9B076001FD661 /* RemoteAccess */,
|
||||||
5CB924DF27A8678B00ACCCDD /* UserSettings */,
|
5CB924DF27A8678B00ACCCDD /* UserSettings */,
|
||||||
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
|
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
|
||||||
@@ -582,11 +589,11 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5C777BD52B99B38B00C72EFF /* libffi.a */,
|
5C746DB32BA0DA920049D734 /* libffi.a */,
|
||||||
5C777BD32B99B38B00C72EFF /* libgmp.a */,
|
5C746DB62BA0DA920049D734 /* libgmp.a */,
|
||||||
5C777BD42B99B38B00C72EFF /* libgmpxx.a */,
|
5C746DB42BA0DA920049D734 /* libgmpxx.a */,
|
||||||
5C777BD62B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a */,
|
5C746DB72BA0DA920049D734 /* libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo-ghc9.6.3.a */,
|
||||||
5C777BD72B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a */,
|
5C746DB52BA0DA920049D734 /* libHSsimplex-chat-5.6.0.2-CrEKCx0J5BfIirfPSOWUVo.a */,
|
||||||
);
|
);
|
||||||
path = Libraries;
|
path = Libraries;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -766,6 +773,7 @@
|
|||||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */,
|
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */,
|
||||||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */,
|
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */,
|
||||||
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */,
|
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */,
|
||||||
|
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */,
|
||||||
);
|
);
|
||||||
path = UserSettings;
|
path = UserSettings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -893,6 +901,15 @@
|
|||||||
path = Group;
|
path = Group;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
8C7D94982B8894D300B7B9E1 /* Migration */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */,
|
||||||
|
8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */,
|
||||||
|
);
|
||||||
|
path = Migration;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXHeadersBuildPhase section */
|
/* Begin PBXHeadersBuildPhase section */
|
||||||
@@ -1124,6 +1141,7 @@
|
|||||||
5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */,
|
5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */,
|
||||||
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */,
|
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */,
|
||||||
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
|
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
|
||||||
|
8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */,
|
||||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
|
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
|
||||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
|
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
|
||||||
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */,
|
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */,
|
||||||
@@ -1179,6 +1197,7 @@
|
|||||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */,
|
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */,
|
||||||
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
|
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
|
||||||
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
|
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
|
||||||
|
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */,
|
||||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
||||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
||||||
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */,
|
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */,
|
||||||
@@ -1220,6 +1239,7 @@
|
|||||||
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
|
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
|
||||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
||||||
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
|
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
|
||||||
|
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */,
|
||||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
|
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
|
||||||
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
|
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
|
||||||
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
|
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
|
||||||
@@ -1509,7 +1529,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 202;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1531,7 +1551,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1552,7 +1572,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 202;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1574,7 +1594,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1633,7 +1653,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 202;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1646,7 +1666,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -1665,7 +1685,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 202;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1678,7 +1698,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -1697,7 +1717,7 @@
|
|||||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 202;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
@@ -1721,7 +1741,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Libraries/sim",
|
"$(PROJECT_DIR)/Libraries/sim",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1743,7 +1763,7 @@
|
|||||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 202;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
@@ -1767,7 +1787,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Libraries/sim",
|
"$(PROJECT_DIR)/Libraries/sim",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|||||||
@@ -54,6 +54,38 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func chatInitTemporaryDatabase(url: URL, key: String? = nil, confirmation: MigrationConfirmation = .error) -> (DBMigrationResult, chat_ctrl?) {
|
||||||
|
let dbPath = url.path
|
||||||
|
let dbKey = key ?? randomDatabasePassword()
|
||||||
|
logger.debug("chatInitTemporaryDatabase path: \(dbPath)")
|
||||||
|
var temporaryController: chat_ctrl? = nil
|
||||||
|
var cPath = dbPath.cString(using: .utf8)!
|
||||||
|
var cKey = dbKey.cString(using: .utf8)!
|
||||||
|
var cConfirm = confirmation.rawValue.cString(using: .utf8)!
|
||||||
|
let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &temporaryController)!
|
||||||
|
return (dbMigrationResult(fromCString(cjson)), temporaryController)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func chatInitControllerRemovingDatabases() {
|
||||||
|
let dbPath = getAppDatabasePath().path
|
||||||
|
let fm = FileManager.default
|
||||||
|
// Remove previous databases, otherwise, can be .errorNotADatabase with nil controller
|
||||||
|
try? fm.removeItem(atPath: dbPath + CHAT_DB)
|
||||||
|
try? fm.removeItem(atPath: dbPath + AGENT_DB)
|
||||||
|
|
||||||
|
let dbKey = randomDatabasePassword()
|
||||||
|
logger.debug("chatInitControllerRemovingDatabases path: \(dbPath)")
|
||||||
|
var cPath = dbPath.cString(using: .utf8)!
|
||||||
|
var cKey = dbKey.cString(using: .utf8)!
|
||||||
|
var cConfirm = MigrationConfirmation.error.rawValue.cString(using: .utf8)!
|
||||||
|
chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &chatController)
|
||||||
|
|
||||||
|
// We need only controller, not databases
|
||||||
|
try? fm.removeItem(atPath: dbPath + CHAT_DB)
|
||||||
|
try? fm.removeItem(atPath: dbPath + AGENT_DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public func chatCloseStore() {
|
public func chatCloseStore() {
|
||||||
let err = fromCString(chat_close_store(getChatCtrl()))
|
let err = fromCString(chat_close_store(getChatCtrl()))
|
||||||
if err != "" {
|
if err != "" {
|
||||||
@@ -73,17 +105,17 @@ public func resetChatCtrl() {
|
|||||||
migrationResult = nil
|
migrationResult = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
|
public func sendSimpleXCmd(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) -> ChatResponse {
|
||||||
var c = cmd.cmdString.cString(using: .utf8)!
|
var c = cmd.cmdString.cString(using: .utf8)!
|
||||||
let cjson = chat_send_cmd(getChatCtrl(), &c)!
|
let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c)!
|
||||||
return chatResponse(fromCString(cjson))
|
return chatResponse(fromCString(cjson))
|
||||||
}
|
}
|
||||||
|
|
||||||
// in microseconds
|
// in microseconds
|
||||||
let MESSAGE_TIMEOUT: Int32 = 15_000_000
|
let MESSAGE_TIMEOUT: Int32 = 15_000_000
|
||||||
|
|
||||||
public func recvSimpleXMsg() -> ChatResponse? {
|
public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil) -> ChatResponse? {
|
||||||
if let cjson = chat_recv_msg_wait(getChatCtrl(), MESSAGE_TIMEOUT) {
|
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), MESSAGE_TIMEOUT) {
|
||||||
let s = fromCString(cjson)
|
let s = fromCString(cjson)
|
||||||
return s == "" ? nil : chatResponse(s)
|
return s == "" ? nil : chatResponse(s)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,12 +32,15 @@ public enum ChatCommand {
|
|||||||
case setTempFolder(tempFolder: String)
|
case setTempFolder(tempFolder: String)
|
||||||
case setFilesFolder(filesFolder: String)
|
case setFilesFolder(filesFolder: String)
|
||||||
case apiSetEncryptLocalFiles(enable: Bool)
|
case apiSetEncryptLocalFiles(enable: Bool)
|
||||||
case apiSetPQEnabled(enable: Bool)
|
case apiSetPQEncryption(enable: Bool)
|
||||||
case apiAllowContactPQ(contactId: Int64)
|
case apiSetContactPQ(contactId: Int64, enable: Bool)
|
||||||
case apiExportArchive(config: ArchiveConfig)
|
case apiExportArchive(config: ArchiveConfig)
|
||||||
case apiImportArchive(config: ArchiveConfig)
|
case apiImportArchive(config: ArchiveConfig)
|
||||||
case apiDeleteStorage
|
case apiDeleteStorage
|
||||||
case apiStorageEncryption(config: DBEncryptionConfig)
|
case apiStorageEncryption(config: DBEncryptionConfig)
|
||||||
|
case testStorageEncryption(key: String)
|
||||||
|
case apiSaveSettings(settings: AppSettings)
|
||||||
|
case apiGetSettings(settings: AppSettings)
|
||||||
case apiGetChats(userId: Int64)
|
case apiGetChats(userId: Int64)
|
||||||
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
|
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
|
||||||
case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
|
case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
|
||||||
@@ -132,6 +135,9 @@ public enum ChatCommand {
|
|||||||
case listRemoteCtrls
|
case listRemoteCtrls
|
||||||
case stopRemoteCtrl
|
case stopRemoteCtrl
|
||||||
case deleteRemoteCtrl(remoteCtrlId: Int64)
|
case deleteRemoteCtrl(remoteCtrlId: Int64)
|
||||||
|
case apiUploadStandaloneFile(userId: Int64, file: CryptoFile)
|
||||||
|
case apiDownloadStandaloneFile(userId: Int64, url: String, file: CryptoFile)
|
||||||
|
case apiStandaloneFileInfo(url: String)
|
||||||
// misc
|
// misc
|
||||||
case showVersion
|
case showVersion
|
||||||
case string(String)
|
case string(String)
|
||||||
@@ -164,12 +170,15 @@ public enum ChatCommand {
|
|||||||
case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)"
|
case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)"
|
||||||
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
|
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
|
||||||
case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))"
|
case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))"
|
||||||
case let .apiSetPQEnabled(enable): return "/_pq \(onOff(enable))"
|
case let .apiSetPQEncryption(enable): return "/pq \(onOff(enable))"
|
||||||
case let .apiAllowContactPQ(contactId): return "/_pq allow \(contactId)"
|
case let .apiSetContactPQ(contactId, enable): return "/_pq @\(contactId) \(onOff(enable))"
|
||||||
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
|
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
|
||||||
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
||||||
case .apiDeleteStorage: return "/_db delete"
|
case .apiDeleteStorage: return "/_db delete"
|
||||||
case let .apiStorageEncryption(cfg): return "/_db encryption \(encodeJSON(cfg))"
|
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 .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
|
case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
|
||||||
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
|
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
|
||||||
(search == "" ? "" : " search=\(search)")
|
(search == "" ? "" : " search=\(search)")
|
||||||
@@ -282,6 +291,9 @@ public enum ChatCommand {
|
|||||||
case .listRemoteCtrls: return "/list remote ctrls"
|
case .listRemoteCtrls: return "/list remote ctrls"
|
||||||
case .stopRemoteCtrl: return "/stop remote ctrl"
|
case .stopRemoteCtrl: return "/stop remote ctrl"
|
||||||
case let .deleteRemoteCtrl(rcId): return "/delete remote ctrl \(rcId)"
|
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 .showVersion: return "/version"
|
||||||
case let .string(str): return str
|
case let .string(str): return str
|
||||||
}
|
}
|
||||||
@@ -310,12 +322,15 @@ public enum ChatCommand {
|
|||||||
case .setTempFolder: return "setTempFolder"
|
case .setTempFolder: return "setTempFolder"
|
||||||
case .setFilesFolder: return "setFilesFolder"
|
case .setFilesFolder: return "setFilesFolder"
|
||||||
case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles"
|
case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles"
|
||||||
case .apiSetPQEnabled: return "apiSetPQEnabled"
|
case .apiSetPQEncryption: return "apiSetPQEncryption"
|
||||||
case .apiAllowContactPQ: return "apiAllowContactPQ"
|
case .apiSetContactPQ: return "apiSetContactPQ"
|
||||||
case .apiExportArchive: return "apiExportArchive"
|
case .apiExportArchive: return "apiExportArchive"
|
||||||
case .apiImportArchive: return "apiImportArchive"
|
case .apiImportArchive: return "apiImportArchive"
|
||||||
case .apiDeleteStorage: return "apiDeleteStorage"
|
case .apiDeleteStorage: return "apiDeleteStorage"
|
||||||
case .apiStorageEncryption: return "apiStorageEncryption"
|
case .apiStorageEncryption: return "apiStorageEncryption"
|
||||||
|
case .testStorageEncryption: return "testStorageEncryption"
|
||||||
|
case .apiSaveSettings: return "apiSaveSettings"
|
||||||
|
case .apiGetSettings: return "apiGetSettings"
|
||||||
case .apiGetChats: return "apiGetChats"
|
case .apiGetChats: return "apiGetChats"
|
||||||
case .apiGetChat: return "apiGetChat"
|
case .apiGetChat: return "apiGetChat"
|
||||||
case .apiGetChatItemInfo: return "apiGetChatItemInfo"
|
case .apiGetChatItemInfo: return "apiGetChatItemInfo"
|
||||||
@@ -408,6 +423,9 @@ public enum ChatCommand {
|
|||||||
case .listRemoteCtrls: return "listRemoteCtrls"
|
case .listRemoteCtrls: return "listRemoteCtrls"
|
||||||
case .stopRemoteCtrl: return "stopRemoteCtrl"
|
case .stopRemoteCtrl: return "stopRemoteCtrl"
|
||||||
case .deleteRemoteCtrl: return "deleteRemoteCtrl"
|
case .deleteRemoteCtrl: return "deleteRemoteCtrl"
|
||||||
|
case .apiUploadStandaloneFile: return "apiUploadStandaloneFile"
|
||||||
|
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
|
||||||
|
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
|
||||||
case .showVersion: return "showVersion"
|
case .showVersion: return "showVersion"
|
||||||
case .string: return "console command"
|
case .string: return "console command"
|
||||||
}
|
}
|
||||||
@@ -442,6 +460,8 @@ public enum ChatCommand {
|
|||||||
return .apiUnhideUser(userId: userId, viewPwd: obfuscate(viewPwd))
|
return .apiUnhideUser(userId: userId, viewPwd: obfuscate(viewPwd))
|
||||||
case let .apiDeleteUser(userId, delSMPQueues, viewPwd):
|
case let .apiDeleteUser(userId, delSMPQueues, viewPwd):
|
||||||
return .apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: obfuscate(viewPwd))
|
return .apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: obfuscate(viewPwd))
|
||||||
|
case let .testStorageEncryption(key):
|
||||||
|
return .testStorageEncryption(key: obfuscate(key))
|
||||||
default: return self
|
default: return self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -590,20 +610,28 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
// receiving file events
|
// receiving file events
|
||||||
case rcvFileAccepted(user: UserRef, chatItem: AChatItem)
|
case rcvFileAccepted(user: UserRef, chatItem: AChatItem)
|
||||||
case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer)
|
case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer)
|
||||||
case rcvFileStart(user: UserRef, chatItem: AChatItem)
|
case standaloneFileInfo(fileMeta: MigrationFileLinkData?)
|
||||||
case rcvFileProgressXFTP(user: UserRef, chatItem: AChatItem, receivedSize: Int64, totalSize: Int64)
|
case rcvStandaloneFileCreated(user: UserRef, rcvFileTransfer: RcvFileTransfer)
|
||||||
|
case rcvFileStart(user: UserRef, chatItem: AChatItem) // send by chats
|
||||||
|
case rcvFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, receivedSize: Int64, totalSize: Int64, rcvFileTransfer: RcvFileTransfer)
|
||||||
case rcvFileComplete(user: UserRef, chatItem: AChatItem)
|
case rcvFileComplete(user: UserRef, chatItem: AChatItem)
|
||||||
case rcvFileCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
|
case rcvStandaloneFileComplete(user: UserRef, targetPath: String, rcvFileTransfer: RcvFileTransfer)
|
||||||
|
case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
|
||||||
case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
|
case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
|
||||||
case rcvFileError(user: UserRef, chatItem: AChatItem)
|
case rcvFileError(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
|
||||||
// sending file events
|
// sending file events
|
||||||
case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||||
case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||||
case sndFileCancelled(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
|
case sndFileRcvCancelled(user: UserRef, chatItem_: AChatItem?, sndFileTransfer: SndFileTransfer)
|
||||||
case sndFileRcvCancelled(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
case sndFileCancelled(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
|
||||||
case sndFileProgressXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
|
case sndStandaloneFileCreated(user: UserRef, fileTransferMeta: FileTransferMeta) // returned by _upload
|
||||||
|
case sndFileStartXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) // not used
|
||||||
|
case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
|
||||||
|
case sndFileRedirectStartXFTP(user: UserRef, fileTransferMeta: FileTransferMeta, redirectMeta: FileTransferMeta)
|
||||||
case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta)
|
case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta)
|
||||||
case sndFileError(user: UserRef, chatItem: AChatItem)
|
case sndStandaloneFileComplete(user: UserRef, fileTransferMeta: FileTransferMeta, rcvURIs: [String])
|
||||||
|
case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
|
||||||
|
case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
|
||||||
// call events
|
// call events
|
||||||
case callInvitation(callInvitation: RcvCallInvitation)
|
case callInvitation(callInvitation: RcvCallInvitation)
|
||||||
case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
|
case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
|
||||||
@@ -624,7 +652,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
|
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
|
||||||
case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason)
|
case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason)
|
||||||
// pq
|
// pq
|
||||||
case contactPQAllowed(user: UserRef, contact: Contact)
|
case contactPQAllowed(user: UserRef, contact: Contact, pqEncryption: Bool)
|
||||||
case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool)
|
case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool)
|
||||||
// misc
|
// misc
|
||||||
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
|
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
|
||||||
@@ -632,6 +660,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
||||||
case chatError(user_: UserRef?, chatError: ChatError)
|
case chatError(user_: UserRef?, chatError: ChatError)
|
||||||
case archiveImported(archiveErrors: [ArchiveError])
|
case archiveImported(archiveErrors: [ArchiveError])
|
||||||
|
case appSettings(appSettings: AppSettings)
|
||||||
|
|
||||||
public var responseType: String {
|
public var responseType: String {
|
||||||
get {
|
get {
|
||||||
@@ -744,18 +773,26 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .newMemberContactReceivedInv: return "newMemberContactReceivedInv"
|
case .newMemberContactReceivedInv: return "newMemberContactReceivedInv"
|
||||||
case .rcvFileAccepted: return "rcvFileAccepted"
|
case .rcvFileAccepted: return "rcvFileAccepted"
|
||||||
case .rcvFileAcceptedSndCancelled: return "rcvFileAcceptedSndCancelled"
|
case .rcvFileAcceptedSndCancelled: return "rcvFileAcceptedSndCancelled"
|
||||||
|
case .standaloneFileInfo: return "standaloneFileInfo"
|
||||||
|
case .rcvStandaloneFileCreated: return "rcvStandaloneFileCreated"
|
||||||
case .rcvFileStart: return "rcvFileStart"
|
case .rcvFileStart: return "rcvFileStart"
|
||||||
case .rcvFileProgressXFTP: return "rcvFileProgressXFTP"
|
case .rcvFileProgressXFTP: return "rcvFileProgressXFTP"
|
||||||
case .rcvFileComplete: return "rcvFileComplete"
|
case .rcvFileComplete: return "rcvFileComplete"
|
||||||
|
case .rcvStandaloneFileComplete: return "rcvStandaloneFileComplete"
|
||||||
case .rcvFileCancelled: return "rcvFileCancelled"
|
case .rcvFileCancelled: return "rcvFileCancelled"
|
||||||
case .rcvFileSndCancelled: return "rcvFileSndCancelled"
|
case .rcvFileSndCancelled: return "rcvFileSndCancelled"
|
||||||
case .rcvFileError: return "rcvFileError"
|
case .rcvFileError: return "rcvFileError"
|
||||||
case .sndFileStart: return "sndFileStart"
|
case .sndFileStart: return "sndFileStart"
|
||||||
case .sndFileComplete: return "sndFileComplete"
|
case .sndFileComplete: return "sndFileComplete"
|
||||||
case .sndFileCancelled: return "sndFileCancelled"
|
case .sndFileCancelled: return "sndFileCancelled"
|
||||||
case .sndFileRcvCancelled: return "sndFileRcvCancelled"
|
case .sndStandaloneFileCreated: return "sndStandaloneFileCreated"
|
||||||
|
case .sndFileStartXFTP: return "sndFileStartXFTP"
|
||||||
case .sndFileProgressXFTP: return "sndFileProgressXFTP"
|
case .sndFileProgressXFTP: return "sndFileProgressXFTP"
|
||||||
|
case .sndFileRedirectStartXFTP: return "sndFileRedirectStartXFTP"
|
||||||
|
case .sndFileRcvCancelled: return "sndFileRcvCancelled"
|
||||||
case .sndFileCompleteXFTP: return "sndFileCompleteXFTP"
|
case .sndFileCompleteXFTP: return "sndFileCompleteXFTP"
|
||||||
|
case .sndStandaloneFileComplete: return "sndStandaloneFileComplete"
|
||||||
|
case .sndFileCancelledXFTP: return "sndFileCancelledXFTP"
|
||||||
case .sndFileError: return "sndFileError"
|
case .sndFileError: return "sndFileError"
|
||||||
case .callInvitation: return "callInvitation"
|
case .callInvitation: return "callInvitation"
|
||||||
case .callOffer: return "callOffer"
|
case .callOffer: return "callOffer"
|
||||||
@@ -781,6 +818,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .chatCmdError: return "chatCmdError"
|
case .chatCmdError: return "chatCmdError"
|
||||||
case .chatError: return "chatError"
|
case .chatError: return "chatError"
|
||||||
case .archiveImported: return "archiveImported"
|
case .archiveImported: return "archiveImported"
|
||||||
|
case .appSettings: return "appSettings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -896,19 +934,27 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .newMemberContactReceivedInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
|
case let .newMemberContactReceivedInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
|
||||||
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
|
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||||
case .rcvFileAcceptedSndCancelled: return noDetails
|
case .rcvFileAcceptedSndCancelled: return noDetails
|
||||||
|
case let .standaloneFileInfo(fileMeta): return String(describing: fileMeta)
|
||||||
|
case .rcvStandaloneFileCreated: return noDetails
|
||||||
case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem))
|
case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||||
case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)")
|
case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize, _): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)")
|
||||||
|
case let .rcvStandaloneFileComplete(u, targetPath, _): return withUser(u, targetPath)
|
||||||
case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem))
|
case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||||
case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .rcvFileError(u, chatItem): return withUser(u, String(describing: chatItem))
|
case let .rcvFileError(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
|
case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
|
||||||
|
case .sndStandaloneFileCreated: return noDetails
|
||||||
|
case let .sndFileStartXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)")
|
case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)")
|
||||||
|
case let .sndFileRedirectStartXFTP(u, _, redirectMeta): return withUser(u, String(describing: redirectMeta))
|
||||||
case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .sndFileError(u, chatItem): return withUser(u, String(describing: chatItem))
|
case let .sndStandaloneFileComplete(u, _, rcvURIs): return withUser(u, String(rcvURIs.count))
|
||||||
|
case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
|
case let .sndFileError(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .callInvitation(inv): return String(describing: inv)
|
case let .callInvitation(inv): return String(describing: inv)
|
||||||
case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))")
|
case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))")
|
||||||
case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")
|
case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")
|
||||||
@@ -926,13 +972,14 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)"
|
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)"
|
||||||
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
||||||
case .remoteCtrlStopped: return noDetails
|
case .remoteCtrlStopped: return noDetails
|
||||||
case let .contactPQAllowed(u, contact): return withUser(u, "contact: \(String(describing: contact))")
|
case let .contactPQAllowed(u, contact, pqEncryption): return withUser(u, "contact: \(String(describing: contact))\npqEncryption: \(pqEncryption)")
|
||||||
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
|
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
|
||||||
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
|
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
|
||||||
case .cmdOk: return noDetails
|
case .cmdOk: return noDetails
|
||||||
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
||||||
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
|
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
|
||||||
case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
|
case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
|
||||||
|
case let .appSettings(appSettings): return String(describing: appSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1534,7 +1581,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem {
|
|||||||
public static var values: [NotificationsMode] = [.instant, .periodic, .off]
|
public static var values: [NotificationsMode] = [.instant, .periodic, .off]
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum NotificationPreviewMode: String, SelectableItem {
|
public enum NotificationPreviewMode: String, SelectableItem, Codable {
|
||||||
case hidden
|
case hidden
|
||||||
case contact
|
case contact
|
||||||
case message
|
case message
|
||||||
@@ -1734,6 +1781,7 @@ public enum StoreError: Decodable {
|
|||||||
case fileIdNotFoundBySharedMsgId(sharedMsgId: String)
|
case fileIdNotFoundBySharedMsgId(sharedMsgId: String)
|
||||||
case sndFileNotFoundXFTP(agentSndFileId: String)
|
case sndFileNotFoundXFTP(agentSndFileId: String)
|
||||||
case rcvFileNotFoundXFTP(agentRcvFileId: String)
|
case rcvFileNotFoundXFTP(agentRcvFileId: String)
|
||||||
|
case extraFileDescrNotFoundXFTP(fileId: Int64)
|
||||||
case connectionNotFound(agentConnId: String)
|
case connectionNotFound(agentConnId: String)
|
||||||
case connectionNotFoundById(connId: Int64)
|
case connectionNotFoundById(connId: Int64)
|
||||||
case connectionNotFoundByMemberId(groupMemberId: Int64)
|
case connectionNotFoundByMemberId(groupMemberId: Int64)
|
||||||
@@ -1895,3 +1943,147 @@ public enum RemoteCtrlError: Decodable {
|
|||||||
case badVersion(appVersion: String)
|
case badVersion(appVersion: String)
|
||||||
// case protocolError(protocolError: RemoteProtocolError)
|
// case protocolError(protocolError: RemoteProtocolError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct MigrationFileLinkData: Codable {
|
||||||
|
let networkConfig: NetworkConfig?
|
||||||
|
|
||||||
|
public init(networkConfig: NetworkConfig) {
|
||||||
|
self.networkConfig = networkConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct NetworkConfig: Codable {
|
||||||
|
let socksProxy: String?
|
||||||
|
let hostMode: HostMode?
|
||||||
|
let requiredHostMode: Bool?
|
||||||
|
|
||||||
|
public init(socksProxy: String?, hostMode: HostMode?, requiredHostMode: Bool?) {
|
||||||
|
self.socksProxy = socksProxy
|
||||||
|
self.hostMode = hostMode
|
||||||
|
self.requiredHostMode = requiredHostMode
|
||||||
|
}
|
||||||
|
|
||||||
|
public func transformToPlatformSupported() -> NetworkConfig {
|
||||||
|
return if let hostMode, let requiredHostMode {
|
||||||
|
NetworkConfig(
|
||||||
|
socksProxy: nil,
|
||||||
|
hostMode: hostMode == .onionViaSocks ? .onionHost : hostMode,
|
||||||
|
requiredHostMode: requiredHostMode
|
||||||
|
)
|
||||||
|
} else { self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func addToLink(link: String) -> String {
|
||||||
|
"\(link)&data=\(encodeJSON(self).addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!)"
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func readFromLink(link: String) -> MigrationFileLinkData? {
|
||||||
|
// standaloneFileInfo(link)
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AppSettings: Codable, Equatable {
|
||||||
|
public var networkConfig: NetCfg? = nil
|
||||||
|
public var privacyEncryptLocalFiles: Bool? = nil
|
||||||
|
public var privacyAcceptImages: Bool? = nil
|
||||||
|
public var privacyLinkPreviews: Bool? = nil
|
||||||
|
public var privacyShowChatPreviews: Bool? = nil
|
||||||
|
public var privacySaveLastDraft: Bool? = nil
|
||||||
|
public var privacyProtectScreen: Bool? = nil
|
||||||
|
public var notificationMode: AppSettingsNotificationMode? = nil
|
||||||
|
public var notificationPreviewMode: NotificationPreviewMode? = nil
|
||||||
|
public var webrtcPolicyRelay: Bool? = nil
|
||||||
|
public var webrtcICEServers: [String]? = nil
|
||||||
|
public var confirmRemoteSessions: Bool? = nil
|
||||||
|
public var connectRemoteViaMulticast: Bool? = nil
|
||||||
|
public var connectRemoteViaMulticastAuto: Bool? = nil
|
||||||
|
public var developerTools: Bool? = nil
|
||||||
|
public var confirmDBUpgrades: Bool? = nil
|
||||||
|
public var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil
|
||||||
|
public var iosCallKitEnabled: Bool? = nil
|
||||||
|
public var iosCallKitCallsInRecents: Bool? = nil
|
||||||
|
|
||||||
|
public func prepareForExport() -> AppSettings {
|
||||||
|
var empty = AppSettings()
|
||||||
|
let def = AppSettings.defaults
|
||||||
|
if networkConfig != def.networkConfig { empty.networkConfig = networkConfig }
|
||||||
|
if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||||
|
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
|
||||||
|
if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews }
|
||||||
|
if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||||
|
if privacySaveLastDraft != def.privacySaveLastDraft { empty.privacySaveLastDraft = privacySaveLastDraft }
|
||||||
|
if privacyProtectScreen != def.privacyProtectScreen { empty.privacyProtectScreen = privacyProtectScreen }
|
||||||
|
if notificationMode != def.notificationMode { empty.notificationMode = notificationMode }
|
||||||
|
if notificationPreviewMode != def.notificationPreviewMode { empty.notificationPreviewMode = notificationPreviewMode }
|
||||||
|
if webrtcPolicyRelay != def.webrtcPolicyRelay { empty.webrtcPolicyRelay = webrtcPolicyRelay }
|
||||||
|
if webrtcICEServers != def.webrtcICEServers { empty.webrtcICEServers = webrtcICEServers }
|
||||||
|
if confirmRemoteSessions != def.confirmRemoteSessions { empty.confirmRemoteSessions = confirmRemoteSessions }
|
||||||
|
if connectRemoteViaMulticast != def.connectRemoteViaMulticast {empty.connectRemoteViaMulticast = connectRemoteViaMulticast }
|
||||||
|
if connectRemoteViaMulticastAuto != def.connectRemoteViaMulticastAuto { empty.connectRemoteViaMulticastAuto = connectRemoteViaMulticastAuto }
|
||||||
|
if developerTools != def.developerTools { empty.developerTools = developerTools }
|
||||||
|
if confirmDBUpgrades != def.confirmDBUpgrades { empty.confirmDBUpgrades = confirmDBUpgrades }
|
||||||
|
if androidCallOnLockScreen != def.androidCallOnLockScreen { empty.androidCallOnLockScreen = androidCallOnLockScreen }
|
||||||
|
if iosCallKitEnabled != def.iosCallKitEnabled { empty.iosCallKitEnabled = iosCallKitEnabled }
|
||||||
|
if iosCallKitCallsInRecents != def.iosCallKitCallsInRecents { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var defaults: AppSettings {
|
||||||
|
AppSettings (
|
||||||
|
networkConfig: NetCfg.defaults,
|
||||||
|
privacyEncryptLocalFiles: true,
|
||||||
|
privacyAcceptImages: true,
|
||||||
|
privacyLinkPreviews: true,
|
||||||
|
privacyShowChatPreviews: true,
|
||||||
|
privacySaveLastDraft: true,
|
||||||
|
privacyProtectScreen: false,
|
||||||
|
notificationMode: AppSettingsNotificationMode.instant,
|
||||||
|
notificationPreviewMode: NotificationPreviewMode.message,
|
||||||
|
webrtcPolicyRelay: true,
|
||||||
|
webrtcICEServers: [],
|
||||||
|
confirmRemoteSessions: false,
|
||||||
|
connectRemoteViaMulticast: true,
|
||||||
|
connectRemoteViaMulticastAuto: true,
|
||||||
|
developerTools: false,
|
||||||
|
confirmDBUpgrades: false,
|
||||||
|
androidCallOnLockScreen: AppSettingsLockScreenCalls.show,
|
||||||
|
iosCallKitEnabled: true,
|
||||||
|
iosCallKitCallsInRecents: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AppSettingsNotificationMode: String, Codable {
|
||||||
|
case off
|
||||||
|
case periodic
|
||||||
|
case instant
|
||||||
|
|
||||||
|
public func toNotificationsMode() -> NotificationsMode {
|
||||||
|
switch self {
|
||||||
|
case .instant: .instant
|
||||||
|
case .periodic: .periodic
|
||||||
|
case .off: .off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func from(_ mode: NotificationsMode) -> AppSettingsNotificationMode {
|
||||||
|
switch mode {
|
||||||
|
case .instant: .instant
|
||||||
|
case .periodic: .periodic
|
||||||
|
case .off: .off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//public enum NotificationPreviewMode: Codable {
|
||||||
|
// case hidden
|
||||||
|
// case contact
|
||||||
|
// case message
|
||||||
|
//}
|
||||||
|
|
||||||
|
public enum AppSettingsLockScreenCalls: String, Codable {
|
||||||
|
case disable
|
||||||
|
case show
|
||||||
|
case accept
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun"
|
|||||||
let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode"
|
let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode"
|
||||||
public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" // no longer used
|
public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" // no longer used
|
||||||
public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer used
|
public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer used
|
||||||
|
// This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES
|
||||||
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||||
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
|
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
|
||||||
public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles"
|
public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles"
|
||||||
@@ -36,7 +37,7 @@ let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl"
|
|||||||
let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt"
|
let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt"
|
||||||
public let GROUP_DEFAULT_INCOGNITO = "incognito"
|
public let GROUP_DEFAULT_INCOGNITO = "incognito"
|
||||||
let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase"
|
let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase"
|
||||||
let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase"
|
public let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase"
|
||||||
public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades"
|
public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades"
|
||||||
public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled"
|
public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled"
|
||||||
public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled"
|
public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled"
|
||||||
@@ -169,6 +170,7 @@ public let ntfPreviewModeGroupDefault = EnumDefault<NotificationPreviewMode>(
|
|||||||
|
|
||||||
public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INCOGNITO)
|
public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INCOGNITO)
|
||||||
|
|
||||||
|
// This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES
|
||||||
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||||
|
|
||||||
public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES)
|
public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES)
|
||||||
|
|||||||
@@ -1532,14 +1532,15 @@ public struct Connection: Decodable {
|
|||||||
public var viaGroupLink: Bool
|
public var viaGroupLink: Bool
|
||||||
public var customUserProfileId: Int64?
|
public var customUserProfileId: Int64?
|
||||||
public var connectionCode: SecurityCode?
|
public var connectionCode: SecurityCode?
|
||||||
public var enablePQ: Bool
|
public var pqSupport: Bool
|
||||||
|
public var pqEncryption: Bool
|
||||||
public var pqSndEnabled: Bool?
|
public var pqSndEnabled: Bool?
|
||||||
public var pqRcvEnabled: Bool?
|
public var pqRcvEnabled: Bool?
|
||||||
|
|
||||||
public var connectionStats: ConnectionStats? = nil
|
public var connectionStats: ConnectionStats? = nil
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, enablePQ, pqSndEnabled, pqRcvEnabled
|
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
public var id: ChatId { get { ":\(connId)" } }
|
public var id: ChatId { get { ":\(connId)" } }
|
||||||
@@ -1555,7 +1556,8 @@ public struct Connection: Decodable {
|
|||||||
connStatus: .ready,
|
connStatus: .ready,
|
||||||
connLevel: 0,
|
connLevel: 0,
|
||||||
viaGroupLink: false,
|
viaGroupLink: false,
|
||||||
enablePQ: false
|
pqSupport: false,
|
||||||
|
pqEncryption: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2782,23 +2784,23 @@ public enum CIContent: Decodable, ItemContent {
|
|||||||
case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
||||||
case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
||||||
case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item")
|
case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item")
|
||||||
case let .sndDirectE2EEInfo(e2eeInfo): return directE2EEInfoToText(e2eeInfo)
|
case let .sndDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo)
|
||||||
case let .rcvDirectE2EEInfo(e2eeInfo): return directE2EEInfoToText(e2eeInfo)
|
case let .rcvDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo)
|
||||||
case .sndGroupE2EEInfo: return e2eeInfoNoPQText
|
case .sndGroupE2EEInfo: return e2eeInfoNoPQStr
|
||||||
case .rcvGroupE2EEInfo: return e2eeInfoNoPQText
|
case .rcvGroupE2EEInfo: return e2eeInfoNoPQStr
|
||||||
case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item")
|
case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func directE2EEInfoToText(_ e2eeInfo: E2EEInfo) -> String {
|
private func directE2EEInfoStr(_ e2eeInfo: E2EEInfo) -> String {
|
||||||
e2eeInfo.pqEnabled
|
e2eeInfo.pqEnabled
|
||||||
? NSLocalizedString("This conversation is protected by quantum resistant end-to-end encryption. It has perfect forward secrecy, repudiation and quantum resistant break-in recovery.", comment: "E2EE info chat item")
|
? NSLocalizedString("This chat is protected by quantum resistant end-to-end encryption.", comment: "E2EE info chat item")
|
||||||
: e2eeInfoNoPQText
|
: e2eeInfoNoPQStr
|
||||||
}
|
}
|
||||||
|
|
||||||
private var e2eeInfoNoPQText: String {
|
private var e2eeInfoNoPQStr: String {
|
||||||
NSLocalizedString("This conversation is protected by end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery.", comment: "E2EE info chat item")
|
NSLocalizedString("This chat is protected by end-to-end encryption.", comment: "E2EE info chat item")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String {
|
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String {
|
||||||
@@ -3408,11 +3410,14 @@ public struct SndFileTransfer: Decodable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct RcvFileTransfer: Decodable {
|
public struct RcvFileTransfer: Decodable {
|
||||||
|
public let fileId: Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct FileTransferMeta: Decodable {
|
public struct FileTransferMeta: Decodable {
|
||||||
|
public let fileId: Int64
|
||||||
|
public let fileName: String
|
||||||
|
public let filePath: String
|
||||||
|
public let fileSize: Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum CICallStatus: String, Decodable {
|
public enum CICallStatus: String, Decodable {
|
||||||
@@ -3623,9 +3628,9 @@ public enum RcvConnEvent: Decodable {
|
|||||||
return NSLocalizedString("security code changed", comment: "chat item text")
|
return NSLocalizedString("security code changed", comment: "chat item text")
|
||||||
case let .pqEnabled(enabled):
|
case let .pqEnabled(enabled):
|
||||||
if enabled {
|
if enabled {
|
||||||
return NSLocalizedString("enabled post-quantum encryption", comment: "chat item text")
|
return NSLocalizedString("quantum resistant e2e encryption", comment: "chat item text")
|
||||||
} else {
|
} else {
|
||||||
return NSLocalizedString("disabled post-quantum encryption", comment: "chat item text")
|
return NSLocalizedString("standard end-to-end encryption", comment: "chat item text")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3670,9 +3675,9 @@ public enum SndConnEvent: Decodable {
|
|||||||
return ratchetSyncStatusToText(syncStatus)
|
return ratchetSyncStatusToText(syncStatus)
|
||||||
case let .pqEnabled(enabled):
|
case let .pqEnabled(enabled):
|
||||||
if enabled {
|
if enabled {
|
||||||
return NSLocalizedString("enabled post-quantum encryption", comment: "chat item text")
|
return NSLocalizedString("quantum resistant e2e encryption", comment: "chat item text")
|
||||||
} else {
|
} else {
|
||||||
return NSLocalizedString("disabled post-quantum encryption", comment: "chat item text")
|
return NSLocalizedString("standard end-to-end encryption", comment: "chat item text")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ public let MAX_FILE_SIZE_SMP: Int64 = 8000000
|
|||||||
|
|
||||||
public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(300)
|
public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(300)
|
||||||
|
|
||||||
private let CHAT_DB: String = "_chat.db"
|
let CHAT_DB: String = "_chat.db"
|
||||||
|
|
||||||
private let AGENT_DB: String = "_agent.db"
|
let AGENT_DB: String = "_agent.db"
|
||||||
|
|
||||||
private let CHAT_DB_BAK: String = "_chat.db.bak"
|
private let CHAT_DB_BAK: String = "_chat.db.bak"
|
||||||
|
|
||||||
@@ -83,6 +83,7 @@ public func deleteAppDatabaseAndFiles() {
|
|||||||
try? fm.removeItem(atPath: dbPath + CHAT_DB_BAK)
|
try? fm.removeItem(atPath: dbPath + CHAT_DB_BAK)
|
||||||
try? fm.removeItem(atPath: dbPath + AGENT_DB_BAK)
|
try? fm.removeItem(atPath: dbPath + AGENT_DB_BAK)
|
||||||
try? fm.removeItem(at: getTempFilesDirectory())
|
try? fm.removeItem(at: getTempFilesDirectory())
|
||||||
|
try? fm.removeItem(at: getMigrationTempFilesDirectory())
|
||||||
try? fm.createDirectory(at: getTempFilesDirectory(), withIntermediateDirectories: true)
|
try? fm.createDirectory(at: getTempFilesDirectory(), withIntermediateDirectories: true)
|
||||||
deleteAppFiles()
|
deleteAppFiles()
|
||||||
_ = kcDatabasePassword.remove()
|
_ = kcDatabasePassword.remove()
|
||||||
@@ -183,6 +184,10 @@ public func getTempFilesDirectory() -> URL {
|
|||||||
getAppDirectory().appendingPathComponent("temp_files", isDirectory: true)
|
getAppDirectory().appendingPathComponent("temp_files", isDirectory: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func getMigrationTempFilesDirectory() -> URL {
|
||||||
|
getDocumentsDirectory().appendingPathComponent("migration_temp_files", isDirectory: true)
|
||||||
|
}
|
||||||
|
|
||||||
public func getAppFilesDirectory() -> URL {
|
public func getAppFilesDirectory() -> URL {
|
||||||
getAppDirectory().appendingPathComponent("app_files", isDirectory: true)
|
getAppDirectory().appendingPathComponent("app_files", isDirectory: true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package chat.simplex.app
|
package chat.simplex.app
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import chat.simplex.common.platform.Log
|
import chat.simplex.common.platform.Log
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.media.AudioManager
|
||||||
import android.os.*
|
import android.os.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import chat.simplex.app.model.NtfManager
|
import chat.simplex.app.model.NtfManager
|
||||||
@@ -18,8 +24,7 @@ import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
|||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.CurrentColors
|
import chat.simplex.common.ui.theme.CurrentColors
|
||||||
import chat.simplex.common.ui.theme.DefaultTheme
|
import chat.simplex.common.ui.theme.DefaultTheme
|
||||||
import chat.simplex.common.views.call.RcvCallInvitation
|
import chat.simplex.common.views.call.*
|
||||||
import chat.simplex.common.views.call.activeCallDestroyWebView
|
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||||
@@ -65,7 +70,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
tmpDir.deleteRecursively()
|
tmpDir.deleteRecursively()
|
||||||
tmpDir.mkdir()
|
tmpDir.mkdir()
|
||||||
|
|
||||||
if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
// Present screen for continue migration if it wasn't finished yet
|
||||||
|
if (chatModel.migrationState.value != null) {
|
||||||
|
// It's important, otherwise, user may be locked in undefined state
|
||||||
|
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||||
|
} else if (DatabaseUtils.ksAppPassword.get() == null || DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||||
initChatControllerAndRunMigrations()
|
initChatControllerAndRunMigrations()
|
||||||
}
|
}
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
||||||
@@ -282,6 +291,21 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
activeCallDestroyWebView()
|
activeCallDestroyWebView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SourceLockedOrientationActivity")
|
||||||
|
@Composable
|
||||||
|
override fun androidLockPortraitOrientation() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
||||||
|
// Lock orientation to portrait in order to have good experience with calls
|
||||||
|
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
onDispose {
|
||||||
|
// Unlock orientation
|
||||||
|
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun androidAskToAllowBackgroundCalls(): Boolean {
|
override suspend fun androidAskToAllowBackgroundCalls(): Boolean {
|
||||||
if (SimplexService.isBackgroundRestricted()) {
|
if (SimplexService.isBackgroundRestricted()) {
|
||||||
val userChoice: CompletableDeferred<Boolean> = CompletableDeferred()
|
val userChoice: CompletableDeferred<Boolean> = CompletableDeferred()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package chat.simplex.common.views.database
|
|||||||
|
|
||||||
import SectionItemView
|
import SectionItemView
|
||||||
import SectionTextFooter
|
import SectionTextFooter
|
||||||
|
import TextIconSpaced
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -22,8 +23,9 @@ actual fun SavePassphraseSetting(
|
|||||||
useKeychain: Boolean,
|
useKeychain: Boolean,
|
||||||
initialRandomDBPassphrase: Boolean,
|
initialRandomDBPassphrase: Boolean,
|
||||||
storedKey: Boolean,
|
storedKey: Boolean,
|
||||||
progressIndicator: Boolean,
|
|
||||||
minHeight: Dp,
|
minHeight: Dp,
|
||||||
|
enabled: Boolean,
|
||||||
|
smallPadding: Boolean,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
SectionItemView(minHeight = minHeight) {
|
SectionItemView(minHeight = minHeight) {
|
||||||
@@ -33,7 +35,11 @@ actual fun SavePassphraseSetting(
|
|||||||
stringResource(MR.strings.save_passphrase_in_keychain),
|
stringResource(MR.strings.save_passphrase_in_keychain),
|
||||||
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
|
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
|
||||||
)
|
)
|
||||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
if (smallPadding) {
|
||||||
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
|
} else {
|
||||||
|
TextIconSpaced(false)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
stringResource(MR.strings.save_passphrase_in_keychain),
|
stringResource(MR.strings.save_passphrase_in_keychain),
|
||||||
Modifier.padding(end = 24.dp),
|
Modifier.padding(end = 24.dp),
|
||||||
@@ -43,7 +49,7 @@ actual fun SavePassphraseSetting(
|
|||||||
DefaultSwitch(
|
DefaultSwitch(
|
||||||
checked = useKeychain,
|
checked = useKeychain,
|
||||||
onCheckedChange = onCheckedChange,
|
onCheckedChange = onCheckedChange,
|
||||||
enabled = !initialRandomDBPassphrase && !progressIndicator
|
enabled = enabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,13 +61,14 @@ actual fun DatabaseEncryptionFooter(
|
|||||||
chatDbEncrypted: Boolean?,
|
chatDbEncrypted: Boolean?,
|
||||||
storedKey: MutableState<Boolean>,
|
storedKey: MutableState<Boolean>,
|
||||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||||
|
migration: Boolean,
|
||||||
) {
|
) {
|
||||||
if (chatDbEncrypted == false) {
|
if (chatDbEncrypted == false) {
|
||||||
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
|
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
|
||||||
} else if (useKeychain.value) {
|
} else if (useKeychain.value) {
|
||||||
if (storedKey.value) {
|
if (storedKey.value) {
|
||||||
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
|
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
|
||||||
if (initialRandomDBPassphrase.value) {
|
if (initialRandomDBPassphrase.value && !migration) {
|
||||||
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
|
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
|
||||||
} else {
|
} else {
|
||||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||||
|
|||||||
@@ -110,6 +110,13 @@ fun MainScreen() {
|
|||||||
val localUserCreated = chatModel.localUserCreated.value
|
val localUserCreated = chatModel.localUserCreated.value
|
||||||
var showInitializationView by remember { mutableStateOf(false) }
|
var showInitializationView by remember { mutableStateOf(false) }
|
||||||
when {
|
when {
|
||||||
|
onboarding == OnboardingStage.Step1_SimpleXInfo && chatModel.migrationState.value != null -> {
|
||||||
|
// In migration process. Nothing should interrupt it, that's why it's the first branch in when()
|
||||||
|
SimpleXInfo(chatModel, onboarding = true)
|
||||||
|
if (appPlatform.isDesktop) {
|
||||||
|
ModalManager.fullscreen.showInView()
|
||||||
|
}
|
||||||
|
}
|
||||||
chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress))
|
chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress))
|
||||||
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
|
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
|
||||||
showChatDatabaseError -> {
|
showChatDatabaseError -> {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import chat.simplex.common.ui.theme.*
|
|||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
import chat.simplex.common.views.chat.ComposeState
|
import chat.simplex.common.views.chat.ComposeState
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.views.migration.MigrationToDeviceState
|
||||||
|
import chat.simplex.common.views.migration.MigrationToState
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.ImageResource
|
import dev.icerock.moko.resources.ImageResource
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
@@ -104,6 +106,8 @@ object ChatModel {
|
|||||||
// currently showing invitation
|
// currently showing invitation
|
||||||
val showingInvitation = mutableStateOf(null as ShowingInvitation?)
|
val showingInvitation = mutableStateOf(null as ShowingInvitation?)
|
||||||
|
|
||||||
|
val migrationState: MutableState<MigrationToState?> by lazy { mutableStateOf(MigrationToDeviceState.makeMigrationState()) }
|
||||||
|
|
||||||
var draft = mutableStateOf(null as ComposeState?)
|
var draft = mutableStateOf(null as ComposeState?)
|
||||||
var draftChatId = mutableStateOf(null as String?)
|
var draftChatId = mutableStateOf(null as String?)
|
||||||
|
|
||||||
@@ -1123,11 +1127,19 @@ data class Connection(
|
|||||||
val viaGroupLink: Boolean,
|
val viaGroupLink: Boolean,
|
||||||
val customUserProfileId: Long? = null,
|
val customUserProfileId: Long? = null,
|
||||||
val connectionCode: SecurityCode? = null,
|
val connectionCode: SecurityCode? = null,
|
||||||
|
val pqSupport: Boolean,
|
||||||
|
val pqEncryption: Boolean,
|
||||||
|
val pqSndEnabled: Boolean? = null,
|
||||||
|
val pqRcvEnabled: Boolean? = null,
|
||||||
val connectionStats: ConnectionStats? = null
|
val connectionStats: ConnectionStats? = null
|
||||||
) {
|
) {
|
||||||
val id: ChatId get() = ":$connId"
|
val id: ChatId get() = ":$connId"
|
||||||
|
|
||||||
|
val connPQEnabled: Boolean
|
||||||
|
get() = pqSndEnabled == true && pqRcvEnabled == true
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null)
|
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1853,6 +1865,10 @@ data class ChatItem (
|
|||||||
is CIContent.SndModerated -> false
|
is CIContent.SndModerated -> false
|
||||||
is CIContent.RcvModerated -> false
|
is CIContent.RcvModerated -> false
|
||||||
is CIContent.RcvBlocked -> false
|
is CIContent.RcvBlocked -> false
|
||||||
|
is CIContent.SndDirectE2EEInfo -> false
|
||||||
|
is CIContent.RcvDirectE2EEInfo -> false
|
||||||
|
is CIContent.SndGroupE2EEInfo -> false
|
||||||
|
is CIContent.RcvGroupE2EEInfo -> false
|
||||||
is CIContent.InvalidJSON -> false
|
is CIContent.InvalidJSON -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2283,6 +2299,10 @@ sealed class CIContent: ItemContent {
|
|||||||
@Serializable @SerialName("sndModerated") object SndModerated: CIContent() { override val msgContent: MsgContent? get() = null }
|
@Serializable @SerialName("sndModerated") object SndModerated: CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
@Serializable @SerialName("rcvModerated") object RcvModerated: CIContent() { override val msgContent: MsgContent? get() = null }
|
@Serializable @SerialName("rcvModerated") object RcvModerated: CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
@Serializable @SerialName("rcvBlocked") object RcvBlocked: CIContent() { override val msgContent: MsgContent? get() = null }
|
@Serializable @SerialName("rcvBlocked") object RcvBlocked: CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
|
@Serializable @SerialName("sndDirectE2EEInfo") class SndDirectE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
|
@Serializable @SerialName("rcvDirectE2EEInfo") class RcvDirectE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
|
@Serializable @SerialName("sndGroupE2EEInfo") class SndGroupE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
|
@Serializable @SerialName("rcvGroupE2EEInfo") class RcvGroupE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
@Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null }
|
@Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
|
|
||||||
override val text: String get() = when (this) {
|
override val text: String get() = when (this) {
|
||||||
@@ -2312,6 +2332,10 @@ sealed class CIContent: ItemContent {
|
|||||||
is SndModerated -> generalGetString(MR.strings.moderated_description)
|
is SndModerated -> generalGetString(MR.strings.moderated_description)
|
||||||
is RcvModerated -> generalGetString(MR.strings.moderated_description)
|
is RcvModerated -> generalGetString(MR.strings.moderated_description)
|
||||||
is RcvBlocked -> generalGetString(MR.strings.blocked_by_admin_item_description)
|
is RcvBlocked -> generalGetString(MR.strings.blocked_by_admin_item_description)
|
||||||
|
is SndDirectE2EEInfo -> directE2EEInfoStr(e2eeInfo)
|
||||||
|
is RcvDirectE2EEInfo -> directE2EEInfoStr(e2eeInfo)
|
||||||
|
is SndGroupE2EEInfo -> e2eeInfoNoPQStr
|
||||||
|
is RcvGroupE2EEInfo -> e2eeInfoNoPQStr
|
||||||
is InvalidJSON -> "invalid data"
|
is InvalidJSON -> "invalid data"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2330,6 +2354,15 @@ sealed class CIContent: ItemContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
fun directE2EEInfoStr(e2EEInfo: E2EEInfo): String =
|
||||||
|
if (e2EEInfo.pqEnabled) {
|
||||||
|
generalGetString(MR.strings.e2ee_info_pq_short)
|
||||||
|
} else {
|
||||||
|
e2eeInfoNoPQStr
|
||||||
|
}
|
||||||
|
|
||||||
|
private val e2eeInfoNoPQStr: String = generalGetString(MR.strings.e2ee_info_no_pq_short)
|
||||||
|
|
||||||
fun featureText(feature: Feature, enabled: String, param: Int?): String =
|
fun featureText(feature: Feature, enabled: String, param: Int?): String =
|
||||||
if (feature.hasParam) {
|
if (feature.hasParam) {
|
||||||
"${feature.text}: ${timeText(param)}"
|
"${feature.text}: ${timeText(param)}"
|
||||||
@@ -2744,6 +2777,9 @@ enum class CIGroupInvitationStatus {
|
|||||||
@SerialName("expired") Expired;
|
@SerialName("expired") Expired;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class E2EEInfo (val pqEnabled: Boolean) {}
|
||||||
|
|
||||||
object MsgContentSerializer : KSerializer<MsgContent> {
|
object MsgContentSerializer : KSerializer<MsgContent> {
|
||||||
override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) {
|
override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) {
|
||||||
element("MCText", buildClassSerialDescriptor("MCText") {
|
element("MCText", buildClassSerialDescriptor("MCText") {
|
||||||
@@ -2941,10 +2977,17 @@ enum class FormatColor(val color: String) {
|
|||||||
class SndFileTransfer() {}
|
class SndFileTransfer() {}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class RcvFileTransfer() {}
|
data class RcvFileTransfer(
|
||||||
|
val fileId: Long,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class FileTransferMeta() {}
|
data class FileTransferMeta(
|
||||||
|
val fileId: Long,
|
||||||
|
val fileName: String,
|
||||||
|
val filePath: String,
|
||||||
|
val fileSize: Long,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class CICallStatus {
|
enum class CICallStatus {
|
||||||
@@ -3097,6 +3140,7 @@ sealed class RcvConnEvent {
|
|||||||
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent()
|
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent()
|
||||||
@Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState): RcvConnEvent()
|
@Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState): RcvConnEvent()
|
||||||
@Serializable @SerialName("verificationCodeReset") object VerificationCodeReset: RcvConnEvent()
|
@Serializable @SerialName("verificationCodeReset") object VerificationCodeReset: RcvConnEvent()
|
||||||
|
@Serializable @SerialName("pqEnabled") class PQEnabled(val enabled: Boolean): RcvConnEvent()
|
||||||
|
|
||||||
val text: String get() = when (this) {
|
val text: String get() = when (this) {
|
||||||
is SwitchQueue -> when (phase) {
|
is SwitchQueue -> when (phase) {
|
||||||
@@ -3105,6 +3149,11 @@ sealed class RcvConnEvent {
|
|||||||
}
|
}
|
||||||
is RatchetSync -> ratchetSyncStatusToText(syncStatus)
|
is RatchetSync -> ratchetSyncStatusToText(syncStatus)
|
||||||
is VerificationCodeReset -> generalGetString(MR.strings.rcv_conn_event_verification_code_reset)
|
is VerificationCodeReset -> generalGetString(MR.strings.rcv_conn_event_verification_code_reset)
|
||||||
|
is PQEnabled -> if (enabled) {
|
||||||
|
generalGetString(MR.strings.conn_event_enabled_pq)
|
||||||
|
} else {
|
||||||
|
generalGetString(MR.strings.conn_event_disabled_pq)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3122,6 +3171,7 @@ fun ratchetSyncStatusToText(ratchetSyncStatus: RatchetSyncState): String {
|
|||||||
sealed class SndConnEvent {
|
sealed class SndConnEvent {
|
||||||
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent()
|
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent()
|
||||||
@Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState, val member: GroupMemberRef? = null): SndConnEvent()
|
@Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState, val member: GroupMemberRef? = null): SndConnEvent()
|
||||||
|
@Serializable @SerialName("pqEnabled") class PQEnabled(val enabled: Boolean): SndConnEvent()
|
||||||
|
|
||||||
val text: String
|
val text: String
|
||||||
get() = when (this) {
|
get() = when (this) {
|
||||||
@@ -3150,6 +3200,12 @@ sealed class SndConnEvent {
|
|||||||
}
|
}
|
||||||
ratchetSyncStatusToText(syncStatus)
|
ratchetSyncStatusToText(syncStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is PQEnabled -> if (enabled) {
|
||||||
|
generalGetString(MR.strings.conn_event_enabled_pq)
|
||||||
|
} else {
|
||||||
|
generalGetString(MR.strings.conn_event_disabled_pq)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import chat.simplex.common.views.helpers.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import chat.simplex.common.model.ChatController.getNetCfg
|
||||||
|
import chat.simplex.common.model.ChatController.setNetCfg
|
||||||
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||||
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
|
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
|
import chat.simplex.common.views.migration.MigrationFileLinkData
|
||||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||||
import chat.simplex.common.views.usersettings.*
|
import chat.simplex.common.views.usersettings.*
|
||||||
import com.charleskorn.kaml.Yaml
|
import com.charleskorn.kaml.Yaml
|
||||||
@@ -144,6 +147,8 @@ class AppPreferences {
|
|||||||
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
|
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
|
||||||
|
|
||||||
val onboardingStage = mkEnumPreference(SHARED_PREFS_ONBOARDING_STAGE, OnboardingStage.OnboardingComplete) { OnboardingStage.values().firstOrNull { it.name == this } }
|
val onboardingStage = mkEnumPreference(SHARED_PREFS_ONBOARDING_STAGE, OnboardingStage.OnboardingComplete) { OnboardingStage.values().firstOrNull { it.name == this } }
|
||||||
|
val migrationToStage = mkStrPreference(SHARED_PREFS_MIGRATION_TO_STAGE, null)
|
||||||
|
val migrationFromStage = mkStrPreference(SHARED_PREFS_MIGRATION_FROM_STAGE, null)
|
||||||
val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true)
|
val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true)
|
||||||
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
|
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
|
||||||
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
|
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
|
||||||
@@ -156,6 +161,7 @@ class AppPreferences {
|
|||||||
val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false)
|
val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false)
|
||||||
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
|
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
|
||||||
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
|
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
|
||||||
|
val pqExperimentalEnabled = mkBoolPreference(SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED, false)
|
||||||
|
|
||||||
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
|
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
|
||||||
val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name)
|
val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name)
|
||||||
@@ -176,6 +182,11 @@ class AppPreferences {
|
|||||||
val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true)
|
val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true)
|
||||||
|
|
||||||
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
||||||
|
|
||||||
|
|
||||||
|
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
||||||
|
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
||||||
|
|
||||||
|
|
||||||
private fun mkIntPreference(prefName: String, default: Int) =
|
private fun mkIntPreference(prefName: String, default: Int) =
|
||||||
SharedPreference(
|
SharedPreference(
|
||||||
@@ -276,6 +287,8 @@ class AppPreferences {
|
|||||||
private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime"
|
private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime"
|
||||||
private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage"
|
private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage"
|
||||||
private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage"
|
private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage"
|
||||||
|
const val SHARED_PREFS_MIGRATION_TO_STAGE = "MigrationToStage"
|
||||||
|
const val SHARED_PREFS_MIGRATION_FROM_STAGE = "MigrationFromStage"
|
||||||
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
|
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
|
||||||
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
|
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
|
||||||
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
|
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
|
||||||
@@ -312,6 +325,7 @@ class AppPreferences {
|
|||||||
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
|
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
|
||||||
private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct"
|
private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct"
|
||||||
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
||||||
|
private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled"
|
||||||
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
|
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
|
||||||
private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme"
|
private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme"
|
||||||
private const val SHARED_PREFS_THEMES = "Themes"
|
private const val SHARED_PREFS_THEMES = "Themes"
|
||||||
@@ -324,6 +338,9 @@ class AppPreferences {
|
|||||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||||
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
||||||
|
|
||||||
|
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
||||||
|
private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,6 +417,16 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun startChatWithTemporaryDatabase(ctrl: ChatCtrl, netCfg: NetCfg): User? {
|
||||||
|
Log.d(TAG, "startChatWithTemporaryDatabase")
|
||||||
|
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
|
||||||
|
apiSetNetworkConfig(netCfg, ctrl)
|
||||||
|
apiSetTempFolder(getMigrationTempFilesDirectory().absolutePath, ctrl)
|
||||||
|
apiSetFilesFolder(getMigrationTempFilesDirectory().absolutePath, ctrl)
|
||||||
|
apiStartChat(ctrl)
|
||||||
|
return migrationActiveUser
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun changeActiveUser(rhId: Long?, toUserId: Long, viewPwd: String?) {
|
suspend fun changeActiveUser(rhId: Long?, toUserId: Long, viewPwd: String?) {
|
||||||
try {
|
try {
|
||||||
changeActiveUser_(rhId, toUserId, viewPwd)
|
changeActiveUser_(rhId, toUserId, viewPwd)
|
||||||
@@ -476,8 +503,8 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendCmd(rhId: Long?, cmd: CC): CR {
|
suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null): CR {
|
||||||
val ctrl = ctrl ?: throw Exception("Controller is not initialized")
|
val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized")
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val c = cmd.cmdString
|
val c = cmd.cmdString
|
||||||
@@ -494,7 +521,7 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun recvMsg(ctrl: ChatCtrl): APIResponse? {
|
fun recvMsg(ctrl: ChatCtrl): APIResponse? {
|
||||||
val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT)
|
val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT)
|
||||||
return if (json == "") {
|
return if (json == "") {
|
||||||
null
|
null
|
||||||
@@ -507,8 +534,8 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiGetActiveUser(rh: Long?): User? {
|
suspend fun apiGetActiveUser(rh: Long?, ctrl: ChatCtrl? = null): User? {
|
||||||
val r = sendCmd(rh, CC.ShowActiveUser())
|
val r = sendCmd(rh, CC.ShowActiveUser(), ctrl)
|
||||||
if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh)
|
if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh)
|
||||||
Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}")
|
Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}")
|
||||||
if (rh == null) {
|
if (rh == null) {
|
||||||
@@ -517,8 +544,8 @@ object ChatController {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, sameServers: Boolean = false, pastTimestamp: Boolean = false): User? {
|
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, sameServers: Boolean = false, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? {
|
||||||
val r = sendCmd(rh, CC.CreateActiveUser(p, sameServers = sameServers, pastTimestamp = pastTimestamp))
|
val r = sendCmd(rh, CC.CreateActiveUser(p, sameServers = sameServers, pastTimestamp = pastTimestamp), ctrl)
|
||||||
if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh)
|
if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh)
|
||||||
else if (
|
else if (
|
||||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName ||
|
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName ||
|
||||||
@@ -596,8 +623,8 @@ object ChatController {
|
|||||||
throw Exception("failed to delete the user ${r.responseType} ${r.details}")
|
throw Exception("failed to delete the user ${r.responseType} ${r.details}")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiStartChat(): Boolean {
|
suspend fun apiStartChat(ctrl: ChatCtrl? = null): Boolean {
|
||||||
val r = sendCmd(null, CC.StartChat(mainApp = true))
|
val r = sendCmd(null, CC.StartChat(mainApp = true), ctrl)
|
||||||
when (r) {
|
when (r) {
|
||||||
is CR.ChatStarted -> return true
|
is CR.ChatStarted -> return true
|
||||||
is CR.ChatRunning -> return false
|
is CR.ChatRunning -> return false
|
||||||
@@ -613,14 +640,14 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiSetTempFolder(tempFolder: String) {
|
suspend fun apiSetTempFolder(tempFolder: String, ctrl: ChatCtrl? = null) {
|
||||||
val r = sendCmd(null, CC.SetTempFolder(tempFolder))
|
val r = sendCmd(null, CC.SetTempFolder(tempFolder), ctrl)
|
||||||
if (r is CR.CmdOk) return
|
if (r is CR.CmdOk) return
|
||||||
throw Error("failed to set temp folder: ${r.responseType} ${r.details}")
|
throw Error("failed to set temp folder: ${r.responseType} ${r.details}")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiSetFilesFolder(filesFolder: String) {
|
suspend fun apiSetFilesFolder(filesFolder: String, ctrl: ChatCtrl? = null) {
|
||||||
val r = sendCmd(null, CC.SetFilesFolder(filesFolder))
|
val r = sendCmd(null, CC.SetFilesFolder(filesFolder), ctrl)
|
||||||
if (r is CR.CmdOk) return
|
if (r is CR.CmdOk) return
|
||||||
throw Error("failed to set files folder: ${r.responseType} ${r.details}")
|
throw Error("failed to set files folder: ${r.responseType} ${r.details}")
|
||||||
}
|
}
|
||||||
@@ -633,6 +660,27 @@ object ChatController {
|
|||||||
|
|
||||||
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
|
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
|
||||||
|
|
||||||
|
suspend fun apiSaveAppSettings(settings: AppSettings) {
|
||||||
|
val r = sendCmd(null, CC.ApiSaveSettings(settings))
|
||||||
|
if (r is CR.CmdOk) return
|
||||||
|
throw Error("failed to set app settings: ${r.responseType} ${r.details}")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun apiGetAppSettings(settings: AppSettings): AppSettings {
|
||||||
|
val r = sendCmd(null, CC.ApiGetSettings(settings))
|
||||||
|
if (r is CR.AppSettingsR) return r.appSettings
|
||||||
|
throw Error("failed to get app settings: ${r.responseType} ${r.details}")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun apiSetPQEncryption(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetPQEncryption(enable))
|
||||||
|
|
||||||
|
suspend fun apiSetContactPQ(rh: Long?, contactId: Long, enable: Boolean): Contact? {
|
||||||
|
val r = sendCmd(rh, CC.ApiSetContactPQ(contactId, enable))
|
||||||
|
if (r is CR.ContactPQAllowed) return r.contact
|
||||||
|
apiErrorAlert("apiSetContactPQ", "Error allowing contact PQ", r)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiExportArchive(config: ArchiveConfig) {
|
suspend fun apiExportArchive(config: ArchiveConfig) {
|
||||||
val r = sendCmd(null, CC.ApiExportArchive(config))
|
val r = sendCmd(null, CC.ApiExportArchive(config))
|
||||||
if (r is CR.CmdOk) return
|
if (r is CR.CmdOk) return
|
||||||
@@ -658,6 +706,13 @@ object ChatController {
|
|||||||
throw Exception("failed to set storage encryption: ${r.responseType} ${r.details}")
|
throw Exception("failed to set storage encryption: ${r.responseType} ${r.details}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun testStorageEncryption(key: String, ctrl: ChatCtrl? = null): CR.ChatCmdError? {
|
||||||
|
val r = sendCmd(null, CC.TestStorageEncryption(key), ctrl)
|
||||||
|
if (r is CR.CmdOk) return null
|
||||||
|
else if (r is CR.ChatCmdError) return r
|
||||||
|
throw Exception("failed to test storage encryption: ${r.responseType} ${r.details}")
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiGetChats(rh: Long?): List<Chat> {
|
suspend fun apiGetChats(rh: Long?): List<Chat> {
|
||||||
val userId = kotlin.runCatching { currentUserId("apiGetChats") }.getOrElse { return emptyList() }
|
val userId = kotlin.runCatching { currentUserId("apiGetChats") }.getOrElse { return emptyList() }
|
||||||
val r = sendCmd(rh, CC.ApiGetChats(userId))
|
val r = sendCmd(rh, CC.ApiGetChats(userId))
|
||||||
@@ -794,8 +849,8 @@ object ChatController {
|
|||||||
throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}")
|
throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiSetNetworkConfig(cfg: NetCfg): Boolean {
|
suspend fun apiSetNetworkConfig(cfg: NetCfg, ctrl: ChatCtrl? = null): Boolean {
|
||||||
val r = sendCmd(null, CC.APISetNetworkConfig(cfg))
|
val r = sendCmd(null, CC.APISetNetworkConfig(cfg), ctrl)
|
||||||
return when (r) {
|
return when (r) {
|
||||||
is CR.CmdOk -> true
|
is CR.CmdOk -> true
|
||||||
else -> {
|
else -> {
|
||||||
@@ -1225,6 +1280,36 @@ object ChatController {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun uploadStandaloneFile(user: UserLike, file: CryptoFile, ctrl: ChatCtrl? = null): Pair<FileTransferMeta?, String?> {
|
||||||
|
val r = sendCmd(null, CC.ApiUploadStandaloneFile(user.userId, file), ctrl)
|
||||||
|
return if (r is CR.SndStandaloneFileCreated) {
|
||||||
|
r.fileTransferMeta to null
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "uploadStandaloneFile error: $r")
|
||||||
|
null to r.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadStandaloneFile(user: UserLike, url: String, file: CryptoFile, ctrl: ChatCtrl? = null): Pair<RcvFileTransfer?, String?> {
|
||||||
|
val r = sendCmd(null, CC.ApiDownloadStandaloneFile(user.userId, url, file), ctrl)
|
||||||
|
return if (r is CR.RcvStandaloneFileCreated) {
|
||||||
|
r.rcvFileTransfer to null
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "downloadStandaloneFile error: $r")
|
||||||
|
null to r.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun standaloneFileInfo(url: String, ctrl: ChatCtrl? = null): MigrationFileLinkData? {
|
||||||
|
val r = sendCmd(null, CC.ApiStandaloneFileInfo(url), ctrl)
|
||||||
|
return if (r is CR.StandaloneFileInfo) {
|
||||||
|
r.fileMeta
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "standaloneFileInfo error: $r")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
||||||
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
|
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
|
||||||
val r = sendCmd(rh, CC.ReceiveFile(fileId, encrypted, inline))
|
val r = sendCmd(rh, CC.ReceiveFile(fileId, encrypted, inline))
|
||||||
@@ -1263,11 +1348,11 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiCancelFile(rh: Long?, fileId: Long): AChatItem? {
|
suspend fun apiCancelFile(rh: Long?, fileId: Long, ctrl: ChatCtrl? = null): AChatItem? {
|
||||||
val r = sendCmd(rh, CC.CancelFile(fileId))
|
val r = sendCmd(rh, CC.CancelFile(fileId), ctrl)
|
||||||
return when (r) {
|
return when (r) {
|
||||||
is CR.SndFileCancelled -> r.chatItem
|
is CR.SndFileCancelled -> r.chatItem_
|
||||||
is CR.RcvFileCancelled -> r.chatItem
|
is CR.RcvFileCancelled -> r.chatItem_
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(TAG, "apiCancelFile bad response: ${r.responseType} ${r.details}")
|
Log.d(TAG, "apiCancelFile bad response: ${r.responseType} ${r.details}")
|
||||||
null
|
null
|
||||||
@@ -1554,8 +1639,8 @@ object ChatController {
|
|||||||
|
|
||||||
suspend fun deleteRemoteCtrl(rcId: Long): Boolean = sendCommandOkResp(null, CC.DeleteRemoteCtrl(rcId))
|
suspend fun deleteRemoteCtrl(rcId: Long): Boolean = sendCommandOkResp(null, CC.DeleteRemoteCtrl(rcId))
|
||||||
|
|
||||||
private suspend fun sendCommandOkResp(rh: Long?, cmd: CC): Boolean {
|
private suspend fun sendCommandOkResp(rh: Long?, cmd: CC, ctrl: ChatCtrl? = null): Boolean {
|
||||||
val r = sendCmd(rh, cmd)
|
val r = sendCmd(rh, cmd, ctrl)
|
||||||
val ok = r is CR.CmdOk
|
val ok = r is CR.CmdOk
|
||||||
if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error_alert_title), r)
|
if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error_alert_title), r)
|
||||||
return ok
|
return ok
|
||||||
@@ -1845,11 +1930,16 @@ object ChatController {
|
|||||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||||
cleanupFile(r.chatItem)
|
cleanupFile(r.chatItem)
|
||||||
}
|
}
|
||||||
is CR.RcvFileProgressXFTP ->
|
is CR.RcvFileProgressXFTP -> {
|
||||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
if (r.chatItem_ != null) {
|
||||||
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||||
|
}
|
||||||
|
}
|
||||||
is CR.RcvFileError -> {
|
is CR.RcvFileError -> {
|
||||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
if (r.chatItem_ != null) {
|
||||||
cleanupFile(r.chatItem)
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||||
|
cleanupFile(r.chatItem_)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is CR.SndFileStart ->
|
is CR.SndFileStart ->
|
||||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||||
@@ -1858,18 +1948,25 @@ object ChatController {
|
|||||||
cleanupDirectFile(r.chatItem)
|
cleanupDirectFile(r.chatItem)
|
||||||
}
|
}
|
||||||
is CR.SndFileRcvCancelled -> {
|
is CR.SndFileRcvCancelled -> {
|
||||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
if (r.chatItem_ != null) {
|
||||||
cleanupDirectFile(r.chatItem)
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||||
|
cleanupDirectFile(r.chatItem_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CR.SndFileProgressXFTP -> {
|
||||||
|
if (r.chatItem_ != null) {
|
||||||
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is CR.SndFileProgressXFTP ->
|
|
||||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
|
||||||
is CR.SndFileCompleteXFTP -> {
|
is CR.SndFileCompleteXFTP -> {
|
||||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||||
cleanupFile(r.chatItem)
|
cleanupFile(r.chatItem)
|
||||||
}
|
}
|
||||||
is CR.SndFileError -> {
|
is CR.SndFileError -> {
|
||||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
if (r.chatItem_ != null) {
|
||||||
cleanupFile(r.chatItem)
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||||
|
cleanupFile(r.chatItem_)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is CR.CallInvitation -> {
|
is CR.CallInvitation -> {
|
||||||
chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId))
|
chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId))
|
||||||
@@ -2016,6 +2113,10 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is CR.ContactPQEnabled ->
|
||||||
|
if (active(r.user)) {
|
||||||
|
chatModel.updateContact(rhId, r.contact)
|
||||||
|
}
|
||||||
is CR.ChatCmdError -> when {
|
is CR.ChatCmdError -> when {
|
||||||
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> {
|
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> {
|
||||||
chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart)
|
chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart)
|
||||||
@@ -2234,21 +2335,13 @@ object ChatController {
|
|||||||
|
|
||||||
class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) {
|
class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) {
|
||||||
val set: (T) -> Unit
|
val set: (T) -> Unit
|
||||||
private val _state: MutableState<T> by lazy { mutableStateOf(get()) }
|
private val _state: MutableState<T> = mutableStateOf(get())
|
||||||
val state: State<T> by lazy { _state }
|
val state: State<T> = _state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.set = { value ->
|
this.set = { value ->
|
||||||
set(value)
|
set(value)
|
||||||
try {
|
_state.value = value
|
||||||
_state.value = value
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
// Can be `Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied`
|
|
||||||
Log.i(TAG, e.stackTraceToString())
|
|
||||||
withApi {
|
|
||||||
_state.value = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2274,10 +2367,15 @@ sealed class CC {
|
|||||||
class SetFilesFolder(val filesFolder: String): CC()
|
class SetFilesFolder(val filesFolder: String): CC()
|
||||||
class SetRemoteHostsFolder(val remoteHostsFolder: String): CC()
|
class SetRemoteHostsFolder(val remoteHostsFolder: String): CC()
|
||||||
class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
|
class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
|
||||||
|
class ApiSetPQEncryption(val enable: Boolean): CC()
|
||||||
|
class ApiSetContactPQ(val contactId: Long, val enable: Boolean): CC()
|
||||||
class ApiExportArchive(val config: ArchiveConfig): CC()
|
class ApiExportArchive(val config: ArchiveConfig): CC()
|
||||||
class ApiImportArchive(val config: ArchiveConfig): CC()
|
class ApiImportArchive(val config: ArchiveConfig): CC()
|
||||||
class ApiDeleteStorage: CC()
|
class ApiDeleteStorage: CC()
|
||||||
class ApiStorageEncryption(val config: DBEncryptionConfig): CC()
|
class ApiStorageEncryption(val config: DBEncryptionConfig): CC()
|
||||||
|
class TestStorageEncryption(val key: String): CC()
|
||||||
|
class ApiSaveSettings(val settings: AppSettings): CC()
|
||||||
|
class ApiGetSettings(val settings: AppSettings): CC()
|
||||||
class ApiGetChats(val userId: Long): CC()
|
class ApiGetChats(val userId: Long): CC()
|
||||||
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
|
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
|
||||||
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
|
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
|
||||||
@@ -2371,6 +2469,9 @@ sealed class CC {
|
|||||||
class ListRemoteCtrls(): CC()
|
class ListRemoteCtrls(): CC()
|
||||||
class StopRemoteCtrl(): CC()
|
class StopRemoteCtrl(): CC()
|
||||||
class DeleteRemoteCtrl(val remoteCtrlId: Long): CC()
|
class DeleteRemoteCtrl(val remoteCtrlId: Long): CC()
|
||||||
|
class ApiUploadStandaloneFile(val userId: Long, val file: CryptoFile): CC()
|
||||||
|
class ApiDownloadStandaloneFile(val userId: Long, val url: String, val file: CryptoFile): CC()
|
||||||
|
class ApiStandaloneFileInfo(val url: String): CC()
|
||||||
// misc
|
// misc
|
||||||
class ShowVersion(): CC()
|
class ShowVersion(): CC()
|
||||||
|
|
||||||
@@ -2403,10 +2504,15 @@ sealed class CC {
|
|||||||
is SetFilesFolder -> "/_files_folder $filesFolder"
|
is SetFilesFolder -> "/_files_folder $filesFolder"
|
||||||
is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder"
|
is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder"
|
||||||
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
|
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
|
||||||
|
is ApiSetPQEncryption -> "/pq ${onOff(enable)}"
|
||||||
|
is ApiSetContactPQ -> "/_pq @$contactId ${onOff(enable)}"
|
||||||
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
||||||
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
||||||
is ApiDeleteStorage -> "/_db delete"
|
is ApiDeleteStorage -> "/_db delete"
|
||||||
is ApiStorageEncryption -> "/_db encryption ${json.encodeToString(config)}"
|
is ApiStorageEncryption -> "/_db encryption ${json.encodeToString(config)}"
|
||||||
|
is TestStorageEncryption -> "/db test key $key"
|
||||||
|
is ApiSaveSettings -> "/_save app settings ${json.encodeToString(settings)}"
|
||||||
|
is ApiGetSettings -> "/_get app settings ${json.encodeToString(settings)}"
|
||||||
is ApiGetChats -> "/_get chats $userId pcc=on"
|
is ApiGetChats -> "/_get chats $userId pcc=on"
|
||||||
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||||
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
|
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
|
||||||
@@ -2514,6 +2620,9 @@ sealed class CC {
|
|||||||
is ListRemoteCtrls -> "/list remote ctrls"
|
is ListRemoteCtrls -> "/list remote ctrls"
|
||||||
is StopRemoteCtrl -> "/stop remote ctrl"
|
is StopRemoteCtrl -> "/stop remote ctrl"
|
||||||
is DeleteRemoteCtrl -> "/delete remote ctrl $remoteCtrlId"
|
is DeleteRemoteCtrl -> "/delete remote ctrl $remoteCtrlId"
|
||||||
|
is ApiUploadStandaloneFile -> "/_upload $userId ${file.filePath}"
|
||||||
|
is ApiDownloadStandaloneFile -> "/_download $userId $url ${file.filePath}"
|
||||||
|
is ApiStandaloneFileInfo -> "/_download info $url"
|
||||||
is ShowVersion -> "/version"
|
is ShowVersion -> "/version"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2537,10 +2646,15 @@ sealed class CC {
|
|||||||
is SetFilesFolder -> "setFilesFolder"
|
is SetFilesFolder -> "setFilesFolder"
|
||||||
is SetRemoteHostsFolder -> "setRemoteHostsFolder"
|
is SetRemoteHostsFolder -> "setRemoteHostsFolder"
|
||||||
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
|
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
|
||||||
|
is ApiSetPQEncryption -> "apiSetPQEncryption"
|
||||||
|
is ApiSetContactPQ -> "apiSetContactPQ"
|
||||||
is ApiExportArchive -> "apiExportArchive"
|
is ApiExportArchive -> "apiExportArchive"
|
||||||
is ApiImportArchive -> "apiImportArchive"
|
is ApiImportArchive -> "apiImportArchive"
|
||||||
is ApiDeleteStorage -> "apiDeleteStorage"
|
is ApiDeleteStorage -> "apiDeleteStorage"
|
||||||
is ApiStorageEncryption -> "apiStorageEncryption"
|
is ApiStorageEncryption -> "apiStorageEncryption"
|
||||||
|
is TestStorageEncryption -> "testStorageEncryption"
|
||||||
|
is ApiSaveSettings -> "apiSaveSettings"
|
||||||
|
is ApiGetSettings -> "apiGetSettings"
|
||||||
is ApiGetChats -> "apiGetChats"
|
is ApiGetChats -> "apiGetChats"
|
||||||
is ApiGetChat -> "apiGetChat"
|
is ApiGetChat -> "apiGetChat"
|
||||||
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
|
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
|
||||||
@@ -2633,6 +2747,9 @@ sealed class CC {
|
|||||||
is ListRemoteCtrls -> "listRemoteCtrls"
|
is ListRemoteCtrls -> "listRemoteCtrls"
|
||||||
is StopRemoteCtrl -> "stopRemoteCtrl"
|
is StopRemoteCtrl -> "stopRemoteCtrl"
|
||||||
is DeleteRemoteCtrl -> "deleteRemoteCtrl"
|
is DeleteRemoteCtrl -> "deleteRemoteCtrl"
|
||||||
|
is ApiUploadStandaloneFile -> "apiUploadStandaloneFile"
|
||||||
|
is ApiDownloadStandaloneFile -> "apiDownloadStandaloneFile"
|
||||||
|
is ApiStandaloneFileInfo -> "apiStandaloneFileInfo"
|
||||||
is ShowVersion -> "showVersion"
|
is ShowVersion -> "showVersion"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2650,6 +2767,7 @@ sealed class CC {
|
|||||||
is ApiHideUser -> ApiHideUser(userId, obfuscate(viewPwd))
|
is ApiHideUser -> ApiHideUser(userId, obfuscate(viewPwd))
|
||||||
is ApiUnhideUser -> ApiUnhideUser(userId, obfuscate(viewPwd))
|
is ApiUnhideUser -> ApiUnhideUser(userId, obfuscate(viewPwd))
|
||||||
is ApiDeleteUser -> ApiDeleteUser(userId, delSMPQueues, obfuscateOrNull(viewPwd))
|
is ApiDeleteUser -> ApiDeleteUser(userId, delSMPQueues, obfuscateOrNull(viewPwd))
|
||||||
|
is TestStorageEncryption -> TestStorageEncryption(obfuscate(key))
|
||||||
else -> this
|
else -> this
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3775,6 +3893,13 @@ val json = Json {
|
|||||||
explicitNulls = false
|
explicitNulls = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val jsonShort = Json {
|
||||||
|
prettyPrint = false
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
encodeDefaults = true
|
||||||
|
explicitNulls = false
|
||||||
|
}
|
||||||
|
|
||||||
val yaml = Yaml(configuration = YamlConfiguration(
|
val yaml = Yaml(configuration = YamlConfiguration(
|
||||||
strictMode = false,
|
strictMode = false,
|
||||||
encodeDefaults = false,
|
encodeDefaults = false,
|
||||||
@@ -3964,20 +4089,28 @@ sealed class CR {
|
|||||||
// receiving file events
|
// receiving file events
|
||||||
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR()
|
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR()
|
||||||
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR()
|
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
@Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: UserRef, val chatItem: AChatItem): CR()
|
@Serializable @SerialName("standaloneFileInfo") class StandaloneFileInfo(val fileMeta: MigrationFileLinkData?): CR()
|
||||||
|
@Serializable @SerialName("rcvStandaloneFileCreated") class RcvStandaloneFileCreated(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
|
@Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: UserRef, val chatItem: AChatItem): CR() // send by chats
|
||||||
|
@Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: UserRef, val chatItem_: AChatItem?, val receivedSize: Long, val totalSize: Long, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
@Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: UserRef, val chatItem: AChatItem): CR()
|
@Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: UserRef, val chatItem: AChatItem): CR()
|
||||||
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
|
@Serializable @SerialName("rcvStandaloneFileComplete") class RcvStandaloneFileComplete(val user: UserRef, val targetPath: String, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
|
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
@Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
|
@Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
@Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: UserRef, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): CR()
|
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem: AChatItem): CR()
|
|
||||||
// sending file events
|
// sending file events
|
||||||
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
||||||
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
||||||
@Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR()
|
@Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: UserRef, val chatItem_: AChatItem?, val sndFileTransfer: SndFileTransfer): CR()
|
||||||
@Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
@Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR()
|
||||||
@Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR()
|
@Serializable @SerialName("sndStandaloneFileCreated") class SndStandaloneFileCreated(val user: UserRef, val fileTransferMeta: FileTransferMeta): CR() // returned by _upload
|
||||||
|
@Serializable @SerialName("sndFileStartXFTP") class SndFileStartXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() // not used
|
||||||
|
@Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR()
|
||||||
|
@Serializable @SerialName("sndFileRedirectStartXFTP") class SndFileRedirectStartXFTP(val user: UserRef, val fileTransferMeta: FileTransferMeta, val redirectMeta: FileTransferMeta): CR()
|
||||||
@Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR()
|
@Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR()
|
||||||
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem: AChatItem): CR()
|
@Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List<String>): CR()
|
||||||
|
@Serializable @SerialName("sndFileCancelledXFTP") class SndFileCancelledXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
|
||||||
|
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
|
||||||
// call events
|
// call events
|
||||||
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
|
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
|
||||||
@Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List<RcvCallInvitation>): CR()
|
@Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List<RcvCallInvitation>): CR()
|
||||||
@@ -4002,11 +4135,16 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("remoteCtrlSessionCode") class RemoteCtrlSessionCode(val remoteCtrl_: RemoteCtrlInfo?, val sessionCode: String): CR()
|
@Serializable @SerialName("remoteCtrlSessionCode") class RemoteCtrlSessionCode(val remoteCtrl_: RemoteCtrlInfo?, val sessionCode: String): CR()
|
||||||
@Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrl: RemoteCtrlInfo): CR()
|
@Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrl: RemoteCtrlInfo): CR()
|
||||||
@Serializable @SerialName("remoteCtrlStopped") class RemoteCtrlStopped(val rcsState: RemoteCtrlSessionState, val rcStopReason: RemoteCtrlStopReason): CR()
|
@Serializable @SerialName("remoteCtrlStopped") class RemoteCtrlStopped(val rcsState: RemoteCtrlSessionState, val rcStopReason: RemoteCtrlStopReason): CR()
|
||||||
|
// pq
|
||||||
|
@Serializable @SerialName("contactPQAllowed") class ContactPQAllowed(val user: UserRef, val contact: Contact, val pqEncryption: Boolean): CR()
|
||||||
|
@Serializable @SerialName("contactPQEnabled") class ContactPQEnabled(val user: UserRef, val contact: Contact, val pqEnabled: Boolean): CR()
|
||||||
|
// misc
|
||||||
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR()
|
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR()
|
||||||
@Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR()
|
@Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR()
|
||||||
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR()
|
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR()
|
||||||
@Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR()
|
@Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR()
|
||||||
@Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List<ArchiveError>): CR()
|
@Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List<ArchiveError>): CR()
|
||||||
|
@Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR()
|
||||||
// general
|
// general
|
||||||
@Serializable class Response(val type: String, val json: String): CR()
|
@Serializable class Response(val type: String, val json: String): CR()
|
||||||
@Serializable class Invalid(val str: String): CR()
|
@Serializable class Invalid(val str: String): CR()
|
||||||
@@ -4116,19 +4254,27 @@ sealed class CR {
|
|||||||
is NewMemberContactSentInv -> "newMemberContactSentInv"
|
is NewMemberContactSentInv -> "newMemberContactSentInv"
|
||||||
is NewMemberContactReceivedInv -> "newMemberContactReceivedInv"
|
is NewMemberContactReceivedInv -> "newMemberContactReceivedInv"
|
||||||
is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled"
|
is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled"
|
||||||
|
is StandaloneFileInfo -> "standaloneFileInfo"
|
||||||
|
is RcvStandaloneFileCreated -> "rcvStandaloneFileCreated"
|
||||||
is RcvFileAccepted -> "rcvFileAccepted"
|
is RcvFileAccepted -> "rcvFileAccepted"
|
||||||
is RcvFileStart -> "rcvFileStart"
|
is RcvFileStart -> "rcvFileStart"
|
||||||
is RcvFileComplete -> "rcvFileComplete"
|
is RcvFileComplete -> "rcvFileComplete"
|
||||||
|
is RcvStandaloneFileComplete -> "rcvStandaloneFileComplete"
|
||||||
is RcvFileCancelled -> "rcvFileCancelled"
|
is RcvFileCancelled -> "rcvFileCancelled"
|
||||||
|
is SndStandaloneFileCreated -> "sndStandaloneFileCreated"
|
||||||
|
is SndFileStartXFTP -> "sndFileStartXFTP"
|
||||||
is RcvFileSndCancelled -> "rcvFileSndCancelled"
|
is RcvFileSndCancelled -> "rcvFileSndCancelled"
|
||||||
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
|
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
|
||||||
|
is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP"
|
||||||
is RcvFileError -> "rcvFileError"
|
is RcvFileError -> "rcvFileError"
|
||||||
is SndFileCancelled -> "sndFileCancelled"
|
is SndFileStart -> "sndFileStart"
|
||||||
is SndFileComplete -> "sndFileComplete"
|
is SndFileComplete -> "sndFileComplete"
|
||||||
is SndFileRcvCancelled -> "sndFileRcvCancelled"
|
is SndFileRcvCancelled -> "sndFileRcvCancelled"
|
||||||
is SndFileStart -> "sndFileStart"
|
is SndFileCancelled -> "sndFileCancelled"
|
||||||
is SndFileProgressXFTP -> "sndFileProgressXFTP"
|
is SndFileProgressXFTP -> "sndFileProgressXFTP"
|
||||||
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
|
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
|
||||||
|
is SndStandaloneFileComplete -> "sndStandaloneFileComplete"
|
||||||
|
is SndFileCancelledXFTP -> "sndFileCancelledXFTP"
|
||||||
is SndFileError -> "sndFileError"
|
is SndFileError -> "sndFileError"
|
||||||
is CallInvitations -> "callInvitations"
|
is CallInvitations -> "callInvitations"
|
||||||
is CallInvitation -> "callInvitation"
|
is CallInvitation -> "callInvitation"
|
||||||
@@ -4151,11 +4297,14 @@ sealed class CR {
|
|||||||
is RemoteCtrlSessionCode -> "remoteCtrlSessionCode"
|
is RemoteCtrlSessionCode -> "remoteCtrlSessionCode"
|
||||||
is RemoteCtrlConnected -> "remoteCtrlConnected"
|
is RemoteCtrlConnected -> "remoteCtrlConnected"
|
||||||
is RemoteCtrlStopped -> "remoteCtrlStopped"
|
is RemoteCtrlStopped -> "remoteCtrlStopped"
|
||||||
|
is ContactPQAllowed -> "contactPQAllowed"
|
||||||
|
is ContactPQEnabled -> "contactPQEnabled"
|
||||||
is VersionInfo -> "versionInfo"
|
is VersionInfo -> "versionInfo"
|
||||||
is CmdOk -> "cmdOk"
|
is CmdOk -> "cmdOk"
|
||||||
is ChatCmdError -> "chatCmdError"
|
is ChatCmdError -> "chatCmdError"
|
||||||
is ChatRespError -> "chatError"
|
is ChatRespError -> "chatError"
|
||||||
is ArchiveImported -> "archiveImported"
|
is ArchiveImported -> "archiveImported"
|
||||||
|
is AppSettingsR -> "appSettings"
|
||||||
is Response -> "* $type"
|
is Response -> "* $type"
|
||||||
is Invalid -> "* invalid json"
|
is Invalid -> "* invalid json"
|
||||||
}
|
}
|
||||||
@@ -4168,7 +4317,7 @@ sealed class CR {
|
|||||||
is ChatStopped -> noDetails()
|
is ChatStopped -> noDetails()
|
||||||
is ApiChats -> withUser(user, json.encodeToString(chats))
|
is ApiChats -> withUser(user, json.encodeToString(chats))
|
||||||
is ApiChat -> withUser(user, json.encodeToString(chat))
|
is ApiChat -> withUser(user, json.encodeToString(chat))
|
||||||
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(AChatItem)}\n${json.encodeToString(chatItemInfo)}")
|
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
|
||||||
is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}")
|
is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}")
|
||||||
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
|
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
|
||||||
is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL))
|
is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL))
|
||||||
@@ -4265,20 +4414,28 @@ sealed class CR {
|
|||||||
is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
|
is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
|
||||||
is NewMemberContactReceivedInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
|
is NewMemberContactReceivedInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
|
||||||
is RcvFileAcceptedSndCancelled -> withUser(user, noDetails())
|
is RcvFileAcceptedSndCancelled -> withUser(user, noDetails())
|
||||||
|
is StandaloneFileInfo -> json.encodeToString(fileMeta)
|
||||||
|
is RcvStandaloneFileCreated -> noDetails()
|
||||||
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
|
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
|
||||||
is RcvFileStart -> withUser(user, json.encodeToString(chatItem))
|
is RcvFileStart -> withUser(user, json.encodeToString(chatItem))
|
||||||
is RcvFileComplete -> withUser(user, json.encodeToString(chatItem))
|
is RcvFileComplete -> withUser(user, json.encodeToString(chatItem))
|
||||||
is RcvFileCancelled -> withUser(user, json.encodeToString(chatItem))
|
is RcvFileCancelled -> withUser(user, json.encodeToString(chatItem_))
|
||||||
is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem))
|
is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem))
|
||||||
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
|
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
|
||||||
is RcvFileError -> withUser(user, json.encodeToString(chatItem))
|
is RcvStandaloneFileComplete -> withUser(user, targetPath)
|
||||||
is SndFileCancelled -> json.encodeToString(chatItem)
|
is RcvFileError -> withUser(user, json.encodeToString(chatItem_))
|
||||||
|
is SndFileCancelled -> json.encodeToString(chatItem_)
|
||||||
|
is SndStandaloneFileCreated -> noDetails()
|
||||||
|
is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||||
is SndFileComplete -> withUser(user, json.encodeToString(chatItem))
|
is SndFileComplete -> withUser(user, json.encodeToString(chatItem))
|
||||||
is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem))
|
is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem_))
|
||||||
is SndFileStart -> withUser(user, json.encodeToString(chatItem))
|
is SndFileStart -> withUser(user, json.encodeToString(chatItem))
|
||||||
is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize")
|
is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nsentSize: $sentSize\ntotalSize: $totalSize")
|
||||||
|
is SndFileRedirectStartXFTP -> withUser(user, json.encodeToString(redirectMeta))
|
||||||
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
|
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||||
is SndFileError -> withUser(user, json.encodeToString(chatItem))
|
is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString())
|
||||||
|
is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_))
|
||||||
|
is SndFileError -> withUser(user, json.encodeToString(chatItem_))
|
||||||
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
|
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
|
||||||
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
|
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
|
||||||
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
|
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
|
||||||
@@ -4315,6 +4472,8 @@ sealed class CR {
|
|||||||
"\nsessionCode: $sessionCode"
|
"\nsessionCode: $sessionCode"
|
||||||
is RemoteCtrlConnected -> json.encodeToString(remoteCtrl)
|
is RemoteCtrlConnected -> json.encodeToString(remoteCtrl)
|
||||||
is RemoteCtrlStopped -> noDetails()
|
is RemoteCtrlStopped -> noDetails()
|
||||||
|
is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption")
|
||||||
|
is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled")
|
||||||
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
|
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
|
||||||
"chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" +
|
"chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" +
|
||||||
"agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}"
|
"agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}"
|
||||||
@@ -4322,6 +4481,7 @@ sealed class CR {
|
|||||||
is ChatCmdError -> withUser(user_, chatError.string)
|
is ChatCmdError -> withUser(user_, chatError.string)
|
||||||
is ChatRespError -> withUser(user_, chatError.string)
|
is ChatRespError -> withUser(user_, chatError.string)
|
||||||
is ArchiveImported -> "${archiveErrors.map { it.string } }"
|
is ArchiveImported -> "${archiveErrors.map { it.string } }"
|
||||||
|
is AppSettingsR -> json.encodeToString(appSettings)
|
||||||
is Response -> json
|
is Response -> json
|
||||||
is Invalid -> str
|
is Invalid -> str
|
||||||
}
|
}
|
||||||
@@ -4735,6 +4895,7 @@ sealed class StoreError {
|
|||||||
is FileIdNotFoundBySharedMsgId -> "fileIdNotFoundBySharedMsgId"
|
is FileIdNotFoundBySharedMsgId -> "fileIdNotFoundBySharedMsgId"
|
||||||
is SndFileNotFoundXFTP -> "sndFileNotFoundXFTP"
|
is SndFileNotFoundXFTP -> "sndFileNotFoundXFTP"
|
||||||
is RcvFileNotFoundXFTP -> "rcvFileNotFoundXFTP"
|
is RcvFileNotFoundXFTP -> "rcvFileNotFoundXFTP"
|
||||||
|
is ExtraFileDescrNotFoundXFTP -> "extraFileDescrNotFoundXFTP"
|
||||||
is ConnectionNotFound -> "connectionNotFound"
|
is ConnectionNotFound -> "connectionNotFound"
|
||||||
is ConnectionNotFoundById -> "connectionNotFoundById"
|
is ConnectionNotFoundById -> "connectionNotFoundById"
|
||||||
is ConnectionNotFoundByMemberId -> "connectionNotFoundByMemberId"
|
is ConnectionNotFoundByMemberId -> "connectionNotFoundByMemberId"
|
||||||
@@ -4793,6 +4954,7 @@ sealed class StoreError {
|
|||||||
@Serializable @SerialName("fileIdNotFoundBySharedMsgId") class FileIdNotFoundBySharedMsgId(val sharedMsgId: String): StoreError()
|
@Serializable @SerialName("fileIdNotFoundBySharedMsgId") class FileIdNotFoundBySharedMsgId(val sharedMsgId: String): StoreError()
|
||||||
@Serializable @SerialName("sndFileNotFoundXFTP") class SndFileNotFoundXFTP(val agentSndFileId: String): StoreError()
|
@Serializable @SerialName("sndFileNotFoundXFTP") class SndFileNotFoundXFTP(val agentSndFileId: String): StoreError()
|
||||||
@Serializable @SerialName("rcvFileNotFoundXFTP") class RcvFileNotFoundXFTP(val agentRcvFileId: String): StoreError()
|
@Serializable @SerialName("rcvFileNotFoundXFTP") class RcvFileNotFoundXFTP(val agentRcvFileId: String): StoreError()
|
||||||
|
@Serializable @SerialName("extraFileDescrNotFoundXFTP") class ExtraFileDescrNotFoundXFTP(val fileId: Long): StoreError()
|
||||||
@Serializable @SerialName("connectionNotFound") class ConnectionNotFound(val agentConnId: String): StoreError()
|
@Serializable @SerialName("connectionNotFound") class ConnectionNotFound(val agentConnId: String): StoreError()
|
||||||
@Serializable @SerialName("connectionNotFoundById") class ConnectionNotFoundById(val connId: Long): StoreError()
|
@Serializable @SerialName("connectionNotFoundById") class ConnectionNotFoundById(val connId: Long): StoreError()
|
||||||
@Serializable @SerialName("connectionNotFoundByMemberId") class ConnectionNotFoundByMemberId(val groupMemberId: Long): StoreError()
|
@Serializable @SerialName("connectionNotFoundByMemberId") class ConnectionNotFoundByMemberId(val groupMemberId: Long): StoreError()
|
||||||
@@ -5138,3 +5300,205 @@ enum class NotificationsMode() {
|
|||||||
val default: NotificationsMode = SERVICE
|
val default: NotificationsMode = SERVICE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AppSettings(
|
||||||
|
var networkConfig: NetCfg? = null,
|
||||||
|
var privacyEncryptLocalFiles: Boolean? = null,
|
||||||
|
var privacyAcceptImages: Boolean? = null,
|
||||||
|
var privacyLinkPreviews: Boolean? = null,
|
||||||
|
var privacyShowChatPreviews: Boolean? = null,
|
||||||
|
var privacySaveLastDraft: Boolean? = null,
|
||||||
|
var privacyProtectScreen: Boolean? = null,
|
||||||
|
var notificationMode: AppSettingsNotificationMode? = null,
|
||||||
|
var notificationPreviewMode: AppSettingsNotificationPreviewMode? = null,
|
||||||
|
var webrtcPolicyRelay: Boolean? = null,
|
||||||
|
var webrtcICEServers: List<String>? = null,
|
||||||
|
var confirmRemoteSessions: Boolean? = null,
|
||||||
|
var connectRemoteViaMulticast: Boolean? = null,
|
||||||
|
var connectRemoteViaMulticastAuto: Boolean? = null,
|
||||||
|
var developerTools: Boolean? = null,
|
||||||
|
var confirmDBUpgrades: Boolean? = null,
|
||||||
|
var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null,
|
||||||
|
var iosCallKitEnabled: Boolean? = null,
|
||||||
|
var iosCallKitCallsInRecents: Boolean? = null,
|
||||||
|
) {
|
||||||
|
fun prepareForExport(): AppSettings {
|
||||||
|
val empty = AppSettings()
|
||||||
|
val def = defaults
|
||||||
|
if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig }
|
||||||
|
if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||||
|
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
|
||||||
|
if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews }
|
||||||
|
if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||||
|
if (privacySaveLastDraft != def.privacySaveLastDraft) { empty.privacySaveLastDraft = privacySaveLastDraft }
|
||||||
|
if (privacyProtectScreen != def.privacyProtectScreen) { empty.privacyProtectScreen = privacyProtectScreen }
|
||||||
|
if (notificationMode != def.notificationMode) { empty.notificationMode = notificationMode }
|
||||||
|
if (notificationPreviewMode != def.notificationPreviewMode) { empty.notificationPreviewMode = notificationPreviewMode }
|
||||||
|
if (webrtcPolicyRelay != def.webrtcPolicyRelay) { empty.webrtcPolicyRelay = webrtcPolicyRelay }
|
||||||
|
if (webrtcICEServers != def.webrtcICEServers) { empty.webrtcICEServers = webrtcICEServers }
|
||||||
|
if (confirmRemoteSessions != def.confirmRemoteSessions) { empty.confirmRemoteSessions = confirmRemoteSessions }
|
||||||
|
if (connectRemoteViaMulticast != def.connectRemoteViaMulticast) { empty.connectRemoteViaMulticast = connectRemoteViaMulticast }
|
||||||
|
if (connectRemoteViaMulticastAuto != def.connectRemoteViaMulticastAuto) { empty.connectRemoteViaMulticastAuto = connectRemoteViaMulticastAuto }
|
||||||
|
if (developerTools != def.developerTools) { empty.developerTools = developerTools }
|
||||||
|
if (confirmDBUpgrades != def.confirmDBUpgrades) { empty.confirmDBUpgrades = confirmDBUpgrades }
|
||||||
|
if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen }
|
||||||
|
if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled }
|
||||||
|
if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importIntoApp() {
|
||||||
|
val def = appPreferences
|
||||||
|
var net = networkConfig?.copy()
|
||||||
|
if (net != null) {
|
||||||
|
// migrating from iOS BUT shouldn't be here ever because it should be changed on migration stage
|
||||||
|
if (net.hostMode == HostMode.Onion) {
|
||||||
|
net = net.copy(hostMode = HostMode.Public, requiredHostMode = true)
|
||||||
|
}
|
||||||
|
setNetCfg(net)
|
||||||
|
}
|
||||||
|
privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) }
|
||||||
|
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
|
||||||
|
privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) }
|
||||||
|
privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) }
|
||||||
|
privacySaveLastDraft?.let { def.privacySaveLastDraft.set(it) }
|
||||||
|
privacyProtectScreen?.let { def.privacyProtectScreen.set(it) }
|
||||||
|
notificationMode?.let { def.notificationsMode.set(it.toNotificationsMode()) }
|
||||||
|
notificationPreviewMode?.let { def.notificationPreviewMode.set(it.toNotificationPreviewMode().name) }
|
||||||
|
webrtcPolicyRelay?.let { def.webrtcPolicyRelay.set(it) }
|
||||||
|
webrtcICEServers?.let { def.webrtcIceServers.set(it.joinToString(separator = "\n")) }
|
||||||
|
confirmRemoteSessions?.let { def.confirmRemoteSessions.set(it) }
|
||||||
|
connectRemoteViaMulticast?.let { def.connectRemoteViaMulticast.set(it) }
|
||||||
|
connectRemoteViaMulticastAuto?.let { def.connectRemoteViaMulticastAuto.set(it) }
|
||||||
|
developerTools?.let { def.developerTools.set(it) }
|
||||||
|
confirmDBUpgrades?.let { def.confirmDBUpgrades.set(it) }
|
||||||
|
androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) }
|
||||||
|
iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) }
|
||||||
|
iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val defaults: AppSettings
|
||||||
|
get() = AppSettings(
|
||||||
|
networkConfig = NetCfg.defaults,
|
||||||
|
privacyEncryptLocalFiles = true,
|
||||||
|
privacyAcceptImages = true,
|
||||||
|
privacyLinkPreviews = true,
|
||||||
|
privacyShowChatPreviews = true,
|
||||||
|
privacySaveLastDraft = true,
|
||||||
|
privacyProtectScreen = false,
|
||||||
|
notificationMode = AppSettingsNotificationMode.INSTANT,
|
||||||
|
notificationPreviewMode = AppSettingsNotificationPreviewMode.MESSAGE,
|
||||||
|
webrtcPolicyRelay = true,
|
||||||
|
webrtcICEServers = emptyList(),
|
||||||
|
confirmRemoteSessions = false,
|
||||||
|
connectRemoteViaMulticast = true,
|
||||||
|
connectRemoteViaMulticastAuto = true,
|
||||||
|
developerTools = false,
|
||||||
|
confirmDBUpgrades = false,
|
||||||
|
androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW,
|
||||||
|
iosCallKitEnabled = true,
|
||||||
|
iosCallKitCallsInRecents = false
|
||||||
|
)
|
||||||
|
|
||||||
|
val current: AppSettings
|
||||||
|
get() {
|
||||||
|
val def = appPreferences
|
||||||
|
return defaults.copy(
|
||||||
|
networkConfig = getNetCfg(),
|
||||||
|
privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(),
|
||||||
|
privacyAcceptImages = def.privacyAcceptImages.get(),
|
||||||
|
privacyLinkPreviews = def.privacyLinkPreviews.get(),
|
||||||
|
privacyShowChatPreviews = def.privacyShowChatPreviews.get(),
|
||||||
|
privacySaveLastDraft = def.privacySaveLastDraft.get(),
|
||||||
|
privacyProtectScreen = def.privacyProtectScreen.get(),
|
||||||
|
notificationMode = AppSettingsNotificationMode.from(def.notificationsMode.get()),
|
||||||
|
notificationPreviewMode = AppSettingsNotificationPreviewMode.from(NotificationPreviewMode.valueOf(def.notificationPreviewMode.get()!!)),
|
||||||
|
webrtcPolicyRelay = def.webrtcPolicyRelay.get(),
|
||||||
|
webrtcICEServers = def.webrtcIceServers.get()?.lines(),
|
||||||
|
confirmRemoteSessions = def.confirmRemoteSessions.get(),
|
||||||
|
connectRemoteViaMulticast = def.connectRemoteViaMulticast.get(),
|
||||||
|
connectRemoteViaMulticastAuto = def.connectRemoteViaMulticastAuto.get(),
|
||||||
|
developerTools = def.developerTools.get(),
|
||||||
|
confirmDBUpgrades = def.confirmDBUpgrades.get(),
|
||||||
|
androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()),
|
||||||
|
iosCallKitEnabled = def.iosCallKitEnabled.get(),
|
||||||
|
iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class AppSettingsNotificationMode {
|
||||||
|
@SerialName("off") OFF,
|
||||||
|
@SerialName("periodic") PERIODIC,
|
||||||
|
@SerialName("instant") INSTANT;
|
||||||
|
|
||||||
|
fun toNotificationsMode(): NotificationsMode =
|
||||||
|
when (this) {
|
||||||
|
INSTANT -> NotificationsMode.SERVICE
|
||||||
|
PERIODIC -> NotificationsMode.PERIODIC
|
||||||
|
OFF -> NotificationsMode.OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(mode: NotificationsMode): AppSettingsNotificationMode =
|
||||||
|
when (mode) {
|
||||||
|
NotificationsMode.SERVICE -> INSTANT
|
||||||
|
NotificationsMode.PERIODIC -> PERIODIC
|
||||||
|
NotificationsMode.OFF -> OFF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class AppSettingsNotificationPreviewMode {
|
||||||
|
@SerialName("message") MESSAGE,
|
||||||
|
@SerialName("contact") CONTACT,
|
||||||
|
@SerialName("hidden") HIDDEN;
|
||||||
|
|
||||||
|
fun toNotificationPreviewMode(): NotificationPreviewMode =
|
||||||
|
when (this) {
|
||||||
|
MESSAGE -> NotificationPreviewMode.MESSAGE
|
||||||
|
CONTACT -> NotificationPreviewMode.CONTACT
|
||||||
|
HIDDEN -> NotificationPreviewMode.HIDDEN
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val default: AppSettingsNotificationPreviewMode = MESSAGE
|
||||||
|
|
||||||
|
fun from(mode: NotificationPreviewMode): AppSettingsNotificationPreviewMode =
|
||||||
|
when (mode) {
|
||||||
|
NotificationPreviewMode.MESSAGE -> MESSAGE
|
||||||
|
NotificationPreviewMode.CONTACT -> CONTACT
|
||||||
|
NotificationPreviewMode.HIDDEN -> HIDDEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class AppSettingsLockScreenCalls {
|
||||||
|
@SerialName("disable") DISABLE,
|
||||||
|
@SerialName("show") SHOW,
|
||||||
|
@SerialName("accept") ACCEPT;
|
||||||
|
|
||||||
|
fun toCallOnLockScreen(): CallOnLockScreen =
|
||||||
|
when (this) {
|
||||||
|
DISABLE -> CallOnLockScreen.DISABLE
|
||||||
|
SHOW -> CallOnLockScreen.SHOW
|
||||||
|
ACCEPT -> CallOnLockScreen.ACCEPT
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val default = SHOW
|
||||||
|
|
||||||
|
fun from(mode: CallOnLockScreen): AppSettingsLockScreenCalls =
|
||||||
|
when (mode) {
|
||||||
|
CallOnLockScreen.DISABLE -> DISABLE
|
||||||
|
CallOnLockScreen.SHOW -> SHOW
|
||||||
|
CallOnLockScreen.ACCEPT -> ACCEPT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import chat.simplex.common.model.ChatModel.controller
|
|||||||
import chat.simplex.common.model.ChatModel.currentUser
|
import chat.simplex.common.model.ChatModel.currentUser
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword
|
import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword
|
||||||
|
import chat.simplex.common.views.helpers.DatabaseUtils.randomDatabasePassword
|
||||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import java.io.File
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
// ghc's rts
|
// ghc's rts
|
||||||
@@ -92,6 +94,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
|||||||
controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
|
controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
|
||||||
}
|
}
|
||||||
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
|
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
|
||||||
|
controller.apiSetPQEncryption(controller.appPrefs.pqExperimentalEnabled.get())
|
||||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||||
val user = chatController.apiGetActiveUser(null)
|
val user = chatController.apiGetActiveUser(null)
|
||||||
@@ -136,6 +139,37 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun chatInitTemporaryDatabase(dbPath: String, key: String? = null, confirmation: MigrationConfirmation = MigrationConfirmation.Error): Pair<DBMigrationResult, ChatCtrl?> {
|
||||||
|
val dbKey = key ?: randomDatabasePassword()
|
||||||
|
Log.d(TAG, "chatInitTemporaryDatabase path: $dbPath")
|
||||||
|
val migrated = chatMigrateInit(dbPath, dbKey, confirmation.value)
|
||||||
|
val res = runCatching {
|
||||||
|
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
||||||
|
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
||||||
|
|
||||||
|
return res to migrated[1] as ChatCtrl
|
||||||
|
}
|
||||||
|
|
||||||
|
fun chatInitControllerRemovingDatabases() {
|
||||||
|
val dbPath = dbAbsolutePrefixPath
|
||||||
|
// Remove previous databases, otherwise, can be .errorNotADatabase with null controller
|
||||||
|
File(dbPath + "_chat.db").delete()
|
||||||
|
File(dbPath + "_agent.db").delete()
|
||||||
|
|
||||||
|
val dbKey = randomDatabasePassword()
|
||||||
|
Log.d(TAG, "chatInitControllerRemovingDatabases path: $dbPath")
|
||||||
|
val migrated = chatMigrateInit(dbPath, dbKey, MigrationConfirmation.Error.value)
|
||||||
|
val res = runCatching {
|
||||||
|
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
||||||
|
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
||||||
|
|
||||||
|
val ctrl = migrated[1] as Long
|
||||||
|
chatController.ctrl = ctrl
|
||||||
|
// We need only controller, not databases
|
||||||
|
File(dbPath + "_chat.db").delete()
|
||||||
|
File(dbPath + "_agent.db").delete()
|
||||||
|
}
|
||||||
|
|
||||||
fun showStartChatAfterRestartAlert(): CompletableDeferred<Boolean> {
|
fun showStartChatAfterRestartAlert(): CompletableDeferred<Boolean> {
|
||||||
val deferred = CompletableDeferred<Boolean>()
|
val deferred = CompletableDeferred<Boolean>()
|
||||||
AlertManager.shared.showAlertDialog(
|
AlertManager.shared.showAlertDialog(
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ fun copyBytesToFile(bytes: ByteArrayInputStream, to: URI, finally: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMigrationTempFilesDirectory(): File = File(dataDir, "migration_temp_files")
|
||||||
|
|
||||||
fun getAppFilePath(fileName: String): String {
|
fun getAppFilePath(fileName: String): String {
|
||||||
val rh = chatModel.currentRemoteHost.value
|
val rh = chatModel.currentRemoteHost.value
|
||||||
val s = File.separator
|
val s = File.separator
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package chat.simplex.common.platform
|
package chat.simplex.common.platform
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import chat.simplex.common.model.ChatId
|
import chat.simplex.common.model.ChatId
|
||||||
import chat.simplex.common.model.NotificationsMode
|
import chat.simplex.common.model.NotificationsMode
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ interface PlatformInterface {
|
|||||||
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
|
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
|
||||||
fun androidPictureInPictureAllowed(): Boolean = true
|
fun androidPictureInPictureAllowed(): Boolean = true
|
||||||
fun androidCallEnded() {}
|
fun androidCallEnded() {}
|
||||||
|
@Composable fun androidLockPortraitOrientation() {}
|
||||||
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ fun ChatInfoView(
|
|||||||
val currentUser = remember { chatModel.currentUser }.value
|
val currentUser = remember { chatModel.currentUser }.value
|
||||||
val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) }
|
val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) }
|
||||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||||
|
val pqExperimentalEnabled = chatModel.controller.appPrefs.pqExperimentalEnabled.get()
|
||||||
if (chat != null && currentUser != null) {
|
if (chat != null && currentUser != null) {
|
||||||
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) {
|
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) {
|
||||||
mutableStateOf(chatModel.contactNetworkStatus(contact))
|
mutableStateOf(chatModel.contactNetworkStatus(contact))
|
||||||
@@ -80,6 +81,7 @@ fun ChatInfoView(
|
|||||||
localAlias,
|
localAlias,
|
||||||
connectionCode,
|
connectionCode,
|
||||||
developerTools,
|
developerTools,
|
||||||
|
pqExperimentalEnabled,
|
||||||
onLocalAliasChanged = {
|
onLocalAliasChanged = {
|
||||||
setContactAlias(chat, it, chatModel)
|
setContactAlias(chat, it, chatModel)
|
||||||
},
|
},
|
||||||
@@ -138,6 +140,17 @@ fun ChatInfoView(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
allowContactPQ = {
|
||||||
|
showAllowContactPQAlert(allowContactPQ = {
|
||||||
|
withBGApi {
|
||||||
|
val ct = chatModel.controller.apiSetContactPQ(chatRh, contact.contactId, true)
|
||||||
|
if (ct != null) {
|
||||||
|
chatModel.updateContact(chatRh, contact)
|
||||||
|
}
|
||||||
|
close.invoke()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
verifyClicked = {
|
verifyClicked = {
|
||||||
ModalManager.end.showModalCloseable { close ->
|
ModalManager.end.showModalCloseable { close ->
|
||||||
remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
|
remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
|
||||||
@@ -288,6 +301,7 @@ fun ChatInfoLayout(
|
|||||||
localAlias: String,
|
localAlias: String,
|
||||||
connectionCode: String?,
|
connectionCode: String?,
|
||||||
developerTools: Boolean,
|
developerTools: Boolean,
|
||||||
|
pqExperimentalEnabled: Boolean,
|
||||||
onLocalAliasChanged: (String) -> Unit,
|
onLocalAliasChanged: (String) -> Unit,
|
||||||
openPreferences: () -> Unit,
|
openPreferences: () -> Unit,
|
||||||
deleteContact: () -> Unit,
|
deleteContact: () -> Unit,
|
||||||
@@ -296,6 +310,7 @@ fun ChatInfoLayout(
|
|||||||
abortSwitchContactAddress: () -> Unit,
|
abortSwitchContactAddress: () -> Unit,
|
||||||
syncContactConnection: () -> Unit,
|
syncContactConnection: () -> Unit,
|
||||||
syncContactConnectionForce: () -> Unit,
|
syncContactConnectionForce: () -> Unit,
|
||||||
|
allowContactPQ: () -> Unit,
|
||||||
verifyClicked: () -> Unit,
|
verifyClicked: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val cStats = connStats.value
|
val cStats = connStats.value
|
||||||
@@ -345,6 +360,18 @@ fun ChatInfoLayout(
|
|||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val conn = contact.activeConn
|
||||||
|
if (pqExperimentalEnabled && conn != null) {
|
||||||
|
SectionView("Quantum resistant E2E encryption") {
|
||||||
|
InfoRow("E2E encryption", if (conn.connPQEnabled) "Quantum resistant" else "Standard")
|
||||||
|
if (!conn.pqEncryption) {
|
||||||
|
AllowContactPQButton(allowContactPQ)
|
||||||
|
SectionTextFooter("After allowing quantum resistant e2e encryption, it will be enabled after several messages if your contact also allows it.")
|
||||||
|
}
|
||||||
|
SectionDividerSpaced()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (contact.contactLink != null) {
|
if (contact.contactLink != null) {
|
||||||
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
|
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
|
||||||
SimpleXLinkQRCode(contact.contactLink)
|
SimpleXLinkQRCode(contact.contactLink)
|
||||||
@@ -601,6 +628,17 @@ fun SynchronizeConnectionButtonForce(syncConnectionForce: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AllowContactPQButton(allowContactPQ: () -> Unit) {
|
||||||
|
SettingsActionItem(
|
||||||
|
painterResource(MR.images.ic_warning),
|
||||||
|
"Allow PQ encryption",
|
||||||
|
click = allowContactPQ,
|
||||||
|
textColor = WarningOrange,
|
||||||
|
iconColor = WarningOrange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
|
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
|
||||||
SettingsActionItem(
|
SettingsActionItem(
|
||||||
@@ -704,6 +742,16 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showAllowContactPQAlert(allowContactPQ: () -> Unit) {
|
||||||
|
AlertManager.shared.showAlertDialog(
|
||||||
|
title = "Allow quantum resistant encryption?",
|
||||||
|
text = "This is an experimental feature, it is not recommended to enable it for important chats.",
|
||||||
|
confirmText = "Allow",
|
||||||
|
onConfirm = allowContactPQ,
|
||||||
|
destructive = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewChatInfoLayout() {
|
fun PreviewChatInfoLayout() {
|
||||||
@@ -721,6 +769,7 @@ fun PreviewChatInfoLayout() {
|
|||||||
localAlias = "",
|
localAlias = "",
|
||||||
connectionCode = "123",
|
connectionCode = "123",
|
||||||
developerTools = false,
|
developerTools = false,
|
||||||
|
pqExperimentalEnabled = false,
|
||||||
connStats = remember { mutableStateOf(null) },
|
connStats = remember { mutableStateOf(null) },
|
||||||
contactNetworkStatus = NetworkStatus.Connected(),
|
contactNetworkStatus = NetworkStatus.Connected(),
|
||||||
onLocalAliasChanged = {},
|
onLocalAliasChanged = {},
|
||||||
@@ -732,6 +781,7 @@ fun PreviewChatInfoLayout() {
|
|||||||
abortSwitchContactAddress = {},
|
abortSwitchContactAddress = {},
|
||||||
syncContactConnection = {},
|
syncContactConnection = {},
|
||||||
syncContactConnectionForce = {},
|
syncContactConnectionForce = {},
|
||||||
|
allowContactPQ = {},
|
||||||
verifyClicked = {},
|
verifyClicked = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -379,6 +379,30 @@ fun ChatItemView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun E2EEInfoNoPQText() {
|
||||||
|
Text(
|
||||||
|
buildAnnotatedString {
|
||||||
|
withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_no_pq)) }
|
||||||
|
},
|
||||||
|
Modifier.padding(horizontal = 6.dp, vertical = 6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) {
|
||||||
|
if (e2EEInfo.pqEnabled) {
|
||||||
|
Text(
|
||||||
|
buildAnnotatedString {
|
||||||
|
withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_pq)) }
|
||||||
|
},
|
||||||
|
Modifier.padding(horizontal = 6.dp, vertical = 6.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
E2EEInfoNoPQText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when (val c = cItem.content) {
|
when (val c = cItem.content) {
|
||||||
is CIContent.SndMsgContent -> ContentItem()
|
is CIContent.SndMsgContent -> ContentItem()
|
||||||
is CIContent.RcvMsgContent -> ContentItem()
|
is CIContent.RcvMsgContent -> ContentItem()
|
||||||
@@ -452,6 +476,10 @@ fun ChatItemView(
|
|||||||
is CIContent.SndModerated -> DeletedItem()
|
is CIContent.SndModerated -> DeletedItem()
|
||||||
is CIContent.RcvModerated -> DeletedItem()
|
is CIContent.RcvModerated -> DeletedItem()
|
||||||
is CIContent.RcvBlocked -> DeletedItem()
|
is CIContent.RcvBlocked -> DeletedItem()
|
||||||
|
is CIContent.SndDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo)
|
||||||
|
is CIContent.RcvDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo)
|
||||||
|
is CIContent.SndGroupE2EEInfo -> E2EEInfoNoPQText()
|
||||||
|
is CIContent.RcvGroupE2EEInfo -> E2EEInfoNoPQText()
|
||||||
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -535,7 +535,9 @@ private fun filteredChats(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun filtered(chat: Chat): Boolean =
|
private fun filtered(chat: Chat): Boolean =
|
||||||
(chat.chatInfo.chatSettings?.favorite ?: false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
|
(chat.chatInfo.chatSettings?.favorite ?: false) ||
|
||||||
|
chat.chatStats.unreadChat ||
|
||||||
|
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||||
|
|
||||||
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
||||||
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package chat.simplex.common.views.database
|
package chat.simplex.common.views.database
|
||||||
|
|
||||||
import SectionBottomSpacer
|
import SectionBottomSpacer
|
||||||
import SectionItemView
|
|
||||||
import SectionItemViewSpaceBetween
|
import SectionItemViewSpaceBetween
|
||||||
import SectionTextFooter
|
import SectionSpacer
|
||||||
import SectionView
|
import SectionView
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
@@ -24,20 +23,22 @@ import androidx.compose.ui.text.*
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.*
|
import androidx.compose.ui.text.input.*
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.platform.appPreferences
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||||
import chat.simplex.common.platform.appPlatform
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlin.math.log2
|
import kotlin.math.log2
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DatabaseEncryptionView(m: ChatModel) {
|
fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
||||||
val progressIndicator = remember { mutableStateOf(false) }
|
val progressIndicator = remember { mutableStateOf(false) }
|
||||||
val prefs = m.controller.appPrefs
|
val prefs = m.controller.appPrefs
|
||||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||||
@@ -61,9 +62,10 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
|||||||
storedKey,
|
storedKey,
|
||||||
initialRandomDBPassphrase,
|
initialRandomDBPassphrase,
|
||||||
progressIndicator,
|
progressIndicator,
|
||||||
|
migration,
|
||||||
onConfirmEncrypt = {
|
onConfirmEncrypt = {
|
||||||
withLongRunningApi {
|
withLongRunningApi {
|
||||||
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
|
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator, migration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -95,24 +97,34 @@ fun DatabaseEncryptionLayout(
|
|||||||
storedKey: MutableState<Boolean>,
|
storedKey: MutableState<Boolean>,
|
||||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||||
progressIndicator: MutableState<Boolean>,
|
progressIndicator: MutableState<Boolean>,
|
||||||
|
migration: Boolean,
|
||||||
onConfirmEncrypt: () -> Unit,
|
onConfirmEncrypt: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
if (!migration) Modifier.fillMaxWidth().verticalScroll(rememberScrollState()) else Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
AppBarTitle(stringResource(MR.strings.database_passphrase))
|
if (!migration) {
|
||||||
SectionView(null) {
|
AppBarTitle(stringResource(MR.strings.database_passphrase))
|
||||||
SavePassphraseSetting(useKeychain.value, initialRandomDBPassphrase.value, storedKey.value, progressIndicator.value) { checked ->
|
} else {
|
||||||
|
ChatStoppedView()
|
||||||
|
SectionSpacer()
|
||||||
|
}
|
||||||
|
SectionView(if (migration) generalGetString(MR.strings.database_passphrase).uppercase() else null) {
|
||||||
|
SavePassphraseSetting(
|
||||||
|
useKeychain.value,
|
||||||
|
initialRandomDBPassphrase.value,
|
||||||
|
storedKey.value,
|
||||||
|
enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration
|
||||||
|
) { checked ->
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setUseKeychain(true, useKeychain, prefs)
|
setUseKeychain(true, useKeychain, prefs, migration)
|
||||||
} else if (storedKey.value) {
|
} else if (storedKey.value && !migration) {
|
||||||
|
// Don't show in migration process since it will remove the key after successful encryption
|
||||||
removePassphraseAlert {
|
removePassphraseAlert {
|
||||||
DatabaseUtils.ksDatabasePassword.remove()
|
removePassphraseFromKeyChain(useKeychain, prefs, storedKey, false)
|
||||||
setUseKeychain(false, useKeychain, prefs)
|
|
||||||
storedKey.value = false
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setUseKeychain(false, useKeychain, prefs)
|
setUseKeychain(false, useKeychain, prefs, migration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,12 +181,12 @@ fun DatabaseEncryptionLayout(
|
|||||||
)
|
)
|
||||||
|
|
||||||
SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) {
|
SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) {
|
||||||
Text(generalGetString(MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
Text(generalGetString(if (migration) MR.strings.set_passphrase else MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase)
|
DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase, migration)
|
||||||
}
|
}
|
||||||
SectionBottomSpacer()
|
SectionBottomSpacer()
|
||||||
}
|
}
|
||||||
@@ -211,8 +223,9 @@ expect fun SavePassphraseSetting(
|
|||||||
useKeychain: Boolean,
|
useKeychain: Boolean,
|
||||||
initialRandomDBPassphrase: Boolean,
|
initialRandomDBPassphrase: Boolean,
|
||||||
storedKey: Boolean,
|
storedKey: Boolean,
|
||||||
progressIndicator: Boolean,
|
|
||||||
minHeight: Dp = TextFieldDefaults.MinHeight,
|
minHeight: Dp = TextFieldDefaults.MinHeight,
|
||||||
|
enabled: Boolean,
|
||||||
|
smallPadding: Boolean = true,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -222,8 +235,18 @@ expect fun DatabaseEncryptionFooter(
|
|||||||
chatDbEncrypted: Boolean?,
|
chatDbEncrypted: Boolean?,
|
||||||
storedKey: MutableState<Boolean>,
|
storedKey: MutableState<Boolean>,
|
||||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||||
|
migration: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatStoppedView() {
|
||||||
|
SettingsActionItem(
|
||||||
|
icon = painterResource(MR.images.ic_report_filled),
|
||||||
|
text = stringResource(MR.strings.chat_is_stopped),
|
||||||
|
iconColor = Color.Red,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun resetFormAfterEncryption(
|
fun resetFormAfterEncryption(
|
||||||
m: ChatModel,
|
m: ChatModel,
|
||||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||||
@@ -242,9 +265,18 @@ fun resetFormAfterEncryption(
|
|||||||
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
|
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences) {
|
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences, migration: Boolean) {
|
||||||
useKeychain.value = value
|
useKeychain.value = value
|
||||||
prefs.storeDBPassphrase.set(value)
|
// Postpone it when migrating to the end of encryption process
|
||||||
|
if (!migration) {
|
||||||
|
prefs.storeDBPassphrase.set(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removePassphraseFromKeyChain(useKeychain: MutableState<Boolean>, prefs: AppPreferences, storedKey: MutableState<Boolean>, migration: Boolean) {
|
||||||
|
DatabaseUtils.ksDatabasePassword.remove()
|
||||||
|
setUseKeychain(false, useKeychain, prefs, migration)
|
||||||
|
storedKey.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun storeSecurelySaved() = generalGetString(MR.strings.store_passphrase_securely)
|
fun storeSecurelySaved() = generalGetString(MR.strings.store_passphrase_securely)
|
||||||
@@ -267,6 +299,7 @@ fun PassphraseField(
|
|||||||
isValid: (String) -> Boolean,
|
isValid: (String) -> Boolean,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||||
dependsOn: State<Any?>? = null,
|
dependsOn: State<Any?>? = null,
|
||||||
|
requestFocus: Boolean = false,
|
||||||
) {
|
) {
|
||||||
var valid by remember { mutableStateOf(validKey(key.value)) }
|
var valid by remember { mutableStateOf(validKey(key.value)) }
|
||||||
var showKey by remember { mutableStateOf(false) }
|
var showKey by remember { mutableStateOf(false) }
|
||||||
@@ -295,6 +328,7 @@ fun PassphraseField(
|
|||||||
val color = MaterialTheme.colors.onBackground
|
val color = MaterialTheme.colors.onBackground
|
||||||
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
|
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = state.value,
|
value = state.value,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -304,7 +338,8 @@ fun PassphraseField(
|
|||||||
.defaultMinSize(
|
.defaultMinSize(
|
||||||
minWidth = TextFieldDefaults.MinWidth,
|
minWidth = TextFieldDefaults.MinWidth,
|
||||||
minHeight = TextFieldDefaults.MinHeight
|
minHeight = TextFieldDefaults.MinHeight
|
||||||
),
|
)
|
||||||
|
.focusRequester(focusRequester),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
state.value = it
|
state.value = it
|
||||||
key.value = it.text
|
key.value = it.text
|
||||||
@@ -347,6 +382,12 @@ fun PassphraseField(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (requestFocus) {
|
||||||
|
delay(200)
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
snapshotFlow { dependsOn?.value }
|
snapshotFlow { dependsOn?.value }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
@@ -363,13 +404,17 @@ suspend fun encryptDatabase(
|
|||||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||||
useKeychain: MutableState<Boolean>,
|
useKeychain: MutableState<Boolean>,
|
||||||
storedKey: MutableState<Boolean>,
|
storedKey: MutableState<Boolean>,
|
||||||
progressIndicator: MutableState<Boolean>
|
progressIndicator: MutableState<Boolean>,
|
||||||
|
migration: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val m = ChatModel
|
val m = ChatModel
|
||||||
val prefs = ChatController.appPrefs
|
val prefs = ChatController.appPrefs
|
||||||
progressIndicator.value = true
|
progressIndicator.value = true
|
||||||
return try {
|
return try {
|
||||||
prefs.encryptionStartedAt.set(Clock.System.now())
|
prefs.encryptionStartedAt.set(Clock.System.now())
|
||||||
|
if (!m.chatDbChanged.value) {
|
||||||
|
m.controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
|
||||||
|
}
|
||||||
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
||||||
prefs.encryptionStartedAt.set(null)
|
prefs.encryptionStartedAt.set(null)
|
||||||
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
||||||
@@ -393,9 +438,14 @@ suspend fun encryptDatabase(
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val new = newKey.value
|
val new = newKey.value
|
||||||
|
if (migration) {
|
||||||
|
appPreferences.storeDBPassphrase.set(useKeychain.value)
|
||||||
|
}
|
||||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||||
if (useKeychain.value) {
|
if (useKeychain.value) {
|
||||||
DatabaseUtils.ksDatabasePassword.set(new)
|
DatabaseUtils.ksDatabasePassword.set(new)
|
||||||
|
} else if (migration) {
|
||||||
|
removePassphraseFromKeyChain(useKeychain, prefs, storedKey, true)
|
||||||
}
|
}
|
||||||
operationEnded(m, progressIndicator) {
|
operationEnded(m, progressIndicator) {
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
||||||
@@ -474,6 +524,7 @@ fun PreviewDatabaseEncryptionLayout() {
|
|||||||
storedKey = remember { mutableStateOf(true) },
|
storedKey = remember { mutableStateOf(true) },
|
||||||
initialRandomDBPassphrase = remember { mutableStateOf(true) },
|
initialRandomDBPassphrase = remember { mutableStateOf(true) },
|
||||||
progressIndicator = remember { mutableStateOf(false) },
|
progressIndicator = remember { mutableStateOf(false) },
|
||||||
|
migration = false,
|
||||||
onConfirmEncrypt = {},
|
onConfirmEncrypt = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,6 +206,14 @@ private fun runChat(
|
|||||||
is DBMigrationResult.OK -> {
|
is DBMigrationResult.OK -> {
|
||||||
platform.androidChatStartedAfterBeingOff()
|
platform.androidChatStartedAfterBeingOff()
|
||||||
}
|
}
|
||||||
|
null -> {}
|
||||||
|
else -> showErrorOnMigrationIfNeeded(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showErrorOnMigrationIfNeeded(status: DBMigrationResult) =
|
||||||
|
when (status) {
|
||||||
|
is DBMigrationResult.OK -> {}
|
||||||
is DBMigrationResult.ErrorNotADatabase ->
|
is DBMigrationResult.ErrorNotADatabase ->
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.wrong_passphrase_title), generalGetString(MR.strings.enter_correct_passphrase))
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.wrong_passphrase_title), generalGetString(MR.strings.enter_correct_passphrase))
|
||||||
is DBMigrationResult.ErrorSQL ->
|
is DBMigrationResult.ErrorSQL ->
|
||||||
@@ -217,9 +225,7 @@ private fun runChat(
|
|||||||
is DBMigrationResult.InvalidConfirmation ->
|
is DBMigrationResult.InvalidConfirmation ->
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.invalid_migration_confirmation))
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.invalid_migration_confirmation))
|
||||||
is DBMigrationResult.ErrorMigration -> {}
|
is DBMigrationResult.ErrorMigration -> {}
|
||||||
null -> {}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldShowRestoreDbButton(prefs: AppPreferences): Boolean {
|
private fun shouldShowRestoreDbButton(prefs: AppPreferences): Boolean {
|
||||||
val startedAt = prefs.encryptionStartedAt.get() ?: return false
|
val startedAt = prefs.encryptionStartedAt.get() ?: return false
|
||||||
@@ -246,7 +252,7 @@ private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPref
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mtrErrorDescription(err: MTRError): String =
|
fun mtrErrorDescription(err: MTRError): String =
|
||||||
when (err) {
|
when (err) {
|
||||||
is MTRError.NoDown ->
|
is MTRError.NoDown ->
|
||||||
String.format(generalGetString(MR.strings.mtr_error_no_down_migration), err.dbMigrations.joinToString(", "))
|
String.format(generalGetString(MR.strings.mtr_error_no_down_migration), err.dbMigrations.joinToString(", "))
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ fun DatabaseLayout(
|
|||||||
if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled)
|
if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled)
|
||||||
else painterResource(MR.images.ic_lock),
|
else painterResource(MR.images.ic_lock),
|
||||||
stringResource(MR.strings.database_passphrase),
|
stringResource(MR.strings.database_passphrase),
|
||||||
click = showSettingsModal() { DatabaseEncryptionView(it) },
|
click = showSettingsModal() { DatabaseEncryptionView(it, false) },
|
||||||
iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary,
|
iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary,
|
||||||
disabled = operationsDisabled
|
disabled = operationsDisabled
|
||||||
)
|
)
|
||||||
@@ -486,6 +486,7 @@ fun deleteChatDatabaseFilesAndState() {
|
|||||||
filesDir.mkdir()
|
filesDir.mkdir()
|
||||||
remoteHostsDir.deleteRecursively()
|
remoteHostsDir.deleteRecursively()
|
||||||
tmpDir.deleteRecursively()
|
tmpDir.deleteRecursively()
|
||||||
|
getMigrationTempFilesDirectory().deleteRecursively()
|
||||||
tmpDir.mkdir()
|
tmpDir.mkdir()
|
||||||
DatabaseUtils.ksDatabasePassword.remove()
|
DatabaseUtils.ksDatabasePassword.remove()
|
||||||
controller.appPrefs.storeDBPassphrase.set(true)
|
controller.appPrefs.storeDBPassphrase.set(true)
|
||||||
@@ -509,7 +510,7 @@ private fun exportArchive(
|
|||||||
progressIndicator.value = true
|
progressIndicator.value = true
|
||||||
withLongRunningApi {
|
withLongRunningApi {
|
||||||
try {
|
try {
|
||||||
val archiveFile = exportChatArchive(m, chatArchiveName, chatArchiveTime, chatArchiveFile)
|
val archiveFile = exportChatArchive(m, null, chatArchiveName, chatArchiveTime, chatArchiveFile)
|
||||||
chatArchiveFile.value = archiveFile
|
chatArchiveFile.value = archiveFile
|
||||||
saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator))
|
saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator))
|
||||||
progressIndicator.value = false
|
progressIndicator.value = false
|
||||||
@@ -520,8 +521,9 @@ private fun exportArchive(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun exportChatArchive(
|
suspend fun exportChatArchive(
|
||||||
m: ChatModel,
|
m: ChatModel,
|
||||||
|
storagePath: File?,
|
||||||
chatArchiveName: MutableState<String?>,
|
chatArchiveName: MutableState<String?>,
|
||||||
chatArchiveTime: MutableState<Instant?>,
|
chatArchiveTime: MutableState<Instant?>,
|
||||||
chatArchiveFile: MutableState<String?>
|
chatArchiveFile: MutableState<String?>
|
||||||
@@ -529,13 +531,19 @@ private suspend fun exportChatArchive(
|
|||||||
val archiveTime = Clock.System.now()
|
val archiveTime = Clock.System.now()
|
||||||
val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant()))
|
val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant()))
|
||||||
val archiveName = "simplex-chat.$ts.zip"
|
val archiveName = "simplex-chat.$ts.zip"
|
||||||
val archivePath = "${filesDir.absolutePath}${File.separator}$archiveName"
|
val archivePath = "${(storagePath ?: filesDir).absolutePath}${File.separator}$archiveName"
|
||||||
val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString())
|
val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString())
|
||||||
|
// Settings should be saved before changing a passphrase, otherwise the database needs to be migrated first
|
||||||
|
if (!m.chatDbChanged.value) {
|
||||||
|
controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
|
||||||
|
}
|
||||||
m.controller.apiExportArchive(config)
|
m.controller.apiExportArchive(config)
|
||||||
deleteOldArchive(m)
|
if (storagePath == null) {
|
||||||
m.controller.appPrefs.chatArchiveName.set(archiveName)
|
deleteOldArchive(m)
|
||||||
|
m.controller.appPrefs.chatArchiveName.set(archiveName)
|
||||||
|
m.controller.appPrefs.chatArchiveTime.set(archiveTime)
|
||||||
|
}
|
||||||
chatArchiveName.value = archiveName
|
chatArchiveName.value = archiveName
|
||||||
m.controller.appPrefs.chatArchiveTime.set(archiveTime)
|
|
||||||
chatArchiveTime.value = archiveTime
|
chatArchiveTime.value = archiveTime
|
||||||
chatArchiveFile.value = archivePath
|
chatArchiveFile.value = archivePath
|
||||||
return archivePath
|
return archivePath
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ object DatabaseUtils {
|
|||||||
return dbKey
|
return dbKey
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun randomDatabasePassword(): String {
|
fun randomDatabasePassword(): String {
|
||||||
val s = ByteArray(32)
|
val s = ByteArray(32)
|
||||||
SecureRandom().nextBytes(s)
|
SecureRandom().nextBytes(s)
|
||||||
return s.toBase64StringForPassphrase().replace("\n", "")
|
return s.toBase64StringForPassphrase().replace("\n", "")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ fun DefaultProgressView(description: String?) {
|
|||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(bottom = DEFAULT_PADDING)
|
.padding(bottom = if (description != null) DEFAULT_PADDING else 0.dp)
|
||||||
.size(30.dp),
|
.size(30.dp),
|
||||||
color = MaterialTheme.colors.secondary,
|
color = MaterialTheme.colors.secondary,
|
||||||
strokeWidth = 2.5.dp
|
strokeWidth = 2.5.dp
|
||||||
|
|||||||
@@ -19,17 +19,18 @@ import kotlin.math.min
|
|||||||
fun ModalView(
|
fun ModalView(
|
||||||
close: () -> Unit,
|
close: () -> Unit,
|
||||||
showClose: Boolean = true,
|
showClose: Boolean = true,
|
||||||
|
enableClose: Boolean = true,
|
||||||
background: Color = MaterialTheme.colors.background,
|
background: Color = MaterialTheme.colors.background,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
endButtons: @Composable RowScope.() -> Unit = {},
|
endButtons: @Composable RowScope.() -> Unit = {},
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
if (showClose) {
|
if (showClose) {
|
||||||
BackHandler(onBack = close)
|
BackHandler(enabled = enableClose, onBack = close)
|
||||||
}
|
}
|
||||||
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
||||||
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
||||||
CloseSheetBar(close, showClose, endButtons = endButtons)
|
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons)
|
||||||
Box(modifier) { content() }
|
Box(modifier) { content() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,707 @@
|
|||||||
|
package chat.simplex.common.views.migration
|
||||||
|
|
||||||
|
import SectionBottomSpacer
|
||||||
|
import SectionSpacer
|
||||||
|
import SectionTextFooter
|
||||||
|
import SectionView
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatController.getNetCfg
|
||||||
|
import chat.simplex.common.model.ChatController.startChat
|
||||||
|
import chat.simplex.common.model.ChatController.startChatWithTemporaryDatabase
|
||||||
|
import chat.simplex.common.model.ChatCtrl
|
||||||
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.database.*
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.views.newchat.LinkTextView
|
||||||
|
import chat.simplex.common.views.newchat.SimpleXLinkQRCode
|
||||||
|
import chat.simplex.common.views.usersettings.*
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.datetime.*
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MigrationFileLinkData(
|
||||||
|
val networkConfig: NetworkConfig?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class NetworkConfig(
|
||||||
|
val socksProxy: String?,
|
||||||
|
val hostMode: HostMode?,
|
||||||
|
val requiredHostMode: Boolean?
|
||||||
|
) {
|
||||||
|
fun hasOnionConfigured(): Boolean = socksProxy != null || hostMode == HostMode.Onion
|
||||||
|
|
||||||
|
fun transformToPlatformSupported(): NetworkConfig {
|
||||||
|
return if (hostMode != null && requiredHostMode != null) {
|
||||||
|
NetworkConfig(
|
||||||
|
socksProxy = if (hostMode == HostMode.Onion) socksProxy ?: NetCfg.proxyDefaults.socksProxy else socksProxy,
|
||||||
|
hostMode = if (hostMode == HostMode.Onion) HostMode.OnionViaSocks else hostMode,
|
||||||
|
requiredHostMode = requiredHostMode
|
||||||
|
)
|
||||||
|
} else this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addToLink(link: String) = link + "&data=" + URLEncoder.encode(jsonShort.encodeToString(this), "UTF-8")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
suspend fun readFromLink(link: String): MigrationFileLinkData? =
|
||||||
|
try {
|
||||||
|
// val data = link.substringAfter("&data=").substringBefore("&")
|
||||||
|
// json.decodeFromString(URLDecoder.decode(data, "UTF-8"))
|
||||||
|
controller.standaloneFileInfo(link)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private sealed class MigrationFromState {
|
||||||
|
@Serializable object ChatStopInProgress: MigrationFromState()
|
||||||
|
@Serializable data class ChatStopFailed(val reason: String): MigrationFromState()
|
||||||
|
@Serializable object PassphraseNotSet: MigrationFromState()
|
||||||
|
@Serializable object PassphraseConfirmation: MigrationFromState()
|
||||||
|
@Serializable object UploadConfirmation: MigrationFromState()
|
||||||
|
@Serializable object Archiving: MigrationFromState()
|
||||||
|
@Serializable data class DatabaseInit(val totalBytes: Long, val archivePath: String): MigrationFromState()
|
||||||
|
@Serializable data class UploadProgress(val uploadedBytes: Long, val totalBytes: Long, val fileId: Long, val archivePath: String, val ctrl: ChatCtrl, val user: User): MigrationFromState()
|
||||||
|
@Serializable data class UploadFailed(val totalBytes: Long, val archivePath: String): MigrationFromState()
|
||||||
|
@Serializable object LinkCreation: MigrationFromState()
|
||||||
|
@Serializable data class LinkShown(val fileId: Long, val link: String, val ctrl: ChatCtrl): MigrationFromState()
|
||||||
|
@Serializable data class Finished(val chatDeletion: Boolean): MigrationFromState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var MutableState<MigrationFromState>.state: MigrationFromState
|
||||||
|
get() = value
|
||||||
|
set(v) { value = v }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MigrateFromDeviceView(close: () -> Unit) {
|
||||||
|
val migrationState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf<MigrationFromState>(MigrationFromState.ChatStopInProgress) }
|
||||||
|
// Prevent from hiding the view until migration is finished or app deleted
|
||||||
|
val backDisabled = remember {
|
||||||
|
derivedStateOf {
|
||||||
|
when (migrationState.value) {
|
||||||
|
is MigrationFromState.ChatStopInProgress,
|
||||||
|
is MigrationFromState.DatabaseInit,
|
||||||
|
is MigrationFromState.Archiving,
|
||||||
|
is MigrationFromState.LinkShown,
|
||||||
|
is MigrationFromState.Finished -> true
|
||||||
|
|
||||||
|
is MigrationFromState.ChatStopFailed,
|
||||||
|
is MigrationFromState.PassphraseNotSet,
|
||||||
|
is MigrationFromState.PassphraseConfirmation,
|
||||||
|
is MigrationFromState.UploadConfirmation,
|
||||||
|
is MigrationFromState.UploadProgress,
|
||||||
|
is MigrationFromState.UploadFailed,
|
||||||
|
is MigrationFromState.LinkCreation -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val chatReceiver = remember { mutableStateOf(null as MigrationFromChatReceiver?) }
|
||||||
|
ModalView(
|
||||||
|
enableClose = !backDisabled.value,
|
||||||
|
close = {
|
||||||
|
withBGApi {
|
||||||
|
migrationState.cleanUpOnBack(chatReceiver.value)
|
||||||
|
}
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MigrateFromDeviceLayout(
|
||||||
|
migrationState = migrationState,
|
||||||
|
chatReceiver = chatReceiver
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MigrateFromDeviceLayout(
|
||||||
|
migrationState: MutableState<MigrationFromState>,
|
||||||
|
chatReceiver: MutableState<MigrationFromChatReceiver?>
|
||||||
|
) {
|
||||||
|
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).height(IntrinsicSize.Max),
|
||||||
|
) {
|
||||||
|
AppBarTitle(stringResource(MR.strings.migrate_from_device_title))
|
||||||
|
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver)
|
||||||
|
SectionBottomSpacer()
|
||||||
|
}
|
||||||
|
platform.androidLockPortraitOrientation()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionByState(
|
||||||
|
migrationState: MutableState<MigrationFromState>,
|
||||||
|
tempDatabaseFile: File,
|
||||||
|
chatReceiver: MutableState<MigrationFromChatReceiver?>
|
||||||
|
) {
|
||||||
|
when (val s = migrationState.value) {
|
||||||
|
is MigrationFromState.ChatStopInProgress -> migrationState.ChatStopInProgressView()
|
||||||
|
is MigrationFromState.ChatStopFailed -> migrationState.ChatStopFailedView(s.reason)
|
||||||
|
is MigrationFromState.PassphraseNotSet -> migrationState.PassphraseNotSetView()
|
||||||
|
is MigrationFromState.PassphraseConfirmation -> migrationState.PassphraseConfirmationView()
|
||||||
|
is MigrationFromState.UploadConfirmation -> migrationState.UploadConfirmationView()
|
||||||
|
is MigrationFromState.Archiving -> migrationState.ArchivingView()
|
||||||
|
is MigrationFromState.DatabaseInit -> migrationState.DatabaseInitView(tempDatabaseFile, s.totalBytes, s.archivePath)
|
||||||
|
is MigrationFromState.UploadProgress -> migrationState.UploadProgressView(s.uploadedBytes, s.totalBytes, s.ctrl, s.user, tempDatabaseFile, chatReceiver, s.archivePath)
|
||||||
|
is MigrationFromState.UploadFailed -> migrationState.UploadFailedView(s.totalBytes, s.archivePath, chatReceiver.value)
|
||||||
|
is MigrationFromState.LinkCreation -> LinkCreationView()
|
||||||
|
is MigrationFromState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl)
|
||||||
|
is MigrationFromState.Finished -> migrationState.FinishedView(s.chatDeletion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.ChatStopInProgressView() {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_from_device_stopping_chat).uppercase()) {}
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
stopChat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.ChatStopFailedView(reason: String) {
|
||||||
|
SectionView(stringResource(MR.strings.error_stopping_chat).uppercase()) {
|
||||||
|
Text(reason)
|
||||||
|
SectionSpacer()
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_report_filled),
|
||||||
|
text = stringResource(MR.strings.auth_stop_chat),
|
||||||
|
textColor = MaterialTheme.colors.error,
|
||||||
|
click = ::stopChat
|
||||||
|
){}
|
||||||
|
SectionTextFooter(stringResource(MR.strings.migrate_from_device_chat_should_be_stopped))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.PassphraseNotSetView() {
|
||||||
|
DatabaseEncryptionView(chatModel, true)
|
||||||
|
KeyChangeEffect(appPreferences.initialRandomDBPassphrase.state.value) {
|
||||||
|
if (!appPreferences.initialRandomDBPassphrase.get()) {
|
||||||
|
state = MigrationFromState.UploadConfirmation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.PassphraseConfirmationView() {
|
||||||
|
val useKeychain = remember { appPreferences.storeDBPassphrase.get() }
|
||||||
|
val currentKey = rememberSaveable { mutableStateOf("") }
|
||||||
|
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
|
||||||
|
Box {
|
||||||
|
val view = LocalMultiplatformView()
|
||||||
|
Column {
|
||||||
|
ChatStoppedView()
|
||||||
|
SectionSpacer()
|
||||||
|
|
||||||
|
SectionView(stringResource(MR.strings.migrate_from_device_verify_database_passphrase).uppercase()) {
|
||||||
|
PassphraseField(currentKey, placeholder = stringResource(MR.strings.current_passphrase), Modifier.padding(horizontal = DEFAULT_PADDING), isValid = ::validKey, requestFocus = true)
|
||||||
|
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(if (useKeychain) MR.images.ic_vpn_key_filled else MR.images.ic_lock),
|
||||||
|
text = stringResource(MR.strings.migrate_from_device_verify_passphrase),
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
disabled = verifyingPassphrase.value || currentKey.value.isEmpty(),
|
||||||
|
click = {
|
||||||
|
verifyingPassphrase.value = true
|
||||||
|
hideKeyboard(view)
|
||||||
|
withBGApi {
|
||||||
|
verifyDatabasePassphrase(currentKey.value)
|
||||||
|
verifyingPassphrase.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
SectionTextFooter(stringResource(MR.strings.migrate_from_device_confirm_you_remember_passphrase))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verifyingPassphrase.value) {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.UploadConfirmationView() {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_from_device_confirm_upload).uppercase()) {
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_ios_share),
|
||||||
|
text = stringResource(MR.strings.migrate_from_device_archive_and_upload),
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
click = { state = MigrationFromState.Archiving }
|
||||||
|
){}
|
||||||
|
SectionTextFooter(stringResource(MR.strings.migrate_from_device_all_data_will_be_uploaded))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.ArchivingView() {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_from_device_archiving_database).uppercase()) {}
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
exportArchive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.DatabaseInitView(tempDatabaseFile: File, totalBytes: Long, archivePath: String) {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_from_device_database_init).uppercase()) {}
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
prepareDatabase(tempDatabaseFile, totalBytes, archivePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.UploadProgressView(
|
||||||
|
uploadedBytes: Long,
|
||||||
|
totalBytes: Long,
|
||||||
|
ctrl: ChatCtrl,
|
||||||
|
user: User,
|
||||||
|
tempDatabaseFile: File,
|
||||||
|
chatReceiver: MutableState<MigrationFromChatReceiver?>,
|
||||||
|
archivePath: String,
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_from_device_uploading_archive).uppercase()) {
|
||||||
|
val ratio = uploadedBytes.toFloat() / max(totalBytes, 1)
|
||||||
|
LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_from_device_bytes_uploaded).format(formatBytes(uploadedBytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
startUploading(totalBytes, ctrl, user, tempDatabaseFile, chatReceiver, archivePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.UploadFailedView(totalBytes: Long, archivePath: String, chatReceiver: MigrationFromChatReceiver?) {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_from_device_upload_failed).uppercase()) {
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_ios_share),
|
||||||
|
text = stringResource(MR.strings.migrate_from_device_repeat_upload),
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
click = {
|
||||||
|
state = MigrationFromState.DatabaseInit(totalBytes, archivePath)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
SectionTextFooter(stringResource(MR.strings.migrate_from_device_try_again))
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
chatReceiver?.stopAndCleanUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LinkCreationView() {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_from_device_creating_archive_link).uppercase()) {}
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) {
|
||||||
|
SectionView {
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_close),
|
||||||
|
text = stringResource(MR.strings.migrate_from_device_cancel_migration),
|
||||||
|
textColor = MaterialTheme.colors.error,
|
||||||
|
click = {
|
||||||
|
cancelMigration(fileId, ctrl)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_check),
|
||||||
|
text = stringResource(MR.strings.migrate_from_device_finalize_migration),
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
click = {
|
||||||
|
finishMigration(fileId, ctrl)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_archive_will_be_deleted))
|
||||||
|
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_choose_migrate_from_another_device))
|
||||||
|
}
|
||||||
|
SectionSpacer()
|
||||||
|
SectionView(stringResource(MR.strings.show_QR_code).uppercase()) {
|
||||||
|
SimpleXLinkQRCode(link, onShare = {})
|
||||||
|
}
|
||||||
|
SectionSpacer()
|
||||||
|
SectionView(stringResource(MR.strings.migrate_from_device_or_share_this_file_link).uppercase()) {
|
||||||
|
LinkTextView(link, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationFromState>.FinishedView(chatDeletion: Boolean) {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_from_device_migration_complete).uppercase()) {
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_delete_forever),
|
||||||
|
text = stringResource(MR.strings.migrate_from_device_delete_database_from_device),
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
click = {
|
||||||
|
AlertManager.shared.showAlertDialog(
|
||||||
|
title = generalGetString(MR.strings.delete_chat_profile_question),
|
||||||
|
text = generalGetString(MR.strings.delete_chat_profile_action_cannot_be_undone_warning),
|
||||||
|
confirmText = generalGetString(MR.strings.delete_verb),
|
||||||
|
onConfirm = {
|
||||||
|
deleteChatAndDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_play_arrow_filled),
|
||||||
|
text = stringResource(MR.strings.migrate_from_device_start_chat),
|
||||||
|
textColor = MaterialTheme.colors.error,
|
||||||
|
click = {
|
||||||
|
AlertManager.shared.showAlertDialog(
|
||||||
|
title = generalGetString(MR.strings.start_chat_question),
|
||||||
|
text = generalGetString(MR.strings.migrate_from_device_starting_chat_on_multiple_devices_unsupported),
|
||||||
|
confirmText = generalGetString(MR.strings.migrate_from_device_start_chat),
|
||||||
|
onConfirm = {
|
||||||
|
withLongRunningApi { startChatAndDismiss() }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device))
|
||||||
|
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption))
|
||||||
|
}
|
||||||
|
if (chatDeletion) {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProgressView() {
|
||||||
|
DefaultProgressView(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LargeProgressView(value: Float, title: String, description: String) {
|
||||||
|
Box(Modifier.padding(DEFAULT_PADDING).fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = value,
|
||||||
|
(if (appPlatform.isDesktop) Modifier.size(DEFAULT_START_MODAL_WIDTH) else Modifier.size(windowWidth() - DEFAULT_PADDING * 2))
|
||||||
|
.rotate(-90f),
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
strokeWidth = 25.dp
|
||||||
|
)
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(description, color = Color.Transparent)
|
||||||
|
Text(title, style = MaterialTheme.typography.h1.copy(fontSize = 50.sp, fontWeight = FontWeight.Bold), color = MaterialTheme.colors.primary)
|
||||||
|
Text(description, style = MaterialTheme.typography.subtitle1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableState<MigrationFromState>.stopChat() {
|
||||||
|
withBGApi {
|
||||||
|
try {
|
||||||
|
stopChatAsync(chatModel)
|
||||||
|
try {
|
||||||
|
controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
|
||||||
|
state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationFromState.PassphraseNotSet else MigrationFromState.PassphraseConfirmation
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.migrate_from_device_error_saving_settings),
|
||||||
|
text = e.stackTraceToString()
|
||||||
|
)
|
||||||
|
state = MigrationFromState.ChatStopFailed(reason = generalGetString(MR.strings.migrate_from_device_error_saving_settings))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
state = MigrationFromState.ChatStopFailed(reason = e.stackTraceToString().take(10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun MutableState<MigrationFromState>.verifyDatabasePassphrase(dbKey: String) {
|
||||||
|
val error = controller.testStorageEncryption(dbKey)
|
||||||
|
if (error == null) {
|
||||||
|
state = MigrationFromState.UploadConfirmation
|
||||||
|
} else if (((error.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorOpen)?.sqliteError is SQLiteError.ErrorNotADatabase) {
|
||||||
|
showErrorOnMigrationIfNeeded(DBMigrationResult.ErrorNotADatabase(""))
|
||||||
|
} else {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.error),
|
||||||
|
text = generalGetString(MR.strings.migrate_from_device_error_verifying_passphrase) + " " + error.details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableState<MigrationFromState>.exportArchive() {
|
||||||
|
withLongRunningApi {
|
||||||
|
try {
|
||||||
|
getMigrationTempFilesDirectory().mkdir()
|
||||||
|
val archivePath = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf(""), mutableStateOf(Instant.DISTANT_PAST), mutableStateOf(""))
|
||||||
|
val totalBytes = File(archivePath).length()
|
||||||
|
if (totalBytes > 0L) {
|
||||||
|
state = MigrationFromState.DatabaseInit(totalBytes, archivePath)
|
||||||
|
} else {
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist))
|
||||||
|
state = MigrationFromState.UploadConfirmation
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.migrate_from_device_error_exporting_archive),
|
||||||
|
text = e.stackTraceToString()
|
||||||
|
)
|
||||||
|
state = MigrationFromState.UploadConfirmation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun initTemporaryDatabase(tempDatabaseFile: File, netCfg: NetCfg): Pair<ChatCtrl, User>? {
|
||||||
|
val (status, ctrl) = chatInitTemporaryDatabase(tempDatabaseFile.absolutePath)
|
||||||
|
showErrorOnMigrationIfNeeded(status)
|
||||||
|
try {
|
||||||
|
if (ctrl != null) {
|
||||||
|
val user = startChatWithTemporaryDatabase(ctrl, netCfg)
|
||||||
|
return if (user != null) ctrl to user else null
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Error while starting chat in temporary database: ${e.stackTraceToString()}")
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableState<MigrationFromState>.prepareDatabase(
|
||||||
|
tempDatabaseFile: File,
|
||||||
|
totalBytes: Long,
|
||||||
|
archivePath: String,
|
||||||
|
) {
|
||||||
|
withLongRunningApi {
|
||||||
|
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, getNetCfg())
|
||||||
|
if (ctrlAndUser == null) {
|
||||||
|
state = MigrationFromState.UploadFailed(totalBytes, archivePath)
|
||||||
|
return@withLongRunningApi
|
||||||
|
}
|
||||||
|
|
||||||
|
val (ctrl, user) = ctrlAndUser
|
||||||
|
state = MigrationFromState.UploadProgress(0L, totalBytes, 0L, archivePath, ctrl, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableState<MigrationFromState>.startUploading(
|
||||||
|
totalBytes: Long,
|
||||||
|
ctrl: ChatCtrl,
|
||||||
|
user: User,
|
||||||
|
tempDatabaseFile: File,
|
||||||
|
chatReceiver: MutableState<MigrationFromChatReceiver?>,
|
||||||
|
archivePath: String,
|
||||||
|
) {
|
||||||
|
withBGApi {
|
||||||
|
chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg ->
|
||||||
|
when (msg) {
|
||||||
|
is CR.SndFileProgressXFTP -> {
|
||||||
|
val s = state
|
||||||
|
if (s is MigrationFromState.UploadProgress && s.uploadedBytes != s.totalBytes) {
|
||||||
|
state = MigrationFromState.UploadProgress(msg.sentSize, msg.totalSize, msg.fileTransferMeta.fileId, archivePath, ctrl, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CR.SndFileRedirectStartXFTP -> {
|
||||||
|
delay(500)
|
||||||
|
state = MigrationFromState.LinkCreation
|
||||||
|
}
|
||||||
|
is CR.SndStandaloneFileComplete -> {
|
||||||
|
delay(500)
|
||||||
|
val cfg = getNetCfg()
|
||||||
|
val data = MigrationFileLinkData(
|
||||||
|
networkConfig = MigrationFileLinkData.NetworkConfig(
|
||||||
|
socksProxy = cfg.socksProxy,
|
||||||
|
hostMode = cfg.hostMode,
|
||||||
|
requiredHostMode = cfg.requiredHostMode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
state = MigrationFromState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl)
|
||||||
|
}
|
||||||
|
is CR.SndFileError -> {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.migrate_from_device_upload_failed),
|
||||||
|
generalGetString(MR.strings.migrate_from_device_check_connection_and_try_again)
|
||||||
|
)
|
||||||
|
state = MigrationFromState.UploadFailed(totalBytes, archivePath)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "unsupported event: ${msg.responseType}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chatReceiver.value?.start()
|
||||||
|
|
||||||
|
val (res, error) = controller.uploadStandaloneFile(user, CryptoFile.plain(File(archivePath).name), ctrl)
|
||||||
|
if (res == null) {
|
||||||
|
state = MigrationFromState.UploadFailed(totalBytes, archivePath)
|
||||||
|
return@withBGApi AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.migrate_from_device_error_uploading_archive),
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
state = MigrationFromState.UploadProgress(0, res.fileSize, res.fileId, archivePath, ctrl, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun cancelUploadedArchive(fileId: Long, ctrl: ChatCtrl) {
|
||||||
|
controller.apiCancelFile(null, fileId, ctrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelMigration(fileId: Long, ctrl: ChatCtrl) {
|
||||||
|
withBGApi {
|
||||||
|
cancelUploadedArchive(fileId, ctrl)
|
||||||
|
startChatAndDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableState<MigrationFromState>.finishMigration(fileId: Long, ctrl: ChatCtrl) {
|
||||||
|
withBGApi {
|
||||||
|
cancelUploadedArchive(fileId, ctrl)
|
||||||
|
state = MigrationFromState.Finished(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableState<MigrationFromState>.deleteChatAndDismiss() {
|
||||||
|
withBGApi {
|
||||||
|
try {
|
||||||
|
deleteChatAsync(chatModel)
|
||||||
|
chatModel.chatDbChanged.value = true
|
||||||
|
state = MigrationFromState.Finished(true)
|
||||||
|
try {
|
||||||
|
initChatController(startChat = { CompletableDeferred(false) })
|
||||||
|
chatModel.chatDbChanged.value = false
|
||||||
|
ModalManager.fullscreen.closeModals()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception(generalGetString(MR.strings.error_starting_chat) + "\n" + e.stackTraceToString())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.migrate_from_device_error_deleting_database),
|
||||||
|
text = e.stackTraceToString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startChatAndDismiss(dismiss: Boolean = true) {
|
||||||
|
try {
|
||||||
|
val user = chatModel.currentUser.value
|
||||||
|
if (chatModel.chatDbChanged.value) {
|
||||||
|
initChatController()
|
||||||
|
chatModel.chatDbChanged.value = false
|
||||||
|
} else if (user != null) {
|
||||||
|
startChat(user)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.error_starting_chat),
|
||||||
|
text = e.stackTraceToString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered
|
||||||
|
if (dismiss || chatModel.chatDbStatus.value != DBMigrationResult.OK) {
|
||||||
|
ModalManager.fullscreen.closeModals()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun MutableState<MigrationFromState>.cleanUpOnBack(chatReceiver: MigrationFromChatReceiver?) {
|
||||||
|
val s = state
|
||||||
|
if (s !is MigrationFromState.LinkShown && s !is MigrationFromState.Finished) {
|
||||||
|
chatModel.switchingUsersAndHosts.value = true
|
||||||
|
startChatAndDismiss(false)
|
||||||
|
chatModel.switchingUsersAndHosts.value = false
|
||||||
|
}
|
||||||
|
if (s is MigrationFromState.UploadProgress) {
|
||||||
|
cancelUploadedArchive(s.fileId, s.ctrl)
|
||||||
|
}
|
||||||
|
chatReceiver?.stopAndCleanUp()
|
||||||
|
getMigrationTempFilesDirectory().deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileForTemporaryDatabase(): File =
|
||||||
|
File(getMigrationTempFilesDirectory(), generateNewFileName("migration", "db", getMigrationTempFilesDirectory()))
|
||||||
|
|
||||||
|
private class MigrationFromChatReceiver(
|
||||||
|
val ctrl: ChatCtrl,
|
||||||
|
val databaseUrl: File,
|
||||||
|
var receiveMessages: Boolean = true,
|
||||||
|
val processReceivedMsg: suspend (CR) -> Unit
|
||||||
|
) {
|
||||||
|
fun start() {
|
||||||
|
Log.d(TAG, "MigrationChatReceiver startReceiver")
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
while (receiveMessages) {
|
||||||
|
try {
|
||||||
|
val msg = ChatController.recvMsg(ctrl)
|
||||||
|
if (msg != null && receiveMessages) {
|
||||||
|
val r = msg.resp
|
||||||
|
val rhId = msg.remoteHostId
|
||||||
|
Log.d(TAG, "processReceivedMsg: ${r.responseType}")
|
||||||
|
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
||||||
|
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
|
||||||
|
processReceivedMsg(r)
|
||||||
|
}
|
||||||
|
if (finishedWithoutTimeout == null) {
|
||||||
|
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
|
||||||
|
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.possible_slow_function_title),
|
||||||
|
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
|
||||||
|
shareText = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg exception: " + e.stackTraceToString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg throwable: " + e.stackTraceToString())
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopAndCleanUp() {
|
||||||
|
Log.d(TAG, "MigrationChatReceiver.stop")
|
||||||
|
receiveMessages = false
|
||||||
|
chatCloseStore(ctrl)
|
||||||
|
File(databaseUrl.absolutePath + "_chat.db").delete()
|
||||||
|
File(databaseUrl.absolutePath + "_agent.db").delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,737 @@
|
|||||||
|
package chat.simplex.common.views.migration
|
||||||
|
|
||||||
|
import SectionBottomSpacer
|
||||||
|
import SectionItemView
|
||||||
|
import SectionSpacer
|
||||||
|
import SectionTextFooter
|
||||||
|
import SectionView
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_TO_STAGE
|
||||||
|
import chat.simplex.common.model.ChatController.getNetCfg
|
||||||
|
import chat.simplex.common.model.ChatController.startChat
|
||||||
|
import chat.simplex.common.model.ChatCtrl
|
||||||
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.database.*
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword
|
||||||
|
import chat.simplex.common.views.newchat.QRCodeScanner
|
||||||
|
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||||
|
import chat.simplex.common.views.usersettings.*
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.toJavaInstant
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class MigrationToDeviceState {
|
||||||
|
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
|
||||||
|
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
|
||||||
|
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
|
||||||
|
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationToDeviceState()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Here we check whether it's needed to show migration process after app restart or not
|
||||||
|
// It's important to NOT show the process when archive was corrupted/not fully downloaded
|
||||||
|
fun makeMigrationState(): MigrationToState? {
|
||||||
|
val stage = settings.getStringOrNull(SHARED_PREFS_MIGRATION_TO_STAGE)
|
||||||
|
val state: MigrationToDeviceState? = if (stage != null) json.decodeFromString(stage) else null
|
||||||
|
val initial: MigrationToState? = when(state) {
|
||||||
|
null -> null
|
||||||
|
is DownloadProgress -> {
|
||||||
|
// No migration happens at the moment actually since archive were not downloaded fully
|
||||||
|
Log.e(TAG, "MigrateToDevice: archive wasn't fully downloaded, removed broken file")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
is Onion -> null
|
||||||
|
is ArchiveImport -> {
|
||||||
|
if (!File(getMigrationTempFilesDirectory(), state.archiveName).exists()) {
|
||||||
|
Log.e(TAG, "MigrateToDevice: archive was removed unintentionally or state is broken, dropping migration")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
|
||||||
|
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Passphrase -> MigrationToState.Passphrase("", state.netCfg)
|
||||||
|
}
|
||||||
|
if (initial == null) {
|
||||||
|
settings.remove(SHARED_PREFS_MIGRATION_TO_STAGE)
|
||||||
|
getMigrationTempFilesDirectory().deleteRecursively()
|
||||||
|
}
|
||||||
|
return initial
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(state: MigrationToDeviceState?) {
|
||||||
|
if (state != null) {
|
||||||
|
appPreferences.migrationToStage.set(json.encodeToString(state))
|
||||||
|
} else {
|
||||||
|
appPreferences.migrationToStage.set(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class MigrationToState {
|
||||||
|
@Serializable object PasteOrScanLink: MigrationToState()
|
||||||
|
@Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToState()
|
||||||
|
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationToState()
|
||||||
|
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||||
|
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationToState()
|
||||||
|
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||||
|
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||||
|
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||||
|
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationToState()
|
||||||
|
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
|
||||||
|
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var MutableState<MigrationToState?>.state: MigrationToState?
|
||||||
|
get() = value
|
||||||
|
set(v) { value = v }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModalData.MigrateToDeviceView(close: () -> Unit) {
|
||||||
|
val migrationState = remember { chatModel.migrationState }
|
||||||
|
// Prevent from hiding the view until migration is finished or app deleted
|
||||||
|
val backDisabled = remember {
|
||||||
|
derivedStateOf {
|
||||||
|
when (chatModel.migrationState.value) {
|
||||||
|
null,
|
||||||
|
is MigrationToState.PasteOrScanLink,
|
||||||
|
is MigrationToState.Onion,
|
||||||
|
is MigrationToState.LinkDownloading,
|
||||||
|
is MigrationToState.DownloadProgress,
|
||||||
|
is MigrationToState.DownloadFailed,
|
||||||
|
is MigrationToState.ArchiveImportFailed -> false
|
||||||
|
|
||||||
|
is MigrationToState.ArchiveImport,
|
||||||
|
is MigrationToState.DatabaseInit,
|
||||||
|
is MigrationToState.Migration,
|
||||||
|
is MigrationToState.MigrationConfirmation,
|
||||||
|
is MigrationToState.Passphrase -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val chatReceiver = remember { mutableStateOf(null as MigrationToChatReceiver?) }
|
||||||
|
ModalView(
|
||||||
|
enableClose = !backDisabled.value,
|
||||||
|
close = {
|
||||||
|
withBGApi {
|
||||||
|
migrationState.cleanUpOnBack(chatReceiver.value)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MigrateToDeviceLayout(
|
||||||
|
migrationState = migrationState,
|
||||||
|
chatReceiver = chatReceiver,
|
||||||
|
close = close,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ModalData.MigrateToDeviceLayout(
|
||||||
|
migrationState: MutableState<MigrationToState?>,
|
||||||
|
chatReceiver: MutableState<MigrationToChatReceiver?>,
|
||||||
|
close: () -> Unit,
|
||||||
|
) {
|
||||||
|
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).height(IntrinsicSize.Max),
|
||||||
|
) {
|
||||||
|
AppBarTitle(stringResource(MR.strings.migrate_to_device_title))
|
||||||
|
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close)
|
||||||
|
SectionBottomSpacer()
|
||||||
|
}
|
||||||
|
platform.androidLockPortraitOrientation()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ModalData.SectionByState(
|
||||||
|
migrationState: MutableState<MigrationToState?>,
|
||||||
|
tempDatabaseFile: File,
|
||||||
|
chatReceiver: MutableState<MigrationToChatReceiver?>,
|
||||||
|
close: () -> Unit
|
||||||
|
) {
|
||||||
|
when (val s = migrationState.value) {
|
||||||
|
null -> {}
|
||||||
|
is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView()
|
||||||
|
is MigrationToState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState)
|
||||||
|
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg)
|
||||||
|
is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg)
|
||||||
|
is MigrationToState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes)
|
||||||
|
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg)
|
||||||
|
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg)
|
||||||
|
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg)
|
||||||
|
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg)
|
||||||
|
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg)
|
||||||
|
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationToState?>.PasteOrScanLinkView() {
|
||||||
|
if (appPlatform.isAndroid) {
|
||||||
|
SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) {
|
||||||
|
QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text ->
|
||||||
|
withBGApi { checkUserLink(text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SectionSpacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appPlatform.isDesktop || appPreferences.developerTools.get()) {
|
||||||
|
SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) {
|
||||||
|
PasteLinkView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationToState?>.PasteLinkView() {
|
||||||
|
val clipboard = LocalClipboardManager.current
|
||||||
|
SectionItemView({
|
||||||
|
val str = clipboard.getText()?.text ?: return@SectionItemView
|
||||||
|
withBGApi { checkUserLink(str) }
|
||||||
|
}) {
|
||||||
|
Text(stringResource(MR.strings.tap_to_paste_link))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
|
||||||
|
val onionHosts = remember { stateGetOrPut("onionHosts") {
|
||||||
|
getNetCfg().copy(socksProxy = socksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts
|
||||||
|
} }
|
||||||
|
val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { socksProxy != null } }
|
||||||
|
val sessionMode = remember { stateGetOrPut("sessionMode") { TransportSessionMode.User} }
|
||||||
|
val networkProxyHostPort = remember { stateGetOrPut("networkHostProxyPort") {
|
||||||
|
var proxy = (socksProxy ?: chatModel.controller.appPrefs.networkProxyHostPort.get())
|
||||||
|
if (proxy?.startsWith(":") == true) proxy = "localhost$proxy"
|
||||||
|
proxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val proxyPort = remember { derivedStateOf { networkProxyHostPort.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
|
||||||
|
|
||||||
|
val netCfg = rememberSaveable(stateSaver = serializableSaver()) {
|
||||||
|
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) {
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_check),
|
||||||
|
text = stringResource(MR.strings.migrate_to_device_apply_onion),
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
click = {
|
||||||
|
val updated = netCfg.value
|
||||||
|
.withOnionHosts(onionHosts.value)
|
||||||
|
.withHostPort(if (networkUseSocksProxy.value) networkProxyHostPort.value else null, null)
|
||||||
|
.copy(
|
||||||
|
sessionMode = sessionMode.value
|
||||||
|
)
|
||||||
|
withBGApi {
|
||||||
|
state.value = MigrationToState.DatabaseInit(link, updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
){}
|
||||||
|
SectionTextFooter(stringResource(MR.strings.migrate_to_device_confirm_network_settings_footer))
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
|
||||||
|
val networkProxyHostPortPref = SharedPreference(get = { networkProxyHostPort.value }, set = {
|
||||||
|
networkProxyHostPort.value = it
|
||||||
|
})
|
||||||
|
SectionView(stringResource(MR.strings.network_settings_title).uppercase()) {
|
||||||
|
OnionRelatedLayout(
|
||||||
|
appPreferences.developerTools.get(),
|
||||||
|
networkUseSocksProxy,
|
||||||
|
onionHosts,
|
||||||
|
sessionMode,
|
||||||
|
networkProxyHostPortPref,
|
||||||
|
proxyPort,
|
||||||
|
toggleSocksProxy = { enable ->
|
||||||
|
networkUseSocksProxy.value = enable
|
||||||
|
},
|
||||||
|
useOnion = {
|
||||||
|
onionHosts.value = it
|
||||||
|
},
|
||||||
|
updateSessionMode = {
|
||||||
|
sessionMode.value = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {}
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
prepareDatabase(link, tempDatabaseFile, netCfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationToState?>.LinkDownloadingView(
|
||||||
|
link: String,
|
||||||
|
ctrl: ChatCtrl,
|
||||||
|
user: User,
|
||||||
|
archivePath: String,
|
||||||
|
tempDatabaseFile: File,
|
||||||
|
chatReceiver: MutableState<MigrationToChatReceiver?>,
|
||||||
|
netCfg: NetCfg
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_to_device_downloading_details).uppercase()) {}
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_to_device_downloading_archive).uppercase()) {
|
||||||
|
val ratio = downloadedBytes.toFloat() / max(totalBytes, 1)
|
||||||
|
LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_to_device_bytes_downloaded).format(formatBytes(downloadedBytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg) {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) {
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_download),
|
||||||
|
text = stringResource(MR.strings.migrate_to_device_repeat_download),
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
click = {
|
||||||
|
state = MigrationToState.DatabaseInit(link, netCfg)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
chatReceiver?.stopAndCleanUp()
|
||||||
|
File(archivePath).delete()
|
||||||
|
MigrationToDeviceState.save(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg) {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {}
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
importArchive(archivePath, netCfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) {
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_download),
|
||||||
|
text = stringResource(MR.strings.migrate_to_device_repeat_import),
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
click = {
|
||||||
|
state = MigrationToState.ArchiveImport(archivePath, netCfg)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) {
|
||||||
|
val currentKey = rememberSaveable { mutableStateOf(currentKey) }
|
||||||
|
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
|
||||||
|
val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
|
||||||
|
|
||||||
|
Box {
|
||||||
|
val view = LocalMultiplatformView()
|
||||||
|
SectionView(stringResource(MR.strings.migrate_to_device_enter_passphrase).uppercase()) {
|
||||||
|
SavePassphraseSetting(
|
||||||
|
useKeychain.value,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
enabled = !verifyingPassphrase.value,
|
||||||
|
smallPadding = false
|
||||||
|
) { checked -> useKeychain.value = checked }
|
||||||
|
|
||||||
|
PassphraseField(currentKey, placeholder = stringResource(MR.strings.current_passphrase), Modifier.padding(horizontal = DEFAULT_PADDING), isValid = ::validKey, requestFocus = true)
|
||||||
|
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_vpn_key_filled),
|
||||||
|
text = stringResource(MR.strings.open_chat),
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
disabled = verifyingPassphrase.value || currentKey.value.isEmpty(),
|
||||||
|
click = {
|
||||||
|
verifyingPassphrase.value = true
|
||||||
|
hideKeyboard(view)
|
||||||
|
withBGApi {
|
||||||
|
val (status, _) = chatInitTemporaryDatabase(dbAbsolutePrefixPath, key = currentKey.value, confirmation = MigrationConfirmation.YesUp)
|
||||||
|
val success = status == DBMigrationResult.OK || status == DBMigrationResult.InvalidConfirmation
|
||||||
|
if (success) {
|
||||||
|
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg)
|
||||||
|
} else if (status is DBMigrationResult.ErrorMigration) {
|
||||||
|
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg)
|
||||||
|
} else {
|
||||||
|
showErrorOnMigrationIfNeeded(status)
|
||||||
|
}
|
||||||
|
verifyingPassphrase.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
DatabaseEncryptionFooter(useKeychain, chatDbEncrypted = true, remember { mutableStateOf(false) }, remember { mutableStateOf(false) }, true)
|
||||||
|
}
|
||||||
|
if (verifyingPassphrase.value) {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) {
|
||||||
|
data class Tuple4<A,B,C,D>(val a: A, val b: B, val c: C, val d: D)
|
||||||
|
val (header: String, button: String?, footer: String, confirmation: MigrationConfirmation?) = when (status) {
|
||||||
|
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
|
||||||
|
is MigrationError.Upgrade ->
|
||||||
|
Tuple4(
|
||||||
|
generalGetString(MR.strings.database_upgrade),
|
||||||
|
generalGetString(MR.strings.upgrade_and_open_chat),
|
||||||
|
"",
|
||||||
|
MigrationConfirmation.YesUp
|
||||||
|
)
|
||||||
|
is MigrationError.Downgrade ->
|
||||||
|
Tuple4(
|
||||||
|
generalGetString(MR.strings.database_downgrade),
|
||||||
|
generalGetString(MR.strings.downgrade_and_open_chat),
|
||||||
|
generalGetString(MR.strings.database_downgrade_warning),
|
||||||
|
MigrationConfirmation.YesUpDown
|
||||||
|
)
|
||||||
|
is MigrationError.Error ->
|
||||||
|
Tuple4(
|
||||||
|
generalGetString(MR.strings.incompatible_database_version),
|
||||||
|
null,
|
||||||
|
mtrErrorDescription(err.mtrError),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> Tuple4(generalGetString(MR.strings.error), null, generalGetString(MR.strings.unknown_error), null)
|
||||||
|
}
|
||||||
|
SectionView(header.uppercase()) {
|
||||||
|
if (button != null && confirmation != null) {
|
||||||
|
SettingsActionItemWithContent(
|
||||||
|
icon = painterResource(MR.images.ic_download),
|
||||||
|
text = button,
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
click = {
|
||||||
|
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
SectionTextFooter(footer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
|
||||||
|
Box {
|
||||||
|
SectionView(stringResource(MR.strings.migrate_to_device_migrating).uppercase()) {}
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
startChat(passphrase, confirmation, useKeychain, netCfg, close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProgressView() {
|
||||||
|
DefaultProgressView(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String) {
|
||||||
|
if (strHasSimplexFileLink(link.trim())) {
|
||||||
|
val data = MigrationFileLinkData.readFromLink(link)
|
||||||
|
val hasOnionConfigured = data?.networkConfig?.hasOnionConfigured() ?: false
|
||||||
|
val networkConfig = data?.networkConfig?.transformToPlatformSupported()
|
||||||
|
// If any of iOS or Android had onion enabled, show onion screen
|
||||||
|
if (hasOnionConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) {
|
||||||
|
state = MigrationToState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
|
||||||
|
MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
|
||||||
|
} else {
|
||||||
|
val current = getNetCfg()
|
||||||
|
state = MigrationToState.DatabaseInit(link.trim(), current.copy(
|
||||||
|
socksProxy = networkConfig?.socksProxy,
|
||||||
|
hostMode = networkConfig?.hostMode ?: current.hostMode,
|
||||||
|
requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.invalid_file_link),
|
||||||
|
text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableState<MigrationToState?>.prepareDatabase(
|
||||||
|
link: String,
|
||||||
|
tempDatabaseFile: File,
|
||||||
|
netCfg: NetCfg,
|
||||||
|
) {
|
||||||
|
withLongRunningApi {
|
||||||
|
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg)
|
||||||
|
if (ctrlAndUser == null) {
|
||||||
|
state = MigrationToState.DownloadFailed(0, link, archivePath(), netCfg)
|
||||||
|
return@withLongRunningApi
|
||||||
|
}
|
||||||
|
|
||||||
|
val (ctrl, user) = ctrlAndUser
|
||||||
|
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableState<MigrationToState?>.startDownloading(
|
||||||
|
totalBytes: Long,
|
||||||
|
ctrl: ChatCtrl,
|
||||||
|
user: User,
|
||||||
|
tempDatabaseFile: File,
|
||||||
|
chatReceiver: MutableState<MigrationToChatReceiver?>,
|
||||||
|
link: String,
|
||||||
|
archivePath: String,
|
||||||
|
netCfg: NetCfg,
|
||||||
|
) {
|
||||||
|
withBGApi {
|
||||||
|
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
|
||||||
|
when (msg) {
|
||||||
|
is CR.RcvFileProgressXFTP -> {
|
||||||
|
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl)
|
||||||
|
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg))
|
||||||
|
}
|
||||||
|
is CR.RcvStandaloneFileComplete -> {
|
||||||
|
delay(500)
|
||||||
|
// User closed the whole screen before new state was saved
|
||||||
|
if (state == null) {
|
||||||
|
MigrationToDeviceState.save(null)
|
||||||
|
} else {
|
||||||
|
state = MigrationToState.ArchiveImport(archivePath, netCfg)
|
||||||
|
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CR.RcvFileError -> {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.migrate_to_device_download_failed),
|
||||||
|
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
|
||||||
|
)
|
||||||
|
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||||
|
}
|
||||||
|
is CR.ChatRespError -> {
|
||||||
|
if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.migrate_to_device_download_failed),
|
||||||
|
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
|
||||||
|
)
|
||||||
|
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "unsupported error: ${msg.responseType}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Log.d(TAG, "unsupported event: ${msg.responseType}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chatReceiver.value?.start()
|
||||||
|
|
||||||
|
val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl)
|
||||||
|
if (res == null) {
|
||||||
|
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.migrate_to_device_error_downloading_archive),
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg) {
|
||||||
|
withLongRunningApi {
|
||||||
|
try {
|
||||||
|
if (ChatController.ctrl == null || ChatController.ctrl == -1L) {
|
||||||
|
chatInitControllerRemovingDatabases()
|
||||||
|
}
|
||||||
|
controller.apiDeleteStorage()
|
||||||
|
try {
|
||||||
|
val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString())
|
||||||
|
val archiveErrors = controller.apiImportArchive(config)
|
||||||
|
if (archiveErrors.isNotEmpty()) {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.chat_database_imported),
|
||||||
|
generalGetString(MR.strings.non_fatal_errors_occured_during_import)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
state = MigrationToState.Passphrase("", netCfg)
|
||||||
|
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
|
||||||
|
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_importing_database), e.stackTraceToString())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
|
||||||
|
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun stopArchiveDownloading(fileId: Long, ctrl: ChatCtrl) {
|
||||||
|
controller.apiCancelFile(null, fileId, ctrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
|
||||||
|
if (useKeychain) {
|
||||||
|
ksDatabasePassword.set(passphrase)
|
||||||
|
} else {
|
||||||
|
ksDatabasePassword.remove()
|
||||||
|
}
|
||||||
|
appPreferences.storeDBPassphrase.set(useKeychain)
|
||||||
|
appPreferences.initialRandomDBPassphrase.set(false)
|
||||||
|
withBGApi {
|
||||||
|
try {
|
||||||
|
initChatController(useKey = passphrase, confirmMigrations = confirmation) { CompletableDeferred(false) }
|
||||||
|
val appSettings = controller.apiGetAppSettings(AppSettings.current.prepareForExport()).copy(
|
||||||
|
networkConfig = netCfg
|
||||||
|
)
|
||||||
|
finishMigration(appSettings, close)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
hideView(close)
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.stackTraceToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun finishMigration(appSettings: AppSettings, close: () -> Unit) {
|
||||||
|
try {
|
||||||
|
getMigrationTempFilesDirectory().deleteRecursively()
|
||||||
|
appSettings.importIntoApp()
|
||||||
|
val user = chatModel.currentUser.value
|
||||||
|
if (user != null) {
|
||||||
|
startChat(user)
|
||||||
|
}
|
||||||
|
hideView(close)
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_chat_migrated), generalGetString(MR.strings.migrate_to_device_finalize_migration))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.stackTraceToString())
|
||||||
|
}
|
||||||
|
MigrationToDeviceState.save(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideView(close: () -> Unit) {
|
||||||
|
appPreferences.onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||||
|
chatModel.migrationState.value = null
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun MutableState<MigrationToState?>.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) {
|
||||||
|
val state = state
|
||||||
|
if (state is MigrationToState.ArchiveImportFailed) {
|
||||||
|
// Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state
|
||||||
|
deleteChatDatabaseFilesAndState()
|
||||||
|
initChatControllerAndRunMigrations()
|
||||||
|
} else if (state is MigrationToState.DownloadProgress && state.ctrl != null) {
|
||||||
|
stopArchiveDownloading(state.fileId, state.ctrl)
|
||||||
|
}
|
||||||
|
chatReceiver?.stopAndCleanUp()
|
||||||
|
getMigrationTempFilesDirectory().deleteRecursively()
|
||||||
|
MigrationToDeviceState.save(null)
|
||||||
|
chatModel.migrationState.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun strHasSimplexFileLink(text: String): Boolean =
|
||||||
|
text.startsWith("simplex:/file") || text.startsWith("https://simplex.chat/file")
|
||||||
|
|
||||||
|
private fun fileForTemporaryDatabase(): File =
|
||||||
|
File(getMigrationTempFilesDirectory(), generateNewFileName("migration", "db", getMigrationTempFilesDirectory()))
|
||||||
|
|
||||||
|
private fun archivePath(): String {
|
||||||
|
val archiveTime = Clock.System.now()
|
||||||
|
val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant()))
|
||||||
|
val archiveName = "simplex-chat.$ts.zip"
|
||||||
|
val archivePath = File(getMigrationTempFilesDirectory(), archiveName)
|
||||||
|
return archivePath.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MigrationToChatReceiver(
|
||||||
|
val ctrl: ChatCtrl,
|
||||||
|
val databaseUrl: File,
|
||||||
|
var receiveMessages: Boolean = true,
|
||||||
|
val processReceivedMsg: suspend (CR) -> Unit
|
||||||
|
) {
|
||||||
|
fun start() {
|
||||||
|
Log.d(TAG, "MigrationChatReceiver startReceiver")
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
while (receiveMessages) {
|
||||||
|
try {
|
||||||
|
val msg = ChatController.recvMsg(ctrl)
|
||||||
|
if (msg != null && receiveMessages) {
|
||||||
|
val r = msg.resp
|
||||||
|
val rhId = msg.remoteHostId
|
||||||
|
Log.d(TAG, "processReceivedMsg: ${r.responseType}")
|
||||||
|
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
||||||
|
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
|
||||||
|
processReceivedMsg(r)
|
||||||
|
}
|
||||||
|
if (finishedWithoutTimeout == null) {
|
||||||
|
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
|
||||||
|
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.possible_slow_function_title),
|
||||||
|
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
|
||||||
|
shareText = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg exception: " + e.stackTraceToString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg throwable: " + e.stackTraceToString())
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopAndCleanUp() {
|
||||||
|
Log.d(TAG, "MigrationChatReceiver.stop")
|
||||||
|
receiveMessages = false
|
||||||
|
chatCloseStore(ctrl)
|
||||||
|
File(databaseUrl.absolutePath + "_chat.db").delete()
|
||||||
|
File(databaseUrl.absolutePath + "_agent.db").delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -301,7 +301,7 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, showQRC
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LinkTextView(link: String, share: Boolean) {
|
fun LinkTextView(link: String, share: Boolean) {
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
Row(Modifier.fillMaxWidth().heightIn(min = 46.dp).padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.fillMaxWidth().heightIn(min = 46.dp).padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Box(Modifier.weight(1f).clickable {
|
Box(Modifier.weight(1f).clickable {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
|
|||||||
prefs.storeDBPassphrase.set(false)
|
prefs.storeDBPassphrase.set(false)
|
||||||
|
|
||||||
val newKeyValue = newKey.value
|
val newKeyValue = newKey.value
|
||||||
val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator)
|
val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator, false)
|
||||||
if (success) {
|
if (success) {
|
||||||
startChat(newKeyValue)
|
startChat(newKeyValue)
|
||||||
nextStep()
|
nextStep()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.compose.foundation.*
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -16,8 +16,11 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.platform.chatModel
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.views.migration.MigrateToDeviceView
|
||||||
|
import chat.simplex.common.views.migration.MigrationToState
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
|
||||||
@@ -62,17 +65,33 @@ fun SimpleXInfoLayout(
|
|||||||
OnboardingActionButton(user, onboardingStage)
|
OnboardingActionButton(user, onboardingStage)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = DEFAULT_PADDING), contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
SimpleButtonDecorated(text = stringResource(MR.strings.migrate_from_another_device), icon = painterResource(MR.images.ic_download),
|
||||||
|
click = {
|
||||||
|
chatModel.migrationState.value = MigrationToState.PasteOrScanLink
|
||||||
|
ModalManager.fullscreen.showCustomModal { close -> MigrateToDeviceView(close) } })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = DEFAULT_PADDING.times(1.5f), top = DEFAULT_PADDING), contentAlignment = Alignment.Center
|
.padding(bottom = DEFAULT_PADDING.times(1.5f), top = if (onboardingStage == null) DEFAULT_PADDING else 0.dp), contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
SimpleButtonDecorated(text = stringResource(MR.strings.how_it_works), icon = painterResource(MR.images.ic_info),
|
SimpleButtonDecorated(text = stringResource(MR.strings.how_it_works), icon = painterResource(MR.images.ic_info),
|
||||||
click = showModal { HowItWorks(user, onboardingStage) })
|
click = showModal { HowItWorks(user, onboardingStage) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (chatModel.migrationState.value != null && !ModalManager.fullscreen.hasModalsOpen()) {
|
||||||
|
ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateToDeviceView(close) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ fun DeveloperView(
|
|||||||
SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors)
|
SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors)
|
||||||
SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls)
|
SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
SectionView("Experimental".uppercase()) {
|
||||||
|
SettingsPreferenceItem(painterResource(MR.images.ic_vpn_key_filled), "Post-quantum E2EE", m.controller.appPrefs.pqExperimentalEnabled, onChange = { enable ->
|
||||||
|
withBGApi { m.controller.apiSetPQEncryption(enable) }
|
||||||
|
})
|
||||||
|
SectionTextFooter("In this version applies only to new contacts.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SectionBottomSpacer()
|
SectionBottomSpacer()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,7 @@ import chat.simplex.common.views.helpers.annotatedStringResource
|
|||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NetworkAndServersView(
|
fun NetworkAndServersView() {
|
||||||
chatModel: ChatModel,
|
|
||||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
|
||||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
|
||||||
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
|
||||||
) {
|
|
||||||
val currentRemoteHost by remember { chatModel.currentRemoteHost }
|
val currentRemoteHost by remember { chatModel.currentRemoteHost }
|
||||||
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations
|
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations
|
||||||
val netCfg = remember { chatModel.controller.getNetCfg() }
|
val netCfg = remember { chatModel.controller.getNetCfg() }
|
||||||
@@ -55,9 +50,6 @@ fun NetworkAndServersView(
|
|||||||
onionHosts = onionHosts,
|
onionHosts = onionHosts,
|
||||||
sessionMode = sessionMode,
|
sessionMode = sessionMode,
|
||||||
proxyPort = proxyPort,
|
proxyPort = proxyPort,
|
||||||
showModal = showModal,
|
|
||||||
showSettingsModal = showSettingsModal,
|
|
||||||
showCustomModal = showCustomModal,
|
|
||||||
toggleSocksProxy = { enable ->
|
toggleSocksProxy = { enable ->
|
||||||
if (enable) {
|
if (enable) {
|
||||||
AlertManager.shared.showAlertDialog(
|
AlertManager.shared.showAlertDialog(
|
||||||
@@ -154,13 +146,11 @@ fun NetworkAndServersView(
|
|||||||
onionHosts: MutableState<OnionHosts>,
|
onionHosts: MutableState<OnionHosts>,
|
||||||
sessionMode: MutableState<TransportSessionMode>,
|
sessionMode: MutableState<TransportSessionMode>,
|
||||||
proxyPort: State<Int>,
|
proxyPort: State<Int>,
|
||||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
|
||||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
|
||||||
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
|
||||||
toggleSocksProxy: (Boolean) -> Unit,
|
toggleSocksProxy: (Boolean) -> Unit,
|
||||||
useOnion: (OnionHosts) -> Unit,
|
useOnion: (OnionHosts) -> Unit,
|
||||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val m = chatModel
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
@@ -168,17 +158,18 @@ fun NetworkAndServersView(
|
|||||||
AppBarTitle(stringResource(MR.strings.network_and_servers))
|
AppBarTitle(stringResource(MR.strings.network_and_servers))
|
||||||
if (!chatModel.desktopNoUserNoRemote) {
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
|
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
|
||||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) })
|
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) } })
|
||||||
|
|
||||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) })
|
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } })
|
||||||
|
|
||||||
if (currentRemoteHost == null) {
|
if (currentRemoteHost == null) {
|
||||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
|
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
|
||||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
|
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false)
|
||||||
|
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||||
if (developerTools) {
|
if (developerTools) {
|
||||||
SessionModePicker(sessionMode, showSettingsModal, updateSessionMode)
|
SessionModePicker(sessionMode, showModal, updateSessionMode)
|
||||||
}
|
}
|
||||||
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showModal { AdvancedNetworkSettingsView(m) } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,18 +187,39 @@ fun NetworkAndServersView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
|
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
|
||||||
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), showModal { RTCServersView(it) })
|
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
|
||||||
}
|
}
|
||||||
SectionBottomSpacer()
|
SectionBottomSpacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable fun OnionRelatedLayout(
|
||||||
|
developerTools: Boolean,
|
||||||
|
networkUseSocksProxy: MutableState<Boolean>,
|
||||||
|
onionHosts: MutableState<OnionHosts>,
|
||||||
|
sessionMode: MutableState<TransportSessionMode>,
|
||||||
|
networkProxyHostPort: SharedPreference<String?>,
|
||||||
|
proxyPort: State<Int>,
|
||||||
|
toggleSocksProxy: (Boolean) -> Unit,
|
||||||
|
useOnion: (OnionHosts) -> Unit,
|
||||||
|
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||||
|
) {
|
||||||
|
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.fullscreen.showModal(content = it) }
|
||||||
|
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, networkProxyHostPort, true)
|
||||||
|
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||||
|
if (developerTools) {
|
||||||
|
SessionModePicker(sessionMode, showModal, updateSessionMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UseSocksProxySwitch(
|
fun UseSocksProxySwitch(
|
||||||
networkUseSocksProxy: MutableState<Boolean>,
|
networkUseSocksProxy: MutableState<Boolean>,
|
||||||
proxyPort: State<Int>,
|
proxyPort: State<Int>,
|
||||||
toggleSocksProxy: (Boolean) -> Unit,
|
toggleSocksProxy: (Boolean) -> Unit,
|
||||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
|
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||||
|
networkProxyHostPort: SharedPreference<String?> = chatModel.controller.appPrefs.networkProxyHostPort,
|
||||||
|
migration: Boolean = false,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING),
|
Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING),
|
||||||
@@ -227,8 +239,11 @@ fun UseSocksProxySwitch(
|
|||||||
val text = buildAnnotatedString {
|
val text = buildAnnotatedString {
|
||||||
append(generalGetString(MR.strings.network_socks_toggle_use_socks_proxy) + " (")
|
append(generalGetString(MR.strings.network_socks_toggle_use_socks_proxy) + " (")
|
||||||
val style = SpanStyle(color = MaterialTheme.colors.primary)
|
val style = SpanStyle(color = MaterialTheme.colors.primary)
|
||||||
|
val disabledStyle = SpanStyle(color = MaterialTheme.colors.onBackground)
|
||||||
withAnnotation(tag = "PORT", annotation = generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) {
|
withAnnotation(tag = "PORT", annotation = generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) {
|
||||||
withStyle(style) { append(generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) }
|
withStyle(if (networkUseSocksProxy.value || !migration) style else disabledStyle) {
|
||||||
|
append(generalGetString(MR.strings.network_proxy_port).format(proxyPort.value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
append(")")
|
append(")")
|
||||||
}
|
}
|
||||||
@@ -238,7 +253,9 @@ fun UseSocksProxySwitch(
|
|||||||
onClick = { offset ->
|
onClick = { offset ->
|
||||||
text.getStringAnnotations(tag = "PORT", start = offset, end = offset)
|
text.getStringAnnotations(tag = "PORT", start = offset, end = offset)
|
||||||
.firstOrNull()?.let { _ ->
|
.firstOrNull()?.let { _ ->
|
||||||
showSettingsModal { SockProxySettings(it) }()
|
if (networkUseSocksProxy.value || !migration) {
|
||||||
|
showModal { SockProxySettings(chatModel, networkProxyHostPort, migration) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shouldConsumeEvent = { offset ->
|
shouldConsumeEvent = { offset ->
|
||||||
@@ -254,7 +271,11 @@ fun UseSocksProxySwitch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SockProxySettings(m: ChatModel) {
|
fun SockProxySettings(
|
||||||
|
m: ChatModel,
|
||||||
|
networkProxyHostPort: SharedPreference<String?> = m.controller.appPrefs.networkProxyHostPort,
|
||||||
|
migration: Boolean,
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -262,17 +283,17 @@ fun SockProxySettings(m: ChatModel) {
|
|||||||
) {
|
) {
|
||||||
val defaultHostPort = remember { "localhost:9050" }
|
val defaultHostPort = remember { "localhost:9050" }
|
||||||
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
|
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
|
||||||
val hostPort by remember { m.controller.appPrefs.networkProxyHostPort.state }
|
val hostPortSaved by remember { networkProxyHostPort.state }
|
||||||
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||||
mutableStateOf(TextFieldValue(hostPort?.split(":")?.firstOrNull() ?: "localhost"))
|
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"))
|
||||||
}
|
}
|
||||||
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||||
mutableStateOf(TextFieldValue(hostPort?.split(":")?.lastOrNull() ?: "9050"))
|
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050"))
|
||||||
}
|
}
|
||||||
val save = {
|
val save = {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
m.controller.appPrefs.networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
||||||
if (m.controller.appPrefs.networkUseSocksProxy.get()) {
|
if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) {
|
||||||
m.controller.apiSetNetworkConfig(m.controller.getNetCfg())
|
m.controller.apiSetNetworkConfig(m.controller.getNetCfg())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,21 +302,21 @@ fun SockProxySettings(m: ChatModel) {
|
|||||||
SectionItemView {
|
SectionItemView {
|
||||||
ResetToDefaultsButton({
|
ResetToDefaultsButton({
|
||||||
val reset = {
|
val reset = {
|
||||||
m.controller.appPrefs.networkProxyHostPort.set(defaultHostPort)
|
networkProxyHostPort.set(defaultHostPort)
|
||||||
val newHost = defaultHostPort.split(":").first()
|
val newHost = defaultHostPort.split(":").first()
|
||||||
val newPort = defaultHostPort.split(":").last()
|
val newPort = defaultHostPort.split(":").last()
|
||||||
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
|
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
|
||||||
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
|
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
if (m.controller.appPrefs.networkUseSocksProxy.get()) {
|
if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) {
|
||||||
showUpdateNetworkSettingsDialog {
|
showUpdateNetworkSettingsDialog {
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
}, disabled = hostPort == defaultHostPort)
|
}, disabled = hostPortSaved == defaultHostPort)
|
||||||
}
|
}
|
||||||
SectionItemView {
|
SectionItemView {
|
||||||
DefaultConfigurableTextField(
|
DefaultConfigurableTextField(
|
||||||
@@ -321,14 +342,14 @@ fun SockProxySettings(m: ChatModel) {
|
|||||||
SectionCustomFooter {
|
SectionCustomFooter {
|
||||||
NetworkSectionFooter(
|
NetworkSectionFooter(
|
||||||
revert = {
|
revert = {
|
||||||
val prevHost = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.firstOrNull() ?: "localhost"
|
val prevHost = hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"
|
||||||
val prevPort = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.lastOrNull() ?: "9050"
|
val prevPort = hostPortSaved?.split(":")?.lastOrNull() ?: "9050"
|
||||||
hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length))
|
hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length))
|
||||||
portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length))
|
portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length))
|
||||||
},
|
},
|
||||||
save = { if (m.controller.appPrefs.networkUseSocksProxy.get()) showUpdateNetworkSettingsDialog { save() } else save() },
|
save = { if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) showUpdateNetworkSettingsDialog { save() } else save() },
|
||||||
revertDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text),
|
revertDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text),
|
||||||
saveDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
|
saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
|
||||||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
|
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
|
||||||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
|
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
|
||||||
)
|
)
|
||||||
@@ -341,7 +362,7 @@ fun SockProxySettings(m: ChatModel) {
|
|||||||
private fun UseOnionHosts(
|
private fun UseOnionHosts(
|
||||||
onionHosts: MutableState<OnionHosts>,
|
onionHosts: MutableState<OnionHosts>,
|
||||||
enabled: State<Boolean>,
|
enabled: State<Boolean>,
|
||||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||||
useOnion: (OnionHosts) -> Unit,
|
useOnion: (OnionHosts) -> Unit,
|
||||||
) {
|
) {
|
||||||
val values = remember {
|
val values = remember {
|
||||||
@@ -353,29 +374,43 @@ private fun UseOnionHosts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val onSelected = showModal {
|
val onSelected = {
|
||||||
Column(
|
showModal {
|
||||||
Modifier.fillMaxWidth(),
|
Column(
|
||||||
) {
|
Modifier.fillMaxWidth(),
|
||||||
AppBarTitle(stringResource(MR.strings.network_use_onion_hosts))
|
) {
|
||||||
SectionViewSelectable(null, onionHosts, values, useOnion)
|
AppBarTitle(stringResource(MR.strings.network_use_onion_hosts))
|
||||||
|
SectionViewSelectable(null, onionHosts, values, useOnion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SectionItemWithValue(
|
if (enabled.value) {
|
||||||
generalGetString(MR.strings.network_use_onion_hosts),
|
SectionItemWithValue(
|
||||||
onionHosts,
|
generalGetString(MR.strings.network_use_onion_hosts),
|
||||||
values,
|
onionHosts,
|
||||||
icon = painterResource(MR.images.ic_security),
|
values,
|
||||||
enabled = enabled,
|
icon = painterResource(MR.images.ic_security),
|
||||||
onSelected = onSelected
|
enabled = enabled,
|
||||||
)
|
onSelected = onSelected
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before
|
||||||
|
SectionItemWithValue(
|
||||||
|
generalGetString(MR.strings.network_use_onion_hosts),
|
||||||
|
remember { mutableStateOf(OnionHosts.NEVER) },
|
||||||
|
listOf(ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_no_desc)))),
|
||||||
|
icon = painterResource(MR.images.ic_security),
|
||||||
|
enabled = enabled,
|
||||||
|
onSelected = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SessionModePicker(
|
private fun SessionModePicker(
|
||||||
sessionMode: MutableState<TransportSessionMode>,
|
sessionMode: MutableState<TransportSessionMode>,
|
||||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -393,12 +428,14 @@ private fun SessionModePicker(
|
|||||||
sessionMode,
|
sessionMode,
|
||||||
values,
|
values,
|
||||||
icon = painterResource(MR.images.ic_safety_divider),
|
icon = painterResource(MR.images.ic_safety_divider),
|
||||||
onSelected = showModal {
|
onSelected = {
|
||||||
Column(
|
showModal {
|
||||||
Modifier.fillMaxWidth(),
|
Column(
|
||||||
) {
|
Modifier.fillMaxWidth(),
|
||||||
AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation))
|
) {
|
||||||
SectionViewSelectable(null, sessionMode, values, updateSessionMode)
|
AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation))
|
||||||
|
SectionViewSelectable(null, sessionMode, values, updateSessionMode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -455,9 +492,6 @@ fun PreviewNetworkAndServersLayout() {
|
|||||||
developerTools = true,
|
developerTools = true,
|
||||||
networkUseSocksProxy = remember { mutableStateOf(true) },
|
networkUseSocksProxy = remember { mutableStateOf(true) },
|
||||||
proxyPort = remember { mutableStateOf(9050) },
|
proxyPort = remember { mutableStateOf(9050) },
|
||||||
showModal = { {} },
|
|
||||||
showSettingsModal = { {} },
|
|
||||||
showCustomModal = { {} },
|
|
||||||
toggleSocksProxy = {},
|
toggleSocksProxy = {},
|
||||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||||
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import chat.simplex.common.ui.theme.*
|
|||||||
import chat.simplex.common.views.CreateProfile
|
import chat.simplex.common.views.CreateProfile
|
||||||
import chat.simplex.common.views.database.DatabaseView
|
import chat.simplex.common.views.database.DatabaseView
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.views.migration.MigrateFromDeviceView
|
||||||
import chat.simplex.common.views.onboarding.SimpleXInfo
|
import chat.simplex.common.views.onboarding.SimpleXInfo
|
||||||
import chat.simplex.common.views.onboarding.WhatsNewView
|
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||||
import chat.simplex.common.views.remote.ConnectDesktopView
|
import chat.simplex.common.views.remote.ConnectDesktopView
|
||||||
@@ -135,12 +136,13 @@ fun SettingsLayout(
|
|||||||
} else {
|
} else {
|
||||||
SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true)
|
||||||
}
|
}
|
||||||
|
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } }}, disabled = stopped, extraPadding = true)
|
||||||
}
|
}
|
||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
|
|
||||||
SectionView(stringResource(MR.strings.settings_section_title_settings)) {
|
SectionView(stringResource(MR.strings.settings_section_title_settings)) {
|
||||||
SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped, extraPadding = true)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal, showCustomModal) }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped, extraPadding = true)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true)
|
||||||
@@ -366,7 +368,7 @@ fun SettingsActionItem(icon: Painter, text: String, click: (() -> Unit)? = null,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (() -> Unit)? = null, iconColor: Color = MaterialTheme.colors.secondary, disabled: Boolean = false, extraPadding: Boolean = false, content: @Composable RowScope.() -> Unit) {
|
fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (() -> Unit)? = null, iconColor: Color = MaterialTheme.colors.secondary, textColor: Color = MaterialTheme.colors.onBackground, disabled: Boolean = false, extraPadding: Boolean = false, content: @Composable RowScope.() -> Unit) {
|
||||||
SectionItemView(
|
SectionItemView(
|
||||||
click,
|
click,
|
||||||
extraPadding = extraPadding,
|
extraPadding = extraPadding,
|
||||||
@@ -382,7 +384,7 @@ fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (
|
|||||||
}
|
}
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
val padding = with(LocalDensity.current) { 6.sp.toDp() }
|
val padding = with(LocalDensity.current) { 6.sp.toDp() }
|
||||||
Text(text, Modifier.weight(1f).padding(vertical = padding), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground)
|
Text(text, Modifier.weight(1f).padding(vertical = padding), color = if (disabled) MaterialTheme.colors.secondary else textColor)
|
||||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||||
Row(Modifier.widthIn(max = (windowWidth() - DEFAULT_PADDING * 2) / 2)) {
|
Row(Modifier.widthIn(max = (windowWidth() - DEFAULT_PADDING * 2) / 2)) {
|
||||||
content()
|
content()
|
||||||
|
|||||||
@@ -54,6 +54,11 @@
|
|||||||
<string name="decryption_error">Decryption error</string>
|
<string name="decryption_error">Decryption error</string>
|
||||||
<string name="encryption_renegotiation_error">Encryption re-negotiation error</string>
|
<string name="encryption_renegotiation_error">Encryption re-negotiation error</string>
|
||||||
|
|
||||||
|
<string name="e2ee_info_no_pq"><![CDATA[Messages, files and calls are protected by <b>end-to-end encryption</b> with perfect forward secrecy, repudiation and break-in recovery.]]></string>
|
||||||
|
<string name="e2ee_info_pq"><![CDATA[Messages, files and calls are protected by <b>quantum resistant e2e encryption</b> with perfect forward secrecy, repudiation and break-in recovery.]]></string>
|
||||||
|
<string name="e2ee_info_no_pq_short">This chat is protected by end-to-end encryption.</string>
|
||||||
|
<string name="e2ee_info_pq_short">This chat is protected by quantum resistant end-to-end encryption.</string>
|
||||||
|
|
||||||
<!-- NoteFolder - ChatModel.kt -->
|
<!-- NoteFolder - ChatModel.kt -->
|
||||||
<string name="note_folder_local_display_name">Private notes</string>
|
<string name="note_folder_local_display_name">Private notes</string>
|
||||||
|
|
||||||
@@ -240,6 +245,7 @@
|
|||||||
<string name="auth_stop_chat">Stop chat</string>
|
<string name="auth_stop_chat">Stop chat</string>
|
||||||
<string name="auth_open_chat_console">Open chat console</string>
|
<string name="auth_open_chat_console">Open chat console</string>
|
||||||
<string name="auth_open_chat_profiles">Open chat profiles</string>
|
<string name="auth_open_chat_profiles">Open chat profiles</string>
|
||||||
|
<string name="auth_open_migration_to_another_device">Open migration screen</string>
|
||||||
<string name="lock_not_enabled">SimpleX Lock not enabled!</string>
|
<string name="lock_not_enabled">SimpleX Lock not enabled!</string>
|
||||||
<string name="you_can_turn_on_lock">You can turn on SimpleX Lock via Settings.</string>
|
<string name="you_can_turn_on_lock">You can turn on SimpleX Lock via Settings.</string>
|
||||||
|
|
||||||
@@ -818,6 +824,7 @@
|
|||||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol and code – anybody can run the servers.</string>
|
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol and code – anybody can run the servers.</string>
|
||||||
<string name="create_your_profile">Create your profile</string>
|
<string name="create_your_profile">Create your profile</string>
|
||||||
<string name="make_private_connection">Make a private connection</string>
|
<string name="make_private_connection">Make a private connection</string>
|
||||||
|
<string name="migrate_from_another_device">Migrate from another device</string>
|
||||||
<string name="how_it_works">How it works</string>
|
<string name="how_it_works">How it works</string>
|
||||||
|
|
||||||
<!-- How SimpleX Works -->
|
<!-- How SimpleX Works -->
|
||||||
@@ -1076,6 +1083,7 @@
|
|||||||
<string name="confirm_new_passphrase">Confirm new passphrase…</string>
|
<string name="confirm_new_passphrase">Confirm new passphrase…</string>
|
||||||
<string name="update_database_passphrase">Update database passphrase</string>
|
<string name="update_database_passphrase">Update database passphrase</string>
|
||||||
<string name="set_database_passphrase">Set database passphrase</string>
|
<string name="set_database_passphrase">Set database passphrase</string>
|
||||||
|
<string name="set_passphrase">Set passphrase</string>
|
||||||
<string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string>
|
<string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string>
|
||||||
<string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string>
|
<string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string>
|
||||||
<string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string>
|
<string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string>
|
||||||
@@ -1239,6 +1247,8 @@
|
|||||||
<string name="snd_conn_event_ratchet_sync_started">agreeing encryption for %s…</string>
|
<string name="snd_conn_event_ratchet_sync_started">agreeing encryption for %s…</string>
|
||||||
<string name="snd_conn_event_ratchet_sync_agreed">encryption agreed for %s</string>
|
<string name="snd_conn_event_ratchet_sync_agreed">encryption agreed for %s</string>
|
||||||
<string name="rcv_conn_event_verification_code_reset">security code changed</string>
|
<string name="rcv_conn_event_verification_code_reset">security code changed</string>
|
||||||
|
<string name="conn_event_enabled_pq">quantum resistant e2e encryption</string>
|
||||||
|
<string name="conn_event_disabled_pq">standard end-to-end encryption</string>
|
||||||
|
|
||||||
<!-- GroupMemberRole -->
|
<!-- GroupMemberRole -->
|
||||||
<string name="group_member_role_observer">observer</string>
|
<string name="group_member_role_observer">observer</string>
|
||||||
@@ -1835,4 +1845,67 @@
|
|||||||
<string name="agent_internal_error_title">Internal error</string>
|
<string name="agent_internal_error_title">Internal error</string>
|
||||||
<string name="agent_internal_error_desc">Please report it to the developers: \n%s</string>
|
<string name="agent_internal_error_desc">Please report it to the developers: \n%s</string>
|
||||||
<string name="restart_chat_button">Restart chat</string>
|
<string name="restart_chat_button">Restart chat</string>
|
||||||
|
|
||||||
|
<!-- MigrateToDevice.kt -->
|
||||||
|
<string name="migrate_to_device_title">Migrate here</string>
|
||||||
|
<string name="or_paste_archive_link">Or paste archive link</string>
|
||||||
|
<string name="paste_archive_link">Paste archive link</string>
|
||||||
|
<string name="invalid_file_link">Invalid link</string>
|
||||||
|
<string name="migrate_to_device_migrating">Migrating</string>
|
||||||
|
<string name="migrate_to_device_database_init">Preparing download</string>
|
||||||
|
<string name="migrate_to_device_downloading_details">Downloading link details</string>
|
||||||
|
<string name="migrate_to_device_downloading_archive">Downloading archive</string>
|
||||||
|
<string name="migrate_to_device_bytes_downloaded">%s downloaded</string>
|
||||||
|
<string name="migrate_to_device_download_failed">Download failed</string>
|
||||||
|
<string name="migrate_to_device_repeat_download">Repeat download</string>
|
||||||
|
<string name="migrate_to_device_try_again">You can give another try.</string>
|
||||||
|
<string name="migrate_to_device_importing_archive">Importing archive</string>
|
||||||
|
<string name="migrate_to_device_import_failed">Import failed</string>
|
||||||
|
<string name="migrate_to_device_repeat_import">Repeat import</string>
|
||||||
|
<string name="migrate_to_device_enter_passphrase">Enter passphrase</string>
|
||||||
|
<string name="migrate_to_device_file_delete_or_link_invalid">File was deleted or link is invalid</string>
|
||||||
|
<string name="migrate_to_device_error_downloading_archive">Error downloading the archive</string>
|
||||||
|
<string name="migrate_to_device_chat_migrated">Chat migrated!</string>
|
||||||
|
<string name="migrate_to_device_finalize_migration">Finalize migration on another device.</string>
|
||||||
|
<string name="migrate_to_device_confirm_network_settings">Confirm network settings</string>
|
||||||
|
<string name="migrate_to_device_confirm_network_settings_footer">Please confirm that network settings are correct for this device.</string>
|
||||||
|
<string name="migrate_to_device_apply_onion">Apply</string>
|
||||||
|
|
||||||
|
<!-- MigrateFromDevice.kt -->
|
||||||
|
<string name="migrate_from_device_title">Migrate device</string>
|
||||||
|
<string name="migrate_from_device_to_another_device">Migrate to another device</string>
|
||||||
|
<string name="migrate_from_device_error_saving_settings">Error saving settings</string>
|
||||||
|
<string name="migrate_from_device_exported_file_doesnt_exist">Exported file doesn\'t exist</string>
|
||||||
|
<string name="migrate_from_device_error_exporting_archive">Error exporting chat database</string>
|
||||||
|
<string name="migrate_from_device_database_init">Preparing upload</string>
|
||||||
|
<string name="migrate_from_device_error_uploading_archive">Error uploading the archive</string>
|
||||||
|
<string name="migrate_from_device_error_deleting_database">Error deleting database</string>
|
||||||
|
<string name="migrate_from_device_stopping_chat">Stopping chat</string>
|
||||||
|
<string name="migrate_from_device_chat_should_be_stopped">In order to continue, chat should be stopped.</string>
|
||||||
|
<string name="migrate_from_device_archive_and_upload">Archive and upload</string>
|
||||||
|
<string name="migrate_from_device_confirm_upload">Confirm upload</string>
|
||||||
|
<string name="migrate_from_device_all_data_will_be_uploaded">All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</string>
|
||||||
|
<string name="migrate_from_device_archiving_database">Archiving database</string>
|
||||||
|
<string name="migrate_from_device_bytes_uploaded">%s uploaded</string>
|
||||||
|
<string name="migrate_from_device_uploading_archive">Uploading archive</string>
|
||||||
|
<string name="migrate_from_device_upload_failed">Upload failed</string>
|
||||||
|
<string name="migrate_from_device_repeat_upload">Repeat upload</string>
|
||||||
|
<string name="migrate_from_device_try_again">You can give another try.</string>
|
||||||
|
<string name="migrate_from_device_creating_archive_link">Creating archive link</string>
|
||||||
|
<string name="migrate_from_device_cancel_migration">Cancel migration</string>
|
||||||
|
<string name="migrate_from_device_finalize_migration">Finalize migration</string>
|
||||||
|
<string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Choose <i>Migrate from another device</i> on the new device and scan QR code.]]></string>
|
||||||
|
<string name="migrate_from_device_or_share_this_file_link">Or securely share this file link</string>
|
||||||
|
<string name="migrate_from_device_delete_database_from_device">Delete database from this device</string>
|
||||||
|
<string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Warning: starting chat on multiple devices is not supported and will cause message delivery failures</string>
|
||||||
|
<string name="migrate_from_device_start_chat">Start chat</string>
|
||||||
|
<string name="migrate_from_device_migration_complete">Migration complete</string>
|
||||||
|
<string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[You <b>must not</b> use the same database on two devices.]]></string>
|
||||||
|
<string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Please note</b>: using the same database on two devices will break the decryption of messages from your connections, as a security protection.]]></string>
|
||||||
|
<string name="migrate_from_device_verify_database_passphrase">Verify database passphrase</string>
|
||||||
|
<string name="migrate_from_device_verify_passphrase">Verify passphrase</string>
|
||||||
|
<string name="migrate_from_device_confirm_you_remember_passphrase">Confirm that you remember database passphrase to migrate it.</string>
|
||||||
|
<string name="migrate_from_device_check_connection_and_try_again">Check your internet connection and try again</string>
|
||||||
|
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Warning</b>: the archive will be deleted.]]></string>
|
||||||
|
<string name="migrate_from_device_error_verifying_passphrase">Error verifying passphrase:</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,6 +2,7 @@ package chat.simplex.common.views.database
|
|||||||
|
|
||||||
import SectionItemView
|
import SectionItemView
|
||||||
import SectionTextFooter
|
import SectionTextFooter
|
||||||
|
import TextIconSpaced
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -22,8 +23,9 @@ actual fun SavePassphraseSetting(
|
|||||||
useKeychain: Boolean,
|
useKeychain: Boolean,
|
||||||
initialRandomDBPassphrase: Boolean,
|
initialRandomDBPassphrase: Boolean,
|
||||||
storedKey: Boolean,
|
storedKey: Boolean,
|
||||||
progressIndicator: Boolean,
|
|
||||||
minHeight: Dp,
|
minHeight: Dp,
|
||||||
|
enabled: Boolean,
|
||||||
|
smallPadding: Boolean,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
SectionItemView(minHeight = minHeight) {
|
SectionItemView(minHeight = minHeight) {
|
||||||
@@ -33,7 +35,11 @@ actual fun SavePassphraseSetting(
|
|||||||
stringResource(MR.strings.save_passphrase_in_settings),
|
stringResource(MR.strings.save_passphrase_in_settings),
|
||||||
tint = if (storedKey) WarningOrange else MaterialTheme.colors.secondary
|
tint = if (storedKey) WarningOrange else MaterialTheme.colors.secondary
|
||||||
)
|
)
|
||||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
if (smallPadding) {
|
||||||
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
|
} else {
|
||||||
|
TextIconSpaced(false)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
stringResource(MR.strings.save_passphrase_in_settings),
|
stringResource(MR.strings.save_passphrase_in_settings),
|
||||||
Modifier.padding(end = 24.dp),
|
Modifier.padding(end = 24.dp),
|
||||||
@@ -43,7 +49,7 @@ actual fun SavePassphraseSetting(
|
|||||||
DefaultSwitch(
|
DefaultSwitch(
|
||||||
checked = useKeychain,
|
checked = useKeychain,
|
||||||
onCheckedChange = onCheckedChange,
|
onCheckedChange = onCheckedChange,
|
||||||
enabled = !initialRandomDBPassphrase && !progressIndicator
|
enabled = enabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,13 +61,14 @@ actual fun DatabaseEncryptionFooter(
|
|||||||
chatDbEncrypted: Boolean?,
|
chatDbEncrypted: Boolean?,
|
||||||
storedKey: MutableState<Boolean>,
|
storedKey: MutableState<Boolean>,
|
||||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||||
|
migration: Boolean,
|
||||||
) {
|
) {
|
||||||
if (chatDbEncrypted == false) {
|
if (chatDbEncrypted == false) {
|
||||||
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
|
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
|
||||||
} else if (useKeychain.value) {
|
} else if (useKeychain.value) {
|
||||||
if (storedKey.value) {
|
if (storedKey.value) {
|
||||||
SectionTextFooter(generalGetString(MR.strings.settings_is_storing_in_clear_text))
|
SectionTextFooter(generalGetString(MR.strings.settings_is_storing_in_clear_text))
|
||||||
if (initialRandomDBPassphrase.value) {
|
if (initialRandomDBPassphrase.value && !migration) {
|
||||||
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
|
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
|
||||||
} else {
|
} else {
|
||||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||||
|
|
||||||
android.version_name=5.5.6
|
android.version_name=5.6-beta.0
|
||||||
android.version_code=187
|
android.version_code=189
|
||||||
|
|
||||||
desktop.version_name=5.5.6
|
desktop.version_name=5.6-beta.0
|
||||||
desktop.version_code=32
|
desktop.version_code=33
|
||||||
|
|
||||||
kotlin.version=1.8.20
|
kotlin.version=1.8.20
|
||||||
gradle.plugin.version=7.4.2
|
gradle.plugin.version=7.4.2
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ mkChatOpts BroadcastBotOpts {coreOptions} =
|
|||||||
chatCmdLog = CCLNone,
|
chatCmdLog = CCLNone,
|
||||||
chatServerPort = Nothing,
|
chatServerPort = Nothing,
|
||||||
optFilesFolder = Nothing,
|
optFilesFolder = Nothing,
|
||||||
|
optTempDirectory = Nothing,
|
||||||
showReactions = False,
|
showReactions = False,
|
||||||
allowInstantFiles = True,
|
allowInstantFiles = True,
|
||||||
autoAcceptFileSize = 0,
|
autoAcceptFileSize = 0,
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ mkChatOpts DirectoryOpts {coreOptions} =
|
|||||||
chatCmdLog = CCLNone,
|
chatCmdLog = CCLNone,
|
||||||
chatServerPort = Nothing,
|
chatServerPort = Nothing,
|
||||||
optFilesFolder = Nothing,
|
optFilesFolder = Nothing,
|
||||||
|
optTempDirectory = Nothing,
|
||||||
showReactions = False,
|
showReactions = False,
|
||||||
allowInstantFiles = True,
|
allowInstantFiles = True,
|
||||||
autoAcceptFileSize = 0,
|
autoAcceptFileSize = 0,
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
---
|
||||||
|
layout: layouts/article.html
|
||||||
|
title: "SimpleX Chat v5.6 (beta): adding quantum resistance to Signal double ratchet algorithm"
|
||||||
|
date: 2024-03-14
|
||||||
|
previewBody: blog_previews/20240314.html
|
||||||
|
image: images/20240314-kem.jpg
|
||||||
|
imageWide: true
|
||||||
|
permalink: "/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.html"
|
||||||
|
---
|
||||||
|
|
||||||
|
# SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm
|
||||||
|
|
||||||
|
This is a major upgrade for SimpleX messaging protocols, we are really proud to present the results of the hard work of our whole team on the [Pi day](https://en.wikipedia.org/wiki/Pi_Day).
|
||||||
|
|
||||||
|
This post also covers various aspects of end-to-end encryption, compares different messengers, and explains why and how quantum-resistant encryption is added to SimpleX Chat:
|
||||||
|
|
||||||
|
- [Why do we need end-to-end encryption?](#why-do-we-need-end-to-end-encryption)
|
||||||
|
- [Why encryption is even allowed?](#why-encryption-is-even-allowed)
|
||||||
|
- [End-to-end encryption security: attacks and defense.](#end-to-end-encryption-security-attacks-and-defense)
|
||||||
|
- Compromised message size - mitigated by padding messages to a fixed block size.
|
||||||
|
- Compromised confidentiality - mitigated by repudiation (deniability).
|
||||||
|
- Compromised message keys - mitigated by forward secrecy.
|
||||||
|
- Compromised long-term or session - mitigated by break-in recovery.
|
||||||
|
- Man-in-the-middle attack - mitigated by two-factor key exchange.
|
||||||
|
- "Record now, decrypt later" attacks - mitigated by post-quantum cryptography.
|
||||||
|
- [How secure is encryption in different messengers?](#how-secure-is-end-to-end-encryption-in-different-messengers)
|
||||||
|
- [Adding quantum resistance to Signal double ratchet algorithm.](#adding-quantum-resistance-to-signal-double-ratchet-algorithm)
|
||||||
|
- [When can you start using quantum resistant chats?](#when-can-you-start-using-quantum-resistant-chats)
|
||||||
|
- [Next for post-quantum crypto - all direct chats, small groups and security audit.](#next-for-post-quantum-crypto---all-direct-chats-small-groups-and-security-audit)
|
||||||
|
|
||||||
|
## Why do we need end-to-end encryption?
|
||||||
|
|
||||||
|
The objective of end-to-end encryption is to make any potential attackers, such as traffic observers or communication providers who pass the messages between senders and recipients, unable to recover *any* message content or meaningful information about the messages, even if these attackers possess very advanced computing and mathematical capabilities.
|
||||||
|
|
||||||
|
While human eyes are unable to see any difference between simply scrambled and encrypted messages, the difference between unreadable scrambling and unbreakable encryption can be as huge as just a few seconds to unscramble a message on an average laptop and more time than the Universe existed required to break the encryption on the most powerful computer in the world.
|
||||||
|
|
||||||
|
Achieving the latter requires a lot of mathematical precision in both the cryptographic algorithms and in how they are used, and effectively makes encrypted messages indistinguishable from random noise, without any discoverable patterns or statistical irregularities that a computer could use to break the message encryption any faster than it it would take to try every possible combination of bits in the key.
|
||||||
|
|
||||||
|
End-to-end encryption is an important component of our individual and business security, privacy and sovereignty. Having our private communications protected from any observers is both the natural condition and our inalienable human right.
|
||||||
|
|
||||||
|
It's very sad to see the same people who keep their financial affairs private to protect from financial crimes, lock their doors to protect from thieves, and curtain their windows to protect from the occasional prying eyes, when it comes to protecting their personal lives from the data criminals say "we don't care about privacy, we have nothing to hide". Everybody's safety depends on keeping their affairs and relations private, not visible to a vast and ruthless data gathering machines, that abuse our data for commercial gain, without any regard to our interests or even [the safety of our families and children](https://nmdoj.gov/press-release/attorney-general-raul-torrez-files-lawsuit-against-meta-platforms-and-mark-zuckerberg-to-protect-children-from-sexual-abuse-and-human-trafficking/).
|
||||||
|
|
||||||
|
## Why encryption is even allowed?
|
||||||
|
|
||||||
|
<img src="./images/20240314-djb.jpg" class="float-to-right">
|
||||||
|
|
||||||
|
If encryption is such a powerful tool to protect our lives, it also can be used to conceal crimes, so why the governments don't consider it similar to arms, and don't heavily regulate its use?
|
||||||
|
|
||||||
|
Prior to 1996 the cryptography was considered munition, and its export from the United States was controlled under this category, [alongside flamethrowers and B-1 bombers](https://cr.yp.to/export/1995/0303-eff.txt). When [Daniel J. Bernstein](https://en.wikipedia.org/wiki/Daniel_J._Bernstein) (DJB), then a student of Mathematics at University of California, Berkeley, wanted to publish the paper and the source code of his Snuffle encryption system, the Office of Defense Trade Controls of the Department of State (DOS) after more than a year of correspondence requested that DJB registers as the arms dealer.
|
||||||
|
|
||||||
|
In 1995 DJB represented by the Electronic Frontier Foundation brought a case against the DOS to overturn cryptography restrictions. The ruling in the case declared that the export control over cryptographic software and related technical data constitute [an impermissible infringement on speech in violation of the First Amendment](https://cr.yp.to/export/1996/1206-order.txt). This decision resulted in regulatory changes, reducing controls on encryption exports, particularly for open-source algorithms. The case continued until 2003, when it was put on hold after the commitment from the US government not to enforce any remaining regulations.
|
||||||
|
|
||||||
|
This case is very important for the whole industry, as to this day we can freely create and use open-source cryptography without export control restrictions. It also shows the importance of engaging with the system and challenging its views in an open dialogue, rather than either blindly complying or violating regulations.
|
||||||
|
|
||||||
|
DJB role for cryptography and open-source goes beyond this case – many cryptographic algorithms that are considered to be the most advanced, and many of which we use in SimpleX Chat, were designed and developed by him:
|
||||||
|
|
||||||
|
- Ed25519 cryptographic signature algorithm we use to authorize commands to the servers.
|
||||||
|
- NaCL library with cryptobox and secretbox constructions that combine X25519 Diffie-Hellman key agreement with Salsa20 encryption and Poly1305 authentication. We use cryptobox to encrypt messages in two of three encryption layers and secretbox to encrypt files.
|
||||||
|
- Streamlined NTRU Prime algorithm for quantum resistant key agreement that we used in the protocol for linking mobile app with desktop, and now added to Signal double ratchet algorithm, as explained below.
|
||||||
|
|
||||||
|
Without DJB's work the world would have been in a much worse place privacy- and security-wise.
|
||||||
|
|
||||||
|
Daniel, we are really grateful for the work you did and continue doing. Thank you, and congratulations on the International Mathematics Day!
|
||||||
|
|
||||||
|
## End-to-end encryption security: attacks and defense
|
||||||
|
|
||||||
|
End-to-end encryption is offered by many messaging apps and protocols, but the security of different implementations are not the same. While many users know about the importance of forward secrecy - the quality of end-to-end encryption that preserves security of the encryption of the past messages, even if the keys used to encrypt some of the messages were compromised - there are many other qualities that protect from different attacks. Below there is the overview of these attacks and the properties of end-to-end encryption schemes that mitigate these attacks.
|
||||||
|
|
||||||
|
### 1. Compromised message size - mitigated by padding messages to a fixed block size
|
||||||
|
|
||||||
|
While the content encryption is the most important, concealing the actual message size is almost as important for several reasons:
|
||||||
|
|
||||||
|
- attacker able to observe even approximate message sizes can use these sizes as an additional signal for machine learning to de-anonymise the users and to categorize the relationships between the users.
|
||||||
|
- if a messenger conceals the routing of the messages to hide the transport identities (IP addresses) of senders and recipients, message sizes can be used by traffic observers to confirm the fact of communication with a much higher degree of certainty.
|
||||||
|
|
||||||
|
The only effective mitigation to these attacks is to pad all messages to a fixed size. Using space-efficient schemes like Padme, or padding to encryption block size is ineffective for mitigating these attacks, as they still allow differentiating message sizes.
|
||||||
|
|
||||||
|
To the best of our knowledge the only messenger other than SimpleX Chat that padded all messages to a fixed packet size was [Pond](https://github.com/agl/pond) - SimpleX design as an evolution of it.
|
||||||
|
|
||||||
|
### 2. Compromised confidential messages - mitigated by repudiation (deniability)
|
||||||
|
|
||||||
|
Many users are very interested in having ability to irreversibly delete sent messages from the recipients devices. But not only would this ability violate data sovereignty of device owners, it is also completely ineffective, as the recipients could simply put the device offline or use a modified client app to ignore message deletion requests. While SimpleX Chat provides such features as [disappearing messages](./20230103-simplex-chat-v4.4-disappearing-messages.md#disappearing-messages) and the ability to [irreversibly delete sent messages](./20221206-simplex-chat-v4.3-voice-messages.md#irreversible-message-deletion) provided both parties agree to that, these are convenience features, and they cannot be considered security measures.
|
||||||
|
|
||||||
|
The solution to that is well known to cryptographers - it is the quality of the encryption algorithms called "repudiation", sometimes also called "deniability". This is the ability of the senders to plausibly deny having sent any messages, because cryptographic algorithms used to encrypt allow recipients forging these messages on their devices, so while the encryption proves authenticity of the message to the recipient, it cannot be used as a proof to any third party.
|
||||||
|
|
||||||
|
Putting it all in a simpler language - a sender can claim that the recipient forged messages on their device, and deny ever having sent them. The recipient will not be able to provide any cryptographic proof. This quality makes digital conversation having the same qualities as private off-the-record conversation - that's why the family of algorithms that provide these qualities are called off-the-record (OTR) encryption.
|
||||||
|
|
||||||
|
Repudiation is still a rather new concept - the first off-the-record algorithms were proposed in 2004 and were only offered to a wide range of users in Signal messenger. This concept is still quite badly understood by users and society, and yet to have been used as the defense in any public court cases, as legal systems evolve much slower than technology. In high profile cases repudiation can be used as an effective evidence for the defense.
|
||||||
|
|
||||||
|
Repudiation in messaging systems can be undermined by adding cryptographic signature to the protocol, and many messengers that use OTR encryption algorithms do exactly that, unfortunately. SimpleX Chat does not use signature in any part of client-client protocol, but the signature is currently used when authorizing sender's messages to the relays. v5.7 will improve deniability by enabling a different authorization scheme that will provide full-stack repudiation in all protocol layers.
|
||||||
|
|
||||||
|
### 3. Compromised message keys - mitigated by forward secrecy
|
||||||
|
|
||||||
|
The attacker who obtained or broke the keys used to encrypt individual messages, may try to use these keys to decrypt past or future messages. This attack is unlikely to succeed via message interception, and it is likely to require breaking into the device storage. But in any case, if the key was broken or obtained in some other way it's important that this key cannot be used to decrypt other messages - this is achieved by forward secrecy.
|
||||||
|
|
||||||
|
This property is well understood by the users, and most messengers that focus on privacy and security, with the exception of Session, provide forward secrecy as part of their encryption schemes design.
|
||||||
|
|
||||||
|
### 4. Compromised long-term or session - mitigated by break-in recovery
|
||||||
|
|
||||||
|
This attack is much less understood by the users, and forward secrecy does not protect from it. Arguably, it's almost impossible to compromise individual message keys without compromising long-term or session keys. So the ability of the encryption to recover from break-in (attacker making a copy of the device data without retaining the ongoing access) is both very and pragmatic - break-in attacks are simpler to execute on mobile devices during short-term device access than long-term ongoing compromise.
|
||||||
|
|
||||||
|
Out of all encryption algorithms known to us only Signal double ratchet algorithm provides the ability to encryption security after break-ins. This recovery happens automatically and transparently to the users, without them doing anything special even knowing about break-in, by simply sending messages. Every time one of the communication parties replies to another party message, new random keys are generated and previously stolen keys become useless.
|
||||||
|
|
||||||
|
Signal double ratchet algorithm is used in Signal, Cwtch and SimpleX Chat. This is why you cannot use SimpleX Chat profile on more than one device at the same time - the encryption scheme rotates the long term keys, randomly, and keys on another device become useless, as they would become useless for the attacker who stole them. Security always has some costs to the convenience.
|
||||||
|
|
||||||
|
### 5. Man-in-the-middle attack - mitigated by two-factor key exchange
|
||||||
|
|
||||||
|
Many people incorrectly believe that security of end-to-end encryption cannot be broken by communication provider. But end-to-end encryption is as secure as key exchange. While any intermediary passing the keys between senders and recipients cannot recover the private keys from the public keys, they can simply replace the passed public keys with their own and then proxy all communication between the users having full access to the original messages. So instead of having an end-to-end encrypted channel, users would have two half-way encrypted channels - between users and their communication intermediary.
|
||||||
|
|
||||||
|
Pictures below illustrate how this attack works for RSA encryption.
|
||||||
|
|
||||||
|
#### 1) Alice sends the key to Bob (e.g. via p2p network or via the messaging server).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 2) Now Bob can send encrypted messages to Alice - he believes they are secure!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 3) But the key could have been intercepted and substituted by Tom (the attacker, or a service provider).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 4) Now the attacker can read the messages without Alice and Bob knowing.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The attack on Diffie-Hellman (or on quantum-resistant) key exchange, when both parties send their public keys (or public key and ciphertext), requires the attacker to intercept and replace both keys, but the outcome remains the same - if all communication is passed via a single channel, as it is usually the case with communication services, then any attacker that has inside access to the service can selectively compromise some of the conversations. Two years ago I wrote the post about this [vulnerability of end-to-end encryption to MITM attacks](https://www.poberezkin.com/posts/2022-12-07-why-privacy-needs-to-be-redefined.html#e2e-encryption-is-not-bulletproof).
|
||||||
|
|
||||||
|
All known mitigations of this attack require using the secondary communication channel to ensure that the keys have not been substituted. The most secure approach is to make user's key (or key fingerprint) a part of the user's address or connection link, thus making two-factor key exchange non-optional. This approach is used in Session, Cwtch and SimpleX Chat.
|
||||||
|
|
||||||
|
A less secure approach is to provide users an optional way to compare security codes - this is what is done by Signal, Element and many other messengers. The problem with this post-key-exchange verification is that it is optional, and is usually skipped by the majority of the users. Also, this security code can change because the user changed the device, or as a result of the attack via the service provider. When you see in the client app the notification that the security code changed, it's pointless to ask in the same messenger whether the device was changed, as if it were an attack, the attacker would simply confirm it. Instead, the security code needs to be re-validated again via another channel. A good security practice for the users would be to warn their communication partners about the intention to switch the device in advance, before the security code is changed.
|
||||||
|
|
||||||
|
### 6. "Record now, decrypt later" attacks - mitigated by post-quantum cryptography.
|
||||||
|
|
||||||
|
This is the idea based on the assumption that commercially viable quantum computers will become available during the next 10 years, and then they can use time-efficient [Shor's algorithm](https://en.wikipedia.org/wiki/Shor%27s_algorithm) developed in 1994 to break asymmetric encryption with quantum computer (symmetric encryption is not vulnerable to this algorithm).
|
||||||
|
|
||||||
|
Post-quantum cryptography, or encryption algorithms that are resistant to quantum computers, has been the area of ongoing research for several decades, and there are some algorithms that _might_ protect from quantum computers. It's important to account for these limitations:
|
||||||
|
|
||||||
|
- _none of the post-quantum algorithms are proven to be secure_ against quantum or conventional computers. They are usually referred to as "believed to be secure" by the researchers and security experts. There is continuous research to break post-quantum algorithms, and to prove their security, and many of these algorithms are broken every year, often by conventional computers.
|
||||||
|
- because of the lack of proofs or guarantees that post-quantum cryptography delivers on its promise, these algorithms can only be used in hybrid encryption schemes to augment conventional cryptography, and never to replace it, contrary to some expert recommendations, as DJB explains in this [blog post](https://blog.cr.yp.to/20240102-hybrid.html).
|
||||||
|
- they are much more computationally expensive and less space efficient, and the encryption schemes have to balance their usability and security.
|
||||||
|
- many of post-quantum algorithms have known patent claims, so any system deploying them accepts the risks of patent litigation.
|
||||||
|
- the silver lining to these limitations is that the risk of appearance of commercially viable quantum computers in the next decade may be exaggerated.
|
||||||
|
|
||||||
|
So, to put it bluntly and provocatively, post-quantum cryptography can be compared with a remedy against the illness that nobody has, without any guarantee that it will work. The closest analogy in the history of medicine is _snake oil_.
|
||||||
|
|
||||||
|
<img src="./images/20240314-datacenter.jpg" width="400" class="float-to-right">
|
||||||
|
|
||||||
|
Does it mean that post-quantum cryptography is useless and should be ignored? Absolutely not. The risks of "record now, decrypt later" attacks are real, particularly for high profile targets, including millions of people - journalists, whistle-blowers, freedom-fighters in oppressive regimes, and even some ordinary people who may become targets of information crimes. Large scale collection of encrypted communication data is ongoing, and this data may be used in the future. So having the solution that _may_ protect you (post-quantum cryptography), as long as it doesn't replace the solution that is _proven_ to protect you (conventional cryptography), is highly beneficial in any communication solution, and has already been deployed in many tools and in some messengers.
|
||||||
|
|
||||||
|
## How secure is end-to-end encryption in different messengers?
|
||||||
|
|
||||||
|
This comparison may be incorrect in some of the columns. We apologize if some of the points are incorrect, please let us know about any mistakes so we can amend them!
|
||||||
|
|
||||||
|
The main objective here is to establish the framework for comparing the security of end-to-end encryption schemes, and to highlight any areas for improvement, not to criticize any implementations.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<sup>1</sup> Repudiation in SimpleX Chat will include client-server protocol from v5.7 or v5.8. Currently it is implemented but not enabled yet, as its support requires releasing the relay protocol that breaks backward compatibility.
|
||||||
|
|
||||||
|
<sup>2</sup> Post-quantum cryptography is available in beta version, as opt-in only for direct conversations. See below how it will be rolled-out further.
|
||||||
|
|
||||||
|
Some columns are marked with a yellow checkmark:
|
||||||
|
- when messages are padded, but not to a fixed size.
|
||||||
|
- when repudiation does not include client-server connection. In case of Cwtch it appears that the presence of cryptographic signatures compromises repudiation (deniability), but it needs to be clarified.
|
||||||
|
- when 2-factor key exchange is optional, via security code verification.
|
||||||
|
- when post-quantum cryptography is only added to the initial key agreement, does not protect break-in recovery.
|
||||||
|
|
||||||
|
## Adding quantum resistance to Signal double ratchet algorithm
|
||||||
|
|
||||||
|
We have been exploring post-quantum cryptography since early 2022, when SimpleX Chat was first released, and we did not want to be pioneers here - cryptography is critically important to make it right.
|
||||||
|
|
||||||
|
We hoped to adopt the algorithm that will be standardized by NIST, but the standardization process turned out to be hugely disappointing, and the ML-KEM (Kyber) algorithm that was accepted as a standard was modified to remove an important hashing step (see the lines 304-314 in [the published spec](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.ipd.pdf))), that mitigates the attacks via a compromised random numbers generator, ignoring strong criticism from many expert cryptographers, including DJB (see [this discussion](https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/WFRDl8DqYQ4) and [the comments NIST received](https://csrc.nist.gov/files/pubs/fips/203/ipd/docs/fips-203-initial-public-comments-2023.pdf)). To make it even worse, the calculation of security levels of Kyber appears to have been done incorrectly, and overall, the chosen Kyber seems worse than rejected NTRU according to [the analysis by DJB](https://blog.cr.yp.to/20231003-countcorrectly.html).
|
||||||
|
|
||||||
|
We also analyzed the encryption schemes proposed in Tutanota in 2021, and another scheme adopted by Signal last year, and published the design of [quantum resistant double ratchet algorithm](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/rfcs/2023-09-30-pq-double-ratchet.md) that we believe provides better security than these schemes:
|
||||||
|
|
||||||
|
- unlike Tutanota design, it augments rather than replaces conventional cryptography, and also avoids using signatures when the new keys are agreed (ratchet steps).
|
||||||
|
- unlike other messengers that adopted or plan to adopt ML-KEM, we used Streamlined NTRU Prime algorithm (specifically, strnup761) that has no problems of ML-KEM, no known patent claims, and seems less likely to be compromised than other algorithms - it is exactly the same algorithm that is used in SSH. You can review the comparison of [the risks of various post-quantum algorithms](https://ntruprime.cr.yp.to/warnings.html).
|
||||||
|
- unlike Signal design that only added quantum resistance to the initial key exchange by replacing X3DH key agreement scheme with post-quantum [PQXDH](https://signal.org/docs/specifications/pqxdh/), but did not improve Signal algorithm itself, our design added quantum-resistant key agreements inside double algorithm, making its break-in recovery property also quantum resistant.
|
||||||
|
|
||||||
|
The we could make break-in recovery property of Signal algorithm quantum-resistant, and why, probably, Signal didn't, is because irrespective of the message size SimpleX Chat uses a fixed block size of 16kb to provide security and privacy against any traffic observers and against messaging relays. So we had an extra space to accommodate additional ~2.2kb worth of keys in each message without any additional traffic costs.
|
||||||
|
|
||||||
|
In case when the message is larger than the remaining block size, e.g. when the message contains image or link preview, or a large text, we used [zstd compression](https://en.wikipedia.org/wiki/Zstd) to provide additional space for the required keys without reducing image preview quality or creating additional traffic - our previously inefficient JSON encoding of chat messages was helpful in this case.
|
||||||
|
|
||||||
|
<image src="./images/20240314-kem.jpg" alt="Double KEM agreement" width="500" class="float-to-right">
|
||||||
|
|
||||||
|
The additional challenge in adding sntrup761 was that unlike Diffie-Hellman key exchange, which is symmetric (that is, the parties can share their public keys in any order and the shared secret can be computed from two public keys), sntrup761 is interactive key-encapsulation mechanism (KEM) that requires that one party shares its public key, and another party uses it to encapsulate (which is a fancy term for "encrypt" - that is why it has asterisks in the image) a random shared secret, and sends it back - making it somewhat similar to RSA cryptography. But this asymmetric design does not fit the symmetric operation of Signal double ratchet algorithm, where both sides need to generate random public keys and to compute new shared secrets every time messaging direction changes for them. So to achieve that symmetry we had to use two KEM key agreements running in parallel, in a lock-step fashion, as shown on the diagram. In this case both parties generate random public keys and also use the public key of another party to encapsulate the random shared secret. Effectively, this design adds a double quantum-resistant key agreement to double ratchet algorithm steps that provide break-in recovery.
|
||||||
|
|
||||||
|
## When can you start using quantum resistant chats?
|
||||||
|
|
||||||
|
<img src="./images/20240314-pq1.png" width="288"> <img src="./images/20240314-pq2.png" width="288"> <img src="./images/20240314-pq3.png" width="288">
|
||||||
|
|
||||||
|
Quantum resistant double ratchet algorithm is already available in v5.6 (beta) of SimpleX Chat as an optional feature that can be enabled for the new and, separately, for the existing direct conversations.
|
||||||
|
|
||||||
|
The reason it is released as opt-in is because once the conversation is upgraded to be quantum resistant, it will no longer work in the previous version of the app, and we see this ability to downgrade the app if something is not working correctly as very important for the users who use the app for critical communications.
|
||||||
|
|
||||||
|
**To enable quantum resistance for the new conversations**:
|
||||||
|
- open the app settings (tap user avatar in the top left corner).
|
||||||
|
- scroll down to _Developer tools_ and open them.
|
||||||
|
- enable _Show developer options_ toggle.
|
||||||
|
- now you will see _Post-quantum E2EE_ toggle - enable it as well.
|
||||||
|
|
||||||
|
Now all new contacts you add to the app will use quantum resistant Signal double ratchet algorithm.
|
||||||
|
|
||||||
|
Once you have enabled it for the new contacts, you can also **enable it for some of the existing contacts**:
|
||||||
|
- open the chat with the contact you want to upgrade to be quantum resistant.
|
||||||
|
- tap contact name above the chat.
|
||||||
|
- tap Allow PQ encryption.
|
||||||
|
- exchange several messages back and forth with that contact - the quantum resistant double ratchet will kick in after 3-5 messages (depending on how many messages you send in each direction), and you will see the notice in the chat once it enables.
|
||||||
|
|
||||||
|
## Next for post-quantum crypto - all direct chats, small groups and security audit
|
||||||
|
|
||||||
|
We will be making quantum resistance default for all direct chats in v5.7, and they will be upgraded for all users without any action.
|
||||||
|
|
||||||
|
We will also be adding quantum resistance to small groups up to 10-20 members. Computing cryptographic keys is much slower, in comparison, and it would be very inefficient (and completely unnecessary) for large public groups.
|
||||||
|
|
||||||
|
We have also arranged a 3rd party cryptographic review of our protocol and encryption schemes design for June/July 2024 - it will cover the additions to SimpleX protocols since [the previous security audit](./20221108-simplex-chat-v4.2-security-audit-new-website.md) in November 2022, including [XFTP protocol](./20230301-simplex-file-transfer-protocol.md) we use for file transfers and quantum resistant Signal double ratchet algorithm we just released in this beta version.
|
||||||
|
|
||||||
|
In November 2024 we will be conducting further implementation audit, with double the scope of our 2022 audit.
|
||||||
|
|
||||||
|
Security audits are very expensive, as they require employing exceptionally competent engineers and cryptographers, and it does stretch our budgets - so any donations to help us cover the costs would be hugely helpful.
|
||||||
|
|
||||||
|
That's it for now!
|
||||||
|
|
||||||
|
Thank you for helping us improve the app, and look forward to your feedback.
|
||||||
|
|
||||||
|
## SimpleX network
|
||||||
|
|
||||||
|
Some links to answer the most common questions:
|
||||||
|
|
||||||
|
[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers).
|
||||||
|
|
||||||
|
[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users).
|
||||||
|
|
||||||
|
[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations).
|
||||||
|
|
||||||
|
[How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions).
|
||||||
|
|
||||||
|
Please also see our [website](https://simplex.chat).
|
||||||
|
|
||||||
|
## Help us with donations
|
||||||
|
|
||||||
|
Huge thank you to everybody who donates to SimpleX Chat!
|
||||||
|
|
||||||
|
As I wrote, we are planning a 3rd party security audit for the protocols and cryptography design, and also for an app implementation, and it would hugely help us if some part of this $50,000+ expense is covered with donations.
|
||||||
|
|
||||||
|
We are prioritizing users privacy and security - it would be impossible without your support.
|
||||||
|
|
||||||
|
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX network based on the same principles as email and web, but much more private and secure.
|
||||||
|
|
||||||
|
Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us.
|
||||||
|
|
||||||
|
See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate.
|
||||||
|
|
||||||
|
Thank you,
|
||||||
|
|
||||||
|
Evgeny
|
||||||
|
|
||||||
|
SimpleX Chat founder
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
# Blog
|
# Blog
|
||||||
|
|
||||||
|
Mar 14, 2024 [SimpleX Chat v5.6 (beta): adding quantum resistance to Signal double ratchet algorithm](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
|
||||||
|
|
||||||
|
This is a major upgrade for SimpleX Chat messaging protocol stack, I am really proud to present this work of the whole team.
|
||||||
|
|
||||||
|
This post also covers various aspects of end-to-end encryption, compares different messengers, and explains how and why quantum-resistant encryption is added to SimpleX Chat:
|
||||||
|
|
||||||
|
- Why do we need end-to-end encryption?
|
||||||
|
- Why encryption is even allowed?
|
||||||
|
- End-to-end encryption security: attacks and defense.
|
||||||
|
- How secure is encryption in different messengers?
|
||||||
|
- Adding quantum resistance to Signal double ratchet algorithm.
|
||||||
|
- When can you start using quantum resistant chats?
|
||||||
|
- Next for post-quantum crypto: all direct chats, small groups and security audit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Jan 24, 2024 [SimpleX Chat: free infrastructure from Linode, v5.5 released](./20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
Jan 24, 2024 [SimpleX Chat: free infrastructure from Linode, v5.5 released](./20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
||||||
|
|
||||||
SimpleX Chat infrastructure on Linode:
|
SimpleX Chat infrastructure on Linode:
|
||||||
|
|||||||
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 365 KiB |
|
After Width: | Height: | Size: 258 KiB |
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/simplexmq.git
|
location: https://github.com/simplex-chat/simplexmq.git
|
||||||
tag: 11288866f90bafb0892701b0e0679eddb030b5df
|
tag: ca68eca86ef92ae266a4005ab1ad57b589f83933
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ _Please note_: when you change the servers in the app configuration, it only aff
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
0. First, install `smp-server`:
|
1. First, install `smp-server`:
|
||||||
|
|
||||||
- Manual deployment (see below)
|
- Manual deployment (see below)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ _Please note_: when you change the servers in the app configuration, it only aff
|
|||||||
|
|
||||||
Manual installation requires some preliminary actions:
|
Manual installation requires some preliminary actions:
|
||||||
|
|
||||||
0. Install binary:
|
1. Install binary:
|
||||||
|
|
||||||
- Using offical binaries:
|
- Using offical binaries:
|
||||||
|
|
||||||
@@ -40,20 +40,20 @@ Manual installation requires some preliminary actions:
|
|||||||
|
|
||||||
Please refer to [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution)
|
Please refer to [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution)
|
||||||
|
|
||||||
1. Create user and group for `smp-server`:
|
2. Create user and group for `smp-server`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo useradd -m smp
|
sudo useradd -m smp
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create necessary directories and assign permissions:
|
3. Create necessary directories and assign permissions:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo mkdir -p /var/opt/simplex /etc/opt/simplex
|
sudo mkdir -p /var/opt/simplex /etc/opt/simplex
|
||||||
sudo chown smp:smp /var/opt/simplex /etc/opt/simplex
|
sudo chown smp:smp /var/opt/simplex /etc/opt/simplex
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Allow `smp-server` port in firewall:
|
4. Allow `smp-server` port in firewall:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# For Ubuntu
|
# For Ubuntu
|
||||||
@@ -63,7 +63,7 @@ Manual installation requires some preliminary actions:
|
|||||||
sudo firewall-cmd --reload
|
sudo firewall-cmd --reload
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/smp-server.service` file with the following content:
|
5. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/smp-server.service` file with the following content:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
[Unit]
|
[Unit]
|
||||||
@@ -398,20 +398,20 @@ To import `csv` to `Grafana` one should:
|
|||||||
|
|
||||||
2. Allow local mode by appending following:
|
2. Allow local mode by appending following:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
[plugin.marcusolsson-csv-datasource]
|
[plugin.marcusolsson-csv-datasource]
|
||||||
allow_local_mode = true
|
allow_local_mode = true
|
||||||
```
|
```
|
||||||
|
|
||||||
... to `/etc/grafana/grafana.ini`
|
... to `/etc/grafana/grafana.ini`
|
||||||
|
|
||||||
3. Add a CSV data source:
|
3. Add a CSV data source:
|
||||||
|
|
||||||
- In the side menu, click the Configuration tab (cog icon)
|
- In the side menu, click the Configuration tab (cog icon)
|
||||||
- Click Add data source in the top-right corner of the Data Sources tab
|
- Click Add data source in the top-right corner of the Data Sources tab
|
||||||
- Enter "CSV" in the search box to find the CSV data source
|
- Enter "CSV" in the search box to find the CSV data source
|
||||||
- Click the search result that says "CSV"
|
- Click the search result that says "CSV"
|
||||||
- In URL, enter a file that points to CSV content
|
- In URL, enter a file that points to CSV content
|
||||||
|
|
||||||
4. You're done! You should be able to create your own dashboard with statistics.
|
4. You're done! You should be able to create your own dashboard with statistics.
|
||||||
|
|
||||||
@@ -445,7 +445,7 @@ To update your smp-server to latest version, choose your installation method and
|
|||||||
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
|
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
|
||||||
1. Stop and remove the container:
|
1. Stop and remove the container:
|
||||||
```sh
|
```sh
|
||||||
docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/smp-server --format="{{.ID}}"))
|
docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/smp-server --format="\{\{.ID\}\}"))
|
||||||
```
|
```
|
||||||
2. Pull latest image:
|
2. Pull latest image:
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ To update your XFTP server to latest version, choose your installation method an
|
|||||||
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
|
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
|
||||||
1. Stop and remove the container:
|
1. Stop and remove the container:
|
||||||
```sh
|
```sh
|
||||||
docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/xftp-server --format="{{.ID}}"))
|
docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/xftp-server --format="\{\{.ID\}\}"))
|
||||||
```
|
```
|
||||||
2. Pull latest image:
|
2. Pull latest image:
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 5.5.6.0
|
version: 5.6.0.2
|
||||||
#synopsis:
|
#synopsis:
|
||||||
#description:
|
#description:
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."00ae2cb6e134e3cd7c8089e30f95a9430d3c4e3d" = "1dvghlsrf0dw8g279gnb4m2s7jrj9bwdibcq61hkkb9h5975f93d";
|
"https://github.com/simplex-chat/simplexmq.git"."ca68eca86ef92ae266a4005ab1ad57b589f83933" = "10p1bn42hbmisdjk272q6jshrcx1vq1072r50n80hj6n6z1a0szf";
|
||||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
|||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
|
|
||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 5.5.6.0
|
version: 5.6.0.2
|
||||||
category: Web, System, Services, Cryptography
|
category: Web, System, Services, Cryptography
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
author: simplex.chat
|
author: simplex.chat
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ import Data.Time (NominalDiffTime, UTCTime)
|
|||||||
import Data.Time.Clock.System (systemToUTCTime)
|
import Data.Time.Clock.System (systemToUTCTime)
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
import Data.Word (Word16)
|
import Data.Word (Word16)
|
||||||
|
import Database.SQLite.Simple (SQLError)
|
||||||
|
import qualified Database.SQLite.Simple as SQL
|
||||||
import Language.Haskell.TH (Exp, Q, runIO)
|
import Language.Haskell.TH (Exp, Q, runIO)
|
||||||
import Numeric.Natural
|
import Numeric.Natural
|
||||||
import qualified Paths_simplex_chat as SC
|
import qualified Paths_simplex_chat as SC
|
||||||
@@ -80,7 +82,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), Cor
|
|||||||
import Simplex.Messaging.TMap (TMap)
|
import Simplex.Messaging.TMap (TMap)
|
||||||
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
|
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
|
||||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||||
import Simplex.Messaging.Util (allFinally, catchAllErrors, liftEitherError, tryAllErrors, (<$$>))
|
import Simplex.Messaging.Util (allFinally, catchAllErrors, liftIOEither, tryAllErrors, (<$$>))
|
||||||
import Simplex.RemoteControl.Client
|
import Simplex.RemoteControl.Client
|
||||||
import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation)
|
import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation)
|
||||||
import Simplex.RemoteControl.Types
|
import Simplex.RemoteControl.Types
|
||||||
@@ -244,8 +246,9 @@ data ChatCommand
|
|||||||
| SetRemoteHostsFolder FilePath
|
| SetRemoteHostsFolder FilePath
|
||||||
| APISetEncryptLocalFiles Bool
|
| APISetEncryptLocalFiles Bool
|
||||||
| SetContactMergeEnabled Bool
|
| SetContactMergeEnabled Bool
|
||||||
| APISetPQEnabled PQSupport
|
| APISetPQEncryption PQSupport
|
||||||
| APIAllowContactPQ ContactId
|
| APISetContactPQ ContactId PQEncryption
|
||||||
|
| SetContactPQ ContactName PQEncryption
|
||||||
| APIExportArchive ArchiveConfig
|
| APIExportArchive ArchiveConfig
|
||||||
| ExportArchive
|
| ExportArchive
|
||||||
| APIImportArchive ArchiveConfig
|
| APIImportArchive ArchiveConfig
|
||||||
@@ -458,6 +461,7 @@ data ChatCommand
|
|||||||
| DeleteRemoteCtrl RemoteCtrlId -- Remove all local data associated with a remote controller session
|
| DeleteRemoteCtrl RemoteCtrlId -- Remove all local data associated with a remote controller session
|
||||||
| APIUploadStandaloneFile UserId CryptoFile
|
| APIUploadStandaloneFile UserId CryptoFile
|
||||||
| APIDownloadStandaloneFile UserId FileDescriptionURI CryptoFile
|
| APIDownloadStandaloneFile UserId FileDescriptionURI CryptoFile
|
||||||
|
| APIStandaloneFileInfo FileDescriptionURI
|
||||||
| QuitChat
|
| QuitChat
|
||||||
| ShowVersion
|
| ShowVersion
|
||||||
| DebugLocks
|
| DebugLocks
|
||||||
@@ -597,6 +601,7 @@ data ChatResponse
|
|||||||
| CRRcvFileAccepted {user :: User, chatItem :: AChatItem}
|
| CRRcvFileAccepted {user :: User, chatItem :: AChatItem}
|
||||||
| CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
|
| CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
|
||||||
| CRRcvFileDescrNotReady {user :: User, chatItem :: AChatItem}
|
| CRRcvFileDescrNotReady {user :: User, chatItem :: AChatItem}
|
||||||
|
| CRStandaloneFileInfo {fileMeta :: Maybe J.Value}
|
||||||
| CRRcvStandaloneFileCreated {user :: User, rcvFileTransfer :: RcvFileTransfer} -- returned by _download
|
| CRRcvStandaloneFileCreated {user :: User, rcvFileTransfer :: RcvFileTransfer} -- returned by _download
|
||||||
| CRRcvFileStart {user :: User, chatItem :: AChatItem} -- sent by chats
|
| CRRcvFileStart {user :: User, chatItem :: AChatItem} -- sent by chats
|
||||||
| CRRcvFileProgressXFTP {user :: User, chatItem_ :: Maybe AChatItem, receivedSize :: Int64, totalSize :: Int64, rcvFileTransfer :: RcvFileTransfer}
|
| CRRcvFileProgressXFTP {user :: User, chatItem_ :: Maybe AChatItem, receivedSize :: Int64, totalSize :: Int64, rcvFileTransfer :: RcvFileTransfer}
|
||||||
@@ -616,7 +621,7 @@ data ChatResponse
|
|||||||
| CRSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
|
| CRSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
|
||||||
| CRSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]}
|
| CRSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]}
|
||||||
| CRSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta}
|
| CRSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta}
|
||||||
| CRSndFileError {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta}
|
| CRSndFileError {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text}
|
||||||
| CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile, updateSummary :: UserProfileUpdateSummary}
|
| CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile, updateSummary :: UserProfileUpdateSummary}
|
||||||
| CRUserProfileImage {user :: User, profile :: Profile}
|
| CRUserProfileImage {user :: User, profile :: Profile}
|
||||||
| CRContactAliasUpdated {user :: User, toContact :: Contact}
|
| CRContactAliasUpdated {user :: User, toContact :: Contact}
|
||||||
@@ -700,7 +705,7 @@ data ChatResponse
|
|||||||
| CRRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text}
|
| CRRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text}
|
||||||
| CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo}
|
| CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo}
|
||||||
| CRRemoteCtrlStopped {rcsState :: RemoteCtrlSessionState, rcStopReason :: RemoteCtrlStopReason}
|
| CRRemoteCtrlStopped {rcsState :: RemoteCtrlSessionState, rcStopReason :: RemoteCtrlStopReason}
|
||||||
| CRContactPQAllowed {user :: User, contact :: Contact}
|
| CRContactPQAllowed {user :: User, contact :: Contact, pqEncryption :: PQEncryption}
|
||||||
| CRContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption}
|
| CRContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption}
|
||||||
| CRSQLResult {rows :: [Text]}
|
| CRSQLResult {rows :: [Text]}
|
||||||
| CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]}
|
| CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]}
|
||||||
@@ -1287,36 +1292,23 @@ withStore' :: ChatMonad m => (DB.Connection -> IO a) -> m a
|
|||||||
withStore' action = withStore $ liftIO . action
|
withStore' action = withStore $ liftIO . action
|
||||||
|
|
||||||
withStore :: ChatMonad m => (DB.Connection -> ExceptT StoreError IO a) -> m a
|
withStore :: ChatMonad m => (DB.Connection -> ExceptT StoreError IO a) -> m a
|
||||||
withStore = withStoreCtx Nothing
|
withStore action = do
|
||||||
|
|
||||||
withStoreCtx' :: ChatMonad m => Maybe String -> (DB.Connection -> IO a) -> m a
|
|
||||||
withStoreCtx' ctx_ action = withStoreCtx ctx_ $ liftIO . action
|
|
||||||
|
|
||||||
withStoreCtx :: ChatMonad m => Maybe String -> (DB.Connection -> ExceptT StoreError IO a) -> m a
|
|
||||||
withStoreCtx ctx_ action = do
|
|
||||||
ChatController {chatStore} <- ask
|
ChatController {chatStore} <- ask
|
||||||
liftEitherError ChatErrorStore $ case ctx_ of
|
liftIOEither $ withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors
|
||||||
Nothing -> withTransaction chatStore (runExceptT . action) `catch` handleInternal ""
|
|
||||||
-- uncomment to debug store performance
|
|
||||||
-- Just ctx -> do
|
|
||||||
-- t1 <- liftIO getCurrentTime
|
|
||||||
-- putStrLn $ "withStoreCtx start :: " <> show t1 <> " :: " <> ctx
|
|
||||||
-- r <- withTransactionCtx ctx_ chatStore (runExceptT . action) `E.catch` handleInternal (" (" <> ctx <> ")")
|
|
||||||
-- t2 <- liftIO getCurrentTime
|
|
||||||
-- putStrLn $ "withStoreCtx end :: " <> show t2 <> " :: " <> ctx <> " :: duration=" <> show (diffToMilliseconds $ diffUTCTime t2 t1)
|
|
||||||
-- pure r
|
|
||||||
Just _ -> withTransaction chatStore (runExceptT . action) `catch` handleInternal ""
|
|
||||||
where
|
|
||||||
handleInternal :: String -> SomeException -> IO (Either StoreError a)
|
|
||||||
handleInternal ctxStr e = pure . Left . SEInternalError $ show e <> ctxStr
|
|
||||||
|
|
||||||
withStoreBatch :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO (Either ChatError a))) -> m (t (Either ChatError a))
|
withStoreBatch :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO (Either ChatError a))) -> m (t (Either ChatError a))
|
||||||
withStoreBatch actions = do
|
withStoreBatch actions = do
|
||||||
ChatController {chatStore} <- ask
|
ChatController {chatStore} <- ask
|
||||||
liftIO $ withTransaction chatStore $ mapM (`E.catch` handleInternal) . actions
|
liftIO $ withTransaction chatStore $ mapM (`E.catches` handleDBErrors) . actions
|
||||||
where
|
|
||||||
handleInternal :: E.SomeException -> IO (Either ChatError a)
|
handleDBErrors :: [E.Handler IO (Either ChatError a)]
|
||||||
handleInternal = pure . Left . ChatError . CEInternalError . show
|
handleDBErrors =
|
||||||
|
[ E.Handler $ \(e :: SQLError) ->
|
||||||
|
let se = SQL.sqlError e
|
||||||
|
busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked
|
||||||
|
in pure . Left . ChatErrorStore $ if busy then SEDBBusyError $ show se else SEDBException $ show e,
|
||||||
|
E.Handler $ \(E.SomeException e) -> pure . Left . ChatErrorStore . SEDBException $ show e
|
||||||
|
]
|
||||||
|
|
||||||
withStoreBatch' :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO a)) -> m (t (Either ChatError a))
|
withStoreBatch' :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO a)) -> m (t (Either ChatError a))
|
||||||
withStoreBatch' actions = withStoreBatch $ fmap (fmap Right) . actions
|
withStoreBatch' actions = withStoreBatch $ fmap (fmap Right) . actions
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ contactsHelpInfo =
|
|||||||
indent <> highlight "/verify @<name> " <> " - clear security code verification",
|
indent <> highlight "/verify @<name> " <> " - clear security code verification",
|
||||||
indent <> highlight "/info @<name> " <> " - info about contact connection",
|
indent <> highlight "/info @<name> " <> " - info about contact connection",
|
||||||
indent <> highlight "/switch @<name> " <> " - switch receiving messages to another SMP relay",
|
indent <> highlight "/switch @<name> " <> " - switch receiving messages to another SMP relay",
|
||||||
|
indent <> highlight "/pq @<name> on/off " <> " - [BETA] toggle quantum resistant / standard e2e encryption for a contact",
|
||||||
|
indent <> " " <> " (both have to enable for quantum resistance)",
|
||||||
"",
|
"",
|
||||||
green "Contact chat preferences:",
|
green "Contact chat preferences:",
|
||||||
indent <> highlight "/set voice @<name> yes/no/always " <> " - allow/prohibit voice messages with the contact",
|
indent <> highlight "/set voice @<name> yes/no/always " <> " - allow/prohibit voice messages with the contact",
|
||||||
@@ -320,6 +322,7 @@ settingsInfo =
|
|||||||
map
|
map
|
||||||
styleMarkdown
|
styleMarkdown
|
||||||
[ green "Chat settings:",
|
[ green "Chat settings:",
|
||||||
|
indent <> highlight "/pq on/off " <> " - [BETA] toggle quantum resistant / standard e2e encryption for the new contacts",
|
||||||
indent <> highlight "/network " <> " - show / set network access options",
|
indent <> highlight "/network " <> " - show / set network access options",
|
||||||
indent <> highlight "/smp " <> " - show / set configured SMP servers",
|
indent <> highlight "/smp " <> " - show / set configured SMP servers",
|
||||||
indent <> highlight "/xftp " <> " - show / set configured XFTP servers",
|
indent <> highlight "/xftp " <> " - show / set configured XFTP servers",
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ ciContentToText = \case
|
|||||||
|
|
||||||
directE2EInfoToText :: E2EInfo -> Text
|
directE2EInfoToText :: E2EInfo -> Text
|
||||||
directE2EInfoToText E2EInfo {pqEnabled} = case pqEnabled of
|
directE2EInfoToText E2EInfo {pqEnabled} = case pqEnabled of
|
||||||
PQEncOn -> "This conversation is protected by quantum resistant end-to-end encryption. It has perfect forward secrecy, repudiation and quantum resistant break-in recovery."
|
PQEncOn -> e2eInfoPQText
|
||||||
PQEncOff -> e2eInfoNoPQText
|
PQEncOff -> e2eInfoNoPQText
|
||||||
|
|
||||||
groupE2EInfoToText :: E2EInfo -> Text
|
groupE2EInfoToText :: E2EInfo -> Text
|
||||||
@@ -280,6 +280,10 @@ e2eInfoNoPQText :: Text
|
|||||||
e2eInfoNoPQText =
|
e2eInfoNoPQText =
|
||||||
"This conversation is protected by end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery."
|
"This conversation is protected by end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery."
|
||||||
|
|
||||||
|
e2eInfoPQText :: Text
|
||||||
|
e2eInfoPQText =
|
||||||
|
"This conversation is protected by quantum resistant end-to-end encryption. It has perfect forward secrecy, repudiation and quantum resistant break-in recovery."
|
||||||
|
|
||||||
ciGroupInvitationToText :: CIGroupInvitation -> GroupMemberRole -> Text
|
ciGroupInvitationToText :: CIGroupInvitation -> GroupMemberRole -> Text
|
||||||
ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role =
|
ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role =
|
||||||
"invitation to join group " <> displayName <> optionalFullName displayName fullName <> " as " <> (decodeLatin1 . strEncode $ role)
|
"invitation to join group " <> displayName <> optionalFullName displayName fullName <> " as " <> (decodeLatin1 . strEncode $ role)
|
||||||
@@ -324,8 +328,8 @@ rcvConnEventToText = \case
|
|||||||
RCERatchetSync syncStatus -> ratchetSyncStatusToText syncStatus
|
RCERatchetSync syncStatus -> ratchetSyncStatusToText syncStatus
|
||||||
RCEVerificationCodeReset -> "security code changed"
|
RCEVerificationCodeReset -> "security code changed"
|
||||||
RCEPqEnabled pqEnc -> case pqEnc of
|
RCEPqEnabled pqEnc -> case pqEnc of
|
||||||
PQEncOn -> "post-quantum encryption enabled"
|
PQEncOn -> "quantum resistant e2e encryption"
|
||||||
PQEncOff -> "post-quantum encryption disabled"
|
PQEncOff -> "standard end-to-end encryption"
|
||||||
|
|
||||||
ratchetSyncStatusToText :: RatchetSyncState -> Text
|
ratchetSyncStatusToText :: RatchetSyncState -> Text
|
||||||
ratchetSyncStatusToText = \case
|
ratchetSyncStatusToText = \case
|
||||||
@@ -344,8 +348,8 @@ sndConnEventToText = \case
|
|||||||
SPCompleted -> "you changed address" <> forMember m
|
SPCompleted -> "you changed address" <> forMember m
|
||||||
SCERatchetSync syncStatus m -> ratchetSyncStatusToText syncStatus <> forMember m
|
SCERatchetSync syncStatus m -> ratchetSyncStatusToText syncStatus <> forMember m
|
||||||
SCEPqEnabled pqEnc -> case pqEnc of
|
SCEPqEnabled pqEnc -> case pqEnc of
|
||||||
PQEncOn -> "post-quantum encryption enabled"
|
PQEncOn -> "quantum resistant e2e encryption"
|
||||||
PQEncOff -> "post-quantum encryption disabled"
|
PQEncOff -> "standard end-to-end encryption"
|
||||||
where
|
where
|
||||||
forMember member_ =
|
forMember member_ =
|
||||||
maybe "" (\GroupMemberRef {profile = Profile {displayName}} -> " for " <> displayName) member_
|
maybe "" (\GroupMemberRef {profile = Profile {displayName}} -> " for " <> displayName) member_
|
||||||
|
|||||||
@@ -8,15 +8,23 @@ import Database.SQLite.Simple.QQ (sql)
|
|||||||
m20240228_pq :: Query
|
m20240228_pq :: Query
|
||||||
m20240228_pq =
|
m20240228_pq =
|
||||||
[sql|
|
[sql|
|
||||||
ALTER TABLE connections ADD COLUMN enable_pq INTEGER;
|
ALTER TABLE connections ADD COLUMN conn_chat_version INTEGER;
|
||||||
|
ALTER TABLE connections ADD COLUMN pq_support INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE connections ADD COLUMN pq_encryption INTEGER NOT NULL DEFAULT 0;
|
||||||
ALTER TABLE connections ADD COLUMN pq_snd_enabled INTEGER;
|
ALTER TABLE connections ADD COLUMN pq_snd_enabled INTEGER;
|
||||||
ALTER TABLE connections ADD COLUMN pq_rcv_enabled INTEGER;
|
ALTER TABLE connections ADD COLUMN pq_rcv_enabled INTEGER;
|
||||||
|
|
||||||
|
ALTER TABLE contact_requests ADD COLUMN pq_support INTEGER NOT NULL DEFAULT 0;
|
||||||
|]
|
|]
|
||||||
|
|
||||||
down_m20240228_pq :: Query
|
down_m20240228_pq :: Query
|
||||||
down_m20240228_pq =
|
down_m20240228_pq =
|
||||||
[sql|
|
[sql|
|
||||||
ALTER TABLE connections DROP COLUMN enable_pq;
|
ALTER TABLE contact_requests DROP COLUMN pq_support;
|
||||||
|
|
||||||
|
ALTER TABLE connections DROP COLUMN conn_chat_version;
|
||||||
|
ALTER TABLE connections DROP COLUMN pq_support;
|
||||||
|
ALTER TABLE connections DROP COLUMN pq_encryption;
|
||||||
ALTER TABLE connections DROP COLUMN pq_snd_enabled;
|
ALTER TABLE connections DROP COLUMN pq_snd_enabled;
|
||||||
ALTER TABLE connections DROP COLUMN pq_rcv_enabled;
|
ALTER TABLE connections DROP COLUMN pq_rcv_enabled;
|
||||||
|]
|
|]
|
||||||
|
|||||||
@@ -277,7 +277,9 @@ CREATE TABLE connections(
|
|||||||
peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
|
peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
|
||||||
to_subscribe INTEGER DEFAULT 0 NOT NULL,
|
to_subscribe INTEGER DEFAULT 0 NOT NULL,
|
||||||
contact_conn_initiated INTEGER NOT NULL DEFAULT 0,
|
contact_conn_initiated INTEGER NOT NULL DEFAULT 0,
|
||||||
enable_pq INTEGER,
|
conn_chat_version INTEGER,
|
||||||
|
pq_support INTEGER NOT NULL DEFAULT 0,
|
||||||
|
pq_encryption INTEGER NOT NULL DEFAULT 0,
|
||||||
pq_snd_enabled INTEGER,
|
pq_snd_enabled INTEGER,
|
||||||
pq_rcv_enabled INTEGER,
|
pq_rcv_enabled INTEGER,
|
||||||
FOREIGN KEY(snd_file_id, connection_id)
|
FOREIGN KEY(snd_file_id, connection_id)
|
||||||
@@ -315,6 +317,7 @@ CREATE TABLE contact_requests(
|
|||||||
xcontact_id BLOB,
|
xcontact_id BLOB,
|
||||||
peer_chat_min_version INTEGER NOT NULL DEFAULT 1,
|
peer_chat_min_version INTEGER NOT NULL DEFAULT 1,
|
||||||
peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
|
peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
|
||||||
|
pq_support INTEGER NOT NULL DEFAULT 0,
|
||||||
FOREIGN KEY(user_id, local_display_name)
|
FOREIGN KEY(user_id, local_display_name)
|
||||||
REFERENCES display_names(user_id, local_display_name)
|
REFERENCES display_names(user_id, local_display_name)
|
||||||
ON UPDATE CASCADE
|
ON UPDATE CASCADE
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ mobileChatOpts dbFilePrefix =
|
|||||||
chatCmdLog = CCLNone,
|
chatCmdLog = CCLNone,
|
||||||
chatServerPort = Nothing,
|
chatServerPort = Nothing,
|
||||||
optFilesFolder = Nothing,
|
optFilesFolder = Nothing,
|
||||||
|
optTempDirectory = Nothing,
|
||||||
showReactions = False,
|
showReactions = False,
|
||||||
allowInstantFiles = True,
|
allowInstantFiles = True,
|
||||||
autoAcceptFileSize = 0,
|
autoAcceptFileSize = 0,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ data ChatOpts = ChatOpts
|
|||||||
chatCmdLog :: ChatCmdLog,
|
chatCmdLog :: ChatCmdLog,
|
||||||
chatServerPort :: Maybe String,
|
chatServerPort :: Maybe String,
|
||||||
optFilesFolder :: Maybe FilePath,
|
optFilesFolder :: Maybe FilePath,
|
||||||
|
optTempDirectory :: Maybe FilePath,
|
||||||
showReactions :: Bool,
|
showReactions :: Bool,
|
||||||
allowInstantFiles :: Bool,
|
allowInstantFiles :: Bool,
|
||||||
autoAcceptFileSize :: Integer,
|
autoAcceptFileSize :: Integer,
|
||||||
@@ -258,6 +259,13 @@ chatOptsP appDir defaultDbFileName = do
|
|||||||
<> metavar "FOLDER"
|
<> metavar "FOLDER"
|
||||||
<> help "Folder to use for sent and received files"
|
<> help "Folder to use for sent and received files"
|
||||||
)
|
)
|
||||||
|
optTempDirectory <-
|
||||||
|
optional $
|
||||||
|
strOption
|
||||||
|
( long "temp-folder"
|
||||||
|
<> metavar "FOLDER"
|
||||||
|
<> help "Folder for temporary encrypted files (default: system temp directory)"
|
||||||
|
)
|
||||||
showReactions <-
|
showReactions <-
|
||||||
switch
|
switch
|
||||||
( long "reactions"
|
( long "reactions"
|
||||||
@@ -304,6 +312,7 @@ chatOptsP appDir defaultDbFileName = do
|
|||||||
chatCmdLog,
|
chatCmdLog,
|
||||||
chatServerPort,
|
chatServerPort,
|
||||||
optFilesFolder,
|
optFilesFolder,
|
||||||
|
optTempDirectory,
|
||||||
showReactions,
|
showReactions,
|
||||||
allowInstantFiles,
|
allowInstantFiles,
|
||||||
autoAcceptFileSize,
|
autoAcceptFileSize,
|
||||||
|
|||||||
@@ -46,15 +46,25 @@ import Database.SQLite.Simple.ToField (ToField (..))
|
|||||||
import Simplex.Chat.Call
|
import Simplex.Chat.Call
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
import Simplex.Chat.Types.Util
|
import Simplex.Chat.Types.Util
|
||||||
|
import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion)
|
||||||
import Simplex.Messaging.Compression (CompressCtx, compress, decompressBatch)
|
import Simplex.Messaging.Compression (CompressCtx, compress, decompressBatch)
|
||||||
import Simplex.Messaging.Crypto.Ratchet (PQSupport (..), pattern PQSupportOn, pattern PQSupportOff)
|
import Simplex.Messaging.Crypto.Ratchet (PQSupport (..), pattern PQSupportOn, pattern PQSupportOff)
|
||||||
import Simplex.Messaging.Encoding
|
import Simplex.Messaging.Encoding
|
||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON)
|
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON)
|
||||||
import Simplex.Messaging.Protocol (MsgBody)
|
import Simplex.Messaging.Protocol (MsgBody)
|
||||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$$>), (<$?>))
|
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
||||||
import Simplex.Messaging.Version hiding (version)
|
import Simplex.Messaging.Version hiding (version)
|
||||||
|
|
||||||
|
-- Chat version history:
|
||||||
|
-- 1 - support chat versions in connections (9/1/2023)
|
||||||
|
-- 2 - create contacts for group members only via x.grp.direct.inv (9/16/2023)
|
||||||
|
-- 3 - faster joining via group links without creating contact (10/30/2023)
|
||||||
|
-- 4 - group message forwarding (11/18/2023)
|
||||||
|
-- 5 - batch sending messages (12/23/2023)
|
||||||
|
-- 6 - send group welcome message after history (12/29/2023)
|
||||||
|
-- 7 - update member profiles (1/15/2024)
|
||||||
|
|
||||||
-- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig.
|
-- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig.
|
||||||
-- This indirection is needed for backward/forward compatibility testing.
|
-- This indirection is needed for backward/forward compatibility testing.
|
||||||
-- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code.
|
-- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code.
|
||||||
@@ -64,42 +74,43 @@ currentChatVersion = VersionChat 7
|
|||||||
-- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above)
|
-- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above)
|
||||||
-- TODO remove parameterization in 5.7
|
-- TODO remove parameterization in 5.7
|
||||||
supportedChatVRange :: PQSupport -> VersionRangeChat
|
supportedChatVRange :: PQSupport -> VersionRangeChat
|
||||||
supportedChatVRange pq = mkVersionRange (VersionChat 1) $ case pq of
|
supportedChatVRange pq = mkVersionRange initialChatVersion $ case pq of
|
||||||
PQSupportOn -> compressedBatchingVersion
|
PQSupportOn -> pqEncryptionCompressionVersion
|
||||||
PQSupportOff -> currentChatVersion
|
PQSupportOff -> currentChatVersion
|
||||||
{-# INLINE supportedChatVRange #-}
|
{-# INLINE supportedChatVRange #-}
|
||||||
|
|
||||||
-- version range that supports skipping establishing direct connections in a group
|
-- version range that supports skipping establishing direct connections in a group and establishing direct connection via x.grp.direct.inv
|
||||||
groupNoDirectVRange :: VersionRangeChat
|
groupDirectInvVersion :: VersionChat
|
||||||
groupNoDirectVRange = mkVersionRange (VersionChat 2) currentChatVersion
|
groupDirectInvVersion = VersionChat 2
|
||||||
|
|
||||||
-- version range that supports establishing direct connection via x.grp.direct.inv with a group member
|
|
||||||
xGrpDirectInvVRange :: VersionRangeChat
|
|
||||||
xGrpDirectInvVRange = mkVersionRange (VersionChat 2) currentChatVersion
|
|
||||||
|
|
||||||
-- version range that supports joining group via group link without creating direct contact
|
-- version range that supports joining group via group link without creating direct contact
|
||||||
groupLinkNoContactVRange :: VersionRangeChat
|
groupFastLinkJoinVersion :: VersionChat
|
||||||
groupLinkNoContactVRange = mkVersionRange (VersionChat 3) currentChatVersion
|
groupFastLinkJoinVersion = VersionChat 3
|
||||||
|
|
||||||
-- version range that supports group forwarding
|
-- version range that supports group forwarding
|
||||||
groupForwardVRange :: VersionRangeChat
|
groupForwardVersion :: VersionChat
|
||||||
groupForwardVRange = mkVersionRange (VersionChat 4) currentChatVersion
|
groupForwardVersion = VersionChat 4
|
||||||
|
|
||||||
-- version range that supports batch sending in groups
|
-- version range that supports batch sending in groups
|
||||||
batchSendVRange :: VersionRangeChat
|
batchSendVersion :: VersionChat
|
||||||
batchSendVRange = mkVersionRange (VersionChat 5) currentChatVersion
|
batchSendVersion = VersionChat 5
|
||||||
|
|
||||||
-- version range that supports sending group welcome message in group history
|
-- version range that supports sending group welcome message in group history
|
||||||
groupHistoryIncludeWelcomeVRange :: VersionRangeChat
|
groupHistoryIncludeWelcomeVersion :: VersionChat
|
||||||
groupHistoryIncludeWelcomeVRange = mkVersionRange (VersionChat 6) currentChatVersion
|
groupHistoryIncludeWelcomeVersion = VersionChat 6
|
||||||
|
|
||||||
-- version range that supports sending member profile updates to groups
|
-- version range that supports sending member profile updates to groups
|
||||||
memberProfileUpdateVRange :: VersionRangeChat
|
memberProfileUpdateVersion :: VersionChat
|
||||||
memberProfileUpdateVRange = mkVersionRange (VersionChat 7) currentChatVersion
|
memberProfileUpdateVersion = VersionChat 7
|
||||||
|
|
||||||
-- version range that supports compressing messages
|
-- version range that supports compressing messages and PQ e2e encryption
|
||||||
compressedBatchingVersion :: VersionChat
|
pqEncryptionCompressionVersion :: VersionChat
|
||||||
compressedBatchingVersion = VersionChat 8
|
pqEncryptionCompressionVersion = VersionChat 8
|
||||||
|
|
||||||
|
agentToChatVersion :: VersionSMPA -> VersionChat
|
||||||
|
agentToChatVersion v
|
||||||
|
| v < pqdrSMPAgentVersion = initialChatVersion
|
||||||
|
| otherwise = pqEncryptionCompressionVersion
|
||||||
|
|
||||||
data ConnectionEntity
|
data ConnectionEntity
|
||||||
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
|
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
|
||||||
@@ -520,29 +531,29 @@ $(JQ.deriveJSON defaultJSON ''QuotedMsg)
|
|||||||
|
|
||||||
-- this limit reserves space for metadata in forwarded messages
|
-- this limit reserves space for metadata in forwarded messages
|
||||||
-- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, round to 15610
|
-- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, round to 15610
|
||||||
maxRawMsgLength :: Int
|
maxEncodedMsgLength :: Int
|
||||||
maxRawMsgLength = 15610
|
maxEncodedMsgLength = 15610
|
||||||
|
|
||||||
maxEncodedMsgLength :: PQSupport -> Int
|
-- maxEncodedMsgLength - 2222, see e2eEncUserMsgLength in agent
|
||||||
maxEncodedMsgLength = \case
|
maxCompressedMsgLength :: Int
|
||||||
PQSupportOn -> 13410 -- reduced by 2200 (original message should be compressed)
|
maxCompressedMsgLength = 13388
|
||||||
PQSupportOff -> maxRawMsgLength
|
|
||||||
{-# INLINE maxEncodedMsgLength #-}
|
|
||||||
|
|
||||||
maxConnInfoLength :: PQSupport -> Int
|
-- maxEncodedMsgLength - delta between MSG and INFO + 100 (returned for forward overhead)
|
||||||
maxConnInfoLength = \case
|
-- delta between MSG and INFO = e2eEncUserMsgLength (no PQ) - e2eEncConnInfoLength (no PQ) = 1008
|
||||||
PQSupportOn -> 10902 -- reduced by 3700
|
maxEncodedInfoLength :: Int
|
||||||
PQSupportOff -> 14602 -- 15610 - delta in agent between MSG and INFO
|
maxEncodedInfoLength = 14702
|
||||||
{-# INLINE maxConnInfoLength #-}
|
|
||||||
|
maxCompressedInfoLength :: Int
|
||||||
|
maxCompressedInfoLength = 10976 -- maxEncodedInfoLength - 3726, see e2eEncConnInfoLength in agent
|
||||||
|
|
||||||
data EncodedChatMessage = ECMEncoded ByteString | ECMLarge
|
data EncodedChatMessage = ECMEncoded ByteString | ECMLarge
|
||||||
|
|
||||||
encodeChatMessage :: MsgEncodingI e => (PQSupport -> Int) -> ChatMessage e -> EncodedChatMessage
|
encodeChatMessage :: MsgEncodingI e => Int -> ChatMessage e -> EncodedChatMessage
|
||||||
encodeChatMessage getMaxSize msg = do
|
encodeChatMessage maxSize msg = do
|
||||||
case chatToAppMessage msg of
|
case chatToAppMessage msg of
|
||||||
AMJson m -> do
|
AMJson m -> do
|
||||||
let body = LB.toStrict $ J.encode m
|
let body = LB.toStrict $ J.encode m
|
||||||
if B.length body > getMaxSize PQSupportOff
|
if B.length body > maxSize
|
||||||
then ECMLarge
|
then ECMLarge
|
||||||
else ECMEncoded body
|
else ECMEncoded body
|
||||||
AMBinary m -> ECMEncoded $ strEncode m
|
AMBinary m -> ECMEncoded $ strEncode m
|
||||||
@@ -562,10 +573,11 @@ parseChatMessages s = case B.head s of
|
|||||||
decodeCompressed :: ByteString -> [Either String AChatMessage]
|
decodeCompressed :: ByteString -> [Either String AChatMessage]
|
||||||
decodeCompressed s' = case smpDecode s' of
|
decodeCompressed s' = case smpDecode s' of
|
||||||
Left e -> [Left e]
|
Left e -> [Left e]
|
||||||
Right compressed -> concatMap (either (pure . Left) parseChatMessages) . L.toList $ decompressBatch maxRawMsgLength compressed
|
-- TODO v5.7 don't reserve multiple large buffers when decoding batches
|
||||||
|
Right compressed -> concatMap (either (pure . Left) parseChatMessages) . L.toList $ decompressBatch maxEncodedMsgLength compressed
|
||||||
|
|
||||||
compressedBatchMsgBody_ :: CompressCtx -> MsgBody -> IO (Either String ByteString)
|
compressedBatchMsgBody_ :: CompressCtx -> MsgBody -> IO ByteString
|
||||||
compressedBatchMsgBody_ ctx msgBody = markCompressedBatch . smpEncode . (L.:| []) <$$> compress ctx msgBody
|
compressedBatchMsgBody_ ctx msgBody = markCompressedBatch . smpEncode . (L.:| []) <$> compress ctx msgBody
|
||||||
|
|
||||||
markCompressedBatch :: ByteString -> ByteString
|
markCompressedBatch :: ByteString -> ByteString
|
||||||
markCompressedBatch = B.cons 'X'
|
markCompressedBatch = B.cons 'X'
|
||||||
|
|||||||
@@ -72,11 +72,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis
|
|||||||
|
|
||||||
-- when acting as host
|
-- when acting as host
|
||||||
minRemoteCtrlVersion :: AppVersion
|
minRemoteCtrlVersion :: AppVersion
|
||||||
minRemoteCtrlVersion = AppVersion [5, 5, 0, 2]
|
minRemoteCtrlVersion = AppVersion [5, 6, 0, 0]
|
||||||
|
|
||||||
-- when acting as controller
|
-- when acting as controller
|
||||||
minRemoteHostVersion :: AppVersion
|
minRemoteHostVersion :: AppVersion
|
||||||
minRemoteHostVersion = AppVersion [5, 5, 0, 2]
|
minRemoteHostVersion = AppVersion [5, 6, 0, 0]
|
||||||
|
|
||||||
currentAppVersion :: AppVersion
|
currentAppVersion :: AppVersion
|
||||||
currentAppVersion = AppVersion SC.version
|
currentAppVersion = AppVersion SC.version
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ import Simplex.Chat.Types.Preferences
|
|||||||
import Simplex.Messaging.Agent.Protocol (ConnId)
|
import Simplex.Messaging.Agent.Protocol (ConnId)
|
||||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
|
import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
|
import Simplex.Messaging.Crypto.Ratchet (PQSupport)
|
||||||
import Simplex.Messaging.Util (eitherToMaybe)
|
import Simplex.Messaging.Util (eitherToMaybe)
|
||||||
|
|
||||||
getConnectionEntity :: DB.Connection -> VersionRangeChat -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity
|
getConnectionEntity :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity
|
||||||
getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
|
getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
|
||||||
c@Connection {connType, entityId} <- getConnection_
|
c@Connection {connType, entityId} <- getConnection_
|
||||||
case entityId of
|
case entityId of
|
||||||
@@ -54,14 +55,14 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
|
|||||||
where
|
where
|
||||||
getConnection_ :: ExceptT StoreError IO Connection
|
getConnection_ :: ExceptT StoreError IO Connection
|
||||||
getConnection_ = ExceptT $ do
|
getConnection_ = ExceptT $ do
|
||||||
firstRow toConnection (SEConnectionNotFound agentConnId) $
|
firstRow (toConnection vr) (SEConnectionNotFound agentConnId) $
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
||||||
conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id,
|
conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id,
|
||||||
created_at, security_code, security_code_verified_at, enable_pq, pq_snd_enabled, pq_rcv_enabled, auth_err_counter,
|
created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter,
|
||||||
peer_chat_min_version, peer_chat_max_version
|
conn_chat_version, peer_chat_min_version, peer_chat_max_version
|
||||||
FROM connections
|
FROM connections
|
||||||
WHERE user_id = ? AND agent_conn_id = ?
|
WHERE user_id = ? AND agent_conn_id = ?
|
||||||
|]
|
|]
|
||||||
@@ -157,7 +158,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
|
|||||||
userContact_ [(cReq, groupId)] = Right UserContact {userContactLinkId, connReqContact = cReq, groupId}
|
userContact_ [(cReq, groupId)] = Right UserContact {userContactLinkId, connReqContact = cReq, groupId}
|
||||||
userContact_ _ = Left SEUserContactLinkNotFound
|
userContact_ _ = Left SEUserContactLinkNotFound
|
||||||
|
|
||||||
getConnectionEntityByConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity)
|
getConnectionEntityByConnReq :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity)
|
||||||
getConnectionEntityByConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do
|
getConnectionEntityByConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do
|
||||||
connId_ <-
|
connId_ <-
|
||||||
maybeFirstRow fromOnly $
|
maybeFirstRow fromOnly $
|
||||||
@@ -168,7 +169,7 @@ getConnectionEntityByConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2)
|
|||||||
-- multiple connections can have same via_contact_uri_hash if request was repeated;
|
-- multiple connections can have same via_contact_uri_hash if request was repeated;
|
||||||
-- this function searches for latest connection with contact so that "known contact" plan would be chosen;
|
-- this function searches for latest connection with contact so that "known contact" plan would be chosen;
|
||||||
-- deleted connections are filtered out to allow re-connecting via same contact address
|
-- deleted connections are filtered out to allow re-connecting via same contact address
|
||||||
getContactConnEntityByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity)
|
getContactConnEntityByConnReqHash :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity)
|
||||||
getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2) = do
|
getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2) = do
|
||||||
connId_ <-
|
connId_ <-
|
||||||
maybeFirstRow fromOnly $
|
maybeFirstRow fromOnly $
|
||||||
@@ -188,7 +189,7 @@ getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2
|
|||||||
(userId, cReqHash1, cReqHash2, ConnDeleted)
|
(userId, cReqHash1, cReqHash2, ConnDeleted)
|
||||||
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_
|
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_
|
||||||
|
|
||||||
getConnectionsToSubscribe :: DB.Connection -> VersionRangeChat -> IO ([ConnId], [ConnectionEntity])
|
getConnectionsToSubscribe :: DB.Connection -> (PQSupport -> VersionRangeChat) -> IO ([ConnId], [ConnectionEntity])
|
||||||
getConnectionsToSubscribe db vr = do
|
getConnectionsToSubscribe db vr = do
|
||||||
aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1"
|
aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1"
|
||||||
entities <- forM aConnIds $ \acId -> do
|
entities <- forM aConnIds $ \acId -> do
|
||||||
|
|||||||
@@ -125,14 +125,14 @@ deletePendingContactConnection db userId connId =
|
|||||||
|]
|
|]
|
||||||
(userId, connId, ConnContact)
|
(userId, connId, ConnContact)
|
||||||
|
|
||||||
createAddressContactConnection :: DB.Connection -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> PQSupport -> ExceptT StoreError IO Contact
|
createAddressContactConnection :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO Contact
|
||||||
createAddressContactConnection db user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode pqSup = do
|
createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode chatV pqSup = do
|
||||||
PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode pqSup
|
PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode chatV pqSup
|
||||||
liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId)
|
liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId)
|
||||||
getContact db user contactId
|
getContact db vr user contactId
|
||||||
|
|
||||||
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> PQSupport -> IO PendingContactConnection
|
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection
|
||||||
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode pqSup = do
|
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup = do
|
||||||
createdAt <- getCurrentTime
|
createdAt <- getCurrentTime
|
||||||
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
|
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
|
||||||
let pccConnStatus = ConnJoined
|
let pccConnStatus = ConnJoined
|
||||||
@@ -142,19 +142,19 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou
|
|||||||
INSERT INTO connections (
|
INSERT INTO connections (
|
||||||
user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated,
|
user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated,
|
||||||
via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id,
|
via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id,
|
||||||
created_at, updated_at, to_subscribe, enable_pq
|
created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
( (userId, acId, pccConnStatus, ConnContact, True, cReqHash, xContactId)
|
( (userId, acId, pccConnStatus, ConnContact, True, cReqHash, xContactId)
|
||||||
:. (customUserProfileId, isJust groupLinkId, groupLinkId)
|
:. (customUserProfileId, isJust groupLinkId, groupLinkId)
|
||||||
:. (createdAt, createdAt, subMode == SMOnlyCreate, pqSup)
|
:. (createdAt, createdAt, subMode == SMOnlyCreate, chatV, pqSup, pqSup)
|
||||||
)
|
)
|
||||||
pccConnId <- insertedRowId db
|
pccConnId <- insertedRowId db
|
||||||
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt}
|
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt}
|
||||||
|
|
||||||
getConnReqContactXContactId :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId)
|
getConnReqContactXContactId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId)
|
||||||
getConnReqContactXContactId db user@User {userId} cReqHash = do
|
getConnReqContactXContactId db vr user@User {userId} cReqHash = do
|
||||||
getContactByConnReqHash db user cReqHash >>= \case
|
getContactByConnReqHash db vr user cReqHash >>= \case
|
||||||
c@(Just _) -> pure (c, Nothing)
|
c@(Just _) -> pure (c, Nothing)
|
||||||
Nothing -> (Nothing,) <$> getXContactId
|
Nothing -> (Nothing,) <$> getXContactId
|
||||||
where
|
where
|
||||||
@@ -166,9 +166,9 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
|
|||||||
"SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1"
|
"SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1"
|
||||||
(userId, cReqHash)
|
(userId, cReqHash)
|
||||||
|
|
||||||
getContactByConnReqHash :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe Contact)
|
getContactByConnReqHash :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ConnReqUriHash -> IO (Maybe Contact)
|
||||||
getContactByConnReqHash db user@User {userId} cReqHash =
|
getContactByConnReqHash db vr user@User {userId} cReqHash =
|
||||||
maybeFirstRow (toContact user) $
|
maybeFirstRow (toContact vr user) $
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
@@ -178,8 +178,8 @@ getContactByConnReqHash db user@User {userId} cReqHash =
|
|||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
|
||||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.enable_pq, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||||
c.peer_chat_min_version, c.peer_chat_max_version
|
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||||
FROM contacts ct
|
FROM contacts ct
|
||||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||||
JOIN connections c ON c.contact_id = ct.contact_id
|
JOIN connections c ON c.contact_id = ct.contact_id
|
||||||
@@ -189,8 +189,8 @@ getContactByConnReqHash db user@User {userId} cReqHash =
|
|||||||
|]
|
|]
|
||||||
(userId, cReqHash, CSActive)
|
(userId, cReqHash, CSActive)
|
||||||
|
|
||||||
createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> PQSupport -> IO PendingContactConnection
|
createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection
|
||||||
createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode pqSup = do
|
createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode chatV pqSup = do
|
||||||
createdAt <- getCurrentTime
|
createdAt <- getCurrentTime
|
||||||
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
|
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
|
||||||
let contactConnInitiated = pccConnStatus == ConnNew
|
let contactConnInitiated = pccConnStatus == ConnNew
|
||||||
@@ -199,11 +199,11 @@ createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile
|
|||||||
[sql|
|
[sql|
|
||||||
INSERT INTO connections
|
INSERT INTO connections
|
||||||
(user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id,
|
(user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id,
|
||||||
created_at, updated_at, to_subscribe, enable_pq)
|
created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
( (userId, acId, cReq, pccConnStatus, ConnContact, contactConnInitiated, customUserProfileId)
|
( (userId, acId, cReq, pccConnStatus, ConnContact, contactConnInitiated, customUserProfileId)
|
||||||
:. (createdAt, createdAt, subMode == SMOnlyCreate, pqSup)
|
:. (createdAt, createdAt, subMode == SMOnlyCreate, chatV, pqSup, pqSup)
|
||||||
)
|
)
|
||||||
pccConnId <- insertedRowId db
|
pccConnId <- insertedRowId db
|
||||||
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt}
|
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt}
|
||||||
@@ -278,13 +278,13 @@ setContactDeleted db user@User {userId} ct@Contact {contactId} = do
|
|||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
DB.execute db "UPDATE contacts SET deleted = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId)
|
DB.execute db "UPDATE contacts SET deleted = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId)
|
||||||
|
|
||||||
getDeletedContacts :: DB.Connection -> User -> IO [Contact]
|
getDeletedContacts :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> IO [Contact]
|
||||||
getDeletedContacts db user@User {userId} = do
|
getDeletedContacts db vr user@User {userId} = do
|
||||||
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 1" (Only userId)
|
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 1" (Only userId)
|
||||||
rights <$> mapM (runExceptT . getDeletedContact db user) contactIds
|
rights <$> mapM (runExceptT . getDeletedContact db vr user) contactIds
|
||||||
|
|
||||||
getDeletedContact :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Contact
|
getDeletedContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO Contact
|
||||||
getDeletedContact db user contactId = getContact_ db user contactId True
|
getDeletedContact db vr user contactId = getContact_ db vr user contactId True
|
||||||
|
|
||||||
deleteContactProfile_ :: DB.Connection -> UserId -> ContactId -> IO ()
|
deleteContactProfile_ :: DB.Connection -> UserId -> ContactId -> IO ()
|
||||||
deleteContactProfile_ db userId contactId =
|
deleteContactProfile_ db userId contactId =
|
||||||
@@ -520,19 +520,19 @@ updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt
|
|||||||
(newName, updatedAt, userId, contactId)
|
(newName, updatedAt, userId, contactId)
|
||||||
safeDeleteLDN db user displayName
|
safeDeleteLDN db user displayName
|
||||||
|
|
||||||
getContactByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Contact
|
getContactByName :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactName -> ExceptT StoreError IO Contact
|
||||||
getContactByName db user localDisplayName = do
|
getContactByName db vr user localDisplayName = do
|
||||||
cId <- getContactIdByName db user localDisplayName
|
cId <- getContactIdByName db user localDisplayName
|
||||||
getContact db user cId
|
getContact db vr user cId
|
||||||
|
|
||||||
getUserContacts :: DB.Connection -> User -> IO [Contact]
|
getUserContacts :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> IO [Contact]
|
||||||
getUserContacts db user@User {userId} = do
|
getUserContacts db vr user@User {userId} = do
|
||||||
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId)
|
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId)
|
||||||
contacts <- rights <$> mapM (runExceptT . getContact db user) contactIds
|
contacts <- rights <$> mapM (runExceptT . getContact db vr user) contactIds
|
||||||
pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts
|
pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts
|
||||||
|
|
||||||
createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
|
createOrUpdateContactRequest :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> ExceptT StoreError IO ContactOrRequest
|
||||||
createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ =
|
createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ pqSup =
|
||||||
liftIO (maybeM getContact' xContactId_) >>= \case
|
liftIO (maybeM getContact' xContactId_) >>= \case
|
||||||
Just contact -> pure $ CORContact contact
|
Just contact -> pure $ CORContact contact
|
||||||
Nothing -> CORRequest <$> createOrUpdate_
|
Nothing -> CORRequest <$> createOrUpdate_
|
||||||
@@ -561,14 +561,17 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
INSERT INTO contact_requests
|
INSERT INTO contact_requests
|
||||||
(user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, created_at, updated_at, xcontact_id)
|
(user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id,
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?)
|
created_at, updated_at, xcontact_id, pq_support)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
(userContactLinkId, invId, minV, maxV, profileId, ldn, userId, currentTs, currentTs, xContactId_)
|
( (userContactLinkId, invId, minV, maxV, profileId, ldn, userId)
|
||||||
|
:. (currentTs, currentTs, xContactId_, pqSup)
|
||||||
|
)
|
||||||
insertedRowId db
|
insertedRowId db
|
||||||
getContact' :: XContactId -> IO (Maybe Contact)
|
getContact' :: XContactId -> IO (Maybe Contact)
|
||||||
getContact' xContactId =
|
getContact' xContactId =
|
||||||
maybeFirstRow (toContact user) $
|
maybeFirstRow (toContact vr user) $
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
@@ -578,8 +581,8 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
|
|||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
|
||||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.enable_pq, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||||
c.peer_chat_min_version, c.peer_chat_max_version
|
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||||
FROM contacts ct
|
FROM contacts ct
|
||||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||||
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||||
@@ -596,7 +599,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
|
|||||||
[sql|
|
[sql|
|
||||||
SELECT
|
SELECT
|
||||||
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
||||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
|
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at,
|
||||||
cr.peer_chat_min_version, cr.peer_chat_max_version
|
cr.peer_chat_min_version, cr.peer_chat_max_version
|
||||||
FROM contact_requests cr
|
FROM contact_requests cr
|
||||||
JOIN connections c USING (user_contact_link_id)
|
JOIN connections c USING (user_contact_link_id)
|
||||||
@@ -617,20 +620,20 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
UPDATE contact_requests
|
UPDATE contact_requests
|
||||||
SET agent_invitation_id = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ?
|
SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ?
|
||||||
WHERE user_id = ? AND contact_request_id = ?
|
WHERE user_id = ? AND contact_request_id = ?
|
||||||
|]
|
|]
|
||||||
(invId, minV, maxV, currentTs, userId, cReqId)
|
(invId, pqSup, minV, maxV, currentTs, userId, cReqId)
|
||||||
else withLocalDisplayName db userId displayName $ \ldn ->
|
else withLocalDisplayName db userId displayName $ \ldn ->
|
||||||
Right <$> do
|
Right <$> do
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
UPDATE contact_requests
|
UPDATE contact_requests
|
||||||
SET agent_invitation_id = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ?
|
SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ?
|
||||||
WHERE user_id = ? AND contact_request_id = ?
|
WHERE user_id = ? AND contact_request_id = ?
|
||||||
|]
|
|]
|
||||||
(invId, minV, maxV, ldn, currentTs, userId, cReqId)
|
(invId, pqSup, minV, maxV, ldn, currentTs, userId, cReqId)
|
||||||
safeDeleteLDN db user oldLdn
|
safeDeleteLDN db user oldLdn
|
||||||
where
|
where
|
||||||
updateProfile currentTs =
|
updateProfile currentTs =
|
||||||
@@ -665,7 +668,7 @@ getContactRequest db User {userId} contactRequestId =
|
|||||||
[sql|
|
[sql|
|
||||||
SELECT
|
SELECT
|
||||||
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
||||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
|
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at,
|
||||||
cr.peer_chat_min_version, cr.peer_chat_max_version
|
cr.peer_chat_min_version, cr.peer_chat_max_version
|
||||||
FROM contact_requests cr
|
FROM contact_requests cr
|
||||||
JOIN connections c USING (user_contact_link_id)
|
JOIN connections c USING (user_contact_link_id)
|
||||||
@@ -706,8 +709,8 @@ deleteContactRequest db User {userId} contactRequestId = do
|
|||||||
(userId, userId, contactRequestId, userId)
|
(userId, userId, contactRequestId, userId)
|
||||||
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
|
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
|
||||||
|
|
||||||
createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO Contact
|
createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO Contact
|
||||||
createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do
|
createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do
|
||||||
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
|
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
|
||||||
createdAt <- getCurrentTime
|
createdAt <- getCurrentTime
|
||||||
customUserProfileId <- forM incognitoProfile $ \case
|
customUserProfileId <- forM incognitoProfile $ \case
|
||||||
@@ -719,7 +722,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
|
|||||||
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||||
(userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId, contactUsed)
|
(userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId, contactUsed)
|
||||||
contactId <- insertedRowId db
|
contactId <- insertedRowId db
|
||||||
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
|
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
|
||||||
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
|
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
|
||||||
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False}
|
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False}
|
||||||
|
|
||||||
@@ -728,12 +731,12 @@ getContactIdByName db User {userId} cName =
|
|||||||
ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $
|
ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $
|
||||||
DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ? AND deleted = 0" (userId, cName)
|
DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ? AND deleted = 0" (userId, cName)
|
||||||
|
|
||||||
getContact :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Contact
|
getContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO Contact
|
||||||
getContact db user contactId = getContact_ db user contactId False
|
getContact db vr user contactId = getContact_ db vr user contactId False
|
||||||
|
|
||||||
getContact_ :: DB.Connection -> User -> Int64 -> Bool -> ExceptT StoreError IO Contact
|
getContact_ :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> Bool -> ExceptT StoreError IO Contact
|
||||||
getContact_ db user@User {userId} contactId deleted =
|
getContact_ db vr user@User {userId} contactId deleted =
|
||||||
ExceptT . firstRow (toContact user) (SEContactNotFound contactId) $
|
ExceptT . firstRow (toContact vr user) (SEContactNotFound contactId) $
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
@@ -743,8 +746,8 @@ getContact_ db user@User {userId} contactId deleted =
|
|||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
|
||||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.enable_pq, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||||
c.peer_chat_min_version, c.peer_chat_max_version
|
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||||
FROM contacts ct
|
FROM contacts ct
|
||||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||||
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||||
@@ -787,8 +790,8 @@ getPendingContactConnections db User {userId} = do
|
|||||||
|]
|
|]
|
||||||
[":user_id" := userId, ":conn_type" := ConnContact]
|
[":user_id" := userId, ":conn_type" := ConnContact]
|
||||||
|
|
||||||
getContactConnections :: DB.Connection -> UserId -> Contact -> IO [Connection]
|
getContactConnections :: DB.Connection -> (PQSupport -> VersionRangeChat) -> UserId -> Contact -> IO [Connection]
|
||||||
getContactConnections db userId Contact {contactId} =
|
getContactConnections db vr userId Contact {contactId} =
|
||||||
connections =<< liftIO getConnections_
|
connections =<< liftIO getConnections_
|
||||||
where
|
where
|
||||||
getConnections_ =
|
getConnections_ =
|
||||||
@@ -797,26 +800,26 @@ getContactConnections db userId Contact {contactId} =
|
|||||||
[sql|
|
[sql|
|
||||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||||
c.created_at, c.security_code, c.security_code_verified_at, c.enable_pq, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||||
c.peer_chat_min_version, c.peer_chat_max_version
|
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||||
FROM connections c
|
FROM connections c
|
||||||
JOIN contacts ct ON ct.contact_id = c.contact_id
|
JOIN contacts ct ON ct.contact_id = c.contact_id
|
||||||
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
|
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
|
||||||
|]
|
|]
|
||||||
(userId, userId, contactId)
|
(userId, userId, contactId)
|
||||||
connections [] = pure []
|
connections [] = pure []
|
||||||
connections rows = pure $ map toConnection rows
|
connections rows = pure $ map (toConnection vr) rows
|
||||||
|
|
||||||
getConnectionById :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Connection
|
getConnectionById :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO Connection
|
||||||
getConnectionById db User {userId} connId = ExceptT $ do
|
getConnectionById db vr User {userId} connId = ExceptT $ do
|
||||||
firstRow toConnection (SEConnectionNotFoundById connId) $
|
firstRow (toConnection vr) (SEConnectionNotFoundById connId) $
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
||||||
conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id,
|
conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id,
|
||||||
created_at, security_code, security_code_verified_at, enable_pq, pq_snd_enabled, pq_rcv_enabled, auth_err_counter,
|
created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter,
|
||||||
peer_chat_min_version, peer_chat_max_version
|
conn_chat_version, peer_chat_min_version, peer_chat_max_version
|
||||||
FROM connections
|
FROM connections
|
||||||
WHERE user_id = ? AND connection_id = ?
|
WHERE user_id = ? AND connection_id = ?
|
||||||
|]
|
|]
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
|||||||
import qualified Simplex.Messaging.Crypto.File as CF
|
import qualified Simplex.Messaging.Crypto.File as CF
|
||||||
import Simplex.Messaging.Crypto.Ratchet as CR
|
import Simplex.Messaging.Crypto.Ratchet as CR
|
||||||
import Simplex.Messaging.Protocol (SubscriptionMode (..))
|
import Simplex.Messaging.Protocol (SubscriptionMode (..))
|
||||||
|
import Simplex.Messaging.Version
|
||||||
import System.FilePath (takeFileName)
|
import System.FilePath (takeFileName)
|
||||||
|
|
||||||
getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer]
|
getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer]
|
||||||
@@ -173,10 +174,10 @@ getPendingSndChunks db fileId connId =
|
|||||||
|]
|
|]
|
||||||
(fileId, connId)
|
(fileId, connId)
|
||||||
|
|
||||||
createSndDirectFTConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> SubscriptionMode -> IO ()
|
createSndDirectFTConnection :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> (CommandId, ConnId) -> SubscriptionMode -> IO ()
|
||||||
createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) subMode = do
|
createSndDirectFTConnection db vr user@User {userId} fileId (cmdId, acId) subMode = do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode
|
Connection {connId} <- createSndFileConnection_ db vr userId fileId acId subMode
|
||||||
setCommandConnId db user cmdId connId
|
setCommandConnId db user cmdId connId
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
@@ -193,10 +194,10 @@ createSndGroupFileTransfer db userId GroupInfo {groupId} filePath FileInvitation
|
|||||||
fileId <- insertedRowId db
|
fileId <- insertedRowId db
|
||||||
pure FileTransferMeta {fileId, xftpSndFile = Nothing, xftpRedirectFor = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False}
|
pure FileTransferMeta {fileId, xftpSndFile = Nothing, xftpRedirectFor = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False}
|
||||||
|
|
||||||
createSndGroupFileTransferConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> SubscriptionMode -> IO ()
|
createSndGroupFileTransferConnection :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> SubscriptionMode -> IO ()
|
||||||
createSndGroupFileTransferConnection db user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} subMode = do
|
createSndGroupFileTransferConnection db vr user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} subMode = do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode
|
Connection {connId} <- createSndFileConnection_ db vr userId fileId acId subMode
|
||||||
setCommandConnId db user cmdId connId
|
setCommandConnId db user cmdId connId
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
@@ -429,10 +430,10 @@ lookupChatRefByFileId db User {userId} fileId =
|
|||||||
(userId, fileId)
|
(userId, fileId)
|
||||||
|
|
||||||
-- TODO v6.0 remove
|
-- TODO v6.0 remove
|
||||||
createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection
|
createSndFileConnection_ :: DB.Connection -> (PQSupport -> VersionRangeChat) -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection
|
||||||
createSndFileConnection_ db userId fileId agentConnId subMode = do
|
createSndFileConnection_ db vr userId fileId agentConnId subMode = do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
createConnection_ db userId ConnSndFile (Just fileId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
|
createConnection_ db userId ConnSndFile (Just fileId) agentConnId (minVersion $ vr PQSupportOff) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
|
||||||
|
|
||||||
updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO ()
|
updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO ()
|
||||||
updateSndFileStatus db SndFileTransfer {fileId, connId} status = do
|
updateSndFileStatus db SndFileTransfer {fileId, connId} status = do
|
||||||
@@ -694,7 +695,7 @@ getRcvFileTransfer_ db userId fileId = do
|
|||||||
_ -> pure Nothing
|
_ -> pure Nothing
|
||||||
cancelled = fromMaybe False cancelled_
|
cancelled = fromMaybe False cancelled_
|
||||||
|
|
||||||
acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem
|
acceptRcvFileTransfer :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem
|
||||||
acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do
|
acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
acceptRcvFT_ db user fileId filePath Nothing currentTs
|
acceptRcvFT_ db user fileId filePath Nothing currentTs
|
||||||
@@ -706,16 +707,16 @@ acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus f
|
|||||||
setCommandConnId db user cmdId connId
|
setCommandConnId db user cmdId connId
|
||||||
runExceptT $ getChatItemByFileId db vr user fileId
|
runExceptT $ getChatItemByFileId db vr user fileId
|
||||||
|
|
||||||
getContactByFileId :: DB.Connection -> User -> FileTransferId -> ExceptT StoreError IO Contact
|
getContactByFileId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> FileTransferId -> ExceptT StoreError IO Contact
|
||||||
getContactByFileId db user@User {userId} fileId = do
|
getContactByFileId db vr user@User {userId} fileId = do
|
||||||
cId <- getContactIdByFileId
|
cId <- getContactIdByFileId
|
||||||
getContact db user cId
|
getContact db vr user cId
|
||||||
where
|
where
|
||||||
getContactIdByFileId =
|
getContactIdByFileId =
|
||||||
ExceptT . firstRow fromOnly (SEContactNotFoundByFileId fileId) $
|
ExceptT . firstRow fromOnly (SEContactNotFoundByFileId fileId) $
|
||||||
DB.query db "SELECT contact_id FROM files WHERE user_id = ? AND file_id = ?" (userId, fileId)
|
DB.query db "SELECT contact_id FROM files WHERE user_id = ? AND file_id = ?" (userId, fileId)
|
||||||
|
|
||||||
acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
|
acceptRcvInlineFT :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
|
||||||
acceptRcvInlineFT db vr user fileId filePath = do
|
acceptRcvInlineFT db vr user fileId filePath = do
|
||||||
liftIO $ acceptRcvFT_ db user fileId filePath (Just IFMOffer) =<< getCurrentTime
|
liftIO $ acceptRcvFT_ db user fileId filePath (Just IFMOffer) =<< getCurrentTime
|
||||||
getChatItemByFileId db vr user fileId
|
getChatItemByFileId db vr user fileId
|
||||||
@@ -724,7 +725,7 @@ startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Mayb
|
|||||||
startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline =
|
startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline =
|
||||||
acceptRcvFT_ db user fileId filePath rcvFileInline =<< getCurrentTime
|
acceptRcvFT_ db user fileId filePath rcvFileInline =<< getCurrentTime
|
||||||
|
|
||||||
xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
|
xftpAcceptRcvFT :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
|
||||||
xftpAcceptRcvFT db vr user fileId filePath = do
|
xftpAcceptRcvFT db vr user fileId filePath = do
|
||||||
liftIO $ acceptRcvFT_ db user fileId filePath Nothing =<< getCurrentTime
|
liftIO $ acceptRcvFT_ db user fileId filePath Nothing =<< getCurrentTime
|
||||||
getChatItemByFileId db vr user fileId
|
getChatItemByFileId db vr user fileId
|
||||||
@@ -999,7 +1000,7 @@ getLocalCryptoFile db userId fileId sent =
|
|||||||
pure $ CryptoFile filePath fileCryptoArgs
|
pure $ CryptoFile filePath fileCryptoArgs
|
||||||
_ -> throwError $ SEFileNotFound fileId
|
_ -> throwError $ SEFileNotFound fileId
|
||||||
|
|
||||||
updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> VersionRangeChat -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem
|
updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem
|
||||||
updateDirectCIFileStatus db vr user fileId fileStatus = do
|
updateDirectCIFileStatus db vr user fileId fileStatus = do
|
||||||
aci@(AChatItem cType d cInfo ci) <- getChatItemByFileId db vr user fileId
|
aci@(AChatItem cType d cInfo ci) <- getChatItemByFileId db vr user fileId
|
||||||
case (cType, testEquality d $ msgDirection @d) of
|
case (cType, testEquality d $ msgDirection @d) of
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
|||||||
import Database.SQLite.Simple (NamedParam (..), Only (..), Query (..), (:.) (..))
|
import Database.SQLite.Simple (NamedParam (..), Only (..), Query (..), (:.) (..))
|
||||||
import Database.SQLite.Simple.QQ (sql)
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
import Simplex.Chat.Messages
|
import Simplex.Chat.Messages
|
||||||
import Simplex.Chat.Protocol (groupForwardVRange)
|
import Simplex.Chat.Protocol (groupForwardVersion)
|
||||||
import Simplex.Chat.Store.Direct
|
import Simplex.Chat.Store.Direct
|
||||||
import Simplex.Chat.Store.Shared
|
import Simplex.Chat.Store.Shared
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
@@ -142,7 +142,7 @@ import Simplex.Messaging.Agent.Protocol (ConnId, UserId)
|
|||||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff, pattern PQSupportOff)
|
import Simplex.Messaging.Crypto.Ratchet (PQSupport, pattern PQEncOff, pattern PQSupportOff)
|
||||||
import Simplex.Messaging.Protocol (SubscriptionMode (..))
|
import Simplex.Messaging.Protocol (SubscriptionMode (..))
|
||||||
import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>))
|
import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>))
|
||||||
import Simplex.Messaging.Version
|
import Simplex.Messaging.Version
|
||||||
@@ -154,9 +154,9 @@ type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupM
|
|||||||
|
|
||||||
type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences))
|
type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences))
|
||||||
|
|
||||||
toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo
|
toGroupInfo :: (PQSupport -> VersionRangeChat) -> Int64 -> GroupInfoRow -> GroupInfo
|
||||||
toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. userMemberRow) =
|
toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. userMemberRow) =
|
||||||
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = JVersionRange vr}
|
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr PQSupportOff}
|
||||||
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
|
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
|
||||||
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
||||||
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
|
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
|
||||||
@@ -169,7 +169,7 @@ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer,
|
|||||||
blockedByAdmin = maybe False mrsBlocked memberRestriction_
|
blockedByAdmin = maybe False mrsBlocked memberRestriction_
|
||||||
invitedBy = toInvitedBy userContactId invitedById
|
invitedBy = toInvitedBy userContactId invitedById
|
||||||
activeConn = Nothing
|
activeConn = Nothing
|
||||||
memberChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
memberChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
||||||
in GroupMember {..}
|
in GroupMember {..}
|
||||||
|
|
||||||
toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember
|
toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember
|
||||||
@@ -186,18 +186,18 @@ createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName}
|
|||||||
"INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
|
"INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||||
(userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs)
|
(userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs)
|
||||||
userContactLinkId <- insertedRowId db
|
userContactLinkId <- insertedRowId db
|
||||||
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff
|
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff
|
||||||
|
|
||||||
getGroupLinkConnection :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO Connection
|
getGroupLinkConnection :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> ExceptT StoreError IO Connection
|
||||||
getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
|
getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} =
|
||||||
ExceptT . firstRow toConnection (SEGroupLinkNotFound groupInfo) $
|
ExceptT . firstRow (toConnection vr) (SEGroupLinkNotFound groupInfo) $
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||||
c.created_at, c.security_code, c.security_code_verified_at, c.enable_pq, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||||
c.peer_chat_min_version, c.peer_chat_max_version
|
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||||
FROM connections c
|
FROM connections c
|
||||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||||
WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ?
|
WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ?
|
||||||
@@ -261,7 +261,7 @@ setGroupLinkMemberRole :: DB.Connection -> User -> Int64 -> GroupMemberRole -> I
|
|||||||
setGroupLinkMemberRole db User {userId} userContactLinkId memberRole =
|
setGroupLinkMemberRole db User {userId} userContactLinkId memberRole =
|
||||||
DB.execute db "UPDATE user_contact_links SET group_link_member_role = ? WHERE user_id = ? AND user_contact_link_id = ?" (memberRole, userId, userContactLinkId)
|
DB.execute db "UPDATE user_contact_links SET group_link_member_role = ? WHERE user_id = ? AND user_contact_link_id = ?" (memberRole, userId, userContactLinkId)
|
||||||
|
|
||||||
getGroupAndMember :: DB.Connection -> User -> Int64 -> VersionRangeChat -> ExceptT StoreError IO (GroupInfo, GroupMember)
|
getGroupAndMember :: DB.Connection -> User -> Int64 -> (PQSupport -> VersionRangeChat) -> ExceptT StoreError IO (GroupInfo, GroupMember)
|
||||||
getGroupAndMember db User {userId, userContactId} groupMemberId vr =
|
getGroupAndMember db User {userId, userContactId} groupMemberId vr =
|
||||||
ExceptT . firstRow toGroupAndMember (SEInternalError "referenced group member not found") $
|
ExceptT . firstRow toGroupAndMember (SEInternalError "referenced group member not found") $
|
||||||
DB.query
|
DB.query
|
||||||
@@ -282,8 +282,8 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr =
|
|||||||
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||||
c.created_at, c.security_code, c.security_code_verified_at, c.enable_pq, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||||
c.peer_chat_min_version, c.peer_chat_max_version
|
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||||
FROM group_members m
|
FROM group_members m
|
||||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||||
JOIN groups g ON g.group_id = m.group_id
|
JOIN groups g ON g.group_id = m.group_id
|
||||||
@@ -303,10 +303,10 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr =
|
|||||||
toGroupAndMember (groupInfoRow :. memberRow :. connRow) =
|
toGroupAndMember (groupInfoRow :. memberRow :. connRow) =
|
||||||
let groupInfo = toGroupInfo vr userContactId groupInfoRow
|
let groupInfo = toGroupInfo vr userContactId groupInfoRow
|
||||||
member = toGroupMember userContactId memberRow
|
member = toGroupMember userContactId memberRow
|
||||||
in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow})
|
in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection vr connRow})
|
||||||
|
|
||||||
-- | creates completely new group with a single member - the current user
|
-- | creates completely new group with a single member - the current user
|
||||||
createNewGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo
|
createNewGroup :: DB.Connection -> (PQSupport -> VersionRangeChat) -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo
|
||||||
createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do
|
createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do
|
||||||
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
|
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
|
||||||
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
||||||
@@ -348,7 +348,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
|
|||||||
}
|
}
|
||||||
|
|
||||||
-- | creates a new group record for the group the current user was invited to, or returns an existing one
|
-- | creates a new group record for the group the current user was invited to, or returns an existing one
|
||||||
createGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId)
|
createGroupInvitation :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId)
|
||||||
createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName
|
createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName
|
||||||
createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {customUserProfileId, peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} incognitoProfileId = do
|
createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {customUserProfileId, peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} incognitoProfileId = do
|
||||||
liftIO getInvitationGroupId_ >>= \case
|
liftIO getInvitationGroupId_ >>= \case
|
||||||
@@ -393,7 +393,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
|
|||||||
|]
|
|]
|
||||||
(profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs)
|
(profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs)
|
||||||
insertedRowId db
|
insertedRowId db
|
||||||
let JVersionRange hostVRange = peerChatVRange
|
let hostVRange = const $ adjustedMemberVRange vr peerChatVRange
|
||||||
GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange
|
GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange
|
||||||
membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs vr
|
membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs vr
|
||||||
let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False}
|
let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False}
|
||||||
@@ -414,13 +414,18 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
|
|||||||
groupMemberId
|
groupMemberId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
adjustedMemberVRange :: (PQSupport -> VersionRangeChat) -> VersionRangeChat -> VersionRangeChat
|
||||||
|
adjustedMemberVRange getVR vr@(VersionRange minV maxV) =
|
||||||
|
let maxV' = min maxV (maxVersion $ getVR PQSupportOff)
|
||||||
|
in fromMaybe vr $ safeVersionRange minV (max minV maxV')
|
||||||
|
|
||||||
getHostMemberId_ :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId
|
getHostMemberId_ :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId
|
||||||
getHostMemberId_ db User {userId} groupId =
|
getHostMemberId_ db User {userId} groupId =
|
||||||
ExceptT . firstRow fromOnly (SEHostMemberIdNotFound groupId) $
|
ExceptT . firstRow fromOnly (SEHostMemberIdNotFound groupId) $
|
||||||
DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND member_category = ?" (userId, groupId, GCHostMember)
|
DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND member_category = ?" (userId, groupId, GCHostMember)
|
||||||
|
|
||||||
createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> Maybe GroupMemberId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> UTCTime -> VersionRangeChat -> ExceptT StoreError IO GroupMember
|
createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> Maybe GroupMemberId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> UTCTime -> (PQSupport -> VersionRangeChat) -> ExceptT StoreError IO GroupMember
|
||||||
createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMemberId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfileId createdAt memberChatVRange@(VersionRange minV maxV) = do
|
createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMemberId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfileId createdAt vr = do
|
||||||
incognitoProfile <- forM incognitoProfileId $ \profileId -> getProfileById db userId profileId
|
incognitoProfile <- forM incognitoProfileId $ \profileId -> getProfileById db userId profileId
|
||||||
(localDisplayName, memberProfile) <- case (incognitoProfile, incognitoProfileId) of
|
(localDisplayName, memberProfile) <- case (incognitoProfile, incognitoProfileId) of
|
||||||
(Just profile@LocalProfile {displayName}, Just profileId) ->
|
(Just profile@LocalProfile {displayName}, Just profileId) ->
|
||||||
@@ -444,9 +449,10 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe
|
|||||||
memberContactId = Just $ contactId' userOrContact,
|
memberContactId = Just $ contactId' userOrContact,
|
||||||
memberContactProfileId = localProfileId (profile' userOrContact),
|
memberContactProfileId = localProfileId (profile' userOrContact),
|
||||||
activeConn = Nothing,
|
activeConn = Nothing,
|
||||||
memberChatVRange = JVersionRange memberChatVRange
|
memberChatVRange
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
|
memberChatVRange@(VersionRange minV maxV) = vr PQSupportOff
|
||||||
insertMember_ :: IO ContactName
|
insertMember_ :: IO ContactName
|
||||||
insertMember_ = do
|
insertMember_ = do
|
||||||
let localDisplayName = localDisplayName' userOrContact
|
let localDisplayName = localDisplayName' userOrContact
|
||||||
@@ -482,7 +488,7 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe
|
|||||||
)
|
)
|
||||||
pure $ Right incognitoLdn
|
pure $ Right incognitoLdn
|
||||||
|
|
||||||
createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember)
|
createGroupInvitedViaLink :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember)
|
||||||
createGroupInvitedViaLink
|
createGroupInvitedViaLink
|
||||||
db
|
db
|
||||||
vr
|
vr
|
||||||
@@ -496,7 +502,7 @@ createGroupInvitedViaLink
|
|||||||
-- using IBUnknown since host is created without contact
|
-- using IBUnknown since host is created without contact
|
||||||
void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember GSMemAccepted IBUnknown customUserProfileId currentTs vr
|
void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember GSMemAccepted IBUnknown customUserProfileId currentTs vr
|
||||||
liftIO $ setViaGroupLinkHash db groupId connId
|
liftIO $ setViaGroupLinkHash db groupId connId
|
||||||
(,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db user hostMemberId
|
(,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user hostMemberId
|
||||||
where
|
where
|
||||||
insertGroup_ currentTs = ExceptT $ do
|
insertGroup_ currentTs = ExceptT $ do
|
||||||
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
|
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
|
||||||
@@ -553,10 +559,10 @@ setGroupInvitationChatItemId db User {userId} groupId chatItemId = do
|
|||||||
|
|
||||||
-- TODO return the last connection that is ready, not any last connection
|
-- TODO return the last connection that is ready, not any last connection
|
||||||
-- requires updating connection status
|
-- requires updating connection status
|
||||||
getGroup :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO Group
|
getGroup :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupId -> ExceptT StoreError IO Group
|
||||||
getGroup db vr user groupId = do
|
getGroup db vr user groupId = do
|
||||||
gInfo <- getGroupInfo db vr user groupId
|
gInfo <- getGroupInfo db vr user groupId
|
||||||
members <- liftIO $ getGroupMembers db user gInfo
|
members <- liftIO $ getGroupMembers db vr user gInfo
|
||||||
pure $ Group gInfo members
|
pure $ Group gInfo members
|
||||||
|
|
||||||
deleteGroupConnectionsAndFiles :: DB.Connection -> User -> GroupInfo -> [GroupMember] -> IO ()
|
deleteGroupConnectionsAndFiles :: DB.Connection -> User -> GroupInfo -> [GroupMember] -> IO ()
|
||||||
@@ -608,12 +614,12 @@ deleteGroupProfile_ db userId groupId =
|
|||||||
|]
|
|]
|
||||||
(userId, groupId)
|
(userId, groupId)
|
||||||
|
|
||||||
getUserGroups :: DB.Connection -> VersionRangeChat -> User -> IO [Group]
|
getUserGroups :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> IO [Group]
|
||||||
getUserGroups db vr user@User {userId} = do
|
getUserGroups db vr user@User {userId} = do
|
||||||
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId)
|
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId)
|
||||||
rights <$> mapM (runExceptT . getGroup db vr user) groupIds
|
rights <$> mapM (runExceptT . getGroup db vr user) groupIds
|
||||||
|
|
||||||
getUserGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo]
|
getUserGroupDetails :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo]
|
||||||
getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
|
getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
|
||||||
map (toGroupInfo vr userContactId)
|
map (toGroupInfo vr userContactId)
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
@@ -636,7 +642,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
|
|||||||
where
|
where
|
||||||
search = fromMaybe "" search_
|
search = fromMaybe "" search_
|
||||||
|
|
||||||
getUserGroupsWithSummary :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [(GroupInfo, GroupSummary)]
|
getUserGroupsWithSummary :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Maybe ContactId -> Maybe String -> IO [(GroupInfo, GroupSummary)]
|
||||||
getUserGroupsWithSummary db vr user _contactId_ search_ =
|
getUserGroupsWithSummary db vr user _contactId_ search_ =
|
||||||
getUserGroupDetails db vr user _contactId_ search_
|
getUserGroupDetails db vr user _contactId_ search_
|
||||||
>>= mapM (\g@GroupInfo {groupId} -> (g,) <$> getGroupSummary db user groupId)
|
>>= mapM (\g@GroupInfo {groupId} -> (g,) <$> getGroupSummary db user groupId)
|
||||||
@@ -677,7 +683,7 @@ checkContactHasGroups :: DB.Connection -> User -> Contact -> IO (Maybe GroupId)
|
|||||||
checkContactHasGroups db User {userId} Contact {contactId} =
|
checkContactHasGroups db User {userId} Contact {contactId} =
|
||||||
maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM group_members WHERE user_id = ? AND contact_id = ? LIMIT 1" (userId, contactId)
|
maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM group_members WHERE user_id = ? AND contact_id = ? LIMIT 1" (userId, contactId)
|
||||||
|
|
||||||
getGroupInfoByName :: DB.Connection -> VersionRangeChat -> User -> GroupName -> ExceptT StoreError IO GroupInfo
|
getGroupInfoByName :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupName -> ExceptT StoreError IO GroupInfo
|
||||||
getGroupInfoByName db vr user gName = do
|
getGroupInfoByName db vr user gName = do
|
||||||
gId <- getGroupIdByName db user gName
|
gId <- getGroupIdByName db user gName
|
||||||
getGroupInfo db vr user gId
|
getGroupInfo db vr user gId
|
||||||
@@ -690,8 +696,8 @@ groupMemberQuery =
|
|||||||
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||||
c.created_at, c.security_code, c.security_code_verified_at, c.enable_pq, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||||
c.peer_chat_min_version, c.peer_chat_max_version
|
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||||
FROM group_members m
|
FROM group_members m
|
||||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||||
LEFT JOIN connections c ON c.connection_id = (
|
LEFT JOIN connections c ON c.connection_id = (
|
||||||
@@ -701,41 +707,41 @@ groupMemberQuery =
|
|||||||
)
|
)
|
||||||
|]
|
|]
|
||||||
|
|
||||||
getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember
|
getGroupMember :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember
|
||||||
getGroupMember db user@User {userId} groupId groupMemberId =
|
getGroupMember db vr user@User {userId} groupId groupMemberId =
|
||||||
ExceptT . firstRow (toContactMember user) (SEGroupMemberNotFound groupMemberId) $
|
ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFound groupMemberId) $
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
(groupMemberQuery <> " WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ?")
|
(groupMemberQuery <> " WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ?")
|
||||||
(userId, groupId, groupMemberId, userId)
|
(userId, groupId, groupMemberId, userId)
|
||||||
|
|
||||||
getGroupMemberById :: DB.Connection -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember
|
getGroupMemberById :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember
|
||||||
getGroupMemberById db user@User {userId} groupMemberId =
|
getGroupMemberById db vr user@User {userId} groupMemberId =
|
||||||
ExceptT . firstRow (toContactMember user) (SEGroupMemberNotFound groupMemberId) $
|
ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFound groupMemberId) $
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
(groupMemberQuery <> " WHERE m.group_member_id = ? AND m.user_id = ?")
|
(groupMemberQuery <> " WHERE m.group_member_id = ? AND m.user_id = ?")
|
||||||
(userId, groupMemberId, userId)
|
(userId, groupMemberId, userId)
|
||||||
|
|
||||||
getGroupMemberByMemberId :: DB.Connection -> User -> GroupInfo -> MemberId -> ExceptT StoreError IO GroupMember
|
getGroupMemberByMemberId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> MemberId -> ExceptT StoreError IO GroupMember
|
||||||
getGroupMemberByMemberId db user@User {userId} GroupInfo {groupId} memberId =
|
getGroupMemberByMemberId db vr user@User {userId} GroupInfo {groupId} memberId =
|
||||||
ExceptT . firstRow (toContactMember user) (SEGroupMemberNotFoundByMemberId memberId) $
|
ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFoundByMemberId memberId) $
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
(groupMemberQuery <> " WHERE m.group_id = ? AND m.member_id = ?")
|
(groupMemberQuery <> " WHERE m.group_id = ? AND m.member_id = ?")
|
||||||
(userId, groupId, memberId)
|
(userId, groupId, memberId)
|
||||||
|
|
||||||
getGroupMembers :: DB.Connection -> User -> GroupInfo -> IO [GroupMember]
|
getGroupMembers :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> IO [GroupMember]
|
||||||
getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do
|
getGroupMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||||
map (toContactMember user)
|
map (toContactMember vr user)
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
(groupMemberQuery <> " WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)")
|
(groupMemberQuery <> " WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)")
|
||||||
(userId, groupId, userId, userContactId)
|
(userId, groupId, userId, userContactId)
|
||||||
|
|
||||||
getGroupMembersForExpiration :: DB.Connection -> User -> GroupInfo -> IO [GroupMember]
|
getGroupMembersForExpiration :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> IO [GroupMember]
|
||||||
getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {groupId} = do
|
getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||||
map (toContactMember user)
|
map (toContactMember vr user)
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
( groupMemberQuery
|
( groupMemberQuery
|
||||||
@@ -749,9 +755,9 @@ getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {gro
|
|||||||
)
|
)
|
||||||
(userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown)
|
(userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown)
|
||||||
|
|
||||||
toContactMember :: User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember
|
toContactMember :: (PQSupport -> VersionRangeChat) -> User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember
|
||||||
toContactMember User {userContactId} (memberRow :. connRow) =
|
toContactMember vr User {userContactId} (memberRow :. connRow) =
|
||||||
(toGroupMember userContactId memberRow) {activeConn = toMaybeConnection connRow}
|
(toGroupMember userContactId memberRow) {activeConn = toMaybeConnection vr connRow}
|
||||||
|
|
||||||
getGroupCurrentMembersCount :: DB.Connection -> User -> GroupInfo -> IO Int
|
getGroupCurrentMembersCount :: DB.Connection -> User -> GroupInfo -> IO Int
|
||||||
getGroupCurrentMembersCount db User {userId} GroupInfo {groupId} = do
|
getGroupCurrentMembersCount db User {userId} GroupInfo {groupId} = do
|
||||||
@@ -767,14 +773,14 @@ getGroupCurrentMembersCount db User {userId} GroupInfo {groupId} = do
|
|||||||
(groupId, userId)
|
(groupId, userId)
|
||||||
pure $ length $ filter memberCurrent' statuses
|
pure $ length $ filter memberCurrent' statuses
|
||||||
|
|
||||||
getGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation
|
getGroupInvitation :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation
|
||||||
getGroupInvitation db vr user groupId =
|
getGroupInvitation db vr user groupId =
|
||||||
getConnRec_ user >>= \case
|
getConnRec_ user >>= \case
|
||||||
Just connRequest -> do
|
Just connRequest -> do
|
||||||
groupInfo@GroupInfo {membership} <- getGroupInfo db vr user groupId
|
groupInfo@GroupInfo {membership} <- getGroupInfo db vr user groupId
|
||||||
when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined
|
when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined
|
||||||
hostId <- getHostMemberId_ db user groupId
|
hostId <- getHostMemberId_ db user groupId
|
||||||
fromMember <- getGroupMember db user groupId hostId
|
fromMember <- getGroupMember db vr user groupId hostId
|
||||||
pure ReceivedGroupInvitation {fromMember, connRequest, groupInfo}
|
pure ReceivedGroupInvitation {fromMember, connRequest, groupInfo}
|
||||||
_ -> throwError SEGroupInvitationNotFound
|
_ -> throwError SEGroupInvitationNotFound
|
||||||
where
|
where
|
||||||
@@ -785,14 +791,14 @@ getGroupInvitation db vr user groupId =
|
|||||||
|
|
||||||
createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> SubscriptionMode -> ExceptT StoreError IO GroupMember
|
createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> SubscriptionMode -> ExceptT StoreError IO GroupMember
|
||||||
createNewContactMember _ _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ _ _ = throwError $ SEContactNotReady localDisplayName
|
createNewContactMember _ _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ _ _ = throwError $ SEContactNotReady localDisplayName
|
||||||
createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile, activeConn = Just Connection {peerChatVRange}} memberRole agentConnId connRequest subMode =
|
createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile, activeConn = Just Connection {connChatVersion, peerChatVRange}} memberRole agentConnId connRequest subMode =
|
||||||
createWithRandomId gVar $ \memId -> do
|
createWithRandomId gVar $ \memId -> do
|
||||||
createdAt <- liftIO getCurrentTime
|
createdAt <- liftIO getCurrentTime
|
||||||
member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt
|
member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt
|
||||||
void $ createMemberConnection_ db userId groupMemberId agentConnId (fromJVersionRange peerChatVRange) Nothing 0 createdAt subMode
|
void $ createMemberConnection_ db userId groupMemberId agentConnId connChatVersion peerChatVRange Nothing 0 createdAt subMode
|
||||||
pure member
|
pure member
|
||||||
where
|
where
|
||||||
JVersionRange (VersionRange minV maxV) = peerChatVRange
|
VersionRange minV maxV = peerChatVRange
|
||||||
invitedByGroupMemberId = groupMemberId' membership
|
invitedByGroupMemberId = groupMemberId' membership
|
||||||
createMember_ memberId createdAt = do
|
createMember_ memberId createdAt = do
|
||||||
insertMember_
|
insertMember_
|
||||||
@@ -832,13 +838,13 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId,
|
|||||||
:. (minV, maxV)
|
:. (minV, maxV)
|
||||||
)
|
)
|
||||||
|
|
||||||
createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO ()
|
createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO ()
|
||||||
createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) peerChatVRange subMode =
|
createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) chatV peerChatVRange subMode =
|
||||||
createWithRandomId gVar $ \memId -> do
|
createWithRandomId gVar $ \memId -> do
|
||||||
createdAt <- liftIO getCurrentTime
|
createdAt <- liftIO getCurrentTime
|
||||||
insertMember_ (MemberId memId) createdAt
|
insertMember_ (MemberId memId) createdAt
|
||||||
groupMemberId <- liftIO $ insertedRowId db
|
groupMemberId <- liftIO $ insertedRowId db
|
||||||
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt subMode
|
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 createdAt subMode
|
||||||
setCommandConnId db user cmdId connId
|
setCommandConnId db user cmdId connId
|
||||||
where
|
where
|
||||||
VersionRange minV maxV = peerChatVRange
|
VersionRange minV maxV = peerChatVRange
|
||||||
@@ -873,7 +879,7 @@ createAcceptedMember
|
|||||||
groupMemberId <- liftIO $ insertedRowId db
|
groupMemberId <- liftIO $ insertedRowId db
|
||||||
pure (groupMemberId, MemberId memId)
|
pure (groupMemberId, MemberId memId)
|
||||||
where
|
where
|
||||||
JVersionRange (VersionRange minV maxV) = cReqChatVRange
|
VersionRange minV maxV = cReqChatVRange
|
||||||
insertMember_ memberId createdAt =
|
insertMember_ memberId createdAt =
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
@@ -889,20 +895,21 @@ createAcceptedMember
|
|||||||
:. (minV, maxV)
|
:. (minV, maxV)
|
||||||
)
|
)
|
||||||
|
|
||||||
createAcceptedMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO ()
|
createAcceptedMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> VersionChat -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO ()
|
||||||
createAcceptedMemberConnection
|
createAcceptedMemberConnection
|
||||||
db
|
db
|
||||||
user@User {userId}
|
user@User {userId}
|
||||||
(cmdId, agentConnId)
|
(cmdId, agentConnId)
|
||||||
|
chatV
|
||||||
UserContactRequest {cReqChatVRange, userContactLinkId}
|
UserContactRequest {cReqChatVRange, userContactLinkId}
|
||||||
groupMemberId
|
groupMemberId
|
||||||
subMode = do
|
subMode = do
|
||||||
createdAt <- liftIO getCurrentTime
|
createdAt <- liftIO getCurrentTime
|
||||||
Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId (fromJVersionRange cReqChatVRange) Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff
|
Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff
|
||||||
setCommandConnId db user cmdId connId
|
setCommandConnId db user cmdId connId
|
||||||
|
|
||||||
getContactViaMember :: DB.Connection -> User -> GroupMember -> ExceptT StoreError IO Contact
|
getContactViaMember :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> ExceptT StoreError IO Contact
|
||||||
getContactViaMember db user@User {userId} GroupMember {groupMemberId} = do
|
getContactViaMember db vr user@User {userId} GroupMember {groupMemberId} = do
|
||||||
contactId <-
|
contactId <-
|
||||||
ExceptT $
|
ExceptT $
|
||||||
firstRow fromOnly (SEContactNotFoundByMemberId groupMemberId) $
|
firstRow fromOnly (SEContactNotFoundByMemberId groupMemberId) $
|
||||||
@@ -916,7 +923,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} = do
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
|]
|
|]
|
||||||
(userId, groupMemberId)
|
(userId, groupMemberId)
|
||||||
getContact db user contactId
|
getContact db vr user contactId
|
||||||
|
|
||||||
setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO ()
|
setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO ()
|
||||||
setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do
|
setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do
|
||||||
@@ -928,15 +935,15 @@ getMemberInvitation db User {userId} groupMemberId =
|
|||||||
fmap join . maybeFirstRow fromOnly $
|
fmap join . maybeFirstRow fromOnly $
|
||||||
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId)
|
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId)
|
||||||
|
|
||||||
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionRangeChat -> SubscriptionMode -> IO ()
|
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO ()
|
||||||
createMemberConnection db userId GroupMember {groupMemberId} agentConnId peerChatVRange subMode = do
|
createMemberConnection db userId GroupMember {groupMemberId} agentConnId chatV peerChatVRange subMode = do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs subMode
|
void $ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode
|
||||||
|
|
||||||
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionRangeChat -> SubscriptionMode -> IO ()
|
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO ()
|
||||||
createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) peerChatVRange subMode = do
|
createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) chatV peerChatVRange subMode = do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs subMode
|
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode
|
||||||
setCommandConnId db user cmdId connId
|
setCommandConnId db user cmdId connId
|
||||||
|
|
||||||
updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO ()
|
updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO ()
|
||||||
@@ -1002,7 +1009,7 @@ createNewMember_
|
|||||||
createdAt = do
|
createdAt = do
|
||||||
let invitedById = fromInvitedBy userContactId invitedBy
|
let invitedById = fromInvitedBy userContactId invitedBy
|
||||||
activeConn = Nothing
|
activeConn = Nothing
|
||||||
mcvr@(VersionRange minV maxV) = maybe chatInitialVRange fromChatVRange memChatVRange
|
memberChatVRange@(VersionRange minV maxV) = maybe chatInitialVRange fromChatVRange memChatVRange
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
@@ -1034,7 +1041,7 @@ createNewMember_
|
|||||||
memberContactId,
|
memberContactId,
|
||||||
memberContactProfileId,
|
memberContactProfileId,
|
||||||
activeConn,
|
activeConn,
|
||||||
memberChatVRange = JVersionRange mcvr
|
memberChatVRange
|
||||||
}
|
}
|
||||||
|
|
||||||
checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId)
|
checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId)
|
||||||
@@ -1162,10 +1169,10 @@ getIntroduction db reMember toMember = ExceptT $ do
|
|||||||
in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation}
|
in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation}
|
||||||
toIntro _ = Left SEIntroNotFound
|
toIntro _ = Left SEIntroNotFound
|
||||||
|
|
||||||
getForwardIntroducedMembers :: DB.Connection -> User -> GroupMember -> Bool -> IO [GroupMember]
|
getForwardIntroducedMembers :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> Bool -> IO [GroupMember]
|
||||||
getForwardIntroducedMembers db user invitee highlyAvailable = do
|
getForwardIntroducedMembers db vr user invitee highlyAvailable = do
|
||||||
memberIds <- map fromOnly <$> query
|
memberIds <- map fromOnly <$> query
|
||||||
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds
|
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||||
where
|
where
|
||||||
mId = groupMemberId' invitee
|
mId = groupMemberId' invitee
|
||||||
query
|
query
|
||||||
@@ -1174,7 +1181,7 @@ getForwardIntroducedMembers db user invitee highlyAvailable = do
|
|||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
(q <> " AND intro_chat_protocol_version >= ?")
|
(q <> " AND intro_chat_protocol_version >= ?")
|
||||||
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, minVersion groupForwardVRange)
|
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, groupForwardVersion)
|
||||||
q =
|
q =
|
||||||
[sql|
|
[sql|
|
||||||
SELECT re_group_member_id
|
SELECT re_group_member_id
|
||||||
@@ -1182,10 +1189,10 @@ getForwardIntroducedMembers db user invitee highlyAvailable = do
|
|||||||
WHERE to_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
WHERE to_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
||||||
|]
|
|]
|
||||||
|
|
||||||
getForwardInvitedMembers :: DB.Connection -> User -> GroupMember -> Bool -> IO [GroupMember]
|
getForwardInvitedMembers :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> Bool -> IO [GroupMember]
|
||||||
getForwardInvitedMembers db user forwardMember highlyAvailable = do
|
getForwardInvitedMembers db vr user forwardMember highlyAvailable = do
|
||||||
memberIds <- map fromOnly <$> query
|
memberIds <- map fromOnly <$> query
|
||||||
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds
|
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||||
where
|
where
|
||||||
mId = groupMemberId' forwardMember
|
mId = groupMemberId' forwardMember
|
||||||
query
|
query
|
||||||
@@ -1194,7 +1201,7 @@ getForwardInvitedMembers db user forwardMember highlyAvailable = do
|
|||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
(q <> " AND intro_chat_protocol_version >= ?")
|
(q <> " AND intro_chat_protocol_version >= ?")
|
||||||
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, minVersion groupForwardVRange)
|
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, groupForwardVersion)
|
||||||
q =
|
q =
|
||||||
[sql|
|
[sql|
|
||||||
SELECT to_group_member_id
|
SELECT to_group_member_id
|
||||||
@@ -1202,12 +1209,13 @@ getForwardInvitedMembers db user forwardMember highlyAvailable = do
|
|||||||
WHERE re_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
WHERE re_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
||||||
|]
|
|]
|
||||||
|
|
||||||
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> Maybe MemberRestrictions -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> ExceptT StoreError IO GroupMember
|
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> VersionChat -> MemberInfo -> Maybe MemberRestrictions -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> ExceptT StoreError IO GroupMember
|
||||||
createIntroReMember
|
createIntroReMember
|
||||||
db
|
db
|
||||||
user@User {userId}
|
user@User {userId}
|
||||||
gInfo@GroupInfo {groupId}
|
gInfo@GroupInfo {groupId}
|
||||||
_host@GroupMember {memberContactId, activeConn}
|
_host@GroupMember {memberContactId, activeConn}
|
||||||
|
chatV
|
||||||
memInfo@(MemberInfo _ _ memChatVRange memberProfile)
|
memInfo@(MemberInfo _ _ memChatVRange memberProfile)
|
||||||
memRestrictions_
|
memRestrictions_
|
||||||
(groupCmdId, groupAgentConnId)
|
(groupCmdId, groupAgentConnId)
|
||||||
@@ -1220,7 +1228,7 @@ createIntroReMember
|
|||||||
currentTs <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
newMember <- case directConnIds of
|
newMember <- case directConnIds of
|
||||||
Just (directCmdId, directAgentConnId) -> do
|
Just (directCmdId, directAgentConnId) -> do
|
||||||
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff
|
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId chatV mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff
|
||||||
liftIO $ setCommandConnId db user directCmdId directConnId
|
liftIO $ setCommandConnId db user directCmdId directConnId
|
||||||
(localDisplayName, contactId, memProfileId) <- createContact_ db userId memberProfile "" (Just groupId) currentTs False
|
(localDisplayName, contactId, memProfileId) <- createContact_ db userId memberProfile "" (Just groupId) currentTs False
|
||||||
liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, directConnId)
|
liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, directConnId)
|
||||||
@@ -1230,18 +1238,18 @@ createIntroReMember
|
|||||||
pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memRestriction, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId}
|
pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memRestriction, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId}
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
member <- createNewMember_ db user gInfo newMember currentTs
|
member <- createNewMember_ db user gInfo newMember currentTs
|
||||||
conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId mcvr memberContactId cLevel currentTs subMode
|
conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId chatV mcvr memberContactId cLevel currentTs subMode
|
||||||
liftIO $ setCommandConnId db user groupCmdId groupConnId
|
liftIO $ setCommandConnId db user groupCmdId groupConnId
|
||||||
pure (member :: GroupMember) {activeConn = Just conn}
|
pure (member :: GroupMember) {activeConn = Just conn}
|
||||||
|
|
||||||
createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionRangeChat -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO ()
|
createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionChat -> VersionRangeChat -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO ()
|
||||||
createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do
|
createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} chatV mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do
|
||||||
let cLevel = 1 + maybe 0 (\Connection {connLevel} -> connLevel) activeConn
|
let cLevel = 1 + maybe 0 (\Connection {connLevel} -> connLevel) activeConn
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId mcvr viaContactId cLevel currentTs subMode
|
Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId chatV mcvr viaContactId cLevel currentTs subMode
|
||||||
setCommandConnId db user groupCmdId groupConnId
|
setCommandConnId db user groupCmdId groupConnId
|
||||||
forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do
|
forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do
|
||||||
Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff
|
Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId chatV mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff
|
||||||
setCommandConnId db user directCmdId directConnId
|
setCommandConnId db user directCmdId directConnId
|
||||||
contactId <- createMemberContact_ directConnId currentTs
|
contactId <- createMemberContact_ directConnId currentTs
|
||||||
updateMember_ contactId currentTs
|
updateMember_ contactId currentTs
|
||||||
@@ -1271,11 +1279,11 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId =
|
|||||||
|]
|
|]
|
||||||
[":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId]
|
[":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId]
|
||||||
|
|
||||||
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionRangeChat -> Maybe Int64 -> Int -> UTCTime -> SubscriptionMode -> IO Connection
|
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionChat -> VersionRangeChat -> Maybe Int64 -> Int -> UTCTime -> SubscriptionMode -> IO Connection
|
||||||
createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange viaContact connLevel currentTs subMode =
|
createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange viaContact connLevel currentTs subMode =
|
||||||
createConnection_ db userId ConnMember (Just groupMemberId) agentConnId peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff
|
createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff
|
||||||
|
|
||||||
getViaGroupMember :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember))
|
getViaGroupMember :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember))
|
||||||
getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
|
getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
|
||||||
maybeFirstRow toGroupAndMember $
|
maybeFirstRow toGroupAndMember $
|
||||||
DB.query
|
DB.query
|
||||||
@@ -1296,8 +1304,8 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
|
|||||||
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||||
c.created_at, c.security_code, c.security_code_verified_at, c.enable_pq, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||||
c.peer_chat_min_version, c.peer_chat_max_version
|
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||||
FROM group_members m
|
FROM group_members m
|
||||||
JOIN contacts ct ON ct.contact_id = m.contact_id
|
JOIN contacts ct ON ct.contact_id = m.contact_id
|
||||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||||
@@ -1318,10 +1326,10 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
|
|||||||
toGroupAndMember (groupInfoRow :. memberRow :. connRow) =
|
toGroupAndMember (groupInfoRow :. memberRow :. connRow) =
|
||||||
let groupInfo = toGroupInfo vr userContactId groupInfoRow
|
let groupInfo = toGroupInfo vr userContactId groupInfoRow
|
||||||
member = toGroupMember userContactId memberRow
|
member = toGroupMember userContactId memberRow
|
||||||
in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow})
|
in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection vr connRow})
|
||||||
|
|
||||||
getViaGroupContact :: DB.Connection -> User -> GroupMember -> IO (Maybe Contact)
|
getViaGroupContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> IO (Maybe Contact)
|
||||||
getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = do
|
getViaGroupContact db vr user@User {userId} GroupMember {groupMemberId} = do
|
||||||
contactId_ <-
|
contactId_ <-
|
||||||
maybeFirstRow fromOnly $
|
maybeFirstRow fromOnly $
|
||||||
DB.query
|
DB.query
|
||||||
@@ -1335,7 +1343,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = do
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
|]
|
|]
|
||||||
(userId, groupMemberId)
|
(userId, groupMemberId)
|
||||||
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db user) contactId_
|
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db vr user) contactId_
|
||||||
|
|
||||||
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
||||||
updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
|
updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
|
||||||
@@ -1371,7 +1379,7 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName,
|
|||||||
(ldn, currentTs, userId, groupId)
|
(ldn, currentTs, userId, groupId)
|
||||||
safeDeleteLDN db user localDisplayName
|
safeDeleteLDN db user localDisplayName
|
||||||
|
|
||||||
getGroupInfo :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO GroupInfo
|
getGroupInfo :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO GroupInfo
|
||||||
getGroupInfo db vr User {userId, userContactId} groupId =
|
getGroupInfo db vr User {userId, userContactId} groupId =
|
||||||
ExceptT . firstRow (toGroupInfo vr userContactId) (SEGroupNotFound groupId) $
|
ExceptT . firstRow (toGroupInfo vr userContactId) (SEGroupNotFound groupId) $
|
||||||
DB.query
|
DB.query
|
||||||
@@ -1394,7 +1402,7 @@ getGroupInfo db vr User {userId, userContactId} groupId =
|
|||||||
|]
|
|]
|
||||||
(groupId, userId, userContactId)
|
(groupId, userId, userContactId)
|
||||||
|
|
||||||
getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo)
|
getGroupInfoByUserContactLinkConnReq :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo)
|
||||||
getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do
|
getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do
|
||||||
groupId_ <-
|
groupId_ <-
|
||||||
maybeFirstRow fromOnly $
|
maybeFirstRow fromOnly $
|
||||||
@@ -1408,7 +1416,7 @@ getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReq
|
|||||||
(userId, cReqSchema1, cReqSchema2)
|
(userId, cReqSchema1, cReqSchema2)
|
||||||
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_
|
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_
|
||||||
|
|
||||||
getGroupInfoByGroupLinkHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo)
|
getGroupInfoByGroupLinkHash :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo)
|
||||||
getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do
|
getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do
|
||||||
groupId_ <-
|
groupId_ <-
|
||||||
maybeFirstRow fromOnly $
|
maybeFirstRow fromOnly $
|
||||||
@@ -1435,7 +1443,7 @@ getGroupMemberIdByName db User {userId} groupId groupMemberName =
|
|||||||
ExceptT . firstRow fromOnly (SEGroupMemberNameNotFound groupId groupMemberName) $
|
ExceptT . firstRow fromOnly (SEGroupMemberNameNotFound groupId groupMemberName) $
|
||||||
DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND local_display_name = ?" (userId, groupId, groupMemberName)
|
DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND local_display_name = ?" (userId, groupId, groupMemberName)
|
||||||
|
|
||||||
getActiveMembersByName :: DB.Connection -> VersionRangeChat -> User -> ContactName -> ExceptT StoreError IO [(GroupInfo, GroupMember)]
|
getActiveMembersByName :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactName -> ExceptT StoreError IO [(GroupInfo, GroupMember)]
|
||||||
getActiveMembersByName db vr user@User {userId} groupMemberName = do
|
getActiveMembersByName db vr user@User {userId} groupMemberName = do
|
||||||
groupMemberIds :: [(GroupId, GroupMemberId)] <-
|
groupMemberIds :: [(GroupId, GroupMemberId)] <-
|
||||||
liftIO $
|
liftIO $
|
||||||
@@ -1450,19 +1458,19 @@ getActiveMembersByName db vr user@User {userId} groupMemberName = do
|
|||||||
(userId, groupMemberName, GSMemConnected, GSMemComplete, GCUserMember)
|
(userId, groupMemberName, GSMemConnected, GSMemComplete, GCUserMember)
|
||||||
possibleMembers <- forM groupMemberIds $ \(groupId, groupMemberId) -> do
|
possibleMembers <- forM groupMemberIds $ \(groupId, groupMemberId) -> do
|
||||||
groupInfo <- getGroupInfo db vr user groupId
|
groupInfo <- getGroupInfo db vr user groupId
|
||||||
groupMember <- getGroupMember db user groupId groupMemberId
|
groupMember <- getGroupMember db vr user groupId groupMemberId
|
||||||
pure (groupInfo, groupMember)
|
pure (groupInfo, groupMember)
|
||||||
pure $ sortOn (Down . ts . fst) possibleMembers
|
pure $ sortOn (Down . ts . fst) possibleMembers
|
||||||
where
|
where
|
||||||
ts GroupInfo {chatTs, updatedAt} = fromMaybe updatedAt chatTs
|
ts GroupInfo {chatTs, updatedAt} = fromMaybe updatedAt chatTs
|
||||||
|
|
||||||
getMatchingContacts :: DB.Connection -> User -> Contact -> IO [Contact]
|
getMatchingContacts :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> IO [Contact]
|
||||||
getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do
|
getMatchingContacts db vr user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do
|
||||||
contactIds <-
|
contactIds <-
|
||||||
map fromOnly <$> case image of
|
map fromOnly <$> case image of
|
||||||
Just img -> DB.query db (q <> " AND p.image = ?") (userId, contactId, CSActive, displayName, fullName, img)
|
Just img -> DB.query db (q <> " AND p.image = ?") (userId, contactId, CSActive, displayName, fullName, img)
|
||||||
Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, contactId, CSActive, displayName, fullName)
|
Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, contactId, CSActive, displayName, fullName)
|
||||||
rights <$> mapM (runExceptT . getContact db user) contactIds
|
rights <$> mapM (runExceptT . getContact db vr user) contactIds
|
||||||
where
|
where
|
||||||
-- this query is different from one in getMatchingMemberContacts
|
-- this query is different from one in getMatchingMemberContacts
|
||||||
-- it checks that it's not the same contact
|
-- it checks that it's not the same contact
|
||||||
@@ -1476,13 +1484,13 @@ getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalPro
|
|||||||
AND p.display_name = ? AND p.full_name = ?
|
AND p.display_name = ? AND p.full_name = ?
|
||||||
|]
|
|]
|
||||||
|
|
||||||
getMatchingMembers :: DB.Connection -> User -> Contact -> IO [GroupMember]
|
getMatchingMembers :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> IO [GroupMember]
|
||||||
getMatchingMembers db user@User {userId} Contact {profile = LocalProfile {displayName, fullName, image}} = do
|
getMatchingMembers db vr user@User {userId} Contact {profile = LocalProfile {displayName, fullName, image}} = do
|
||||||
memberIds <-
|
memberIds <-
|
||||||
map fromOnly <$> case image of
|
map fromOnly <$> case image of
|
||||||
Just img -> DB.query db (q <> " AND p.image = ?") (userId, GCUserMember, displayName, fullName, img)
|
Just img -> DB.query db (q <> " AND p.image = ?") (userId, GCUserMember, displayName, fullName, img)
|
||||||
Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, GCUserMember, displayName, fullName)
|
Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, GCUserMember, displayName, fullName)
|
||||||
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds
|
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||||
where
|
where
|
||||||
-- only match with members without associated contact
|
-- only match with members without associated contact
|
||||||
q =
|
q =
|
||||||
@@ -1495,14 +1503,14 @@ getMatchingMembers db user@User {userId} Contact {profile = LocalProfile {displa
|
|||||||
AND p.display_name = ? AND p.full_name = ?
|
AND p.display_name = ? AND p.full_name = ?
|
||||||
|]
|
|]
|
||||||
|
|
||||||
getMatchingMemberContacts :: DB.Connection -> User -> GroupMember -> IO [Contact]
|
getMatchingMemberContacts :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> IO [Contact]
|
||||||
getMatchingMemberContacts _ _ GroupMember {memberContactId = Just _} = pure []
|
getMatchingMemberContacts _ _ _ GroupMember {memberContactId = Just _} = pure []
|
||||||
getMatchingMemberContacts db user@User {userId} GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} = do
|
getMatchingMemberContacts db vr user@User {userId} GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} = do
|
||||||
contactIds <-
|
contactIds <-
|
||||||
map fromOnly <$> case image of
|
map fromOnly <$> case image of
|
||||||
Just img -> DB.query db (q <> " AND p.image = ?") (userId, CSActive, displayName, fullName, img)
|
Just img -> DB.query db (q <> " AND p.image = ?") (userId, CSActive, displayName, fullName, img)
|
||||||
Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, CSActive, displayName, fullName)
|
Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, CSActive, displayName, fullName)
|
||||||
rights <$> mapM (runExceptT . getContact db user) contactIds
|
rights <$> mapM (runExceptT . getContact db vr user) contactIds
|
||||||
where
|
where
|
||||||
q =
|
q =
|
||||||
[sql|
|
[sql|
|
||||||
@@ -1534,8 +1542,8 @@ createSentProbeHash db userId probeId to = do
|
|||||||
"INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, group_member_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
|
"INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, group_member_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
|
||||||
(probeId, ctId, gmId, userId, currentTs, currentTs)
|
(probeId, ctId, gmId, userId, currentTs, currentTs)
|
||||||
|
|
||||||
matchReceivedProbe :: DB.Connection -> User -> ContactOrMember -> Probe -> IO [ContactOrMember]
|
matchReceivedProbe :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactOrMember -> Probe -> IO [ContactOrMember]
|
||||||
matchReceivedProbe db user@User {userId} from (Probe probe) = do
|
matchReceivedProbe db vr user@User {userId} from (Probe probe) = do
|
||||||
let probeHash = C.sha256Hash probe
|
let probeHash = C.sha256Hash probe
|
||||||
cgmIds <-
|
cgmIds <-
|
||||||
DB.query
|
DB.query
|
||||||
@@ -1556,7 +1564,7 @@ matchReceivedProbe db user@User {userId} from (Probe probe) = do
|
|||||||
"INSERT INTO received_probes (contact_id, group_member_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
"INSERT INTO received_probes (contact_id, group_member_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||||
(ctId, gmId, probe, probeHash, userId, currentTs, currentTs)
|
(ctId, gmId, probe, probeHash, userId, currentTs, currentTs)
|
||||||
let cgmIds' = filterFirstContactId cgmIds
|
let cgmIds' = filterFirstContactId cgmIds
|
||||||
catMaybes <$> mapM (getContactOrMember_ db user) cgmIds'
|
catMaybes <$> mapM (getContactOrMember_ db vr user) cgmIds'
|
||||||
where
|
where
|
||||||
filterFirstContactId :: [(Maybe ContactId, Maybe GroupId, Maybe GroupMemberId)] -> [(Maybe ContactId, Maybe GroupId, Maybe GroupMemberId)]
|
filterFirstContactId :: [(Maybe ContactId, Maybe GroupId, Maybe GroupMemberId)] -> [(Maybe ContactId, Maybe GroupId, Maybe GroupMemberId)]
|
||||||
filterFirstContactId cgmIds = do
|
filterFirstContactId cgmIds = do
|
||||||
@@ -1566,8 +1574,8 @@ matchReceivedProbe db user@User {userId} from (Probe probe) = do
|
|||||||
(x : _) -> [x]
|
(x : _) -> [x]
|
||||||
ctIds' <> memIds
|
ctIds' <> memIds
|
||||||
|
|
||||||
matchReceivedProbeHash :: DB.Connection -> User -> ContactOrMember -> ProbeHash -> IO (Maybe (ContactOrMember, Probe))
|
matchReceivedProbeHash :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactOrMember -> ProbeHash -> IO (Maybe (ContactOrMember, Probe))
|
||||||
matchReceivedProbeHash db user@User {userId} from (ProbeHash probeHash) = do
|
matchReceivedProbeHash db vr user@User {userId} from (ProbeHash probeHash) = do
|
||||||
probeIds <-
|
probeIds <-
|
||||||
maybeFirstRow id $
|
maybeFirstRow id $
|
||||||
DB.query
|
DB.query
|
||||||
@@ -1587,11 +1595,11 @@ matchReceivedProbeHash db user@User {userId} from (ProbeHash probeHash) = do
|
|||||||
db
|
db
|
||||||
"INSERT INTO received_probes (contact_id, group_member_id, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
|
"INSERT INTO received_probes (contact_id, group_member_id, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
|
||||||
(ctId, gmId, probeHash, userId, currentTs, currentTs)
|
(ctId, gmId, probeHash, userId, currentTs, currentTs)
|
||||||
pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrMember_ db user cgmIds
|
pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrMember_ db vr user cgmIds
|
||||||
|
|
||||||
matchSentProbe :: DB.Connection -> User -> ContactOrMember -> Probe -> IO (Maybe ContactOrMember)
|
matchSentProbe :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactOrMember -> Probe -> IO (Maybe ContactOrMember)
|
||||||
matchSentProbe db user@User {userId} _from (Probe probe) = do
|
matchSentProbe db vr user@User {userId} _from (Probe probe) = do
|
||||||
cgmIds $>>= getContactOrMember_ db user
|
cgmIds $>>= getContactOrMember_ db vr user
|
||||||
where
|
where
|
||||||
(ctId, gmId) = contactOrMemberIds _from
|
(ctId, gmId) = contactOrMemberIds _from
|
||||||
cgmIds =
|
cgmIds =
|
||||||
@@ -1610,16 +1618,16 @@ matchSentProbe db user@User {userId} _from (Probe probe) = do
|
|||||||
|]
|
|]
|
||||||
(userId, probe, ctId, gmId)
|
(userId, probe, ctId, gmId)
|
||||||
|
|
||||||
getContactOrMember_ :: DB.Connection -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrMember)
|
getContactOrMember_ :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrMember)
|
||||||
getContactOrMember_ db user ids =
|
getContactOrMember_ db vr user ids =
|
||||||
fmap eitherToMaybe . runExceptT $ case ids of
|
fmap eitherToMaybe . runExceptT $ case ids of
|
||||||
(Just ctId, _, _) -> COMContact <$> getContact db user ctId
|
(Just ctId, _, _) -> COMContact <$> getContact db vr user ctId
|
||||||
(_, Just gId, Just gmId) -> COMGroupMember <$> getGroupMember db user gId gmId
|
(_, Just gId, Just gmId) -> COMGroupMember <$> getGroupMember db vr user gId gmId
|
||||||
_ -> throwError $ SEInternalError ""
|
_ -> throwError $ SEInternalError ""
|
||||||
|
|
||||||
-- if requested merge direction is overruled (toFromContacts), keepLDN is kept
|
-- if requested merge direction is overruled (toFromContacts), keepLDN is kept
|
||||||
mergeContactRecords :: DB.Connection -> User -> Contact -> Contact -> ExceptT StoreError IO Contact
|
mergeContactRecords :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> Contact -> ExceptT StoreError IO Contact
|
||||||
mergeContactRecords db user@User {userId} to@Contact {localDisplayName = keepLDN} from = do
|
mergeContactRecords db vr user@User {userId} to@Contact {localDisplayName = keepLDN} from = do
|
||||||
let (toCt, fromCt) = toFromContacts to from
|
let (toCt, fromCt) = toFromContacts to from
|
||||||
Contact {contactId = toContactId, localDisplayName = toLDN} = toCt
|
Contact {contactId = toContactId, localDisplayName = toLDN} = toCt
|
||||||
Contact {contactId = fromContactId, localDisplayName = fromLDN} = fromCt
|
Contact {contactId = fromContactId, localDisplayName = fromLDN} = fromCt
|
||||||
@@ -1677,7 +1685,7 @@ mergeContactRecords db user@User {userId} to@Contact {localDisplayName = keepLDN
|
|||||||
WHERE user_id = ? AND local_display_name = ?
|
WHERE user_id = ? AND local_display_name = ?
|
||||||
|]
|
|]
|
||||||
(keepLDN, currentTs, userId, toLDN)
|
(keepLDN, currentTs, userId, toLDN)
|
||||||
getContact db user toContactId
|
getContact db vr user toContactId
|
||||||
where
|
where
|
||||||
toFromContacts :: Contact -> Contact -> (Contact, Contact)
|
toFromContacts :: Contact -> Contact -> (Contact, Contact)
|
||||||
toFromContacts c1 c2
|
toFromContacts c1 c2
|
||||||
@@ -1708,9 +1716,10 @@ associateMemberWithContactRecord
|
|||||||
when (memProfileId /= profileId) $ deleteUnusedProfile_ db userId memProfileId
|
when (memProfileId /= profileId) $ deleteUnusedProfile_ db userId memProfileId
|
||||||
when (memLDN /= localDisplayName) $ deleteUnusedDisplayName_ db userId memLDN
|
when (memLDN /= localDisplayName) $ deleteUnusedDisplayName_ db userId memLDN
|
||||||
|
|
||||||
associateContactWithMemberRecord :: DB.Connection -> User -> GroupMember -> Contact -> ExceptT StoreError IO Contact
|
associateContactWithMemberRecord :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> Contact -> ExceptT StoreError IO Contact
|
||||||
associateContactWithMemberRecord
|
associateContactWithMemberRecord
|
||||||
db
|
db
|
||||||
|
vr
|
||||||
user@User {userId}
|
user@User {userId}
|
||||||
GroupMember {groupId, groupMemberId, localDisplayName = memLDN, memberProfile = LocalProfile {profileId = memProfileId}}
|
GroupMember {groupId, groupMemberId, localDisplayName = memLDN, memberProfile = LocalProfile {profileId = memProfileId}}
|
||||||
Contact {contactId, localDisplayName, profile = LocalProfile {profileId}} = do
|
Contact {contactId, localDisplayName, profile = LocalProfile {profileId}} = do
|
||||||
@@ -1734,7 +1743,7 @@ associateContactWithMemberRecord
|
|||||||
(memLDN, memProfileId, currentTs, userId, contactId)
|
(memLDN, memProfileId, currentTs, userId, contactId)
|
||||||
when (profileId /= memProfileId) $ deleteUnusedProfile_ db userId profileId
|
when (profileId /= memProfileId) $ deleteUnusedProfile_ db userId profileId
|
||||||
when (localDisplayName /= memLDN) $ deleteUnusedDisplayName_ db userId localDisplayName
|
when (localDisplayName /= memLDN) $ deleteUnusedDisplayName_ db userId localDisplayName
|
||||||
getContact db user contactId
|
getContact db vr user contactId
|
||||||
|
|
||||||
deleteUnusedDisplayName_ :: DB.Connection -> UserId -> ContactName -> IO ()
|
deleteUnusedDisplayName_ :: DB.Connection -> UserId -> ContactName -> IO ()
|
||||||
deleteUnusedDisplayName_ db userId localDisplayName =
|
deleteUnusedDisplayName_ db userId localDisplayName =
|
||||||
@@ -1882,7 +1891,7 @@ createMemberContact
|
|||||||
cReq
|
cReq
|
||||||
gInfo
|
gInfo
|
||||||
GroupMember {groupMemberId, localDisplayName, memberProfile, memberContactProfileId}
|
GroupMember {groupMemberId, localDisplayName, memberProfile, memberContactProfileId}
|
||||||
Connection {connLevel, peerChatVRange = peerChatVRange@(JVersionRange (VersionRange minV maxV))}
|
Connection {connLevel, connChatVersion, peerChatVRange = peerChatVRange@(VersionRange minV maxV)}
|
||||||
subMode = do
|
subMode = do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
let incognitoProfile = incognitoMembershipProfile gInfo
|
let incognitoProfile = incognitoMembershipProfile gInfo
|
||||||
@@ -1909,11 +1918,11 @@ createMemberContact
|
|||||||
[sql|
|
[sql|
|
||||||
INSERT INTO connections (
|
INSERT INTO connections (
|
||||||
user_id, agent_conn_id, conn_req_inv, conn_level, conn_status, conn_type, contact_conn_initiated, contact_id, custom_user_profile_id,
|
user_id, agent_conn_id, conn_req_inv, conn_level, conn_status, conn_type, contact_conn_initiated, contact_id, custom_user_profile_id,
|
||||||
peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe
|
conn_chat_version, peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
( (userId, acId, cReq, connLevel, ConnNew, ConnContact, True, contactId, customUserProfileId)
|
( (userId, acId, cReq, connLevel, ConnNew, ConnContact, True, contactId, customUserProfileId)
|
||||||
:. (minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate)
|
:. (connChatVersion, minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate)
|
||||||
)
|
)
|
||||||
connId <- insertedRowId db
|
connId <- insertedRowId db
|
||||||
let ctConn =
|
let ctConn =
|
||||||
@@ -1921,6 +1930,7 @@ createMemberContact
|
|||||||
{ connId,
|
{ connId,
|
||||||
agentConnId = AgentConnId acId,
|
agentConnId = AgentConnId acId,
|
||||||
peerChatVRange,
|
peerChatVRange,
|
||||||
|
connChatVersion,
|
||||||
connType = ConnContact,
|
connType = ConnContact,
|
||||||
contactConnInitiated = True,
|
contactConnInitiated = True,
|
||||||
entityId = Just contactId,
|
entityId = Just contactId,
|
||||||
@@ -1943,14 +1953,14 @@ createMemberContact
|
|||||||
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
|
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
|
||||||
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False}
|
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False}
|
||||||
|
|
||||||
getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
|
getMemberContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
|
||||||
getMemberContact db vr user contactId = do
|
getMemberContact db vr user contactId = do
|
||||||
ct <- getContact db user contactId
|
ct <- getContact db vr user contactId
|
||||||
let Contact {contactGroupMemberId, activeConn} = ct
|
let Contact {contactGroupMemberId, activeConn} = ct
|
||||||
case (activeConn, contactGroupMemberId) of
|
case (activeConn, contactGroupMemberId) of
|
||||||
(Just Connection {connId}, Just groupMemberId) -> do
|
(Just Connection {connId}, Just groupMemberId) -> do
|
||||||
cReq <- getConnReqInv db connId
|
cReq <- getConnReqInv db connId
|
||||||
m@GroupMember {groupId} <- getGroupMemberById db user groupMemberId
|
m@GroupMember {groupId} <- getGroupMemberById db vr user groupMemberId
|
||||||
g <- getGroupInfo db vr user groupId
|
g <- getGroupInfo db vr user groupId
|
||||||
pure (g, m, ct, cReq)
|
pure (g, m, ct, cReq)
|
||||||
_ ->
|
_ ->
|
||||||
@@ -2030,7 +2040,7 @@ createMemberContactConn_
|
|||||||
user@User {userId}
|
user@User {userId}
|
||||||
(cmdId, acId)
|
(cmdId, acId)
|
||||||
gInfo
|
gInfo
|
||||||
_memberConn@Connection {connLevel, peerChatVRange = peerChatVRange@(JVersionRange (VersionRange minV maxV))}
|
_memberConn@Connection {connLevel, connChatVersion, peerChatVRange = peerChatVRange@(VersionRange minV maxV)}
|
||||||
contactId
|
contactId
|
||||||
subMode = do
|
subMode = do
|
||||||
currentTs <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
@@ -2040,11 +2050,11 @@ createMemberContactConn_
|
|||||||
[sql|
|
[sql|
|
||||||
INSERT INTO connections (
|
INSERT INTO connections (
|
||||||
user_id, agent_conn_id, conn_level, conn_status, conn_type, contact_id, custom_user_profile_id,
|
user_id, agent_conn_id, conn_level, conn_status, conn_type, contact_id, custom_user_profile_id,
|
||||||
peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe
|
conn_chat_version, peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
( (userId, acId, connLevel, ConnJoined, ConnContact, contactId, customUserProfileId)
|
( (userId, acId, connLevel, ConnJoined, ConnContact, contactId, customUserProfileId)
|
||||||
:. (minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate)
|
:. (connChatVersion, minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate)
|
||||||
)
|
)
|
||||||
connId <- insertedRowId db
|
connId <- insertedRowId db
|
||||||
setCommandConnId db user cmdId connId
|
setCommandConnId db user cmdId connId
|
||||||
@@ -2052,6 +2062,7 @@ createMemberContactConn_
|
|||||||
Connection
|
Connection
|
||||||
{ connId,
|
{ connId,
|
||||||
agentConnId = AgentConnId acId,
|
agentConnId = AgentConnId acId,
|
||||||
|
connChatVersion,
|
||||||
peerChatVRange,
|
peerChatVRange,
|
||||||
connType = ConnContact,
|
connType = ConnContact,
|
||||||
contactConnInitiated = False,
|
contactConnInitiated = False,
|
||||||
@@ -2122,7 +2133,7 @@ setXGrpLinkMemReceived db mId xGrpLinkMemReceived = do
|
|||||||
"UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ?"
|
"UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ?"
|
||||||
(xGrpLinkMemReceived, currentTs, mId)
|
(xGrpLinkMemReceived, currentTs, mId)
|
||||||
|
|
||||||
createNewUnknownGroupMember :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> Text -> ExceptT StoreError IO GroupMember
|
createNewUnknownGroupMember :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> MemberId -> Text -> ExceptT StoreError IO GroupMember
|
||||||
createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {groupId} memberId memberName = do
|
createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {groupId} memberId memberName = do
|
||||||
currentTs <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
let memberProfile = profileFromName memberName
|
let memberProfile = profileFromName memberName
|
||||||
@@ -2142,12 +2153,12 @@ createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {g
|
|||||||
:. (minV, maxV)
|
:. (minV, maxV)
|
||||||
)
|
)
|
||||||
insertedRowId db
|
insertedRowId db
|
||||||
getGroupMemberById db user groupMemberId
|
getGroupMemberById db vr user groupMemberId
|
||||||
where
|
where
|
||||||
VersionRange minV maxV = vr
|
VersionRange minV maxV = vr PQSupportOff
|
||||||
|
|
||||||
updateUnknownMemberAnnounced :: DB.Connection -> User -> GroupMember -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember
|
updateUnknownMemberAnnounced :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember
|
||||||
updateUnknownMemberAnnounced db user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do
|
updateUnknownMemberAnnounced db vr user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do
|
||||||
_ <- updateMemberProfile db user unknownMember profile
|
_ <- updateMemberProfile db user unknownMember profile
|
||||||
currentTs <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
liftIO $
|
liftIO $
|
||||||
@@ -2167,9 +2178,9 @@ updateUnknownMemberAnnounced db user@User {userId} invitingMember unknownMember@
|
|||||||
( (memberRole, GCPostMember, GSMemAnnounced, groupMemberId' invitingMember)
|
( (memberRole, GCPostMember, GSMemAnnounced, groupMemberId' invitingMember)
|
||||||
:. (minV, maxV, currentTs, userId, groupMemberId)
|
:. (minV, maxV, currentTs, userId, groupMemberId)
|
||||||
)
|
)
|
||||||
getGroupMemberById db user groupMemberId
|
getGroupMemberById db vr user groupMemberId
|
||||||
where
|
where
|
||||||
VersionRange minV maxV = maybe (fromJVersionRange memberChatVRange) fromChatVRange v
|
VersionRange minV maxV = maybe memberChatVRange fromChatVRange v
|
||||||
|
|
||||||
updateUserMemberProfileSentAt :: DB.Connection -> User -> GroupInfo -> UTCTime -> IO ()
|
updateUserMemberProfileSentAt :: DB.Connection -> User -> GroupInfo -> UTCTime -> IO ()
|
||||||
updateUserMemberProfileSentAt db User {userId} GroupInfo {groupId} sentTs =
|
updateUserMemberProfileSentAt db User {userId} GroupInfo {groupId} sentTs =
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserI
|
|||||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
|
import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
|
import Simplex.Messaging.Crypto.Ratchet (PQSupport)
|
||||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||||
import Simplex.Messaging.Util (eitherToMaybe)
|
import Simplex.Messaging.Util (eitherToMaybe)
|
||||||
import UnliftIO.STM
|
import UnliftIO.STM
|
||||||
@@ -470,7 +471,8 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe
|
|||||||
FROM group_members m
|
FROM group_members m
|
||||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||||
LEFT JOIN contacts c ON m.contact_id = c.contact_id
|
LEFT JOIN contacts c ON m.contact_id = c.contact_id
|
||||||
LEFT JOIN chat_items i ON i.group_id = m.group_id
|
LEFT JOIN chat_items i ON i.user_id = m.user_id
|
||||||
|
AND i.group_id = m.group_id
|
||||||
AND m.group_member_id = i.group_member_id
|
AND m.group_member_id = i.group_member_id
|
||||||
AND i.shared_msg_id = :msg_id
|
AND i.shared_msg_id = :msg_id
|
||||||
WHERE m.user_id = :user_id AND m.group_id = :group_id AND m.member_id = :member_id
|
WHERE m.user_id = :user_id AND m.group_id = :group_id AND m.member_id = :member_id
|
||||||
@@ -481,7 +483,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe
|
|||||||
ciQuoteGroup [] = ciQuote Nothing $ CIQGroupRcv Nothing
|
ciQuoteGroup [] = ciQuote Nothing $ CIQGroupRcv Nothing
|
||||||
ciQuoteGroup ((Only itemId :. memberRow) : _) = ciQuote itemId . CIQGroupRcv . Just $ toGroupMember userContactId memberRow
|
ciQuoteGroup ((Only itemId :. memberRow) : _) = ciQuote itemId . CIQGroupRcv . Just $ toGroupMember userContactId memberRow
|
||||||
|
|
||||||
getChatPreviews :: DB.Connection -> VersionRangeChat -> User -> Bool -> PaginationByTime -> ChatListQuery -> IO [Either StoreError AChat]
|
getChatPreviews :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Bool -> PaginationByTime -> ChatListQuery -> IO [Either StoreError AChat]
|
||||||
getChatPreviews db vr user withPCC pagination query = do
|
getChatPreviews db vr user withPCC pagination query = do
|
||||||
directChats <- findDirectChatPreviews_ db user pagination query
|
directChats <- findDirectChatPreviews_ db user pagination query
|
||||||
groupChats <- findGroupChatPreviews_ db user pagination query
|
groupChats <- findGroupChatPreviews_ db user pagination query
|
||||||
@@ -504,7 +506,7 @@ getChatPreviews db vr user withPCC pagination query = do
|
|||||||
PTBefore _ count -> take count . sortBy (comparing $ Down . ts)
|
PTBefore _ count -> take count . sortBy (comparing $ Down . ts)
|
||||||
getChatPreview :: AChatPreviewData -> ExceptT StoreError IO AChat
|
getChatPreview :: AChatPreviewData -> ExceptT StoreError IO AChat
|
||||||
getChatPreview (ACPD cType cpd) = case cType of
|
getChatPreview (ACPD cType cpd) = case cType of
|
||||||
SCTDirect -> getDirectChatPreview_ db user cpd
|
SCTDirect -> getDirectChatPreview_ db vr user cpd
|
||||||
SCTGroup -> getGroupChatPreview_ db vr user cpd
|
SCTGroup -> getGroupChatPreview_ db vr user cpd
|
||||||
SCTLocal -> getLocalChatPreview_ db user cpd
|
SCTLocal -> getLocalChatPreview_ db user cpd
|
||||||
SCTContactRequest -> let (ContactRequestPD _ chat) = cpd in pure chat
|
SCTContactRequest -> let (ContactRequestPD _ chat) = cpd in pure chat
|
||||||
@@ -618,9 +620,9 @@ findDirectChatPreviews_ db User {userId} pagination clq =
|
|||||||
)
|
)
|
||||||
([":user_id" := userId, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams)
|
([":user_id" := userId, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams)
|
||||||
|
|
||||||
getDirectChatPreview_ :: DB.Connection -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat
|
getDirectChatPreview_ :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat
|
||||||
getDirectChatPreview_ db user (DirectChatPD _ contactId lastItemId_ stats) = do
|
getDirectChatPreview_ db vr user (DirectChatPD _ contactId lastItemId_ stats) = do
|
||||||
contact <- getContact db user contactId
|
contact <- getContact db vr user contactId
|
||||||
lastItem <- case lastItemId_ of
|
lastItem <- case lastItemId_ of
|
||||||
Just lastItemId -> (: []) <$> getDirectChatItem db user contactId lastItemId
|
Just lastItemId -> (: []) <$> getDirectChatItem db user contactId lastItemId
|
||||||
Nothing -> pure []
|
Nothing -> pure []
|
||||||
@@ -714,7 +716,7 @@ findGroupChatPreviews_ db User {userId} pagination clq =
|
|||||||
)
|
)
|
||||||
([":user_id" := userId, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams)
|
([":user_id" := userId, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams)
|
||||||
|
|
||||||
getGroupChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat
|
getGroupChatPreview_ :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat
|
||||||
getGroupChatPreview_ db vr user (GroupChatPD _ groupId lastItemId_ stats) = do
|
getGroupChatPreview_ db vr user (GroupChatPD _ groupId lastItemId_ stats) = do
|
||||||
groupInfo <- getGroupInfo db vr user groupId
|
groupInfo <- getGroupInfo db vr user groupId
|
||||||
lastItem <- case lastItemId_ of
|
lastItem <- case lastItemId_ of
|
||||||
@@ -856,7 +858,7 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of
|
|||||||
( [sql|
|
( [sql|
|
||||||
SELECT
|
SELECT
|
||||||
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
||||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences,
|
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences,
|
||||||
cr.created_at, cr.updated_at as ts,
|
cr.created_at, cr.updated_at as ts,
|
||||||
cr.peer_chat_min_version, cr.peer_chat_max_version
|
cr.peer_chat_min_version, cr.peer_chat_max_version
|
||||||
FROM contact_requests cr
|
FROM contact_requests cr
|
||||||
@@ -919,10 +921,10 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of
|
|||||||
aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats
|
aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats
|
||||||
in ACPD SCTContactConnection $ ContactConnectionPD updatedAt aChat
|
in ACPD SCTContactConnection $ ContactConnectionPD updatedAt aChat
|
||||||
|
|
||||||
getDirectChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect)
|
getDirectChat :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect)
|
||||||
getDirectChat db user contactId pagination search_ = do
|
getDirectChat db vr user contactId pagination search_ = do
|
||||||
let search = fromMaybe "" search_
|
let search = fromMaybe "" search_
|
||||||
ct <- getContact db user contactId
|
ct <- getContact db vr user contactId
|
||||||
liftIO $ case pagination of
|
liftIO $ case pagination of
|
||||||
CPLast count -> getDirectChatLast_ db user ct count search
|
CPLast count -> getDirectChatLast_ db user ct count search
|
||||||
CPAfter afterId count -> getDirectChatAfter_ db user ct afterId count search
|
CPAfter afterId count -> getDirectChatAfter_ db user ct afterId count search
|
||||||
@@ -1039,7 +1041,7 @@ getDirectChatBefore_ db user@User {userId} ct@Contact {contactId} beforeChatItem
|
|||||||
|]
|
|]
|
||||||
(userId, contactId, search, beforeChatItemId, count)
|
(userId, contactId, search, beforeChatItemId, count)
|
||||||
|
|
||||||
getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup)
|
getGroupChat :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup)
|
||||||
getGroupChat db vr user groupId pagination search_ = do
|
getGroupChat db vr user groupId pagination search_ = do
|
||||||
let search = fromMaybe "" search_
|
let search = fromMaybe "" search_
|
||||||
g <- getGroupInfo db vr user groupId
|
g <- getGroupInfo db vr user groupId
|
||||||
@@ -1505,7 +1507,7 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir,
|
|||||||
ciTimed :: Maybe CITimed
|
ciTimed :: Maybe CITimed
|
||||||
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
||||||
|
|
||||||
getAllChatItems :: DB.Connection -> VersionRangeChat -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem]
|
getAllChatItems :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem]
|
||||||
getAllChatItems db vr user@User {userId} pagination search_ = do
|
getAllChatItems db vr user@User {userId} pagination search_ = do
|
||||||
itemRefs <-
|
itemRefs <-
|
||||||
rights . map toChatItemRef <$> case pagination of
|
rights . map toChatItemRef <$> case pagination of
|
||||||
@@ -2149,7 +2151,7 @@ deleteLocalChatItem db User {userId} NoteFolder {noteFolderId} ci = do
|
|||||||
|]
|
|]
|
||||||
(userId, noteFolderId, itemId)
|
(userId, noteFolderId, itemId)
|
||||||
|
|
||||||
getChatItemByFileId :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO AChatItem
|
getChatItemByFileId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO AChatItem
|
||||||
getChatItemByFileId db vr user@User {userId} fileId = do
|
getChatItemByFileId db vr user@User {userId} fileId = do
|
||||||
(chatRef, itemId) <-
|
(chatRef, itemId) <-
|
||||||
ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByFileId fileId) $
|
ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByFileId fileId) $
|
||||||
@@ -2165,13 +2167,13 @@ getChatItemByFileId db vr user@User {userId} fileId = do
|
|||||||
(userId, fileId)
|
(userId, fileId)
|
||||||
getAChatItem db vr user chatRef itemId
|
getAChatItem db vr user chatRef itemId
|
||||||
|
|
||||||
lookupChatItemByFileId :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO (Maybe AChatItem)
|
lookupChatItemByFileId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO (Maybe AChatItem)
|
||||||
lookupChatItemByFileId db vr user fileId = do
|
lookupChatItemByFileId db vr user fileId = do
|
||||||
fmap Just (getChatItemByFileId db vr user fileId) `catchError` \case
|
fmap Just (getChatItemByFileId db vr user fileId) `catchError` \case
|
||||||
SEChatItemNotFoundByFileId {} -> pure Nothing
|
SEChatItemNotFoundByFileId {} -> pure Nothing
|
||||||
e -> throwError e
|
e -> throwError e
|
||||||
|
|
||||||
getChatItemByGroupId :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO AChatItem
|
getChatItemByGroupId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupId -> ExceptT StoreError IO AChatItem
|
||||||
getChatItemByGroupId db vr user@User {userId} groupId = do
|
getChatItemByGroupId db vr user@User {userId} groupId = do
|
||||||
(chatRef, itemId) <-
|
(chatRef, itemId) <-
|
||||||
ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByGroupId groupId) $
|
ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByGroupId groupId) $
|
||||||
@@ -2197,10 +2199,10 @@ getChatRefViaItemId db User {userId} itemId = do
|
|||||||
(Nothing, Just groupId) -> Right $ ChatRef CTGroup groupId
|
(Nothing, Just groupId) -> Right $ ChatRef CTGroup groupId
|
||||||
(_, _) -> Left $ SEBadChatItem itemId Nothing
|
(_, _) -> Left $ SEBadChatItem itemId Nothing
|
||||||
|
|
||||||
getAChatItem :: DB.Connection -> VersionRangeChat -> User -> ChatRef -> ChatItemId -> ExceptT StoreError IO AChatItem
|
getAChatItem :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ChatRef -> ChatItemId -> ExceptT StoreError IO AChatItem
|
||||||
getAChatItem db vr user chatRef itemId = case chatRef of
|
getAChatItem db vr user chatRef itemId = case chatRef of
|
||||||
ChatRef CTDirect contactId -> do
|
ChatRef CTDirect contactId -> do
|
||||||
ct <- getContact db user contactId
|
ct <- getContact db vr user contactId
|
||||||
(CChatItem msgDir ci) <- getDirectChatItem db user contactId itemId
|
(CChatItem msgDir ci) <- getDirectChatItem db user contactId itemId
|
||||||
pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci
|
pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci
|
||||||
ChatRef CTGroup groupId -> do
|
ChatRef CTGroup groupId -> do
|
||||||
@@ -2437,9 +2439,9 @@ createCIModeration db GroupInfo {groupId} moderatorMember itemMemberId itemShare
|
|||||||
|]
|
|]
|
||||||
(groupId, groupMemberId' moderatorMember, itemMemberId, itemSharedMId, msgId, moderatedAtTs)
|
(groupId, groupMemberId' moderatorMember, itemMemberId, itemSharedMId, msgId, moderatedAtTs)
|
||||||
|
|
||||||
getCIModeration :: DB.Connection -> User -> GroupInfo -> MemberId -> Maybe SharedMsgId -> IO (Maybe CIModeration)
|
getCIModeration :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> MemberId -> Maybe SharedMsgId -> IO (Maybe CIModeration)
|
||||||
getCIModeration _ _ _ _ Nothing = pure Nothing
|
getCIModeration _ _ _ _ _ Nothing = pure Nothing
|
||||||
getCIModeration db user GroupInfo {groupId} itemMemberId (Just sharedMsgId) = do
|
getCIModeration db vr user GroupInfo {groupId} itemMemberId (Just sharedMsgId) = do
|
||||||
r_ <-
|
r_ <-
|
||||||
maybeFirstRow id $
|
maybeFirstRow id $
|
||||||
DB.query
|
DB.query
|
||||||
@@ -2453,7 +2455,7 @@ getCIModeration db user GroupInfo {groupId} itemMemberId (Just sharedMsgId) = do
|
|||||||
(groupId, itemMemberId, sharedMsgId)
|
(groupId, itemMemberId, sharedMsgId)
|
||||||
case r_ of
|
case r_ of
|
||||||
Just (moderationId, moderatorId, createdByMsgId, moderatedAt) -> do
|
Just (moderationId, moderatorId, createdByMsgId, moderatedAt) -> do
|
||||||
runExceptT (getGroupMember db user groupId moderatorId) >>= \case
|
runExceptT (getGroupMember db vr user groupId moderatorId) >>= \case
|
||||||
Right moderatorMember -> pure (Just CIModeration {moderationId, moderatorMember, createdByMsgId, moderatedAt})
|
Right moderatorMember -> pure (Just CIModeration {moderationId, moderatorMember, createdByMsgId, moderatedAt})
|
||||||
_ -> pure Nothing
|
_ -> pure Nothing
|
||||||
_ -> pure Nothing
|
_ -> pure Nothing
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
|||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||||
|
import Simplex.Messaging.Crypto.Ratchet (PQSupport)
|
||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
import Simplex.Messaging.Parsers (defaultJSON)
|
import Simplex.Messaging.Parsers (defaultJSON)
|
||||||
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode)
|
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode)
|
||||||
@@ -324,39 +325,39 @@ createUserContactLink db User {userId} agentConnId cReq subMode =
|
|||||||
"INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)"
|
"INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)"
|
||||||
(userId, cReq, currentTs, currentTs)
|
(userId, cReq, currentTs, currentTs)
|
||||||
userContactLinkId <- insertedRowId db
|
userContactLinkId <- insertedRowId db
|
||||||
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
|
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
|
||||||
|
|
||||||
getUserAddressConnections :: DB.Connection -> User -> ExceptT StoreError IO [Connection]
|
getUserAddressConnections :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ExceptT StoreError IO [Connection]
|
||||||
getUserAddressConnections db User {userId} = do
|
getUserAddressConnections db vr User {userId} = do
|
||||||
cs <- liftIO getUserAddressConnections_
|
cs <- liftIO getUserAddressConnections_
|
||||||
if null cs then throwError SEUserContactLinkNotFound else pure cs
|
if null cs then throwError SEUserContactLinkNotFound else pure cs
|
||||||
where
|
where
|
||||||
getUserAddressConnections_ :: IO [Connection]
|
getUserAddressConnections_ :: IO [Connection]
|
||||||
getUserAddressConnections_ =
|
getUserAddressConnections_ =
|
||||||
map toConnection
|
map (toConnection vr)
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||||
c.created_at, c.security_code, c.security_code_verified_at, c.enable_pq, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||||
c.peer_chat_min_version, c.peer_chat_max_version
|
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||||
FROM connections c
|
FROM connections c
|
||||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||||
WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL
|
WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL
|
||||||
|]
|
|]
|
||||||
(userId, userId)
|
(userId, userId)
|
||||||
|
|
||||||
getUserContactLinks :: DB.Connection -> User -> IO [(Connection, UserContact)]
|
getUserContactLinks :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> IO [(Connection, UserContact)]
|
||||||
getUserContactLinks db User {userId} =
|
getUserContactLinks db vr User {userId} =
|
||||||
map toUserContactConnection
|
map toUserContactConnection
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||||
c.created_at, c.security_code, c.security_code_verified_at, c.enable_pq, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||||
c.peer_chat_min_version, c.peer_chat_max_version,
|
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version,
|
||||||
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
|
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
|
||||||
FROM connections c
|
FROM connections c
|
||||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||||
@@ -365,7 +366,7 @@ getUserContactLinks db User {userId} =
|
|||||||
(userId, userId)
|
(userId, userId)
|
||||||
where
|
where
|
||||||
toUserContactConnection :: (ConnectionRow :. (Int64, ConnReqContact, Maybe GroupId)) -> (Connection, UserContact)
|
toUserContactConnection :: (ConnectionRow :. (Int64, ConnReqContact, Maybe GroupId)) -> (Connection, UserContact)
|
||||||
toUserContactConnection (connRow :. (userContactLinkId, connReqContact, groupId)) = (toConnection connRow, UserContact {userContactLinkId, connReqContact, groupId})
|
toUserContactConnection (connRow :. (userContactLinkId, connReqContact, groupId)) = (toConnection vr connRow, UserContact {userContactLinkId, connReqContact, groupId})
|
||||||
|
|
||||||
deleteUserAddress :: DB.Connection -> User -> IO ()
|
deleteUserAddress :: DB.Connection -> User -> IO ()
|
||||||
deleteUserAddress db user@User {userId} = do
|
deleteUserAddress db user@User {userId} = do
|
||||||
@@ -473,8 +474,8 @@ getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) =
|
|||||||
|]
|
|]
|
||||||
(userId, cReqSchema1, cReqSchema2)
|
(userId, cReqSchema1, cReqSchema2)
|
||||||
|
|
||||||
getContactWithoutConnViaAddress :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact)
|
getContactWithoutConnViaAddress :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact)
|
||||||
getContactWithoutConnViaAddress db user@User {userId} (cReqSchema1, cReqSchema2) = do
|
getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchema2) = do
|
||||||
ctId_ <-
|
ctId_ <-
|
||||||
maybeFirstRow fromOnly $
|
maybeFirstRow fromOnly $
|
||||||
DB.query
|
DB.query
|
||||||
@@ -487,7 +488,7 @@ getContactWithoutConnViaAddress db user@User {userId} (cReqSchema1, cReqSchema2)
|
|||||||
WHERE cp.user_id = ? AND cp.contact_link IN (?,?) AND c.connection_id IS NULL
|
WHERE cp.user_id = ? AND cp.contact_link IN (?,?) AND c.connection_id IS NULL
|
||||||
|]
|
|]
|
||||||
(userId, cReqSchema1, cReqSchema2)
|
(userId, cReqSchema1, cReqSchema2)
|
||||||
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db user) ctId_
|
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db vr user) ctId_
|
||||||
|
|
||||||
updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink
|
updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink
|
||||||
updateUserAddressAutoAccept db user@User {userId} autoAccept = do
|
updateUserAddressAutoAccept db user@User {userId} autoAccept = do
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import Simplex.Messaging.Agent.Protocol (ConnId, UserId)
|
|||||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQSupportOff)
|
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..))
|
||||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||||
import Simplex.Messaging.Protocol (SubscriptionMode (..))
|
import Simplex.Messaging.Protocol (SubscriptionMode (..))
|
||||||
@@ -95,6 +95,8 @@ data StoreError
|
|||||||
| SEUniqueID
|
| SEUniqueID
|
||||||
| SELargeMsg
|
| SELargeMsg
|
||||||
| SEInternalError {message :: String}
|
| SEInternalError {message :: String}
|
||||||
|
| SEDBException {message :: String}
|
||||||
|
| SEDBBusyError {message :: String}
|
||||||
| SEBadChatItem {itemId :: ChatItemId, itemTs :: Maybe ChatItemTs}
|
| SEBadChatItem {itemId :: ChatItemId, itemTs :: Maybe ChatItemTs}
|
||||||
| SEChatItemNotFound {itemId :: ChatItemId}
|
| SEChatItemNotFound {itemId :: ChatItemId}
|
||||||
| SEChatItemNotFoundByText {text :: Text}
|
| SEChatItemNotFoundByText {text :: Text}
|
||||||
@@ -151,17 +153,17 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file
|
|||||||
|
|
||||||
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
|
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
|
||||||
|
|
||||||
-- TODO PQ nullable?
|
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Maybe VersionChat, VersionChat, VersionChat)
|
||||||
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, VersionChat, VersionChat)
|
|
||||||
|
|
||||||
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe VersionChat, Maybe VersionChat)
|
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat)
|
||||||
|
|
||||||
toConnection :: ConnectionRow -> Connection
|
toConnection :: (PQSupport -> VersionRangeChat) -> ConnectionRow -> Connection
|
||||||
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqEncryption_, pqSndEnabled, pqRcvEnabled, authErrCounter, minVer, maxVer)) =
|
toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, chatV, minVer, maxVer)) =
|
||||||
Connection
|
Connection
|
||||||
{ connId,
|
{ connId,
|
||||||
agentConnId = AgentConnId acId,
|
agentConnId = AgentConnId acId,
|
||||||
peerChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer,
|
connChatVersion = fromMaybe (vr pqSupport `peerConnChatVersion` peerChatVRange) chatV,
|
||||||
|
peerChatVRange = peerChatVRange,
|
||||||
connLevel,
|
connLevel,
|
||||||
viaContact,
|
viaContact,
|
||||||
viaUserContactLink,
|
viaUserContactLink,
|
||||||
@@ -174,15 +176,15 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup
|
|||||||
localAlias,
|
localAlias,
|
||||||
entityId = entityId_ connType,
|
entityId = entityId_ connType,
|
||||||
connectionCode = SecurityCode <$> code_ <*> verifiedAt_,
|
connectionCode = SecurityCode <$> code_ <*> verifiedAt_,
|
||||||
-- TODO PQ add field
|
pqSupport,
|
||||||
pqSupport = maybe PQSupportOff CR.pqEncToSupport pqEncryption_,
|
pqEncryption,
|
||||||
pqEncryption = fromMaybe PQEncOff pqEncryption_,
|
|
||||||
pqSndEnabled,
|
pqSndEnabled,
|
||||||
pqRcvEnabled,
|
pqRcvEnabled,
|
||||||
authErrCounter,
|
authErrCounter,
|
||||||
createdAt
|
createdAt
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
|
peerChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
||||||
entityId_ :: ConnType -> Maybe Int64
|
entityId_ :: ConnType -> Maybe Int64
|
||||||
entityId_ ConnContact = contactId
|
entityId_ ConnContact = contactId
|
||||||
entityId_ ConnMember = groupMemberId
|
entityId_ ConnMember = groupMemberId
|
||||||
@@ -190,36 +192,36 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup
|
|||||||
entityId_ ConnSndFile = sndFileId
|
entityId_ ConnSndFile = sndFileId
|
||||||
entityId_ ConnUserContact = userContactLinkId
|
entityId_ ConnUserContact = userContactLinkId
|
||||||
|
|
||||||
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection
|
toMaybeConnection :: (PQSupport -> VersionRangeChat) -> MaybeConnectionRow -> Maybe Connection
|
||||||
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, enablePQ_, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, Just minVer, Just maxVer)) =
|
toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, connChatVersion, Just minVer, Just maxVer)) =
|
||||||
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, enablePQ_, pqSndEnabled_, pqRcvEnabled_, authErrCounter, minVer, maxVer))
|
Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, connChatVersion, minVer, maxVer))
|
||||||
toMaybeConnection _ = Nothing
|
toMaybeConnection _ _ = Nothing
|
||||||
|
|
||||||
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection
|
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionChat -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection
|
||||||
createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode pqSup = do
|
createConnection_ db userId connType entityId acId connChatVersion peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode pqSup = do
|
||||||
viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId ->
|
viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId ->
|
||||||
maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId)
|
maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId)
|
||||||
let viaGroupLink = isJust viaLinkGroupId
|
let viaGroupLink = isJust viaLinkGroupId
|
||||||
-- TODO PQ store pq_support
|
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
INSERT INTO connections (
|
INSERT INTO connections (
|
||||||
user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id, conn_status, conn_type,
|
user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id, conn_status, conn_type,
|
||||||
contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at,
|
contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at,
|
||||||
peer_chat_min_version, peer_chat_max_version, to_subscribe, enable_pq
|
conn_chat_version, peer_chat_min_version, peer_chat_max_version, to_subscribe, pq_support, pq_encryption
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType)
|
( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType)
|
||||||
:. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs)
|
:. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs)
|
||||||
:. (minV, maxV, subMode == SMOnlyCreate, pqSup)
|
:. (connChatVersion, minV, maxV, subMode == SMOnlyCreate, pqSup, pqSup)
|
||||||
)
|
)
|
||||||
connId <- insertedRowId db
|
connId <- insertedRowId db
|
||||||
pure
|
pure
|
||||||
Connection
|
Connection
|
||||||
{ connId,
|
{ connId,
|
||||||
agentConnId = AgentConnId acId,
|
agentConnId = AgentConnId acId,
|
||||||
peerChatVRange = JVersionRange peerChatVRange,
|
connChatVersion,
|
||||||
|
peerChatVRange,
|
||||||
connType,
|
connType,
|
||||||
contactConnInitiated = False,
|
contactConnInitiated = False,
|
||||||
entityId,
|
entityId,
|
||||||
@@ -253,18 +255,17 @@ createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, imag
|
|||||||
(displayName, fullName, image, userId, Just True, createdAt, createdAt)
|
(displayName, fullName, image, userId, Just True, createdAt, createdAt)
|
||||||
insertedRowId db
|
insertedRowId db
|
||||||
|
|
||||||
allowConnEnablePQ :: DB.Connection -> Int64 -> IO ()
|
updateConnSupportPQ :: DB.Connection -> Int64 -> PQSupport -> PQEncryption -> IO ()
|
||||||
allowConnEnablePQ db connId =
|
updateConnSupportPQ db connId pqSup pqEnc =
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
UPDATE connections
|
UPDATE connections
|
||||||
SET enable_pq = 1
|
SET pq_support = ?, pq_encryption = ?
|
||||||
WHERE connection_id = ?
|
WHERE connection_id = ?
|
||||||
|]
|
|]
|
||||||
(Only connId)
|
(pqSup, pqEnc, connId)
|
||||||
|
|
||||||
-- TODO PQ possibly combine all functions
|
|
||||||
updateConnPQSndEnabled :: DB.Connection -> Int64 -> PQEncryption -> IO ()
|
updateConnPQSndEnabled :: DB.Connection -> Int64 -> PQEncryption -> IO ()
|
||||||
updateConnPQSndEnabled db connId pqSndEnabled =
|
updateConnPQSndEnabled db connId pqSndEnabled =
|
||||||
DB.execute
|
DB.execute
|
||||||
@@ -298,16 +299,16 @@ updateConnPQEnabledCON db connId pqEnabled =
|
|||||||
|]
|
|]
|
||||||
(pqEnabled, pqEnabled, connId)
|
(pqEnabled, pqEnabled, connId)
|
||||||
|
|
||||||
setPeerChatVRange :: DB.Connection -> Int64 -> VersionRangeChat -> IO ()
|
setPeerChatVRange :: DB.Connection -> Int64 -> VersionChat -> VersionRangeChat -> IO ()
|
||||||
setPeerChatVRange db connId (VersionRange minVer maxVer) =
|
setPeerChatVRange db connId chatV (VersionRange minVer maxVer) =
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
UPDATE connections
|
UPDATE connections
|
||||||
SET peer_chat_min_version = ?, peer_chat_max_version = ?
|
SET conn_chat_version = ?, peer_chat_min_version = ?, peer_chat_max_version = ?
|
||||||
WHERE connection_id = ?
|
WHERE connection_id = ?
|
||||||
|]
|
|]
|
||||||
(minVer, maxVer, connId)
|
(chatV, minVer, maxVer, connId)
|
||||||
|
|
||||||
setMemberChatVRange :: DB.Connection -> GroupMemberId -> VersionRangeChat -> IO ()
|
setMemberChatVRange :: DB.Connection -> GroupMemberId -> VersionRangeChat -> IO ()
|
||||||
setMemberChatVRange db mId (VersionRange minVer maxVer) =
|
setMemberChatVRange db mId (VersionRange minVer maxVer) =
|
||||||
@@ -372,10 +373,10 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|
|||||||
|
|
||||||
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)
|
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)
|
||||||
|
|
||||||
toContact :: User -> ContactRow :. MaybeConnectionRow -> Contact
|
toContact :: (PQSupport -> VersionRangeChat) -> User -> ContactRow :. MaybeConnectionRow -> Contact
|
||||||
toContact user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
|
toContact vr user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
|
||||||
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
||||||
activeConn = toMaybeConnection connRow
|
activeConn = toMaybeConnection vr connRow
|
||||||
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
|
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
|
||||||
incognito = maybe False connIncognito activeConn
|
incognito = maybe False connIncognito activeConn
|
||||||
mergedPreferences = contactUserPreferences user userPreferences preferences incognito
|
mergedPreferences = contactUserPreferences user userPreferences preferences incognito
|
||||||
@@ -396,13 +397,13 @@ getProfileById db userId profileId =
|
|||||||
toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile
|
toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile
|
||||||
toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
||||||
|
|
||||||
type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat)
|
type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat)
|
||||||
|
|
||||||
toContactRequest :: ContactRequestRow -> UserContactRequest
|
toContactRequest :: ContactRequestRow -> UserContactRequest
|
||||||
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, preferences, createdAt, updatedAt, minVer, maxVer)) = do
|
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, preferences, createdAt, updatedAt, minVer, maxVer)) = do
|
||||||
let profile = Profile {displayName, fullName, image, contactLink, preferences}
|
let profile = Profile {displayName, fullName, image, contactLink, preferences}
|
||||||
cReqChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
||||||
in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt}
|
in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, createdAt, updatedAt}
|
||||||
|
|
||||||
userQuery :: Query
|
userQuery :: Query
|
||||||
userQuery =
|
userQuery =
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
module Simplex.Chat.Types where
|
module Simplex.Chat.Types where
|
||||||
|
|
||||||
import Crypto.Number.Serialize (os2ip)
|
import Crypto.Number.Serialize (os2ip)
|
||||||
import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.=))
|
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||||
import qualified Data.Aeson as J
|
import qualified Data.Aeson as J
|
||||||
import qualified Data.Aeson.Encoding as JE
|
import qualified Data.Aeson.Encoding as JE
|
||||||
import qualified Data.Aeson.TH as JQ
|
import qualified Data.Aeson.TH as JQ
|
||||||
@@ -49,7 +49,7 @@ import Simplex.Chat.Types.Util
|
|||||||
import Simplex.FileTransfer.Description (FileDigest)
|
import Simplex.FileTransfer.Description (FileDigest)
|
||||||
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId)
|
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId)
|
||||||
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
|
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
|
||||||
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport)
|
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff)
|
||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON, taggedObjectJSON)
|
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON, taggedObjectJSON)
|
||||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI)
|
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI)
|
||||||
@@ -57,57 +57,6 @@ import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>))
|
|||||||
import Simplex.Messaging.Version
|
import Simplex.Messaging.Version
|
||||||
import Simplex.Messaging.Version.Internal
|
import Simplex.Messaging.Version.Internal
|
||||||
|
|
||||||
-- TODO PQ replace with actual instances
|
|
||||||
instance Eq (ConnectionRequestUri m) where _ == _ = True
|
|
||||||
|
|
||||||
instance Eq (APartyCmdTag p) where
|
|
||||||
t1 == t2 = case (t1, t2) of
|
|
||||||
(APCT SAEConn NEW_, APCT SAEConn NEW_) -> True
|
|
||||||
(APCT SAEConn INV_, APCT SAEConn INV_) -> True
|
|
||||||
(APCT SAEConn JOIN_, APCT SAEConn JOIN_) -> True
|
|
||||||
(APCT SAEConn CONF_, APCT SAEConn CONF_) -> True
|
|
||||||
(APCT SAEConn LET_, APCT SAEConn LET_) -> True
|
|
||||||
(APCT SAEConn REQ_, APCT SAEConn REQ_) -> True
|
|
||||||
(APCT SAEConn ACPT_, APCT SAEConn ACPT_) -> True
|
|
||||||
(APCT SAEConn RJCT_, APCT SAEConn RJCT_) -> True
|
|
||||||
(APCT SAEConn INFO_, APCT SAEConn INFO_) -> True
|
|
||||||
(APCT SAEConn CON_, APCT SAEConn CON_) -> True
|
|
||||||
(APCT SAEConn SUB_, APCT SAEConn SUB_) -> True
|
|
||||||
(APCT SAEConn END_, APCT SAEConn END_) -> True
|
|
||||||
(APCT SAENone CONNECT_, APCT SAENone CONNECT_) -> True
|
|
||||||
(APCT SAENone DISCONNECT_, APCT SAENone DISCONNECT_) -> True
|
|
||||||
(APCT SAENone DOWN_, APCT SAENone DOWN_) -> True
|
|
||||||
(APCT SAENone UP_, APCT SAENone UP_) -> True
|
|
||||||
(APCT SAEConn SWITCH_, APCT SAEConn SWITCH_) -> True
|
|
||||||
(APCT SAEConn RSYNC_, APCT SAEConn RSYNC_) -> True
|
|
||||||
(APCT SAEConn SEND_, APCT SAEConn SEND_) -> True
|
|
||||||
(APCT SAEConn MID_, APCT SAEConn MID_) -> True
|
|
||||||
(APCT SAEConn SENT_, APCT SAEConn SENT_) -> True
|
|
||||||
(APCT SAEConn MERR_, APCT SAEConn MERR_) -> True
|
|
||||||
(APCT SAEConn MERRS_, APCT SAEConn MERRS_) -> True
|
|
||||||
(APCT SAEConn MSG_, APCT SAEConn MSG_) -> True
|
|
||||||
(APCT SAEConn MSGNTF_, APCT SAEConn MSGNTF_) -> True
|
|
||||||
(APCT SAEConn ACK_, APCT SAEConn ACK_) -> True
|
|
||||||
(APCT SAEConn RCVD_, APCT SAEConn RCVD_) -> True
|
|
||||||
(APCT SAEConn SWCH_, APCT SAEConn SWCH_) -> True
|
|
||||||
(APCT SAEConn OFF_, APCT SAEConn OFF_) -> True
|
|
||||||
(APCT SAEConn DEL_, APCT SAEConn DEL_) -> True
|
|
||||||
(APCT SAEConn DEL_RCVQ_, APCT SAEConn DEL_RCVQ_) -> True
|
|
||||||
(APCT SAEConn DEL_CONN_, APCT SAEConn DEL_CONN_) -> True
|
|
||||||
(APCT SAENone DEL_USER_, APCT SAENone DEL_USER_) -> True
|
|
||||||
(APCT SAEConn CHK_, APCT SAEConn CHK_) -> True
|
|
||||||
(APCT SAEConn STAT_, APCT SAEConn STAT_) -> True
|
|
||||||
(APCT SAEConn OK_, APCT SAEConn OK_) -> True
|
|
||||||
(APCT SAEConn ERR_, APCT SAEConn ERR_) -> True
|
|
||||||
(APCT SAENone SUSPENDED_, APCT SAENone SUSPENDED_) -> True
|
|
||||||
(APCT SAERcvFile RFDONE_, APCT SAERcvFile RFDONE_) -> True
|
|
||||||
(APCT SAERcvFile RFPROG_, APCT SAERcvFile RFPROG_) -> True
|
|
||||||
(APCT SAERcvFile RFERR_, APCT SAERcvFile RFERR_) -> True
|
|
||||||
(APCT SAESndFile SFPROG_, APCT SAESndFile SFPROG_) -> True
|
|
||||||
(APCT SAESndFile SFDONE_, APCT SAESndFile SFDONE_) -> True
|
|
||||||
(APCT SAESndFile SFERR_, APCT SAESndFile SFERR_) -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
class IsContact a where
|
class IsContact a where
|
||||||
contactId' :: a -> ContactId
|
contactId' :: a -> ContactId
|
||||||
profile' :: a -> LocalProfile
|
profile' :: a -> LocalProfile
|
||||||
@@ -265,8 +214,8 @@ contactDeleted Contact {contactStatus} = contactStatus == CSDeleted
|
|||||||
contactSecurityCode :: Contact -> Maybe SecurityCode
|
contactSecurityCode :: Contact -> Maybe SecurityCode
|
||||||
contactSecurityCode Contact {activeConn} = connectionCode =<< activeConn
|
contactSecurityCode Contact {activeConn} = connectionCode =<< activeConn
|
||||||
|
|
||||||
contactPQEnabled :: Contact -> Bool
|
contactPQEnabled :: Contact -> PQEncryption
|
||||||
contactPQEnabled Contact {activeConn} = maybe False connPQEnabled activeConn
|
contactPQEnabled Contact {activeConn} = maybe PQEncOff connPQEnabled activeConn
|
||||||
|
|
||||||
data ContactStatus
|
data ContactStatus
|
||||||
= CSActive
|
= CSActive
|
||||||
@@ -329,15 +278,14 @@ data UserContactRequest = UserContactRequest
|
|||||||
agentInvitationId :: AgentInvId,
|
agentInvitationId :: AgentInvId,
|
||||||
userContactLinkId :: Int64,
|
userContactLinkId :: Int64,
|
||||||
agentContactConnId :: AgentConnId, -- connection id of user contact
|
agentContactConnId :: AgentConnId, -- connection id of user contact
|
||||||
cReqChatVRange :: JVersionRange,
|
cReqChatVRange :: VersionRangeChat,
|
||||||
localDisplayName :: ContactName,
|
localDisplayName :: ContactName,
|
||||||
profileId :: Int64,
|
profileId :: Int64,
|
||||||
profile :: Profile,
|
profile :: Profile,
|
||||||
createdAt :: UTCTime,
|
createdAt :: UTCTime,
|
||||||
updatedAt :: UTCTime,
|
updatedAt :: UTCTime,
|
||||||
xContactId :: Maybe XContactId
|
xContactId :: Maybe XContactId,
|
||||||
-- TODO PQ save pqSupport from REQ to database
|
pqSupport :: PQSupport
|
||||||
-- pqSupport :: PQSupport
|
|
||||||
}
|
}
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
@@ -661,7 +609,7 @@ memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} =
|
|||||||
MemberInfo
|
MemberInfo
|
||||||
{ memberId,
|
{ memberId,
|
||||||
memberRole,
|
memberRole,
|
||||||
v = ChatVersionRange . fromJVersionRange . peerChatVRange <$> activeConn,
|
v = ChatVersionRange . peerChatVRange <$> activeConn,
|
||||||
profile = redactedMemberProfile $ fromLocalProfile memberProfile
|
profile = redactedMemberProfile $ fromLocalProfile memberProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,7 +691,7 @@ data GroupMember = GroupMember
|
|||||||
-- member chat protocol version range; if member has active connection, its version range is preferred;
|
-- member chat protocol version range; if member has active connection, its version range is preferred;
|
||||||
-- for membership current supportedChatVRange is set, it's not updated on protocol version increase in database,
|
-- for membership current supportedChatVRange is set, it's not updated on protocol version increase in database,
|
||||||
-- but it's correctly set on read (see toGroupInfo)
|
-- but it's correctly set on read (see toGroupInfo)
|
||||||
memberChatVRange :: JVersionRange
|
memberChatVRange :: VersionRangeChat
|
||||||
}
|
}
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
@@ -761,10 +709,12 @@ memberConnId :: GroupMember -> Maybe ConnId
|
|||||||
memberConnId GroupMember {activeConn} = aConnId <$> activeConn
|
memberConnId GroupMember {activeConn} = aConnId <$> activeConn
|
||||||
|
|
||||||
memberChatVRange' :: GroupMember -> VersionRangeChat
|
memberChatVRange' :: GroupMember -> VersionRangeChat
|
||||||
memberChatVRange' GroupMember {activeConn, memberChatVRange} =
|
memberChatVRange' GroupMember {activeConn, memberChatVRange} = case activeConn of
|
||||||
fromJVersionRange $ case activeConn of
|
Just Connection {peerChatVRange} -> peerChatVRange
|
||||||
Just Connection {peerChatVRange} -> peerChatVRange
|
Nothing -> memberChatVRange
|
||||||
Nothing -> memberChatVRange
|
|
||||||
|
supportsVersion :: GroupMember -> VersionChat -> Bool
|
||||||
|
supportsVersion m v = maxVersion (memberChatVRange' m) >= v
|
||||||
|
|
||||||
groupMemberId' :: GroupMember -> GroupMemberId
|
groupMemberId' :: GroupMember -> GroupMemberId
|
||||||
groupMemberId' GroupMember {groupMemberId} = groupMemberId
|
groupMemberId' GroupMember {groupMemberId} = groupMemberId
|
||||||
@@ -1341,7 +1291,8 @@ type ConnReqContact = ConnectionRequestUri 'CMContact
|
|||||||
data Connection = Connection
|
data Connection = Connection
|
||||||
{ connId :: Int64,
|
{ connId :: Int64,
|
||||||
agentConnId :: AgentConnId,
|
agentConnId :: AgentConnId,
|
||||||
peerChatVRange :: JVersionRange,
|
connChatVersion :: VersionChat,
|
||||||
|
peerChatVRange :: VersionRangeChat,
|
||||||
connLevel :: Int,
|
connLevel :: Int,
|
||||||
viaContact :: Maybe Int64, -- group member contact ID, if not direct connection
|
viaContact :: Maybe Int64, -- group member contact ID, if not direct connection
|
||||||
viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address"
|
viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address"
|
||||||
@@ -1392,9 +1343,9 @@ aConnId Connection {agentConnId = AgentConnId cId} = cId
|
|||||||
connIncognito :: Connection -> Bool
|
connIncognito :: Connection -> Bool
|
||||||
connIncognito Connection {customUserProfileId} = isJust customUserProfileId
|
connIncognito Connection {customUserProfileId} = isJust customUserProfileId
|
||||||
|
|
||||||
connPQEnabled :: Connection -> Bool
|
connPQEnabled :: Connection -> PQEncryption
|
||||||
connPQEnabled Connection {pqSndEnabled = Just (PQEncryption s), pqRcvEnabled = Just (PQEncryption r)} = s && r
|
connPQEnabled Connection {pqSndEnabled = Just (PQEncryption s), pqRcvEnabled = Just (PQEncryption r)} = PQEncryption $ s && r
|
||||||
connPQEnabled _ = False
|
connPQEnabled _ = PQEncOff
|
||||||
|
|
||||||
data PendingContactConnection = PendingContactConnection
|
data PendingContactConnection = PendingContactConnection
|
||||||
{ pccConnId :: Int64,
|
{ pccConnId :: Int64,
|
||||||
@@ -1695,8 +1646,16 @@ type VersionRangeChat = VersionRange ChatVersion
|
|||||||
pattern VersionChat :: Word16 -> VersionChat
|
pattern VersionChat :: Word16 -> VersionChat
|
||||||
pattern VersionChat v = Version v
|
pattern VersionChat v = Version v
|
||||||
|
|
||||||
|
-- this newtype exists to have a concise JSON encoding of version ranges in chat protocol messages in the form of "1-2" or just "1"
|
||||||
newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRangeChat} deriving (Eq, Show)
|
newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRangeChat} deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- TODO v6.0 review
|
||||||
|
peerConnChatVersion :: VersionRangeChat -> VersionRangeChat -> VersionChat
|
||||||
|
peerConnChatVersion _local@(VersionRange lmin lmax) _peer@(VersionRange rmin rmax)
|
||||||
|
| lmin <= rmax && rmin <= lmax = min lmax rmax -- compatible
|
||||||
|
| rmin > lmax = rmin
|
||||||
|
| otherwise = rmax
|
||||||
|
|
||||||
initialChatVersion :: VersionChat
|
initialChatVersion :: VersionChat
|
||||||
initialChatVersion = VersionChat 1
|
initialChatVersion = VersionChat 1
|
||||||
|
|
||||||
@@ -1710,18 +1669,6 @@ instance ToJSON ChatVersionRange where
|
|||||||
toJSON (ChatVersionRange vr) = strToJSON vr
|
toJSON (ChatVersionRange vr) = strToJSON vr
|
||||||
toEncoding (ChatVersionRange vr) = strToJEncoding vr
|
toEncoding (ChatVersionRange vr) = strToJEncoding vr
|
||||||
|
|
||||||
newtype JVersionRange = JVersionRange {fromJVersionRange :: VersionRangeChat} deriving (Eq, Show)
|
|
||||||
|
|
||||||
instance FromJSON JVersionRange where
|
|
||||||
parseJSON = J.withObject "JVersionRange" $ \o -> do
|
|
||||||
minv <- o .: "minVersion"
|
|
||||||
maxv <- o .: "maxVersion"
|
|
||||||
maybe (fail "bad version range") (pure . JVersionRange) $ safeVersionRange minv maxv
|
|
||||||
|
|
||||||
instance ToJSON JVersionRange where
|
|
||||||
toJSON (JVersionRange (VersionRange minV maxV)) = J.object ["minVersion" .= minV, "maxVersion" .= maxV]
|
|
||||||
toEncoding (JVersionRange (VersionRange minV maxV)) = J.pairs $ "minVersion" .= minV <> "maxVersion" .= maxV
|
|
||||||
|
|
||||||
$(JQ.deriveJSON defaultJSON ''UserContact)
|
$(JQ.deriveJSON defaultJSON ''UserContact)
|
||||||
|
|
||||||
$(JQ.deriveJSON defaultJSON ''Profile)
|
$(JQ.deriveJSON defaultJSON ''Profile)
|
||||||
|
|||||||