Compare commits
1 Commits
av/android
...
ep/rfc-att
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f0d400fdc |
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -484,7 +484,6 @@ func deleteChatAsync() async throws {
|
||||
try await apiDeleteStorage()
|
||||
_ = kcDatabasePassword.remove()
|
||||
storeDBPassphraseGroupDefault.set(true)
|
||||
deleteAppDatabaseAndFiles()
|
||||
}
|
||||
|
||||
struct DatabaseView_Previews: PreviewProvider {
|
||||
|
||||
@@ -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: "")
|
||||
|
||||
@@ -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: {}
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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!"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>";
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,4 @@
|
||||
|
||||
void haskell_init(void);
|
||||
|
||||
void haskell_init_nse(void);
|
||||
|
||||
#endif /* hs_init_h */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package chat.simplex.common.views.database
|
||||
|
||||
import chat.simplex.common.views.usersettings.restartApp
|
||||
|
||||
actual fun restartChatOrApp() {
|
||||
restartApp()
|
||||
}
|
||||
@@ -28,7 +28,7 @@ actual fun SettingsSectionApp(
|
||||
}
|
||||
|
||||
|
||||
fun restartApp() {
|
||||
private fun restartApp() {
|
||||
ProcessPhoenix.triggerRebirth(androidAppContext)
|
||||
shutdownApp()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@ fun CIGroupInvitationView(
|
||||
}) else Modifier,
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (sent) sentColor else receivedColor,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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() } } },
|
||||
) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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 |
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
39
docs/rfcs/2024-01-02-multiple-attachments.md
Normal file
39
docs/rfcs/2024-01-02-multiple-attachments.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -415,7 +415,6 @@ xftpServerConfig =
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
|
||||
serverStatsBackupFile = Nothing,
|
||||
controlPort = Nothing,
|
||||
transportConfig = defaultTransportServerConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -1149,7 +1149,7 @@ testSubscribeAppNSE tmp =
|
||||
alice ##> "/_app suspend 1"
|
||||
alice <## "ok"
|
||||
alice <## "chat suspended"
|
||||
nseAlice ##> "/_start 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
Reference in New Issue
Block a user