mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 677eedfa8d | |||
| 25a4719414 | |||
| f802ec75fe | |||
| 045b195483 | |||
| 283c90f5ae | |||
| 99a9fb2e1f | |||
| ce9d583b39 | |||
| 53414608db | |||
| c7cf206585 | |||
| 6067ac3c93 | |||
| fc56873f1c | |||
| a30da38af7 | |||
| 0bf3d054c6 | |||
| a55a8b116a | |||
| 7a207fd641 | |||
| 4508e0dfc1 | |||
| a853ba3a15 | |||
| a2f190a6c6 | |||
| 267178dddb | |||
| fadce0c140 | |||
| 58ad97fe6d | |||
| 3ccd9903a7 | |||
| e294999044 | |||
| 2bbc687f4a | |||
| 61c507e7da | |||
| 64230f3545 | |||
| bb61b9c658 | |||
| 3428f4d2ee | |||
| fe865c5e11 | |||
| 9e87fe73a5 | |||
| 0ef2c55983 | |||
| 8882284fb7 | |||
| 575d899f5a | |||
| 825257e898 |
@@ -31,6 +31,7 @@ struct ContentView: View {
|
|||||||
@State private var showWhatsNew = false
|
@State private var showWhatsNew = false
|
||||||
@State private var showChooseLAMode = false
|
@State private var showChooseLAMode = false
|
||||||
@State private var showSetPasscode = false
|
@State private var showSetPasscode = false
|
||||||
|
@State private var waitingForOrPassedAuth = true
|
||||||
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
||||||
|
|
||||||
private enum ChatListActionSheet: Identifiable {
|
private enum ChatListActionSheet: Identifiable {
|
||||||
@@ -61,6 +62,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
if !showSettings, let la = chatModel.laRequest {
|
if !showSettings, let la = chatModel.laRequest {
|
||||||
LocalAuthView(authRequest: la)
|
LocalAuthView(authRequest: la)
|
||||||
|
.onDisappear {
|
||||||
|
// this flag is separate from accessAuthenticated to show initializationView while we wait for authentication
|
||||||
|
waitingForOrPassedAuth = accessAuthenticated
|
||||||
|
}
|
||||||
} else if showSetPasscode {
|
} else if showSetPasscode {
|
||||||
SetAppPasscodeView {
|
SetAppPasscodeView {
|
||||||
chatModel.contentViewAccessAuthenticated = true
|
chatModel.contentViewAccessAuthenticated = true
|
||||||
@@ -73,8 +78,7 @@ struct ContentView: View {
|
|||||||
showSetPasscode = false
|
showSetPasscode = false
|
||||||
alertManager.showAlert(laPasscodeNotSetAlert())
|
alertManager.showAlert(laPasscodeNotSetAlert())
|
||||||
}
|
}
|
||||||
}
|
} else if chatModel.chatDbStatus == nil && AppChatState.shared.value != .stopped && waitingForOrPassedAuth {
|
||||||
if chatModel.chatDbStatus == nil {
|
|
||||||
initializationView()
|
initializationView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ final class ChatModel: ObservableObject {
|
|||||||
@Published var chatDbChanged = false
|
@Published var chatDbChanged = false
|
||||||
@Published var chatDbEncrypted: Bool?
|
@Published var chatDbEncrypted: Bool?
|
||||||
@Published var chatDbStatus: DBMigrationResult?
|
@Published var chatDbStatus: DBMigrationResult?
|
||||||
|
@Published var ctrlInitInProgress: Bool = false
|
||||||
// local authentication
|
// local authentication
|
||||||
@Published var contentViewAccessAuthenticated: Bool = false
|
@Published var contentViewAccessAuthenticated: Bool = false
|
||||||
@Published var laRequest: LocalAuthRequest?
|
@Published var laRequest: LocalAuthRequest?
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ private var nseSubscribers: [UUID:NSESubscriber] = [:]
|
|||||||
private let SUSPENDING_TIMEOUT: TimeInterval = 2
|
private let SUSPENDING_TIMEOUT: TimeInterval = 2
|
||||||
|
|
||||||
// timeout should be larger than SUSPENDING_TIMEOUT
|
// timeout should be larger than SUSPENDING_TIMEOUT
|
||||||
func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = DispatchQueue.main, suspended: @escaping (Bool) -> Void) {
|
func waitNSESuspended(timeout: TimeInterval, suspended: @escaping (Bool) -> Void) {
|
||||||
if timeout <= SUSPENDING_TIMEOUT {
|
if timeout <= SUSPENDING_TIMEOUT {
|
||||||
logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)")
|
logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)")
|
||||||
}
|
}
|
||||||
var state = nseStateGroupDefault.get()
|
var state = nseStateGroupDefault.get()
|
||||||
if case .suspended = state {
|
if case .suspended = state {
|
||||||
dispatchQueue.async { suspended(true) }
|
DispatchQueue.main.async { suspended(true) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
@@ -45,7 +45,7 @@ func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = Disp
|
|||||||
logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))")
|
logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))")
|
||||||
suspendedCalled = true
|
suspendedCalled = true
|
||||||
nseSubscribers.removeValue(forKey: id)
|
nseSubscribers.removeValue(forKey: id)
|
||||||
dispatchQueue.async { suspended(ok) }
|
DispatchQueue.main.async { suspended(ok) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func apiStartChat() throws -> Bool {
|
func apiStartChat() throws -> Bool {
|
||||||
let r = chatSendCmdSync(.startChat(subscribe: true, expire: true, xftp: true))
|
let r = chatSendCmdSync(.startChat(mainApp: true))
|
||||||
switch r {
|
switch r {
|
||||||
case .chatStarted: return true
|
case .chatStarted: return true
|
||||||
case .chatRunning: return false
|
case .chatRunning: return false
|
||||||
@@ -403,7 +403,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
|
|||||||
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
|
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
|
||||||
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
|
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
|
||||||
default:
|
default:
|
||||||
logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)")
|
logger.debug("apiGetNtfToken response: \(String(describing: r))")
|
||||||
return (nil, nil, .off)
|
return (nil, nil, .off)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1215,6 +1215,8 @@ private func currentUserId(_ funcName: String) throws -> Int64 {
|
|||||||
func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws {
|
func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws {
|
||||||
logger.debug("initializeChat")
|
logger.debug("initializeChat")
|
||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
|
m.ctrlInitInProgress = true
|
||||||
|
defer { m.ctrlInitInProgress = false }
|
||||||
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations)
|
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations)
|
||||||
if m.chatDbStatus != .ok { return }
|
if m.chatDbStatus != .ok { return }
|
||||||
// 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
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ let terminationTimeout: Int = 3 // seconds
|
|||||||
|
|
||||||
let activationDelay: TimeInterval = 1.5
|
let activationDelay: TimeInterval = 1.5
|
||||||
|
|
||||||
|
let nseSuspendTimeout: TimeInterval = 5
|
||||||
|
|
||||||
private func _suspendChat(timeout: Int) {
|
private func _suspendChat(timeout: Int) {
|
||||||
// this is a redundant check to prevent logical errors, like the one fixed in this PR
|
// this is a redundant check to prevent logical errors, like the one fixed in this PR
|
||||||
let state = AppChatState.shared.value
|
let state = AppChatState.shared.value
|
||||||
if !state.canSuspend {
|
if !state.canSuspend {
|
||||||
logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)")
|
logger.error("_suspendChat called, current state: \(state.rawValue)")
|
||||||
} else if ChatModel.ok {
|
} else if ChatModel.ok {
|
||||||
AppChatState.shared.set(.suspending)
|
AppChatState.shared.set(.suspending)
|
||||||
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
|
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
|
||||||
@@ -124,20 +126,33 @@ func initChatAndMigrate(refreshInvitations: Bool = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startChatAndActivate(dispatchQueue: DispatchQueue = DispatchQueue.main, _ completion: @escaping () -> Void) {
|
func startChatForCall() {
|
||||||
|
logger.debug("DEBUGGING: startChatForCall")
|
||||||
|
if ChatModel.shared.chatRunning == true {
|
||||||
|
ChatReceiver.shared.start()
|
||||||
|
logger.debug("DEBUGGING: startChatForCall: after ChatReceiver.shared.start")
|
||||||
|
}
|
||||||
|
if .active != AppChatState.shared.value {
|
||||||
|
logger.debug("DEBUGGING: startChatForCall: before activateChat")
|
||||||
|
activateChat()
|
||||||
|
logger.debug("DEBUGGING: startChatForCall: after activateChat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startChatAndActivate(_ completion: @escaping () -> Void) {
|
||||||
logger.debug("DEBUGGING: startChatAndActivate")
|
logger.debug("DEBUGGING: startChatAndActivate")
|
||||||
if ChatModel.shared.chatRunning == true {
|
if ChatModel.shared.chatRunning == true {
|
||||||
ChatReceiver.shared.start()
|
ChatReceiver.shared.start()
|
||||||
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
|
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
|
||||||
}
|
}
|
||||||
if .active == AppChatState.shared.value {
|
if case .active = AppChatState.shared.value {
|
||||||
completion()
|
completion()
|
||||||
} else if nseStateGroupDefault.get().inactive {
|
} else if nseStateGroupDefault.get().inactive {
|
||||||
activate()
|
activate()
|
||||||
} else {
|
} else {
|
||||||
// setting app state to "activating" to notify NSE that it should suspend
|
// setting app state to "activating" to notify NSE that it should suspend
|
||||||
setAppState(.activating)
|
setAppState(.activating)
|
||||||
waitNSESuspended(timeout: 10, dispatchQueue: dispatchQueue) { ok in
|
waitNSESuspended(timeout: nseSuspendTimeout) { ok in
|
||||||
if !ok {
|
if !ok {
|
||||||
// if for some reason NSE failed to suspend,
|
// if for some reason NSE failed to suspend,
|
||||||
// e.g., it crashed previously without setting its state to "suspended",
|
// e.g., it crashed previously without setting its state to "suspended",
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ struct SimpleXApp: App {
|
|||||||
chatModel.appOpenUrl = url
|
chatModel.appOpenUrl = url
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
|
||||||
initChatAndMigrate()
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||||||
|
initChatAndMigrate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase) { phase in
|
.onChange(of: scenePhase) { phase in
|
||||||
@@ -98,12 +100,12 @@ struct SimpleXApp: App {
|
|||||||
if legacyDatabase, case .documents = dbContainerGroupDefault.get() {
|
if legacyDatabase, case .documents = dbContainerGroupDefault.get() {
|
||||||
dbContainerGroupDefault.set(.documents)
|
dbContainerGroupDefault.set(.documents)
|
||||||
setMigrationState(.offer)
|
setMigrationState(.offer)
|
||||||
logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db")
|
logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath())*.db")
|
||||||
} else {
|
} else {
|
||||||
dbContainerGroupDefault.set(.group)
|
dbContainerGroupDefault.set(.group)
|
||||||
setMigrationState(.ready)
|
setMigrationState(.ready)
|
||||||
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db")
|
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath())*.db")
|
||||||
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present")
|
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not") present")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ struct ActiveCallView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)")
|
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
|
||||||
AppDelegate.keepScreenOn(true)
|
AppDelegate.keepScreenOn(true)
|
||||||
createWebRTCClient()
|
createWebRTCClient()
|
||||||
dismissAllSheets()
|
dismissAllSheets()
|
||||||
}
|
}
|
||||||
.onChange(of: canConnectCall) { _ in
|
.onChange(of: canConnectCall) { _ in
|
||||||
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall, privacy: .public)")
|
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
|
||||||
createWebRTCClient()
|
createWebRTCClient()
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
|||||||
// The delay allows to accept the second call before suspending a chat
|
// The delay allows to accept the second call before suspending a chat
|
||||||
// see `.onChange(of: scenePhase)` in SimpleXApp
|
// see `.onChange(of: scenePhase)` in SimpleXApp
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||||
logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat), privacy: .public)")
|
logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat))")
|
||||||
if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true {
|
if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true {
|
||||||
self?.shouldSuspendChat = false
|
self?.shouldSuspendChat = false
|
||||||
suspendChat()
|
suspendChat()
|
||||||
@@ -142,45 +142,57 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
|||||||
|
|
||||||
@objc(pushRegistry:didUpdatePushCredentials:forType:)
|
@objc(pushRegistry:didUpdatePushCredentials:forType:)
|
||||||
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
||||||
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue, privacy: .public)")
|
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
||||||
logger.debug("CallController: did receive push with type \(type.rawValue, privacy: .public)")
|
logger.debug("CallController: did receive push with type \(type.rawValue)")
|
||||||
if type != .voIP {
|
if type != .voIP {
|
||||||
completion()
|
completion()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.debug("CallController: initializing chat")
|
if AppChatState.shared.value == .stopped {
|
||||||
if (!ChatModel.shared.chatInitialized) {
|
self.reportExpiredCall(payload: payload, completion)
|
||||||
initChatAndMigrate(refreshInvitations: false)
|
return
|
||||||
}
|
}
|
||||||
startChatAndActivate(dispatchQueue: DispatchQueue.global()) {
|
if (!ChatModel.shared.chatInitialized) {
|
||||||
self.shouldSuspendChat = true
|
logger.debug("CallController: initializing chat")
|
||||||
// There are no invitations in the model, as it was processed by NSE
|
do {
|
||||||
_ = try? justRefreshCallInvitations()
|
try initializeChat(start: true, refreshInvitations: false)
|
||||||
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
|
} catch let error {
|
||||||
// Extract the call information from the push notification payload
|
logger.error("CallController: initializing chat error: \(error)")
|
||||||
let m = ChatModel.shared
|
self.reportExpiredCall(payload: payload, completion)
|
||||||
if let contactId = payload.dictionaryPayload["contactId"] as? String,
|
return
|
||||||
let invitation = m.callInvitations[contactId] {
|
}
|
||||||
|
}
|
||||||
|
logger.debug("CallController: initialized chat")
|
||||||
|
startChatForCall()
|
||||||
|
logger.debug("CallController: started chat")
|
||||||
|
self.shouldSuspendChat = true
|
||||||
|
// There are no invitations in the model, as it was processed by NSE
|
||||||
|
_ = try? justRefreshCallInvitations()
|
||||||
|
logger.debug("CallController: updated call invitations chat")
|
||||||
|
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
|
||||||
|
// Extract the call information from the push notification payload
|
||||||
|
let m = ChatModel.shared
|
||||||
|
if let contactId = payload.dictionaryPayload["contactId"] as? String,
|
||||||
|
let invitation = m.callInvitations[contactId] {
|
||||||
|
let update = self.cxCallUpdate(invitation: invitation)
|
||||||
|
if let uuid = invitation.callkitUUID {
|
||||||
|
logger.debug("CallController: report pushkit call via CallKit")
|
||||||
let update = self.cxCallUpdate(invitation: invitation)
|
let update = self.cxCallUpdate(invitation: invitation)
|
||||||
if let uuid = invitation.callkitUUID {
|
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
||||||
logger.debug("CallController: report pushkit call via CallKit")
|
if error != nil {
|
||||||
let update = self.cxCallUpdate(invitation: invitation)
|
m.callInvitations.removeValue(forKey: contactId)
|
||||||
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
|
||||||
if error != nil {
|
|
||||||
m.callInvitations.removeValue(forKey: contactId)
|
|
||||||
}
|
|
||||||
// Tell PushKit that the notification is handled.
|
|
||||||
completion()
|
|
||||||
}
|
}
|
||||||
} else {
|
// Tell PushKit that the notification is handled.
|
||||||
self.reportExpiredCall(update: update, completion)
|
completion()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.reportExpiredCall(payload: payload, completion)
|
self.reportExpiredCall(update: update, completion)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self.reportExpiredCall(payload: payload, completion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +223,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
|
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
|
||||||
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID), privacy: .public)")
|
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID))")
|
||||||
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
|
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
|
||||||
if invitation.callTs.timeIntervalSinceNow >= -180 {
|
if invitation.callTs.timeIntervalSinceNow >= -180 {
|
||||||
let update = cxCallUpdate(invitation: invitation)
|
let update = cxCallUpdate(invitation: invitation)
|
||||||
@@ -351,7 +363,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
|||||||
private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) {
|
private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) {
|
||||||
controller.request(CXTransaction(action: action)) { error in
|
controller.request(CXTransaction(action: action)) { error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription, privacy: .public)")
|
logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)")
|
||||||
} else {
|
} else {
|
||||||
logger.debug("CallController.requestTransaction requested transaction successfully")
|
logger.debug("CallController.requestTransaction requested transaction successfully")
|
||||||
onSuccess()
|
onSuccess()
|
||||||
|
|||||||
@@ -484,6 +484,7 @@ func deleteChatAsync() async throws {
|
|||||||
try await apiDeleteStorage()
|
try await apiDeleteStorage()
|
||||||
_ = kcDatabasePassword.remove()
|
_ = kcDatabasePassword.remove()
|
||||||
storeDBPassphraseGroupDefault.set(true)
|
storeDBPassphraseGroupDefault.set(true)
|
||||||
|
deleteAppDatabaseAndFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DatabaseView_Previews: PreviewProvider {
|
struct DatabaseView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -13,19 +13,28 @@ struct LocalAuthView: View {
|
|||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
var authRequest: LocalAuthRequest
|
var authRequest: LocalAuthRequest
|
||||||
@State private var password = ""
|
@State private var password = ""
|
||||||
|
@State private var allowToReact = true
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") {
|
PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit",
|
||||||
|
buttonsEnabled: $allowToReact) {
|
||||||
if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword {
|
if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword {
|
||||||
|
allowToReact = false
|
||||||
deleteStorageAndRestart(sdPassword) { r in
|
deleteStorageAndRestart(sdPassword) { r in
|
||||||
m.laRequest = nil
|
m.laRequest = nil
|
||||||
authRequest.completed(r)
|
authRequest.completed(r)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let r: LAResult = password == authRequest.password
|
let r: LAResult
|
||||||
? .success
|
if password == authRequest.password {
|
||||||
: .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
|
if authRequest.selfDestruct && kcSelfDestructPassword.get() != nil && !m.chatInitialized {
|
||||||
|
initChatAndMigrate()
|
||||||
|
}
|
||||||
|
r = .success
|
||||||
|
} else {
|
||||||
|
r = .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
|
||||||
|
}
|
||||||
m.laRequest = nil
|
m.laRequest = nil
|
||||||
authRequest.completed(r)
|
authRequest.completed(r)
|
||||||
} cancel: {
|
} cancel: {
|
||||||
@@ -37,8 +46,27 @@ struct LocalAuthView: View {
|
|||||||
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
|
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await stopChatAsync()
|
/** Waiting until [initializeChat] finishes */
|
||||||
try await deleteChatAsync()
|
while (m.ctrlInitInProgress) {
|
||||||
|
try await Task.sleep(nanoseconds: 50_000000)
|
||||||
|
}
|
||||||
|
if m.chatRunning == true {
|
||||||
|
try await stopChatAsync()
|
||||||
|
}
|
||||||
|
if m.chatInitialized {
|
||||||
|
/**
|
||||||
|
* The following sequence can bring a user here:
|
||||||
|
* the user opened the app, entered app passcode, went to background, returned back, entered self-destruct code.
|
||||||
|
* In this case database should be closed to prevent possible situation when OS can deny database removal command
|
||||||
|
* */
|
||||||
|
chatCloseStore()
|
||||||
|
}
|
||||||
|
deleteAppDatabaseAndFiles()
|
||||||
|
// Clear sensitive data on screen just in case app fails to hide its views while new database is created
|
||||||
|
m.chatId = nil
|
||||||
|
m.reversedChatItems = []
|
||||||
|
m.chats = []
|
||||||
|
m.users = []
|
||||||
_ = kcAppPassword.set(password)
|
_ = kcAppPassword.set(password)
|
||||||
_ = kcSelfDestructPassword.remove()
|
_ = kcSelfDestructPassword.remove()
|
||||||
await NtfManager.shared.removeAllNotifications()
|
await NtfManager.shared.removeAllNotifications()
|
||||||
@@ -53,7 +81,7 @@ struct LocalAuthView: View {
|
|||||||
try initializeChat(start: true)
|
try initializeChat(start: true)
|
||||||
m.chatDbChanged = false
|
m.chatDbChanged = false
|
||||||
AppChatState.shared.set(.active)
|
AppChatState.shared.set(.active)
|
||||||
if m.currentUser != nil { return }
|
if m.currentUser != nil || !m.chatInitialized { return }
|
||||||
var profile: Profile? = nil
|
var profile: Profile? = nil
|
||||||
if let displayName = displayName, displayName != "" {
|
if let displayName = displayName, displayName != "" {
|
||||||
profile = Profile(displayName: displayName, fullName: "")
|
profile = Profile(displayName: displayName, fullName: "")
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ struct PasscodeView: View {
|
|||||||
var reason: String? = nil
|
var reason: String? = nil
|
||||||
var submitLabel: LocalizedStringKey
|
var submitLabel: LocalizedStringKey
|
||||||
var submitEnabled: ((String) -> Bool)?
|
var submitEnabled: ((String) -> Bool)?
|
||||||
|
@Binding var buttonsEnabled: Bool
|
||||||
|
|
||||||
var submit: () -> Void
|
var submit: () -> Void
|
||||||
var cancel: () -> Void
|
var cancel: () -> Void
|
||||||
|
|
||||||
@@ -70,11 +72,11 @@ struct PasscodeView: View {
|
|||||||
@ViewBuilder private func buttonsView() -> some View {
|
@ViewBuilder private func buttonsView() -> some View {
|
||||||
Button(action: cancel) {
|
Button(action: cancel) {
|
||||||
Label("Cancel", systemImage: "multiply")
|
Label("Cancel", systemImage: "multiply")
|
||||||
}
|
}.disabled(!buttonsEnabled)
|
||||||
Button(action: submit) {
|
Button(action: submit) {
|
||||||
Label(submitLabel, systemImage: "checkmark")
|
Label(submitLabel, systemImage: "checkmark")
|
||||||
}
|
}
|
||||||
.disabled(submitEnabled?(passcode) == false || passcode.count < 4)
|
.disabled(submitEnabled?(passcode) == false || passcode.count < 4 || !buttonsEnabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +87,7 @@ struct PasscodeViewView_Previews: PreviewProvider {
|
|||||||
title: "Enter Passcode",
|
title: "Enter Passcode",
|
||||||
reason: "Unlock app",
|
reason: "Unlock app",
|
||||||
submitLabel: "Submit",
|
submitLabel: "Submit",
|
||||||
|
buttonsEnabled: Binding.constant(true),
|
||||||
submit: {},
|
submit: {},
|
||||||
cancel: {}
|
cancel: {}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
|||||||
|
|
||||||
struct SetAppPasscodeView: View {
|
struct SetAppPasscodeView: View {
|
||||||
var passcodeKeychain: KeyChainItem = kcAppPassword
|
var passcodeKeychain: KeyChainItem = kcAppPassword
|
||||||
|
var prohibitedPasscodeKeychain: KeyChainItem = kcSelfDestructPassword
|
||||||
var title: LocalizedStringKey = "New Passcode"
|
var title: LocalizedStringKey = "New Passcode"
|
||||||
var reason: String?
|
var reason: String?
|
||||||
var submit: () -> Void
|
var submit: () -> Void
|
||||||
@@ -41,7 +42,10 @@ struct SetAppPasscodeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setPasswordView(title: title, submitLabel: "Save") {
|
setPasswordView(title: title,
|
||||||
|
submitLabel: "Save",
|
||||||
|
// Do not allow to set app passcode == selfDestruct passcode
|
||||||
|
submitEnabled: { pwd in pwd != prohibitedPasscodeKeychain.get() }) {
|
||||||
enteredPassword = passcode
|
enteredPassword = passcode
|
||||||
passcode = ""
|
passcode = ""
|
||||||
confirming = true
|
confirming = true
|
||||||
@@ -54,7 +58,7 @@ struct SetAppPasscodeView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View {
|
private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View {
|
||||||
PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) {
|
PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, buttonsEnabled: Binding.constant(true), submit: submit) {
|
||||||
dismiss()
|
dismiss()
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import SimpleXChat
|
|||||||
|
|
||||||
enum UserProfileAlert: Identifiable {
|
enum UserProfileAlert: Identifiable {
|
||||||
case duplicateUserError
|
case duplicateUserError
|
||||||
|
case invalidDisplayNameError
|
||||||
case createUserError(error: LocalizedStringKey)
|
case createUserError(error: LocalizedStringKey)
|
||||||
case invalidNameError(validName: String)
|
case invalidNameError(validName: String)
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .duplicateUserError: return "duplicateUserError"
|
case .duplicateUserError: return "duplicateUserError"
|
||||||
|
case .invalidDisplayNameError: return "invalidDisplayNameError"
|
||||||
case .createUserError: return "createUserError"
|
case .createUserError: return "createUserError"
|
||||||
case let .invalidNameError(validName): return "invalidNameError \(validName)"
|
case let .invalidNameError(validName): return "invalidNameError \(validName)"
|
||||||
}
|
}
|
||||||
@@ -187,6 +189,12 @@ private func createProfile(_ displayName: String, showAlert: (UserProfileAlert)
|
|||||||
} else {
|
} else {
|
||||||
showAlert(.duplicateUserError)
|
showAlert(.duplicateUserError)
|
||||||
}
|
}
|
||||||
|
case .chatCmdError(_, .error(.invalidDisplayName)):
|
||||||
|
if m.currentUser == nil {
|
||||||
|
AlertManager.shared.showAlert(invalidDisplayNameAlert)
|
||||||
|
} else {
|
||||||
|
showAlert(.invalidDisplayNameError)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
let err: LocalizedStringKey = "Error: \(responseError(error))"
|
let err: LocalizedStringKey = "Error: \(responseError(error))"
|
||||||
if m.currentUser == nil {
|
if m.currentUser == nil {
|
||||||
@@ -207,6 +215,7 @@ private func canCreateProfile(_ displayName: String) -> Bool {
|
|||||||
func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding<String>) -> Alert {
|
func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding<String>) -> Alert {
|
||||||
switch alert {
|
switch alert {
|
||||||
case .duplicateUserError: return duplicateUserAlert
|
case .duplicateUserError: return duplicateUserAlert
|
||||||
|
case .invalidDisplayNameError: return invalidDisplayNameAlert
|
||||||
case let .createUserError(err): return creatUserErrorAlert(err)
|
case let .createUserError(err): return creatUserErrorAlert(err)
|
||||||
case let .invalidNameError(name): return createInvalidNameAlert(name, displayName)
|
case let .invalidNameError(name): return createInvalidNameAlert(name, displayName)
|
||||||
}
|
}
|
||||||
@@ -219,6 +228,13 @@ private var duplicateUserAlert: Alert {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var invalidDisplayNameAlert: Alert {
|
||||||
|
Alert(
|
||||||
|
title: Text("Invalid display name!"),
|
||||||
|
message: Text("This display name is invalid. Please choose another name.")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
|
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Error creating profile!"),
|
title: Text("Error creating profile!"),
|
||||||
|
|||||||
@@ -491,14 +491,23 @@ struct SimplexLockView: View {
|
|||||||
showLAAlert(.laPasscodeNotChangedAlert)
|
showLAAlert(.laPasscodeNotChangedAlert)
|
||||||
}
|
}
|
||||||
case .enableSelfDestruct:
|
case .enableSelfDestruct:
|
||||||
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) {
|
SetAppPasscodeView(
|
||||||
|
passcodeKeychain: kcSelfDestructPassword,
|
||||||
|
prohibitedPasscodeKeychain: kcAppPassword,
|
||||||
|
title: "Set passcode",
|
||||||
|
reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")
|
||||||
|
) {
|
||||||
updateSelfDestruct()
|
updateSelfDestruct()
|
||||||
showLAAlert(.laSelfDestructPasscodeSetAlert)
|
showLAAlert(.laSelfDestructPasscodeSetAlert)
|
||||||
} cancel: {
|
} cancel: {
|
||||||
revertSelfDestruct()
|
revertSelfDestruct()
|
||||||
}
|
}
|
||||||
case .changeSelfDestructPasscode:
|
case .changeSelfDestructPasscode:
|
||||||
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) {
|
SetAppPasscodeView(
|
||||||
|
passcodeKeychain: kcSelfDestructPassword,
|
||||||
|
prohibitedPasscodeKeychain: kcAppPassword,
|
||||||
|
reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")
|
||||||
|
) {
|
||||||
showLAAlert(.laSelfDestructPasscodeChangedAlert)
|
showLAAlert(.laSelfDestructPasscodeChangedAlert)
|
||||||
} cancel: {
|
} cancel: {
|
||||||
showLAAlert(.laPasscodeNotChangedAlert)
|
showLAAlert(.laPasscodeNotChangedAlert)
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ let logger = Logger()
|
|||||||
|
|
||||||
let appSuspendingDelay: UInt64 = 2_500_000_000
|
let appSuspendingDelay: UInt64 = 2_500_000_000
|
||||||
|
|
||||||
let nseSuspendDelay: TimeInterval = 2
|
typealias SuspendSchedule = (delay: TimeInterval, timeout: Int)
|
||||||
|
|
||||||
let nseSuspendTimeout: Int = 5
|
let nseSuspendSchedule: SuspendSchedule = (2, 4)
|
||||||
|
|
||||||
|
let fastNSESuspendSchedule: SuspendSchedule = (1, 1)
|
||||||
|
|
||||||
typealias NtfStream = ConcurrentQueue<NSENotification>
|
typealias NtfStream = ConcurrentQueue<NSENotification>
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ actor PendingNtfs {
|
|||||||
private var ntfStreams: [String: NtfStream] = [:]
|
private var ntfStreams: [String: NtfStream] = [:]
|
||||||
|
|
||||||
func createStream(_ id: String) async {
|
func createStream(_ id: String) async {
|
||||||
logger.debug("NotificationService PendingNtfs.createStream: \(id, privacy: .public)")
|
logger.debug("NotificationService PendingNtfs.createStream: \(id)")
|
||||||
if ntfStreams[id] == nil {
|
if ntfStreams[id] == nil {
|
||||||
ntfStreams[id] = ConcurrentQueue()
|
ntfStreams[id] = ConcurrentQueue()
|
||||||
logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue")
|
logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue")
|
||||||
@@ -40,14 +42,14 @@ actor PendingNtfs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async {
|
func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async {
|
||||||
logger.debug("NotificationService PendingNtfs.readStream: \(id, privacy: .public) \(ntfInfo.ntfMessages.count, privacy: .public)")
|
logger.debug("NotificationService PendingNtfs.readStream: \(id) \(ntfInfo.ntfMessages.count)")
|
||||||
if !ntfInfo.user.showNotifications {
|
if !ntfInfo.user.showNotifications {
|
||||||
nse.setBestAttemptNtf(.empty)
|
nse.setBestAttemptNtf(.empty)
|
||||||
}
|
}
|
||||||
if let s = ntfStreams[id] {
|
if let s = ntfStreams[id] {
|
||||||
logger.debug("NotificationService PendingNtfs.readStream: has stream")
|
logger.debug("NotificationService PendingNtfs.readStream: has stream")
|
||||||
var expected = Set(ntfInfo.ntfMessages.map { $0.msgId })
|
var expected = Set(ntfInfo.ntfMessages.map { $0.msgId })
|
||||||
logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected, privacy: .public)")
|
logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected)")
|
||||||
var readCancelled = false
|
var readCancelled = false
|
||||||
var dequeued: DequeueElement<NSENotification>?
|
var dequeued: DequeueElement<NSENotification>?
|
||||||
nse.cancelRead = {
|
nse.cancelRead = {
|
||||||
@@ -66,7 +68,7 @@ actor PendingNtfs {
|
|||||||
} else if case let .msgInfo(info) = ntf {
|
} else if case let .msgInfo(info) = ntf {
|
||||||
let found = expected.remove(info.msgId)
|
let found = expected.remove(info.msgId)
|
||||||
if found != nil {
|
if found != nil {
|
||||||
logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty, privacy: .public)")
|
logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty)")
|
||||||
if expected.isEmpty { break }
|
if expected.isEmpty { break }
|
||||||
} else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs {
|
} else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs {
|
||||||
logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo")
|
logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo")
|
||||||
@@ -88,7 +90,7 @@ actor PendingNtfs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeStream(_ id: String, _ ntf: NSENotification) async {
|
func writeStream(_ id: String, _ ntf: NSENotification) async {
|
||||||
logger.debug("NotificationService PendingNtfs.writeStream: \(id, privacy: .public)")
|
logger.debug("NotificationService PendingNtfs.writeStream: \(id)")
|
||||||
if let s = ntfStreams[id] {
|
if let s = ntfStreams[id] {
|
||||||
logger.debug("NotificationService PendingNtfs.writeStream: writing ntf")
|
logger.debug("NotificationService PendingNtfs.writeStream: writing ntf")
|
||||||
s.enqueue(ntf)
|
s.enqueue(ntf)
|
||||||
@@ -208,7 +210,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
registerGroupDefaults()
|
registerGroupDefaults()
|
||||||
let appState = appStateGroupDefault.get()
|
let appState = appStateGroupDefault.get()
|
||||||
logger.debug("NotificationService: app is \(appState.rawValue, privacy: .public)")
|
logger.debug("NotificationService: app is \(appState.rawValue)")
|
||||||
switch appState {
|
switch appState {
|
||||||
case .stopped:
|
case .stopped:
|
||||||
setBadgeCount()
|
setBadgeCount()
|
||||||
@@ -238,7 +240,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug("NotificationService: app state is now \(state.rawValue, privacy: .public)")
|
logger.debug("NotificationService: app state is now \(state.rawValue)")
|
||||||
if state.inactive {
|
if state.inactive {
|
||||||
receiveNtfMessages(request, contentHandler)
|
receiveNtfMessages(request, contentHandler)
|
||||||
} else {
|
} else {
|
||||||
@@ -267,7 +269,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
let dbStatus = startChat()
|
let dbStatus = startChat()
|
||||||
if case .ok = dbStatus,
|
if case .ok = dbStatus,
|
||||||
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||||
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count), privacy: .public)")
|
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count))")
|
||||||
if let connEntity = ntfInfo.connEntity_ {
|
if let connEntity = ntfInfo.connEntity_ {
|
||||||
setBestAttemptNtf(
|
setBestAttemptNtf(
|
||||||
ntfInfo.ntfsEnabled
|
ntfInfo.ntfsEnabled
|
||||||
@@ -279,7 +281,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
NtfStreamSemaphores.shared.waitForStream(id)
|
NtfStreamSemaphores.shared.waitForStream(id)
|
||||||
if receiveEntityId != nil {
|
if receiveEntityId != nil {
|
||||||
Task {
|
Task {
|
||||||
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)")
|
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id)")
|
||||||
await PendingNtfs.shared.createStream(id)
|
await PendingNtfs.shared.createStream(id)
|
||||||
await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo)
|
await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo)
|
||||||
deliverBestAttemptNtf()
|
deliverBestAttemptNtf()
|
||||||
@@ -297,7 +299,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
|
|
||||||
override func serviceExtensionTimeWillExpire() {
|
override func serviceExtensionTimeWillExpire() {
|
||||||
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
|
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
|
||||||
deliverBestAttemptNtf()
|
deliverBestAttemptNtf(urgent: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setBadgeCount() {
|
func setBadgeCount() {
|
||||||
@@ -319,7 +321,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deliverBestAttemptNtf() {
|
private func deliverBestAttemptNtf(urgent: Bool = false) {
|
||||||
logger.debug("NotificationService.deliverBestAttemptNtf")
|
logger.debug("NotificationService.deliverBestAttemptNtf")
|
||||||
if let cancel = cancelRead {
|
if let cancel = cancelRead {
|
||||||
cancelRead = nil
|
cancelRead = nil
|
||||||
@@ -329,20 +331,55 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
receiveEntityId = nil
|
receiveEntityId = nil
|
||||||
NtfStreamSemaphores.shared.signalStreamReady(id)
|
NtfStreamSemaphores.shared.signalStreamReady(id)
|
||||||
}
|
}
|
||||||
|
let suspend: Bool
|
||||||
if let t = threadId {
|
if let t = threadId {
|
||||||
threadId = nil
|
threadId = nil
|
||||||
if NSEThreads.shared.endThread(t) {
|
suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads
|
||||||
logger.debug("NotificationService.deliverBestAttemptNtf: will suspend")
|
} else {
|
||||||
// suspension is delayed to allow chat core finalise any processing
|
suspend = false
|
||||||
// (e.g., send delivery receipts)
|
}
|
||||||
DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendDelay) {
|
deliverCallkitOrNotification(urgent: urgent, suspend: suspend)
|
||||||
if NSEThreads.shared.noThreads {
|
}
|
||||||
logger.debug("NotificationService.deliverBestAttemptNtf: suspending...")
|
|
||||||
suspendChat(nseSuspendTimeout)
|
private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) {
|
||||||
|
if case .callkit = bestAttemptNtf {
|
||||||
|
logger.debug("NotificationService.deliverCallkitOrNotification: will suspend, callkit")
|
||||||
|
if urgent {
|
||||||
|
// suspending NSE even though there may be other notifications
|
||||||
|
// to allow the app to process callkit call
|
||||||
|
suspendChat(0)
|
||||||
|
deliverNotification()
|
||||||
|
} else {
|
||||||
|
// suspending NSE with delay and delivering after the suspension
|
||||||
|
// because pushkit notification must be processed without delay
|
||||||
|
// to avoid app termination
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + fastNSESuspendSchedule.delay) {
|
||||||
|
suspendChat(fastNSESuspendSchedule.timeout)
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + Double(fastNSESuspendSchedule.timeout)) {
|
||||||
|
self.deliverNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if suspend {
|
||||||
|
logger.debug("NotificationService.deliverCallkitOrNotification: will suspend")
|
||||||
|
if urgent {
|
||||||
|
suspendChat(0)
|
||||||
|
} else {
|
||||||
|
// suspension is delayed to allow chat core finalise any processing
|
||||||
|
// (e.g., send delivery receipts)
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendSchedule.delay) {
|
||||||
|
if NSEThreads.shared.noThreads {
|
||||||
|
suspendChat(nseSuspendSchedule.timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deliverNotification()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deliverNotification() {
|
||||||
if let handler = contentHandler, let ntf = bestAttemptNtf {
|
if let handler = contentHandler, let ntf = bestAttemptNtf {
|
||||||
contentHandler = nil
|
contentHandler = nil
|
||||||
bestAttemptNtf = nil
|
bestAttemptNtf = nil
|
||||||
@@ -357,17 +394,14 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
switch ntf {
|
switch ntf {
|
||||||
case let .nse(content): deliver(content)
|
case let .nse(content): deliver(content)
|
||||||
case let .callkit(invitation):
|
case let .callkit(invitation):
|
||||||
|
logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)")
|
||||||
CXProvider.reportNewIncomingVoIPPushPayload([
|
CXProvider.reportNewIncomingVoIPPushPayload([
|
||||||
"displayName": invitation.contact.displayName,
|
"displayName": invitation.contact.displayName,
|
||||||
"contactId": invitation.contact.id,
|
"contactId": invitation.contact.id,
|
||||||
"media": invitation.callType.media.rawValue
|
"media": invitation.callType.media.rawValue
|
||||||
]) { error in
|
]) { error in
|
||||||
if error == nil {
|
logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
|
||||||
deliver(nil)
|
deliver(error == nil ? nil : createCallInvitationNtf(invitation))
|
||||||
} else {
|
|
||||||
logger.debug("NotificationService reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
|
|
||||||
deliver(createCallInvitationNtf(invitation))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet
|
case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet
|
||||||
case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo
|
case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo
|
||||||
@@ -402,14 +436,14 @@ var appSubscriber: AppSubscriber = appStateSubscriber { state in
|
|||||||
logger.debug("NotificationService: appSubscriber")
|
logger.debug("NotificationService: appSubscriber")
|
||||||
if state.running && NSEChatState.shared.value.canSuspend {
|
if state.running && NSEChatState.shared.value.canSuspend {
|
||||||
logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending")
|
logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending")
|
||||||
suspendChat(nseSuspendTimeout)
|
suspendChat(fastNSESuspendSchedule.timeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber {
|
func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber {
|
||||||
appMessageSubscriber { msg in
|
appMessageSubscriber { msg in
|
||||||
if case let .state(state) = msg {
|
if case let .state(state) = msg {
|
||||||
logger.debug("NotificationService: appStateSubscriber \(state.rawValue, privacy: .public)")
|
logger.debug("NotificationService: appStateSubscriber \(state.rawValue)")
|
||||||
onState(state)
|
onState(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,24 +459,33 @@ let xftpConfig: XFTPFileConfig? = getXFTPCfg()
|
|||||||
// Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active
|
// Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active
|
||||||
func startChat() -> DBMigrationResult? {
|
func startChat() -> DBMigrationResult? {
|
||||||
logger.debug("NotificationService: startChat")
|
logger.debug("NotificationService: startChat")
|
||||||
if case .active = NSEChatState.shared.value { return .ok }
|
// only skip creating if there is chat controller
|
||||||
|
if case .active = NSEChatState.shared.value, hasChatCtrl() { return .ok }
|
||||||
|
|
||||||
startLock.wait()
|
startLock.wait()
|
||||||
defer { startLock.signal() }
|
defer { startLock.signal() }
|
||||||
|
|
||||||
return switch NSEChatState.shared.value {
|
if hasChatCtrl() {
|
||||||
case .created: doStartChat()
|
return switch NSEChatState.shared.value {
|
||||||
case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock
|
case .created: doStartChat()
|
||||||
case .active: .ok
|
case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock
|
||||||
case .suspending: activateChat()
|
case .active: .ok
|
||||||
case .suspended: activateChat()
|
case .suspending: activateChat()
|
||||||
|
case .suspended: activateChat()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ignore state in preference if there is no chat controller.
|
||||||
|
// State in preference may have failed to update e.g. because of a crash.
|
||||||
|
NSEChatState.shared.set(.created)
|
||||||
|
return doStartChat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func doStartChat() -> DBMigrationResult? {
|
func doStartChat() -> DBMigrationResult? {
|
||||||
logger.debug("NotificationService: doStartChat")
|
logger.debug("NotificationService: doStartChat")
|
||||||
hs_init(0, nil)
|
haskell_init_nse()
|
||||||
let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true)
|
let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true)
|
||||||
|
logger.debug("NotificationService: doStartChat \(String(describing: dbStatus))")
|
||||||
if dbStatus != .ok {
|
if dbStatus != .ok {
|
||||||
resetChatCtrl()
|
resetChatCtrl()
|
||||||
NSEChatState.shared.set(.created)
|
NSEChatState.shared.set(.created)
|
||||||
@@ -477,7 +520,7 @@ func doStartChat() -> DBMigrationResult? {
|
|||||||
return .ok
|
return .ok
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
|
logger.error("NotificationService startChat error: \(responseError(error))")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.debug("NotificationService: no active user")
|
logger.debug("NotificationService: no active user")
|
||||||
@@ -504,8 +547,10 @@ func suspendChat(_ timeout: Int) {
|
|||||||
logger.debug("NotificationService: suspendChat")
|
logger.debug("NotificationService: suspendChat")
|
||||||
let state = NSEChatState.shared.value
|
let state = NSEChatState.shared.value
|
||||||
if !state.canSuspend {
|
if !state.canSuspend {
|
||||||
logger.error("NotificationService suspendChat called, current state: \(state.rawValue, privacy: .public)")
|
logger.error("NotificationService suspendChat called, current state: \(state.rawValue)")
|
||||||
} else {
|
} else if hasChatCtrl() {
|
||||||
|
// only suspend if we have chat controller to avoid crashes when suspension is
|
||||||
|
// attempted when chat controller was not created
|
||||||
suspendLock.wait()
|
suspendLock.wait()
|
||||||
defer { suspendLock.signal() }
|
defer { suspendLock.signal() }
|
||||||
|
|
||||||
@@ -571,7 +616,7 @@ private let isInChina = SKStorefront().countryCode == "CHN"
|
|||||||
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
|
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
|
||||||
|
|
||||||
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
||||||
logger.debug("NotificationService receivedMsgNtf: \(res.responseType, privacy: .public)")
|
logger.debug("NotificationService receivedMsgNtf: \(res.responseType)")
|
||||||
switch res {
|
switch res {
|
||||||
case let .contactConnected(user, contact, _):
|
case let .contactConnected(user, contact, _):
|
||||||
return (contact.id, .nse(createContactConnectedNtf(user, contact)))
|
return (contact.id, .nse(createContactConnectedNtf(user, contact)))
|
||||||
@@ -613,6 +658,9 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
|||||||
case .chatSuspended:
|
case .chatSuspended:
|
||||||
chatSuspended()
|
chatSuspended()
|
||||||
return nil
|
return nil
|
||||||
|
case let .chatError(_, err):
|
||||||
|
logger.error("NotificationService receivedMsgNtf error: \(String(describing: err))")
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)")
|
logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)")
|
||||||
return nil
|
return nil
|
||||||
@@ -627,17 +675,22 @@ func updateNetCfg() {
|
|||||||
try setNetworkConfig(networkConfig)
|
try setNetworkConfig(networkConfig)
|
||||||
networkConfig = newNetConfig
|
networkConfig = newNetConfig
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("NotificationService apply changed network config error: \(responseError(error), privacy: .public)")
|
logger.error("NotificationService apply changed network config error: \(responseError(error))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGetActiveUser() -> User? {
|
func apiGetActiveUser() -> User? {
|
||||||
let r = sendSimpleXCmd(.showActiveUser)
|
let r = sendSimpleXCmd(.showActiveUser)
|
||||||
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(String(describing: r))")
|
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)")
|
||||||
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)):
|
||||||
|
logger.debug("apiGetActiveUser sendSimpleXCmd no active user")
|
||||||
|
return nil
|
||||||
|
case let .chatCmdError(_, err):
|
||||||
|
logger.debug("apiGetActiveUser sendSimpleXCmd error: \(String(describing: err))")
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))")
|
logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))")
|
||||||
return nil
|
return nil
|
||||||
@@ -645,7 +698,7 @@ func apiGetActiveUser() -> User? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func apiStartChat() throws -> Bool {
|
func apiStartChat() throws -> Bool {
|
||||||
let r = sendSimpleXCmd(.startChat(subscribe: false, expire: false, xftp: false))
|
let r = sendSimpleXCmd(.startChat(mainApp: false))
|
||||||
switch r {
|
switch r {
|
||||||
case .chatStarted: return true
|
case .chatStarted: return true
|
||||||
case .chatRunning: return false
|
case .chatRunning: return false
|
||||||
@@ -699,11 +752,12 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
|||||||
}
|
}
|
||||||
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
|
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||||
if case let .ntfMessages(user, connEntity_, msgTs, ntfMessages) = r, let user = user {
|
if case let .ntfMessages(user, connEntity_, msgTs, ntfMessages) = r, let user = user {
|
||||||
|
logger.debug("apiGetNtfMessage response ntfMessages: \(ntfMessages.count)")
|
||||||
return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages)
|
return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages)
|
||||||
} else if case let .chatCmdError(_, error) = r {
|
} else if case let .chatCmdError(_, error) = r {
|
||||||
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
|
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
|
||||||
} else {
|
} else {
|
||||||
logger.debug("apiGetNtfMessage ignored response: \(r.responseType, privacy: .public) \(String.init(describing: r), privacy: .private)")
|
logger.debug("apiGetNtfMessage ignored response: \(r.responseType) \(String.init(describing: r))")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
|
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
|
||||||
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
|
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
|
||||||
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
|
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
|
||||||
|
5C245F192B4DB982001CC39F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C245F142B4DB982001CC39F /* libgmpxx.a */; };
|
||||||
|
5C245F1A2B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C245F152B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a */; };
|
||||||
|
5C245F1B2B4DB982001CC39F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C245F162B4DB982001CC39F /* libgmp.a */; };
|
||||||
|
5C245F1C2B4DB982001CC39F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C245F172B4DB982001CC39F /* libffi.a */; };
|
||||||
|
5C245F1D2B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C245F182B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a */; };
|
||||||
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
|
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
|
||||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
|
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
|
||||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; };
|
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; };
|
||||||
@@ -42,11 +47,6 @@
|
|||||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
|
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
|
||||||
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
|
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
|
||||||
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
|
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
|
||||||
5C4E80E42B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80DF2B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a */; };
|
|
||||||
5C4E80E52B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E02B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a */; };
|
|
||||||
5C4E80E62B40A96C0080FAE2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E12B40A96C0080FAE2 /* libgmp.a */; };
|
|
||||||
5C4E80E72B40A96C0080FAE2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E22B40A96C0080FAE2 /* libgmpxx.a */; };
|
|
||||||
5C4E80E82B40A96C0080FAE2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E32B40A96C0080FAE2 /* libffi.a */; };
|
|
||||||
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
|
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
|
||||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
|
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
|
||||||
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
|
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
|
||||||
@@ -275,6 +275,11 @@
|
|||||||
5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; };
|
5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; };
|
||||||
5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||||
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; };
|
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; };
|
||||||
|
5C245F142B4DB982001CC39F /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
|
5C245F152B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||||
|
5C245F162B4DB982001CC39F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
|
5C245F172B4DB982001CC39F /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
|
5C245F182B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a"; sourceTree = "<group>"; };
|
||||||
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
|
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
|
||||||
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
|
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
|
||||||
5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
||||||
@@ -289,11 +294,6 @@
|
|||||||
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
|
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
|
||||||
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
|
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
|
||||||
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
|
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
|
||||||
5C4E80DF2B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a"; sourceTree = "<group>"; };
|
|
||||||
5C4E80E02B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a"; sourceTree = "<group>"; };
|
|
||||||
5C4E80E12B40A96C0080FAE2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
|
||||||
5C4E80E22B40A96C0080FAE2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
|
||||||
5C4E80E32B40A96C0080FAE2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
|
||||||
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
|
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
|
||||||
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
|
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
|
||||||
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
|
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
|
||||||
@@ -511,13 +511,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5C4E80E72B40A96C0080FAE2 /* libgmpxx.a in Frameworks */,
|
5C245F192B4DB982001CC39F /* libgmpxx.a in Frameworks */,
|
||||||
|
5C245F1C2B4DB982001CC39F /* libffi.a in Frameworks */,
|
||||||
|
5C245F1D2B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a in Frameworks */,
|
||||||
|
5C245F1B2B4DB982001CC39F /* libgmp.a in Frameworks */,
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
5C4E80E62B40A96C0080FAE2 /* libgmp.a in Frameworks */,
|
5C245F1A2B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a in Frameworks */,
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
5C4E80E82B40A96C0080FAE2 /* libffi.a in Frameworks */,
|
|
||||||
5C4E80E52B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a in Frameworks */,
|
|
||||||
5C4E80E42B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a in Frameworks */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -579,11 +579,11 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5C4E80E32B40A96C0080FAE2 /* libffi.a */,
|
5C245F172B4DB982001CC39F /* libffi.a */,
|
||||||
5C4E80E12B40A96C0080FAE2 /* libgmp.a */,
|
5C245F162B4DB982001CC39F /* libgmp.a */,
|
||||||
5C4E80E22B40A96C0080FAE2 /* libgmpxx.a */,
|
5C245F142B4DB982001CC39F /* libgmpxx.a */,
|
||||||
5C4E80E02B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a */,
|
5C245F152B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a */,
|
||||||
5C4E80DF2B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a */,
|
5C245F182B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a */,
|
||||||
);
|
);
|
||||||
path = Libraries;
|
path = Libraries;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1400"
|
LastUpgradeVersion = "1400"
|
||||||
wasCreatedForAppExtension = "YES"
|
wasCreatedForAppExtension = "YES"
|
||||||
version = "2.0">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
@@ -47,16 +47,14 @@
|
|||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = ""
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
askForAppToLaunch = "Yes"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
allowLocationSimulation = "YES"
|
allowLocationSimulation = "YES">
|
||||||
launchAutomaticallySubstyle = "2">
|
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ private var chatController: chat_ctrl?
|
|||||||
|
|
||||||
private var migrationResult: (Bool, DBMigrationResult)?
|
private var migrationResult: (Bool, DBMigrationResult)?
|
||||||
|
|
||||||
public func getChatCtrl(_ useKey: String? = nil) -> chat_ctrl {
|
public func hasChatCtrl() -> Bool {
|
||||||
|
chatController != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getChatCtrl() -> chat_ctrl {
|
||||||
if let controller = chatController { return controller }
|
if let controller = chatController { return controller }
|
||||||
fatalError("chat controller not initialized")
|
fatalError("chat controller not initialized")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public enum ChatCommand {
|
|||||||
case apiMuteUser(userId: Int64)
|
case apiMuteUser(userId: Int64)
|
||||||
case apiUnmuteUser(userId: Int64)
|
case apiUnmuteUser(userId: Int64)
|
||||||
case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?)
|
case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?)
|
||||||
case startChat(subscribe: Bool, expire: Bool, xftp: Bool)
|
case startChat(mainApp: Bool)
|
||||||
case apiStopChat
|
case apiStopChat
|
||||||
case apiActivateChat(restoreChat: Bool)
|
case apiActivateChat(restoreChat: Bool)
|
||||||
case apiSuspendChat(timeoutMicroseconds: Int)
|
case apiSuspendChat(timeoutMicroseconds: Int)
|
||||||
@@ -154,7 +154,7 @@ public enum ChatCommand {
|
|||||||
case let .apiMuteUser(userId): return "/_mute user \(userId)"
|
case let .apiMuteUser(userId): return "/_mute user \(userId)"
|
||||||
case let .apiUnmuteUser(userId): return "/_unmute user \(userId)"
|
case let .apiUnmuteUser(userId): return "/_unmute user \(userId)"
|
||||||
case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))"
|
case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))"
|
||||||
case let .startChat(subscribe, expire, xftp): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire)) xftp=\(onOff(xftp))"
|
case let .startChat(mainApp): return "/_start main=\(onOff(mainApp))"
|
||||||
case .apiStopChat: return "/_stop"
|
case .apiStopChat: return "/_stop"
|
||||||
case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
|
case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
|
||||||
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
|
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
|
||||||
@@ -1610,6 +1610,7 @@ public enum ChatErrorType: Decodable {
|
|||||||
case userUnknown
|
case userUnknown
|
||||||
case activeUserExists
|
case activeUserExists
|
||||||
case userExists
|
case userExists
|
||||||
|
case invalidDisplayName
|
||||||
case differentActiveUser(commandUserId: Int64, activeUserId: Int64)
|
case differentActiveUser(commandUserId: Int64, activeUserId: Int64)
|
||||||
case cantDeleteActiveUser(userId: Int64)
|
case cantDeleteActiveUser(userId: Int64)
|
||||||
case cantDeleteLastUser(userId: Int64)
|
case cantDeleteLastUser(userId: Int64)
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct UserProfileUpdateSummary: Decodable {
|
public struct UserProfileUpdateSummary: Decodable {
|
||||||
public var notChanged: Int
|
|
||||||
public var updateSuccesses: Int
|
public var updateSuccesses: Int
|
||||||
public var updateFailures: Int
|
public var updateFailures: Int
|
||||||
public var changedContacts: [Contact]
|
public var changedContacts: [Contact]
|
||||||
|
|||||||
@@ -69,13 +69,29 @@ func fileModificationDate(_ path: String) -> Date? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func deleteAppDatabaseAndFiles() {
|
||||||
|
let fm = FileManager.default
|
||||||
|
let dbPath = getAppDatabasePath().path
|
||||||
|
do {
|
||||||
|
try fm.removeItem(atPath: dbPath + CHAT_DB)
|
||||||
|
try fm.removeItem(atPath: dbPath + AGENT_DB)
|
||||||
|
} catch let error {
|
||||||
|
logger.error("Failed to delete all databases: \(error)")
|
||||||
|
}
|
||||||
|
try? fm.removeItem(atPath: dbPath + CHAT_DB_BAK)
|
||||||
|
try? fm.removeItem(atPath: dbPath + AGENT_DB_BAK)
|
||||||
|
try? fm.removeItem(at: getTempFilesDirectory())
|
||||||
|
try? fm.createDirectory(at: getTempFilesDirectory(), withIntermediateDirectories: true)
|
||||||
|
deleteAppFiles()
|
||||||
|
_ = kcDatabasePassword.remove()
|
||||||
|
storeDBPassphraseGroupDefault.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
public func deleteAppFiles() {
|
public func deleteAppFiles() {
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
do {
|
do {
|
||||||
let fileNames = try fm.contentsOfDirectory(atPath: getAppFilesDirectory().path)
|
try fm.removeItem(at: getAppFilesDirectory())
|
||||||
for fileName in fileNames {
|
try fm.createDirectory(at: getAppFilesDirectory(), withIntermediateDirectories: true)
|
||||||
removeFile(fileName)
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)")
|
logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,3 +23,19 @@ void haskell_init(void) {
|
|||||||
char **pargv = argv;
|
char **pargv = argv;
|
||||||
hs_init_with_rtsopts(&argc, &pargv);
|
hs_init_with_rtsopts(&argc, &pargv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void haskell_init_nse(void) {
|
||||||
|
int argc = 7;
|
||||||
|
char *argv[] = {
|
||||||
|
"simplex",
|
||||||
|
"+RTS", // requires `hs_init_with_rtsopts`
|
||||||
|
"-A1m", // chunk size for new allocations
|
||||||
|
"-H1m", // initial heap size
|
||||||
|
"-F0.5", // heap growth triggering GC
|
||||||
|
"-Fd1", // memory return
|
||||||
|
"-c", // compacting garbage collector
|
||||||
|
0
|
||||||
|
};
|
||||||
|
char **pargv = argv;
|
||||||
|
hs_init_with_rtsopts(&argc, &pargv);
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,4 +11,6 @@
|
|||||||
|
|
||||||
void haskell_init(void);
|
void haskell_init(void);
|
||||||
|
|
||||||
|
void haskell_init_nse(void);
|
||||||
|
|
||||||
#endif /* hs_init_h */
|
#endif /* hs_init_h */
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package chat.simplex.app
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import chat.simplex.app.*
|
|
||||||
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
|
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
|
||||||
import chat.simplex.common.model.ChatController
|
import chat.simplex.common.model.ChatController
|
||||||
import chat.simplex.common.views.helpers.DBMigrationResult
|
import chat.simplex.common.views.helpers.DBMigrationResult
|
||||||
import chat.simplex.app.BuildConfig
|
import chat.simplex.common.platform.chatModel
|
||||||
|
import chat.simplex.common.platform.initChatControllerAndRunMigrations
|
||||||
|
import chat.simplex.common.views.helpers.DatabaseUtils
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -57,6 +58,10 @@ class MessagesFetcherWork(
|
|||||||
val durationSeconds = inputData.getInt(INPUT_DATA_DURATION, 60)
|
val durationSeconds = inputData.getInt(INPUT_DATA_DURATION, 60)
|
||||||
var shouldReschedule = true
|
var shouldReschedule = true
|
||||||
try {
|
try {
|
||||||
|
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
||||||
|
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
||||||
|
initChatControllerAndRunMigrations()
|
||||||
|
}
|
||||||
withTimeout(durationSeconds * 1000L) {
|
withTimeout(durationSeconds * 1000L) {
|
||||||
val chatController = ChatController
|
val chatController = ChatController
|
||||||
SimplexService.waitDbMigrationEnds(chatController)
|
SimplexService.waitDbMigrationEnds(chatController)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
const val TAG = "SIMPLEX"
|
const val TAG = "SIMPLEX"
|
||||||
|
|
||||||
@@ -46,8 +47,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
try {
|
try {
|
||||||
Looper.loop()
|
Looper.loop()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (e.message != null && e.message!!.startsWith("Unable to start activity")) {
|
if (e is UnsatisfiedLinkError || e.message?.startsWith("Unable to start activity") == true) {
|
||||||
android.os.Process.killProcess(android.os.Process.myPid())
|
Process.killProcess(Process.myPid())
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
// Send it to our exception handled because it will not get the exception otherwise
|
// Send it to our exception handled because it will not get the exception otherwise
|
||||||
@@ -63,7 +64,9 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
tmpDir.deleteRecursively()
|
tmpDir.deleteRecursively()
|
||||||
tmpDir.mkdir()
|
tmpDir.mkdir()
|
||||||
|
|
||||||
initChatControllerAndRunMigrations(false)
|
if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||||
|
initChatControllerAndRunMigrations()
|
||||||
|
}
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +80,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
updatingChatsMutex.withLock {
|
updatingChatsMutex.withLock {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val currentUserId = chatModel.currentUser.value?.userId
|
val currentUserId = chatModel.currentUser.value?.userId
|
||||||
val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId()))
|
val chats = ArrayList(chatController.apiGetChatsWithoutAlert(chatModel.remoteHostId()) ?: return@runCatching)
|
||||||
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
|
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
|
||||||
if (chatModel.currentUser.value?.userId == currentUserId) {
|
if (chatModel.currentUser.value?.userId == currentUserId) {
|
||||||
val currentChatId = chatModel.chatId.value
|
val currentChatId = chatModel.chatId.value
|
||||||
@@ -171,13 +174,14 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
androidAppContext = this
|
androidAppContext = this
|
||||||
APPLICATION_ID = BuildConfig.APPLICATION_ID
|
APPLICATION_ID = BuildConfig.APPLICATION_ID
|
||||||
ntfManager = object : chat.simplex.common.platform.NtfManager() {
|
ntfManager = object : chat.simplex.common.platform.NtfManager() {
|
||||||
override fun notifyCallInvitation(invitation: RcvCallInvitation) = NtfManager.notifyCallInvitation(invitation)
|
override fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean = NtfManager.notifyCallInvitation(invitation)
|
||||||
override fun hasNotificationsForChat(chatId: String): Boolean = NtfManager.hasNotificationsForChat(chatId)
|
override fun hasNotificationsForChat(chatId: String): Boolean = NtfManager.hasNotificationsForChat(chatId)
|
||||||
override fun cancelNotificationsForChat(chatId: String) = NtfManager.cancelNotificationsForChat(chatId)
|
override fun cancelNotificationsForChat(chatId: String) = NtfManager.cancelNotificationsForChat(chatId)
|
||||||
override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions.map { it.first })
|
override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions.map { it.first })
|
||||||
override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert()
|
override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert()
|
||||||
override fun cancelCallNotification() = NtfManager.cancelCallNotification()
|
override fun cancelCallNotification() = NtfManager.cancelCallNotification()
|
||||||
override fun cancelAllNotifications() = NtfManager.cancelAllNotifications()
|
override fun cancelAllNotifications() = NtfManager.cancelAllNotifications()
|
||||||
|
override fun showMessage(title: String, text: String) = NtfManager.showMessage(title, text)
|
||||||
}
|
}
|
||||||
platform = object : PlatformInterface {
|
platform = object : PlatformInterface {
|
||||||
override suspend fun androidServiceStart() {
|
override suspend fun androidServiceStart() {
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ class SimplexService: Service() {
|
|||||||
stopSelf()
|
stopSelf()
|
||||||
} else {
|
} else {
|
||||||
isServiceStarted = true
|
isServiceStarted = true
|
||||||
|
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
||||||
|
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
||||||
|
initChatControllerAndRunMigrations()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-5
@@ -30,7 +30,7 @@ object NtfManager {
|
|||||||
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
|
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
|
||||||
|
|
||||||
// DO NOT change notification channel settings / names
|
// DO NOT change notification channel settings / names
|
||||||
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
|
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_2"
|
||||||
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
|
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
|
||||||
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
|
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
|
||||||
const val CallNotificationId: Int = -1
|
const val CallNotificationId: Int = -1
|
||||||
@@ -59,7 +59,7 @@ object NtfManager {
|
|||||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||||
.build()
|
.build()
|
||||||
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
|
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/ring_once")
|
||||||
Log.d(TAG, "callNotificationChannel sound: $soundUri")
|
Log.d(TAG, "callNotificationChannel sound: $soundUri")
|
||||||
callChannel.setSound(soundUri, attrs)
|
callChannel.setSound(soundUri, attrs)
|
||||||
callChannel.enableVibration(true)
|
callChannel.enableVibration(true)
|
||||||
@@ -140,7 +140,7 @@ object NtfManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notifyCallInvitation(invitation: RcvCallInvitation) {
|
fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean {
|
||||||
val keyguardManager = getKeyguardManager(context)
|
val keyguardManager = getKeyguardManager(context)
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
@@ -149,7 +149,7 @@ object NtfManager {
|
|||||||
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " +
|
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " +
|
||||||
"onForeground ${isAppOnForeground}"
|
"onForeground ${isAppOnForeground}"
|
||||||
)
|
)
|
||||||
if (isAppOnForeground) return
|
if (isAppOnForeground) return false
|
||||||
val contactId = invitation.contact.id
|
val contactId = invitation.contact.id
|
||||||
Log.d(TAG, "notifyCallInvitation $contactId")
|
Log.d(TAG, "notifyCallInvitation $contactId")
|
||||||
val image = invitation.contact.image
|
val image = invitation.contact.image
|
||||||
@@ -163,7 +163,7 @@ object NtfManager {
|
|||||||
.setFullScreenIntent(fullScreenPendingIntent, true)
|
.setFullScreenIntent(fullScreenPendingIntent, true)
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
} else {
|
} else {
|
||||||
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
|
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/ring_once")
|
||||||
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
NotificationCompat.Builder(context, CallChannel)
|
NotificationCompat.Builder(context, CallChannel)
|
||||||
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id))
|
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id))
|
||||||
@@ -206,6 +206,39 @@ object NtfManager {
|
|||||||
notify(CallNotificationId, notification)
|
notify(CallNotificationId, notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMessage(title: String, text: String) {
|
||||||
|
val builder = NotificationCompat.Builder(context, MessageChannel)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setGroup(MessageGroup)
|
||||||
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
||||||
|
.setSmallIcon(R.drawable.ntf_icon)
|
||||||
|
.setLargeIcon(null)
|
||||||
|
.setColor(0x88FFFF)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setVibrate(null)
|
||||||
|
.setContentIntent(chatPendingIntent(ShowChatsAction, null, null))
|
||||||
|
.setSilent(false)
|
||||||
|
|
||||||
|
val summary = NotificationCompat.Builder(context, MessageChannel)
|
||||||
|
.setSmallIcon(R.drawable.ntf_icon)
|
||||||
|
.setColor(0x88FFFF)
|
||||||
|
.setGroup(MessageGroup)
|
||||||
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
||||||
|
.setGroupSummary(true)
|
||||||
|
.setContentIntent(chatPendingIntent(ShowChatsAction, null))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
with(NotificationManagerCompat.from(context)) {
|
||||||
|
if (ActivityCompat.checkSelfPermission(SimplexApp.context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
notify("MESSAGE".hashCode(), builder.build())
|
||||||
|
notify(0, summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelCallNotification() {
|
fun cancelCallNotification() {
|
||||||
@@ -248,6 +281,7 @@ object NtfManager {
|
|||||||
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(MR.strings.ntf_channel_calls)))
|
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(MR.strings.ntf_channel_calls)))
|
||||||
// Remove old channels since they can't be edited
|
// Remove old channels since they can't be edited
|
||||||
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
|
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
|
||||||
|
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION_1")
|
||||||
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
|
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -97,7 +97,8 @@ fun IncomingCallActivityView(m: ChatModel) {
|
|||||||
Surface(
|
Surface(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
color = MaterialTheme.colors.background
|
color = MaterialTheme.colors.background,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
if (showCallView) {
|
if (showCallView) {
|
||||||
Box {
|
Box {
|
||||||
@@ -200,7 +201,8 @@ private fun SimpleXLogo() {
|
|||||||
private fun LockScreenCallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
|
private fun LockScreenCallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
color = Color.Transparent
|
color = Color.Transparent,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
@@ -227,7 +229,8 @@ fun PreviewIncomingCallLockScreenAlert() {
|
|||||||
Surface(
|
Surface(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
color = MaterialTheme.colors.background
|
color = MaterialTheme.colors.background,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
IncomingCallLockScreenAlertLayout(
|
IncomingCallLockScreenAlertLayout(
|
||||||
invitation = RcvCallInvitation(
|
invitation = RcvCallInvitation(
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package chat.simplex.common.views.database
|
||||||
|
|
||||||
|
import chat.simplex.common.views.usersettings.restartApp
|
||||||
|
|
||||||
|
actual fun restartChatOrApp() {
|
||||||
|
restartApp()
|
||||||
|
}
|
||||||
+1
-1
@@ -28,7 +28,7 @@ actual fun SettingsSectionApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun restartApp() {
|
fun restartApp() {
|
||||||
ProcessPhoenix.triggerRebirth(androidAppContext)
|
ProcessPhoenix.triggerRebirth(androidAppContext)
|
||||||
shutdownApp()
|
shutdownApp()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ data class SettingsViewState(
|
|||||||
fun AppScreen() {
|
fun AppScreen() {
|
||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||||
Surface(color = MaterialTheme.colors.background) {
|
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||||
MainScreen()
|
MainScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ fun MainScreen() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthView() {
|
fun AuthView() {
|
||||||
Surface(color = MaterialTheme.colors.background) {
|
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||||
Box(
|
Box(
|
||||||
Modifier.fillMaxSize(),
|
Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package chat.simplex.common
|
package chat.simplex.common
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
@@ -107,7 +106,7 @@ object AppLock {
|
|||||||
private fun setPasscode() {
|
private fun setPasscode() {
|
||||||
val appPrefs = ChatController.appPrefs
|
val appPrefs = ChatController.appPrefs
|
||||||
ModalManager.fullscreen.showCustomModal { close ->
|
ModalManager.fullscreen.showCustomModal { close ->
|
||||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||||
SetAppPasscodeView(
|
SetAppPasscodeView(
|
||||||
submit = {
|
submit = {
|
||||||
ChatModel.performLA.value = true
|
ChatModel.performLA.value = true
|
||||||
|
|||||||
+3
-1
@@ -125,6 +125,9 @@ object ChatModel {
|
|||||||
val remoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null)
|
val remoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null)
|
||||||
val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null)
|
val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null)
|
||||||
|
|
||||||
|
val processedCriticalError: ProcessedErrors<AgentErrorType.CRITICAL> = ProcessedErrors(60_000)
|
||||||
|
val processedInternalError: ProcessedErrors<AgentErrorType.INTERNAL> = ProcessedErrors(20_000)
|
||||||
|
|
||||||
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
|
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
|
||||||
currentUser.value
|
currentUser.value
|
||||||
} else {
|
} else {
|
||||||
@@ -1151,7 +1154,6 @@ data class LocalProfile(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserProfileUpdateSummary(
|
data class UserProfileUpdateSummary(
|
||||||
val notChanged: Int,
|
|
||||||
val updateSuccesses: Int,
|
val updateSuccesses: Int,
|
||||||
val updateFailures: Int,
|
val updateFailures: Int,
|
||||||
val changedContacts: List<Contact>
|
val changedContacts: List<Contact>
|
||||||
|
|||||||
+111
-6
@@ -108,6 +108,7 @@ class AppPreferences {
|
|||||||
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
|
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
|
||||||
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
|
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
|
||||||
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
|
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
|
||||||
|
val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false)
|
||||||
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
|
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
|
||||||
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
|
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
|
||||||
val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050")
|
val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050")
|
||||||
@@ -276,6 +277,7 @@ class AppPreferences {
|
|||||||
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"
|
||||||
|
private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors"
|
||||||
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
|
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
|
||||||
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
|
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
|
||||||
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
|
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
|
||||||
@@ -504,6 +506,10 @@ object ChatController {
|
|||||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.UserExists
|
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.UserExists
|
||||||
) {
|
) {
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc))
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc))
|
||||||
|
} else if (
|
||||||
|
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.InvalidDisplayName
|
||||||
|
) {
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_invalid_title), generalGetString(MR.strings.failed_to_create_user_invalid_desc))
|
||||||
} else {
|
} else {
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_title), r.details)
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_title), r.details)
|
||||||
}
|
}
|
||||||
@@ -572,7 +578,7 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiStartChat(): Boolean {
|
suspend fun apiStartChat(): Boolean {
|
||||||
val r = sendCmd(null, CC.StartChat(expire = true))
|
val r = sendCmd(null, CC.StartChat(mainApp = true))
|
||||||
when (r) {
|
when (r) {
|
||||||
is CR.ChatStarted -> return true
|
is CR.ChatStarted -> return true
|
||||||
is CR.ChatRunning -> return false
|
is CR.ChatRunning -> return false
|
||||||
@@ -648,6 +654,15 @@ object ChatController {
|
|||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It's useful for situations when active user can be changed concurrently and there is no need to show alert in case of failure
|
||||||
|
suspend fun apiGetChatsWithoutAlert(rh: Long?): List<Chat>? {
|
||||||
|
val userId = kotlin.runCatching { currentUserId("apiGetChats") }.getOrElse { return null }
|
||||||
|
val r = sendCmd(rh, CC.ApiGetChats(userId))
|
||||||
|
if (r is CR.ApiChats) return if (rh == null) r.chats else r.chats.map { it.copy(remoteHostId = rh) }
|
||||||
|
Log.e(TAG, "failed getting the list of chats: ${r.responseType} ${r.details}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search: String = ""): Chat? {
|
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search: String = ""): Chat? {
|
||||||
val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search))
|
val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search))
|
||||||
if (r is CR.ApiChat) return if (rh == null) r.chat else r.chat.copy(remoteHostId = rh)
|
if (r is CR.ApiChat) return if (rh == null) r.chat else r.chat.copy(remoteHostId = rh)
|
||||||
@@ -1122,6 +1137,13 @@ object ChatController {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun apiGetCallInvitations(rh: Long?): List<RcvCallInvitation> {
|
||||||
|
val r = sendCmd(rh, CC.ApiGetCallInvitations())
|
||||||
|
if (r is CR.CallInvitations) return r.callInvitations
|
||||||
|
Log.e(TAG, "apiGetCallInvitations bad response: ${r.responseType} ${r.details}")
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiSendCallInvitation(rh: Long?, contact: Contact, callType: CallType): Boolean {
|
suspend fun apiSendCallInvitation(rh: Long?, contact: Contact, callType: CallType): Boolean {
|
||||||
val r = sendCmd(rh, CC.ApiSendCallInvitation(contact, callType))
|
val r = sendCmd(rh, CC.ApiSendCallInvitation(contact, callType))
|
||||||
return r is CR.CmdOk
|
return r is CR.CmdOk
|
||||||
@@ -1878,9 +1900,34 @@ object ChatController {
|
|||||||
val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ }
|
val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ }
|
||||||
chatModel.remoteHostPairing.value = null
|
chatModel.remoteHostPairing.value = null
|
||||||
if (disconnectedHost != null) {
|
if (disconnectedHost != null) {
|
||||||
showToast(
|
val deviceName = disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() }
|
||||||
generalGetString(MR.strings.remote_host_was_disconnected_toast).format(disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() })
|
when (r.rhStopReason) {
|
||||||
)
|
is RemoteHostStopReason.ConnectionFailed -> {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.remote_host_was_disconnected_title),
|
||||||
|
if (r.rhStopReason.chatError is ChatError.ChatErrorRemoteHost) {
|
||||||
|
r.rhStopReason.chatError.remoteHostError.localizedString(deviceName)
|
||||||
|
} else {
|
||||||
|
generalGetString(MR.strings.remote_host_disconnected_from).format(deviceName, r.rhStopReason.chatError.string)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RemoteHostStopReason.Crashed -> {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.remote_host_was_disconnected_title),
|
||||||
|
if (r.rhStopReason.chatError is ChatError.ChatErrorRemoteHost) {
|
||||||
|
r.rhStopReason.chatError.remoteHostError.localizedString(deviceName)
|
||||||
|
} else {
|
||||||
|
generalGetString(MR.strings.remote_host_disconnected_from).format(deviceName, r.rhStopReason.chatError.string)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RemoteHostStopReason.Disconnected -> {
|
||||||
|
if (r.rhsState is RemoteHostSessionState.Connected || r.rhsState is RemoteHostSessionState.Confirmed) {
|
||||||
|
showToast(generalGetString(MR.strings.remote_host_was_disconnected_toast).format(deviceName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (chatModel.remoteHostId() == r.remoteHostId_) {
|
if (chatModel.remoteHostId() == r.remoteHostId_) {
|
||||||
chatModel.currentRemoteHost.value = null
|
chatModel.currentRemoteHost.value = null
|
||||||
@@ -1911,11 +1958,40 @@ object ChatController {
|
|||||||
val sess = chatModel.remoteCtrlSession.value
|
val sess = chatModel.remoteCtrlSession.value
|
||||||
if (sess != null) {
|
if (sess != null) {
|
||||||
chatModel.remoteCtrlSession.value = null
|
chatModel.remoteCtrlSession.value = null
|
||||||
|
fun showAlert(chatError: ChatError) {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
|
||||||
|
if (chatError is ChatError.ChatErrorRemoteCtrl) {
|
||||||
|
chatError.remoteCtrlError.localizedString
|
||||||
|
} else {
|
||||||
|
generalGetString(MR.strings.remote_ctrl_disconnected_with_reason).format(chatError.string)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
when (r.rcStopReason) {
|
||||||
|
is RemoteCtrlStopReason.DiscoveryFailed -> showAlert(r.rcStopReason.chatError)
|
||||||
|
is RemoteCtrlStopReason.ConnectionFailed -> showAlert(r.rcStopReason.chatError)
|
||||||
|
is RemoteCtrlStopReason.SetupFailed -> showAlert(r.rcStopReason.chatError)
|
||||||
|
is RemoteCtrlStopReason.Disconnected -> {
|
||||||
|
/*AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
|
||||||
|
)*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sess.sessionState is UIRemoteCtrlSessionState.Connected) {
|
if (sess.sessionState is UIRemoteCtrlSessionState.Connected) {
|
||||||
switchToLocalSession()
|
switchToLocalSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is CR.ChatCmdError -> when {
|
||||||
|
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> {
|
||||||
|
chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart)
|
||||||
|
}
|
||||||
|
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.INTERNAL && appPrefs.showInternalErrors.get() -> {
|
||||||
|
chatModel.processedInternalError.newError(r.chatError.agentError, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
else ->
|
else ->
|
||||||
Log.d(TAG , "unsupported event: ${r.responseType}")
|
Log.d(TAG , "unsupported event: ${r.responseType}")
|
||||||
}
|
}
|
||||||
@@ -2165,7 +2241,7 @@ sealed class CC {
|
|||||||
class ApiMuteUser(val userId: Long): CC()
|
class ApiMuteUser(val userId: Long): CC()
|
||||||
class ApiUnmuteUser(val userId: Long): CC()
|
class ApiUnmuteUser(val userId: Long): CC()
|
||||||
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
|
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
|
||||||
class StartChat(val expire: Boolean): CC()
|
class StartChat(val mainApp: Boolean): CC()
|
||||||
class ApiStopChat: CC()
|
class ApiStopChat: CC()
|
||||||
class SetTempFolder(val tempFolder: String): CC()
|
class SetTempFolder(val tempFolder: String): CC()
|
||||||
class SetFilesFolder(val filesFolder: String): CC()
|
class SetFilesFolder(val filesFolder: String): CC()
|
||||||
@@ -2236,6 +2312,7 @@ sealed class CC {
|
|||||||
class ApiShowMyAddress(val userId: Long): CC()
|
class ApiShowMyAddress(val userId: Long): CC()
|
||||||
class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC()
|
class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC()
|
||||||
class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC()
|
class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC()
|
||||||
|
class ApiGetCallInvitations: CC()
|
||||||
class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC()
|
class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC()
|
||||||
class ApiRejectCall(val contact: Contact): CC()
|
class ApiRejectCall(val contact: Contact): CC()
|
||||||
class ApiSendCallOffer(val contact: Contact, val callOffer: WebRTCCallOffer): CC()
|
class ApiSendCallOffer(val contact: Contact, val callOffer: WebRTCCallOffer): CC()
|
||||||
@@ -2292,7 +2369,7 @@ sealed class CC {
|
|||||||
is ApiMuteUser -> "/_mute user $userId"
|
is ApiMuteUser -> "/_mute user $userId"
|
||||||
is ApiUnmuteUser -> "/_unmute user $userId"
|
is ApiUnmuteUser -> "/_unmute user $userId"
|
||||||
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
|
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
|
||||||
is StartChat -> "/_start subscribe=on expire=${onOff(expire)} xftp=on"
|
is StartChat -> "/_start main=${onOff(mainApp)}"
|
||||||
is ApiStopChat -> "/_stop"
|
is ApiStopChat -> "/_stop"
|
||||||
is SetTempFolder -> "/_temp_folder $tempFolder"
|
is SetTempFolder -> "/_temp_folder $tempFolder"
|
||||||
is SetFilesFolder -> "/_files_folder $filesFolder"
|
is SetFilesFolder -> "/_files_folder $filesFolder"
|
||||||
@@ -2372,6 +2449,7 @@ sealed class CC {
|
|||||||
is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}"
|
is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}"
|
||||||
is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId"
|
is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId"
|
||||||
is ApiRejectContact -> "/_reject $contactReqId"
|
is ApiRejectContact -> "/_reject $contactReqId"
|
||||||
|
is ApiGetCallInvitations -> "/_call get"
|
||||||
is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}"
|
is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}"
|
||||||
is ApiRejectCall -> "/_call reject @${contact.apiId}"
|
is ApiRejectCall -> "/_call reject @${contact.apiId}"
|
||||||
is ApiSendCallOffer -> "/_call offer @${contact.apiId} ${json.encodeToString(callOffer)}"
|
is ApiSendCallOffer -> "/_call offer @${contact.apiId} ${json.encodeToString(callOffer)}"
|
||||||
@@ -2495,6 +2573,7 @@ sealed class CC {
|
|||||||
is ApiAddressAutoAccept -> "apiAddressAutoAccept"
|
is ApiAddressAutoAccept -> "apiAddressAutoAccept"
|
||||||
is ApiAcceptContact -> "apiAcceptContact"
|
is ApiAcceptContact -> "apiAcceptContact"
|
||||||
is ApiRejectContact -> "apiRejectContact"
|
is ApiRejectContact -> "apiRejectContact"
|
||||||
|
is ApiGetCallInvitations -> "apiGetCallInvitations"
|
||||||
is ApiSendCallInvitation -> "apiSendCallInvitation"
|
is ApiSendCallInvitation -> "apiSendCallInvitation"
|
||||||
is ApiRejectCall -> "apiRejectCall"
|
is ApiRejectCall -> "apiRejectCall"
|
||||||
is ApiSendCallOffer -> "apiSendCallOffer"
|
is ApiSendCallOffer -> "apiSendCallOffer"
|
||||||
@@ -3870,6 +3949,7 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem: AChatItem): CR()
|
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem: AChatItem): 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("callOffer") class CallOffer(val user: UserRef, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR()
|
@Serializable @SerialName("callOffer") class CallOffer(val user: UserRef, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR()
|
||||||
@Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR()
|
@Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR()
|
||||||
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
|
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
|
||||||
@@ -4017,6 +4097,7 @@ sealed class CR {
|
|||||||
is SndFileProgressXFTP -> "sndFileProgressXFTP"
|
is SndFileProgressXFTP -> "sndFileProgressXFTP"
|
||||||
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
|
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
|
||||||
is SndFileError -> "sndFileError"
|
is SndFileError -> "sndFileError"
|
||||||
|
is CallInvitations -> "callInvitations"
|
||||||
is CallInvitation -> "callInvitation"
|
is CallInvitation -> "callInvitation"
|
||||||
is CallOffer -> "callOffer"
|
is CallOffer -> "callOffer"
|
||||||
is CallAnswer -> "callAnswer"
|
is CallAnswer -> "callAnswer"
|
||||||
@@ -4163,6 +4244,7 @@ sealed class CR {
|
|||||||
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 SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
|
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||||
is SndFileError -> withUser(user, json.encodeToString(chatItem))
|
is SndFileError -> withUser(user, json.encodeToString(chatItem))
|
||||||
|
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)}")
|
||||||
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
|
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
|
||||||
@@ -4437,6 +4519,7 @@ sealed class ChatErrorType {
|
|||||||
is EmptyUserPassword -> "emptyUserPassword"
|
is EmptyUserPassword -> "emptyUserPassword"
|
||||||
is UserAlreadyHidden -> "userAlreadyHidden"
|
is UserAlreadyHidden -> "userAlreadyHidden"
|
||||||
is UserNotHidden -> "userNotHidden"
|
is UserNotHidden -> "userNotHidden"
|
||||||
|
is InvalidDisplayName -> "invalidDisplayName"
|
||||||
is ChatNotStarted -> "chatNotStarted"
|
is ChatNotStarted -> "chatNotStarted"
|
||||||
is ChatNotStopped -> "chatNotStopped"
|
is ChatNotStopped -> "chatNotStopped"
|
||||||
is ChatStoreChanged -> "chatStoreChanged"
|
is ChatStoreChanged -> "chatStoreChanged"
|
||||||
@@ -4514,6 +4597,7 @@ sealed class ChatErrorType {
|
|||||||
@Serializable @SerialName("emptyUserPassword") class EmptyUserPassword(val userId: Long): ChatErrorType()
|
@Serializable @SerialName("emptyUserPassword") class EmptyUserPassword(val userId: Long): ChatErrorType()
|
||||||
@Serializable @SerialName("userAlreadyHidden") class UserAlreadyHidden(val userId: Long): ChatErrorType()
|
@Serializable @SerialName("userAlreadyHidden") class UserAlreadyHidden(val userId: Long): ChatErrorType()
|
||||||
@Serializable @SerialName("userNotHidden") class UserNotHidden(val userId: Long): ChatErrorType()
|
@Serializable @SerialName("userNotHidden") class UserNotHidden(val userId: Long): ChatErrorType()
|
||||||
|
@Serializable @SerialName("invalidDisplayName") object InvalidDisplayName: ChatErrorType()
|
||||||
@Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType()
|
@Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType()
|
||||||
@Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType()
|
@Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType()
|
||||||
@Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType()
|
@Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType()
|
||||||
@@ -4731,6 +4815,7 @@ sealed class AgentErrorType {
|
|||||||
is AGENT -> "AGENT ${agentErr.string}"
|
is AGENT -> "AGENT ${agentErr.string}"
|
||||||
is INTERNAL -> "INTERNAL $internalErr"
|
is INTERNAL -> "INTERNAL $internalErr"
|
||||||
is INACTIVE -> "INACTIVE"
|
is INACTIVE -> "INACTIVE"
|
||||||
|
is CRITICAL -> "CRITICAL $offerRestart $criticalErr"
|
||||||
}
|
}
|
||||||
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType()
|
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType()
|
||||||
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType()
|
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType()
|
||||||
@@ -4742,6 +4827,7 @@ sealed class AgentErrorType {
|
|||||||
@Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType()
|
@Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType()
|
||||||
@Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType()
|
@Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType()
|
||||||
@Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType()
|
@Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType()
|
||||||
|
@Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -4961,6 +5047,15 @@ sealed class RemoteHostError {
|
|||||||
is BadVersion -> "badVersion"
|
is BadVersion -> "badVersion"
|
||||||
is Disconnected -> "disconnected"
|
is Disconnected -> "disconnected"
|
||||||
}
|
}
|
||||||
|
fun localizedString(name: String): String = when (this) {
|
||||||
|
is Missing -> generalGetString(MR.strings.remote_host_error_missing)
|
||||||
|
is Inactive -> generalGetString(MR.strings.remote_host_error_inactive)
|
||||||
|
is Busy -> generalGetString(MR.strings.remote_host_error_busy)
|
||||||
|
is Timeout -> generalGetString(MR.strings.remote_host_error_timeout)
|
||||||
|
is BadState -> generalGetString(MR.strings.remote_host_error_bad_state)
|
||||||
|
is BadVersion -> generalGetString(MR.strings.remote_host_error_bad_version)
|
||||||
|
is Disconnected -> generalGetString(MR.strings.remote_host_error_disconnected)
|
||||||
|
}.format(name)
|
||||||
@Serializable @SerialName("missing") object Missing: RemoteHostError()
|
@Serializable @SerialName("missing") object Missing: RemoteHostError()
|
||||||
@Serializable @SerialName("inactive") object Inactive: RemoteHostError()
|
@Serializable @SerialName("inactive") object Inactive: RemoteHostError()
|
||||||
@Serializable @SerialName("busy") object Busy: RemoteHostError()
|
@Serializable @SerialName("busy") object Busy: RemoteHostError()
|
||||||
@@ -4981,6 +5076,16 @@ sealed class RemoteCtrlError {
|
|||||||
is BadInvitation -> "badInvitation"
|
is BadInvitation -> "badInvitation"
|
||||||
is BadVersion -> "badVersion"
|
is BadVersion -> "badVersion"
|
||||||
}
|
}
|
||||||
|
val localizedString: String get() = when (this) {
|
||||||
|
is Inactive -> generalGetString(MR.strings.remote_ctrl_error_inactive)
|
||||||
|
is BadState -> generalGetString(MR.strings.remote_ctrl_error_bad_state)
|
||||||
|
is Busy -> generalGetString(MR.strings.remote_ctrl_error_busy)
|
||||||
|
is Timeout -> generalGetString(MR.strings.remote_ctrl_error_timeout)
|
||||||
|
is Disconnected -> generalGetString(MR.strings.remote_ctrl_error_disconnected)
|
||||||
|
is BadInvitation -> generalGetString(MR.strings.remote_ctrl_error_bad_invitation)
|
||||||
|
is BadVersion -> generalGetString(MR.strings.remote_ctrl_error_bad_version)
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable @SerialName("inactive") object Inactive: RemoteCtrlError()
|
@Serializable @SerialName("inactive") object Inactive: RemoteCtrlError()
|
||||||
@Serializable @SerialName("badState") object BadState: RemoteCtrlError()
|
@Serializable @SerialName("badState") object BadState: RemoteCtrlError()
|
||||||
@Serializable @SerialName("busy") object Busy: RemoteCtrlError()
|
@Serializable @SerialName("busy") object Busy: RemoteCtrlError()
|
||||||
|
|||||||
+8
-9
@@ -41,21 +41,20 @@ val appPreferences: AppPreferences
|
|||||||
|
|
||||||
val chatController: ChatController = ChatController
|
val chatController: ChatController = ChatController
|
||||||
|
|
||||||
fun initChatControllerAndRunMigrations(ignoreSelfDestruct: Boolean) {
|
fun initChatControllerAndRunMigrations() {
|
||||||
if (ignoreSelfDestruct || DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
withBGApi {
|
||||||
withBGApi {
|
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
||||||
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
||||||
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
} else {
|
||||||
} else {
|
initChatController()
|
||||||
initChatController()
|
|
||||||
}
|
|
||||||
runMigrations()
|
|
||||||
}
|
}
|
||||||
|
runMigrations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred<Boolean> = { CompletableDeferred(true) }) {
|
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred<Boolean> = { CompletableDeferred(true) }) {
|
||||||
try {
|
try {
|
||||||
|
if (chatModel.ctrlInitInProgress.value) return
|
||||||
chatModel.ctrlInitInProgress.value = true
|
chatModel.ctrlInitInProgress.value = true
|
||||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||||
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||||
|
|||||||
+2
-1
@@ -93,12 +93,13 @@ abstract class NtfManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun notifyCallInvitation(invitation: RcvCallInvitation)
|
abstract fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean
|
||||||
abstract fun hasNotificationsForChat(chatId: String): Boolean
|
abstract fun hasNotificationsForChat(chatId: String): Boolean
|
||||||
abstract fun cancelNotificationsForChat(chatId: String)
|
abstract fun cancelNotificationsForChat(chatId: String)
|
||||||
abstract fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<Pair<NotificationAction, () -> Unit>> = emptyList())
|
abstract fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<Pair<NotificationAction, () -> Unit>> = emptyList())
|
||||||
abstract fun cancelCallNotification()
|
abstract fun cancelCallNotification()
|
||||||
abstract fun cancelAllNotifications()
|
abstract fun cancelAllNotifications()
|
||||||
|
abstract fun showMessage(title: String, text: String)
|
||||||
// Android only
|
// Android only
|
||||||
abstract fun androidCreateNtfChannelsMaybeShowAlert()
|
abstract fun androidCreateNtfChannelsMaybeShowAlert()
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,7 @@
|
|||||||
package chat.simplex.common.ui.theme
|
package chat.simplex.common.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material.LocalContentColor
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val Purple200 = Color(0xFFBB86FC)
|
val Purple200 = Color(0xFFBB86FC)
|
||||||
@@ -25,4 +27,5 @@ val WarningOrange = Color(255, 127, 0, 255)
|
|||||||
val WarningYellow = Color(255, 192, 0, 255)
|
val WarningYellow = Color(255, 192, 0, 255)
|
||||||
val FileLight = Color(183, 190, 199, 255)
|
val FileLight = Color(183, 190, 199, 255)
|
||||||
val FileDark = Color(101, 101, 106, 255)
|
val FileDark = Color(101, 101, 106, 255)
|
||||||
val MenuTextColorDark = Color.White.copy(alpha = 0.8f)
|
|
||||||
|
val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black
|
||||||
|
|||||||
+2
-19
@@ -283,27 +283,10 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
|||||||
val theme by CurrentColors.collectAsState()
|
val theme by CurrentColors.collectAsState()
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colors = theme.colors,
|
colors = theme.colors,
|
||||||
typography = Typography.copy(
|
typography = Typography,
|
||||||
h1 = Typography.h1.copy(color = theme.colors.onBackground),
|
|
||||||
h2 = Typography.h2.copy(color = theme.colors.onBackground),
|
|
||||||
h3 = Typography.h3.copy(color = theme.colors.onBackground),
|
|
||||||
h4 = Typography.h4.copy(color = theme.colors.onBackground),
|
|
||||||
h5 = Typography.h5.copy(color = theme.colors.onBackground),
|
|
||||||
h6 = Typography.h6.copy(color = theme.colors.onBackground),
|
|
||||||
subtitle1 = Typography.subtitle1.copy(color = theme.colors.onBackground),
|
|
||||||
subtitle2 = Typography.subtitle2.copy(color = theme.colors.onBackground),
|
|
||||||
body1 = Typography.body1.copy(color = theme.colors.onBackground),
|
|
||||||
body2 = Typography.body2.copy(color = theme.colors.onBackground),
|
|
||||||
button = Typography.button.copy(color = theme.colors.onBackground),
|
|
||||||
caption = Typography.caption.copy(color = theme.colors.onBackground),
|
|
||||||
overline = Typography.overline.copy(color = theme.colors.onBackground)
|
|
||||||
),
|
|
||||||
shapes = Shapes,
|
shapes = Shapes,
|
||||||
content = {
|
content = {
|
||||||
ProvideTextStyle(
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onBackground, content = content)
|
||||||
value = TextStyle(color = theme.colors.onBackground),
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,7 @@
|
|||||||
package chat.simplex.common.views
|
package chat.simplex.common.views
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
@@ -11,7 +10,8 @@ fun SplashView() {
|
|||||||
Surface(
|
Surface(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
color = MaterialTheme.colors.background
|
color = MaterialTheme.colors.background,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
// Image(
|
// Image(
|
||||||
// painter = painterResource(MR.images.logo),
|
// painter = painterResource(MR.images.logo),
|
||||||
|
|||||||
+4
-1
@@ -101,13 +101,16 @@ fun TerminalLayout(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
contentColor = LocalContentColor.current,
|
||||||
|
drawerContentColor = LocalContentColor.current,
|
||||||
modifier = Modifier.navigationBarsWithImePadding()
|
modifier = Modifier.navigationBarsWithImePadding()
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
color = MaterialTheme.colors.background
|
color = MaterialTheme.colors.background,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
TerminalLog()
|
TerminalLog()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -239,7 +239,7 @@ fun OnboardingButtons(displayName: MutableState<String>, close: () -> Unit) {
|
|||||||
val enabled = canCreateProfile(displayName.value)
|
val enabled = canCreateProfile(displayName.value)
|
||||||
val createModifier: Modifier = Modifier.clickable(enabled) { createProfileOnboarding(chatModel, displayName.value, close) }.padding(8.dp)
|
val createModifier: Modifier = Modifier.clickable(enabled) { createProfileOnboarding(chatModel, displayName.value, close) }.padding(8.dp)
|
||||||
val createColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
val createColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||||
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) {
|
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent, contentColor = LocalContentColor.current) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
|
||||||
Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
|
Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
|
||||||
Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor)
|
Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor)
|
||||||
|
|||||||
+1
-1
@@ -13,8 +13,8 @@ class CallManager(val chatModel: ChatModel) {
|
|||||||
callInvitations[invitation.contact.id] = invitation
|
callInvitations[invitation.contact.id] = invitation
|
||||||
if (invitation.user.showNotifications) {
|
if (invitation.user.showNotifications) {
|
||||||
if (Clock.System.now() - invitation.callTs <= 3.minutes) {
|
if (Clock.System.now() - invitation.callTs <= 3.minutes) {
|
||||||
|
invitation.sentNotification = ntfManager.notifyCallInvitation(invitation)
|
||||||
activeCallInvitation.value = invitation
|
activeCallInvitation.value = invitation
|
||||||
ntfManager.notifyCallInvitation(invitation)
|
|
||||||
} else {
|
} else {
|
||||||
val contact = invitation.contact
|
val contact = invitation.contact
|
||||||
ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
|
ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
|
||||||
|
|||||||
+8
-4
@@ -15,11 +15,10 @@ import dev.icerock.moko.resources.compose.painterResource
|
|||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.ProfileImage
|
import chat.simplex.common.views.helpers.ProfileImage
|
||||||
import chat.simplex.common.views.usersettings.ProfilePreview
|
import chat.simplex.common.views.usersettings.ProfilePreview
|
||||||
import chat.simplex.common.platform.ntfManager
|
|
||||||
import chat.simplex.common.platform.SoundPlayer
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
@@ -27,7 +26,11 @@ import kotlinx.datetime.Clock
|
|||||||
fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
||||||
val cm = chatModel.callManager
|
val cm = chatModel.callManager
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
LaunchedEffect(true) { SoundPlayer.start(scope, sound = !chatModel.showCallView.value) }
|
LaunchedEffect(Unit) {
|
||||||
|
if (chatModel.activeCallInvitation.value?.sentNotification == false || appPlatform.isDesktop) {
|
||||||
|
SoundPlayer.start(scope, sound = !chatModel.showCallView.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
DisposableEffect(true) { onDispose { SoundPlayer.stop() } }
|
DisposableEffect(true) { onDispose { SoundPlayer.stop() } }
|
||||||
IncomingCallAlertLayout(
|
IncomingCallAlertLayout(
|
||||||
invitation,
|
invitation,
|
||||||
@@ -85,7 +88,8 @@ fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
|||||||
private fun CallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
|
private fun CallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
color = Color.Transparent
|
color = Color.Transparent,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+3
@@ -112,6 +112,9 @@ sealed class WCallResponse {
|
|||||||
CallMediaType.Video -> MR.strings.incoming_video_call
|
CallMediaType.Video -> MR.strings.incoming_video_call
|
||||||
CallMediaType.Audio -> MR.strings.incoming_audio_call
|
CallMediaType.Audio -> MR.strings.incoming_audio_call
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Shows whether notification was shown or not to prevent playing sound twice in both notification and in-app
|
||||||
|
var sentNotification: Boolean = false
|
||||||
}
|
}
|
||||||
@Serializable data class CallCapabilities(val encryption: Boolean)
|
@Serializable data class CallCapabilities(val encryption: Boolean)
|
||||||
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
||||||
|
|||||||
+11
-5
@@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.ImageBitmap
|
|||||||
import androidx.compose.ui.text.*
|
import androidx.compose.ui.text.*
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
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.chat.group.*
|
import chat.simplex.common.views.chat.group.*
|
||||||
@@ -317,11 +318,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
},
|
},
|
||||||
acceptCall = { contact ->
|
acceptCall = { contact ->
|
||||||
hideKeyboard(view)
|
hideKeyboard(view)
|
||||||
val invitation = chatModel.callInvitations.remove(contact.id)
|
withApi {
|
||||||
if (invitation == null) {
|
val invitation = chatModel.callInvitations.remove(contact.id)
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
|
||||||
} else {
|
if (invitation == null) {
|
||||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
||||||
|
} else {
|
||||||
|
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
acceptFeature = { contact, feature, param ->
|
acceptFeature = { contact, feature, param ->
|
||||||
@@ -570,6 +574,8 @@ fun ChatLayout(
|
|||||||
bottomBar = composeView,
|
bottomBar = composeView,
|
||||||
modifier = Modifier.navigationBarsWithImePadding(),
|
modifier = Modifier.navigationBarsWithImePadding(),
|
||||||
floatingActionButton = { floatingButton.value() },
|
floatingActionButton = { floatingButton.value() },
|
||||||
|
contentColor = LocalContentColor.current,
|
||||||
|
drawerContentColor = LocalContentColor.current,
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
BoxWithConstraints(Modifier
|
BoxWithConstraints(Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
|
|||||||
+2
-1
@@ -258,7 +258,8 @@ private fun CustomDisappearingMessageDialog(
|
|||||||
|
|
||||||
DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
|
DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
|||||||
+2
-1
@@ -131,7 +131,8 @@ fun CIFileView(
|
|||||||
Surface(
|
Surface(
|
||||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
|
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Box(Modifier.size(32.dp))
|
Box(Modifier.size(32.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -88,6 +88,7 @@ fun CIGroupInvitationView(
|
|||||||
}) else Modifier,
|
}) else Modifier,
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
color = if (sent) sentColor else receivedColor,
|
color = if (sent) sentColor else receivedColor,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+2
@@ -142,6 +142,7 @@ fun DecryptionErrorItemFixButton(
|
|||||||
Modifier.clickable(onClick = onClick),
|
Modifier.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
color = receivedColor,
|
color = receivedColor,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
||||||
@@ -188,6 +189,7 @@ fun DecryptionErrorItem(
|
|||||||
Modifier.clickable(onClick = onClick),
|
Modifier.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
color = receivedColor,
|
color = receivedColor,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
||||||
|
|||||||
+4
-2
@@ -153,7 +153,8 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit,
|
|||||||
Surface(
|
Surface(
|
||||||
Modifier.align(Alignment.Center),
|
Modifier.align(Alignment.Center),
|
||||||
color = Color.Black.copy(alpha = 0.25f),
|
color = Color.Black.copy(alpha = 0.25f),
|
||||||
shape = RoundedCornerShape(percent = 50)
|
shape = RoundedCornerShape(percent = 50),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
@@ -264,7 +265,8 @@ private fun progressCircle(progress: Long, total: Long) {
|
|||||||
Surface(
|
Surface(
|
||||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
|
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Box(Modifier.size(16.dp))
|
Box(Modifier.size(16.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -225,7 +225,8 @@ private fun PlayPauseButton(
|
|||||||
Surface(
|
Surface(
|
||||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||||
color = if (sent) sentColor else receivedColor,
|
color = if (sent) sentColor else receivedColor,
|
||||||
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
|
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+2
-2
@@ -613,7 +613,7 @@ private fun ShrinkItemAction(revealed: MutableState<Boolean>, showMenu: MutableS
|
|||||||
@Composable
|
@Composable
|
||||||
fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, onClick: () -> Unit) {
|
fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, onClick: () -> Unit) {
|
||||||
val finalColor = if (color == Color.Unspecified) {
|
val finalColor = if (color == Color.Unspecified) {
|
||||||
if (isInDarkTheme()) MenuTextColorDark else Color.Black
|
MenuTextColor
|
||||||
} else color
|
} else color
|
||||||
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
@@ -633,7 +633,7 @@ fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, on
|
|||||||
@Composable
|
@Composable
|
||||||
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) {
|
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) {
|
||||||
val finalColor = if (color == Color.Unspecified) {
|
val finalColor = if (color == Color.Unspecified) {
|
||||||
if (isInDarkTheme()) MenuTextColorDark else Color.Black
|
MenuTextColor
|
||||||
} else color
|
} else color
|
||||||
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
|||||||
+1
@@ -23,6 +23,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
|||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
color = if (sent) sentColor else receivedColor,
|
color = if (sent) sentColor else receivedColor,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
|||||||
+1
@@ -56,6 +56,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
|||||||
Modifier.clickable(onClick = onClick),
|
Modifier.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
color = receivedColor,
|
color = receivedColor,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
|||||||
+1
@@ -26,6 +26,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl
|
|||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
color = if (ci.chatDir.sent) sentColor else receivedColor,
|
color = if (ci.chatDir.sent) sentColor else receivedColor,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
|||||||
+2
@@ -75,6 +75,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
|||||||
SettingsView(chatModel, setPerformLA, scaffoldState.drawerState)
|
SettingsView(chatModel, setPerformLA, scaffoldState.drawerState)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
contentColor = LocalContentColor.current,
|
||||||
|
drawerContentColor = LocalContentColor.current,
|
||||||
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
||||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
|
|||||||
+2
@@ -30,6 +30,8 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
|||||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||||
Scaffold(
|
Scaffold(
|
||||||
Modifier.padding(end = endPadding),
|
Modifier.padding(end = endPadding),
|
||||||
|
contentColor = LocalContentColor.current,
|
||||||
|
drawerContentColor = LocalContentColor.current,
|
||||||
scaffoldState = scaffoldState,
|
scaffoldState = scaffoldState,
|
||||||
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||||
) {
|
) {
|
||||||
|
|||||||
+8
-9
@@ -31,7 +31,6 @@ import chat.simplex.common.views.remote.*
|
|||||||
import chat.simplex.common.views.usersettings.doWithAuth
|
import chat.simplex.common.views.usersettings.doWithAuth
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -303,7 +302,7 @@ fun UserProfileRow(u: User) {
|
|||||||
u.displayName,
|
u.displayName,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 10.dp, end = 8.dp),
|
.padding(start = 10.dp, end = 8.dp),
|
||||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
color = MenuTextColor,
|
||||||
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
|
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -346,7 +345,7 @@ fun RemoteHostRow(h: RemoteHostInfo) {
|
|||||||
Text(
|
Text(
|
||||||
h.hostDeviceName,
|
h.hostDeviceName,
|
||||||
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
|
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
|
||||||
color = if (h.activeHost) MaterialTheme.colors.onBackground else if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
color = if (h.activeHost) MaterialTheme.colors.onBackground else MenuTextColor,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -387,7 +386,7 @@ fun LocalDeviceRow(active: Boolean) {
|
|||||||
Text(
|
Text(
|
||||||
stringResource(MR.strings.this_device),
|
stringResource(MR.strings.this_device),
|
||||||
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
|
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
|
||||||
color = if (active) MaterialTheme.colors.onBackground else if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
color = if (active) MaterialTheme.colors.onBackground else MenuTextColor,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -399,7 +398,7 @@ private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
|
|||||||
val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current)
|
val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current)
|
||||||
Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||||
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
|
Text(text, color = MenuTextColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,7 +408,7 @@ private fun LinkAMobilePickerItem(onClick: () -> Unit) {
|
|||||||
val text = generalGetString(MR.strings.link_a_mobile)
|
val text = generalGetString(MR.strings.link_a_mobile)
|
||||||
Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||||
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
|
Text(text, color = MenuTextColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +418,7 @@ private fun CreateInitialProfile(onClick: () -> Unit) {
|
|||||||
val text = generalGetString(MR.strings.create_chat_profile)
|
val text = generalGetString(MR.strings.create_chat_profile)
|
||||||
Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||||
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
|
Text(text, color = MenuTextColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +428,7 @@ private fun SettingsPickerItem(onClick: () -> Unit) {
|
|||||||
val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current)
|
val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current)
|
||||||
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||||
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
|
Text(text, color = MenuTextColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +438,7 @@ private fun CancelPickerItem(onClick: () -> Unit) {
|
|||||||
val text = generalGetString(MR.strings.cancel_verb)
|
val text = generalGetString(MR.strings.cancel_verb)
|
||||||
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||||
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
|
Text(text, color = MenuTextColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-7
@@ -366,7 +366,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
|
|||||||
return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive)
|
return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
|
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
|
||||||
withApi {
|
withApi {
|
||||||
try {
|
try {
|
||||||
if (chatDbChanged.value) {
|
if (chatDbChanged.value) {
|
||||||
@@ -406,6 +406,8 @@ private fun stopChatAlert(m: ChatModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect fun restartChatOrApp()
|
||||||
|
|
||||||
private fun exportProhibitedAlert() {
|
private fun exportProhibitedAlert() {
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = generalGetString(MR.strings.set_password_to_export),
|
title = generalGetString(MR.strings.set_password_to_export),
|
||||||
@@ -413,7 +415,7 @@ private fun exportProhibitedAlert() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun authStopChat(m: ChatModel) {
|
fun authStopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
|
||||||
if (m.controller.appPrefs.performLA.get()) {
|
if (m.controller.appPrefs.performLA.get()) {
|
||||||
authenticate(
|
authenticate(
|
||||||
generalGetString(MR.strings.auth_stop_chat),
|
generalGetString(MR.strings.auth_stop_chat),
|
||||||
@@ -421,7 +423,7 @@ private fun authStopChat(m: ChatModel) {
|
|||||||
completed = { laResult ->
|
completed = { laResult ->
|
||||||
when (laResult) {
|
when (laResult) {
|
||||||
LAResult.Success, is LAResult.Unavailable -> {
|
LAResult.Success, is LAResult.Unavailable -> {
|
||||||
stopChat(m)
|
stopChat(m, onStop)
|
||||||
}
|
}
|
||||||
is LAResult.Error -> {
|
is LAResult.Error -> {
|
||||||
m.chatRunning.value = true
|
m.chatRunning.value = true
|
||||||
@@ -434,15 +436,16 @@ private fun authStopChat(m: ChatModel) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
stopChat(m)
|
stopChat(m, onStop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopChat(m: ChatModel) {
|
private fun stopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
|
||||||
withApi {
|
withApi {
|
||||||
try {
|
try {
|
||||||
stopChatAsync(m)
|
stopChatAsync(m)
|
||||||
platform.androidChatStopped()
|
platform.androidChatStopped()
|
||||||
|
onStop?.invoke()
|
||||||
} catch (e: Error) {
|
} catch (e: Error) {
|
||||||
m.chatRunning.value = true
|
m.chatRunning.value = true
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString())
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString())
|
||||||
@@ -460,10 +463,10 @@ suspend fun deleteChatAsync(m: ChatModel) {
|
|||||||
m.controller.apiDeleteStorage()
|
m.controller.apiDeleteStorage()
|
||||||
DatabaseUtils.ksDatabasePassword.remove()
|
DatabaseUtils.ksDatabasePassword.remove()
|
||||||
m.controller.appPrefs.storeDBPassphrase.set(true)
|
m.controller.appPrefs.storeDBPassphrase.set(true)
|
||||||
deleteChatDatabaseFiles()
|
deleteAppDatabaseAndFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteChatDatabaseFiles() {
|
fun deleteAppDatabaseAndFiles() {
|
||||||
val chat = File(dataDir, chatDatabaseFileName)
|
val chat = File(dataDir, chatDatabaseFileName)
|
||||||
val chatBak = File(dataDir, "$chatDatabaseFileName.bak")
|
val chatBak = File(dataDir, "$chatDatabaseFileName.bak")
|
||||||
val agent = File(dataDir, agentDatabaseFileName)
|
val agent = File(dataDir, agentDatabaseFileName)
|
||||||
@@ -473,6 +476,7 @@ fun deleteChatDatabaseFiles() {
|
|||||||
agent.delete()
|
agent.delete()
|
||||||
agentBak.delete()
|
agentBak.delete()
|
||||||
filesDir.deleteRecursively()
|
filesDir.deleteRecursively()
|
||||||
|
filesDir.mkdir()
|
||||||
remoteHostsDir.deleteRecursively()
|
remoteHostsDir.deleteRecursively()
|
||||||
tmpDir.deleteRecursively()
|
tmpDir.deleteRecursively()
|
||||||
tmpDir.mkdir()
|
tmpDir.mkdir()
|
||||||
|
|||||||
+2
-1
@@ -152,7 +152,8 @@ fun CustomTimePickerDialog(
|
|||||||
) {
|
) {
|
||||||
DefaultDialog(onDismissRequest = cancel) {
|
DefaultDialog(onDismissRequest = cancel) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ object DatabaseUtils {
|
|||||||
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
|
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
|
||||||
val ksSelfDestructPassword = KeyStoreItem(SELF_DESTRUCT_PASSWORD_ALIAS, appPreferences.encryptedSelfDestructPassphrase, appPreferences.initializationVectorSelfDestructPassphrase)
|
val ksSelfDestructPassword = KeyStoreItem(SELF_DESTRUCT_PASSWORD_ALIAS, appPreferences.encryptedSelfDestructPassphrase, appPreferences.initializationVectorSelfDestructPassphrase)
|
||||||
|
|
||||||
class KeyStoreItem(val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
|
class KeyStoreItem(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
|
||||||
fun get(): String? {
|
fun get(): String? {
|
||||||
return cryptor.decryptData(
|
return cryptor.decryptData(
|
||||||
passphrase.get()?.toByteArrayFromBase64ForPassphrase() ?: return null,
|
passphrase.get()?.toByteArrayFromBase64ForPassphrase() ?: return null,
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ fun <T> ExposedDropDownSetting(
|
|||||||
selectionOption.second + (if (label != null) " $label" else ""),
|
selectionOption.second + (if (label != null) " $label" else ""),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
color = MenuTextColor,
|
||||||
fontSize = fontSize,
|
fontSize = fontSize,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -1,8 +1,7 @@
|
|||||||
package chat.simplex.common.views.helpers
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import chat.simplex.common.model.ChatController
|
import chat.simplex.common.model.ChatController
|
||||||
import chat.simplex.common.model.ChatModel
|
import chat.simplex.common.model.ChatModel
|
||||||
@@ -50,7 +49,7 @@ fun authenticateWithPasscode(
|
|||||||
close()
|
close()
|
||||||
completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled)))
|
completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled)))
|
||||||
}
|
}
|
||||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||||
LocalAuthView(ChatModel, LocalAuthRequest(promptTitle, promptSubtitle, password, selfDestruct && ChatController.appPrefs.selfDestruct.get()) {
|
LocalAuthView(ChatModel, LocalAuthRequest(promptTitle, promptSubtitle, password, selfDestruct && ChatController.appPrefs.selfDestruct.get()) {
|
||||||
close()
|
close()
|
||||||
completed(it)
|
completed(it)
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ fun ModalView(
|
|||||||
if (showClose) {
|
if (showClose) {
|
||||||
BackHandler(onBack = close)
|
BackHandler(onBack = close)
|
||||||
}
|
}
|
||||||
Surface(Modifier.fillMaxSize()) {
|
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)
|
CloseSheetBar(close, showClose, endButtons)
|
||||||
Box(modifier) { content() }
|
Box(modifier) { content() }
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
|
import chat.simplex.common.model.AgentErrorType
|
||||||
|
import chat.simplex.common.platform.Log
|
||||||
|
import chat.simplex.common.platform.TAG
|
||||||
|
import chat.simplex.common.platform.ntfManager
|
||||||
|
import chat.simplex.common.views.database.restartChatOrApp
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class ProcessedErrors <T: AgentErrorType>(val interval: Long) {
|
||||||
|
private var lastShownTimestamp: Long = -1
|
||||||
|
private var lastShownOfferRestart: Boolean = false
|
||||||
|
private var timer: Job = Job()
|
||||||
|
|
||||||
|
fun newError(error: T, offerRestart: Boolean) {
|
||||||
|
timer.cancel()
|
||||||
|
timer = withBGApi {
|
||||||
|
val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis()
|
||||||
|
if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) {
|
||||||
|
delay(delayBeforeNext)
|
||||||
|
}
|
||||||
|
lastShownTimestamp = System.currentTimeMillis()
|
||||||
|
lastShownOfferRestart = offerRestart
|
||||||
|
AlertManager.shared.hideAllAlerts()
|
||||||
|
showMessage(error, offerRestart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showMessage(error: T, offerRestart: Boolean) {
|
||||||
|
when (error) {
|
||||||
|
is AgentErrorType.CRITICAL -> {
|
||||||
|
val title = generalGetString(MR.strings.agent_critical_error_title)
|
||||||
|
val text = generalGetString(MR.strings.agent_critical_error_desc).format(error.criticalErr)
|
||||||
|
try {
|
||||||
|
ntfManager.showMessage(title, text)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, e.stackTraceToString())
|
||||||
|
}
|
||||||
|
if (offerRestart) {
|
||||||
|
AlertManager.shared.showAlertDialog(
|
||||||
|
title = title,
|
||||||
|
text = text,
|
||||||
|
confirmText = generalGetString(MR.strings.restart_chat_button),
|
||||||
|
onConfirm = {
|
||||||
|
withApi { restartChatOrApp() }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = title,
|
||||||
|
text = text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is AgentErrorType.INTERNAL -> {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.agent_internal_error_title),
|
||||||
|
text = generalGetString(MR.strings.agent_internal_error_desc).format(error.internalErr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -34,7 +34,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
|
|||||||
} else {
|
} else {
|
||||||
val r: LAResult = if (passcode.value == authRequest.password) {
|
val r: LAResult = if (passcode.value == authRequest.password) {
|
||||||
if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) {
|
if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) {
|
||||||
initChatControllerAndRunMigrations(true)
|
initChatControllerAndRunMigrations()
|
||||||
}
|
}
|
||||||
LAResult.Success
|
LAResult.Success
|
||||||
} else {
|
} else {
|
||||||
@@ -67,8 +67,8 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
|
|||||||
* */
|
* */
|
||||||
chatCloseStore(ctrl)
|
chatCloseStore(ctrl)
|
||||||
}
|
}
|
||||||
deleteChatDatabaseFiles()
|
deleteAppDatabaseAndFiles()
|
||||||
// Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself
|
// Clear sensitive data on screen just in case ModalManager fails to hide its modals while new database is created
|
||||||
m.chatId.value = null
|
m.chatId.value = null
|
||||||
m.chatItems.clear()
|
m.chatItems.clear()
|
||||||
m.chats.clear()
|
m.chats.clear()
|
||||||
|
|||||||
+2
-1
@@ -12,6 +12,7 @@ import chat.simplex.res.MR
|
|||||||
@Composable
|
@Composable
|
||||||
fun SetAppPasscodeView(
|
fun SetAppPasscodeView(
|
||||||
passcodeKeychain: DatabaseUtils.KeyStoreItem = ksAppPassword,
|
passcodeKeychain: DatabaseUtils.KeyStoreItem = ksAppPassword,
|
||||||
|
prohibitedPasscodeKeychain: DatabaseUtils.KeyStoreItem = ksSelfDestructPassword,
|
||||||
title: String = generalGetString(MR.strings.new_passcode),
|
title: String = generalGetString(MR.strings.new_passcode),
|
||||||
reason: String? = null,
|
reason: String? = null,
|
||||||
submit: () -> Unit,
|
submit: () -> Unit,
|
||||||
@@ -51,7 +52,7 @@ fun SetAppPasscodeView(
|
|||||||
} else {
|
} else {
|
||||||
SetPasswordView(title, generalGetString(MR.strings.save_verb),
|
SetPasswordView(title, generalGetString(MR.strings.save_verb),
|
||||||
// Do not allow to set app passcode == selfDestruct passcode
|
// Do not allow to set app passcode == selfDestruct passcode
|
||||||
submitEnabled = { pwd -> pwd != (if (passcodeKeychain.alias == ksSelfDestructPassword.alias) ksAppPassword else ksSelfDestructPassword).get() }) {
|
submitEnabled = { pwd -> pwd != prohibitedPasscodeKeychain.get() }) {
|
||||||
enteredPassword = passcode.value
|
enteredPassword = passcode.value
|
||||||
passcode.value = ""
|
passcode.value = ""
|
||||||
confirming = true
|
confirming = true
|
||||||
|
|||||||
+2
-2
@@ -175,7 +175,7 @@ fun ActionButton(
|
|||||||
disabled: Boolean = false,
|
disabled: Boolean = false,
|
||||||
click: () -> Unit = {}
|
click: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent) {
|
Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent, contentColor = LocalContentColor.current) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.clickable(onClick = click)
|
.clickable(onClick = click)
|
||||||
@@ -220,7 +220,7 @@ fun ActionButton(
|
|||||||
disabled: Boolean = false,
|
disabled: Boolean = false,
|
||||||
click: () -> Unit = {}
|
click: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Surface(modifier, shape = RoundedCornerShape(18.dp)) {
|
Surface(modifier, shape = RoundedCornerShape(18.dp), contentColor = LocalContentColor.current) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
+2
-1
@@ -380,7 +380,8 @@ fun SettingsSectionFooter(revert: () -> Unit, save: () -> Unit, disabled: Boolea
|
|||||||
fun FooterButton(icon: Painter, title: String, action: () -> Unit, disabled: Boolean) {
|
fun FooterButton(icon: Painter, title: String, action: () -> Unit, disabled: Boolean) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
color = Color.Black.copy(alpha = 0f)
|
color = Color.Black.copy(alpha = 0f),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
val modifier = if (disabled) Modifier else Modifier.clickable { action() }
|
val modifier = if (disabled) Modifier else Modifier.clickable { action() }
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
+3
-1
@@ -10,10 +10,11 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import chat.simplex.common.model.*
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import chat.simplex.common.model.ChatModel
|
|
||||||
import chat.simplex.common.platform.appPlatform
|
import chat.simplex.common.platform.appPlatform
|
||||||
|
import chat.simplex.common.platform.appPreferences
|
||||||
import chat.simplex.common.views.TerminalView
|
import chat.simplex.common.views.TerminalView
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
@@ -44,6 +45,7 @@ fun DeveloperView(
|
|||||||
m.controller.appPrefs.terminalAlwaysVisible.set(false)
|
m.controller.appPrefs.terminalAlwaysVisible.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SectionTextFooter(
|
SectionTextFooter(
|
||||||
|
|||||||
+7
-6
@@ -383,7 +383,7 @@ fun SimplexLockView(
|
|||||||
}
|
}
|
||||||
LAMode.PASSCODE -> {
|
LAMode.PASSCODE -> {
|
||||||
ModalManager.fullscreen.showCustomModal { close ->
|
ModalManager.fullscreen.showCustomModal { close ->
|
||||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||||
SetAppPasscodeView(
|
SetAppPasscodeView(
|
||||||
submit = {
|
submit = {
|
||||||
laLockDelay.set(30)
|
laLockDelay.set(30)
|
||||||
@@ -427,7 +427,7 @@ fun SimplexLockView(
|
|||||||
when (laResult) {
|
when (laResult) {
|
||||||
LAResult.Success -> {
|
LAResult.Success -> {
|
||||||
ModalManager.fullscreen.showCustomModal { close ->
|
ModalManager.fullscreen.showCustomModal { close ->
|
||||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||||
SetAppPasscodeView(
|
SetAppPasscodeView(
|
||||||
reason = generalGetString(MR.strings.la_app_passcode),
|
reason = generalGetString(MR.strings.la_app_passcode),
|
||||||
submit = {
|
submit = {
|
||||||
@@ -451,9 +451,10 @@ fun SimplexLockView(
|
|||||||
when (laResult) {
|
when (laResult) {
|
||||||
LAResult.Success -> {
|
LAResult.Success -> {
|
||||||
ModalManager.fullscreen.showCustomModal { close ->
|
ModalManager.fullscreen.showCustomModal { close ->
|
||||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||||
SetAppPasscodeView(
|
SetAppPasscodeView(
|
||||||
passcodeKeychain = ksSelfDestructPassword,
|
passcodeKeychain = ksSelfDestructPassword,
|
||||||
|
prohibitedPasscodeKeychain = ksAppPassword,
|
||||||
reason = generalGetString(MR.strings.self_destruct),
|
reason = generalGetString(MR.strings.self_destruct),
|
||||||
submit = {
|
submit = {
|
||||||
selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_changed))
|
selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_changed))
|
||||||
@@ -487,7 +488,7 @@ fun SimplexLockView(
|
|||||||
}
|
}
|
||||||
LAMode.PASSCODE -> {
|
LAMode.PASSCODE -> {
|
||||||
ModalManager.fullscreen.showCustomModal { close ->
|
ModalManager.fullscreen.showCustomModal { close ->
|
||||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||||
SetAppPasscodeView(
|
SetAppPasscodeView(
|
||||||
submit = {
|
submit = {
|
||||||
laLockDelay.set(30)
|
laLockDelay.set(30)
|
||||||
@@ -598,9 +599,9 @@ private fun EnableSelfDestruct(
|
|||||||
selfDestruct: SharedPreference<Boolean>,
|
selfDestruct: SharedPreference<Boolean>,
|
||||||
close: () -> Unit
|
close: () -> Unit
|
||||||
) {
|
) {
|
||||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||||
SetAppPasscodeView(
|
SetAppPasscodeView(
|
||||||
passcodeKeychain = ksSelfDestructPassword, title = generalGetString(MR.strings.set_passcode), reason = generalGetString(MR.strings.enabled_self_destruct_passcode),
|
passcodeKeychain = ksSelfDestructPassword, prohibitedPasscodeKeychain = ksAppPassword, title = generalGetString(MR.strings.set_passcode), reason = generalGetString(MR.strings.enabled_self_destruct_passcode),
|
||||||
submit = {
|
submit = {
|
||||||
selfDestruct.set(true)
|
selfDestruct.set(true)
|
||||||
selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_enabled))
|
selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_enabled))
|
||||||
|
|||||||
+2
-1
@@ -155,7 +155,8 @@ fun RTCServersLayout(
|
|||||||
.height(160.dp)
|
.height(160.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant)
|
border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
SelectionContainer(
|
SelectionContainer(
|
||||||
Modifier.verticalScroll(rememberScrollState())
|
Modifier.verticalScroll(rememberScrollState())
|
||||||
|
|||||||
+1
-1
@@ -155,7 +155,7 @@ fun UserAddressView(
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
if (userAddress.value != null) {
|
if (userAddress.value != null) {
|
||||||
Surface(Modifier.size(50.dp), color = MaterialTheme.colors.background.copy(0.9f), shape = RoundedCornerShape(50)){}
|
Surface(Modifier.size(50.dp), color = MaterialTheme.colors.background.copy(0.9f), contentColor = LocalContentColor.current, shape = RoundedCornerShape(50)){}
|
||||||
}
|
}
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
<string name="failed_to_create_user_title">Error creating profile!</string>
|
<string name="failed_to_create_user_title">Error creating profile!</string>
|
||||||
<string name="failed_to_create_user_duplicate_title">Duplicate display name!</string>
|
<string name="failed_to_create_user_duplicate_title">Duplicate display name!</string>
|
||||||
<string name="failed_to_create_user_duplicate_desc">You already have a chat profile with the same display name. Please choose another name.</string>
|
<string name="failed_to_create_user_duplicate_desc">You already have a chat profile with the same display name. Please choose another name.</string>
|
||||||
|
<string name="failed_to_create_user_invalid_title">Invalid display name!</string>
|
||||||
|
<string name="failed_to_create_user_invalid_desc">This display name is invalid. Please choose another name.</string>
|
||||||
<string name="failed_to_active_user_title">Error switching profile!</string>
|
<string name="failed_to_active_user_title">Error switching profile!</string>
|
||||||
|
|
||||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||||
@@ -684,6 +686,7 @@
|
|||||||
<string name="hide_dev_options">Hide:</string>
|
<string name="hide_dev_options">Hide:</string>
|
||||||
<string name="show_developer_options">Show developer options</string>
|
<string name="show_developer_options">Show developer options</string>
|
||||||
<string name="developer_options">Database IDs and Transport isolation option.</string>
|
<string name="developer_options">Database IDs and Transport isolation option.</string>
|
||||||
|
<string name="show_internal_errors">Show internal errors</string>
|
||||||
<string name="shutdown_alert_question">Shutdown?</string>
|
<string name="shutdown_alert_question">Shutdown?</string>
|
||||||
<string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string>
|
<string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string>
|
||||||
|
|
||||||
@@ -1693,6 +1696,10 @@
|
|||||||
<string name="disconnect_remote_host">Disconnect</string>
|
<string name="disconnect_remote_host">Disconnect</string>
|
||||||
<string name="disconnect_remote_hosts">Disconnect mobiles</string>
|
<string name="disconnect_remote_hosts">Disconnect mobiles</string>
|
||||||
<string name="remote_host_was_disconnected_toast"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
|
<string name="remote_host_was_disconnected_toast"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
|
||||||
|
<string name="remote_host_was_disconnected_title">Connection stopped</string>
|
||||||
|
<string name="remote_ctrl_was_disconnected_title">Connection stopped</string>
|
||||||
|
<string name="remote_host_disconnected_from"><![CDATA[Disconnected from mobile <b>%s</b> with the reason: %s]]></string>
|
||||||
|
<string name="remote_ctrl_disconnected_with_reason">Disconnected with the reason: %s</string>
|
||||||
<string name="disconnect_desktop_question">Disconnect desktop?</string>
|
<string name="disconnect_desktop_question">Disconnect desktop?</string>
|
||||||
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
|
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
|
||||||
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string>
|
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string>
|
||||||
@@ -1727,6 +1734,20 @@
|
|||||||
<string name="random_port">Random</string>
|
<string name="random_port">Random</string>
|
||||||
<string name="open_port_in_firewall_title">Open port in firewall</string>
|
<string name="open_port_in_firewall_title">Open port in firewall</string>
|
||||||
<string name="open_port_in_firewall_desc">To allow a mobile app to connect to the desktop, open this port in your firewall, if you have it enabled</string>
|
<string name="open_port_in_firewall_desc">To allow a mobile app to connect to the desktop, open this port in your firewall, if you have it enabled</string>
|
||||||
|
<string name="remote_host_error_missing"><![CDATA[Mobile <b>%s</b> is missing]]></string>
|
||||||
|
<string name="remote_host_error_inactive"><![CDATA[Mobile <b>%s</b> is inactive]]></string>
|
||||||
|
<string name="remote_host_error_busy"><![CDATA[Mobile <b>%s</b> is busy]]></string>
|
||||||
|
<string name="remote_host_error_timeout"><![CDATA[Timeout reached while connecting to the mobile <b>%s</b>]]></string>
|
||||||
|
<string name="remote_host_error_bad_state"><![CDATA[Connection to the mobile <b>%s</b> is in a bad state]]></string>
|
||||||
|
<string name="remote_host_error_bad_version"><![CDATA[Mobile <b>%s</b> has an unsupported version. Please, make sure you use the same version on both devices]]></string>
|
||||||
|
<string name="remote_host_error_disconnected"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
|
||||||
|
<string name="remote_ctrl_error_inactive">Desktop is inactive</string>
|
||||||
|
<string name="remote_ctrl_error_bad_state">Connection to the desktop is in a bad state</string>
|
||||||
|
<string name="remote_ctrl_error_busy">Desktop is busy</string>
|
||||||
|
<string name="remote_ctrl_error_timeout">Timeout reached while connecting to the desktop</string>
|
||||||
|
<string name="remote_ctrl_error_disconnected">Desktop was disconnected</string>
|
||||||
|
<string name="remote_ctrl_error_bad_invitation">Desktop has wrong invitation code</string>
|
||||||
|
<string name="remote_ctrl_error_bad_version">Desktop has an unsupported version. Please, make sure you use the same version on both devices</string>
|
||||||
|
|
||||||
<!-- Under development -->
|
<!-- Under development -->
|
||||||
<string name="in_developing_title">Coming soon!</string>
|
<string name="in_developing_title">Coming soon!</string>
|
||||||
@@ -1751,4 +1772,11 @@
|
|||||||
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">You are already joining the group via this link.</string>
|
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">You are already joining the group via this link.</string>
|
||||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[You are already in group <b>%1$s</b>.]]></string>
|
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[You are already in group <b>%1$s</b>.]]></string>
|
||||||
<string name="connect_plan_connect_via_link">Connect via link?</string>
|
<string name="connect_plan_connect_via_link">Connect via link?</string>
|
||||||
|
|
||||||
|
<!-- Errors -->
|
||||||
|
<string name="agent_critical_error_title">Critical error</string>
|
||||||
|
<string name="agent_critical_error_desc">Please report it to the developers: \n%s\n\nIt is recommended to restart the app.</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="restart_chat_button">Restart chat</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M479.895-284Q494-284 504-293.895q10-9.894 10-24Q514-332 504.105-342q-9.894-10-24-10Q466-352 456-342.105q-10 9.894-10 24Q446-304 455.895-294q9.894 10 24 10ZM451.5-425H509v-261h-57.5v261ZM332-124.5 124.5-332.176V-628l207.676-207.5H628l207.5 207.676V-332L627.824-124.5H332Zm24.222-57.5h248.243L778-356.222v-248.243L604.242-778H356L182-604.242V-356l174.222 174ZM480-480Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 472 B |
+7
-2
@@ -16,8 +16,8 @@ import javax.imageio.ImageIO
|
|||||||
object NtfManager {
|
object NtfManager {
|
||||||
private val prevNtfs = arrayListOf<Pair<ChatId, Slice>>()
|
private val prevNtfs = arrayListOf<Pair<ChatId, Slice>>()
|
||||||
|
|
||||||
fun notifyCallInvitation(invitation: RcvCallInvitation) {
|
fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean {
|
||||||
if (simplexWindowState.windowFocused.value) return
|
if (simplexWindowState.windowFocused.value) return false
|
||||||
val contactId = invitation.contact.id
|
val contactId = invitation.contact.id
|
||||||
Log.d(TAG, "notifyCallInvitation $contactId")
|
Log.d(TAG, "notifyCallInvitation $contactId")
|
||||||
val image = invitation.contact.image
|
val image = invitation.contact.image
|
||||||
@@ -45,6 +45,11 @@ object NtfManager {
|
|||||||
displayNotificationViaLib(contactId, title, text, prepareIconPath(largeIcon), actions) {
|
displayNotificationViaLib(contactId, title, text, prepareIconPath(largeIcon), actions) {
|
||||||
ntfManager.openChatAction(invitation.user.userId, contactId)
|
ntfManager.openChatAction(invitation.user.userId, contactId)
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMessage(title: String, text: String) {
|
||||||
|
displayNotificationViaLib("MESSAGE", title, text, null, emptyList()) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasNotificationsForChat(chatId: ChatId) = false//prevNtfs.any { it.first == chatId }
|
fun hasNotificationsForChat(chatId: ChatId) = false//prevNtfs.any { it.first == chatId }
|
||||||
|
|||||||
+5
-2
@@ -15,16 +15,19 @@ val defaultLocale: Locale = Locale.getDefault()
|
|||||||
|
|
||||||
fun initApp() {
|
fun initApp() {
|
||||||
ntfManager = object : NtfManager() {
|
ntfManager = object : NtfManager() {
|
||||||
override fun notifyCallInvitation(invitation: RcvCallInvitation) = chat.simplex.common.model.NtfManager.notifyCallInvitation(invitation)
|
override fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean = chat.simplex.common.model.NtfManager.notifyCallInvitation(invitation)
|
||||||
override fun hasNotificationsForChat(chatId: String): Boolean = chat.simplex.common.model.NtfManager.hasNotificationsForChat(chatId)
|
override fun hasNotificationsForChat(chatId: String): Boolean = chat.simplex.common.model.NtfManager.hasNotificationsForChat(chatId)
|
||||||
override fun cancelNotificationsForChat(chatId: String) = chat.simplex.common.model.NtfManager.cancelNotificationsForChat(chatId)
|
override fun cancelNotificationsForChat(chatId: String) = chat.simplex.common.model.NtfManager.cancelNotificationsForChat(chatId)
|
||||||
override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = chat.simplex.common.model.NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions)
|
override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = chat.simplex.common.model.NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions)
|
||||||
override fun androidCreateNtfChannelsMaybeShowAlert() {}
|
override fun androidCreateNtfChannelsMaybeShowAlert() {}
|
||||||
override fun cancelCallNotification() {}
|
override fun cancelCallNotification() {}
|
||||||
override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications()
|
override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications()
|
||||||
|
override fun showMessage(title: String, text: String) = chat.simplex.common.model.NtfManager.showMessage(title, text)
|
||||||
}
|
}
|
||||||
applyAppLocale()
|
applyAppLocale()
|
||||||
initChatControllerAndRunMigrations(false)
|
if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||||
|
initChatControllerAndRunMigrations()
|
||||||
|
}
|
||||||
// LALAL
|
// LALAL
|
||||||
//testCrypto()
|
//testCrypto()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -167,7 +167,8 @@ actual fun PlatformTextField(
|
|||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
|
border = BorderStroke(1.dp, MaterialTheme.colors.secondary),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
Modifier.background(MaterialTheme.colors.background),
|
Modifier.background(MaterialTheme.colors.background),
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package chat.simplex.common.views.database
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import chat.simplex.common.platform.chatModel
|
||||||
|
import chat.simplex.common.views.helpers.withApi
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
actual fun restartChatOrApp() {
|
||||||
|
if (chatModel.chatRunning.value == false) {
|
||||||
|
chatModel.chatDbChanged.value = true
|
||||||
|
startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged)
|
||||||
|
} else {
|
||||||
|
authStopChat(chatModel) {
|
||||||
|
withApi {
|
||||||
|
// adding delay in order to prevent locked database by previous initialization
|
||||||
|
delay(1000)
|
||||||
|
chatModel.chatDbChanged.value = true
|
||||||
|
startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -2,8 +2,7 @@ package chat.simplex.common.views.helpers
|
|||||||
|
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.key.*
|
import androidx.compose.ui.input.key.*
|
||||||
@@ -39,7 +38,8 @@ actual fun DefaultDialog(
|
|||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
Modifier
|
Modifier
|
||||||
.border(border = BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.3F)), shape = RoundedCornerShape(8))
|
.border(border = BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.3F)), shape = RoundedCornerShape(8)),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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: d0588bd0ac23a459cbfc9a4789633014e91ffa19
|
tag: ad8cd1d5154617663065652b45c784ad5a0a584d
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 5.5.0.0
|
version: 5.5.0.1
|
||||||
#synopsis:
|
#synopsis:
|
||||||
#description:
|
#description:
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ u="$USER"
|
|||||||
tmp="$(mktemp -d -t)"
|
tmp="$(mktemp -d -t)"
|
||||||
folder="$tmp/simplex-chat"
|
folder="$tmp/simplex-chat"
|
||||||
|
|
||||||
nix_ver="nix-2.15.1"
|
nix_ver="nix-2.19.2"
|
||||||
nix_url="https://releases.nixos.org/nix/$nix_ver/install"
|
nix_url="https://releases.nixos.org/nix/$nix_ver/install"
|
||||||
nix_hash="67aa37f0115195d8ddf32b5d6f471f1e60ecca0fdb3e98bcf54bc147c3078640"
|
nix_hash="435f0d7e11f7c7dffeeab0ec9cc55723f6d3c03352379d785633cf4ddb5caf90"
|
||||||
nix_config="sandbox = true
|
nix_config="sandbox = true
|
||||||
max-jobs = auto
|
max-jobs = auto
|
||||||
experimental-features = nix-command flakes"
|
experimental-features = nix-command flakes"
|
||||||
@@ -102,8 +102,19 @@ build() {
|
|||||||
sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts"
|
sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts"
|
||||||
|
|
||||||
for arch in $arches; do
|
for arch in $arches; do
|
||||||
android_simplex_lib="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux"
|
|
||||||
android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux"
|
tag_full="$(git tag --points-at HEAD)"
|
||||||
|
tag_version="${tag_full%%-*}"
|
||||||
|
|
||||||
|
if [ "$arch" = "armv7a" ] && [ -n "$tag_full" ] ; then
|
||||||
|
git checkout "${tag_version}-armv7a"
|
||||||
|
android_simplex_lib="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux"
|
||||||
|
android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux"
|
||||||
|
else
|
||||||
|
android_simplex_lib="${folder}#hydraJobs.x86_64-linux.${arch}-android:lib:simplex-chat"
|
||||||
|
android_support_lib="${folder}#hydraJobs.x86_64-linux.${arch}-android:lib:support"
|
||||||
|
fi
|
||||||
|
|
||||||
android_simplex_lib_output="${PWD}/result/pkg-${arch}-android-libsimplex.zip"
|
android_simplex_lib_output="${PWD}/result/pkg-${arch}-android-libsimplex.zip"
|
||||||
android_support_lib_output="${PWD}/result/pkg-${arch}-android-libsupport.zip"
|
android_support_lib_output="${PWD}/result/pkg-${arch}-android-libsupport.zip"
|
||||||
|
|
||||||
@@ -139,6 +150,10 @@ build() {
|
|||||||
zipalign -p -f 4 "$tmp/$android_apk_output_final" "$PWD/$android_apk_output_final"
|
zipalign -p -f 4 "$tmp/$android_apk_output_final" "$PWD/$android_apk_output_final"
|
||||||
|
|
||||||
rm -rf "$libs_folder/$android_arch"
|
rm -rf "$libs_folder/$android_arch"
|
||||||
|
|
||||||
|
if [ "$arch" = "armv7a" ] && [ -n "$tag_full" ] ; then
|
||||||
|
git checkout "${tag_full}"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ for ((i = 0 ; i < ${#arches[@]}; i++)); do
|
|||||||
|
|
||||||
mkdir -p "$output_dir" 2> /dev/null
|
mkdir -p "$output_dir" 2> /dev/null
|
||||||
|
|
||||||
curl --location -o libsupport.zip $job_repo/$arch-android:lib:support.x86_64-linux/latest/download/1 && \
|
curl --location -o libsupport.zip $job_repo/x86_64-linux."$arch"-android:lib:support/latest/download/1 && \
|
||||||
unzip -o libsupport.zip && \
|
unzip -o libsupport.zip && \
|
||||||
mv libsupport.so "$output_dir" && \
|
mv libsupport.so "$output_dir" && \
|
||||||
rm libsupport.zip
|
rm libsupport.zip
|
||||||
|
|
||||||
curl --location -o libsimplex.zip "$job_repo"/"$arch"-android:lib:simplex-chat.x86_64-linux/latest/download/1 && \
|
curl --location -o libsimplex.zip "$job_repo"/x86_64-linux."$arch"-android:lib:simplex-chat/latest/download/1 && \
|
||||||
unzip -o libsimplex.zip && \
|
unzip -o libsimplex.zip && \
|
||||||
mv libsimplex.so "$output_dir" && \
|
mv libsimplex.so "$output_dir" && \
|
||||||
rm libsimplex.zip
|
rm libsimplex.zip
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ for ((i = 0 ; i < ${#arches[@]}; i++)); do
|
|||||||
output_arch="${output_arches[$i]}"
|
output_arch="${output_arches[$i]}"
|
||||||
output_dir="$HOME/Downloads"
|
output_dir="$HOME/Downloads"
|
||||||
|
|
||||||
curl --location -o "$output_dir"/pkg-ios-"$arch"-swift-json.zip "$job_repo"/"$arch"-darwin-ios:lib:simplex-chat."$arch"-darwin/latest/download/1 && \
|
curl --location -o "$output_dir"/pkg-ios-"$arch"-swift-json.zip "$job_repo"/"$arch"-darwin."$arch"-darwin-ios:lib:simplex-chat/latest/download/1 && \
|
||||||
unzip -o "$output_dir"/pkg-ios-"$output_arch"-swift-json.zip -d ~/Downloads/pkg-ios-"$output_arch"-swift-json
|
unzip -o "$output_dir"/pkg-ios-"$output_arch"-swift-json.zip -d ~/Downloads/pkg-ios-"$output_arch"-swift-json
|
||||||
done
|
done
|
||||||
sh "$root_dir"/scripts/ios/prepare-x86_64.sh
|
sh "$root_dir"/scripts/ios/prepare-x86_64.sh
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."d0588bd0ac23a459cbfc9a4789633014e91ffa19" = "0b17qy74capb0jyli8f3pg1xi4aawhcgpmaz2ykl9g3605png1na";
|
"https://github.com/simplex-chat/simplexmq.git"."ad8cd1d5154617663065652b45c784ad5a0a584d" = "19sinz1gynab776x8h9va7r6ifm9pmgzljsbc7z5cbkcnjl5sfh3";
|
||||||
"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";
|
||||||
|
|||||||
+1
-1
@@ -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.0.0
|
version: 5.5.0.1
|
||||||
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
|
||||||
|
|||||||
+224
-135
@@ -24,7 +24,7 @@ import Control.Monad.Reader
|
|||||||
import qualified Data.Aeson as J
|
import qualified Data.Aeson as J
|
||||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||||
import Data.Bifunctor (bimap, first)
|
import Data.Bifunctor (bimap, first, second)
|
||||||
import Data.ByteArray (ScrubbedBytes)
|
import Data.ByteArray (ScrubbedBytes)
|
||||||
import qualified Data.ByteArray as BA
|
import qualified Data.ByteArray as BA
|
||||||
import qualified Data.ByteString.Base64 as B64
|
import qualified Data.ByteString.Base64 as B64
|
||||||
@@ -37,6 +37,7 @@ import Data.Constraint (Dict (..))
|
|||||||
import Data.Either (fromRight, lefts, partitionEithers, rights)
|
import Data.Either (fromRight, lefts, partitionEithers, rights)
|
||||||
import Data.Fixed (div')
|
import Data.Fixed (div')
|
||||||
import Data.Functor (($>))
|
import Data.Functor (($>))
|
||||||
|
import Data.Functor.Identity
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.List (find, foldl', isSuffixOf, partition, sortOn)
|
import Data.List (find, foldl', isSuffixOf, partition, sortOn)
|
||||||
import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|))
|
import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|))
|
||||||
@@ -87,6 +88,7 @@ import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentCl
|
|||||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
||||||
import Simplex.Messaging.Agent.Lock
|
import Simplex.Messaging.Agent.Lock
|
||||||
import Simplex.Messaging.Agent.Protocol
|
import Simplex.Messaging.Agent.Protocol
|
||||||
|
import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..))
|
||||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection)
|
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection)
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
@@ -233,6 +235,7 @@ newChatController
|
|||||||
expireCIFlags <- newTVarIO M.empty
|
expireCIFlags <- newTVarIO M.empty
|
||||||
cleanupManagerAsync <- newTVarIO Nothing
|
cleanupManagerAsync <- newTVarIO Nothing
|
||||||
timedItemThreads <- atomically TM.empty
|
timedItemThreads <- atomically TM.empty
|
||||||
|
chatActivated <- newTVarIO True
|
||||||
showLiveItems <- newTVarIO False
|
showLiveItems <- newTVarIO False
|
||||||
encryptLocalFiles <- newTVarIO False
|
encryptLocalFiles <- newTVarIO False
|
||||||
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
|
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
|
||||||
@@ -268,6 +271,7 @@ newChatController
|
|||||||
expireCIFlags,
|
expireCIFlags,
|
||||||
cleanupManagerAsync,
|
cleanupManagerAsync,
|
||||||
timedItemThreads,
|
timedItemThreads,
|
||||||
|
chatActivated,
|
||||||
showLiveItems,
|
showLiveItems,
|
||||||
encryptLocalFiles,
|
encryptLocalFiles,
|
||||||
userXFTPFileConfig,
|
userXFTPFileConfig,
|
||||||
@@ -311,10 +315,10 @@ cfgServers p DefaultAgentServers {smp, xftp} = case p of
|
|||||||
SPSMP -> smp
|
SPSMP -> smp
|
||||||
SPXFTP -> xftp
|
SPXFTP -> xftp
|
||||||
|
|
||||||
startChatController :: forall m. ChatMonad' m => Bool -> Bool -> Bool -> m (Async ())
|
startChatController :: forall m. ChatMonad' m => Bool -> m (Async ())
|
||||||
startChatController subConns enableExpireCIs startXFTPWorkers = do
|
startChatController mainApp = do
|
||||||
asks smpAgent >>= resumeAgentClient
|
asks smpAgent >>= resumeAgentClient
|
||||||
unless subConns $
|
unless mainApp $
|
||||||
chatWriteVar subscriptionMode SMOnlyCreate
|
chatWriteVar subscriptionMode SMOnlyCreate
|
||||||
users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers)
|
users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers)
|
||||||
restoreCalls
|
restoreCalls
|
||||||
@@ -324,15 +328,15 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do
|
|||||||
start s users = do
|
start s users = do
|
||||||
a1 <- async agentSubscriber
|
a1 <- async agentSubscriber
|
||||||
a2 <-
|
a2 <-
|
||||||
if subConns
|
if mainApp
|
||||||
then Just <$> async (subscribeUsers False users)
|
then Just <$> async (subscribeUsers False users)
|
||||||
else pure Nothing
|
else pure Nothing
|
||||||
atomically . writeTVar s $ Just (a1, a2)
|
atomically . writeTVar s $ Just (a1, a2)
|
||||||
when startXFTPWorkers $ do
|
when mainApp $ do
|
||||||
startXFTP
|
startXFTP
|
||||||
void $ forkIO $ startFilesToReceive users
|
void $ forkIO $ startFilesToReceive users
|
||||||
startCleanupManager
|
startCleanupManager
|
||||||
when enableExpireCIs $ startExpireCIs users
|
startExpireCIs users
|
||||||
pure a1
|
pure a1
|
||||||
startXFTP = do
|
startXFTP = do
|
||||||
tmp <- readTVarIO =<< asks tempDirectory
|
tmp <- readTVarIO =<< asks tempDirectory
|
||||||
@@ -454,8 +458,9 @@ processChatCommand' vr = \case
|
|||||||
withStore' getUsers >>= \case
|
withStore' getUsers >>= \case
|
||||||
[] -> pure 1
|
[] -> pure 1
|
||||||
users -> do
|
users -> do
|
||||||
when (any (\User {localDisplayName = n} -> n == displayName) users) $
|
forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} ->
|
||||||
throwChatError (CEUserExists displayName)
|
when (n == displayName) . throwChatError $
|
||||||
|
if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""}
|
||||||
withAgent (\a -> createUser a smp xftp)
|
withAgent (\a -> createUser a smp xftp)
|
||||||
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
|
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
|
||||||
user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts
|
user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts
|
||||||
@@ -544,16 +549,17 @@ processChatCommand' vr = \case
|
|||||||
checkDeleteChatUser user'
|
checkDeleteChatUser user'
|
||||||
withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues
|
withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues
|
||||||
DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_
|
DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_
|
||||||
StartChat subConns enableExpireCIs startXFTPWorkers -> withUser' $ \_ ->
|
StartChat mainApp -> withUser' $ \_ ->
|
||||||
asks agentAsync >>= readTVarIO >>= \case
|
asks agentAsync >>= readTVarIO >>= \case
|
||||||
Just _ -> pure CRChatRunning
|
Just _ -> pure CRChatRunning
|
||||||
_ -> checkStoreNotChanged $ startChatController subConns enableExpireCIs startXFTPWorkers $> CRChatStarted
|
_ -> checkStoreNotChanged $ startChatController mainApp $> CRChatStarted
|
||||||
APIStopChat -> do
|
APIStopChat -> do
|
||||||
ask >>= stopChatController
|
ask >>= stopChatController
|
||||||
pure CRChatStopped
|
pure CRChatStopped
|
||||||
APIActivateChat restoreChat -> withUser $ \_ -> do
|
APIActivateChat restoreChat -> withUser $ \_ -> do
|
||||||
when restoreChat restoreCalls
|
when restoreChat restoreCalls
|
||||||
withAgent foregroundAgent
|
withAgent foregroundAgent
|
||||||
|
chatWriteVar chatActivated True
|
||||||
when restoreChat $ do
|
when restoreChat $ do
|
||||||
users <- withStoreCtx' (Just "APIActivateChat, getUsers") getUsers
|
users <- withStoreCtx' (Just "APIActivateChat, getUsers") getUsers
|
||||||
void . forkIO $ subscribeUsers True users
|
void . forkIO $ subscribeUsers True users
|
||||||
@@ -561,6 +567,7 @@ processChatCommand' vr = \case
|
|||||||
setAllExpireCIFlags True
|
setAllExpireCIFlags True
|
||||||
ok_
|
ok_
|
||||||
APISuspendChat t -> do
|
APISuspendChat t -> do
|
||||||
|
chatWriteVar chatActivated False
|
||||||
setAllExpireCIFlags False
|
setAllExpireCIFlags False
|
||||||
stopRemoteCtrl
|
stopRemoteCtrl
|
||||||
withAgent (`suspendAgent` t)
|
withAgent (`suspendAgent` t)
|
||||||
@@ -2135,31 +2142,41 @@ processChatCommand' vr = \case
|
|||||||
| otherwise = do
|
| otherwise = do
|
||||||
when (n /= n') $ checkValidName n'
|
when (n /= n') $ checkValidName n'
|
||||||
-- read contacts before user update to correctly merge preferences
|
-- read contacts before user update to correctly merge preferences
|
||||||
-- [incognito] filter out contacts with whom user has incognito connections
|
contacts <- withStore' (`getUserContacts` user)
|
||||||
contacts <-
|
|
||||||
filter (\ct -> contactReady ct && contactActive ct && not (contactConnIncognito ct))
|
|
||||||
<$> withStore' (`getUserContacts` user)
|
|
||||||
user' <- updateUser
|
user' <- updateUser
|
||||||
asks currentUser >>= atomically . (`writeTVar` Just user')
|
asks currentUser >>= atomically . (`writeTVar` Just user')
|
||||||
withChatLock "updateProfile" . procCmd $ do
|
withChatLock "updateProfile" . procCmd $ do
|
||||||
ChatConfig {logLevel} <- asks config
|
let changedCts = foldr (addChangedProfileContact user') [] contacts
|
||||||
summary <- foldM (processAndCount user' logLevel) (UserProfileUpdateSummary 0 0 0 []) contacts
|
idsEvts = map ctSndMsg changedCts
|
||||||
|
msgReqs_ <- zipWith ctMsgReq changedCts <$> createSndMessages idsEvts
|
||||||
|
(errs, cts) <- partitionEithers . zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_
|
||||||
|
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
||||||
|
let changedCts' = filter (\ChangedProfileContact {ct, ct'} -> directOrUsed ct' && mergedPreferences ct' /= mergedPreferences ct) cts
|
||||||
|
createContactsSndFeatureItems user' changedCts'
|
||||||
|
let summary =
|
||||||
|
UserProfileUpdateSummary
|
||||||
|
{ updateSuccesses = length cts,
|
||||||
|
updateFailures = length errs,
|
||||||
|
changedContacts = map (\ChangedProfileContact {ct'} -> ct') changedCts'
|
||||||
|
}
|
||||||
pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary
|
pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary
|
||||||
where
|
where
|
||||||
processAndCount user' ll s@UserProfileUpdateSummary {notChanged, updateSuccesses, updateFailures, changedContacts = cts} ct = do
|
-- [incognito] filter out contacts with whom user has incognito connections
|
||||||
let mergedProfile = userProfileToSend user Nothing $ Just ct
|
addChangedProfileContact :: User -> Contact -> [ChangedProfileContact] -> [ChangedProfileContact]
|
||||||
ct' = updateMergedPreferences user' ct
|
addChangedProfileContact user' ct changedCts = case contactSendConn_ ct' of
|
||||||
mergedProfile' = userProfileToSend user' Nothing $ Just ct'
|
Left _ -> changedCts
|
||||||
if mergedProfile' == mergedProfile
|
Right conn
|
||||||
then pure s {notChanged = notChanged + 1}
|
| connIncognito conn || mergedProfile' == mergedProfile -> changedCts
|
||||||
else
|
| otherwise -> ChangedProfileContact ct ct' mergedProfile' conn : changedCts
|
||||||
let cts' = if mergedPreferences ct == mergedPreferences ct' then cts else ct' : cts
|
|
||||||
in (notifyContact mergedProfile' ct' $> s {updateSuccesses = updateSuccesses + 1, changedContacts = cts'})
|
|
||||||
`catchChatError` \e -> when (ll <= CLLInfo) (toView $ CRChatError (Just user) e) $> s {updateFailures = updateFailures + 1, changedContacts = cts'}
|
|
||||||
where
|
where
|
||||||
notifyContact mergedProfile' ct' = do
|
mergedProfile = userProfileToSend user Nothing $ Just ct
|
||||||
void $ sendDirectContactMessage ct' (XInfo mergedProfile')
|
ct' = updateMergedPreferences user' ct
|
||||||
when (directOrUsed ct') $ createSndFeatureItems user' ct ct'
|
mergedProfile' = userProfileToSend user' Nothing $ Just ct'
|
||||||
|
ctSndMsg :: ChangedProfileContact -> (ConnOrGroupId, ChatMsgEvent 'Json)
|
||||||
|
ctSndMsg ChangedProfileContact {mergedProfile', conn = Connection {connId}} = (ConnectionId connId, XInfo mergedProfile')
|
||||||
|
ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError MsgReq
|
||||||
|
ctMsgReq ChangedProfileContact {conn} = fmap $ \SndMessage {msgId, msgBody} ->
|
||||||
|
(conn, MsgFlags {notification = hasNotification XInfo_}, msgBody, msgId)
|
||||||
updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse
|
updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse
|
||||||
updateContactPrefs _ ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotActive ct
|
updateContactPrefs _ ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotActive ct
|
||||||
updateContactPrefs user@User {userId} ct@Contact {activeConn = Just Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs'
|
updateContactPrefs user@User {userId} ct@Contact {activeConn = Just Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs'
|
||||||
@@ -2399,6 +2416,13 @@ processChatCommand' vr = \case
|
|||||||
cReqHashes = bimap hash hash cReqSchemas
|
cReqHashes = bimap hash hash cReqSchemas
|
||||||
hash = ConnReqUriHash . C.sha256Hash . strEncode
|
hash = ConnReqUriHash . C.sha256Hash . strEncode
|
||||||
|
|
||||||
|
data ChangedProfileContact = ChangedProfileContact
|
||||||
|
{ ct :: Contact,
|
||||||
|
ct' :: Contact,
|
||||||
|
mergedProfile' :: Profile,
|
||||||
|
conn :: Connection
|
||||||
|
}
|
||||||
|
|
||||||
prepareGroupMsg :: forall m. ChatMonad m => User -> GroupInfo -> MsgContent -> Maybe ChatItemId -> Maybe FileInvitation -> Maybe CITimed -> Bool -> m (MsgContainer, Maybe (CIQuote 'CTGroup))
|
prepareGroupMsg :: forall m. ChatMonad m => User -> GroupInfo -> MsgContent -> Maybe ChatItemId -> Maybe FileInvitation -> Maybe CITimed -> Bool -> m (MsgContainer, Maybe (CIQuote 'CTGroup))
|
||||||
prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ fInv_ timed_ live = case quotedItemId_ of
|
prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ fInv_ timed_ live = case quotedItemId_ of
|
||||||
Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
|
Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
|
||||||
@@ -2480,6 +2504,7 @@ startExpireCIThread user@User {userId} = do
|
|||||||
flip catchChatError (toView . CRChatError (Just user)) $ do
|
flip catchChatError (toView . CRChatError (Just user)) $ do
|
||||||
expireFlags <- asks expireCIFlags
|
expireFlags <- asks expireCIFlags
|
||||||
atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry
|
atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry
|
||||||
|
waitChatStartedAndActivated
|
||||||
ttl <- withStoreCtx' (Just "startExpireCIThread, getChatItemTTL") (`getChatItemTTL` user)
|
ttl <- withStoreCtx' (Just "startExpireCIThread, getChatItemTTL") (`getChatItemTTL` user)
|
||||||
forM_ ttl $ \t -> expireChatItems user t False
|
forM_ ttl $ \t -> expireChatItems user t False
|
||||||
liftIO $ threadDelay' interval
|
liftIO $ threadDelay' interval
|
||||||
@@ -2973,7 +2998,7 @@ cleanupManager = do
|
|||||||
stepDelay <- asks (cleanupManagerStepDelay . config)
|
stepDelay <- asks (cleanupManagerStepDelay . config)
|
||||||
forever $ do
|
forever $ do
|
||||||
flip catchChatError (toView . CRChatError Nothing) $ do
|
flip catchChatError (toView . CRChatError Nothing) $ do
|
||||||
waitChatStarted
|
waitChatStartedAndActivated
|
||||||
users <- withStoreCtx' (Just "cleanupManager, getUsers 1") getUsers
|
users <- withStoreCtx' (Just "cleanupManager, getUsers 1") getUsers
|
||||||
let (us, us') = partition activeUser users
|
let (us, us') = partition activeUser users
|
||||||
forM_ us $ cleanupUser interval stepDelay
|
forM_ us $ cleanupUser interval stepDelay
|
||||||
@@ -2983,7 +3008,7 @@ cleanupManager = do
|
|||||||
liftIO $ threadDelay' $ diffToMicroseconds interval
|
liftIO $ threadDelay' $ diffToMicroseconds interval
|
||||||
where
|
where
|
||||||
runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CRChatError Nothing) $ do
|
runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CRChatError Nothing) $ do
|
||||||
waitChatStarted
|
waitChatStartedAndActivated
|
||||||
users <- withStoreCtx' (Just "cleanupManager, getUsers 2") getUsers
|
users <- withStoreCtx' (Just "cleanupManager, getUsers 2") getUsers
|
||||||
let (us, us') = partition activeUser users
|
let (us, us') = partition activeUser users
|
||||||
forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CRChatError (Just u))
|
forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CRChatError (Just u))
|
||||||
@@ -3038,7 +3063,7 @@ deleteTimedItem :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m
|
|||||||
deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do
|
deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do
|
||||||
ts <- liftIO getCurrentTime
|
ts <- liftIO getCurrentTime
|
||||||
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts
|
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts
|
||||||
waitChatStarted
|
waitChatStartedAndActivated
|
||||||
vr <- chatVersionRange
|
vr <- chatVersionRange
|
||||||
case cType of
|
case cType of
|
||||||
CTDirect -> do
|
CTDirect -> do
|
||||||
@@ -3064,8 +3089,10 @@ expireChatItems user@User {userId} ttl sync = do
|
|||||||
let expirationDate = addUTCTime (-1 * fromIntegral ttl) currentTs
|
let expirationDate = addUTCTime (-1 * fromIntegral ttl) currentTs
|
||||||
-- this is to keep group messages created during last 12 hours even if they're expired according to item_ts
|
-- this is to keep group messages created during last 12 hours even if they're expired according to item_ts
|
||||||
createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs
|
createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs
|
||||||
|
waitChatStartedAndActivated
|
||||||
contacts <- withStoreCtx' (Just "expireChatItems, getUserContacts") (`getUserContacts` user)
|
contacts <- withStoreCtx' (Just "expireChatItems, getUserContacts") (`getUserContacts` user)
|
||||||
loop contacts $ processContact expirationDate
|
loop contacts $ processContact expirationDate
|
||||||
|
waitChatStartedAndActivated
|
||||||
groups <- withStoreCtx' (Just "expireChatItems, getUserGroupDetails") (\db -> getUserGroupDetails db vr user Nothing Nothing)
|
groups <- withStoreCtx' (Just "expireChatItems, getUserGroupDetails") (\db -> getUserGroupDetails db vr user Nothing Nothing)
|
||||||
loop groups $ processGroup expirationDate createdAtCutoff
|
loop groups $ processGroup expirationDate createdAtCutoff
|
||||||
where
|
where
|
||||||
@@ -3084,11 +3111,13 @@ expireChatItems user@User {userId} ttl sync = do
|
|||||||
when (expire == Just True) $ threadDelay 100000 >> a
|
when (expire == Just True) $ threadDelay 100000 >> a
|
||||||
processContact :: UTCTime -> Contact -> m ()
|
processContact :: UTCTime -> Contact -> m ()
|
||||||
processContact expirationDate ct = do
|
processContact expirationDate ct = do
|
||||||
|
waitChatStartedAndActivated
|
||||||
filesInfo <- withStoreCtx' (Just "processContact, getContactExpiredFileInfo") $ \db -> getContactExpiredFileInfo db user ct expirationDate
|
filesInfo <- withStoreCtx' (Just "processContact, getContactExpiredFileInfo") $ \db -> getContactExpiredFileInfo db user ct expirationDate
|
||||||
deleteFilesAndConns user filesInfo
|
deleteFilesAndConns user filesInfo
|
||||||
withStoreCtx' (Just "processContact, deleteContactExpiredCIs") $ \db -> deleteContactExpiredCIs db user ct expirationDate
|
withStoreCtx' (Just "processContact, deleteContactExpiredCIs") $ \db -> deleteContactExpiredCIs db user ct expirationDate
|
||||||
processGroup :: UTCTime -> UTCTime -> GroupInfo -> m ()
|
processGroup :: UTCTime -> UTCTime -> GroupInfo -> m ()
|
||||||
processGroup expirationDate createdAtCutoff gInfo = do
|
processGroup expirationDate createdAtCutoff gInfo = do
|
||||||
|
waitChatStartedAndActivated
|
||||||
filesInfo <- withStoreCtx' (Just "processGroup, getGroupExpiredFileInfo") $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff
|
filesInfo <- withStoreCtx' (Just "processGroup, getGroupExpiredFileInfo") $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff
|
||||||
deleteFilesAndConns user filesInfo
|
deleteFilesAndConns user filesInfo
|
||||||
withStoreCtx' (Just "processGroup, deleteGroupExpiredCIs") $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff
|
withStoreCtx' (Just "processGroup, deleteGroupExpiredCIs") $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff
|
||||||
@@ -3356,6 +3385,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont
|
sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont
|
||||||
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
|
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
|
||||||
MSG msgMeta _msgFlags msgBody -> do
|
MSG msgMeta _msgFlags msgBody -> do
|
||||||
|
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||||
cmdId <- createAckCmd conn
|
cmdId <- createAckCmd conn
|
||||||
withAckMessage agentConnId cmdId msgMeta $ do
|
withAckMessage agentConnId cmdId msgMeta $ do
|
||||||
(conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn msgMeta cmdId msgBody
|
(conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn msgMeta cmdId msgBody
|
||||||
@@ -3364,14 +3394,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
updateChatLock "directMessage" event
|
updateChatLock "directMessage" event
|
||||||
case event of
|
case event of
|
||||||
XMsgNew mc -> newContentMessage ct' mc msg msgMeta
|
XMsgNew mc -> newContentMessage ct' mc msg msgMeta
|
||||||
XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr msgMeta
|
XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr
|
||||||
XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct' sharedMsgId mContent msg msgMeta ttl live
|
XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct' sharedMsgId mContent msg msgMeta ttl live
|
||||||
XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta
|
XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta
|
||||||
XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta
|
XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta
|
||||||
-- TODO discontinue XFile
|
-- TODO discontinue XFile
|
||||||
XFile fInv -> processFileInvitation' ct' fInv msg msgMeta
|
XFile fInv -> processFileInvitation' ct' fInv msg msgMeta
|
||||||
XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta
|
XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId
|
||||||
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta
|
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName
|
||||||
XInfo p -> xInfo ct' p
|
XInfo p -> xInfo ct' p
|
||||||
XDirectDel -> xDirectDel ct' msg msgMeta
|
XDirectDel -> xDirectDel ct' msg msgMeta
|
||||||
XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
|
XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
|
||||||
@@ -3379,10 +3409,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct') probeHash
|
XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct') probeHash
|
||||||
XInfoProbeOk probe -> xInfoProbeOk (COMContact ct') probe
|
XInfoProbeOk probe -> xInfoProbeOk (COMContact ct') probe
|
||||||
XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta
|
XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta
|
||||||
XCallOffer callId offer -> xCallOffer ct' callId offer msg msgMeta
|
XCallOffer callId offer -> xCallOffer ct' callId offer msg
|
||||||
XCallAnswer callId answer -> xCallAnswer ct' callId answer msg msgMeta
|
XCallAnswer callId answer -> xCallAnswer ct' callId answer msg
|
||||||
XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg msgMeta
|
XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg
|
||||||
XCallEnd callId -> xCallEnd ct' callId msg msgMeta
|
XCallEnd callId -> xCallEnd ct' callId msg
|
||||||
BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta
|
BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta
|
||||||
_ -> messageError $ "unsupported message: " <> T.pack (show event)
|
_ -> messageError $ "unsupported message: " <> T.pack (show event)
|
||||||
let Contact {chatSettings = ChatSettings {sendRcpts}} = ct'
|
let Contact {chatSettings = ChatSettings {sendRcpts}} = ct'
|
||||||
@@ -3740,7 +3770,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
void $ sendDirectMessage imConn (XGrpMemCon memberId) (GroupId groupId)
|
void $ sendDirectMessage imConn (XGrpMemCon memberId) (GroupId groupId)
|
||||||
_ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected"
|
_ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected"
|
||||||
MSG msgMeta _msgFlags msgBody -> do
|
MSG msgMeta _msgFlags msgBody -> do
|
||||||
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
|
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta
|
||||||
cmdId <- createAckCmd conn
|
cmdId <- createAckCmd conn
|
||||||
let aChatMsgs = parseChatMessages msgBody
|
let aChatMsgs = parseChatMessages msgBody
|
||||||
withAckMessage agentConnId cmdId msgMeta $ do
|
withAckMessage agentConnId cmdId msgMeta $ do
|
||||||
@@ -4231,7 +4261,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
|
newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
|
||||||
newContentMessage ct@Contact {contactUsed} mc msg@RcvMessage {sharedMsgId_} msgMeta = do
|
newContentMessage ct@Contact {contactUsed} mc msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||||
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
|
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
|
||||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
|
||||||
let ExtMsgContent content fInv_ _ _ = mcExtMsgContent mc
|
let ExtMsgContent content fInv_ _ _ = mcExtMsgContent mc
|
||||||
-- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete
|
-- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete
|
||||||
-- case content of
|
-- case content of
|
||||||
@@ -4261,9 +4290,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
ChatConfig {autoAcceptFileSize = sz} <- asks config
|
ChatConfig {autoAcceptFileSize = sz} <- asks config
|
||||||
when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView
|
when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView
|
||||||
|
|
||||||
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> MsgMeta -> m ()
|
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> m ()
|
||||||
messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr msgMeta = do
|
messageFileDescription Contact {contactId} sharedMsgId fileDescr = do
|
||||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
|
||||||
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
|
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
|
||||||
processFDMessage fileId fileDescr
|
processFDMessage fileId fileDescr
|
||||||
|
|
||||||
@@ -4306,7 +4334,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
|
|
||||||
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
|
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
|
||||||
messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
|
messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
|
||||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
|
||||||
updateRcvChatItem `catchCINotFound` \_ -> do
|
updateRcvChatItem `catchCINotFound` \_ -> do
|
||||||
-- This patches initial sharedMsgId into chat item when locally deleted chat item
|
-- This patches initial sharedMsgId into chat item when locally deleted chat item
|
||||||
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
|
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
|
||||||
@@ -4339,10 +4366,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
_ -> messageError "x.msg.update: contact attempted invalid message update"
|
_ -> messageError "x.msg.update: contact attempted invalid message update"
|
||||||
|
|
||||||
messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m ()
|
messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m ()
|
||||||
messageDelete ct@Contact {contactId} sharedMsgId RcvMessage {msgId} msgMeta@MsgMeta {broker = (_, brokerTs)} = do
|
messageDelete ct@Contact {contactId} sharedMsgId RcvMessage {msgId} msgMeta = do
|
||||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
|
||||||
deleteRcvChatItem `catchCINotFound` (toView . CRChatItemDeletedNotFound user ct)
|
deleteRcvChatItem `catchCINotFound` (toView . CRChatItemDeletedNotFound user ct)
|
||||||
where
|
where
|
||||||
|
brokerTs = metaBrokerTs msgMeta
|
||||||
deleteRcvChatItem = do
|
deleteRcvChatItem = do
|
||||||
CChatItem msgDir ci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId
|
CChatItem msgDir ci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId
|
||||||
case msgDir of
|
case msgDir of
|
||||||
@@ -4510,7 +4537,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
-- TODO remove once XFile is discontinued
|
-- TODO remove once XFile is discontinued
|
||||||
processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
|
processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
|
||||||
processFileInvitation' ct fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
|
processFileInvitation' ct fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
|
||||||
ChatConfig {fileChunkSize} <- asks config
|
ChatConfig {fileChunkSize} <- asks config
|
||||||
inline <- receiveInlineMode fInv Nothing fileChunkSize
|
inline <- receiveInlineMode fInv Nothing fileChunkSize
|
||||||
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
|
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
|
||||||
@@ -4547,9 +4573,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
inline' receiveInstant = if mode == IFMOffer || (receiveInstant && maybe False isVoice mc_) then fileInline else Nothing
|
inline' receiveInstant = if mode == IFMOffer || (receiveInstant && maybe False isVoice mc_) then fileInline else Nothing
|
||||||
_ -> pure Nothing
|
_ -> pure Nothing
|
||||||
|
|
||||||
xFileCancel :: Contact -> SharedMsgId -> MsgMeta -> m ()
|
xFileCancel :: Contact -> SharedMsgId -> m ()
|
||||||
xFileCancel ct@Contact {contactId} sharedMsgId msgMeta = do
|
xFileCancel Contact {contactId} sharedMsgId = do
|
||||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
|
||||||
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
|
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
|
||||||
ft <- withStore (\db -> getRcvFileTransfer db user fileId)
|
ft <- withStore (\db -> getRcvFileTransfer db user fileId)
|
||||||
unless (rcvFileCompleteOrCancelled ft) $ do
|
unless (rcvFileCompleteOrCancelled ft) $ do
|
||||||
@@ -4557,9 +4582,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
ci <- withStore $ \db -> getChatItemByFileId db vr user fileId
|
ci <- withStore $ \db -> getChatItemByFileId db vr user fileId
|
||||||
toView $ CRRcvFileSndCancelled user ci ft
|
toView $ CRRcvFileSndCancelled user ci ft
|
||||||
|
|
||||||
xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m ()
|
xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> m ()
|
||||||
xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta = do
|
xFileAcptInv ct sharedMsgId fileConnReq_ fName = do
|
||||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
|
||||||
fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId
|
fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId
|
||||||
(AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId
|
(AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId
|
||||||
assertSMPAcceptNotProhibited ci
|
assertSMPAcceptNotProhibited ci
|
||||||
@@ -4693,7 +4717,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
let Contact {localDisplayName = c, activeConn} = ct
|
let Contact {localDisplayName = c, activeConn} = ct
|
||||||
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
|
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
|
||||||
forM_ activeConn $ \Connection {connId, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do
|
forM_ activeConn $ \Connection {connId, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do
|
||||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
|
||||||
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
|
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
|
||||||
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
|
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
|
||||||
-- [incognito] if direct connection with host is incognito, create membership using the same incognito profile
|
-- [incognito] if direct connection with host is incognito, create membership using the same incognito profile
|
||||||
@@ -4725,7 +4748,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m ()
|
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m ()
|
||||||
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
|
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
|
||||||
MsgOk -> pure ()
|
MsgOk -> pure ()
|
||||||
MsgError e -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs)
|
MsgError e ->
|
||||||
|
createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs)
|
||||||
|
`catchChatError` \_ -> pure ()
|
||||||
|
|
||||||
xInfo :: Contact -> Profile -> m ()
|
xInfo :: Contact -> Profile -> m ()
|
||||||
xInfo c p' = void $ processContactProfileUpdate c p' True
|
xInfo c p' = void $ processContactProfileUpdate c p' True
|
||||||
@@ -4734,7 +4759,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
xDirectDel c msg msgMeta =
|
xDirectDel c msg msgMeta =
|
||||||
if directOrUsed c
|
if directOrUsed c
|
||||||
then do
|
then do
|
||||||
checkIntegrityCreateItem (CDDirectRcv c) msgMeta
|
|
||||||
ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted
|
ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted
|
||||||
contactConns <- withStore' $ \db -> getContactConnections db userId ct'
|
contactConns <- withStore' $ \db -> getContactConnections db userId ct'
|
||||||
deleteAgentConnectionsAsync user $ map aConnId contactConns
|
deleteAgentConnectionsAsync user $ map aConnId contactConns
|
||||||
@@ -4894,7 +4918,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
-- to party accepting call
|
-- to party accepting call
|
||||||
xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m ()
|
xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m ()
|
||||||
xCallInv ct@Contact {contactId} callId CallInvitation {callType, callDhPubKey} msg@RcvMessage {sharedMsgId_} msgMeta = do
|
xCallInv ct@Contact {contactId} callId CallInvitation {callType, callDhPubKey} msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
|
||||||
if featureAllowed SCFCalls forContact ct
|
if featureAllowed SCFCalls forContact ct
|
||||||
then do
|
then do
|
||||||
g <- asks random
|
g <- asks random
|
||||||
@@ -4921,9 +4944,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
|
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
|
||||||
|
|
||||||
-- to party initiating call
|
-- to party initiating call
|
||||||
xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> MsgMeta -> m ()
|
xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> m ()
|
||||||
xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey} msg msgMeta = do
|
xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey} msg = do
|
||||||
msgCurrentCall ct callId "x.call.offer" msg msgMeta $
|
msgCurrentCall ct callId "x.call.offer" msg $
|
||||||
\call -> case callState call of
|
\call -> case callState call of
|
||||||
CallInvitationSent {localCallType, localDhPrivKey} -> do
|
CallInvitationSent {localCallType, localDhPrivKey} -> do
|
||||||
let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> localDhPrivKey)
|
let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> localDhPrivKey)
|
||||||
@@ -4936,9 +4959,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
pure (Just call, Nothing)
|
pure (Just call, Nothing)
|
||||||
|
|
||||||
-- to party accepting call
|
-- to party accepting call
|
||||||
xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> MsgMeta -> m ()
|
xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> m ()
|
||||||
xCallAnswer ct callId CallAnswer {rtcSession} msg msgMeta = do
|
xCallAnswer ct callId CallAnswer {rtcSession} msg = do
|
||||||
msgCurrentCall ct callId "x.call.answer" msg msgMeta $
|
msgCurrentCall ct callId "x.call.answer" msg $
|
||||||
\call -> case callState call of
|
\call -> case callState call of
|
||||||
CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do
|
CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do
|
||||||
let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession = rtcSession, sharedKey}
|
let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession = rtcSession, sharedKey}
|
||||||
@@ -4949,9 +4972,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
pure (Just call, Nothing)
|
pure (Just call, Nothing)
|
||||||
|
|
||||||
-- to any call party
|
-- to any call party
|
||||||
xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> MsgMeta -> m ()
|
xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> m ()
|
||||||
xCallExtra ct callId CallExtraInfo {rtcExtraInfo} msg msgMeta = do
|
xCallExtra ct callId CallExtraInfo {rtcExtraInfo} msg = do
|
||||||
msgCurrentCall ct callId "x.call.extra" msg msgMeta $
|
msgCurrentCall ct callId "x.call.extra" msg $
|
||||||
\call -> case callState call of
|
\call -> case callState call of
|
||||||
CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do
|
CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do
|
||||||
-- TODO update the list of ice servers in peerCallSession
|
-- TODO update the list of ice servers in peerCallSession
|
||||||
@@ -4968,15 +4991,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
pure (Just call, Nothing)
|
pure (Just call, Nothing)
|
||||||
|
|
||||||
-- to any call party
|
-- to any call party
|
||||||
xCallEnd :: Contact -> CallId -> RcvMessage -> MsgMeta -> m ()
|
xCallEnd :: Contact -> CallId -> RcvMessage -> m ()
|
||||||
xCallEnd ct callId msg msgMeta =
|
xCallEnd ct callId msg =
|
||||||
msgCurrentCall ct callId "x.call.end" msg msgMeta $ \Call {chatItemId} -> do
|
msgCurrentCall ct callId "x.call.end" msg $ \Call {chatItemId} -> do
|
||||||
toView $ CRCallEnded user ct
|
toView $ CRCallEnded user ct
|
||||||
(Nothing,) <$> callStatusItemContent user ct chatItemId WCSDisconnected
|
(Nothing,) <$> callStatusItemContent user ct chatItemId WCSDisconnected
|
||||||
|
|
||||||
msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> MsgMeta -> (Call -> m (Maybe Call, Maybe ACIContent)) -> m ()
|
msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> (Call -> m (Maybe Call, Maybe ACIContent)) -> m ()
|
||||||
msgCurrentCall ct@Contact {contactId = ctId'} callId' eventName RcvMessage {msgId} msgMeta action = do
|
msgCurrentCall ct@Contact {contactId = ctId'} callId' eventName RcvMessage {msgId} action = do
|
||||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
|
||||||
calls <- asks currentCalls
|
calls <- asks currentCalls
|
||||||
atomically (TM.lookup ctId' calls) >>= \case
|
atomically (TM.lookup ctId' calls) >>= \case
|
||||||
Nothing -> messageError $ eventName <> ": no current call"
|
Nothing -> messageError $ eventName <> ": no current call"
|
||||||
@@ -5631,12 +5653,20 @@ deleteOrUpdateMemberRecord user@User {userId} member =
|
|||||||
Nothing -> deleteGroupMember db user member
|
Nothing -> deleteGroupMember db user member
|
||||||
|
|
||||||
sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64)
|
sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64)
|
||||||
sendDirectContactMessage ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotReady ct
|
sendDirectContactMessage ct chatMsgEvent = do
|
||||||
sendDirectContactMessage ct@Contact {activeConn = Just conn@Connection {connId, connStatus}, contactStatus} chatMsgEvent
|
conn@Connection {connId} <- liftEither $ contactSendConn_ ct
|
||||||
| connStatus /= ConnReady && connStatus /= ConnSndReady = throwChatError $ CEContactNotReady ct
|
sendDirectMessage conn chatMsgEvent (ConnectionId connId)
|
||||||
| contactStatus /= CSActive = throwChatError $ CEContactNotActive ct
|
|
||||||
| connDisabled conn = throwChatError $ CEContactDisabled ct
|
contactSendConn_ :: Contact -> Either ChatError Connection
|
||||||
| otherwise = sendDirectMessage conn chatMsgEvent (ConnectionId connId)
|
contactSendConn_ ct@Contact {activeConn} = case activeConn of
|
||||||
|
Nothing -> err $ CEContactNotReady ct
|
||||||
|
Just conn
|
||||||
|
| not (connReady conn) -> err $ CEContactNotReady ct
|
||||||
|
| not (contactActive ct) -> err $ CEContactNotActive ct
|
||||||
|
| connDisabled conn -> err $ CEContactDisabled ct
|
||||||
|
| otherwise -> Right conn
|
||||||
|
where
|
||||||
|
err = Left . ChatError
|
||||||
|
|
||||||
sendDirectMessage :: (MsgEncodingI e, ChatMonad m) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m (SndMessage, Int64)
|
sendDirectMessage :: (MsgEncodingI e, ChatMonad m) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m (SndMessage, Int64)
|
||||||
sendDirectMessage conn chatMsgEvent connOrGroupId = do
|
sendDirectMessage conn chatMsgEvent connOrGroupId = do
|
||||||
@@ -5645,18 +5675,25 @@ sendDirectMessage conn chatMsgEvent connOrGroupId = do
|
|||||||
(msg,) <$> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
|
(msg,) <$> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
|
||||||
|
|
||||||
createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage
|
createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage
|
||||||
createSndMessage chatMsgEvent connOrGroupId = do
|
createSndMessage chatMsgEvent connOrGroupId =
|
||||||
|
liftEither . runIdentity =<< createSndMessages (Identity (connOrGroupId, chatMsgEvent))
|
||||||
|
|
||||||
|
createSndMessages :: forall e m t. (MsgEncodingI e, ChatMonad' m, Traversable t) => t (ConnOrGroupId, ChatMsgEvent e) -> m (t (Either ChatError SndMessage))
|
||||||
|
createSndMessages idsEvents = do
|
||||||
gVar <- asks random
|
gVar <- asks random
|
||||||
vr <- chatVersionRange
|
vr <- chatVersionRange
|
||||||
withStore $ \db -> createNewSndMessage db gVar connOrGroupId chatMsgEvent (encodeMessage vr)
|
withStoreBatch $ \db -> fmap (uncurry (createMsg db gVar vr)) idsEvents
|
||||||
where
|
where
|
||||||
encodeMessage chatVRange sharedMsgId =
|
createMsg db gVar chatVRange connOrGroupId evnt = runExceptT $ do
|
||||||
encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent}
|
withExceptT ChatErrorStore $ createNewSndMessage db gVar connOrGroupId evnt (encodeMessage chatVRange evnt)
|
||||||
|
encodeMessage chatVRange evnt sharedMsgId =
|
||||||
|
encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent = evnt}
|
||||||
|
|
||||||
sendGroupMemberMessages :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> Connection -> NonEmpty (ChatMsgEvent e) -> GroupId -> m ()
|
sendGroupMemberMessages :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> Connection -> NonEmpty (ChatMsgEvent e) -> GroupId -> m ()
|
||||||
sendGroupMemberMessages user conn@Connection {connId} events groupId = do
|
sendGroupMemberMessages user conn@Connection {connId} events groupId = do
|
||||||
when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn)
|
when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn)
|
||||||
(errs, msgs) <- partitionEithers <$> createSndMessages
|
let idsEvts = L.map (GroupId groupId,) events
|
||||||
|
(errs, msgs) <- partitionEithers . L.toList <$> createSndMessages idsEvts
|
||||||
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
||||||
unless (null msgs) $ do
|
unless (null msgs) $ do
|
||||||
let (errs', msgBatches) = partitionEithers $ batchMessages maxChatMsgSize msgs
|
let (errs', msgBatches) = partitionEithers $ batchMessages maxChatMsgSize msgs
|
||||||
@@ -5671,16 +5708,6 @@ sendGroupMemberMessages user conn@Connection {connId} events groupId = do
|
|||||||
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) MsgFlags {notification = True} batchBody
|
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) MsgFlags {notification = True} batchBody
|
||||||
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
|
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
|
||||||
void . withStoreBatch' $ \db -> map (\SndMessage {msgId} -> createSndMsgDelivery db sndMsgDelivery msgId) sndMsgs
|
void . withStoreBatch' $ \db -> map (\SndMessage {msgId} -> createSndMsgDelivery db sndMsgDelivery msgId) sndMsgs
|
||||||
createSndMessages :: m [Either ChatError SndMessage]
|
|
||||||
createSndMessages = do
|
|
||||||
gVar <- asks random
|
|
||||||
vr <- chatVersionRange
|
|
||||||
withStoreBatch $ \db -> map (createMsg db gVar vr) (toList events)
|
|
||||||
createMsg db gVar chatVRange evnt = do
|
|
||||||
r <- runExceptT $ createNewSndMessage db gVar (GroupId groupId) evnt (encodeMessage chatVRange evnt)
|
|
||||||
pure $ first ChatErrorStore r
|
|
||||||
encodeMessage chatVRange evnt sharedMsgId =
|
|
||||||
encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent = evnt}
|
|
||||||
|
|
||||||
directMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> m ByteString
|
directMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> m ByteString
|
||||||
directMessage chatMsgEvent = do
|
directMessage chatMsgEvent = do
|
||||||
@@ -5701,14 +5728,23 @@ deliverMessage' conn msgFlags msgBody msgId =
|
|||||||
[r] -> liftEither r
|
[r] -> liftEither r
|
||||||
rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
|
rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
|
||||||
|
|
||||||
deliverMessages :: ChatMonad' m => [(Connection, MsgFlags, LazyMsgBody, MessageId)] -> m [Either ChatError Int64]
|
type MsgReq = (Connection, MsgFlags, LazyMsgBody, MessageId)
|
||||||
deliverMessages msgReqs = do
|
|
||||||
sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessages` aReqs)
|
deliverMessages :: ChatMonad' m => [MsgReq] -> m [Either ChatError Int64]
|
||||||
|
deliverMessages = deliverMessagesB . map Right
|
||||||
|
|
||||||
|
deliverMessagesB :: ChatMonad' m => [Either ChatError MsgReq] -> m [Either ChatError Int64]
|
||||||
|
deliverMessagesB msgReqs = do
|
||||||
|
sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessagesB` map toAgent msgReqs)
|
||||||
withStoreBatch $ \db -> map (bindRight $ createDelivery db) sent
|
withStoreBatch $ \db -> map (bindRight $ createDelivery db) sent
|
||||||
where
|
where
|
||||||
aReqs = map (\(conn, msgFlags, msgBody, _msgId) -> (aConnId conn, msgFlags, LB.toStrict msgBody)) msgReqs
|
toAgent = \case
|
||||||
prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,)
|
Right (conn, msgFlags, msgBody, _msgId) -> Right (aConnId conn, msgFlags, LB.toStrict msgBody)
|
||||||
createDelivery :: DB.Connection -> ((Connection, MsgFlags, LazyMsgBody, MessageId), AgentMsgId) -> IO (Either ChatError Int64)
|
Left _ce -> Left (AP.INTERNAL "ChatError, skip") -- as long as it is Left, the agent batchers should just step over it
|
||||||
|
prepareBatch (Right req) (Right ar) = Right (req, ar)
|
||||||
|
prepareBatch (Left ce) _ = Left ce -- restore original ChatError
|
||||||
|
prepareBatch _ (Left ae) = Left $ ChatErrorAgent ae Nothing
|
||||||
|
createDelivery :: DB.Connection -> (MsgReq, AgentMsgId) -> IO (Either ChatError Int64)
|
||||||
createDelivery db ((Connection {connId}, _, _, msgId), agentMsgId) =
|
createDelivery db ((Connection {connId}, _, _, msgId), agentMsgId) =
|
||||||
Right <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId
|
Right <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId
|
||||||
|
|
||||||
@@ -5854,7 +5890,7 @@ saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem
|
|||||||
ciId <- createNewSndChatItem db user cd msg content quotedItem itemTimed live createdAt
|
ciId <- createNewSndChatItem db user cd msg content quotedItem itemTimed live createdAt
|
||||||
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
||||||
pure ciId
|
pure ciId
|
||||||
liftIO $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemTimed live createdAt Nothing createdAt
|
pure $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemTimed live createdAt Nothing createdAt
|
||||||
|
|
||||||
saveRcvChatItem :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> m (ChatItem c 'MDRcv)
|
saveRcvChatItem :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> m (ChatItem c 'MDRcv)
|
||||||
saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content =
|
saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content =
|
||||||
@@ -5868,14 +5904,14 @@ saveRcvChatItem' user cd msg@RcvMessage {forwardedByMember} sharedMsgId_ brokerT
|
|||||||
(ciId, quotedItem) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt
|
(ciId, quotedItem) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt
|
||||||
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
||||||
pure (ciId, quotedItem)
|
pure (ciId, quotedItem)
|
||||||
liftIO $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemTimed live brokerTs forwardedByMember createdAt
|
pure $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemTimed live brokerTs forwardedByMember createdAt
|
||||||
|
|
||||||
mkChatItem :: forall c d. MsgDirectionI d => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CITimed -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> IO (ChatItem c d)
|
mkChatItem :: forall c d. MsgDirectionI d => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CITimed -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d
|
||||||
mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs forwardedByMember currentTs = do
|
mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs forwardedByMember currentTs =
|
||||||
let itemText = ciContentToText content
|
let itemText = ciContentToText content
|
||||||
itemStatus = ciCreateStatus content
|
itemStatus = ciCreateStatus content
|
||||||
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs
|
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs
|
||||||
pure ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file}
|
in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file}
|
||||||
|
|
||||||
deleteDirectCI :: (ChatMonad m, MsgDirectionI d) => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> m ChatResponse
|
deleteDirectCI :: (ChatMonad m, MsgDirectionI d) => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> m ChatResponse
|
||||||
deleteDirectCI user ct ci@ChatItem {file} byUser timed = do
|
deleteDirectCI user ct ci@ChatItem {file} byUser timed = do
|
||||||
@@ -5988,6 +6024,15 @@ createSndFeatureItems user ct ct' =
|
|||||||
CUPContact {preference} -> preference
|
CUPContact {preference} -> preference
|
||||||
CUPUser {preference} -> preference
|
CUPUser {preference} -> preference
|
||||||
|
|
||||||
|
createContactsSndFeatureItems :: forall m. ChatMonad m => User -> [ChangedProfileContact] -> m ()
|
||||||
|
createContactsSndFeatureItems user cts =
|
||||||
|
createContactsFeatureItems user cts' CDDirectSnd CISndChatFeature CISndChatPreference getPref
|
||||||
|
where
|
||||||
|
cts' = map (\ChangedProfileContact {ct, ct'} -> (ct, ct')) cts
|
||||||
|
getPref ContactUserPreference {userPreference} = case userPreference of
|
||||||
|
CUPContact {preference} -> preference
|
||||||
|
CUPUser {preference} -> preference
|
||||||
|
|
||||||
type FeatureContent a d = ChatFeature -> a -> Maybe Int -> CIContent d
|
type FeatureContent a d = ChatFeature -> a -> Maybe Int -> CIContent d
|
||||||
|
|
||||||
createFeatureItems ::
|
createFeatureItems ::
|
||||||
@@ -6001,24 +6046,44 @@ createFeatureItems ::
|
|||||||
FeatureContent FeatureAllowed d ->
|
FeatureContent FeatureAllowed d ->
|
||||||
(forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) ->
|
(forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) ->
|
||||||
m ()
|
m ()
|
||||||
createFeatureItems user Contact {mergedPreferences = cups} ct'@Contact {mergedPreferences = cups'} chatDir ciFeature ciOffer getPref =
|
createFeatureItems user ct ct' = createContactsFeatureItems user [(ct, ct')]
|
||||||
forM_ allChatFeatures $ \(ACF f) -> createItem f
|
|
||||||
|
createContactsFeatureItems ::
|
||||||
|
forall d m.
|
||||||
|
(MsgDirectionI d, ChatMonad m) =>
|
||||||
|
User ->
|
||||||
|
[(Contact, Contact)] ->
|
||||||
|
(Contact -> ChatDirection 'CTDirect d) ->
|
||||||
|
FeatureContent PrefEnabled d ->
|
||||||
|
FeatureContent FeatureAllowed d ->
|
||||||
|
(forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) ->
|
||||||
|
m ()
|
||||||
|
createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do
|
||||||
|
let dirsCIContents = map contactChangedFeatures cts
|
||||||
|
(errs, acis) <- partitionEithers <$> createInternalItemsForChats user Nothing dirsCIContents
|
||||||
|
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
||||||
|
forM_ acis $ \aci -> toView $ CRNewChatItem user aci
|
||||||
where
|
where
|
||||||
createItem :: forall f. FeatureI f => SChatFeature f -> m ()
|
contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, [CIContent d])
|
||||||
createItem f
|
contactChangedFeatures (Contact {mergedPreferences = cups}, ct'@Contact {mergedPreferences = cups'}) = do
|
||||||
| state /= state' = create ciFeature state'
|
let contents = mapMaybe (\(ACF f) -> featureCIContent_ f) allChatFeatures
|
||||||
| prefState /= prefState' = create ciOffer prefState'
|
(chatDir ct', contents)
|
||||||
| otherwise = pure ()
|
|
||||||
where
|
where
|
||||||
create :: FeatureContent a d -> (a, Maybe Int) -> m ()
|
featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d)
|
||||||
create ci (s, param) = createInternalChatItem user (chatDir ct') (ci f' s param) Nothing
|
featureCIContent_ f
|
||||||
f' = chatFeature f
|
| state /= state' = Just $ fContent ciFeature state'
|
||||||
state = featureState cup
|
| prefState /= prefState' = Just $ fContent ciOffer prefState'
|
||||||
state' = featureState cup'
|
| otherwise = Nothing
|
||||||
prefState = preferenceState $ getPref cup
|
where
|
||||||
prefState' = preferenceState $ getPref cup'
|
fContent :: FeatureContent a d -> (a, Maybe Int) -> CIContent d
|
||||||
cup = getContactUserPreference f cups
|
fContent ci (s, param) = ci f' s param
|
||||||
cup' = getContactUserPreference f cups'
|
f' = chatFeature f
|
||||||
|
state = featureState cup
|
||||||
|
state' = featureState cup'
|
||||||
|
prefState = preferenceState $ getPref cup
|
||||||
|
prefState' = preferenceState $ getPref cup'
|
||||||
|
cup = getContactUserPreference f cups
|
||||||
|
cup' = getContactUserPreference f cups'
|
||||||
|
|
||||||
createGroupFeatureChangedItems :: (MsgDirectionI d, ChatMonad m) => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> CIContent d) -> GroupInfo -> GroupInfo -> m ()
|
createGroupFeatureChangedItems :: (MsgDirectionI d, ChatMonad m) => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> CIContent d) -> GroupInfo -> GroupInfo -> m ()
|
||||||
createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} =
|
createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} =
|
||||||
@@ -6032,15 +6097,35 @@ createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences
|
|||||||
sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool
|
sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool
|
||||||
sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing}
|
sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing}
|
||||||
|
|
||||||
createInternalChatItem :: forall c d m. (ChatTypeI c, MsgDirectionI d, ChatMonad m) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> m ()
|
createInternalChatItem :: (ChatTypeI c, MsgDirectionI d, ChatMonad m) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> m ()
|
||||||
createInternalChatItem user cd content itemTs_ = do
|
createInternalChatItem user cd content itemTs_ =
|
||||||
|
createInternalItemsForChats user itemTs_ [(cd, [content])] >>= \case
|
||||||
|
[Right aci] -> toView $ CRNewChatItem user aci
|
||||||
|
[Left e] -> throwError e
|
||||||
|
rs -> throwChatError $ CEInternalError $ "createInternalChatItem: expected 1 result, got " <> show (length rs)
|
||||||
|
|
||||||
|
createInternalItemsForChats ::
|
||||||
|
forall c d m.
|
||||||
|
(ChatTypeI c, MsgDirectionI d, ChatMonad' m) =>
|
||||||
|
User ->
|
||||||
|
Maybe UTCTime ->
|
||||||
|
[(ChatDirection c d, [CIContent d])] ->
|
||||||
|
m [Either ChatError AChatItem]
|
||||||
|
createInternalItemsForChats user itemTs_ dirsCIContents = do
|
||||||
createdAt <- liftIO getCurrentTime
|
createdAt <- liftIO getCurrentTime
|
||||||
let itemTs = fromMaybe createdAt itemTs_
|
let itemTs = fromMaybe createdAt itemTs_
|
||||||
ciId <- withStore' $ \db -> do
|
void . withStoreBatch' $ \db -> map (uncurry $ updateChat db createdAt) dirsCIContents
|
||||||
when (ciRequiresAttention content) $ updateChatTs db user cd createdAt
|
withStoreBatch' $ \db -> concatMap (uncurry $ createACIs db itemTs createdAt) dirsCIContents
|
||||||
createNewChatItemNoMsg db user cd content itemTs createdAt
|
where
|
||||||
ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs Nothing createdAt
|
updateChat :: DB.Connection -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO ()
|
||||||
toView $ CRNewChatItem user (AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci)
|
updateChat db createdAt cd contents
|
||||||
|
| any ciRequiresAttention contents = updateChatTs db user cd createdAt
|
||||||
|
| otherwise = pure ()
|
||||||
|
createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem]
|
||||||
|
createACIs db itemTs createdAt cd = map $ \content -> do
|
||||||
|
ciId <- createNewChatItemNoMsg db user cd content itemTs createdAt
|
||||||
|
let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs Nothing createdAt
|
||||||
|
pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci
|
||||||
|
|
||||||
getCreateActiveUser :: SQLiteStore -> Bool -> IO User
|
getCreateActiveUser :: SQLiteStore -> Bool -> IO User
|
||||||
getCreateActiveUser st testView = do
|
getCreateActiveUser st testView = do
|
||||||
@@ -6133,10 +6218,14 @@ checkSameUser userId User {userId = activeUserId} = when (userId /= activeUserId
|
|||||||
chatStarted :: ChatMonad m => m Bool
|
chatStarted :: ChatMonad m => m Bool
|
||||||
chatStarted = fmap isJust . readTVarIO =<< asks agentAsync
|
chatStarted = fmap isJust . readTVarIO =<< asks agentAsync
|
||||||
|
|
||||||
waitChatStarted :: ChatMonad m => m ()
|
waitChatStartedAndActivated :: ChatMonad m => m ()
|
||||||
waitChatStarted = do
|
waitChatStartedAndActivated = do
|
||||||
agentStarted <- asks agentAsync
|
agentStarted <- asks agentAsync
|
||||||
atomically $ readTVar agentStarted >>= \a -> unless (isJust a) retry
|
chatActivated <- asks chatActivated
|
||||||
|
atomically $ do
|
||||||
|
started <- readTVar agentStarted
|
||||||
|
activated <- readTVar chatActivated
|
||||||
|
unless (isJust started && activated) retry
|
||||||
|
|
||||||
chatVersionRange :: ChatMonad' m => m VersionRange
|
chatVersionRange :: ChatMonad' m => m VersionRange
|
||||||
chatVersionRange = do
|
chatVersionRange = do
|
||||||
@@ -6173,8 +6262,8 @@ chatCommandP =
|
|||||||
"/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)),
|
"/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)),
|
||||||
"/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)),
|
"/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)),
|
||||||
("/user" <|> "/u") $> ShowActiveUser,
|
("/user" <|> "/u") $> ShowActiveUser,
|
||||||
"/_start subscribe=" *> (StartChat <$> onOffP <* " expire=" <*> onOffP <* " xftp=" <*> onOffP),
|
"/_start main=" *> (StartChat <$> onOffP),
|
||||||
"/_start" $> StartChat True True True,
|
"/_start" $> StartChat True,
|
||||||
"/_stop" $> APIStopChat,
|
"/_stop" $> APIStopChat,
|
||||||
"/_app activate restore=" *> (APIActivateChat <$> onOffP),
|
"/_app activate restore=" *> (APIActivateChat <$> onOffP),
|
||||||
"/_app activate" $> APIActivateChat True,
|
"/_app activate" $> APIActivateChat True,
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ data ChatController = ChatController
|
|||||||
expireCIThreads :: TMap UserId (Maybe (Async ())),
|
expireCIThreads :: TMap UserId (Maybe (Async ())),
|
||||||
expireCIFlags :: TMap UserId Bool,
|
expireCIFlags :: TMap UserId Bool,
|
||||||
cleanupManagerAsync :: TVar (Maybe (Async ())),
|
cleanupManagerAsync :: TVar (Maybe (Async ())),
|
||||||
|
chatActivated :: TVar Bool,
|
||||||
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
|
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
|
||||||
showLiveItems :: TVar Bool,
|
showLiveItems :: TVar Bool,
|
||||||
encryptLocalFiles :: TVar Bool,
|
encryptLocalFiles :: TVar Bool,
|
||||||
@@ -233,7 +234,7 @@ data ChatCommand
|
|||||||
| UnmuteUser
|
| UnmuteUser
|
||||||
| APIDeleteUser UserId Bool (Maybe UserPwd)
|
| APIDeleteUser UserId Bool (Maybe UserPwd)
|
||||||
| DeleteUser UserName Bool (Maybe UserPwd)
|
| DeleteUser UserName Bool (Maybe UserPwd)
|
||||||
| StartChat {subscribeConnections :: Bool, enableExpireChatItems :: Bool, startXFTPWorkers :: Bool}
|
| StartChat {mainApp :: Bool}
|
||||||
| APIStopChat
|
| APIStopChat
|
||||||
| APIActivateChat {restoreChat :: Bool}
|
| APIActivateChat {restoreChat :: Bool}
|
||||||
| APISuspendChat {suspendTimeout :: Int}
|
| APISuspendChat {suspendTimeout :: Int}
|
||||||
@@ -894,8 +895,7 @@ data PendingSubStatus = PendingSubStatus
|
|||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data UserProfileUpdateSummary = UserProfileUpdateSummary
|
data UserProfileUpdateSummary = UserProfileUpdateSummary
|
||||||
{ notChanged :: Int,
|
{ updateSuccesses :: Int,
|
||||||
updateSuccesses :: Int,
|
|
||||||
updateFailures :: Int,
|
updateFailures :: Int,
|
||||||
changedContacts :: [Contact]
|
changedContacts :: [Contact]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController
|
|||||||
runSimplexChat ChatOpts {maintenance} u cc chat
|
runSimplexChat ChatOpts {maintenance} u cc chat
|
||||||
| maintenance = wait =<< async (chat u cc)
|
| maintenance = wait =<< async (chat u cc)
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
a1 <- runReaderT (startChatController True True True) cc
|
a1 <- runReaderT (startChatController True) cc
|
||||||
a2 <- async $ chat u cc
|
a2 <- async $ chat u cc
|
||||||
waitEither_ a1 a2
|
waitEither_ a1 a2
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Simplex.Chat.Mobile.Shared where
|
module Simplex.Chat.Mobile.Shared where
|
||||||
|
|
||||||
import qualified Data.ByteString as B
|
import qualified Data.ByteString as B
|
||||||
import Data.ByteString.Internal (ByteString (..), memcpy)
|
import Data.ByteString.Internal (ByteString (..))
|
||||||
import qualified Data.ByteString.Lazy as LB
|
import qualified Data.ByteString.Lazy as LB
|
||||||
import qualified Data.ByteString.Lazy.Internal as LB
|
import qualified Data.ByteString.Lazy.Internal as LB
|
||||||
import Foreign
|
import Foreign
|
||||||
@@ -21,7 +21,7 @@ getByteString ptr len = do
|
|||||||
|
|
||||||
putByteString :: Ptr Word8 -> ByteString -> IO ()
|
putByteString :: Ptr Word8 -> ByteString -> IO ()
|
||||||
putByteString ptr (PS fp offset len) =
|
putByteString ptr (PS fp offset len) =
|
||||||
withForeignPtr fp $ \p -> memcpy ptr (p `plusPtr` offset) len
|
withForeignPtr fp $ \p -> copyBytes ptr (p `plusPtr` offset) len
|
||||||
{-# INLINE putByteString #-}
|
{-# INLINE putByteString #-}
|
||||||
|
|
||||||
putLazyByteString :: Ptr Word8 -> LB.ByteString -> IO ()
|
putLazyByteString :: Ptr Word8 -> LB.ByteString -> IO ()
|
||||||
|
|||||||
@@ -460,24 +460,23 @@ createGroupInvitedViaLink
|
|||||||
"INSERT INTO groups (group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?,?)"
|
"INSERT INTO groups (group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?,?)"
|
||||||
(profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs)
|
(profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs)
|
||||||
insertedRowId db
|
insertedRowId db
|
||||||
insertHost_ currentTs groupId = ExceptT $ do
|
insertHost_ currentTs groupId = do
|
||||||
let fromMemberProfile = profileFromName fromMemberName
|
let fromMemberProfile = profileFromName fromMemberName
|
||||||
withLocalDisplayName db userId fromMemberName $ \localDisplayName -> runExceptT $ do
|
(localDisplayName, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs
|
||||||
(_, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs
|
let MemberIdRole {memberId, memberRole} = fromMember
|
||||||
let MemberIdRole {memberId, memberRole} = fromMember
|
liftIO $ do
|
||||||
liftIO $ do
|
DB.execute
|
||||||
DB.execute
|
db
|
||||||
db
|
[sql|
|
||||||
[sql|
|
INSERT INTO group_members
|
||||||
INSERT INTO group_members
|
( group_id, member_id, member_role, member_category, member_status, invited_by,
|
||||||
( group_id, member_id, member_role, member_category, member_status, invited_by,
|
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
|
||||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
|]
|
||||||
|]
|
( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown)
|
||||||
( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown)
|
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
|
||||||
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
|
)
|
||||||
)
|
insertedRowId db
|
||||||
insertedRowId db
|
|
||||||
|
|
||||||
setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
|
setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
|
||||||
setViaGroupLinkHash db groupId connId =
|
setViaGroupLinkHash db groupId connId =
|
||||||
|
|||||||
@@ -415,6 +415,7 @@ xftpServerConfig =
|
|||||||
logStatsStartTime = 0,
|
logStatsStartTime = 0,
|
||||||
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
|
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
|
||||||
serverStatsBackupFile = Nothing,
|
serverStatsBackupFile = Nothing,
|
||||||
|
controlPort = Nothing,
|
||||||
transportConfig = defaultTransportServerConfig
|
transportConfig = defaultTransportServerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1149,7 +1149,7 @@ testSubscribeAppNSE tmp =
|
|||||||
alice ##> "/_app suspend 1"
|
alice ##> "/_app suspend 1"
|
||||||
alice <## "ok"
|
alice <## "ok"
|
||||||
alice <## "chat suspended"
|
alice <## "chat suspended"
|
||||||
nseAlice ##> "/_start subscribe=off expire=off xftp=off"
|
nseAlice ##> "/_start main=off"
|
||||||
nseAlice <## "chat started"
|
nseAlice <## "chat started"
|
||||||
nseAlice ##> "/ad"
|
nseAlice ##> "/ad"
|
||||||
cLink <- getContactLink nseAlice True
|
cLink <- getContactLink nseAlice True
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ chatProfileTests = do
|
|||||||
xit'' "enable timed messages with contact" testEnableTimedMessagesContact
|
xit'' "enable timed messages with contact" testEnableTimedMessagesContact
|
||||||
it "enable timed messages in group" testEnableTimedMessagesGroup
|
it "enable timed messages in group" testEnableTimedMessagesGroup
|
||||||
xit'' "timed messages enabled globally, contact turns on" testTimedMessagesEnabledGlobally
|
xit'' "timed messages enabled globally, contact turns on" testTimedMessagesEnabledGlobally
|
||||||
|
it "update multiple user preferences for multiple contacts" testUpdateMultipleUserPrefs
|
||||||
|
|
||||||
testUpdateProfile :: HasCallStack => FilePath -> IO ()
|
testUpdateProfile :: HasCallStack => FilePath -> IO ()
|
||||||
testUpdateProfile =
|
testUpdateProfile =
|
||||||
@@ -1864,3 +1865,30 @@ testTimedMessagesEnabledGlobally =
|
|||||||
bob <## "timed message deleted: hey"
|
bob <## "timed message deleted: hey"
|
||||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")])
|
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")])
|
||||||
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")])
|
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")])
|
||||||
|
|
||||||
|
testUpdateMultipleUserPrefs :: HasCallStack => FilePath -> IO ()
|
||||||
|
testUpdateMultipleUserPrefs = testChat3 aliceProfile bobProfile cathProfile $
|
||||||
|
\alice bob cath -> do
|
||||||
|
connectUsers alice bob
|
||||||
|
alice #> "@bob hi bob"
|
||||||
|
bob <# "alice> hi bob"
|
||||||
|
|
||||||
|
connectUsers alice cath
|
||||||
|
alice #> "@cath hi cath"
|
||||||
|
cath <# "alice> hi cath"
|
||||||
|
|
||||||
|
alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"fullDelete\": {\"allow\": \"always\"}, \"reactions\": {\"allow\": \"no\"}, \"receipts\": {\"allow\": \"yes\", \"activated\": true}}}"
|
||||||
|
alice <## "updated preferences:"
|
||||||
|
alice <## "Full deletion allowed: always"
|
||||||
|
alice <## "Message reactions allowed: no"
|
||||||
|
|
||||||
|
bob <## "alice updated preferences for you:"
|
||||||
|
bob <## "Full deletion: enabled for you (you allow: default (no), contact allows: always)"
|
||||||
|
bob <## "Message reactions: off (you allow: default (yes), contact allows: no)"
|
||||||
|
|
||||||
|
cath <## "alice updated preferences for you:"
|
||||||
|
cath <## "Full deletion: enabled for you (you allow: default (no), contact allows: always)"
|
||||||
|
cath <## "Message reactions: off (you allow: default (yes), contact allows: no)"
|
||||||
|
|
||||||
|
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi bob"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")])
|
||||||
|
alice #$> ("/_get chat @3 count=100", chat, chatFeatures <> [(1, "hi cath"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")])
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ import qualified Data.Aeson.TH as JQ
|
|||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
import qualified Data.ByteString as B
|
import qualified Data.ByteString as B
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import Data.ByteString.Internal (create, memcpy)
|
import Data.ByteString.Internal (create)
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||||
import Data.Word (Word8, Word32)
|
import Data.Word (Word8, Word32)
|
||||||
import Foreign.C
|
import Foreign.C
|
||||||
import Foreign.Marshal.Alloc (mallocBytes)
|
import Foreign.Marshal.Alloc (mallocBytes)
|
||||||
|
import Foreign.Marshal.Utils (copyBytes)
|
||||||
import Foreign.Ptr
|
import Foreign.Ptr
|
||||||
import Foreign.StablePtr
|
import Foreign.StablePtr
|
||||||
import Foreign.Storable (peek)
|
import Foreign.Storable (peek)
|
||||||
@@ -291,7 +292,7 @@ testFileCApi fileName tmp = do
|
|||||||
peek ptr' `shouldReturn` (0 :: Word8)
|
peek ptr' `shouldReturn` (0 :: Word8)
|
||||||
sz :: Word32 <- peek (ptr' `plusPtr` 1)
|
sz :: Word32 <- peek (ptr' `plusPtr` 1)
|
||||||
let sz' = fromIntegral sz
|
let sz' = fromIntegral sz
|
||||||
contents <- create sz' $ \toPtr -> memcpy toPtr (ptr' `plusPtr` 5) sz'
|
contents <- create sz' $ \toPtr -> copyBytes toPtr (ptr' `plusPtr` 5) sz'
|
||||||
contents `shouldBe` src
|
contents `shouldBe` src
|
||||||
sz' `shouldBe` fromIntegral len
|
sz' `shouldBe` fromIntegral len
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user