Merge pull request #753 from simplex-chat/ios-notifications

iOS notifications
This commit is contained in:
JRoberts
2022-07-04 16:27:53 +04:00
committed by GitHub
106 changed files with 5673 additions and 3153 deletions
+15 -24
View File
@@ -8,6 +8,7 @@
import Foundation
import UIKit
import SimpleXChat
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
@@ -20,18 +21,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
let m = ChatModel.shared
m.deviceToken = token
UserDefaults.standard.set(false, forKey: DEFAULT_USE_NOTIFICATIONS)
// let useNotifications = UserDefaults.standard.bool(forKey: "useNotifications")
// if useNotifications {
// Task {
// do {
// m.tokenStatus = try await apiRegisterToken(token: token)
// } catch {
// logger.error("apiRegisterToken error: \(responseError(error))")
// }
// }
// }
let deviceToken = DeviceToken(pushProvider: PushProvider(env: pushEnvironment), token: token)
m.deviceToken = deviceToken
if m.savedToken != nil {
registerToken(token: deviceToken)
}
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
@@ -42,19 +36,19 @@ class AppDelegate: NSObject, UIApplicationDelegate {
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
logger.debug("AppDelegate: didReceiveRemoteNotification")
print("*** userInfo", userInfo)
let m = ChatModel.shared
if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any],
UserDefaults.standard.bool(forKey: "useNotifications") {
m.notificationMode != .off {
if let verification = ntfData["verification"] as? String,
let nonce = ntfData["nonce"] as? String {
if let token = ChatModel.shared.deviceToken {
logger.debug("AppDelegate: didReceiveRemoteNotification: verification, confirming \(verification)")
Task {
let m = ChatModel.shared
do {
if case .active = m.tokenStatus {} else { m.tokenStatus = .confirmed }
try await apiVerifyToken(token: token, code: verification, nonce: nonce)
try await apiVerifyToken(token: token, nonce: nonce, code: verification)
m.tokenStatus = .active
try await apiIntervalNofication(token: token, interval: 20)
} catch {
if let cr = error as? ChatResponse, case .chatCmdError(.errorAgent(.NTF(.AUTH))) = cr {
m.tokenStatus = .expired
@@ -67,15 +61,12 @@ class AppDelegate: NSObject, UIApplicationDelegate {
completionHandler(.noData)
}
} else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages {
// TODO check if app in background
logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages")
// TODO remove
// NtfManager.shared.notifyCheckingMessages()
receiveMessages(completionHandler)
} else if let smpQueue = ntfData["checkMessage"] as? String {
// TODO check if app in background
logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessage \(smpQueue)")
receiveMessages(completionHandler)
if appStateGroupDefault.get().inactive {
receiveMessages(completionHandler)
} else {
completionHandler(.noData)
}
} else {
completionHandler(.noData)
}
+32 -26
View File
@@ -22,39 +22,45 @@ struct ContentView: View {
ZStack {
if prefPerformLA && userAuthorized != true {
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
} else {
if let step = chatModel.onboardingStage {
if case .onboardingComplete = step,
chatModel.currentUser != nil {
ZStack(alignment: .top) {
ChatListView(showChatInfo: $showChatInfo)
.onAppear {
NtfManager.shared.requestAuthorization(onDeny: {
alertManager.showAlert(notificationAlert())
})
// Local Authentication notice is to be shown on next start after onboarding is complete
if (!prefLANoticeShown && prefShowLANotice) {
prefLANoticeShown = true
alertManager.showAlert(laNoticeAlert())
}
prefShowLANotice = true
}
if chatModel.showCallView, let call = chatModel.activeCall {
ActiveCallView(call: call)
}
IncomingCallView()
}
} else {
OnboardingView(onboarding: step)
}
} else if !chatModel.v3DBMigration.startChat {
MigrateToAppGroupView()
} else if let step = chatModel.onboardingStage {
if case .onboardingComplete = step,
chatModel.currentUser != nil {
mainView()
} else {
OnboardingView(onboarding: step)
}
}
}
.onAppear { if doAuthenticate { runAuthenticate() } }
.onAppear {
if doAuthenticate { runAuthenticate() }
}
.onChange(of: doAuthenticate) { _ in if doAuthenticate { runAuthenticate() } }
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
}
private func mainView() -> some View {
ZStack(alignment: .top) {
ChatListView(showChatInfo: $showChatInfo)
.onAppear {
NtfManager.shared.requestAuthorization(onDeny: {
alertManager.showAlert(notificationAlert())
})
// Local Authentication notice is to be shown on next start after onboarding is complete
if (!prefLANoticeShown && prefShowLANotice) {
prefLANoticeShown = true
alertManager.showAlert(laNoticeAlert())
}
prefShowLANotice = true
}
if chatModel.showCallView, let call = chatModel.activeCall {
ActiveCallView(call: call)
}
IncomingCallView()
}
}
private func runAuthenticate() {
if !prefPerformLA {
userAuthorized = true
+23 -4
View File
@@ -8,6 +8,7 @@
import Foundation
import BackgroundTasks
import SimpleXChat
private let receiveTaskId = "chat.simplex.app.receive"
@@ -16,11 +17,14 @@ private let waitForMessages: TimeInterval = 6
private let bgRefreshInterval: TimeInterval = 450
private let maxTimerCount = 9
class BGManager {
static let shared = BGManager()
var chatReceiver: ChatReceiver?
var bgTimer: Timer?
var completed = true
var timerCount = 0
func register() {
logger.debug("BGManager.register")
@@ -43,11 +47,16 @@ class BGManager {
private func handleRefresh(_ task: BGAppRefreshTask) {
logger.debug("BGManager.handleRefresh")
schedule()
let completeRefresh = completionHandler {
if appStateGroupDefault.get().inactive {
let completeRefresh = completionHandler {
task.setTaskCompleted(success: true)
}
task.expirationHandler = { completeRefresh("expirationHandler") }
receiveMessages(completeRefresh)
} else {
logger.debug("BGManager.completionHandler: already active, not started")
task.setTaskCompleted(success: true)
}
task.expirationHandler = { completeRefresh("expirationHandler") }
receiveMessages(completeRefresh)
}
func completionHandler(_ complete: @escaping () -> Void) -> ((String) -> Void) {
@@ -59,6 +68,8 @@ class BGManager {
self.chatReceiver = nil
self.bgTimer?.invalidate()
self.bgTimer = nil
self.timerCount = 0
suspendBgRefresh()
complete()
}
}
@@ -71,20 +82,28 @@ class BGManager {
}
self.completed = false
DispatchQueue.main.async {
initializeChat()
do {
try initializeChat(start: true)
} catch let error {
fatalError("Failed to start or load chats: \(responseError(error))")
}
if ChatModel.shared.currentUser == nil {
completeReceiving("no current user")
return
}
logger.debug("BGManager.receiveMessages: starting chat")
activateChat(appState: .bgRefresh)
let cr = ChatReceiver()
self.chatReceiver = cr
cr.start()
RunLoop.current.add(Timer(timeInterval: 2, repeats: true) { timer in
logger.debug("BGManager.receiveMessages: timer")
self.bgTimer = timer
self.timerCount += 1
if cr.lastMsgTime.distance(to: Date.now) >= waitForMessages {
completeReceiving("timer (no messages after \(waitForMessages) seconds)")
} else if self.timerCount >= maxTimerCount {
completeReceiving("timer (called \(maxTimerCount) times")
}
}, forMode: .default)
}
+26 -2
View File
@@ -10,10 +10,14 @@ import Foundation
import Combine
import SwiftUI
import WebKit
import SimpleXChat
final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage?
@Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get()
@Published var currentUser: User?
@Published var chatRunning: Bool?
@Published var chatDbChanged = false
// list of chat "previews"
@Published var chats: [Chat] = []
// current chat
@@ -25,8 +29,11 @@ final class ChatModel: ObservableObject {
@Published var userAddress: String?
@Published var userSMPServers: [String]?
@Published var appOpenUrl: URL?
@Published var deviceToken: String?
@Published var tokenStatus = NtfTknStatus.new
@Published var deviceToken: DeviceToken?
@Published var savedToken: DeviceToken?
@Published var tokenStatus: NtfTknStatus?
@Published var notificationMode = NotificationsMode.off
@Published var notificationPreview: NotificationPreviewMode? = .message
// current WebRTC call
@Published var callInvitations: Dictionary<ChatId, CallInvitation> = [:]
@Published var activeCall: Call?
@@ -86,13 +93,27 @@ final class ChatModel: ObservableObject {
func replaceChat(_ id: String, _ chat: Chat) {
if let i = getChatIndex(id) {
let serverInfo = chats[i].serverInfo
chats[i] = chat
chats[i].serverInfo = serverInfo
} else {
// invalid state, correcting
chats.insert(chat, at: 0)
}
}
func updateChats(with newChats: [ChatData]) {
for c in newChats {
if let chat = getChat(c.id) {
chat.chatInfo = c.chatInfo
chat.chatItems = c.chatItems
chat.chatStats = c.chatStats
} else {
addChat(Chat(c))
}
}
}
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
// update previews
if let i = getChatIndex(cInfo.id) {
@@ -242,6 +263,7 @@ final class Chat: ObservableObject, Identifiable {
@Published var chatItems: [ChatItem]
@Published var chatStats: ChatStats
@Published var serverInfo = ServerInfo(networkStatus: .unknown)
var created = Date.now
struct ServerInfo: Decodable {
var networkStatus: NetworkStatus
@@ -299,4 +321,6 @@ final class Chat: ObservableObject, Identifiable {
}
var id: ChatId { get { chatInfo.id } }
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
}
+1
View File
@@ -9,6 +9,7 @@
import Foundation
import UserNotifications
import UIKit
import SimpleXChat
let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT"
let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL"
@@ -0,0 +1,41 @@
//
// PushEnvironment.swift
// SimpleX (iOS)
//
// Created by Evgeny on 27/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import SimpleXChat
let pushEnvironment: PushEnvironment = {
guard let provisioningProfile = try? provisioningProfile(),
let entitlements = provisioningProfile["Entitlements"] as? [String: Any],
let environment = entitlements["aps-environment"] as? String,
let env = PushEnvironment(rawValue: environment)
else {
logger.warning("pushEnvironment: unknown, assuming production")
return .production
}
logger.debug("pushEnvironment: \(env.rawValue)")
return env
}()
private func provisioningProfile() throws -> [String: Any]? {
guard let url = Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") else {
return nil
}
let binaryString = try String(contentsOf: url, encoding: .isoLatin1)
let scanner = Scanner(string: binaryString)
guard scanner.scanUpToString("<plist") != nil,
let plistString = scanner.scanUpToString("</plist>"),
let data = (plistString + "</plist>").data(using: .isoLatin1)
else {
return nil
}
return try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any]
}
@@ -1,72 +0,0 @@
//
// CallTypes.swift
// SimpleX (iOS)
//
// Created by Evgeny on 05/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
struct WebRTCCallOffer: Encodable {
var callType: CallType
var rtcSession: WebRTCSession
}
struct WebRTCSession: Codable {
var rtcSession: String
var rtcIceCandidates: String
}
struct WebRTCExtraInfo: Codable {
var rtcIceCandidates: String
}
struct CallInvitation {
var contact: Contact
var callkitUUID: UUID?
var peerMedia: CallMediaType
var sharedKey: String?
var callTs: Date
var callTypeText: LocalizedStringKey {
get {
switch peerMedia {
case .video: return sharedKey == nil ? "video call (not e2e encrypted)" : "**e2e encrypted** video call"
case .audio: return sharedKey == nil ? "audio call (not e2e encrypted)" : "**e2e encrypted** audio call"
}
}
}
static let sampleData = CallInvitation(
contact: Contact.sampleData,
peerMedia: .audio,
callTs: .now
)
}
struct CallType: Codable {
var media: CallMediaType
var capabilities: CallCapabilities
}
enum CallMediaType: String, Codable, Equatable {
case video = "video"
case audio = "audio"
}
enum VideoCamera: String, Codable, Equatable {
case user = "user"
case environment = "environment"
}
struct CallCapabilities: Codable, Equatable {
var encryption: Bool
}
enum WebRTCCallStatus: String, Encodable {
case connected = "connected"
case connecting = "connecting"
case disconnected = "disconnected"
case failed = "failed"
}
@@ -1,30 +0,0 @@
//
// GroupDefaults.swift
// SimpleX (iOS)
//
// Created by Evgeny on 26/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
func getGroupDefaults() -> UserDefaults? {
UserDefaults(suiteName: "5NN7GUYB6T.group.chat.simplex.app")
}
func setAppState(_ phase: ScenePhase) {
if let defaults = getGroupDefaults() {
defaults.set(phase == .background, forKey: "appInBackground")
defaults.synchronize()
}
}
func getAppState() -> ScenePhase {
if let defaults = getGroupDefaults() {
if defaults.bool(forKey: "appInBackground") {
return .background
}
}
return .active
}
+121 -54
View File
@@ -11,6 +11,7 @@ import UIKit
import Dispatch
import BackgroundTasks
import SwiftUI
import SimpleXChat
private var chatController: chat_ctrl?
@@ -46,7 +47,7 @@ enum TerminalItem: Identifiable {
}
}
private func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) {
func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) {
var id: UIBackgroundTaskIdentifier!
var running = true
let endTask = {
@@ -71,7 +72,7 @@ private func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) {
let msgDelay: Double = 7.5
let maxTaskDuration: Double = 15
private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> ChatResponse) -> ChatResponse {
private func withBGTask<T>(bgDelay: Double? = nil, f: @escaping () -> T) -> T {
let endTask = beginBGTask()
DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask)
let r = f()
@@ -92,11 +93,9 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
if case let .response(_, json) = resp {
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
}
if case .apiParseMarkdown = cmd {} else {
DispatchQueue.main.async {
ChatModel.shared.terminalItems.append(.cmd(.now, cmd))
ChatModel.shared.terminalItems.append(.resp(.now, resp))
}
DispatchQueue.main.async {
ChatModel.shared.terminalItems.append(.cmd(.now, cmd))
ChatModel.shared.terminalItems.append(.resp(.now, resp))
}
return resp
}
@@ -107,10 +106,10 @@ func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil
}
}
func chatRecvMsg() async -> ChatResponse {
func chatRecvMsg() async -> ChatResponse? {
await withCheckedContinuation { cont in
_ = withBGTask(bgDelay: msgDelay) {
let resp = chatResponse(chat_recv_msg(getChatCtrl())!)
_ = withBGTask(bgDelay: msgDelay) { () -> ChatResponse? in
let resp = recvSimpleXMsg()
cont.resume(returning: resp)
return resp
}
@@ -134,7 +133,7 @@ func apiCreateActiveUser(_ p: Profile) throws -> User {
}
func apiStartChat() throws -> Bool {
let r = chatSendCmdSync(.startChat)
let r = chatSendCmdSync(.startChat(subscribe: true))
switch r {
case .chatStarted: return true
case .chatRunning: return false
@@ -142,15 +141,47 @@ func apiStartChat() throws -> Bool {
}
}
func apiStopChat() async throws {
let r = await chatSendCmd(.apiStopChat)
switch r {
case .chatStopped: return
default: throw r
}
}
func apiActivateChat() {
let r = chatSendCmdSync(.apiActivateChat)
if case .cmdOk = r { return }
logger.error("apiActivateChat error: \(String(describing: r))")
}
func apiSuspendChat(timeoutMicroseconds: Int) {
let r = chatSendCmdSync(.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds))
if case .cmdOk = r { return }
logger.error("apiSuspendChat error: \(String(describing: r))")
}
func apiSetFilesFolder(filesFolder: String) throws {
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder))
if case .cmdOk = r { return }
throw r
}
func apiGetChats() throws -> [Chat] {
func apiExportArchive(config: ArchiveConfig) async throws {
try await sendCommandOkResp(.apiExportArchive(config: config))
}
func apiImportArchive(config: ArchiveConfig) async throws {
try await sendCommandOkResp(.apiImportArchive(config: config))
}
func apiDeleteStorage() async throws {
try await sendCommandOkResp(.apiDeleteStorage)
}
func apiGetChats() throws -> [ChatData] {
let r = chatSendCmdSync(.apiGetChats)
if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } }
if case let .apiChats(chats) = r { return chats }
throw r
}
@@ -160,6 +191,18 @@ func apiGetChat(type: ChatType, id: Int64) throws -> Chat {
throw r
}
func loadChat(chat: Chat) {
do {
let cInfo = chat.chatInfo
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId)
let m = ChatModel.shared
m.updateChatInfo(chat.chatInfo)
m.chatItems = chat.chatItems
} catch let error {
logger.error("loadChat error: \(responseError(error))")
}
}
func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) async throws -> ChatItem {
let chatModel = ChatModel.shared
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg)
@@ -195,21 +238,42 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM
throw r
}
func apiRegisterToken(token: String) async throws -> NtfTknStatus {
let r = await chatSendCmd(.apiRegisterToken(token: token))
func apiGetNtfToken() throws -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
let r = chatSendCmdSync(.apiGetNtfToken)
switch r {
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
case .chatCmdError(.errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
default: throw r
}
}
func apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) async throws -> NtfTknStatus {
let r = await chatSendCmd(.apiRegisterToken(token: token, notificationMode: notificationMode))
if case let .ntfTokenStatus(status) = r { return status }
throw r
}
func apiVerifyToken(token: String, code: String, nonce: String) async throws {
try await sendCommandOkResp(.apiVerifyToken(token: token, code: code, nonce: nonce))
func registerToken(token: DeviceToken) {
let m = ChatModel.shared
let mode = m.notificationMode
if mode != .off {
logger.debug("registerToken \(mode.rawValue)")
Task {
do {
let status = try await apiRegisterToken(token: token, notificationMode: mode)
await MainActor.run { m.tokenStatus = status }
} catch let error {
logger.error("registerToken apiRegisterToken error: \(responseError(error))")
}
}
}
}
func apiIntervalNofication(token: String, interval: Int) async throws {
try await sendCommandOkResp(.apiIntervalNofication(token: token, interval: interval))
func apiVerifyToken(token: DeviceToken, nonce: String, code: String) async throws {
try await sendCommandOkResp(.apiVerifyToken(token: token, nonce: nonce, code: code))
}
func apiDeleteToken(token: String) async throws {
func apiDeleteToken(token: DeviceToken) async throws {
try await sendCommandOkResp(.apiDeleteToken(token: token))
}
@@ -311,12 +375,6 @@ func apiUpdateProfile(profile: Profile) async throws -> Profile? {
}
}
func apiParseMarkdown(text: String) throws -> [FormattedText]? {
let r = chatSendCmdSync(.apiParseMarkdown(text: text))
if case let .apiParsedMarkdown(formattedText) = r { return formattedText }
throw r
}
func apiCreateUserAddress() async throws -> String {
let r = await chatSendCmd(.createMyAddress)
if case let .userContactLinkCreated(connReq) = r { return connReq }
@@ -453,42 +511,48 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
throw r
}
func initializeChat() {
func initializeChat(start: Bool) throws {
logger.debug("initializeChat")
do {
let m = ChatModel.shared
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
m.currentUser = try apiGetActiveUser()
if m.currentUser == nil {
m.onboardingStage = .step1_SimpleXInfo
} else if start {
try startChat()
} else {
startChat()
m.chatRunning = false
}
} catch {
fatalError("Failed to initialize chat controller or database: \(error)")
fatalError("Failed to initialize chat controller or database: \(responseError(error))")
}
}
func startChat() {
func startChat() throws {
logger.debug("startChat")
do {
let m = ChatModel.shared
// TODO set file folder once, before chat is started
let justStarted = try apiStartChat()
if justStarted {
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
m.userAddress = try apiGetUserAddress()
m.userSMPServers = try getUserSMPServers()
m.chats = try apiGetChats()
withAnimation {
m.onboardingStage = m.chats.isEmpty
? .step3_MakeConnection
: .onboardingComplete
}
let m = ChatModel.shared
let justStarted = try apiStartChat()
if justStarted {
m.userAddress = try apiGetUserAddress()
m.userSMPServers = try getUserSMPServers()
let chats = try apiGetChats()
m.chats = chats.map { Chat.init($0) }
(m.savedToken, m.tokenStatus, m.notificationMode) = try apiGetNtfToken()
if let token = m.deviceToken {
registerToken(token: token)
}
withAnimation {
m.onboardingStage = m.onboardingStage == .step2_CreateProfile
? .step3_SetNotificationsMode
: m.chats.isEmpty
? .step4_MakeConnection
: .onboardingComplete
}
ChatReceiver.shared.start()
} catch {
fatalError("Failed to start or load chats: \(error)")
}
ChatReceiver.shared.start()
m.chatRunning = true
chatLastStartGroupDefault.set(Date.now)
}
class ChatReceiver {
@@ -509,12 +573,13 @@ class ChatReceiver {
}
func receiveMsgLoop() async {
let msg = await chatRecvMsg()
self._lastMsgTime = .now
processReceivedMsg(msg)
// TODO use function that has timeout
if let msg = await chatRecvMsg() {
self._lastMsgTime = .now
await processReceivedMsg(msg)
}
if self.receiveMessages {
do { try await Task.sleep(nanoseconds: 7_500_000) }
catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") }
_ = try? await Task.sleep(nanoseconds: 7_500_000)
await receiveMsgLoop()
}
}
@@ -527,9 +592,9 @@ class ChatReceiver {
}
}
func processReceivedMsg(_ res: ChatResponse) {
func processReceivedMsg(_ res: ChatResponse) async {
let m = ChatModel.shared
DispatchQueue.main.async {
await MainActor.run {
m.terminalItems.append(.resp(.now, res))
logger.debug("processReceivedMsg: \(res.responseType)")
switch res {
@@ -681,6 +746,8 @@ func processReceivedMsg(_ res: ChatResponse) {
m.callCommand = .end
// CallController.shared.reportCallRemoteEnded(call: call)
}
case .chatSuspended:
chatSuspended()
default:
logger.debug("unsupported event: \(res.responseType)")
}
+57
View File
@@ -0,0 +1,57 @@
//
// SuspendChat.swift
// SimpleX (iOS)
//
// Created by Evgeny on 26/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import UIKit
import SimpleXChat
private let suspendLockQueue = DispatchQueue(label: "chat.simplex.app.suspend.lock")
let appSuspendTimeout: Int = 15 // seconds
let bgSuspendTimeout: Int = 5 // seconds
private func _suspendChat(timeout: Int) {
appStateGroupDefault.set(.suspending)
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
let endTask = beginBGTask(chatSuspended)
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask)
}
func suspendChat() {
suspendLockQueue.sync {
_suspendChat(timeout: appSuspendTimeout)
}
}
func suspendBgRefresh() {
suspendLockQueue.sync {
if case .bgRefresh = appStateGroupDefault.get() {
_suspendChat(timeout: bgSuspendTimeout)
}
}
}
func chatSuspended() {
suspendLockQueue.sync {
if case .suspending = appStateGroupDefault.get() {
logger.debug("chatSuspended")
appStateGroupDefault.set(.suspended)
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.stop()
}
}
}
}
func activateChat(appState: AppState = .active) {
suspendLockQueue.sync {
appStateGroupDefault.set(appState)
apiActivateChat()
}
}
@@ -1,11 +0,0 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
extern void hs_init(int argc, char **argv[]);
typedef void* chat_ctrl;
extern chat_ctrl chat_init(char *path);
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
extern char *chat_recv_msg(chat_ctrl ctl);
+60 -2
View File
@@ -7,6 +7,7 @@
import SwiftUI
import OSLog
import SimpleXChat
let logger = Logger()
@@ -24,6 +25,7 @@ struct SimpleXApp: App {
init() {
hs_init(0, nil)
UserDefaults.standard.register(defaults: appDefaults)
setDbContainer()
BGManager.shared.register()
NtfManager.shared.registerCategories()
}
@@ -37,19 +39,33 @@ struct SimpleXApp: App {
chatModel.appOpenUrl = url
}
.onAppear() {
initializeChat()
do {
chatModel.v3DBMigration = v3DBMigrationDefault.get()
try initializeChat(start: chatModel.v3DBMigration.startChat)
} catch let error {
fatalError("Failed to start or load chats: \(responseError(error))")
}
}
.onChange(of: scenePhase) { phase in
logger.debug("scenePhase \(String(describing: scenePhase))")
setAppState(phase)
switch (phase) {
case .background:
suspendChat()
BGManager.shared.schedule()
if userAuthorized == true {
enteredBackground = ProcessInfo.processInfo.systemUptime
}
doAuthenticate = false
case .active:
if chatModel.chatRunning == true {
ChatReceiver.shared.start()
}
let appState = appStateGroupDefault.get()
activateChat()
if appState.inactive && chatModel.chatRunning == true {
// TODO refresh call invitation
updateChats()
}
doAuthenticate = authenticationExpired()
default:
break
@@ -58,6 +74,30 @@ struct SimpleXApp: App {
}
}
private func setDbContainer() {
// Uncomment and run once to open DB in app documents folder:
// dbContainerGroupDefault.set(.documents)
// v3DBMigrationDefault.set(.offer)
// to create database in app documents folder also uncomment:
// let legacyDatabase = true
let legacyDatabase = hasLegacyDatabase()
if legacyDatabase, case .documents = dbContainerGroupDefault.get() {
dbContainerGroupDefault.set(.documents)
setMigrationState(.offer)
logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db")
} else {
dbContainerGroupDefault.set(.group)
setMigrationState(.ready)
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db")
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present")
}
}
private func setMigrationState(_ state: V3DBMigrationState) {
if case .migrated = v3DBMigrationDefault.get() { return }
v3DBMigrationDefault.set(state)
}
private func authenticationExpired() -> Bool {
if let enteredBackground = enteredBackground {
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
@@ -65,4 +105,22 @@ struct SimpleXApp: App {
return true
}
}
private func updateChats() {
do {
let chats = try apiGetChats()
chatModel.updateChats(with: chats)
if let id = chatModel.chatId,
let chat = chatModel.getChat(id) {
loadChat(chat: chat)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if chatModel.chatId == chat.id {
Task { await markChatRead(chat) }
}
}
}
} catch let error {
logger.error("apiGetChats: cannot update chats \(responseError(error))")
}
}
}
@@ -8,6 +8,7 @@
import SwiftUI
import WebKit
import SimpleXChat
struct ActiveCallView: View {
@EnvironmentObject var m: ChatModel
@@ -9,6 +9,7 @@
import Foundation
//import CallKit
import AVFoundation
import SimpleXChat
//class CallController: NSObject, CXProviderDelegate, ObservableObject {
class CallController: NSObject, ObservableObject {
@@ -7,6 +7,7 @@
//
import Foundation
import SimpleXChat
class CallManager {
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID {
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct IncomingCallView: View {
@EnvironmentObject var m: ChatModel
+1
View File
@@ -8,6 +8,7 @@
import Foundation
import SwiftUI
import SimpleXChat
class Call: ObservableObject, Equatable {
static func == (lhs: Call, rhs: Call) -> Bool {
@@ -8,6 +8,7 @@
import SwiftUI
import WebKit
import SimpleXChat
class WebRTCCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate {
var rtcWebView: Binding<WKWebView?>
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
private let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9)
private let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2 )
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct CICallItemView: View {
@EnvironmentObject var m: ChatModel
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct CIFileView: View {
@Environment(\.colorScheme) var colorScheme
@@ -133,7 +134,7 @@ struct CIFileView: View {
struct CIFileView_Previews: PreviewProvider {
static var previews: some View {
let sentFile = ChatItem(
let sentFile: ChatItem = ChatItem(
chatDir: .directSnd,
meta: CIMeta.getSample(1, .now, "", .sndSent, false, true, false),
content: .sndMsgContent(msgContent: .file("")),
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct CIImageView: View {
@Environment(\.colorScheme) var colorScheme
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct CILinkView: View {
@Environment(\.colorScheme) var colorScheme
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct CIMetaView: View {
var chatItem: ChatItem
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct DeletedItemView: View {
var chatItem: ChatItem
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct EmojiItemView: View {
var chatItem: ChatItem
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct IntegrityErrorItemView: View {
var chatItem: ChatItem
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
private let linkColor = Color(uiColor: uiLinkColor)
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ChatItemView: View {
var chatInfo: ChatInfo
+2 -1
View File
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
private let memberImageSize: CGFloat = 34
@@ -224,7 +225,7 @@ struct ChatView: View {
}
func markAllRead() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if chatModel.chatId == chat.id {
Task { await markChatRead(chat) }
}
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ComposeImageView: View {
@Environment(\.colorScheme) var colorScheme
@@ -8,7 +8,7 @@
import SwiftUI
import LinkPresentation
import SimpleXChat
func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
logger.debug("getLinkMetadata: fetching URL preview")
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
enum ComposePreview {
case noPreview
@@ -209,9 +210,8 @@ struct ComposeView: View {
allowedContentTypes: [.data],
allowsMultipleSelection: false
) { result in
if case .success = result {
if case let .success(files) = result, let fileURL = files.first {
do {
let fileURL: URL = try result.get().first!
var fileSize: Int? = nil
if fileURL.startAccessingSecurityScopedResource() {
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
@@ -391,17 +391,12 @@ struct ComposeView: View {
}
private func parseMessage(_ msg: String) -> URL? {
do {
let parsedMsg = try apiParseMarkdown(text: msg)
let uri = parsedMsg?.first(where: { ft in
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
})
if let uri = uri { return URL(string: uri.text) }
else { return nil }
} catch {
logger.error("apiParseMarkdown error: \(error.localizedDescription)")
return nil
}
let parsedMsg = parseSimpleXMarkdown(msg)
let uri = parsedMsg?.first(where: { ft in
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
})
if let uri = uri { return URL(string: uri.text) }
else { return nil }
}
private func isSimplexLink(_ link: String) -> Bool {
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ContextItemView: View {
@Environment(\.colorScheme) var colorScheme
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct SendMessageView: View {
@Binding var composeState: ComposeState
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel
@@ -29,16 +30,7 @@ struct ChatListNavLink: View {
private func chatView() -> some View {
ChatView(chat: chat, showChatInfo: $showChatInfo)
.onAppear {
do {
let cInfo = chat.chatInfo
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId)
chatModel.updateChatInfo(chat.chatInfo)
chatModel.chatItems = chat.chatItems
} catch {
logger.error("ChatListNavLink.chatView apiGetChatItems error: \(error.localizedDescription)")
}
}
.onAppear { loadChat(chat: chat) }
}
@ViewBuilder private func contactNavLink(_ contact: Contact) -> some View {
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@@ -19,9 +20,10 @@ struct ChatListView: View {
var body: some View {
let v = NavigationView {
List {
ForEach(filteredChats()) { chat in
ForEach(filteredChats(), id: \.viewId) { chat in
ChatListNavLink(chat: chat, showChatInfo: $showChatInfo)
.padding(.trailing, -16)
.disabled(chatModel.chatRunning != true)
}
}
.onChange(of: chatModel.chatId) { _ in
@@ -32,7 +34,7 @@ struct ChatListView: View {
}
.onChange(of: chatModel.chats.isEmpty) { empty in
if !empty { return }
withAnimation { chatModel.onboardingStage = .step3_MakeConnection }
withAnimation { chatModel.onboardingStage = .step4_MakeConnection }
}
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
.onAppear() { connectViaUrl() }
@@ -45,7 +47,11 @@ struct ChatListView: View {
SettingsButton()
}
ToolbarItem(placement: .navigationBarTrailing) {
NewChatButton()
switch chatModel.chatRunning {
case .some(true): NewChatButton()
case .some(false): chatStoppedIcon()
case .none: EmptyView()
}
}
}
}
@@ -73,6 +79,17 @@ struct ChatListView: View {
}
}
func chatStoppedIcon() -> some View {
Button {
AlertManager.shared.showAlertMsg(
title: "Chat is stopped",
message: "You can start chat via app Settings / Database or by restarting the app"
)
} label: {
Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red)
}
}
struct ChatListView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ChatPreviewView: View {
@ObservedObject var chat: Chat
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ContactConnectionView: View {
var contactConnection: PendingContactConnection
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ContactRequestView: View {
var contactRequest: UserContactRequest
@@ -0,0 +1,65 @@
//
// ChatArchiveView.swift
// SimpleXChat
//
// Created by Evgeny on 23/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ChatArchiveView: View {
var archiveName: String
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
@State private var showDeleteAlert = false
var body: some View {
let fileUrl = getDocumentsDirectory().appendingPathComponent(archiveName)
let fileTs = chatArchiveTimeDefault.get()
List {
Section {
settingsRow("square.and.arrow.up") {
Button {
showShareSheet(items: [fileUrl])
} label: {
Text("Save archive")
}
}
settingsRow("trash") {
Button {
showDeleteAlert = true
} label: {
Text("Delete archive").foregroundColor(.red)
}
}
} header: {
Text("Chat archive")
} footer: {
Text("Created on \(fileTs)")
}
}
.alert(isPresented: $showDeleteAlert) {
Alert(
title: Text("Delete chat archive?"),
primaryButton: .destructive(Text("Delete")) {
do {
try FileManager.default.removeItem(atPath: fileUrl.path)
chatArchiveName = nil
chatArchiveTime = 0
} catch let error {
logger.error("removeItem error \(String(describing: error))")
}
},
secondaryButton: .cancel()
)
}
}
}
struct ChatArchiveView_Previews: PreviewProvider {
static var previews: some View {
ChatArchiveView(archiveName: "")
}
}
@@ -0,0 +1,333 @@
//
// DatabaseView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 19/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
enum DatabaseAlert: Identifiable {
case stopChat
case importArchive
case archiveImported
case deleteChat
case chatDeleted
case deleteLegacyDatabase
case error(title: LocalizedStringKey, error: String = "")
var id: String {
switch self {
case .stopChat: return "stopChat"
case .importArchive: return "importArchive"
case .archiveImported: return "archiveImported"
case .deleteChat: return "deleteChat"
case .chatDeleted: return "chatDeleted"
case .deleteLegacyDatabase: return "deleteLegacyDatabase"
case let .error(title, _): return "error \(title)"
}
}
}
struct DatabaseView: View {
@EnvironmentObject var m: ChatModel
@Binding var showSettings: Bool
@State private var runChat = false
@State private var alert: DatabaseAlert? = nil
@State private var showFileImporter = false
@State private var importedArchivePath: URL?
@State private var progressIndicator = false
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
@State private var dbContainer = dbContainerGroupDefault.get()
@State private var legacyDatabase = hasLegacyDatabase()
var body: some View {
ZStack {
chatDatabaseView()
if progressIndicator {
ProgressView().scaleEffect(2)
}
}
}
private func chatDatabaseView() -> some View {
List {
let stopped = m.chatRunning == false
Section {
settingsRow(
stopped ? "exclamationmark.octagon.fill" : "play.fill",
color: stopped ? .red : .green
) {
Toggle(
stopped ? "Chat is stopped" : "Chat is running",
isOn: $runChat
)
.onChange(of: runChat) { _ in
if (runChat) {
startChat()
} else {
alert = .stopChat
}
}
}
} header: {
Text("Run chat")
} footer: {
if case .documents = dbContainer {
Text("Database will be migrated when the app restarts")
}
}
Section {
settingsRow("square.and.arrow.up") {
Button {
exportArchive()
} label: {
Text("Export database")
}
}
settingsRow("square.and.arrow.down") {
Button(role: .destructive) {
showFileImporter = true
} label: {
Text("Import database")
}
}
if let archiveName = chatArchiveName {
let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get()
? "Old database archive"
: "New database archive"
settingsRow("archivebox") {
NavigationLink {
ChatArchiveView(archiveName: archiveName)
.navigationTitle(title)
} label: {
Text(title)
}
}
}
settingsRow("trash.slash") {
Button(role: .destructive) {
alert = .deleteChat
} label: {
Text("Delete database")
}
}
} header: {
Text("Chat database")
} footer: {
Text(
stopped
? "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts."
: "Stop chat to enable database actions"
)
}
.disabled(!stopped)
if case .group = dbContainer, legacyDatabase {
Section("Old database") {
settingsRow("trash") {
Button {
alert = .deleteLegacyDatabase
} label: {
Text("Delete old database")
}
}
}
}
}
.onAppear { runChat = m.chatRunning ?? true }
.alert(item: $alert) { item in databaseAlert(item) }
.fileImporter(
isPresented: $showFileImporter,
allowedContentTypes: [.zip],
allowsMultipleSelection: false
) { result in
if case let .success(files) = result, let fileURL = files.first {
importedArchivePath = fileURL
alert = .importArchive
}
}
}
private func databaseAlert(_ alertItem: DatabaseAlert) -> Alert {
switch alertItem {
case .stopChat:
return Alert(
title: Text("Stop chat?"),
message: Text("Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped."),
primaryButton: .destructive(Text("Stop")) {
stopChat()
},
secondaryButton: .cancel {
withAnimation { runChat = true }
}
)
case .importArchive:
if let fileURL = importedArchivePath {
return Alert(
title: Text("Import chat database?"),
message: Text("Your current chat database will be DELETED and REPLACED with the imported one.\n") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
primaryButton: .destructive(Text("Import")) {
importArchive(fileURL)
},
secondaryButton: .cancel()
)
} else {
return Alert(title: Text("Error: no database file"))
}
case .archiveImported:
return Alert(
title: Text("Chat database imported"),
message: Text("Restart the app to use imported chat database"),
primaryButton: .default(Text("Ok")),
secondaryButton: .cancel()
)
case .deleteChat:
return Alert(
title: Text("Delete chat profile?"),
message: Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
primaryButton: .destructive(Text("Delete")) {
deleteChat()
},
secondaryButton: .cancel()
)
case .chatDeleted:
return Alert(
title: Text("Chat database deleted"),
message: Text("Restart the app to create a new chat profile"),
primaryButton: .default(Text("Ok")),
secondaryButton: .cancel()
)
case .deleteLegacyDatabase:
return Alert(
title: Text("Delete old database?"),
message: Text("The old database was not removed during the migration, it can be deleted."),
primaryButton: .destructive(Text("Delete")) {
deleteLegacyDatabase()
},
secondaryButton: .cancel()
)
case let .error(title, error):
return Alert(title: Text(title), message: Text("\(error)"))
}
}
private func stopChat() {
Task {
do {
try await apiStopChat()
ChatReceiver.shared.stop()
await MainActor.run { m.chatRunning = false }
} catch let error {
await MainActor.run {
runChat = true
alert = .error(title: "Error stopping chat", error: responseError(error))
}
}
}
}
private func exportArchive() {
progressIndicator = true
Task {
do {
let archivePath = try await exportChatArchive()
showShareSheet(items: [archivePath])
await MainActor.run { progressIndicator = false }
} catch let error {
await MainActor.run {
alert = .error(title: "Error exporting chat database", error: responseError(error))
progressIndicator = false
}
}
}
}
private func importArchive(_ archivePath: URL) {
if archivePath.startAccessingSecurityScopedResource() {
progressIndicator = true
Task {
do {
try await apiDeleteStorage()
do {
let config = ArchiveConfig(archivePath: archivePath.path)
try await apiImportArchive(config: config)
await operationEnded(.archiveImported)
} catch let error {
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)))
}
} catch let error {
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)))
}
archivePath.stopAccessingSecurityScopedResource()
}
} else {
alert = .error(title: "Error accessing database file")
}
}
private func deleteChat() {
progressIndicator = true
Task {
do {
try await apiDeleteStorage()
await operationEnded(.chatDeleted)
} catch let error {
await operationEnded(.error(title: "Error deleting database", error: responseError(error)))
}
}
}
private func deleteLegacyDatabase() {
if removeLegacyDatabaseAndFiles() {
legacyDatabase = false
} else {
alert = .error(title: "Error deleting old database")
}
}
private func operationEnded(_ dbAlert: DatabaseAlert) async {
await MainActor.run {
m.chatDbChanged = true
progressIndicator = false
alert = dbAlert
}
}
private func startChat() {
if m.chatDbChanged {
showSettings = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
resetChatCtrl()
do {
try initializeChat(start: true)
m.chatDbChanged = false
} catch let error {
fatalError("Error starting chat \(responseError(error))")
}
}
} else {
do {
_ = try apiStartChat()
runChat = true
m.chatRunning = true
ChatReceiver.shared.start()
chatLastStartGroupDefault.set(Date.now)
} catch let error {
runChat = false
alert = .error(title: "Error starting chat", error: responseError(error))
}
}
}
}
struct DatabaseView_Previews: PreviewProvider {
static var previews: some View {
DatabaseView(showSettings: Binding.constant(false))
}
}
@@ -0,0 +1,251 @@
//
// MigrateToGroupView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 20/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
enum V3DBMigrationState: String {
case offer
case postponed
case exporting
case export_error
case exported
case migrating
case migration_error
case migrated
case ready
var startChat: Bool {
switch self {
case .postponed: return true
case .ready: return true
default: return false
}
}
}
let v3DBMigrationDefault = EnumDefault<V3DBMigrationState>(
defaults: UserDefaults.standard,
forKey: DEFAULT_CHAT_V3_DB_MIGRATION,
withDefault: .offer
)
struct MigrateToAppGroupView: View {
@EnvironmentObject var chatModel: ChatModel
@State private var migrationError = ""
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
var body: some View {
ZStack(alignment: .topLeading) {
Text("Push notifications").font(.largeTitle)
switch chatModel.v3DBMigration {
case .offer:
VStack(alignment: .leading, spacing: 16) {
Text("To support instant push notifications the chat database has to be migrated.")
Text("If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).")
}
.padding(.top, 56)
center {
Button {
migrateDatabaseToV3()
} label: {
Text("Start migration")
.font(.title)
.frame(maxWidth: .infinity)
}
}
skipMigration()
case .exporting:
center {
ProgressView(value: 0.33)
Text("Exporting database archive...")
}
migrationProgress()
case .export_error:
migrationFailed().padding(.top, 56)
center {
Text("Export error:").font(.headline)
Text(migrationError)
}
skipMigration()
case .exported:
center {
Text("Exported database archive.")
}
case .migrating:
center {
ProgressView(value: 0.67)
Text("Migrating database archive...")
}
migrationProgress()
case .migration_error:
VStack(alignment: .leading, spacing: 16) {
migrationFailed()
Text("The created archive is available via app Settings / Database / Old database archive.")
}
.padding(.top, 56)
center {
Text("Migration error:").font(.headline)
Text(migrationError)
}
skipMigration()
case .migrated:
center {
ProgressView(value: 1.0)
Text("Migration is completed")
}
VStack {
Spacer()
Spacer()
Spacer()
Button {
do {
resetChatCtrl()
try initializeChat(start: true)
chatModel.onboardingStage = .step3_SetNotificationsMode
setV3DBMigration(.ready)
} catch let error {
dbContainerGroupDefault.set(.documents)
setV3DBMigration(.migration_error)
migrationError = "Error starting chat: \(responseError(error))"
}
deleteOldArchive()
} label: {
Text("Start chat")
.font(.title)
.frame(maxWidth: .infinity)
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
default:
Spacer()
Text("Unexpected migration state")
Text("\(chatModel.v3DBMigration.rawValue)")
Spacer()
skipMigration()
}
}
.padding()
}
private func center<Content>(@ViewBuilder c: @escaping () -> Content) -> some View where Content: View {
VStack(alignment: .leading, spacing: 8) { c() }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
private func migrationProgress() -> some View {
VStack {
Spacer()
ProgressView().scaleEffect(2)
Spacer()
Spacer()
Spacer()
}
.frame(maxWidth: .infinity)
}
private func migrationFailed() -> some View {
Text("Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).")
}
private func skipMigration() -> some View {
ZStack {
Button {
setV3DBMigration(.postponed)
do {
try startChat()
} catch let error {
fatalError("Failed to start or load chats: \(responseError(error))")
}
} label: {
Text("Do it later")
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
}
private func setV3DBMigration(_ state: V3DBMigrationState) {
chatModel.v3DBMigration = state
v3DBMigrationDefault.set(state)
}
func migrateDatabaseToV3() {
setV3DBMigration(.exporting)
let archiveTime = Date.now
let archiveName = "simplex-chat.\(archiveTime.ISO8601Format()).zip"
chatArchiveTime = archiveTime.timeIntervalSince1970
chatArchiveName = archiveName
let config = ArchiveConfig(archivePath: getDocumentsDirectory().appendingPathComponent(archiveName).path)
Task {
do {
try await apiExportArchive(config: config)
await MainActor.run { setV3DBMigration(.exported) }
} catch let error {
await MainActor.run {
setV3DBMigration(.export_error)
migrationError = responseError(error)
}
return
}
do {
await MainActor.run { setV3DBMigration(.migrating) }
dbContainerGroupDefault.set(.group)
resetChatCtrl()
try await MainActor.run { try initializeChat(start: false) }
try await apiImportArchive(config: config)
await MainActor.run { setV3DBMigration(.migrated) }
} catch let error {
dbContainerGroupDefault.set(.documents)
await MainActor.run {
setV3DBMigration(.migration_error)
migrationError = responseError(error)
}
}
}
}
}
func exportChatArchive() async throws -> URL {
let archiveTime = Date.now
let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted))
let archiveName = "simplex-chat.\(ts).zip"
let archivePath = getDocumentsDirectory().appendingPathComponent(archiveName)
let config = ArchiveConfig(archivePath: archivePath.path)
try await apiExportArchive(config: config)
deleteOldArchive()
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
chatArchiveTimeDefault.set(archiveTime)
return archivePath
}
func deleteOldArchive() {
let d = UserDefaults.standard
if let archiveName = d.string(forKey: DEFAULT_CHAT_ARCHIVE_NAME) {
do {
try FileManager.default.removeItem(atPath: getDocumentsDirectory().appendingPathComponent(archiveName).path)
d.set(nil, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
d.set(0, forKey: DEFAULT_CHAT_ARCHIVE_TIME)
} catch let error {
logger.error("removeItem error \(String(describing: error))")
}
}
}
struct MigrateToGroupView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.v3DBMigration = .migrated
return MigrateToAppGroupView()
.environmentObject(chatModel)
}
}
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ChatInfoImage: View {
@ObservedObject var chat: Chat
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct ProfileImage: View {
var imageStr: String? = nil
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
enum NewChatAction: Identifiable {
case createLink
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct CreateProfile: View {
@EnvironmentObject var m: ChatModel
@@ -94,11 +95,11 @@ struct CreateProfile: View {
)
do {
m.currentUser = try apiCreateActiveUser(profile)
startChat()
withAnimation { m.onboardingStage = .step3_MakeConnection }
try startChat()
withAnimation { m.onboardingStage = .step3_SetNotificationsMode }
} catch {
fatalError("Failed to create user: \(error)")
fatalError("Failed to create user or start chat: \(responseError(error))")
}
}
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct MakeConnection: View {
@EnvironmentObject var m: ChatModel
@@ -15,7 +16,14 @@ struct MakeConnection: View {
var body: some View {
VStack(alignment: .leading) {
SettingsButton().padding(.bottom, 1)
HStack {
SettingsButton()
if m.chatRunning == false {
Spacer()
chatStoppedIcon()
}
}
.padding(.bottom, 1)
if let user = m.currentUser {
Text("Welcome \(user.displayName)!")
@@ -65,6 +73,7 @@ struct MakeConnection: View {
}
}
}
.disabled(m.chatRunning != true)
}
Spacer()
@@ -15,7 +15,8 @@ struct OnboardingView: View {
switch onboarding {
case .step1_SimpleXInfo: SimpleXInfo(onboarding: true)
case .step2_CreateProfile: CreateProfile()
case .step3_MakeConnection: MakeConnection()
case .step3_SetNotificationsMode: SetNotificationsMode()
case .step4_MakeConnection: MakeConnection()
case .onboardingComplete: EmptyView()
}
}
@@ -24,7 +25,8 @@ struct OnboardingView: View {
enum OnboardingStage {
case step1_SimpleXInfo
case step2_CreateProfile
case step3_MakeConnection
case step3_SetNotificationsMode
case step4_MakeConnection
case onboardingComplete
}
@@ -0,0 +1,112 @@
//
// NotificationsModeView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 03/07/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct SetNotificationsMode: View {
@EnvironmentObject var m: ChatModel
@State private var notificationMode = NotificationsMode.instant
@State private var showAlert: NotificationAlert?
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("Push notifications").font(.largeTitle)
Text("Send notifications:")
ForEach(NotificationsMode.values) { mode in
NtfModeSelector(mode: mode, selection: $notificationMode)
}
Spacer()
Button {
if let token = m.deviceToken {
setNotificationsMode(token, notificationMode)
} else {
AlertManager.shared.showAlertMsg(title: "No device token!")
}
m.onboardingStage = m.chats.isEmpty
? .step4_MakeConnection
: .onboardingComplete
} label: {
if case .off = notificationMode {
Text("Use chat")
} else {
Text("Enable notifications")
}
}
.font(.title)
.frame(maxWidth: .infinity)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
}
}
private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) {
switch mode {
case .off:
m.tokenStatus = .new
m.notificationMode = .off
default:
Task {
do {
let status = try await apiRegisterToken(token: token, notificationMode: mode)
await MainActor.run {
m.tokenStatus = status
m.notificationMode = mode
}
} catch let error {
AlertManager.shared.showAlertMsg(
title: "Error enabling notifications",
message: "\(responseError(error))"
)
}
}
}
}
}
struct NtfModeSelector: View {
var mode: NotificationsMode
@Binding var selection: NotificationsMode
@State private var tapped = false
var body: some View {
ZStack {
VStack(alignment: .leading, spacing: 4) {
Text(mode.label)
.font(.headline)
.foregroundColor(selection == mode ? .accentColor : .secondary)
Text(ntfModeDescription(mode))
.lineLimit(10)
.font(.subheadline)
}
.padding(12)
}
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(uiColor: tapped ? .secondarySystemFill : .systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 18))
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(selection == mode ? Color.accentColor : Color(uiColor: .secondarySystemFill), lineWidth: 2)
)
._onButtonGesture { down in
tapped = down
if down { selection = mode }
} perform: {}
}
}
struct NotificationsModeView_Previews: PreviewProvider {
static var previews: some View {
SetNotificationsMode()
}
}
@@ -77,7 +77,7 @@ struct OnboardingActionButton: View {
if m.currentUser == nil {
actionButton("Create your profile", onboarding: .step2_CreateProfile)
} else {
actionButton("Make a private connection", onboarding: .step3_MakeConnection)
actionButton("Make a private connection", onboarding: .step4_MakeConnection)
}
}
+1
View File
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
private let terminalFont = Font.custom("Menlo", size: 16)
@@ -0,0 +1,212 @@
//
// NotificationsView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 26/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct NotificationsView: View {
@EnvironmentObject var m: ChatModel
@State private var notificationMode: NotificationsMode?
@State private var showAlert: NotificationAlert?
@State private var legacyDatabase = dbContainerGroupDefault.get() == .documents
var body: some View {
List {
Section {
NavigationLink {
List {
Section {
SelectionListView(list: NotificationsMode.values, selection: $notificationMode) { mode in
showAlert = .setMode(mode: mode)
}
} footer: {
VStack(alignment: .leading) {
if let mode = notificationMode {
Text(ntfModeDescription(mode))
}
}
.font(.callout)
.padding(.top, 1)
}
}
.navigationTitle("Send notifications")
.navigationBarTitleDisplayMode(.inline)
.alert(item: $showAlert) { alert in
if let token = m.deviceToken {
return notificationAlert(alert, token)
} else {
return Alert(title: Text("No device token!"))
}
}
.onAppear { notificationMode = m.notificationMode }
} label: {
HStack {
Text("Send notifications")
Spacer()
Text(m.notificationMode.label)
}
}
NavigationLink {
List {
Section {
SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview)
} footer: {
}
}
.navigationTitle("Show preview")
.navigationBarTitleDisplayMode(.inline)
} label: {
HStack {
Text("Show preview")
Spacer()
Text(m.notificationPreview?.label ?? "")
}
}
} header: {
Text("Push notifications")
} footer: {
if legacyDatabase {
Text("Please restart the app and migrate the database to enable push notifications.")
.font(.callout)
.padding(.top, 1)
}
}
.disabled(legacyDatabase)
}
}
private func notificationAlert(_ alert: NotificationAlert, _ token: DeviceToken) -> Alert {
switch alert {
case let .setMode(mode):
return Alert(
title: Text(ntfModeAlertTitle(mode)),
message: Text(ntfModeDescription(mode)),
primaryButton: .default(Text(mode == .off ? "Turn off" : "Enable")) {
setNotificationsMode(token, mode)
},
secondaryButton: .cancel() {
notificationMode = m.notificationMode
}
)
case let .error(title, error):
return Alert(title: Text(title), message: Text(error))
}
}
private func ntfModeAlertTitle(_ mode: NotificationsMode) -> LocalizedStringKey {
switch mode {
case .off: return "Turn off notifications?"
case .periodic: return "Enable periodic notifications?"
case .instant: return "Enable instant notifications?"
}
}
private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) {
Task {
switch mode {
case .off:
do {
try await apiDeleteToken(token: token)
await MainActor.run {
m.tokenStatus = .new
notificationMode = .off
m.notificationMode = .off
}
} catch let error {
await MainActor.run {
let err = responseError(error)
logger.error("apiDeleteToken error: \(err)")
showAlert = .error(title: "Error deleting token", error: err)
}
}
default:
do {
let status = try await apiRegisterToken(token: token, notificationMode: mode)
await MainActor.run {
m.tokenStatus = status
notificationMode = mode
m.notificationMode = mode
}
} catch let error {
await MainActor.run {
let err = responseError(error)
logger.error("apiRegisterToken error: \(err)")
showAlert = .error(title: "Error enabling notifications", error: err)
}
}
}
}
}
}
func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey {
switch mode {
case .off: return "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)."
case .periodic: return "**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have."
case .instant: return "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from."
}
}
struct SelectionListView<Item: SelectableItem>: View {
var list: [Item]
@Binding var selection: Item?
var onSelection: ((Item) -> Void)?
@State private var tapped: Item? = nil
var body: some View {
ForEach(list) { item in
HStack {
Text(item.label)
Spacer()
if selection == item {
Image(systemName: "checkmark")
.resizable().scaledToFit().frame(width: 16)
.foregroundColor(.accentColor)
}
}
.contentShape(Rectangle())
.listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground))
.onTapGesture {
if selection == item { return }
if let f = onSelection {
f(item)
} else {
selection = item
}
}
._onButtonGesture { down in
if down {
tapped = item
} else {
tapped = nil
}
} perform: {}
}
.environment(\.editMode, .constant(.active))
}
}
enum NotificationAlert: Identifiable {
case setMode(mode: NotificationsMode)
case error(title: LocalizedStringKey, error: String)
var id: String {
switch self {
case let .setMode(mode): return "enable \(mode.rawValue)"
case let .error(title, error): return "error \(title): \(error)"
}
}
}
struct NotificationsView_Previews: PreviewProvider {
static var previews: some View {
NotificationsView()
}
}
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
private let serversFont = Font.custom("Menlo", size: 14)
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")!
@@ -17,41 +18,42 @@ let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as?
let DEFAULT_SHOW_LA_NOTICE = "showLocalAuthenticationNotice"
let DEFAULT_LA_NOTICE_SHOWN = "localAuthenticationNoticeShown"
let DEFAULT_PERFORM_LA = "performLocalAuthentication"
let DEFAULT_USE_NOTIFICATIONS = "useNotifications"
let DEFAULT_PENDING_CONNECTIONS = "pendingConnections"
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls"
let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration"
let appDefaults: [String: Any] = [
DEFAULT_SHOW_LA_NOTICE: false,
DEFAULT_LA_NOTICE_SHOWN: false,
DEFAULT_PERFORM_LA: false,
DEFAULT_USE_NOTIFICATIONS: false,
DEFAULT_PENDING_CONNECTIONS: true,
DEFAULT_WEBRTC_POLICY_RELAY: true,
DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
DEFAULT_PRIVACY_LINK_PREVIEWS: true,
DEFAULT_EXPERIMENTAL_CALLS: false
DEFAULT_EXPERIMENTAL_CALLS: false,
DEFAULT_CHAT_V3_DB_MIGRATION: "offer"
]
private var indent: CGFloat = 36
let chatArchiveTimeDefault = DateDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CHAT_ARCHIVE_TIME)
struct SettingsView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
@AppStorage(DEFAULT_USE_NOTIFICATIONS) private var useNotifications = false
@AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true
@AppStorage(DEFAULT_EXPERIMENTAL_CALLS) private var enableCalls = false
@State var showNotificationsAlert: Bool = false
@State var whichNotificationsAlert = NotificationAlert.enable
var body: some View {
let user: User = chatModel.currentUser!
return NavigationView {
NavigationView {
List {
Section("You") {
NavigationLink {
@@ -61,15 +63,42 @@ struct SettingsView: View {
ProfilePreview(profileOf: user)
.padding(.leading, -8)
}
.disabled(chatModel.chatRunning != true)
NavigationLink {
UserAddress()
.navigationTitle("Your chat address")
} label: {
settingsRow("qrcode") { Text("Your SimpleX contact address") }
}
.disabled(chatModel.chatRunning != true)
NavigationLink {
DatabaseView(showSettings: $showSettings)
.navigationTitle("Your chat database")
} label: {
settingsRow("internaldrive") {
HStack {
Text("Database export & import")
Spacer()
if chatModel.chatRunning == false {
Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red)
}
}
}
}
}
Section("Settings") {
NavigationLink {
NotificationsView()
.navigationTitle("Notifications")
} label: {
HStack {
notificationsIcon()
Text("Notifications")
}
}
if enableCalls {
NavigationLink {
CallSettings()
@@ -94,6 +123,7 @@ struct SettingsView: View {
settingsRow("server.rack") { Text("SMP servers") }
}
}
.disabled(chatModel.chatRunning != true)
Section("Help") {
NavigationLink {
@@ -127,6 +157,7 @@ struct SettingsView: View {
Text("Chat with the developers")
}
}
.disabled(chatModel.chatRunning != true)
settingsRow("envelope") { Text("[Send us email](mailto:chat@simplex.chat)") }
}
@@ -136,6 +167,7 @@ struct SettingsView: View {
} label: {
settingsRow("terminal") { Text("Chat console") }
}
.disabled(chatModel.chatRunning != true)
ZStack(alignment: .leading) {
Image(colorScheme == .dark ? "github_light" : "github")
.resizable()
@@ -150,12 +182,6 @@ struct SettingsView: View {
} label: {
settingsRow("gauge") { Text("Experimental features") }
}
// if let token = chatModel.deviceToken {
// HStack {
// notificationsIcon()
// notificationsToggle(token)
// }
// }
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
}
}
@@ -174,13 +200,13 @@ struct SettingsView: View {
switch (chatModel.tokenStatus) {
case .new:
icon = "bolt"
color = .primary
color = .secondary
case .registered:
icon = "bolt.fill"
color = .primary
color = .secondary
case .invalid:
icon = "bolt.slash"
color = .primary
color = .secondary
case .confirmed:
icon = "bolt.fill"
color = .yellow
@@ -189,80 +215,20 @@ struct SettingsView: View {
color = .green
case .expired:
icon = "bolt.slash.fill"
color = .primary
color = .secondary
case .none:
icon = "bolt"
color = .secondary
}
return Image(systemName: icon)
.padding(.trailing, 9)
.foregroundColor(color)
}
private func notificationsToggle(_ token: String) -> some View {
Toggle("Check messages", isOn: $useNotifications)
.onChange(of: useNotifications) { enable in
if enable {
showNotificationsAlert = true
whichNotificationsAlert = .enable
} else {
Task {
do {
try await apiDeleteToken(token: token)
chatModel.tokenStatus = .new
}
catch {
DispatchQueue.main.async {
if let cr = error as? ChatResponse {
let err = String(describing: cr)
logger.error("apiDeleteToken error: \(err)")
showNotificationsAlert = true
whichNotificationsAlert = .error("Error deleting token", err)
} else {
logger.error("apiDeleteToken unknown error: \(error.localizedDescription)")
}
}
}
}
}
}
.alert(isPresented: $showNotificationsAlert) {
switch (whichNotificationsAlert) {
case .enable: return enableNotificationsAlert(token)
case let .error(title, err): return Alert(title: Text(title), message: Text(err))
}
}
}
private func enableNotificationsAlert(_ token: String) -> Alert {
Alert(
title: Text("Enable notifications? (BETA)"),
message: Text("The app can receive background notifications every 20 minutes to check the new messages.\n*Please note*: if you confirm, your device token will be sent to SimpleX Chat notifications server."),
primaryButton: .destructive(Text("Confirm")) {
Task {
do {
chatModel.tokenStatus = try await apiRegisterToken(token: token)
} catch {
DispatchQueue.main.async {
useNotifications = false
if let cr = error as? ChatResponse {
let err = String(describing: cr)
logger.error("apiRegisterToken error: \(err)")
showNotificationsAlert = true
whichNotificationsAlert = .error("Error registering token", err)
} else {
logger.error("apiRegisterToken unknown error: \(error.localizedDescription)")
}
}
}
}
}, secondaryButton: .cancel() {
withAnimation() { useNotifications = false }
}
)
}
}
func settingsRow<Content : View>(_ icon: String, content: @escaping () -> Content) -> some View {
func settingsRow<Content : View>(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View {
ZStack(alignment: .leading) {
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.secondary)
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(color)
content().padding(.leading, indent)
}
}
@@ -7,6 +7,7 @@
//
import SwiftUI
import SimpleXChat
struct UserProfile: View {
@EnvironmentObject var chatModel: ChatModel
-23
View File
@@ -1,23 +0,0 @@
//
// dummy.m
// SimpleX
//
// Created by Evgeny Poberezkin on 22/01/2022.
//
#import <Foundation/Foundation.h>
#if defined(__x86_64__) && TARGET_IPHONE_SIMULATOR
#import <dirent.h>
int readdir_r$INODE64(DIR *restrict dirp, struct dirent *restrict entry,
struct dirent **restrict result) {
return readdir_r(dirp, entry, result);
}
DIR *opendir$INODE64(const char *name) {
return opendir(name);
}
#endif
+101 -55
View File
@@ -8,46 +8,72 @@
import UserNotifications
import OSLog
import SimpleXChat
let logger = Logger()
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
logger.debug("NotificationService.didReceive")
if getAppState() != .background {
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
let appState = appStateGroupDefault.get()
switch appState {
case .suspended:
logger.debug("NotificationService: app is suspended")
self.contentHandler = contentHandler
receiveNtfMessages(request, contentHandler)
case .suspending:
self.contentHandler = contentHandler
receiveNtfMessages(request, contentHandler)
default:
print("userInfo", request.content.userInfo)
contentHandler(request.content)
}
}
func receiveNtfMessages(_ request: UNNotificationRequest, _ contentHandler: @escaping (UNNotificationContent) -> Void) {
if case .documents = dbContainerGroupDefault.get() {
contentHandler(request.content)
return
}
logger.debug("NotificationService: app is in the background")
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let _ = startChat() {
let content = receiveMessages()
contentHandler (content)
return
}
if let bestAttemptContent = bestAttemptContent {
// Modify the notification content here...
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
contentHandler(bestAttemptContent)
let userInfo = request.content.userInfo
if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any],
let nonce = ntfData["nonce"] as? String,
let encNtfInfo = ntfData["message"] as? String,
let _ = startChat() {
if let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
if let content = receiveMessageForNotification() {
contentHandler(content)
} else if let connEntity = ntfMsgInfo.connEntity {
switch connEntity {
case let .rcvDirectMsgConnection(_, contact):
()
case let .rcvGroupMsgConnection(_, groupInfo, groupMember):
()
case let .sndFileConnection(_, sndFileTransfer):
()
case let .rcvFileConnection(_, rcvFileTransfer):
()
case let .userContactConnection(_, userContact):
()
}
contentHandler(request.content)
}
}
}
}
override func serviceExtensionTimeWillExpire() {
logger.debug("NotificationService.serviceExtensionTimeWillExpire")
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
if let contentHandler = self.contentHandler, let bestAttemptContent = self.bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
func startChat() -> User? {
@@ -57,9 +83,10 @@ func startChat() -> User? {
do {
try apiStartChat()
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
chatLastStartGroupDefault.set(Date.now)
return user
} catch {
logger.error("NotificationService startChat error: \(responseError(error))")
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
}
} else {
logger.debug("no active user")
@@ -67,40 +94,43 @@ func startChat() -> User? {
return nil
}
func receiveMessages() -> UNNotificationContent {
func receiveMessageForNotification() -> UNNotificationContent? {
logger.debug("NotificationService receiveMessages started")
while true {
let res = chatResponse(chat_recv_msg(getChatCtrl())!)
logger.debug("NotificationService receiveMessages: \(res.responseType)")
switch res {
// case let .newContactConnection(connection):
// case let .contactConnectionDeleted(connection):
case let .contactConnected(contact):
return createContactConnectedNtf(contact)
// case let .contactConnecting(contact):
// TODO profile update
case let .receivedContactRequest(contactRequest):
return createContactRequestNtf(contactRequest)
// case let .contactUpdated(toContact):
// TODO profile updated
case let .newChatItem(aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
return createMessageReceivedNtf(cInfo, cItem)
// case let .chatItemUpdated(aChatItem):
// TODO message updated
// let cInfo = aChatItem.chatInfo
// let cItem = aChatItem.chatItem
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
// case let .chatItemDeleted(_, toChatItem):
// TODO message updated
// case let .rcvFileComplete(aChatItem):
// TODO file received?
// let cInfo = aChatItem.chatInfo
// let cItem = aChatItem.chatItem
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
default:
logger.debug("NotificationService ignored event: \(res.responseType)")
if let res = recvSimpleXMsg() {
logger.debug("NotificationService receiveMessages: \(res.responseType)")
switch res {
// case let .newContactConnection(connection):
// case let .contactConnectionDeleted(connection):
case let .contactConnected(contact):
return createContactConnectedNtf(contact)
// case let .contactConnecting(contact):
// TODO profile update
case let .receivedContactRequest(contactRequest):
return createContactRequestNtf(contactRequest)
// case let .contactUpdated(toContact):
// TODO profile updated
case let .newChatItem(aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
return createMessageReceivedNtf(cInfo, cItem)
// case let .chatItemUpdated(aChatItem):
// TODO message updated
// let cInfo = aChatItem.chatInfo
// let cItem = aChatItem.chatItem
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
// case let .chatItemDeleted(_, toChatItem):
// TODO message updated
// case let .rcvFileComplete(aChatItem):
// TODO file received?
// let cInfo = aChatItem.chatInfo
// let cItem = aChatItem.chatItem
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
default:
logger.debug("NotificationService ignored event: \(res.responseType)")
}
} else {
return nil
}
}
}
@@ -119,9 +149,12 @@ func apiGetActiveUser() -> User? {
}
func apiStartChat() throws {
let r = sendSimpleXCmd(.startChat)
if case .chatStarted = r { return }
throw r
let r = sendSimpleXCmd(.startChat(subscribe: false))
switch r {
case .chatStarted: return
case .chatRunning: return
default: throw r
}
}
func apiSetFilesFolder(filesFolder: String) throws {
@@ -130,3 +163,16 @@ func apiSetFilesFolder(filesFolder: String) throws {
throw r
}
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r {
return NtfMessages(connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages)
}
return nil
}
struct NtfMessages {
var connEntity: ConnectionEntity?
var msgTs: Date?
var ntfMessages: [NtfMsgInfo]
}
@@ -1,11 +0,0 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
extern void hs_init(int argc, char **argv[]);
typedef void* chat_ctrl;
extern chat_ctrl chat_init(char *path);
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
extern char *chat_recv_msg(chat_ctrl ctl);
+340 -124
View File
@@ -18,6 +18,11 @@
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
5C14247E286DAC1D0004E3EE /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C142479286DAC1D0004E3EE /* libffi.a */; };
5C14247F286DAC1D0004E3EE /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C14247A286DAC1D0004E3EE /* libgmpxx.a */; };
5C142480286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C14247B286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a */; };
5C142481286DAC1D0004E3EE /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C14247C286DAC1D0004E3EE /* libgmp.a */; };
5C142482286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C14247D286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a */; };
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
@@ -31,6 +36,7 @@
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
5C3F1D5A2844B4DE00EC8A82 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */; };
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
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 */; };
@@ -38,33 +44,18 @@
5C55A92E283D0FDE00C4E99E /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = 5C55A92D283D0FDE00C4E99E /* sounds */; };
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; };
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; };
5C5E5D3D282447AB00B0488A /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; };
5C5E5D3E282447BF00B0488A /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; };
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; };
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; };
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
5C6F2A3C28522C9A00103588 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3728522C9A00103588 /* libgmp.a */; };
5C6F2A3D28522C9A00103588 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3728522C9A00103588 /* libgmp.a */; };
5C6F2A3E28522C9A00103588 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3828522C9A00103588 /* libffi.a */; };
5C6F2A3F28522C9A00103588 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3828522C9A00103588 /* libffi.a */; };
5C6F2A4028522C9A00103588 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3928522C9A00103588 /* libgmpxx.a */; };
5C6F2A4128522C9A00103588 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3928522C9A00103588 /* libgmpxx.a */; };
5C6F2A4228522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */; };
5C6F2A4328522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */; };
5C6F2A4428522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */; };
5C6F2A4528522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */; };
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
5C764E80279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; };
5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C764E7B279C71D4000C6508 /* libiconv.tbd */; };
5C764E83279C748B000C6508 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C764E7C279C71DB000C6508 /* libz.tbd */; };
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; };
5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */; };
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */; };
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
5C9FD96B27A56D4D0075386C /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96A27A56D4D0075386C /* JSON.swift */; };
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; };
@@ -78,6 +69,9 @@
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; };
5CB0BA962827143500B3292C /* MakeConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA952827143500B3292C /* MakeConnection.swift */; };
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; };
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; };
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; };
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */; };
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D327A853F100ACCCDD /* SettingsButton.swift */; };
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; };
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; };
@@ -91,25 +85,30 @@
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
5CDCAD5328186F9500503DA2 /* GroupDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */; };
5CDCAD5428186F9700503DA2 /* GroupDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */; };
5CDCAD5828187C7500503DA2 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5728187C7500503DA2 /* dummy.m */; };
5CDCAD5F28187D6900503DA2 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */; };
5CDCAD6128187D8000503DA2 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDCAD6028187D7900503DA2 /* libz.tbd */; };
5CDCAD682818876500503DA2 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96A27A56D4D0075386C /* JSON.swift */; };
5CDCAD7628188D3600503DA2 /* APITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7428188D2900503DA2 /* APITypes.swift */; };
5CDCAD7728188D3800503DA2 /* ChatTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */; };
5CDCAD7828188FD300503DA2 /* ChatTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */; };
5CDCAD7928188FD600503DA2 /* APITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7428188D2900503DA2 /* APITypes.swift */; };
5CDCAD7C2818924D00503DA2 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64DAE1502809D9F5000DA960 /* FileUtils.swift */; };
5CDCAD7E2818941F00503DA2 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7D2818941F00503DA2 /* API.swift */; };
5CDCAD7F281894FB00503DA2 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7D2818941F00503DA2 /* API.swift */; };
5CDCAD81281A7E2700503DA2 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD80281A7E2700503DA2 /* Notifications.swift */; };
5CDCAD82281A7E2700503DA2 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD80281A7E2700503DA2 /* Notifications.swift */; };
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; };
5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */ = {isa = PBXBuildFile; fileRef = 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */; };
5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */; };
5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* AppGroup.swift */; };
5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; };
5CE2BA8E284533A300EC33A6 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7D2818941F00503DA2 /* API.swift */; };
5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7428188D2900503DA2 /* APITypes.swift */; };
5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96A27A56D4D0075386C /* JSON.swift */; };
5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD80281A7E2700503DA2 /* Notifications.swift */; };
5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64DAE1502809D9F5000DA960 /* FileUtils.swift */; };
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */; };
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDCAD6028187D7900503DA2 /* libz.tbd */; };
5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA8A2845332200EC33A6 /* SimpleX.h */; settings = {ATTRIBUTES = (Public, ); }; };
5CE2BA97284537A800EC33A6 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CE2BA96284537A800EC33A6 /* dummy.m */; };
5CE2BA9D284555F500EC33A6 /* SimpleX NSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
5CE2BAA62845617C00EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; platformFilter = ios; };
5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407127ADB1D0007B033A /* Emoji.swift */; };
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; };
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; };
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; };
@@ -121,7 +120,6 @@
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
64DAE1512809D9F5000DA960 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64DAE1502809D9F5000DA960 /* FileUtils.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -132,8 +130,54 @@
remoteGlobalIDString = 5CA059C9279559F40002BEB4;
remoteInfo = "SimpleX (iOS)";
};
5CE2BA6E2845308900EC33A6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5CA059BE279559F40002BEB4 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5CE2BA672845308900EC33A6;
remoteInfo = SimpleXChat;
};
5CE2BA9E284555F500EC33A6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5CA059BE279559F40002BEB4 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5CDCAD442818589900503DA2;
remoteInfo = "SimpleX NSE";
};
5CE2BAA82845617C00EC33A6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5CA059BE279559F40002BEB4 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5CE2BA672845308900EC33A6;
remoteInfo = SimpleXChat;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
5CE2BA722845308900EC33A6 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
5CE2BAA0284555F500EC33A6 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
5CE2BA9D284555F500EC33A6 /* SimpleX NSE.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
3C714776281C081000CB4D4B /* WebRTCView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCView.swift; sourceTree = "<group>"; };
3C714779281C0F6800CB4D4B /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; name = www; path = ../android/app/src/main/assets/www; sourceTree = "<group>"; };
@@ -147,6 +191,11 @@
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; };
5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; };
5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
5C142479286DAC1D0004E3EE /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C14247A286DAC1D0004E3EE /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C14247B286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a"; sourceTree = "<group>"; };
5C14247C286DAC1D0004E3EE /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C14247D286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a"; sourceTree = "<group>"; };
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; };
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
@@ -161,6 +210,7 @@
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeaturesView.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>"; };
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>"; };
@@ -172,21 +222,13 @@
5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = "<group>"; };
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
5C6F2A3728522C9A00103588 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C6F2A3828522C9A00103588 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C6F2A3928522C9A00103588 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a"; sourceTree = "<group>"; };
5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a"; sourceTree = "<group>"; };
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
5C764E7B279C71D4000C6508 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; };
5C764E7C279C71DB000C6508 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; };
5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX (iOS)-Bridging-Header.h"; sourceTree = "<group>"; };
5C764E7F279C7276000C6508 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = "<group>"; };
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = "<group>"; };
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = "<group>"; };
5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNotificationsMode.swift; sourceTree = "<group>"; };
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
@@ -204,6 +246,9 @@
5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = "<group>"; };
5CB0BA952827143500B3292C /* MakeConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeConnection.swift; sourceTree = "<group>"; };
5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = "<group>"; };
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; };
5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEnvironment.swift; sourceTree = "<group>"; };
5CB924D327A853F100ACCCDD /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = "<group>"; };
5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; };
@@ -220,19 +265,24 @@
5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX NSE.entitlements"; sourceTree = "<group>"; };
5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDefaults.swift; sourceTree = "<group>"; };
5CDCAD5628187C7500503DA2 /* SimpleX NSE-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX NSE-Bridging-Header.h"; sourceTree = "<group>"; };
5CDCAD5728187C7500503DA2 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = "<group>"; };
5CDCAD5228186F9500503DA2 /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = "<group>"; };
5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; };
5CDCAD6028187D7900503DA2 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; };
5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTypes.swift; sourceTree = "<group>"; };
5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = "<group>"; };
5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SimpleXChat.h; sourceTree = "<group>"; };
5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = SimpleXChat.docc; sourceTree = "<group>"; };
5CE2BA8A2845332200EC33A6 /* SimpleX.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimpleX.h; sourceTree = "<group>"; };
5CE2BA96284537A800EC33A6 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = "<group>"; };
5CE4407127ADB1D0007B033A /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = "<group>"; };
5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = "<group>"; };
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
@@ -252,15 +302,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5C6F2A4228522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */,
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */,
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */,
5C6F2A4428522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */,
5C6F2A3E28522C9A00103588 /* libffi.a in Frameworks */,
5C6F2A3C28522C9A00103588 /* libgmp.a in Frameworks */,
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */,
5C764E83279C748B000C6508 /* libz.tbd in Frameworks */,
5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */,
5C6F2A4028522C9A00103588 /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -275,13 +319,21 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5C6F2A4528522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */,
5C6F2A4328522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */,
5CDCAD5F28187D6900503DA2 /* libiconv.tbd in Frameworks */,
5C6F2A4128522C9A00103588 /* libgmpxx.a in Frameworks */,
5C6F2A3D28522C9A00103588 /* libgmp.a in Frameworks */,
5CDCAD6128187D8000503DA2 /* libz.tbd in Frameworks */,
5C6F2A3F28522C9A00103588 /* libffi.a in Frameworks */,
5CE2BAA62845617C00EC33A6 /* SimpleXChat.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
5CE2BA652845308900EC33A6 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5C14247F286DAC1D0004E3EE /* libgmpxx.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C142480286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a in Frameworks */,
5C14247E286DAC1D0004E3EE /* libffi.a in Frameworks */,
5C142482286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a in Frameworks */,
5C142481286DAC1D0004E3EE /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -311,6 +363,7 @@
5C5F4AC227A5E9AF00B51EF1 /* Chat */,
5CB9250B27A942F300ACCCDD /* ChatList */,
5CB924DD27A8622200ACCCDD /* NewChat */,
5CFA59C22860B04D00863A68 /* Database */,
5CB924DF27A8678B00ACCCDD /* UserSettings */,
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
);
@@ -334,11 +387,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
5C6F2A3828522C9A00103588 /* libffi.a */,
5C6F2A3728522C9A00103588 /* libgmp.a */,
5C6F2A3928522C9A00103588 /* libgmpxx.a */,
5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */,
5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */,
5C142479286DAC1D0004E3EE /* libffi.a */,
5C14247C286DAC1D0004E3EE /* libgmp.a */,
5C14247A286DAC1D0004E3EE /* libgmpxx.a */,
5C14247B286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a */,
5C14247D286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -349,8 +402,6 @@
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */,
5CDCAD6028187D7900503DA2 /* libz.tbd */,
5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */,
5C764E7C279C71DB000C6508 /* libz.tbd */,
5C764E7B279C71D4000C6508 /* libiconv.tbd */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -358,11 +409,12 @@
5C764E87279CBC8E000C6508 /* Model */ = {
isa = PBXGroup;
children = (
5CDCAD7128188CEB00503DA2 /* Shared */,
5C764E88279CBCB3000C6508 /* ChatModel.swift */,
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */,
5C35CFC727B2782E00FB6C6D /* BGManager.swift */,
5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */,
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */,
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */,
);
path = Model;
sourceTree = "<group>";
@@ -394,6 +446,7 @@
5CA059C2279559F40002BEB4 /* Shared */,
5CDCAD462818589900503DA2 /* SimpleX NSE */,
5CA059DA279559F40002BEB4 /* Tests iOS */,
5CE2BA692845308900EC33A6 /* SimpleXChat */,
5CA059CB279559F40002BEB4 /* Products */,
5C764E7A279C71D4000C6508 /* Frameworks */,
);
@@ -405,12 +458,9 @@
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */,
5C36027227F47AD5009F19D9 /* AppDelegate.swift */,
5CA059C4279559F40002BEB4 /* ContentView.swift */,
64DAE1502809D9F5000DA960 /* FileUtils.swift */,
5C764E87279CBC8E000C6508 /* Model */,
5C2E260D27A30E2400F70299 /* Views */,
5CA059C5279559F40002BEB4 /* Assets.xcassets */,
5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */,
5C764E7F279C7276000C6508 /* dummy.m */,
5C13730C2815740A00F43030 /* DebugJSON.playground */,
);
path = Shared;
@@ -422,6 +472,7 @@
5CA059CA279559F40002BEB4 /* SimpleX.app */,
5CA059D7279559F40002BEB4 /* Tests iOS.xctest */,
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */,
5CE2BA682845308900EC33A6 /* SimpleXChat.framework */,
);
name = Products;
sourceTree = "<group>";
@@ -440,9 +491,10 @@
children = (
5CB0BA8D2827126500B3292C /* OnboardingView.swift */,
5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */,
5CB0BA91282713FD00B3292C /* CreateProfile.swift */,
5CB0BA952827143500B3292C /* MakeConnection.swift */,
5CB0BA992827FD8800B3292C /* HowItWorks.swift */,
5CB0BA91282713FD00B3292C /* CreateProfile.swift */,
5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */,
5CB0BA952827143500B3292C /* MakeConnection.swift */,
);
path = Onboarding;
sourceTree = "<group>";
@@ -465,6 +517,7 @@
children = (
5CB924D327A853F100ACCCDD /* SettingsButton.swift */,
5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
5CB346E62868D76D001FD2EF /* NotificationsView.swift */,
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
5CB924E327A8683A00ACCCDD /* UserAddress.swift */,
@@ -497,24 +550,27 @@
5CDCAD492818589900503DA2 /* Info.plist */,
5CB0BA892826CB3A00B3292C /* Localizable.strings */,
5CB0BA862826CB3A00B3292C /* InfoPlist.strings */,
5CDCAD5728187C7500503DA2 /* dummy.m */,
5CDCAD5628187C7500503DA2 /* SimpleX NSE-Bridging-Header.h */,
);
path = "SimpleX NSE";
sourceTree = "<group>";
};
5CDCAD7128188CEB00503DA2 /* Shared */ = {
5CE2BA692845308900EC33A6 /* SimpleXChat */ = {
isa = PBXGroup;
children = (
5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */,
5CDCAD5228186F9500503DA2 /* AppGroup.swift */,
5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */,
5CDCAD7428188D2900503DA2 /* APITypes.swift */,
5C5E5D3C282447AB00B0488A /* CallTypes.swift */,
5C9FD96A27A56D4D0075386C /* JSON.swift */,
5CDCAD7D2818941F00503DA2 /* API.swift */,
5CDCAD80281A7E2700503DA2 /* Notifications.swift */,
64DAE1502809D9F5000DA960 /* FileUtils.swift */,
5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */,
5CE2BA8A2845332200EC33A6 /* SimpleX.h */,
5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */,
5CE2BA96284537A800EC33A6 /* dummy.m */,
);
path = Shared;
path = SimpleXChat;
sourceTree = "<group>";
};
5CE4407427ADB657007B033A /* ChatItem */ = {
@@ -547,8 +603,30 @@
path = ComposeMessage;
sourceTree = "<group>";
};
5CFA59C22860B04D00863A68 /* Database */ = {
isa = PBXGroup;
children = (
5C4B3B09285FB130003915F2 /* DatabaseView.swift */,
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */,
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */,
);
path = Database;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
5CE2BA632845308900EC33A6 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */,
5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
5CA059C9279559F40002BEB4 /* SimpleX (iOS) */ = {
isa = PBXNativeTarget;
@@ -557,10 +635,14 @@
5CA059C6279559F40002BEB4 /* Sources */,
5CA059C7279559F40002BEB4 /* Frameworks */,
5CA059C8279559F40002BEB4 /* Resources */,
5CE2BA722845308900EC33A6 /* Embed Frameworks */,
5CE2BAA0284555F500EC33A6 /* Embed App Extensions */,
);
buildRules = (
);
dependencies = (
5CE2BA6F2845308900EC33A6 /* PBXTargetDependency */,
5CE2BA9F284555F500EC33A6 /* PBXTargetDependency */,
);
name = "SimpleX (iOS)";
packageProductDependencies = (
@@ -599,12 +681,31 @@
buildRules = (
);
dependencies = (
5CE2BAA92845617C00EC33A6 /* PBXTargetDependency */,
);
name = "SimpleX NSE";
productName = "SimpleX NSE";
productReference = 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */;
productType = "com.apple.product-type.app-extension";
};
5CE2BA672845308900EC33A6 /* SimpleXChat */ = {
isa = PBXNativeTarget;
buildConfigurationList = 5CE2BA752845308900EC33A6 /* Build configuration list for PBXNativeTarget "SimpleXChat" */;
buildPhases = (
5CE2BA632845308900EC33A6 /* Headers */,
5CE2BA642845308900EC33A6 /* Sources */,
5CE2BA652845308900EC33A6 /* Frameworks */,
5CE2BA662845308900EC33A6 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SimpleXChat;
productName = SimpleXChat;
productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */;
productType = "com.apple.product-type.framework";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -613,7 +714,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1330;
LastUpgradeCheck = 1330;
LastUpgradeCheck = 1340;
ORGANIZATIONNAME = "SimpleX Chat";
TargetAttributes = {
5CA059C9279559F40002BEB4 = {
@@ -628,6 +729,10 @@
CreatedOnToolsVersion = 13.3;
LastSwiftMigration = 1330;
};
5CE2BA672845308900EC33A6 = {
CreatedOnToolsVersion = 13.3;
LastSwiftMigration = 1330;
};
};
};
buildConfigurationList = 5CA059C1279559F40002BEB4 /* Build configuration list for PBXProject "SimpleX" */;
@@ -637,6 +742,7 @@
knownRegions = (
en,
ru,
Base,
);
mainGroup = 5CA059BD279559F40002BEB4;
packageReferences = (
@@ -649,6 +755,7 @@
5CA059C9279559F40002BEB4 /* SimpleX (iOS) */,
5CA059D6279559F40002BEB4 /* Tests iOS */,
5CDCAD442818589900503DA2 /* SimpleX NSE */,
5CE2BA672845308900EC33A6 /* SimpleXChat */,
);
};
/* End PBXProject section */
@@ -682,6 +789,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
5CE2BA662845308900EC33A6 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -691,25 +805,22 @@
files = (
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */,
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */,
5CDCAD7F281894FB00503DA2 /* API.swift in Sources */,
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */,
5CDCAD81281A7E2700503DA2 /* Notifications.swift in Sources */,
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */,
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */,
5CDCAD7728188D3800503DA2 /* ChatTypes.swift in Sources */,
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */,
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */,
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */,
5CDCAD5328186F9500503DA2 /* GroupDefaults.swift in Sources */,
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */,
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */,
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */,
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */,
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */,
5C764E80279C7276000C6508 /* dummy.m in Sources */,
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */,
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */,
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */,
@@ -719,10 +830,9 @@
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */,
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */,
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */,
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */,
5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */,
5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */,
5CDCAD7628188D3600503DA2 /* APITypes.swift in Sources */,
5C9FD96B27A56D4D0075386C /* JSON.swift in Sources */,
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */,
5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */,
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */,
@@ -735,6 +845,7 @@
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */,
5CB0BA962827143500B3292C /* MakeConnection.swift in Sources */,
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */,
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */,
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */,
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */,
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
@@ -746,12 +857,14 @@
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */,
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */,
5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */,
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */,
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */,
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */,
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */,
5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */,
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */,
64DAE1512809D9F5000DA960 /* FileUtils.swift in Sources */,
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */,
5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */,
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */,
@@ -761,7 +874,6 @@
3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */,
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
5C5E5D3D282447AB00B0488A /* CallTypes.swift in Sources */,
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */,
@@ -784,17 +896,25 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5CDCAD7928188FD600503DA2 /* APITypes.swift in Sources */,
5C5E5D3E282447BF00B0488A /* CallTypes.swift in Sources */,
5CDCAD5828187C7500503DA2 /* dummy.m in Sources */,
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */,
5CDCAD82281A7E2700503DA2 /* Notifications.swift in Sources */,
5CDCAD7C2818924D00503DA2 /* FileUtils.swift in Sources */,
5CDCAD682818876500503DA2 /* JSON.swift in Sources */,
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */,
5CDCAD7828188FD300503DA2 /* ChatTypes.swift in Sources */,
5CDCAD7E2818941F00503DA2 /* API.swift in Sources */,
5CDCAD5428186F9700503DA2 /* GroupDefaults.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
5CE2BA642845308900EC33A6 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5CE2BA97284537A800EC33A6 /* dummy.m in Sources */,
5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */,
5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */,
5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */,
5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */,
5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */,
5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */,
5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */,
5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */,
5CE2BA8E284533A300EC33A6 /* API.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -806,6 +926,22 @@
target = 5CA059C9279559F40002BEB4 /* SimpleX (iOS) */;
targetProxy = 5CA059D8279559F40002BEB4 /* PBXContainerItemProxy */;
};
5CE2BA6F2845308900EC33A6 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5CE2BA672845308900EC33A6 /* SimpleXChat */;
targetProxy = 5CE2BA6E2845308900EC33A6 /* PBXContainerItemProxy */;
};
5CE2BA9F284555F500EC33A6 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5CDCAD442818589900503DA2 /* SimpleX NSE */;
targetProxy = 5CE2BA9E284555F500EC33A6 /* PBXContainerItemProxy */;
};
5CE2BAA92845617C00EC33A6 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilter = ios;
target = 5CE2BA672845308900EC33A6 /* SimpleXChat */;
targetProxy = 5CE2BAA82845617C00EC33A6 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -849,6 +985,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
@@ -909,6 +1046,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
@@ -961,12 +1099,13 @@
5CA059F4279559F40002BEB4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 53;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -987,15 +1126,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim";
MARKETING_VERSION = 2.2.1;
MARKETING_VERSION = 3.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Shared/SimpleX (iOS)-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
@@ -1004,12 +1139,13 @@
5CA059F5279559F40002BEB4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 53;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1030,14 +1166,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim";
MARKETING_VERSION = 2.2.1;
MARKETING_VERSION = 3.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Shared/SimpleX (iOS)-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
@@ -1091,7 +1224,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 53;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1104,22 +1237,12 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
"$(PROJECT_DIR)/Libraries/ios",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = (
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 2.2.1;
MARKETING_VERSION = 3.0;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "SimpleX NSE/SimpleX NSE-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
@@ -1131,7 +1254,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 53;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1144,6 +1267,40 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
5CE2BA732845308900EC33A6 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved.";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
"$(PROJECT_DIR)/Libraries/ios",
@@ -1152,16 +1309,66 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 2.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "SimpleX NSE/SimpleX NSE-Bridging-Header.h";
SWIFT_INCLUDE_PATHS = "";
SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
5CE2BA742845308900EC33A6 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved.";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
"$(PROJECT_DIR)/Libraries/ios",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = (
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_INCLUDE_PATHS = "";
SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
@@ -1204,6 +1411,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
5CE2BA752845308900EC33A6 /* Build configuration list for PBXNativeTarget "SimpleXChat" */ = {
isa = XCConfigurationList;
buildConfigurations = (
5CE2BA732845308900EC33A6 /* Debug */,
5CE2BA742845308900EC33A6 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1330"
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -10,23 +10,63 @@ import Foundation
private var chatController: chat_ctrl?
func getChatCtrl() -> chat_ctrl {
public func getChatCtrl() -> chat_ctrl {
if let controller = chatController { return controller }
let dataDir = getDocumentsDirectory().path + "/mobile_v1"
logger.debug("documents directory \(dataDir)")
var cstr = dataDir.cString(using: .utf8)!
let dbPath = getAppDatabasePath().path
logger.debug("getChatCtrl DB path: \(dbPath)")
var cstr = dbPath.cString(using: .utf8)!
chatController = chat_init(&cstr)
logger.debug("getChatCtrl: chat_init")
return chatController!
}
func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
var c = cmd.cmdString.cString(using: .utf8)!
return chatResponse(chat_send_cmd(getChatCtrl(), &c))
public func resetChatCtrl() {
chatController = nil
}
func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse {
let s = String.init(cString: cjson)
public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
var c = cmd.cmdString.cString(using: .utf8)!
let cjson = chat_send_cmd(getChatCtrl(), &c)!
return chatResponse(fromCString(cjson))
}
// in microseconds
let MESSAGE_TIMEOUT: Int32 = 15_000_000
public func recvSimpleXMsg() -> ChatResponse? {
if let cjson = chat_recv_msg_wait(getChatCtrl(), MESSAGE_TIMEOUT) {
let s = fromCString(cjson)
return s == "" ? nil : chatResponse(s)
}
return nil
}
public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
var c = s.cString(using: .utf8)!
if let cjson = chat_parse_markdown(&c) {
if let d = fromCString(cjson).data(using: .utf8) {
do {
let r = try jsonDecoder.decode(ParsedMarkdown.self, from: d)
return r.formattedText
} catch {
logger.error("parseSimpleXMarkdown jsonDecoder.decode error: \(error.localizedDescription)")
}
}
}
return nil
}
struct ParsedMarkdown: Decodable {
var formattedText: [FormattedText]?
}
private func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String {
let s = String.init(cString: c)
free(c)
return s
}
public func chatResponse(_ s: String) -> ChatResponse {
let d = s.data(using: .utf8)!
// TODO is there a way to do it without copying the data? e.g:
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
@@ -46,7 +86,6 @@ func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse {
}
json = prettyJSON(j)
}
free(cjson)
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
}
@@ -57,7 +96,7 @@ func prettyJSON(_ obj: NSDictionary) -> String? {
return nil
}
func responseError(_ err: Error) -> String {
public func responseError(_ err: Error) -> String {
if let r = err as? ChatResponse {
return String(describing: r)
} else {
@@ -7,24 +7,32 @@
//
import Foundation
import SwiftUI
let jsonDecoder = getJSONDecoder()
let jsonEncoder = getJSONEncoder()
enum ChatCommand {
public enum ChatCommand {
case showActiveUser
case createActiveUser(profile: Profile)
case startChat
case startChat(subscribe: Bool)
case apiStopChat
case apiActivateChat
case apiSuspendChat(timeoutMicroseconds: Int)
case setFilesFolder(filesFolder: String)
case apiExportArchive(config: ArchiveConfig)
case apiImportArchive(config: ArchiveConfig)
case apiDeleteStorage
case apiGetChats
case apiGetChat(type: ChatType, id: Int64)
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent)
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent)
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
case apiRegisterToken(token: String)
case apiVerifyToken(token: String, code: String, nonce: String)
case apiIntervalNofication(token: String, interval: Int)
case apiDeleteToken(token: String)
case apiGetNtfToken
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
case apiDeleteToken(token: DeviceToken)
case apiGetNtfMessage(nonce: String, encNtfInfo: String)
case getUserSMPServers
case setUserSMPServers(smpServers: [String])
case addContact
@@ -32,7 +40,6 @@ enum ChatCommand {
case apiDeleteChat(type: ChatType, id: Int64)
case apiClearChat(type: ChatType, id: Int64)
case apiUpdateProfile(profile: Profile)
case apiParseMarkdown(text: String)
case createMyAddress
case deleteMyAddress
case showMyAddress
@@ -50,13 +57,19 @@ enum ChatCommand {
case receiveFile(fileId: Int64)
case string(String)
var cmdString: String {
public var cmdString: String {
get {
switch self {
case .showActiveUser: return "/u"
case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)"
case .startChat: return "/_start"
case let .startChat(subscribe): return "/_start subscribe=\(subscribe ? "on" : "off")"
case .apiStopChat: return "/_stop"
case .apiActivateChat: return "/_app activate"
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
case .apiDeleteStorage: return "/_db delete"
case .apiGetChats: return "/_get chats pcc=on"
case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
case let .apiSendMessage(type, id, file, quotedItemId, mc):
@@ -64,10 +77,11 @@ enum ChatCommand {
return "/_send \(ref(type, id)) json \(msg)"
case let .apiUpdateChatItem(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)"
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
case let .apiRegisterToken(token): return "/_ntf register apns \(token)"
case let .apiVerifyToken(token, code, nonce): return "/_ntf verify apns \(token) \(code) \(nonce)"
case let .apiIntervalNofication(token, interval): return "/_ntf interval apns \(token) \(interval)"
case let .apiDeleteToken(token): return "/_ntf delete apns \(token)"
case .apiGetNtfToken: return "/_ntf get "
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)"
case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)"
case .getUserSMPServers: return "/smp_servers"
case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))"
case .addContact: return "/connect"
@@ -75,7 +89,6 @@ enum ChatCommand {
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))"
case let .apiParseMarkdown(text): return "/_parse \(text)"
case .createMyAddress: return "/address"
case .deleteMyAddress: return "/delete_address"
case .showMyAddress: return "/show_address"
@@ -95,22 +108,29 @@ enum ChatCommand {
}
}
var cmdType: String {
public var cmdType: String {
get {
switch self {
case .showActiveUser: return "showActiveUser"
case .createActiveUser: return "createActiveUser"
case .startChat: return "startChat"
case .apiStopChat: return "apiStopChat"
case .apiActivateChat: return "apiActivateChat"
case .apiSuspendChat: return "apiSuspendChat"
case .setFilesFolder: return "setFilesFolder"
case .apiExportArchive: return "apiExportArchive"
case .apiImportArchive: return "apiImportArchive"
case .apiDeleteStorage: return "apiDeleteStorage"
case .apiGetChats: return "apiGetChats"
case .apiGetChat: return "apiGetChat"
case .apiSendMessage: return "apiSendMessage"
case .apiUpdateChatItem: return "apiUpdateChatItem"
case .apiDeleteChatItem: return "apiDeleteChatItem"
case .apiGetNtfToken: return "apiGetNtfToken"
case .apiRegisterToken: return "apiRegisterToken"
case .apiVerifyToken: return "apiVerifyToken"
case .apiIntervalNofication: return "apiIntervalNofication"
case .apiDeleteToken: return "apiDeleteToken"
case .apiGetNtfMessage: return "apiGetNtfMessage"
case .getUserSMPServers: return "getUserSMPServers"
case .setUserSMPServers: return "setUserSMPServers"
case .addContact: return "addContact"
@@ -118,7 +138,6 @@ enum ChatCommand {
case .apiDeleteChat: return "apiDeleteChat"
case .apiClearChat: return "apiClearChat"
case .apiUpdateProfile: return "apiUpdateProfile"
case .apiParseMarkdown: return "apiParseMarkdown"
case .createMyAddress: return "createMyAddress"
case .deleteMyAddress: return "deleteMyAddress"
case .showMyAddress: return "showMyAddress"
@@ -151,11 +170,13 @@ struct APIResponse: Decodable {
var resp: ChatResponse
}
enum ChatResponse: Decodable, Error {
public enum ChatResponse: Decodable, Error {
case response(type: String, json: String)
case activeUser(user: User)
case chatStarted
case chatRunning
case chatStopped
case chatSuspended
case apiChats(chats: [ChatData])
case apiChat(chat: ChatData)
case userSMPServers(smpServers: [String])
@@ -167,7 +188,6 @@ enum ChatResponse: Decodable, Error {
case chatCleared(chatInfo: ChatInfo)
case userProfileNoChange
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
case apiParsedMarkdown(formattedText: [FormattedText]?)
case userContactLink(connReqContact: String)
case userContactLinkCreated(connReqContact: String)
case userContactLinkDeleted
@@ -205,19 +225,23 @@ enum ChatResponse: Decodable, Error {
case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
case callEnded(contact: Contact)
case ntfTokenStatus(status: NtfTknStatus)
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
case ntfMessages(connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
case newContactConnection(connection: PendingContactConnection)
case contactConnectionDeleted(connection: PendingContactConnection)
case cmdOk
case chatCmdError(chatError: ChatError)
case chatError(chatError: ChatError)
var responseType: String {
public var responseType: String {
get {
switch self {
case let .response(type, _): return "* \(type)"
case .activeUser: return "activeUser"
case .chatStarted: return "chatStarted"
case .chatRunning: return "chatRunning"
case .chatStopped: return "chatStopped"
case .chatSuspended: return "chatSuspended"
case .apiChats: return "apiChats"
case .apiChat: return "apiChat"
case .userSMPServers: return "userSMPServers"
@@ -229,7 +253,6 @@ enum ChatResponse: Decodable, Error {
case .chatCleared: return "chatCleared"
case .userProfileNoChange: return "userProfileNoChange"
case .userProfileUpdated: return "userProfileUpdated"
case .apiParsedMarkdown: return "apiParsedMarkdown"
case .userContactLink: return "userContactLink"
case .userContactLinkCreated: return "userContactLinkCreated"
case .userContactLinkDeleted: return "userContactLinkDeleted"
@@ -265,6 +288,8 @@ enum ChatResponse: Decodable, Error {
case .callExtraInfo: return "callExtraInfo"
case .callEnded: return "callEnded"
case .ntfTokenStatus: return "ntfTokenStatus"
case .ntfToken: return "ntfToken"
case .ntfMessages: return "ntfMessages"
case .newContactConnection: return "newContactConnection"
case .contactConnectionDeleted: return "contactConnectionDeleted"
case .cmdOk: return "cmdOk"
@@ -274,13 +299,15 @@ enum ChatResponse: Decodable, Error {
}
}
var details: String {
public var details: String {
get {
switch self {
case let .response(_, json): return json
case let .activeUser(user): return String(describing: user)
case .chatStarted: return noDetails
case .chatRunning: return noDetails
case .chatStopped: return noDetails
case .chatSuspended: return noDetails
case let .apiChats(chats): return String(describing: chats)
case let .apiChat(chat): return String(describing: chat)
case let .userSMPServers(smpServers): return String(describing: smpServers)
@@ -292,7 +319,6 @@ enum ChatResponse: Decodable, Error {
case let .chatCleared(chatInfo): return String(describing: chatInfo)
case .userProfileNoChange: return noDetails
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
case let .apiParsedMarkdown(formattedText): return String(describing: formattedText)
case let .userContactLink(connReq): return connReq
case let .userContactLinkCreated(connReq): return connReq
case .userContactLinkDeleted: return noDetails
@@ -328,6 +354,8 @@ enum ChatResponse: Decodable, Error {
case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))"
case let .callEnded(contact): return "contact: \(contact.id)"
case let .ntfTokenStatus(status): return String(describing: status)
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
case let .ntfMessages(connEntity, msgTs, ntfMessages): return "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))"
case let .newContactConnection(connection): return String(describing: connection)
case let .contactConnectionDeleted(connection): return String(describing: connection)
case .cmdOk: return noDetails
@@ -346,7 +374,89 @@ struct ComposedMessage: Encodable {
var msgContent: MsgContent
}
func decodeJSON<T: Decodable>(_ json: String) -> T? {
public struct ArchiveConfig: Encodable {
var archivePath: String
var disableCompression: Bool?
public init(archivePath: String, disableCompression: Bool? = nil) {
self.archivePath = archivePath
self.disableCompression = disableCompression
}
}
public protocol SelectableItem: Hashable, Identifiable {
var label: LocalizedStringKey { get }
static var values: [Self] { get }
}
public struct DeviceToken: Decodable {
var pushProvider: PushProvider
var token: String
public init(pushProvider: PushProvider, token: String) {
self.pushProvider = pushProvider
self.token = token
}
public var cmdString: String {
"\(pushProvider) \(token)"
}
}
public enum PushEnvironment: String {
case development
case production
}
public enum PushProvider: String, Decodable {
case apns_dev
case apns_prod
public init(env: PushEnvironment) {
switch env {
case .development: self = .apns_dev
case .production: self = .apns_prod
}
}
}
public enum NotificationsMode: String, Decodable, SelectableItem {
case off = "OFF"
case periodic = "PERIODIC"
case instant = "INSTANT"
public var label: LocalizedStringKey {
switch self {
case .off: return "Off (Local)"
case .periodic: return "Periodically"
case .instant: return "Instantly"
}
}
public var id: String { self.rawValue }
public static var values: [NotificationsMode] = [.instant, .periodic, .off]
}
public enum NotificationPreviewMode: String, SelectableItem {
case hidden
case contact
case message
public var label: LocalizedStringKey {
switch self {
case .hidden: return "Hidden"
case .contact: return "Contact"
case .message: return "Message"
}
}
public var id: String { self.rawValue }
public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden]
}
public func decodeJSON<T: Decodable>(_ json: String) -> T? {
if let data = json.data(using: .utf8) {
return try? jsonDecoder.decode(T.self, from: data)
}
@@ -366,7 +476,7 @@ private func getJSONObject(_ cjson: UnsafePointer<CChar>) -> NSDictionary? {
return try? JSONSerialization.jsonObject(with: d) as? NSDictionary
}
func encodeJSON<T: Encodable>(_ value: T) -> String {
public func encodeJSON<T: Encodable>(_ value: T) -> String {
let data = try! jsonEncoder.encode(value)
return String(decoding: data, as: UTF8.self)
}
@@ -375,13 +485,13 @@ private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
encodeJSON(value).cString(using: .utf8)!
}
enum ChatError: Decodable {
public enum ChatError: Decodable {
case error(errorType: ChatErrorType)
case errorAgent(agentError: AgentErrorType)
case errorStore(storeError: StoreError)
}
enum ChatErrorType: Decodable {
public enum ChatErrorType: Decodable {
case noActiveUser
case activeUserExists
case chatNotStarted
@@ -415,7 +525,7 @@ enum ChatErrorType: Decodable {
case commandError(message: String)
}
enum StoreError: Decodable {
public enum StoreError: Decodable {
case duplicateName
case contactNotFound(contactId: Int64)
case contactNotFoundByName(contactName: ContactName)
@@ -448,7 +558,7 @@ enum StoreError: Decodable {
case chatItemNotFoundByFileId(fileId: Int64)
}
enum AgentErrorType: Decodable {
public enum AgentErrorType: Decodable {
case CMD(cmdErr: CommandErrorType)
case CONN(connErr: ConnectionErrorType)
case SMP(smpErr: ProtocolErrorType)
@@ -458,7 +568,7 @@ enum AgentErrorType: Decodable {
case INTERNAL(internalErr: String)
}
enum CommandErrorType: Decodable {
public enum CommandErrorType: Decodable {
case PROHIBITED
case SYNTAX
case NO_CONN
@@ -466,7 +576,7 @@ enum CommandErrorType: Decodable {
case LARGE
}
enum ConnectionErrorType: Decodable {
public enum ConnectionErrorType: Decodable {
case NOT_FOUND
case DUPLICATE
case SIMPLEX
@@ -474,7 +584,7 @@ enum ConnectionErrorType: Decodable {
case NOT_AVAILABLE
}
enum BrokerErrorType: Decodable {
public enum BrokerErrorType: Decodable {
case RESPONSE(smpErr: ProtocolErrorType)
case UNEXPECTED
case NETWORK
@@ -482,7 +592,7 @@ enum BrokerErrorType: Decodable {
case TIMEOUT
}
enum ProtocolErrorType: Decodable {
public enum ProtocolErrorType: Decodable {
case BLOCK
case SESSION
case CMD(cmdErr: ProtocolCommandError)
@@ -493,7 +603,7 @@ enum ProtocolErrorType: Decodable {
case INTERNAL
}
enum ProtocolCommandError: Decodable {
public enum ProtocolCommandError: Decodable {
case UNKNOWN
case SYNTAX
case NO_AUTH
@@ -501,20 +611,20 @@ enum ProtocolCommandError: Decodable {
case NO_ENTITY
}
enum ProtocolTransportError: Decodable {
public enum ProtocolTransportError: Decodable {
case badBlock
case largeMsg
case badSession
case handshake(handshakeErr: SMPHandshakeError)
}
enum SMPHandshakeError: Decodable {
public enum SMPHandshakeError: Decodable {
case PARSE
case VERSION
case IDENTITY
}
enum SMPAgentError: Decodable {
public enum SMPAgentError: Decodable {
case A_MESSAGE
case A_PROHIBITED
case A_VERSION
+97
View File
@@ -0,0 +1,97 @@
//
// GroupDefaults.swift
// SimpleX (iOS)
//
// Created by Evgeny on 26/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
let GROUP_DEFAULT_APP_STATE = "appState"
let GROUP_DEFAULT_DB_CONTAINER = "dbContainer"
public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart"
let APP_GROUP_NAME = "group.chat.simplex.app"
public let groupDefaults = UserDefaults(suiteName: APP_GROUP_NAME)!
public enum AppState: String {
case active
case bgRefresh
case suspending
case suspended
public var inactive: Bool {
switch self {
case .suspending: return true
case .suspended: return true
default: return false
}
}
}
public enum DBContainer: String {
case documents
case group
}
public let appStateGroupDefault = EnumDefault<AppState>(
defaults: groupDefaults,
forKey: GROUP_DEFAULT_APP_STATE,
withDefault: .active
)
public let dbContainerGroupDefault = EnumDefault<DBContainer>(
defaults: groupDefaults,
forKey: GROUP_DEFAULT_DB_CONTAINER,
withDefault: .documents
)
public let chatLastStartGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_START)
public class DateDefault {
var defaults: UserDefaults
var key: String
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String) {
self.defaults = defaults
self.key = forKey
}
public func get() -> Date {
let ts = defaults.double(forKey: key)
return Date(timeIntervalSince1970: ts)
}
public func set(_ ts: Date) {
defaults.set(ts.timeIntervalSince1970, forKey: key)
defaults.synchronize()
}
}
public class EnumDefault<T: RawRepresentable> where T.RawValue == String {
var defaults: UserDefaults
var key: String
var defaultValue: T
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: T) {
self.defaults = defaults
self.key = forKey
self.defaultValue = withDefault
}
public func get() -> T {
if let rawValue = defaults.string(forKey: key),
let value = T(rawValue: rawValue) {
return value
}
return defaultValue
}
public func set(_ value: T) {
defaults.set(value.rawValue, forKey: key)
defaults.synchronize()
}
}
+99
View File
@@ -0,0 +1,99 @@
//
// CallTypes.swift
// SimpleX (iOS)
//
// Created by Evgeny on 05/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
public struct WebRTCCallOffer: Encodable {
public init(callType: CallType, rtcSession: WebRTCSession) {
self.callType = callType
self.rtcSession = rtcSession
}
public var callType: CallType
public var rtcSession: WebRTCSession
}
public struct WebRTCSession: Codable {
public init(rtcSession: String, rtcIceCandidates: String) {
self.rtcSession = rtcSession
self.rtcIceCandidates = rtcIceCandidates
}
public var rtcSession: String
public var rtcIceCandidates: String
}
public struct WebRTCExtraInfo: Codable {
public init(rtcIceCandidates: String) {
self.rtcIceCandidates = rtcIceCandidates
}
public var rtcIceCandidates: String
}
public struct CallInvitation {
public init(contact: Contact, callkitUUID: UUID? = nil, peerMedia: CallMediaType, sharedKey: String? = nil, callTs: Date) {
self.contact = contact
self.callkitUUID = callkitUUID
self.peerMedia = peerMedia
self.sharedKey = sharedKey
self.callTs = callTs
}
public var contact: Contact
public var callkitUUID: UUID?
public var peerMedia: CallMediaType
public var sharedKey: String?
public var callTs: Date
public var callTypeText: LocalizedStringKey {
get {
switch peerMedia {
case .video: return sharedKey == nil ? "video call (not e2e encrypted)" : "**e2e encrypted** video call"
case .audio: return sharedKey == nil ? "audio call (not e2e encrypted)" : "**e2e encrypted** audio call"
}
}
}
public static let sampleData = CallInvitation(
contact: Contact.sampleData,
peerMedia: .audio,
callTs: .now
)
}
public struct CallType: Codable {
public init(media: CallMediaType, capabilities: CallCapabilities) {
self.media = media
self.capabilities = capabilities
}
public var media: CallMediaType
public var capabilities: CallCapabilities
}
public enum CallMediaType: String, Codable, Equatable {
case video = "video"
case audio = "audio"
}
public enum VideoCamera: String, Codable, Equatable {
case user = "user"
case environment = "environment"
}
public struct CallCapabilities: Codable, Equatable {
public var encryption: Bool
}
public enum WebRTCCallStatus: String, Encodable {
case connected = "connected"
case connecting = "connecting"
case disconnected = "disconnected"
case failed = "failed"
}
@@ -9,18 +9,18 @@
import Foundation
import SwiftUI
struct User: Decodable, NamedChat {
public struct User: Decodable, NamedChat {
var userId: Int64
var userContactId: Int64
var localDisplayName: ContactName
var profile: Profile
public var profile: Profile
var activeUser: Bool
var displayName: String { get { profile.displayName } }
var fullName: String { get { profile.fullName } }
var image: String? { get { profile.image } }
public var displayName: String { get { profile.displayName } }
public var fullName: String { get { profile.fullName } }
public var image: String? { get { profile.image } }
static let sampleData = User(
public static let sampleData = User(
userId: 1,
userContactId: 1,
localDisplayName: "alice",
@@ -29,14 +29,20 @@ struct User: Decodable, NamedChat {
)
}
typealias ContactName = String
public typealias ContactName = String
typealias GroupName = String
public typealias GroupName = String
struct Profile: Codable, NamedChat {
var displayName: String
var fullName: String
var image: String?
public struct Profile: Codable, NamedChat {
public init(displayName: String, fullName: String, image: String? = nil) {
self.displayName = displayName
self.fullName = fullName
self.image = image
}
public var displayName: String
public var fullName: String
public var image: String?
static let sampleData = Profile(
displayName: "alice",
@@ -44,34 +50,34 @@ struct Profile: Codable, NamedChat {
)
}
enum ChatType: String {
public enum ChatType: String {
case direct = "@"
case group = "#"
case contactRequest = "<@"
case contactConnection = ":"
}
protocol NamedChat {
public protocol NamedChat {
var displayName: String { get }
var fullName: String { get }
var image: String? { get }
}
extension NamedChat {
var chatViewName: String {
public var chatViewName: String {
get { displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") }
}
}
typealias ChatId = String
public typealias ChatId = String
enum ChatInfo: Identifiable, Decodable, NamedChat {
public enum ChatInfo: Identifiable, Decodable, NamedChat {
case direct(contact: Contact)
case group(groupInfo: GroupInfo)
case contactRequest(contactRequest: UserContactRequest)
case contactConnection(contactConnection: PendingContactConnection)
var localDisplayName: String {
public var localDisplayName: String {
get {
switch self {
case let .direct(contact): return contact.localDisplayName
@@ -82,7 +88,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
var displayName: String {
public var displayName: String {
get {
switch self {
case let .direct(contact): return contact.displayName
@@ -93,7 +99,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
var fullName: String {
public var fullName: String {
get {
switch self {
case let .direct(contact): return contact.fullName
@@ -104,7 +110,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
var image: String? {
public var image: String? {
get {
switch self {
case let .direct(contact): return contact.image
@@ -115,7 +121,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
var id: ChatId {
public var id: ChatId {
get {
switch self {
case let .direct(contact): return contact.id
@@ -126,7 +132,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
var chatType: ChatType {
public var chatType: ChatType {
get {
switch self {
case .direct: return .direct
@@ -137,7 +143,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
var apiId: Int64 {
public var apiId: Int64 {
get {
switch self {
case let .direct(contact): return contact.apiId
@@ -148,7 +154,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
var ready: Bool {
public var ready: Bool {
get {
switch self {
case let .direct(contact): return contact.ready
@@ -168,7 +174,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
var updatedAt: Date {
public var updatedAt: Date {
switch self {
case let .direct(contact): return contact.updatedAt
case let .group(groupInfo): return groupInfo.updatedAt
@@ -177,49 +183,54 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
struct SampleData {
var direct: ChatInfo
var group: ChatInfo
var contactRequest: ChatInfo
public struct SampleData {
public var direct: ChatInfo
public var group: ChatInfo
public var contactRequest: ChatInfo
}
static var sampleData: ChatInfo.SampleData = SampleData(
public static var sampleData: ChatInfo.SampleData = SampleData(
direct: ChatInfo.direct(contact: Contact.sampleData),
group: ChatInfo.group(groupInfo: GroupInfo.sampleData),
contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData)
)
}
struct ChatData: Decodable, Identifiable {
var chatInfo: ChatInfo
var chatItems: [ChatItem]
var chatStats: ChatStats
public struct ChatData: Decodable, Identifiable {
public var chatInfo: ChatInfo
public var chatItems: [ChatItem]
public var chatStats: ChatStats
var id: ChatId { get { chatInfo.id } }
public var id: ChatId { get { chatInfo.id } }
}
struct ChatStats: Decodable {
var unreadCount: Int = 0
var minUnreadItemId: Int64 = 0
public struct ChatStats: Decodable {
public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0) {
self.unreadCount = unreadCount
self.minUnreadItemId = minUnreadItemId
}
public var unreadCount: Int = 0
public var minUnreadItemId: Int64 = 0
}
struct Contact: Identifiable, Decodable, NamedChat {
public struct Contact: Identifiable, Decodable, NamedChat {
var contactId: Int64
var localDisplayName: ContactName
var profile: Profile
var activeConn: Connection
public var profile: Profile
public var activeConn: Connection
var viaGroup: Int64?
var createdAt: Date
var updatedAt: Date
var id: ChatId { get { "@\(contactId)" } }
var apiId: Int64 { get { contactId } }
var ready: Bool { get { activeConn.connStatus == .ready } }
var displayName: String { get { profile.displayName } }
var fullName: String { get { profile.fullName } }
var image: String? { get { profile.image } }
public var id: ChatId { get { "@\(contactId)" } }
public var apiId: Int64 { get { contactId } }
public var ready: Bool { get { activeConn.connStatus == .ready } }
public var displayName: String { get { profile.displayName } }
public var fullName: String { get { profile.fullName } }
public var image: String? { get { profile.image } }
static let sampleData = Contact(
public static let sampleData = Contact(
contactId: 1,
localDisplayName: "alice",
profile: Profile.sampleData,
@@ -229,23 +240,23 @@ struct Contact: Identifiable, Decodable, NamedChat {
)
}
struct ContactRef: Decodable, Equatable {
public struct ContactRef: Decodable, Equatable {
var contactId: Int64
var localDisplayName: ContactName
var id: ChatId { get { "@\(contactId)" } }
public var id: ChatId { get { "@\(contactId)" } }
}
struct ContactSubStatus: Decodable {
var contact: Contact
var contactError: ChatError?
public struct ContactSubStatus: Decodable {
public var contact: Contact
public var contactError: ChatError?
}
struct Connection: Decodable {
public struct Connection: Decodable {
var connId: Int64
var connStatus: ConnStatus
var id: ChatId { get { ":\(connId)" } }
public var id: ChatId { get { ":\(connId)" } }
static let sampleData = Connection(
connId: 1,
@@ -253,21 +264,24 @@ struct Connection: Decodable {
)
}
struct UserContactRequest: Decodable, NamedChat {
public struct UserContact: Decodable {
}
public struct UserContactRequest: Decodable, NamedChat {
var contactRequestId: Int64
var localDisplayName: ContactName
var profile: Profile
var createdAt: Date
var updatedAt: Date
public var updatedAt: Date
var id: ChatId { get { "<@\(contactRequestId)" } }
var apiId: Int64 { get { contactRequestId } }
public var id: ChatId { get { "<@\(contactRequestId)" } }
public var apiId: Int64 { get { contactRequestId } }
var ready: Bool { get { true } }
var displayName: String { get { profile.displayName } }
var fullName: String { get { profile.fullName } }
var image: String? { get { profile.image } }
public var displayName: String { get { profile.displayName } }
public var fullName: String { get { profile.fullName } }
public var image: String? { get { profile.image } }
static let sampleData = UserContactRequest(
public static let sampleData = UserContactRequest(
contactRequestId: 1,
localDisplayName: "alice",
profile: Profile.sampleData,
@@ -276,21 +290,21 @@ struct UserContactRequest: Decodable, NamedChat {
)
}
struct PendingContactConnection: Decodable, NamedChat {
public struct PendingContactConnection: Decodable, NamedChat {
var pccConnId: Int64
var pccAgentConnId: String
var pccConnStatus: ConnStatus
var viaContactUri: Bool
public var viaContactUri: Bool
var createdAt: Date
var updatedAt: Date
public var updatedAt: Date
var id: ChatId { get { ":\(pccConnId)" } }
var apiId: Int64 { get { pccConnId } }
public var id: ChatId { get { ":\(pccConnId)" } }
public var apiId: Int64 { get { pccConnId } }
var ready: Bool { get { false } }
var localDisplayName: String {
get { String.localizedStringWithFormat(NSLocalizedString("connection:%@", comment: "connection information"), pccConnId) }
}
var displayName: String {
public var displayName: String {
get {
if let initiated = pccConnStatus.initiated {
return initiated && !viaContactUri
@@ -302,11 +316,11 @@ struct PendingContactConnection: Decodable, NamedChat {
}
}
}
var fullName: String { get { "" } }
var image: String? { get { nil } }
var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } }
public var fullName: String { get { "" } }
public var image: String? { get { nil } }
public var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } }
var description: String {
public var description: String {
get {
if let initiated = pccConnStatus.initiated {
return initiated && !viaContactUri
@@ -320,7 +334,7 @@ struct PendingContactConnection: Decodable, NamedChat {
}
}
static func getSampleData(_ status: ConnStatus = .new, viaContactUri: Bool = false) -> PendingContactConnection {
public static func getSampleData(_ status: ConnStatus = .new, viaContactUri: Bool = false) -> PendingContactConnection {
PendingContactConnection(
pccConnId: 1,
pccAgentConnId: "abcd",
@@ -332,7 +346,7 @@ struct PendingContactConnection: Decodable, NamedChat {
}
}
enum ConnStatus: String, Decodable {
public enum ConnStatus: String, Decodable {
case new = "new"
case joined = "joined"
case requested = "requested"
@@ -356,19 +370,19 @@ enum ConnStatus: String, Decodable {
}
}
struct GroupInfo: Identifiable, Decodable, NamedChat {
public struct GroupInfo: Identifiable, Decodable, NamedChat {
var groupId: Int64
var localDisplayName: GroupName
var groupProfile: GroupProfile
var createdAt: Date
var updatedAt: Date
var id: ChatId { get { "#\(groupId)" } }
public var id: ChatId { get { "#\(groupId)" } }
var apiId: Int64 { get { groupId } }
var ready: Bool { get { true } }
var displayName: String { get { groupProfile.displayName } }
var fullName: String { get { groupProfile.fullName } }
var image: String? { get { groupProfile.image } }
public var ready: Bool { get { true } }
public var displayName: String { get { groupProfile.displayName } }
public var fullName: String { get { groupProfile.fullName } }
public var image: String? { get { groupProfile.image } }
static let sampleData = GroupInfo(
groupId: 1,
@@ -379,10 +393,10 @@ struct GroupInfo: Identifiable, Decodable, NamedChat {
)
}
struct GroupProfile: Codable, NamedChat {
var displayName: String
var fullName: String
var image: String?
public struct GroupProfile: Codable, NamedChat {
public var displayName: String
public var fullName: String
public var image: String?
static let sampleData = GroupProfile(
displayName: "team",
@@ -390,15 +404,15 @@ struct GroupProfile: Codable, NamedChat {
)
}
struct GroupMember: Decodable {
var groupMemberId: Int64
public struct GroupMember: Decodable {
public var groupMemberId: Int64
var memberId: String
// var memberRole: GroupMemberRole
// var memberCategory: GroupMemberCategory
// var memberStatus: GroupMemberStatus
// var invitedBy: InvitedBy
var localDisplayName: ContactName
var memberProfile: Profile
public var memberProfile: Profile
var memberContactId: Int64?
// var activeConn: Connection?
@@ -412,7 +426,7 @@ struct GroupMember: Decodable {
}
}
static let sampleData = GroupMember(
public static let sampleData = GroupMember(
groupMemberId: 1,
memberId: "abcd",
localDisplayName: "alice",
@@ -421,29 +435,50 @@ struct GroupMember: Decodable {
)
}
struct MemberSubError: Decodable {
public struct MemberSubError: Decodable {
var member: GroupMember
var memberError: ChatError
}
struct AChatItem: Decodable {
var chatInfo: ChatInfo
var chatItem: ChatItem
public enum ConnectionEntity: Decodable {
case rcvDirectMsgConnection(entityConnection: Connection, contact: Contact?)
case rcvGroupMsgConnection(entityConnection: Connection, groupInfo: GroupInfo, groupMember: GroupMember)
case sndFileConnection(entityConnection: Connection, sndFileTransfer: SndFileTransfer)
case rcvFileConnection(entityConnection: Connection, rcvFileTransfer: RcvFileTransfer)
case userContactConnection(entityConnection: Connection, userContact: UserContact)
}
struct ChatItem: Identifiable, Decodable {
var chatDir: CIDirection
var meta: CIMeta
var content: CIContent
var formattedText: [FormattedText]?
var quotedItem: CIQuote?
var file: CIFile?
public struct NtfMsgInfo: Decodable {
var id: Int64 { get { meta.itemId } }
}
var timestampText: Text { get { meta.timestampText } }
public struct AChatItem: Decodable {
public var chatInfo: ChatInfo
public var chatItem: ChatItem
}
var text: String {
public struct ChatItem: Identifiable, Decodable {
public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, file: CIFile? = nil) {
self.chatDir = chatDir
self.meta = meta
self.content = content
self.formattedText = formattedText
self.quotedItem = quotedItem
self.file = file
}
public var chatDir: CIDirection
public var meta: CIMeta
public var content: CIContent
public var formattedText: [FormattedText]?
public var quotedItem: CIQuote?
public var file: CIFile?
public var id: Int64 { get { meta.itemId } }
public var timestampText: Text { get { meta.timestampText } }
public var text: String {
get {
switch (content.text, file) {
case let ("", .some(file)): return file.fileName
@@ -452,12 +487,12 @@ struct ChatItem: Identifiable, Decodable {
}
}
func isRcvNew() -> Bool {
public func isRcvNew() -> Bool {
if case .rcvNew = meta.itemStatus { return true }
return false
}
func isMsgContent() -> Bool {
public func isMsgContent() -> Bool {
switch content {
case .sndMsgContent: return true
case .rcvMsgContent: return true
@@ -465,7 +500,7 @@ struct ChatItem: Identifiable, Decodable {
}
}
func isDeletedContent() -> Bool {
public func isDeletedContent() -> Bool {
switch content {
case .sndDeleted: return true
case .rcvDeleted: return true
@@ -473,7 +508,7 @@ struct ChatItem: Identifiable, Decodable {
}
}
func isCall() -> Bool {
public func isCall() -> Bool {
switch content {
case .sndCall: return true
case .rcvCall: return true
@@ -481,7 +516,7 @@ struct ChatItem: Identifiable, Decodable {
}
}
var memberDisplayName: String? {
public var memberDisplayName: String? {
get {
if case let .groupRcv(groupMember) = chatDir {
return groupMember.memberProfile.displayName
@@ -491,7 +526,7 @@ struct ChatItem: Identifiable, Decodable {
}
}
static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> ChatItem {
public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> ChatItem {
ChatItem(
chatDir: dir,
meta: CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
@@ -501,7 +536,7 @@ struct ChatItem: Identifiable, Decodable {
)
}
static func getFileMsgContentSample (id: Int64 = 1, text: String = "", fileName: String = "test.txt", fileSize: Int64 = 100, fileStatus: CIFileStatus = .rcvComplete) -> ChatItem {
public static func getFileMsgContentSample (id: Int64 = 1, text: String = "", fileName: String = "test.txt", fileSize: Int64 = 100, fileStatus: CIFileStatus = .rcvComplete) -> ChatItem {
ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(id, .now, text, .rcvRead, false, false, false),
@@ -511,7 +546,7 @@ struct ChatItem: Identifiable, Decodable {
)
}
static func getDeletedContentSample (_ id: Int64 = 1, dir: CIDirection = .directRcv, _ ts: Date = .now, _ text: String = "this item is deleted", _ status: CIStatus = .rcvRead) -> ChatItem {
public static func getDeletedContentSample (_ id: Int64 = 1, dir: CIDirection = .directRcv, _ ts: Date = .now, _ text: String = "this item is deleted", _ status: CIStatus = .rcvRead) -> ChatItem {
ChatItem(
chatDir: dir,
meta: CIMeta.getSample(id, ts, text, status, false, false, false),
@@ -521,7 +556,7 @@ struct ChatItem: Identifiable, Decodable {
)
}
static func getIntegrityErrorSample (_ status: CIStatus = .rcvRead, fromMsgId: Int64 = 1, toMsgId: Int64 = 2) -> ChatItem {
public static func getIntegrityErrorSample (_ status: CIStatus = .rcvRead, fromMsgId: Int64 = 1, toMsgId: Int64 = 2) -> ChatItem {
ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "1 skipped message", status, false, false, false),
@@ -532,13 +567,13 @@ struct ChatItem: Identifiable, Decodable {
}
}
enum CIDirection: Decodable {
public enum CIDirection: Decodable {
case directSnd
case directRcv
case groupSnd
case groupRcv(groupMember: GroupMember)
var sent: Bool {
public var sent: Bool {
get {
switch self {
case .directSnd: return true
@@ -550,19 +585,19 @@ enum CIDirection: Decodable {
}
}
struct CIMeta: Decodable {
public struct CIMeta: Decodable {
var itemId: Int64
var itemTs: Date
var itemText: String
var itemStatus: CIStatus
public var itemStatus: CIStatus
var createdAt: Date
var itemDeleted: Bool
var itemEdited: Bool
var editable: Bool
public var itemDeleted: Bool
public var itemEdited: Bool
public var editable: Bool
var timestampText: Text { get { formatTimestampText(itemTs) } }
static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> CIMeta {
public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> CIMeta {
CIMeta(
itemId: id,
itemTs: ts,
@@ -579,14 +614,14 @@ struct CIMeta: Decodable {
let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute()
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
func formatTimestampText(_ date: Date) -> Text {
public func formatTimestampText(_ date: Date) -> Text {
let now = Calendar.current.dateComponents([.day, .hour], from: .now)
let dc = Calendar.current.dateComponents([.day, .hour], from: date)
let recent = now.day == dc.day || ((now.day ?? 0) - (dc.day ?? 0) == 1 && (dc.hour ?? 0) >= 18 && (now.hour ?? 0) < 12)
return Text(date, format: recent ? msgTimeFormat : msgDateFormat)
}
enum CIStatus: Decodable {
public enum CIStatus: Decodable {
case sndNew
case sndSent
case sndErrorAuth
@@ -595,7 +630,7 @@ enum CIStatus: Decodable {
case rcvRead
}
enum CIDeleteMode: String, Decodable {
public enum CIDeleteMode: String, Decodable {
case cidmBroadcast = "broadcast"
case cidmInternal = "internal"
}
@@ -604,7 +639,7 @@ protocol ItemContent {
var text: String { get }
}
enum CIContent: Decodable, ItemContent {
public enum CIContent: Decodable, ItemContent {
case sndMsgContent(msgContent: MsgContent)
case rcvMsgContent(msgContent: MsgContent)
case sndDeleted(deleteMode: CIDeleteMode)
@@ -613,7 +648,7 @@ enum CIContent: Decodable, ItemContent {
case rcvCall(status: CICallStatus, duration: Int)
case rcvIntegrityError(msgError: MsgErrorType)
var text: String {
public var text: String {
get {
switch self {
case let .sndMsgContent(mc): return mc.text
@@ -627,7 +662,7 @@ enum CIContent: Decodable, ItemContent {
}
}
var msgContent: MsgContent? {
public var msgContent: MsgContent? {
get {
switch self {
case let .sndMsgContent(mc): return mc
@@ -638,17 +673,17 @@ enum CIContent: Decodable, ItemContent {
}
}
struct CIQuote: Decodable, ItemContent {
public struct CIQuote: Decodable, ItemContent {
var chatDir: CIDirection?
var itemId: Int64?
var sharedMsgId: String? = nil
var sentAt: Date
var content: MsgContent
var formattedText: [FormattedText]?
public var content: MsgContent
public var formattedText: [FormattedText]?
var text: String { get { content.text } }
public var text: String { get { content.text } }
func getSender(_ currentUser: User?) -> String? {
public func getSender(_ currentUser: User?) -> String? {
switch (chatDir) {
case .directSnd: return "you"
case .directRcv: return nil
@@ -658,7 +693,7 @@ struct CIQuote: Decodable, ItemContent {
}
}
static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIDirection?, image: String? = nil) -> CIQuote {
public static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIDirection?, image: String? = nil) -> CIQuote {
let mc: MsgContent
if let image = image {
mc = .image(text: text, image: image)
@@ -669,14 +704,14 @@ struct CIQuote: Decodable, ItemContent {
}
}
struct CIFile: Decodable {
var fileId: Int64
var fileName: String
var fileSize: Int64
var filePath: String?
var fileStatus: CIFileStatus
public struct CIFile: Decodable {
public var fileId: Int64
public var fileName: String
public var fileSize: Int64
public var filePath: String?
public var fileStatus: CIFileStatus
static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile {
public static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile {
CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus)
}
@@ -697,7 +732,7 @@ struct CIFile: Decodable {
}
}
enum CIFileStatus: String, Decodable {
public enum CIFileStatus: String, Decodable {
case sndStored = "snd_stored"
case sndTransfer = "snd_transfer"
case sndComplete = "snd_complete"
@@ -709,7 +744,7 @@ enum CIFileStatus: String, Decodable {
case rcvCancelled = "rcv_cancelled"
}
enum MsgContent {
public enum MsgContent {
case text(String)
case link(text: String, preview: LinkPreview)
case image(text: String, image: String)
@@ -738,7 +773,7 @@ enum MsgContent {
}
}
func isFile() -> Bool {
public func isFile() -> Bool {
switch self {
case .file: return true
default: return false
@@ -754,7 +789,7 @@ enum MsgContent {
}
extension MsgContent: Decodable {
init(from decoder: Decoder) throws {
public init(from decoder: Decoder) throws {
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: CodingKeys.type)
@@ -784,7 +819,7 @@ extension MsgContent: Decodable {
}
extension MsgContent: Encodable {
func encode(to encoder: Encoder) throws {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .text(text):
@@ -809,12 +844,12 @@ extension MsgContent: Encodable {
}
}
struct FormattedText: Decodable {
var text: String
var format: Format?
public struct FormattedText: Decodable {
public var text: String
public var format: Format?
}
enum Format: Decodable, Equatable {
public enum Format: Decodable, Equatable {
case bold
case italic
case strikeThrough
@@ -826,7 +861,7 @@ enum Format: Decodable, Equatable {
case phone
}
enum FormatColor: String, Decodable {
public enum FormatColor: String, Decodable {
case red = "red"
case green = "green"
case blue = "blue"
@@ -836,7 +871,7 @@ enum FormatColor: String, Decodable {
case black = "black"
case white = "white"
var uiColor: Color {
public var uiColor: Color {
get {
switch (self) {
case .red: return .red
@@ -853,15 +888,22 @@ enum FormatColor: String, Decodable {
}
// Struct to use with simplex API
struct LinkPreview: Codable {
var uri: URL
var title: String
public struct LinkPreview: Codable {
public init(uri: URL, title: String, description: String = "", image: String) {
self.uri = uri
self.title = title
self.description = description
self.image = image
}
public var uri: URL
public var title: String
// TODO remove once optional in haskell
var description: String = ""
var image: String
public var description: String = ""
public var image: String
}
enum NtfTknStatus: String, Decodable {
public enum NtfTknStatus: String, Decodable {
case new = "NEW"
case registered = "REGISTERED"
case invalid = "INVALID"
@@ -870,15 +912,19 @@ enum NtfTknStatus: String, Decodable {
case expired = "EXPIRED"
}
struct SndFileTransfer: Decodable {
public struct SndFileTransfer: Decodable {
}
struct FileTransferMeta: Decodable {
public struct RcvFileTransfer: Decodable {
}
public struct FileTransferMeta: Decodable {
}
enum CICallStatus: String, Decodable {
public enum CICallStatus: String, Decodable {
case pending
case missed
case rejected
@@ -901,12 +947,12 @@ enum CICallStatus: String, Decodable {
}
}
static func durationText(_ sec: Int) -> String {
public static func durationText(_ sec: Int) -> String {
String(format: "%02d:%02d", sec / 60, sec % 60)
}
}
enum MsgErrorType: Decodable {
public enum MsgErrorType: Decodable {
case msgSkipped(fromMsgId: Int64, toMsgId: Int64)
case msgBadId(msgId: Int64)
case msgBadHash
@@ -8,25 +8,71 @@
import Foundation
import SwiftUI
import OSLog
let logger = Logger()
// maximum image file size to be auto-accepted
let maxImageSize: Int64 = 236700
public let maxImageSize: Int64 = 236700
let maxFileSize: Int64 = 8000000
public let maxFileSize: Int64 = 8000000
func getDocumentsDirectory() -> URL {
public func getDocumentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
}
func getAppFilesDirectory() -> URL {
getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true)
func getGroupContainerDirectory() -> URL {
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)!
}
func getAppDirectory() -> URL {
dbContainerGroupDefault.get() == .group
? getGroupContainerDirectory()
: getDocumentsDirectory()
// getDocumentsDirectory()
}
let DB_FILE_PREFIX = "simplex_v1"
func getLegacyDatabasePath() -> URL {
getDocumentsDirectory().appendingPathComponent("mobile_v1", isDirectory: false)
}
public func getAppDatabasePath() -> URL {
dbContainerGroupDefault.get() == .group
? getGroupContainerDirectory().appendingPathComponent(DB_FILE_PREFIX, isDirectory: false)
: getLegacyDatabasePath()
// getLegacyDatabasePath()
}
public func hasLegacyDatabase() -> Bool {
let dbPath = getLegacyDatabasePath()
let fm = FileManager.default
return fm.isReadableFile(atPath: dbPath.path + "_agent.db") &&
fm.isReadableFile(atPath: dbPath.path + "_chat.db")
}
public func removeLegacyDatabaseAndFiles() -> Bool {
let dbPath = getLegacyDatabasePath()
let appFiles = getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true)
let fm = FileManager.default
let r1 = nil != (try? fm.removeItem(atPath: dbPath.path + "_agent.db"))
let r2 = nil != (try? fm.removeItem(atPath: dbPath.path + "_chat.db"))
try? fm.removeItem(atPath: dbPath.path + "_agent.db.bak")
try? fm.removeItem(atPath: dbPath.path + "_chat.db.bak")
try? fm.removeItem(at: appFiles)
return r1 && r2
}
public func getAppFilesDirectory() -> URL {
getAppDirectory().appendingPathComponent("app_files", isDirectory: true)
}
func getAppFilePath(_ fileName: String) -> URL {
getAppFilesDirectory().appendingPathComponent(fileName)
}
func getLoadedFilePath(_ file: CIFile?) -> String? {
public func getLoadedFilePath(_ file: CIFile?) -> String? {
if let file = file,
file.loaded,
let savedFile = file.filePath {
@@ -35,14 +81,14 @@ func getLoadedFilePath(_ file: CIFile?) -> String? {
return nil
}
func getLoadedImage(_ file: CIFile?) -> UIImage? {
public func getLoadedImage(_ file: CIFile?) -> UIImage? {
if let filePath = getLoadedFilePath(file) {
return UIImage(contentsOfFile: filePath)
}
return nil
}
func saveFileFromURL(_ url: URL) -> String? {
public func saveFileFromURL(_ url: URL) -> String? {
let savedFile: String?
if url.startAccessingSecurityScopedResource() {
do {
@@ -61,7 +107,7 @@ func saveFileFromURL(_ url: URL) -> String? {
return savedFile
}
func saveImage(_ uiImage: UIImage) -> String? {
public func saveImage(_ uiImage: UIImage) -> String? {
if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: maxImageSize) {
let timestamp = Date().getFormattedDate("yyyyMMdd_HHmmss")
let fileName = uniqueCombine("IMG_\(timestamp).jpg")
@@ -92,8 +138,9 @@ private func saveFile(_ data: Data, _ fileName: String) -> String? {
private func uniqueCombine(_ fileName: String) -> String {
func tryCombine(_ fileName: String, _ n: Int) -> String {
let name = fileName.deletingPathExtension
let ext = fileName.pathExtension
let ns = fileName as NSString
let name = ns.deletingPathExtension
let ext = ns.pathExtension
let suffix = (n == 0) ? "" : "_\(n)"
let f = "\(name)\(suffix).\(ext)"
return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
@@ -101,19 +148,7 @@ private func uniqueCombine(_ fileName: String) -> String {
return tryCombine(fileName, 0)
}
private extension String {
var ns: NSString {
return self as NSString
}
var pathExtension: String {
return ns.pathExtension
}
var deletingPathExtension: String {
return ns.deletingPathExtension
}
}
func removeFile(_ fileName: String) {
public func removeFile(_ fileName: String) {
do {
try FileManager.default.removeItem(atPath: getAppFilePath(fileName).path)
} catch {
@@ -123,7 +158,7 @@ func removeFile(_ fileName: String) {
// image utils
func dropImagePrefix(_ s: String) -> String {
public func dropImagePrefix(_ s: String) -> String {
dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,")
}
@@ -131,7 +166,7 @@ private func dropPrefix(_ s: String, _ prefix: String) -> String {
s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s
}
func cropToSquare(_ image: UIImage) -> UIImage {
public func cropToSquare(_ image: UIImage) -> UIImage {
let size = image.size
let side = min(size.width, size.height)
let newSize = CGSize(width: side, height: side)
@@ -159,7 +194,7 @@ func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64) -> Data? {
return data
}
func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
var img = image
var str = compressImageStr(img)
var dataSize = str?.count ?? 0
@@ -8,7 +8,7 @@
import Foundation
func getJSONDecoder() -> JSONDecoder {
public func getJSONDecoder() -> JSONDecoder {
let jd = JSONDecoder()
let fracSeconds = getDateFormatter("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ")
let noFracSeconds = getDateFormatter("yyyy-MM-dd'T'HH:mm:ssZZZZZ")
@@ -23,7 +23,7 @@ func getJSONDecoder() -> JSONDecoder {
return jd
}
func getJSONEncoder() -> JSONEncoder {
public func getJSONEncoder() -> JSONEncoder {
let je = JSONEncoder()
je.dateEncodingStrategy = .iso8601
return je
@@ -10,17 +10,17 @@ import Foundation
import UserNotifications
import SwiftUI
let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST"
let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED"
let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED"
let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION"
let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
public let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST"
public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED"
public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED"
public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION"
public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
// TODO remove
let ntfCategoryCheckingMessages = "NTF_CAT_CHECKING_MESSAGES"
public let ntfCategoryCheckingMessages = "NTF_CAT_CHECKING_MESSAGES"
let appNotificationId = "chat.simplex.app.notification"
public let appNotificationId = "chat.simplex.app.notification"
func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
public func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
createNotification(
categoryIdentifier: ntfCategoryContactRequest,
title: String.localizedStringWithFormat(NSLocalizedString("%@ wants to connect!", comment: "notification title"), contactRequest.displayName),
@@ -30,7 +30,7 @@ func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableN
)
}
func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent {
public func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent {
createNotification(
categoryIdentifier: ntfCategoryContactConnected,
title: String.localizedStringWithFormat(NSLocalizedString("%@ is connected!", comment: "notification title"), contact.displayName),
@@ -40,7 +40,7 @@ func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationConte
)
}
func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
createNotification(
categoryIdentifier: ntfCategoryMessageReceived,
title: "\(cInfo.chatViewName):",
@@ -50,7 +50,7 @@ func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutable
)
}
func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificationContent {
public func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificationContent {
let text = invitation.peerMedia == .video
? NSLocalizedString("Incoming video call", comment: "notification")
: NSLocalizedString("Incoming audio call", comment: "notification")
@@ -63,7 +63,7 @@ func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificat
)
}
func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil,
public func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil,
targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent {
let content = UNMutableNotificationContent()
content.categoryIdentifier = categoryIdentifier
+22
View File
@@ -0,0 +1,22 @@
//
// SimpleX.h
// SimpleX
//
// Created by Evgeny on 30/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
#ifndef SimpleX_h
#define SimpleX_h
#endif /* SimpleX_h */
extern void hs_init(int argc, char **argv[]);
typedef void* chat_ctrl;
extern chat_ctrl chat_init(char *path);
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
extern char *chat_recv_msg(chat_ctrl ctl);
extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
extern char *chat_parse_markdown(char *str);
+13
View File
@@ -0,0 +1,13 @@
# ``SimpleXChat``
<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
## Overview
<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
## Topics
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->
+17
View File
@@ -0,0 +1,17 @@
//
// SimpleXChat.h
// SimpleXChat
//
// Created by Evgeny on 30/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for SimpleXChat.
FOUNDATION_EXPORT double SimpleXChatVersionNumber;
//! Project version string for SimpleXChat.
FOUNDATION_EXPORT const unsigned char SimpleXChatVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SimpleXChat/PublicHeader.h>
@@ -1,8 +1,8 @@
//
// dummy.m
// SimpleX NSE
// SimpleXChat
//
// Created by Evgeny on 26/04/2022.
// Created by Evgeny on 30/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
+3 -1
View File
@@ -1,9 +1,11 @@
packages: .
constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 5b61c6bccdce8563bca2041484054a28707d8e50
tag: 4c0164c49effe8d1b3fd98aca184245a8b4cac93
source-repository-package
type: git
+25
View File
@@ -0,0 +1,25 @@
# Portable archive file format
## Problems
- database migration for notifications support
- export and import of the database
The first problem could have been solved in an ad hoc way, but it may cause data loss, so the proposal is to have migration performed via export/import steps.
Out of scope of this doc - what will be the UX for database migration. It may be fully automatic, via code, with zero user interactions, or it could be via step by step wizard - irrespective of this choice it would include export and import steps.
# Proposal
Implement creating archive file and restoring from the archive in Haskell, application would only provide a source and target folders, respectively
Archive files structure:
- simplex_v1_chat.db
- simplex_v1_agent.db
- simplex_v1_files
- ...
Archive file name (includes UTC time):
simplex-chat.YYYY-MM-DDTHH:MM:SSZ.zip
+3 -1
View File
@@ -31,7 +31,7 @@ dependencies:
- optparse-applicative >= 0.15 && < 0.17
- process == 1.6.*
- simple-logger == 0.1.*
- simplexmq >= 1.1 && < 3.0
- simplexmq >= 3.0
- sqlite-simple == 0.4.*
- stm == 2.5.*
- terminal == 0.2.*
@@ -39,6 +39,7 @@ dependencies:
- time == 1.9.*
- unliftio == 0.2.*
- unliftio-core == 0.2.*
- zip == 1.7.*
library:
source-dirs: src
@@ -77,6 +78,7 @@ tests:
dependencies:
- simplex-chat
- async == 2.2.*
- deepseq == 1.4.*
- hspec == 2.7.*
- network == 3.1.*
- stm == 2.5.*
+15
View File
@@ -0,0 +1,15 @@
# Updating nix package config
1. Install `nix`, `gawk` and `jq`.
2. Start nix-shell from repo root:
```sh
nix-shell -p nix-prefetch-git
```
3. Run in nix shell:
```sh
gawk -f ./scripts/nix/update-sha256.awk cabal.project > ./scripts/nix/sha256map.nix
```
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."5b61c6bccdce8563bca2041484054a28707d8e50" = "11bsndjrj0s2vwgz2lg6plj1c1hjg2wqrgwqfsy1w0hx19mv4c0m";
"https://github.com/simplex-chat/simplexmq.git"."4c0164c49effe8d1b3fd98aca184245a8b4cac93" = "1ni3phxal10rfrkgnhcih3ksh7nfnj5b7vkk0qs9k52hmc8g306b";
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
"https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97";
+14 -5
View File
@@ -20,6 +20,7 @@ extra-source-files:
library
exposed-modules:
Simplex.Chat
Simplex.Chat.Archive
Simplex.Chat.Bot
Simplex.Chat.Call
Simplex.Chat.Controller
@@ -38,6 +39,8 @@ library
Simplex.Chat.Migrations.M20220321_chat_item_edited
Simplex.Chat.Migrations.M20220404_files_status_fields
Simplex.Chat.Migrations.M20220514_profiles_user_id
Simplex.Chat.Migrations.M20220626_auto_reply
Simplex.Chat.Migrations.M20220702_calls
Simplex.Chat.Mobile
Simplex.Chat.Options
Simplex.Chat.Protocol
@@ -75,7 +78,7 @@ library
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, simple-logger ==0.1.*
, simplexmq >=1.1 && <3.0
, simplexmq >=3.0
, sqlite-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
@@ -83,6 +86,7 @@ library
, time ==1.9.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, zip ==1.7.*
default-language: Haskell2010
executable simplex-bot
@@ -113,7 +117,7 @@ executable simplex-bot
, process ==1.6.*
, simple-logger ==0.1.*
, simplex-chat
, simplexmq >=1.1 && <3.0
, simplexmq >=3.0
, sqlite-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
@@ -121,6 +125,7 @@ executable simplex-bot
, time ==1.9.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, zip ==1.7.*
default-language: Haskell2010
executable simplex-bot-advanced
@@ -151,7 +156,7 @@ executable simplex-bot-advanced
, process ==1.6.*
, simple-logger ==0.1.*
, simplex-chat
, simplexmq >=1.1 && <3.0
, simplexmq >=3.0
, sqlite-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
@@ -159,6 +164,7 @@ executable simplex-bot-advanced
, time ==1.9.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, zip ==1.7.*
default-language: Haskell2010
executable simplex-chat
@@ -191,7 +197,7 @@ executable simplex-chat
, process ==1.6.*
, simple-logger ==0.1.*
, simplex-chat
, simplexmq >=1.1 && <3.0
, simplexmq >=3.0
, sqlite-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
@@ -200,6 +206,7 @@ executable simplex-chat
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, websockets ==0.12.*
, zip ==1.7.*
default-language: Haskell2010
test-suite simplex-chat-test
@@ -227,6 +234,7 @@ test-suite simplex-chat-test
, composition ==1.0.*
, containers ==0.6.*
, cryptonite >=0.27 && <0.30
, deepseq ==1.4.*
, directory ==1.3.*
, email-validate ==2.3.*
, exceptions ==0.10.*
@@ -239,7 +247,7 @@ test-suite simplex-chat-test
, process ==1.6.*
, simple-logger ==0.1.*
, simplex-chat
, simplexmq >=1.1 && <3.0
, simplexmq >=3.0
, sqlite-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
@@ -247,4 +255,5 @@ test-suite simplex-chat-test
, time ==1.9.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, zip ==1.7.*
default-language: Haskell2010
+397 -327
View File
File diff suppressed because it is too large Load Diff
+81
View File
@@ -0,0 +1,81 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
module Simplex.Chat.Archive where
import qualified Codec.Archive.Zip as Z
import Control.Monad.Reader
import Simplex.Chat.Controller
import Simplex.Messaging.Agent.Client (agentDbPath)
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..))
import Simplex.Messaging.Util (whenM)
import System.FilePath
import UnliftIO.Directory
import UnliftIO.STM
import UnliftIO.Temporary
archiveAgentDbFile :: String
archiveAgentDbFile = "simplex_v1_agent.db"
archiveChatDbFile :: String
archiveChatDbFile = "simplex_v1_chat.db"
archiveFilesFolder :: String
archiveFilesFolder = "simplex_v1_files"
exportArchive :: ChatMonad m => ArchiveConfig -> m ()
exportArchive ArchiveConfig {archivePath, disableCompression} =
withSystemTempDirectory "simplex-chat." $ \dir -> do
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
copyFile chatDb $ dir </> archiveChatDbFile
copyFile agentDb $ dir </> archiveAgentDbFile
forM_ filesPath $ \fp ->
copyDirectoryFiles fp $ dir </> archiveFilesFolder
let method = if disableCompression == Just True then Z.Store else Z.Deflate
Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir
importArchive :: ChatMonad m => ArchiveConfig -> m ()
importArchive ArchiveConfig {archivePath} =
withSystemTempDirectory "simplex-chat." $ \dir -> do
Z.withArchive archivePath $ Z.unpackInto dir
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
backup chatDb
backup agentDb
copyFile (dir </> archiveChatDbFile) chatDb
copyFile (dir </> archiveAgentDbFile) agentDb
let filesDir = dir </> archiveFilesFolder
forM_ filesPath $ \fp ->
whenM (doesDirectoryExist filesDir) $
copyDirectoryFiles filesDir fp
where
backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak"
copyDirectoryFiles :: MonadIO m => FilePath -> FilePath -> m ()
copyDirectoryFiles fromDir toDir = do
createDirectoryIfMissing False toDir
fs <- listDirectory fromDir
forM_ fs $ \f -> do
let fn = takeFileName f
f' = fromDir </> fn
whenM (doesFileExist f') $ copyFile f' $ toDir </> fn
deleteStorage :: ChatMonad m => m ()
deleteStorage = do
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
removeFile chatDb
removeFile agentDb
mapM_ removePathForcibly filesPath
data StorageFiles = StorageFiles
{ chatDb :: FilePath,
agentDb :: FilePath,
filesPath :: Maybe FilePath
}
storageFiles :: ChatMonad m => m StorageFiles
storageFiles = do
ChatController {chatStore, filesFolder, smpAgent} <- ask
let SQLiteStore {dbFilePath = chatDb} = chatStore
agentDb = agentDbPath smpAgent
filesPath <- readTVarIO filesFolder
pure StorageFiles {chatDb, agentDb, filesPath}
+1 -1
View File
@@ -38,7 +38,7 @@ chatBotRepl welcome answer _user cc = do
initializeBotAddress :: ChatController -> IO ()
initializeBotAddress cc = do
sendChatCmd cc "/show_address" >>= \case
CRUserContactLink uri _ -> showBotAddress uri
CRUserContactLink uri _ _ -> showBotAddress uri
CRChatCmdError (ChatErrorStore SEUserContactLinkNotFound) -> do
putStrLn $ "No bot address, creating..."
sendChatCmd cc "/address" >>= \case
+47 -3
View File
@@ -12,20 +12,33 @@ import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Int (Int64)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.Time.Clock (UTCTime)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic)
import Simplex.Chat.Types (Contact, ContactId)
import Simplex.Chat.Util (safeDecodeUtf8)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, enumJSON)
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON)
data Call = Call
{ contactId :: Int64,
{ contactId :: ContactId,
callId :: CallId,
chatItemId :: Int64,
callState :: CallState
callState :: CallState,
callTs :: UTCTime
}
isRcvInvitation :: Call -> Bool
isRcvInvitation Call {callState} = case callState of
CallInvitationReceived {} -> True
_ -> False
data CallStateTag
= CSTCallInvitationSent
| CSTCallInvitationReceived
@@ -75,6 +88,21 @@ data CallState
peerCallSession :: WebRTCSession,
sharedKey :: Maybe C.Key
}
deriving (Generic)
-- database representation
instance FromJSON CallState where
parseJSON = J.genericParseJSON $ singleFieldJSON fstToLower
instance ToJSON CallState where
toJSON = J.genericToJSON $ singleFieldJSON fstToLower
toEncoding = J.genericToEncoding $ singleFieldJSON fstToLower
instance ToField CallState where
toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode
instance FromField CallState where
fromField = fromTextField_ $ J.decode . LB.fromStrict . encodeUtf8
newtype CallId = CallId ByteString
deriving (Eq, Show)
@@ -91,6 +119,22 @@ instance ToJSON CallId where
toJSON = strToJSON
toEncoding = strToJEncoding
instance FromField CallId where fromField f = CallId <$> fromField f
instance ToField CallId where toField (CallId m) = toField m
data RcvCallInvitation = RcvCallInvitation
{ contact :: Contact,
callType :: CallType,
sharedKey :: Maybe C.Key,
callTs :: UTCTime
}
deriving (Show, Generic)
instance ToJSON RcvCallInvitation where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
data CallType = CallType
{ media :: CallMedia,
capabilities :: CallCapabilities
+38 -12
View File
@@ -4,6 +4,7 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Chat.Controller where
@@ -23,7 +24,6 @@ import Data.Text (Text)
import Data.Time (ZonedTime)
import Data.Time.Clock (UTCTime)
import Data.Version (showVersion)
import Data.Word (Word16)
import GHC.Generics (Generic)
import Numeric.Natural
import qualified Paths_simplex_chat as SC
@@ -40,7 +40,7 @@ import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, sumTypeJSON)
import Simplex.Messaging.Protocol (CorrId)
import Simplex.Messaging.Protocol (CorrId, MsgFlags)
import Simplex.Messaging.TMap (TMap)
import System.IO (Handle)
import UnliftIO.STM
@@ -56,7 +56,6 @@ updateStr = "To update run: curl -o- https://raw.githubusercontent.com/simplex-c
data ChatConfig = ChatConfig
{ agentConfig :: AgentConfig,
dbPoolSize :: Int,
yesToMigrations :: Bool,
defaultServers :: InitialAgentServers,
tbqSize :: Natural,
@@ -74,8 +73,9 @@ data ChatController = ChatController
activeTo :: TVar ActiveTo,
firstTime :: Bool,
smpAgent :: AgentClient,
agentAsync :: TVar (Maybe (Async ())),
agentAsync :: TVar (Maybe (Async (), Maybe (Async ()))),
chatStore :: SQLiteStore,
chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted
idsDrg :: TVar ChaChaDRG,
inputQ :: TBQueue String,
outputQ :: TBQueue (Maybe CorrId, ChatResponse),
@@ -99,9 +99,15 @@ instance ToJSON HelpSection where
data ChatCommand
= ShowActiveUser
| CreateActiveUser Profile
| StartChat
| StartChat {subscribeConnections :: Bool}
| APIStopChat
| APIActivateChat
| APISuspendChat {suspendTimeout :: Int}
| ResubscribeAllConnections
| SetFilesFolder FilePath
| APIExportArchive ArchiveConfig
| APIImportArchive ArchiveConfig
| APIDeleteStorage
| APIGetChats {pendingConnections :: Bool}
| APIGetChat ChatRef ChatPagination
| APIGetChatItems Int
@@ -120,13 +126,15 @@ data ChatCommand
| APISendCallAnswer ContactId WebRTCSession
| APISendCallExtraInfo ContactId WebRTCExtraInfo
| APIEndCall ContactId
| APIGetCallInvitations
| APICallStatus ContactId WebRTCCallStatus
| APIUpdateProfile Profile
| APIParseMarkdown Text
| APIRegisterToken DeviceToken
| APIVerifyToken DeviceToken ByteString C.CbNonce
| APIIntervalNofication DeviceToken Word16
| APIGetNtfToken
| APIRegisterToken DeviceToken NotificationsMode
| APIVerifyToken DeviceToken C.CbNonce ByteString
| APIDeleteToken DeviceToken
| APIGetNtfMessage {nonce :: C.CbNonce, encNtfInfo :: ByteString}
| GetUserSMPServers
| SetUserSMPServers [SMPServer]
| ChatHelp HelpSection
@@ -140,7 +148,7 @@ data ChatCommand
| CreateMyAddress
| DeleteMyAddress
| ShowMyAddress
| AddressAutoAccept Bool
| AddressAutoAccept Bool (Maybe MsgContent)
| AcceptContact ContactName
| RejectContact ContactName
| SendMessage ChatName ByteString
@@ -178,6 +186,8 @@ data ChatResponse
= CRActiveUser {user :: User}
| CRChatStarted
| CRChatRunning
| CRChatStopped
| CRChatSuspended
| CRApiChats {chats :: [AChat]}
| CRApiChat {chat :: AChat}
| CRLastMessages {chatItems :: [AChatItem]}
@@ -197,8 +207,8 @@ data ChatResponse
| CRGroupCreated {groupInfo :: GroupInfo}
| CRGroupMembers {group :: Group}
| CRContactsList {contacts :: [Contact]}
| CRUserContactLink {connReqContact :: ConnReqContact, autoAccept :: Bool}
| CRUserContactLinkUpdated {connReqContact :: ConnReqContact, autoAccept :: Bool}
| CRUserContactLink {connReqContact :: ConnReqContact, autoAccept :: Bool, autoReply :: Maybe MsgContent}
| CRUserContactLinkUpdated {connReqContact :: ConnReqContact, autoAccept :: Bool, autoReply :: Maybe MsgContent}
| CRContactRequestRejected {contactRequest :: UserContactRequest}
| CRUserAcceptedGroupSent {groupInfo :: GroupInfo}
| CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember}
@@ -260,14 +270,17 @@ data ChatResponse
| CRPendingSubSummary {pendingSubStatus :: [PendingSubStatus]}
| CRSndFileSubError {sndFileTransfer :: SndFileTransfer, chatError :: ChatError}
| CRRcvFileSubError {rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError}
| CRCallInvitation {contact :: Contact, callType :: CallType, sharedKey :: Maybe C.Key, callTs :: UTCTime}
| CRCallInvitation {callInvitation :: RcvCallInvitation}
| CRCallOffer {contact :: Contact, callType :: CallType, offer :: WebRTCSession, sharedKey :: Maybe C.Key, askConfirmation :: Bool}
| CRCallAnswer {contact :: Contact, answer :: WebRTCSession}
| CRCallExtraInfo {contact :: Contact, extraInfo :: WebRTCExtraInfo}
| CRCallEnded {contact :: Contact}
| CRCallInvitations {callInvitations :: [RcvCallInvitation]}
| CRUserContactLinkSubscribed
| CRUserContactLinkSubError {chatError :: ChatError}
| CRNtfTokenStatus {status :: NtfTknStatus}
| CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode}
| CRNtfMessages {connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]}
| CRNewContactConnection {connection :: PendingContactConnection}
| CRContactConnectionDeleted {connection :: PendingContactConnection}
| CRMessageError {severity :: Text, errorMessage :: Text}
@@ -279,6 +292,9 @@ instance ToJSON ChatResponse where
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR"
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR"
data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool}
deriving (Show, Generic, FromJSON)
data ContactSubStatus = ContactSubStatus
{ contact :: Contact,
contactError :: Maybe ChatError
@@ -315,6 +331,14 @@ data ComposedMessage = ComposedMessage
}
deriving (Show, Generic, FromJSON)
data NtfMsgInfo = NtfMsgInfo {msgTs :: UTCTime, msgFlags :: MsgFlags}
deriving (Show, Generic)
instance ToJSON NtfMsgInfo where toEncoding = J.genericToEncoding J.defaultOptions
crNtfToken :: (DeviceToken, NtfTknStatus, NotificationsMode) -> ChatResponse
crNtfToken (token, status, ntfMode) = CRNtfToken {token, status, ntfMode}
data ChatError
= ChatError {errorType :: ChatErrorType}
| ChatErrorAgent {agentError :: AgentErrorType}
@@ -329,6 +353,8 @@ data ChatErrorType
= CENoActiveUser
| CEActiveUserExists
| CEChatNotStarted
| CEChatNotStopped
| CEChatStoreChanged
| CEInvalidConnReq
| CEInvalidChatMessage {message :: String}
| CEContactNotReady {contact :: Contact}
+10 -8
View File
@@ -15,7 +15,7 @@ import Simplex.Chat.Types
import UnliftIO.Async
simplexChatCore :: ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> (User -> ChatController -> IO ()) -> IO ()
simplexChatCore cfg@ChatConfig {dbPoolSize, yesToMigrations} opts sendToast chat
simplexChatCore cfg@ChatConfig {yesToMigrations} opts sendToast chat
| logAgent opts = do
setLogLevel LogInfo -- LogError
withGlobalLogging logCfg initRun
@@ -23,16 +23,18 @@ simplexChatCore cfg@ChatConfig {dbPoolSize, yesToMigrations} opts sendToast chat
where
initRun = do
let f = chatStoreFile $ dbFilePrefix opts
st <- createStore f dbPoolSize yesToMigrations
st <- createStore f yesToMigrations
u <- getCreateActiveUser st
cc <- newChatController st (Just u) cfg opts sendToast
runSimplexChat u cc chat
runSimplexChat opts u cc chat
runSimplexChat :: User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
runSimplexChat u cc chat = do
a1 <- async $ chat u cc
a2 <- runReaderT (startChatController u) cc
waitEither_ a1 a2
runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
runSimplexChat ChatOpts {maintenance} u cc chat
| maintenance = wait =<< async (chat u cc)
| otherwise = do
a1 <- async $ chat u cc
a2 <- runReaderT (startChatController u True) cc
waitEither_ a1 a2
sendChatCmd :: ChatController -> String -> IO ChatResponse
sendChatCmd cc s = runReaderT (execChatCommand . encodeUtf8 $ T.pack s) cc
+8 -1
View File
@@ -89,6 +89,13 @@ instance IsString FormattedText where
type MarkdownList = [FormattedText]
data ParsedMarkdown = ParsedMarkdown {formattedText :: Maybe MarkdownList}
deriving (Generic)
instance ToJSON ParsedMarkdown where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
unmarked :: Text -> Markdown
unmarked = Markdown Nothing
@@ -183,6 +190,6 @@ markdownP = mconcat <$> A.many' fragmentP
| isUri s = markdown Uri s
| isEmail s = markdown Email s
| otherwise = unmarked s
isUri s = "http://" `T.isPrefixOf` s || "https://" `T.isPrefixOf` s || "simplex:/" `T.isPrefixOf` s
isUri s = T.length s >= 10 && any (`T.isPrefixOf` s) ["http://", "https://", "simplex:/"]
isEmail s = T.any (== '@') s && Email.isValid (encodeUtf8 s)
noFormat = pure . unmarked
+2 -2
View File
@@ -32,12 +32,12 @@ import GHC.Generics (Generic)
import Simplex.Chat.Markdown
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8)
import Simplex.Chat.Util (safeDecodeUtf8)
import Simplex.Messaging.Agent.Protocol (AgentErrorType, AgentMsgId, MsgErrorType (..), MsgMeta (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, singleFieldJSON, sumTypeJSON)
import Simplex.Messaging.Protocol (MsgBody)
import Simplex.Messaging.Util ((<$?>))
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection
deriving (Show, Generic)
@@ -0,0 +1,15 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20220626_auto_reply where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20220626_auto_reply :: Query
m20220626_auto_reply =
[sql|
ALTER TABLE user_contact_links ADD COLUMN auto_reply_msg_content TEXT DEFAULT NULL;
ALTER TABLE connections ADD COLUMN via_user_contact_link INTEGER DEFAULT NULL
REFERENCES user_contact_links (user_contact_link_id) ON DELETE SET NULL;
|]
@@ -0,0 +1,22 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20220702_calls where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20220702_calls :: Query
m20220702_calls =
[sql|
CREATE TABLE calls ( -- stores call invitations state for communicating state between NSE and app when call notification comes
call_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE,
shared_call_id BLOB NOT NULL,
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
call_state BLOB NOT NULL,
call_ts TEXT NOT NULL,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
|]
+244 -144
View File
@@ -1,130 +1,167 @@
CREATE TABLE migrations (
name TEXT NOT NULL,
ts TEXT NOT NULL,
PRIMARY KEY (name)
);
CREATE TABLE contact_profiles ( -- remote user profile
CREATE TABLE migrations(
name TEXT NOT NULL,
ts TEXT NOT NULL,
PRIMARY KEY(name)
);
CREATE TABLE contact_profiles(
-- remote user profile
contact_profile_id INTEGER PRIMARY KEY,
display_name TEXT NOT NULL, -- contact name set by remote user (not unique), this name must not contain spaces
display_name TEXT NOT NULL, -- contact name set by remote user(not unique), this name must not contain spaces
full_name TEXT NOT NULL,
properties TEXT NOT NULL DEFAULT '{}' -- JSON with contact profile properties
, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), image TEXT, user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE);
CREATE INDEX contact_profiles_index ON contact_profiles (display_name, full_name);
CREATE TABLE users (
,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
image TEXT,
user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE
);
CREATE INDEX contact_profiles_index ON contact_profiles(
display_name,
full_name
);
CREATE TABLE users(
user_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED,
DEFERRABLE INITIALLY DEFERRED,
local_display_name TEXT NOT NULL UNIQUE,
active_user INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- 1 for active user
FOREIGN KEY (user_id, local_display_name)
REFERENCES display_names (user_id, local_display_name)
ON DELETE CASCADE
ON UPDATE CASCADE
DEFERRABLE INITIALLY DEFERRED
active_user INTEGER NOT NULL DEFAULT 0,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL), -- 1 for active user
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
ON UPDATE CASCADE
DEFERRABLE INITIALLY DEFERRED
);
CREATE TABLE display_names (
CREATE TABLE display_names(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
local_display_name TEXT NOT NULL,
ldn_base TEXT NOT NULL,
ldn_suffix INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL),
PRIMARY KEY (user_id, local_display_name) ON CONFLICT FAIL,
UNIQUE (user_id, ldn_base, ldn_suffix) ON CONFLICT FAIL
ldn_suffix INTEGER NOT NULL DEFAULT 0,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
PRIMARY KEY(user_id, local_display_name) ON CONFLICT FAIL,
UNIQUE(user_id, ldn_base, ldn_suffix) ON CONFLICT FAIL
) WITHOUT ROWID;
CREATE TABLE contacts (
CREATE TABLE contacts(
contact_id INTEGER PRIMARY KEY,
contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, -- NULL if it's an incognito profile
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
local_display_name TEXT NOT NULL,
is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user
via_group INTEGER REFERENCES groups (group_id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT CHECK (updated_at NOT NULL), xcontact_id BLOB,
FOREIGN KEY (user_id, local_display_name)
REFERENCES display_names (user_id, local_display_name)
ON DELETE CASCADE
ON UPDATE CASCADE,
UNIQUE (user_id, local_display_name),
UNIQUE (user_id, contact_profile_id)
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
local_display_name TEXT NOT NULL,
is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user
via_group INTEGER REFERENCES groups(group_id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT CHECK(updated_at NOT NULL),
xcontact_id BLOB,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
ON UPDATE CASCADE,
UNIQUE(user_id, local_display_name),
UNIQUE(user_id, contact_profile_id)
);
CREATE TABLE sent_probes (
CREATE TABLE sent_probes(
sent_probe_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE,
probe BLOB NOT NULL,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL),
UNIQUE (user_id, probe)
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
UNIQUE(user_id, probe)
);
CREATE TABLE sent_probe_hashes (
CREATE TABLE sent_probe_hashes(
sent_probe_hash_id INTEGER PRIMARY KEY,
sent_probe_id INTEGER NOT NULL REFERENCES sent_probes ON DELETE CASCADE,
contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL),
UNIQUE (sent_probe_id, contact_id)
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
UNIQUE(sent_probe_id, contact_id)
);
CREATE TABLE received_probes (
CREATE TABLE received_probes(
received_probe_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE,
probe BLOB,
probe_hash BLOB NOT NULL,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE
, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL));
,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL)
);
CREATE TABLE known_servers(
server_id INTEGER PRIMARY KEY,
host TEXT NOT NULL,
port TEXT NOT NULL,
key_hash BLOB,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL),
UNIQUE (user_id, host, port)
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
UNIQUE(user_id, host, port)
) WITHOUT ROWID;
CREATE TABLE group_profiles ( -- shared group profiles
CREATE TABLE group_profiles(
-- shared group profiles
group_profile_id INTEGER PRIMARY KEY,
display_name TEXT NOT NULL, -- this name must not contain spaces
full_name TEXT NOT NULL,
properties TEXT NOT NULL DEFAULT '{}' -- JSON with user or contact profile
, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), image TEXT, user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE);
CREATE TABLE groups (
,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
image TEXT,
user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE
);
CREATE TABLE groups(
group_id INTEGER PRIMARY KEY, -- local group ID
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
local_display_name TEXT NOT NULL, -- local group name without spaces
group_profile_id INTEGER REFERENCES group_profiles ON DELETE SET NULL, -- shared group profile
inv_queue_info BLOB, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- received
FOREIGN KEY (user_id, local_display_name)
REFERENCES display_names (user_id, local_display_name)
ON DELETE CASCADE
ON UPDATE CASCADE,
UNIQUE (user_id, local_display_name),
UNIQUE (user_id, group_profile_id)
inv_queue_info BLOB,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL), -- received
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
ON UPDATE CASCADE,
UNIQUE(user_id, local_display_name),
UNIQUE(user_id, group_profile_id)
);
CREATE INDEX idx_groups_inv_queue_info ON groups (inv_queue_info);
CREATE TABLE group_members ( -- group members, excluding the local user
CREATE INDEX idx_groups_inv_queue_info ON groups(inv_queue_info);
CREATE TABLE group_members(
-- group members, excluding the local user
group_member_id INTEGER PRIMARY KEY,
group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE,
member_id BLOB NOT NULL, -- shared member ID, unique per group
member_role TEXT NOT NULL, -- owner, admin, member
member_category TEXT NOT NULL, -- see GroupMemberCategory
member_status TEXT NOT NULL, -- see GroupMemberStatus
invited_by INTEGER REFERENCES contacts (contact_id) ON DELETE SET NULL, -- NULL for the members who joined before the current user and for the group creator
invited_by INTEGER REFERENCES contacts(contact_id) ON DELETE SET NULL, -- NULL for the members who joined before the current user and for the group creator
sent_inv_queue_info BLOB, -- sent
group_queue_info BLOB, -- received
direct_queue_info BLOB, -- received
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
local_display_name TEXT NOT NULL, -- should be the same as contact
contact_profile_id INTEGER NOT NULL REFERENCES contact_profiles ON DELETE CASCADE,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL),
FOREIGN KEY (user_id, local_display_name)
REFERENCES display_names (user_id, local_display_name)
ON DELETE CASCADE
ON UPDATE CASCADE,
UNIQUE (group_id, member_id)
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
ON UPDATE CASCADE,
UNIQUE(group_id, member_id)
);
CREATE TABLE group_member_intros (
CREATE TABLE group_member_intros(
group_member_intro_id INTEGER PRIMARY KEY,
re_group_member_id INTEGER NOT NULL REFERENCES group_members (group_member_id) ON DELETE CASCADE,
to_group_member_id INTEGER NOT NULL REFERENCES group_members (group_member_id) ON DELETE CASCADE,
re_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE,
to_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE,
group_queue_info BLOB,
direct_queue_info BLOB,
intro_status TEXT NOT NULL, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- see GroupMemberIntroStatus
UNIQUE (re_group_member_id, to_group_member_id)
intro_status TEXT NOT NULL,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL), -- see GroupMemberIntroStatus
UNIQUE(re_group_member_id, to_group_member_id)
);
CREATE TABLE files (
CREATE TABLE files(
file_id INTEGER PRIMARY KEY,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
@@ -132,152 +169,215 @@ CREATE TABLE files (
file_path TEXT,
file_size INTEGER NOT NULL,
chunk_size INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
created_at TEXT NOT NULL DEFAULT(datetime('now')),
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE
, chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), cancelled INTEGER, ci_file_status TEXT);
CREATE TABLE snd_files (
,
chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE CASCADE,
updated_at TEXT CHECK(updated_at NOT NULL),
cancelled INTEGER,
ci_file_status TEXT
);
CREATE TABLE snd_files(
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE,
file_status TEXT NOT NULL, -- new, accepted, connected, completed
group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL),
PRIMARY KEY (file_id, connection_id)
group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
PRIMARY KEY(file_id, connection_id)
) WITHOUT ROWID;
CREATE TABLE rcv_files (
CREATE TABLE rcv_files(
file_id INTEGER PRIMARY KEY REFERENCES files ON DELETE CASCADE,
file_status TEXT NOT NULL, -- new, accepted, connected, completed
group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE,
file_queue_info BLOB
, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL));
CREATE TABLE snd_file_chunks (
,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL)
);
CREATE TABLE snd_file_chunks(
file_id INTEGER NOT NULL,
connection_id INTEGER NOT NULL,
chunk_number INTEGER NOT NULL,
chunk_agent_msg_id INTEGER,
chunk_sent INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- 0 (sent to agent), 1 (sent to server)
FOREIGN KEY (file_id, connection_id) REFERENCES snd_files ON DELETE CASCADE,
PRIMARY KEY (file_id, connection_id, chunk_number)
chunk_sent INTEGER NOT NULL DEFAULT 0,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL), -- 0(sent to agent), 1(sent to server)
FOREIGN KEY(file_id, connection_id) REFERENCES snd_files ON DELETE CASCADE,
PRIMARY KEY(file_id, connection_id, chunk_number)
) WITHOUT ROWID;
CREATE TABLE rcv_file_chunks (
CREATE TABLE rcv_file_chunks(
file_id INTEGER NOT NULL REFERENCES rcv_files ON DELETE CASCADE,
chunk_number INTEGER NOT NULL,
chunk_agent_msg_id INTEGER NOT NULL,
chunk_stored INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- 0 (received), 1 (appended to file)
PRIMARY KEY (file_id, chunk_number)
chunk_stored INTEGER NOT NULL DEFAULT 0,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL), -- 0(received), 1(appended to file)
PRIMARY KEY(file_id, chunk_number)
) WITHOUT ROWID;
CREATE TABLE connections ( -- all SMP agent connections
CREATE TABLE connections(
-- all SMP agent connections
connection_id INTEGER PRIMARY KEY,
agent_conn_id BLOB NOT NULL UNIQUE,
conn_level INTEGER NOT NULL DEFAULT 0,
via_contact INTEGER REFERENCES contacts (contact_id) ON DELETE SET NULL,
via_contact INTEGER REFERENCES contacts(contact_id) ON DELETE SET NULL,
conn_status TEXT NOT NULL,
conn_type TEXT NOT NULL, -- contact, member, rcv_file, snd_file
user_contact_link_id INTEGER REFERENCES user_contact_links ON DELETE CASCADE,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE,
snd_file_id INTEGER,
rcv_file_id INTEGER REFERENCES rcv_files (file_id) ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), via_contact_uri_hash BLOB, xcontact_id BLOB,
FOREIGN KEY (snd_file_id, connection_id)
REFERENCES snd_files (file_id, connection_id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED
rcv_file_id INTEGER REFERENCES rcv_files(file_id) ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
updated_at TEXT CHECK(updated_at NOT NULL),
via_contact_uri_hash BLOB,
xcontact_id BLOB,
via_user_contact_link INTEGER DEFAULT NULL
REFERENCES user_contact_links(user_contact_link_id) ON DELETE SET NULL,
FOREIGN KEY(snd_file_id, connection_id)
REFERENCES snd_files(file_id, connection_id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED
);
CREATE TABLE user_contact_links (
CREATE TABLE user_contact_links(
user_contact_link_id INTEGER PRIMARY KEY,
conn_req_contact BLOB NOT NULL,
local_display_name TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), auto_accept INTEGER DEFAULT 0,
UNIQUE (user_id, local_display_name)
created_at TEXT NOT NULL DEFAULT(datetime('now')),
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
updated_at TEXT CHECK(updated_at NOT NULL),
auto_accept INTEGER DEFAULT 0,
auto_reply_msg_content TEXT DEFAULT NULL,
UNIQUE(user_id, local_display_name)
);
CREATE TABLE contact_requests (
CREATE TABLE contact_requests(
contact_request_id INTEGER PRIMARY KEY,
user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links
ON UPDATE CASCADE ON DELETE CASCADE,
agent_invitation_id BLOB NOT NULL,
ON UPDATE CASCADE ON DELETE CASCADE,
agent_invitation_id BLOB NOT NULL,
contact_profile_id INTEGER REFERENCES contact_profiles
ON DELETE SET NULL -- NULL if it's an incognito profile
DEFERRABLE INITIALLY DEFERRED,
local_display_name TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), xcontact_id BLOB,
FOREIGN KEY (user_id, local_display_name)
REFERENCES display_names (user_id, local_display_name)
ON UPDATE CASCADE
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED,
UNIQUE (user_id, local_display_name),
UNIQUE (user_id, contact_profile_id)
ON DELETE SET NULL -- NULL if it's an incognito profile
DEFERRABLE INITIALLY DEFERRED,
local_display_name TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK(updated_at NOT NULL), xcontact_id BLOB,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON UPDATE CASCADE
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED,
UNIQUE(user_id, local_display_name),
UNIQUE(user_id, contact_profile_id)
);
CREATE TABLE messages (
CREATE TABLE messages(
message_id INTEGER PRIMARY KEY,
msg_sent INTEGER NOT NULL, -- 0 for received, 1 for sent
chat_msg_event TEXT NOT NULL, -- message event tag (the constructor of CMEventTag)
chat_msg_event TEXT NOT NULL, -- message event tag(the constructor of CMEventTag)
msg_body BLOB, -- agent message body as received or sent
created_at TEXT NOT NULL DEFAULT (datetime('now'))
, updated_at TEXT CHECK (updated_at NOT NULL), connection_id INTEGER DEFAULT NULL REFERENCES connections ON DELETE CASCADE, group_id INTEGER DEFAULT NULL REFERENCES groups ON DELETE CASCADE, shared_msg_id BLOB, shared_msg_id_user INTEGER);
CREATE TABLE msg_deliveries (
created_at TEXT NOT NULL DEFAULT(datetime('now'))
,
updated_at TEXT CHECK(updated_at NOT NULL),
connection_id INTEGER DEFAULT NULL REFERENCES connections ON DELETE CASCADE,
group_id INTEGER DEFAULT NULL REFERENCES groups ON DELETE CASCADE,
shared_msg_id BLOB,
shared_msg_id_user INTEGER
);
CREATE TABLE msg_deliveries(
msg_delivery_id INTEGER PRIMARY KEY,
message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, -- non UNIQUE for group messages
connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE,
agent_msg_id INTEGER, -- internal agent message ID (NULL while pending)
agent_msg_id INTEGER, -- internal agent message ID(NULL while pending)
agent_msg_meta TEXT, -- JSON with timestamps etc. sent in MSG, NULL for sent
chat_ts TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- broker_ts for received, created_at for sent
UNIQUE (connection_id, agent_msg_id)
chat_ts TEXT NOT NULL DEFAULT(datetime('now')),
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL), -- broker_ts for received, created_at for sent
UNIQUE(connection_id, agent_msg_id)
);
CREATE TABLE msg_delivery_events (
CREATE TABLE msg_delivery_events(
msg_delivery_event_id INTEGER PRIMARY KEY,
msg_delivery_id INTEGER NOT NULL REFERENCES msg_deliveries ON DELETE CASCADE, -- non UNIQUE for multiple events per msg delivery
delivery_status TEXT NOT NULL, -- see MsgDeliveryStatus for allowed values
created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT CHECK (updated_at NOT NULL),
UNIQUE (msg_delivery_id, delivery_status)
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT CHECK(updated_at NOT NULL),
UNIQUE(msg_delivery_id, delivery_status)
);
CREATE TABLE pending_group_messages (
CREATE TABLE pending_group_messages(
pending_group_message_id INTEGER PRIMARY KEY,
group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE,
message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE,
group_member_intro_id INTEGER REFERENCES group_member_intros ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE TABLE chat_items (
CREATE TABLE chat_items(
chat_item_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, -- NULL for sent even if group_id is not
chat_msg_id INTEGER, -- sent as part of the message that created the item
created_by_msg_id INTEGER UNIQUE REFERENCES messages (message_id) ON DELETE SET NULL,
created_by_msg_id INTEGER UNIQUE REFERENCES messages(message_id) ON DELETE SET NULL,
item_sent INTEGER NOT NULL, -- 0 for received, 1 for sent
item_ts TEXT NOT NULL, -- broker_ts of creating message for received, created_at for sent
item_deleted INTEGER NOT NULL DEFAULT 0, -- 1 for deleted,
item_content TEXT NOT NULL, -- JSON
item_text TEXT NOT NULL, -- textual representation
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
, item_status TEXT CHECK (item_status NOT NULL), shared_msg_id BLOB, quoted_shared_msg_id BLOB, quoted_sent_at TEXT, quoted_content TEXT, quoted_sent INTEGER, quoted_member_id BLOB, item_edited INTEGER);
CREATE TABLE chat_item_messages (
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
,
item_status TEXT CHECK(item_status NOT NULL),
shared_msg_id BLOB,
quoted_shared_msg_id BLOB,
quoted_sent_at TEXT,
quoted_content TEXT,
quoted_sent INTEGER,
quoted_member_id BLOB,
item_edited INTEGER
);
CREATE TABLE chat_item_messages(
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
message_id INTEGER NOT NULL UNIQUE REFERENCES messages ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (chat_item_id, message_id)
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
UNIQUE(chat_item_id, message_id)
);
CREATE INDEX idx_connections_via_contact_uri_hash ON connections (via_contact_uri_hash);
CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests (xcontact_id);
CREATE INDEX idx_contacts_xcontact_id ON contacts (xcontact_id);
CREATE TABLE smp_servers (
CREATE INDEX idx_connections_via_contact_uri_hash ON connections(
via_contact_uri_hash
);
CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests(xcontact_id);
CREATE INDEX idx_contacts_xcontact_id ON contacts(xcontact_id);
CREATE TABLE smp_servers(
smp_server_id INTEGER PRIMARY KEY,
host TEXT NOT NULL,
port TEXT NOT NULL,
key_hash BLOB NOT NULL,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (host, port)
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
UNIQUE(host, port)
);
CREATE INDEX idx_messages_shared_msg_id ON messages(shared_msg_id);
CREATE UNIQUE INDEX idx_messages_direct_shared_msg_id ON messages(
connection_id,
shared_msg_id_user,
shared_msg_id
);
CREATE UNIQUE INDEX idx_messages_group_shared_msg_id ON messages(
group_id,
shared_msg_id_user,
shared_msg_id
);
CREATE INDEX idx_chat_items_shared_msg_id ON chat_items(shared_msg_id);
CREATE TABLE calls(
-- stores call invitations state for communicating state between NSE and app when call notification comes
call_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE,
shared_call_id BLOB NOT NULL,
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
call_state BLOB NOT NULL,
call_ts TEXT NOT NULL,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE INDEX idx_messages_shared_msg_id ON messages (shared_msg_id);
CREATE UNIQUE INDEX idx_messages_direct_shared_msg_id ON messages (connection_id, shared_msg_id_user, shared_msg_id);
CREATE UNIQUE INDEX idx_messages_group_shared_msg_id ON messages (group_id, shared_msg_id_user, shared_msg_id);
CREATE INDEX idx_chat_items_shared_msg_id ON chat_items (shared_msg_id);
+27 -3
View File
@@ -12,16 +12,21 @@ import qualified Data.Aeson as J
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.List (find)
import Data.Maybe (fromMaybe)
import Foreign.C.String
import Foreign.C.Types (CInt (..))
import Foreign.StablePtr
import GHC.Generics (Generic)
import Simplex.Chat
import Simplex.Chat.Controller
import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList)
import Simplex.Chat.Options
import Simplex.Chat.Store
import Simplex.Chat.Types
import Simplex.Chat.Util (safeDecodeUtf8)
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (yesToMigrations))
import Simplex.Messaging.Protocol (CorrId (..))
import System.Timeout (timeout)
foreign export ccall "chat_init" cChatInit :: CString -> IO (StablePtr ChatController)
@@ -29,6 +34,10 @@ foreign export ccall "chat_send_cmd" cChatSendCmd :: StablePtr ChatController ->
foreign export ccall "chat_recv_msg" cChatRecvMsg :: StablePtr ChatController -> IO CJSONString
foreign export ccall "chat_recv_msg_wait" cChatRecvMsgWait :: StablePtr ChatController -> CInt -> IO CJSONString
foreign export ccall "chat_parse_markdown" cChatParseMarkdown :: CString -> IO CJSONString
-- | initialize chat controller
-- The active user has to be created and the chat has to be started before most commands can be used.
cChatInit :: CString -> IO (StablePtr ChatController)
@@ -45,6 +54,14 @@ cChatSendCmd cPtr cCmd = do
cChatRecvMsg :: StablePtr ChatController -> IO CJSONString
cChatRecvMsg cc = deRefStablePtr cc >>= chatRecvMsg >>= newCAString
-- | receive message from chat (blocking up to `t` microseconds (1/10^6 sec), returns empty string if times out)
cChatRecvMsgWait :: StablePtr ChatController -> CInt -> IO CJSONString
cChatRecvMsgWait cc t = deRefStablePtr cc >>= (`chatRecvMsgWait` fromIntegral t) >>= newCAString
-- | parse markdown - returns ParsedMarkdown type JSON
cChatParseMarkdown :: CString -> IO CJSONString
cChatParseMarkdown s = newCAString . chatParseMarkdown =<< peekCAString s
mobileChatOpts :: ChatOpts
mobileChatOpts =
ChatOpts
@@ -54,7 +71,8 @@ mobileChatOpts =
logAgent = False,
chatCmd = "",
chatCmdDelay = 3,
chatServerPort = Nothing
chatServerPort = Nothing,
maintenance = True
}
defaultMobileConfig :: ChatConfig
@@ -67,12 +85,12 @@ defaultMobileConfig =
type CJSONString = CString
getActiveUser_ :: SQLiteStore -> IO (Maybe User)
getActiveUser_ st = find activeUser <$> getUsers st
getActiveUser_ st = find activeUser <$> withTransaction st getUsers
chatInit :: String -> IO ChatController
chatInit dbFilePrefix = do
let f = chatStoreFile dbFilePrefix
chatStore <- createStore f (dbPoolSize defaultMobileConfig) (yesToMigrations (defaultMobileConfig :: ChatConfig))
chatStore <- createStore f (yesToMigrations (defaultMobileConfig :: ChatConfig))
user_ <- getActiveUser_ chatStore
newChatController chatStore user_ defaultMobileConfig mobileChatOpts {dbFilePrefix} Nothing
@@ -84,6 +102,12 @@ chatRecvMsg ChatController {outputQ} = json <$> atomically (readTBQueue outputQ)
where
json (corr, resp) = LB.unpack $ J.encode APIResponse {corr, resp}
chatRecvMsgWait :: ChatController -> Int -> IO JSONString
chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc)
chatParseMarkdown :: String -> JSONString
chatParseMarkdown = LB.unpack . J.encode . ParsedMarkdown . parseMaybeMarkdownList . safeDecodeUtf8 . B.pack
data APIResponse = APIResponse {corr :: Maybe CorrId, resp :: ChatResponse}
deriving (Generic)
+9 -2
View File
@@ -25,7 +25,8 @@ data ChatOpts = ChatOpts
logAgent :: Bool,
chatCmd :: String,
chatCmdDelay :: Int,
chatServerPort :: Maybe String
chatServerPort :: Maybe String,
maintenance :: Bool
}
chatOpts :: FilePath -> FilePath -> Parser ChatOpts
@@ -88,7 +89,13 @@ chatOpts appDir defaultDbFileName = do
<> help "Run chat server on specified port"
<> value Nothing
)
pure ChatOpts {dbFilePrefix, smpServers, logConnections, logAgent, chatCmd, chatCmdDelay, chatServerPort}
maintenance <-
switch
( long "maintenance"
<> short 'm'
<> help "Run in maintenance mode (/_start to start chat)"
)
pure ChatOpts {dbFilePrefix, smpServers, logConnections, logAgent, chatCmd, chatCmdDelay, chatServerPort, maintenance}
where
defaultDbFilePath = combine appDir defaultDbFileName
+18 -4
View File
@@ -31,10 +31,10 @@ import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic)
import Simplex.Chat.Call
import Simplex.Chat.Types
import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8)
import Simplex.Chat.Util (safeDecodeUtf8)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (fromTextField_)
import Simplex.Messaging.Util ((<$?>))
import Simplex.Messaging.Parsers (fromTextField_, fstToLower, sumTypeJSON)
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
data ConnectionEntity
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
@@ -42,7 +42,11 @@ data ConnectionEntity
| SndFileConnection {entityConnection :: Connection, sndFileTransfer :: SndFileTransfer}
| RcvFileConnection {entityConnection :: Connection, rcvFileTransfer :: RcvFileTransfer}
| UserContactConnection {entityConnection :: Connection, userContact :: UserContact}
deriving (Eq, Show)
deriving (Eq, Show, Generic)
instance ToJSON ConnectionEntity where
toJSON = J.genericToJSON $ sumTypeJSON fstToLower
toEncoding = J.genericToEncoding $ sumTypeJSON fstToLower
updateEntityConnStatus :: ConnectionEntity -> ConnStatus -> ConnectionEntity
updateEntityConnStatus connEntity connStatus = case connEntity of
@@ -437,6 +441,16 @@ instance FromField CMEventTag where fromField = fromTextField_ cmEventTagT
instance ToField CMEventTag where toField = toField . serializeCMEventTag
hasNotification :: CMEventTag -> Bool
hasNotification = \case
XMsgNew_ -> True
XFile_ -> True
XContact_ -> True
XGrpInv_ -> True
XGrpDel_ -> True
XCallInv_ -> True
_ -> False
appToChatMessage :: AppMessage -> Either String ChatMessage
appToChatMessage AppMessage {msgId, event, params} = do
eventTag <- strDecode $ encodeUtf8 event
+1539 -1695
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -28,7 +28,7 @@ terminalChatConfig =
"smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im",
"smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im"
],
ntf = ["smp://ZH1Dkt2_EQRbxUUyjLlcUjg1KAhBrqfvE0xfn7Ki0Zg=@ntf1.simplex.im"]
ntf = ["smp://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im"]
}
}
+7 -2
View File
@@ -99,7 +99,11 @@ data UserContact = UserContact
{ userContactLinkId :: Int64,
connReqContact :: ConnReqContact
}
deriving (Eq, Show)
deriving (Eq, Show, Generic)
instance ToJSON UserContact where
toJSON = J.genericToJSON J.defaultOptions
toEncoding = J.genericToEncoding J.defaultOptions
data UserContactRequest = UserContactRequest
{ contactRequestId :: Int64,
@@ -665,7 +669,8 @@ data Connection = Connection
{ connId :: Int64,
agentConnId :: AgentConnId,
connLevel :: Int,
viaContact :: Maybe Int64,
viaContact :: Maybe Int64, -- group member contact ID, if not direct connection
viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address"
connType :: ConnType,
connStatus :: ConnStatus,
entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID
+2 -12
View File
@@ -1,6 +1,5 @@
module Simplex.Chat.Util where
import Control.Monad (when)
import Data.ByteString.Char8 (ByteString)
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8With)
@@ -10,14 +9,5 @@ safeDecodeUtf8 = decodeUtf8With onError
where
onError _ _ = Just '?'
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM ba t f = ba >>= \b -> if b then t else f
whenM :: Monad m => m Bool -> m () -> m ()
whenM ba a = ba >>= (`when` a)
unlessM :: Monad m => m Bool -> m () -> m ()
unlessM b = ifM b $ pure ()
eitherToMaybe :: Either a b -> Maybe b
eitherToMaybe = either (const Nothing) Just
uncurry3 :: (a -> b -> c -> d) -> ((a, b, c) -> d)
uncurry3 f ~(a, b, c) = f a b c
+16 -4
View File
@@ -53,7 +53,9 @@ responseToView :: Bool -> ChatResponse -> [StyledString]
responseToView testView = \case
CRActiveUser User {profile} -> viewUserProfile profile
CRChatStarted -> ["chat started"]
CRChatRunning -> []
CRChatRunning -> ["chat is running"]
CRChatStopped -> ["chat stopped"]
CRChatSuspended -> ["chat suspended"]
CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats]
CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat]
CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft]
@@ -77,8 +79,8 @@ responseToView testView = \case
HSMarkdown -> markdownInfo
CRWelcome user -> chatWelcome user
CRContactsList cs -> viewContactsList cs
CRUserContactLink cReqUri _ -> connReqContact_ "Your chat address:" cReqUri
CRUserContactLinkUpdated _ autoAccept -> ["auto_accept " <> if autoAccept then "on" else "off"]
CRUserContactLink cReqUri autoAccept autoReply -> connReqContact_ "Your chat address:" cReqUri <> autoAcceptStatus_ autoAccept autoReply
CRUserContactLinkUpdated _ autoAccept autoReply -> autoAcceptStatus_ autoAccept autoReply
CRContactRequestRejected UserContactRequest {localDisplayName = c} -> [ttyContact c <> ": contact request rejected"]
CRGroupCreated g -> viewGroupCreated g
CRGroupMembers g -> viewGroupMembers g
@@ -150,16 +152,19 @@ responseToView testView = \case
["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
CRRcvFileSubError RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e ->
["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
CRCallInvitation {contact, callType, sharedKey} -> viewCallInvitation contact callType sharedKey
CRCallInvitation RcvCallInvitation {contact, callType, sharedKey} -> viewCallInvitation contact callType sharedKey
CRCallOffer {contact, callType, offer, sharedKey} -> viewCallOffer contact callType offer sharedKey
CRCallAnswer {contact, answer} -> viewCallAnswer contact answer
CRCallExtraInfo {contact} -> ["call extra info from " <> ttyContact' contact]
CRCallEnded {contact} -> ["call with " <> ttyContact' contact <> " ended"]
CRCallInvitations _ -> []
CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"]
CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"]
CRNewContactConnection _ -> []
CRContactConnectionDeleted PendingContactConnection {pccConnId} -> ["connection :" <> sShow pccConnId <> " deleted"]
CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)]
CRNtfToken _ status mode -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode)]
CRNtfMessages {} -> []
CRMessageError prefix err -> [plain prefix <> ": " <> plain err]
CRChatError e -> viewChatError e
where
@@ -357,6 +362,11 @@ connReqContact_ intro cReq =
"to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)"
]
autoAcceptStatus_ :: Bool -> Maybe MsgContent -> [StyledString]
autoAcceptStatus_ autoAccept autoReply =
("auto_accept " <> if autoAccept then "on" else "off") :
maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply
viewReceivedContactRequest :: ContactName -> Profile -> [StyledString]
viewReceivedContactRequest c Profile {fullName} =
[ ttyFullName c fullName <> " wants to connect to you!",
@@ -721,6 +731,8 @@ viewChatError = \case
CENoActiveUser -> ["error: active user is required"]
CEActiveUserExists -> ["error: active user already exists"]
CEChatNotStarted -> ["error: chat not started"]
CEChatNotStopped -> ["error: chat not stopped"]
CEChatStoreChanged -> ["error: chat store changed"]
CEInvalidConnReq -> viewInvalidConnReq
CEInvalidChatMessage e -> ["chat message error: " <> sShow e]
CEContactNotReady c -> [ttyContact' c <> ": not ready"]

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