mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-26 09:44:55 +00:00
ios: share extension (#4466)
* ios: share extension (#4414) * ios: add share extension target * ios: Add UI * ios: send file from share-sheet * image utils * ShareError * error handling; ui-cleanup * progress bar; completion for direct chat * cleanup * cleanup * ios: unify filter and sort between forward and share sheets * ios: match share sheet styling with the main app * ios: fix text input stroke width * ios: align compose views * more of the same... * ShareAPI * remove combine * minor * Better error descriptions --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: enable file sending workers in share extension (#4474) * ios: align compose background, row height and fallback images for share-sheet (#4467) Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: coordinate database access between share extension, the app and notifications extension (#4472) * ios: database management proposal * Add SEState * Global event loop * minor * reset state * use apiCreateItem for local chats * simplify waiting for suspension * loading bar * Dismiss share sheet with error --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * send image message (#4481) Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: improve share extension completion handling (#4486) * improve completion handling * minor * show only spinner for group send * rework event loop, errorAlert * group chat timeout loading bar * state machine WIP * event loop actor * alert * errors text * default * file error --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: add remaining share types; process attachment in background on launch (#4510) * add remaining share types; process attachment in background on launch * cleanup diff * revert `makeVideoQualityLower` * reduce diff * reduce diff * iOS15 support * process events when sharing link and text * cleanup * remove video file on failure * cleanup CompletionHandler --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: share extension - additional alerts and media previews (#4521) * add remaining share types; process attachment in background on launch * cleanup diff * revert `makeVideoQualityLower` * reduce diff * reduce diff * iOS15 support * process events when sharing link and text * cleanup * remove video file on failure * cleanup CompletionHandler * media previews * network timeout alert * revert framework compiler optimisation flag * suspend chat after sheet dismiss * activate chat * update * fix search * sendMessageColor, file preview, chat deselect, simplify error action * cleanup * interupt database closing when sheet is reopened quickly * cleanup redundant alert check * restore package * refactor previews, remove link preview * show link preview when becomes available * comment * dont fail on invalid image * suspend --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: descriptive database errors (#4527) * ios: set share extension as inactive when suspending chat --------- Co-authored-by: Arturs Krumins <auth@levitatingpineapple.com>
This commit is contained in:
committed by
GitHub
parent
5ee6f40e75
commit
6865515f43
@@ -778,7 +778,7 @@ struct UnreadChatItemCounts: Equatable {
|
||||
var unreadBelow: Int
|
||||
}
|
||||
|
||||
final class Chat: ObservableObject, Identifiable {
|
||||
final class Chat: ObservableObject, Identifiable, ChatLike {
|
||||
@Published var chatInfo: ChatInfo
|
||||
@Published var chatItems: [ChatItem]
|
||||
@Published var chatStats: ChatStats
|
||||
|
||||
@@ -1091,62 +1091,6 @@ func deleteRemoteCtrl(_ rcId: Int64) async throws {
|
||||
try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId))
|
||||
}
|
||||
|
||||
struct ErrorAlert {
|
||||
var title: LocalizedStringKey
|
||||
var message: LocalizedStringKey
|
||||
}
|
||||
|
||||
func getNetworkErrorAlert(_ r: ChatResponse) -> ErrorAlert? {
|
||||
switch r {
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
|
||||
return ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
|
||||
return ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .HOST))):
|
||||
return ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).")
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TRANSPORT(.version)))):
|
||||
return ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).")
|
||||
case let .chatCmdError(_, .errorAgent(.SMP(serverAddress, .PROXY(proxyErr)))):
|
||||
return smpProxyErrorAlert(proxyErr, serverAddress)
|
||||
case let .chatCmdError(_, .errorAgent(.PROXY(proxyServer, relayServer, .protocolError(.PROXY(proxyErr))))):
|
||||
return proxyDestinationErrorAlert(proxyErr, proxyServer, relayServer)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> ErrorAlert? {
|
||||
switch proxyErr {
|
||||
case .BROKER(brokerErr: .TIMEOUT):
|
||||
return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.")
|
||||
case .BROKER(brokerErr: .NETWORK):
|
||||
return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.")
|
||||
case .BROKER(brokerErr: .HOST):
|
||||
return ErrorAlert(title: "Private routing error", message: "Forwarding server address is incompatible with network settings: \(serverHostname(srvAddr)).")
|
||||
case .BROKER(brokerErr: .TRANSPORT(.version)):
|
||||
return ErrorAlert(title: "Private routing error", message: "Forwarding server version is incompatible with network settings: \(serverHostname(srvAddr)).")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> ErrorAlert? {
|
||||
switch proxyErr {
|
||||
case .BROKER(brokerErr: .TIMEOUT):
|
||||
return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.")
|
||||
case .BROKER(brokerErr: .NETWORK):
|
||||
return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.")
|
||||
case .NO_SESSION:
|
||||
return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.")
|
||||
case .BROKER(brokerErr: .HOST):
|
||||
return ErrorAlert(title: "Private routing error", message: "Destination server address of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)) settings.")
|
||||
case .BROKER(brokerErr: .TRANSPORT(.version)):
|
||||
return ErrorAlert(title: "Private routing error", message: "Destination server version of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)).")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func networkErrorAlert(_ r: ChatResponse) -> Alert? {
|
||||
if let alert = getNetworkErrorAlert(r) {
|
||||
return mkAlert(title: alert.title, message: alert.message)
|
||||
|
||||
@@ -36,6 +36,18 @@ private func _suspendChat(timeout: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
let seSubscriber = seMessageSubscriber {
|
||||
switch $0 {
|
||||
case let .state(state):
|
||||
switch state {
|
||||
case .inactive:
|
||||
if AppChatState.shared.value.inactive { activateChat() }
|
||||
case .sendingMessage:
|
||||
if AppChatState.shared.value.canSuspend { suspendChat() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func suspendChat() {
|
||||
suspendLockQueue.sync {
|
||||
_suspendChat(timeout: appSuspendTimeout)
|
||||
|
||||
@@ -112,7 +112,7 @@ struct ChatInfoView: View {
|
||||
case abortSwitchAddressAlert
|
||||
case syncConnectionForceAlert
|
||||
case queueInfo(info: String)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
|
||||
@@ -27,7 +27,7 @@ struct CIRcvDecryptionError: View {
|
||||
case syncNotSupportedContactAlert
|
||||
case syncNotSupportedMemberAlert
|
||||
case decryptionErrorAlert
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -62,7 +62,7 @@ struct CIRcvDecryptionError: View {
|
||||
case .syncNotSupportedContactAlert: return Alert(title: Text("Fix not supported by contact"), message: message())
|
||||
case .syncNotSupportedMemberAlert: return Alert(title: Text("Fix not supported by group member"), message: message())
|
||||
case .decryptionErrorAlert: return Alert(title: Text("Decryption error"), message: message())
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ struct ChatItemForwardingView: View {
|
||||
@FocusState private var searchFocused
|
||||
@State private var alert: SomeAlert?
|
||||
@State private var hasSimplexLink_: Bool?
|
||||
private let chatsToForwardTo = filterChatsToForwardTo()
|
||||
private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats)
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
@@ -67,22 +67,6 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func foundChat(_ chat: Chat, _ searchStr: String) -> Bool {
|
||||
let cInfo = chat.chatInfo
|
||||
return switch cInfo {
|
||||
case let .direct(contact):
|
||||
viewNameContains(cInfo, searchStr) ||
|
||||
contact.profile.displayName.localizedLowercase.contains(searchStr) ||
|
||||
contact.fullName.localizedLowercase.contains(searchStr)
|
||||
default:
|
||||
viewNameContains(cInfo, searchStr)
|
||||
}
|
||||
|
||||
func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool {
|
||||
cInfo.chatViewName.localizedLowercase.contains(s)
|
||||
}
|
||||
}
|
||||
|
||||
private func prohibitedByPref(_ chat: Chat) -> Bool {
|
||||
// preference checks should match checks in compose view
|
||||
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
|
||||
@@ -162,27 +146,6 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func filterChatsToForwardTo() -> [Chat] {
|
||||
var filteredChats = ChatModel.shared.chats.filter { c in
|
||||
c.chatInfo.chatType != .local && canForwardToChat(c)
|
||||
}
|
||||
if let privateNotes = ChatModel.shared.chats.first(where: { $0.chatInfo.chatType == .local }) {
|
||||
filteredChats.insert(privateNotes, at: 0)
|
||||
}
|
||||
return filteredChats
|
||||
}
|
||||
|
||||
private func canForwardToChat(_ chat: Chat) -> Bool {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
|
||||
case let .group(groupInfo): groupInfo.sendMsgEnabled
|
||||
case let .local(noteFolder): noteFolder.sendMsgEnabled
|
||||
case .contactRequest: false
|
||||
case .contactConnection: false
|
||||
case .invalidJSON: false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ChatItemForwardingView(
|
||||
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
|
||||
@@ -190,3 +153,4 @@ private func canForwardToChat(_ chat: Chat) -> Bool {
|
||||
composeState: Binding.constant(ComposeState(message: "hello"))
|
||||
).environmentObject(CurrentColors.toAppTheme())
|
||||
}
|
||||
|
||||
|
||||
@@ -10,35 +10,6 @@ import SwiftUI
|
||||
import LinkPresentation
|
||||
import SimpleXChat
|
||||
|
||||
func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
|
||||
logger.debug("getLinkMetadata: fetching URL preview")
|
||||
LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in
|
||||
if let e = error {
|
||||
logger.error("Error retrieving link metadata: \(e.localizedDescription)")
|
||||
}
|
||||
if let metadata = metadata,
|
||||
let imageProvider = metadata.imageProvider,
|
||||
imageProvider.canLoadObject(ofClass: UIImage.self) {
|
||||
imageProvider.loadObject(ofClass: UIImage.self){ object, error in
|
||||
var linkPreview: LinkPreview? = nil
|
||||
if let error = error {
|
||||
logger.error("Couldn't load image preview from link metadata with error: \(error.localizedDescription)")
|
||||
} else {
|
||||
if let image = object as? UIImage,
|
||||
let resized = resizeImageToStrSize(image, maxDataSize: 14000),
|
||||
let title = metadata.title,
|
||||
let uri = metadata.originalURL {
|
||||
linkPreview = LinkPreview(uri: uri, title: title, image: resized)
|
||||
}
|
||||
}
|
||||
cb(linkPreview)
|
||||
}
|
||||
} else {
|
||||
cb(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposeLinkView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let linkPreview: LinkPreview?
|
||||
|
||||
@@ -67,7 +67,6 @@ struct SendMessageView: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
if progressByTimeout {
|
||||
ProgressView()
|
||||
.scaleEffect(1.4)
|
||||
@@ -87,7 +86,7 @@ struct SendMessageView: View {
|
||||
.padding(.vertical, 1)
|
||||
.background(theme.colors.background)
|
||||
.clipShape(composeShape)
|
||||
.overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true))
|
||||
.overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7))
|
||||
}
|
||||
.onChange(of: composeState.message, perform: { text in updateFont(text) })
|
||||
.onChange(of: composeState.inProgress) { inProgress in
|
||||
|
||||
@@ -35,7 +35,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
|
||||
private enum AddGroupMembersAlert: Identifiable {
|
||||
case prohibitedToInviteIncognito
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -122,7 +122,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
message: Text("You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile")
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedContacts) { _ in
|
||||
|
||||
@@ -40,7 +40,7 @@ struct GroupChatInfoView: View {
|
||||
case blockForAllAlert(mem: GroupMember)
|
||||
case unblockForAllAlert(mem: GroupMember)
|
||||
case removeMemberAlert(mem: GroupMember)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -158,7 +158,7 @@ struct GroupChatInfoView: View {
|
||||
case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem)
|
||||
case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem)
|
||||
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
||||
@@ -22,7 +22,7 @@ struct GroupLinkView: View {
|
||||
|
||||
private enum GroupLinkAlert: Identifiable {
|
||||
case deleteLink
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -113,7 +113,7 @@ struct GroupLinkView: View {
|
||||
}, secondaryButton: .cancel()
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
.onChange(of: groupLinkMemberRole) { _ in
|
||||
|
||||
@@ -37,7 +37,7 @@ struct GroupMemberInfoView: View {
|
||||
case syncConnectionForceAlert
|
||||
case planAndConnectAlert(alert: PlanAndConnectAlert)
|
||||
case queueInfo(info: String)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -237,7 +237,7 @@ struct GroupMemberInfoView: View {
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
|
||||
case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true)
|
||||
case let .queueInfo(info): return queueInfoAlert(info)
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
|
||||
|
||||
@@ -21,7 +21,7 @@ struct ContactConnectionInfo: View {
|
||||
|
||||
enum CCInfoAlert: Identifiable {
|
||||
case deleteInvitationAlert
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -102,7 +102,7 @@ struct ContactConnectionInfo: View {
|
||||
} success: {
|
||||
dismiss()
|
||||
}
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
||||
@@ -64,7 +64,7 @@ struct DatabaseErrorView: View {
|
||||
case let .migrationError(mtrError):
|
||||
titleText("Incompatible database version")
|
||||
fileNameText(dbFile)
|
||||
Text("Error: ") + Text(DatabaseErrorView.mtrErrorDescription(mtrError))
|
||||
Text("Error: ") + Text(mtrErrorDescription(mtrError))
|
||||
}
|
||||
case let .errorSQL(dbFile, migrationSQLError):
|
||||
titleText("Database error")
|
||||
@@ -105,15 +105,6 @@ struct DatabaseErrorView: View {
|
||||
Text("Migrations: \(ms.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
static func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey {
|
||||
switch err {
|
||||
case let .noDown(dbMigrations):
|
||||
return "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))"
|
||||
case let .different(appMigration, dbMigration):
|
||||
return "different migration in the app/database: \(appMigration) / \(dbMigration)"
|
||||
}
|
||||
}
|
||||
|
||||
private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View {
|
||||
PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit)
|
||||
}
|
||||
|
||||
@@ -16,18 +16,10 @@ struct ChatInfoImage: View {
|
||||
var color = Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
|
||||
var body: some View {
|
||||
var iconName: String
|
||||
switch chat.chatInfo {
|
||||
case .direct: iconName = "person.crop.circle.fill"
|
||||
case .group: iconName = "person.2.circle.fill"
|
||||
case .local: iconName = "folder.circle.fill"
|
||||
case .contactRequest: iconName = "person.crop.circle.fill"
|
||||
default: iconName = "circle.fill"
|
||||
}
|
||||
let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color }
|
||||
return ProfileImage(
|
||||
imageStr: chat.chatInfo.image,
|
||||
iconName: iconName,
|
||||
iconName: chatIconName(chat.chatInfo),
|
||||
size: size,
|
||||
color: iconColor
|
||||
)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// VideoUtils.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 25.12.2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import SimpleXChat
|
||||
|
||||
func makeVideoQualityLower(_ input: URL, outputUrl: URL) async -> Bool {
|
||||
let asset: AVURLAsset = AVURLAsset(url: input, options: nil)
|
||||
if let s = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset640x480) {
|
||||
s.outputURL = outputUrl
|
||||
s.outputFileType = .mp4
|
||||
s.metadataItemFilter = AVMetadataItemFilter.forSharing()
|
||||
await s.export()
|
||||
if let err = s.error {
|
||||
logger.error("Failed to export video with error: \(err)")
|
||||
}
|
||||
return s.status == .completed
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -331,7 +331,7 @@ struct MigrateToDevice: View {
|
||||
case let .migrationError(mtrError):
|
||||
("Incompatible database version",
|
||||
nil,
|
||||
"\(NSLocalizedString("Error: ", comment: "")) \(DatabaseErrorView.mtrErrorDescription(mtrError))",
|
||||
"\(NSLocalizedString("Error: ", comment: "")) \(mtrErrorDescription(mtrError))",
|
||||
nil)
|
||||
}
|
||||
default: ("Error", nil, "Unknown error", nil)
|
||||
|
||||
@@ -37,7 +37,7 @@ struct ConnectDesktopView: View {
|
||||
case badInvitationError
|
||||
case badVersionError(version: String?)
|
||||
case desktopDisconnectedError
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -160,7 +160,7 @@ struct ConnectDesktopView: View {
|
||||
case .desktopDisconnectedError:
|
||||
Alert(title: Text("Connection terminated"))
|
||||
case let .error(title, error):
|
||||
Alert(title: Text(title), message: Text(error))
|
||||
mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled(m.activeRemoteCtrl)
|
||||
|
||||
@@ -175,10 +175,6 @@ func testServerConnection(server: Binding<ServerCfg>) async -> ProtocolTestFailu
|
||||
}
|
||||
}
|
||||
|
||||
func serverHostname(_ srv: String) -> String {
|
||||
parseServerAddress(srv)?.hostnames.first ?? srv
|
||||
}
|
||||
|
||||
struct ProtocolServerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProtocolServerView(
|
||||
|
||||
@@ -30,7 +30,7 @@ struct UserAddressView: View {
|
||||
case deleteAddress
|
||||
case profileAddress(on: Bool)
|
||||
case shareOnCreate
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -185,7 +185,7 @@ struct UserAddressView: View {
|
||||
}, secondaryButton: .cancel()
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ struct UserProfilesView: View {
|
||||
case hiddenProfilesNotice
|
||||
case muteProfileAlert
|
||||
case activateUserError(error: String)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -172,7 +172,7 @@ struct UserProfilesView: View {
|
||||
message: Text(err)
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,6 +390,16 @@ func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
let seSubscriber = seMessageSubscriber {
|
||||
switch $0 {
|
||||
case let .state(state):
|
||||
if state == .sendingMessage && NSEChatState.shared.value.canSuspend {
|
||||
logger.debug("NotificationService: seSubscriber app state \(state.rawValue), suspending")
|
||||
suspendChat(fastNSESuspendSchedule.timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var receiverStarted = false
|
||||
let startLock = DispatchSemaphore(value: 1)
|
||||
let suspendLock = DispatchSemaphore(value: 1)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
|
||||
<integer>0</integer>
|
||||
<key>NSExtensionActivationSupportsAttachmentsWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>ShareViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// SEChatState.swift
|
||||
// SimpleX SE
|
||||
//
|
||||
// Created by User on 18/07/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
// SEStateGroupDefault must not be used in the share extension directly, only via this singleton
|
||||
class SEChatState {
|
||||
static let shared = SEChatState()
|
||||
private var value_ = seStateGroupDefault.get()
|
||||
|
||||
var value: SEState {
|
||||
value_
|
||||
}
|
||||
|
||||
func set(_ state: SEState) {
|
||||
seStateGroupDefault.set(state)
|
||||
sendSEState(state)
|
||||
value_ = state
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for other processes to set their state to suspended
|
||||
/// Will wait for maximum of two seconds, since they might not be running
|
||||
func waitForOtherProcessesToSuspend() async {
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
while CFAbsoluteTimeGetCurrent() - startTime < 2 {
|
||||
try? await Task.sleep(nanoseconds: 100 * NSEC_PER_MSEC)
|
||||
if appStateGroupDefault.get() == .suspended &&
|
||||
nseStateGroupDefault.get() == .suspended {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// ShareAPI.swift
|
||||
// SimpleX SE
|
||||
//
|
||||
// Created by User on 15/07/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import OSLog
|
||||
import Foundation
|
||||
import SimpleXChat
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
func apiGetActiveUser() throws -> User? {
|
||||
let r = sendSimpleXCmd(.showActiveUser)
|
||||
switch r {
|
||||
case let .activeUser(user): return user
|
||||
case .chatCmdError(_, .error(.noActiveUser)): return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiStartChat() throws -> Bool {
|
||||
let r = sendSimpleXCmd(.startChat(mainApp: false, enableSndFiles: true))
|
||||
switch r {
|
||||
case .chatStarted: return true
|
||||
case .chatRunning: return false
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiSetNetworkConfig(_ cfg: NetCfg) throws {
|
||||
let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws {
|
||||
let r = sendSimpleXCmd(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
||||
let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetChats(userId: User.ID) throws -> Array<ChatData> {
|
||||
let r = sendSimpleXCmd(.apiGetChats(userId: userId))
|
||||
if case let .apiChats(user: _, chats: chats) = r { return chats }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSendMessage(
|
||||
chatInfo: ChatInfo,
|
||||
cryptoFile: CryptoFile?,
|
||||
msgContent: MsgContent
|
||||
) throws -> AChatItem {
|
||||
let r = sendSimpleXCmd(
|
||||
chatInfo.chatType == .local
|
||||
? .apiCreateChatItem(
|
||||
noteFolderId: chatInfo.apiId,
|
||||
file: cryptoFile,
|
||||
msg: msgContent
|
||||
)
|
||||
: .apiSendMessage(
|
||||
type: chatInfo.chatType,
|
||||
id: chatInfo.apiId,
|
||||
file: cryptoFile,
|
||||
quotedItemId: nil,
|
||||
msg: msgContent,
|
||||
live: false,
|
||||
ttl: nil
|
||||
)
|
||||
)
|
||||
if case let .newChatItem(_, chatItem) = r {
|
||||
return chatItem
|
||||
} else {
|
||||
if let filePath = cryptoFile?.filePath { removeFile(filePath) }
|
||||
throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiActivateChat() throws {
|
||||
chatReopenStore()
|
||||
let r = sendSimpleXCmd(.apiActivateChat(restoreChat: false))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSuspendChat(expired: Bool) {
|
||||
let r = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: expired ? 0 : 3_000000))
|
||||
// Block until `chatSuspended` received or 3 seconds has passed
|
||||
var suspended = false
|
||||
if case .cmdOk = r, !expired {
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
while CFAbsoluteTimeGetCurrent() - startTime < 3 {
|
||||
switch recvSimpleXMsg(messageTimeout: 3_500000) {
|
||||
case .chatSuspended:
|
||||
suspended = false
|
||||
break
|
||||
default: continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if !suspended {
|
||||
_ = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: 0))
|
||||
}
|
||||
logger.debug("close store")
|
||||
chatCloseStore()
|
||||
SEChatState.shared.set(.inactive)
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
//
|
||||
// ShareModel.swift
|
||||
// SimpleX SE
|
||||
//
|
||||
// Created by Levitating Pineapple on 09/07/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import UniformTypeIdentifiers
|
||||
import AVFoundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
/// Maximum size of hex encoded media previews
|
||||
private let MAX_DATA_SIZE: Int64 = 14000
|
||||
|
||||
/// Maximum dimension (width or height) of an image, before passed for processing
|
||||
private let MAX_DOWNSAMPLE_SIZE: Int64 = 2000
|
||||
|
||||
class ShareModel: ObservableObject {
|
||||
@Published var sharedContent: SharedContent?
|
||||
@Published var chats = Array<ChatData>()
|
||||
@Published var profileImages = Dictionary<ChatInfo.ID, UIImage>()
|
||||
@Published var search = String()
|
||||
@Published var comment = String()
|
||||
@Published var selected: ChatData?
|
||||
@Published var isLoaded = false
|
||||
@Published var bottomBar: BottomBar = .loadingSpinner
|
||||
@Published var errorAlert: ErrorAlert?
|
||||
|
||||
enum BottomBar {
|
||||
case sendButton
|
||||
case loadingSpinner
|
||||
case loadingBar(progress: Double)
|
||||
|
||||
var isLoading: Bool {
|
||||
switch self {
|
||||
case .sendButton: false
|
||||
case .loadingSpinner: true
|
||||
case .loadingBar: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var completion: () -> Void = {
|
||||
fatalError("completion has not been set")
|
||||
}
|
||||
|
||||
private var itemProvider: NSItemProvider?
|
||||
|
||||
var isSendDisbled: Bool { sharedContent == nil || selected == nil }
|
||||
|
||||
var filteredChats: Array<ChatData> {
|
||||
search.isEmpty
|
||||
? filterChatsToForwardTo(chats: chats)
|
||||
: filterChatsToForwardTo(chats: chats)
|
||||
.filter { foundChat($0, search.localizedLowercase) }
|
||||
}
|
||||
|
||||
func setup(context: NSExtensionContext) {
|
||||
if let item = context.inputItems.first as? NSExtensionItem,
|
||||
let itemProvider = item.attachments?.first {
|
||||
self.itemProvider = itemProvider
|
||||
self.completion = {
|
||||
ShareModel.CompletionHandler.isEventLoopEnabled = false
|
||||
context.completeRequest(returningItems: [item]) {
|
||||
apiSuspendChat(expired: $0)
|
||||
}
|
||||
}
|
||||
// Init Chat
|
||||
Task {
|
||||
if let e = initChat() {
|
||||
await MainActor.run { errorAlert = e }
|
||||
} else {
|
||||
// Load Chats
|
||||
Task {
|
||||
switch fetchChats() {
|
||||
case let .success(chats):
|
||||
// Decode base64 images on background thread
|
||||
let profileImages = chats.reduce(into: Dictionary<ChatInfo.ID, UIImage>()) { dict, chatData in
|
||||
if let profileImage = chatData.chatInfo.image,
|
||||
let uiImage = UIImage(base64Encoded: profileImage) {
|
||||
dict[chatData.id] = uiImage
|
||||
}
|
||||
}
|
||||
await MainActor.run {
|
||||
self.chats = chats
|
||||
self.profileImages = profileImages
|
||||
withAnimation { isLoaded = true }
|
||||
}
|
||||
case let .failure(error):
|
||||
await MainActor.run { errorAlert = error }
|
||||
}
|
||||
}
|
||||
// Process Attachment
|
||||
Task {
|
||||
switch await self.itemProvider!.sharedContent() {
|
||||
case let .success(chatItemContent):
|
||||
await MainActor.run {
|
||||
self.sharedContent = chatItemContent
|
||||
self.bottomBar = .sendButton
|
||||
if case let .text(string) = chatItemContent { comment = string }
|
||||
}
|
||||
case let .failure(errorAlert):
|
||||
await MainActor.run { self.errorAlert = errorAlert }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send() {
|
||||
if let sharedContent, let selected {
|
||||
Task {
|
||||
await MainActor.run { self.bottomBar = .loadingSpinner }
|
||||
do {
|
||||
SEChatState.shared.set(.sendingMessage)
|
||||
await waitForOtherProcessesToSuspend()
|
||||
let ci = try apiSendMessage(
|
||||
chatInfo: selected.chatInfo,
|
||||
cryptoFile: sharedContent.cryptoFile,
|
||||
msgContent: sharedContent.msgContent(comment: self.comment)
|
||||
)
|
||||
if selected.chatInfo.chatType == .local {
|
||||
completion()
|
||||
} else {
|
||||
await MainActor.run { self.bottomBar = .loadingBar(progress: .zero) }
|
||||
if let e = await handleEvents(
|
||||
isGroupChat: ci.chatInfo.chatType == .group,
|
||||
isWithoutFile: sharedContent.cryptoFile == nil,
|
||||
chatItemId: ci.chatItem.id
|
||||
) {
|
||||
await MainActor.run { errorAlert = e }
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if let e = error as? ErrorAlert {
|
||||
await MainActor.run { errorAlert = e }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func initChat() -> ErrorAlert? {
|
||||
do {
|
||||
if hasChatCtrl() {
|
||||
try apiActivateChat()
|
||||
} else {
|
||||
registerGroupDefaults()
|
||||
haskell_init_se()
|
||||
let (_, result) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation())
|
||||
if let e = migrationError(result) { return e }
|
||||
try apiSetAppFilePaths(
|
||||
filesFolder: getAppFilesDirectory().path,
|
||||
tempFolder: getTempFilesDirectory().path,
|
||||
assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path
|
||||
)
|
||||
let isRunning = try apiStartChat()
|
||||
logger.log(level: .debug, "chat started, running: \(isRunning)")
|
||||
}
|
||||
try apiSetNetworkConfig(getNetCfg())
|
||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||
} catch { return ErrorAlert(error) }
|
||||
return nil
|
||||
}
|
||||
|
||||
private func migrationError(_ r: DBMigrationResult) -> ErrorAlert? {
|
||||
let useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
let storedDBKey = kcDatabasePassword.get()
|
||||
// This switch duplicates DatabaseErrorView.
|
||||
// TODO allow entering passphrase and make messages the same as in DatabaseErrorView.
|
||||
return switch r {
|
||||
case .errorNotADatabase:
|
||||
if useKeychain && storedDBKey != nil && storedDBKey != "" {
|
||||
ErrorAlert(
|
||||
title: "Wrong database passphrase",
|
||||
message: "Database passphrase is different from saved in the keychain."
|
||||
)
|
||||
} else {
|
||||
ErrorAlert(
|
||||
title: "Encrypted database",
|
||||
message: "Sharing is not supported when passphrase is not stored in KeyChain."
|
||||
)
|
||||
}
|
||||
case let .errorMigration(_, migrationError):
|
||||
switch migrationError {
|
||||
case .upgrade:
|
||||
ErrorAlert(
|
||||
title: "Database upgrade required",
|
||||
message: "Open the app to upgrade the database."
|
||||
)
|
||||
case .downgrade:
|
||||
ErrorAlert(
|
||||
title: "Database downgrade required",
|
||||
message: "Open the app to downgrade the database."
|
||||
)
|
||||
case let .migrationError(mtrError):
|
||||
ErrorAlert(
|
||||
title: "Incompatible database version",
|
||||
message: mtrErrorDescription(mtrError)
|
||||
)
|
||||
}
|
||||
case let .errorSQL(_, migrationSQLError):
|
||||
ErrorAlert(
|
||||
title: "Database error",
|
||||
message: "Error: \(migrationSQLError)"
|
||||
)
|
||||
case .errorKeychain:
|
||||
ErrorAlert(
|
||||
title: "Keychain error",
|
||||
message: "Cannot access keychain to save database password"
|
||||
)
|
||||
case .invalidConfirmation:
|
||||
ErrorAlert("Invalid migration confirmation")
|
||||
case let .unknown(json):
|
||||
ErrorAlert(
|
||||
title: "Database error",
|
||||
message: "Unknown database error: \(json)"
|
||||
)
|
||||
case .ok: nil
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchChats() -> Result<Array<ChatData>, ErrorAlert> {
|
||||
do {
|
||||
guard let user = try apiGetActiveUser() else {
|
||||
return .failure(
|
||||
ErrorAlert(
|
||||
title: "No active profile",
|
||||
message: "Please create a profile in the SimpleX app"
|
||||
)
|
||||
)
|
||||
}
|
||||
return .success(try apiGetChats(userId: user.id))
|
||||
} catch {
|
||||
return .failure(ErrorAlert(error))
|
||||
}
|
||||
}
|
||||
|
||||
actor CompletionHandler {
|
||||
static var isEventLoopEnabled = false
|
||||
private var fileCompleted = false
|
||||
private var messageCompleted = false
|
||||
|
||||
func completeFile() { fileCompleted = true }
|
||||
|
||||
func completeMessage() { messageCompleted = true }
|
||||
|
||||
var isRunning: Bool {
|
||||
Self.isEventLoopEnabled && !(fileCompleted && messageCompleted)
|
||||
}
|
||||
}
|
||||
|
||||
/// Polls and processes chat events
|
||||
/// Returns when message sending has completed optionally returning and error.
|
||||
private func handleEvents(isGroupChat: Bool, isWithoutFile: Bool, chatItemId: ChatItem.ID) async -> ErrorAlert? {
|
||||
func isMessage(for item: AChatItem?) -> Bool {
|
||||
item.map { $0.chatItem.id == chatItemId } ?? false
|
||||
}
|
||||
|
||||
CompletionHandler.isEventLoopEnabled = true
|
||||
let ch = CompletionHandler()
|
||||
if isWithoutFile { await ch.completeFile() }
|
||||
var networkTimeout = CFAbsoluteTimeGetCurrent()
|
||||
while await ch.isRunning {
|
||||
if CFAbsoluteTimeGetCurrent() - networkTimeout > 30 {
|
||||
networkTimeout = CFAbsoluteTimeGetCurrent()
|
||||
await MainActor.run {
|
||||
self.errorAlert = ErrorAlert(title: "No network connection") {
|
||||
Button("Keep Trying", role: .cancel) { }
|
||||
Button("Dismiss Sheet", role: .destructive) { self.completion() }
|
||||
}
|
||||
}
|
||||
}
|
||||
switch recvSimpleXMsg(messageTimeout: 1_000_000) {
|
||||
case let .sndFileProgressXFTP(_, ci, _, sentSize, totalSize):
|
||||
guard isMessage(for: ci) else { continue }
|
||||
networkTimeout = CFAbsoluteTimeGetCurrent()
|
||||
await MainActor.run {
|
||||
withAnimation {
|
||||
let progress = Double(sentSize) / Double(totalSize)
|
||||
bottomBar = .loadingBar(progress: progress)
|
||||
}
|
||||
}
|
||||
case let .sndFileCompleteXFTP(_, ci, _):
|
||||
guard isMessage(for: ci) else { continue }
|
||||
if isGroupChat {
|
||||
await MainActor.run { bottomBar = .loadingSpinner }
|
||||
}
|
||||
await ch.completeFile()
|
||||
if await !ch.isRunning { break }
|
||||
case let .chatItemStatusUpdated(_, ci):
|
||||
guard isMessage(for: ci) else { continue }
|
||||
if let (title, message) = ci.chatItem.meta.itemStatus.statusInfo {
|
||||
// `title` and `message` already localized and interpolated
|
||||
return ErrorAlert(
|
||||
title: "\(title)",
|
||||
message: "\(message)"
|
||||
)
|
||||
} else if case let .sndSent(sndProgress) = ci.chatItem.meta.itemStatus {
|
||||
switch sndProgress {
|
||||
case .complete:
|
||||
await ch.completeMessage()
|
||||
case .partial:
|
||||
if isGroupChat {
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 5 * NSEC_PER_SEC)
|
||||
await ch.completeMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .sndFileError(_, ci, _, errorMessage):
|
||||
guard isMessage(for: ci) else { continue }
|
||||
if let ci { cleanupFile(ci) }
|
||||
return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)")
|
||||
case let .sndFileWarning(_, ci, _, errorMessage):
|
||||
guard isMessage(for: ci) else { continue }
|
||||
if let ci { cleanupFile(ci) }
|
||||
return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)")
|
||||
case let .chatError(_, chatError):
|
||||
return ErrorAlert(chatError)
|
||||
case let .chatCmdError(_, chatError):
|
||||
return ErrorAlert(chatError)
|
||||
default: continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func fileErrorInfo(_ ci: AChatItem?) -> String? {
|
||||
switch ci?.chatItem.file?.fileStatus {
|
||||
case let .sndError(e): e.errorInfo
|
||||
case let .sndWarning(e): e.errorInfo
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chat Item Content extracted from `NSItemProvider` without the comment
|
||||
enum SharedContent {
|
||||
case image(preview: String, cryptoFile: CryptoFile)
|
||||
case movie(preview: String, duration: Int, cryptoFile: CryptoFile)
|
||||
case url(preview: LinkPreview)
|
||||
case text(string: String)
|
||||
case data(cryptoFile: CryptoFile)
|
||||
|
||||
var cryptoFile: CryptoFile? {
|
||||
switch self {
|
||||
case let .image(_, cryptoFile): cryptoFile
|
||||
case let .movie(_, _, cryptoFile): cryptoFile
|
||||
case .url: nil
|
||||
case .text: nil
|
||||
case let .data(cryptoFile): cryptoFile
|
||||
}
|
||||
}
|
||||
|
||||
func msgContent(comment: String) -> MsgContent {
|
||||
switch self {
|
||||
case let .image(preview, _): .image(text: comment, image: preview)
|
||||
case let .movie(preview, duration, _): .video(text: comment, image: preview, duration: duration)
|
||||
case let .url(preview): .link(text: comment, preview: preview)
|
||||
case .text: .text(comment)
|
||||
case .data: .file(comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSItemProvider {
|
||||
fileprivate func sharedContent() async -> Result<SharedContent, ErrorAlert> {
|
||||
if let type = firstMatching(of: [.image, .movie, .fileURL, .url, .text]) {
|
||||
switch type {
|
||||
// Prepare Image message
|
||||
case .image:
|
||||
|
||||
// Animated
|
||||
return if hasItemConformingToTypeIdentifier(UTType.gif.identifier) {
|
||||
if let url = try? await inPlaceUrl(type: type),
|
||||
let data = try? Data(contentsOf: url),
|
||||
let image = UIImage(data: data),
|
||||
let cryptoFile = saveFile(data, generateNewFileName("IMG", "gif"), encrypted: privacyEncryptLocalFilesGroupDefault.get()),
|
||||
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) {
|
||||
.success(.image(preview: preview, cryptoFile: cryptoFile))
|
||||
} else { .failure(ErrorAlert("Error preparing message")) }
|
||||
|
||||
// Static
|
||||
} else {
|
||||
if let url = try? await inPlaceUrl(type: type),
|
||||
let image = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE),
|
||||
let cryptoFile = saveImage(image),
|
||||
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) {
|
||||
.success(.image(preview: preview, cryptoFile: cryptoFile))
|
||||
} else { .failure(ErrorAlert("Error preparing message")) }
|
||||
}
|
||||
|
||||
// Prepare Movie message
|
||||
case .movie:
|
||||
if let url = try? await inPlaceUrl(type: type),
|
||||
let trancodedUrl = await transcodeVideo(from: url),
|
||||
let (image, duration) = AVAsset(url: trancodedUrl).generatePreview(),
|
||||
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE),
|
||||
let cryptoFile = moveTempFileFromURL(trancodedUrl) {
|
||||
try? FileManager.default.removeItem(at: trancodedUrl)
|
||||
return .success(.movie(preview: preview, duration: duration, cryptoFile: cryptoFile))
|
||||
} else { return .failure(ErrorAlert("Error preparing message")) }
|
||||
|
||||
// Prepare Data message
|
||||
case .fileURL:
|
||||
if let url = try? await inPlaceUrl(type: .data) {
|
||||
if isFileTooLarge(for: url) {
|
||||
let sizeString = ByteCountFormatter.string(
|
||||
fromByteCount: Int64(getMaxFileSize(.xftp)),
|
||||
countStyle: .binary
|
||||
)
|
||||
return .failure(
|
||||
ErrorAlert(
|
||||
title: "Large file!",
|
||||
message: "Currently maximum supported file size is \(sizeString)."
|
||||
)
|
||||
)
|
||||
}
|
||||
if let file = saveFileFromURL(url) {
|
||||
return .success(.data(cryptoFile: file))
|
||||
}
|
||||
}
|
||||
return .failure(ErrorAlert("Error preparing file"))
|
||||
|
||||
// Prepare Link message
|
||||
case .url:
|
||||
if let url = try? await loadItem(forTypeIdentifier: type.identifier) as? URL {
|
||||
let content: SharedContent =
|
||||
// Option to disable previews needs to be taken into account
|
||||
// if let linkPreview = await getLinkPreview(for: url) {
|
||||
// .url(preview: linkPreview)
|
||||
// } else {
|
||||
.text(string: url.absoluteString)
|
||||
// }
|
||||
return .success(content)
|
||||
} else { return .failure(ErrorAlert("Error preparing message")) }
|
||||
|
||||
// Prepare Text message
|
||||
case .text:
|
||||
return if let text = try? await loadItem(forTypeIdentifier: type.identifier) as? String {
|
||||
.success(.text(string: text))
|
||||
} else { .failure(ErrorAlert("Error preparing message")) }
|
||||
default: return .failure(ErrorAlert("Unsupported format"))
|
||||
}
|
||||
} else {
|
||||
return .failure(ErrorAlert("Unsupported format"))
|
||||
}
|
||||
}
|
||||
|
||||
private func inPlaceUrl(type: UTType) async throws -> URL {
|
||||
try await withCheckedThrowingContinuation { cont in
|
||||
let _ = loadInPlaceFileRepresentation(forTypeIdentifier: type.identifier) { url, bool, error in
|
||||
if let url = url {
|
||||
cont.resume(returning: url)
|
||||
} else if let error = error {
|
||||
cont.resume(throwing: error)
|
||||
} else {
|
||||
fatalError("Either `url` or `error` must be present")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func firstMatching(of types: Array<UTType>) -> UTType? {
|
||||
for type in types {
|
||||
if hasItemConformingToTypeIdentifier(type.identifier) { return type }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate func transcodeVideo(from input: URL) async -> URL? {
|
||||
let outputUrl = URL(
|
||||
fileURLWithPath: generateNewFileName(
|
||||
getTempFilesDirectory().path + "/" + "video", "mp4",
|
||||
fullPath: true
|
||||
)
|
||||
)
|
||||
if await makeVideoQualityLower(input, outputUrl: outputUrl) {
|
||||
return outputUrl
|
||||
} else {
|
||||
try? FileManager.default.removeItem(at: outputUrl)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func isFileTooLarge(for url: URL) -> Bool {
|
||||
fileSize(url)
|
||||
.map { $0 > getMaxFileSize(.xftp) }
|
||||
?? false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// ShareView.swift
|
||||
// SimpleX SE
|
||||
//
|
||||
// Created by Levitating Pineapple on 09/07/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ShareView: View {
|
||||
@ObservedObject var model: ShareModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack(alignment: .bottom) {
|
||||
if model.isLoaded {
|
||||
List(model.filteredChats) { chat in
|
||||
HStack {
|
||||
profileImage(
|
||||
chatInfoId: chat.chatInfo.id,
|
||||
systemFallback: chatIconName(chat.chatInfo),
|
||||
size: 30
|
||||
)
|
||||
Text(chat.chatInfo.displayName)
|
||||
Spacer()
|
||||
radioButton(selected: chat == model.selected)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { model.selected = model.selected == chat ? nil : chat }
|
||||
.tag(chat)
|
||||
}
|
||||
} else {
|
||||
ProgressView().frame(maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Share")
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
switch model.bottomBar {
|
||||
case .sendButton:
|
||||
compose(isLoading: false)
|
||||
case .loadingSpinner:
|
||||
compose(isLoading: true)
|
||||
case .loadingBar(let progress):
|
||||
loadingBar(progress: progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(
|
||||
text: $model.search,
|
||||
placement: .navigationBarDrawer(displayMode: .always)
|
||||
)
|
||||
.alert($model.errorAlert) { alert in
|
||||
Button("Ok") { model.completion() }
|
||||
}
|
||||
}
|
||||
|
||||
private func compose(isLoading: Bool) -> some View {
|
||||
VStack(spacing: .zero) {
|
||||
Divider()
|
||||
if let content = model.sharedContent {
|
||||
itemPreview(content)
|
||||
}
|
||||
HStack {
|
||||
Group {
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
TextField("Comment", text: $model.comment, axis: .vertical)
|
||||
} else {
|
||||
TextField("Comment", text: $model.comment)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.disabled(isLoading)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 4)
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Button(action: model.send) {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.resizable()
|
||||
}
|
||||
.disabled(model.isSendDisbled)
|
||||
}
|
||||
}
|
||||
.frame(width: 28, height: 28)
|
||||
.padding(6)
|
||||
|
||||
}
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7)
|
||||
)
|
||||
.padding(8)
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
}
|
||||
|
||||
@ViewBuilder private func itemPreview(_ content: SharedContent) -> some View {
|
||||
switch content {
|
||||
case let .image(preview, _): imagePreview(preview)
|
||||
case let .movie(preview, _, _): imagePreview(preview)
|
||||
case let .url(linkPreview): imagePreview(linkPreview.image)
|
||||
case let .data(cryptoFile):
|
||||
previewArea {
|
||||
Image(systemName: "doc.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30, height: 30)
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.padding(.leading, 4)
|
||||
Text(cryptoFile.filePath)
|
||||
}
|
||||
case .text: EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func imagePreview(_ img: String) -> some View {
|
||||
if let img = UIImage(base64Encoded: img) {
|
||||
previewArea {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(minHeight: 40, maxHeight: 60)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func previewArea<V: View>(@ViewBuilder content: @escaping () -> V) -> some View {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
content()
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 1)
|
||||
.frame(minHeight: 54)
|
||||
.background {
|
||||
switch colorScheme {
|
||||
case .light: LightColorPaletteApp.sentMessage
|
||||
case .dark: DarkColorPaletteApp.sentMessage
|
||||
@unknown default: Color(.tertiarySystemBackground)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
private func loadingBar(progress: Double) -> some View {
|
||||
VStack {
|
||||
Text("Sending File")
|
||||
ProgressView(value: progress)
|
||||
}
|
||||
.padding()
|
||||
.background(Material.ultraThin)
|
||||
}
|
||||
|
||||
private func profileImage(chatInfoId: ChatInfo.ID, systemFallback: String, size: Double) -> some View {
|
||||
Group {
|
||||
if let uiImage = model.profileImages[chatInfoId] {
|
||||
Image(uiImage: uiImage).resizable()
|
||||
} else {
|
||||
Image(systemName: systemFallback).resizable()
|
||||
}
|
||||
}
|
||||
.foregroundStyle(Color(.tertiaryLabel))
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(RoundedRectangle(cornerRadius: size * 0.225, style: .continuous))
|
||||
}
|
||||
|
||||
private func radioButton(selected: Bool) -> some View {
|
||||
Image(systemName: selected ? "checkmark.circle.fill" : "circle")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(selected ? Color.accentColor : Color(.tertiaryLabel))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// ShareViewController.swift
|
||||
// SimpleX SE
|
||||
//
|
||||
// Created by Levitating Pineapple on 08/07/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
/// Extension Entry point
|
||||
/// System will create this controller each time share sheet is invoked
|
||||
/// using `NSExtensionPrincipalClass` in the info.plist
|
||||
@objc(ShareViewController)
|
||||
class ShareViewController: UIHostingController<ShareView> {
|
||||
private let model = ShareModel()
|
||||
// Assuming iOS continues to only allow single share sheet to be presented at once
|
||||
static var isVisible: Bool = false
|
||||
|
||||
@objc init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
||||
super.init(rootView: ShareView(model: model))
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
ShareModel.CompletionHandler.isEventLoopEnabled = false
|
||||
model.setup(context: extensionContext!)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
logger.debug("ShareSheet will appear")
|
||||
super.viewWillAppear(animated)
|
||||
Self.isVisible = true
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
logger.debug("ShareSheet will dissappear")
|
||||
super.viewWillDisappear(animated)
|
||||
ShareModel.CompletionHandler.isEventLoopEnabled = false
|
||||
Self.isVisible = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)chat.simplex.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -183,7 +183,6 @@
|
||||
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
|
||||
64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; };
|
||||
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
|
||||
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; };
|
||||
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; };
|
||||
8C74C3E52C1B900600039E77 /* ThemeTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */; };
|
||||
8C74C3E72C1B901900039E77 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C852B072C1086D100BA61E8 /* Color.swift */; };
|
||||
@@ -199,9 +198,18 @@
|
||||
8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */; };
|
||||
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
|
||||
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
||||
CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; };
|
||||
CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */; };
|
||||
CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */; };
|
||||
CE38A29A2C3FCA54005ED185 /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */; };
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = CE38A29B2C3FCD72005ED185 /* SwiftyGif */; };
|
||||
CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */; };
|
||||
CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; };
|
||||
CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; };
|
||||
CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
CEE723D02C3C21C90009AE93 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
CEE723F02C3D25C70009AE93 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723EF2C3D25C70009AE93 /* ShareView.swift */; };
|
||||
CEE723F22C3D25ED0009AE93 /* ShareModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723F12C3D25ED0009AE93 /* ShareModel.swift */; };
|
||||
CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */; };
|
||||
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
|
||||
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
|
||||
@@ -241,6 +249,20 @@
|
||||
remoteGlobalIDString = 5CE2BA672845308900EC33A6;
|
||||
remoteInfo = SimpleXChat;
|
||||
};
|
||||
CEE723AF2C3BD3D70009AE93 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 5CA059BE279559F40002BEB4 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = CEE723A62C3BD3D70009AE93;
|
||||
remoteInfo = "SimpleX SE";
|
||||
};
|
||||
CEE723D12C3C21C90009AE93 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 5CA059BE279559F40002BEB4 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 5CE2BA672845308900EC33A6;
|
||||
remoteInfo = SimpleXChat;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -261,11 +283,23 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */,
|
||||
5CE2BA9D284555F500EC33A6 /* SimpleX NSE.appex in Embed App Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CEE723D32C3C21C90009AE93 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
CEE723D02C3C21C90009AE93 /* SimpleXChat.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -494,7 +528,6 @@
|
||||
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
|
||||
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
|
||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
|
||||
8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = "<group>"; };
|
||||
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||
8C74C3EB2C1B92A900039E77 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
|
||||
8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaper.swift; sourceTree = "<group>"; };
|
||||
@@ -509,7 +542,17 @@
|
||||
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = "<group>"; };
|
||||
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
|
||||
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
|
||||
CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = "<group>"; };
|
||||
CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = "<group>"; };
|
||||
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
|
||||
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = "<group>"; };
|
||||
CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = "<group>"; };
|
||||
CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX SE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
CEE723AE2C3BD3D70009AE93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX SE.entitlements"; sourceTree = "<group>"; };
|
||||
CEE723EF2C3D25C70009AE93 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = "<group>"; };
|
||||
CEE723F12C3D25ED0009AE93 /* ShareModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareModel.swift; sourceTree = "<group>"; };
|
||||
CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseList.swift; sourceTree = "<group>"; };
|
||||
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
|
||||
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
@@ -684,7 +727,6 @@
|
||||
64466DCB29FFE3E800E3D48D /* MailView.swift */,
|
||||
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */,
|
||||
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */,
|
||||
8C05382D2B39887E006436DC /* VideoUtils.swift */,
|
||||
8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */,
|
||||
8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */,
|
||||
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */,
|
||||
@@ -703,6 +745,7 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */,
|
||||
5CA059C2279559F40002BEB4 /* Shared */,
|
||||
5CDCAD462818589900503DA2 /* SimpleX NSE */,
|
||||
CEE723A82C3BD3D70009AE93 /* SimpleX SE */,
|
||||
5CA059DA279559F40002BEB4 /* Tests iOS */,
|
||||
5CE2BA692845308900EC33A6 /* SimpleXChat */,
|
||||
5CA059CB279559F40002BEB4 /* Products */,
|
||||
@@ -733,6 +776,7 @@
|
||||
5CA059D7279559F40002BEB4 /* Tests iOS.xctest */,
|
||||
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */,
|
||||
5CE2BA682845308900EC33A6 /* SimpleXChat.framework */,
|
||||
CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -856,10 +900,12 @@
|
||||
5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */,
|
||||
5CDCAD7428188D2900503DA2 /* APITypes.swift */,
|
||||
5C5E5D3C282447AB00B0488A /* CallTypes.swift */,
|
||||
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */,
|
||||
5C9FD96A27A56D4D0075386C /* JSON.swift */,
|
||||
5CDCAD7D2818941F00503DA2 /* API.swift */,
|
||||
5CDCAD80281A7E2700503DA2 /* Notifications.swift */,
|
||||
5CBD2859295711D700EC2CF4 /* ImageUtils.swift */,
|
||||
CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */,
|
||||
64DAE1502809D9F5000DA960 /* FileUtils.swift */,
|
||||
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */,
|
||||
5C00168028C4FE760094D739 /* KeyChain.swift */,
|
||||
@@ -974,6 +1020,20 @@
|
||||
path = Theme;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE723A82C3BD3D70009AE93 /* SimpleX SE */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */,
|
||||
CEE723AE2C3BD3D70009AE93 /* Info.plist */,
|
||||
CEDE70212C48FD9500233B1F /* SEChatState.swift */,
|
||||
CE1EB0E32C459A660099D896 /* ShareAPI.swift */,
|
||||
CEE723F12C3D25ED0009AE93 /* ShareModel.swift */,
|
||||
CEE723EF2C3D25C70009AE93 /* ShareView.swift */,
|
||||
CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */,
|
||||
);
|
||||
path = "SimpleX SE";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -1005,6 +1065,7 @@
|
||||
dependencies = (
|
||||
5CE2BA6F2845308900EC33A6 /* PBXTargetDependency */,
|
||||
5CE2BA9F284555F500EC33A6 /* PBXTargetDependency */,
|
||||
CEE723B02C3BD3D70009AE93 /* PBXTargetDependency */,
|
||||
);
|
||||
name = "SimpleX (iOS)";
|
||||
packageProductDependencies = (
|
||||
@@ -1076,6 +1137,24 @@
|
||||
productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
CEE723A62C3BD3D70009AE93 /* SimpleX SE */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CEE723B42C3BD3D70009AE93 /* Build configuration list for PBXNativeTarget "SimpleX SE" */;
|
||||
buildPhases = (
|
||||
CEE723A32C3BD3D70009AE93 /* Sources */,
|
||||
CEE723A52C3BD3D70009AE93 /* Resources */,
|
||||
CEE723D32C3C21C90009AE93 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
CEE723D22C3C21C90009AE93 /* PBXTargetDependency */,
|
||||
);
|
||||
name = "SimpleX SE";
|
||||
productName = "SimpleX SE";
|
||||
productReference = CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -1083,7 +1162,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1330;
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1340;
|
||||
ORGANIZATIONNAME = "SimpleX Chat";
|
||||
TargetAttributes = {
|
||||
@@ -1103,6 +1182,9 @@
|
||||
CreatedOnToolsVersion = 13.3;
|
||||
LastSwiftMigration = 1330;
|
||||
};
|
||||
CEE723A62C3BD3D70009AE93 = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 5CA059C1279559F40002BEB4 /* Build configuration list for PBXProject "SimpleX" */;
|
||||
@@ -1144,6 +1226,7 @@
|
||||
5CA059C9279559F40002BEB4 /* SimpleX (iOS) */,
|
||||
5CA059D6279559F40002BEB4 /* Tests iOS */,
|
||||
5CDCAD442818589900503DA2 /* SimpleX NSE */,
|
||||
CEE723A62C3BD3D70009AE93 /* SimpleX SE */,
|
||||
5CE2BA672845308900EC33A6 /* SimpleXChat */,
|
||||
);
|
||||
};
|
||||
@@ -1183,6 +1266,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CEE723A52C3BD3D70009AE93 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -1299,7 +1389,6 @@
|
||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */,
|
||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
|
||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
|
||||
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */,
|
||||
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */,
|
||||
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */,
|
||||
5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */,
|
||||
@@ -1368,6 +1457,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */,
|
||||
CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */,
|
||||
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */,
|
||||
5CE2BA97284537A800EC33A6 /* dummy.m in Sources */,
|
||||
5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */,
|
||||
@@ -1384,10 +1474,23 @@
|
||||
8C74C3E52C1B900600039E77 /* ThemeTypes.swift in Sources */,
|
||||
5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */,
|
||||
8C74C3E82C1B905B00039E77 /* ChatWallpaperTypes.swift in Sources */,
|
||||
CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */,
|
||||
5CE2BA8E284533A300EC33A6 /* API.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CEE723A32C3BD3D70009AE93 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */,
|
||||
CEE723F02C3D25C70009AE93 /* ShareView.swift in Sources */,
|
||||
CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */,
|
||||
CEE723F22C3D25ED0009AE93 /* ShareModel.swift in Sources */,
|
||||
CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
@@ -1412,6 +1515,16 @@
|
||||
target = 5CE2BA672845308900EC33A6 /* SimpleXChat */;
|
||||
targetProxy = 5CE2BAA82845617C00EC33A6 /* PBXContainerItemProxy */;
|
||||
};
|
||||
CEE723B02C3BD3D70009AE93 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = CEE723A62C3BD3D70009AE93 /* SimpleX SE */;
|
||||
targetProxy = CEE723AF2C3BD3D70009AE93 /* PBXContainerItemProxy */;
|
||||
};
|
||||
CEE723D22C3C21C90009AE93 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 5CE2BA672845308900EC33A6 /* SimpleXChat */;
|
||||
targetProxy = CEE723D12C3C21C90009AE93 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
@@ -1922,6 +2035,74 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CEE723B22C3BD3D70009AE93 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "SimpleX SE/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "SimpleX SE";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 SimpleX Chat. All rights reserved.";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CEE723B32C3BD3D70009AE93 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "SimpleX SE/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "SimpleX SE";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 SimpleX Chat. All rights reserved.";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -1970,6 +2151,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CEE723B42C3BD3D70009AE93 /* Build configuration list for PBXNativeTarget "SimpleX SE" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CEE723B22C3BD3D70009AE93 /* Debug */,
|
||||
CEE723B32C3BD3D70009AE93 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CEE723A62C3BD3D70009AE93"
|
||||
BuildableName = "SimpleX SE.appex"
|
||||
BlueprintName = "SimpleX SE"
|
||||
ReferencedContainer = "container:SimpleX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5CA059C9279559F40002BEB4"
|
||||
BuildableName = "SimpleX.app"
|
||||
BlueprintName = "SimpleX (iOS)"
|
||||
ReferencedContainer = "container:SimpleX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
askForAppToLaunch = "Yes"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5CA059C9279559F40002BEB4"
|
||||
BuildableName = "SimpleX.app"
|
||||
BlueprintName = "SimpleX (iOS)"
|
||||
ReferencedContainer = "container:SimpleX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
askForAppToLaunch = "Yes"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5CA059C9279559F40002BEB4"
|
||||
BuildableName = "SimpleX.app"
|
||||
BlueprintName = "SimpleX (iOS)"
|
||||
ReferencedContainer = "container:SimpleX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -117,10 +117,10 @@ public func sendSimpleXCmd(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) -> Chat
|
||||
}
|
||||
|
||||
// in microseconds
|
||||
let MESSAGE_TIMEOUT: Int32 = 15_000_000
|
||||
public let MESSAGE_TIMEOUT: Int32 = 15_000_000
|
||||
|
||||
public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil) -> ChatResponse? {
|
||||
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), MESSAGE_TIMEOUT) {
|
||||
public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> ChatResponse? {
|
||||
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout) {
|
||||
let s = fromCString(cjson)
|
||||
return s == "" ? nil : chatResponse(s)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public let appSuspendTimeout: Int = 15 // seconds
|
||||
|
||||
let GROUP_DEFAULT_APP_STATE = "appState"
|
||||
let GROUP_DEFAULT_NSE_STATE = "nseState"
|
||||
let GROUP_DEFAULT_SE_STATE = "seState"
|
||||
let GROUP_DEFAULT_DB_CONTAINER = "dbContainer"
|
||||
public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart"
|
||||
public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun"
|
||||
@@ -136,6 +137,11 @@ public enum NSEState: String, Codable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SEState: String, Codable {
|
||||
case inactive
|
||||
case sendingMessage
|
||||
}
|
||||
|
||||
public enum DBContainer: String {
|
||||
case documents
|
||||
case group
|
||||
@@ -155,6 +161,12 @@ public let nseStateGroupDefault = EnumDefault<NSEState>(
|
||||
withDefault: .suspended // so that NSE that was never launched does not delay the app from resuming
|
||||
)
|
||||
|
||||
public let seStateGroupDefault = EnumDefault<SEState>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_SE_STATE,
|
||||
withDefault: .inactive
|
||||
)
|
||||
|
||||
// inactive app states do not include "stopped" state
|
||||
public func allowBackgroundRefresh() -> Bool {
|
||||
appStateGroupDefault.get().inactive && nseStateGroupDefault.get().inactive
|
||||
|
||||
@@ -1463,7 +1463,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct ChatData: Decodable, Identifiable, Hashable {
|
||||
public struct ChatData: Decodable, Identifiable, Hashable, ChatLike {
|
||||
public var chatInfo: ChatInfo
|
||||
public var chatItems: [ChatItem]
|
||||
public var chatStats: ChatStats
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// ChatUtils.swift
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Levitating Pineapple on 15/07/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol ChatLike {
|
||||
var chatInfo: ChatInfo { get}
|
||||
var chatItems: [ChatItem] { get }
|
||||
var chatStats: ChatStats { get }
|
||||
}
|
||||
|
||||
public func filterChatsToForwardTo<C: ChatLike>(chats: [C]) -> [C] {
|
||||
var filteredChats = chats.filter { c in
|
||||
c.chatInfo.chatType != .local && canForwardToChat(c.chatInfo)
|
||||
}
|
||||
if let privateNotes = chats.first(where: { $0.chatInfo.chatType == .local }) {
|
||||
filteredChats.insert(privateNotes, at: 0)
|
||||
}
|
||||
return filteredChats
|
||||
}
|
||||
|
||||
public func foundChat(_ chat: ChatLike, _ searchStr: String) -> Bool {
|
||||
let cInfo = chat.chatInfo
|
||||
return switch cInfo {
|
||||
case let .direct(contact):
|
||||
viewNameContains(cInfo, searchStr) ||
|
||||
contact.profile.displayName.localizedLowercase.contains(searchStr) ||
|
||||
contact.fullName.localizedLowercase.contains(searchStr)
|
||||
default:
|
||||
viewNameContains(cInfo, searchStr)
|
||||
}
|
||||
|
||||
func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool {
|
||||
cInfo.chatViewName.localizedLowercase.contains(s)
|
||||
}
|
||||
}
|
||||
|
||||
private func canForwardToChat(_ cInfo: ChatInfo) -> Bool {
|
||||
switch cInfo {
|
||||
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
|
||||
case let .group(groupInfo): groupInfo.sendMsgEnabled
|
||||
case let .local(noteFolder): noteFolder.sendMsgEnabled
|
||||
case .contactRequest: false
|
||||
case .contactConnection: false
|
||||
case .invalidJSON: false
|
||||
}
|
||||
}
|
||||
|
||||
public func chatIconName(_ cInfo: ChatInfo) -> String {
|
||||
switch cInfo {
|
||||
case .direct: "person.crop.circle.fill"
|
||||
case .group: "person.2.circle.fill"
|
||||
case .local: "folder.circle.fill"
|
||||
case .contactRequest: "person.crop.circle.fill"
|
||||
default: "circle.fill"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
//
|
||||
// ErrorAlert.swift
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Levitating Pineapple on 20/07/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct ErrorAlert: Error {
|
||||
public let title: LocalizedStringKey
|
||||
public let message: LocalizedStringKey?
|
||||
public let actions: Optional<() -> AnyView>
|
||||
|
||||
public init(
|
||||
title: LocalizedStringKey,
|
||||
message: LocalizedStringKey? = nil
|
||||
) {
|
||||
self.title = title
|
||||
self.message = message
|
||||
self.actions = nil
|
||||
}
|
||||
|
||||
public init<A: View>(
|
||||
title: LocalizedStringKey,
|
||||
message: LocalizedStringKey? = nil,
|
||||
@ViewBuilder actions: @escaping () -> A
|
||||
) {
|
||||
self.title = title
|
||||
self.message = message
|
||||
self.actions = { AnyView(actions()) }
|
||||
}
|
||||
|
||||
public init(_ title: LocalizedStringKey) {
|
||||
self = ErrorAlert(title: title)
|
||||
}
|
||||
|
||||
public init(_ error: any Error) {
|
||||
self = if let chatResponse = error as? ChatResponse {
|
||||
ErrorAlert(chatResponse)
|
||||
} else {
|
||||
ErrorAlert(LocalizedStringKey(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
|
||||
public init(_ chatError: ChatError) {
|
||||
self = ErrorAlert("\(chatErrorString(chatError))")
|
||||
}
|
||||
|
||||
public init(_ chatResponse: ChatResponse) {
|
||||
self = if let networkErrorAlert = getNetworkErrorAlert(chatResponse) {
|
||||
networkErrorAlert
|
||||
} else {
|
||||
ErrorAlert("\(responseError(chatResponse))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedStringKey: @unchecked Sendable { }
|
||||
|
||||
extension View {
|
||||
/// Bridges ``ErrorAlert`` to the generic alert API.
|
||||
/// - Parameters:
|
||||
/// - errorAlert: Binding to the Error, which is rendered in the alert
|
||||
/// - actions: View Builder containing action buttons.
|
||||
/// System defaults to `Ok` dismiss error action, when no actions are provided.
|
||||
/// System implicitly adds `Cancel` action, if a destructive action is present
|
||||
///
|
||||
/// - Returns: View, which displays ErrorAlert?, when set.
|
||||
@ViewBuilder public func alert<A: View>(
|
||||
_ errorAlert: Binding<ErrorAlert?>,
|
||||
@ViewBuilder actions: (ErrorAlert) -> A = { _ in EmptyView() }
|
||||
) -> some View {
|
||||
if let alert = errorAlert.wrappedValue {
|
||||
self.alert(
|
||||
alert.title,
|
||||
isPresented: Binding<Bool>(
|
||||
get: { errorAlert.wrappedValue != nil },
|
||||
set: { if !$0 { errorAlert.wrappedValue = nil } }
|
||||
),
|
||||
actions: {
|
||||
if let actions_ = alert.actions {
|
||||
actions_()
|
||||
} else {
|
||||
actions(alert)
|
||||
}
|
||||
},
|
||||
message: {
|
||||
if let message = alert.message { Text(message) }
|
||||
}
|
||||
)
|
||||
} else { self }
|
||||
}
|
||||
}
|
||||
|
||||
public func getNetworkErrorAlert(_ r: ChatResponse) -> ErrorAlert? {
|
||||
switch r {
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
|
||||
return ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
|
||||
return ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .HOST))):
|
||||
return ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).")
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TRANSPORT(.version)))):
|
||||
return ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).")
|
||||
case let .chatCmdError(_, .errorAgent(.SMP(serverAddress, .PROXY(proxyErr)))):
|
||||
return smpProxyErrorAlert(proxyErr, serverAddress)
|
||||
case let .chatCmdError(_, .errorAgent(.PROXY(proxyServer, relayServer, .protocolError(.PROXY(proxyErr))))):
|
||||
return proxyDestinationErrorAlert(proxyErr, proxyServer, relayServer)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> ErrorAlert? {
|
||||
switch proxyErr {
|
||||
case .BROKER(brokerErr: .TIMEOUT):
|
||||
return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.")
|
||||
case .BROKER(brokerErr: .NETWORK):
|
||||
return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.")
|
||||
case .BROKER(brokerErr: .HOST):
|
||||
return ErrorAlert(title: "Private routing error", message: "Forwarding server address is incompatible with network settings: \(serverHostname(srvAddr)).")
|
||||
case .BROKER(brokerErr: .TRANSPORT(.version)):
|
||||
return ErrorAlert(title: "Private routing error", message: "Forwarding server version is incompatible with network settings: \(serverHostname(srvAddr)).")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> ErrorAlert? {
|
||||
switch proxyErr {
|
||||
case .BROKER(brokerErr: .TIMEOUT):
|
||||
return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.")
|
||||
case .BROKER(brokerErr: .NETWORK):
|
||||
return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.")
|
||||
case .NO_SESSION:
|
||||
return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.")
|
||||
case .BROKER(brokerErr: .HOST):
|
||||
return ErrorAlert(title: "Private routing error", message: "Destination server address of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)) settings.")
|
||||
case .BROKER(brokerErr: .TRANSPORT(.version)):
|
||||
return ErrorAlert(title: "Private routing error", message: "Destination server version of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)).")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func serverHostname(_ srv: String) -> String {
|
||||
parseServerAddress(srv)?.hostnames.first ?? srv
|
||||
}
|
||||
|
||||
public func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey {
|
||||
switch err {
|
||||
case let .noDown(dbMigrations):
|
||||
"database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))"
|
||||
case let .different(appMigration, dbMigration):
|
||||
"different migration in the app/database: \(appMigration) / \(dbMigration)"
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
import SwiftyGif
|
||||
import LinkPresentation
|
||||
|
||||
public func getLoadedFileSource(_ file: CIFile?) -> CryptoFile? {
|
||||
if let file = file, file.loaded {
|
||||
@@ -158,6 +159,34 @@ public func imageHasAlpha(_ img: UIImage) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
/// Reduces image size, while consuming less RAM
|
||||
///
|
||||
/// Used by ShareExtension to downsize large images
|
||||
/// before passing them to regular image processing pipeline
|
||||
/// to avoid exceeding 120MB memory
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: Location of the image data
|
||||
/// - size: Maximum dimension (width or height)
|
||||
/// - Returns: Downsampled image or `nil`, if the image can't be located
|
||||
public func downsampleImage(at url: URL, to size: Int64) -> UIImage? {
|
||||
autoreleasepool {
|
||||
if let source = CGImageSourceCreateWithURL(url as CFURL, nil) {
|
||||
CGImageSourceCreateThumbnailAtIndex(
|
||||
source,
|
||||
Int.zero,
|
||||
[
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceShouldCacheImmediately: true,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: String(size) as CFString
|
||||
] as CFDictionary
|
||||
)
|
||||
.map { UIImage(cgImage: $0) }
|
||||
} else { nil }
|
||||
}
|
||||
}
|
||||
|
||||
public func saveFileFromURL(_ url: URL) -> CryptoFile? {
|
||||
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
|
||||
let savedFile: CryptoFile?
|
||||
@@ -281,6 +310,21 @@ private func dropPrefix(_ s: String, _ prefix: String) -> String {
|
||||
s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s
|
||||
}
|
||||
|
||||
public func makeVideoQualityLower(_ input: URL, outputUrl: URL) async -> Bool {
|
||||
let asset: AVURLAsset = AVURLAsset(url: input, options: nil)
|
||||
if let s = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset640x480) {
|
||||
s.outputURL = outputUrl
|
||||
s.outputFileType = .mp4
|
||||
s.metadataItemFilter = AVMetadataItemFilter.forSharing()
|
||||
await s.export()
|
||||
if let err = s.error {
|
||||
logger.error("Failed to export video with error: \(err)")
|
||||
}
|
||||
return s.status == .completed
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
extension AVAsset {
|
||||
public func generatePreview() -> (UIImage, Int)? {
|
||||
let generator = AVAssetImageGenerator(asset: self)
|
||||
@@ -348,3 +392,39 @@ extension UIImage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
|
||||
logger.debug("getLinkMetadata: fetching URL preview")
|
||||
LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in
|
||||
if let e = error {
|
||||
logger.error("Error retrieving link metadata: \(e.localizedDescription)")
|
||||
}
|
||||
if let metadata = metadata,
|
||||
let imageProvider = metadata.imageProvider,
|
||||
imageProvider.canLoadObject(ofClass: UIImage.self) {
|
||||
imageProvider.loadObject(ofClass: UIImage.self){ object, error in
|
||||
var linkPreview: LinkPreview? = nil
|
||||
if let error = error {
|
||||
logger.error("Couldn't load image preview from link metadata with error: \(error.localizedDescription)")
|
||||
} else {
|
||||
if let image = object as? UIImage,
|
||||
let resized = resizeImageToStrSize(image, maxDataSize: 14000),
|
||||
let title = metadata.title,
|
||||
let uri = metadata.originalURL {
|
||||
linkPreview = LinkPreview(uri: uri, title: title, image: resized)
|
||||
}
|
||||
}
|
||||
cb(linkPreview)
|
||||
}
|
||||
} else {
|
||||
logger.error("Could not load link preview image")
|
||||
cb(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func getLinkPreview(for url: URL) async -> LinkPreview? {
|
||||
await withCheckedContinuation { cont in
|
||||
getLinkPreview(url: url) { cont.resume(returning: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ public typealias AppSubscriber = SharedFileSubscriber<ProcessMessage<AppProcessM
|
||||
|
||||
public typealias NSESubscriber = SharedFileSubscriber<ProcessMessage<NSEProcessMessage>>
|
||||
|
||||
public typealias SESubscriber = SharedFileSubscriber<ProcessMessage<SEProcessMessage>>
|
||||
|
||||
public class SharedFileSubscriber<Message: Codable>: NSObject, NSFilePresenter {
|
||||
var fileURL: URL
|
||||
public var presentedItemURL: URL?
|
||||
@@ -57,6 +59,8 @@ let appMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent(
|
||||
|
||||
let nseMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent("chat.simplex.app.SimpleX-NSE.messages", isDirectory: false)
|
||||
|
||||
let seMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent("chat.simplex.app.SimpleX-SE.messages", isDirectory: false)
|
||||
|
||||
public struct ProcessMessage<Message: Codable>: Codable {
|
||||
var createdAt: Date = Date.now
|
||||
var message: Message
|
||||
@@ -70,6 +74,10 @@ public enum NSEProcessMessage: Codable {
|
||||
case state(state: NSEState)
|
||||
}
|
||||
|
||||
public enum SEProcessMessage: Codable {
|
||||
case state(state: SEState)
|
||||
}
|
||||
|
||||
public func sendAppProcessMessage(_ message: AppProcessMessage) {
|
||||
SharedFileSubscriber.notify(url: appMessagesSharedFile, message: ProcessMessage(message: message))
|
||||
}
|
||||
@@ -78,6 +86,10 @@ public func sendNSEProcessMessage(_ message: NSEProcessMessage) {
|
||||
SharedFileSubscriber.notify(url: nseMessagesSharedFile, message: ProcessMessage(message: message))
|
||||
}
|
||||
|
||||
public func sendSEProcessMessage(_ message: SEProcessMessage) {
|
||||
SharedFileSubscriber.notify(url: seMessagesSharedFile, message: ProcessMessage(message: message))
|
||||
}
|
||||
|
||||
public func appMessageSubscriber(onMessage: @escaping (AppProcessMessage) -> Void) -> AppSubscriber {
|
||||
SharedFileSubscriber(fileURL: appMessagesSharedFile) { (msg: ProcessMessage<AppProcessMessage>) in
|
||||
onMessage(msg.message)
|
||||
@@ -90,6 +102,12 @@ public func nseMessageSubscriber(onMessage: @escaping (NSEProcessMessage) -> Voi
|
||||
}
|
||||
}
|
||||
|
||||
public func seMessageSubscriber(onMessage: @escaping (SEProcessMessage) -> Void) -> SESubscriber {
|
||||
SharedFileSubscriber(fileURL: seMessagesSharedFile) { (msg: ProcessMessage<SEProcessMessage>) in
|
||||
onMessage(msg.message)
|
||||
}
|
||||
}
|
||||
|
||||
public func sendAppState(_ state: AppState) {
|
||||
sendAppProcessMessage(.state(state: state))
|
||||
}
|
||||
@@ -97,3 +115,7 @@ public func sendAppState(_ state: AppState) {
|
||||
public func sendNSEState(_ state: NSEState) {
|
||||
sendNSEProcessMessage(.state(state: state))
|
||||
}
|
||||
|
||||
public func sendSEState(_ state: SEState) {
|
||||
sendSEProcessMessage(.state(state: state))
|
||||
}
|
||||
|
||||
@@ -39,3 +39,19 @@ void haskell_init_nse(void) {
|
||||
char **pargv = argv;
|
||||
hs_init_with_rtsopts(&argc, &pargv);
|
||||
}
|
||||
|
||||
void haskell_init_se(void) {
|
||||
int argc = 7;
|
||||
char *argv[] = {
|
||||
"simplex",
|
||||
"+RTS", // requires `hs_init_with_rtsopts`
|
||||
"-A1m", // chunk size for new allocations
|
||||
"-H1m", // initial heap size
|
||||
"-F0.5", // heap growth triggering GC
|
||||
"-Fd1", // memory return
|
||||
"-c", // compacting garbage collector
|
||||
0
|
||||
};
|
||||
char **pargv = argv;
|
||||
hs_init_with_rtsopts(&argc, &pargv);
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ void haskell_init(void);
|
||||
|
||||
void haskell_init_nse(void);
|
||||
|
||||
void haskell_init_se(void);
|
||||
|
||||
#endif /* hs_init_h */
|
||||
|
||||
Reference in New Issue
Block a user