Compare commits
10 Commits
stable
...
_archived-
Author | SHA1 | Date | |
---|---|---|---|
|
88c57c82d4 | ||
|
e01be483da | ||
|
c7b5d73512 | ||
|
360553deeb | ||
|
b62f2acca7 | ||
|
af3dcc4a9a | ||
|
b0a81252c9 | ||
|
e15d4ac6b6 | ||
|
17b8101d88 | ||
|
1b972bc7cc |
@ -8,7 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
|
@ -70,8 +70,8 @@ class BGManager {
|
||||
return
|
||||
}
|
||||
self.completed = false
|
||||
DispatchQueue.main.async {
|
||||
initializeChat()
|
||||
Task {
|
||||
await initializeChat()
|
||||
if ChatModel.shared.currentUser == nil {
|
||||
completeReceiving("no current user")
|
||||
return
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
|
@ -9,7 +9,8 @@
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import UIKit
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT"
|
||||
let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL"
|
||||
|
@ -11,9 +11,8 @@ import UIKit
|
||||
import Dispatch
|
||||
import BackgroundTasks
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private var chatController: chat_ctrl?
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
enum TerminalItem: Identifiable {
|
||||
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 running = true
|
||||
let endTask = {
|
||||
@ -72,10 +71,10 @@ 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(bgDelay: Double? = nil, f: @escaping () async -> ChatResponse) async -> ChatResponse {
|
||||
let endTask = beginBGTask()
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask)
|
||||
let r = f()
|
||||
let r = await f()
|
||||
if let d = bgDelay {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + d, execute: endTask)
|
||||
} else {
|
||||
@ -84,10 +83,10 @@ private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> ChatResponse)
|
||||
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)")
|
||||
let resp = bgTask
|
||||
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd) }
|
||||
let resp = await bgTask
|
||||
? withBGTask(bgDelay: bgDelay) { await sendSimpleXCmd(cmd) }
|
||||
: sendSimpleXCmd(cmd)
|
||||
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
||||
if case let .response(_, json) = resp {
|
||||
@ -102,25 +101,14 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
|
||||
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 {
|
||||
await withCheckedContinuation { cont in
|
||||
_ = withBGTask(bgDelay: msgDelay) {
|
||||
let resp = chatResponse(chat_recv_msg(getChatCtrl())!)
|
||||
cont.resume(returning: resp)
|
||||
return resp
|
||||
}
|
||||
await withBGTask(bgDelay: msgDelay) {
|
||||
await recvSimpleXMsg()
|
||||
}
|
||||
}
|
||||
|
||||
func apiGetActiveUser() throws -> User? {
|
||||
let _ = getChatCtrl()
|
||||
let r = chatSendCmdSync(.showActiveUser)
|
||||
func apiGetActiveUser() async throws -> User? {
|
||||
let r = await chatSendCmd(.showActiveUser)
|
||||
switch r {
|
||||
case let .activeUser(user): return user
|
||||
case .chatCmdError(.error(.noActiveUser)): return nil
|
||||
@ -128,14 +116,14 @@ func apiGetActiveUser() throws -> User? {
|
||||
}
|
||||
}
|
||||
|
||||
func apiCreateActiveUser(_ p: Profile) throws -> User {
|
||||
let r = chatSendCmdSync(.createActiveUser(profile: p))
|
||||
func apiCreateActiveUser(_ p: Profile) async throws -> User {
|
||||
let r = await chatSendCmd(.createActiveUser(profile: p))
|
||||
if case let .activeUser(user) = r { return user }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiStartChat() throws -> Bool {
|
||||
let r = chatSendCmdSync(.startChat)
|
||||
func apiStartChat() async throws -> Bool {
|
||||
let r = await chatSendCmd(.startChat)
|
||||
switch r {
|
||||
case .chatStarted: return true
|
||||
case .chatRunning: return false
|
||||
@ -143,20 +131,20 @@ func apiStartChat() throws -> Bool {
|
||||
}
|
||||
}
|
||||
|
||||
func apiSetFilesFolder(filesFolder: String) throws {
|
||||
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder))
|
||||
func apiSetFilesFolder(filesFolder: String) async throws {
|
||||
let r = await chatSendCmd(.setFilesFolder(filesFolder: filesFolder))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetChats() throws -> [Chat] {
|
||||
let r = chatSendCmdSync(.apiGetChats)
|
||||
func apiGetChats() async throws -> [Chat] {
|
||||
let r = await chatSendCmd(.apiGetChats)
|
||||
if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetChat(type: ChatType, id: Int64) throws -> Chat {
|
||||
let r = chatSendCmdSync(.apiGetChat(type: type, id: id))
|
||||
func apiGetChat(type: ChatType, id: Int64) async throws -> Chat {
|
||||
let r = await chatSendCmd(.apiGetChat(type: type, id: id))
|
||||
if case let .apiChat(chat) = r { return Chat.init(chat) }
|
||||
throw r
|
||||
}
|
||||
@ -214,8 +202,8 @@ func apiDeleteToken(token: String) async throws {
|
||||
try await sendCommandOkResp(.apiDeleteToken(token: token))
|
||||
}
|
||||
|
||||
func getUserSMPServers() throws -> [String] {
|
||||
let r = chatSendCmdSync(.getUserSMPServers)
|
||||
func getUserSMPServers() async throws -> [String] {
|
||||
let r = await chatSendCmd(.getUserSMPServers)
|
||||
if case let .userSMPServers(smpServers) = r { return smpServers }
|
||||
throw r
|
||||
}
|
||||
@ -224,8 +212,8 @@ func setUserSMPServers(smpServers: [String]) async throws {
|
||||
try await sendCommandOkResp(.setUserSMPServers(smpServers: smpServers))
|
||||
}
|
||||
|
||||
func apiAddContact() throws -> String {
|
||||
let r = chatSendCmdSync(.addContact, bgTask: false)
|
||||
func apiAddContact() async throws -> String {
|
||||
let r = await chatSendCmd(.addContact, bgTask: false)
|
||||
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
||||
throw r
|
||||
}
|
||||
@ -312,8 +300,8 @@ func apiUpdateProfile(profile: Profile) async throws -> Profile? {
|
||||
}
|
||||
}
|
||||
|
||||
func apiParseMarkdown(text: String) throws -> [FormattedText]? {
|
||||
let r = chatSendCmdSync(.apiParseMarkdown(text: text))
|
||||
func apiParseMarkdown(text: String) async throws -> [FormattedText]? {
|
||||
let r = await sendSimpleXCmd(.apiParseMarkdown(text: text))
|
||||
if case let .apiParsedMarkdown(formattedText) = r { return formattedText }
|
||||
throw r
|
||||
}
|
||||
@ -330,8 +318,8 @@ func apiDeleteUserAddress() async throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetUserAddress() throws -> String? {
|
||||
let r = chatSendCmdSync(.showMyAddress)
|
||||
func apiGetUserAddress() async throws -> String? {
|
||||
let r = await sendSimpleXCmd(.showMyAddress)
|
||||
switch r {
|
||||
case let .userContactLink(connReq):
|
||||
return connReq
|
||||
@ -454,36 +442,45 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func initializeChat() {
|
||||
func initializeChat() async {
|
||||
logger.debug("initializeChat")
|
||||
do {
|
||||
let m = ChatModel.shared
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
if m.currentUser == nil {
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
} else {
|
||||
startChat()
|
||||
let user = try await apiGetActiveUser()
|
||||
await MainActor.run {
|
||||
m.currentUser = user
|
||||
if user == nil {
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
}
|
||||
}
|
||||
if user != nil {
|
||||
await startChat()
|
||||
}
|
||||
} catch {
|
||||
fatalError("Failed to initialize chat controller or database: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func startChat() {
|
||||
func startChat() async {
|
||||
logger.debug("startChat")
|
||||
do {
|
||||
let m = ChatModel.shared
|
||||
// TODO set file folder once, before chat is started
|
||||
let justStarted = try apiStartChat()
|
||||
let justStarted = try await 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
|
||||
try await apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
let userAddress = try await apiGetUserAddress()
|
||||
let userSMPServers = try await getUserSMPServers()
|
||||
let chats = try await apiGetChats()
|
||||
DispatchQueue.main.async {
|
||||
let m = ChatModel.shared
|
||||
m.userAddress = userAddress
|
||||
m.userSMPServers = userSMPServers
|
||||
m.chats = chats
|
||||
withAnimation {
|
||||
m.onboardingStage = m.chats.isEmpty
|
||||
? .step3_MakeConnection
|
||||
: .onboardingComplete
|
||||
}
|
||||
}
|
||||
}
|
||||
ChatReceiver.shared.start()
|
||||
@ -512,7 +509,7 @@ class ChatReceiver {
|
||||
func receiveMsgLoop() async {
|
||||
let msg = await chatRecvMsg()
|
||||
self._lastMsgTime = .now
|
||||
processReceivedMsg(msg)
|
||||
await processReceivedMsg(msg)
|
||||
if self.receiveMessages {
|
||||
do { try await Task.sleep(nanoseconds: 7_500_000) }
|
||||
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
|
||||
DispatchQueue.main.async {
|
||||
m.terminalItems.append(.resp(.now, res))
|
||||
|
@ -7,17 +7,10 @@
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SimpleXChat
|
||||
import SimpleXAppShared
|
||||
|
||||
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
|
||||
struct SimpleXApp: App {
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@ -30,11 +23,10 @@ struct SimpleXApp: App {
|
||||
@State private var enteredBackground: Double? = nil
|
||||
|
||||
init() {
|
||||
hs_init(0, nil)
|
||||
// hs_init(0, nil)
|
||||
UserDefaults.standard.register(defaults: appDefaults)
|
||||
BGManager.shared.register()
|
||||
NtfManager.shared.registerCategories()
|
||||
machMessenger.start()
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
@ -45,13 +37,13 @@ struct SimpleXApp: App {
|
||||
logger.debug("ContentView.onOpenURL: \(url)")
|
||||
chatModel.appOpenUrl = url
|
||||
}
|
||||
.onAppear() {
|
||||
initializeChat()
|
||||
.onAppear {
|
||||
Task { await initializeChat() }
|
||||
}
|
||||
.onChange(of: scenePhase) { phase in
|
||||
logger.debug("scenePhase \(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)")
|
||||
// let res = machMessenger.sendMessageWithReply(NSE_MACH_PORT, msg: "App scenePhase changed to \(String(describing: scenePhase))")
|
||||
// logger.debug("MachMessenger \(String(describing: res), privacy: .public)")
|
||||
setAppState(phase)
|
||||
switch (phase) {
|
||||
case .background:
|
||||
@ -60,10 +52,10 @@ struct SimpleXApp: App {
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
}
|
||||
doAuthenticate = false
|
||||
machMessenger.stop()
|
||||
// machMessenger.stop()
|
||||
case .active:
|
||||
doAuthenticate = authenticationExpired()
|
||||
machMessenger.start()
|
||||
// machMessenger.start()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ActiveCallView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
//import CallKit
|
||||
import AVFoundation
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
//class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
class CallController: NSObject, ObservableObject {
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
class CallManager {
|
||||
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID {
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct IncomingCallView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
class Call: ObservableObject, Equatable {
|
||||
static func == (lhs: Call, rhs: Call) -> Bool {
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
class WebRTCCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate {
|
||||
var rtcWebView: Binding<WKWebView?>
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
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,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct CICallItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
|
@ -7,7 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
struct CIFileView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
@ -7,7 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
struct CIImageView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
@ -7,7 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
struct CILinkView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct CIMetaView: View {
|
||||
var chatItem: ChatItem
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct DeletedItemView: View {
|
||||
var chatItem: ChatItem
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct EmojiItemView: View {
|
||||
var chatItem: ChatItem
|
||||
|
@ -7,7 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
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,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct IntegrityErrorItemView: View {
|
||||
var chatItem: ChatItem
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
|
||||
private let linkColor = Color(uiColor: uiLinkColor)
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatItemView: View {
|
||||
var chatInfo: ChatInfo
|
||||
|
@ -7,7 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
private let memberImageSize: CGFloat = 34
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXAppShared
|
||||
|
||||
struct ComposeImageView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
@ -8,7 +8,8 @@
|
||||
|
||||
import SwiftUI
|
||||
import LinkPresentation
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
|
||||
logger.debug("getLinkMetadata: fetching URL preview")
|
||||
|
@ -7,7 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
enum ComposePreview {
|
||||
case noPreview
|
||||
@ -164,7 +165,7 @@ struct ComposeView: View {
|
||||
.onChange(of: composeState.message) { _ in
|
||||
if composeState.linkPreviewAllowed() {
|
||||
if composeState.message.count > 0 {
|
||||
showLinkPreview(composeState.message)
|
||||
Task { await showLinkPreview(composeState.message) }
|
||||
} else {
|
||||
resetLinkPreview()
|
||||
}
|
||||
@ -306,7 +307,7 @@ struct ComposeView: View {
|
||||
case .noPreview:
|
||||
mc = .text(composeState.message)
|
||||
case .linkPreview:
|
||||
mc = checkLinkPreview()
|
||||
mc = await checkLinkPreview()
|
||||
case let .imagePreview(imagePreview: image):
|
||||
if let uiImage = chosenImage,
|
||||
let savedFile = saveImage(uiImage) {
|
||||
@ -357,12 +358,12 @@ struct ComposeView: View {
|
||||
chosenFile = nil
|
||||
}
|
||||
|
||||
private func updateMsgContent(_ msgContent: MsgContent) -> MsgContent {
|
||||
private func updateMsgContent(_ msgContent: MsgContent) async -> MsgContent {
|
||||
switch msgContent {
|
||||
case .text:
|
||||
return checkLinkPreview()
|
||||
return await checkLinkPreview()
|
||||
case .link:
|
||||
return checkLinkPreview()
|
||||
return await checkLinkPreview()
|
||||
case .image(_, let image):
|
||||
return .image(text: composeState.message, image: image)
|
||||
case .file:
|
||||
@ -372,9 +373,9 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func showLinkPreview(_ s: String) {
|
||||
private func showLinkPreview(_ s: String) async {
|
||||
prevLinkUrl = linkUrl
|
||||
linkUrl = parseMessage(s)
|
||||
linkUrl = await parseMessage(s)
|
||||
if let url = linkUrl {
|
||||
if url != composeState.linkPreview()?.uri && url != pendingLinkUrl {
|
||||
pendingLinkUrl = url
|
||||
@ -391,9 +392,9 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func parseMessage(_ msg: String) -> URL? {
|
||||
private func parseMessage(_ msg: String) async -> URL? {
|
||||
do {
|
||||
let parsedMsg = try apiParseMarkdown(text: msg)
|
||||
let parsedMsg = try await apiParseMarkdown(text: msg)
|
||||
let uri = parsedMsg?.first(where: { ft in
|
||||
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
|
||||
})
|
||||
@ -437,10 +438,10 @@ struct ComposeView: View {
|
||||
cancelledLinks = []
|
||||
}
|
||||
|
||||
private func checkLinkPreview() -> MsgContent {
|
||||
private func checkLinkPreview() async -> MsgContent {
|
||||
switch (composeState.preview) {
|
||||
case let .linkPreview(linkPreview: linkPreview):
|
||||
if let url = parseMessage(composeState.message),
|
||||
if let url = await parseMessage(composeState.message),
|
||||
let linkPreview = linkPreview,
|
||||
url == linkPreview.uri {
|
||||
return .link(text: composeState.message, preview: linkPreview)
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ContextItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct SendMessageView: View {
|
||||
@Binding var composeState: ComposeState
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatListNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ -31,13 +31,17 @@ 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)")
|
||||
Task {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
let chat = try await apiGetChat(type: cInfo.chatType, id: cInfo.apiId)
|
||||
DispatchQueue.main.async {
|
||||
chatModel.updateChatInfo(chat.chatInfo)
|
||||
chatModel.chatItems = chat.chatItems
|
||||
}
|
||||
} catch {
|
||||
logger.error("ChatListNavLink.chatView apiGetChatItems error: \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatPreviewView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ContactConnectionView: View {
|
||||
var contactConnection: PendingContactConnection
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ContactRequestView: View {
|
||||
var contactRequest: UserContactRequest
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatInfoImage: View {
|
||||
@ObservedObject var chat: Chat
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXAppShared
|
||||
|
||||
struct ProfileImage: View {
|
||||
var imageStr: String? = nil
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
enum NewChatAction: Identifiable {
|
||||
case createLink
|
||||
@ -27,7 +27,7 @@ struct NewChatButton: View {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
}
|
||||
.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("Scan QR code") { actionSheet = .scanQRCode }
|
||||
}
|
||||
@ -40,9 +40,9 @@ struct NewChatButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
func addContactAction() {
|
||||
func addContactAction() async {
|
||||
do {
|
||||
connReq = try apiAddContact()
|
||||
connReq = try await apiAddContact()
|
||||
actionSheet = .createLink
|
||||
} catch {
|
||||
DispatchQueue.global().async {
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct CreateProfile: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@ -43,7 +43,7 @@ struct CreateProfile: View {
|
||||
.focused($focusFullName)
|
||||
.submitLabel(.go)
|
||||
.onSubmit {
|
||||
if canCreateProfile() { createProfile() }
|
||||
if canCreateProfile() { Task { await createProfile() } }
|
||||
else { focusFullName = true }
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ struct CreateProfile: View {
|
||||
|
||||
HStack {
|
||||
Button {
|
||||
createProfile()
|
||||
Task { await createProfile() }
|
||||
} label: {
|
||||
Text("Create")
|
||||
Image(systemName: "greaterthan")
|
||||
@ -87,17 +87,19 @@ struct CreateProfile: View {
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
func createProfile() {
|
||||
func createProfile() async {
|
||||
hideKeyboard()
|
||||
let profile = Profile(
|
||||
displayName: displayName,
|
||||
fullName: fullName
|
||||
)
|
||||
do {
|
||||
m.currentUser = try apiCreateActiveUser(profile)
|
||||
startChat()
|
||||
withAnimation { m.onboardingStage = .step3_MakeConnection }
|
||||
|
||||
let user = try await apiCreateActiveUser(profile)
|
||||
await MainActor.run { m.currentUser = user }
|
||||
await startChat()
|
||||
DispatchQueue.main.async {
|
||||
withAnimation { m.onboardingStage = .step3_MakeConnection }
|
||||
}
|
||||
} catch {
|
||||
fatalError("Failed to create user: \(error)")
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct MakeConnection: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@ -38,7 +38,7 @@ struct MakeConnection: View {
|
||||
icon: "link.badge.plus",
|
||||
title: "Create 1-time link / QR code",
|
||||
text: "It's secure to share - only one contact can use it."
|
||||
) { addContactAction() }
|
||||
) { Task { await addContactAction() } }
|
||||
|
||||
actionRow(
|
||||
icon: "doc.plaintext",
|
||||
@ -102,9 +102,9 @@ struct MakeConnection: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func addContactAction() {
|
||||
private func addContactAction() async {
|
||||
do {
|
||||
connReq = try apiAddContact()
|
||||
connReq = try await apiAddContact()
|
||||
actionSheet = .createLink
|
||||
} catch {
|
||||
DispatchQueue.global().async {
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
private let terminalFont = Font.custom("Menlo", size: 16)
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
|
||||
private let serversFont = Font.custom("Menlo", size: 14)
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
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")!
|
||||
|
||||
|
@ -7,7 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
struct UserProfile: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
|
@ -8,7 +8,12 @@
|
||||
|
||||
import UserNotifications
|
||||
import OSLog
|
||||
import SimpleXChat
|
||||
import FileProvider
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
import SimpleXServiceProtocol
|
||||
|
||||
import Foundation
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
@ -31,20 +36,22 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
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)
|
||||
machMessenger.stop()
|
||||
return
|
||||
}
|
||||
Task {
|
||||
if let _ = await startChat() {
|
||||
let content = await receiveMessages()
|
||||
contentHandler (content)
|
||||
machMessenger.stop()
|
||||
return
|
||||
}
|
||||
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
// Modify the notification content here...
|
||||
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
|
||||
|
||||
contentHandler(bestAttemptContent)
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
// Modify the notification content here...
|
||||
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
|
||||
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
machMessenger.stop()
|
||||
}
|
||||
machMessenger.stop()
|
||||
}
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
@ -62,13 +69,13 @@ func receivedAppMachMessage(msgId: Int32, msg: String) -> String? {
|
||||
return "reply from NSE to: \(msg)"
|
||||
}
|
||||
|
||||
func startChat() -> User? {
|
||||
hs_init(0, nil)
|
||||
if let user = apiGetActiveUser() {
|
||||
func startChat() async -> User? {
|
||||
// hs_init(0, nil)
|
||||
if let user = await apiGetActiveUser() {
|
||||
logger.debug("active user \(String(describing: user))")
|
||||
do {
|
||||
try apiStartChat()
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try await apiStartChat()
|
||||
try await apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
return user
|
||||
} catch {
|
||||
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
|
||||
@ -79,10 +86,11 @@ func startChat() -> User? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func receiveMessages() -> UNNotificationContent {
|
||||
func receiveMessages() async -> UNNotificationContent {
|
||||
logger.debug("NotificationService receiveMessages started")
|
||||
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)")
|
||||
switch res {
|
||||
// case let .newContactConnection(connection):
|
||||
@ -117,9 +125,9 @@ func receiveMessages() -> UNNotificationContent {
|
||||
}
|
||||
}
|
||||
|
||||
func apiGetActiveUser() -> User? {
|
||||
let _ = getChatCtrl()
|
||||
let r = sendSimpleXCmd(.showActiveUser)
|
||||
func apiGetActiveUser() async -> User? {
|
||||
// let _ = getChatCtrl()
|
||||
let r = await sendSimpleXCmd(.showActiveUser)
|
||||
logger.debug("apiGetActiveUser sendSimpleXCmd responce: \(String(describing: r))")
|
||||
switch r {
|
||||
case let .activeUser(user): return user
|
||||
@ -130,8 +138,8 @@ func apiGetActiveUser() -> User? {
|
||||
}
|
||||
}
|
||||
|
||||
func apiStartChat() throws {
|
||||
let r = sendSimpleXCmd(.startChat)
|
||||
func apiStartChat() async throws {
|
||||
let r = await sendSimpleXCmd(.startChat)
|
||||
switch r {
|
||||
case .chatStarted: return
|
||||
case .chatRunning: return
|
||||
@ -139,9 +147,8 @@ func apiStartChat() throws {
|
||||
}
|
||||
}
|
||||
|
||||
func apiSetFilesFolder(filesFolder: String) throws {
|
||||
let r = sendSimpleXCmd(.setFilesFolder(filesFolder: filesFolder))
|
||||
func apiSetFilesFolder(filesFolder: String) async throws {
|
||||
let r = await sendSimpleXCmd(.setFilesFolder(filesFolder: filesFolder))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
|
58
apps/ios/SimpleX Service/FileProviderEnumerator.swift
Normal file
58
apps/ios/SimpleX Service/FileProviderEnumerator.swift
Normal file
@ -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
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
237
apps/ios/SimpleX Service/FileProviderExtension.swift
Normal file
237
apps/ios/SimpleX Service/FileProviderExtension.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
33
apps/ios/SimpleX Service/FileProviderItem.swift
Normal file
33
apps/ios/SimpleX Service/FileProviderItem.swift
Normal file
@ -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 }
|
||||
}
|
72
apps/ios/SimpleX Service/FileProviderService.swift
Normal file
72
apps/ios/SimpleX Service/FileProviderService.swift
Normal file
@ -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
apps/ios/SimpleX Service/Info.plist
Normal file
17
apps/ios/SimpleX Service/Info.plist
Normal 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>
|
10
apps/ios/SimpleX Service/SimpleX_Service.entitlements
Normal file
10
apps/ios/SimpleX Service/SimpleX_Service.entitlements
Normal file
@ -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>
|
@ -28,5 +28,9 @@
|
||||
<string>remote-notification</string>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<string>YES</string>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<string>YES</string>
|
||||
</dict>
|
||||
</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 FPS_MACH_PORT = "\(APP_GROUP_NAME).fps" as CFString
|
||||
|
||||
func getGroupDefaults() -> UserDefaults? {
|
||||
UserDefaults(suiteName: APP_GROUP_NAME)
|
||||
}
|
||||
@ -55,6 +57,7 @@ public class MachMessenger {
|
||||
case sndTimeout
|
||||
case rcvTimeout
|
||||
case portInvalid
|
||||
case portBecameInvalid
|
||||
case sendError(Int32)
|
||||
case msgError
|
||||
}
|
||||
@ -75,7 +78,7 @@ public class MachMessenger {
|
||||
case kCFMessagePortSendTimeout: return .sndTimeout
|
||||
case kCFMessagePortReceiveTimeout: return .rcvTimeout
|
||||
case kCFMessagePortIsInvalid: return .portInvalid
|
||||
case kCFMessagePortBecameInvalidError: return .portInvalid
|
||||
case kCFMessagePortBecameInvalidError: return .portBecameInvalid
|
||||
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> {
|
||||
logger.debug("MachMessenger.sendMessageWithReply")
|
||||
if let port = createRemotePort(remotePortName) {
|
||||
@ -149,14 +163,17 @@ public class MachMessenger {
|
||||
private func sendMessage(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> SendError? {
|
||||
if let data = msg.data(using: .utf8) {
|
||||
logger.debug("MachMessenger sendMessage")
|
||||
let msgData = data as CFData
|
||||
let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, 0, nil, nil)
|
||||
logger.debug("MachMessenger sendMessage \(code)")
|
||||
return MachMessenger.sendError(code)
|
||||
return sendMessage(remotePort, msgId: msgId, data: data)
|
||||
}
|
||||
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> {
|
||||
if let data = msg.data(using: .utf8) {
|
||||
let msgData = data as CFData
|
@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SimpleXChatSDK
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
@ -17,7 +18,7 @@ public let maxImageSize: Int64 = 236700
|
||||
|
||||
public let maxFileSize: Int64 = 8000000
|
||||
|
||||
func getDocumentsDirectory() -> URL {
|
||||
public func getDocumentsDirectory() -> URL {
|
||||
// FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)!
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
public let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST"
|
||||
public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED"
|
62
apps/ios/SimpleXAppShared/ServiceAPI.swift
Normal file
62
apps/ios/SimpleXAppShared/ServiceAPI.swift
Normal file
@ -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
|
||||
}
|
13
apps/ios/SimpleXAppShared/SimpleXAppShared.docc/SimpleXAppShared.md
Executable file
13
apps/ios/SimpleXAppShared/SimpleXAppShared.docc/SimpleXAppShared.md
Executable file
@ -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@-->
|
19
apps/ios/SimpleXAppShared/SimpleXAppShared.h
Normal file
19
apps/ios/SimpleXAppShared/SimpleXAppShared.h
Normal file
@ -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>
|
||||
|
||||
|
@ -7,60 +7,76 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
private var chatController: chat_ctrl?
|
||||
|
||||
public func getChatCtrl() -> chat_ctrl {
|
||||
private 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 dbFilePrefix = getDocumentsDirectory().path + "/mobile_v1"
|
||||
logger.debug("getChatCtrl: dbFilePrefix \(dbFilePrefix)")
|
||||
var cstr = dbFilePrefix.cString(using: .utf8)!
|
||||
chatController = chat_init(&cstr)
|
||||
logger.debug("getChatCtrl: chat_init")
|
||||
return chatController!
|
||||
}
|
||||
|
||||
public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
|
||||
var c = cmd.cmdString.cString(using: .utf8)!
|
||||
return chatResponse(chat_send_cmd(getChatCtrl(), &c))
|
||||
func getDocumentsDirectory() -> URL {
|
||||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
// 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 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)
|
||||
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
||||
return s
|
||||
}
|
||||
|
||||
func prettyJSON(_ obj: NSDictionary) -> String? {
|
||||
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) {
|
||||
return String(decoding: d, as: UTF8.self)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func responseError(_ err: Error) -> String {
|
||||
if let r = err as? ChatResponse {
|
||||
return String(describing: r)
|
||||
} else {
|
||||
return err.localizedDescription
|
||||
}
|
||||
}
|
||||
//public func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse {
|
||||
// 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)
|
||||
// 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 OSLog
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
let jsonDecoder = getJSONDecoder()
|
||||
let jsonEncoder = getJSONEncoder()
|
||||
@ -520,3 +523,34 @@ public enum SMPAgentError: Decodable {
|
||||
case A_VERSION
|
||||
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)
|
||||
}
|
||||
|
||||
var loaded: Bool {
|
||||
public var loaded: Bool {
|
||||
get {
|
||||
switch self.fileStatus {
|
||||
case .sndStored: return true
|
@ -36,3 +36,10 @@ private func getDateFormatter(_ format: String) -> DateFormatter {
|
||||
df.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
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
|
||||
}
|
13
apps/ios/SimpleXChatSDK/SimpleXChatSDK.docc/SimpleXChatSDK.md
Executable file
13
apps/ios/SimpleXChatSDK/SimpleXChatSDK.docc/SimpleXChatSDK.md
Executable file
@ -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
apps/ios/SimpleXChatSDK/SimpleXChatSDK.h
Normal file
19
apps/ios/SimpleXChatSDK/SimpleXChatSDK.h
Normal 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>
|
||||
|
||||
|
27
apps/ios/SimpleXServiceProtocol/ServiceProtocol.swift
Normal file
27
apps/ios/SimpleXServiceProtocol/ServiceProtocol.swift
Normal file
@ -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@-->
|
19
apps/ios/SimpleXServiceProtocol/SimpleXServiceProtocol.h
Normal file
19
apps/ios/SimpleXServiceProtocol/SimpleXServiceProtocol.h
Normal file
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user