Compare commits

..

1 Commits

Author SHA1 Message Date
Evgeny Poberezkin
9f0d400fdc rfc: handling multiple files/attachments in a message 2024-01-03 10:49:58 +00:00
102 changed files with 546 additions and 1128 deletions

View File

@@ -31,7 +31,6 @@ 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 {
@@ -62,10 +61,6 @@ 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
@@ -78,7 +73,8 @@ struct ContentView: View {
showSetPasscode = false
alertManager.showAlert(laPasscodeNotSetAlert())
}
} else if chatModel.chatDbStatus == nil && AppChatState.shared.value != .stopped && waitingForOrPassedAuth {
}
if chatModel.chatDbStatus == nil {
initializationView()
}
}

View File

@@ -54,7 +54,6 @@ 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?

View File

@@ -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, suspended: @escaping (Bool) -> Void) {
func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = DispatchQueue.main, 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.main.async { suspended(true) }
dispatchQueue.async { suspended(true) }
return
}
let id = UUID()
@@ -45,7 +45,7 @@ func waitNSESuspended(timeout: TimeInterval, suspended: @escaping (Bool) -> Void
logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))")
suspendedCalled = true
nseSubscribers.removeValue(forKey: id)
DispatchQueue.main.async { suspended(ok) }
dispatchQueue.async { suspended(ok) }
}
}

View File

@@ -211,7 +211,7 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn
}
func apiStartChat() throws -> Bool {
let r = chatSendCmdSync(.startChat(mainApp: true))
let r = chatSendCmdSync(.startChat(subscribe: true, expire: true, xftp: 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))")
logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)")
return (nil, nil, .off)
}
}
@@ -1215,8 +1215,6 @@ 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

View File

@@ -19,13 +19,11 @@ 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)")
logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)")
} else if ChatModel.ok {
AppChatState.shared.set(.suspending)
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
@@ -126,33 +124,20 @@ func initChatAndMigrate(refreshInvitations: Bool = true) {
}
}
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) {
func startChatAndActivate(dispatchQueue: DispatchQueue = DispatchQueue.main, _ completion: @escaping () -> Void) {
logger.debug("DEBUGGING: startChatAndActivate")
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start()
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
}
if case .active = AppChatState.shared.value {
if .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: nseSuspendTimeout) { ok in
waitNSESuspended(timeout: 10, dispatchQueue: dispatchQueue) { ok in
if !ok {
// if for some reason NSE failed to suspend,
// e.g., it crashed previously without setting its state to "suspended",

View File

@@ -44,10 +44,8 @@ struct SimpleXApp: App {
chatModel.appOpenUrl = url
}
.onAppear() {
if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
initChatAndMigrate()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
initChatAndMigrate()
}
}
.onChange(of: scenePhase) { phase in
@@ -100,12 +98,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())*.db")
logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db")
} else {
dbContainerGroupDefault.set(.group)
setMigrationState(.ready)
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath())*.db")
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not") present")
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")
}
}

View File

@@ -38,13 +38,13 @@ struct ActiveCallView: View {
}
}
.onAppear {
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)")
AppDelegate.keepScreenOn(true)
createWebRTCClient()
dismissAllSheets()
}
.onChange(of: canConnectCall) { _ in
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall, privacy: .public)")
createWebRTCClient()
}
.onDisappear {

View File

@@ -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))")
logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat), privacy: .public)")
if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true {
self?.shouldSuspendChat = false
suspendChat()
@@ -142,57 +142,45 @@ 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)")
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue, privacy: .public)")
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
logger.debug("CallController: did receive push with type \(type.rawValue)")
logger.debug("CallController: did receive push with type \(type.rawValue, privacy: .public)")
if type != .voIP {
completion()
return
}
if AppChatState.shared.value == .stopped {
self.reportExpiredCall(payload: payload, completion)
return
}
logger.debug("CallController: initializing chat")
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
}
initChatAndMigrate(refreshInvitations: false)
}
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")
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] {
let update = self.cxCallUpdate(invitation: invitation)
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error != nil {
m.callInvitations.removeValue(forKey: contactId)
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()
}
// Tell PushKit that the notification is handled.
completion()
} else {
self.reportExpiredCall(update: update, completion)
}
} else {
self.reportExpiredCall(update: update, completion)
self.reportExpiredCall(payload: payload, completion)
}
} else {
self.reportExpiredCall(payload: payload, completion)
}
}
@@ -223,7 +211,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID))")
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID), privacy: .public)")
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
if invitation.callTs.timeIntervalSinceNow >= -180 {
let update = cxCallUpdate(invitation: invitation)
@@ -363,7 +351,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)")
logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription, privacy: .public)")
} else {
logger.debug("CallController.requestTransaction requested transaction successfully")
onSuccess()

View File

@@ -484,7 +484,6 @@ func deleteChatAsync() async throws {
try await apiDeleteStorage()
_ = kcDatabasePassword.remove()
storeDBPassphraseGroupDefault.set(true)
deleteAppDatabaseAndFiles()
}
struct DatabaseView_Previews: PreviewProvider {

View File

@@ -13,28 +13,19 @@ 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",
buttonsEnabled: $allowToReact) {
PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") {
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
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"))
}
let r: LAResult = password == authRequest.password
? .success
: .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
m.laRequest = nil
authRequest.completed(r)
} cancel: {
@@ -46,27 +37,8 @@ struct LocalAuthView: View {
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
Task {
do {
/** 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 = []
try await stopChatAsync()
try await deleteChatAsync()
_ = kcAppPassword.set(password)
_ = kcSelfDestructPassword.remove()
await NtfManager.shared.removeAllNotifications()
@@ -81,7 +53,7 @@ struct LocalAuthView: View {
try initializeChat(start: true)
m.chatDbChanged = false
AppChatState.shared.set(.active)
if m.currentUser != nil || !m.chatInitialized { return }
if m.currentUser != nil { return }
var profile: Profile? = nil
if let displayName = displayName, displayName != "" {
profile = Profile(displayName: displayName, fullName: "")

View File

@@ -14,8 +14,6 @@ struct PasscodeView: View {
var reason: String? = nil
var submitLabel: LocalizedStringKey
var submitEnabled: ((String) -> Bool)?
@Binding var buttonsEnabled: Bool
var submit: () -> Void
var cancel: () -> Void
@@ -72,11 +70,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 || !buttonsEnabled)
.disabled(submitEnabled?(passcode) == false || passcode.count < 4)
}
}
@@ -87,7 +85,6 @@ struct PasscodeViewView_Previews: PreviewProvider {
title: "Enter Passcode",
reason: "Unlock app",
submitLabel: "Submit",
buttonsEnabled: Binding.constant(true),
submit: {},
cancel: {}
)

View File

@@ -11,7 +11,6 @@ import SimpleXChat
struct SetAppPasscodeView: View {
var passcodeKeychain: KeyChainItem = kcAppPassword
var prohibitedPasscodeKeychain: KeyChainItem = kcSelfDestructPassword
var title: LocalizedStringKey = "New Passcode"
var reason: String?
var submit: () -> Void
@@ -42,10 +41,7 @@ struct SetAppPasscodeView: View {
}
}
} else {
setPasswordView(title: title,
submitLabel: "Save",
// Do not allow to set app passcode == selfDestruct passcode
submitEnabled: { pwd in pwd != prohibitedPasscodeKeychain.get() }) {
setPasswordView(title: title, submitLabel: "Save") {
enteredPassword = passcode
passcode = ""
confirming = true
@@ -58,7 +54,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, buttonsEnabled: Binding.constant(true), submit: submit) {
PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) {
dismiss()
cancel()
}

View File

@@ -11,14 +11,12 @@ 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)"
}
@@ -189,12 +187,6 @@ 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 {
@@ -215,7 +207,6 @@ 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)
}
@@ -228,13 +219,6 @@ 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!"),

View File

@@ -491,23 +491,14 @@ struct SimplexLockView: View {
showLAAlert(.laPasscodeNotChangedAlert)
}
case .enableSelfDestruct:
SetAppPasscodeView(
passcodeKeychain: kcSelfDestructPassword,
prohibitedPasscodeKeychain: kcAppPassword,
title: "Set passcode",
reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")
) {
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) {
updateSelfDestruct()
showLAAlert(.laSelfDestructPasscodeSetAlert)
} cancel: {
revertSelfDestruct()
}
case .changeSelfDestructPasscode:
SetAppPasscodeView(
passcodeKeychain: kcSelfDestructPassword,
prohibitedPasscodeKeychain: kcAppPassword,
reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")
) {
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) {
showLAAlert(.laSelfDestructPasscodeChangedAlert)
} cancel: {
showLAAlert(.laPasscodeNotChangedAlert)

View File

@@ -16,11 +16,9 @@ let logger = Logger()
let appSuspendingDelay: UInt64 = 2_500_000_000
typealias SuspendSchedule = (delay: TimeInterval, timeout: Int)
let nseSuspendDelay: TimeInterval = 2
let nseSuspendSchedule: SuspendSchedule = (2, 4)
let fastNSESuspendSchedule: SuspendSchedule = (1, 1)
let nseSuspendTimeout: Int = 5
typealias NtfStream = ConcurrentQueue<NSENotification>
@@ -34,7 +32,7 @@ actor PendingNtfs {
private var ntfStreams: [String: NtfStream] = [:]
func createStream(_ id: String) async {
logger.debug("NotificationService PendingNtfs.createStream: \(id)")
logger.debug("NotificationService PendingNtfs.createStream: \(id, privacy: .public)")
if ntfStreams[id] == nil {
ntfStreams[id] = ConcurrentQueue()
logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue")
@@ -42,14 +40,14 @@ actor PendingNtfs {
}
func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async {
logger.debug("NotificationService PendingNtfs.readStream: \(id) \(ntfInfo.ntfMessages.count)")
logger.debug("NotificationService PendingNtfs.readStream: \(id, privacy: .public) \(ntfInfo.ntfMessages.count, privacy: .public)")
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)")
logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected, privacy: .public)")
var readCancelled = false
var dequeued: DequeueElement<NSENotification>?
nse.cancelRead = {
@@ -68,7 +66,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)")
logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty, privacy: .public)")
if expected.isEmpty { break }
} else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs {
logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo")
@@ -90,7 +88,7 @@ actor PendingNtfs {
}
func writeStream(_ id: String, _ ntf: NSENotification) async {
logger.debug("NotificationService PendingNtfs.writeStream: \(id)")
logger.debug("NotificationService PendingNtfs.writeStream: \(id, privacy: .public)")
if let s = ntfStreams[id] {
logger.debug("NotificationService PendingNtfs.writeStream: writing ntf")
s.enqueue(ntf)
@@ -210,7 +208,7 @@ class NotificationService: UNNotificationServiceExtension {
self.contentHandler = contentHandler
registerGroupDefaults()
let appState = appStateGroupDefault.get()
logger.debug("NotificationService: app is \(appState.rawValue)")
logger.debug("NotificationService: app is \(appState.rawValue, privacy: .public)")
switch appState {
case .stopped:
setBadgeCount()
@@ -240,7 +238,7 @@ class NotificationService: UNNotificationServiceExtension {
}
}
}
logger.debug("NotificationService: app state is now \(state.rawValue)")
logger.debug("NotificationService: app state is now \(state.rawValue, privacy: .public)")
if state.inactive {
receiveNtfMessages(request, contentHandler)
} else {
@@ -269,7 +267,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))")
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count), privacy: .public)")
if let connEntity = ntfInfo.connEntity_ {
setBestAttemptNtf(
ntfInfo.ntfsEnabled
@@ -281,7 +279,7 @@ class NotificationService: UNNotificationServiceExtension {
NtfStreamSemaphores.shared.waitForStream(id)
if receiveEntityId != nil {
Task {
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id)")
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)")
await PendingNtfs.shared.createStream(id)
await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo)
deliverBestAttemptNtf()
@@ -299,7 +297,7 @@ class NotificationService: UNNotificationServiceExtension {
override func serviceExtensionTimeWillExpire() {
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
deliverBestAttemptNtf(urgent: true)
deliverBestAttemptNtf()
}
func setBadgeCount() {
@@ -321,7 +319,7 @@ class NotificationService: UNNotificationServiceExtension {
}
}
private func deliverBestAttemptNtf(urgent: Bool = false) {
private func deliverBestAttemptNtf() {
logger.debug("NotificationService.deliverBestAttemptNtf")
if let cancel = cancelRead {
cancelRead = nil
@@ -331,55 +329,20 @@ class NotificationService: UNNotificationServiceExtension {
receiveEntityId = nil
NtfStreamSemaphores.shared.signalStreamReady(id)
}
let suspend: Bool
if let t = threadId {
threadId = nil
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()
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)
}
}
}
} 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
@@ -394,14 +357,17 @@ 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
logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
deliver(error == nil ? nil : createCallInvitationNtf(invitation))
if error == nil {
deliver(nil)
} else {
logger.debug("NotificationService reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
deliver(createCallInvitationNtf(invitation))
}
}
case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet
case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo
@@ -436,14 +402,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(fastNSESuspendSchedule.timeout)
suspendChat(nseSuspendTimeout)
}
}
func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber {
appMessageSubscriber { msg in
if case let .state(state) = msg {
logger.debug("NotificationService: appStateSubscriber \(state.rawValue)")
logger.debug("NotificationService: appStateSubscriber \(state.rawValue, privacy: .public)")
onState(state)
}
}
@@ -459,33 +425,24 @@ 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")
// only skip creating if there is chat controller
if case .active = NSEChatState.shared.value, hasChatCtrl() { return .ok }
if case .active = NSEChatState.shared.value { return .ok }
startLock.wait()
defer { startLock.signal() }
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()
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()
}
}
func doStartChat() -> DBMigrationResult? {
logger.debug("NotificationService: doStartChat")
haskell_init_nse()
hs_init(0, nil)
let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true)
logger.debug("NotificationService: doStartChat \(String(describing: dbStatus))")
if dbStatus != .ok {
resetChatCtrl()
NSEChatState.shared.set(.created)
@@ -520,7 +477,7 @@ func doStartChat() -> DBMigrationResult? {
return .ok
}
} catch {
logger.error("NotificationService startChat error: \(responseError(error))")
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
}
} else {
logger.debug("NotificationService: no active user")
@@ -547,10 +504,8 @@ 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)")
} else if hasChatCtrl() {
// only suspend if we have chat controller to avoid crashes when suspension is
// attempted when chat controller was not created
logger.error("NotificationService suspendChat called, current state: \(state.rawValue, privacy: .public)")
} else {
suspendLock.wait()
defer { suspendLock.signal() }
@@ -616,7 +571,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)")
logger.debug("NotificationService receivedMsgNtf: \(res.responseType, privacy: .public)")
switch res {
case let .contactConnected(user, contact, _):
return (contact.id, .nse(createContactConnectedNtf(user, contact)))
@@ -658,9 +613,6 @@ 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
@@ -675,22 +627,17 @@ func updateNetCfg() {
try setNetworkConfig(networkConfig)
networkConfig = newNetConfig
} catch {
logger.error("NotificationService apply changed network config error: \(responseError(error))")
logger.error("NotificationService apply changed network config error: \(responseError(error), privacy: .public)")
}
}
}
func apiGetActiveUser() -> User? {
let r = sendSimpleXCmd(.showActiveUser)
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)")
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(String(describing: r))")
switch r {
case let .activeUser(user): return user
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
case .chatCmdError(_, .error(.noActiveUser)): return nil
default:
logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))")
return nil
@@ -698,7 +645,7 @@ func apiGetActiveUser() -> User? {
}
func apiStartChat() throws -> Bool {
let r = sendSimpleXCmd(.startChat(mainApp: false))
let r = sendSimpleXCmd(.startChat(subscribe: false, expire: false, xftp: false))
switch r {
case .chatStarted: return true
case .chatRunning: return false
@@ -752,12 +699,11 @@ 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) \(String.init(describing: r))")
logger.debug("apiGetNtfMessage ignored response: \(r.responseType, privacy: .public) \(String.init(describing: r), privacy: .private)")
}
return nil
}

View File

@@ -29,11 +29,6 @@
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 */; };
@@ -47,6 +42,11 @@
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,11 +275,6 @@
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>"; };
@@ -294,6 +289,11 @@
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 = (
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 */,
5C4E80E72B40A96C0080FAE2 /* libgmpxx.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C245F1A2B4DB982001CC39F /* libHSsimplex-chat-5.5.0.0-K5xQiJJwtSUKGqIyB7d1Tl-ghc9.6.3.a in Frameworks */,
5C4E80E62B40A96C0080FAE2 /* libgmp.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 = (
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 */,
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 */,
);
path = Libraries;
sourceTree = "<group>";

View File

@@ -41,7 +41,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"

View File

@@ -2,7 +2,7 @@
<Scheme
LastUpgradeVersion = "1400"
wasCreatedForAppExtension = "YES"
version = "1.3">
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@@ -47,14 +47,16 @@
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference

View File

@@ -12,11 +12,7 @@ private var chatController: chat_ctrl?
private var migrationResult: (Bool, DBMigrationResult)?
public func hasChatCtrl() -> Bool {
chatController != nil
}
public func getChatCtrl() -> chat_ctrl {
public func getChatCtrl(_ useKey: String? = nil) -> chat_ctrl {
if let controller = chatController { return controller }
fatalError("chat controller not initialized")
}

View File

@@ -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(mainApp: Bool)
case startChat(subscribe: Bool, expire: Bool, xftp: 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(mainApp): return "/_start main=\(onOff(mainApp))"
case let .startChat(subscribe, expire, xftp): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire)) xftp=\(onOff(xftp))"
case .apiStopChat: return "/_stop"
case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
@@ -1610,7 +1610,6 @@ 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)

View File

@@ -172,6 +172,7 @@ 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]

View File

@@ -69,29 +69,13 @@ 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 {
try fm.removeItem(at: getAppFilesDirectory())
try fm.createDirectory(at: getAppFilesDirectory(), withIntermediateDirectories: true)
let fileNames = try fm.contentsOfDirectory(atPath: getAppFilesDirectory().path)
for fileName in fileNames {
removeFile(fileName)
}
} catch {
logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)")
}

View File

@@ -23,19 +23,3 @@ 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);
}

View File

@@ -11,6 +11,4 @@
void haskell_init(void);
void haskell_init_nse(void);
#endif /* hs_init_h */

View File

@@ -3,12 +3,11 @@ 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.common.platform.chatModel
import chat.simplex.common.platform.initChatControllerAndRunMigrations
import chat.simplex.common.views.helpers.DatabaseUtils
import chat.simplex.app.BuildConfig
import kotlinx.coroutines.*
import java.util.Date
import java.util.concurrent.TimeUnit
@@ -58,10 +57,6 @@ 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)

View File

@@ -26,7 +26,6 @@ import kotlinx.coroutines.sync.withLock
import java.io.*
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
const val TAG = "SIMPLEX"
@@ -47,8 +46,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
try {
Looper.loop()
} catch (e: Throwable) {
if (e is UnsatisfiedLinkError || e.message?.startsWith("Unable to start activity") == true) {
Process.killProcess(Process.myPid())
if (e.message != null && e.message!!.startsWith("Unable to start activity")) {
android.os.Process.killProcess(android.os.Process.myPid())
break
} else {
// Send it to our exception handled because it will not get the exception otherwise
@@ -64,9 +63,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
tmpDir.deleteRecursively()
tmpDir.mkdir()
if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
initChatControllerAndRunMigrations()
}
initChatControllerAndRunMigrations(false)
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
}
@@ -80,7 +77,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
updatingChatsMutex.withLock {
kotlin.runCatching {
val currentUserId = chatModel.currentUser.value?.userId
val chats = ArrayList(chatController.apiGetChatsWithoutAlert(chatModel.remoteHostId()) ?: return@runCatching)
val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId()))
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
if (chatModel.currentUser.value?.userId == currentUserId) {
val currentChatId = chatModel.chatId.value
@@ -174,14 +171,13 @@ class SimplexApp: Application(), LifecycleEventObserver {
androidAppContext = this
APPLICATION_ID = BuildConfig.APPLICATION_ID
ntfManager = object : chat.simplex.common.platform.NtfManager() {
override fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean = NtfManager.notifyCallInvitation(invitation)
override fun notifyCallInvitation(invitation: RcvCallInvitation) = 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() {

View File

@@ -72,10 +72,6 @@ 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()
}
}
}

View File

@@ -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_2"
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
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 + "/raw/ring_once")
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.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): Boolean {
fun notifyCallInvitation(invitation: RcvCallInvitation) {
val keyguardManager = getKeyguardManager(context)
Log.d(
TAG,
@@ -149,7 +149,7 @@ object NtfManager {
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " +
"onForeground ${isAppOnForeground}"
)
if (isAppOnForeground) return false
if (isAppOnForeground) return
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 + "/raw/ring_once")
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.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,39 +206,6 @@ 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() {
@@ -281,7 +248,6 @@ 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")
}

View File

@@ -97,8 +97,7 @@ fun IncomingCallActivityView(m: ChatModel) {
Surface(
Modifier
.fillMaxSize(),
color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
color = MaterialTheme.colors.background
) {
if (showCallView) {
Box {
@@ -201,8 +200,7 @@ private fun SimpleXLogo() {
private fun LockScreenCallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
Surface(
shape = RoundedCornerShape(10.dp),
color = Color.Transparent,
contentColor = LocalContentColor.current
color = Color.Transparent
) {
Column(
Modifier
@@ -229,8 +227,7 @@ fun PreviewIncomingCallLockScreenAlert() {
Surface(
Modifier
.fillMaxSize(),
color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
color = MaterialTheme.colors.background
) {
IncomingCallLockScreenAlertLayout(
invitation = RcvCallInvitation(

View File

@@ -1,7 +0,0 @@
package chat.simplex.common.views.database
import chat.simplex.common.views.usersettings.restartApp
actual fun restartChatOrApp() {
restartApp()
}

View File

@@ -28,7 +28,7 @@ actual fun SettingsSectionApp(
}
fun restartApp() {
private fun restartApp() {
ProcessPhoenix.triggerRebirth(androidAppContext)
shutdownApp()
}

View File

@@ -44,7 +44,7 @@ data class SettingsViewState(
fun AppScreen() {
SimpleXTheme {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
Surface(color = MaterialTheme.colors.background) {
MainScreen()
}
}
@@ -85,7 +85,7 @@ fun MainScreen() {
@Composable
fun AuthView() {
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
Surface(color = MaterialTheme.colors.background) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center

View File

@@ -1,7 +1,8 @@
package chat.simplex.common
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import chat.simplex.common.model.*
@@ -106,7 +107,7 @@ object AppLock {
private fun setPasscode() {
val appPrefs = ChatController.appPrefs
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
ChatModel.performLA.value = true

View File

@@ -125,9 +125,6 @@ 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 {
@@ -1154,6 +1151,7 @@ data class LocalProfile(
@Serializable
data class UserProfileUpdateSummary(
val notChanged: Int,
val updateSuccesses: Int,
val updateFailures: Int,
val changedContacts: List<Contact>

View File

@@ -108,7 +108,6 @@ 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")
@@ -277,7 +276,6 @@ 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"
@@ -506,10 +504,6 @@ 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)
}
@@ -578,7 +572,7 @@ object ChatController {
}
suspend fun apiStartChat(): Boolean {
val r = sendCmd(null, CC.StartChat(mainApp = true))
val r = sendCmd(null, CC.StartChat(expire = true))
when (r) {
is CR.ChatStarted -> return true
is CR.ChatRunning -> return false
@@ -654,15 +648,6 @@ 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)
@@ -1137,13 +1122,6 @@ 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
@@ -1900,34 +1878,9 @@ object ChatController {
val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ }
chatModel.remoteHostPairing.value = null
if (disconnectedHost != null) {
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))
}
}
}
showToast(
generalGetString(MR.strings.remote_host_was_disconnected_toast).format(disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() })
)
}
if (chatModel.remoteHostId() == r.remoteHostId_) {
chatModel.currentRemoteHost.value = null
@@ -1958,40 +1911,11 @@ 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}")
}
@@ -2241,7 +2165,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 mainApp: Boolean): CC()
class StartChat(val expire: Boolean): CC()
class ApiStopChat: CC()
class SetTempFolder(val tempFolder: String): CC()
class SetFilesFolder(val filesFolder: String): CC()
@@ -2312,7 +2236,6 @@ 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()
@@ -2369,7 +2292,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 main=${onOff(mainApp)}"
is StartChat -> "/_start subscribe=on expire=${onOff(expire)} xftp=on"
is ApiStopChat -> "/_stop"
is SetTempFolder -> "/_temp_folder $tempFolder"
is SetFilesFolder -> "/_files_folder $filesFolder"
@@ -2449,7 +2372,6 @@ 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)}"
@@ -2573,7 +2495,6 @@ sealed class CC {
is ApiAddressAutoAccept -> "apiAddressAutoAccept"
is ApiAcceptContact -> "apiAcceptContact"
is ApiRejectContact -> "apiRejectContact"
is ApiGetCallInvitations -> "apiGetCallInvitations"
is ApiSendCallInvitation -> "apiSendCallInvitation"
is ApiRejectCall -> "apiRejectCall"
is ApiSendCallOffer -> "apiSendCallOffer"
@@ -3949,7 +3870,6 @@ 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()
@@ -4097,7 +4017,6 @@ sealed class CR {
is SndFileProgressXFTP -> "sndFileProgressXFTP"
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
is SndFileError -> "sndFileError"
is CallInvitations -> "callInvitations"
is CallInvitation -> "callInvitation"
is CallOffer -> "callOffer"
is CallAnswer -> "callAnswer"
@@ -4244,7 +4163,6 @@ 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)}")
@@ -4519,7 +4437,6 @@ sealed class ChatErrorType {
is EmptyUserPassword -> "emptyUserPassword"
is UserAlreadyHidden -> "userAlreadyHidden"
is UserNotHidden -> "userNotHidden"
is InvalidDisplayName -> "invalidDisplayName"
is ChatNotStarted -> "chatNotStarted"
is ChatNotStopped -> "chatNotStopped"
is ChatStoreChanged -> "chatStoreChanged"
@@ -4597,7 +4514,6 @@ 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()
@@ -4815,7 +4731,6 @@ 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()
@@ -4827,7 +4742,6 @@ 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
@@ -5047,15 +4961,6 @@ 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()
@@ -5076,16 +4981,6 @@ 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()

View File

@@ -41,20 +41,21 @@ val appPreferences: AppPreferences
val chatController: ChatController = ChatController
fun initChatControllerAndRunMigrations() {
withBGApi {
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
initChatController(startChat = ::showStartChatAfterRestartAlert)
} else {
initChatController()
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()
}
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

View File

@@ -93,13 +93,12 @@ abstract class NtfManager {
}
}
abstract fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean
abstract fun notifyCallInvitation(invitation: RcvCallInvitation)
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()

View File

@@ -1,7 +1,5 @@
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)
@@ -27,5 +25,4 @@ 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 MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black
val MenuTextColorDark = Color.White.copy(alpha = 0.8f)

View File

@@ -283,10 +283,27 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
val theme by CurrentColors.collectAsState()
MaterialTheme(
colors = theme.colors,
typography = Typography,
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)
),
shapes = Shapes,
content = {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onBackground, content = content)
ProvideTextStyle(
value = TextStyle(color = theme.colors.onBackground),
content = content
)
}
)
}

View File

@@ -1,7 +1,8 @@
package chat.simplex.common.views
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -10,8 +11,7 @@ fun SplashView() {
Surface(
Modifier
.fillMaxSize(),
color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
color = MaterialTheme.colors.background
) {
// Image(
// painter = painterResource(MR.images.logo),

View File

@@ -101,16 +101,13 @@ fun TerminalLayout(
)
}
},
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Surface(
modifier = Modifier
.padding(contentPadding)
.fillMaxWidth(),
color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
color = MaterialTheme.colors.background
) {
TerminalLog()
}

View File

@@ -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, contentColor = LocalContentColor.current) {
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) {
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)

View File

@@ -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)

View File

@@ -15,10 +15,11 @@ 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
@@ -26,11 +27,7 @@ import kotlinx.datetime.Clock
fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
if (chatModel.activeCallInvitation.value?.sentNotification == false || appPlatform.isDesktop) {
SoundPlayer.start(scope, sound = !chatModel.showCallView.value)
}
}
LaunchedEffect(true) { SoundPlayer.start(scope, sound = !chatModel.showCallView.value) }
DisposableEffect(true) { onDispose { SoundPlayer.stop() } }
IncomingCallAlertLayout(
invitation,
@@ -88,8 +85,7 @@ 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,
contentColor = LocalContentColor.current
color = Color.Transparent
) {
Column(
Modifier

View File

@@ -112,9 +112,6 @@ 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?) {

View File

@@ -24,7 +24,6 @@ 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.*
@@ -318,14 +317,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
},
acceptCall = { contact ->
hideKeyboard(view)
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)
}
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)
}
},
acceptFeature = { contact, feature, param ->
@@ -574,8 +570,6 @@ fun ChatLayout(
bottomBar = composeView,
modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() },
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
) { contentPadding ->
BoxWithConstraints(Modifier
.fillMaxHeight()

View File

@@ -258,8 +258,7 @@ private fun CustomDisappearingMessageDialog(
DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
contentColor = LocalContentColor.current
shape = RoundedCornerShape(corner = CornerSize(25.dp))
) {
Box(
contentAlignment = Alignment.Center

View File

@@ -131,8 +131,7 @@ fun CIFileView(
Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = Color.Transparent,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
contentColor = LocalContentColor.current
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
) {
Box(Modifier.size(32.dp))
}

View File

@@ -88,7 +88,6 @@ fun CIGroupInvitationView(
}) else Modifier,
shape = RoundedCornerShape(18.dp),
color = if (sent) sentColor else receivedColor,
contentColor = LocalContentColor.current
) {
Box(
Modifier

View File

@@ -142,7 +142,6 @@ fun DecryptionErrorItemFixButton(
Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp),
color = receivedColor,
contentColor = LocalContentColor.current
) {
Box(
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
@@ -189,7 +188,6 @@ fun DecryptionErrorItem(
Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp),
color = receivedColor,
contentColor = LocalContentColor.current
) {
Box(
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),

View File

@@ -153,8 +153,7 @@ 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),
contentColor = LocalContentColor.current
shape = RoundedCornerShape(percent = 50)
) {
Box(
Modifier
@@ -265,8 +264,7 @@ private fun progressCircle(progress: Long, total: Long) {
Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = Color.Transparent,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
contentColor = LocalContentColor.current
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
) {
Box(Modifier.size(16.dp))
}

View File

@@ -225,8 +225,7 @@ private fun PlayPauseButton(
Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = if (sent) sentColor else receivedColor,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
contentColor = LocalContentColor.current
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
) {
Box(
Modifier

View File

@@ -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) {
MenuTextColor
if (isInDarkTheme()) MenuTextColorDark else Color.Black
} 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) {
MenuTextColor
if (isInDarkTheme()) MenuTextColorDark else Color.Black
} else color
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Row(verticalAlignment = Alignment.CenterVertically) {

View File

@@ -23,7 +23,6 @@ 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),

View File

@@ -56,7 +56,6 @@ 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),

View File

@@ -26,7 +26,6 @@ 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),

View File

@@ -75,8 +75,6 @@ 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 = {

View File

@@ -30,8 +30,6 @@ 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() } } },
) {

View File

@@ -31,6 +31,7 @@ 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
@@ -302,7 +303,7 @@ fun UserProfileRow(u: User) {
u.displayName,
modifier = Modifier
.padding(start = 10.dp, end = 8.dp),
color = MenuTextColor,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
)
}
@@ -345,7 +346,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 MenuTextColor,
color = if (h.activeHost) MaterialTheme.colors.onBackground else if (isInDarkTheme()) MenuTextColorDark else Color.Black,
fontSize = 14.sp,
)
}
@@ -386,7 +387,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 MenuTextColor,
color = if (active) MaterialTheme.colors.onBackground else if (isInDarkTheme()) MenuTextColorDark else Color.Black,
fontSize = 14.sp,
)
}
@@ -398,7 +399,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 = MenuTextColor)
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
}
}
@@ -408,7 +409,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 = MenuTextColor)
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
}
}
@@ -418,7 +419,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 = MenuTextColor)
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
}
}
@@ -428,7 +429,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 = MenuTextColor)
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
}
}
@@ -438,7 +439,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 = MenuTextColor)
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
}
}

View File

@@ -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)
}
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
private fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
withApi {
try {
if (chatDbChanged.value) {
@@ -406,8 +406,6 @@ private fun stopChatAlert(m: ChatModel) {
)
}
expect fun restartChatOrApp()
private fun exportProhibitedAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.set_password_to_export),
@@ -415,7 +413,7 @@ private fun exportProhibitedAlert() {
)
}
fun authStopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
private fun authStopChat(m: ChatModel) {
if (m.controller.appPrefs.performLA.get()) {
authenticate(
generalGetString(MR.strings.auth_stop_chat),
@@ -423,7 +421,7 @@ fun authStopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
completed = { laResult ->
when (laResult) {
LAResult.Success, is LAResult.Unavailable -> {
stopChat(m, onStop)
stopChat(m)
}
is LAResult.Error -> {
m.chatRunning.value = true
@@ -436,16 +434,15 @@ fun authStopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
}
)
} else {
stopChat(m, onStop)
stopChat(m)
}
}
private fun stopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
private fun stopChat(m: ChatModel) {
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())
@@ -463,10 +460,10 @@ suspend fun deleteChatAsync(m: ChatModel) {
m.controller.apiDeleteStorage()
DatabaseUtils.ksDatabasePassword.remove()
m.controller.appPrefs.storeDBPassphrase.set(true)
deleteAppDatabaseAndFiles()
deleteChatDatabaseFiles()
}
fun deleteAppDatabaseAndFiles() {
fun deleteChatDatabaseFiles() {
val chat = File(dataDir, chatDatabaseFileName)
val chatBak = File(dataDir, "$chatDatabaseFileName.bak")
val agent = File(dataDir, agentDatabaseFileName)
@@ -476,7 +473,6 @@ fun deleteAppDatabaseAndFiles() {
agent.delete()
agentBak.delete()
filesDir.deleteRecursively()
filesDir.mkdir()
remoteHostsDir.deleteRecursively()
tmpDir.deleteRecursively()
tmpDir.mkdir()

View File

@@ -152,8 +152,7 @@ fun CustomTimePickerDialog(
) {
DefaultDialog(onDismissRequest = cancel) {
Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
contentColor = LocalContentColor.current
shape = RoundedCornerShape(corner = CornerSize(25.dp))
) {
Box(
contentAlignment = Alignment.Center

View File

@@ -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(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
class KeyStoreItem(val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
fun get(): String? {
return cryptor.decryptData(
passphrase.get()?.toByteArrayFromBase64ForPassphrase() ?: return null,

View File

@@ -70,7 +70,7 @@ fun <T> ExposedDropDownSetting(
selectionOption.second + (if (label != null) " $label" else ""),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MenuTextColor,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
fontSize = fontSize,
)
}

View File

@@ -1,7 +1,8 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import chat.simplex.common.model.ChatController
import chat.simplex.common.model.ChatModel
@@ -49,7 +50,7 @@ fun authenticateWithPasscode(
close()
completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled)))
}
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
LocalAuthView(ChatModel, LocalAuthRequest(promptTitle, promptSubtitle, password, selfDestruct && ChatController.appPrefs.selfDestruct.get()) {
close()
completed(it)

View File

@@ -26,7 +26,7 @@ fun ModalView(
if (showClose) {
BackHandler(onBack = close)
}
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
Surface(Modifier.fillMaxSize()) {
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
CloseSheetBar(close, showClose, endButtons)
Box(modifier) { content() }

View File

@@ -1,64 +0,0 @@
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),
)
}
}
}
}

View File

@@ -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()
initChatControllerAndRunMigrations(true)
}
LAResult.Success
} else {
@@ -67,8 +67,8 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
* */
chatCloseStore(ctrl)
}
deleteAppDatabaseAndFiles()
// Clear sensitive data on screen just in case ModalManager fails to hide its modals while new database is created
deleteChatDatabaseFiles()
// Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself
m.chatId.value = null
m.chatItems.clear()
m.chats.clear()

View File

@@ -12,7 +12,6 @@ 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,
@@ -52,7 +51,7 @@ fun SetAppPasscodeView(
} else {
SetPasswordView(title, generalGetString(MR.strings.save_verb),
// Do not allow to set app passcode == selfDestruct passcode
submitEnabled = { pwd -> pwd != prohibitedPasscodeKeychain.get() }) {
submitEnabled = { pwd -> pwd != (if (passcodeKeychain.alias == ksSelfDestructPassword.alias) ksAppPassword else ksSelfDestructPassword).get() }) {
enteredPassword = passcode.value
passcode.value = ""
confirming = true

View File

@@ -175,7 +175,7 @@ fun ActionButton(
disabled: Boolean = false,
click: () -> Unit = {}
) {
Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent, contentColor = LocalContentColor.current) {
Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent) {
Column(
Modifier
.clickable(onClick = click)
@@ -220,7 +220,7 @@ fun ActionButton(
disabled: Boolean = false,
click: () -> Unit = {}
) {
Surface(modifier, shape = RoundedCornerShape(18.dp), contentColor = LocalContentColor.current) {
Surface(modifier, shape = RoundedCornerShape(18.dp)) {
Column(
Modifier
.fillMaxWidth()

View File

@@ -380,8 +380,7 @@ 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),
contentColor = LocalContentColor.current
color = Color.Black.copy(alpha = 0f)
) {
val modifier = if (disabled) Modifier else Modifier.clickable { action() }
Row(

View File

@@ -10,11 +10,10 @@ 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
@@ -45,7 +44,6 @@ fun DeveloperView(
m.controller.appPrefs.terminalAlwaysVisible.set(false)
}
}
SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors)
}
}
SectionTextFooter(

View File

@@ -383,7 +383,7 @@ fun SimplexLockView(
}
LAMode.PASSCODE -> {
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
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, contentColor = LocalContentColor.current) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
reason = generalGetString(MR.strings.la_app_passcode),
submit = {
@@ -451,10 +451,9 @@ fun SimplexLockView(
when (laResult) {
LAResult.Success -> {
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
passcodeKeychain = ksSelfDestructPassword,
prohibitedPasscodeKeychain = ksAppPassword,
reason = generalGetString(MR.strings.self_destruct),
submit = {
selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_changed))
@@ -488,7 +487,7 @@ fun SimplexLockView(
}
LAMode.PASSCODE -> {
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
laLockDelay.set(30)
@@ -599,9 +598,9 @@ private fun EnableSelfDestruct(
selfDestruct: SharedPreference<Boolean>,
close: () -> Unit
) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
passcodeKeychain = ksSelfDestructPassword, prohibitedPasscodeKeychain = ksAppPassword, title = generalGetString(MR.strings.set_passcode), reason = generalGetString(MR.strings.enabled_self_destruct_passcode),
passcodeKeychain = ksSelfDestructPassword, 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))

View File

@@ -155,8 +155,7 @@ fun RTCServersLayout(
.height(160.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(10.dp),
border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant),
contentColor = LocalContentColor.current
border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant)
) {
SelectionContainer(
Modifier.verticalScroll(rememberScrollState())

View File

@@ -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), contentColor = LocalContentColor.current, shape = RoundedCornerShape(50)){}
Surface(Modifier.size(50.dp), color = MaterialTheme.colors.background.copy(0.9f), shape = RoundedCornerShape(50)){}
}
CircularProgressIndicator(
Modifier

View File

@@ -90,8 +90,6 @@
<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 -->
@@ -686,7 +684,6 @@
<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>
@@ -1696,10 +1693,6 @@
<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>
@@ -1734,20 +1727,6 @@
<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>
@@ -1772,11 +1751,4 @@
<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>

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 472 B

View File

@@ -16,8 +16,8 @@ import javax.imageio.ImageIO
object NtfManager {
private val prevNtfs = arrayListOf<Pair<ChatId, Slice>>()
fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean {
if (simplexWindowState.windowFocused.value) return false
fun notifyCallInvitation(invitation: RcvCallInvitation) {
if (simplexWindowState.windowFocused.value) return
val contactId = invitation.contact.id
Log.d(TAG, "notifyCallInvitation $contactId")
val image = invitation.contact.image
@@ -45,11 +45,6 @@ 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 }

View File

@@ -15,19 +15,16 @@ val defaultLocale: Locale = Locale.getDefault()
fun initApp() {
ntfManager = object : NtfManager() {
override fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean = chat.simplex.common.model.NtfManager.notifyCallInvitation(invitation)
override fun notifyCallInvitation(invitation: RcvCallInvitation) = 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()
if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
initChatControllerAndRunMigrations()
}
initChatControllerAndRunMigrations(false)
// LALAL
//testCrypto()
}

View File

@@ -167,8 +167,7 @@ actual fun PlatformTextField(
decorationBox = { innerTextField ->
Surface(
shape = RoundedCornerShape(18.dp),
border = BorderStroke(1.dp, MaterialTheme.colors.secondary),
contentColor = LocalContentColor.current
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
) {
Row(
Modifier.background(MaterialTheme.colors.background),

View File

@@ -1,23 +0,0 @@
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)
}
}
}
}

View File

@@ -2,7 +2,8 @@ package chat.simplex.common.views.helpers
import androidx.compose.foundation.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.*
@@ -38,8 +39,7 @@ actual fun DefaultDialog(
) {
Surface(
Modifier
.border(border = BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.3F)), shape = RoundedCornerShape(8)),
contentColor = LocalContentColor.current
.border(border = BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.3F)), shape = RoundedCornerShape(8))
) {
content()
}

View File

@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: ad8cd1d5154617663065652b45c784ad5a0a584d
tag: d0588bd0ac23a459cbfc9a4789633014e91ffa19
source-repository-package
type: git

View File

@@ -0,0 +1,39 @@
# Sending multiple files with a message
## Problem
The immediate problem we need to solve is encrypting local videos, the absense of which creates a lot of confusion and perception that the videos are sent unencrypted.
The reason videos are not encrypted is because they are usually large files that would be slow to decrypt in order to play, and they are also used to generate previews on the fly. The latter is slow anyway, and causes bad rendering experience.
Videos on iOS are compressed from 5.4.2, and the solution to keep them encrypted is to keep video preview in a separate file. It can be done either on the receiving side, when the video is first rendered, or on the sending side, in which case we can send the preview as a separate file.
In general, attaching multiple files to a message could be beneficial for other cases, such as sending hi- and low-res images at the same time, or sending long-form messages with attachments.
## Solutions
1. Extend chat protocol to allow multiple attachments in a single protocol message. This PR has types to support it, and database schema already supports multiple files with chat item. Different message types can allow a limited number of files and interpret them according to their indices in the array. This seems an ad hoc approach, as semantics of the additional attachments are not defined in the protocol and should be implied by file positions.
2. Still allow multiple attachments but add file semantics in the protocol message, alongside FileInvitation object, e.g.:
```
data FileAttachment = FileAttachment
{ file :: FileInvitation,
fileType :: FileAttachmentType
}
```
This format is marginally more complex, but it is more extensible.
3. Instead of allowing multiple attachments in a message, we could allow up to two files for a single attachment (which is what we need now), that could later be useful for messages with multiple attachments as well. This way FileInvitation will be replaced with:
```
data FileAttachment = FileAttachment
{ preview :: Maybe FileInvitation, -- received automatically if "receive images" is enabled
file :: FileInvitation, -- received automatically if "receive images" is enabled for images, in the absense of preview
}
```
4. Add additional protocol message to send additional attachments separately.
5. To solve only original problem of videos, we could add an API to save previews on the first render - this seems the worst approach, as it both complicates the logic of the recipient, without allowing other use cases.

View File

@@ -1,5 +1,5 @@
name: simplex-chat
version: 5.5.0.1
version: 5.5.0.0
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme

View File

@@ -8,9 +8,9 @@ u="$USER"
tmp="$(mktemp -d -t)"
folder="$tmp/simplex-chat"
nix_ver="nix-2.19.2"
nix_ver="nix-2.15.1"
nix_url="https://releases.nixos.org/nix/$nix_ver/install"
nix_hash="435f0d7e11f7c7dffeeab0ec9cc55723f6d3c03352379d785633cf4ddb5caf90"
nix_hash="67aa37f0115195d8ddf32b5d6f471f1e60ecca0fdb3e98bcf54bc147c3078640"
nix_config="sandbox = true
max-jobs = auto
experimental-features = nix-command flakes"
@@ -102,19 +102,8 @@ build() {
sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts"
for arch in $arches; do
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="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux"
android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux"
android_simplex_lib_output="${PWD}/result/pkg-${arch}-android-libsimplex.zip"
android_support_lib_output="${PWD}/result/pkg-${arch}-android-libsupport.zip"
@@ -150,10 +139,6 @@ 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
}

View File

@@ -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/x86_64-linux."$arch"-android:lib:support/latest/download/1 && \
curl --location -o libsupport.zip $job_repo/$arch-android:lib:support.x86_64-linux/latest/download/1 && \
unzip -o libsupport.zip && \
mv libsupport.so "$output_dir" && \
rm libsupport.zip
curl --location -o libsimplex.zip "$job_repo"/x86_64-linux."$arch"-android:lib:simplex-chat/latest/download/1 && \
curl --location -o libsimplex.zip "$job_repo"/"$arch"-android:lib:simplex-chat.x86_64-linux/latest/download/1 && \
unzip -o libsimplex.zip && \
mv libsimplex.so "$output_dir" && \
rm libsimplex.zip

View File

@@ -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."$arch"-darwin-ios:lib:simplex-chat/latest/download/1 && \
curl --location -o "$output_dir"/pkg-ios-"$arch"-swift-json.zip "$job_repo"/"$arch"-darwin-ios:lib:simplex-chat."$arch"-darwin/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

View File

@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."ad8cd1d5154617663065652b45c784ad5a0a584d" = "19sinz1gynab776x8h9va7r6ifm9pmgzljsbc7z5cbkcnjl5sfh3";
"https://github.com/simplex-chat/simplexmq.git"."d0588bd0ac23a459cbfc9a4789633014e91ffa19" = "0b17qy74capb0jyli8f3pg1xi4aawhcgpmaz2ykl9g3605png1na";
"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";

View File

@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 5.5.0.1
version: 5.5.0.0
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat

View File

@@ -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, second)
import Data.Bifunctor (bimap, first)
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA
import qualified Data.ByteString.Base64 as B64
@@ -37,7 +37,6 @@ 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, (<|))
@@ -88,7 +87,6 @@ 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
@@ -235,7 +233,6 @@ 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
@@ -271,7 +268,6 @@ newChatController
expireCIFlags,
cleanupManagerAsync,
timedItemThreads,
chatActivated,
showLiveItems,
encryptLocalFiles,
userXFTPFileConfig,
@@ -315,10 +311,10 @@ cfgServers p DefaultAgentServers {smp, xftp} = case p of
SPSMP -> smp
SPXFTP -> xftp
startChatController :: forall m. ChatMonad' m => Bool -> m (Async ())
startChatController mainApp = do
startChatController :: forall m. ChatMonad' m => Bool -> Bool -> Bool -> m (Async ())
startChatController subConns enableExpireCIs startXFTPWorkers = do
asks smpAgent >>= resumeAgentClient
unless mainApp $
unless subConns $
chatWriteVar subscriptionMode SMOnlyCreate
users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers)
restoreCalls
@@ -328,15 +324,15 @@ startChatController mainApp = do
start s users = do
a1 <- async agentSubscriber
a2 <-
if mainApp
if subConns
then Just <$> async (subscribeUsers False users)
else pure Nothing
atomically . writeTVar s $ Just (a1, a2)
when mainApp $ do
when startXFTPWorkers $ do
startXFTP
void $ forkIO $ startFilesToReceive users
startCleanupManager
startExpireCIs users
startCleanupManager
when enableExpireCIs $ startExpireCIs users
pure a1
startXFTP = do
tmp <- readTVarIO =<< asks tempDirectory
@@ -458,9 +454,8 @@ processChatCommand' vr = \case
withStore' getUsers >>= \case
[] -> pure 1
users -> do
forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} ->
when (n == displayName) . throwChatError $
if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""}
when (any (\User {localDisplayName = n} -> n == displayName) users) $
throwChatError (CEUserExists displayName)
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
@@ -549,17 +544,16 @@ processChatCommand' vr = \case
checkDeleteChatUser user'
withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues
DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_
StartChat mainApp -> withUser' $ \_ ->
StartChat subConns enableExpireCIs startXFTPWorkers -> withUser' $ \_ ->
asks agentAsync >>= readTVarIO >>= \case
Just _ -> pure CRChatRunning
_ -> checkStoreNotChanged $ startChatController mainApp $> CRChatStarted
_ -> checkStoreNotChanged $ startChatController subConns enableExpireCIs startXFTPWorkers $> 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
@@ -567,7 +561,6 @@ processChatCommand' vr = \case
setAllExpireCIFlags True
ok_
APISuspendChat t -> do
chatWriteVar chatActivated False
setAllExpireCIFlags False
stopRemoteCtrl
withAgent (`suspendAgent` t)
@@ -695,15 +688,15 @@ processChatCommand' vr = \case
pure (fileInvitation, ciFile, ft)
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> m (MsgContainer, Maybe (CIQuote 'CTDirect))
prepareMsg fInv_ timed_ = case quotedItemId_ of
Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
Nothing -> pure (MCSimple (ExtMsgContent mc (maybeToList fInv_) (ttl' <$> timed_) (justTrue live)), Nothing)
Just quotedItemId -> do
CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <-
withStore $ \db -> getDirectChatItem db user chatId quotedItemId
(origQmc, qd, sent) <- quoteData qci
let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing}
qmc = quoteContent mc origQmc file
qmc = quoteContent mc origQmc (listToMaybe file)
quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc (maybeToList fInv_) (ttl' <$> timed_) (justTrue live)), Just quotedItem)
where
quoteData :: ChatItem c d -> m (MsgContent, CIQDirection 'CTDirect, Bool)
quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidQuote
@@ -1547,7 +1540,7 @@ processChatCommand' vr = \case
sendAndCount user ll (s, f) ct =
(sendToContact user ct $> (s + 1, f)) `catchChatError` \e -> when (ll <= CLLInfo) (toView $ CRChatError (Just user) e) $> (s, f + 1)
sendToContact user ct = do
(sndMsg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc Nothing))
(sndMsg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc []))
void $ saveSndChatItem user (CDDirectSnd ct) sndMsg (CISndMsgContent mc)
SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do
contactId <- withStore $ \db -> getContactIdByName db user cName
@@ -1913,7 +1906,7 @@ processChatCommand' vr = \case
FileStatus fileId -> withUser $ \user -> do
ci@(AChatItem _ _ _ ChatItem {file}) <- withStore $ \db -> getChatItemByFileId db vr user fileId
case file of
Just CIFile {fileProtocol = FPXFTP} ->
CIFile {fileProtocol = FPXFTP} : _ ->
pure $ CRFileTransferStatusXFTP user ci
_ -> do
fileStatus <- withStore $ \db -> getFileTransferProgress db user fileId
@@ -2142,41 +2135,31 @@ processChatCommand' vr = \case
| otherwise = do
when (n /= n') $ checkValidName n'
-- read contacts before user update to correctly merge preferences
contacts <- withStore' (`getUserContacts` user)
-- [incognito] filter out contacts with whom user has incognito connections
contacts <-
filter (\ct -> contactReady ct && contactActive ct && not (contactConnIncognito ct))
<$> withStore' (`getUserContacts` user)
user' <- updateUser
asks currentUser >>= atomically . (`writeTVar` Just user')
withChatLock "updateProfile" . procCmd $ do
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'
}
ChatConfig {logLevel} <- asks config
summary <- foldM (processAndCount user' logLevel) (UserProfileUpdateSummary 0 0 0 []) contacts
pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary
where
-- [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
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'}
where
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)
notifyContact mergedProfile' ct' = do
void $ sendDirectContactMessage ct' (XInfo mergedProfile')
when (directOrUsed ct') $ createSndFeatureItems user' ct ct'
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'
@@ -2416,24 +2399,17 @@ 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)
Nothing -> pure (MCSimple (ExtMsgContent mc (maybeToList fInv_) (ttl' <$> timed_) (justTrue live)), Nothing)
Just quotedItemId -> do
CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <-
withStore $ \db -> getGroupChatItem db user groupId quotedItemId
(origQmc, qd, sent, GroupMember {memberId}) <- quoteData qci membership
let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Just memberId}
qmc = quoteContent mc origQmc file
qmc = quoteContent mc origQmc (listToMaybe file)
quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc (maybeToList fInv_) (ttl' <$> timed_) (justTrue live)), Just quotedItem)
where
quoteData :: ChatItem c d -> GroupMember -> m (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember)
quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwChatError CEInvalidQuote
@@ -2504,7 +2480,6 @@ 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
@@ -2998,7 +2973,7 @@ cleanupManager = do
stepDelay <- asks (cleanupManagerStepDelay . config)
forever $ do
flip catchChatError (toView . CRChatError Nothing) $ do
waitChatStartedAndActivated
waitChatStarted
users <- withStoreCtx' (Just "cleanupManager, getUsers 1") getUsers
let (us, us') = partition activeUser users
forM_ us $ cleanupUser interval stepDelay
@@ -3008,7 +2983,7 @@ cleanupManager = do
liftIO $ threadDelay' $ diffToMicroseconds interval
where
runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CRChatError Nothing) $ do
waitChatStartedAndActivated
waitChatStarted
users <- withStoreCtx' (Just "cleanupManager, getUsers 2") getUsers
let (us, us') = partition activeUser users
forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CRChatError (Just u))
@@ -3063,7 +3038,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
waitChatStartedAndActivated
waitChatStarted
vr <- chatVersionRange
case cType of
CTDirect -> do
@@ -3089,10 +3064,8 @@ 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
@@ -3111,13 +3084,11 @@ 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
@@ -3240,7 +3211,7 @@ processAgentMsgSndFile _corrId aFileId msg =
-- returns msgDeliveryId of the last file description message
loopSend :: NonEmpty FileDescr -> m Int64
loopSend (fileDescr :| fds) = do
(_, msgDeliveryId) <- sendMsg $ XMsgFileDescr {msgId, fileDescr}
(_, msgDeliveryId) <- sendMsg $ XMsgFileDescr {msgId, fileId = Nothing, fileDescr}
case L.nonEmpty fds of
Just fds' -> loopSend fds'
Nothing -> pure msgDeliveryId
@@ -3385,7 +3356,6 @@ 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
@@ -3394,14 +3364,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
XMsgFileDescr sharedMsgId _fileId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr msgMeta
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
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName
XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta
XInfo p -> xInfo ct' p
XDirectDel -> xDirectDel ct' msg msgMeta
XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
@@ -3409,10 +3379,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
XCallAnswer callId answer -> xCallAnswer ct' callId answer msg
XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg
XCallEnd callId -> xCallEnd ct' callId msg
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
BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta
_ -> messageError $ "unsupported message: " <> T.pack (show event)
let Contact {chatSettings = ChatSettings {sendRcpts}} = ct'
@@ -3468,7 +3438,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
let (UserContactLink {autoAccept}, groupId_, gLinkMemRole) = ucl
forM_ autoAccept $ \(AutoAccept {autoReply = mc_}) ->
forM_ mc_ $ \mc -> do
(msg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc Nothing))
(msg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc []))
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc)
toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
forM_ groupId_ $ \groupId -> do
@@ -3679,15 +3649,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
descrEvent_
| isCompatibleRange (memberChatVRange' m) groupHistoryIncludeWelcomeVRange = do
let GroupInfo {groupProfile = GroupProfile {description}} = gInfo
fmap (\descr -> XMsgNew $ MCSimple $ extMsgContent (MCText descr) Nothing) description
fmap (\descr -> XMsgNew $ MCSimple $ extMsgContent (MCText descr) []) description
| otherwise = Nothing
itemForwardEvents :: CChatItem 'CTGroup -> m [ChatMsgEvent 'Json]
itemForwardEvents cci = case cci of
(CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv sender, content = CIRcvMsgContent mc, file}) -> do
fInvDescr_ <- join <$> forM file getRcvFileInvDescr
fInvDescr_ <- join <$> forM (listToMaybe file) getRcvFileInvDescr
processContentItem sender ci mc fInvDescr_
(CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent mc, file}) -> do
fInvDescr_ <- join <$> forM file getSndFileInvDescr
fInvDescr_ <- join <$> forM (listToMaybe file) getSndFileInvDescr
processContentItem membership ci mc fInvDescr_
_ -> pure []
where
@@ -3735,7 +3705,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of
(Just fileDescrText, Just msgId) -> do
parts <- splitFileDescr fileDescrText
pure . toList $ L.map (XMsgFileDescr msgId) parts
pure . toList $ L.map (XMsgFileDescr msgId Nothing) parts
_ -> pure []
let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents
GroupMember {memberId} = sender
@@ -3770,7 +3740,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
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
cmdId <- createAckCmd conn
let aChatMsgs = parseChatMessages msgBody
withAckMessage agentConnId cmdId msgMeta $ do
@@ -3792,7 +3762,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
updateChatLock "groupMessage" event
case event of
XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False
XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr
XMsgFileDescr sharedMsgId _fileId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr
XMsgUpdate sharedMsgId mContent ttl live -> memberCanSend m' $ groupMessageUpdate gInfo m' sharedMsgId mContent msg brokerTs ttl live
XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m' sharedMsgId memberId msg brokerTs
XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m' sharedMsgId memberId reaction add msg brokerTs
@@ -4261,6 +4231,7 @@ 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
@@ -4275,7 +4246,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
let ExtMsgContent _ _ itemTTL live_ = mcExtMsgContent mc
timed_ = rcvContactCITimed ct itemTTL
live = fromMaybe False live_
file_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct
file_ <- processFileInvitation (listToMaybe fInv_) content $ \db -> createRcvFileTransfer db userId ct
newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live
autoAcceptFile file_
where
@@ -4290,8 +4261,9 @@ 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 -> m ()
messageFileDescription Contact {contactId} sharedMsgId fileDescr = do
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> MsgMeta -> m ()
messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
processFDMessage fileId fileDescr
@@ -4334,6 +4306,7 @@ 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).
@@ -4366,10 +4339,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 = do
messageDelete ct@Contact {contactId} sharedMsgId RcvMessage {msgId} msgMeta@MsgMeta {broker = (_, brokerTs)} = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
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
@@ -4433,7 +4406,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> UTCTime -> Bool -> m ()
newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} brokerTs forwarded
| isVoice content && not (groupFeatureAllowed SGFVoice gInfo) = rejected GFVoice
| not (isVoice content) && isJust fInv_ && not (groupFeatureAllowed SGFFiles gInfo) = rejected GFFiles
| not (isVoice content) && isJust (listToMaybe fInv_) && not (groupFeatureAllowed SGFFiles gInfo) = rejected GFFiles
| otherwise = do
let timed_ =
if forwarded
@@ -4456,11 +4429,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
ci' <- withStore' $ \db -> updateGroupChatItemModerated db user gInfo ci moderator moderatedAt
toView $ CRNewChatItem user $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci'
| otherwise = do
file_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m
file_ <- processFileInvitation (listToMaybe fInv_) content $ \db -> createRcvGroupFileTransfer db userId m
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs (CIRcvMsgContent content) (snd <$> file_) timed_ False
toView =<< markGroupCIDeleted user gInfo ci createdByMsgId False (Just moderator) moderatedAt
createItem timed_ live = do
file_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m
file_ <- processFileInvitation (listToMaybe fInv_) content $ \db -> createRcvGroupFileTransfer db userId m
newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live
when (showMessages $ memberSettings m) $ autoAcceptFile file_
newChatItem ciContent ciFile_ timed_ live = do
@@ -4537,6 +4510,7 @@ 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
@@ -4573,8 +4547,9 @@ 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 -> m ()
xFileCancel Contact {contactId} sharedMsgId = do
xFileCancel :: Contact -> SharedMsgId -> MsgMeta -> m ()
xFileCancel ct@Contact {contactId} sharedMsgId msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
ft <- withStore (\db -> getRcvFileTransfer db user fileId)
unless (rcvFileCompleteOrCancelled ft) $ do
@@ -4582,8 +4557,9 @@ 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 -> m ()
xFileAcptInv ct sharedMsgId fileConnReq_ fName = do
xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m ()
xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId
(AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId
assertSMPAcceptNotProhibited ci
@@ -4611,7 +4587,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
else messageError "x.file.acpt.inv: fileName is different from expected"
assertSMPAcceptNotProhibited :: ChatItem c d -> m ()
assertSMPAcceptNotProhibited ChatItem {file = Just CIFile {fileId, fileProtocol}, content}
assertSMPAcceptNotProhibited ChatItem {file = CIFile {fileId, fileProtocol} : _, content}
| fileProtocol == FPXFTP && not (imageOrVoice content) = throwChatError $ CEFallbackToSMPProhibited fileId
| otherwise = pure ()
where
@@ -4630,7 +4606,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
liftIO $ deleteSndFileChunks db sft
updateDirectCIFileStatus db vr user fileId CIFSSndComplete
case file of
Just CIFile {fileProtocol = FPXFTP} -> do
CIFile {fileProtocol = FPXFTP} : _ -> do
ft <- withStore $ \db -> getFileTransferMeta db user fileId
toView $ CRSndFileCompleteXFTP user ci ft
_ -> toView $ CRSndFileComplete user ci sft
@@ -4717,6 +4693,7 @@ 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
@@ -4748,9 +4725,7 @@ 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)
`catchChatError` \_ -> pure ()
MsgError e -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs)
xInfo :: Contact -> Profile -> m ()
xInfo c p' = void $ processContactProfileUpdate c p' True
@@ -4759,6 +4734,7 @@ 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
@@ -4918,6 +4894,7 @@ 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
@@ -4944,9 +4921,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 -> m ()
xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey} msg = do
msgCurrentCall ct callId "x.call.offer" msg $
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 $
\call -> case callState call of
CallInvitationSent {localCallType, localDhPrivKey} -> do
let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> localDhPrivKey)
@@ -4959,9 +4936,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
pure (Just call, Nothing)
-- to party accepting call
xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> m ()
xCallAnswer ct callId CallAnswer {rtcSession} msg = do
msgCurrentCall ct callId "x.call.answer" msg $
xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> MsgMeta -> m ()
xCallAnswer ct callId CallAnswer {rtcSession} msg msgMeta = do
msgCurrentCall ct callId "x.call.answer" msg msgMeta $
\call -> case callState call of
CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do
let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession = rtcSession, sharedKey}
@@ -4972,9 +4949,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
pure (Just call, Nothing)
-- to any call party
xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> m ()
xCallExtra ct callId CallExtraInfo {rtcExtraInfo} msg = do
msgCurrentCall ct callId "x.call.extra" msg $
xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> MsgMeta -> m ()
xCallExtra ct callId CallExtraInfo {rtcExtraInfo} msg msgMeta = do
msgCurrentCall ct callId "x.call.extra" msg msgMeta $
\call -> case callState call of
CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do
-- TODO update the list of ice servers in peerCallSession
@@ -4991,14 +4968,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
pure (Just call, Nothing)
-- to any call party
xCallEnd :: Contact -> CallId -> RcvMessage -> m ()
xCallEnd ct callId msg =
msgCurrentCall ct callId "x.call.end" msg $ \Call {chatItemId} -> do
xCallEnd :: Contact -> CallId -> RcvMessage -> MsgMeta -> m ()
xCallEnd ct callId msg msgMeta =
msgCurrentCall ct callId "x.call.end" msg msgMeta $ \Call {chatItemId} -> do
toView $ CRCallEnded user ct
(Nothing,) <$> callStatusItemContent user ct chatItemId WCSDisconnected
msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> (Call -> m (Maybe Call, Maybe ACIContent)) -> m ()
msgCurrentCall ct@Contact {contactId = ctId'} callId' eventName RcvMessage {msgId} action = do
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
calls <- asks currentCalls
atomically (TM.lookup ctId' calls) >>= \case
Nothing -> messageError $ eventName <> ": no current call"
@@ -5365,7 +5343,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg
case event of
XMsgNew mc -> memberCanSend author $ newGroupContentMessage gInfo author mc rcvMsg msgTs True
XMsgFileDescr sharedMsgId fileDescr -> memberCanSend author $ groupMessageFileDescription gInfo author sharedMsgId fileDescr
XMsgFileDescr sharedMsgId _fileId fileDescr -> memberCanSend author $ groupMessageFileDescription gInfo author sharedMsgId fileDescr
XMsgUpdate sharedMsgId mContent ttl live -> memberCanSend author $ groupMessageUpdate gInfo author sharedMsgId mContent rcvMsg msgTs ttl live
XMsgDel sharedMsgId memId -> groupMessageDelete gInfo author sharedMsgId memId rcvMsg msgTs
XMsgReact sharedMsgId (Just memId) reaction add -> groupMsgReaction gInfo author sharedMsgId memId reaction add rcvMsg msgTs
@@ -5653,20 +5631,12 @@ deleteOrUpdateMemberRecord user@User {userId} member =
Nothing -> deleteGroupMember db user member
sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64)
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
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)
sendDirectMessage :: (MsgEncodingI e, ChatMonad m) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m (SndMessage, Int64)
sendDirectMessage conn chatMsgEvent connOrGroupId = do
@@ -5675,25 +5645,18 @@ 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 =
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
createSndMessage chatMsgEvent connOrGroupId = do
gVar <- asks random
vr <- chatVersionRange
withStoreBatch $ \db -> fmap (uncurry (createMsg db gVar vr)) idsEvents
withStore $ \db -> createNewSndMessage db gVar connOrGroupId chatMsgEvent (encodeMessage vr)
where
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}
encodeMessage chatVRange sharedMsgId =
encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent}
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)
let idsEvts = L.map (GroupId groupId,) events
(errs, msgs) <- partitionEithers . L.toList <$> createSndMessages idsEvts
(errs, msgs) <- partitionEithers <$> createSndMessages
unless (null errs) $ toView $ CRChatErrors (Just user) errs
unless (null msgs) $ do
let (errs', msgBatches) = partitionEithers $ batchMessages maxChatMsgSize msgs
@@ -5708,6 +5671,16 @@ 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
@@ -5728,23 +5701,14 @@ deliverMessage' conn msgFlags msgBody msgId =
[r] -> liftEither r
rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
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)
deliverMessages :: ChatMonad' m => [(Connection, MsgFlags, LazyMsgBody, MessageId)] -> m [Either ChatError Int64]
deliverMessages msgReqs = do
sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessages` aReqs)
withStoreBatch $ \db -> map (bindRight $ createDelivery db) sent
where
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)
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)
createDelivery db ((Connection {connId}, _, _, msgId), agentMsgId) =
Right <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId
@@ -5890,7 +5854,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
pure $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemTimed live createdAt Nothing createdAt
liftIO $ 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 =
@@ -5904,24 +5868,24 @@ 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)
pure $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemTimed live brokerTs forwardedByMember createdAt
liftIO $ 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 -> ChatItem c d
mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs forwardedByMember currentTs =
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
let itemText = ciContentToText content
itemStatus = ciCreateStatus content
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs
in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file}
pure ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file = maybeToList file}
deleteDirectCI :: (ChatMonad m, MsgDirectionI d) => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> m ChatResponse
deleteDirectCI user ct ci@ChatItem {file} byUser timed = do
deleteCIFile user file
deleteCIFile user $ listToMaybe file
withStoreCtx' (Just "deleteDirectCI, deleteDirectChatItem") $ \db -> deleteDirectChatItem db user ct ci
pure $ CRChatItemDeleted user (AChatItem SCTDirect msgDirection (DirectChat ct) ci) Nothing byUser timed
deleteGroupCI :: (ChatMonad m, MsgDirectionI d) => User -> GroupInfo -> ChatItem 'CTGroup d -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse
deleteGroupCI user gInfo ci@ChatItem {file} byUser timed byGroupMember_ deletedTs = do
deleteCIFile user file
deleteCIFile user $ listToMaybe file
toCi <- withStoreCtx' (Just "deleteGroupCI, deleteGroupChatItem ...") $ \db ->
case byGroupMember_ of
Nothing -> deleteGroupChatItem db user gInfo ci $> Nothing
@@ -5938,7 +5902,7 @@ deleteCIFile user file_ =
markDirectCIDeleted :: (ChatMonad m, MsgDirectionI d) => User -> Contact -> ChatItem 'CTDirect d -> MessageId -> Bool -> UTCTime -> m ChatResponse
markDirectCIDeleted user ct ci@ChatItem {file} msgId byUser deletedTs = do
cancelCIFile user file
cancelCIFile user $ listToMaybe file
ci' <- withStore' $ \db -> markDirectChatItemDeleted db user ct ci msgId deletedTs
pure $ CRChatItemDeleted user (ctItem ci) (Just $ ctItem ci') byUser False
where
@@ -5946,7 +5910,7 @@ markDirectCIDeleted user ct ci@ChatItem {file} msgId byUser deletedTs = do
markGroupCIDeleted :: (ChatMonad m, MsgDirectionI d) => User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse
markGroupCIDeleted user gInfo ci@ChatItem {file} msgId byUser byGroupMember_ deletedTs = do
cancelCIFile user file
cancelCIFile user $ listToMaybe file
ci' <- withStore' $ \db -> markGroupChatItemDeleted db user gInfo ci msgId byGroupMember_ deletedTs
pure $ CRChatItemDeleted user (gItem ci) (Just $ gItem ci') byUser False
where
@@ -6024,15 +5988,6 @@ 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 ::
@@ -6046,44 +6001,24 @@ createFeatureItems ::
FeatureContent FeatureAllowed d ->
(forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) ->
m ()
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
createFeatureItems user Contact {mergedPreferences = cups} ct'@Contact {mergedPreferences = cups'} chatDir ciFeature ciOffer getPref =
forM_ allChatFeatures $ \(ACF f) -> createItem f
where
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)
createItem :: forall f. FeatureI f => SChatFeature f -> m ()
createItem f
| state /= state' = create ciFeature state'
| prefState /= prefState' = create ciOffer prefState'
| otherwise = pure ()
where
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'
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'
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'} =
@@ -6097,35 +6032,15 @@ createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences
sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool
sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing}
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
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
createdAt <- liftIO getCurrentTime
let itemTs = fromMaybe createdAt itemTs_
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
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)
getCreateActiveUser :: SQLiteStore -> Bool -> IO User
getCreateActiveUser st testView = do
@@ -6218,14 +6133,10 @@ checkSameUser userId User {userId = activeUserId} = when (userId /= activeUserId
chatStarted :: ChatMonad m => m Bool
chatStarted = fmap isJust . readTVarIO =<< asks agentAsync
waitChatStartedAndActivated :: ChatMonad m => m ()
waitChatStartedAndActivated = do
waitChatStarted :: ChatMonad m => m ()
waitChatStarted = do
agentStarted <- asks agentAsync
chatActivated <- asks chatActivated
atomically $ do
started <- readTVar agentStarted
activated <- readTVar chatActivated
unless (isJust started && activated) retry
atomically $ readTVar agentStarted >>= \a -> unless (isJust a) retry
chatVersionRange :: ChatMonad' m => m VersionRange
chatVersionRange = do
@@ -6262,8 +6173,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 main=" *> (StartChat <$> onOffP),
"/_start" $> StartChat True,
"/_start subscribe=" *> (StartChat <$> onOffP <* " expire=" <*> onOffP <* " xftp=" <*> onOffP),
"/_start" $> StartChat True True True,
"/_stop" $> APIStopChat,
"/_app activate restore=" *> (APIActivateChat <$> onOffP),
"/_app activate" $> APIActivateChat True,

View File

@@ -200,7 +200,6 @@ 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,
@@ -234,7 +233,7 @@ data ChatCommand
| UnmuteUser
| APIDeleteUser UserId Bool (Maybe UserPwd)
| DeleteUser UserName Bool (Maybe UserPwd)
| StartChat {mainApp :: Bool}
| StartChat {subscribeConnections :: Bool, enableExpireChatItems :: Bool, startXFTPWorkers :: Bool}
| APIStopChat
| APIActivateChat {restoreChat :: Bool}
| APISuspendChat {suspendTimeout :: Int}
@@ -895,7 +894,8 @@ data PendingSubStatus = PendingSubStatus
deriving (Show)
data UserProfileUpdateSummary = UserProfileUpdateSummary
{ updateSuccesses :: Int,
{ notChanged :: Int,
updateSuccesses :: Int,
updateFailures :: Int,
changedContacts :: [Contact]
}

View File

@@ -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) cc
a1 <- runReaderT (startChatController True True True) cc
a2 <- async $ chat u cc
waitEither_ a1 a2

View File

@@ -146,7 +146,7 @@ data ChatItem (c :: ChatType) (d :: MsgDirection) = ChatItem
formattedText :: Maybe MarkdownList,
quotedItem :: Maybe (CIQuote c),
reactions :: [CIReactionCount],
file :: Maybe (CIFile d)
file :: [CIFile d]
}
deriving (Show)
@@ -300,10 +300,11 @@ aChatItemId (AChatItem _ _ _ ci) = chatItemId' ci
aChatItemTs :: AChatItem -> UTCTime
aChatItemTs (AChatItem _ _ _ ci) = chatItemTs' ci
-- TODO multiple files
updateFileStatus :: forall c d. ChatItem c d -> CIFileStatus d -> ChatItem c d
updateFileStatus ci@ChatItem {file} status = case file of
Just f -> ci {file = Just (f :: CIFile d) {fileStatus = status}}
Nothing -> ci
f : _ -> ci {file = [(f :: CIFile d) {fileStatus = status}]}
[] -> ci
-- This type is not saved to DB, so all JSON encodings are platform-specific
data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta

View File

@@ -3,7 +3,7 @@
module Simplex.Chat.Mobile.Shared where
import qualified Data.ByteString as B
import Data.ByteString.Internal (ByteString (..))
import Data.ByteString.Internal (ByteString (..), memcpy)
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 -> copyBytes ptr (p `plusPtr` offset) len
withForeignPtr fp $ \p -> memcpy ptr (p `plusPtr` offset) len
{-# INLINE putByteString #-}
putLazyByteString :: Ptr Word8 -> LB.ByteString -> IO ()

View File

@@ -225,7 +225,7 @@ data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMess
data ChatMsgEvent (e :: MsgEncoding) where
XMsgNew :: MsgContainer -> ChatMsgEvent 'Json
XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr} -> ChatMsgEvent 'Json
XMsgFileDescr :: {msgId :: SharedMsgId, fileId :: Maybe Int, fileDescr :: FileDescr} -> ChatMsgEvent 'Json
XMsgUpdate :: {msgId :: SharedMsgId, content :: MsgContent, ttl :: Maybe Int, live :: Maybe Bool} -> ChatMsgEvent 'Json
XMsgDel :: SharedMsgId -> Maybe MemberId -> ChatMsgEvent 'Json
XMsgDeleted :: ChatMsgEvent 'Json
@@ -277,10 +277,10 @@ deriving instance Show AChatMsgEvent
isForwardedGroupMsg :: ChatMsgEvent e -> Bool
isForwardedGroupMsg ev = case ev of
XMsgNew mc -> case mcExtMsgContent mc of
ExtMsgContent {file = Just FileInvitation {fileInline = Just _}} -> False
_ -> True
XMsgFileDescr _ _ -> True
XMsgNew mc ->
let ExtMsgContent {file} = mcExtMsgContent mc
in all (\case FileInvitation {fileInline = Just _} -> False; _ -> True) file
XMsgFileDescr _ _ _ -> True
XMsgUpdate {} -> True
XMsgDel _ _ -> True
XMsgReact {} -> True
@@ -488,7 +488,7 @@ msgContentTag = \case
MCFile {} -> MCFile_
MCUnknown {tag} -> MCUnknown_ tag
data ExtMsgContent = ExtMsgContent {content :: MsgContent, file :: Maybe FileInvitation, ttl :: Maybe Int, live :: Maybe Bool}
data ExtMsgContent = ExtMsgContent {content :: MsgContent, file :: [FileInvitation], ttl :: Maybe Int, live :: Maybe Bool}
deriving (Eq, Show)
$(JQ.deriveJSON defaultJSON ''QuotedMsg)
@@ -528,9 +528,9 @@ parseMsgContainer v =
<|> (v .: "forward" >>= \f -> (if f then MCForward else MCSimple) <$> mc)
<|> MCSimple <$> mc
where
mc = ExtMsgContent <$> v .: "content" <*> v .:? "file" <*> v .:? "ttl" <*> v .:? "live"
mc = ExtMsgContent <$> v .: "content" <*> (maybe [] msgFiles <$> v .:? "file") <*> v .:? "ttl" <*> v .:? "live"
extMsgContent :: MsgContent -> Maybe FileInvitation -> ExtMsgContent
extMsgContent :: MsgContent -> [FileInvitation] -> ExtMsgContent
extMsgContent mc file = ExtMsgContent mc file Nothing Nothing
justTrue :: Bool -> Maybe Bool
@@ -575,7 +575,19 @@ msgContainerJSON = \case
MCSimple mc -> o $ msgContent mc
where
o = JM.fromList
msgContent (ExtMsgContent c file ttl live) = ("file" .=? file) $ ("ttl" .=? ttl) $ ("live" .=? live) ["content" .= c]
msgContent (ExtMsgContent c file ttl live) = ("file" .=? filesToJSON file) $ ("ttl" .=? ttl) $ ("live" .=? live) ["content" .= c]
newtype MsgFiles = MsgFiles {msgFiles :: [FileInvitation]}
deriving (Eq, Show)
instance FromJSON MsgFiles where
parseJSON v = MsgFiles <$> (((: []) <$> parseJSON v) <|> parseJSON v)
filesToJSON :: [FileInvitation] -> Maybe J.Value
filesToJSON = \case
[] -> Nothing
[f] -> Just $ J.toJSON f
fs -> Just $ J.toJSON fs
instance ToJSON MsgContent where
toJSON = \case
@@ -750,7 +762,7 @@ instance StrEncoding ACMEventTag where
toCMEventTag :: ChatMsgEvent e -> CMEventTag e
toCMEventTag msg = case msg of
XMsgNew _ -> XMsgNew_
XMsgFileDescr _ _ -> XMsgFileDescr_
XMsgFileDescr _ _ _ -> XMsgFileDescr_
XMsgUpdate {} -> XMsgUpdate_
XMsgDel {} -> XMsgDel_
XMsgDeleted -> XMsgDeleted_
@@ -849,7 +861,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do
msg :: CMEventTag 'Json -> Either String (ChatMsgEvent 'Json)
msg = \case
XMsgNew_ -> XMsgNew <$> JT.parseEither parseMsgContainer params
XMsgFileDescr_ -> XMsgFileDescr <$> p "msgId" <*> p "fileDescr"
XMsgFileDescr_ -> XMsgFileDescr <$> p "msgId" <*> opt "fileId" <*> p "fileDescr"
XMsgUpdate_ -> XMsgUpdate <$> p "msgId" <*> p "content" <*> opt "ttl" <*> opt "live"
XMsgDel_ -> XMsgDel <$> p "msgId" <*> opt "memberId"
XMsgDeleted_ -> pure XMsgDeleted
@@ -909,7 +921,7 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @
params :: ChatMsgEvent 'Json -> J.Object
params = \case
XMsgNew container -> msgContainerJSON container
XMsgFileDescr msgId' fileDescr -> o ["msgId" .= msgId', "fileDescr" .= fileDescr]
XMsgFileDescr msgId' fileId fileDescr -> o $ ("fileId" .=? fileId) ["msgId" .= msgId', "fileDescr" .= fileDescr]
XMsgUpdate msgId' content ttl live -> o $ ("ttl" .=? ttl) $ ("live" .=? live) ["msgId" .= msgId', "content" .= content]
XMsgDel msgId' memberId -> o $ ("memberId" .=? memberId) ["msgId" .= msgId']
XMsgDeleted -> JM.empty

View File

@@ -460,23 +460,24 @@ 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 = do
insertHost_ currentTs groupId = ExceptT $ do
let fromMemberProfile = profileFromName fromMemberName
(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
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
setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
setViaGroupLinkHash db groupId connId =

View File

@@ -112,7 +112,7 @@ import Data.ByteString.Char8 (ByteString)
import Data.Either (fromRight, rights)
import Data.Int (Int64)
import Data.List (sortBy)
import Data.Maybe (fromMaybe, isJust, mapMaybe)
import Data.Maybe (fromMaybe, isJust, mapMaybe, maybeToList)
import Data.Ord (Down (..), comparing)
import Data.Text (Text)
import Data.Time (addUTCTime)
@@ -1122,7 +1122,7 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
_ -> Nothing
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTDirect d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTDirect
cItem d chatDir ciStatus content file =
CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, formattedText = parseMaybeMarkdownList itemText, quotedItem = toDirectQuote quoteRow, reactions = [], file}
CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, formattedText = parseMaybeMarkdownList itemText, quotedItem = toDirectQuote quoteRow, reactions = [], file = maybeToList file}
badItem = Left $ SEBadChatItem itemId
ciMeta :: CIContent d -> CIStatus d -> CIMeta 'CTDirect d
ciMeta content status =
@@ -1173,7 +1173,7 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir,
_ -> Nothing
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTGroup d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTGroup
cItem d chatDir ciStatus content file =
CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, formattedText = parseMaybeMarkdownList itemText, quotedItem = toGroupQuote quoteRow quotedMember_, reactions = [], file}
CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, formattedText = parseMaybeMarkdownList itemText, quotedItem = toGroupQuote quoteRow quotedMember_, reactions = [], file = maybeToList file}
badItem = Left $ SEBadChatItem itemId
ciMeta :: CIContent d -> CIStatus d -> CIMeta 'CTGroup d
ciMeta content status =

View File

@@ -24,7 +24,7 @@ import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe)
import Data.Maybe (fromMaybe, isJust, isNothing, listToMaybe, mapMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1)
@@ -411,7 +411,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
Just CIQuote {chatDir = quoteDir, content} ->
Just (msgDirectionInt $ quoteMsgDirection quoteDir, msgContentText content)
fPath = case file of
Just CIFile {fileSource = Just (CryptoFile fp _)} -> Just fp
CIFile {fileSource = Just (CryptoFile fp _)} : _ -> Just fp
_ -> Nothing
testViewItem :: CChatItem c -> Maybe GroupMember -> Text
testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText}}) membership_ =
@@ -561,12 +561,12 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember},
Just _ -> item <> styled (colored Yellow) (" [>>]" :: String)
withSndFile = withFile viewSentFileInvitation
withRcvFile = withFile viewReceivedFileInvitation
withFile view dir l = maybe l (\f -> l <> view dir f ts tz meta) file
withFile view dir l = maybe l (\f -> l <> view dir f ts tz meta) $ listToMaybe file
sndMsg = msg viewSentMessage
rcvMsg = msg viewReceivedMessage
msg view dir quote mc = case (msgContentText mc, file, quote) of
("", Just _, []) -> []
("", Just CIFile {fileName}, _) -> view dir quote (MCText $ T.pack fileName) ts tz meta
("", _ : _, []) -> []
("", CIFile {fileName} : _, _) -> view dir quote (MCText $ T.pack fileName) ts tz meta
_ -> view dir quote mc ts tz meta
showSndItem to = showItem $ sentWithTime_ ts tz [to <> plainContent content] meta
showRcvItem from = showItem $ receivedWithTime_ ts tz from [] meta [plainContent content] False
@@ -1514,9 +1514,9 @@ sendingFile_ status ft@SndFileTransfer {recipientDisplayName = c} =
[status <> " sending " <> sndFile ft <> " to " <> ttyContact c]
uploadingFile :: StyledString -> AChatItem -> [StyledString]
uploadingFile status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectSnd}) =
uploadingFile status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = CIFile {fileId, fileName} : _, chatDir = CIDirectSnd}) =
[status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyContact c]
uploadingFile status (AChatItem _ _ (GroupChat g) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd}) =
uploadingFile status (AChatItem _ _ (GroupChat g) ChatItem {file = CIFile {fileId, fileName} : _, chatDir = CIGroupSnd}) =
[status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyGroup' g]
uploadingFile status _ = [status <> " uploading file"] -- shouldn't happen
@@ -1546,12 +1546,12 @@ humanReadableSize size
gB = mB * 1024
savingFile' :: AChatItem -> [StyledString]
savingFile' (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath _)}, chatDir}) =
savingFile' (AChatItem _ _ chat ChatItem {file = CIFile {fileId, fileSource = Just (CryptoFile filePath _)} : _, chatDir}) =
["saving file " <> sShow fileId <> fileFrom chat chatDir <> " to " <> plain filePath]
savingFile' _ = ["saving file"] -- shouldn't happen
receivingFile_' :: (Maybe RemoteHostId, Maybe User) -> Bool -> String -> AChatItem -> [StyledString]
receivingFile_' hu testView status (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileName, fileSource = Just f@(CryptoFile _ cfArgs_)}, chatDir}) =
receivingFile_' hu testView status (AChatItem _ _ chat ChatItem {file = CIFile {fileId, fileName, fileSource = Just f@(CryptoFile _ cfArgs_)} : _, chatDir}) =
[plain status <> " receiving " <> fileTransferStr fileId fileName <> fileFrom chat chatDir] <> cfArgsStr cfArgs_ <> getRemoteFileStr
where
cfArgsStr (Just cfArgs) = [plain (cryptoFileArgsStr testView cfArgs) | status == "completed"]
@@ -1620,7 +1620,7 @@ viewFileTransferStatus (FTRcv ft@RcvFileTransfer {fileId, fileInvitation = FileI
RFSCancelled Nothing -> "cancelled"
viewFileTransferStatusXFTP :: AChatItem -> [StyledString]
viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName, fileSize, fileStatus, fileSource}}) =
viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = CIFile {fileId, fileName, fileSize, fileStatus, fileSource} : _}) =
case fileStatus of
CIFSSndStored -> ["sending " <> fstr <> " just started"]
CIFSSndTransfer progress total -> ["sending " <> fstr <> " in progress " <> fileProgressXFTP progress total fileSize]

View File

@@ -415,7 +415,6 @@ xftpServerConfig =
logStatsStartTime = 0,
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
serverStatsBackupFile = Nothing,
controlPort = Nothing,
transportConfig = defaultTransportServerConfig
}

View File

@@ -1149,7 +1149,7 @@ testSubscribeAppNSE tmp =
alice ##> "/_app suspend 1"
alice <## "ok"
alice <## "chat suspended"
nseAlice ##> "/_start main=off"
nseAlice ##> "/_start subscribe=off expire=off xftp=off"
nseAlice <## "chat started"
nseAlice ##> "/ad"
cLink <- getContactLink nseAlice True

Some files were not shown because too many files have changed in this diff Show More