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 showChooseLAMode = false
|
||||
@State private var showSetPasscode = false
|
||||
@State private var waitingForOrPassedAuth = true
|
||||
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
||||
|
||||
private enum ChatListActionSheet: Identifiable {
|
||||
@@ -61,6 +62,10 @@ struct ContentView: View {
|
||||
}
|
||||
if !showSettings, let la = chatModel.laRequest {
|
||||
LocalAuthView(authRequest: la)
|
||||
.onDisappear {
|
||||
// this flag is separate from accessAuthenticated to show initializationView while we wait for authentication
|
||||
waitingForOrPassedAuth = accessAuthenticated
|
||||
}
|
||||
} else if showSetPasscode {
|
||||
SetAppPasscodeView {
|
||||
chatModel.contentViewAccessAuthenticated = true
|
||||
@@ -73,8 +78,7 @@ struct ContentView: View {
|
||||
showSetPasscode = false
|
||||
alertManager.showAlert(laPasscodeNotSetAlert())
|
||||
}
|
||||
}
|
||||
if chatModel.chatDbStatus == nil {
|
||||
} else if chatModel.chatDbStatus == nil && AppChatState.shared.value != .stopped && waitingForOrPassedAuth {
|
||||
initializationView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var chatDbChanged = false
|
||||
@Published var chatDbEncrypted: Bool?
|
||||
@Published var chatDbStatus: DBMigrationResult?
|
||||
@Published var ctrlInitInProgress: Bool = false
|
||||
// local authentication
|
||||
@Published var contentViewAccessAuthenticated: Bool = false
|
||||
@Published var laRequest: LocalAuthRequest?
|
||||
|
||||
@@ -16,13 +16,13 @@ private var nseSubscribers: [UUID:NSESubscriber] = [:]
|
||||
private let SUSPENDING_TIMEOUT: TimeInterval = 2
|
||||
|
||||
// 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 {
|
||||
logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)")
|
||||
}
|
||||
var state = nseStateGroupDefault.get()
|
||||
if case .suspended = state {
|
||||
dispatchQueue.async { suspended(true) }
|
||||
DispatchQueue.main.async { suspended(true) }
|
||||
return
|
||||
}
|
||||
let id = UUID()
|
||||
@@ -45,7 +45,7 @@ func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = Disp
|
||||
logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))")
|
||||
suspendedCalled = true
|
||||
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 {
|
||||
let r = chatSendCmdSync(.startChat(subscribe: true, expire: true, xftp: true))
|
||||
let r = chatSendCmdSync(.startChat(mainApp: true))
|
||||
switch r {
|
||||
case .chatStarted: return true
|
||||
case .chatRunning: return false
|
||||
@@ -403,7 +403,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
|
||||
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
|
||||
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
|
||||
default:
|
||||
logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)")
|
||||
logger.debug("apiGetNtfToken response: \(String(describing: r))")
|
||||
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 {
|
||||
logger.debug("initializeChat")
|
||||
let m = ChatModel.shared
|
||||
m.ctrlInitInProgress = true
|
||||
defer { m.ctrlInitInProgress = false }
|
||||
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations)
|
||||
if m.chatDbStatus != .ok { return }
|
||||
// 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 nseSuspendTimeout: TimeInterval = 5
|
||||
|
||||
private func _suspendChat(timeout: Int) {
|
||||
// this is a redundant check to prevent logical errors, like the one fixed in this PR
|
||||
let state = AppChatState.shared.value
|
||||
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 {
|
||||
AppChatState.shared.set(.suspending)
|
||||
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")
|
||||
if ChatModel.shared.chatRunning == true {
|
||||
ChatReceiver.shared.start()
|
||||
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
|
||||
}
|
||||
if .active == AppChatState.shared.value {
|
||||
if case .active = AppChatState.shared.value {
|
||||
completion()
|
||||
} else if nseStateGroupDefault.get().inactive {
|
||||
activate()
|
||||
} else {
|
||||
// setting app state to "activating" to notify NSE that it should suspend
|
||||
setAppState(.activating)
|
||||
waitNSESuspended(timeout: 10, dispatchQueue: dispatchQueue) { ok in
|
||||
waitNSESuspended(timeout: nseSuspendTimeout) { ok in
|
||||
if !ok {
|
||||
// if for some reason NSE failed to suspend,
|
||||
// e.g., it crashed previously without setting its state to "suspended",
|
||||
|
||||
@@ -44,8 +44,10 @@ struct SimpleXApp: App {
|
||||
chatModel.appOpenUrl = url
|
||||
}
|
||||
.onAppear() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||||
initChatAndMigrate()
|
||||
if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||||
initChatAndMigrate()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { phase in
|
||||
@@ -98,12 +100,12 @@ struct SimpleXApp: App {
|
||||
if legacyDatabase, case .documents = dbContainerGroupDefault.get() {
|
||||
dbContainerGroupDefault.set(.documents)
|
||||
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 {
|
||||
dbContainerGroupDefault.set(.group)
|
||||
setMigrationState(.ready)
|
||||
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db")
|
||||
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present")
|
||||
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath())*.db")
|
||||
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not") present")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ struct ActiveCallView: View {
|
||||
}
|
||||
}
|
||||
.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)
|
||||
createWebRTCClient()
|
||||
dismissAllSheets()
|
||||
}
|
||||
.onChange(of: canConnectCall) { _ in
|
||||
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall, privacy: .public)")
|
||||
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
|
||||
createWebRTCClient()
|
||||
}
|
||||
.onDisappear {
|
||||
|
||||
@@ -130,7 +130,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
// The delay allows to accept the second call before suspending a chat
|
||||
// see `.onChange(of: scenePhase)` in SimpleXApp
|
||||
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 {
|
||||
self?.shouldSuspendChat = false
|
||||
suspendChat()
|
||||
@@ -142,45 +142,57 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
@objc(pushRegistry:didUpdatePushCredentials:forType:)
|
||||
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) {
|
||||
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 {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
logger.debug("CallController: initializing chat")
|
||||
if (!ChatModel.shared.chatInitialized) {
|
||||
initChatAndMigrate(refreshInvitations: false)
|
||||
if AppChatState.shared.value == .stopped {
|
||||
self.reportExpiredCall(payload: payload, completion)
|
||||
return
|
||||
}
|
||||
startChatAndActivate(dispatchQueue: DispatchQueue.global()) {
|
||||
self.shouldSuspendChat = true
|
||||
// There are no invitations in the model, as it was processed by NSE
|
||||
_ = try? justRefreshCallInvitations()
|
||||
// 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] {
|
||||
if (!ChatModel.shared.chatInitialized) {
|
||||
logger.debug("CallController: initializing chat")
|
||||
do {
|
||||
try initializeChat(start: true, refreshInvitations: false)
|
||||
} catch let error {
|
||||
logger.error("CallController: initializing chat error: \(error)")
|
||||
self.reportExpiredCall(payload: payload, completion)
|
||||
return
|
||||
}
|
||||
}
|
||||
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)
|
||||
if let uuid = invitation.callkitUUID {
|
||||
logger.debug("CallController: report pushkit call via CallKit")
|
||||
let update = self.cxCallUpdate(invitation: invitation)
|
||||
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()
|
||||
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
||||
if error != nil {
|
||||
m.callInvitations.removeValue(forKey: contactId)
|
||||
}
|
||||
} else {
|
||||
self.reportExpiredCall(update: update, completion)
|
||||
// Tell PushKit that the notification is handled.
|
||||
completion()
|
||||
}
|
||||
} 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) {
|
||||
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 invitation.callTs.timeIntervalSinceNow >= -180 {
|
||||
let update = cxCallUpdate(invitation: invitation)
|
||||
@@ -351,7 +363,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) {
|
||||
controller.request(CXTransaction(action: action)) { error in
|
||||
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 {
|
||||
logger.debug("CallController.requestTransaction requested transaction successfully")
|
||||
onSuccess()
|
||||
|
||||
@@ -484,6 +484,7 @@ func deleteChatAsync() async throws {
|
||||
try await apiDeleteStorage()
|
||||
_ = kcDatabasePassword.remove()
|
||||
storeDBPassphraseGroupDefault.set(true)
|
||||
deleteAppDatabaseAndFiles()
|
||||
}
|
||||
|
||||
struct DatabaseView_Previews: PreviewProvider {
|
||||
|
||||
@@ -13,19 +13,28 @@ struct LocalAuthView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
var authRequest: LocalAuthRequest
|
||||
@State private var password = ""
|
||||
@State private var allowToReact = true
|
||||
|
||||
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 {
|
||||
allowToReact = false
|
||||
deleteStorageAndRestart(sdPassword) { r in
|
||||
m.laRequest = nil
|
||||
authRequest.completed(r)
|
||||
}
|
||||
return
|
||||
}
|
||||
let r: LAResult = password == authRequest.password
|
||||
? .success
|
||||
: .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
|
||||
let r: LAResult
|
||||
if password == authRequest.password {
|
||||
if authRequest.selfDestruct && kcSelfDestructPassword.get() != nil && !m.chatInitialized {
|
||||
initChatAndMigrate()
|
||||
}
|
||||
r = .success
|
||||
} else {
|
||||
r = .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
|
||||
}
|
||||
m.laRequest = nil
|
||||
authRequest.completed(r)
|
||||
} cancel: {
|
||||
@@ -37,8 +46,27 @@ struct LocalAuthView: View {
|
||||
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
|
||||
Task {
|
||||
do {
|
||||
try await stopChatAsync()
|
||||
try await deleteChatAsync()
|
||||
/** Waiting until [initializeChat] finishes */
|
||||
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)
|
||||
_ = kcSelfDestructPassword.remove()
|
||||
await NtfManager.shared.removeAllNotifications()
|
||||
@@ -53,7 +81,7 @@ struct LocalAuthView: View {
|
||||
try initializeChat(start: true)
|
||||
m.chatDbChanged = false
|
||||
AppChatState.shared.set(.active)
|
||||
if m.currentUser != nil { return }
|
||||
if m.currentUser != nil || !m.chatInitialized { return }
|
||||
var profile: Profile? = nil
|
||||
if let displayName = displayName, displayName != "" {
|
||||
profile = Profile(displayName: displayName, fullName: "")
|
||||
|
||||
@@ -14,6 +14,8 @@ struct PasscodeView: View {
|
||||
var reason: String? = nil
|
||||
var submitLabel: LocalizedStringKey
|
||||
var submitEnabled: ((String) -> Bool)?
|
||||
@Binding var buttonsEnabled: Bool
|
||||
|
||||
var submit: () -> Void
|
||||
var cancel: () -> Void
|
||||
|
||||
@@ -70,11 +72,11 @@ struct PasscodeView: View {
|
||||
@ViewBuilder private func buttonsView() -> some View {
|
||||
Button(action: cancel) {
|
||||
Label("Cancel", systemImage: "multiply")
|
||||
}
|
||||
}.disabled(!buttonsEnabled)
|
||||
Button(action: submit) {
|
||||
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",
|
||||
reason: "Unlock app",
|
||||
submitLabel: "Submit",
|
||||
buttonsEnabled: Binding.constant(true),
|
||||
submit: {},
|
||||
cancel: {}
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct SetAppPasscodeView: View {
|
||||
var passcodeKeychain: KeyChainItem = kcAppPassword
|
||||
var prohibitedPasscodeKeychain: KeyChainItem = kcSelfDestructPassword
|
||||
var title: LocalizedStringKey = "New Passcode"
|
||||
var reason: String?
|
||||
var submit: () -> Void
|
||||
@@ -41,7 +42,10 @@ struct SetAppPasscodeView: View {
|
||||
}
|
||||
}
|
||||
} 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
|
||||
passcode = ""
|
||||
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 {
|
||||
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()
|
||||
cancel()
|
||||
}
|
||||
|
||||
@@ -11,12 +11,14 @@ import SimpleXChat
|
||||
|
||||
enum UserProfileAlert: Identifiable {
|
||||
case duplicateUserError
|
||||
case invalidDisplayNameError
|
||||
case createUserError(error: LocalizedStringKey)
|
||||
case invalidNameError(validName: String)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .duplicateUserError: return "duplicateUserError"
|
||||
case .invalidDisplayNameError: return "invalidDisplayNameError"
|
||||
case .createUserError: return "createUserError"
|
||||
case let .invalidNameError(validName): return "invalidNameError \(validName)"
|
||||
}
|
||||
@@ -187,6 +189,12 @@ private func createProfile(_ displayName: String, showAlert: (UserProfileAlert)
|
||||
} else {
|
||||
showAlert(.duplicateUserError)
|
||||
}
|
||||
case .chatCmdError(_, .error(.invalidDisplayName)):
|
||||
if m.currentUser == nil {
|
||||
AlertManager.shared.showAlert(invalidDisplayNameAlert)
|
||||
} else {
|
||||
showAlert(.invalidDisplayNameError)
|
||||
}
|
||||
default:
|
||||
let err: LocalizedStringKey = "Error: \(responseError(error))"
|
||||
if m.currentUser == nil {
|
||||
@@ -207,6 +215,7 @@ private func canCreateProfile(_ displayName: String) -> Bool {
|
||||
func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding<String>) -> Alert {
|
||||
switch alert {
|
||||
case .duplicateUserError: return duplicateUserAlert
|
||||
case .invalidDisplayNameError: return invalidDisplayNameAlert
|
||||
case let .createUserError(err): return creatUserErrorAlert(err)
|
||||
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 {
|
||||
Alert(
|
||||
title: Text("Error creating profile!"),
|
||||
|
||||
@@ -491,14 +491,23 @@ struct SimplexLockView: View {
|
||||
showLAAlert(.laPasscodeNotChangedAlert)
|
||||
}
|
||||
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()
|
||||
showLAAlert(.laSelfDestructPasscodeSetAlert)
|
||||
} cancel: {
|
||||
revertSelfDestruct()
|
||||
}
|
||||
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)
|
||||
} cancel: {
|
||||
showLAAlert(.laPasscodeNotChangedAlert)
|
||||
|
||||
@@ -16,9 +16,11 @@ let logger = Logger()
|
||||
|
||||
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>
|
||||
|
||||
@@ -32,7 +34,7 @@ actor PendingNtfs {
|
||||
private var ntfStreams: [String: NtfStream] = [:]
|
||||
|
||||
func createStream(_ id: String) async {
|
||||
logger.debug("NotificationService PendingNtfs.createStream: \(id, privacy: .public)")
|
||||
logger.debug("NotificationService PendingNtfs.createStream: \(id)")
|
||||
if ntfStreams[id] == nil {
|
||||
ntfStreams[id] = ConcurrentQueue()
|
||||
logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue")
|
||||
@@ -40,14 +42,14 @@ actor PendingNtfs {
|
||||
}
|
||||
|
||||
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 {
|
||||
nse.setBestAttemptNtf(.empty)
|
||||
}
|
||||
if let s = ntfStreams[id] {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: has stream")
|
||||
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 dequeued: DequeueElement<NSENotification>?
|
||||
nse.cancelRead = {
|
||||
@@ -66,7 +68,7 @@ actor PendingNtfs {
|
||||
} else if case let .msgInfo(info) = ntf {
|
||||
let found = expected.remove(info.msgId)
|
||||
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 }
|
||||
} else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo")
|
||||
@@ -88,7 +90,7 @@ actor PendingNtfs {
|
||||
}
|
||||
|
||||
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] {
|
||||
logger.debug("NotificationService PendingNtfs.writeStream: writing ntf")
|
||||
s.enqueue(ntf)
|
||||
@@ -208,7 +210,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
self.contentHandler = contentHandler
|
||||
registerGroupDefaults()
|
||||
let appState = appStateGroupDefault.get()
|
||||
logger.debug("NotificationService: app is \(appState.rawValue, privacy: .public)")
|
||||
logger.debug("NotificationService: app is \(appState.rawValue)")
|
||||
switch appState {
|
||||
case .stopped:
|
||||
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 {
|
||||
receiveNtfMessages(request, contentHandler)
|
||||
} else {
|
||||
@@ -267,7 +269,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
let dbStatus = startChat()
|
||||
if case .ok = dbStatus,
|
||||
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_ {
|
||||
setBestAttemptNtf(
|
||||
ntfInfo.ntfsEnabled
|
||||
@@ -279,7 +281,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
NtfStreamSemaphores.shared.waitForStream(id)
|
||||
if receiveEntityId != nil {
|
||||
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.readStream(id, for: self, ntfInfo: ntfInfo)
|
||||
deliverBestAttemptNtf()
|
||||
@@ -297,7 +299,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
|
||||
deliverBestAttemptNtf()
|
||||
deliverBestAttemptNtf(urgent: true)
|
||||
}
|
||||
|
||||
func setBadgeCount() {
|
||||
@@ -319,7 +321,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
}
|
||||
|
||||
private func deliverBestAttemptNtf() {
|
||||
private func deliverBestAttemptNtf(urgent: Bool = false) {
|
||||
logger.debug("NotificationService.deliverBestAttemptNtf")
|
||||
if let cancel = cancelRead {
|
||||
cancelRead = nil
|
||||
@@ -329,20 +331,55 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
receiveEntityId = nil
|
||||
NtfStreamSemaphores.shared.signalStreamReady(id)
|
||||
}
|
||||
let suspend: Bool
|
||||
if let t = threadId {
|
||||
threadId = nil
|
||||
if NSEThreads.shared.endThread(t) {
|
||||
logger.debug("NotificationService.deliverBestAttemptNtf: will suspend")
|
||||
// suspension is delayed to allow chat core finalise any processing
|
||||
// (e.g., send delivery receipts)
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendDelay) {
|
||||
if NSEThreads.shared.noThreads {
|
||||
logger.debug("NotificationService.deliverBestAttemptNtf: suspending...")
|
||||
suspendChat(nseSuspendTimeout)
|
||||
suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads
|
||||
} else {
|
||||
suspend = false
|
||||
}
|
||||
deliverCallkitOrNotification(urgent: urgent, suspend: suspend)
|
||||
}
|
||||
|
||||
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 {
|
||||
contentHandler = nil
|
||||
bestAttemptNtf = nil
|
||||
@@ -357,17 +394,14 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
switch ntf {
|
||||
case let .nse(content): deliver(content)
|
||||
case let .callkit(invitation):
|
||||
logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)")
|
||||
CXProvider.reportNewIncomingVoIPPushPayload([
|
||||
"displayName": invitation.contact.displayName,
|
||||
"contactId": invitation.contact.id,
|
||||
"media": invitation.callType.media.rawValue
|
||||
]) { error in
|
||||
if error == nil {
|
||||
deliver(nil)
|
||||
} else {
|
||||
logger.debug("NotificationService reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
|
||||
deliver(createCallInvitationNtf(invitation))
|
||||
}
|
||||
logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
|
||||
deliver(error == nil ? nil : createCallInvitationNtf(invitation))
|
||||
}
|
||||
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
|
||||
@@ -402,14 +436,14 @@ var appSubscriber: AppSubscriber = appStateSubscriber { state in
|
||||
logger.debug("NotificationService: appSubscriber")
|
||||
if state.running && NSEChatState.shared.value.canSuspend {
|
||||
logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending")
|
||||
suspendChat(nseSuspendTimeout)
|
||||
suspendChat(fastNSESuspendSchedule.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber {
|
||||
appMessageSubscriber { msg in
|
||||
if case let .state(state) = msg {
|
||||
logger.debug("NotificationService: appStateSubscriber \(state.rawValue, privacy: .public)")
|
||||
logger.debug("NotificationService: appStateSubscriber \(state.rawValue)")
|
||||
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
|
||||
func startChat() -> DBMigrationResult? {
|
||||
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()
|
||||
defer { startLock.signal() }
|
||||
|
||||
return switch NSEChatState.shared.value {
|
||||
case .created: doStartChat()
|
||||
case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock
|
||||
case .active: .ok
|
||||
case .suspending: activateChat()
|
||||
case .suspended: activateChat()
|
||||
if hasChatCtrl() {
|
||||
return switch NSEChatState.shared.value {
|
||||
case .created: doStartChat()
|
||||
case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock
|
||||
case .active: .ok
|
||||
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? {
|
||||
logger.debug("NotificationService: doStartChat")
|
||||
hs_init(0, nil)
|
||||
haskell_init_nse()
|
||||
let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true)
|
||||
logger.debug("NotificationService: doStartChat \(String(describing: dbStatus))")
|
||||
if dbStatus != .ok {
|
||||
resetChatCtrl()
|
||||
NSEChatState.shared.set(.created)
|
||||
@@ -477,7 +520,7 @@ func doStartChat() -> DBMigrationResult? {
|
||||
return .ok
|
||||
}
|
||||
} catch {
|
||||
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
|
||||
logger.error("NotificationService startChat error: \(responseError(error))")
|
||||
}
|
||||
} else {
|
||||
logger.debug("NotificationService: no active user")
|
||||
@@ -504,8 +547,10 @@ func suspendChat(_ timeout: Int) {
|
||||
logger.debug("NotificationService: suspendChat")
|
||||
let state = NSEChatState.shared.value
|
||||
if !state.canSuspend {
|
||||
logger.error("NotificationService suspendChat called, current state: \(state.rawValue, privacy: .public)")
|
||||
} else {
|
||||
logger.error("NotificationService suspendChat called, current state: \(state.rawValue)")
|
||||
} 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()
|
||||
defer { suspendLock.signal() }
|
||||
|
||||
@@ -571,7 +616,7 @@ private let isInChina = SKStorefront().countryCode == "CHN"
|
||||
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
|
||||
|
||||
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
||||
logger.debug("NotificationService receivedMsgNtf: \(res.responseType, privacy: .public)")
|
||||
logger.debug("NotificationService receivedMsgNtf: \(res.responseType)")
|
||||
switch res {
|
||||
case let .contactConnected(user, contact, _):
|
||||
return (contact.id, .nse(createContactConnectedNtf(user, contact)))
|
||||
@@ -613,6 +658,9 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
||||
case .chatSuspended:
|
||||
chatSuspended()
|
||||
return nil
|
||||
case let .chatError(_, err):
|
||||
logger.error("NotificationService receivedMsgNtf error: \(String(describing: err))")
|
||||
return nil
|
||||
default:
|
||||
logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)")
|
||||
return nil
|
||||
@@ -627,17 +675,22 @@ func updateNetCfg() {
|
||||
try setNetworkConfig(networkConfig)
|
||||
networkConfig = newNetConfig
|
||||
} 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? {
|
||||
let r = sendSimpleXCmd(.showActiveUser)
|
||||
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(String(describing: r))")
|
||||
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)")
|
||||
switch r {
|
||||
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:
|
||||
logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))")
|
||||
return nil
|
||||
@@ -645,7 +698,7 @@ func apiGetActiveUser() -> User? {
|
||||
}
|
||||
|
||||
func apiStartChat() throws -> Bool {
|
||||
let r = sendSimpleXCmd(.startChat(subscribe: false, expire: false, xftp: false))
|
||||
let r = sendSimpleXCmd(.startChat(mainApp: false))
|
||||
switch r {
|
||||
case .chatStarted: return true
|
||||
case .chatRunning: return false
|
||||
@@ -699,11 +752,12 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
||||
}
|
||||
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||
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)
|
||||
} else if case let .chatCmdError(_, error) = r {
|
||||
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
|
||||
} 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
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@
|
||||
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
|
||||
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.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 */; };
|
||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.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 */; };
|
||||
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.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 */; };
|
||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -289,11 +294,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -511,13 +511,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
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 */,
|
||||
5C4E80E62B40A96C0080FAE2 /* libgmp.a in Frameworks */,
|
||||
5C245F1A2B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a 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;
|
||||
};
|
||||
@@ -579,11 +579,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C4E80E32B40A96C0080FAE2 /* libffi.a */,
|
||||
5C4E80E12B40A96C0080FAE2 /* libgmp.a */,
|
||||
5C4E80E22B40A96C0080FAE2 /* libgmpxx.a */,
|
||||
5C4E80E02B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a */,
|
||||
5C4E80DF2B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a */,
|
||||
5C245F172B4DB982001CC39F /* libffi.a */,
|
||||
5C245F162B4DB982001CC39F /* libgmp.a */,
|
||||
5C245F142B4DB982001CC39F /* libgmpxx.a */,
|
||||
5C245F152B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a */,
|
||||
5C245F182B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1400"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
@@ -47,16 +47,14 @@
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
askForAppToLaunch = "Yes"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
|
||||
@@ -12,7 +12,11 @@ private var chatController: chat_ctrl?
|
||||
|
||||
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 }
|
||||
fatalError("chat controller not initialized")
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public enum ChatCommand {
|
||||
case apiMuteUser(userId: Int64)
|
||||
case apiUnmuteUser(userId: Int64)
|
||||
case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?)
|
||||
case startChat(subscribe: Bool, expire: Bool, xftp: Bool)
|
||||
case startChat(mainApp: Bool)
|
||||
case apiStopChat
|
||||
case apiActivateChat(restoreChat: Bool)
|
||||
case apiSuspendChat(timeoutMicroseconds: Int)
|
||||
@@ -154,7 +154,7 @@ public enum ChatCommand {
|
||||
case let .apiMuteUser(userId): return "/_mute user \(userId)"
|
||||
case let .apiUnmuteUser(userId): return "/_unmute user \(userId)"
|
||||
case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))"
|
||||
case let .startChat(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 let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
|
||||
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
|
||||
@@ -1610,6 +1610,7 @@ public enum ChatErrorType: Decodable {
|
||||
case userUnknown
|
||||
case activeUserExists
|
||||
case userExists
|
||||
case invalidDisplayName
|
||||
case differentActiveUser(commandUserId: Int64, activeUserId: Int64)
|
||||
case cantDeleteActiveUser(userId: Int64)
|
||||
case cantDeleteLastUser(userId: Int64)
|
||||
|
||||
@@ -172,7 +172,6 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile {
|
||||
}
|
||||
|
||||
public struct UserProfileUpdateSummary: Decodable {
|
||||
public var notChanged: Int
|
||||
public var updateSuccesses: Int
|
||||
public var updateFailures: Int
|
||||
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() {
|
||||
let fm = FileManager.default
|
||||
do {
|
||||
let fileNames = try fm.contentsOfDirectory(atPath: getAppFilesDirectory().path)
|
||||
for fileName in fileNames {
|
||||
removeFile(fileName)
|
||||
}
|
||||
try fm.removeItem(at: getAppFilesDirectory())
|
||||
try fm.createDirectory(at: getAppFilesDirectory(), withIntermediateDirectories: true)
|
||||
} catch {
|
||||
logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
@@ -23,3 +23,19 @@ void haskell_init(void) {
|
||||
char **pargv = argv;
|
||||
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_nse(void);
|
||||
|
||||
#endif /* hs_init_h */
|
||||
|
||||
@@ -3,11 +3,12 @@ package chat.simplex.app
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
|
||||
import chat.simplex.common.model.ChatController
|
||||
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 java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -57,6 +58,10 @@ class MessagesFetcherWork(
|
||||
val durationSeconds = inputData.getInt(INPUT_DATA_DURATION, 60)
|
||||
var shouldReschedule = true
|
||||
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) {
|
||||
val chatController = ChatController
|
||||
SimplexService.waitDbMigrationEnds(chatController)
|
||||
|
||||
@@ -26,6 +26,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
const val TAG = "SIMPLEX"
|
||||
|
||||
@@ -46,8 +47,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
try {
|
||||
Looper.loop()
|
||||
} catch (e: Throwable) {
|
||||
if (e.message != null && e.message!!.startsWith("Unable to start activity")) {
|
||||
android.os.Process.killProcess(android.os.Process.myPid())
|
||||
if (e is UnsatisfiedLinkError || e.message?.startsWith("Unable to start activity") == true) {
|
||||
Process.killProcess(Process.myPid())
|
||||
break
|
||||
} else {
|
||||
// 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.mkdir()
|
||||
|
||||
initChatControllerAndRunMigrations(false)
|
||||
if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||
initChatControllerAndRunMigrations()
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
||||
}
|
||||
|
||||
@@ -77,7 +80,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
updatingChatsMutex.withLock {
|
||||
kotlin.runCatching {
|
||||
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 */
|
||||
if (chatModel.currentUser.value?.userId == currentUserId) {
|
||||
val currentChatId = chatModel.chatId.value
|
||||
@@ -171,13 +174,14 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
androidAppContext = this
|
||||
APPLICATION_ID = BuildConfig.APPLICATION_ID
|
||||
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 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 androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert()
|
||||
override fun cancelCallNotification() = NtfManager.cancelCallNotification()
|
||||
override fun cancelAllNotifications() = NtfManager.cancelAllNotifications()
|
||||
override fun showMessage(title: String, text: String) = NtfManager.showMessage(title, text)
|
||||
}
|
||||
platform = object : PlatformInterface {
|
||||
override suspend fun androidServiceStart() {
|
||||
|
||||
@@ -72,6 +72,10 @@ class SimplexService: Service() {
|
||||
stopSelf()
|
||||
} else {
|
||||
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"
|
||||
|
||||
// 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 RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
|
||||
const val CallNotificationId: Int = -1
|
||||
@@ -59,7 +59,7 @@ object NtfManager {
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
.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")
|
||||
callChannel.setSound(soundUri, attrs)
|
||||
callChannel.enableVibration(true)
|
||||
@@ -140,7 +140,7 @@ object NtfManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyCallInvitation(invitation: RcvCallInvitation) {
|
||||
fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean {
|
||||
val keyguardManager = getKeyguardManager(context)
|
||||
Log.d(
|
||||
TAG,
|
||||
@@ -149,7 +149,7 @@ object NtfManager {
|
||||
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " +
|
||||
"onForeground ${isAppOnForeground}"
|
||||
)
|
||||
if (isAppOnForeground) return
|
||||
if (isAppOnForeground) return false
|
||||
val contactId = invitation.contact.id
|
||||
Log.d(TAG, "notifyCallInvitation $contactId")
|
||||
val image = invitation.contact.image
|
||||
@@ -163,7 +163,7 @@ object NtfManager {
|
||||
.setFullScreenIntent(fullScreenPendingIntent, true)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
} 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)
|
||||
NotificationCompat.Builder(context, CallChannel)
|
||||
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id))
|
||||
@@ -206,6 +206,39 @@ object NtfManager {
|
||||
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() {
|
||||
@@ -248,6 +281,7 @@ object NtfManager {
|
||||
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(MR.strings.ntf_channel_calls)))
|
||||
// Remove old channels since they can't be edited
|
||||
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
|
||||
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION_1")
|
||||
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -97,7 +97,8 @@ fun IncomingCallActivityView(m: ChatModel) {
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background
|
||||
color = MaterialTheme.colors.background,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
if (showCallView) {
|
||||
Box {
|
||||
@@ -200,7 +201,8 @@ private fun SimpleXLogo() {
|
||||
private fun LockScreenCallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
color = Color.Transparent
|
||||
color = Color.Transparent,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -227,7 +229,8 @@ fun PreviewIncomingCallLockScreenAlert() {
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background
|
||||
color = MaterialTheme.colors.background,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
IncomingCallLockScreenAlertLayout(
|
||||
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)
|
||||
shutdownApp()
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ data class SettingsViewState(
|
||||
fun AppScreen() {
|
||||
SimpleXTheme {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ fun MainScreen() {
|
||||
|
||||
@Composable
|
||||
fun AuthView() {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package chat.simplex.common
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.common.model.*
|
||||
@@ -107,7 +106,7 @@ object AppLock {
|
||||
private fun setPasscode() {
|
||||
val appPrefs = ChatController.appPrefs
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
ChatModel.performLA.value = true
|
||||
|
||||
+3
-1
@@ -125,6 +125,9 @@ object ChatModel {
|
||||
val remoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(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) {
|
||||
currentUser.value
|
||||
} else {
|
||||
@@ -1151,7 +1154,6 @@ data class LocalProfile(
|
||||
|
||||
@Serializable
|
||||
data class UserProfileUpdateSummary(
|
||||
val notChanged: Int,
|
||||
val updateSuccesses: Int,
|
||||
val updateFailures: Int,
|
||||
val changedContacts: List<Contact>
|
||||
|
||||
+111
-6
@@ -108,6 +108,7 @@ class AppPreferences {
|
||||
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
|
||||
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, 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 networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
|
||||
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_STOPPED = "ChatStopped"
|
||||
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_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
|
||||
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
|
||||
) {
|
||||
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 {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_title), r.details)
|
||||
}
|
||||
@@ -572,7 +578,7 @@ object ChatController {
|
||||
}
|
||||
|
||||
suspend fun apiStartChat(): Boolean {
|
||||
val r = sendCmd(null, CC.StartChat(expire = true))
|
||||
val r = sendCmd(null, CC.StartChat(mainApp = true))
|
||||
when (r) {
|
||||
is CR.ChatStarted -> return true
|
||||
is CR.ChatRunning -> return false
|
||||
@@ -648,6 +654,15 @@ object ChatController {
|
||||
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? {
|
||||
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)
|
||||
@@ -1122,6 +1137,13 @@ object ChatController {
|
||||
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 {
|
||||
val r = sendCmd(rh, CC.ApiSendCallInvitation(contact, callType))
|
||||
return r is CR.CmdOk
|
||||
@@ -1878,9 +1900,34 @@ object ChatController {
|
||||
val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ }
|
||||
chatModel.remoteHostPairing.value = null
|
||||
if (disconnectedHost != null) {
|
||||
showToast(
|
||||
generalGetString(MR.strings.remote_host_was_disconnected_toast).format(disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() })
|
||||
)
|
||||
val deviceName = 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_) {
|
||||
chatModel.currentRemoteHost.value = null
|
||||
@@ -1911,11 +1958,40 @@ object ChatController {
|
||||
val sess = chatModel.remoteCtrlSession.value
|
||||
if (sess != 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) {
|
||||
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 ->
|
||||
Log.d(TAG , "unsupported event: ${r.responseType}")
|
||||
}
|
||||
@@ -2165,7 +2241,7 @@ sealed class CC {
|
||||
class ApiMuteUser(val userId: Long): CC()
|
||||
class ApiUnmuteUser(val userId: Long): 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 SetTempFolder(val tempFolder: String): CC()
|
||||
class SetFilesFolder(val filesFolder: String): CC()
|
||||
@@ -2236,6 +2312,7 @@ sealed class CC {
|
||||
class ApiShowMyAddress(val userId: Long): CC()
|
||||
class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC()
|
||||
class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC()
|
||||
class ApiGetCallInvitations: CC()
|
||||
class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC()
|
||||
class ApiRejectCall(val contact: Contact): CC()
|
||||
class ApiSendCallOffer(val contact: Contact, val callOffer: WebRTCCallOffer): CC()
|
||||
@@ -2292,7 +2369,7 @@ sealed class CC {
|
||||
is ApiMuteUser -> "/_mute user $userId"
|
||||
is ApiUnmuteUser -> "/_unmute user $userId"
|
||||
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 SetTempFolder -> "/_temp_folder $tempFolder"
|
||||
is SetFilesFolder -> "/_files_folder $filesFolder"
|
||||
@@ -2372,6 +2449,7 @@ sealed class CC {
|
||||
is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}"
|
||||
is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId"
|
||||
is ApiRejectContact -> "/_reject $contactReqId"
|
||||
is ApiGetCallInvitations -> "/_call get"
|
||||
is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}"
|
||||
is ApiRejectCall -> "/_call reject @${contact.apiId}"
|
||||
is ApiSendCallOffer -> "/_call offer @${contact.apiId} ${json.encodeToString(callOffer)}"
|
||||
@@ -2495,6 +2573,7 @@ sealed class CC {
|
||||
is ApiAddressAutoAccept -> "apiAddressAutoAccept"
|
||||
is ApiAcceptContact -> "apiAcceptContact"
|
||||
is ApiRejectContact -> "apiRejectContact"
|
||||
is ApiGetCallInvitations -> "apiGetCallInvitations"
|
||||
is ApiSendCallInvitation -> "apiSendCallInvitation"
|
||||
is ApiRejectCall -> "apiRejectCall"
|
||||
is ApiSendCallOffer -> "apiSendCallOffer"
|
||||
@@ -3870,6 +3949,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
// call events
|
||||
@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("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()
|
||||
@@ -4017,6 +4097,7 @@ sealed class CR {
|
||||
is SndFileProgressXFTP -> "sndFileProgressXFTP"
|
||||
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
|
||||
is SndFileError -> "sndFileError"
|
||||
is CallInvitations -> "callInvitations"
|
||||
is CallInvitation -> "callInvitation"
|
||||
is CallOffer -> "callOffer"
|
||||
is CallAnswer -> "callAnswer"
|
||||
@@ -4163,6 +4244,7 @@ sealed class CR {
|
||||
is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize")
|
||||
is SndFileCompleteXFTP -> 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 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)}")
|
||||
@@ -4437,6 +4519,7 @@ sealed class ChatErrorType {
|
||||
is EmptyUserPassword -> "emptyUserPassword"
|
||||
is UserAlreadyHidden -> "userAlreadyHidden"
|
||||
is UserNotHidden -> "userNotHidden"
|
||||
is InvalidDisplayName -> "invalidDisplayName"
|
||||
is ChatNotStarted -> "chatNotStarted"
|
||||
is ChatNotStopped -> "chatNotStopped"
|
||||
is ChatStoreChanged -> "chatStoreChanged"
|
||||
@@ -4514,6 +4597,7 @@ sealed class ChatErrorType {
|
||||
@Serializable @SerialName("emptyUserPassword") class EmptyUserPassword(val userId: Long): ChatErrorType()
|
||||
@Serializable @SerialName("userAlreadyHidden") class UserAlreadyHidden(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("chatNotStopped") object ChatNotStopped: ChatErrorType()
|
||||
@Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType()
|
||||
@@ -4731,6 +4815,7 @@ sealed class AgentErrorType {
|
||||
is AGENT -> "AGENT ${agentErr.string}"
|
||||
is INTERNAL -> "INTERNAL $internalErr"
|
||||
is INACTIVE -> "INACTIVE"
|
||||
is CRITICAL -> "CRITICAL $offerRestart $criticalErr"
|
||||
}
|
||||
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): 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("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType()
|
||||
@Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType()
|
||||
@Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -4961,6 +5047,15 @@ sealed class RemoteHostError {
|
||||
is BadVersion -> "badVersion"
|
||||
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("inactive") object Inactive: RemoteHostError()
|
||||
@Serializable @SerialName("busy") object Busy: RemoteHostError()
|
||||
@@ -4981,6 +5076,16 @@ sealed class RemoteCtrlError {
|
||||
is BadInvitation -> "badInvitation"
|
||||
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("badState") object BadState: RemoteCtrlError()
|
||||
@Serializable @SerialName("busy") object Busy: RemoteCtrlError()
|
||||
|
||||
+8
-9
@@ -41,21 +41,20 @@ val appPreferences: AppPreferences
|
||||
|
||||
val chatController: ChatController = ChatController
|
||||
|
||||
fun initChatControllerAndRunMigrations(ignoreSelfDestruct: Boolean) {
|
||||
if (ignoreSelfDestruct || DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||
withBGApi {
|
||||
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
||||
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
||||
} else {
|
||||
initChatController()
|
||||
}
|
||||
runMigrations()
|
||||
fun initChatControllerAndRunMigrations() {
|
||||
withBGApi {
|
||||
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
||||
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
||||
} else {
|
||||
initChatController()
|
||||
}
|
||||
runMigrations()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred<Boolean> = { CompletableDeferred(true) }) {
|
||||
try {
|
||||
if (chatModel.ctrlInitInProgress.value) return
|
||||
chatModel.ctrlInitInProgress.value = true
|
||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||
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 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 cancelCallNotification()
|
||||
abstract fun cancelAllNotifications()
|
||||
abstract fun showMessage(title: String, text: String)
|
||||
// Android only
|
||||
abstract fun androidCreateNtfChannelsMaybeShowAlert()
|
||||
|
||||
|
||||
+4
-1
@@ -1,5 +1,7 @@
|
||||
package chat.simplex.common.ui.theme
|
||||
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple200 = Color(0xFFBB86FC)
|
||||
@@ -25,4 +27,5 @@ val WarningOrange = Color(255, 127, 0, 255)
|
||||
val WarningYellow = Color(255, 192, 0, 255)
|
||||
val FileLight = Color(183, 190, 199, 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()
|
||||
MaterialTheme(
|
||||
colors = theme.colors,
|
||||
typography = Typography.copy(
|
||||
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)
|
||||
),
|
||||
typography = Typography,
|
||||
shapes = Shapes,
|
||||
content = {
|
||||
ProvideTextStyle(
|
||||
value = TextStyle(color = theme.colors.onBackground),
|
||||
content = content
|
||||
)
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onBackground, content = content)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,8 +1,7 @@
|
||||
package chat.simplex.common.views
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@@ -11,7 +10,8 @@ fun SplashView() {
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background
|
||||
color = MaterialTheme.colors.background,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
// Image(
|
||||
// painter = painterResource(MR.images.logo),
|
||||
|
||||
+4
-1
@@ -101,13 +101,16 @@ fun TerminalLayout(
|
||||
)
|
||||
}
|
||||
},
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
modifier = Modifier.navigationBarsWithImePadding()
|
||||
) { contentPadding ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxWidth(),
|
||||
color = MaterialTheme.colors.background
|
||||
color = MaterialTheme.colors.background,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
TerminalLog()
|
||||
}
|
||||
|
||||
+1
-1
@@ -239,7 +239,7 @@ fun OnboardingButtons(displayName: MutableState<String>, close: () -> Unit) {
|
||||
val enabled = canCreateProfile(displayName.value)
|
||||
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
|
||||
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) {
|
||||
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)
|
||||
|
||||
+1
-1
@@ -13,8 +13,8 @@ class CallManager(val chatModel: ChatModel) {
|
||||
callInvitations[invitation.contact.id] = invitation
|
||||
if (invitation.user.showNotifications) {
|
||||
if (Clock.System.now() - invitation.callTs <= 3.minutes) {
|
||||
invitation.sentNotification = ntfManager.notifyCallInvitation(invitation)
|
||||
activeCallInvitation.value = invitation
|
||||
ntfManager.notifyCallInvitation(invitation)
|
||||
} else {
|
||||
val contact = invitation.contact
|
||||
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 androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.ProfileImage
|
||||
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 kotlinx.datetime.Clock
|
||||
|
||||
@@ -27,7 +26,11 @@ import kotlinx.datetime.Clock
|
||||
fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
||||
val cm = chatModel.callManager
|
||||
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() } }
|
||||
IncomingCallAlertLayout(
|
||||
invitation,
|
||||
@@ -85,7 +88,8 @@ fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
||||
private fun CallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
color = Color.Transparent
|
||||
color = Color.Transparent,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
|
||||
+3
@@ -112,6 +112,9 @@ sealed class WCallResponse {
|
||||
CallMediaType.Video -> MR.strings.incoming_video_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 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.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.group.*
|
||||
@@ -317,11 +318,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
},
|
||||
acceptCall = { contact ->
|
||||
hideKeyboard(view)
|
||||
val invitation = chatModel.callInvitations.remove(contact.id)
|
||||
if (invitation == null) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
||||
} else {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
withApi {
|
||||
val invitation = chatModel.callInvitations.remove(contact.id)
|
||||
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
|
||||
if (invitation == null) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
||||
} else {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
}
|
||||
},
|
||||
acceptFeature = { contact, feature, param ->
|
||||
@@ -570,6 +574,8 @@ fun ChatLayout(
|
||||
bottomBar = composeView,
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
floatingActionButton = { floatingButton.value() },
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
) { contentPadding ->
|
||||
BoxWithConstraints(Modifier
|
||||
.fillMaxHeight()
|
||||
|
||||
+2
-1
@@ -258,7 +258,8 @@ private fun CustomDisappearingMessageDialog(
|
||||
|
||||
DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
|
||||
+2
-1
@@ -131,7 +131,8 @@ fun CIFileView(
|
||||
Surface(
|
||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||
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))
|
||||
}
|
||||
|
||||
+1
@@ -88,6 +88,7 @@ fun CIGroupInvitationView(
|
||||
}) else Modifier,
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (sent) sentColor else receivedColor,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
|
||||
+2
@@ -142,6 +142,7 @@ fun DecryptionErrorItemFixButton(
|
||||
Modifier.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = receivedColor,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Box(
|
||||
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
||||
@@ -188,6 +189,7 @@ fun DecryptionErrorItem(
|
||||
Modifier.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = receivedColor,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Box(
|
||||
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
||||
|
||||
+4
-2
@@ -153,7 +153,8 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit,
|
||||
Surface(
|
||||
Modifier.align(Alignment.Center),
|
||||
color = Color.Black.copy(alpha = 0.25f),
|
||||
shape = RoundedCornerShape(percent = 50)
|
||||
shape = RoundedCornerShape(percent = 50),
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
@@ -264,7 +265,8 @@ private fun progressCircle(progress: Long, total: Long) {
|
||||
Surface(
|
||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||
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))
|
||||
}
|
||||
|
||||
+2
-1
@@ -225,7 +225,8 @@ private fun PlayPauseButton(
|
||||
Surface(
|
||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||
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(
|
||||
Modifier
|
||||
|
||||
+2
-2
@@ -613,7 +613,7 @@ private fun ShrinkItemAction(revealed: MutableState<Boolean>, showMenu: MutableS
|
||||
@Composable
|
||||
fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, onClick: () -> Unit) {
|
||||
val finalColor = if (color == Color.Unspecified) {
|
||||
if (isInDarkTheme()) MenuTextColorDark else Color.Black
|
||||
MenuTextColor
|
||||
} else color
|
||||
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -633,7 +633,7 @@ fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, on
|
||||
@Composable
|
||||
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) {
|
||||
val finalColor = if (color == Color.Unspecified) {
|
||||
if (isInDarkTheme()) MenuTextColorDark else Color.Black
|
||||
MenuTextColor
|
||||
} else color
|
||||
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (sent) sentColor else receivedColor,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Row(
|
||||
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),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = receivedColor,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (ci.chatDir.sent) sentColor else receivedColor,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Row(
|
||||
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)
|
||||
}
|
||||
},
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||
floatingActionButton = {
|
||||
|
||||
+2
@@ -30,6 +30,8 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
Scaffold(
|
||||
Modifier.padding(end = endPadding),
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
scaffoldState = scaffoldState,
|
||||
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.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
@@ -303,7 +302,7 @@ fun UserProfileRow(u: User) {
|
||||
u.displayName,
|
||||
modifier = Modifier
|
||||
.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
|
||||
)
|
||||
}
|
||||
@@ -346,7 +345,7 @@ fun RemoteHostRow(h: RemoteHostInfo) {
|
||||
Text(
|
||||
h.hostDeviceName,
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -387,7 +386,7 @@ fun LocalDeviceRow(active: Boolean) {
|
||||
Text(
|
||||
stringResource(MR.strings.this_device),
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -399,7 +398,7 @@ private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
|
||||
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)
|
||||
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)
|
||||
Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
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)
|
||||
Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
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)
|
||||
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
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)
|
||||
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
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)
|
||||
}
|
||||
|
||||
private fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
|
||||
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
|
||||
withApi {
|
||||
try {
|
||||
if (chatDbChanged.value) {
|
||||
@@ -406,6 +406,8 @@ private fun stopChatAlert(m: ChatModel) {
|
||||
)
|
||||
}
|
||||
|
||||
expect fun restartChatOrApp()
|
||||
|
||||
private fun exportProhibitedAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
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()) {
|
||||
authenticate(
|
||||
generalGetString(MR.strings.auth_stop_chat),
|
||||
@@ -421,7 +423,7 @@ private fun authStopChat(m: ChatModel) {
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success, is LAResult.Unavailable -> {
|
||||
stopChat(m)
|
||||
stopChat(m, onStop)
|
||||
}
|
||||
is LAResult.Error -> {
|
||||
m.chatRunning.value = true
|
||||
@@ -434,15 +436,16 @@ private fun authStopChat(m: ChatModel) {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
stopChat(m)
|
||||
stopChat(m, onStop)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopChat(m: ChatModel) {
|
||||
private fun stopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
|
||||
withApi {
|
||||
try {
|
||||
stopChatAsync(m)
|
||||
platform.androidChatStopped()
|
||||
onStop?.invoke()
|
||||
} catch (e: Error) {
|
||||
m.chatRunning.value = true
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString())
|
||||
@@ -460,10 +463,10 @@ suspend fun deleteChatAsync(m: ChatModel) {
|
||||
m.controller.apiDeleteStorage()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
m.controller.appPrefs.storeDBPassphrase.set(true)
|
||||
deleteChatDatabaseFiles()
|
||||
deleteAppDatabaseAndFiles()
|
||||
}
|
||||
|
||||
fun deleteChatDatabaseFiles() {
|
||||
fun deleteAppDatabaseAndFiles() {
|
||||
val chat = File(dataDir, chatDatabaseFileName)
|
||||
val chatBak = File(dataDir, "$chatDatabaseFileName.bak")
|
||||
val agent = File(dataDir, agentDatabaseFileName)
|
||||
@@ -473,6 +476,7 @@ fun deleteChatDatabaseFiles() {
|
||||
agent.delete()
|
||||
agentBak.delete()
|
||||
filesDir.deleteRecursively()
|
||||
filesDir.mkdir()
|
||||
remoteHostsDir.deleteRecursively()
|
||||
tmpDir.deleteRecursively()
|
||||
tmpDir.mkdir()
|
||||
|
||||
+2
-1
@@ -152,7 +152,8 @@ fun CustomTimePickerDialog(
|
||||
) {
|
||||
DefaultDialog(onDismissRequest = cancel) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ object DatabaseUtils {
|
||||
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
|
||||
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? {
|
||||
return cryptor.decryptData(
|
||||
passphrase.get()?.toByteArrayFromBase64ForPassphrase() ?: return null,
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ fun <T> ExposedDropDownSetting(
|
||||
selectionOption.second + (if (label != null) " $label" else ""),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||
color = MenuTextColor,
|
||||
fontSize = fontSize,
|
||||
)
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,8 +1,7 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.model.ChatModel
|
||||
@@ -50,7 +49,7 @@ fun authenticateWithPasscode(
|
||||
close()
|
||||
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()) {
|
||||
close()
|
||||
completed(it)
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ fun ModalView(
|
||||
if (showClose) {
|
||||
BackHandler(onBack = close)
|
||||
}
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
||||
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
||||
CloseSheetBar(close, showClose, endButtons)
|
||||
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 {
|
||||
val r: LAResult = if (passcode.value == authRequest.password) {
|
||||
if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) {
|
||||
initChatControllerAndRunMigrations(true)
|
||||
initChatControllerAndRunMigrations()
|
||||
}
|
||||
LAResult.Success
|
||||
} else {
|
||||
@@ -67,8 +67,8 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
|
||||
* */
|
||||
chatCloseStore(ctrl)
|
||||
}
|
||||
deleteChatDatabaseFiles()
|
||||
// Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself
|
||||
deleteAppDatabaseAndFiles()
|
||||
// Clear sensitive data on screen just in case ModalManager fails to hide its modals while new database is created
|
||||
m.chatId.value = null
|
||||
m.chatItems.clear()
|
||||
m.chats.clear()
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import chat.simplex.res.MR
|
||||
@Composable
|
||||
fun SetAppPasscodeView(
|
||||
passcodeKeychain: DatabaseUtils.KeyStoreItem = ksAppPassword,
|
||||
prohibitedPasscodeKeychain: DatabaseUtils.KeyStoreItem = ksSelfDestructPassword,
|
||||
title: String = generalGetString(MR.strings.new_passcode),
|
||||
reason: String? = null,
|
||||
submit: () -> Unit,
|
||||
@@ -51,7 +52,7 @@ fun SetAppPasscodeView(
|
||||
} else {
|
||||
SetPasswordView(title, generalGetString(MR.strings.save_verb),
|
||||
// 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
|
||||
passcode.value = ""
|
||||
confirming = true
|
||||
|
||||
+2
-2
@@ -175,7 +175,7 @@ fun ActionButton(
|
||||
disabled: Boolean = false,
|
||||
click: () -> Unit = {}
|
||||
) {
|
||||
Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent) {
|
||||
Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent, contentColor = LocalContentColor.current) {
|
||||
Column(
|
||||
Modifier
|
||||
.clickable(onClick = click)
|
||||
@@ -220,7 +220,7 @@ fun ActionButton(
|
||||
disabled: Boolean = false,
|
||||
click: () -> Unit = {}
|
||||
) {
|
||||
Surface(modifier, shape = RoundedCornerShape(18.dp)) {
|
||||
Surface(modifier, shape = RoundedCornerShape(18.dp), contentColor = LocalContentColor.current) {
|
||||
Column(
|
||||
Modifier
|
||||
.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) {
|
||||
Surface(
|
||||
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() }
|
||||
Row(
|
||||
|
||||
+3
-1
@@ -10,10 +10,11 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import chat.simplex.common.model.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.platform.appPreferences
|
||||
import chat.simplex.common.views.TerminalView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -44,6 +45,7 @@ fun DeveloperView(
|
||||
m.controller.appPrefs.terminalAlwaysVisible.set(false)
|
||||
}
|
||||
}
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(
|
||||
|
||||
+7
-6
@@ -383,7 +383,7 @@ fun SimplexLockView(
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
laLockDelay.set(30)
|
||||
@@ -427,7 +427,7 @@ fun SimplexLockView(
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
SetAppPasscodeView(
|
||||
reason = generalGetString(MR.strings.la_app_passcode),
|
||||
submit = {
|
||||
@@ -451,9 +451,10 @@ fun SimplexLockView(
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
SetAppPasscodeView(
|
||||
passcodeKeychain = ksSelfDestructPassword,
|
||||
prohibitedPasscodeKeychain = ksAppPassword,
|
||||
reason = generalGetString(MR.strings.self_destruct),
|
||||
submit = {
|
||||
selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_changed))
|
||||
@@ -487,7 +488,7 @@ fun SimplexLockView(
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
laLockDelay.set(30)
|
||||
@@ -598,9 +599,9 @@ private fun EnableSelfDestruct(
|
||||
selfDestruct: SharedPreference<Boolean>,
|
||||
close: () -> Unit
|
||||
) {
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
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 = {
|
||||
selfDestruct.set(true)
|
||||
selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_enabled))
|
||||
|
||||
+2
-1
@@ -155,7 +155,8 @@ fun RTCServersLayout(
|
||||
.height(160.dp)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant)
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant),
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
SelectionContainer(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
|
||||
+1
-1
@@ -155,7 +155,7 @@ fun UserAddressView(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
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(
|
||||
Modifier
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
<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_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>
|
||||
|
||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||
@@ -684,6 +686,7 @@
|
||||
<string name="hide_dev_options">Hide:</string>
|
||||
<string name="show_developer_options">Show developer options</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_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_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_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="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>
|
||||
@@ -1727,6 +1734,20 @@
|
||||
<string name="random_port">Random</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="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 -->
|
||||
<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_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>
|
||||
|
||||
<!-- 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>
|
||||
@@ -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 {
|
||||
private val prevNtfs = arrayListOf<Pair<ChatId, Slice>>()
|
||||
|
||||
fun notifyCallInvitation(invitation: RcvCallInvitation) {
|
||||
if (simplexWindowState.windowFocused.value) return
|
||||
fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean {
|
||||
if (simplexWindowState.windowFocused.value) return false
|
||||
val contactId = invitation.contact.id
|
||||
Log.d(TAG, "notifyCallInvitation $contactId")
|
||||
val image = invitation.contact.image
|
||||
@@ -45,6 +45,11 @@ object NtfManager {
|
||||
displayNotificationViaLib(contactId, title, text, prepareIconPath(largeIcon), actions) {
|
||||
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 }
|
||||
|
||||
+5
-2
@@ -15,16 +15,19 @@ val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
fun initApp() {
|
||||
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 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 androidCreateNtfChannelsMaybeShowAlert() {}
|
||||
override fun cancelCallNotification() {}
|
||||
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()
|
||||
initChatControllerAndRunMigrations(false)
|
||||
if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||
initChatControllerAndRunMigrations()
|
||||
}
|
||||
// LALAL
|
||||
//testCrypto()
|
||||
}
|
||||
|
||||
+2
-1
@@ -167,7 +167,8 @@ actual fun PlatformTextField(
|
||||
decorationBox = { innerTextField ->
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondary),
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Row(
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.*
|
||||
@@ -39,7 +38,8 @@ actual fun DefaultDialog(
|
||||
) {
|
||||
Surface(
|
||||
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()
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: d0588bd0ac23a459cbfc9a4789633014e91ffa19
|
||||
tag: ad8cd1d5154617663065652b45c784ad5a0a584d
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 5.5.0.0
|
||||
version: 5.5.0.1
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -8,9 +8,9 @@ u="$USER"
|
||||
tmp="$(mktemp -d -t)"
|
||||
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_hash="67aa37f0115195d8ddf32b5d6f471f1e60ecca0fdb3e98bcf54bc147c3078640"
|
||||
nix_hash="435f0d7e11f7c7dffeeab0ec9cc55723f6d3c03352379d785633cf4ddb5caf90"
|
||||
nix_config="sandbox = true
|
||||
max-jobs = auto
|
||||
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"
|
||||
|
||||
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_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"
|
||||
|
||||
rm -rf "$libs_folder/$android_arch"
|
||||
|
||||
if [ "$arch" = "armv7a" ] && [ -n "$tag_full" ] ; then
|
||||
git checkout "${tag_full}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@@ -37,12 +37,12 @@ for ((i = 0 ; i < ${#arches[@]}; i++)); do
|
||||
|
||||
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 && \
|
||||
mv libsupport.so "$output_dir" && \
|
||||
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 && \
|
||||
mv libsimplex.so "$output_dir" && \
|
||||
rm libsimplex.zip
|
||||
|
||||
@@ -35,7 +35,7 @@ for ((i = 0 ; i < ${#arches[@]}; i++)); do
|
||||
output_arch="${output_arches[$i]}"
|
||||
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
|
||||
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/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"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
|
||||
|
||||
name: simplex-chat
|
||||
version: 5.5.0.0
|
||||
version: 5.5.0.1
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
||||
+224
-135
@@ -24,7 +24,7 @@ import Control.Monad.Reader
|
||||
import qualified Data.Aeson as J
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import Data.Bifunctor (bimap, first, second)
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import qualified Data.ByteArray as BA
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
@@ -37,6 +37,7 @@ import Data.Constraint (Dict (..))
|
||||
import Data.Either (fromRight, lefts, partitionEithers, rights)
|
||||
import Data.Fixed (div')
|
||||
import Data.Functor (($>))
|
||||
import Data.Functor.Identity
|
||||
import Data.Int (Int64)
|
||||
import Data.List (find, foldl', isSuffixOf, partition, sortOn)
|
||||
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.Lock
|
||||
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.DB (SlowQueryStats (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
@@ -233,6 +235,7 @@ newChatController
|
||||
expireCIFlags <- newTVarIO M.empty
|
||||
cleanupManagerAsync <- newTVarIO Nothing
|
||||
timedItemThreads <- atomically TM.empty
|
||||
chatActivated <- newTVarIO True
|
||||
showLiveItems <- newTVarIO False
|
||||
encryptLocalFiles <- newTVarIO False
|
||||
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
|
||||
@@ -268,6 +271,7 @@ newChatController
|
||||
expireCIFlags,
|
||||
cleanupManagerAsync,
|
||||
timedItemThreads,
|
||||
chatActivated,
|
||||
showLiveItems,
|
||||
encryptLocalFiles,
|
||||
userXFTPFileConfig,
|
||||
@@ -311,10 +315,10 @@ cfgServers p DefaultAgentServers {smp, xftp} = case p of
|
||||
SPSMP -> smp
|
||||
SPXFTP -> xftp
|
||||
|
||||
startChatController :: forall m. ChatMonad' m => Bool -> Bool -> Bool -> m (Async ())
|
||||
startChatController subConns enableExpireCIs startXFTPWorkers = do
|
||||
startChatController :: forall m. ChatMonad' m => Bool -> m (Async ())
|
||||
startChatController mainApp = do
|
||||
asks smpAgent >>= resumeAgentClient
|
||||
unless subConns $
|
||||
unless mainApp $
|
||||
chatWriteVar subscriptionMode SMOnlyCreate
|
||||
users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers)
|
||||
restoreCalls
|
||||
@@ -324,15 +328,15 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do
|
||||
start s users = do
|
||||
a1 <- async agentSubscriber
|
||||
a2 <-
|
||||
if subConns
|
||||
if mainApp
|
||||
then Just <$> async (subscribeUsers False users)
|
||||
else pure Nothing
|
||||
atomically . writeTVar s $ Just (a1, a2)
|
||||
when startXFTPWorkers $ do
|
||||
when mainApp $ do
|
||||
startXFTP
|
||||
void $ forkIO $ startFilesToReceive users
|
||||
startCleanupManager
|
||||
when enableExpireCIs $ startExpireCIs users
|
||||
startCleanupManager
|
||||
startExpireCIs users
|
||||
pure a1
|
||||
startXFTP = do
|
||||
tmp <- readTVarIO =<< asks tempDirectory
|
||||
@@ -454,8 +458,9 @@ processChatCommand' vr = \case
|
||||
withStore' getUsers >>= \case
|
||||
[] -> pure 1
|
||||
users -> do
|
||||
when (any (\User {localDisplayName = n} -> n == displayName) users) $
|
||||
throwChatError (CEUserExists displayName)
|
||||
forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} ->
|
||||
when (n == displayName) . throwChatError $
|
||||
if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""}
|
||||
withAgent (\a -> createUser a smp xftp)
|
||||
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
|
||||
user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts
|
||||
@@ -544,16 +549,17 @@ processChatCommand' vr = \case
|
||||
checkDeleteChatUser user'
|
||||
withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues
|
||||
DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_
|
||||
StartChat subConns enableExpireCIs startXFTPWorkers -> withUser' $ \_ ->
|
||||
StartChat mainApp -> withUser' $ \_ ->
|
||||
asks agentAsync >>= readTVarIO >>= \case
|
||||
Just _ -> pure CRChatRunning
|
||||
_ -> checkStoreNotChanged $ startChatController subConns enableExpireCIs startXFTPWorkers $> CRChatStarted
|
||||
_ -> checkStoreNotChanged $ startChatController mainApp $> CRChatStarted
|
||||
APIStopChat -> do
|
||||
ask >>= stopChatController
|
||||
pure CRChatStopped
|
||||
APIActivateChat restoreChat -> withUser $ \_ -> do
|
||||
when restoreChat restoreCalls
|
||||
withAgent foregroundAgent
|
||||
chatWriteVar chatActivated True
|
||||
when restoreChat $ do
|
||||
users <- withStoreCtx' (Just "APIActivateChat, getUsers") getUsers
|
||||
void . forkIO $ subscribeUsers True users
|
||||
@@ -561,6 +567,7 @@ processChatCommand' vr = \case
|
||||
setAllExpireCIFlags True
|
||||
ok_
|
||||
APISuspendChat t -> do
|
||||
chatWriteVar chatActivated False
|
||||
setAllExpireCIFlags False
|
||||
stopRemoteCtrl
|
||||
withAgent (`suspendAgent` t)
|
||||
@@ -2135,31 +2142,41 @@ processChatCommand' vr = \case
|
||||
| otherwise = do
|
||||
when (n /= n') $ checkValidName n'
|
||||
-- read contacts before user update to correctly merge preferences
|
||||
-- [incognito] filter out contacts with whom user has incognito connections
|
||||
contacts <-
|
||||
filter (\ct -> contactReady ct && contactActive ct && not (contactConnIncognito ct))
|
||||
<$> withStore' (`getUserContacts` user)
|
||||
contacts <- withStore' (`getUserContacts` user)
|
||||
user' <- updateUser
|
||||
asks currentUser >>= atomically . (`writeTVar` Just user')
|
||||
withChatLock "updateProfile" . procCmd $ do
|
||||
ChatConfig {logLevel} <- asks config
|
||||
summary <- foldM (processAndCount user' logLevel) (UserProfileUpdateSummary 0 0 0 []) contacts
|
||||
let changedCts = foldr (addChangedProfileContact user') [] 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
|
||||
where
|
||||
processAndCount user' ll s@UserProfileUpdateSummary {notChanged, updateSuccesses, updateFailures, changedContacts = cts} ct = do
|
||||
let mergedProfile = userProfileToSend user Nothing $ Just ct
|
||||
ct' = updateMergedPreferences user' ct
|
||||
mergedProfile' = userProfileToSend user' Nothing $ Just ct'
|
||||
if mergedProfile' == mergedProfile
|
||||
then pure s {notChanged = notChanged + 1}
|
||||
else
|
||||
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'}
|
||||
-- [incognito] filter out contacts with whom user has incognito connections
|
||||
addChangedProfileContact :: User -> Contact -> [ChangedProfileContact] -> [ChangedProfileContact]
|
||||
addChangedProfileContact user' ct changedCts = case contactSendConn_ ct' of
|
||||
Left _ -> changedCts
|
||||
Right conn
|
||||
| connIncognito conn || mergedProfile' == mergedProfile -> changedCts
|
||||
| otherwise -> ChangedProfileContact ct ct' mergedProfile' conn : changedCts
|
||||
where
|
||||
notifyContact mergedProfile' ct' = do
|
||||
void $ sendDirectContactMessage ct' (XInfo mergedProfile')
|
||||
when (directOrUsed ct') $ createSndFeatureItems user' ct ct'
|
||||
mergedProfile = userProfileToSend user Nothing $ Just ct
|
||||
ct' = updateMergedPreferences user' 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 _ ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotActive ct
|
||||
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
|
||||
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 user GroupInfo {groupId, membership} mc quotedItemId_ fInv_ timed_ live = case quotedItemId_ of
|
||||
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
|
||||
expireFlags <- asks expireCIFlags
|
||||
atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry
|
||||
waitChatStartedAndActivated
|
||||
ttl <- withStoreCtx' (Just "startExpireCIThread, getChatItemTTL") (`getChatItemTTL` user)
|
||||
forM_ ttl $ \t -> expireChatItems user t False
|
||||
liftIO $ threadDelay' interval
|
||||
@@ -2973,7 +2998,7 @@ cleanupManager = do
|
||||
stepDelay <- asks (cleanupManagerStepDelay . config)
|
||||
forever $ do
|
||||
flip catchChatError (toView . CRChatError Nothing) $ do
|
||||
waitChatStarted
|
||||
waitChatStartedAndActivated
|
||||
users <- withStoreCtx' (Just "cleanupManager, getUsers 1") getUsers
|
||||
let (us, us') = partition activeUser users
|
||||
forM_ us $ cleanupUser interval stepDelay
|
||||
@@ -2983,7 +3008,7 @@ cleanupManager = do
|
||||
liftIO $ threadDelay' $ diffToMicroseconds interval
|
||||
where
|
||||
runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CRChatError Nothing) $ do
|
||||
waitChatStarted
|
||||
waitChatStartedAndActivated
|
||||
users <- withStoreCtx' (Just "cleanupManager, getUsers 2") getUsers
|
||||
let (us, us') = partition activeUser users
|
||||
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
|
||||
ts <- liftIO getCurrentTime
|
||||
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts
|
||||
waitChatStarted
|
||||
waitChatStartedAndActivated
|
||||
vr <- chatVersionRange
|
||||
case cType of
|
||||
CTDirect -> do
|
||||
@@ -3064,8 +3089,10 @@ expireChatItems user@User {userId} ttl sync = do
|
||||
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
|
||||
createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs
|
||||
waitChatStartedAndActivated
|
||||
contacts <- withStoreCtx' (Just "expireChatItems, getUserContacts") (`getUserContacts` user)
|
||||
loop contacts $ processContact expirationDate
|
||||
waitChatStartedAndActivated
|
||||
groups <- withStoreCtx' (Just "expireChatItems, getUserGroupDetails") (\db -> getUserGroupDetails db vr user Nothing Nothing)
|
||||
loop groups $ processGroup expirationDate createdAtCutoff
|
||||
where
|
||||
@@ -3084,11 +3111,13 @@ expireChatItems user@User {userId} ttl sync = do
|
||||
when (expire == Just True) $ threadDelay 100000 >> a
|
||||
processContact :: UTCTime -> Contact -> m ()
|
||||
processContact expirationDate ct = do
|
||||
waitChatStartedAndActivated
|
||||
filesInfo <- withStoreCtx' (Just "processContact, getContactExpiredFileInfo") $ \db -> getContactExpiredFileInfo db user ct expirationDate
|
||||
deleteFilesAndConns user filesInfo
|
||||
withStoreCtx' (Just "processContact, deleteContactExpiredCIs") $ \db -> deleteContactExpiredCIs db user ct expirationDate
|
||||
processGroup :: UTCTime -> UTCTime -> GroupInfo -> m ()
|
||||
processGroup expirationDate createdAtCutoff gInfo = do
|
||||
waitChatStartedAndActivated
|
||||
filesInfo <- withStoreCtx' (Just "processGroup, getGroupExpiredFileInfo") $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff
|
||||
deleteFilesAndConns user filesInfo
|
||||
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
|
||||
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
cmdId <- createAckCmd conn
|
||||
withAckMessage agentConnId cmdId msgMeta $ do
|
||||
(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
|
||||
case event of
|
||||
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
|
||||
XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta
|
||||
XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta
|
||||
-- TODO discontinue XFile
|
||||
XFile fInv -> processFileInvitation' ct' fInv msg msgMeta
|
||||
XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta
|
||||
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta
|
||||
XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId
|
||||
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName
|
||||
XInfo p -> xInfo ct' p
|
||||
XDirectDel -> xDirectDel ct' 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
|
||||
XInfoProbeOk probe -> xInfoProbeOk (COMContact ct') probe
|
||||
XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta
|
||||
XCallOffer callId offer -> xCallOffer ct' callId offer msg msgMeta
|
||||
XCallAnswer callId answer -> xCallAnswer ct' callId answer msg msgMeta
|
||||
XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg msgMeta
|
||||
XCallEnd callId -> xCallEnd ct' callId msg msgMeta
|
||||
XCallOffer callId offer -> xCallOffer ct' callId offer msg
|
||||
XCallAnswer callId answer -> xCallAnswer ct' callId answer msg
|
||||
XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg
|
||||
XCallEnd callId -> xCallEnd ct' callId msg
|
||||
BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta
|
||||
_ -> messageError $ "unsupported message: " <> T.pack (show event)
|
||||
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)
|
||||
_ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected"
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
|
||||
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta
|
||||
cmdId <- createAckCmd conn
|
||||
let aChatMsgs = parseChatMessages msgBody
|
||||
withAckMessage agentConnId cmdId msgMeta $ do
|
||||
@@ -4231,7 +4261,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
|
||||
newContentMessage ct@Contact {contactUsed} mc msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
let ExtMsgContent content fInv_ _ _ = mcExtMsgContent mc
|
||||
-- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete
|
||||
-- case content of
|
||||
@@ -4261,9 +4290,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
ChatConfig {autoAcceptFileSize = sz} <- asks config
|
||||
when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView
|
||||
|
||||
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> MsgMeta -> m ()
|
||||
messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr msgMeta = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> m ()
|
||||
messageFileDescription Contact {contactId} sharedMsgId fileDescr = do
|
||||
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
|
||||
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 ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
updateRcvChatItem `catchCINotFound` \_ -> do
|
||||
-- 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).
|
||||
@@ -4339,10 +4366,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
_ -> messageError "x.msg.update: contact attempted invalid message update"
|
||||
|
||||
messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m ()
|
||||
messageDelete ct@Contact {contactId} sharedMsgId RcvMessage {msgId} msgMeta@MsgMeta {broker = (_, brokerTs)} = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
messageDelete ct@Contact {contactId} sharedMsgId RcvMessage {msgId} msgMeta = do
|
||||
deleteRcvChatItem `catchCINotFound` (toView . CRChatItemDeletedNotFound user ct)
|
||||
where
|
||||
brokerTs = metaBrokerTs msgMeta
|
||||
deleteRcvChatItem = do
|
||||
CChatItem msgDir ci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId
|
||||
case msgDir of
|
||||
@@ -4510,7 +4537,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
-- TODO remove once XFile is discontinued
|
||||
processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
|
||||
processFileInvitation' ct fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
ChatConfig {fileChunkSize} <- asks config
|
||||
inline <- receiveInlineMode fInv Nothing 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
|
||||
_ -> pure Nothing
|
||||
|
||||
xFileCancel :: Contact -> SharedMsgId -> MsgMeta -> m ()
|
||||
xFileCancel ct@Contact {contactId} sharedMsgId msgMeta = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
xFileCancel :: Contact -> SharedMsgId -> m ()
|
||||
xFileCancel Contact {contactId} sharedMsgId = do
|
||||
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
|
||||
ft <- withStore (\db -> getRcvFileTransfer db user fileId)
|
||||
unless (rcvFileCompleteOrCancelled ft) $ do
|
||||
@@ -4557,9 +4582,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
ci <- withStore $ \db -> getChatItemByFileId db vr user fileId
|
||||
toView $ CRRcvFileSndCancelled user ci ft
|
||||
|
||||
xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m ()
|
||||
xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> m ()
|
||||
xFileAcptInv ct sharedMsgId fileConnReq_ fName = do
|
||||
fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId
|
||||
(AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId
|
||||
assertSMPAcceptNotProhibited ci
|
||||
@@ -4693,7 +4717,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
let Contact {localDisplayName = c, activeConn} = ct
|
||||
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
|
||||
forM_ activeConn $ \Connection {connId, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
|
||||
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
|
||||
-- [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 cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
|
||||
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 c p' = void $ processContactProfileUpdate c p' True
|
||||
@@ -4734,7 +4759,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
xDirectDel c msg msgMeta =
|
||||
if directOrUsed c
|
||||
then do
|
||||
checkIntegrityCreateItem (CDDirectRcv c) msgMeta
|
||||
ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted
|
||||
contactConns <- withStore' $ \db -> getContactConnections db userId ct'
|
||||
deleteAgentConnectionsAsync user $ map aConnId contactConns
|
||||
@@ -4894,7 +4918,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
-- to party accepting call
|
||||
xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m ()
|
||||
xCallInv ct@Contact {contactId} callId CallInvitation {callType, callDhPubKey} msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
if featureAllowed SCFCalls forContact ct
|
||||
then do
|
||||
g <- asks random
|
||||
@@ -4921,9 +4944,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
|
||||
|
||||
-- to party initiating call
|
||||
xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> MsgMeta -> m ()
|
||||
xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey} msg msgMeta = do
|
||||
msgCurrentCall ct callId "x.call.offer" msg msgMeta $
|
||||
xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> m ()
|
||||
xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey} msg = do
|
||||
msgCurrentCall ct callId "x.call.offer" msg $
|
||||
\call -> case callState call of
|
||||
CallInvitationSent {localCallType, localDhPrivKey} -> do
|
||||
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)
|
||||
|
||||
-- to party accepting call
|
||||
xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> MsgMeta -> m ()
|
||||
xCallAnswer ct callId CallAnswer {rtcSession} msg msgMeta = do
|
||||
msgCurrentCall ct callId "x.call.answer" msg msgMeta $
|
||||
xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> m ()
|
||||
xCallAnswer ct callId CallAnswer {rtcSession} msg = do
|
||||
msgCurrentCall ct callId "x.call.answer" msg $
|
||||
\call -> case callState call of
|
||||
CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do
|
||||
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)
|
||||
|
||||
-- to any call party
|
||||
xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> MsgMeta -> m ()
|
||||
xCallExtra ct callId CallExtraInfo {rtcExtraInfo} msg msgMeta = do
|
||||
msgCurrentCall ct callId "x.call.extra" msg msgMeta $
|
||||
xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> m ()
|
||||
xCallExtra ct callId CallExtraInfo {rtcExtraInfo} msg = do
|
||||
msgCurrentCall ct callId "x.call.extra" msg $
|
||||
\call -> case callState call of
|
||||
CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do
|
||||
-- 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)
|
||||
|
||||
-- to any call party
|
||||
xCallEnd :: Contact -> CallId -> RcvMessage -> MsgMeta -> m ()
|
||||
xCallEnd ct callId msg msgMeta =
|
||||
msgCurrentCall ct callId "x.call.end" msg msgMeta $ \Call {chatItemId} -> do
|
||||
xCallEnd :: Contact -> CallId -> RcvMessage -> m ()
|
||||
xCallEnd ct callId msg =
|
||||
msgCurrentCall ct callId "x.call.end" msg $ \Call {chatItemId} -> do
|
||||
toView $ CRCallEnded user ct
|
||||
(Nothing,) <$> callStatusItemContent user ct chatItemId WCSDisconnected
|
||||
|
||||
msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> MsgMeta -> (Call -> m (Maybe Call, Maybe ACIContent)) -> m ()
|
||||
msgCurrentCall ct@Contact {contactId = ctId'} callId' eventName RcvMessage {msgId} msgMeta action = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> (Call -> m (Maybe Call, Maybe ACIContent)) -> m ()
|
||||
msgCurrentCall ct@Contact {contactId = ctId'} callId' eventName RcvMessage {msgId} action = do
|
||||
calls <- asks currentCalls
|
||||
atomically (TM.lookup ctId' calls) >>= \case
|
||||
Nothing -> messageError $ eventName <> ": no current call"
|
||||
@@ -5631,12 +5653,20 @@ deleteOrUpdateMemberRecord user@User {userId} member =
|
||||
Nothing -> deleteGroupMember db user member
|
||||
|
||||
sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64)
|
||||
sendDirectContactMessage ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotReady ct
|
||||
sendDirectContactMessage ct@Contact {activeConn = Just conn@Connection {connId, connStatus}, contactStatus} chatMsgEvent
|
||||
| connStatus /= ConnReady && connStatus /= ConnSndReady = throwChatError $ CEContactNotReady ct
|
||||
| contactStatus /= CSActive = throwChatError $ CEContactNotActive ct
|
||||
| connDisabled conn = throwChatError $ CEContactDisabled ct
|
||||
| otherwise = sendDirectMessage conn chatMsgEvent (ConnectionId connId)
|
||||
sendDirectContactMessage ct chatMsgEvent = do
|
||||
conn@Connection {connId} <- liftEither $ contactSendConn_ ct
|
||||
sendDirectMessage conn chatMsgEvent (ConnectionId connId)
|
||||
|
||||
contactSendConn_ :: Contact -> Either ChatError Connection
|
||||
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 conn chatMsgEvent connOrGroupId = do
|
||||
@@ -5645,18 +5675,25 @@ sendDirectMessage conn chatMsgEvent connOrGroupId = do
|
||||
(msg,) <$> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
|
||||
|
||||
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
|
||||
vr <- chatVersionRange
|
||||
withStore $ \db -> createNewSndMessage db gVar connOrGroupId chatMsgEvent (encodeMessage vr)
|
||||
withStoreBatch $ \db -> fmap (uncurry (createMsg db gVar vr)) idsEvents
|
||||
where
|
||||
encodeMessage chatVRange sharedMsgId =
|
||||
encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent}
|
||||
createMsg db gVar chatVRange connOrGroupId evnt = runExceptT $ do
|
||||
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 user conn@Connection {connId} events groupId = do
|
||||
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 msgs) $ do
|
||||
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
|
||||
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
|
||||
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 chatMsgEvent = do
|
||||
@@ -5701,14 +5728,23 @@ deliverMessage' conn msgFlags msgBody msgId =
|
||||
[r] -> liftEither r
|
||||
rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
|
||||
|
||||
deliverMessages :: ChatMonad' m => [(Connection, MsgFlags, LazyMsgBody, MessageId)] -> m [Either ChatError Int64]
|
||||
deliverMessages msgReqs = do
|
||||
sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessages` aReqs)
|
||||
type MsgReq = (Connection, MsgFlags, LazyMsgBody, MessageId)
|
||||
|
||||
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
|
||||
where
|
||||
aReqs = map (\(conn, msgFlags, msgBody, _msgId) -> (aConnId conn, msgFlags, LB.toStrict msgBody)) msgReqs
|
||||
prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,)
|
||||
createDelivery :: DB.Connection -> ((Connection, MsgFlags, LazyMsgBody, MessageId), AgentMsgId) -> IO (Either ChatError Int64)
|
||||
toAgent = \case
|
||||
Right (conn, msgFlags, msgBody, _msgId) -> Right (aConnId conn, msgFlags, LB.toStrict msgBody)
|
||||
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) =
|
||||
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
|
||||
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
||||
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 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
|
||||
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
||||
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 cd ciId content file quotedItem sharedMsgId itemTimed live itemTs forwardedByMember currentTs = do
|
||||
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 =
|
||||
let itemText = ciContentToText content
|
||||
itemStatus = ciCreateStatus content
|
||||
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 user ct ci@ChatItem {file} byUser timed = do
|
||||
@@ -5988,6 +6024,15 @@ createSndFeatureItems user ct ct' =
|
||||
CUPContact {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
|
||||
|
||||
createFeatureItems ::
|
||||
@@ -6001,24 +6046,44 @@ createFeatureItems ::
|
||||
FeatureContent FeatureAllowed d ->
|
||||
(forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) ->
|
||||
m ()
|
||||
createFeatureItems user Contact {mergedPreferences = cups} ct'@Contact {mergedPreferences = cups'} chatDir ciFeature ciOffer getPref =
|
||||
forM_ allChatFeatures $ \(ACF f) -> createItem f
|
||||
createFeatureItems user ct ct' = createContactsFeatureItems user [(ct, ct')]
|
||||
|
||||
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
|
||||
createItem :: forall f. FeatureI f => SChatFeature f -> m ()
|
||||
createItem f
|
||||
| state /= state' = create ciFeature state'
|
||||
| prefState /= prefState' = create ciOffer prefState'
|
||||
| otherwise = pure ()
|
||||
contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, [CIContent d])
|
||||
contactChangedFeatures (Contact {mergedPreferences = cups}, ct'@Contact {mergedPreferences = cups'}) = do
|
||||
let contents = mapMaybe (\(ACF f) -> featureCIContent_ f) allChatFeatures
|
||||
(chatDir ct', contents)
|
||||
where
|
||||
create :: FeatureContent a d -> (a, Maybe Int) -> m ()
|
||||
create ci (s, param) = createInternalChatItem user (chatDir ct') (ci f' s param) Nothing
|
||||
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'
|
||||
featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d)
|
||||
featureCIContent_ f
|
||||
| state /= state' = Just $ fContent ciFeature state'
|
||||
| prefState /= prefState' = Just $ fContent ciOffer prefState'
|
||||
| otherwise = Nothing
|
||||
where
|
||||
fContent :: FeatureContent a d -> (a, Maybe Int) -> CIContent d
|
||||
fContent ci (s, param) = ci f' s param
|
||||
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 user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} =
|
||||
@@ -6032,15 +6097,35 @@ createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences
|
||||
sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool
|
||||
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 user cd content itemTs_ = do
|
||||
createInternalChatItem :: (ChatTypeI c, MsgDirectionI d, ChatMonad m) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> m ()
|
||||
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
|
||||
let itemTs = fromMaybe createdAt itemTs_
|
||||
ciId <- withStore' $ \db -> do
|
||||
when (ciRequiresAttention content) $ updateChatTs db user cd createdAt
|
||||
createNewChatItemNoMsg db user cd content itemTs createdAt
|
||||
ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs Nothing createdAt
|
||||
toView $ CRNewChatItem user (AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci)
|
||||
void . withStoreBatch' $ \db -> map (uncurry $ updateChat db createdAt) dirsCIContents
|
||||
withStoreBatch' $ \db -> concatMap (uncurry $ createACIs db itemTs createdAt) dirsCIContents
|
||||
where
|
||||
updateChat :: DB.Connection -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO ()
|
||||
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 st testView = do
|
||||
@@ -6133,10 +6218,14 @@ checkSameUser userId User {userId = activeUserId} = when (userId /= activeUserId
|
||||
chatStarted :: ChatMonad m => m Bool
|
||||
chatStarted = fmap isJust . readTVarIO =<< asks agentAsync
|
||||
|
||||
waitChatStarted :: ChatMonad m => m ()
|
||||
waitChatStarted = do
|
||||
waitChatStartedAndActivated :: ChatMonad m => m ()
|
||||
waitChatStartedAndActivated = do
|
||||
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 = do
|
||||
@@ -6173,8 +6262,8 @@ chatCommandP =
|
||||
"/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)),
|
||||
"/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)),
|
||||
("/user" <|> "/u") $> ShowActiveUser,
|
||||
"/_start subscribe=" *> (StartChat <$> onOffP <* " expire=" <*> onOffP <* " xftp=" <*> onOffP),
|
||||
"/_start" $> StartChat True True True,
|
||||
"/_start main=" *> (StartChat <$> onOffP),
|
||||
"/_start" $> StartChat True,
|
||||
"/_stop" $> APIStopChat,
|
||||
"/_app activate restore=" *> (APIActivateChat <$> onOffP),
|
||||
"/_app activate" $> APIActivateChat True,
|
||||
|
||||
@@ -200,6 +200,7 @@ data ChatController = ChatController
|
||||
expireCIThreads :: TMap UserId (Maybe (Async ())),
|
||||
expireCIFlags :: TMap UserId Bool,
|
||||
cleanupManagerAsync :: TVar (Maybe (Async ())),
|
||||
chatActivated :: TVar Bool,
|
||||
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
|
||||
showLiveItems :: TVar Bool,
|
||||
encryptLocalFiles :: TVar Bool,
|
||||
@@ -233,7 +234,7 @@ data ChatCommand
|
||||
| UnmuteUser
|
||||
| APIDeleteUser UserId Bool (Maybe UserPwd)
|
||||
| DeleteUser UserName Bool (Maybe UserPwd)
|
||||
| StartChat {subscribeConnections :: Bool, enableExpireChatItems :: Bool, startXFTPWorkers :: Bool}
|
||||
| StartChat {mainApp :: Bool}
|
||||
| APIStopChat
|
||||
| APIActivateChat {restoreChat :: Bool}
|
||||
| APISuspendChat {suspendTimeout :: Int}
|
||||
@@ -894,8 +895,7 @@ data PendingSubStatus = PendingSubStatus
|
||||
deriving (Show)
|
||||
|
||||
data UserProfileUpdateSummary = UserProfileUpdateSummary
|
||||
{ notChanged :: Int,
|
||||
updateSuccesses :: Int,
|
||||
{ updateSuccesses :: Int,
|
||||
updateFailures :: Int,
|
||||
changedContacts :: [Contact]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController
|
||||
runSimplexChat ChatOpts {maintenance} u cc chat
|
||||
| maintenance = wait =<< async (chat u cc)
|
||||
| otherwise = do
|
||||
a1 <- runReaderT (startChatController True True True) cc
|
||||
a1 <- runReaderT (startChatController True) cc
|
||||
a2 <- async $ chat u cc
|
||||
waitEither_ a1 a2
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module Simplex.Chat.Mobile.Shared where
|
||||
|
||||
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.Internal as LB
|
||||
import Foreign
|
||||
@@ -21,7 +21,7 @@ getByteString ptr len = do
|
||||
|
||||
putByteString :: Ptr Word8 -> ByteString -> IO ()
|
||||
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 #-}
|
||||
|
||||
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 (?,?,?,?,?,?,?,?)"
|
||||
(profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs)
|
||||
insertedRowId db
|
||||
insertHost_ currentTs groupId = ExceptT $ do
|
||||
insertHost_ currentTs groupId = do
|
||||
let fromMemberProfile = profileFromName fromMemberName
|
||||
withLocalDisplayName db userId fromMemberName $ \localDisplayName -> runExceptT $ do
|
||||
(_, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs
|
||||
let MemberIdRole {memberId, memberRole} = fromMember
|
||||
liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( 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)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown)
|
||||
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
|
||||
)
|
||||
insertedRowId db
|
||||
(localDisplayName, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs
|
||||
let MemberIdRole {memberId, memberRole} = fromMember
|
||||
liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_members
|
||||
( 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)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown)
|
||||
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
|
||||
)
|
||||
insertedRowId db
|
||||
|
||||
setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
|
||||
setViaGroupLinkHash db groupId connId =
|
||||
|
||||
@@ -415,6 +415,7 @@ xftpServerConfig =
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
|
||||
serverStatsBackupFile = Nothing,
|
||||
controlPort = Nothing,
|
||||
transportConfig = defaultTransportServerConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -1149,7 +1149,7 @@ testSubscribeAppNSE tmp =
|
||||
alice ##> "/_app suspend 1"
|
||||
alice <## "ok"
|
||||
alice <## "chat suspended"
|
||||
nseAlice ##> "/_start subscribe=off expire=off xftp=off"
|
||||
nseAlice ##> "/_start main=off"
|
||||
nseAlice <## "chat started"
|
||||
nseAlice ##> "/ad"
|
||||
cLink <- getContactLink nseAlice True
|
||||
|
||||
@@ -67,6 +67,7 @@ chatProfileTests = do
|
||||
xit'' "enable timed messages with contact" testEnableTimedMessagesContact
|
||||
it "enable timed messages in group" testEnableTimedMessagesGroup
|
||||
xit'' "timed messages enabled globally, contact turns on" testTimedMessagesEnabledGlobally
|
||||
it "update multiple user preferences for multiple contacts" testUpdateMultipleUserPrefs
|
||||
|
||||
testUpdateProfile :: HasCallStack => FilePath -> IO ()
|
||||
testUpdateProfile =
|
||||
@@ -1864,3 +1865,30 @@ testTimedMessagesEnabledGlobally =
|
||||
bob <## "timed message deleted: hey"
|
||||
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)")])
|
||||
|
||||
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 qualified Data.ByteString as B
|
||||
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 Data.Word (Word8, Word32)
|
||||
import Foreign.C
|
||||
import Foreign.Marshal.Alloc (mallocBytes)
|
||||
import Foreign.Marshal.Utils (copyBytes)
|
||||
import Foreign.Ptr
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Storable (peek)
|
||||
@@ -291,7 +292,7 @@ testFileCApi fileName tmp = do
|
||||
peek ptr' `shouldReturn` (0 :: Word8)
|
||||
sz :: Word32 <- peek (ptr' `plusPtr` 1)
|
||||
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
|
||||
sz' `shouldBe` fromIntegral len
|
||||
|
||||
|
||||
Reference in New Issue
Block a user