Compare commits
44 Commits
v2.2.0
...
_archived-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88c57c82d4 | ||
|
|
e01be483da | ||
|
|
c7b5d73512 | ||
|
|
235fb8dc0c | ||
|
|
44a9ea2102 | ||
|
|
121541759b | ||
|
|
716a941dc6 | ||
|
|
16bd9ccc4f | ||
|
|
33e702d453 | ||
|
|
883bf768af | ||
|
|
f341e54128 | ||
|
|
99bd3f6133 | ||
|
|
7590502f29 | ||
|
|
f770a8396e | ||
|
|
b0f3d59cb0 | ||
|
|
cab5bc2daf | ||
|
|
935c3987b3 | ||
|
|
084d1d09a5 | ||
|
|
47ec486201 | ||
|
|
72103949a7 | ||
|
|
3b708105a4 | ||
|
|
800efb3a34 | ||
|
|
360553deeb | ||
|
|
b62f2acca7 | ||
|
|
af3dcc4a9a | ||
|
|
b0a81252c9 | ||
|
|
949fb17406 | ||
|
|
e15d4ac6b6 | ||
|
|
b435c0145f | ||
|
|
87c0c9de91 | ||
|
|
cd1af400bb | ||
|
|
17b8101d88 | ||
|
|
1b972bc7cc | ||
|
|
e1e161539d | ||
|
|
fa844c48e9 | ||
|
|
7e96da95f9 | ||
|
|
247e7f1ea7 | ||
|
|
3fe005e252 | ||
|
|
e057f9e407 | ||
|
|
cb529e3202 | ||
|
|
91a0283a36 | ||
|
|
18c3f49f96 | ||
|
|
1a653649ec | ||
|
|
43e560c901 |
68
README.md
68
README.md
@@ -24,7 +24,24 @@
|
||||
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
|
||||
- 🖥 Available as a terminal (console) app / CLI on Linux, MacOS, Windows.
|
||||
|
||||
## Why privacy of communications matter
|
||||
## Contents
|
||||
|
||||
- [Why privacy matters](#why-privacy-matters)
|
||||
- [SimpleX approach to privacy and security](#simplex-approach-to-privacy-and-security)
|
||||
- [Complete privacy](#complete-privacy-of-your-identity-profile-contacts-and-metadata)
|
||||
- [Protection against spam and abuse](#the-best-protection-against-spam-and-abuse)
|
||||
- [Ownership and security of your data](#complete-ownership-control-and-security-of-your-data)
|
||||
- [Users own SimpleX network](#users-own-simplex-network)
|
||||
- [Frequently asked questions](#frequently-asked-questions)
|
||||
- [News and updates](#news-and-updates)
|
||||
- [Make a private connection](#make-a-private-connection)
|
||||
- [Quick installation of a terminal app](#zap-quick-installation-of-a-terminal-app)
|
||||
- [SimpleX Platform design](#simplex-platform-design)
|
||||
- [For developers](#for-developers)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Disclaimer, License](#disclaimer)
|
||||
|
||||
## Why privacy matters
|
||||
|
||||
Everyone should care about privacy and security of their communications - innocuous conversations can put you in danger even if there is nothing to hide.
|
||||
|
||||
@@ -32,9 +49,9 @@ One of the most shocking stories is the experience of [Mohamedou Ould Salahi](ht
|
||||
|
||||
It is not enough to use an end-to-end encrypted messenger, we all should use the messengers that protect the privacy of our personal networks - who we are connected with.
|
||||
|
||||
## SimpleX unique approach to privacy and security
|
||||
## SimpleX approach to privacy and security
|
||||
|
||||
### Full privacy of your identity, profile, contacts and metadata
|
||||
### Complete privacy of your identity, profile, contacts and metadata
|
||||
|
||||
**Unlike any other existing messaging platform, SimpleX has no identifiers assigned to the users** - not even random numbers. This protects the privacy of who are you communicating with, hiding it from SimpleX platform servers and from any observers. [Read more](./docs/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata).
|
||||
|
||||
@@ -50,25 +67,24 @@ SimpleX stores all user data on client devices, the messages are only held tempo
|
||||
|
||||
You can use SimpleX with your own servers and still communicate with people using the servers that are pre-configured in the apps or any other SimpleX servers. [Read more](./docs/SIMPLEX.md#users-own-simplex-network).
|
||||
|
||||
## For developers
|
||||
## Frequently asked questions
|
||||
|
||||
We plan that the SimpleX platform will grow into the platform supporting any distributed Internet application. This will allow you to build any service that people can access via chat, with custom web-based UI widgets that anybody with basic HTML/CSS/JavaScript knowledge can create in a few hours.
|
||||
1. _How SimpleX can deliver messages without any user identifiers?_ See [v2 release annoucement](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) explaining how SimpleX works.
|
||||
|
||||
You already can:
|
||||
2. _Why should I not just use Signal?_ This [post](https://github.com/dessalines/essays/blob/master/why_not_signal.md) shows why Signal cannot be considered a private messenger. Signal is a centralised platform that uses phone numbers to identify its users and their contacts.
|
||||
|
||||
- use SimpleX Chat library to integrate chat functionality into your apps.
|
||||
- use SimpleX Chat bot templates in Haskell to build your own chat bot services (TypeScript SDK is coming soon).
|
||||
|
||||
If you are considering developing with SimpleX platform please get in touch for any advice and support.
|
||||
3. _How is it different from Matrix, Session, Ricochet, Cwtch, etc., that also don't require user identites?_ Although these platforms do not require a _real identity_, they do rely on anonymous user identities to deliver messages – it can be, for example, an identity key or a random number. Using a persistent user identity, even anonymous, creates a risk that user's connection graph becomes known to the observers and/or service providers, and it can lead to de-anonymizing some users. If the same user profile is used to connect to two different people via any messenger other than SimpleX, these two people can confirm if they are connected to the same person - they would use the same user identifier in the messages. With SimpleX there is no meta-data in common between your conversations with different contacts - the quality that no other messaging platform has.
|
||||
|
||||
## News and updates
|
||||
|
||||
[Jun 4, 2022. v2.2: the new Privacy and Security settings](./blog/20220604-simplex-chat-new-privacy-security-settings.md)
|
||||
|
||||
[May 11, 2022. v2.0 released - sending images and files in mobile apps](./blog/20220511-simplex-chat-v2-images-files.md)
|
||||
|
||||
[Apr 04, 2022. Instant notifications for SimpleX Chat mobile apps](./blog/20220404-simplex-chat-instant-notifications.md)
|
||||
|
||||
[Mar 08, 2022 Mobile apps for iOS and Android released](./blog/20220308-simplex-chat-mobile-apps.md)
|
||||
|
||||
[Jan 12, 2022. SimpleX v1 released: the only messaging and application platform without user identities](./20220112-simplex-chat-v1-released.md)
|
||||
|
||||
[All updates](./blog)
|
||||
|
||||
## Make a private connection
|
||||
@@ -103,6 +119,17 @@ Only the client devices have information about users, their contacts and groups.
|
||||
|
||||
See [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information on platform objectives and technical design.
|
||||
|
||||
## For developers
|
||||
|
||||
We plan that the SimpleX platform will grow into the platform supporting any distributed Internet application. This will allow you to build any service that people can access via chat, with custom web-based UI widgets that anybody with basic HTML/CSS/JavaScript knowledge can create in a few hours.
|
||||
|
||||
You already can:
|
||||
|
||||
- use SimpleX Chat library to integrate chat functionality into your apps.
|
||||
- use SimpleX Chat bot templates in Haskell to build your own chat bot services (TypeScript SDK is coming soon).
|
||||
|
||||
If you are considering developing with SimpleX platform please get in touch for any advice and support.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- ✅ Easy to deploy SimpleX server with in-memory message storage, without any dependencies.
|
||||
@@ -113,12 +140,13 @@ See [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/p
|
||||
- ✅ Private instant notifications for Android using background service.
|
||||
- ✅ Haskell chat bot templates.
|
||||
- ✅ v2.0 - supporting images and files in mobile apps.
|
||||
- 🏗 End-to-end encrypted audio and video calls via the mobile apps.
|
||||
- 🏗 Automatic chat history deletion.
|
||||
- ✅ Manual chat history deletion.
|
||||
- 🚀 End-to-end encrypted audio and video calls via the mobile apps (enable via Experimental Features).
|
||||
- 🏗 Privacy preserving instant notifications for iOS using Apple Push Notification service (in progress).
|
||||
- 🏗 Chat server and TypeScript client SDK to develop chat interfaces, integrations and chat bots (in progress).
|
||||
- 🏗 Chat database portability and encryption.
|
||||
- Groups support for mobile apps.
|
||||
- Chat database portability and encryption.
|
||||
- Disappearing messages, with mutual agreement.
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
- SMP protocol improvements:
|
||||
- SMP queue redundancy and rotation.
|
||||
@@ -139,3 +167,13 @@ You are likely to discover some bugs - we would really appreciate if you use it
|
||||
## License
|
||||
|
||||
[AGPL v3](./LICENSE)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084)
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=chat.simplex.app)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/f_droid.svg" alt="F-Droid" height="41">](https://app.simplex.chat)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/website/raw/master/simplex.apk)
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
@@ -21,17 +23,16 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
|
||||
let m = ChatModel.shared
|
||||
m.deviceToken = token
|
||||
UserDefaults.standard.set(false, forKey: DEFAULT_USE_NOTIFICATIONS)
|
||||
// let useNotifications = UserDefaults.standard.bool(forKey: "useNotifications")
|
||||
// if useNotifications {
|
||||
// Task {
|
||||
// do {
|
||||
// m.tokenStatus = try await apiRegisterToken(token: token)
|
||||
// } catch {
|
||||
// logger.error("apiRegisterToken error: \(responseError(error))")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
let useNotifications = UserDefaults.standard.bool(forKey: "useNotifications")
|
||||
if useNotifications {
|
||||
Task {
|
||||
do {
|
||||
m.tokenStatus = try await apiRegisterToken(token: token)
|
||||
} catch {
|
||||
logger.error("apiRegisterToken error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||
|
||||
@@ -12,21 +12,22 @@ struct ContentView: View {
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@ObservedObject var callController = CallController.shared
|
||||
@Binding var doAuthenticate: Bool
|
||||
@Binding var enteredBackground: Double?
|
||||
@State private var userAuthorized: Bool?
|
||||
@State private var laFailed: Bool = false
|
||||
@Binding var userAuthorized: Bool?
|
||||
@State private var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication
|
||||
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
|
||||
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if userAuthorized == true {
|
||||
if prefPerformLA && userAuthorized != true {
|
||||
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
|
||||
} else {
|
||||
if let step = chatModel.onboardingStage {
|
||||
if case .onboardingComplete = step,
|
||||
let user = chatModel.currentUser {
|
||||
chatModel.currentUser != nil {
|
||||
ZStack(alignment: .top) {
|
||||
ChatListView(user: user)
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.onAppear {
|
||||
NtfManager.shared.requestAuthorization(onDeny: {
|
||||
alertManager.showAlert(notificationAlert())
|
||||
@@ -47,54 +48,39 @@ struct ContentView: View {
|
||||
OnboardingView(onboarding: step)
|
||||
}
|
||||
}
|
||||
} else if prefPerformLA && laFailed {
|
||||
retryAuthView()
|
||||
}
|
||||
}
|
||||
.onChange(of: doAuthenticate) { doAuth in
|
||||
if doAuth, authenticationExpired() {
|
||||
runAuthenticate()
|
||||
}
|
||||
}
|
||||
.onAppear { if doAuthenticate { runAuthenticate() } }
|
||||
.onChange(of: doAuthenticate) { _ in if doAuthenticate { runAuthenticate() } }
|
||||
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
|
||||
}
|
||||
|
||||
private func retryAuthView() -> some View {
|
||||
Button {
|
||||
laFailed = false
|
||||
runAuthenticate()
|
||||
} label: { Label("Retry", systemImage: "arrow.counterclockwise") }
|
||||
}
|
||||
|
||||
private func runAuthenticate() {
|
||||
if !prefPerformLA {
|
||||
userAuthorized = true
|
||||
} else {
|
||||
chatModel.showChatInfo = false
|
||||
DispatchQueue.main.async() {
|
||||
userAuthorized = false
|
||||
authenticate(reason: NSLocalizedString("Unlock", comment: "authentication reason")) { laResult in
|
||||
switch (laResult) {
|
||||
case .success:
|
||||
userAuthorized = true
|
||||
case .failed:
|
||||
laFailed = true
|
||||
AlertManager.shared.showAlert(laFailedAlert())
|
||||
case .unavailable:
|
||||
userAuthorized = true
|
||||
prefPerformLA = false
|
||||
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
|
||||
}
|
||||
}
|
||||
} else if showChatInfo {
|
||||
showChatInfo = false
|
||||
DispatchQueue.main.async {
|
||||
justAuthenticate()
|
||||
}
|
||||
} else {
|
||||
justAuthenticate()
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticationExpired() -> Bool {
|
||||
if let enteredBackground = enteredBackground {
|
||||
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
|
||||
} else {
|
||||
return true
|
||||
private func justAuthenticate() {
|
||||
userAuthorized = false
|
||||
authenticate(reason: NSLocalizedString("Unlock", comment: "authentication reason")) { laResult in
|
||||
switch (laResult) {
|
||||
case .success:
|
||||
userAuthorized = true
|
||||
case .failed:
|
||||
AlertManager.shared.showAlert(laFailedAlert())
|
||||
case .unavailable:
|
||||
userAuthorized = true
|
||||
prefPerformLA = false
|
||||
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,11 +10,11 @@ import Foundation
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import SimpleXChatSDK
|
||||
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var currentUser: User?
|
||||
@Published var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication
|
||||
// list of chat "previews"
|
||||
@Published var chats: [Chat] = []
|
||||
// current chat
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import UIKit
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT"
|
||||
let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL"
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
//
|
||||
// API.swift
|
||||
// SimpleX NSE
|
||||
//
|
||||
// Created by Evgeny on 26/04/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private var chatController: chat_ctrl?
|
||||
|
||||
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)!
|
||||
chatController = chat_init(&cstr)
|
||||
logger.debug("getChatCtrl: chat_init")
|
||||
return chatController!
|
||||
}
|
||||
|
||||
func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
|
||||
var c = cmd.cmdString.cString(using: .utf8)!
|
||||
return chatResponse(chat_send_cmd(getChatCtrl(), &c))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func prettyJSON(_ obj: NSDictionary) -> String? {
|
||||
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) {
|
||||
return String(decoding: d, as: UTF8.self)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func responseError(_ err: Error) -> String {
|
||||
if let r = err as? ChatResponse {
|
||||
return String(describing: r)
|
||||
} else {
|
||||
return err.localizedDescription
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// CallTypes.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 05/05/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct WebRTCCallOffer: Encodable {
|
||||
var callType: CallType
|
||||
var rtcSession: WebRTCSession
|
||||
}
|
||||
|
||||
struct WebRTCSession: Codable {
|
||||
var rtcSession: String
|
||||
var rtcIceCandidates: String
|
||||
}
|
||||
|
||||
struct WebRTCExtraInfo: Codable {
|
||||
var rtcIceCandidates: String
|
||||
}
|
||||
|
||||
struct CallInvitation {
|
||||
var contact: Contact
|
||||
var callkitUUID: UUID?
|
||||
var peerMedia: CallMediaType
|
||||
var sharedKey: String?
|
||||
var callTs: Date
|
||||
var callTypeText: LocalizedStringKey {
|
||||
get {
|
||||
switch peerMedia {
|
||||
case .video: return sharedKey == nil ? "video call (not e2e encrypted)" : "**e2e encrypted** video call"
|
||||
case .audio: return sharedKey == nil ? "audio call (not e2e encrypted)" : "**e2e encrypted** audio call"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static let sampleData = CallInvitation(
|
||||
contact: Contact.sampleData,
|
||||
peerMedia: .audio,
|
||||
callTs: .now
|
||||
)
|
||||
}
|
||||
|
||||
struct CallType: Codable {
|
||||
var media: CallMediaType
|
||||
var capabilities: CallCapabilities
|
||||
}
|
||||
|
||||
enum CallMediaType: String, Codable, Equatable {
|
||||
case video = "video"
|
||||
case audio = "audio"
|
||||
}
|
||||
|
||||
enum VideoCamera: String, Codable, Equatable {
|
||||
case user = "user"
|
||||
case environment = "environment"
|
||||
}
|
||||
|
||||
struct CallCapabilities: Codable, Equatable {
|
||||
var encryption: Bool
|
||||
}
|
||||
|
||||
enum WebRTCCallStatus: String, Encodable {
|
||||
case connected = "connected"
|
||||
case connecting = "connecting"
|
||||
case disconnected = "disconnected"
|
||||
case failed = "failed"
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// GroupDefaults.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 26/04/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
func getGroupDefaults() -> UserDefaults? {
|
||||
UserDefaults(suiteName: "5NN7GUYB6T.group.chat.simplex.app")
|
||||
}
|
||||
|
||||
func setAppState(_ phase: ScenePhase) {
|
||||
if let defaults = getGroupDefaults() {
|
||||
defaults.set(phase == .background, forKey: "appInBackground")
|
||||
defaults.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
func getAppState() -> ScenePhase {
|
||||
if let defaults = getGroupDefaults() {
|
||||
if defaults.bool(forKey: "appInBackground") {
|
||||
return .background
|
||||
}
|
||||
}
|
||||
return .active
|
||||
}
|
||||
@@ -11,9 +11,8 @@ import UIKit
|
||||
import Dispatch
|
||||
import BackgroundTasks
|
||||
import SwiftUI
|
||||
import CallKit
|
||||
|
||||
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))
|
||||
@@ -680,7 +677,7 @@ func processReceivedMsg(_ res: ChatResponse) {
|
||||
}
|
||||
withCall(contact) { call in
|
||||
m.callCommand = .end
|
||||
CallController.shared.reportCallRemoteEnded(call: call)
|
||||
// CallController.shared.reportCallRemoteEnded(call: call)
|
||||
}
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SimpleXAppShared
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
@@ -17,12 +18,12 @@ struct SimpleXApp: App {
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@State private var userAuthorized: Bool? = nil
|
||||
@State private var doAuthenticate: Bool = false
|
||||
@State private var userAuthorized: Bool?
|
||||
@State private var doAuthenticate = false
|
||||
@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()
|
||||
@@ -30,29 +31,43 @@ struct SimpleXApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
return WindowGroup {
|
||||
ContentView(doAuthenticate: $doAuthenticate, enteredBackground: $enteredBackground)
|
||||
ContentView(doAuthenticate: $doAuthenticate, userAuthorized: $userAuthorized)
|
||||
.environmentObject(chatModel)
|
||||
.onOpenURL { url in
|
||||
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)")
|
||||
setAppState(phase)
|
||||
switch (phase) {
|
||||
case .background:
|
||||
BGManager.shared.schedule()
|
||||
if userAuthorized == true {
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
}
|
||||
doAuthenticate = false
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
// machMessenger.stop()
|
||||
case .active:
|
||||
doAuthenticate = true
|
||||
doAuthenticate = authenticationExpired()
|
||||
// machMessenger.start()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticationExpired() -> Bool {
|
||||
if let enteredBackground = enteredBackground {
|
||||
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ActiveCallView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@@ -93,9 +94,9 @@ struct ActiveCallView: View {
|
||||
case let .connection(state):
|
||||
if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState),
|
||||
case .connected = callStatus {
|
||||
if case .outgoing = call.direction {
|
||||
CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
|
||||
}
|
||||
// if case .outgoing = call.direction {
|
||||
// CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
|
||||
// }
|
||||
call.callState = .connected
|
||||
// CallKit doesn't work well with WKWebView
|
||||
// This is a hack to enable microphone in WKWebView after CallKit takes over it
|
||||
|
||||
@@ -7,90 +7,92 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CallKit
|
||||
//import CallKit
|
||||
import AVFoundation
|
||||
import SimpleXChatSDK
|
||||
|
||||
class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
//class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
class CallController: NSObject, ObservableObject {
|
||||
static let useCallKit = false
|
||||
static let shared = CallController()
|
||||
private let provider = CXProvider(configuration: CallController.configuration)
|
||||
private let controller = CXCallController()
|
||||
// private let provider = CXProvider(configuration: CallController.configuration)
|
||||
// private let controller = CXCallController()
|
||||
private let callManager = CallManager()
|
||||
@Published var activeCallInvitation: CallInvitation?
|
||||
|
||||
// PKPushRegistry will be used from notification service extension
|
||||
// let registry = PKPushRegistry(queue: nil)
|
||||
|
||||
static let configuration: CXProviderConfiguration = {
|
||||
let configuration = CXProviderConfiguration()
|
||||
configuration.supportsVideo = true
|
||||
configuration.supportedHandleTypes = [.generic]
|
||||
configuration.includesCallsInRecents = true // TODO disable or add option
|
||||
configuration.maximumCallsPerCallGroup = 1
|
||||
return configuration
|
||||
}()
|
||||
// static let configuration: CXProviderConfiguration = {
|
||||
// let configuration = CXProviderConfiguration()
|
||||
// configuration.supportsVideo = true
|
||||
// configuration.supportedHandleTypes = [.generic]
|
||||
// configuration.includesCallsInRecents = true // TODO disable or add option
|
||||
// configuration.maximumCallsPerCallGroup = 1
|
||||
// return configuration
|
||||
// }()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
self.provider.setDelegate(self, queue: nil)
|
||||
// self.provider.setDelegate(self, queue: nil)
|
||||
// self.registry.delegate = self
|
||||
// self.registry.desiredPushTypes = [.voIP]
|
||||
}
|
||||
|
||||
func providerDidReset(_ provider: CXProvider) {
|
||||
}
|
||||
// func providerDidReset(_ provider: CXProvider) {
|
||||
// }
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
logger.debug("CallController.provider CXStartCallAction")
|
||||
if callManager.startOutgoingCall(callUUID: action.callUUID) {
|
||||
action.fulfill()
|
||||
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
logger.debug("CallController.provider CXAnswerCallAction")
|
||||
if callManager.answerIncomingCall(callUUID: action.callUUID) {
|
||||
action.fulfill()
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
logger.debug("CallController.provider CXEndCallAction")
|
||||
callManager.endCall(callUUID: action.callUUID) { ok in
|
||||
if ok {
|
||||
action.fulfill()
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
|
||||
print("timed out", #function)
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
print("received", #function)
|
||||
// do {
|
||||
// try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .mixWithOthers)
|
||||
// logger.debug("audioSession category set")
|
||||
// try audioSession.setActive(true)
|
||||
// logger.debug("audioSession activated")
|
||||
// } catch {
|
||||
// print(error)
|
||||
// logger.error("failed activating audio session")
|
||||
// func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
// logger.debug("CallController.provider CXStartCallAction")
|
||||
// if callManager.startOutgoingCall(callUUID: action.callUUID) {
|
||||
// action.fulfill()
|
||||
// provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
|
||||
// } else {
|
||||
// action.fail()
|
||||
// }
|
||||
}
|
||||
// }
|
||||
|
||||
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
print("received", #function)
|
||||
}
|
||||
// func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
// logger.debug("CallController.provider CXAnswerCallAction")
|
||||
// if callManager.answerIncomingCall(callUUID: action.callUUID) {
|
||||
// action.fulfill()
|
||||
// } else {
|
||||
// action.fail()
|
||||
// }
|
||||
// }
|
||||
|
||||
// func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
// logger.debug("CallController.provider CXEndCallAction")
|
||||
// callManager.endCall(callUUID: action.callUUID) { ok in
|
||||
// if ok {
|
||||
// action.fulfill()
|
||||
// } else {
|
||||
// action.fail()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
|
||||
// print("timed out", #function)
|
||||
// action.fulfill()
|
||||
// }
|
||||
|
||||
// func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
// print("received", #function)
|
||||
//// do {
|
||||
//// try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .mixWithOthers)
|
||||
//// logger.debug("audioSession category set")
|
||||
//// try audioSession.setActive(true)
|
||||
//// logger.debug("audioSession activated")
|
||||
//// } catch {
|
||||
//// print(error)
|
||||
//// logger.error("failed activating audio session")
|
||||
//// }
|
||||
// }
|
||||
|
||||
// func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
// print("received", #function)
|
||||
// }
|
||||
|
||||
// func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
||||
//
|
||||
@@ -121,48 +123,49 @@ class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
func reportNewIncomingCall(invitation: CallInvitation, completion: @escaping (Error?) -> Void) {
|
||||
logger.debug("CallController.reportNewIncomingCall")
|
||||
if !UserDefaults.standard.bool(forKey: DEFAULT_EXPERIMENTAL_CALLS) { return }
|
||||
if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = CXHandle(type: .generic, value: invitation.contact.displayName)
|
||||
update.hasVideo = invitation.peerMedia == .video
|
||||
provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
|
||||
} else {
|
||||
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
// let update = CXCallUpdate()
|
||||
// update.remoteHandle = CXHandle(type: .generic, value: invitation.contact.displayName)
|
||||
// update.hasVideo = invitation.peerMedia == .video
|
||||
// provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
|
||||
// } else {
|
||||
NtfManager.shared.notifyCallInvitation(invitation)
|
||||
if invitation.callTs.timeIntervalSinceNow >= -180 {
|
||||
activeCallInvitation = invitation
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
if CallController.useCallKit, let uuid = call.callkitUUID {
|
||||
provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
|
||||
}
|
||||
}
|
||||
// func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
// if CallController.useCallKit, let uuid = call.callkitUUID {
|
||||
// provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
|
||||
// }
|
||||
// }
|
||||
|
||||
func reportCallRemoteEnded(invitation: CallInvitation) {
|
||||
if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
} else if invitation.contact.id == activeCallInvitation?.contact.id {
|
||||
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
// provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
// } else if invitation.contact.id == activeCallInvitation?.contact.id {
|
||||
activeCallInvitation = nil
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
func reportCallRemoteEnded(call: Call) {
|
||||
if CallController.useCallKit, let uuid = call.callkitUUID {
|
||||
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
}
|
||||
}
|
||||
// func reportCallRemoteEnded(call: Call) {
|
||||
// if CallController.useCallKit, let uuid = call.callkitUUID {
|
||||
// provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
// }
|
||||
// }
|
||||
|
||||
func startCall(_ contact: Contact, _ media: CallMediaType) {
|
||||
logger.debug("CallController.startCall")
|
||||
let uuid = callManager.newOutgoingCall(contact, media)
|
||||
if CallController.useCallKit {
|
||||
let handle = CXHandle(type: .generic, value: contact.displayName)
|
||||
let action = CXStartCallAction(call: uuid, handle: handle)
|
||||
action.isVideo = media == .video
|
||||
requestTransaction(with: action)
|
||||
} else if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
// if CallController.useCallKit {
|
||||
// let handle = CXHandle(type: .generic, value: contact.displayName)
|
||||
// let action = CXStartCallAction(call: uuid, handle: handle)
|
||||
// action.isVideo = media == .video
|
||||
// requestTransaction(with: action)
|
||||
// } else if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
logger.debug("CallController.startCall: call started")
|
||||
} else {
|
||||
logger.error("CallController.startCall: no active call")
|
||||
@@ -177,9 +180,9 @@ class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
}
|
||||
|
||||
func endCall(callUUID: UUID) {
|
||||
if CallController.useCallKit {
|
||||
requestTransaction(with: CXEndCallAction(call: callUUID))
|
||||
} else {
|
||||
// if CallController.useCallKit {
|
||||
// requestTransaction(with: CXEndCallAction(call: callUUID))
|
||||
// } else {
|
||||
callManager.endCall(callUUID: callUUID) { ok in
|
||||
if ok {
|
||||
logger.debug("CallController.endCall: call ended")
|
||||
@@ -187,7 +190,7 @@ class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
logger.error("CallController.endCall: no actove call pr call invitation to end")
|
||||
}
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
func endCall(invitation: CallInvitation) {
|
||||
@@ -204,15 +207,15 @@ class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
callManager.endCall(call: call, completed: completed)
|
||||
}
|
||||
|
||||
private func requestTransaction(with action: CXAction) {
|
||||
let t = CXTransaction()
|
||||
t.addAction(action)
|
||||
controller.request(t) { error in
|
||||
if let error = error {
|
||||
logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)")
|
||||
} else {
|
||||
logger.debug("CallController.requestTransaction requested transaction successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
// private func requestTransaction(with action: CXAction) {
|
||||
// let t = CXTransaction()
|
||||
// t.addAction(action)
|
||||
// controller.request(t) { error in
|
||||
// if let error = error {
|
||||
// logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)")
|
||||
// } else {
|
||||
// logger.debug("CallController.requestTransaction requested transaction successfully")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SimpleXChatSDK
|
||||
|
||||
class CallManager {
|
||||
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct IncomingCallView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
class Call: ObservableObject, Equatable {
|
||||
static func == (lhs: Call, rhs: Call) -> Bool {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import SimpleXChatSDK
|
||||
|
||||
class WebRTCCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate {
|
||||
var rtcWebView: Binding<WKWebView?>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
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,11 +7,13 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var showChatInfo: Bool
|
||||
@State var alert: ChatInfoViewAlert? = nil
|
||||
@State var deletingContact: Contact?
|
||||
|
||||
@@ -98,7 +100,7 @@ struct ChatInfoView: View {
|
||||
try await apiDeleteChat(type: .direct, id: contact.apiId)
|
||||
DispatchQueue.main.async {
|
||||
chatModel.removeChat(contact.id)
|
||||
chatModel.showChatInfo = false
|
||||
showChatInfo = false
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
|
||||
@@ -117,7 +119,7 @@ struct ChatInfoView: View {
|
||||
Task {
|
||||
await clearChat(chat)
|
||||
DispatchQueue.main.async {
|
||||
chatModel.showChatInfo = false
|
||||
showChatInfo = false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -129,6 +131,6 @@ struct ChatInfoView: View {
|
||||
struct ChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var showChatInfo = true
|
||||
return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
|
||||
return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct CICallItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
struct CIFileView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@@ -133,7 +135,7 @@ struct CIFileView: View {
|
||||
|
||||
struct CIFileView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let sentFile = ChatItem(
|
||||
let sentFile: ChatItem = ChatItem(
|
||||
chatDir: .directSnd,
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent, false, true, false),
|
||||
content: .sndMsgContent(msgContent: .file("")),
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
struct CIImageView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
struct CILinkView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct CIMetaView: View {
|
||||
var chatItem: ChatItem
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct DeletedItemView: View {
|
||||
var chatItem: ChatItem
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct EmojiItemView: View {
|
||||
var chatItem: ChatItem
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
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,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct IntegrityErrorItemView: View {
|
||||
var chatItem: ChatItem
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
|
||||
private let linkColor = Color(uiColor: uiLinkColor)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatItemView: View {
|
||||
var chatInfo: ChatInfo
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
private let memberImageSize: CGFloat = 34
|
||||
|
||||
@@ -15,6 +17,7 @@ struct ChatView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@AppStorage(DEFAULT_EXPERIMENTAL_CALLS) private var enableCalls = false
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var showChatInfo: Bool
|
||||
@State private var composeState = ComposeState()
|
||||
@State private var deletingItem: ChatItem? = nil
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@@ -97,12 +100,12 @@ struct ChatView: View {
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Button {
|
||||
chatModel.showChatInfo = true
|
||||
showChatInfo = true
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.sheet(isPresented: $chatModel.showChatInfo) {
|
||||
ChatInfoView(chat: chat)
|
||||
.sheet(isPresented: $showChatInfo) {
|
||||
ChatInfoView(chat: chat, showChatInfo: $showChatInfo)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@@ -269,7 +272,8 @@ struct ChatView_Previews: PreviewProvider {
|
||||
ChatItem.getSample(8, .directSnd, .now, "👍👍👍👍"),
|
||||
ChatItem.getSample(9, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
|
||||
]
|
||||
return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
|
||||
@State var showChatInfo = false
|
||||
return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo)
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXAppShared
|
||||
|
||||
struct ComposeImageView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
import SwiftUI
|
||||
import LinkPresentation
|
||||
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
|
||||
logger.debug("getLinkMetadata: fetching URL preview")
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
enum ComposePreview {
|
||||
case noPreview
|
||||
@@ -163,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()
|
||||
}
|
||||
@@ -305,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) {
|
||||
@@ -356,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:
|
||||
@@ -371,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
|
||||
@@ -390,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)
|
||||
})
|
||||
@@ -436,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,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ContextItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct SendMessageView: View {
|
||||
@Binding var composeState: ComposeState
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatListNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State var chat: Chat
|
||||
@Binding var showChatInfo: Bool
|
||||
@State private var showContactRequestDialog = false
|
||||
|
||||
var body: some View {
|
||||
@@ -27,15 +29,19 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
|
||||
private func chatView() -> some View {
|
||||
ChatView(chat: chat)
|
||||
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))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,19 +284,20 @@ struct ChatListNavLink: View {
|
||||
struct ChatListNavLink_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var chatId: String? = "@1"
|
||||
@State var showChatInfo = false
|
||||
return Group {
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
|
||||
))
|
||||
), showChatInfo: $showChatInfo)
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
|
||||
))
|
||||
), showChatInfo: $showChatInfo)
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.contactRequest,
|
||||
chatItems: []
|
||||
))
|
||||
), showChatInfo: $showChatInfo)
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 80))
|
||||
}
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Binding var showChatInfo: Bool
|
||||
// not really used in this view
|
||||
@State private var showSettings = false
|
||||
@State private var searchText = ""
|
||||
@AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true
|
||||
|
||||
var user: User
|
||||
|
||||
var body: some View {
|
||||
let v = NavigationView {
|
||||
List {
|
||||
ForEach(filteredChats()) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
ChatListNavLink(chat: chat, showChatInfo: $showChatInfo)
|
||||
.padding(.trailing, -16)
|
||||
}
|
||||
}
|
||||
@@ -92,10 +92,11 @@ struct ChatListView_Previews: PreviewProvider {
|
||||
)
|
||||
|
||||
]
|
||||
@State var showChatInfo = false
|
||||
return Group {
|
||||
ChatListView(user: User.sampleData)
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.environmentObject(chatModel)
|
||||
ChatListView(user: User.sampleData)
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.environmentObject(ChatModel())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatPreviewView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ContactConnectionView: View {
|
||||
var contactConnection: PendingContactConnection
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ContactRequestView: View {
|
||||
var contactRequest: UserContactRequest
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct ChatInfoImage: View {
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXAppShared
|
||||
|
||||
struct ProfileImage: View {
|
||||
var imageStr: String? = nil
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
enum NewChatAction: Identifiable {
|
||||
case createLink
|
||||
@@ -26,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 }
|
||||
}
|
||||
@@ -39,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,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct CreateProfile: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@@ -42,7 +43,7 @@ struct CreateProfile: View {
|
||||
.focused($focusFullName)
|
||||
.submitLabel(.go)
|
||||
.onSubmit {
|
||||
if canCreateProfile() { createProfile() }
|
||||
if canCreateProfile() { Task { await createProfile() } }
|
||||
else { focusFullName = true }
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ struct CreateProfile: View {
|
||||
|
||||
HStack {
|
||||
Button {
|
||||
createProfile()
|
||||
Task { await createProfile() }
|
||||
} label: {
|
||||
Text("Create")
|
||||
Image(systemName: "greaterthan")
|
||||
@@ -86,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,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
struct MakeConnection: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@@ -37,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",
|
||||
@@ -101,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,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
private let terminalFont = Font.custom("Menlo", size: 16)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
private let serversFont = Font.custom("Menlo", size: 14)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
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")!
|
||||
|
||||
@@ -150,12 +151,12 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
settingsRow("gauge") { Text("Experimental features") }
|
||||
}
|
||||
// if let token = chatModel.deviceToken {
|
||||
// HStack {
|
||||
// notificationsIcon()
|
||||
// notificationsToggle(token)
|
||||
// }
|
||||
// }
|
||||
if let token = chatModel.deviceToken {
|
||||
HStack {
|
||||
notificationsIcon()
|
||||
notificationsToggle(token)
|
||||
}
|
||||
}
|
||||
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
|
||||
struct UserProfile: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// dummy.m
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 22/01/2022.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#if defined(__x86_64__) && TARGET_IPHONE_SIMULATOR
|
||||
|
||||
#import <dirent.h>
|
||||
|
||||
int readdir_r$INODE64(DIR *restrict dirp, struct dirent *restrict entry,
|
||||
struct dirent **restrict result) {
|
||||
return readdir_r(dirp, entry, result);
|
||||
}
|
||||
|
||||
DIR *opendir$INODE64(const char *name) {
|
||||
return opendir(name);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -8,34 +8,49 @@
|
||||
|
||||
import UserNotifications
|
||||
import OSLog
|
||||
import FileProvider
|
||||
import SimpleXChatSDK
|
||||
import SimpleXAppShared
|
||||
import SimpleXServiceProtocol
|
||||
|
||||
import Foundation
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
let machMessenger = MachMessenger(NSE_MACH_PORT, callback: receivedAppMachMessage)
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
logger.debug("NotificationService.didReceive")
|
||||
machMessenger.start()
|
||||
let res = machMessenger.sendMessageWithReply(APP_MACH_PORT, msg: "starting NSE didReceive")
|
||||
logger.debug("MachMessenger \(String(describing: res), privacy: .public)")
|
||||
if getAppState() != .background {
|
||||
contentHandler(request.content)
|
||||
machMessenger.stop()
|
||||
return
|
||||
}
|
||||
logger.debug("NotificationService: app is in the background")
|
||||
self.contentHandler = contentHandler
|
||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
if let _ = startChat() {
|
||||
let content = receiveMessages()
|
||||
contentHandler (content)
|
||||
return
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,19 +62,23 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func startChat() -> User? {
|
||||
hs_init(0, nil)
|
||||
if let user = apiGetActiveUser() {
|
||||
func receivedAppMachMessage(msgId: Int32, msg: String) -> String? {
|
||||
logger.debug("MachMessenger: receivedAppMachMessage \"\(msg)\" from App, replying")
|
||||
return "reply from NSE to: \(msg)"
|
||||
}
|
||||
|
||||
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))")
|
||||
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
|
||||
}
|
||||
} else {
|
||||
logger.debug("no active user")
|
||||
@@ -67,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):
|
||||
@@ -105,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
|
||||
@@ -118,15 +138,17 @@ func apiGetActiveUser() -> User? {
|
||||
}
|
||||
}
|
||||
|
||||
func apiStartChat() throws {
|
||||
let r = sendSimpleXCmd(.startChat)
|
||||
if case .chatStarted = r { return }
|
||||
throw r
|
||||
func apiStartChat() async throws {
|
||||
let r = await sendSimpleXCmd(.startChat)
|
||||
switch r {
|
||||
case .chatStarted: return
|
||||
case .chatRunning: return
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
extern void hs_init(int argc, char **argv[]);
|
||||
|
||||
typedef void* chat_ctrl;
|
||||
|
||||
extern chat_ctrl chat_init(char *path);
|
||||
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctl);
|
||||
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>
|
||||
@@ -23,8 +23,14 @@
|
||||
<false/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<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
194
apps/ios/SimpleXAppShared/AppGroup.swift
Normal file
194
apps/ios/SimpleXAppShared/AppGroup.swift
Normal file
@@ -0,0 +1,194 @@
|
||||
//
|
||||
// GroupDefaults.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 26/04/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
let GROUP_DEFAULT_APP_IN_BACKGROUND = "appInBackground"
|
||||
|
||||
let APP_GROUP_NAME = "group.chat.simplex.app"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
public func setAppState(_ phase: ScenePhase) {
|
||||
if let defaults = getGroupDefaults() {
|
||||
defaults.set(phase == .background, forKey: GROUP_DEFAULT_APP_IN_BACKGROUND)
|
||||
defaults.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
public func getAppState() -> ScenePhase {
|
||||
if let defaults = getGroupDefaults() {
|
||||
if defaults.bool(forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) {
|
||||
return .background
|
||||
}
|
||||
}
|
||||
return .active
|
||||
}
|
||||
|
||||
let MACH_SEND_TIMEOUT: CFTimeInterval = 1.0
|
||||
let MACH_REPLY_TIMEOUT: CFTimeInterval = 1.0
|
||||
|
||||
public class MachMessenger {
|
||||
public init(_ localPortName: CFString, callback: @escaping Callback) {
|
||||
self.localPortName = localPortName
|
||||
self.callback = callback
|
||||
self.localPort = nil
|
||||
}
|
||||
|
||||
var localPortName: CFString
|
||||
var callback: Callback
|
||||
var localPort: CFMessagePort?
|
||||
|
||||
public enum SendError: Error {
|
||||
case sndTimeout
|
||||
case rcvTimeout
|
||||
case portInvalid
|
||||
case portBecameInvalid
|
||||
case sendError(Int32)
|
||||
case msgError
|
||||
}
|
||||
|
||||
public typealias Callback = (_ msgId: Int32, _ msg: String) -> String?
|
||||
|
||||
class CallbackInfo {
|
||||
internal init(callback: @escaping MachMessenger.Callback) {
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
var callback: Callback
|
||||
}
|
||||
|
||||
public static func sendError(_ code: Int32) -> SendError? {
|
||||
switch code {
|
||||
case kCFMessagePortSuccess: return nil
|
||||
case kCFMessagePortSendTimeout: return .sndTimeout
|
||||
case kCFMessagePortReceiveTimeout: return .rcvTimeout
|
||||
case kCFMessagePortIsInvalid: return .portInvalid
|
||||
case kCFMessagePortBecameInvalidError: return .portBecameInvalid
|
||||
default: return .sendError(code)
|
||||
}
|
||||
}
|
||||
|
||||
public func start() {
|
||||
logger.debug("MachMessenger.start")
|
||||
localPort = createLocalPort(localPortName, callback: callback)
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
if let port = localPort {
|
||||
logger.debug("MachMessenger.stop")
|
||||
CFMessagePortInvalidate(port)
|
||||
localPort = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func sendMessage(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> SendError? {
|
||||
logger.debug("MachMessenger.sendMessage")
|
||||
if let port = createRemotePort(remotePortName) {
|
||||
logger.debug("MachMessenger.sendMessage: sending...")
|
||||
return sendMessage(port, msgId: msgId, msg: msg)
|
||||
} else {
|
||||
logger.debug("MachMessenger.sendMessage: no remote port")
|
||||
return .portInvalid
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
logger.debug("MachMessenger.sendMessageWithReply: sending...")
|
||||
return sendMessageWithReply(port, msgId: msgId, msg: msg)
|
||||
} else {
|
||||
logger.debug("MachMessenger.sendMessageWithReply: no remote port")
|
||||
return .failure(.portInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
private func createLocalPort(_ portName: CFString, callback: @escaping Callback) -> CFMessagePort? {
|
||||
logger.debug("MachMessenger.createLocalPort")
|
||||
if let port = localPort { return port }
|
||||
logger.debug("MachMessenger.createLocalPort: creating...")
|
||||
var context = CFMessagePortContext()
|
||||
context.version = 0
|
||||
context.info = Unmanaged.passRetained(CallbackInfo(callback: callback)).toOpaque()
|
||||
let callout: CFMessagePortCallBack = { port, msgId, msgData, info in
|
||||
logger.debug("MachMessenger CFMessagePortCallBack called")
|
||||
if let data = msgData,
|
||||
let msg = String(data: data as Data, encoding: .utf8),
|
||||
let info = info,
|
||||
let resp = Unmanaged<CallbackInfo>.fromOpaque(info).takeUnretainedValue().callback(msgId, msg),
|
||||
let respData = resp.data(using: .utf8) {
|
||||
return Unmanaged.passRetained(respData as CFData)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return withUnsafeMutablePointer(to: &context) { cxt in
|
||||
let port = CFMessagePortCreateLocal(kCFAllocatorDefault, portName, callout, cxt, nil)
|
||||
CFMessagePortSetDispatchQueue(port, DispatchQueue.main);
|
||||
localPort = port
|
||||
logger.debug("MachMessenger.createLocalPort created: \(portName)")
|
||||
return port
|
||||
}
|
||||
}
|
||||
|
||||
private func createRemotePort(_ portName: CFString) -> CFMessagePort? {
|
||||
CFMessagePortCreateRemote(kCFAllocatorDefault, portName)
|
||||
}
|
||||
|
||||
private func sendMessage(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> SendError? {
|
||||
if let data = msg.data(using: .utf8) {
|
||||
logger.debug("MachMessenger sendMessage")
|
||||
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
|
||||
var respData: Unmanaged<CFData>? = nil
|
||||
let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, MACH_REPLY_TIMEOUT, CFRunLoopMode.defaultMode.rawValue, &respData)
|
||||
if let err = MachMessenger.sendError(code) {
|
||||
return .failure(err)
|
||||
} else if let data = respData?.takeUnretainedValue(),
|
||||
let resp = String(data: data as Data, encoding: .utf8) {
|
||||
return .success(resp)
|
||||
} else {
|
||||
return .success(nil)
|
||||
}
|
||||
|
||||
}
|
||||
return .failure(.msgError)
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,22 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SimpleXChatSDK
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
// maximum image file size to be auto-accepted
|
||||
let maxImageSize: Int64 = 236700
|
||||
public let maxImageSize: Int64 = 236700
|
||||
|
||||
let maxFileSize: Int64 = 8000000
|
||||
public let maxFileSize: Int64 = 8000000
|
||||
|
||||
func getDocumentsDirectory() -> URL {
|
||||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
public func getDocumentsDirectory() -> URL {
|
||||
// FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)!
|
||||
}
|
||||
|
||||
func getAppFilesDirectory() -> URL {
|
||||
public func getAppFilesDirectory() -> URL {
|
||||
getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true)
|
||||
}
|
||||
|
||||
@@ -26,7 +31,7 @@ func getAppFilePath(_ fileName: String) -> URL {
|
||||
getAppFilesDirectory().appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
func getLoadedFilePath(_ file: CIFile?) -> String? {
|
||||
public func getLoadedFilePath(_ file: CIFile?) -> String? {
|
||||
if let file = file,
|
||||
file.loaded,
|
||||
let savedFile = file.filePath {
|
||||
@@ -35,14 +40,14 @@ func getLoadedFilePath(_ file: CIFile?) -> String? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLoadedImage(_ file: CIFile?) -> UIImage? {
|
||||
public func getLoadedImage(_ file: CIFile?) -> UIImage? {
|
||||
if let filePath = getLoadedFilePath(file) {
|
||||
return UIImage(contentsOfFile: filePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveFileFromURL(_ url: URL) -> String? {
|
||||
public func saveFileFromURL(_ url: URL) -> String? {
|
||||
let savedFile: String?
|
||||
if url.startAccessingSecurityScopedResource() {
|
||||
do {
|
||||
@@ -61,7 +66,7 @@ func saveFileFromURL(_ url: URL) -> String? {
|
||||
return savedFile
|
||||
}
|
||||
|
||||
func saveImage(_ uiImage: UIImage) -> String? {
|
||||
public func saveImage(_ uiImage: UIImage) -> String? {
|
||||
if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: maxImageSize) {
|
||||
let timestamp = Date().getFormattedDate("yyyyMMdd_HHmmss")
|
||||
let fileName = uniqueCombine("IMG_\(timestamp).jpg")
|
||||
@@ -113,7 +118,7 @@ private extension String {
|
||||
}
|
||||
}
|
||||
|
||||
func removeFile(_ fileName: String) {
|
||||
public func removeFile(_ fileName: String) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: getAppFilePath(fileName).path)
|
||||
} catch {
|
||||
@@ -123,7 +128,7 @@ func removeFile(_ fileName: String) {
|
||||
|
||||
// image utils
|
||||
|
||||
func dropImagePrefix(_ s: String) -> String {
|
||||
public func dropImagePrefix(_ s: String) -> String {
|
||||
dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,")
|
||||
}
|
||||
|
||||
@@ -131,7 +136,7 @@ private func dropPrefix(_ s: String, _ prefix: String) -> String {
|
||||
s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s
|
||||
}
|
||||
|
||||
func cropToSquare(_ image: UIImage) -> UIImage {
|
||||
public func cropToSquare(_ image: UIImage) -> UIImage {
|
||||
let size = image.size
|
||||
let side = min(size.width, size.height)
|
||||
let newSize = CGSize(width: side, height: side)
|
||||
@@ -159,7 +164,7 @@ func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64) -> Data? {
|
||||
return data
|
||||
}
|
||||
|
||||
func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
|
||||
public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
|
||||
var img = image
|
||||
var str = compressImageStr(img)
|
||||
var dataSize = str?.count ?? 0
|
||||
@@ -9,18 +9,19 @@
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import SwiftUI
|
||||
import SimpleXChatSDK
|
||||
|
||||
let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST"
|
||||
let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED"
|
||||
let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED"
|
||||
let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION"
|
||||
let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
|
||||
public let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST"
|
||||
public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED"
|
||||
public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED"
|
||||
public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION"
|
||||
public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
|
||||
// TODO remove
|
||||
let ntfCategoryCheckingMessages = "NTF_CAT_CHECKING_MESSAGES"
|
||||
public let ntfCategoryCheckingMessages = "NTF_CAT_CHECKING_MESSAGES"
|
||||
|
||||
let appNotificationId = "chat.simplex.app.notification"
|
||||
public let appNotificationId = "chat.simplex.app.notification"
|
||||
|
||||
func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
|
||||
public func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
|
||||
createNotification(
|
||||
categoryIdentifier: ntfCategoryContactRequest,
|
||||
title: String.localizedStringWithFormat(NSLocalizedString("%@ wants to connect!", comment: "notification title"), contactRequest.displayName),
|
||||
@@ -30,7 +31,7 @@ func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableN
|
||||
)
|
||||
}
|
||||
|
||||
func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent {
|
||||
public func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent {
|
||||
createNotification(
|
||||
categoryIdentifier: ntfCategoryContactConnected,
|
||||
title: String.localizedStringWithFormat(NSLocalizedString("%@ is connected!", comment: "notification title"), contact.displayName),
|
||||
@@ -40,7 +41,7 @@ func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationConte
|
||||
)
|
||||
}
|
||||
|
||||
func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
|
||||
public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
|
||||
createNotification(
|
||||
categoryIdentifier: ntfCategoryMessageReceived,
|
||||
title: "\(cInfo.chatViewName):",
|
||||
@@ -50,7 +51,7 @@ func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutable
|
||||
)
|
||||
}
|
||||
|
||||
func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificationContent {
|
||||
public func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificationContent {
|
||||
let text = invitation.peerMedia == .video
|
||||
? NSLocalizedString("Incoming video call", comment: "notification")
|
||||
: NSLocalizedString("Incoming audio call", comment: "notification")
|
||||
@@ -63,7 +64,7 @@ func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificat
|
||||
)
|
||||
}
|
||||
|
||||
func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil,
|
||||
public func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil,
|
||||
targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.categoryIdentifier = categoryIdentifier
|
||||
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>
|
||||
|
||||
|
||||
82
apps/ios/SimpleXChat/API.swift
Normal file
82
apps/ios/SimpleXChat/API.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// API.swift
|
||||
// SimpleX NSE
|
||||
//
|
||||
// Created by Evgeny on 26/04/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
private var chatController: chat_ctrl?
|
||||
|
||||
private func getChatCtrl() -> chat_ctrl {
|
||||
if let controller = chatController { return controller }
|
||||
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!
|
||||
}
|
||||
|
||||
func getDocumentsDirectory() -> URL {
|
||||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
// FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)!
|
||||
}
|
||||
|
||||
//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)
|
||||
free(cjson)
|
||||
return s
|
||||
}
|
||||
|
||||
//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
|
||||
// }
|
||||
//}
|
||||
@@ -1,6 +1,15 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
// SimpleX.h
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny on 30/05/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef SimpleX_h
|
||||
#define SimpleX_h
|
||||
|
||||
#endif /* SimpleX_h */
|
||||
|
||||
extern void hs_init(int argc, char **argv[]);
|
||||
|
||||
13
apps/ios/SimpleXChat/SimpleXChat.docc/SimpleXChat.md
Executable file
13
apps/ios/SimpleXChat/SimpleXChat.docc/SimpleXChat.md
Executable file
@@ -0,0 +1,13 @@
|
||||
# ``SimpleXChat``
|
||||
|
||||
<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
|
||||
|
||||
## Overview
|
||||
|
||||
<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
|
||||
|
||||
## Topics
|
||||
|
||||
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->
|
||||
|
||||
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->
|
||||
17
apps/ios/SimpleXChat/SimpleXChat.h
Normal file
17
apps/ios/SimpleXChat/SimpleXChat.h
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// SimpleXChat.h
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Evgeny on 30/05/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for SimpleXChat.
|
||||
FOUNDATION_EXPORT double SimpleXChatVersionNumber;
|
||||
|
||||
//! Project version string for SimpleXChat.
|
||||
FOUNDATION_EXPORT const unsigned char SimpleXChatVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <SimpleXChat/PublicHeader.h>
|
||||
@@ -1,8 +1,8 @@
|
||||
//
|
||||
// dummy.m
|
||||
// SimpleX NSE
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Evgeny on 26/04/2022.
|
||||
// Created by Evgeny on 30/05/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
let jsonDecoder = getJSONDecoder()
|
||||
let jsonEncoder = getJSONEncoder()
|
||||
|
||||
enum ChatCommand {
|
||||
public enum ChatCommand {
|
||||
case showActiveUser
|
||||
case createActiveUser(profile: Profile)
|
||||
case startChat
|
||||
@@ -50,7 +53,7 @@ enum ChatCommand {
|
||||
case receiveFile(fileId: Int64)
|
||||
case string(String)
|
||||
|
||||
var cmdString: String {
|
||||
public var cmdString: String {
|
||||
get {
|
||||
switch self {
|
||||
case .showActiveUser: return "/u"
|
||||
@@ -95,7 +98,7 @@ enum ChatCommand {
|
||||
}
|
||||
}
|
||||
|
||||
var cmdType: String {
|
||||
public var cmdType: String {
|
||||
get {
|
||||
switch self {
|
||||
case .showActiveUser: return "showActiveUser"
|
||||
@@ -151,7 +154,7 @@ struct APIResponse: Decodable {
|
||||
var resp: ChatResponse
|
||||
}
|
||||
|
||||
enum ChatResponse: Decodable, Error {
|
||||
public enum ChatResponse: Decodable, Error {
|
||||
case response(type: String, json: String)
|
||||
case activeUser(user: User)
|
||||
case chatStarted
|
||||
@@ -211,7 +214,7 @@ enum ChatResponse: Decodable, Error {
|
||||
case chatCmdError(chatError: ChatError)
|
||||
case chatError(chatError: ChatError)
|
||||
|
||||
var responseType: String {
|
||||
public var responseType: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .response(type, _): return "* \(type)"
|
||||
@@ -274,7 +277,7 @@ enum ChatResponse: Decodable, Error {
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
public var details: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .response(_, json): return json
|
||||
@@ -346,7 +349,7 @@ struct ComposedMessage: Encodable {
|
||||
var msgContent: MsgContent
|
||||
}
|
||||
|
||||
func decodeJSON<T: Decodable>(_ json: String) -> T? {
|
||||
public func decodeJSON<T: Decodable>(_ json: String) -> T? {
|
||||
if let data = json.data(using: .utf8) {
|
||||
return try? jsonDecoder.decode(T.self, from: data)
|
||||
}
|
||||
@@ -366,7 +369,7 @@ private func getJSONObject(_ cjson: UnsafePointer<CChar>) -> NSDictionary? {
|
||||
return try? JSONSerialization.jsonObject(with: d) as? NSDictionary
|
||||
}
|
||||
|
||||
func encodeJSON<T: Encodable>(_ value: T) -> String {
|
||||
public func encodeJSON<T: Encodable>(_ value: T) -> String {
|
||||
let data = try! jsonEncoder.encode(value)
|
||||
return String(decoding: data, as: UTF8.self)
|
||||
}
|
||||
@@ -375,13 +378,13 @@ private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
|
||||
encodeJSON(value).cString(using: .utf8)!
|
||||
}
|
||||
|
||||
enum ChatError: Decodable {
|
||||
public enum ChatError: Decodable {
|
||||
case error(errorType: ChatErrorType)
|
||||
case errorAgent(agentError: AgentErrorType)
|
||||
case errorStore(storeError: StoreError)
|
||||
}
|
||||
|
||||
enum ChatErrorType: Decodable {
|
||||
public enum ChatErrorType: Decodable {
|
||||
case noActiveUser
|
||||
case activeUserExists
|
||||
case chatNotStarted
|
||||
@@ -415,7 +418,7 @@ enum ChatErrorType: Decodable {
|
||||
case commandError(message: String)
|
||||
}
|
||||
|
||||
enum StoreError: Decodable {
|
||||
public enum StoreError: Decodable {
|
||||
case duplicateName
|
||||
case contactNotFound(contactId: Int64)
|
||||
case contactNotFoundByName(contactName: ContactName)
|
||||
@@ -448,7 +451,7 @@ enum StoreError: Decodable {
|
||||
case chatItemNotFoundByFileId(fileId: Int64)
|
||||
}
|
||||
|
||||
enum AgentErrorType: Decodable {
|
||||
public enum AgentErrorType: Decodable {
|
||||
case CMD(cmdErr: CommandErrorType)
|
||||
case CONN(connErr: ConnectionErrorType)
|
||||
case SMP(smpErr: ProtocolErrorType)
|
||||
@@ -458,7 +461,7 @@ enum AgentErrorType: Decodable {
|
||||
case INTERNAL(internalErr: String)
|
||||
}
|
||||
|
||||
enum CommandErrorType: Decodable {
|
||||
public enum CommandErrorType: Decodable {
|
||||
case PROHIBITED
|
||||
case SYNTAX
|
||||
case NO_CONN
|
||||
@@ -466,7 +469,7 @@ enum CommandErrorType: Decodable {
|
||||
case LARGE
|
||||
}
|
||||
|
||||
enum ConnectionErrorType: Decodable {
|
||||
public enum ConnectionErrorType: Decodable {
|
||||
case NOT_FOUND
|
||||
case DUPLICATE
|
||||
case SIMPLEX
|
||||
@@ -474,7 +477,7 @@ enum ConnectionErrorType: Decodable {
|
||||
case NOT_AVAILABLE
|
||||
}
|
||||
|
||||
enum BrokerErrorType: Decodable {
|
||||
public enum BrokerErrorType: Decodable {
|
||||
case RESPONSE(smpErr: ProtocolErrorType)
|
||||
case UNEXPECTED
|
||||
case NETWORK
|
||||
@@ -482,7 +485,7 @@ enum BrokerErrorType: Decodable {
|
||||
case TIMEOUT
|
||||
}
|
||||
|
||||
enum ProtocolErrorType: Decodable {
|
||||
public enum ProtocolErrorType: Decodable {
|
||||
case BLOCK
|
||||
case SESSION
|
||||
case CMD(cmdErr: ProtocolCommandError)
|
||||
@@ -493,7 +496,7 @@ enum ProtocolErrorType: Decodable {
|
||||
case INTERNAL
|
||||
}
|
||||
|
||||
enum ProtocolCommandError: Decodable {
|
||||
public enum ProtocolCommandError: Decodable {
|
||||
case UNKNOWN
|
||||
case SYNTAX
|
||||
case NO_AUTH
|
||||
@@ -501,22 +504,53 @@ enum ProtocolCommandError: Decodable {
|
||||
case NO_ENTITY
|
||||
}
|
||||
|
||||
enum ProtocolTransportError: Decodable {
|
||||
public enum ProtocolTransportError: Decodable {
|
||||
case badBlock
|
||||
case largeMsg
|
||||
case badSession
|
||||
case handshake(handshakeErr: SMPHandshakeError)
|
||||
}
|
||||
|
||||
enum SMPHandshakeError: Decodable {
|
||||
public enum SMPHandshakeError: Decodable {
|
||||
case PARSE
|
||||
case VERSION
|
||||
case IDENTITY
|
||||
}
|
||||
|
||||
enum SMPAgentError: Decodable {
|
||||
public enum SMPAgentError: Decodable {
|
||||
case A_MESSAGE
|
||||
case A_PROHIBITED
|
||||
case A_VERSION
|
||||
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
|
||||
}
|
||||
}
|
||||
99
apps/ios/SimpleXChatSDK/CallTypes.swift
Normal file
99
apps/ios/SimpleXChatSDK/CallTypes.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// CallTypes.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 05/05/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public struct WebRTCCallOffer: Encodable {
|
||||
public init(callType: CallType, rtcSession: WebRTCSession) {
|
||||
self.callType = callType
|
||||
self.rtcSession = rtcSession
|
||||
}
|
||||
|
||||
public var callType: CallType
|
||||
public var rtcSession: WebRTCSession
|
||||
}
|
||||
|
||||
public struct WebRTCSession: Codable {
|
||||
public init(rtcSession: String, rtcIceCandidates: String) {
|
||||
self.rtcSession = rtcSession
|
||||
self.rtcIceCandidates = rtcIceCandidates
|
||||
}
|
||||
|
||||
public var rtcSession: String
|
||||
public var rtcIceCandidates: String
|
||||
}
|
||||
|
||||
public struct WebRTCExtraInfo: Codable {
|
||||
public init(rtcIceCandidates: String) {
|
||||
self.rtcIceCandidates = rtcIceCandidates
|
||||
}
|
||||
|
||||
public var rtcIceCandidates: String
|
||||
}
|
||||
|
||||
public struct CallInvitation {
|
||||
public init(contact: Contact, callkitUUID: UUID? = nil, peerMedia: CallMediaType, sharedKey: String? = nil, callTs: Date) {
|
||||
self.contact = contact
|
||||
self.callkitUUID = callkitUUID
|
||||
self.peerMedia = peerMedia
|
||||
self.sharedKey = sharedKey
|
||||
self.callTs = callTs
|
||||
}
|
||||
|
||||
public var contact: Contact
|
||||
public var callkitUUID: UUID?
|
||||
public var peerMedia: CallMediaType
|
||||
public var sharedKey: String?
|
||||
public var callTs: Date
|
||||
public var callTypeText: LocalizedStringKey {
|
||||
get {
|
||||
switch peerMedia {
|
||||
case .video: return sharedKey == nil ? "video call (not e2e encrypted)" : "**e2e encrypted** video call"
|
||||
case .audio: return sharedKey == nil ? "audio call (not e2e encrypted)" : "**e2e encrypted** audio call"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static let sampleData = CallInvitation(
|
||||
contact: Contact.sampleData,
|
||||
peerMedia: .audio,
|
||||
callTs: .now
|
||||
)
|
||||
}
|
||||
|
||||
public struct CallType: Codable {
|
||||
public init(media: CallMediaType, capabilities: CallCapabilities) {
|
||||
self.media = media
|
||||
self.capabilities = capabilities
|
||||
}
|
||||
|
||||
public var media: CallMediaType
|
||||
public var capabilities: CallCapabilities
|
||||
}
|
||||
|
||||
public enum CallMediaType: String, Codable, Equatable {
|
||||
case video = "video"
|
||||
case audio = "audio"
|
||||
}
|
||||
|
||||
public enum VideoCamera: String, Codable, Equatable {
|
||||
case user = "user"
|
||||
case environment = "environment"
|
||||
}
|
||||
|
||||
public struct CallCapabilities: Codable, Equatable {
|
||||
public var encryption: Bool
|
||||
}
|
||||
|
||||
public enum WebRTCCallStatus: String, Encodable {
|
||||
case connected = "connected"
|
||||
case connecting = "connecting"
|
||||
case disconnected = "disconnected"
|
||||
case failed = "failed"
|
||||
}
|
||||
@@ -9,18 +9,18 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct User: Decodable, NamedChat {
|
||||
public struct User: Decodable, NamedChat {
|
||||
var userId: Int64
|
||||
var userContactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
var profile: Profile
|
||||
public var profile: Profile
|
||||
var activeUser: Bool
|
||||
|
||||
var displayName: String { get { profile.displayName } }
|
||||
var fullName: String { get { profile.fullName } }
|
||||
var image: String? { get { profile.image } }
|
||||
public var displayName: String { get { profile.displayName } }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
public var image: String? { get { profile.image } }
|
||||
|
||||
static let sampleData = User(
|
||||
public static let sampleData = User(
|
||||
userId: 1,
|
||||
userContactId: 1,
|
||||
localDisplayName: "alice",
|
||||
@@ -29,14 +29,20 @@ struct User: Decodable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
typealias ContactName = String
|
||||
public typealias ContactName = String
|
||||
|
||||
typealias GroupName = String
|
||||
public typealias GroupName = String
|
||||
|
||||
struct Profile: Codable, NamedChat {
|
||||
var displayName: String
|
||||
var fullName: String
|
||||
var image: String?
|
||||
public struct Profile: Codable, NamedChat {
|
||||
public init(displayName: String, fullName: String, image: String? = nil) {
|
||||
self.displayName = displayName
|
||||
self.fullName = fullName
|
||||
self.image = image
|
||||
}
|
||||
|
||||
public var displayName: String
|
||||
public var fullName: String
|
||||
public var image: String?
|
||||
|
||||
static let sampleData = Profile(
|
||||
displayName: "alice",
|
||||
@@ -44,34 +50,34 @@ struct Profile: Codable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
enum ChatType: String {
|
||||
public enum ChatType: String {
|
||||
case direct = "@"
|
||||
case group = "#"
|
||||
case contactRequest = "<@"
|
||||
case contactConnection = ":"
|
||||
}
|
||||
|
||||
protocol NamedChat {
|
||||
public protocol NamedChat {
|
||||
var displayName: String { get }
|
||||
var fullName: String { get }
|
||||
var image: String? { get }
|
||||
}
|
||||
|
||||
extension NamedChat {
|
||||
var chatViewName: String {
|
||||
public var chatViewName: String {
|
||||
get { displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") }
|
||||
}
|
||||
}
|
||||
|
||||
typealias ChatId = String
|
||||
public typealias ChatId = String
|
||||
|
||||
enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case direct(contact: Contact)
|
||||
case group(groupInfo: GroupInfo)
|
||||
case contactRequest(contactRequest: UserContactRequest)
|
||||
case contactConnection(contactConnection: PendingContactConnection)
|
||||
|
||||
var localDisplayName: String {
|
||||
public var localDisplayName: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.localDisplayName
|
||||
@@ -82,7 +88,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
var displayName: String {
|
||||
public var displayName: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.displayName
|
||||
@@ -93,7 +99,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
var fullName: String {
|
||||
public var fullName: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.fullName
|
||||
@@ -104,7 +110,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
var image: String? {
|
||||
public var image: String? {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.image
|
||||
@@ -115,7 +121,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
var id: ChatId {
|
||||
public var id: ChatId {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.id
|
||||
@@ -126,7 +132,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
var chatType: ChatType {
|
||||
public var chatType: ChatType {
|
||||
get {
|
||||
switch self {
|
||||
case .direct: return .direct
|
||||
@@ -137,7 +143,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
var apiId: Int64 {
|
||||
public var apiId: Int64 {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.apiId
|
||||
@@ -148,7 +154,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
var ready: Bool {
|
||||
public var ready: Bool {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.ready
|
||||
@@ -168,7 +174,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
var updatedAt: Date {
|
||||
public var updatedAt: Date {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.updatedAt
|
||||
case let .group(groupInfo): return groupInfo.updatedAt
|
||||
@@ -177,49 +183,54 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
struct SampleData {
|
||||
var direct: ChatInfo
|
||||
var group: ChatInfo
|
||||
var contactRequest: ChatInfo
|
||||
public struct SampleData {
|
||||
public var direct: ChatInfo
|
||||
public var group: ChatInfo
|
||||
public var contactRequest: ChatInfo
|
||||
}
|
||||
|
||||
static var sampleData: ChatInfo.SampleData = SampleData(
|
||||
public static var sampleData: ChatInfo.SampleData = SampleData(
|
||||
direct: ChatInfo.direct(contact: Contact.sampleData),
|
||||
group: ChatInfo.group(groupInfo: GroupInfo.sampleData),
|
||||
contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData)
|
||||
)
|
||||
}
|
||||
|
||||
struct ChatData: Decodable, Identifiable {
|
||||
var chatInfo: ChatInfo
|
||||
var chatItems: [ChatItem]
|
||||
var chatStats: ChatStats
|
||||
public struct ChatData: Decodable, Identifiable {
|
||||
public var chatInfo: ChatInfo
|
||||
public var chatItems: [ChatItem]
|
||||
public var chatStats: ChatStats
|
||||
|
||||
var id: ChatId { get { chatInfo.id } }
|
||||
public var id: ChatId { get { chatInfo.id } }
|
||||
}
|
||||
|
||||
struct ChatStats: Decodable {
|
||||
var unreadCount: Int = 0
|
||||
var minUnreadItemId: Int64 = 0
|
||||
public struct ChatStats: Decodable {
|
||||
public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0) {
|
||||
self.unreadCount = unreadCount
|
||||
self.minUnreadItemId = minUnreadItemId
|
||||
}
|
||||
|
||||
public var unreadCount: Int = 0
|
||||
public var minUnreadItemId: Int64 = 0
|
||||
}
|
||||
|
||||
struct Contact: Identifiable, Decodable, NamedChat {
|
||||
public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
var contactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
var profile: Profile
|
||||
var activeConn: Connection
|
||||
public var profile: Profile
|
||||
public var activeConn: Connection
|
||||
var viaGroup: Int64?
|
||||
var createdAt: Date
|
||||
var updatedAt: Date
|
||||
|
||||
var id: ChatId { get { "@\(contactId)" } }
|
||||
var apiId: Int64 { get { contactId } }
|
||||
var ready: Bool { get { activeConn.connStatus == .ready } }
|
||||
var displayName: String { get { profile.displayName } }
|
||||
var fullName: String { get { profile.fullName } }
|
||||
var image: String? { get { profile.image } }
|
||||
public var id: ChatId { get { "@\(contactId)" } }
|
||||
public var apiId: Int64 { get { contactId } }
|
||||
public var ready: Bool { get { activeConn.connStatus == .ready } }
|
||||
public var displayName: String { get { profile.displayName } }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
public var image: String? { get { profile.image } }
|
||||
|
||||
static let sampleData = Contact(
|
||||
public static let sampleData = Contact(
|
||||
contactId: 1,
|
||||
localDisplayName: "alice",
|
||||
profile: Profile.sampleData,
|
||||
@@ -229,23 +240,23 @@ struct Contact: Identifiable, Decodable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
struct ContactRef: Decodable, Equatable {
|
||||
public struct ContactRef: Decodable, Equatable {
|
||||
var contactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
|
||||
var id: ChatId { get { "@\(contactId)" } }
|
||||
public var id: ChatId { get { "@\(contactId)" } }
|
||||
}
|
||||
|
||||
struct ContactSubStatus: Decodable {
|
||||
var contact: Contact
|
||||
var contactError: ChatError?
|
||||
public struct ContactSubStatus: Decodable {
|
||||
public var contact: Contact
|
||||
public var contactError: ChatError?
|
||||
}
|
||||
|
||||
struct Connection: Decodable {
|
||||
public struct Connection: Decodable {
|
||||
var connId: Int64
|
||||
var connStatus: ConnStatus
|
||||
|
||||
var id: ChatId { get { ":\(connId)" } }
|
||||
public var id: ChatId { get { ":\(connId)" } }
|
||||
|
||||
static let sampleData = Connection(
|
||||
connId: 1,
|
||||
@@ -253,21 +264,21 @@ struct Connection: Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
struct UserContactRequest: Decodable, NamedChat {
|
||||
public struct UserContactRequest: Decodable, NamedChat {
|
||||
var contactRequestId: Int64
|
||||
var localDisplayName: ContactName
|
||||
var profile: Profile
|
||||
var createdAt: Date
|
||||
var updatedAt: Date
|
||||
public var updatedAt: Date
|
||||
|
||||
var id: ChatId { get { "<@\(contactRequestId)" } }
|
||||
var apiId: Int64 { get { contactRequestId } }
|
||||
public var id: ChatId { get { "<@\(contactRequestId)" } }
|
||||
public var apiId: Int64 { get { contactRequestId } }
|
||||
var ready: Bool { get { true } }
|
||||
var displayName: String { get { profile.displayName } }
|
||||
var fullName: String { get { profile.fullName } }
|
||||
var image: String? { get { profile.image } }
|
||||
public var displayName: String { get { profile.displayName } }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
public var image: String? { get { profile.image } }
|
||||
|
||||
static let sampleData = UserContactRequest(
|
||||
public static let sampleData = UserContactRequest(
|
||||
contactRequestId: 1,
|
||||
localDisplayName: "alice",
|
||||
profile: Profile.sampleData,
|
||||
@@ -276,21 +287,21 @@ struct UserContactRequest: Decodable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
struct PendingContactConnection: Decodable, NamedChat {
|
||||
public struct PendingContactConnection: Decodable, NamedChat {
|
||||
var pccConnId: Int64
|
||||
var pccAgentConnId: String
|
||||
var pccConnStatus: ConnStatus
|
||||
var viaContactUri: Bool
|
||||
public var viaContactUri: Bool
|
||||
var createdAt: Date
|
||||
var updatedAt: Date
|
||||
public var updatedAt: Date
|
||||
|
||||
var id: ChatId { get { ":\(pccConnId)" } }
|
||||
var apiId: Int64 { get { pccConnId } }
|
||||
public var id: ChatId { get { ":\(pccConnId)" } }
|
||||
public var apiId: Int64 { get { pccConnId } }
|
||||
var ready: Bool { get { false } }
|
||||
var localDisplayName: String {
|
||||
get { String.localizedStringWithFormat(NSLocalizedString("connection:%@", comment: "connection information"), pccConnId) }
|
||||
}
|
||||
var displayName: String {
|
||||
public var displayName: String {
|
||||
get {
|
||||
if let initiated = pccConnStatus.initiated {
|
||||
return initiated && !viaContactUri
|
||||
@@ -302,11 +313,11 @@ struct PendingContactConnection: Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
}
|
||||
var fullName: String { get { "" } }
|
||||
var image: String? { get { nil } }
|
||||
var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } }
|
||||
public var fullName: String { get { "" } }
|
||||
public var image: String? { get { nil } }
|
||||
public var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } }
|
||||
|
||||
var description: String {
|
||||
public var description: String {
|
||||
get {
|
||||
if let initiated = pccConnStatus.initiated {
|
||||
return initiated && !viaContactUri
|
||||
@@ -320,7 +331,7 @@ struct PendingContactConnection: Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
static func getSampleData(_ status: ConnStatus = .new, viaContactUri: Bool = false) -> PendingContactConnection {
|
||||
public static func getSampleData(_ status: ConnStatus = .new, viaContactUri: Bool = false) -> PendingContactConnection {
|
||||
PendingContactConnection(
|
||||
pccConnId: 1,
|
||||
pccAgentConnId: "abcd",
|
||||
@@ -332,7 +343,7 @@ struct PendingContactConnection: Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
enum ConnStatus: String, Decodable {
|
||||
public enum ConnStatus: String, Decodable {
|
||||
case new = "new"
|
||||
case joined = "joined"
|
||||
case requested = "requested"
|
||||
@@ -356,19 +367,19 @@ enum ConnStatus: String, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
public struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
var groupId: Int64
|
||||
var localDisplayName: GroupName
|
||||
var groupProfile: GroupProfile
|
||||
var createdAt: Date
|
||||
var updatedAt: Date
|
||||
|
||||
var id: ChatId { get { "#\(groupId)" } }
|
||||
public var id: ChatId { get { "#\(groupId)" } }
|
||||
var apiId: Int64 { get { groupId } }
|
||||
var ready: Bool { get { true } }
|
||||
var displayName: String { get { groupProfile.displayName } }
|
||||
var fullName: String { get { groupProfile.fullName } }
|
||||
var image: String? { get { groupProfile.image } }
|
||||
public var ready: Bool { get { true } }
|
||||
public var displayName: String { get { groupProfile.displayName } }
|
||||
public var fullName: String { get { groupProfile.fullName } }
|
||||
public var image: String? { get { groupProfile.image } }
|
||||
|
||||
static let sampleData = GroupInfo(
|
||||
groupId: 1,
|
||||
@@ -379,10 +390,10 @@ struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
struct GroupProfile: Codable, NamedChat {
|
||||
var displayName: String
|
||||
var fullName: String
|
||||
var image: String?
|
||||
public struct GroupProfile: Codable, NamedChat {
|
||||
public var displayName: String
|
||||
public var fullName: String
|
||||
public var image: String?
|
||||
|
||||
static let sampleData = GroupProfile(
|
||||
displayName: "team",
|
||||
@@ -390,15 +401,15 @@ struct GroupProfile: Codable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
struct GroupMember: Decodable {
|
||||
var groupMemberId: Int64
|
||||
public struct GroupMember: Decodable {
|
||||
public var groupMemberId: Int64
|
||||
var memberId: String
|
||||
// var memberRole: GroupMemberRole
|
||||
// var memberCategory: GroupMemberCategory
|
||||
// var memberStatus: GroupMemberStatus
|
||||
// var invitedBy: InvitedBy
|
||||
var localDisplayName: ContactName
|
||||
var memberProfile: Profile
|
||||
public var memberProfile: Profile
|
||||
var memberContactId: Int64?
|
||||
// var activeConn: Connection?
|
||||
|
||||
@@ -412,7 +423,7 @@ struct GroupMember: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
static let sampleData = GroupMember(
|
||||
public static let sampleData = GroupMember(
|
||||
groupMemberId: 1,
|
||||
memberId: "abcd",
|
||||
localDisplayName: "alice",
|
||||
@@ -421,29 +432,38 @@ struct GroupMember: Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
struct MemberSubError: Decodable {
|
||||
public struct MemberSubError: Decodable {
|
||||
var member: GroupMember
|
||||
var memberError: ChatError
|
||||
}
|
||||
|
||||
struct AChatItem: Decodable {
|
||||
var chatInfo: ChatInfo
|
||||
var chatItem: ChatItem
|
||||
public struct AChatItem: Decodable {
|
||||
public var chatInfo: ChatInfo
|
||||
public var chatItem: ChatItem
|
||||
}
|
||||
|
||||
struct ChatItem: Identifiable, Decodable {
|
||||
var chatDir: CIDirection
|
||||
var meta: CIMeta
|
||||
var content: CIContent
|
||||
var formattedText: [FormattedText]?
|
||||
var quotedItem: CIQuote?
|
||||
var file: CIFile?
|
||||
public struct ChatItem: Identifiable, Decodable {
|
||||
public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, file: CIFile? = nil) {
|
||||
self.chatDir = chatDir
|
||||
self.meta = meta
|
||||
self.content = content
|
||||
self.formattedText = formattedText
|
||||
self.quotedItem = quotedItem
|
||||
self.file = file
|
||||
}
|
||||
|
||||
public var chatDir: CIDirection
|
||||
public var meta: CIMeta
|
||||
public var content: CIContent
|
||||
public var formattedText: [FormattedText]?
|
||||
public var quotedItem: CIQuote?
|
||||
public var file: CIFile?
|
||||
|
||||
var id: Int64 { get { meta.itemId } }
|
||||
public var id: Int64 { get { meta.itemId } }
|
||||
|
||||
var timestampText: Text { get { meta.timestampText } }
|
||||
public var timestampText: Text { get { meta.timestampText } }
|
||||
|
||||
var text: String {
|
||||
public var text: String {
|
||||
get {
|
||||
switch (content.text, file) {
|
||||
case let ("", .some(file)): return file.fileName
|
||||
@@ -452,12 +472,12 @@ struct ChatItem: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
func isRcvNew() -> Bool {
|
||||
public func isRcvNew() -> Bool {
|
||||
if case .rcvNew = meta.itemStatus { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
func isMsgContent() -> Bool {
|
||||
public func isMsgContent() -> Bool {
|
||||
switch content {
|
||||
case .sndMsgContent: return true
|
||||
case .rcvMsgContent: return true
|
||||
@@ -465,7 +485,7 @@ struct ChatItem: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
func isDeletedContent() -> Bool {
|
||||
public func isDeletedContent() -> Bool {
|
||||
switch content {
|
||||
case .sndDeleted: return true
|
||||
case .rcvDeleted: return true
|
||||
@@ -473,7 +493,7 @@ struct ChatItem: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
func isCall() -> Bool {
|
||||
public func isCall() -> Bool {
|
||||
switch content {
|
||||
case .sndCall: return true
|
||||
case .rcvCall: return true
|
||||
@@ -481,7 +501,7 @@ struct ChatItem: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
var memberDisplayName: String? {
|
||||
public var memberDisplayName: String? {
|
||||
get {
|
||||
if case let .groupRcv(groupMember) = chatDir {
|
||||
return groupMember.memberProfile.displayName
|
||||
@@ -491,7 +511,7 @@ struct ChatItem: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> ChatItem {
|
||||
public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> ChatItem {
|
||||
ChatItem(
|
||||
chatDir: dir,
|
||||
meta: CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
|
||||
@@ -501,7 +521,7 @@ struct ChatItem: Identifiable, Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
static func getFileMsgContentSample (id: Int64 = 1, text: String = "", fileName: String = "test.txt", fileSize: Int64 = 100, fileStatus: CIFileStatus = .rcvComplete) -> ChatItem {
|
||||
public static func getFileMsgContentSample (id: Int64 = 1, text: String = "", fileName: String = "test.txt", fileSize: Int64 = 100, fileStatus: CIFileStatus = .rcvComplete) -> ChatItem {
|
||||
ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(id, .now, text, .rcvRead, false, false, false),
|
||||
@@ -511,7 +531,7 @@ struct ChatItem: Identifiable, Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
static func getDeletedContentSample (_ id: Int64 = 1, dir: CIDirection = .directRcv, _ ts: Date = .now, _ text: String = "this item is deleted", _ status: CIStatus = .rcvRead) -> ChatItem {
|
||||
public static func getDeletedContentSample (_ id: Int64 = 1, dir: CIDirection = .directRcv, _ ts: Date = .now, _ text: String = "this item is deleted", _ status: CIStatus = .rcvRead) -> ChatItem {
|
||||
ChatItem(
|
||||
chatDir: dir,
|
||||
meta: CIMeta.getSample(id, ts, text, status, false, false, false),
|
||||
@@ -521,7 +541,7 @@ struct ChatItem: Identifiable, Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
static func getIntegrityErrorSample (_ status: CIStatus = .rcvRead, fromMsgId: Int64 = 1, toMsgId: Int64 = 2) -> ChatItem {
|
||||
public static func getIntegrityErrorSample (_ status: CIStatus = .rcvRead, fromMsgId: Int64 = 1, toMsgId: Int64 = 2) -> ChatItem {
|
||||
ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "1 skipped message", status, false, false, false),
|
||||
@@ -532,13 +552,13 @@ struct ChatItem: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
enum CIDirection: Decodable {
|
||||
public enum CIDirection: Decodable {
|
||||
case directSnd
|
||||
case directRcv
|
||||
case groupSnd
|
||||
case groupRcv(groupMember: GroupMember)
|
||||
|
||||
var sent: Bool {
|
||||
public var sent: Bool {
|
||||
get {
|
||||
switch self {
|
||||
case .directSnd: return true
|
||||
@@ -550,19 +570,19 @@ enum CIDirection: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
struct CIMeta: Decodable {
|
||||
public struct CIMeta: Decodable {
|
||||
var itemId: Int64
|
||||
var itemTs: Date
|
||||
var itemText: String
|
||||
var itemStatus: CIStatus
|
||||
public var itemStatus: CIStatus
|
||||
var createdAt: Date
|
||||
var itemDeleted: Bool
|
||||
var itemEdited: Bool
|
||||
var editable: Bool
|
||||
public var itemDeleted: Bool
|
||||
public var itemEdited: Bool
|
||||
public var editable: Bool
|
||||
|
||||
var timestampText: Text { get { formatTimestampText(itemTs) } }
|
||||
|
||||
static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> CIMeta {
|
||||
public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> CIMeta {
|
||||
CIMeta(
|
||||
itemId: id,
|
||||
itemTs: ts,
|
||||
@@ -579,14 +599,14 @@ struct CIMeta: Decodable {
|
||||
let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute()
|
||||
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
|
||||
|
||||
func formatTimestampText(_ date: Date) -> Text {
|
||||
public func formatTimestampText(_ date: Date) -> Text {
|
||||
let now = Calendar.current.dateComponents([.day, .hour], from: .now)
|
||||
let dc = Calendar.current.dateComponents([.day, .hour], from: date)
|
||||
let recent = now.day == dc.day || ((now.day ?? 0) - (dc.day ?? 0) == 1 && (dc.hour ?? 0) >= 18 && (now.hour ?? 0) < 12)
|
||||
return Text(date, format: recent ? msgTimeFormat : msgDateFormat)
|
||||
}
|
||||
|
||||
enum CIStatus: Decodable {
|
||||
public enum CIStatus: Decodable {
|
||||
case sndNew
|
||||
case sndSent
|
||||
case sndErrorAuth
|
||||
@@ -595,7 +615,7 @@ enum CIStatus: Decodable {
|
||||
case rcvRead
|
||||
}
|
||||
|
||||
enum CIDeleteMode: String, Decodable {
|
||||
public enum CIDeleteMode: String, Decodable {
|
||||
case cidmBroadcast = "broadcast"
|
||||
case cidmInternal = "internal"
|
||||
}
|
||||
@@ -604,7 +624,7 @@ protocol ItemContent {
|
||||
var text: String { get }
|
||||
}
|
||||
|
||||
enum CIContent: Decodable, ItemContent {
|
||||
public enum CIContent: Decodable, ItemContent {
|
||||
case sndMsgContent(msgContent: MsgContent)
|
||||
case rcvMsgContent(msgContent: MsgContent)
|
||||
case sndDeleted(deleteMode: CIDeleteMode)
|
||||
@@ -613,7 +633,7 @@ enum CIContent: Decodable, ItemContent {
|
||||
case rcvCall(status: CICallStatus, duration: Int)
|
||||
case rcvIntegrityError(msgError: MsgErrorType)
|
||||
|
||||
var text: String {
|
||||
public var text: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .sndMsgContent(mc): return mc.text
|
||||
@@ -627,7 +647,7 @@ enum CIContent: Decodable, ItemContent {
|
||||
}
|
||||
}
|
||||
|
||||
var msgContent: MsgContent? {
|
||||
public var msgContent: MsgContent? {
|
||||
get {
|
||||
switch self {
|
||||
case let .sndMsgContent(mc): return mc
|
||||
@@ -638,17 +658,17 @@ enum CIContent: Decodable, ItemContent {
|
||||
}
|
||||
}
|
||||
|
||||
struct CIQuote: Decodable, ItemContent {
|
||||
public struct CIQuote: Decodable, ItemContent {
|
||||
var chatDir: CIDirection?
|
||||
var itemId: Int64?
|
||||
var sharedMsgId: String? = nil
|
||||
var sentAt: Date
|
||||
var content: MsgContent
|
||||
var formattedText: [FormattedText]?
|
||||
public var content: MsgContent
|
||||
public var formattedText: [FormattedText]?
|
||||
|
||||
var text: String { get { content.text } }
|
||||
public var text: String { get { content.text } }
|
||||
|
||||
func getSender(_ currentUser: User?) -> String? {
|
||||
public func getSender(_ currentUser: User?) -> String? {
|
||||
switch (chatDir) {
|
||||
case .directSnd: return "you"
|
||||
case .directRcv: return nil
|
||||
@@ -658,7 +678,7 @@ struct CIQuote: Decodable, ItemContent {
|
||||
}
|
||||
}
|
||||
|
||||
static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIDirection?, image: String? = nil) -> CIQuote {
|
||||
public static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIDirection?, image: String? = nil) -> CIQuote {
|
||||
let mc: MsgContent
|
||||
if let image = image {
|
||||
mc = .image(text: text, image: image)
|
||||
@@ -669,18 +689,18 @@ struct CIQuote: Decodable, ItemContent {
|
||||
}
|
||||
}
|
||||
|
||||
struct CIFile: Decodable {
|
||||
var fileId: Int64
|
||||
var fileName: String
|
||||
var fileSize: Int64
|
||||
var filePath: String?
|
||||
var fileStatus: CIFileStatus
|
||||
public struct CIFile: Decodable {
|
||||
public var fileId: Int64
|
||||
public var fileName: String
|
||||
public var fileSize: Int64
|
||||
public var filePath: String?
|
||||
public var fileStatus: CIFileStatus
|
||||
|
||||
static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile {
|
||||
public static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile {
|
||||
CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus)
|
||||
}
|
||||
|
||||
var loaded: Bool {
|
||||
public var loaded: Bool {
|
||||
get {
|
||||
switch self.fileStatus {
|
||||
case .sndStored: return true
|
||||
@@ -697,7 +717,7 @@ struct CIFile: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
enum CIFileStatus: String, Decodable {
|
||||
public enum CIFileStatus: String, Decodable {
|
||||
case sndStored = "snd_stored"
|
||||
case sndTransfer = "snd_transfer"
|
||||
case sndComplete = "snd_complete"
|
||||
@@ -709,7 +729,7 @@ enum CIFileStatus: String, Decodable {
|
||||
case rcvCancelled = "rcv_cancelled"
|
||||
}
|
||||
|
||||
enum MsgContent {
|
||||
public enum MsgContent {
|
||||
case text(String)
|
||||
case link(text: String, preview: LinkPreview)
|
||||
case image(text: String, image: String)
|
||||
@@ -738,7 +758,7 @@ enum MsgContent {
|
||||
}
|
||||
}
|
||||
|
||||
func isFile() -> Bool {
|
||||
public func isFile() -> Bool {
|
||||
switch self {
|
||||
case .file: return true
|
||||
default: return false
|
||||
@@ -754,7 +774,7 @@ enum MsgContent {
|
||||
}
|
||||
|
||||
extension MsgContent: Decodable {
|
||||
init(from decoder: Decoder) throws {
|
||||
public init(from decoder: Decoder) throws {
|
||||
do {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(String.self, forKey: CodingKeys.type)
|
||||
@@ -784,7 +804,7 @@ extension MsgContent: Decodable {
|
||||
}
|
||||
|
||||
extension MsgContent: Encodable {
|
||||
func encode(to encoder: Encoder) throws {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case let .text(text):
|
||||
@@ -809,12 +829,12 @@ extension MsgContent: Encodable {
|
||||
}
|
||||
}
|
||||
|
||||
struct FormattedText: Decodable {
|
||||
var text: String
|
||||
var format: Format?
|
||||
public struct FormattedText: Decodable {
|
||||
public var text: String
|
||||
public var format: Format?
|
||||
}
|
||||
|
||||
enum Format: Decodable, Equatable {
|
||||
public enum Format: Decodable, Equatable {
|
||||
case bold
|
||||
case italic
|
||||
case strikeThrough
|
||||
@@ -826,7 +846,7 @@ enum Format: Decodable, Equatable {
|
||||
case phone
|
||||
}
|
||||
|
||||
enum FormatColor: String, Decodable {
|
||||
public enum FormatColor: String, Decodable {
|
||||
case red = "red"
|
||||
case green = "green"
|
||||
case blue = "blue"
|
||||
@@ -836,7 +856,7 @@ enum FormatColor: String, Decodable {
|
||||
case black = "black"
|
||||
case white = "white"
|
||||
|
||||
var uiColor: Color {
|
||||
public var uiColor: Color {
|
||||
get {
|
||||
switch (self) {
|
||||
case .red: return .red
|
||||
@@ -853,15 +873,22 @@ enum FormatColor: String, Decodable {
|
||||
}
|
||||
|
||||
// Struct to use with simplex API
|
||||
struct LinkPreview: Codable {
|
||||
var uri: URL
|
||||
var title: String
|
||||
public struct LinkPreview: Codable {
|
||||
public init(uri: URL, title: String, description: String = "", image: String) {
|
||||
self.uri = uri
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.image = image
|
||||
}
|
||||
|
||||
public var uri: URL
|
||||
public var title: String
|
||||
// TODO remove once optional in haskell
|
||||
var description: String = ""
|
||||
var image: String
|
||||
public var description: String = ""
|
||||
public var image: String
|
||||
}
|
||||
|
||||
enum NtfTknStatus: String, Decodable {
|
||||
public enum NtfTknStatus: String, Decodable {
|
||||
case new = "NEW"
|
||||
case registered = "REGISTERED"
|
||||
case invalid = "INVALID"
|
||||
@@ -870,15 +897,15 @@ enum NtfTknStatus: String, Decodable {
|
||||
case expired = "EXPIRED"
|
||||
}
|
||||
|
||||
struct SndFileTransfer: Decodable {
|
||||
public struct SndFileTransfer: Decodable {
|
||||
|
||||
}
|
||||
|
||||
struct FileTransferMeta: Decodable {
|
||||
public struct FileTransferMeta: Decodable {
|
||||
|
||||
}
|
||||
|
||||
enum CICallStatus: String, Decodable {
|
||||
public enum CICallStatus: String, Decodable {
|
||||
case pending
|
||||
case missed
|
||||
case rejected
|
||||
@@ -901,12 +928,12 @@ enum CICallStatus: String, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
static func durationText(_ sec: Int) -> String {
|
||||
public static func durationText(_ sec: Int) -> String {
|
||||
String(format: "%02d:%02d", sec / 60, sec % 60)
|
||||
}
|
||||
}
|
||||
|
||||
enum MsgErrorType: Decodable {
|
||||
public enum MsgErrorType: Decodable {
|
||||
case msgSkipped(fromMsgId: Int64, toMsgId: Int64)
|
||||
case msgBadId(msgId: Int64)
|
||||
case msgBadHash
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
func getJSONDecoder() -> JSONDecoder {
|
||||
public func getJSONDecoder() -> JSONDecoder {
|
||||
let jd = JSONDecoder()
|
||||
let fracSeconds = getDateFormatter("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ")
|
||||
let noFracSeconds = getDateFormatter("yyyy-MM-dd'T'HH:mm:ssZZZZZ")
|
||||
@@ -23,7 +23,7 @@ func getJSONDecoder() -> JSONDecoder {
|
||||
return jd
|
||||
}
|
||||
|
||||
func getJSONEncoder() -> JSONEncoder {
|
||||
public func getJSONEncoder() -> JSONEncoder {
|
||||
let je = JSONEncoder()
|
||||
je.dateEncodingStrategy = .iso8601
|
||||
return je
|
||||
@@ -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>
|
||||
|
||||
|
||||
46
blog/20220604-simplex-chat-new-privacy-security-settings.md
Normal file
46
blog/20220604-simplex-chat-new-privacy-security-settings.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# SimpleX Chat v2.2 - the first messaging platform without user identities - 100% private by design!
|
||||
|
||||
**Published:** June 4, 2022
|
||||
|
||||
See [v2 announcement](./20220511-simplex-chat-v2-images-files.md) for more information about SimpleX platform and how it protects your privacy by avoiding user identities of any kind in its design - SimpleX, unlike any other messaging platfom, has no identity keys or any numbers that identify its users.
|
||||
|
||||
## New Privacy and Security settings in version 2.2
|
||||
|
||||
<img src="./images/20220604-privacy-settings.png" width="480">
|
||||
|
||||
### Protect your chats
|
||||
|
||||
To protect your chats you can enable SimpleX Lock. Every time you open the chat after it was in the background for 30 second, you will need to pass biometric or pin code authentication to use the app (provided it is enabled for your device).
|
||||
|
||||
### Save data and avoid sharing you are online
|
||||
|
||||
In case you want to save your mobile data or to avoid showing to your contacts that you are online, you can disable automatic image downloads. For many users it is more convenient to have images downloaded automatically, so it is enabled by default.
|
||||
|
||||
Low resolution image previews would still be shown, the senders have no way to see if you received them or not.
|
||||
|
||||
### Avoid visiting websites of the links you send
|
||||
|
||||
When you receive the links that include link previews, it is fully private - these previews are generated by the sender, and they do not expose your IP address in any way.
|
||||
|
||||
When you send the links, the app automatically downloads the link description and the picture from the website of the link. While it is convenient, it exposes your IP address to the website. To avoid it you can disable sending link previews.
|
||||
|
||||
### Identify any lost messages in the chat
|
||||
|
||||
The app tracks the integrity of the messages you receive by cheching their sequential numbers and by validating that the hash of the previous message matches the hash included in the message – each conversation, effectively, is two blockchains that only you and your contact have access to.
|
||||
|
||||
In case some of the messages are lost, you would see it in the chat. It can happen because of one the following reasons:
|
||||
|
||||
- the messages have expired on the server after 30 days not being delivered.
|
||||
- the messages were removed when the server was restarted. We will add server redundancy later this year to avoid message loss in this case, for now if you see an indication that some messages were lost in the chat, you can check with your contact what it was.
|
||||
- some other app error. Please notify us via chat - we will investigate possible root causes.
|
||||
- the connection is compromised. This is very unlikely, but not an impossible scenario.
|
||||
|
||||
### There is more
|
||||
|
||||
You can discover additional features we are currently testing in Experimental Features - they will be announced later!
|
||||
|
||||
## More information
|
||||
|
||||
See [v1 announcement](./20220112-simplex-chat-v1-released.md) for information on how SimpleX protects the security of the messages.
|
||||
|
||||
Read about SimpleX design in [whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md).
|
||||
@@ -1,8 +1,10 @@
|
||||
# Blog
|
||||
|
||||
May 24, 2022 [Clearing messages for better conversation privacy](./20220524-simplex-chat-better-privacy.md)
|
||||
Jun 4, 2022 [v2.2: the new Privacy and Security settings](./20220604-simplex-chat-new-privacy-security-settings.md)
|
||||
|
||||
May 11, 2022 [Sending images and files in mobile apps](./20220511-simplex-chat-v2-images-files.md)
|
||||
May 24, 2022 [v2.1: clearing messages for better conversation privacy](./20220524-simplex-chat-better-privacy.md)
|
||||
|
||||
May 11, 2022 [v2.0 released - sending images and files in mobile apps](./20220511-simplex-chat-v2-images-files.md)
|
||||
|
||||
Apr 04, 2022 [Instant notifications for SimpleX Chat mobile apps](./20220404-simplex-chat-instant-notifications.md)
|
||||
|
||||
|
||||
BIN
blog/images/20220604-privacy-settings.png
Normal file
BIN
blog/images/20220604-privacy-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 389 KiB |
@@ -3,7 +3,7 @@ packages: .
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 964daf5442e1069634762450bc28cfd69a2968a1
|
||||
tag: c1348aa54fba292d34339d6b111572cb1c74b546
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
25
docs/rfcs/2022-06-03-portable-archive.md
Normal file
25
docs/rfcs/2022-06-03-portable-archive.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Portable archive file format
|
||||
|
||||
## Problems
|
||||
|
||||
- database migration for notifications support
|
||||
- export and import of the database
|
||||
|
||||
The first problem could have been solved in an ad hoc way, but it may cause data loss, so the proposal is to have migration performed via export/import steps.
|
||||
|
||||
Out of scope of this doc - what will be the UX for database migration. It may be fully automatic, via code, with zero user interactions, or it could be via step by step wizard - irrespective of this choice it would include export and import steps.
|
||||
|
||||
# Proposal
|
||||
|
||||
Implement creating archive file and restoring from the archive in Haskell, application would only provide a source and target folders, respectively
|
||||
|
||||
Archive files structure:
|
||||
|
||||
- simplex_v1_chat.db
|
||||
- simplex_v1_agent.db
|
||||
- simplex_v1_files
|
||||
- ...
|
||||
|
||||
Archive file name (includes UTC time):
|
||||
|
||||
simplex-chat.YYYY-MM-DDTHH:MM:SSZ.zip
|
||||
@@ -77,7 +77,7 @@ fi
|
||||
|
||||
chmod +x $BIN_PATH
|
||||
|
||||
echo "$APP_NAME installed sucesfully!"
|
||||
echo "$APP_NAME installed successfully!"
|
||||
|
||||
if [ -z "$(command -v $APP_NAME)" ]; then
|
||||
if [ -n "$($SHELL -c 'echo $ZSH_VERSION')" ]; then
|
||||
|
||||
11
package.yaml
11
package.yaml
@@ -39,6 +39,17 @@ dependencies:
|
||||
- time == 1.9.*
|
||||
- unliftio == 0.2.*
|
||||
- unliftio-core == 0.2.*
|
||||
- zip == 1.7.*
|
||||
|
||||
flags:
|
||||
disable-bzip2:
|
||||
description: removes dependency on bzip2 C library (zip package)
|
||||
manual: True
|
||||
default: True
|
||||
disable-zstd:
|
||||
description: Removes dependency on zstd C library (zip package)
|
||||
manual: True
|
||||
default: True
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
|
||||
15
scripts/nix/README.md
Normal file
15
scripts/nix/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Updating nix package config
|
||||
|
||||
1. Install `nix`, `gawk` and `jq`.
|
||||
|
||||
2. Start nix-shell from repo root:
|
||||
|
||||
```sh
|
||||
nix-shell -p nix-prefetch-git
|
||||
```
|
||||
|
||||
3. Run in nix shell:
|
||||
|
||||
```sh
|
||||
gawk -f ./scripts/nix/update-sha256.awk cabal.project > ./scripts/nix/sha256map.nix
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."964daf5442e1069634762450bc28cfd69a2968a1" = "1vsbiawqlvi6v48ws2rmg5cmp5qphnry3ymg6458p2w8wdm2gsng";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."c1348aa54fba292d34339d6b111572cb1c74b546" = "103hw1h1agy42krf11d98bv3c1w0q0wi2z7r2ll0gmp5xv1r4rf0";
|
||||
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
|
||||
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
|
||||
"https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97";
|
||||
|
||||
@@ -17,9 +17,20 @@ build-type: Simple
|
||||
extra-source-files:
|
||||
README.md
|
||||
|
||||
flag disable-bzip2
|
||||
description: removes dependency on bzip2 C library (zip package)
|
||||
manual: True
|
||||
default: True
|
||||
|
||||
flag disable-zstd
|
||||
description: Removes dependency on zstd C library (zip package)
|
||||
manual: True
|
||||
default: True
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Simplex.Chat
|
||||
Simplex.Chat.Archive
|
||||
Simplex.Chat.Bot
|
||||
Simplex.Chat.Call
|
||||
Simplex.Chat.Controller
|
||||
@@ -83,6 +94,7 @@ library
|
||||
, time ==1.9.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, zip ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
executable simplex-bot
|
||||
@@ -121,6 +133,7 @@ executable simplex-bot
|
||||
, time ==1.9.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, zip ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
executable simplex-bot-advanced
|
||||
@@ -159,6 +172,7 @@ executable simplex-bot-advanced
|
||||
, time ==1.9.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, zip ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
executable simplex-chat
|
||||
@@ -200,6 +214,7 @@ executable simplex-chat
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, zip ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite simplex-chat-test
|
||||
@@ -247,4 +262,5 @@ test-suite simplex-chat-test
|
||||
, time ==1.9.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, zip ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
@@ -41,6 +41,7 @@ import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds)
|
||||
import Data.Time.LocalTime (getCurrentTimeZone, getZonedTime)
|
||||
import Data.Word (Word32)
|
||||
import Simplex.Chat.Archive
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Markdown
|
||||
@@ -49,7 +50,7 @@ import Simplex.Chat.Options (ChatOpts (..), smpServersP)
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (ifM, safeDecodeUtf8, unlessM, whenM)
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Agent
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), defaultAgentConfig)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
@@ -59,10 +60,10 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Client (NtfServer)
|
||||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), PushProvider (..))
|
||||
import Simplex.Messaging.Parsers (base64P, parseAll)
|
||||
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
|
||||
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody, MsgFlags (..))
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (tryError, (<$?>))
|
||||
import Simplex.Messaging.Util (ifM, tryError, unlessM, whenM, (<$?>))
|
||||
import System.Exit (exitFailure, exitSuccess)
|
||||
import System.FilePath (combine, splitExtensions, takeFileName)
|
||||
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout)
|
||||
@@ -134,7 +135,8 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de
|
||||
rcvFiles <- newTVarIO M.empty
|
||||
currentCalls <- atomically TM.empty
|
||||
filesFolder <- newTVarIO Nothing
|
||||
pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder}
|
||||
chatStoreChanged <- newTVarIO False
|
||||
pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder}
|
||||
where
|
||||
resolveServers :: InitialAgentServers -> IO InitialAgentServers
|
||||
resolveServers ss@InitialAgentServers {smp = defaultSMPServers} = case nonEmpty smpServers of
|
||||
@@ -150,6 +152,7 @@ runChatController = race_ notificationSubscriber . agentSubscriber
|
||||
|
||||
startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m (Async ())
|
||||
startChatController user = do
|
||||
asks smpAgent >>= resumeAgentClient
|
||||
s <- asks agentAsync
|
||||
readTVarIO s >>= maybe (start s) pure
|
||||
where
|
||||
@@ -194,13 +197,23 @@ processChatCommand = \case
|
||||
StartChat -> withUser' $ \user ->
|
||||
asks agentAsync >>= readTVarIO >>= \case
|
||||
Just _ -> pure CRChatRunning
|
||||
_ -> startChatController user $> CRChatStarted
|
||||
_ ->
|
||||
ifM
|
||||
(asks chatStoreChanged >>= readTVarIO)
|
||||
(throwChatError CEChatStoreChanged)
|
||||
(startChatController user $> CRChatStarted)
|
||||
APIStopChat -> do
|
||||
ask >>= stopChatController
|
||||
pure CRChatStopped
|
||||
ResubscribeAllConnections -> withUser (subscribeUserConnections resubscribeConnection) $> CRCmdOk
|
||||
SetFilesFolder filesFolder' -> withUser $ \_ -> do
|
||||
createDirectoryIfMissing True filesFolder'
|
||||
ff <- asks filesFolder
|
||||
atomically . writeTVar ff $ Just filesFolder'
|
||||
pure CRCmdOk
|
||||
APIExportArchive cfg -> checkChatStopped $ exportArchive cfg $> CRCmdOk
|
||||
APIImportArchive cfg -> checkChatStopped $ importArchive cfg >> setStoreChanged $> CRCmdOk
|
||||
APIDeleteStorage -> checkChatStopped $ deleteStorage >> setStoreChanged $> CRCmdOk
|
||||
APIGetChats withPCC -> CRApiChats <$> withUser (\user -> withStore $ \st -> getChatPreviews st user withPCC)
|
||||
APIGetChat (ChatRef cType cId) pagination -> withUser $ \user -> case cType of
|
||||
CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\st -> getDirectChat st user cId pagination)
|
||||
@@ -770,6 +783,10 @@ processChatCommand = \case
|
||||
CTDirect -> withStore $ \st -> getContactIdByName st userId name
|
||||
CTGroup -> withStore $ \st -> getGroupIdByName st user name
|
||||
_ -> throwChatError $ CECommandError "not supported"
|
||||
checkChatStopped :: m ChatResponse -> m ChatResponse
|
||||
checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped)
|
||||
setStoreChanged :: m ()
|
||||
setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True)
|
||||
getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64
|
||||
getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of
|
||||
CTDirect -> withStore $ \st -> getDirectChatItemIdByText st userId cId SMDSnd (safeDecodeUtf8 msg)
|
||||
@@ -1115,7 +1132,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
allowAgentConnection conn confId $ XInfo profile
|
||||
INFO connInfo ->
|
||||
saveConnInfo conn connInfo
|
||||
MSG meta msgBody -> do
|
||||
MSG meta _msgFlags msgBody -> do
|
||||
_ <- saveRcvMSG conn (ConnectionId connId) meta msgBody
|
||||
withAckMessage agentConnId meta $ pure ()
|
||||
ackMsgDeliveryEvent conn meta
|
||||
@@ -1128,7 +1145,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
-- TODO add debugging output
|
||||
_ -> pure ()
|
||||
Just ct@Contact {localDisplayName = c, contactId} -> case agentMsg of
|
||||
MSG msgMeta msgBody -> do
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody
|
||||
withAckMessage agentConnId msgMeta $
|
||||
case chatMsgEvent of
|
||||
@@ -1268,7 +1285,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
when (connStatus == ConnReady) $ do
|
||||
notifyMemberConnected gInfo m
|
||||
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct
|
||||
MSG msgMeta msgBody -> do
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody
|
||||
withAckMessage agentConnId msgMeta $
|
||||
case chatMsgEvent of
|
||||
@@ -1327,7 +1344,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
ci <- withStore $ \st -> getChatItemByFileId st user fileId
|
||||
toView $ CRSndFileRcvCancelled ci ft
|
||||
_ -> throwChatError $ CEFileSend fileId err
|
||||
MSG meta _ ->
|
||||
MSG meta _ _ ->
|
||||
withAckMessage agentConnId meta $ pure ()
|
||||
-- TODO print errors
|
||||
ERR _ -> pure ()
|
||||
@@ -1351,7 +1368,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
updateCIFileStatus st user fileId CIFSRcvTransfer
|
||||
getChatItemByFileId st user fileId
|
||||
toView $ CRRcvFileStart ci
|
||||
MSG meta@MsgMeta {recipient = (msgId, _), integrity} msgBody -> withAckMessage agentConnId meta $ do
|
||||
MSG meta@MsgMeta {recipient = (msgId, _), integrity} _ msgBody -> withAckMessage agentConnId meta $ do
|
||||
parseFileChunk msgBody >>= \case
|
||||
FileChunkCancel ->
|
||||
unless cancelled $ do
|
||||
@@ -1894,7 +1911,7 @@ sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentCo
|
||||
sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m ()
|
||||
sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do
|
||||
chunkBytes <- readFileChunk ft chunkNo
|
||||
msgId <- withAgent $ \a -> sendMessage a acId $ smpEncode FileChunk {chunkNo, chunkBytes}
|
||||
msgId <- withAgent $ \a -> sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunk {chunkNo, chunkBytes}
|
||||
withStore $ \st -> updateSndFileChunkMsg st ft chunkNo msgId
|
||||
|
||||
readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString
|
||||
@@ -1988,7 +2005,7 @@ cancelSndFileTransfer ft@SndFileTransfer {agentConnId = AgentConnId acId, fileSt
|
||||
updateSndFileStatus st ft FSCancelled
|
||||
deleteSndFileChunks st ft
|
||||
withAgent $ \a -> do
|
||||
void (sendMessage a acId $ smpEncode FileChunkCancel) `catchError` \_ -> pure ()
|
||||
void (sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunkCancel) `catchError` \_ -> pure ()
|
||||
deleteConnection a acId
|
||||
|
||||
closeFileHandle :: ChatMonad m => Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> m ()
|
||||
@@ -2016,7 +2033,7 @@ sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connS
|
||||
sendDirectMessage :: ChatMonad m => Connection -> ChatMsgEvent -> ConnOrGroupId -> m SndMessage
|
||||
sendDirectMessage conn chatMsgEvent connOrGroupId = do
|
||||
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId
|
||||
deliverMessage conn msgBody msgId
|
||||
deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
|
||||
pure msg
|
||||
|
||||
createSndMessage :: ChatMonad m => ChatMsgEvent -> ConnOrGroupId -> m SndMessage
|
||||
@@ -2029,9 +2046,10 @@ createSndMessage chatMsgEvent connOrGroupId = do
|
||||
directMessage :: ChatMsgEvent -> ByteString
|
||||
directMessage chatMsgEvent = strEncode ChatMessage {msgId = Nothing, chatMsgEvent}
|
||||
|
||||
deliverMessage :: ChatMonad m => Connection -> MsgBody -> MessageId -> m ()
|
||||
deliverMessage conn@Connection {connId} msgBody msgId = do
|
||||
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgBody
|
||||
deliverMessage :: ChatMonad m => Connection -> CMEventTag -> MsgBody -> MessageId -> m ()
|
||||
deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do
|
||||
let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
|
||||
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody
|
||||
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
|
||||
withStore $ \st -> createSndMsgDelivery st sndMsgDelivery msgId
|
||||
|
||||
@@ -2051,10 +2069,12 @@ sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do
|
||||
forM_ (filter memberCurrent members) $ \m@GroupMember {groupMemberId} ->
|
||||
case memberConn m of
|
||||
Nothing -> withStore $ \st -> createPendingGroupMessage st groupMemberId msgId introId_
|
||||
Just conn@Connection {connStatus} ->
|
||||
if not (connStatus == ConnSndReady || connStatus == ConnReady)
|
||||
then unless (connStatus == ConnDeleted) $ withStore (\st -> createPendingGroupMessage st groupMemberId msgId introId_)
|
||||
else (deliverMessage conn msgBody msgId >> postDeliver) `catchError` const (pure ())
|
||||
Just conn@Connection {connStatus}
|
||||
| connStatus == ConnSndReady || connStatus == ConnReady -> do
|
||||
let tag = toCMEventTag chatMsgEvent
|
||||
(deliverMessage conn tag msgBody msgId >> postDeliver) `catchError` const (pure ())
|
||||
| connStatus == ConnDeleted -> pure ()
|
||||
| otherwise -> withStore (\st -> createPendingGroupMessage st groupMemberId msgId introId_)
|
||||
pure msg
|
||||
|
||||
sendPendingGroupMessages :: ChatMonad m => GroupMember -> Connection -> m ()
|
||||
@@ -2062,7 +2082,7 @@ sendPendingGroupMessages GroupMember {groupMemberId, localDisplayName} conn = do
|
||||
pendingMessages <- withStore $ \st -> getPendingGroupMessages st groupMemberId
|
||||
-- TODO ensure order - pending messages interleave with user input messages
|
||||
forM_ pendingMessages $ \PendingGroupMessage {msgId, cmEventTag, msgBody, introId_} -> do
|
||||
deliverMessage conn msgBody msgId
|
||||
deliverMessage conn cmEventTag msgBody msgId
|
||||
withStore (\st -> deletePendingGroupMessage st groupMemberId msgId)
|
||||
when (cmEventTag == XGrpMemFwd_) $ case introId_ of
|
||||
Nothing -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
|
||||
@@ -2212,8 +2232,12 @@ chatCommandP =
|
||||
("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile)
|
||||
<|> ("/user" <|> "/u") $> ShowActiveUser
|
||||
<|> "/_start" $> StartChat
|
||||
<|> "/_stop" $> APIStopChat
|
||||
<|> "/_resubscribe all" $> ResubscribeAllConnections
|
||||
<|> "/_files_folder " *> (SetFilesFolder <$> filePath)
|
||||
<|> "/_db export " *> (APIExportArchive <$> jsonP)
|
||||
<|> "/_db import " *> (APIImportArchive <$> jsonP)
|
||||
<|> "/_db delete" $> APIDeleteStorage
|
||||
<|> "/_get chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False))
|
||||
<|> "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP)
|
||||
<|> "/_get items count=" *> (APIGetChatItems <$> A.decimal)
|
||||
|
||||
81
src/Simplex/Chat/Archive.hs
Normal file
81
src/Simplex/Chat/Archive.hs
Normal file
@@ -0,0 +1,81 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module Simplex.Chat.Archive where
|
||||
|
||||
import qualified Codec.Archive.Zip as Z
|
||||
import Control.Monad.Reader
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Messaging.Agent.Client (agentDbPath)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..))
|
||||
import Simplex.Messaging.Util (whenM)
|
||||
import System.FilePath
|
||||
import UnliftIO.Directory
|
||||
import UnliftIO.STM
|
||||
import UnliftIO.Temporary
|
||||
|
||||
archiveAgentDbFile :: String
|
||||
archiveAgentDbFile = "simplex_v1_agent.db"
|
||||
|
||||
archiveChatDbFile :: String
|
||||
archiveChatDbFile = "simplex_v1_chat.db"
|
||||
|
||||
archiveFilesFolder :: String
|
||||
archiveFilesFolder = "simplex_v1_files"
|
||||
|
||||
exportArchive :: ChatMonad m => ArchiveConfig -> m ()
|
||||
exportArchive ArchiveConfig {archivePath, disableCompression} =
|
||||
withSystemTempDirectory "simplex-chat." $ \dir -> do
|
||||
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
||||
copyFile chatDb $ dir </> archiveChatDbFile
|
||||
copyFile agentDb $ dir </> archiveAgentDbFile
|
||||
forM_ filesPath $ \fp ->
|
||||
copyDirectoryFiles fp $ dir </> archiveFilesFolder
|
||||
let method = if disableCompression == Just True then Z.Store else Z.Deflate
|
||||
Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir
|
||||
|
||||
importArchive :: ChatMonad m => ArchiveConfig -> m ()
|
||||
importArchive ArchiveConfig {archivePath} =
|
||||
withSystemTempDirectory "simplex-chat." $ \dir -> do
|
||||
Z.withArchive archivePath $ Z.unpackInto dir
|
||||
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
||||
backup chatDb
|
||||
backup agentDb
|
||||
copyFile (dir </> archiveChatDbFile) chatDb
|
||||
copyFile (dir </> archiveAgentDbFile) agentDb
|
||||
let filesDir = dir </> archiveFilesFolder
|
||||
forM_ filesPath $ \fp ->
|
||||
whenM (doesDirectoryExist filesDir) $
|
||||
copyDirectoryFiles filesDir fp
|
||||
where
|
||||
backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak"
|
||||
|
||||
copyDirectoryFiles :: MonadIO m => FilePath -> FilePath -> m ()
|
||||
copyDirectoryFiles fromDir toDir = do
|
||||
createDirectoryIfMissing False toDir
|
||||
fs <- listDirectory fromDir
|
||||
forM_ fs $ \f -> do
|
||||
let fn = takeFileName f
|
||||
f' = fromDir </> fn
|
||||
whenM (doesFileExist f') $ copyFile f' $ toDir </> fn
|
||||
|
||||
deleteStorage :: ChatMonad m => m ()
|
||||
deleteStorage = do
|
||||
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
||||
removeFile chatDb
|
||||
removeFile agentDb
|
||||
mapM_ removePathForcibly filesPath
|
||||
|
||||
data StorageFiles = StorageFiles
|
||||
{ chatDb :: FilePath,
|
||||
agentDb :: FilePath,
|
||||
filesPath :: Maybe FilePath
|
||||
}
|
||||
|
||||
storageFiles :: ChatMonad m => m StorageFiles
|
||||
storageFiles = do
|
||||
ChatController {chatStore, filesFolder, smpAgent} <- ask
|
||||
let SQLiteStore {dbFilePath = chatDb} = chatStore
|
||||
agentDb = agentDbPath smpAgent
|
||||
filesPath <- readTVarIO filesFolder
|
||||
pure StorageFiles {chatDb, agentDb, filesPath}
|
||||
@@ -76,6 +76,7 @@ data ChatController = ChatController
|
||||
smpAgent :: AgentClient,
|
||||
agentAsync :: TVar (Maybe (Async ())),
|
||||
chatStore :: SQLiteStore,
|
||||
chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted
|
||||
idsDrg :: TVar ChaChaDRG,
|
||||
inputQ :: TBQueue String,
|
||||
outputQ :: TBQueue (Maybe CorrId, ChatResponse),
|
||||
@@ -100,8 +101,12 @@ data ChatCommand
|
||||
= ShowActiveUser
|
||||
| CreateActiveUser Profile
|
||||
| StartChat
|
||||
| APIStopChat
|
||||
| ResubscribeAllConnections
|
||||
| SetFilesFolder FilePath
|
||||
| APIExportArchive ArchiveConfig
|
||||
| APIImportArchive ArchiveConfig
|
||||
| APIDeleteStorage
|
||||
| APIGetChats {pendingConnections :: Bool}
|
||||
| APIGetChat ChatRef ChatPagination
|
||||
| APIGetChatItems Int
|
||||
@@ -178,6 +183,7 @@ data ChatResponse
|
||||
= CRActiveUser {user :: User}
|
||||
| CRChatStarted
|
||||
| CRChatRunning
|
||||
| CRChatStopped
|
||||
| CRApiChats {chats :: [AChat]}
|
||||
| CRApiChat {chat :: AChat}
|
||||
| CRLastMessages {chatItems :: [AChatItem]}
|
||||
@@ -279,6 +285,9 @@ instance ToJSON ChatResponse where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR"
|
||||
|
||||
data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool}
|
||||
deriving (Show, Generic, FromJSON)
|
||||
|
||||
data ContactSubStatus = ContactSubStatus
|
||||
{ contact :: Contact,
|
||||
contactError :: Maybe ChatError
|
||||
@@ -329,6 +338,8 @@ data ChatErrorType
|
||||
= CENoActiveUser
|
||||
| CEActiveUserExists
|
||||
| CEChatNotStarted
|
||||
| CEChatNotStopped
|
||||
| CEChatStoreChanged
|
||||
| CEInvalidConnReq
|
||||
| CEInvalidChatMessage {message :: String}
|
||||
| CEContactNotReady {contact :: Contact}
|
||||
|
||||
@@ -26,13 +26,15 @@ simplexChatCore cfg@ChatConfig {dbPoolSize, yesToMigrations} opts sendToast chat
|
||||
st <- createStore f dbPoolSize yesToMigrations
|
||||
u <- getCreateActiveUser st
|
||||
cc <- newChatController st (Just u) cfg opts sendToast
|
||||
runSimplexChat u cc chat
|
||||
runSimplexChat opts u cc chat
|
||||
|
||||
runSimplexChat :: User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
|
||||
runSimplexChat u cc chat = do
|
||||
a1 <- async $ chat u cc
|
||||
a2 <- runReaderT (startChatController u) cc
|
||||
waitEither_ a1 a2
|
||||
runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
|
||||
runSimplexChat ChatOpts {maintenance} u cc chat
|
||||
| maintenance = wait =<< async (chat u cc)
|
||||
| otherwise = do
|
||||
a1 <- async $ chat u cc
|
||||
a2 <- runReaderT (startChatController u) cc
|
||||
waitEither_ a1 a2
|
||||
|
||||
sendChatCmd :: ChatController -> String -> IO ChatResponse
|
||||
sendChatCmd cc s = runReaderT (execChatCommand . encodeUtf8 $ T.pack s) cc
|
||||
|
||||
@@ -32,12 +32,12 @@ import GHC.Generics (Generic)
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8)
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Agent.Protocol (AgentErrorType, AgentMsgId, MsgErrorType (..), MsgMeta (..))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, singleFieldJSON, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (MsgBody)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
||||
|
||||
data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection
|
||||
deriving (Show, Generic)
|
||||
|
||||
@@ -54,7 +54,8 @@ mobileChatOpts =
|
||||
logAgent = False,
|
||||
chatCmd = "",
|
||||
chatCmdDelay = 3,
|
||||
chatServerPort = Nothing
|
||||
chatServerPort = Nothing,
|
||||
maintenance = True
|
||||
}
|
||||
|
||||
defaultMobileConfig :: ChatConfig
|
||||
|
||||
@@ -25,7 +25,8 @@ data ChatOpts = ChatOpts
|
||||
logAgent :: Bool,
|
||||
chatCmd :: String,
|
||||
chatCmdDelay :: Int,
|
||||
chatServerPort :: Maybe String
|
||||
chatServerPort :: Maybe String,
|
||||
maintenance :: Bool
|
||||
}
|
||||
|
||||
chatOpts :: FilePath -> FilePath -> Parser ChatOpts
|
||||
@@ -88,7 +89,13 @@ chatOpts appDir defaultDbFileName = do
|
||||
<> help "Run chat server on specified port"
|
||||
<> value Nothing
|
||||
)
|
||||
pure ChatOpts {dbFilePrefix, smpServers, logConnections, logAgent, chatCmd, chatCmdDelay, chatServerPort}
|
||||
maintenance <-
|
||||
switch
|
||||
( long "maintenance"
|
||||
<> short 'm'
|
||||
<> help "Run in maintenance mode (/_start to start chat)"
|
||||
)
|
||||
pure ChatOpts {dbFilePrefix, smpServers, logConnections, logAgent, chatCmd, chatCmdDelay, chatServerPort, maintenance}
|
||||
where
|
||||
defaultDbFilePath = combine appDir defaultDbFileName
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@ import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8)
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (fromTextField_)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
||||
|
||||
data ConnectionEntity
|
||||
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
|
||||
@@ -437,6 +437,16 @@ instance FromField CMEventTag where fromField = fromTextField_ cmEventTagT
|
||||
|
||||
instance ToField CMEventTag where toField = toField . serializeCMEventTag
|
||||
|
||||
hasNotification :: CMEventTag -> Bool
|
||||
hasNotification = \case
|
||||
XMsgNew_ -> True
|
||||
XFile_ -> True
|
||||
XContact_ -> True
|
||||
XGrpInv_ -> True
|
||||
XGrpDel_ -> True
|
||||
XCallInv_ -> True
|
||||
_ -> False
|
||||
|
||||
appToChatMessage :: AppMessage -> Either String ChatMessage
|
||||
appToChatMessage AppMessage {msgId, event, params} = do
|
||||
eventTag <- strDecode $ encodeUtf8 event
|
||||
|
||||
@@ -206,7 +206,6 @@ import Simplex.Chat.Migrations.M20220404_files_status_fields
|
||||
import Simplex.Chat.Migrations.M20220514_profiles_user_id
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (eitherToMaybe)
|
||||
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..))
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, withTransaction)
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||
@@ -214,7 +213,7 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String (StrEncoding (strEncode))
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (ProtocolServer (..), SMPServer, pattern SMPServer)
|
||||
import Simplex.Messaging.Util (liftIOEither, (<$$>))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, liftIOEither, (<$$>))
|
||||
import UnliftIO.STM
|
||||
|
||||
schemaMigrations :: [(String, Query)]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user