Compare commits

...

10 Commits

Author SHA1 Message Date
Evgeny Poberezkin 88c57c82d4 file provider works 2022-06-10 22:49:48 +01:00
Evgeny Poberezkin e01be483da file provider service is working 2022-06-09 17:29:58 +01:00
Evgeny Poberezkin c7b5d73512 Merge branch 'ios-notifications' into ep/ios-file-provider 2022-06-09 15:20:43 +01:00
Evgeny Poberezkin 360553deeb file provider experiment 2022-06-02 22:44:32 +01:00
Evgeny Poberezkin b62f2acca7 trying to send service endpoint via mach message - it does not work 2022-06-02 18:24:45 +01:00
Evgeny Poberezkin af3dcc4a9a file provider does not connect 2022-06-02 15:53:44 +01:00
Evgeny Poberezkin b0a81252c9 Merge branch 'ios-notifications' into ep/ios-file-provider 2022-06-02 13:20:21 +01:00
Evgeny Poberezkin e15d4ac6b6 Merge branch 'ios-notifications' into ep/ios-file-provider 2022-06-02 12:41:07 +01:00
Evgeny Poberezkin 17b8101d88 file provider: making service work 2022-06-02 11:06:45 +01:00
Evgeny Poberezkin 1b972bc7cc ios: file provider service 2022-06-01 17:26:15 +01:00
70 changed files with 1732 additions and 280 deletions
+2 -1
View File
@@ -8,7 +8,8 @@
import Foundation import Foundation
import UIKit import UIKit
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
class AppDelegate: NSObject, UIApplicationDelegate { class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
+2 -2
View File
@@ -70,8 +70,8 @@ class BGManager {
return return
} }
self.completed = false self.completed = false
DispatchQueue.main.async { Task {
initializeChat() await initializeChat()
if ChatModel.shared.currentUser == nil { if ChatModel.shared.currentUser == nil {
completeReceiving("no current user") completeReceiving("no current user")
return return
+1 -1
View File
@@ -10,7 +10,7 @@ import Foundation
import Combine import Combine
import SwiftUI import SwiftUI
import WebKit import WebKit
import SimpleXChat import SimpleXChatSDK
final class ChatModel: ObservableObject { final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage? @Published var onboardingStage: OnboardingStage?
+2 -1
View File
@@ -9,7 +9,8 @@
import Foundation import Foundation
import UserNotifications import UserNotifications
import UIKit import UIKit
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT" let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT"
let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL" let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL"
+58 -61
View File
@@ -11,9 +11,8 @@ import UIKit
import Dispatch import Dispatch
import BackgroundTasks import BackgroundTasks
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
private var chatController: chat_ctrl?
enum TerminalItem: Identifiable { enum TerminalItem: Identifiable {
case cmd(Date, ChatCommand) case cmd(Date, ChatCommand)
@@ -47,7 +46,7 @@ enum TerminalItem: Identifiable {
} }
} }
private func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) { private func beginBGTask(_ handler: (() -> Void)? = nil) -> (@Sendable () -> Void) {
var id: UIBackgroundTaskIdentifier! var id: UIBackgroundTaskIdentifier!
var running = true var running = true
let endTask = { let endTask = {
@@ -72,10 +71,10 @@ private func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) {
let msgDelay: Double = 7.5 let msgDelay: Double = 7.5
let maxTaskDuration: Double = 15 let maxTaskDuration: Double = 15
private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> ChatResponse) -> ChatResponse { private func withBGTask(bgDelay: Double? = nil, f: @escaping () async -> ChatResponse) async -> ChatResponse {
let endTask = beginBGTask() let endTask = beginBGTask()
DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask) DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask)
let r = f() let r = await f()
if let d = bgDelay { if let d = bgDelay {
DispatchQueue.global().asyncAfter(deadline: .now() + d, execute: endTask) DispatchQueue.global().asyncAfter(deadline: .now() + d, execute: endTask)
} else { } else {
@@ -84,10 +83,10 @@ private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> ChatResponse)
return r return r
} }
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) -> ChatResponse { func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) async -> ChatResponse {
logger.debug("chatSendCmd \(cmd.cmdType)") logger.debug("chatSendCmd \(cmd.cmdType)")
let resp = bgTask let resp = await bgTask
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd) } ? withBGTask(bgDelay: bgDelay) { await sendSimpleXCmd(cmd) }
: sendSimpleXCmd(cmd) : sendSimpleXCmd(cmd)
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
if case let .response(_, json) = resp { if case let .response(_, json) = resp {
@@ -102,25 +101,14 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
return resp return resp
} }
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) async -> ChatResponse {
await withCheckedContinuation { cont in
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay))
}
}
func chatRecvMsg() async -> ChatResponse { func chatRecvMsg() async -> ChatResponse {
await withCheckedContinuation { cont in await withBGTask(bgDelay: msgDelay) {
_ = withBGTask(bgDelay: msgDelay) { await recvSimpleXMsg()
let resp = chatResponse(chat_recv_msg(getChatCtrl())!)
cont.resume(returning: resp)
return resp
}
} }
} }
func apiGetActiveUser() throws -> User? { func apiGetActiveUser() async throws -> User? {
let _ = getChatCtrl() let r = await chatSendCmd(.showActiveUser)
let r = chatSendCmdSync(.showActiveUser)
switch r { switch r {
case let .activeUser(user): return user case let .activeUser(user): return user
case .chatCmdError(.error(.noActiveUser)): return nil case .chatCmdError(.error(.noActiveUser)): return nil
@@ -128,14 +116,14 @@ func apiGetActiveUser() throws -> User? {
} }
} }
func apiCreateActiveUser(_ p: Profile) throws -> User { func apiCreateActiveUser(_ p: Profile) async throws -> User {
let r = chatSendCmdSync(.createActiveUser(profile: p)) let r = await chatSendCmd(.createActiveUser(profile: p))
if case let .activeUser(user) = r { return user } if case let .activeUser(user) = r { return user }
throw r throw r
} }
func apiStartChat() throws -> Bool { func apiStartChat() async throws -> Bool {
let r = chatSendCmdSync(.startChat) let r = await chatSendCmd(.startChat)
switch r { switch r {
case .chatStarted: return true case .chatStarted: return true
case .chatRunning: return false case .chatRunning: return false
@@ -143,20 +131,20 @@ func apiStartChat() throws -> Bool {
} }
} }
func apiSetFilesFolder(filesFolder: String) throws { func apiSetFilesFolder(filesFolder: String) async throws {
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder)) let r = await chatSendCmd(.setFilesFolder(filesFolder: filesFolder))
if case .cmdOk = r { return } if case .cmdOk = r { return }
throw r throw r
} }
func apiGetChats() throws -> [Chat] { func apiGetChats() async throws -> [Chat] {
let r = chatSendCmdSync(.apiGetChats) let r = await chatSendCmd(.apiGetChats)
if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } } if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } }
throw r throw r
} }
func apiGetChat(type: ChatType, id: Int64) throws -> Chat { func apiGetChat(type: ChatType, id: Int64) async throws -> Chat {
let r = chatSendCmdSync(.apiGetChat(type: type, id: id)) let r = await chatSendCmd(.apiGetChat(type: type, id: id))
if case let .apiChat(chat) = r { return Chat.init(chat) } if case let .apiChat(chat) = r { return Chat.init(chat) }
throw r throw r
} }
@@ -214,8 +202,8 @@ func apiDeleteToken(token: String) async throws {
try await sendCommandOkResp(.apiDeleteToken(token: token)) try await sendCommandOkResp(.apiDeleteToken(token: token))
} }
func getUserSMPServers() throws -> [String] { func getUserSMPServers() async throws -> [String] {
let r = chatSendCmdSync(.getUserSMPServers) let r = await chatSendCmd(.getUserSMPServers)
if case let .userSMPServers(smpServers) = r { return smpServers } if case let .userSMPServers(smpServers) = r { return smpServers }
throw r throw r
} }
@@ -224,8 +212,8 @@ func setUserSMPServers(smpServers: [String]) async throws {
try await sendCommandOkResp(.setUserSMPServers(smpServers: smpServers)) try await sendCommandOkResp(.setUserSMPServers(smpServers: smpServers))
} }
func apiAddContact() throws -> String { func apiAddContact() async throws -> String {
let r = chatSendCmdSync(.addContact, bgTask: false) let r = await chatSendCmd(.addContact, bgTask: false)
if case let .invitation(connReqInvitation) = r { return connReqInvitation } if case let .invitation(connReqInvitation) = r { return connReqInvitation }
throw r throw r
} }
@@ -312,8 +300,8 @@ func apiUpdateProfile(profile: Profile) async throws -> Profile? {
} }
} }
func apiParseMarkdown(text: String) throws -> [FormattedText]? { func apiParseMarkdown(text: String) async throws -> [FormattedText]? {
let r = chatSendCmdSync(.apiParseMarkdown(text: text)) let r = await sendSimpleXCmd(.apiParseMarkdown(text: text))
if case let .apiParsedMarkdown(formattedText) = r { return formattedText } if case let .apiParsedMarkdown(formattedText) = r { return formattedText }
throw r throw r
} }
@@ -330,8 +318,8 @@ func apiDeleteUserAddress() async throws {
throw r throw r
} }
func apiGetUserAddress() throws -> String? { func apiGetUserAddress() async throws -> String? {
let r = chatSendCmdSync(.showMyAddress) let r = await sendSimpleXCmd(.showMyAddress)
switch r { switch r {
case let .userContactLink(connReq): case let .userContactLink(connReq):
return connReq return connReq
@@ -454,36 +442,45 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
throw r throw r
} }
func initializeChat() { func initializeChat() async {
logger.debug("initializeChat") logger.debug("initializeChat")
do { do {
let m = ChatModel.shared let m = ChatModel.shared
m.currentUser = try apiGetActiveUser() let user = try await apiGetActiveUser()
if m.currentUser == nil { await MainActor.run {
m.onboardingStage = .step1_SimpleXInfo m.currentUser = user
} else { if user == nil {
startChat() m.onboardingStage = .step1_SimpleXInfo
}
}
if user != nil {
await startChat()
} }
} catch { } catch {
fatalError("Failed to initialize chat controller or database: \(error)") fatalError("Failed to initialize chat controller or database: \(error)")
} }
} }
func startChat() { func startChat() async {
logger.debug("startChat") logger.debug("startChat")
do { do {
let m = ChatModel.shared
// TODO set file folder once, before chat is started // TODO set file folder once, before chat is started
let justStarted = try apiStartChat() let justStarted = try await apiStartChat()
if justStarted { if justStarted {
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try await apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
m.userAddress = try apiGetUserAddress() let userAddress = try await apiGetUserAddress()
m.userSMPServers = try getUserSMPServers() let userSMPServers = try await getUserSMPServers()
m.chats = try apiGetChats() let chats = try await apiGetChats()
withAnimation { DispatchQueue.main.async {
m.onboardingStage = m.chats.isEmpty let m = ChatModel.shared
? .step3_MakeConnection m.userAddress = userAddress
: .onboardingComplete m.userSMPServers = userSMPServers
m.chats = chats
withAnimation {
m.onboardingStage = m.chats.isEmpty
? .step3_MakeConnection
: .onboardingComplete
}
} }
} }
ChatReceiver.shared.start() ChatReceiver.shared.start()
@@ -512,7 +509,7 @@ class ChatReceiver {
func receiveMsgLoop() async { func receiveMsgLoop() async {
let msg = await chatRecvMsg() let msg = await chatRecvMsg()
self._lastMsgTime = .now self._lastMsgTime = .now
processReceivedMsg(msg) await processReceivedMsg(msg)
if self.receiveMessages { if self.receiveMessages {
do { try await Task.sleep(nanoseconds: 7_500_000) } do { try await Task.sleep(nanoseconds: 7_500_000) }
catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") } catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") }
@@ -528,7 +525,7 @@ class ChatReceiver {
} }
} }
func processReceivedMsg(_ res: ChatResponse) { func processReceivedMsg(_ res: ChatResponse) async {
let m = ChatModel.shared let m = ChatModel.shared
DispatchQueue.main.async { DispatchQueue.main.async {
m.terminalItems.append(.resp(.now, res)) m.terminalItems.append(.resp(.now, res))
+8 -16
View File
@@ -7,17 +7,10 @@
import SwiftUI import SwiftUI
import OSLog import OSLog
import SimpleXChat import SimpleXAppShared
let logger = Logger() let logger = Logger()
let machMessenger = MachMessenger(APP_MACH_PORT, callback: receivedNSEMachMessage)
func receivedNSEMachMessage(msgId: Int32, msg: String) -> String? {
logger.debug("MachMessenger: receivedNSEMachMessage \"\(msg)\" from NSE, replying")
return "reply from App to: \(msg)"
}
@main @main
struct SimpleXApp: App { struct SimpleXApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@@ -30,11 +23,10 @@ struct SimpleXApp: App {
@State private var enteredBackground: Double? = nil @State private var enteredBackground: Double? = nil
init() { init() {
hs_init(0, nil) // hs_init(0, nil)
UserDefaults.standard.register(defaults: appDefaults) UserDefaults.standard.register(defaults: appDefaults)
BGManager.shared.register() BGManager.shared.register()
NtfManager.shared.registerCategories() NtfManager.shared.registerCategories()
machMessenger.start()
} }
var body: some Scene { var body: some Scene {
@@ -45,13 +37,13 @@ struct SimpleXApp: App {
logger.debug("ContentView.onOpenURL: \(url)") logger.debug("ContentView.onOpenURL: \(url)")
chatModel.appOpenUrl = url chatModel.appOpenUrl = url
} }
.onAppear() { .onAppear {
initializeChat() Task { await initializeChat() }
} }
.onChange(of: scenePhase) { phase in .onChange(of: scenePhase) { phase in
logger.debug("scenePhase \(String(describing: scenePhase))") logger.debug("scenePhase \(String(describing: scenePhase))")
let res = machMessenger.sendMessageWithReply(NSE_MACH_PORT, msg: "App scenePhase changed to \(String(describing: scenePhase))") // let res = machMessenger.sendMessageWithReply(NSE_MACH_PORT, msg: "App scenePhase changed to \(String(describing: scenePhase))")
logger.debug("MachMessenger \(String(describing: res), privacy: .public)") // logger.debug("MachMessenger \(String(describing: res), privacy: .public)")
setAppState(phase) setAppState(phase)
switch (phase) { switch (phase) {
case .background: case .background:
@@ -60,10 +52,10 @@ struct SimpleXApp: App {
enteredBackground = ProcessInfo.processInfo.systemUptime enteredBackground = ProcessInfo.processInfo.systemUptime
} }
doAuthenticate = false doAuthenticate = false
machMessenger.stop() // machMessenger.stop()
case .active: case .active:
doAuthenticate = authenticationExpired() doAuthenticate = authenticationExpired()
machMessenger.start() // machMessenger.start()
default: default:
break break
} }
@@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
import WebKit import WebKit
import SimpleXChat import SimpleXChatSDK
struct ActiveCallView: View { struct ActiveCallView: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@@ -9,7 +9,7 @@
import Foundation import Foundation
//import CallKit //import CallKit
import AVFoundation import AVFoundation
import SimpleXChat import SimpleXChatSDK
//class CallController: NSObject, CXProviderDelegate, ObservableObject { //class CallController: NSObject, CXProviderDelegate, ObservableObject {
class CallController: NSObject, ObservableObject { class CallController: NSObject, ObservableObject {
+1 -1
View File
@@ -7,7 +7,7 @@
// //
import Foundation import Foundation
import SimpleXChat import SimpleXChatSDK
class CallManager { class CallManager {
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID { func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID {
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct IncomingCallView: View { struct IncomingCallView: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
+1 -1
View File
@@ -8,7 +8,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
class Call: ObservableObject, Equatable { class Call: ObservableObject, Equatable {
static func == (lhs: Call, rhs: Call) -> Bool { static func == (lhs: Call, rhs: Call) -> Bool {
+1 -1
View File
@@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
import WebKit import WebKit
import SimpleXChat import SimpleXChatSDK
class WebRTCCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate { class WebRTCCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate {
var rtcWebView: Binding<WKWebView?> var rtcWebView: Binding<WKWebView?>
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
private let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9) 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 ) private let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2 )
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct ChatInfoView: View { struct ChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct CICallItemView: View { struct CICallItemView: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@@ -7,7 +7,8 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
struct CIFileView: View { struct CIFileView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -7,7 +7,8 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
struct CIImageView: View { struct CIImageView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -7,7 +7,8 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
struct CILinkView: View { struct CILinkView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct CIMetaView: View { struct CIMetaView: View {
var chatItem: ChatItem var chatItem: ChatItem
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct DeletedItemView: View { struct DeletedItemView: View {
var chatItem: ChatItem var chatItem: ChatItem
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct EmojiItemView: View { struct EmojiItemView: View {
var chatItem: ChatItem var chatItem: ChatItem
@@ -7,7 +7,8 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12) 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) let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct IntegrityErrorItemView: View { struct IntegrityErrorItemView: View {
var chatItem: ChatItem var chatItem: ChatItem
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1) private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
private let linkColor = Color(uiColor: uiLinkColor) private let linkColor = Color(uiColor: uiLinkColor)
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct ChatItemView: View { struct ChatItemView: View {
var chatInfo: ChatInfo var chatInfo: ChatInfo
+2 -1
View File
@@ -7,7 +7,8 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
private let memberImageSize: CGFloat = 34 private let memberImageSize: CGFloat = 34
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXAppShared
struct ComposeImageView: View { struct ComposeImageView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -8,7 +8,8 @@
import SwiftUI import SwiftUI
import LinkPresentation import LinkPresentation
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
logger.debug("getLinkMetadata: fetching URL preview") logger.debug("getLinkMetadata: fetching URL preview")
@@ -7,7 +7,8 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
enum ComposePreview { enum ComposePreview {
case noPreview case noPreview
@@ -164,7 +165,7 @@ struct ComposeView: View {
.onChange(of: composeState.message) { _ in .onChange(of: composeState.message) { _ in
if composeState.linkPreviewAllowed() { if composeState.linkPreviewAllowed() {
if composeState.message.count > 0 { if composeState.message.count > 0 {
showLinkPreview(composeState.message) Task { await showLinkPreview(composeState.message) }
} else { } else {
resetLinkPreview() resetLinkPreview()
} }
@@ -306,7 +307,7 @@ struct ComposeView: View {
case .noPreview: case .noPreview:
mc = .text(composeState.message) mc = .text(composeState.message)
case .linkPreview: case .linkPreview:
mc = checkLinkPreview() mc = await checkLinkPreview()
case let .imagePreview(imagePreview: image): case let .imagePreview(imagePreview: image):
if let uiImage = chosenImage, if let uiImage = chosenImage,
let savedFile = saveImage(uiImage) { let savedFile = saveImage(uiImage) {
@@ -357,12 +358,12 @@ struct ComposeView: View {
chosenFile = nil chosenFile = nil
} }
private func updateMsgContent(_ msgContent: MsgContent) -> MsgContent { private func updateMsgContent(_ msgContent: MsgContent) async -> MsgContent {
switch msgContent { switch msgContent {
case .text: case .text:
return checkLinkPreview() return await checkLinkPreview()
case .link: case .link:
return checkLinkPreview() return await checkLinkPreview()
case .image(_, let image): case .image(_, let image):
return .image(text: composeState.message, image: image) return .image(text: composeState.message, image: image)
case .file: case .file:
@@ -372,9 +373,9 @@ struct ComposeView: View {
} }
} }
private func showLinkPreview(_ s: String) { private func showLinkPreview(_ s: String) async {
prevLinkUrl = linkUrl prevLinkUrl = linkUrl
linkUrl = parseMessage(s) linkUrl = await parseMessage(s)
if let url = linkUrl { if let url = linkUrl {
if url != composeState.linkPreview()?.uri && url != pendingLinkUrl { if url != composeState.linkPreview()?.uri && url != pendingLinkUrl {
pendingLinkUrl = url pendingLinkUrl = url
@@ -391,9 +392,9 @@ struct ComposeView: View {
} }
} }
private func parseMessage(_ msg: String) -> URL? { private func parseMessage(_ msg: String) async -> URL? {
do { do {
let parsedMsg = try apiParseMarkdown(text: msg) let parsedMsg = try await apiParseMarkdown(text: msg)
let uri = parsedMsg?.first(where: { ft in let uri = parsedMsg?.first(where: { ft in
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
}) })
@@ -437,10 +438,10 @@ struct ComposeView: View {
cancelledLinks = [] cancelledLinks = []
} }
private func checkLinkPreview() -> MsgContent { private func checkLinkPreview() async -> MsgContent {
switch (composeState.preview) { switch (composeState.preview) {
case let .linkPreview(linkPreview: linkPreview): case let .linkPreview(linkPreview: linkPreview):
if let url = parseMessage(composeState.message), if let url = await parseMessage(composeState.message),
let linkPreview = linkPreview, let linkPreview = linkPreview,
url == linkPreview.uri { url == linkPreview.uri {
return .link(text: composeState.message, preview: linkPreview) return .link(text: composeState.message, preview: linkPreview)
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct ContextItemView: View { struct ContextItemView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct SendMessageView: View { struct SendMessageView: View {
@Binding var composeState: ComposeState @Binding var composeState: ComposeState
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct ChatListNavLink: View { struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@@ -31,13 +31,17 @@ struct ChatListNavLink: View {
private func chatView() -> some View { private func chatView() -> some View {
ChatView(chat: chat, showChatInfo: $showChatInfo) ChatView(chat: chat, showChatInfo: $showChatInfo)
.onAppear { .onAppear {
do { Task {
let cInfo = chat.chatInfo do {
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId) let cInfo = chat.chatInfo
chatModel.updateChatInfo(chat.chatInfo) let chat = try await apiGetChat(type: cInfo.chatType, id: cInfo.apiId)
chatModel.chatItems = chat.chatItems DispatchQueue.main.async {
} catch { chatModel.updateChatInfo(chat.chatInfo)
logger.error("ChatListNavLink.chatView apiGetChatItems error: \(error.localizedDescription)") chatModel.chatItems = chat.chatItems
}
} catch {
logger.error("ChatListNavLink.chatView apiGetChatItems error: \(String(describing: error))")
}
} }
} }
} }
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct ChatListView: View { struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct ChatPreviewView: View { struct ChatPreviewView: View {
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct ContactConnectionView: View { struct ContactConnectionView: View {
var contactConnection: PendingContactConnection var contactConnection: PendingContactConnection
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct ContactRequestView: View { struct ContactRequestView: View {
var contactRequest: UserContactRequest var contactRequest: UserContactRequest
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct ChatInfoImage: View { struct ChatInfoImage: View {
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXAppShared
struct ProfileImage: View { struct ProfileImage: View {
var imageStr: String? = nil var imageStr: String? = nil
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
enum NewChatAction: Identifiable { enum NewChatAction: Identifiable {
case createLink case createLink
@@ -27,7 +27,7 @@ struct NewChatButton: View {
Image(systemName: "person.crop.circle.badge.plus") Image(systemName: "person.crop.circle.badge.plus")
} }
.confirmationDialog("Add contact to start a new chat", isPresented: $showAddChat, titleVisibility: .visible) { .confirmationDialog("Add contact to start a new chat", isPresented: $showAddChat, titleVisibility: .visible) {
Button("Create link / QR code") { addContactAction() } Button("Create link / QR code") { Task { await addContactAction() } }
Button("Paste received link") { actionSheet = .pasteLink } Button("Paste received link") { actionSheet = .pasteLink }
Button("Scan QR code") { actionSheet = .scanQRCode } Button("Scan QR code") { actionSheet = .scanQRCode }
} }
@@ -40,9 +40,9 @@ struct NewChatButton: View {
} }
} }
func addContactAction() { func addContactAction() async {
do { do {
connReq = try apiAddContact() connReq = try await apiAddContact()
actionSheet = .createLink actionSheet = .createLink
} catch { } catch {
DispatchQueue.global().async { DispatchQueue.global().async {
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct CreateProfile: View { struct CreateProfile: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@@ -43,7 +43,7 @@ struct CreateProfile: View {
.focused($focusFullName) .focused($focusFullName)
.submitLabel(.go) .submitLabel(.go)
.onSubmit { .onSubmit {
if canCreateProfile() { createProfile() } if canCreateProfile() { Task { await createProfile() } }
else { focusFullName = true } else { focusFullName = true }
} }
@@ -64,7 +64,7 @@ struct CreateProfile: View {
HStack { HStack {
Button { Button {
createProfile() Task { await createProfile() }
} label: { } label: {
Text("Create") Text("Create")
Image(systemName: "greaterthan") Image(systemName: "greaterthan")
@@ -87,17 +87,19 @@ struct CreateProfile: View {
.padding(.bottom) .padding(.bottom)
} }
func createProfile() { func createProfile() async {
hideKeyboard() hideKeyboard()
let profile = Profile( let profile = Profile(
displayName: displayName, displayName: displayName,
fullName: fullName fullName: fullName
) )
do { do {
m.currentUser = try apiCreateActiveUser(profile) let user = try await apiCreateActiveUser(profile)
startChat() await MainActor.run { m.currentUser = user }
withAnimation { m.onboardingStage = .step3_MakeConnection } await startChat()
DispatchQueue.main.async {
withAnimation { m.onboardingStage = .step3_MakeConnection }
}
} catch { } catch {
fatalError("Failed to create user: \(error)") fatalError("Failed to create user: \(error)")
} }
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
struct MakeConnection: View { struct MakeConnection: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@@ -38,7 +38,7 @@ struct MakeConnection: View {
icon: "link.badge.plus", icon: "link.badge.plus",
title: "Create 1-time link / QR code", title: "Create 1-time link / QR code",
text: "It's secure to share - only one contact can use it." text: "It's secure to share - only one contact can use it."
) { addContactAction() } ) { Task { await addContactAction() } }
actionRow( actionRow(
icon: "doc.plaintext", icon: "doc.plaintext",
@@ -102,9 +102,9 @@ struct MakeConnection: View {
} }
} }
private func addContactAction() { private func addContactAction() async {
do { do {
connReq = try apiAddContact() connReq = try await apiAddContact()
actionSheet = .createLink actionSheet = .createLink
} catch { } catch {
DispatchQueue.global().async { DispatchQueue.global().async {
+1 -1
View File
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
private let terminalFont = Font.custom("Menlo", size: 16) private let terminalFont = Font.custom("Menlo", size: 16)
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
private let serversFont = Font.custom("Menlo", size: 14) private let serversFont = Font.custom("Menlo", size: 14)
@@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")! let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")!
@@ -7,7 +7,8 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChatSDK
import SimpleXAppShared
struct UserProfile: View { struct UserProfile: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
+35 -28
View File
@@ -8,7 +8,12 @@
import UserNotifications import UserNotifications
import OSLog import OSLog
import SimpleXChat import FileProvider
import SimpleXChatSDK
import SimpleXAppShared
import SimpleXServiceProtocol
import Foundation
let logger = Logger() let logger = Logger()
@@ -31,20 +36,22 @@ class NotificationService: UNNotificationServiceExtension {
logger.debug("NotificationService: app is in the background") logger.debug("NotificationService: app is in the background")
self.contentHandler = contentHandler self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let _ = startChat() { Task {
let content = receiveMessages() if let _ = await startChat() {
contentHandler (content) let content = await receiveMessages()
machMessenger.stop() contentHandler (content)
return machMessenger.stop()
} return
}
if let bestAttemptContent = bestAttemptContent { if let bestAttemptContent = bestAttemptContent {
// Modify the notification content here... // Modify the notification content here...
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
contentHandler(bestAttemptContent) contentHandler(bestAttemptContent)
}
machMessenger.stop()
} }
machMessenger.stop()
} }
override func serviceExtensionTimeWillExpire() { override func serviceExtensionTimeWillExpire() {
@@ -62,13 +69,13 @@ func receivedAppMachMessage(msgId: Int32, msg: String) -> String? {
return "reply from NSE to: \(msg)" return "reply from NSE to: \(msg)"
} }
func startChat() -> User? { func startChat() async -> User? {
hs_init(0, nil) // hs_init(0, nil)
if let user = apiGetActiveUser() { if let user = await apiGetActiveUser() {
logger.debug("active user \(String(describing: user))") logger.debug("active user \(String(describing: user))")
do { do {
try apiStartChat() try await apiStartChat()
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try await apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
return user return user
} catch { } catch {
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)") logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
@@ -79,10 +86,11 @@ func startChat() -> User? {
return nil return nil
} }
func receiveMessages() -> UNNotificationContent { func receiveMessages() async -> UNNotificationContent {
logger.debug("NotificationService receiveMessages started") logger.debug("NotificationService receiveMessages started")
while true { while true {
let res = chatResponse(chat_recv_msg(getChatCtrl())!) // let res = chatResponse(chat_recv_msg(getChatCtrl())!)
let res = await recvSimpleXMsg()
logger.debug("NotificationService receiveMessages: \(res.responseType)") logger.debug("NotificationService receiveMessages: \(res.responseType)")
switch res { switch res {
// case let .newContactConnection(connection): // case let .newContactConnection(connection):
@@ -117,9 +125,9 @@ func receiveMessages() -> UNNotificationContent {
} }
} }
func apiGetActiveUser() -> User? { func apiGetActiveUser() async -> User? {
let _ = getChatCtrl() // let _ = getChatCtrl()
let r = sendSimpleXCmd(.showActiveUser) let r = await sendSimpleXCmd(.showActiveUser)
logger.debug("apiGetActiveUser sendSimpleXCmd responce: \(String(describing: r))") logger.debug("apiGetActiveUser sendSimpleXCmd responce: \(String(describing: r))")
switch r { switch r {
case let .activeUser(user): return user case let .activeUser(user): return user
@@ -130,8 +138,8 @@ func apiGetActiveUser() -> User? {
} }
} }
func apiStartChat() throws { func apiStartChat() async throws {
let r = sendSimpleXCmd(.startChat) let r = await sendSimpleXCmd(.startChat)
switch r { switch r {
case .chatStarted: return case .chatStarted: return
case .chatRunning: return case .chatRunning: return
@@ -139,9 +147,8 @@ func apiStartChat() throws {
} }
} }
func apiSetFilesFolder(filesFolder: String) throws { func apiSetFilesFolder(filesFolder: String) async throws {
let r = sendSimpleXCmd(.setFilesFolder(filesFolder: filesFolder)) let r = await sendSimpleXCmd(.setFilesFolder(filesFolder: filesFolder))
if case .cmdOk = r { return } if case .cmdOk = r { return }
throw r throw r
} }
@@ -0,0 +1,58 @@
//
// FileProviderEnumerator.swift
// SimpleX Service
//
// Created by Evgeny on 01/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import FileProvider
import SimpleXServiceProtocol
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
var enumeratedItemIdentifier: NSFileProviderItemIdentifier
init(enumeratedItemIdentifier: NSFileProviderItemIdentifier) {
logger.debug("FileProviderExtension FileProviderEnumerator.init")
self.enumeratedItemIdentifier = enumeratedItemIdentifier
super.init()
}
func identifierForItemAtURL(_ url: URL, completionHandler: @escaping (NSFileProviderItemIdentifier) -> Void) {
// logger.debug("FileProviderExtension.identifierForItemAtURL")
completionHandler(SERVICE_PROXY_ITEM_ID)
}
func invalidate() {
// TODO: perform invalidation of server connection if necessary
}
func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
/* TODO:
- inspect the page to determine whether this is an initial or a follow-up request
If this is an enumerator for a directory, the root container or all directories:
- perform a server request to fetch directory contents
If this is an enumerator for the active set:
- perform a server request to update your local database
- fetch the active set from your local database
- inform the observer about the items returned by the server (possibly multiple times)
- inform the observer that you are finished with this page
*/
}
func enumerateChanges(for observer: NSFileProviderChangeObserver, from anchor: NSFileProviderSyncAnchor) {
/* TODO:
- query the server for updates since the passed-in sync anchor
If this is an enumerator for the active set:
- note the changes in your local database
- inform the observer about item deletions and updates (modifications + insertions)
- inform the observer when you have finished enumerating up to a subsequent sync anchor
*/
}
}
@@ -0,0 +1,237 @@
//
// FileProviderExtension.swift
// SimpleX Service
//
// Created by Evgeny on 01/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import FileProvider
import OSLog
import SimpleXChat
import SimpleXServiceProtocol
let logger = Logger()
//let serviceListener = NSXPCListener.anonymous()
//let listenerDelegate = SimpleXFPServiceDelegate()
//var machMessenger = MachMessenger(FPS_MACH_PORT, callback: receivedAppMachMessage)
func receivedAppMachMessage(_ msgId: Int32, msg: String) -> String? {
logger.debug("MachMessenger: FileProviderExtension receivedAppMachMessage \"\(msg)\" from App, replying")
return "reply from FPS to: \(msg)"
}
class FileProviderExtension: NSFileProviderExtension {
var fileManager = FileManager()
override init() {
logger.debug("FileProviderExtension.init")
super.init()
// machMessenger.start()
// serviceListener.delegate = listenerDelegate
// Task { serviceListener.resume() }
// do {
// logger.debug("FileProviderExtension.endPointData...")
// let data = NSMutableData()
// let coder = NSXPCCoder()
// coder.encodeRootObject(serviceListener.endpoint) // serviceListener.endpoint.encode(with: <#T##NSCoder#>)
// let endPointData = try NSKeyedArchiver.archivedData(withRootObject: serviceListener.endpoint, requiringSecureCoding: true)
// logger.debug("FileProviderExtension.endPointData ok")
// let err = machMessenger.sendMessage(APP_MACH_PORT, data: endPointData)
// logger.debug("FileProviderExtension.MachMessenger.sendMessage with endpoint res \(String(describing: err), privacy: .public)")
// let res = machMessenger.sendMessageWithReply(APP_MACH_PORT, msg: "machMessenger in FileProviderExtension")
// logger.debug("FileProviderExtension MachMessenger app reply \(String(describing: res), privacy: .public)")
// } catch let err {
// logger.debug("FileProviderExtension.MachMessenger.sendMessage error \(String(describing: err), privacy: .public)")
// }
let manager = NSFileProviderManager.default
logger.debug("FileProviderExtension.init NSFileProviderManager \(manager.documentStorageURL, privacy: .public)")
// FileManager.default.createFile(atPath: "\(manager.documentStorageURL)123", contents: "hello".data(using: .utf8))
self.providePlaceholder(at: SERVICE_PROXY_ITEM_URL) { err in
if let err = err {
logger.debug("FileProviderExtension.providePlaceholder error \(String(describing: err), privacy: .public)")
} else {
logger.debug("FileProviderExtension.providePlaceholder ok") // <-- this returns ok
// self.startProvidingItem(at: URL(string: "\(manager.documentStorageURL)123")!) { err in
// if let err = err {
// logger.debug("FileProviderExtension.startProvidingItem error \(String(describing: err), privacy: .public)")
// } else {
// logger.debug("FileProviderExtension.startProvidingItem ok")
// }
// }
}
}
// Task { serviceListener.resume() }
}
override func item(for identifier: NSFileProviderItemIdentifier) throws -> NSFileProviderItem {
logger.debug("FileProviderExtension.item")
// resolve the given identifier to a record in the model
// TODO: implement the actual lookup
return FileProviderItem()
}
override func urlForItem(withPersistentIdentifier identifier: NSFileProviderItemIdentifier) -> URL? {
logger.debug("FileProviderExtension.urlForItem")
// resolve the given identifier to a file on disk
guard let item = try? item(for: identifier) else {
return nil
}
// in this implementation, all paths are structured as <base storage directory>/<item identifier>/<item file name>
let manager = NSFileProviderManager.default
let perItemDirectory = manager.documentStorageURL.appendingPathComponent(identifier.rawValue, isDirectory: true)
logger.debug("FileProviderExtension.urlForItem NSFileProviderManager \(manager.documentStorageURL, privacy: .public)")
return perItemDirectory.appendingPathComponent(item.filename, isDirectory:false)
}
func identifierForItemAtURL(_ url: URL, completionHandler: @escaping (NSFileProviderItemIdentifier?) -> Void) {
completionHandler(SERVICE_PROXY_ITEM_ID)
}
override func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? {
logger.debug("FileProviderExtension.persistentIdentifierForItem")
// if url == SERVICE_PROXY_ITEM_URL { return SERVICE_PROXY_ITEM_ID }
return SERVICE_PROXY_ITEM_ID
// resolve the given URL to a persistent identifier using a database
let pathComponents = url.pathComponents
// exploit the fact that the path structure has been defined as
// <base storage directory>/<item identifier>/<item file name> above
assert(pathComponents.count > 2)
return NSFileProviderItemIdentifier(pathComponents[pathComponents.count - 2])
}
override func providePlaceholder(at url: URL, completionHandler: @escaping (Error?) -> Void) {
logger.debug("FileProviderExtension.providePlaceholder")
guard let identifier = persistentIdentifierForItem(at: url) else {
completionHandler(NSFileProviderError(.noSuchItem))
return
}
do {
let fileProviderItem = try item(for: identifier)
let placeholderURL = NSFileProviderManager.placeholderURL(for: url)
try NSFileProviderManager.writePlaceholder(at: placeholderURL, withMetadata: fileProviderItem)
completionHandler(nil)
} catch let error {
completionHandler(error)
}
}
override func startProvidingItem(at url: URL, completionHandler: @escaping ((_ error: Error?) -> Void)) {
logger.debug("FileProviderExtension.startProvidingItem")
completionHandler(nil)
// if url == SERVICE_PROXY_ITEM_URL {
// completionHandler(nil)
// return
// }
// Should ensure that the actual file is in the position returned by URLForItemWithIdentifier:, then call the completion handler
/* TODO:
This is one of the main entry points of the file provider. We need to check whether the file already exists on disk,
whether we know of a more recent version of the file, and implement a policy for these cases. Pseudocode:
if !fileOnDisk {
downloadRemoteFile()
callCompletion(downloadErrorOrNil)
} else if fileIsCurrent {
callCompletion(nil)
} else {
if localFileHasChanges {
// in this case, a version of the file is on disk, but we know of a more recent version
// we need to implement a strategy to resolve this conflict
moveLocalFileAside()
scheduleUploadOfLocalFile()
downloadRemoteFile()
callCompletion(downloadErrorOrNil)
} else {
downloadRemoteFile()
callCompletion(downloadErrorOrNil)
}
}
*/
completionHandler(NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:]))
}
override func itemChanged(at url: URL) {
logger.debug("FileProviderExtension.itemChanged")
// Called at some point after the file has changed; the provider may then trigger an upload
/* TODO:
- mark file at <url> as needing an update in the model
- if there are existing NSURLSessionTasks uploading this file, cancel them
- create a fresh background NSURLSessionTask and schedule it to upload the current modifications
- register the NSURLSessionTask with NSFileProviderManager to provide progress updates
*/
}
override func stopProvidingItem(at url: URL) {
logger.debug("FileProviderExtension.stopProvidingItem")
// Called after the last claim to the file has been released. At this point, it is safe for the file provider to remove the content file.
// Care should be taken that the corresponding placeholder file stays behind after the content file has been deleted.
// Called after the last claim to the file has been released. At this point, it is safe for the file provider to remove the content file.
// TODO: look up whether the file has local changes
let fileHasLocalChanges = false
if !fileHasLocalChanges {
// remove the existing file to free up space
do {
_ = try FileManager.default.removeItem(at: url)
} catch {
// Handle error
}
// write out a placeholder to facilitate future property lookups
self.providePlaceholder(at: url, completionHandler: { error in
// TODO: handle any error, do any necessary cleanup
})
}
}
// MARK: - Actions
/* TODO: implement the actions for items here
each of the actions follows the same pattern:
- make a note of the change in the local model
- schedule a server request as a background task to inform the server of the change
- call the completion block with the modified item in its post-modification state
*/
// MARK: - Enumeration
override func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
logger.debug("FileProviderExtension.enumerator")
let maybeEnumerator: NSFileProviderEnumerator? = nil
if (containerItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
// TODO: instantiate an enumerator for the container root
} else if (containerItemIdentifier == NSFileProviderItemIdentifier.workingSet) {
// TODO: instantiate an enumerator for the working set
} else {
// TODO: determine if the item is a directory or a file
// - for a directory, instantiate an enumerator of its subitems
// - for a file, instantiate an enumerator that observes changes to the file
}
guard let enumerator = maybeEnumerator else {
throw NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])
}
return enumerator
}
}
@@ -0,0 +1,33 @@
//
// FileProviderItem.swift
// SimpleX Service
//
// Created by Evgeny on 01/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import FileProvider
import UniformTypeIdentifiers
import SimpleXServiceProtocol
class FileProviderItem: NSObject, NSFileProviderItem {
// TODO: implement an initializer to create an item from your extension's backing model
// TODO: implement the accessors to return the values from your extension's backing model
var itemIdentifier: NSFileProviderItemIdentifier { SERVICE_PROXY_ITEM_ID }
var parentItemIdentifier: NSFileProviderItemIdentifier { NSFileProviderItemIdentifier("1") }
var capabilities: NSFileProviderItemCapabilities {
[.allowsReading, .allowsWriting, .allowsRenaming, .allowsReparenting, .allowsTrashing, .allowsDeleting]
}
var filename: String { SERVICE_PROXY_ITEM_NAME }
var contentType: UTType {
itemIdentifier == NSFileProviderItemIdentifier.rootContainer ? .folder : .plainText
}
var documentSize: NSNumber? { 1 }
}
@@ -0,0 +1,72 @@
//
// SimpleXFPService.swift
// SimpleX Service
//
// Created by Evgeny on 01/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import FileProvider
import SimpleXChat
import SimpleXServiceProtocol
extension FileProviderExtension {
class FileProviderService: NSObject, NSFileProviderServiceSource, SimpleXServiceProtocol, NSXPCListenerDelegate {
var serviceName: NSFileProviderServiceName { SIMPLEX_SERVICE_NAME }
func makeListenerEndpoint() throws -> NSXPCListenerEndpoint {
logger.debug("FileProviderService.makeListenerEndpoint")
let listener = NSXPCListener.anonymous()
listener.delegate = self
synchronized(self) {
listeners.add(listener)
}
listener.resume()
return listener.endpoint
}
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
logger.debug("FileProviderService.listener")
newConnection.exportedInterface = simpleXServiceInterface
newConnection.exportedObject = self
synchronized(self) {
listeners.remove(listener)
}
newConnection.resume()
return true
}
weak var ext: FileProviderExtension?
let listeners = NSHashTable<NSXPCListener>()
init(_ ext: FileProviderExtension) {
self.ext = ext
hs_init(0, nil)
}
func chatSendCmd(_ cmd: String) async -> String {
logger.debug("chatSendCmd cmd: \(cmd, privacy: .public)")
let r = SimpleXChat.chatSendCmd(cmd)
logger.debug("chatSendCmd resp: \(r, privacy: .public)")
return r
}
func chatRecvMsg() async -> String {
SimpleXChat.chatRecvMsg()
}
}
override func supportedServiceSources(for itemIdentifier: NSFileProviderItemIdentifier) throws -> [NSFileProviderServiceSource] {
logger.debug("FileProviderExtension.supportedServiceSources")
return [FileProviderService(self)]
}
}
private func synchronized<T>(_ lock: AnyObject, _ closure: () throws -> T) rethrows -> T {
objc_sync_enter(lock)
defer { objc_sync_exit(lock) }
return try closure()
}
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionFileProviderDocumentGroup</key>
<string>group.chat.simplex.app</string>
<key>NSExtensionFileProviderSupportsEnumeration</key>
<false/>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.fileprovider-nonui</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).FileProviderExtension</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.chat.simplex.app</string>
</array>
</dict>
</plist>
+4
View File
@@ -28,5 +28,9 @@
<string>remote-notification</string> <string>remote-notification</string>
<string>voip</string> <string>voip</string>
</array> </array>
<key>UIFileSharingEnabled</key>
<string>YES</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<string>YES</string>
</dict> </dict>
</plist> </plist>
File diff suppressed because it is too large Load Diff
@@ -17,6 +17,8 @@ public let NSE_MACH_PORT = "\(APP_GROUP_NAME).nse" as CFString
public let APP_MACH_PORT = "\(APP_GROUP_NAME).app" as CFString public let APP_MACH_PORT = "\(APP_GROUP_NAME).app" as CFString
public let FPS_MACH_PORT = "\(APP_GROUP_NAME).fps" as CFString
func getGroupDefaults() -> UserDefaults? { func getGroupDefaults() -> UserDefaults? {
UserDefaults(suiteName: APP_GROUP_NAME) UserDefaults(suiteName: APP_GROUP_NAME)
} }
@@ -55,6 +57,7 @@ public class MachMessenger {
case sndTimeout case sndTimeout
case rcvTimeout case rcvTimeout
case portInvalid case portInvalid
case portBecameInvalid
case sendError(Int32) case sendError(Int32)
case msgError case msgError
} }
@@ -75,7 +78,7 @@ public class MachMessenger {
case kCFMessagePortSendTimeout: return .sndTimeout case kCFMessagePortSendTimeout: return .sndTimeout
case kCFMessagePortReceiveTimeout: return .rcvTimeout case kCFMessagePortReceiveTimeout: return .rcvTimeout
case kCFMessagePortIsInvalid: return .portInvalid case kCFMessagePortIsInvalid: return .portInvalid
case kCFMessagePortBecameInvalidError: return .portInvalid case kCFMessagePortBecameInvalidError: return .portBecameInvalid
default: return .sendError(code) default: return .sendError(code)
} }
} }
@@ -104,6 +107,17 @@ public class MachMessenger {
} }
} }
public func sendMessage(_ remotePortName: CFString, msgId: Int32 = 0, data: Data) -> SendError? {
logger.debug("MachMessenger.sendMessage")
if let port = createRemotePort(remotePortName) {
logger.debug("MachMessenger.sendMessage: sending...")
return sendMessage(port, msgId: msgId, data: data)
} else {
logger.debug("MachMessenger.sendMessage: no remote port")
return .portInvalid
}
}
public func sendMessageWithReply(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> Result<String?, SendError> { public func sendMessageWithReply(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> Result<String?, SendError> {
logger.debug("MachMessenger.sendMessageWithReply") logger.debug("MachMessenger.sendMessageWithReply")
if let port = createRemotePort(remotePortName) { if let port = createRemotePort(remotePortName) {
@@ -149,14 +163,17 @@ public class MachMessenger {
private func sendMessage(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> SendError? { private func sendMessage(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> SendError? {
if let data = msg.data(using: .utf8) { if let data = msg.data(using: .utf8) {
logger.debug("MachMessenger sendMessage") logger.debug("MachMessenger sendMessage")
let msgData = data as CFData return sendMessage(remotePort, msgId: msgId, data: data)
let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, 0, nil, nil)
logger.debug("MachMessenger sendMessage \(code)")
return MachMessenger.sendError(code)
} }
return .msgError return .msgError
} }
private func sendMessage(_ remotePort: CFMessagePort, msgId: Int32 = 0, data: Data) -> SendError? {
let code = CFMessagePortSendRequest(remotePort, msgId, data as CFData, MACH_SEND_TIMEOUT, 0, nil, nil)
logger.debug("MachMessenger sendMessage \(code)")
return MachMessenger.sendError(code)
}
private func sendMessageWithReply(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> Result<String?, SendError> { private func sendMessageWithReply(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> Result<String?, SendError> {
if let data = msg.data(using: .utf8) { if let data = msg.data(using: .utf8) {
let msgData = data as CFData let msgData = data as CFData
@@ -9,6 +9,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import OSLog import OSLog
import SimpleXChatSDK
let logger = Logger() let logger = Logger()
@@ -17,7 +18,7 @@ public let maxImageSize: Int64 = 236700
public let maxFileSize: Int64 = 8000000 public let maxFileSize: Int64 = 8000000
func getDocumentsDirectory() -> URL { public func getDocumentsDirectory() -> URL {
// FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! // FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)! FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)!
} }
@@ -9,6 +9,7 @@
import Foundation import Foundation
import UserNotifications import UserNotifications
import SwiftUI import SwiftUI
import SimpleXChatSDK
public let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST" public let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST"
public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED" public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED"
@@ -0,0 +1,62 @@
//
// ServiceAPI.swift
// SimpleXAppShared
//
// Created by Evgeny on 09/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import SimpleXServiceProtocol
import SimpleXChatSDK
private var serviceProxy: SimpleXServiceProtocol?
public func sendSimpleXCmd(_ cmd: ChatCommand) async -> ChatResponse {
let proxy = await getServiceProxy()
let resp = await proxy.chatSendCmd(cmd.cmdString)
return chatResponse(resp)
}
public func recvSimpleXMsg() async -> ChatResponse {
let proxy = await getServiceProxy()
let msg = await proxy.chatRecvMsg()
return chatResponse(msg)
}
public func getServiceProxy() async -> SimpleXServiceProtocol {
if let proxy = serviceProxy { return proxy }
var err: Error?
for i in 1...20 {
do {
let proxy = try await _getServiceProxy()
serviceProxy = proxy
logger.debug("getServiceProxy \(i): success")
return proxy
} catch let error {
err = error
logger.debug("getServiceProxy \(i): \(String(describing: error), privacy: .public)")
try! await Task.sleep(nanoseconds: 250_000)
}
}
fatalError("getServiceProxy: error \(String(describing: err))")
}
private func _getServiceProxy() async throws -> SimpleXServiceProtocol {
let services = try await FileManager.default.fileProviderServicesForItem(at: SERVICE_PROXY_ITEM_URL)
guard let service = services[SIMPLEX_SERVICE_NAME] else { throw ServiceError.noService }
let connection = try await service.fileProviderConnection()
connection.remoteObjectInterface = simpleXServiceInterface
connection.resume()
var err: ServiceError?
let rawProxy = connection.remoteObjectProxyWithErrorHandler { error in err = .noProxy(error) }
if let err = err { throw ServiceError.noProxy(err) }
guard let proxy = rawProxy as? SimpleXServiceProtocol else { throw ServiceError.badProxy }
return proxy
}
enum ServiceError: Error {
case noService
case noProxy(Error)
case badProxy
}
@@ -0,0 +1,13 @@
# ``SimpleXAppShared``
<!--@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,19 @@
//
// SimpleXAppShared.h
// SimpleXAppShared
//
// Created by Evgeny on 09/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for SimpleXAppShared.
FOUNDATION_EXPORT double SimpleXAppSharedVersionNumber;
//! Project version string for SimpleXAppShared.
FOUNDATION_EXPORT const unsigned char SimpleXAppSharedVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SimpleXAppShared/PublicHeader.h>
+58 -42
View File
@@ -7,60 +7,76 @@
// //
import Foundation import Foundation
import OSLog
let logger = Logger()
private var chatController: chat_ctrl? private var chatController: chat_ctrl?
public func getChatCtrl() -> chat_ctrl { private func getChatCtrl() -> chat_ctrl {
if let controller = chatController { return controller } if let controller = chatController { return controller }
let dataDir = getDocumentsDirectory().path + "/mobile_v1" let dbFilePrefix = getDocumentsDirectory().path + "/mobile_v1"
logger.debug("documents directory \(dataDir)") logger.debug("getChatCtrl: dbFilePrefix \(dbFilePrefix)")
var cstr = dataDir.cString(using: .utf8)! var cstr = dbFilePrefix.cString(using: .utf8)!
chatController = chat_init(&cstr) chatController = chat_init(&cstr)
logger.debug("getChatCtrl: chat_init") logger.debug("getChatCtrl: chat_init")
return chatController! return chatController!
} }
public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse { func getDocumentsDirectory() -> URL {
var c = cmd.cmdString.cString(using: .utf8)! FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
return chatResponse(chat_send_cmd(getChatCtrl(), &c)) // FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)!
} }
public func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse { //public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
// var c = cmd.cmdString.cString(using: .utf8)!
// return chatResponse(chat_send_cmd(getChatCtrl(), &c))
//}
public func chatSendCmd(_ cmd: String) -> String {
var c = cmd.cString(using: .utf8)!
return rawChatResponse(chat_send_cmd(getChatCtrl(), &c))
}
public func chatRecvMsg() -> String {
rawChatResponse(chat_recv_msg(getChatCtrl()))
}
public func rawChatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> String {
let s = String.init(cString: cjson) let s = String.init(cString: cjson)
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))
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
do {
let r = try jsonDecoder.decode(APIResponse.self, from: d)
return r.resp
} catch {
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
}
var type: String?
var json: String?
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
if let j1 = j["resp"] as? NSDictionary, j1.count == 1 {
type = j1.allKeys[0] as? String
}
json = prettyJSON(j)
}
free(cjson) free(cjson)
return ChatResponse.response(type: type ?? "invalid", json: json ?? s) return s
} }
func prettyJSON(_ obj: NSDictionary) -> String? { //public func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse {
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) { // let s = String.init(cString: cjson)
return String(decoding: d, as: UTF8.self) // let d = s.data(using: .utf8)!
} //// TODO is there a way to do it without copying the data? e.g:
return nil //// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
} //// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
// do {
public func responseError(_ err: Error) -> String { // let r = try jsonDecoder.decode(APIResponse.self, from: d)
if let r = err as? ChatResponse { // return r.resp
return String(describing: r) // } catch {
} else { // logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
return err.localizedDescription // }
} //
} // var type: String?
// var json: String?
// if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
// if let j1 = j["resp"] as? NSDictionary, j1.count == 1 {
// type = j1.allKeys[0] as? String
// }
// json = prettyJSON(j)
// }
// free(cjson)
// return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
//}
//
//public func responseError(_ err: Error) -> String {
// if let r = err as? ChatResponse {
// return String(describing: r)
// } else {
// return err.localizedDescription
// }
//}
@@ -7,6 +7,9 @@
// //
import Foundation import Foundation
import OSLog
let logger = Logger()
let jsonDecoder = getJSONDecoder() let jsonDecoder = getJSONDecoder()
let jsonEncoder = getJSONEncoder() let jsonEncoder = getJSONEncoder()
@@ -520,3 +523,34 @@ public enum SMPAgentError: Decodable {
case A_VERSION case A_VERSION
case A_ENCRYPTION case A_ENCRYPTION
} }
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))
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
do {
let r = try jsonDecoder.decode(APIResponse.self, from: d)
return r.resp
} catch {
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
}
var type: String?
var json: String?
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
if let j1 = j["resp"] as? NSDictionary, j1.count == 1 {
type = j1.allKeys[0] as? String
}
json = prettyJSON(j)
}
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
}
public func responseError(_ err: Error) -> String {
if let r = err as? ChatResponse {
return String(describing: r)
} else {
return err.localizedDescription
}
}
@@ -700,7 +700,7 @@ public struct CIFile: Decodable {
CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus) CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus)
} }
var loaded: Bool { public var loaded: Bool {
get { get {
switch self.fileStatus { switch self.fileStatus {
case .sndStored: return true case .sndStored: return true
@@ -36,3 +36,10 @@ private func getDateFormatter(_ format: String) -> DateFormatter {
df.timeZone = TimeZone(secondsFromGMT: 0) df.timeZone = TimeZone(secondsFromGMT: 0)
return df return df
} }
func prettyJSON(_ obj: NSDictionary) -> String? {
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) {
return String(decoding: d, as: UTF8.self)
}
return nil
}
@@ -0,0 +1,13 @@
# ``SimpleXChatSDK``
<!--@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@-->
+19
View File
@@ -0,0 +1,19 @@
//
// SimpleXChatSDK.h
// SimpleXChatSDK
//
// Created by Evgeny on 09/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for SimpleXChatSDK.
FOUNDATION_EXPORT double SimpleXChatSDKVersionNumber;
//! Project version string for SimpleXChatSDK.
FOUNDATION_EXPORT const unsigned char SimpleXChatSDKVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SimpleXChatSDK/PublicHeader.h>
@@ -0,0 +1,27 @@
//
// ServiceProtocol.swift
// SimpleXServiceProtocol
//
// Created by Evgeny on 09/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import FileProvider
import OSLog
let logger = Logger()
public let SIMPLEX_SERVICE_NAME = NSFileProviderServiceName("group.chat.simplex.app.service")
public let SERVICE_PROXY_ITEM_NAME = "123"
public let SERVICE_PROXY_ITEM_ID = NSFileProviderItemIdentifier(SERVICE_PROXY_ITEM_NAME)
public let SERVICE_PROXY_ITEM_URL = URL(string: "\(NSFileProviderManager.default.documentStorageURL)\(SERVICE_PROXY_ITEM_NAME)")!
public let simpleXServiceInterface: NSXPCInterface = {
NSXPCInterface(with: SimpleXServiceProtocol.self)
}()
@objc public protocol SimpleXServiceProtocol {
func chatSendCmd(_ cmd: String) async -> String
func chatRecvMsg() async -> String
}
@@ -0,0 +1,13 @@
# ``SimpleXServiceProtocol``
<!--@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,19 @@
//
// SimpleXServiceProtocol.h
// SimpleXServiceProtocol
//
// Created by Evgeny on 01/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for SimpleXServiceProtocol.
FOUNDATION_EXPORT double SimpleXServiceProtocolVersionNumber;
//! Project version string for SimpleXServiceProtocol.
FOUNDATION_EXPORT const unsigned char SimpleXServiceProtocolVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SimpleXServiceProtocol/PublicHeader.h>