mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge pull request #753 from simplex-chat/ios-notifications
iOS notifications
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)" } }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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 {
|
||||
+147
-37
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
+208
-162
@@ -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
|
||||
+12
-12
@@ -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
|
||||
@@ -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
@@ -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@-->
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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.*
|
||||
|
||||
@@ -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,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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
);
|
||||
|]
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user