Compare commits

..

1 Commits

Author SHA1 Message Date
Evgeny Poberezkin
0366afbcce rfc: user per-service per-device identities 2023-07-02 14:03:55 +01:00
897 changed files with 19027 additions and 62769 deletions

View File

@@ -52,19 +52,15 @@ jobs:
- os: ubuntu-20.04
cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-20_04-x86-64
desktop_asset_name: simplex-desktop-ubuntu-20_04-x86_64.deb
- os: ubuntu-22.04
cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-22_04-x86-64
desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb
- os: macos-latest
cache_path: ~/.cabal/store
asset_name: simplex-chat-macos-x86-64
desktop_asset_name: simplex-desktop-macos-x86_64.dmg
- os: windows-latest
cache_path: C:/cabal
asset_name: simplex-chat-windows-x86-64
desktop_asset_name: simplex-desktop-windows-x86_64.msi
steps:
- name: Configure pagefile (Windows)
if: matrix.os == 'windows-latest'
@@ -103,10 +99,6 @@ jobs:
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
echo " flags: +openssl" >> cabal.project.local
- name: Install AppImage dependencies
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
run: sudo apt install -y desktop-file-utils
- name: Install pkg-config for Mac
if: matrix.os == 'macos-latest'
run: brew install pkg-config
@@ -119,86 +111,23 @@ jobs:
echo "package direct-sqlcipher" >> cabal.project.local
echo " flags: +openssl" >> cabal.project.local
- name: Unix build CLI
id: unix_cli_build
- name: Unix build
id: unix_build
if: matrix.os != 'windows-latest'
shell: bash
run: |
cabal build --enable-tests
echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)"
- name: Unix upload CLI binary to release
- name: Unix upload binary to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.unix_cli_build.outputs.bin_path }}
file: ${{ steps.unix_build.outputs.bin_path }}
asset_name: ${{ matrix.asset_name }}
tag: ${{ github.ref }}
- name: Setup Java
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/setup-java@v3
with:
distribution: 'corretto'
java-version: '17'
cache: 'gradle'
- name: Linux build desktop
id: linux_desktop_build
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
shell: bash
run: |
scripts/desktop/build-lib-linux.sh
cd apps/multiplatform
./gradlew packageDeb
echo "::set-output name=package_path::$(echo $PWD/release/main/deb/simplex_*_amd64.deb)"
- name: Linux make AppImage
id: linux_appimage_build
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
shell: bash
run: |
scripts/desktop/make-appimage-linux.sh
echo "::set-output name=appimage_path::$(echo $PWD/apps/multiplatform/release/main/*imple*.AppImage)"
- name: Mac build desktop
id: mac_desktop_build
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest'
shell: bash
run: |
scripts/desktop/build-lib-mac.sh
cd apps/multiplatform
./gradlew packageDmg
echo "::set-output name=package_path::$(echo $PWD/release/main/dmg/SimpleX-*.dmg)"
- name: Linux upload desktop package to release
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.linux_desktop_build.outputs.package_path }}
asset_name: ${{ matrix.desktop_asset_name }}
tag: ${{ github.ref }}
- name: Linux upload AppImage to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.linux_appimage_build.outputs.appimage_path }}
asset_name: simplex-desktop-x86_64.AppImage
tag: ${{ github.ref }}
- name: Mac upload desktop package to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.mac_desktop_build.outputs.package_path }}
asset_name: ${{ matrix.desktop_asset_name }}
tag: ${{ github.ref }}
- name: Unix test
if: matrix.os != 'windows-latest'
timeout-minutes: 30

1
.gitignore vendored
View File

@@ -53,7 +53,6 @@ website/src/docs/
website/translations.json
website/src/img/images/
website/src/images/
website/src/js/lottie.min.js
# Generated files
website/package/generated*

View File

@@ -207,8 +207,6 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent updates:
[July 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
@@ -339,8 +337,8 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
- ✅ Message reactions
- ✅ Message editing history
- ✅ Reduced battery and traffic usage in large groups.
- ✅ Message delivery confirmation (with sender opt-out per contact).
- 🏗 Desktop client.
- 🏗 Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
- SMP queue redundancy and rotation (manual is supported).
- Include optional message into connection request sent via contact address.
- Local app files encryption.

View File

@@ -14,28 +14,9 @@ class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
application.registerForRemoteNotifications()
if #available(iOS 17.0, *) { trackKeyboard() }
return true
}
@available(iOS 17.0, *)
private func trackKeyboard() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
@available(iOS 17.0, *)
@objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
ChatModel.shared.keyboardHeight = keyboardFrame.cgRectValue.height
}
}
@available(iOS 17.0, *)
@objc func keyboardWillHide(_ notification: Notification) {
ChatModel.shared.keyboardHeight = 0
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
@@ -61,7 +42,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
m.notificationMode != .off {
if let verification = ntfData["verification"] as? String,
let nonce = ntfData["nonce"] as? String {
if let token = m.deviceToken {
if let token = ChatModel.shared.deviceToken {
logger.debug("AppDelegate: didReceiveRemoteNotification: verification, confirming \(verification)")
Task {
do {
@@ -81,7 +62,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
} else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages {
logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages")
if appStateGroupDefault.get().inactive && m.ntfEnablePeriodic {
if appStateGroupDefault.get().inactive {
receiveMessages(completionHandler)
} else {
completionHandler(.noData)
@@ -95,7 +76,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
func applicationWillTerminate(_ application: UIApplication) {
logger.debug("DEBUGGING: AppDelegate: applicationWillTerminate")
logger.debug("AppDelegate: applicationWillTerminate")
ChatModel.shared.filesToDelete.forEach {
removeFile($0)
}

View File

@@ -28,17 +28,6 @@ struct ContentView: View {
@State private var showWhatsNew = false
@State private var showChooseLAMode = false
@State private var showSetPasscode = false
@State private var chatListActionSheet: ChatListActionSheet? = nil
private enum ChatListActionSheet: Identifiable {
case connectViaUrl(action: ConnReqType, link: String)
var id: String {
switch self {
case .connectViaUrl: return "connectViaUrl \(link)"
}
}
}
var body: some View {
ZStack {
@@ -91,11 +80,6 @@ struct ContentView: View {
if case .onboardingComplete = step,
chatModel.currentUser != nil {
mainView()
.actionSheet(item: $chatListActionSheet) { sheet in
switch sheet {
case let .connectViaUrl(action, link): return connectViaUrlSheet(action, link)
}
}
} else {
OnboardingView(onboarding: step)
}
@@ -148,15 +132,10 @@ struct ContentView: View {
}
}
prefShowLANotice = true
connectViaUrl()
}
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
.sheet(isPresented: $showWhatsNew) {
WhatsNewView()
}
if chatModel.setDeliveryReceipts {
SetDeliveryReceiptsView()
}
IncomingCallView()
}
.onContinueUserActivity("INStartCallIntent", perform: processUserActivity)
@@ -197,13 +176,10 @@ struct ContentView: View {
}
private func runAuthenticate() {
logger.debug("DEBUGGING: runAuthenticate")
if !prefPerformLA {
userAuthorized = true
} else {
logger.debug("DEBUGGING: before dismissAllSheets")
dismissAllSheets(animated: false) {
logger.debug("DEBUGGING: in dismissAllSheets callback")
chatModel.chatId = nil
justAuthenticate()
}
@@ -214,7 +190,7 @@ struct ContentView: View {
userAuthorized = false
let laMode = privacyLocalAuthModeDefault.get()
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
logger.debug("DEBUGGING: authenticate callback: \(String(describing: laResult))")
logger.debug("authenticate callback: \(String(describing: laResult))")
switch (laResult) {
case .success:
userAuthorized = true
@@ -283,38 +259,36 @@ struct ContentView: View {
secondaryButton: .cancel()
)
}
}
func connectViaUrl() {
let m = ChatModel.shared
if let url = m.appOpenUrl {
m.appOpenUrl = nil
var path = url.path
logger.debug("ContentView.connectViaUrl path: \(path)")
if (path == "/contact" || path == "/invitation") {
path.removeFirst()
let action: ConnReqType = path == "contact" ? .contact : .invitation
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
chatListActionSheet = .connectViaUrl(action: action, link: link)
} else {
AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid")))
}
}
func connectViaUrl() {
let m = ChatModel.shared
if let url = m.appOpenUrl {
m.appOpenUrl = nil
AlertManager.shared.showAlert(connectViaUrlAlert(url))
}
}
private func connectViaUrlSheet(_ action: ConnReqType, _ link: String) -> ActionSheet {
func connectViaUrlAlert(_ url: URL) -> Alert {
var path = url.path
logger.debug("ChatListView.connectViaUrlAlert path: \(path)")
if (path == "/contact" || path == "/invitation") {
path.removeFirst()
let action: ConnReqType = path == "contact" ? .contact : .invitation
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
let title: LocalizedStringKey
switch action {
case .contact: title = "Connect via contact link"
case .invitation: title = "Connect via one-time link"
}
return ActionSheet(
if case .contact = action { title = "Connect via contact link?" }
else { title = "Connect via one-time link?" }
return Alert(
title: Text(title),
buttons: [
.default(Text("Use current profile")) { connectViaLink(link, incognito: false) },
.default(Text("Use new incognito profile")) { connectViaLink(link, incognito: true) },
.cancel()
]
message: Text("Your profile will be sent to the contact that you received this link from"),
primaryButton: .default(Text("Connect")) {
connectViaLink(link)
},
secondaryButton: .cancel()
)
} else {
return Alert(title: Text("Error: URL is invalid"))
}
}

View File

@@ -34,10 +34,6 @@ class BGManager {
}
func schedule() {
if !ChatModel.shared.ntfEnableLocal {
logger.debug("BGManager.schedule: disabled")
return
}
logger.debug("BGManager.schedule")
let request = BGAppRefreshTaskRequest(identifier: receiveTaskId)
request.earliestBeginDate = Date(timeIntervalSinceNow: bgRefreshInterval)
@@ -49,10 +45,6 @@ class BGManager {
}
private func handleRefresh(_ task: BGAppRefreshTask) {
if !ChatModel.shared.ntfEnableLocal {
logger.debug("BGManager.handleRefresh: disabled")
return
}
logger.debug("BGManager.handleRefresh")
schedule()
if appStateGroupDefault.get().inactive {

View File

@@ -11,41 +11,8 @@ import Combine
import SwiftUI
import SimpleXChat
actor TerminalItems {
private var terminalItems: [TerminalItem] = []
static let shared = TerminalItems()
func items() -> [TerminalItem] {
terminalItems
}
func add(_ item: TerminalItem) async {
addTermItem(&terminalItems, item)
let m = ChatModel.shared
if m.showingTerminal {
await MainActor.run {
addTermItem(&m.terminalItems, item)
}
}
}
func addCommand(_ start: Date, _ cmd: ChatCommand, _ resp: ChatResponse) async {
addTermItem(&terminalItems, .cmd(start, cmd))
addTermItem(&terminalItems, .resp(.now, resp))
}
}
private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) {
if items.count >= 200 {
items.removeFirst()
}
items.append(item)
}
final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage?
@Published var setDeliveryReceipts = false
@Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get()
@Published var currentUser: User?
@Published var users: [UserInfo] = []
@@ -65,7 +32,6 @@ final class ChatModel: ObservableObject {
@Published var chatToTop: String?
@Published var groupMembers: [GroupMember] = []
// items in the terminal view
@Published var showingTerminal = false
@Published var terminalItems: [TerminalItem] = []
@Published var userAddress: UserContactLink?
@Published var chatItemTTL: ChatItemTTL = .none
@@ -75,9 +41,10 @@ final class ChatModel: ObservableObject {
@Published var tokenRegistered = false
@Published var tokenStatus: NtfTknStatus?
@Published var notificationMode = NotificationsMode.off
@Published var notificationPreview: NotificationPreviewMode = ntfPreviewModeGroupDefault.get()
@Published var notificationPreview: NotificationPreviewMode? = ntfPreviewModeGroupDefault.get()
@Published var incognito: Bool = incognitoGroupDefault.get()
// pending notification actions
@Published var ntfContactRequest: NTFContactRequest?
@Published var ntfContactRequest: ChatId?
@Published var ntfCallInvitationAction: (ChatId, NtfCallAction)?
// current WebRTC call
@Published var callInvitations: Dictionary<ChatId, RcvCallInvitation> = [:]
@@ -90,8 +57,6 @@ final class ChatModel: ObservableObject {
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
@Published var draft: ComposeState?
@Published var draftChatId: String?
// tracks keyboard height via subscription in AppDelegate
@Published var keyboardHeight: CGFloat = 0
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
@@ -101,14 +66,6 @@ final class ChatModel: ObservableObject {
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
var ntfEnableLocal: Bool {
notificationMode == .off || ntfEnableLocalGroupDefault.get()
}
var ntfEnablePeriodic: Bool {
notificationMode == .periodic || ntfEnablePeriodicGroupDefault.get()
}
func getUser(_ userId: Int64) -> User? {
currentUser?.userId == userId
? currentUser
@@ -176,14 +133,6 @@ final class ChatModel: ObservableObject {
updateChat(.direct(contact: contact), addMissing: contact.directOrUsed)
}
func updateContactConnectionStats(_ contact: Contact, _ connectionStats: ConnectionStats) {
var updatedConn = contact.activeConn
updatedConn.connectionStats = connectionStats
var updatedContact = contact
updatedContact.activeConn = updatedConn
updateContact(updatedContact)
}
func updateGroup(_ groupInfo: GroupInfo) {
updateChat(.group(groupInfo: groupInfo))
}
@@ -516,27 +465,14 @@ final class ChatModel: ObservableObject {
users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount })
}
func getConnectedMemberNames(_ ci: ChatItem) -> [String] {
guard var i = getChatItemIndex(ci) else { return [] }
var ns: [String] = []
while i < reversedChatItems.count, let m = reversedChatItems[i].memberConnected {
ns.append(m.displayName)
i += 1
}
return ns
}
func getChatItemNeighbors(_ ci: ChatItem) -> (ChatItem?, ChatItem?) {
if let i = getChatItemIndex(ci) {
return (
i + 1 < reversedChatItems.count ? reversedChatItems[i + 1] : nil,
i - 1 >= 0 ? reversedChatItems[i - 1] : nil
)
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {
if let i = getChatItemIndex(ci), i < reversedChatItems.count - 1 {
return reversedChatItems[i + 1]
} else {
return (nil, nil)
return nil
}
}
func popChat(_ id: String) {
if let i = getChatIndex(id) {
popChat_(i)
@@ -585,16 +521,6 @@ final class ChatModel: ObservableObject {
}
}
func updateGroupMemberConnectionStats(_ groupInfo: GroupInfo, _ member: GroupMember, _ connectionStats: ConnectionStats) {
if let conn = member.activeConn {
var updatedConn = conn
updatedConn.connectionStats = connectionStats
var updatedMember = member
updatedMember.activeConn = updatedConn
_ = upsertGroupMember(groupInfo, updatedMember)
}
}
func unreadChatItemCounts(itemsInView: Set<String>) -> UnreadChatItemCounts {
var i = 0
var totalBelow = 0
@@ -625,11 +551,13 @@ final class ChatModel: ObservableObject {
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
networkStatuses[contact.activeConn.agentConnId] ?? .unknown
}
}
struct NTFContactRequest {
var incognito: Bool
var chatId: String
func addTerminalItem(_ item: TerminalItem) {
if terminalItems.count >= 500 {
terminalItems.remove(at: 0)
}
terminalItems.append(item)
}
}
struct UnreadChatItemCounts {

View File

@@ -12,7 +12,6 @@ import UIKit
import SimpleXChat
let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT"
let ntfActionAcceptContactIncognito = "NTF_ACT_ACCEPT_CONTACT_INCOGNITO"
let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL"
let ntfActionRejectCall = "NTF_ACT_REJECT_CALL"
@@ -42,13 +41,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
userId != chatModel.currentUser?.userId {
changeActiveUser(userId, viewPwd: nil)
}
if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito),
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
let chatId = content.userInfo["chatId"] as? String {
let incognito = action == ntfActionAcceptContactIncognito
if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
Task { await acceptContactRequest(incognito: incognito, contactRequest: contactRequest) }
Task { await acceptContactRequest(contactRequest) }
} else {
chatModel.ntfContactRequest = NTFContactRequest(incognito: incognito, chatId: chatId)
chatModel.ntfContactRequest = chatId
}
} else if let (chatId, ntfAction) = ntfCallAction(content, action) {
if let invitation = chatModel.callInvitations.removeValue(forKey: chatId) {
@@ -136,17 +134,11 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
UNUserNotificationCenter.current().setNotificationCategories([
UNNotificationCategory(
identifier: ntfCategoryContactRequest,
actions: [
UNNotificationAction(
identifier: ntfActionAcceptContact,
title: NSLocalizedString("Accept", comment: "accept contact request via notification"),
options: .foreground
), UNNotificationAction(
identifier: ntfActionAcceptContactIncognito,
title: NSLocalizedString("Accept incognito", comment: "accept contact request via notification"),
options: .foreground
)
],
actions: [UNNotificationAction(
identifier: ntfActionAcceptContact,
title: NSLocalizedString("Accept", comment: "accept contact request via notification"),
options: .foreground
)],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: NSLocalizedString("New contact request", comment: "notification")
),

View File

@@ -94,8 +94,9 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
if case let .response(_, json) = resp {
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
}
Task {
await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp)
DispatchQueue.main.async {
ChatModel.shared.addTerminalItem(.cmd(start, cmd.obfuscated))
ChatModel.shared.addTerminalItem(.resp(.now, resp))
}
return resp
}
@@ -158,24 +159,6 @@ func apiSetActiveUserAsync(_ userId: Int64, viewPwd: String?) async throws -> Us
throw r
}
func apiSetAllContactReceipts(enable: Bool) async throws {
let r = await chatSendCmd(.setAllContactReceipts(enable: enable))
if case .cmdOk = r { return }
throw r
}
func apiSetUserContactReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) async throws {
let r = await chatSendCmd(.apiSetUserContactReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings))
if case .cmdOk = r { return }
throw r
}
func apiSetUserGroupReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) async throws {
let r = await chatSendCmd(.apiSetUserGroupReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings))
if case .cmdOk = r { return }
throw r
}
func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User {
try await setUserPrivacy_(.apiHideUser(userId: userId, viewPwd: viewPwd))
}
@@ -251,6 +234,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
throw r
}
func apiSetIncognito(incognito: Bool) throws {
let r = chatSendCmdSync(.setIncognito(incognito: incognito))
if case .cmdOk = r { return }
throw r
}
func apiExportArchive(config: ArchiveConfig) async throws {
try await sendCommandOkResp(.apiExportArchive(config: config))
}
@@ -320,18 +309,12 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
let r: ChatResponse
if type == .direct {
var cItem: ChatItem? = nil
let endTask = beginBGTask({
if let cItem = cItem {
DispatchQueue.main.async {
chatModel.messageDelivery.removeValue(forKey: cItem.id)
}
}
})
var cItem: ChatItem!
let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } })
r = await chatSendCmd(cmd, bgTask: false)
if case let .newChatItem(_, aChatItem) = r {
cItem = aChatItem.chatItem
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
chatModel.messageDelivery[cItem.id] = endTask
return cItem
}
if let networkErrorAlert = networkErrorAlert(r) {
@@ -481,10 +464,6 @@ func setNetworkConfig(_ cfg: NetCfg) throws {
throw r
}
func reconnectAllServers() async throws {
try await sendCommandOkResp(.reconnectAllServers)
}
func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) async throws {
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
}
@@ -495,9 +474,9 @@ func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profi
throw r
}
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) {
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (ConnectionStats?) {
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) }
if case let .groupMemberInfo(_, _, _, connStats_) = r { return (connStats_) }
throw r
}
@@ -525,18 +504,6 @@ func apiAbortSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) throws
throw r
}
func apiSyncContactRatchet(_ contactId: Int64, _ force: Bool) throws -> ConnectionStats {
let r = chatSendCmdSync(.apiSyncContactRatchet(contactId: contactId, force: force))
if case let .contactRatchetSyncStarted(_, _, connectionStats) = r { return connectionStats }
throw r
}
func apiSyncGroupMemberRatchet(_ groupId: Int64, _ groupMemberId: Int64, _ force: Bool) throws -> (GroupMember, ConnectionStats) {
let r = chatSendCmdSync(.apiSyncGroupMemberRatchet(groupId: groupId, groupMemberId: groupMemberId, force: force))
if case let .groupMemberRatchetSyncStarted(_, _, member, connectionStats) = r { return (member, connectionStats) }
throw r
}
func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
let r = await chatSendCmd(.apiGetContactCode(contactId: contactId))
if case let .contactCode(_, contact, connectionCode) = r { return (contact, connectionCode) }
@@ -563,25 +530,19 @@ func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCo
return nil
}
func apiAddContact(incognito: Bool) async -> (String, PendingContactConnection)? {
func apiAddContact() async -> String? {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiAddContact: no current user")
return nil
}
let r = await chatSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false)
if case let .invitation(_, connReqInvitation, connection) = r { return (connReqInvitation, connection) }
let r = await chatSendCmd(.apiAddContact(userId: userId), bgTask: false)
if case let .invitation(_, connReqInvitation) = r { return connReqInvitation }
AlertManager.shared.showAlert(connectionErrorAlert(r))
return nil
}
func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? {
let r = await chatSendCmd(.apiSetConnectionIncognito(connId: connId, incognito: incognito))
if case let .connectionIncognitoUpdated(_, toConnection) = r { return toConnection }
throw r
}
func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? {
let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
func apiConnect(connReq: String) async -> ConnReqType? {
let (connReqType, alert) = await apiConnect_(connReq: connReq)
if let alert = alert {
AlertManager.shared.showAlert(alert)
return nil
@@ -590,12 +551,12 @@ func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? {
}
}
func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert?) {
func apiConnect_(connReq: String) async -> (ConnReqType?, Alert?) {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiConnect: no current user")
return (nil, nil)
}
let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connReq: connReq))
let r = await chatSendCmd(.apiConnect(userId: userId, connReq: connReq))
switch r {
case .sentConfirmation: return (.invitation, nil)
case .sentInvitation: return (.contact, nil)
@@ -771,8 +732,8 @@ func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContac
}
}
func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? {
let r = await chatSendCmd(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId))
func apiAcceptContactRequest(contactReqId: Int64) async -> Contact? {
let r = await chatSendCmd(.apiAcceptContact(contactReqId: contactReqId))
let am = AlertManager.shared
if case let .acceptingContactRequest(_, contact) = r { return contact }
@@ -807,35 +768,29 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
}
func receiveFile(user: User, fileId: Int64, auto: Bool = false) async {
if let chatItem = await apiReceiveFile(fileId: fileId, auto: auto) {
await chatItemSimpleUpdate(user, chatItem)
func receiveFile(user: User, fileId: Int64) async {
if let chatItem = await apiReceiveFile(fileId: fileId) {
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
}
}
func apiReceiveFile(fileId: Int64, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
func apiReceiveFile(fileId: Int64, inline: Bool? = nil) async -> AChatItem? {
let r = await chatSendCmd(.receiveFile(fileId: fileId, inline: inline))
let am = AlertManager.shared
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
if case .rcvFileAcceptedSndCancelled = r {
logger.debug("apiReceiveFile error: sender cancelled file transfer")
if !auto {
am.showAlertMsg(
title: "Cannot receive file",
message: "Sender cancelled file transfer."
)
}
am.showAlertMsg(
title: "Cannot receive file",
message: "Sender cancelled file transfer."
)
} else if let networkErrorAlert = networkErrorAlert(r) {
logger.error("apiReceiveFile network error: \(String(describing: r))")
am.showAlert(networkErrorAlert)
} else {
switch chatError(r) {
case .fileCancelled:
logger.debug("apiReceiveFile ignoring fileCancelled error")
case .fileAlreadyReceiving:
logger.error("apiReceiveFile error: \(String(describing: r))")
switch r {
case .chatCmdError(_, .error(.fileAlreadyReceiving)):
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
default:
logger.error("apiReceiveFile error: \(String(describing: r))")
am.showAlertMsg(
title: "Error receiving file",
message: "Error: \(String(describing: r))"
@@ -847,7 +802,7 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil, auto: Bool = false) asyn
func cancelFile(user: User, fileId: Int64) async {
if let chatItem = await apiCancelFile(fileId: fileId) {
await chatItemSimpleUpdate(user, chatItem)
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
cleanupFile(chatItem)
}
}
@@ -880,8 +835,8 @@ func networkErrorAlert(_ r: ChatResponse) -> Alert? {
}
}
func acceptContactRequest(incognito: Bool, contactRequest: UserContactRequest) async {
if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequest.apiId) {
func acceptContactRequest(_ contactRequest: UserContactRequest) async {
if let contact = await apiAcceptContactRequest(contactReqId: contactRequest.apiId) {
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
DispatchQueue.main.async { ChatModel.shared.replaceChat(contactRequest.id, chat) }
}
@@ -1115,11 +1070,11 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try setXFTPConfig(getXFTPCfg())
try apiSetIncognito(incognito: incognitoGroupDefault.get())
m.chatInitialized = true
m.currentUser = try apiGetActiveUser()
if m.currentUser == nil {
onboardingStageDefault.set(.step1_SimpleXInfo)
privacyDeliveryReceiptsSet.set(true)
m.onboardingStage = .step1_SimpleXInfo
} else if start {
try startChat(refreshInvitations: refreshInvitations)
@@ -1149,9 +1104,6 @@ func startChat(refreshInvitations: Bool = true) throws {
m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1
? .step3_CreateSimpleXAddress
: savedOnboardingStage
if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() {
m.setDeliveryReceipts = true
}
}
}
ChatReceiver.shared.start()
@@ -1249,50 +1201,38 @@ class ChatReceiver {
}
func processReceivedMsg(_ res: ChatResponse) async {
Task {
await TerminalItems.shared.add(.resp(.now, res))
}
let m = ChatModel.shared
logger.debug("processReceivedMsg: \(res.responseType)")
switch res {
case let .newContactConnection(user, connection):
if active(user) {
await MainActor.run {
await MainActor.run {
m.addTerminalItem(.resp(.now, res))
logger.debug("processReceivedMsg: \(res.responseType)")
switch res {
case let .newContactConnection(user, connection):
if active(user) {
m.updateContactConnection(connection)
}
}
case let .contactConnectionDeleted(user, connection):
if active(user) {
await MainActor.run {
case let .contactConnectionDeleted(user, connection):
if active(user) {
m.removeChat(connection.id)
}
}
case let .contactConnected(user, contact, _):
if active(user) && contact.directOrUsed {
await MainActor.run {
case let .contactConnected(user, contact, _):
if active(user) && contact.directOrUsed {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
}
}
if contact.directOrUsed {
NtfManager.shared.notifyContactConnected(user, contact)
}
await MainActor.run {
if contact.directOrUsed {
NtfManager.shared.notifyContactConnected(user, contact)
}
m.setContactNetworkStatus(contact, .connected)
}
case let .contactConnecting(user, contact):
if active(user) && contact.directOrUsed {
await MainActor.run {
case let .contactConnecting(user, contact):
if active(user) && contact.directOrUsed {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
}
}
case let .receivedContactRequest(user, contactRequest):
if active(user) {
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
await MainActor.run {
case let .receivedContactRequest(user, contactRequest):
if active(user) {
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
if m.hasChat(contactRequest.id) {
m.updateChatInfo(cInfo)
} else {
@@ -1302,285 +1242,223 @@ func processReceivedMsg(_ res: ChatResponse) async {
))
}
}
}
NtfManager.shared.notifyContactRequest(user, contactRequest)
case let .contactUpdated(user, toContact):
if active(user) && m.hasChat(toContact.id) {
await MainActor.run {
NtfManager.shared.notifyContactRequest(user, contactRequest)
case let .contactUpdated(user, toContact):
if active(user) && m.hasChat(toContact.id) {
let cInfo = ChatInfo.direct(contact: toContact)
m.updateChatInfo(cInfo)
}
}
case let .contactsMerged(user, intoContact, mergedContact):
if active(user) && m.hasChat(mergedContact.id) {
await MainActor.run {
case let .contactsMerged(user, intoContact, mergedContact):
if active(user) && m.hasChat(mergedContact.id) {
if m.chatId == mergedContact.id {
m.chatId = intoContact.id
}
m.removeChat(mergedContact.id)
}
}
case let .contactsSubscribed(_, contactRefs):
await updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs):
await updateContactsStatus(contactRefs, status: .disconnected)
case let .contactSubError(user, contact, chatError):
await MainActor.run {
case let .contactsSubscribed(_, contactRefs):
updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs):
updateContactsStatus(contactRefs, status: .disconnected)
case let .contactSubError(user, contact, chatError):
if active(user) {
m.updateContact(contact)
}
processContactSubError(contact, chatError)
}
case let .contactSubSummary(_, contactSubscriptions):
await MainActor.run {
case let .contactSubSummary(user, contactSubscriptions):
for sub in contactSubscriptions {
// no need to update contact here, and it is slow
// if active(user) {
// m.updateContact(sub.contact)
// }
if active(user) {
m.updateContact(sub.contact)
}
if let err = sub.contactError {
processContactSubError(sub.contact, err)
} else {
m.setContactNetworkStatus(sub.contact, .connected)
}
}
}
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
await MainActor.run {
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
if active(user) {
m.addChatItem(cInfo, cItem)
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
m.increaseUnreadCounter(user: user)
}
}
if let file = cItem.autoReceiveFile() {
Task {
await receiveFile(user: user, fileId: file.fileId, auto: true)
}
}
if cItem.showNotification {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
case let .chatItemStatusUpdated(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
if !cItem.isDeletedContent {
let added = active(user) ? await MainActor.run { m.upsertChatItem(cInfo, cItem) } : true
if added && cItem.showNotification {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
}
if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
case .sndSent: endTask()
case .sndErrorAuth: endTask()
case .sndError: endTask()
default: ()
}
}
case let .chatItemUpdated(user, aChatItem):
await chatItemSimpleUpdate(user, aChatItem)
case let .chatItemReaction(user, _, r):
if active(user) {
await MainActor.run {
m.updateChatItem(r.chatInfo, r.chatReaction.chatItem)
}
}
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
if !active(user) {
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
await MainActor.run {
m.decreaseUnreadCounter(user: user)
if let file = cItem.autoReceiveFile() {
Task {
await receiveFile(user: user, fileId: file.fileId)
}
}
return
}
if cItem.showNotification {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
case let .chatItemStatusUpdated(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
if !cItem.isDeletedContent && (!active(user) || m.upsertChatItem(cInfo, cItem)) {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
case .sndSent: endTask()
case .sndErrorAuth: endTask()
case .sndError: endTask()
default: ()
}
}
case let .chatItemUpdated(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .chatItemReaction(user, _, r):
if active(user) {
m.updateChatItem(r.chatInfo, r.chatReaction.chatItem)
}
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
if !active(user) {
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
m.decreaseUnreadCounter(user: user)
}
return
}
await MainActor.run {
if let toChatItem = toChatItem {
_ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem)
} else {
m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem)
}
}
case let .receivedGroupInvitation(user, groupInfo, _, _):
if active(user) {
await MainActor.run {
case let .receivedGroupInvitation(user, groupInfo, _, _):
if active(user) {
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
}
}
case let .userAcceptedGroupSent(user, groupInfo, hostContact):
if !active(user) { return }
case let .userAcceptedGroupSent(user, groupInfo, hostContact):
if !active(user) { return }
await MainActor.run {
m.updateGroup(groupInfo)
if let hostContact = hostContact {
m.dismissConnReqView(hostContact.activeConn.id)
m.removeChat(hostContact.activeConn.id)
}
}
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
if active(user) {
await MainActor.run {
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
}
case let .deletedMemberUser(user, groupInfo, _): // TODO update user member
if active(user) {
await MainActor.run {
case let .deletedMemberUser(user, groupInfo, _): // TODO update user member
if active(user) {
m.updateGroup(groupInfo)
}
}
case let .deletedMember(user, groupInfo, _, deletedMember):
if active(user) {
await MainActor.run {
case let .deletedMember(user, groupInfo, _, deletedMember):
if active(user) {
_ = m.upsertGroupMember(groupInfo, deletedMember)
}
}
case let .leftMember(user, groupInfo, member):
if active(user) {
await MainActor.run {
case let .leftMember(user, groupInfo, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
}
case let .groupDeleted(user, groupInfo, _): // TODO update user member
if active(user) {
await MainActor.run {
case let .groupDeleted(user, groupInfo, _): // TODO update user member
if active(user) {
m.updateGroup(groupInfo)
}
}
case let .userJoinedGroup(user, groupInfo):
if active(user) {
await MainActor.run {
case let .userJoinedGroup(user, groupInfo):
if active(user) {
m.updateGroup(groupInfo)
}
}
case let .joinedGroupMember(user, groupInfo, member):
if active(user) {
await MainActor.run {
case let .joinedGroupMember(user, groupInfo, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
}
case let .connectedToGroupMember(user, groupInfo, member, memberContact):
if active(user) {
await MainActor.run {
case let .connectedToGroupMember(user, groupInfo, member, memberContact):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
}
if let contact = memberContact {
await MainActor.run {
if let contact = memberContact {
m.setContactNetworkStatus(contact, .connected)
}
}
case let .groupUpdated(user, toGroup):
if active(user) {
await MainActor.run {
case let .groupUpdated(user, toGroup):
if active(user) {
m.updateGroup(toGroup)
}
}
case let .memberRole(user, groupInfo, _, _, _, _):
if active(user) {
await MainActor.run {
case let .memberRole(user, groupInfo, _, _, _, _):
if active(user) {
m.updateGroup(groupInfo)
}
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileStart(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileComplete(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileSndCancelled(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem)
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileError(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem)
case let .sndFileStart(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
case let .sndFileComplete(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
cleanupDirectFile(aChatItem)
case let .sndFileRcvCancelled(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
cleanupDirectFile(aChatItem)
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
chatItemSimpleUpdate(user, aChatItem)
case let .sndFileCompleteXFTP(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem)
case let .sndFileError(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem)
case let .callInvitation(invitation):
m.callInvitations[invitation.contact.id] = invitation
activateCall(invitation)
case let .callOffer(_, contact, callType, offer, sharedKey, _):
withCall(contact) { call in
call.callState = .offerReceived
call.peerMedia = callType.media
call.sharedKey = sharedKey
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
let iceServers = getIceServers()
logger.debug(".callOffer useRelay \(useRelay)")
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
m.callCommand = .offer(
offer: offer.rtcSession,
iceCandidates: offer.rtcIceCandidates,
media: callType.media, aesKey: sharedKey,
iceServers: iceServers,
relay: useRelay
)
}
case let .callAnswer(_, contact, answer):
withCall(contact) { call in
call.callState = .answerReceived
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
}
case let .callExtraInfo(_, contact, extraInfo):
withCall(contact) { _ in
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
}
case let .callEnded(_, contact):
if let invitation = m.callInvitations.removeValue(forKey: contact.id) {
CallController.shared.reportCallRemoteEnded(invitation: invitation)
}
withCall(contact) { call in
m.callCommand = .end
CallController.shared.reportCallRemoteEnded(call: call)
}
case .chatSuspended:
chatSuspended()
default:
logger.debug("unsupported event: \(res.responseType)")
}
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
await chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileStart(user, aChatItem):
await chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileComplete(user, aChatItem):
await chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileSndCancelled(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
await chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileError(user, aChatItem):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
case let .sndFileStart(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
case let .sndFileComplete(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupDirectFile(aChatItem) }
case let .sndFileRcvCancelled(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupDirectFile(aChatItem) }
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
await chatItemSimpleUpdate(user, aChatItem)
case let .sndFileCompleteXFTP(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
case let .sndFileError(user, aChatItem):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
case let .callInvitation(invitation):
m.callInvitations[invitation.contact.id] = invitation
activateCall(invitation)
case let .callOffer(_, contact, callType, offer, sharedKey, _):
await withCall(contact) { call in
call.callState = .offerReceived
call.peerMedia = callType.media
call.sharedKey = sharedKey
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
let iceServers = getIceServers()
logger.debug(".callOffer useRelay \(useRelay)")
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
m.callCommand = .offer(
offer: offer.rtcSession,
iceCandidates: offer.rtcIceCandidates,
media: callType.media, aesKey: sharedKey,
iceServers: iceServers,
relay: useRelay
)
}
case let .callAnswer(_, contact, answer):
await withCall(contact) { call in
call.callState = .answerReceived
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
}
case let .callExtraInfo(_, contact, extraInfo):
await withCall(contact) { _ in
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
}
case let .callEnded(_, contact):
if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) {
CallController.shared.reportCallRemoteEnded(invitation: invitation)
}
await withCall(contact) { call in
m.callCommand = .end
CallController.shared.reportCallRemoteEnded(call: call)
}
case .chatSuspended:
chatSuspended()
case let .contactSwitch(_, contact, switchProgress):
await MainActor.run {
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
}
case let .groupMemberSwitch(_, groupInfo, member, switchProgress):
await MainActor.run {
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
}
case let .contactRatchetSync(_, contact, ratchetSyncProgress):
await MainActor.run {
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
}
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress):
await MainActor.run {
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
}
default:
logger.debug("unsupported event: \(res.responseType)")
}
func withCall(_ contact: Contact, _ perform: (Call) -> Void) async {
if let call = m.activeCall, call.contact.apiId == contact.apiId {
await MainActor.run { perform(call) }
} else {
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
func withCall(_ contact: Contact, _ perform: (Call) -> Void) {
if let call = m.activeCall, call.contact.apiId == contact.apiId {
perform(call)
} else {
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
}
}
}
}
@@ -1589,23 +1467,19 @@ func active(_ user: User) -> Bool {
user.id == ChatModel.shared.currentUser?.id
}
func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) async {
func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) {
let m = ChatModel.shared
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
if active(user) {
if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
if active(user) && m.upsertChatItem(cInfo, cItem) {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
}
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async {
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) {
let m = ChatModel.shared
await MainActor.run {
for c in contactRefs {
m.networkStatuses[c.agentConnId] = status
}
for c in contactRefs {
m.networkStatuses[c.agentConnId] = status
}
}
@@ -1644,9 +1518,7 @@ func activateCall(_ callInvitation: RcvCallInvitation) {
let m = ChatModel.shared
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
if let error = error {
DispatchQueue.main.async {
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
}
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
} else {
logger.debug("reportNewIncomingCall success")

View File

@@ -76,11 +76,9 @@ private func _chatSuspended() {
}
func activateChat(appState: AppState = .active) {
logger.debug("DEBUGGING: activateChat")
suspendLockQueue.sync {
appStateGroupDefault.set(appState)
if ChatModel.ok { apiActivateChat() }
logger.debug("DEBUGGING: activateChat: after apiActivateChat")
}
}
@@ -97,14 +95,10 @@ func initChatAndMigrate(refreshInvitations: Bool = true) {
}
func startChatAndActivate() {
logger.debug("DEBUGGING: startChatAndActivate")
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start()
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
}
if .active != appStateGroupDefault.get() {
logger.debug("DEBUGGING: startChatAndActivate: before activateChat")
activateChat()
logger.debug("DEBUGGING: startChatAndActivate: after activateChat")
}
}

View File

@@ -139,10 +139,10 @@ struct SimpleXApp: App {
let chat = chatModel.getChat(id) {
loadChat(chat: chat)
}
if let ncr = chatModel.ntfContactRequest {
if let chatId = chatModel.ntfContactRequest {
chatModel.ntfContactRequest = nil
if case let .contactRequest(contactRequest) = chatModel.getChat(ncr.chatId)?.chatInfo {
Task { await acceptContactRequest(incognito: ncr.incognito, contactRequest: contactRequest) }
if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
Task { await acceptContactRequest(contactRequest) }
}
}
} catch let error {

View File

@@ -57,37 +57,6 @@ private func serverHost(_ s: String) -> String {
}
}
enum SendReceipts: Identifiable, Hashable {
case yes
case no
case userDefault(Bool)
var id: Self { self }
var text: LocalizedStringKey {
switch self {
case .yes: return "yes"
case .no: return "no"
case let .userDefault(on): return on ? "default (yes)" : "default (no)"
}
}
func bool() -> Bool? {
switch self {
case .yes: return true
case .no: return false
case .userDefault: return nil
}
}
static func fromBool(_ enable: Bool?, userDefault def: Bool) -> SendReceipts {
if let enable = enable {
return enable ? .yes : .no
}
return .userDefault(def)
}
}
struct ChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
@@ -99,8 +68,6 @@ struct ChatInfoView: View {
@Binding var connectionCode: String?
@FocusState private var aliasTextFieldFocused: Bool
@State private var alert: ChatInfoViewAlert? = nil
@State private var sendReceipts = SendReceipts.userDefault(true)
@State private var sendReceiptsUserDefault = true
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
enum ChatInfoViewAlert: Identifiable {
@@ -109,7 +76,6 @@ struct ChatInfoView: View {
case networkStatusAlert
case switchAddressAlert
case abortSwitchAddressAlert
case syncConnectionForceAlert
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
@@ -119,7 +85,6 @@ struct ChatInfoView: View {
case .networkStatusAlert: return "networkStatusAlert"
case .switchAddressAlert: return "switchAddressAlert"
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
case let .error(title, _): return "error \(title)"
}
}
@@ -143,26 +108,13 @@ struct ChatInfoView: View {
if let customUserProfile = customUserProfile {
Section("Incognito") {
HStack {
Text("Your random profile")
Spacer()
Text(customUserProfile.chatViewName)
.foregroundStyle(.indigo)
}
infoRow("Your random profile", customUserProfile.chatViewName)
}
}
Section {
if let code = connectionCode { verifyCodeButton(code) }
contactPreferencesButton()
sendReceiptsOption()
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
}
// } else if developerTools {
// synchronizeConnectionButtonForce()
// }
}
if let contactLink = contact.contactLink {
@@ -189,18 +141,12 @@ struct ChatInfoView: View {
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil })
if connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|| connStats.ratchetSyncSendProhibited
)
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch })
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
@@ -222,12 +168,6 @@ struct ChatInfoView: View {
.navigationBarHidden(true)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.onAppear {
if let currentUser = chatModel.currentUser {
sendReceiptsUserDefault = currentUser.sendRcptsContacts
}
sendReceipts = SendReceipts.fromBool(contact.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault)
}
.alert(item: $alert) { alertItem in
switch(alertItem) {
case .deleteContactAlert: return deleteContactAlert()
@@ -235,7 +175,6 @@ struct ChatInfoView: View {
case .networkStatusAlert: return networkStatusAlert()
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
case let .error(title, error): return mkAlert(title: title, message: error)
}
}
@@ -248,30 +187,20 @@ struct ChatInfoView: View {
.frame(width: 192, height: 192)
.padding(.top, 12)
.padding()
if contact.verified {
(
Text(Image(systemName: "checkmark.shield"))
HStack {
if contact.verified {
Image(systemName: "checkmark.shield")
.foregroundColor(.secondary)
.font(.title2)
+ Text(" ")
+ Text(contact.profile.displayName)
.font(.largeTitle)
)
.multilineTextAlignment(.center)
.lineLimit(2)
.padding(.bottom, 2)
} else {
}
Text(contact.profile.displayName)
.font(.largeTitle)
.multilineTextAlignment(.center)
.lineLimit(2)
.lineLimit(1)
.padding(.bottom, 2)
}
if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName {
Text(cInfo.fullName)
.font(.title2)
.multilineTextAlignment(.center)
.lineLimit(4)
.lineLimit(2)
}
}
.frame(maxWidth: .infinity, alignment: .center)
@@ -351,44 +280,6 @@ struct ChatInfoView: View {
}
}
private func sendReceiptsOption() -> some View {
Picker(selection: $sendReceipts) {
ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in
Text(opt.text)
}
} label: {
Label("Send receipts", systemImage: "checkmark.message")
}
.frame(height: 36)
.onChange(of: sendReceipts) { _ in
setSendReceipts()
}
}
private func setSendReceipts() {
var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults
chatSettings.sendRcpts = sendReceipts.bool()
updateChatSettings(chat, chatSettings: chatSettings)
}
private func synchronizeConnectionButton() -> some View {
Button {
syncContactConnection(force: false)
} label: {
Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath")
.foregroundColor(.orange)
}
}
private func synchronizeConnectionButtonForce() -> some View {
Button {
alert = .syncConnectionForceAlert
} label: {
Label("Renegotiate encryption", systemImage: "exclamationmark.triangle")
.foregroundColor(.red)
}
}
private func networkStatusRow() -> some View {
HStack {
Text("Network status")
@@ -479,10 +370,6 @@ struct ChatInfoView: View {
do {
let stats = try apiSwitchContact(contactId: contact.apiId)
connectionStats = stats
await MainActor.run {
chatModel.updateContactConnectionStats(contact, stats)
dismiss()
}
} catch let error {
logger.error("switchContactAddress apiSwitchContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error changing address")
@@ -498,9 +385,6 @@ struct ChatInfoView: View {
do {
let stats = try apiAbortSwitchContact(contact.apiId)
connectionStats = stats
await MainActor.run {
chatModel.updateContactConnectionStats(contact, stats)
}
} catch let error {
logger.error("abortSwitchContactAddress apiAbortSwitchContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error aborting address change")
@@ -510,25 +394,6 @@ struct ChatInfoView: View {
}
}
}
private func syncContactConnection(force: Bool) {
Task {
do {
let stats = try apiSyncContactRatchet(contact.apiId, force)
connectionStats = stats
await MainActor.run {
chatModel.updateContactConnectionStats(contact, stats)
dismiss()
}
} catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
}
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
@@ -549,15 +414,6 @@ func abortSwitchAddressAlert(_ abortSwitchAddress: @escaping () -> Void) -> Aler
)
}
func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Alert {
Alert(
title: Text("Renegotiate encryption?"),
message: Text("The encryption is working and the new encryption agreement is not required. It may result in connection errors!"),
primaryButton: .destructive(Text("Renegotiate"), action: syncConnectionForce),
secondaryButton: .cancel()
)
}
struct ChatInfoView_Previews: PreviewProvider {
static var previews: some View {
ChatInfoView(

View File

@@ -10,11 +10,20 @@ import SwiftUI
import SimpleXChat
struct CIEventView: View {
var eventText: Text
var chatItem: ChatItem
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
eventText
if let member = chatItem.memberDisplayName {
Text(member)
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
+ Text(" ")
+ chatEventText(chatItem)
} else {
chatEventText(chatItem)
}
}
.padding(.leading, 6)
.padding(.bottom, 6)
@@ -22,8 +31,20 @@ struct CIEventView: View {
}
}
func chatEventText(_ ci: ChatItem) -> Text {
Text(ci.content.text)
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
+ Text(" ")
+ ci.timestampText
.font(.caption)
.foregroundColor(Color.secondary)
.fontWeight(.light)
}
struct CIEventView_Previews: PreviewProvider {
static var previews: some View {
CIEventView(eventText: Text("event happened"))
CIEventView(chatItem: ChatItem.getGroupEventSample())
}
}

View File

@@ -62,7 +62,6 @@ struct CIFileView: View {
case .rcvComplete: return true
case .rcvCancelled: return false
case .rcvError: return false
case .invalid: return false
}
}
return false
@@ -150,7 +149,6 @@ struct CIFileView: View {
case .rcvComplete: fileIcon("doc.fill")
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
case .invalid: fileIcon("doc.fill", innerIcon: "questionmark", innerIconSize: 10)
}
} else {
fileIcon("doc.fill")
@@ -197,7 +195,7 @@ struct CIFileView_Previews: PreviewProvider {
static var previews: some View {
let sentFile: ChatItem = ChatItem(
chatDir: .directSnd,
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true),
content: .sndMsgContent(msgContent: .file("")),
quotedItem: nil,
file: CIFile.getSample(fileStatus: .sndComplete)

View File

@@ -99,7 +99,6 @@ struct CIImageView: View {
case .rcvTransfer: progressView()
case .rcvCancelled: fileIcon("xmark", 10, 13)
case .rcvError: fileIcon("xmark", 10, 13)
case .invalid: fileIcon("questionmark", 10, 13)
default: EmptyView()
}
}

View File

@@ -13,47 +13,17 @@ struct CIMetaView: View {
@EnvironmentObject var chat: Chat
var chatItem: ChatItem
var metaColor = Color.secondary
var paleMetaColor = Color(UIColor.tertiaryLabel)
var body: some View {
if chatItem.isDeletedContent {
chatItem.timestampText.font(.caption).foregroundColor(metaColor)
} else {
let meta = chatItem.meta
let ttl = chat.chatInfo.timedMessagesTTL
switch meta.itemStatus {
case let .sndSent(sndProgress):
switch sndProgress {
case .complete: ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .sent)
case .partial: ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .sent)
}
case let .sndRcvd(_, sndProgress):
switch sndProgress {
case .complete:
ZStack {
ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd1)
ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd2)
}
case .partial:
ZStack {
ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .rcvd1)
ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .rcvd2)
}
}
default:
ciMetaText(meta, chatTTL: ttl, color: metaColor)
}
ciMetaText(chatItem.meta, chatTTL: chat.chatInfo.timedMessagesTTL, color: metaColor)
}
}
}
enum SentCheckmark {
case sent
case rcvd1
case rcvd2
}
func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparent: Bool = false, sent: SentCheckmark? = nil) -> Text {
func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparent: Bool = false) -> Text {
var r = Text("")
if meta.itemEdited {
r = r + statusIconText("pencil", color)
@@ -67,16 +37,7 @@ func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparen
r = r + Text(" ")
}
if let (icon, statusColor) = meta.statusIcon(color) {
let t = Text(Image(systemName: icon)).font(.caption2)
let gap = Text(" ").kerning(-1.25)
let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67))
switch sent {
case nil: r = r + t1
case .sent: r = r + t1 + gap
case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + gap
case .rcvd2: r = r + gap + t1
}
r = r + Text(" ")
r = r + statusIconText(icon, transparent ? .clear : statusColor) + Text(" ")
} else if !meta.disappearing {
r = r + statusIconText("circlebadge.fill", .clear) + Text(" ")
}
@@ -90,12 +51,8 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text {
struct CIMetaView_Previews: PreviewProvider {
static var previews: some View {
Group {
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)))
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)))
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)))
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)))
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)))
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true))
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent))
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, itemEdited: true))
CIMetaView(chatItem: ChatItem.getDeletedContentSample())
}
.previewLayout(.fixed(width: 360, height: 100))

View File

@@ -12,204 +12,25 @@ import SimpleXChat
let decryptErrorReason: LocalizedStringKey = "It can happen when you or your connection used the old database backup."
struct CIRcvDecryptionError: View {
@EnvironmentObject var chat: Chat
var msgDecryptError: MsgDecryptError
var msgCount: UInt32
var chatItem: ChatItem
@State private var alert: CIRcvDecryptionErrorAlert?
enum CIRcvDecryptionErrorAlert: Identifiable {
case syncAllowedAlert(_ syncConnection: () -> Void)
case syncNotSupportedContactAlert
case syncNotSupportedMemberAlert
case decryptionErrorAlert
case error(title: LocalizedStringKey, error: LocalizedStringKey)
var id: String {
switch self {
case .syncAllowedAlert: return "syncAllowedAlert"
case .syncNotSupportedContactAlert: return "syncNotSupportedContactAlert"
case .syncNotSupportedMemberAlert: return "syncNotSupportedMemberAlert"
case .decryptionErrorAlert: return "decryptionErrorAlert"
case let .error(title, _): return "error \(title)"
}
}
}
var showMember = false
var body: some View {
viewBody()
.onAppear {
// for direct chat ConnectionStats are populated on opening chat, see ChatView onAppear
if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir {
do {
let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId)
if let s = stats {
ChatModel.shared.updateGroupMemberConnectionStats(groupInfo, member, s)
}
} catch let error {
logger.error("apiGroupMemberInfo error: \(responseError(error))")
}
}
CIMsgError(chatItem: chatItem, showMember: showMember) {
var message: Text
let why = Text(decryptErrorReason)
let permanent = Text("This error is permanent for this connection, please re-connect.")
switch msgDecryptError {
case .ratchetHeader:
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why + Text("\n") + permanent
case .tooManySkipped:
message = Text("\(msgCount) messages skipped.") + Text("\n") + why + Text("\n") + permanent
}
.alert(item: $alert) { alertItem in
switch(alertItem) {
case let .syncAllowedAlert(syncConnection): return syncAllowedAlert(syncConnection)
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))
}
}
}
@ViewBuilder private func viewBody() -> some View {
if case let .direct(contact) = chat.chatInfo,
let contactStats = contact.activeConn.connectionStats {
if contactStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncContactConnection(contact) }
}
} else if !contactStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedContactAlert
}
} else {
basicDecryptionErrorItem()
}
} else if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir,
let modelMember = ChatModel.shared.groupMembers.first(where: { $0.id == groupMember.id }),
let memberStats = modelMember.activeConn?.connectionStats {
if memberStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
}
} else if !memberStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedMemberAlert
}
} else {
basicDecryptionErrorItem()
}
} else {
basicDecryptionErrorItem()
AlertManager.shared.showAlert(Alert(title: Text("Decryption error"), message: message))
}
}
private func basicDecryptionErrorItem() -> some View {
decryptionErrorItem { alert = .decryptionErrorAlert }
}
private func decryptionErrorItemFixButton(syncSupported: Bool, _ onClick: @escaping (() -> Void)) -> some View {
ZStack(alignment: .bottomTrailing) {
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(chatItem.content.text)
.foregroundColor(.red)
.italic()
}
(
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
.foregroundColor(syncSupported ? .accentColor : .secondary)
.font(.callout)
+ Text(" ")
+ Text("Fix connection")
.foregroundColor(syncSupported ? .accentColor : .secondary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, transparent: true)
)
}
.padding(.horizontal, 12)
CIMetaView(chatItem: chatItem)
.padding(.horizontal, 12)
}
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(18)
.textSelection(.disabled)
}
private func decryptionErrorItem(_ onClick: @escaping (() -> Void)) -> some View {
return ZStack(alignment: .bottomTrailing) {
HStack {
Text(chatItem.content.text)
.foregroundColor(.red)
.italic()
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, transparent: true)
}
.padding(.horizontal, 12)
CIMetaView(chatItem: chatItem)
.padding(.horizontal, 12)
}
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(18)
.textSelection(.disabled)
}
private func message() -> Text {
var message: Text
let why = Text(decryptErrorReason)
switch msgDecryptError {
case .ratchetHeader:
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
case .tooManySkipped:
message = Text("\(msgCount) messages skipped.") + Text("\n") + why
case .ratchetEarlier:
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
case .other:
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
}
return message
}
private func syncMemberConnection(_ groupInfo: GroupInfo, _ member: GroupMember) {
Task {
do {
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, false)
await MainActor.run {
ChatModel.shared.updateGroupMemberConnectionStats(groupInfo, mem, stats)
}
} catch let error {
logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
private func syncContactConnection(_ contact: Contact) {
Task {
do {
let stats = try apiSyncContactRatchet(contact.apiId, false)
await MainActor.run {
ChatModel.shared.updateContactConnectionStats(contact, stats)
}
} catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
private func syncAllowedAlert(_ syncConnection: @escaping () -> Void) -> Alert {
Alert(
title: Text("Fix connection?"),
message: message(),
primaryButton: .default(Text("Fix"), action: syncConnection),
secondaryButton: .cancel()
)
}
}
//struct CIRcvDecryptionError_Previews: PreviewProvider {

View File

@@ -212,7 +212,6 @@ struct CIVideoView: View {
}
case .rcvCancelled: fileIcon("xmark", 10, 13)
case .rcvError: fileIcon("xmark", 10, 13)
case .invalid: fileIcon("questionmark", 10, 13)
default: EmptyView()
}
}
@@ -247,10 +246,10 @@ struct CIVideoView: View {
.padding([.trailing, .top], 11)
}
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) {
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64) async -> Void) {
Task {
if let user = ChatModel.shared.currentUser {
await receiveFile(user, file.fileId, false)
await receiveFile(user, file.fileId)
}
}
}

View File

@@ -144,7 +144,6 @@ struct VoiceMessagePlayer: View {
case .rcvComplete: playbackButton()
case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
}
} else {
playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
@@ -269,7 +268,7 @@ struct CIVoiceView_Previews: PreviewProvider {
static var previews: some View {
let sentVoiceMessage: ChatItem = ChatItem(
chatDir: .directSnd,
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true),
content: .sndMsgContent(msgContent: .voice(text: "", duration: 30)),
quotedItem: nil,
file: CIFile.getSample(fileStatus: .sndComplete)

View File

@@ -12,9 +12,13 @@ import SimpleXChat
struct DeletedItemView: View {
@Environment(\.colorScheme) var colorScheme
var chatItem: ChatItem
var showMember = false
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
if showMember, let member = chatItem.memberDisplayName {
Text(member).fontWeight(.medium) + Text(": ")
}
Text(chatItem.content.text)
.foregroundColor(.secondary)
.italic()
@@ -33,7 +37,10 @@ struct DeletedItemView_Previews: PreviewProvider {
static var previews: some View {
Group {
DeletedItemView(chatItem: ChatItem.getDeletedContentSample())
DeletedItemView(chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)))
DeletedItemView(
chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)),
showMember: true
)
}
.previewLayout(.fixed(width: 360, height: 200))
}

View File

@@ -32,7 +32,7 @@ func emojiText(_ text: String) -> Text {
struct EmojiItemView_Previews: PreviewProvider {
static var previews: some View {
Group{
EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete)))
EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent))
EmojiItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "👍"))
}
.previewLayout(.fixed(width: 360, height: 70))

View File

@@ -75,14 +75,14 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
static var previews: some View {
let sentVoiceMessage: ChatItem = ChatItem(
chatDir: .directSnd,
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true),
content: .sndMsgContent(msgContent: .voice(text: "Hello there", duration: 30)),
quotedItem: nil,
file: CIFile.getSample(fileStatus: .sndComplete)
)
let voiceMessageWithQuote: ChatItem = ChatItem(
chatDir: .directSnd,
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true),
content: .sndMsgContent(msgContent: .voice(text: "", duration: 30)),
quotedItem: CIQuote.getSample(1, .now, "Hi", chatDir: .directRcv),
file: CIFile.getSample(fileStatus: .sndComplete)

View File

@@ -18,6 +18,7 @@ struct FramedItemView: View {
@Environment(\.colorScheme) var colorScheme
var chatInfo: ChatInfo
var chatItem: ChatItem
var showMember = false
var maxWidth: CGFloat = .infinity
@State var scrollProxy: ScrollViewProxy? = nil
@State var msgWidth: CGFloat = 0
@@ -56,7 +57,7 @@ struct FramedItemView: View {
}
}
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, msgContentView: framedMsgContentView)
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, msgContentView: framedMsgContentView)
.padding(chatItem.content.msgContent != nil ? 0 : 4)
.overlay(DetermineWidth())
}
@@ -67,7 +68,6 @@ struct FramedItemView: View {
.padding(.horizontal, 12)
.padding(.bottom, 6)
.overlay(DetermineWidth())
.accessibilityLabel("")
}
}
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme))
@@ -107,7 +107,7 @@ struct FramedItemView: View {
value: .white
)
} else {
ciMsgContentView(chatItem)
ciMsgContentView (chatItem, showMember)
}
case let .video(text, image, duration):
CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy)
@@ -120,27 +120,27 @@ struct FramedItemView: View {
value: .white
)
} else {
ciMsgContentView(chatItem)
ciMsgContentView (chatItem, showMember)
}
case let .voice(text, duration):
FramedCIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
.overlay(DetermineWidth())
if text != "" {
ciMsgContentView(chatItem)
ciMsgContentView (chatItem, showMember)
}
case let .file(text):
ciFileView(chatItem, text)
case let .link(_, preview):
CILinkView(linkPreview: preview)
ciMsgContentView(chatItem)
ciMsgContentView (chatItem, showMember)
case let .unknown(_, text: text):
if chatItem.file == nil {
ciMsgContentView(chatItem)
ciMsgContentView (chatItem, showMember)
} else {
ciFileView(chatItem, text)
}
default:
ciMsgContentView(chatItem)
ciMsgContentView (chatItem, showMember)
}
}
}
@@ -232,27 +232,17 @@ struct FramedItemView: View {
}
private func ciQuotedMsgView(_ qi: CIQuote) -> some View {
Group {
if let sender = qi.getSender(membership()) {
VStack(alignment: .leading, spacing: 2) {
Text(sender).font(.caption).foregroundColor(.secondary)
ciQuotedMsgTextView(qi, lines: 2)
}
} else {
ciQuotedMsgTextView(qi, lines: 3)
}
}
.padding(.top, 6)
MsgContentView(
text: qi.text,
formattedText: qi.formattedText,
sender: qi.getSender(membership())
)
.lineLimit(3)
.font(.subheadline)
.padding(.vertical, 6)
.padding(.horizontal, 12)
}
private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View {
MsgContentView(text: qi.text, formattedText: qi.formattedText)
.lineLimit(lines)
.font(.subheadline)
.padding(.bottom, 6)
}
private func ciQuoteIconView(_ image: String) -> some View {
Image(systemName: image)
.resizable()
@@ -270,12 +260,13 @@ struct FramedItemView: View {
}
}
@ViewBuilder private func ciMsgContentView(_ ci: ChatItem) -> some View {
@ViewBuilder private func ciMsgContentView(_ ci: ChatItem, _ showMember: Bool = false) -> some View {
let text = ci.meta.isLive ? ci.content.msgContent?.text ?? ci.text : ci.text
let rtl = isRightToLeft(text)
let v = MsgContentView(
text: text,
formattedText: text == "" ? [] : ci.formattedText,
sender: showMember ? ci.memberDisplayName : nil,
meta: ci.meta,
rightToLeft: rtl
)
@@ -297,7 +288,7 @@ struct FramedItemView: View {
CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited)
.overlay(DetermineWidth())
if text != "" || ci.meta.isLive {
ciMsgContentView (chatItem)
ciMsgContentView (chatItem, showMember)
}
}
@@ -358,8 +349,8 @@ struct FramedItemView_Previews: PreviewProvider {
Group{
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
@@ -372,10 +363,10 @@ struct FramedItemView_Previews: PreviewProvider {
struct FramedItemView_Edited_Previews: PreviewProvider {
static var previews: some View {
Group {
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
@@ -390,10 +381,10 @@ struct FramedItemView_Edited_Previews: PreviewProvider {
struct FramedItemView_Deleted_Previews: PreviewProvider {
static var previews: some View {
Group {
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))

View File

@@ -12,9 +12,10 @@ import SimpleXChat
struct IntegrityErrorItemView: View {
var msgError: MsgErrorType
var chatItem: ChatItem
var showMember = false
var body: some View {
CIMsgError(chatItem: chatItem) {
CIMsgError(chatItem: chatItem, showMember: showMember) {
switch msgError {
case .msgSkipped:
AlertManager.shared.showAlertMsg(
@@ -53,10 +54,14 @@ struct IntegrityErrorItemView: View {
struct CIMsgError: View {
var chatItem: ChatItem
var showMember = false
var onTap: () -> Void
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
if showMember, let member = chatItem.memberDisplayName {
Text(member).fontWeight(.medium) + Text(": ")
}
Text(chatItem.content.text)
.foregroundColor(.red)
.italic()

View File

@@ -12,9 +12,13 @@ import SimpleXChat
struct MarkedDeletedItemView: View {
@Environment(\.colorScheme) var colorScheme
var chatItem: ChatItem
var showMember = false
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
if showMember, let member = chatItem.memberDisplayName {
Text(member).font(.caption).fontWeight(.medium) + Text(": ").font(.caption)
}
if case let .moderated(_, byGroupMember) = chatItem.meta.itemDeleted {
markedDeletedText("moderated by \(byGroupMember.chatViewName)")
} else {
@@ -42,7 +46,7 @@ struct MarkedDeletedItemView: View {
struct MarkedDeletedItemView_Previews: PreviewProvider {
static var previews: some View {
Group {
MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)))
MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)))
}
.previewLayout(.fixed(width: 360, height: 200))
}

View File

@@ -87,7 +87,7 @@ struct MsgContentView: View {
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false) -> Text {
let s = text
var res: Text
if let ft = formattedText, ft.count > 0 && ft.count <= 200 {
if let ft = formattedText, ft.count > 0 {
res = formatText(ft[0], preview)
var i = 1
while i < ft.count {

View File

@@ -13,25 +13,7 @@ struct ChatItemInfoView: View {
@Environment(\.colorScheme) var colorScheme
var ci: ChatItem
@Binding var chatItemInfo: ChatItemInfo?
@State private var selection: CIInfoTab = .history
@State private var alert: CIInfoViewAlert? = nil
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
enum CIInfoTab {
case history
case quote
case delivery
}
enum CIInfoViewAlert: Identifiable {
case alert(title: String, text: String)
var id: String {
switch self {
case let .alert(title, text): return "alert \(title) \(text)"
}
}
}
var body: some View {
NavigationView {
@@ -43,11 +25,6 @@ struct ChatItemInfoView: View {
}
}
}
.alert(item: $alert) { a in
switch(a) {
case let .alert(title, text): return Alert(title: Text(title), message: Text(text))
}
}
}
}
@@ -57,92 +34,44 @@ struct ChatItemInfoView: View {
: NSLocalizedString("Received message", comment: "message info title")
}
private var numTabs: Int {
var numTabs = 1
if chatItemInfo?.memberDeliveryStatuses != nil {
numTabs += 1
}
if ci.quotedItem != nil {
numTabs += 1
}
return numTabs
}
@ViewBuilder private func itemInfoView() -> some View {
if numTabs > 1 {
TabView(selection: $selection) {
if let mdss = chatItemInfo?.memberDeliveryStatuses {
deliveryTab(mdss)
.tabItem {
Label("Delivery", systemImage: "checkmark.message")
}
.tag(CIInfoTab.delivery)
}
historyTab()
.tabItem {
Label("History", systemImage: "clock")
}
.tag(CIInfoTab.history)
if let qi = ci.quotedItem {
quoteTab(qi)
.tabItem {
Label("In reply to", systemImage: "arrowshape.turn.up.left")
}
.tag(CIInfoTab.quote)
}
}
.onAppear {
if chatItemInfo?.memberDeliveryStatuses != nil {
selection = .delivery
}
}
} else {
historyTab()
}
}
@ViewBuilder private func details() -> some View {
let meta = ci.meta
VStack(alignment: .leading, spacing: 16) {
Text(title)
.font(.largeTitle)
.bold()
.padding(.bottom)
infoRow("Sent at", localTimestamp(meta.itemTs))
if !ci.chatDir.sent {
infoRow("Received at", localTimestamp(meta.createdAt))
}
switch (meta.itemDeleted) {
case let .deleted(deletedTs):
if let deletedTs = deletedTs {
infoRow("Deleted at", localTimestamp(deletedTs))
}
case let .moderated(deletedTs, _):
if let deletedTs = deletedTs {
infoRow("Moderated at", localTimestamp(deletedTs))
}
default: EmptyView()
}
if let deleteAt = meta.itemTimed?.deleteAt {
infoRow("Disappears at", localTimestamp(deleteAt))
}
if developerTools {
infoRow("Database ID", "\(meta.itemId)")
infoRow("Record updated at", localTimestamp(meta.updatedAt))
}
}
}
@ViewBuilder private func historyTab() -> some View {
GeometryReader { g in
let maxWidth = (g.size.width - 32) * 0.84
ScrollView {
VStack(alignment: .leading, spacing: 16) {
details()
Divider().padding(.vertical)
Text(title)
.font(.largeTitle)
.bold()
.padding(.bottom)
let maxWidth = (g.size.width - 32) * 0.84
infoRow("Sent at", localTimestamp(meta.itemTs))
if !ci.chatDir.sent {
infoRow("Received at", localTimestamp(meta.createdAt))
}
switch (meta.itemDeleted) {
case let .deleted(deletedTs):
if let deletedTs = deletedTs {
infoRow("Deleted at", localTimestamp(deletedTs))
}
case let .moderated(deletedTs, _):
if let deletedTs = deletedTs {
infoRow("Moderated at", localTimestamp(deletedTs))
}
default: EmptyView()
}
if let deleteAt = meta.itemTimed?.deleteAt {
infoRow("Disappears at", localTimestamp(deleteAt))
}
if developerTools {
infoRow("Database ID", "\(meta.itemId)")
infoRow("Record updated at", localTimestamp(meta.updatedAt))
}
if let chatItemInfo = chatItemInfo,
!chatItemInfo.itemVersions.isEmpty {
Divider().padding(.vertical)
Text("History")
.font(.title2)
.padding(.bottom, 4)
@@ -152,21 +81,16 @@ struct ChatItemInfoView: View {
}
}
}
else {
Text("No history")
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
}
}
.padding()
}
.padding()
.frame(maxHeight: .infinity, alignment: .top)
}
}
@ViewBuilder private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View {
VStack(alignment: .leading, spacing: 4) {
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
versionText(itemVersion)
.allowsHitTesting(false)
.padding(.horizontal, 12)
.padding(.vertical, 6)
@@ -195,9 +119,9 @@ struct ChatItemInfoView: View {
.frame(maxWidth: maxWidth, alignment: .leading)
}
@ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil) -> some View {
if text != "" {
messageText(text, formattedText, sender)
@ViewBuilder private func versionText(_ itemVersion: ChatItemVersion) -> some View {
if itemVersion.msgContent.text != "" {
messageText(itemVersion.msgContent.text, itemVersion.formattedText, nil)
} else {
Text("no text")
.italic()
@@ -205,141 +129,9 @@ struct ChatItemInfoView: View {
}
}
@ViewBuilder private func quoteTab(_ qi: CIQuote) -> some View {
GeometryReader { g in
let maxWidth = (g.size.width - 32) * 0.84
ScrollView {
VStack(alignment: .leading, spacing: 16) {
details()
Divider().padding(.vertical)
Text("In reply to")
.font(.title2)
.padding(.bottom, 4)
quotedMsgView(qi, maxWidth)
}
.padding()
}
.frame(maxHeight: .infinity, alignment: .top)
}
}
@ViewBuilder private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View {
VStack(alignment: .leading, spacing: 4) {
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
.allowsHitTesting(false)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(quotedMsgFrameColor(qi, colorScheme))
.cornerRadius(18)
.contextMenu {
if qi.text != "" {
Button {
showShareSheet(items: [qi.text])
} label: {
Label("Share", systemImage: "square.and.arrow.up")
}
Button {
UIPasteboard.general.string = qi.text
} label: {
Label("Copy", systemImage: "doc.on.doc")
}
}
}
Text(localTimestamp(qi.sentAt))
.foregroundStyle(.secondary)
.font(.caption)
.padding(.horizontal, 12)
}
.frame(maxWidth: maxWidth, alignment: .leading)
}
func quotedMsgFrameColor(_ qi: CIQuote, _ colorScheme: ColorScheme) -> Color {
(qi.chatDir?.sent ?? false)
? (colorScheme == .light ? sentColorLight : sentColorDark)
: Color(uiColor: .tertiarySystemGroupedBackground)
}
@ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
details()
Divider().padding(.vertical)
Text("Delivery")
.font(.title2)
.padding(.bottom, 4)
memberDeliveryStatusesView(memberDeliveryStatuses)
}
.padding()
}
.frame(maxHeight: .infinity, alignment: .top)
}
@ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
VStack(alignment: .leading, spacing: 12) {
let mss = membersStatuses(memberDeliveryStatuses)
if !mss.isEmpty {
ForEach(mss, id: \.0.groupMemberId) { memberStatus in
memberDeliveryStatusView(memberStatus.0, memberStatus.1)
}
} else {
Text("No delivery information")
.foregroundColor(.secondary)
}
}
}
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] {
memberDeliveryStatuses.compactMap({ mds in
if let mem = ChatModel.shared.groupMembers.first(where: { $0.groupMemberId == mds.groupMemberId }) {
return (mem, mds.memberDeliveryStatus)
} else {
return nil
}
})
}
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View {
HStack{
ProfileImage(imageStr: member.image)
.frame(width: 30, height: 30)
.padding(.trailing, 2)
Text(member.chatViewName)
.lineLimit(1)
Spacer()
let v = Group {
if let (icon, statusColor) = status.statusIcon(Color.secondary) {
switch status {
case .sndRcvd:
ZStack(alignment: .trailing) {
Image(systemName: icon)
.foregroundColor(statusColor.opacity(0.67))
.padding(.trailing, 6)
Image(systemName: icon)
.foregroundColor(statusColor.opacity(0.67))
}
default:
Image(systemName: icon)
.foregroundColor(statusColor)
}
} else {
Image(systemName: "ellipsis")
.foregroundColor(Color.secondary)
}
}
if let (title, text) = status.statusInfo {
v.onTapGesture {
alert = .alert(title: title, text: text)
}
} else {
v
}
}
}
private func itemInfoShareText() -> String {
let meta = ci.meta
var shareText: [String] = [String.localizedStringWithFormat(NSLocalizedString("# %@", comment: "copied message info title, # <title>"), title), ""]
var shareText: [String] = [title, ""]
shareText += [String.localizedStringWithFormat(NSLocalizedString("Sent at: %@", comment: "copied message info"), localTimestamp(meta.itemTs))]
if !ci.chatDir.sent {
shareText += [String.localizedStringWithFormat(NSLocalizedString("Received at: %@", comment: "copied message info"), localTimestamp(meta.createdAt))]
@@ -364,27 +156,9 @@ struct ChatItemInfoView: View {
String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt))
]
}
if let qi = ci.quotedItem {
shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")]
let t = qi.text
shareText += [""]
if let sender = qi.getSender(nil) {
shareText += [String.localizedStringWithFormat(
NSLocalizedString("%@ at %@:", comment: "copied message info, <sender> at <time>"),
sender,
localTimestamp(qi.sentAt)
)]
} else {
shareText += [String.localizedStringWithFormat(
NSLocalizedString("%@:", comment: "copied message info"),
localTimestamp(qi.sentAt)
)]
}
shareText += [t != "" ? t : NSLocalizedString("no text", comment: "copied message info in history")]
}
if let chatItemInfo = chatItemInfo,
!chatItemInfo.itemVersions.isEmpty {
shareText += ["", NSLocalizedString("## History", comment: "copied message info")]
shareText += ["", NSLocalizedString("History", comment: "copied message info")]
for (index, itemVersion) in chatItemInfo.itemVersions.enumerated() {
let t = itemVersion.msgContent.text
shareText += [

View File

@@ -12,6 +12,7 @@ import SimpleXChat
struct ChatItemView: View {
var chatInfo: ChatInfo
var chatItem: ChatItem
var showMember = false
var maxWidth: CGFloat = .infinity
@State var scrollProxy: ScrollViewProxy? = nil
@Binding var revealed: Bool
@@ -22,6 +23,7 @@ struct ChatItemView: View {
init(chatInfo: ChatInfo, chatItem: ChatItem, showMember: Bool = false, maxWidth: CGFloat = .infinity, scrollProxy: ScrollViewProxy? = nil, revealed: Binding<Bool>, allowMenu: Binding<Bool> = .constant(false), audioPlayer: Binding<AudioPlayer?> = .constant(nil), playbackState: Binding<VoiceMessagePlaybackState> = .constant(.noPlayback), playbackTime: Binding<TimeInterval?> = .constant(nil)) {
self.chatInfo = chatInfo
self.chatItem = chatItem
self.showMember = showMember
self.maxWidth = maxWidth
_scrollProxy = .init(initialValue: scrollProxy)
_revealed = revealed
@@ -34,14 +36,14 @@ struct ChatItemView: View {
var body: some View {
let ci = chatItem
if chatItem.meta.itemDeleted != nil && !revealed {
MarkedDeletedItemView(chatItem: chatItem)
MarkedDeletedItemView(chatItem: chatItem, showMember: showMember)
} else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
EmojiItemView(chatItem: ci)
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
CIVoiceView(chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu)
} else if ci.content.msgContent == nil {
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
} else {
framedItemView()
}
@@ -51,14 +53,14 @@ struct ChatItemView: View {
}
private func framedItemView() -> some View {
FramedItemView(chatInfo: chatInfo, chatItem: chatItem, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
FramedItemView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
}
}
struct ChatItemContentView<Content: View>: View {
@EnvironmentObject var chatModel: ChatModel
var chatInfo: ChatInfo
var chatItem: ChatItem
var showMember: Bool
var msgContentView: () -> Content
var body: some View {
@@ -69,11 +71,10 @@ struct ChatItemContentView<Content: View>: View {
case .rcvDeleted: deletedItemView()
case let .sndCall(status, duration): callItemView(status, duration)
case let .rcvCall(status, duration): callItemView(status, duration)
case let .rcvIntegrityError(msgError): IntegrityErrorItemView(msgError: msgError, chatItem: chatItem)
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem)
case let .rcvIntegrityError(msgError): IntegrityErrorItemView(msgError: msgError, chatItem: chatItem, showMember: showMember)
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem, showMember: showMember)
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case .rcvGroupEvent(.memberConnected): CIEventView(eventText: membersConnectedItemText)
case .rcvGroupEvent: eventItemView()
case .sndGroupEvent: eventItemView()
case .rcvConnEvent: eventItemView()
@@ -95,7 +96,7 @@ struct ChatItemContentView<Content: View>: View {
}
private func deletedItemView() -> some View {
DeletedItemView(chatItem: chatItem)
DeletedItemView(chatItem: chatItem, showMember: showMember)
}
private func callItemView(_ status: CICallStatus, _ duration: Int) -> some View {
@@ -107,54 +108,12 @@ struct ChatItemContentView<Content: View>: View {
}
private func eventItemView() -> some View {
return CIEventView(eventText: eventItemViewText())
}
private func eventItemViewText() -> Text {
if let member = chatItem.memberDisplayName {
return Text(member + " ")
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
+ chatEventText(chatItem)
} else {
return chatEventText(chatItem)
}
CIEventView(chatItem: chatItem)
}
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
CIChatFeatureView(chatItem: chatItem, feature: feature, iconColor: iconColor)
}
private var membersConnectedItemText: Text {
if let t = membersConnectedText {
return chatEventText(t, chatItem.timestampText)
} else {
return eventItemViewText()
}
}
private var membersConnectedText: LocalizedStringKey? {
let ns = chatModel.getConnectedMemberNames(chatItem)
return ns.count > 3
? "\(ns[0]), \(ns[1]) and \(ns.count - 2) other members connected"
: ns.count == 3
? "\(ns[0] + ", " + ns[1]) and \(ns[2]) connected"
: ns.count == 2
? "\(ns[0]) and \(ns[1]) connected"
: nil
}
}
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text {
(Text(eventText) + Text(" ") + ts)
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
}
func chatEventText(_ ci: ChatItem) -> Text {
chatEventText("\(ci.content.text)", ci.timestampText)
}
struct ChatItemView_Previews: PreviewProvider {
@@ -166,9 +125,9 @@ struct ChatItemView_Previews: PreviewProvider {
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent, itemLive: true), revealed: Binding.constant(true))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemLive: true), revealed: Binding.constant(true))
}
.previewLayout(.fixed(width: 360, height: 70))
.environmentObject(Chat.sampleData)

View File

@@ -22,7 +22,7 @@ struct ChatView: View {
@State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState()
@State private var deletingItem: ChatItem? = nil
@State private var keyboardVisible = false
@FocusState private var keyboardVisible: Bool
@State private var showDeleteMessage = false
@State private var connectionStats: ConnectionStats?
@State private var customUserProfile: Profile?
@@ -39,16 +39,6 @@ struct ChatView: View {
@State private var selectedMember: GroupMember? = nil
var body: some View {
if #available(iOS 16.0, *) {
viewBody
.scrollDismissesKeyboard(.immediately)
.keyboardPadding()
} else {
viewBody
}
}
private var viewBody: some View {
let cInfo = chat.chatInfo
return VStack(spacing: 0) {
if searchMode {
@@ -75,14 +65,17 @@ struct ChatView: View {
.navigationTitle(cInfo.chatViewName)
.navigationBarTitleDisplayMode(.inline)
.onAppear {
initChatView()
}
.onChange(of: chatModel.chatId) { cId in
if cId != nil {
initChatView()
} else {
dismiss()
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
composeState = draft
}
if chat.chatStats.unreadChat {
Task {
await markChatUnread(chat, unreadChat: false)
}
}
}
.onChange(of: chatModel.chatId) { _ in
if chatModel.chatId == nil { dismiss() }
}
.onDisappear {
VideoPlayerView.players.removeAll()
@@ -192,32 +185,6 @@ struct ChatView: View {
}
}
private func initChatView() {
let cInfo = chat.chatInfo
if case let .direct(contact) = cInfo {
Task {
do {
let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId)
await MainActor.run {
if let s = stats {
chatModel.updateContactConnectionStats(contact, s)
}
}
} catch let error {
logger.error("apiContactInfo error: \(responseError(error))")
}
}
}
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
composeState = draft
}
if chat.chatStats.unreadChat {
Task {
await markChatUnread(chat, unreadChat: false)
}
}
}
private func searchToolbar() -> some View {
HStack {
HStack {
@@ -261,7 +228,7 @@ struct ChatView: View {
return GeometryReader { g in
ScrollViewReader { proxy in
ScrollView {
LazyVStack(spacing: 0) {
LazyVStack(spacing: 5) {
ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in
let voiceNoFrame = voiceWithoutFrame(ci)
let maxWidth = cInfo.chatType == .group
@@ -430,77 +397,68 @@ struct ChatView: View {
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
if case let .groupRcv(member) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
let (prevItem, nextItem) = chatModel.getChatItemNeighbors(ci)
if ci.memberConnected != nil && nextItem?.memberConnected != nil {
// memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView
ZStack {} // scroll doesn't work if it's EmptyView()
} else {
if prevItem == nil || showMemberImage(member, prevItem) {
VStack(alignment: .leading, spacing: 4) {
if ci.content.showMemberName {
Text(member.displayName)
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, memberImageSize + 14)
.padding(.top, 7)
let prevItem = chatModel.getPrevChatItem(ci)
HStack(alignment: .top, spacing: 0) {
let showMember = prevItem == nil || showMemberImage(member, prevItem)
if showMember {
ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
.onTapGesture { selectedMember = member }
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
}
HStack(alignment: .top, spacing: 8) {
ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
.onTapGesture { selectedMember = member }
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
}
chatItemWithMenu(ci, maxWidth)
}
}
.padding(.top, 5)
.padding(.trailing)
.padding(.leading, 12)
} else {
chatItemWithMenu(ci, maxWidth)
.padding(.top, 5)
.padding(.trailing)
.padding(.leading, memberImageSize + 8 + 12)
Rectangle().fill(.clear)
.frame(width: memberImageSize, height: memberImageSize)
}
ChatItemWithMenu(
ci: ci,
showMember: showMember,
maxWidth: maxWidth,
scrollProxy: scrollProxy,
deleteMessage: deleteMessage,
deletingItem: $deletingItem,
composeState: $composeState,
showDeleteMessage: $showDeleteMessage
)
.padding(.leading, 8)
.environmentObject(chat)
}
.padding(.trailing)
.padding(.leading, 12)
} else {
chatItemWithMenu(ci, maxWidth)
.padding(.horizontal)
.padding(.top, 5)
ChatItemWithMenu(
ci: ci,
maxWidth: maxWidth,
scrollProxy: scrollProxy,
deleteMessage: deleteMessage,
deletingItem: $deletingItem,
composeState: $composeState,
showDeleteMessage: $showDeleteMessage
)
.padding(.horizontal)
.environmentObject(chat)
}
}
private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
ChatItemWithMenu(
ci: ci,
maxWidth: maxWidth,
scrollProxy: scrollProxy,
deleteMessage: deleteMessage,
deletingItem: $deletingItem,
composeState: $composeState,
showDeleteMessage: $showDeleteMessage
)
.environmentObject(chat)
}
private struct ChatItemWithMenu: View {
@EnvironmentObject var chat: Chat
@Environment(\.colorScheme) var colorScheme
var ci: ChatItem
var showMember: Bool = false
var maxWidth: CGFloat
var scrollProxy: ScrollViewProxy?
var deleteMessage: (CIDeleteMode) -> Void
@Binding var deletingItem: ChatItem?
@Binding var composeState: ComposeState
@Binding var showDeleteMessage: Bool
@State private var revealed = false
@State private var showChatItemInfoSheet: Bool = false
@State private var chatItemInfo: ChatItemInfo?
@State private var allowMenu: Bool = true
@State private var audioPlayer: AudioPlayer?
@State private var playbackState: VoiceMessagePlaybackState = .noPlayback
@State private var playbackTime: TimeInterval?
@@ -513,9 +471,8 @@ struct ChatView: View {
)
VStack(alignment: alignment.horizontal, spacing: 3) {
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
chatItemReactions()
.padding(.bottom, 4)
@@ -637,7 +594,6 @@ struct ChatView: View {
menu.append(viewInfoUIAction())
menu.append(deleteUIAction())
} else if ci.isDeletedContent {
menu.append(viewInfoUIAction())
menu.append(deleteUIAction())
}
return menu
@@ -660,7 +616,7 @@ struct ChatView: View {
private func reactionUIMenuPreiOS16(_ rs: [UIAction]) -> UIMenu {
UIMenu(
title: NSLocalizedString("React", comment: "chat item menu"),
title: NSLocalizedString("React...", comment: "chat item menu"),
image: UIImage(systemName: "face.smiling"),
children: rs
)
@@ -780,12 +736,6 @@ struct ChatView: View {
await MainActor.run {
chatItemInfo = ciInfo
}
if case let .group(gInfo) = chat.chatInfo {
let groupMembers = await apiListMembers(gInfo.groupId)
await MainActor.run {
ChatModel.shared.groupMembers = groupMembers
}
}
} catch let error {
logger.error("apiGetChatItemInfo error: \(responseError(error))")
}

View File

@@ -44,6 +44,7 @@ struct ComposeState {
var contextItem: ComposeContextItem
var voiceMessageRecordingState: VoiceMessageRecordingState
var inProgress = false
var disabled = false
var useLinkPreviews: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
init(
@@ -233,14 +234,13 @@ struct ComposeView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var chat: Chat
@Binding var composeState: ComposeState
@Binding var keyboardVisible: Bool
@FocusState.Binding var keyboardVisible: Bool
@State var linkUrl: URL? = nil
@State var prevLinkUrl: URL? = nil
@State var pendingLinkUrl: URL? = nil
@State var cancelledLinks: Set<String> = []
@Environment(\.colorScheme) private var colorScheme
@State private var showChooseSource = false
@State private var showMediaPicker = false
@State private var showTakePhoto = false
@@ -255,8 +255,6 @@ struct ComposeView: View {
// this is a workaround to fire an explicit event in certain cases
@State private var stopPlayback: Bool = false
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
var body: some View {
VStack(spacing: 0) {
contextItemView()
@@ -311,10 +309,7 @@ struct ComposeView: View {
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages),
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
keyboardVisible: $keyboardVisible,
sendButtonColor: chat.chatInfo.incognito
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
: .accentColor
keyboardVisible: $keyboardVisible
)
.padding(.trailing, 12)
.background(.background)
@@ -447,15 +442,7 @@ struct ComposeView: View {
} else if (composeState.inProgress) {
clearCurrentDraft()
} else if !composeState.empty {
if case .recording = composeState.voiceMessageRecordingState {
finishVoiceMessageRecording()
if let fileName = composeState.voiceMessageRecordingFileName {
chatModel.filesToDelete.insert(getAppFilePath(fileName))
}
}
if saveLastDraft {
saveCurrentDraft()
}
saveCurrentDraft()
} else {
cancelCurrentVoiceRecording()
clearCurrentDraft()
@@ -668,28 +655,27 @@ struct ComposeView: View {
return sent
func sending() async {
await MainActor.run { composeState.inProgress = true }
await MainActor.run { composeState.disabled = true }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if composeState.disabled { composeState.inProgress = true }
}
}
func updateMessage(_ ei: ChatItem, live: Bool) async -> ChatItem? {
if let oldMsgContent = ei.content.msgContent {
do {
let mc = updateMsgContent(oldMsgContent)
if mc != oldMsgContent || (ei.meta.itemLive ?? false) {
let chatItem = try await apiUpdateChatItem(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
itemId: ei.id,
msg: mc,
live: live
)
await MainActor.run {
_ = self.chatModel.upsertChatItem(self.chat.chatInfo, chatItem)
}
return chatItem
} else {
return nil
let chatItem = try await apiUpdateChatItem(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
itemId: ei.id,
msg: mc,
live: live
)
await MainActor.run {
_ = self.chatModel.upsertChatItem(self.chat.chatInfo, chatItem)
}
return chatItem
} catch {
logger.error("ChatView.sendMessage error: \(error.localizedDescription)")
AlertManager.shared.showAlertMsg(title: "Error updating message", message: "Error: \(responseError(error))")
@@ -862,6 +848,7 @@ struct ComposeView: View {
private func clearState(live: Bool = false) {
if live {
composeState.disabled = false
composeState.inProgress = false
} else {
composeState = ComposeState()
@@ -874,6 +861,12 @@ struct ComposeView: View {
}
private func saveCurrentDraft() {
if case .recording = composeState.voiceMessageRecordingState {
finishVoiceMessageRecording()
if let fileName = composeState.voiceMessageRecordingFileName {
chatModel.filesToDelete.insert(getAppFilePath(fileName))
}
}
chatModel.draft = composeState
chatModel.draftChatId = chat.id
}
@@ -950,18 +943,19 @@ struct ComposeView_Previews: PreviewProvider {
static var previews: some View {
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
@State var composeState = ComposeState(message: "hello")
@FocusState var keyboardVisible: Bool
return Group {
ComposeView(
chat: chat,
composeState: $composeState,
keyboardVisible: Binding.constant(true)
keyboardVisible: $keyboardVisible
)
.environmentObject(ChatModel())
ComposeView(
chat: chat,
composeState: $composeState,
keyboardVisible: Binding.constant(true)
keyboardVisible: $keyboardVisible
)
.environmentObject(ChatModel())
}

View File

@@ -22,14 +22,13 @@ struct ContextItemView: View {
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(.secondary)
if let sender = contextItem.memberDisplayName {
VStack(alignment: .leading, spacing: 4) {
Text(sender).font(.caption).foregroundColor(.secondary)
msgContentView(lines: 2)
}
} else {
msgContentView(lines: 3)
}
MsgContentView(
text: contextItem.text,
formattedText: contextItem.formattedText,
sender: contextItem.memberDisplayName
)
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
.lineLimit(3)
Spacer()
Button {
withAnimation {
@@ -45,15 +44,6 @@ struct ContextItemView: View {
.background(chatItemFrameColor(contextItem, colorScheme))
.padding(.top, 8)
}
private func msgContentView(lines: Int) -> some View {
MsgContentView(
text: contextItem.text,
formattedText: contextItem.formattedText
)
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
.lineLimit(lines)
}
}
struct ContextItemView_Previews: PreviewProvider {

View File

@@ -16,7 +16,7 @@ struct NativeTextEditor: UIViewRepresentable {
@Binding var disableEditing: Bool
let height: CGFloat
let font: UIFont
@Binding var focused: Bool
@FocusState.Binding var focused: Bool
let alignment: TextAlignment
let onImagesAdded: ([UploadContent]) -> Void
@@ -144,12 +144,13 @@ private class CustomUITextField: UITextView, UITextViewDelegate {
struct NativeTextEditor_Previews: PreviewProvider{
static var previews: some View {
@FocusState var keyboardVisible: Bool
return NativeTextEditor(
text: Binding.constant("Hello, world!"),
disableEditing: Binding.constant(false),
height: 100,
font: UIFont.preferredFont(forTextStyle: .body),
focused: Binding.constant(false),
focused: $keyboardVisible,
alignment: TextAlignment.leading,
onImagesAdded: { _ in }
)

View File

@@ -27,8 +27,7 @@ struct SendMessageView: View {
var onMediaAdded: ([UploadContent]) -> Void
@State private var holdingVMR = false
@Namespace var namespace
@Binding var keyboardVisible: Bool
var sendButtonColor = Color.accentColor
@FocusState.Binding var keyboardVisible: Bool
@State private var teHeight: CGFloat = 42
@State private var teFont: Font = .body
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
@@ -37,7 +36,6 @@ struct SendMessageView: View {
@State private var showCustomDisappearingMessageDialogue = false
@State private var showCustomTimePicker = false
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
@State private var progressByTimeout = false
var maxHeight: CGFloat = 360
var minHeight: CGFloat = 37
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
@@ -83,7 +81,7 @@ struct SendMessageView: View {
}
}
if progressByTimeout {
if composeState.inProgress {
ProgressView()
.scaleEffect(1.4)
.frame(width: 31, height: 31, alignment: .center)
@@ -104,15 +102,6 @@ struct SendMessageView: View {
.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)
.frame(height: teHeight)
}
.onChange(of: composeState.inProgress) { inProgress in
if inProgress {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
progressByTimeout = composeState.inProgress
}
} else {
progressByTimeout = false
}
}
.padding(.vertical, 8)
}
@@ -130,7 +119,7 @@ struct SendMessageView: View {
startVoiceMessageRecording: startVoiceMessageRecording,
finishVoiceMessageRecording: finishVoiceMessageRecording,
holdingVMR: $holdingVMR,
disabled: composeState.inProgress
disabled: composeState.disabled
)
} else {
voiceMessageNotAllowedButton()
@@ -170,13 +159,13 @@ struct SendMessageView: View {
? "checkmark.circle.fill"
: "arrow.up.circle.fill")
.resizable()
.foregroundColor(sendButtonColor)
.foregroundColor(.accentColor)
.frame(width: sendButtonSize, height: sendButtonSize)
.opacity(sendButtonOpacity)
}
.disabled(
!composeState.sendEnabled ||
composeState.inProgress ||
composeState.disabled ||
(!voiceMessageAllowed && composeState.voicePreview) ||
composeState.endLiveDisabled
)
@@ -304,7 +293,7 @@ struct SendMessageView: View {
Image(systemName: "mic")
.foregroundColor(.secondary)
}
.disabled(composeState.inProgress)
.disabled(composeState.disabled)
.frame(width: 29, height: 29)
.padding([.bottom, .trailing], 4)
}
@@ -389,7 +378,7 @@ struct SendMessageView: View {
Image(systemName: "stop.fill")
.foregroundColor(.accentColor)
}
.disabled(composeState.inProgress)
.disabled(composeState.disabled)
.frame(width: 29, height: 29)
.padding([.bottom, .trailing], 4)
}
@@ -412,6 +401,7 @@ struct SendMessageView_Previews: PreviewProvider {
@State var composeStateNew = ComposeState()
let ci = ChatItem.getSample(1, .directSnd, .now, "hello")
@State var composeStateEditing = ComposeState(editingItem: ci)
@FocusState var keyboardVisible: Bool
@State var sendEnabled: Bool = true
return Group {
@@ -422,7 +412,7 @@ struct SendMessageView_Previews: PreviewProvider {
composeState: $composeStateNew,
sendMessage: { _ in },
onMediaAdded: { _ in },
keyboardVisible: Binding.constant(true)
keyboardVisible: $keyboardVisible
)
}
VStack {
@@ -432,7 +422,7 @@ struct SendMessageView_Previews: PreviewProvider {
composeState: $composeStateEditing,
sendMessage: { _ in },
onMediaAdded: { _ in },
keyboardVisible: Binding.constant(true)
keyboardVisible: $keyboardVisible
)
}
}

View File

@@ -9,8 +9,6 @@
import SwiftUI
import SimpleXChat
let SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
struct GroupChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
@@ -23,8 +21,6 @@ struct GroupChatInfoView: View {
@State private var showAddMembersSheet: Bool = false
@State private var connectionStats: ConnectionStats?
@State private var connectionCode: String?
@State private var sendReceipts = SendReceipts.userDefault(true)
@State private var sendReceiptsUserDefault = true
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State private var searchText: String = ""
@FocusState private var searchFocussed
@@ -34,7 +30,6 @@ struct GroupChatInfoView: View {
case clearChatAlert
case leaveGroupAlert
case cantInviteIncognitoAlert
case largeGroupReceiptsDisabled
var id: GroupChatInfoViewAlert { get { self } }
}
@@ -57,11 +52,6 @@ struct GroupChatInfoView: View {
addOrEditWelcomeMessage()
}
groupPreferencesButton($groupInfo)
if members.filter({ $0.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
sendReceiptsOption()
} else {
sendReceiptsOptionDisabled()
}
} header: {
Text("")
} footer: {
@@ -125,14 +115,9 @@ struct GroupChatInfoView: View {
case .clearChatAlert: return clearChatAlert()
case .leaveGroupAlert: return leaveGroupAlert()
case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert()
case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert()
}
}
.onAppear {
if let currentUser = chatModel.currentUser {
sendReceiptsUserDefault = currentUser.sendRcptsSmallGroups
}
sendReceipts = SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault)
do {
if let link = try apiGetGroupLink(groupInfo.groupId) {
(groupLink, groupLinkMemberRole) = link
@@ -141,7 +126,6 @@ struct GroupChatInfoView: View {
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
}
}
.keyboardPadding()
}
private func groupInfoHeader() -> some View {
@@ -153,14 +137,12 @@ struct GroupChatInfoView: View {
.padding()
Text(cInfo.displayName)
.font(.largeTitle)
.multilineTextAlignment(.center)
.lineLimit(4)
.lineLimit(1)
.padding(.bottom, 2)
if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName {
Text(cInfo.fullName)
.font(.title2)
.multilineTextAlignment(.center)
.lineLimit(8)
.lineLimit(2)
}
}
.frame(maxWidth: .infinity, alignment: .center)
@@ -343,38 +325,6 @@ struct GroupChatInfoView: View {
secondaryButton: .cancel()
)
}
private func sendReceiptsOption() -> some View {
Picker(selection: $sendReceipts) {
ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in
Text(opt.text)
}
} label: {
Label("Send receipts", systemImage: "checkmark.message")
}
.frame(height: 36)
.onChange(of: sendReceipts) { _ in
setSendReceipts()
}
}
private func setSendReceipts() {
var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults
chatSettings.sendRcpts = sendReceipts.bool()
updateChatSettings(chat, chatSettings: chatSettings)
}
private func sendReceiptsOptionDisabled() -> some View {
HStack {
Label("Send receipts", systemImage: "checkmark.message")
Spacer()
Text("disabled")
.foregroundStyle(.secondary)
}
.onTapGesture {
alert = .largeGroupReceiptsDisabled
}
}
}
func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View {
@@ -403,13 +353,6 @@ func cantInviteIncognitoAlert() -> Alert {
)
}
func largeGroupReceiptsDisabledAlert() -> Alert {
Alert(
title: Text("Receipts are disabled"),
message: Text("This group has over \(SMALL_GROUPS_RCPS_MEM_LIMIT) members, delivery receipts are not sent.")
)
}
struct GroupChatInfoView_Previews: PreviewProvider {
static var previews: some View {
GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData)

View File

@@ -19,7 +19,6 @@ struct GroupMemberInfoView: View {
@State private var connectionCode: String? = nil
@State private var newRole: GroupMemberRole = .member
@State private var alert: GroupMemberInfoViewAlert?
@State private var connectToMemberDialog: Bool = false
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State private var justOpened = true
@@ -28,7 +27,6 @@ struct GroupMemberInfoView: View {
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
case switchAddressAlert
case abortSwitchAddressAlert
case syncConnectionForceAlert
case connRequestSentAlert(type: ConnReqType)
case error(title: LocalizedStringKey, error: LocalizedStringKey)
case other(alert: Alert)
@@ -39,7 +37,6 @@ struct GroupMemberInfoView: View {
case let .changeMemberRoleAlert(_, role): return "changeMemberRoleAlert \(role.rawValue)"
case .switchAddressAlert: return "switchAddressAlert"
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
case .connRequestSentAlert: return "connRequestSentAlert"
case let .error(title, _): return "error \(title)"
case let .other(alert): return "other \(alert)"
@@ -80,13 +77,6 @@ struct GroupMemberInfoView: View {
}
}
if let code = connectionCode { verifyCodeButton(code) }
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
}
// } else if developerTools {
// synchronizeConnectionButtonForce()
// }
}
}
@@ -139,18 +129,12 @@ struct GroupMemberInfoView: View {
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil })
if connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|| connStats.ratchetSyncSendProhibited
)
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch })
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
@@ -178,7 +162,7 @@ struct GroupMemberInfoView: View {
}
newRole = member.memberRole
do {
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let stats = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
member = mem
connectionStats = stats
@@ -201,7 +185,6 @@ struct GroupMemberInfoView: View {
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
case let .connRequestSentAlert(type): return connReqSentAlert(type)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
case let .other(alert): return alert
@@ -211,19 +194,15 @@ struct GroupMemberInfoView: View {
func connectViaAddressButton(_ contactLink: String) -> some View {
Button {
connectToMemberDialog = true
connectViaAddress(contactLink)
} label: {
Label("Connect", systemImage: "link")
}
.confirmationDialog("Connect directly", isPresented: $connectToMemberDialog, titleVisibility: .visible) {
Button("Use current profile") { connectViaAddress(incognito: false, contactLink: contactLink) }
Button("Use new incognito profile") { connectViaAddress(incognito: true, contactLink: contactLink) }
}
}
func connectViaAddress(incognito: Bool, contactLink: String) {
func connectViaAddress(_ contactLink: String) {
Task {
let (connReqType, connectAlert) = await apiConnect_(incognito: incognito, connReq: contactLink)
let (connReqType, connectAlert) = await apiConnect_(connReq: contactLink)
if let connReqType = connReqType {
alert = .connRequestSentAlert(type: connReqType)
} else if let connectAlert = connectAlert {
@@ -266,30 +245,19 @@ struct GroupMemberInfoView: View {
.frame(width: 192, height: 192)
.padding(.top, 12)
.padding()
if mem.verified {
(
Text(Image(systemName: "checkmark.shield"))
.foregroundColor(.secondary)
.font(.title2)
+ Text(" ")
+ Text(mem.displayName)
.font(.largeTitle)
)
.multilineTextAlignment(.center)
.lineLimit(2)
.padding(.bottom, 2)
} else {
HStack {
if mem.verified {
Image(systemName: "checkmark.shield")
}
Text(mem.displayName)
.font(.largeTitle)
.multilineTextAlignment(.center)
.lineLimit(2)
.padding(.bottom, 2)
.lineLimit(1)
}
.padding(.bottom, 2)
if mem.fullName != "" && mem.fullName != mem.displayName {
Text(mem.fullName)
.font(.title2)
.multilineTextAlignment(.center)
.lineLimit(4)
.lineLimit(2)
}
}
.frame(maxWidth: .infinity, alignment: .center)
@@ -323,24 +291,7 @@ struct GroupMemberInfoView: View {
systemImage: member.verified ? "checkmark.shield" : "shield"
)
}
}
private func synchronizeConnectionButton() -> some View {
Button {
syncMemberConnection(force: false)
} label: {
Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath")
.foregroundColor(.orange)
}
}
private func synchronizeConnectionButtonForce() -> some View {
Button {
alert = .syncConnectionForceAlert
} label: {
Label("Renegotiate encryption", systemImage: "exclamationmark.triangle")
.foregroundColor(.red)
}
}
private func removeMemberButton(_ mem: GroupMember) -> some View {
@@ -406,11 +357,7 @@ struct GroupMemberInfoView: View {
Task {
do {
let stats = try apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
connectionStats = stats
await MainActor.run {
chatModel.updateGroupMemberConnectionStats(groupInfo, member, stats)
dismiss()
}
connectionStats = stats
} catch let error {
logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))")
let a = getErrorAlert(error, "Error changing address")
@@ -426,9 +373,6 @@ struct GroupMemberInfoView: View {
do {
let stats = try apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
connectionStats = stats
await MainActor.run {
chatModel.updateGroupMemberConnectionStats(groupInfo, member, stats)
}
} catch let error {
logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))")
let a = getErrorAlert(error, "Error aborting address change")
@@ -438,25 +382,6 @@ struct GroupMemberInfoView: View {
}
}
}
private func syncMemberConnection(force: Bool) {
Task {
do {
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force)
connectionStats = stats
await MainActor.run {
chatModel.updateGroupMemberConnectionStats(groupInfo, mem, stats)
dismiss()
}
} catch let error {
logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
}
struct GroupMemberInfoView_Previews: PreviewProvider {

View File

@@ -19,7 +19,7 @@ struct ScanCodeView: View {
VStack(alignment: .leading) {
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.border(.gray)
Text("Scan security code from your contact's app.")
.padding(.top)
}

View File

@@ -64,7 +64,7 @@ struct VerifyCodeView: View {
HStack {
NavigationLink {
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
.navigationBarTitleDisplayMode(.large)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Scan code")
} label: {
Label("Scan code", systemImage: "qrcode")

View File

@@ -222,15 +222,9 @@ struct ChatListNavLink: View {
ContactRequestView(contactRequest: contactRequest, chat: chat)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) }
} label: { Label("Accept", systemImage: "checkmark") }
.tint(.accentColor)
Button {
Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
}
.tint(.indigo)
Task { await acceptContactRequest(contactRequest) }
} label: { Label("Accept", systemImage: chatModel.incognito ? "theatermasks" : "checkmark") }
.tint(chatModel.incognito ? .indigo : .accentColor)
Button {
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest))
} label: {
@@ -240,10 +234,9 @@ struct ChatListNavLink: View {
}
.frame(height: rowHeights[dynamicTypeSize])
.onTapGesture { showContactRequestDialog = true }
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } }
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } }
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } }
.confirmationDialog("Connection request", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
Button(chatModel.incognito ? "Accept incognito" : "Accept contact") { Task { await acceptContactRequest(contactRequest) } }
Button("Reject contact (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } }
}
}
@@ -270,7 +263,6 @@ struct ChatListNavLink: View {
.sheet(isPresented: $showContactConnectionInfo) {
if case let .contactConnection(contactConnection) = chat.chatInfo {
ContactConnectionInfo(contactConnection: contactConnection)
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
}
}
.onTapGesture {
@@ -387,7 +379,6 @@ struct ChatListNavLink: View {
.onTapGesture { showInvalidJSON = true }
.sheet(isPresented: $showInvalidJSON) {
invalidJSONView(json)
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
}
}
}

View File

@@ -18,14 +18,6 @@ struct ChatListView: View {
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
var body: some View {
if #available(iOS 16.0, *) {
viewBody.scrollDismissesKeyboard(.immediately)
} else {
viewBody
}
}
private var viewBody: some View {
ZStack(alignment: .topLeading) {
NavStackCompat(
isActive: Binding(
@@ -60,41 +52,15 @@ struct ChatListView: View {
chatList
}
}
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
.onAppear() { connectViaUrl() }
.onDisappear() { withAnimation { userPickerVisible = false } }
.refreshable {
AlertManager.shared.showAlert(Alert(
title: Text("Reconnect servers?"),
message: Text("Reconnect all connected servers to force message delivery. It uses additional traffic."),
primaryButton: .default(Text("Ok")) {
Task {
do {
try await reconnectAllServers()
} catch let error {
AlertManager.shared.showAlertMsg(title: "Error", message: "\(responseError(error))")
}
}
},
secondaryButton: .cancel()
))
}
.offset(x: -8)
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
let user = chatModel.currentUser ?? User.sampleData
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: user.image, color: Color(uiColor: .quaternaryLabel))
.frame(width: 32, height: 32)
.padding(.trailing, 4)
let allRead = chatModel.users
.filter { u in !u.user.activeUser && !u.user.hidden }
.allSatisfy { u in u.unreadCount == 0 }
if !allRead {
unreadBadge(size: 12)
}
}
.onTapGesture {
Button {
if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 {
withAnimation {
userPickerVisible.toggle()
@@ -102,10 +68,28 @@ struct ChatListView: View {
} else {
showSettings = true
}
} label: {
let user = chatModel.currentUser ?? User.sampleData
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: user.image, color: Color(uiColor: .quaternaryLabel))
.frame(width: 32, height: 32)
.padding(.trailing, 4)
let allRead = chatModel.users
.filter { u in !u.user.activeUser && !u.user.hidden }
.allSatisfy { u in u.unreadCount == 0 }
if !allRead {
unreadBadge(size: 12)
}
}
}
}
ToolbarItem(placement: .principal) {
HStack(spacing: 4) {
if (chatModel.incognito) {
Image(systemName: "theatermasks")
.foregroundColor(.indigo)
.padding(.trailing, 8)
}
Text("Chats")
.font(.headline)
if chatModel.chats.count > 0 {

View File

@@ -15,8 +15,6 @@ struct ChatPreviewView: View {
@Environment(\.colorScheme) var colorScheme
var darkGreen = Color(red: 0, green: 0.5, blue: 0)
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
var body: some View {
let cItem = chat.chatItems.last
return HStack(spacing: 8) {
@@ -43,9 +41,11 @@ struct ChatPreviewView: View {
ZStack(alignment: .topTrailing) {
chatMessagePreview(cItem)
chatStatusImage()
.padding(.top, 26)
.frame(maxWidth: .infinity, alignment: .trailing)
if case .direct = chat.chatInfo {
chatStatusImage()
.padding(.top, 24)
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.padding(.trailing, 8)
@@ -59,9 +59,12 @@ struct ChatPreviewView: View {
@ViewBuilder private func chatPreviewImageOverlayIcon() -> some View {
if case let .group(groupInfo) = chat.chatInfo {
switch (groupInfo.membership.memberStatus) {
case .memLeft: groupInactiveIcon()
case .memRemoved: groupInactiveIcon()
case .memGroupDeleted: groupInactiveIcon()
case .memLeft:
groupInactiveIcon()
case .memRemoved:
groupInactiveIcon()
case .memGroupDeleted:
groupInactiveIcon()
default: EmptyView()
}
} else {
@@ -71,7 +74,7 @@ struct ChatPreviewView: View {
@ViewBuilder private func groupInactiveIcon() -> some View {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary.opacity(0.65))
.foregroundColor(.secondary)
.background(Circle().foregroundColor(Color(uiColor: .systemBackground)))
}
@@ -103,7 +106,7 @@ struct ChatPreviewView: View {
.kerning(-2)
}
private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View {
private func chatPreviewLayout(_ text: Text) -> some View {
ZStack(alignment: .topTrailing) {
text
.lineLimit(2)
@@ -111,8 +114,6 @@ struct ChatPreviewView: View {
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.leading, 8)
.padding(.trailing, 36)
.privacySensitive(!showChatPreviews && !draft)
.redacted(reason: .privacy)
let s = chat.chatStats
if s.unreadCount > 0 || s.unreadChat {
unreadCountText(s.unreadCount)
@@ -174,7 +175,7 @@ struct ChatPreviewView: View {
@ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View {
if chatModel.draftChatId == chat.id, let draft = chatModel.draft {
chatPreviewLayout(messageDraft(draft), draft: true)
chatPreviewLayout(messageDraft(draft))
} else if let cItem = cItem {
chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem))
} else {
@@ -197,7 +198,10 @@ struct ChatPreviewView: View {
@ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View {
groupInfo.membership.memberIncognito
? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)")
: chatPreviewInfoText("you are invited to group")
: (chatModel.incognito
? chatPreviewInfoText("join as \(chatModel.currentUser?.profile.displayName ?? "yourself")")
: chatPreviewInfoText("you are invited to group")
)
}
@ViewBuilder private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View {
@@ -225,7 +229,7 @@ struct ChatPreviewView: View {
switch chat.chatInfo {
case let .direct(contact):
switch (chatModel.contactNetworkStatus(contact)) {
case .connected: incognitoIcon(chat.chatInfo.incognito)
case .connected: EmptyView()
case .error:
Image(systemName: "exclamationmark.circle")
.resizable()
@@ -236,23 +240,11 @@ struct ChatPreviewView: View {
ProgressView()
}
default:
incognitoIcon(chat.chatInfo.incognito)
EmptyView()
}
}
}
@ViewBuilder func incognitoIcon(_ incognito: Bool) -> some View {
if incognito {
Image(systemName: "theatermasks")
.resizable()
.scaledToFit()
.frame(width: 22, height: 22)
.foregroundColor(.secondary)
} else {
EmptyView()
}
}
func unreadCountText(_ n: Int) -> Text {
Text(n > 999 ? "\(n / 1000)k" : n > 0 ? "\(n)" : "")
}
@@ -266,20 +258,20 @@ struct ChatPreviewView_Previews: PreviewProvider {
))
ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))]
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)]
))
ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))],
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)],
chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0)
))
ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))]
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now))]
))
ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))],
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)],
chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0)
))
ChatPreviewView(chat: Chat(

View File

@@ -15,7 +15,6 @@ struct ContactConnectionInfo: View {
@State var contactConnection: PendingContactConnection
@State private var alert: CCInfoAlert?
@State private var localAlias = ""
@State private var showIncognitoSheet = false
@FocusState private var aliasTextFieldFocused: Bool
enum CCInfoAlert: Identifiable {
@@ -32,14 +31,19 @@ struct ContactConnectionInfo: View {
var body: some View {
NavigationView {
let v = List {
List {
Group {
Text(contactConnection.initiated ? "You invited a contact" : "You accepted connection")
Text(contactConnection.initiated ? "You invited your contact" : "You accepted connection")
.font(.largeTitle)
.bold()
.padding(.bottom)
.padding(.bottom, 16)
Text(contactConnectionText(contactConnection))
.padding(.bottom, 16)
if let connReqInv = contactConnection.connReqInv {
OneTimeLinkProfileText(contactConnection: contactConnection, connReqInvitation: connReqInv)
}
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
@@ -61,16 +65,10 @@ struct ContactConnectionInfo: View {
if contactConnection.initiated,
let connReqInv = contactConnection.connReqInv {
QRCode(uri: connReqInv)
incognitoEnabled()
shareLinkButton(connReqInv)
oneTimeLinkLearnMoreButton()
oneTimeLinkSection(contactConnection: contactConnection, connReqInvitation: connReqInv)
} else {
incognitoEnabled()
oneTimeLinkLearnMoreButton()
}
} footer: {
sharedProfileInfo(contactConnection.incognito)
}
Section {
@@ -82,14 +80,6 @@ struct ContactConnectionInfo: View {
}
}
}
if #available(iOS 16, *) {
v
} else {
// navigationBarHidden is added conditionally,
// because the view jumps in iOS 17 if this is added,
// and on iOS 16+ it is hidden without it.
v.navigationBarHidden(true)
}
}
.alert(item: $alert) { _alert in
switch _alert {
@@ -138,30 +128,6 @@ struct ContactConnectionInfo: View {
)
: "You will be connected when your contact's device is online, please wait or check later!"
}
@ViewBuilder private func incognitoEnabled() -> some View {
if contactConnection.incognito {
ZStack(alignment: .leading) {
Image(systemName: "theatermasks.fill")
.frame(maxWidth: 24, maxHeight: 24, alignment: .center)
.foregroundColor(Color.indigo)
.font(.system(size: 14))
HStack(spacing: 6) {
Text("Incognito")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.font(.system(size: 14))
}
.onTapGesture {
showIncognitoSheet = true
}
.padding(.leading, 36)
}
.sheet(isPresented: $showIncognitoSheet) {
IncognitoHelp()
}
}
}
}
struct ContactConnectionInfo_Previews: PreviewProvider {

View File

@@ -58,14 +58,10 @@ struct ContactConnectionView: View {
}
.padding(.bottom, 2)
ZStack(alignment: .topTrailing) {
Text(contactConnection.description)
.frame(maxWidth: .infinity, alignment: .leading)
incognitoIcon(contactConnection.incognito)
.padding(.top, 26)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(.horizontal, 8)
Text(contactConnection.description)
.frame(alignment: .topLeading)
.padding(.horizontal, 8)
.padding(.bottom, 2)
Spacer()
}

View File

@@ -24,7 +24,7 @@ struct ContactRequestView: View {
Text(contactRequest.chatViewName)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(.accentColor)
.foregroundColor(chatModel.incognito ? .indigo : .accentColor)
.padding(.leading, 8)
.frame(alignment: .topLeading)
Spacer()

View File

@@ -65,7 +65,7 @@ struct MigrateToAppGroupView: View {
case .exporting:
center {
ProgressView(value: 0.33)
Text("Exporting database archive")
Text("Exporting database archive...")
}
migrationProgress()
case .export_error:
@@ -82,7 +82,7 @@ struct MigrateToAppGroupView: View {
case .migrating:
center {
ProgressView(value: 0.67)
Text("Migrating database archive")
Text("Migrating database archive...")
}
migrationProgress()
case .migration_error:

View File

@@ -1,9 +0,0 @@
//
// Keyboard.swift
// SimpleX (iOS)
//
// Created by Evgeny on 10/07/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import Foundation

View File

@@ -1,21 +0,0 @@
//
// KeyboardPadding.swift
// SimpleX (iOS)
//
// Created by Evgeny on 10/07/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
extension View {
@ViewBuilder func keyboardPadding() -> some View {
if #available(iOS 17.0, *) {
GeometryReader { g in
self.padding(.bottom, max(0, ChatModel.shared.keyboardHeight - g.safeAreaInsets.bottom))
}
} else {
self
}
}
}

View File

@@ -37,7 +37,7 @@ struct LocalAuthRequest {
}
func authenticate(title: LocalizedStringKey? = nil, reason: String, selfDestruct: Bool = false, completed: @escaping (LAResult) -> Void) {
logger.debug("DEBUGGING: authenticate")
logger.debug("authenticate")
switch privacyLocalAuthModeDefault.get() {
case .system: systemAuthenticate(reason, completed)
case .passcode:
@@ -58,24 +58,21 @@ func authenticate(title: LocalizedStringKey? = nil, reason: String, selfDestruct
}
func systemAuthenticate(_ reason: String, _ completed: @escaping (LAResult) -> Void) {
logger.debug("DEBUGGING: systemAuthenticate")
let laContext = LAContext()
var authAvailabilityError: NSError?
if laContext.canEvaluatePolicy(.deviceOwnerAuthentication, error: &authAvailabilityError) {
logger.debug("DEBUGGING: systemAuthenticate: canEvaluatePolicy callback")
laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authError in
logger.debug("DEBUGGING: systemAuthenticate evaluatePolicy callback")
DispatchQueue.main.async {
if success {
completed(LAResult.success)
} else {
logger.error("DEBUGGING: systemAuthenticate authentication error: \(authError.debugDescription)")
logger.error("authentication error: \(authError.debugDescription)")
completed(LAResult.failed(authError: authError?.localizedDescription))
}
}
}
} else {
logger.error("DEBUGGING: authentication availability error: \(authAvailabilityError.debugDescription)")
logger.error("authentication availability error: \(authAvailabilityError.debugDescription)")
completed(LAResult.unavailable(authError: authAvailabilityError?.localizedDescription))
}
}

View File

@@ -12,92 +12,38 @@ import SimpleXChat
struct AddContactView: View {
@EnvironmentObject private var chatModel: ChatModel
@Binding var contactConnection: PendingContactConnection?
var contactConnection: PendingContactConnection? = nil
var connReqInvitation: String
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
var body: some View {
VStack {
List {
Section {
if connReqInvitation != "" {
QRCode(uri: connReqInvitation)
} else {
ProgressView()
.progressViewStyle(.circular)
.scaleEffect(2)
.frame(maxWidth: .infinity)
.padding(.vertical)
}
IncognitoToggle(incognitoEnabled: $incognitoDefault)
.disabled(contactConnection == nil)
shareLinkButton(connReqInvitation)
oneTimeLinkLearnMoreButton()
} header: {
Text("1-time link")
} footer: {
sharedProfileInfo(incognitoDefault)
}
List {
OneTimeLinkProfileText(contactConnection: contactConnection, connReqInvitation: connReqInvitation)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
Section("1-time link") {
oneTimeLinkSection(contactConnection: contactConnection, connReqInvitation: connReqInvitation)
}
}
.onAppear { chatModel.connReqInv = connReqInvitation }
.onChange(of: incognitoDefault) { incognito in
Task {
do {
if let contactConn = contactConnection,
let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
await MainActor.run {
contactConnection = conn
ChatModel.shared.updateContactConnection(conn)
}
}
} catch {
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
}
}
}
}
}
struct IncognitoToggle: View {
@Binding var incognitoEnabled: Bool
@State private var showIncognitoSheet = false
var body: some View {
ZStack(alignment: .leading) {
Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks")
.frame(maxWidth: 24, maxHeight: 24, alignment: .center)
.foregroundColor(incognitoEnabled ? Color.indigo : .secondary)
.font(.system(size: 14))
Toggle(isOn: $incognitoEnabled) {
HStack(spacing: 6) {
Text("Incognito")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.font(.system(size: 14))
}
.onTapGesture {
showIncognitoSheet = true
}
}
.padding(.leading, 36)
}
.sheet(isPresented: $showIncognitoSheet) {
IncognitoHelp()
}
@ViewBuilder func oneTimeLinkSection(contactConnection: PendingContactConnection? = nil, connReqInvitation: String) -> some View {
if connReqInvitation != "" {
QRCode(uri: connReqInvitation)
} else {
ProgressView()
.progressViewStyle(.circular)
.scaleEffect(2)
.frame(maxWidth: .infinity)
.padding(.vertical)
}
shareLinkButton(connReqInvitation)
oneTimeLinkLearnMoreButton()
}
func sharedProfileInfo(_ incognito: Bool) -> Text {
let name = ChatModel.shared.currentUser?.displayName ?? ""
return Text(
incognito
? "A new random profile will be shared."
: "Your profile **\(name)** will be shared."
)
}
func shareLinkButton(_ connReqInvitation: String) -> some View {
private func shareLinkButton(_ connReqInvitation: String) -> some View {
Button {
showShareSheet(items: [connReqInvitation])
} label: {
@@ -119,11 +65,26 @@ func oneTimeLinkLearnMoreButton() -> some View {
}
}
struct AddContactView_Previews: PreviewProvider {
static var previews: some View {
AddContactView(
contactConnection: Binding.constant(PendingContactConnection.getSampleData()),
connReqInvitation: "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FFe5ICmvrm4wkrr6X1LTMii-lhBqLeB76%23MCowBQYDK2VuAyEAdhZZsHpuaAk3Hh1q0uNb_6hGTpuwBIrsp2z9U2T0oC0%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAcz6jJk71InuxA0bOX7OUhddfB8Ov7xwQIlIDeXBRZaOntUU4brU5Y3rBzroZBdQJi0FKdtt_D7I%3D%2CMEIwBQYDK2VvAzkA-hDvk1duBi1hlOr08VWSI-Ou4JNNSQjseY69QyKm7Kgg1zZjbpGfyBqSZ2eqys6xtoV4ZtoQUXQ%3D"
)
struct OneTimeLinkProfileText: View {
@EnvironmentObject private var chatModel: ChatModel
var contactConnection: PendingContactConnection? = nil
var connReqInvitation: String
var body: some View {
HStack {
if (contactConnection?.incognito ?? chatModel.incognito) {
Image(systemName: "theatermasks").foregroundColor(.indigo)
Text("A random profile will be sent to your contact")
} else {
Image(systemName: "info.circle").foregroundColor(.secondary)
Text("Your chat profile will be sent to your contact")
}
}
}
}
struct AddContactView_Previews: PreviewProvider {
static var previews: some View {
AddContactView(connReqInvitation: "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FFe5ICmvrm4wkrr6X1LTMii-lhBqLeB76%23MCowBQYDK2VuAyEAdhZZsHpuaAk3Hh1q0uNb_6hGTpuwBIrsp2z9U2T0oC0%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAcz6jJk71InuxA0bOX7OUhddfB8Ov7xwQIlIDeXBRZaOntUU4brU5Y3rBzroZBdQJi0FKdtt_D7I%3D%2CMEIwBQYDK2VvAzkA-hDvk1duBi1hlOr08VWSI-Ou4JNNSQjseY69QyKm7Kgg1zZjbpGfyBqSZ2eqys6xtoV4ZtoQUXQ%3D")
}
}

View File

@@ -36,7 +36,7 @@ struct AddGroupView: View {
}
}
} else {
createGroupView().keyboardPadding()
createGroupView()
}
}
@@ -47,13 +47,21 @@ struct AddGroupView: View {
.padding(.vertical, 4)
Text("The group is fully decentralized it is visible only to the members.")
.padding(.bottom, 4)
HStack {
Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
Spacer().frame(width: 8)
Text("Your chat profile will be sent to group members").font(.footnote)
if (m.incognito) {
HStack {
Image(systemName: "info.circle").foregroundColor(.orange).font(.footnote)
Spacer().frame(width: 8)
Text("Incognito mode is not supported here - your main profile will be sent to group members").font(.footnote)
}
.padding(.bottom)
} else {
HStack {
Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
Spacer().frame(width: 8)
Text("Your chat profile will be sent to group members").font(.footnote)
}
.padding(.bottom)
}
.padding(.bottom)
ZStack(alignment: .center) {
ZStack(alignment: .topTrailing) {

View File

@@ -7,7 +7,6 @@
//
import SwiftUI
import SimpleXChat
enum CreateLinkTab {
case oneTime
@@ -25,7 +24,6 @@ struct CreateLinkView: View {
@EnvironmentObject var m: ChatModel
@State var selection: CreateLinkTab
@State var connReqInvitation: String = ""
@State var contactConnection: PendingContactConnection? = nil
@State private var creatingConnReq = false
var viaNavLink = false
@@ -41,7 +39,7 @@ struct CreateLinkView: View {
private func createLinkView() -> some View {
TabView(selection: $selection) {
AddContactView(contactConnection: $contactConnection, connReqInvitation: connReqInvitation)
AddContactView(connReqInvitation: connReqInvitation)
.tabItem {
Label(
connReqInvitation == ""
@@ -58,7 +56,7 @@ struct CreateLinkView: View {
.tag(CreateLinkTab.longTerm)
}
.onChange(of: selection) { _ in
if case .oneTime = selection, connReqInvitation == "", contactConnection == nil && !creatingConnReq {
if case .oneTime = selection, connReqInvitation == "" && !creatingConnReq {
createInvitation()
}
}
@@ -71,14 +69,12 @@ struct CreateLinkView: View {
private func createInvitation() {
creatingConnReq = true
Task {
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
await MainActor.run {
let connReq = await apiAddContact()
await MainActor.run {
if let connReq = connReq {
connReqInvitation = connReq
contactConnection = pcc
m.connReqInv = connReq
}
} else {
await MainActor.run {
} else {
creatingConnReq = false
}
}

View File

@@ -10,13 +10,13 @@ import SwiftUI
import SimpleXChat
enum NewChatAction: Identifiable {
case createLink(link: String, connection: PendingContactConnection)
case createLink(link: String)
case connectViaLink
case createGroup
var id: String {
switch self {
case let .createLink(link, _): return "createLink \(link)"
case let .createLink(link): return "createLink \(link)"
case .connectViaLink: return "connectViaLink"
case .createGroup: return "createGroup"
}
@@ -41,8 +41,8 @@ struct NewChatButton: View {
}
.sheet(item: $actionSheet) { sheet in
switch sheet {
case let .createLink(link, pcc):
CreateLinkView(selection: .oneTime, connReqInvitation: link, contactConnection: pcc)
case let .createLink(link):
CreateLinkView(selection: .oneTime, connReqInvitation: link)
case .connectViaLink: ConnectViaLinkView()
case .createGroup: AddGroupView()
}
@@ -51,8 +51,8 @@ struct NewChatButton: View {
func addContactAction() {
Task {
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
actionSheet = .createLink(link: connReq, connection: pcc)
if let connReq = await apiAddContact() {
actionSheet = .createLink(link: connReq)
}
}
}
@@ -63,9 +63,9 @@ enum ConnReqType: Equatable {
case invitation
}
func connectViaLink(_ connectionLink: String, dismiss: DismissAction? = nil, incognito: Bool) {
func connectViaLink(_ connectionLink: String, _ dismiss: DismissAction? = nil) {
Task {
if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) {
if let connReqType = await apiConnect(connReq: connectionLink) {
DispatchQueue.main.async {
dismiss?()
AlertManager.shared.showAlert(connReqSentAlert(connReqType))
@@ -100,12 +100,12 @@ func checkCRDataGroup(_ crData: CReqClientData) -> Bool {
return crData.type == "group" && crData.groupLinkId != nil
}
func groupLinkAlert(_ connectionLink: String, incognito: Bool) -> Alert {
func groupLinkAlert(_ connectionLink: String) -> Alert {
return Alert(
title: Text("Connect via group link?"),
message: Text("You will join a group this link refers to and connect to its group members."),
primaryButton: .default(Text(incognito ? "Connect incognito" : "Connect")) {
connectViaLink(connectionLink, incognito: incognito)
primaryButton: .default(Text("Connect")) {
connectViaLink(connectionLink)
},
secondaryButton: .cancel()
)

View File

@@ -7,77 +7,76 @@
//
import SwiftUI
import SimpleXChat
struct PasteToConnectView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
@State private var connectionLink: String = ""
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@FocusState private var linkEditorFocused: Bool
var body: some View {
List {
Text("Connect via link")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.onTapGesture { linkEditorFocused = false }
Section {
linkEditor()
Button {
if connectionLink == "" {
connectionLink = UIPasteboard.general.string ?? ""
} else {
connectionLink = ""
ScrollView {
VStack(alignment: .leading) {
Text("Connect via link")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical)
Text("Paste the link you received into the box below to connect with your contact.")
.padding(.bottom, 4)
if (chatModel.incognito) {
HStack {
Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote)
Spacer().frame(width: 8)
Text("A random profile will be sent to the contact that you received this link from").font(.footnote)
}
} label: {
if connectionLink == "" {
settingsRow("doc.plaintext") { Text("Paste") }
} else {
settingsRow("multiply") { Text("Clear") }
.padding(.bottom)
} else {
HStack {
Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
Spacer().frame(width: 8)
Text("Your profile will be sent to the contact that you received this link from").font(.footnote)
}
}
Button {
connect()
} label: {
settingsRow("link") { Text("Connect") }
}
.disabled(connectionLink == "" || connectionLink.trimmingCharacters(in: .whitespaces).firstIndex(of: " ") != nil)
IncognitoToggle(incognitoEnabled: $incognitoDefault)
} footer: {
sharedProfileInfo(incognitoDefault)
+ Text(String("\n\n"))
+ Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
}
}
}
private func linkEditor() -> some View {
ZStack {
Group {
if connectionLink.isEmpty {
TextEditor(text: Binding.constant(NSLocalizedString("Paste the link you received to connect with your contact.", comment: "placeholder")))
.foregroundColor(.secondary)
.disabled(true)
.padding(.bottom)
}
TextEditor(text: $connectionLink)
.onSubmit(connect)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.focused($linkEditorFocused)
.allowsTightening(false)
.frame(height: 180)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)
)
HStack(spacing: 20) {
if connectionLink == "" {
Button {
connectionLink = UIPasteboard.general.string ?? ""
} label: {
Label("Paste", systemImage: "doc.plaintext")
}
} else {
Button {
connectionLink = ""
} label: {
Label("Clear", systemImage: "multiply")
}
}
Spacer()
Button(action: connect, label: {
Label("Connect", systemImage: "link")
})
.disabled(connectionLink == "" || connectionLink.trimmingCharacters(in: .whitespaces).firstIndex(of: " ") != nil)
}
.frame(height: 48)
.padding(.bottom)
Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
}
.allowsTightening(false)
.padding(.horizontal, -5)
.padding(.top, -8)
.frame(height: 180, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
}
@@ -86,9 +85,9 @@ struct PasteToConnectView: View {
if let crData = parseLinkQueryData(link),
checkCRDataGroup(crData) {
dismiss()
AlertManager.shared.showAlert(groupLinkAlert(link, incognito: incognitoDefault))
AlertManager.shared.showAlert(groupLinkAlert(link))
} else {
connectViaLink(link, dismiss: dismiss, incognito: incognitoDefault)
connectViaLink(link, dismiss)
}
}
}

View File

@@ -7,12 +7,11 @@
//
import SwiftUI
import SimpleXChat
import CodeScanner
struct ScanToConnectView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
var body: some View {
ScrollView {
@@ -20,35 +19,34 @@ struct ScanToConnectView: View {
Text("Scan QR code")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical)
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
IncognitoToggle(incognitoEnabled: $incognitoDefault)
.padding(.horizontal)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .systemBackground))
)
.padding(.top)
Group {
sharedProfileInfo(incognitoDefault)
+ Text(String("\n\n"))
+ Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.")
if (chatModel.incognito) {
HStack {
Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote)
Spacer().frame(width: 8)
Text("A random profile will be sent to your contact").font(.footnote)
}
.padding(.bottom)
} else {
HStack {
Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
Spacer().frame(width: 8)
Text("Your chat profile will be sent to your contact").font(.footnote)
}
.padding(.bottom)
}
.font(.footnote)
.foregroundColor(.secondary)
.padding(.horizontal)
ZStack {
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.border(.gray)
}
.padding(.bottom)
Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.")
.padding(.bottom)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
.background(Color(.systemGroupedBackground))
}
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
@@ -57,9 +55,9 @@ struct ScanToConnectView: View {
if let crData = parseLinkQueryData(r.string),
checkCRDataGroup(crData) {
dismiss()
AlertManager.shared.showAlert(groupLinkAlert(r.string, incognito: incognitoDefault))
AlertManager.shared.showAlert(groupLinkAlert(r.string))
} else {
Task { connectViaLink(r.string, dismiss: dismiss, incognito: incognitoDefault) }
Task { connectViaLink(r.string, dismiss) }
}
case let .failure(e):
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")

View File

@@ -104,7 +104,6 @@ struct CreateProfile: View {
}
}
.padding()
.keyboardPadding()
}
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {

View File

@@ -220,37 +220,6 @@ private let versionDescriptions: [VersionDescription] = [
description: "Thanks to the users [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
),
]
),
VersionDescription(
version: "v5.2",
post: URL(string: "https://simplex.chat/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.html"),
features: [
FeatureDescription(
icon: "checkmark",
title: "Message delivery receipts!",
description: "The second tick we missed! ✅"
),
FeatureDescription(
icon: "star",
title: "Find chats faster",
description: "Filter unread and favorite chats."
),
FeatureDescription(
icon: "exclamationmark.arrow.triangle.2.circlepath",
title: "Keep your connections",
description: "Fix encryption after restoring backups."
),
FeatureDescription(
icon: "stopwatch",
title: "Make one message disappear",
description: "Even when disabled in the conversation."
),
FeatureDescription(
icon: "gift",
title: "A few more things",
description: "- more stable message delivery.\n- a bit better groups.\n- and more!"
),
]
)
]

View File

@@ -18,32 +18,14 @@ struct TerminalView: View {
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State var composeState: ComposeState = ComposeState()
@State private var keyboardVisible = false
@FocusState private var keyboardVisible: Bool
@State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
@State private var terminalItem: TerminalItem?
@State private var scrolled = false
@State private var showing = false
var body: some View {
if authorized {
terminalView()
.onAppear {
if showing { return }
showing = true
Task {
let items = await TerminalItems.shared.items()
await MainActor.run {
chatModel.terminalItems = items
chatModel.showingTerminal = true
}
}
}
.onDisappear {
if terminalItem == nil {
chatModel.showingTerminal = false
chatModel.terminalItems = []
}
}
} else {
Button(action: runAuth) { Label("Unlock", systemImage: "lock") }
.onAppear(perform: runAuth)
@@ -136,8 +118,9 @@ struct TerminalView: View {
let cmd = ChatCommand.string(composeState.message)
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
Task {
await TerminalItems.shared.addCommand(.now, cmd, resp)
DispatchQueue.main.async {
ChatModel.shared.addTerminalItem(.cmd(.now, cmd))
ChatModel.shared.addTerminalItem(.resp(.now, resp))
}
} else {
DispatchQueue.global().async {

View File

@@ -51,9 +51,8 @@ struct AdvancedNetworkSettings: View {
}
.disabled(currentNetCfg == NetCfg.proxyDefaults)
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [5_000000, 7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [3_000000, 5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [10_000, 20_000, 40_000, 75_000, 100_000], label: secondsLabel)
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [2_500000, 5_000000, 7_500000, 10_000000, 15_000000, 20_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [1_500000, 3_000000, 5_000000, 7_000000, 10_000000, 15_000000], label: secondsLabel)
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel)
intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "")
Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive)
@@ -153,9 +152,7 @@ struct AdvancedNetworkSettings: View {
private func timeoutSettingPicker(_ title: LocalizedStringKey, selection: Binding<Int>, values: [Int], label: String) -> some View {
Picker(title, selection: selection) {
let v = selection.wrappedValue
let vs = values.contains(v) ? values : values + [v]
ForEach(vs, id: \.self) { value in
ForEach(values, id: \.self) { value in
Text("\(String(format: "%g", (Double(value) / 1000000))) \(secondsLabel)")
}
}

View File

@@ -18,9 +18,10 @@ struct IncognitoHelp: View {
ScrollView {
VStack(alignment: .leading) {
Group {
Text("Incognito mode protects your privacy by using a new random profile for each contact.")
Text("Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.")
Text("It allows having many anonymous connections without any shared data between them in a single chat profile.")
Text("When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.")
Text("To find the profile used for an incognito connection, tap the contact or group name on top of the chat.")
}
.padding(.bottom)
}

View File

@@ -11,12 +11,9 @@ import SimpleXChat
struct NotificationsView: View {
@EnvironmentObject var m: ChatModel
@State private var notificationMode: NotificationsMode = ChatModel.shared.notificationMode
@State private var notificationMode: NotificationsMode?
@State private var showAlert: NotificationAlert?
@State private var legacyDatabase = dbContainerGroupDefault.get() == .documents
// @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
// @AppStorage(GROUP_DEFAULT_NTF_ENABLE_LOCAL, store: groupDefaults) private var ntfEnableLocal = false
// @AppStorage(GROUP_DEFAULT_NTF_ENABLE_PERIODIC, store: groupDefaults) private var ntfEnablePeriodic = false
var body: some View {
List {
@@ -29,7 +26,9 @@ struct NotificationsView: View {
}
} footer: {
VStack(alignment: .leading) {
Text(ntfModeDescription(notificationMode))
if let mode = notificationMode {
Text(ntfModeDescription(mode))
}
}
.font(.callout)
.padding(.top, 1)
@@ -44,6 +43,7 @@ struct NotificationsView: View {
return Alert(title: Text("No device token!"))
}
}
.onAppear { notificationMode = m.notificationMode }
} label: {
HStack {
Text("Send notifications")
@@ -76,7 +76,7 @@ struct NotificationsView: View {
HStack {
Text("Show preview")
Spacer()
Text(m.notificationPreview.label)
Text(m.notificationPreview?.label ?? "")
}
}
} header: {
@@ -88,15 +88,8 @@ struct NotificationsView: View {
.padding(.top, 1)
}
}
// if developerTools {
// Section(String("Experimental")) {
// Toggle(String("Always enable local"), isOn: $ntfEnableLocal)
// Toggle(String("Always enable periodic"), isOn: $ntfEnablePeriodic)
// }
// }
.disabled(legacyDatabase)
}
.disabled(legacyDatabase)
}
private func notificationAlert(_ alert: NotificationAlert, _ token: DeviceToken) -> Alert {
@@ -173,7 +166,7 @@ func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey {
struct SelectionListView<Item: SelectableItem>: View {
var list: [Item]
@Binding var selection: Item
@Binding var selection: Item?
var onSelection: ((Item) -> Void)?
@State private var tapped: Item? = nil

View File

@@ -10,34 +10,12 @@ import SwiftUI
import SimpleXChat
struct PrivacySettings: View {
@EnvironmentObject var m: ChatModel
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@State private var currentLAMode = privacyLocalAuthModeDefault.get()
@State private var contactReceipts = false
@State private var contactReceiptsReset = false
@State private var contactReceiptsOverrides = 0
@State private var contactReceiptsDialogue = false
@State private var groupReceipts = false
@State private var groupReceiptsReset = false
@State private var groupReceiptsOverrides = 0
@State private var groupReceiptsDialogue = false
@State private var alert: PrivacySettingsViewAlert?
enum PrivacySettingsViewAlert: Identifiable {
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
switch self {
case let .error(title, _): return "error \(title)"
}
}
}
var body: some View {
VStack {
@@ -72,18 +50,6 @@ struct PrivacySettings: View {
settingsRow("network") {
Toggle("Send link previews", isOn: $useLinkPreviews)
}
settingsRow("message") {
Toggle("Show last messages", isOn: $showChatPreviews)
}
settingsRow("rectangle.and.pencil.and.ellipsis") {
Toggle("Message draft", isOn: $saveLastDraft)
}
.onChange(of: saveLastDraft) { saveDraft in
if !saveDraft {
m.draft = nil
m.draftChatId = nil
}
}
settingsRow("link") {
Picker("SimpleX links", selection: $simplexLinkMode) {
ForEach(SimpleXLinkMode.values) { mode in
@@ -102,175 +68,6 @@ struct PrivacySettings: View {
Text("Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.")
}
}
Section {
settingsRow("person") {
Toggle("Contacts", isOn: $contactReceipts)
}
settingsRow("person.2") {
Toggle("Small groups (max 20)", isOn: $groupReceipts)
}
} header: {
Text("Send delivery receipts to")
} footer: {
VStack(alignment: .leading) {
Text("These settings are for your current profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.")
Text("They can be overridden in contact and group settings.")
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.confirmationDialog(contactReceiptsDialogTitle, isPresented: $contactReceiptsDialogue, titleVisibility: .visible) {
Button(contactReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") {
setSendReceiptsContacts(contactReceipts, clearOverrides: false)
}
Button(contactReceipts ? "Enable for all" : "Disable for all", role: .destructive) {
setSendReceiptsContacts(contactReceipts, clearOverrides: true)
}
Button("Cancel", role: .cancel) {
contactReceiptsReset = true
contactReceipts.toggle()
}
}
.confirmationDialog(groupReceiptsDialogTitle, isPresented: $groupReceiptsDialogue, titleVisibility: .visible) {
Button(groupReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") {
setSendReceiptsGroups(groupReceipts, clearOverrides: false)
}
Button(contactReceipts ? "Enable for all" : "Disable for all", role: .destructive) {
setSendReceiptsGroups(groupReceipts, clearOverrides: true)
}
Button("Cancel", role: .cancel) {
groupReceiptsReset = true
groupReceipts.toggle()
}
}
}
}
.onChange(of: contactReceipts) { _ in
if contactReceiptsReset {
contactReceiptsReset = false
} else {
setOrAskSendReceiptsContacts(contactReceipts)
}
}
.onChange(of: groupReceipts) { _ in
if groupReceiptsReset {
groupReceiptsReset = false
} else {
setOrAskSendReceiptsGroups(groupReceipts)
}
}
.onAppear {
if let u = m.currentUser {
if contactReceipts != u.sendRcptsContacts {
contactReceiptsReset = true
contactReceipts = u.sendRcptsContacts
}
if groupReceipts != u.sendRcptsSmallGroups {
groupReceiptsReset = true
groupReceipts = u.sendRcptsSmallGroups
}
}
}
.alert(item: $alert) { alert in
switch alert {
case let .error(title, error):
return Alert(title: Text(title), message: Text(error))
}
}
}
private func setOrAskSendReceiptsContacts(_ enable: Bool) {
contactReceiptsOverrides = m.chats.reduce(0) { count, chat in
let sendRcpts = chat.chatInfo.contact?.chatSettings.sendRcpts
return count + (sendRcpts == nil || sendRcpts == enable ? 0 : 1)
}
if contactReceiptsOverrides == 0 {
setSendReceiptsContacts(enable, clearOverrides: false)
} else {
contactReceiptsDialogue = true
}
}
private var contactReceiptsDialogTitle: LocalizedStringKey {
contactReceipts
? "Sending receipts is disabled for \(contactReceiptsOverrides) contacts"
: "Sending receipts is enabled for \(contactReceiptsOverrides) contacts"
}
private func setSendReceiptsContacts(_ enable: Bool, clearOverrides: Bool) {
Task {
do {
if let currentUser = m.currentUser {
let userMsgReceiptSettings = UserMsgReceiptSettings(enable: enable, clearOverrides: clearOverrides)
try await apiSetUserContactReceipts(currentUser.userId, userMsgReceiptSettings: userMsgReceiptSettings)
privacyDeliveryReceiptsSet.set(true)
await MainActor.run {
var updatedUser = currentUser
updatedUser.sendRcptsContacts = enable
m.updateUser(updatedUser)
if clearOverrides {
m.chats.forEach { chat in
if var contact = chat.chatInfo.contact {
let sendRcpts = contact.chatSettings.sendRcpts
if sendRcpts != nil && sendRcpts != enable {
contact.chatSettings.sendRcpts = nil
m.updateContact(contact)
}
}
}
}
}
}
} catch let error {
alert = .error(title: "Error setting delivery receipts!", error: "Error: \(responseError(error))")
}
}
}
private func setOrAskSendReceiptsGroups(_ enable: Bool) {
groupReceiptsOverrides = m.chats.reduce(0) { count, chat in
let sendRcpts = chat.chatInfo.groupInfo?.chatSettings.sendRcpts
return count + (sendRcpts == nil || sendRcpts == enable ? 0 : 1)
}
if groupReceiptsOverrides == 0 {
setSendReceiptsGroups(enable, clearOverrides: false)
} else {
groupReceiptsDialogue = true
}
}
private var groupReceiptsDialogTitle: LocalizedStringKey {
groupReceipts
? "Sending receipts is disabled for \(groupReceiptsOverrides) groups"
: "Sending receipts is enabled for \(groupReceiptsOverrides) groups"
}
private func setSendReceiptsGroups(_ enable: Bool, clearOverrides: Bool) {
Task {
do {
if let currentUser = m.currentUser {
let userMsgReceiptSettings = UserMsgReceiptSettings(enable: enable, clearOverrides: clearOverrides)
try await apiSetUserGroupReceipts(currentUser.userId, userMsgReceiptSettings: userMsgReceiptSettings)
privacyDeliveryReceiptsSet.set(true)
await MainActor.run {
var updatedUser = currentUser
updatedUser.sendRcptsSmallGroups = enable
m.updateUser(updatedUser)
if clearOverrides {
m.chats.forEach { chat in
if var groupInfo = chat.chatInfo.groupInfo {
let sendRcpts = groupInfo.chatSettings.sendRcpts
if sendRcpts != nil && sendRcpts != enable {
groupInfo.chatSettings.sendRcpts = nil
m.updateGroup(groupInfo)
}
}
}
}
}
}
} catch let error {
alert = .error(title: "Error setting delivery receipts!", error: "Error: \(responseError(error))")
}
}
}

View File

@@ -21,10 +21,11 @@ struct ScanProtocolServer: View {
.font(.largeTitle)
.bold()
.padding(.vertical)
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.padding(.top)
ZStack {
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.border(.gray)
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)

View File

@@ -1,100 +0,0 @@
//
// SetDeliveryReceiptsView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 12/07/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct SetDeliveryReceiptsView: View {
@EnvironmentObject var m: ChatModel
var body: some View {
VStack(spacing: 16) {
Text("Delivery receipts!")
.font(.title)
.foregroundColor(.secondary)
.padding(.vertical)
.multilineTextAlignment(.center)
Spacer()
Button("Enable") {
Task {
do {
if let currentUser = m.currentUser {
try await apiSetAllContactReceipts(enable: true)
await MainActor.run {
var updatedUser = currentUser
updatedUser.sendRcptsContacts = true
m.updateUser(updatedUser)
m.setDeliveryReceipts = false
privacyDeliveryReceiptsSet.set(true)
}
do {
let users = try await listUsersAsync()
await MainActor.run { m.users = users }
} catch let error {
logger.debug("listUsers error: \(responseError(error))")
}
}
} catch let error {
AlertManager.shared.showAlert(Alert(
title: Text("Error enabling delivery receipts!"),
message: Text("Error: \(responseError(error))")
))
await MainActor.run {
m.setDeliveryReceipts = false
}
}
}
}
.font(.largeTitle)
Group {
if m.users.count > 1 {
Text("Sending delivery receipts will be enabled for all contacts in all visible chat profiles.")
} else {
Text("Sending delivery receipts will be enabled for all contacts.")
}
}
.multilineTextAlignment(.center)
Spacer()
VStack(spacing: 8) {
Button {
AlertManager.shared.showAlert(Alert(
title: Text("Delivery receipts are disabled!"),
message: Text("You can enable them later via app Privacy & Security settings."),
primaryButton: .default(Text("Don't show again")) {
m.setDeliveryReceipts = false
privacyDeliveryReceiptsSet.set(true)
},
secondaryButton: .default(Text("Ok")) {
m.setDeliveryReceipts = false
}
))
} label: {
HStack {
Text("Don't enable")
Image(systemName: "chevron.right")
}
}
Text("You can enable later via Settings").font(.footnote)
}
}
.padding()
.padding(.horizontal)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(uiColor: .systemBackground))
}
}
struct SetDeliveryReceiptsView_Previews: PreviewProvider {
static var previews: some View {
SetDeliveryReceiptsView()
}
}

View File

@@ -30,10 +30,7 @@ let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents"
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft"
let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen"
let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet"
let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls"
let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
@@ -67,10 +64,7 @@ let appDefaults: [String: Any] = [
DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
DEFAULT_PRIVACY_LINK_PREVIEWS: true,
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue,
DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true,
DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true,
DEFAULT_PRIVACY_PROTECT_SCREEN: false,
DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false,
DEFAULT_EXPERIMENTAL_CALLS: false,
DEFAULT_CHAT_V3_DB_MIGRATION: V3DBMigrationState.offer.rawValue,
DEFAULT_DEVELOPER_TOOLS: false,
@@ -120,8 +114,6 @@ let privacySimplexLinkModeDefault = EnumDefault<SimpleXLinkMode>(defaults: UserD
let privacyLocalAuthModeDefault = EnumDefault<LAMode>(defaults: UserDefaults.standard, forKey: DEFAULT_LA_MODE, withDefault: .system)
let privacyDeliveryReceiptsSet = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET)
let onboardingStageDefault = EnumDefault<OnboardingStage>(defaults: UserDefaults.standard, forKey: DEFAULT_ONBOARDING_STAGE, withDefault: .onboardingComplete)
let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME)
@@ -135,6 +127,7 @@ struct SettingsView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var sceneDelegate: SceneDelegate
@Binding var showSettings: Bool
@State private var settingsSheet: SettingsSheet?
var body: some View {
ZStack {
@@ -164,6 +157,8 @@ struct SettingsView: View {
settingsRow("person.crop.rectangle.stack") { Text("Your chat profiles") }
}
incognitoRow()
NavigationLink {
UserAddressView(shareViaProfile: chatModel.currentUser!.addressShared)
.navigationTitle("SimpleX address")
@@ -299,9 +294,38 @@ struct SettingsView: View {
}
.navigationTitle("Your settings")
}
.onDisappear {
chatModel.showingTerminal = false
chatModel.terminalItems = []
.sheet(item: $settingsSheet) { sheet in
switch sheet {
case .incognitoInfo: IncognitoHelp()
}
}
}
@ViewBuilder private func incognitoRow() -> some View {
ZStack(alignment: .leading) {
Image(systemName: chatModel.incognito ? "theatermasks.fill" : "theatermasks")
.frame(maxWidth: 24, maxHeight: 24, alignment: .center)
.foregroundColor(chatModel.incognito ? Color.indigo : .secondary)
Toggle(isOn: $chatModel.incognito) {
HStack(spacing: 6) {
Text("Incognito")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.font(.system(size: 14))
}
.onTapGesture {
settingsSheet = .incognitoInfo
}
}
.onChange(of: chatModel.incognito) { incognito in
incognitoGroupDefault.set(incognito)
do {
try apiSetIncognito(incognito: incognito)
} catch {
logger.error("apiSetIncognito: cannot set incognito \(responseError(error))")
}
}
.padding(.leading, indent)
}
}
@@ -323,6 +347,12 @@ struct SettingsView: View {
}
}
private enum SettingsSheet: Identifiable {
case incognitoInfo
var id: SettingsSheet { get { self } }
}
private enum NotificationAlert {
case enable
case error(LocalizedStringKey, String)

View File

@@ -3630,31 +3630,6 @@ SimpleX servers cannot see your profile.</source>
<source>\~strike~</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve" approved="no">
<source>%@ servers</source>
<target state="translated">%@ الخوادم</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ (current)" xml:space="preserve" approved="no">
<source>%@ (current)</source>
<target state="translated">%@ (الحالي)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ (current):" xml:space="preserve" approved="no">
<source>%@ (current):</source>
<target state="translated">%@ (الحالي):</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@:" xml:space="preserve" approved="no">
<source>%@:</source>
<target state="needs-translation">%@:</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@ at %@:" xml:space="preserve" approved="no">
<source>%1$@ at %2$@:</source>
<target state="translated">%1$@ في %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ar" datatype="plaintext">

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "cs",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "de",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "en",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "es",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "fr",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "it",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -2,7 +2,7 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
<file original="en.lproj/Localizable.strings" source-language="en" target-language="ja" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="&#10;" xml:space="preserve">
@@ -42,18 +42,6 @@
<target>!1 色付き!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="# %@" xml:space="preserve">
<source># %@</source>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve">
<source>## History</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve">
<source>## In reply to</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="#secret#" xml:space="preserve">
<source>#secret#</source>
<target>シークレット</target>
@@ -84,10 +72,6 @@
<target>%@ / %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ at %@:" xml:space="preserve">
<source>%1$@ at %2$@:</source>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve">
<source>%@ is connected!</source>
<target>%@ 接続中!</target>
@@ -313,12 +297,6 @@
<target>, </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- more stable message delivery.&#10;- a bit better groups.&#10;- and more!" xml:space="preserve">
<source>- more stable message delivery.
- a bit better groups.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes.
- custom time to disappear.
@@ -395,17 +373,19 @@
&lt;p&gt;&lt;a href="%@"&gt;SimpleX Chatでつながろう&lt;/a&gt;&lt;/p&gt;</target>
<note>email text</note>
</trans-unit>
<trans-unit id="A few more things" xml:space="preserve">
<source>A few more things</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A new contact" xml:space="preserve">
<source>A new contact</source>
<target>新しい連絡先</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="A new random profile will be shared." xml:space="preserve">
<source>A new random profile will be shared.</source>
<trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve">
<source>A random profile will be sent to the contact that you received this link from</source>
<target>このリンクの送信元にランダムなプロフィール(ダミー)が送られます</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A random profile will be sent to your contact" xml:space="preserve">
<source>A random profile will be sent to your contact</source>
<target>連絡先にランダムなプロフィール(ダミー)が送られます</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve">
@@ -458,8 +438,8 @@
<note>accept contact request via notification
accept incoming call via notification</note>
</trans-unit>
<trans-unit id="Accept connection request?" xml:space="preserve">
<source>Accept connection request?</source>
<trans-unit id="Accept contact" xml:space="preserve">
<source>Accept contact</source>
<target>連絡を受け入れる</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -471,7 +451,7 @@
<trans-unit id="Accept incognito" xml:space="preserve">
<source>Accept incognito</source>
<target>シークレットモードで承諾</target>
<note>accept contact request via notification</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
@@ -607,10 +587,6 @@
<target>送信済みメッセージの永久削除を許可する。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>音声メッセージの送信を許可する。</target>
@@ -1041,16 +1017,8 @@
<target>接続</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Connect directly" xml:space="preserve">
<source>Connect directly</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via contact link" xml:space="preserve">
<source>Connect via contact link</source>
<trans-unit id="Connect via contact link?" xml:space="preserve">
<source>Connect via contact link?</source>
<target>連絡先リンク経由で接続しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -1069,8 +1037,8 @@
<target>リンク・QRコード経由で接続</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<trans-unit id="Connect via one-time link?" xml:space="preserve">
<source>Connect via one-time link?</source>
<target>使い捨てリンク経由で接続しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -1099,6 +1067,11 @@
<target>接続エラー (AUTH)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection request" xml:space="preserve">
<source>Connection request</source>
<target>接続のリクエスト</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection request sent!" xml:space="preserve">
<source>Connection request sent!</source>
<target>接続リクエストを送信しました!</target>
@@ -1149,10 +1122,6 @@
<target>連絡先の設定</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contacts" xml:space="preserve">
<source>Contacts</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve">
<source>Contacts can mark messages for deletion; you will be able to view them.</source>
<target>連絡先はメッセージを削除対象とすることができます。あなたには閲覧可能です。</target>
@@ -1359,7 +1328,7 @@
<trans-unit id="Decryption error" xml:space="preserve">
<source>Decryption error</source>
<target>復号化エラー</target>
<note>message decrypt error item</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete" xml:space="preserve">
<source>Delete</source>
@@ -1546,18 +1515,6 @@
<target>削除完了: %@</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Delivery" xml:space="preserve">
<source>Delivery</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delivery receipts are disabled!" xml:space="preserve">
<source>Delivery receipts are disabled!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delivery receipts!" xml:space="preserve">
<source>Delivery receipts!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>説明</target>
@@ -1603,19 +1560,11 @@
<target>このグループではメンバー間のダイレクトメッセージが使用禁止です。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Disable (keep overrides)" xml:space="preserve">
<source>Disable (keep overrides)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
<source>Disable SimpleX Lock</source>
<target>SimpleXロックを無効にする</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Disable for all" xml:space="preserve">
<source>Disable for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Disappearing message" xml:space="preserve">
<source>Disappearing message</source>
<target>消えるメッセージ</target>
@@ -1676,10 +1625,6 @@
<target>アドレスを作成しないでください</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't enable" xml:space="preserve">
<source>Don't enable</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't show again" xml:space="preserve">
<source>Don't show again</source>
<target>次から表示しない</target>
@@ -1720,10 +1665,6 @@
<target>有効</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable (keep overrides)" xml:space="preserve">
<source>Enable (keep overrides)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable SimpleX Lock" xml:space="preserve">
<source>Enable SimpleX Lock</source>
<target>SimpleXロックを有効にする</target>
@@ -1739,10 +1680,6 @@
<target>自動メッセージ削除を有効にしますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable instant notifications?" xml:space="preserve">
<source>Enable instant notifications?</source>
<target>即時通知を有効にしますか?</target>
@@ -1952,10 +1889,6 @@
<target>ユーザのプロフィール削除にエラー発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error enabling delivery receipts!" xml:space="preserve">
<source>Error enabling delivery receipts!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error enabling notifications" xml:space="preserve">
<source>Error enabling notifications</source>
<target>通知の有効化にエラー発生</target>
@@ -2036,10 +1969,6 @@
<target>メッセージ送信にエラー発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error setting delivery receipts!" xml:space="preserve">
<source>Error setting delivery receipts!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error starting chat" xml:space="preserve">
<source>Error starting chat</source>
<target>チャット開始にエラー発生</target>
@@ -2055,10 +1984,6 @@
<target>プロフィール切り替えにエラー発生!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error updating group link" xml:space="preserve">
<source>Error updating group link</source>
<target>グループのリンクのアップデートにエラー発生</target>
@@ -2099,10 +2024,6 @@
<target>エラー: データベースが存在しません</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Even when disabled in the conversation." xml:space="preserve">
<source>Even when disabled in the conversation.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Exit without saving" xml:space="preserve">
<source>Exit without saving</source>
<target>保存せずに閉じる</target>
@@ -2123,9 +2044,9 @@
<target>データベースのアーカイブをエクスポートします。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Exporting database archive" xml:space="preserve">
<source>Exporting database archive</source>
<target>データベース アーカイブをエクスポートしています</target>
<trans-unit id="Exporting database archive..." xml:space="preserve">
<source>Exporting database archive...</source>
<target>データベース アーカイブをエクスポートしています...</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Failed to remove passphrase" xml:space="preserve">
@@ -2167,54 +2088,10 @@
<target>ファイルとメディア</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Find chats faster" xml:space="preserve">
<source>Find chats faster</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix" xml:space="preserve">
<source>Fix</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix connection" xml:space="preserve">
<source>Fix connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix connection?" xml:space="preserve">
<source>Fix connection?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix encryption after restoring backups." xml:space="preserve">
<source>Fix encryption after restoring backups.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix not supported by contact" xml:space="preserve">
<source>Fix not supported by contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix not supported by group member" xml:space="preserve">
<source>Fix not supported by group member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="For console" xml:space="preserve">
<source>For console</source>
<target>コンソール</target>
@@ -2320,10 +2197,6 @@
<target>グループのメンバーが消えるメッセージを送信できます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>グループのメンバーが音声メッセージを送信できます。</target>
@@ -2412,7 +2285,7 @@
<trans-unit id="History" xml:space="preserve">
<source>History</source>
<target>履歴</target>
<note>No comment provided by engineer.</note>
<note>copied message info</note>
</trans-unit>
<trans-unit id="How SimpleX works" xml:space="preserve">
<source>How SimpleX works</source>
@@ -2519,10 +2392,6 @@
<target>サーバ設定の向上</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="In reply to" xml:space="preserve">
<source>In reply to</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito" xml:space="preserve">
<source>Incognito</source>
<target>シークレットモード</target>
@@ -2533,8 +2402,14 @@
<target>シークレットモード</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve">
<source>Incognito mode protects your privacy by using a new random profile for each contact.</source>
<trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve">
<source>Incognito mode is not supported here - your main profile will be sent to group members</source>
<target>ここではシークレットモードが無効です。メインのプロフィールがグループのメンバーに送られます</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve">
<source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source>
<target>シークレットモードとは、メインのプロフィールとプロフィール画像を守るために、新しい連絡先を追加する時に、その連絡先に対してランダムなプロフィールが作成されます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incoming audio call" xml:space="preserve">
@@ -2609,10 +2484,6 @@
<target>無効なサーバアドレス!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid status" xml:space="preserve">
<source>Invalid status</source>
<note>item status text</note>
</trans-unit>
<trans-unit id="Invitation expired!" xml:space="preserve">
<source>Invitation expired!</source>
<target>招待が期限切れました!</target>
@@ -2704,10 +2575,6 @@
<target>グループに参加</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="KeyChain error" xml:space="preserve">
<source>KeyChain error</source>
<target>キーチェーンのエラー</target>
@@ -2798,10 +2665,6 @@
<target>プライベートな接続をする</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Make one message disappear" xml:space="preserve">
<source>Make one message disappear</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Make profile private!" xml:space="preserve">
<source>Make profile private!</source>
<target>プロフィールを非表示にできます!</target>
@@ -2870,10 +2733,6 @@
<trans-unit id="Message delivery error" xml:space="preserve">
<source>Message delivery error</source>
<target>メッセージ送信エラー</target>
<note>item status text</note>
</trans-unit>
<trans-unit id="Message delivery receipts!" xml:space="preserve">
<source>Message delivery receipts!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message draft" xml:space="preserve">
@@ -2911,9 +2770,9 @@
<target>メッセージ &amp; ファイル</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migrating database archive" xml:space="preserve">
<source>Migrating database archive</source>
<target>データベースのアーカイブを移行しています</target>
<trans-unit id="Migrating database archive..." xml:space="preserve">
<source>Migrating database archive...</source>
<target>データベースのアーカイブを移行しています...</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migration error:" xml:space="preserve">
@@ -2956,10 +2815,6 @@
<target>まだまだ改善してまいります!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Most likely this connection is deleted." xml:space="preserve">
<source>Most likely this connection is deleted.</source>
<note>item status description</note>
</trans-unit>
<trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve">
<source>Most likely this contact has deleted the connection with you.</source>
<target>恐らくこの連絡先があなたとの接続を削除しました。</target>
@@ -3065,28 +2920,16 @@
<target>追加できる連絡先がありません</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No delivery information" xml:space="preserve">
<source>No delivery information</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No device token!" xml:space="preserve">
<source>No device token!</source>
<target>デバイストークンがありません!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>グループが見つかりません!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No history" xml:space="preserve">
<source>No history</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>音声メッセージを録音する権限がありません</target>
@@ -3171,10 +3014,6 @@
<target>グループ設定を変えられるのはグループのオーナーだけです。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>音声メッセージを利用可能に設定できるのはグループのオーナーだけです。</target>
@@ -3320,10 +3159,10 @@
<target>頂いたリンクを貼り付ける</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve">
<source>Paste the link you received into the box below to connect with your contact.</source>
<target>連絡相手から頂いたリンクを以下の入力欄に貼り付けて繋がります。</target>
<note>placeholder</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
@@ -3495,10 +3334,6 @@
<target>消えるメッセージを使用禁止にする。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>音声メッセージを使用禁止にする。</target>
@@ -3519,10 +3354,6 @@
<target>プロトコル・タイムアウト</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Protocol timeout per KB" xml:space="preserve">
<source>Protocol timeout per KB</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>プッシュ通知</target>
@@ -3533,8 +3364,9 @@
<target>アプリを評価</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="React" xml:space="preserve">
<source>React</source>
<trans-unit id="React..." xml:space="preserve">
<source>React...</source>
<target>リアクション...</target>
<note>chat item menu</note>
</trans-unit>
<trans-unit id="Read" xml:space="preserve">
@@ -3567,10 +3399,6 @@
<target>詳しくは[GitHubリポジトリ](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receipts are disabled" xml:space="preserve">
<source>Receipts are disabled</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Received at" xml:space="preserve">
<source>Received at</source>
<target>受信</target>
@@ -3610,14 +3438,6 @@
<target>受信者には、入力時に更新内容が表示されます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve">
<source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reconnect servers?" xml:space="preserve">
<source>Reconnect servers?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Record updated at" xml:space="preserve">
<source>Record updated at</source>
<target>レコード更新日時</target>
@@ -3638,8 +3458,8 @@
<target>拒否</target>
<note>reject incoming call via notification</note>
</trans-unit>
<trans-unit id="Reject (sender NOT notified)" xml:space="preserve">
<source>Reject (sender NOT notified)</source>
<trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve">
<source>Reject contact (sender NOT notified)</source>
<target>連絡を拒否(送信者には通知されません)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -3678,18 +3498,6 @@
<target>キーチェーンからパスフレーズを削除しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Renegotiate" xml:space="preserve">
<source>Renegotiate</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Renegotiate encryption" xml:space="preserve">
<source>Renegotiate encryption</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Renegotiate encryption?" xml:space="preserve">
<source>Renegotiate encryption?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reply" xml:space="preserve">
<source>Reply</source>
<target>返信</target>
@@ -3945,10 +3753,6 @@
<target>ライブメッセージを送信 (入力しながら宛先の画面で更新される)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send delivery receipts to" xml:space="preserve">
<source>Send delivery receipts to</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send direct message" xml:space="preserve">
<source>Send direct message</source>
<target>ダイレクトメッセージを送信</target>
@@ -3984,10 +3788,6 @@
<target>質問やアイデアを送る</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send receipts" xml:space="preserve">
<source>Send receipts</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve">
<source>Send them from gallery or custom keyboards.</source>
<target>ギャラリーまたはカスタム キーボードから送信します。</target>
@@ -4003,35 +3803,11 @@
<target>送信元が繋がりリクエストを削除したかもしれません。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve">
<source>Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending delivery receipts will be enabled for all contacts." xml:space="preserve">
<source>Sending delivery receipts will be enabled for all contacts.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending file will be stopped." xml:space="preserve">
<source>Sending file will be stopped.</source>
<target>ファイルの送信を停止します。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve">
<source>Sending receipts is disabled for %lld contacts</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve">
<source>Sending receipts is disabled for %lld groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve">
<source>Sending receipts is enabled for %lld contacts</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve">
<source>Sending receipts is enabled for %lld groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending via" xml:space="preserve">
<source>Sending via</source>
<target>経由で送信</target>
@@ -4252,10 +4028,6 @@
<target>飛ばしたメッセージ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Small groups (max 20)" xml:space="preserve">
<source>Small groups (max 20)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve">
<source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source>
<note>No comment provided by engineer.</note>
@@ -4472,10 +4244,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>作成されたアーカイブは、アプリの設定/データベース/過去のデータベースアーカイブから利用できます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve">
<source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The group is fully decentralized it is visible only to the members." xml:space="preserve">
<source>The group is fully decentralized it is visible only to the members.</source>
<target>グループは完全分散型で、メンバーしか内容を見れません。</target>
@@ -4511,10 +4279,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>プロフィールは連絡先にしか共有されません。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The second tick we missed! ✅" xml:space="preserve">
<source>The second tick we missed! ✅</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The sender will NOT be notified" xml:space="preserve">
<source>The sender will NOT be notified</source>
<target>送信者には通知されません</target>
@@ -4540,14 +4304,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>少なくとも1つのユーザープロフィールが表示されている必要があります。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="These settings are for your current profile **%@**." xml:space="preserve">
<source>These settings are for your current profile **%@**.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="They can be overridden in contact and group settings." xml:space="preserve">
<source>They can be overridden in contact and group settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve">
<source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source>
<target>ファイルとメディアが全て削除されます (※元に戻せません※)。低解像度の画像が残ります。</target>
@@ -4563,8 +4319,9 @@ It can happen because of some bug or when the connection is compromised.</source
<target>あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<trans-unit id="This error is permanent for this connection, please re-connect." xml:space="preserve">
<source>This error is permanent for this connection, please re-connect.</source>
<target>このエラーはこの接続では永続的なものです。再接続してください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group no longer exists." xml:space="preserve">
@@ -4587,6 +4344,11 @@ It can happen because of some bug or when the connection is compromised.</source
<target>接続するにはQRコードを読み込むか、アプリ内のリンクを使用する必要があります。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve">
<source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source>
<target>シークレットモード接続のプロフィールを確認するには、チャットの上部の連絡先、またはグループ名をタップします。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source>
<target>新規に接続する場合</target>
@@ -4667,7 +4429,7 @@ You will be prompted to complete authentication before this feature is enabled.<
<trans-unit id="Unexpected error: %@" xml:space="preserve">
<source>Unexpected error: %@</source>
<target>予期しないエラー: %@</target>
<note>item status description</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unexpected migration state" xml:space="preserve">
<source>Unexpected migration state</source>
@@ -4805,10 +4567,6 @@ To connect, please ask your contact to create another connection link and check
<target>チャット</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use current profile" xml:space="preserve">
<source>Use current profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use for new connections" xml:space="preserve">
<source>Use for new connections</source>
<target>新しい接続に使う</target>
@@ -4819,10 +4577,6 @@ To connect, please ask your contact to create another connection link and check
<target>iOS通話インターフェースを使用する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use new incognito profile" xml:space="preserve">
<source>Use new incognito profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>サーバを使う</target>
@@ -5033,14 +4787,6 @@ To connect, please ask your contact to create another connection link and check
<target>後からでも作成できます</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can enable later via Settings" xml:space="preserve">
<source>You can enable later via Settings</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can enable them later via app Privacy &amp; Security settings." xml:space="preserve">
<source>You can enable them later via app Privacy &amp; Security settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve">
<source>You can hide or mute a user profile - swipe it to the right.</source>
<target>ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。</target>
@@ -5111,8 +4857,8 @@ To connect, please ask your contact to create another connection link and check
<target>アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You invited a contact" xml:space="preserve">
<source>You invited a contact</source>
<trans-unit id="You invited your contact" xml:space="preserve">
<source>You invited your contact</source>
<target>連絡先に招待を送りました</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -5241,6 +4987,11 @@ To connect, please ask your contact to create another connection link and check
<target>あなたのチャットプロフィールが他のグループメンバーに送られます</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve">
<source>Your chat profile will be sent to your contact</source>
<target>あなたのチャットプロフィールが連絡相手に送られます</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>あなたのチャットプロフィール</target>
@@ -5295,10 +5046,6 @@ You can change it in Settings.</source>
<target>あなたのプライバシー</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile **%@** will be shared." xml:space="preserve">
<source>Your profile **%@** will be shared.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
@@ -5306,6 +5053,11 @@ SimpleX servers cannot see your profile.</source>
SimpleX サーバーはあなたのプロファイルを参照できません。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve">
<source>Your profile will be sent to the contact that you received this link from</source>
<target>あなたのプロフィールは、このリンクを受け取った連絡先に送信されます</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。</target>
@@ -5371,14 +5123,6 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
<target>管理者</target>
<note>member role</note>
</trans-unit>
<trans-unit id="agreeing encryption for %@…" xml:space="preserve">
<source>agreeing encryption for %@…</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="agreeing encryption…" xml:space="preserve">
<source>agreeing encryption…</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="always" xml:space="preserve">
<source>always</source>
<target>常に</target>
@@ -5439,12 +5183,14 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
<target>あなたの役割を %@ に変更しました</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="changing address for %@" xml:space="preserve">
<source>changing address for %@</source>
<trans-unit id="changing address for %@..." xml:space="preserve">
<source>changing address for %@...</source>
<target>%@ のアドレスを変更しています...</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="changing address" xml:space="preserve">
<source>changing address</source>
<trans-unit id="changing address..." xml:space="preserve">
<source>changing address...</source>
<target>アドレスを変更しています…</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="colored" xml:space="preserve">
@@ -5547,14 +5293,6 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
<target>デフォルト (%@)</target>
<note>pref value</note>
</trans-unit>
<trans-unit id="default (no)" xml:space="preserve">
<source>default (no)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="default (yes)" xml:space="preserve">
<source>default (yes)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="deleted" xml:space="preserve">
<source>deleted</source>
<target>削除完了</target>
@@ -5575,10 +5313,6 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
<target>直接</target>
<note>connection level description</note>
</trans-unit>
<trans-unit id="disabled" xml:space="preserve">
<source>disabled</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="duplicate message" xml:space="preserve">
<source>duplicate message</source>
<target>重複メッセージ</target>
@@ -5604,38 +5338,6 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
<target>あなたに有効</target>
<note>enabled status</note>
</trans-unit>
<trans-unit id="encryption agreed" xml:space="preserve">
<source>encryption agreed</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption agreed for %@" xml:space="preserve">
<source>encryption agreed for %@</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption ok" xml:space="preserve">
<source>encryption ok</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption ok for %@" xml:space="preserve">
<source>encryption ok for %@</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation allowed" xml:space="preserve">
<source>encryption re-negotiation allowed</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve">
<source>encryption re-negotiation allowed for %@</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation required" xml:space="preserve">
<source>encryption re-negotiation required</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation required for %@" xml:space="preserve">
<source>encryption re-negotiation required for %@</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="ended" xml:space="preserve">
<source>ended</source>
<target>終了</target>
@@ -5906,10 +5608,6 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
<target>シークレット</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="security code changed" xml:space="preserve">
<source>security code changed</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="starting…" xml:space="preserve">
<source>starting…</source>
<target>接続中…</target>
@@ -6054,7 +5752,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ja" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="CFBundleName" xml:space="preserve">
@@ -6086,7 +5784,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</file>
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="ja" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "ja",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "nl",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "pl",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "ru",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -2,7 +2,7 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
<file original="en.lproj/Localizable.strings" source-language="en" target-language="zh-Hans" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="&#10;" xml:space="preserve">
@@ -42,18 +42,6 @@
<target>!1 种彩色!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="# %@" xml:space="preserve">
<source># %@</source>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve">
<source>## History</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve">
<source>## In reply to</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="#secret#" xml:space="preserve">
<source>#secret#</source>
<target>#秘密#</target>
@@ -84,10 +72,6 @@
<target>%@ / %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ at %@:" xml:space="preserve">
<source>%1$@ at %2$@:</source>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve">
<source>%@ is connected!</source>
<target>%@ 已连接!</target>
@@ -313,12 +297,6 @@
<target>, </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- more stable message delivery.&#10;- a bit better groups.&#10;- and more!" xml:space="preserve">
<source>- more stable message delivery.
- a bit better groups.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes.
- custom time to disappear.
@@ -395,17 +373,19 @@
&lt;p&gt;&lt;a href="%@"&gt;通过 SimpleX Chat &lt;/a&gt;&lt;/p&gt;与我联系</target>
<note>email text</note>
</trans-unit>
<trans-unit id="A few more things" xml:space="preserve">
<source>A few more things</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A new contact" xml:space="preserve">
<source>A new contact</source>
<target>新联系人</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="A new random profile will be shared." xml:space="preserve">
<source>A new random profile will be shared.</source>
<trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve">
<source>A random profile will be sent to the contact that you received this link from</source>
<target>一个随机个人资料将被发送至给予您链接的联系人那里</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A random profile will be sent to your contact" xml:space="preserve">
<source>A random profile will be sent to your contact</source>
<target>一个随机资料将发送给您的联系人</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve">
@@ -422,17 +402,14 @@
</trans-unit>
<trans-unit id="Abort" xml:space="preserve">
<source>Abort</source>
<target>中止</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address" xml:space="preserve">
<source>Abort changing address</source>
<target>中止地址更改</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address?" xml:space="preserve">
<source>Abort changing address?</source>
<target>中止地址更改?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="About SimpleX" xml:space="preserve">
@@ -461,8 +438,8 @@
<note>accept contact request via notification
accept incoming call via notification</note>
</trans-unit>
<trans-unit id="Accept connection request?" xml:space="preserve">
<source>Accept connection request?</source>
<trans-unit id="Accept contact" xml:space="preserve">
<source>Accept contact</source>
<target>接受联系人</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -474,7 +451,7 @@
<trans-unit id="Accept incognito" xml:space="preserve">
<source>Accept incognito</source>
<target>接受隐身聊天</target>
<note>accept contact request via notification</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
@@ -518,7 +495,6 @@
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target>将中止地址更改。将使用旧接收地址。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
@@ -611,11 +587,6 @@
<target>允许不可撤回地删除已发送消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<target>允许发送文件和媒体。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>允许发送语音消息。</target>
@@ -723,7 +694,7 @@
</trans-unit>
<trans-unit id="Audio and video calls" xml:space="preserve">
<source>Audio and video calls</source>
<target>语音和视频通话</target>
<target>视频通话</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio/video calls" xml:space="preserve">
@@ -1047,16 +1018,8 @@
<target>连接</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Connect directly" xml:space="preserve">
<source>Connect directly</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via contact link" xml:space="preserve">
<source>Connect via contact link</source>
<trans-unit id="Connect via contact link?" xml:space="preserve">
<source>Connect via contact link?</source>
<target>通过联系人链接进行连接?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -1075,8 +1038,8 @@
<target>通过群组链接/二维码连接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<trans-unit id="Connect via one-time link?" xml:space="preserve">
<source>Connect via one-time link?</source>
<target>通过一次性链接连接?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -1105,6 +1068,11 @@
<target>连接错误AUTH</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection request" xml:space="preserve">
<source>Connection request</source>
<target>连接请求</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection request sent!" xml:space="preserve">
<source>Connection request sent!</source>
<target>已发送连接请求!</target>
@@ -1155,10 +1123,6 @@
<target>联系人偏好设置</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contacts" xml:space="preserve">
<source>Contacts</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve">
<source>Contacts can mark messages for deletion; you will be able to view them.</source>
<target>联系人可以将信息标记为删除;您将可以查看这些信息。</target>
@@ -1365,7 +1329,7 @@
<trans-unit id="Decryption error" xml:space="preserve">
<source>Decryption error</source>
<target>解密错误</target>
<note>message decrypt error item</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete" xml:space="preserve">
<source>Delete</source>
@@ -1552,18 +1516,6 @@
<target>已删除于:%@</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Delivery" xml:space="preserve">
<source>Delivery</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delivery receipts are disabled!" xml:space="preserve">
<source>Delivery receipts are disabled!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delivery receipts!" xml:space="preserve">
<source>Delivery receipts!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>描述</target>
@@ -1609,19 +1561,11 @@
<target>此群中禁止成员之间私信。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Disable (keep overrides)" xml:space="preserve">
<source>Disable (keep overrides)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
<source>Disable SimpleX Lock</source>
<target>禁用 SimpleX 锁定</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Disable for all" xml:space="preserve">
<source>Disable for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Disappearing message" xml:space="preserve">
<source>Disappearing message</source>
<target>限时消息</target>
@@ -1682,10 +1626,6 @@
<target>不创建地址</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't enable" xml:space="preserve">
<source>Don't enable</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't show again" xml:space="preserve">
<source>Don't show again</source>
<target>不再显示</target>
@@ -1726,10 +1666,6 @@
<target>启用</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable (keep overrides)" xml:space="preserve">
<source>Enable (keep overrides)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable SimpleX Lock" xml:space="preserve">
<source>Enable SimpleX Lock</source>
<target>启用 SimpleX 锁定</target>
@@ -1745,10 +1681,6 @@
<target>启用自动删除消息?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable instant notifications?" xml:space="preserve">
<source>Enable instant notifications?</source>
<target>启用即时通知?</target>
@@ -1866,7 +1798,6 @@
</trans-unit>
<trans-unit id="Error aborting address change" xml:space="preserve">
<source>Error aborting address change</source>
<target>中止地址更改错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error accepting contact request" xml:space="preserve">
@@ -1959,10 +1890,6 @@
<target>删除用户资料错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error enabling delivery receipts!" xml:space="preserve">
<source>Error enabling delivery receipts!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error enabling notifications" xml:space="preserve">
<source>Error enabling notifications</source>
<target>启用通知错误</target>
@@ -2043,10 +1970,6 @@
<target>发送消息错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error setting delivery receipts!" xml:space="preserve">
<source>Error setting delivery receipts!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error starting chat" xml:space="preserve">
<source>Error starting chat</source>
<target>启动聊天错误</target>
@@ -2062,10 +1985,6 @@
<target>切换资料错误!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error updating group link" xml:space="preserve">
<source>Error updating group link</source>
<target>更新群组链接错误</target>
@@ -2106,10 +2025,6 @@
<target>错误:没有数据库文件</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Even when disabled in the conversation." xml:space="preserve">
<source>Even when disabled in the conversation.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Exit without saving" xml:space="preserve">
<source>Exit without saving</source>
<target>退出而不保存</target>
@@ -2130,9 +2045,9 @@
<target>导出数据库归档。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Exporting database archive" xml:space="preserve">
<source>Exporting database archive</source>
<target>导出数据库档案中…</target>
<trans-unit id="Exporting database archive..." xml:space="preserve">
<source>Exporting database archive...</source>
<target>导出数据库档案中…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Failed to remove passphrase" xml:space="preserve">
@@ -2147,7 +2062,6 @@
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>最喜欢</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File will be deleted from servers." xml:space="preserve">
@@ -2175,58 +2089,11 @@
<target>文件和媒体</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<target>文件和媒体</target>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<target>此群组中禁止文件和媒体。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<target>禁止文件和媒体!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>终于我们有它们了! 🚀</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Find chats faster" xml:space="preserve">
<source>Find chats faster</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix" xml:space="preserve">
<source>Fix</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix connection" xml:space="preserve">
<source>Fix connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix connection?" xml:space="preserve">
<source>Fix connection?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix encryption after restoring backups." xml:space="preserve">
<source>Fix encryption after restoring backups.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix not supported by contact" xml:space="preserve">
<source>Fix not supported by contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix not supported by group member" xml:space="preserve">
<source>Fix not supported by group member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="For console" xml:space="preserve">
<source>For console</source>
<target>用于控制台</target>
@@ -2332,11 +2199,6 @@
<target>群组成员可以发送限时消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<target>群组成员可以发送文件和媒体。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>群组成员可以发送语音消息。</target>
@@ -2425,7 +2287,7 @@
<trans-unit id="History" xml:space="preserve">
<source>History</source>
<target>历史记录</target>
<note>No comment provided by engineer.</note>
<note>copied message info</note>
</trans-unit>
<trans-unit id="How SimpleX works" xml:space="preserve">
<source>How SimpleX works</source>
@@ -2532,10 +2394,6 @@
<target>改进的服务器配置</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="In reply to" xml:space="preserve">
<source>In reply to</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito" xml:space="preserve">
<source>Incognito</source>
<target>隐身聊天</target>
@@ -2546,8 +2404,14 @@
<target>隐身模式</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve">
<source>Incognito mode protects your privacy by using a new random profile for each contact.</source>
<trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve">
<source>Incognito mode is not supported here - your main profile will be sent to group members</source>
<target>此处不支持隐身模式——您的主要个人资料将发送给群组成员</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve">
<source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source>
<target>隐身模式可以保护你的主要个人资料名称和图像的隐私——对于每个新的联系人,都会创建一个新的随机个人资料。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incoming audio call" xml:space="preserve">
@@ -2622,10 +2486,6 @@
<target>无效的服务器地址!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid status" xml:space="preserve">
<source>Invalid status</source>
<note>item status text</note>
</trans-unit>
<trans-unit id="Invitation expired!" xml:space="preserve">
<source>Invitation expired!</source>
<target>邀请已过期!</target>
@@ -2714,11 +2574,7 @@
</trans-unit>
<trans-unit id="Joining group" xml:space="preserve">
<source>Joining group</source>
<target>加入群组</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>加入群组</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="KeyChain error" xml:space="preserve">
@@ -2811,10 +2667,6 @@
<target>建立私密连接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Make one message disappear" xml:space="preserve">
<source>Make one message disappear</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Make profile private!" xml:space="preserve">
<source>Make profile private!</source>
<target>将个人资料设为私密!</target>
@@ -2883,10 +2735,6 @@
<trans-unit id="Message delivery error" xml:space="preserve">
<source>Message delivery error</source>
<target>消息传递错误</target>
<note>item status text</note>
</trans-unit>
<trans-unit id="Message delivery receipts!" xml:space="preserve">
<source>Message delivery receipts!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message draft" xml:space="preserve">
@@ -2924,9 +2772,9 @@
<target>消息</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migrating database archive" xml:space="preserve">
<source>Migrating database archive</source>
<target>迁移数据库档案中…</target>
<trans-unit id="Migrating database archive..." xml:space="preserve">
<source>Migrating database archive...</source>
<target>迁移数据库档案中…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migration error:" xml:space="preserve">
@@ -2969,10 +2817,6 @@
<target>更多改进即将推出!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Most likely this connection is deleted." xml:space="preserve">
<source>Most likely this connection is deleted.</source>
<note>item status description</note>
</trans-unit>
<trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve">
<source>Most likely this contact has deleted the connection with you.</source>
<target>很可能此联系人已经删除了与您的联系。</target>
@@ -3078,29 +2922,16 @@
<target>没有联系人可添加</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No delivery information" xml:space="preserve">
<source>No delivery information</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No device token!" xml:space="preserve">
<source>No device token!</source>
<target>无设备令牌!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<target>无过滤聊天</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>未找到群组!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No history" xml:space="preserve">
<source>No history</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>没有录制语音消息的权限</target>
@@ -3185,11 +3016,6 @@
<target>只有群主可以改变群组偏好设置。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<target>只有组主可以启用文件和媒体。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>只有群主可以启用语音信息。</target>
@@ -3335,10 +3161,10 @@
<target>粘贴收到的链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve">
<source>Paste the link you received into the box below to connect with your contact.</source>
<target>将您收到的链接粘贴到下面的框中以与您的联系人联系。</target>
<note>placeholder</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
@@ -3510,11 +3336,6 @@
<target>禁止发送限时消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<target>禁止发送文件和媒体。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>禁止发送语音消息。</target>
@@ -3535,10 +3356,6 @@
<target>协议超时</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Protocol timeout per KB" xml:space="preserve">
<source>Protocol timeout per KB</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>推送通知</target>
@@ -3549,8 +3366,9 @@
<target>评价此应用程序</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="React" xml:space="preserve">
<source>React</source>
<trans-unit id="React..." xml:space="preserve">
<source>React...</source>
<target>回应……</target>
<note>chat item menu</note>
</trans-unit>
<trans-unit id="Read" xml:space="preserve">
@@ -3583,10 +3401,6 @@
<target>在我们的 [GitHub 仓库](https://github.com/simplex-chat/simplex-chat#readme) 中阅读更多信息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receipts are disabled" xml:space="preserve">
<source>Receipts are disabled</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Received at" xml:space="preserve">
<source>Received at</source>
<target>已收到于</target>
@@ -3609,7 +3423,6 @@
</trans-unit>
<trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve">
<source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source>
<target>接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
@@ -3627,14 +3440,6 @@
<target>对方会在您键入时看到更新。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve">
<source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reconnect servers?" xml:space="preserve">
<source>Reconnect servers?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Record updated at" xml:space="preserve">
<source>Record updated at</source>
<target>记录更新于</target>
@@ -3655,8 +3460,8 @@
<target>拒绝</target>
<note>reject incoming call via notification</note>
</trans-unit>
<trans-unit id="Reject (sender NOT notified)" xml:space="preserve">
<source>Reject (sender NOT notified)</source>
<trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve">
<source>Reject contact (sender NOT notified)</source>
<target>拒绝联系人(发送者不会被通知)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -3695,18 +3500,6 @@
<target>从钥匙串中删除密码?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Renegotiate" xml:space="preserve">
<source>Renegotiate</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Renegotiate encryption" xml:space="preserve">
<source>Renegotiate encryption</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Renegotiate encryption?" xml:space="preserve">
<source>Renegotiate encryption?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reply" xml:space="preserve">
<source>Reply</source>
<target>回复</target>
@@ -3962,10 +3755,6 @@
<target>发送实时消息——它会在您键入时为收件人更新</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send delivery receipts to" xml:space="preserve">
<source>Send delivery receipts to</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send direct message" xml:space="preserve">
<source>Send direct message</source>
<target>发送私信</target>
@@ -4001,10 +3790,6 @@
<target>发送问题和想法</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send receipts" xml:space="preserve">
<source>Send receipts</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve">
<source>Send them from gallery or custom keyboards.</source>
<target>发送它们来自图库或自定义键盘。</target>
@@ -4020,35 +3805,11 @@
<target>发送人可能已删除连接请求。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve">
<source>Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending delivery receipts will be enabled for all contacts." xml:space="preserve">
<source>Sending delivery receipts will be enabled for all contacts.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending file will be stopped." xml:space="preserve">
<source>Sending file will be stopped.</source>
<target>即将停止发送文件。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve">
<source>Sending receipts is disabled for %lld contacts</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve">
<source>Sending receipts is disabled for %lld groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve">
<source>Sending receipts is enabled for %lld contacts</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve">
<source>Sending receipts is enabled for %lld groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sending via" xml:space="preserve">
<source>Sending via</source>
<target>发送通过</target>
@@ -4269,10 +4030,6 @@
<target>已跳过消息</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Small groups (max 20)" xml:space="preserve">
<source>Small groups (max 20)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve">
<source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source>
<target>导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。</target>
@@ -4490,10 +4247,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>创建的归档文件可以通过应用设置/数据库/旧数据库归档访问。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve">
<source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The group is fully decentralized it is visible only to the members." xml:space="preserve">
<source>The group is fully decentralized it is visible only to the members.</source>
<target>该小组是完全分散式的——它只对成员可见。</target>
@@ -4529,10 +4282,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>该资料仅与您的联系人共享。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The second tick we missed! ✅" xml:space="preserve">
<source>The second tick we missed! ✅</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The sender will NOT be notified" xml:space="preserve">
<source>The sender will NOT be notified</source>
<target>发送者将不会收到通知</target>
@@ -4558,14 +4307,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>应该至少有一个可见的用户资料。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="These settings are for your current profile **%@**." xml:space="preserve">
<source>These settings are for your current profile **%@**.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="They can be overridden in contact and group settings." xml:space="preserve">
<source>They can be overridden in contact and group settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve">
<source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source>
<target>此操作无法撤消——所有接收和发送的文件和媒体都将被删除。 低分辨率图片将保留。</target>
@@ -4581,8 +4322,9 @@ It can happen because of some bug or when the connection is compromised.</source
<target>此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<trans-unit id="This error is permanent for this connection, please re-connect." xml:space="preserve">
<source>This error is permanent for this connection, please re-connect.</source>
<target>此错误对于此连接是永久性的,请重新连接。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group no longer exists." xml:space="preserve">
@@ -4605,6 +4347,11 @@ It can happen because of some bug or when the connection is compromised.</source
<target>您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve">
<source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source>
<target>要查找用于隐身聊天连接的资料,点击聊天顶部的联系人或群组名。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source>
<target>建立新连接</target>
@@ -4685,7 +4432,7 @@ You will be prompted to complete authentication before this feature is enabled.<
<trans-unit id="Unexpected error: %@" xml:space="preserve">
<source>Unexpected error: %@</source>
<target>意外错误: @</target>
<note>item status description</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unexpected migration state" xml:space="preserve">
<source>Unexpected migration state</source>
@@ -4694,7 +4441,6 @@ You will be prompted to complete authentication before this feature is enabled.<
</trans-unit>
<trans-unit id="Unfav." xml:space="preserve">
<source>Unfav.</source>
<target>取消最喜欢</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unhide" xml:space="preserve">
@@ -4824,10 +4570,6 @@ To connect, please ask your contact to create another connection link and check
<target>使用聊天</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use current profile" xml:space="preserve">
<source>Use current profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use for new connections" xml:space="preserve">
<source>Use for new connections</source>
<target>用于新连接</target>
@@ -4838,10 +4580,6 @@ To connect, please ask your contact to create another connection link and check
<target>使用 iOS 通话界面</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use new incognito profile" xml:space="preserve">
<source>Use new incognito profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>使用服务器</target>
@@ -5052,14 +4790,6 @@ To connect, please ask your contact to create another connection link and check
<target>您可以以后创建它</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can enable later via Settings" xml:space="preserve">
<source>You can enable later via Settings</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can enable them later via app Privacy &amp; Security settings." xml:space="preserve">
<source>You can enable them later via app Privacy &amp; Security settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve">
<source>You can hide or mute a user profile - swipe it to the right.</source>
<target>您可以隐藏或静音用户个人资料——只需向右滑动。</target>
@@ -5130,8 +4860,8 @@ To connect, please ask your contact to create another connection link and check
<target>您必须在每次应用程序启动时输入密码——它不存储在设备上。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You invited a contact" xml:space="preserve">
<source>You invited a contact</source>
<trans-unit id="You invited your contact" xml:space="preserve">
<source>You invited your contact</source>
<target>您邀请了您的联系人</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -5260,6 +4990,11 @@ To connect, please ask your contact to create another connection link and check
<target>您的聊天资料将被发送给群组成员</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve">
<source>Your chat profile will be sent to your contact</source>
<target>您的聊天资料将被发送给您的联系人</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>您的聊天资料</target>
@@ -5314,10 +5049,6 @@ You can change it in Settings.</source>
<target>您的隐私设置</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile **%@** will be shared." xml:space="preserve">
<source>Your profile **%@** will be shared.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
@@ -5325,6 +5056,11 @@ SimpleX servers cannot see your profile.</source>
SimpleX 服务器无法看到您的资料。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve">
<source>Your profile will be sent to the contact that you received this link from</source>
<target>您的个人资料将发送给您收到此链接的联系人</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>您的资料、联系人和发送的消息存储在您的设备上。</target>
@@ -5390,14 +5126,6 @@ SimpleX 服务器无法看到您的资料。</target>
<target>管理员</target>
<note>member role</note>
</trans-unit>
<trans-unit id="agreeing encryption for %@…" xml:space="preserve">
<source>agreeing encryption for %@…</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="agreeing encryption…" xml:space="preserve">
<source>agreeing encryption…</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="always" xml:space="preserve">
<source>always</source>
<target>始终</target>
@@ -5458,12 +5186,14 @@ SimpleX 服务器无法看到您的资料。</target>
<target>更改您的角色为 %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="changing address for %@" xml:space="preserve">
<source>changing address for %@</source>
<trans-unit id="changing address for %@..." xml:space="preserve">
<source>changing address for %@...</source>
<target>更改 %@... 的地址中</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="changing address" xml:space="preserve">
<source>changing address</source>
<trans-unit id="changing address..." xml:space="preserve">
<source>changing address...</source>
<target>更改地址中……</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="colored" xml:space="preserve">
@@ -5566,14 +5296,6 @@ SimpleX 服务器无法看到您的资料。</target>
<target>默认 (%@)</target>
<note>pref value</note>
</trans-unit>
<trans-unit id="default (no)" xml:space="preserve">
<source>default (no)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="default (yes)" xml:space="preserve">
<source>default (yes)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="deleted" xml:space="preserve">
<source>deleted</source>
<target>已删除</target>
@@ -5594,10 +5316,6 @@ SimpleX 服务器无法看到您的资料。</target>
<target>直接</target>
<note>connection level description</note>
</trans-unit>
<trans-unit id="disabled" xml:space="preserve">
<source>disabled</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="duplicate message" xml:space="preserve">
<source>duplicate message</source>
<target>重复的消息</target>
@@ -5623,38 +5341,6 @@ SimpleX 服务器无法看到您的资料。</target>
<target>为您启用</target>
<note>enabled status</note>
</trans-unit>
<trans-unit id="encryption agreed" xml:space="preserve">
<source>encryption agreed</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption agreed for %@" xml:space="preserve">
<source>encryption agreed for %@</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption ok" xml:space="preserve">
<source>encryption ok</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption ok for %@" xml:space="preserve">
<source>encryption ok for %@</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation allowed" xml:space="preserve">
<source>encryption re-negotiation allowed</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve">
<source>encryption re-negotiation allowed for %@</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation required" xml:space="preserve">
<source>encryption re-negotiation required</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation required for %@" xml:space="preserve">
<source>encryption re-negotiation required for %@</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="ended" xml:space="preserve">
<source>ended</source>
<target>已结束</target>
@@ -5926,10 +5612,6 @@ SimpleX 服务器无法看到您的资料。</target>
<target>秘密</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="security code changed" xml:space="preserve">
<source>security code changed</source>
<note>chat item text</note>
</trans-unit>
<trans-unit id="starting…" xml:space="preserve">
<source>starting…</source>
<target>启动中……</target>
@@ -6074,7 +5756,7 @@ SimpleX 服务器无法看到您的资料。</target>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="zh-Hans" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="CFBundleName" xml:space="preserve">
@@ -6106,7 +5788,7 @@ SimpleX 服务器无法看到您的资料。</target>
</file>
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="zh-Hans" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">

View File

@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "zh-Hans",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -76,7 +76,7 @@ class NotificationService: UNNotificationServiceExtension {
var badgeCount: Int = 0
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
logger.debug("DEBUGGING: NotificationService.didReceive")
logger.debug("NotificationService.didReceive")
if let ntf = request.content.mutableCopy() as? UNMutableNotificationContent {
setBestAttemptNtf(ntf)
}
@@ -127,7 +127,7 @@ class NotificationService: UNNotificationServiceExtension {
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)")
if let connEntity = ntfMsgInfo.connEntity {
setBestAttemptNtf(
ntfMsgInfo.ntfsEnabled
ntfMsgInfo.user.showNotifications
? .nse(notification: createConnectionEventNtf(ntfMsgInfo.user, connEntity))
: .empty
)
@@ -149,7 +149,7 @@ class NotificationService: UNNotificationServiceExtension {
}
override func serviceExtensionTimeWillExpire() {
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
logger.debug("NotificationService.serviceExtensionTimeWillExpire")
deliverBestAttemptNtf()
}
@@ -219,6 +219,7 @@ func startChat() -> DBMigrationResult? {
let justStarted = try apiStartChat()
chatStarted = true
if justStarted {
try apiSetIncognito(incognito: incognitoGroupDefault.get())
chatLastStartGroupDefault.set(Date.now)
Task { await receiveMessages() }
}
@@ -274,7 +275,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
cItem = autoReceiveFile(file) ?? cItem
}
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(notification: createMessageReceivedNtf(user, cInfo, cItem)) : .empty
return cItem.showNotification ? (aChatItem.chatId, ntf) : nil
return cItem.showMutableNotification ? (aChatItem.chatId, ntf) : nil
case let .rcvFileSndCancelled(_, aChatItem, _):
cleanupFile(aChatItem)
return nil
@@ -351,6 +352,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
throw r
}
func apiSetIncognito(incognito: Bool) throws {
let r = sendSimpleXCmd(.setIncognito(incognito: incognito))
if case .cmdOk = r { return }
throw r
}
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
guard apiGetActiveUser() != nil else {
logger.debug("no active user")
@@ -401,8 +408,4 @@ struct NtfMessages {
var connEntity: ConnectionEntity?
var msgTs: Date?
var ntfMessages: [NtfMsgInfo]
var ntfsEnabled: Bool {
user.showNotifications && (connEntity?.ntfsEnabled ?? false)
}
}

View File

@@ -141,8 +141,6 @@
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; };
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; };
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
@@ -161,17 +159,17 @@
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */; };
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
6462EF7A2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF752A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a */; };
6462EF7B2A8F4448003B2EAF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF762A8F4448003B2EAF /* libgmp.a */; };
6462EF7C2A8F4448003B2EAF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF772A8F4448003B2EAF /* libgmpxx.a */; };
6462EF7D2A8F4448003B2EAF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF782A8F4448003B2EAF /* libffi.a */; };
6462EF7E2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF792A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a */; };
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; };
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; };
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; };
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
64A353102A4C84CE007CD71D /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A3530B2A4C84CE007CD71D /* libgmp.a */; };
64A353112A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A3530C2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a */; };
64A353122A4C84CE007CD71D /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A3530D2A4C84CE007CD71D /* libffi.a */; };
64A353132A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A3530E2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a */; };
64A353142A4C84CE007CD71D /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A3530F2A4C84CE007CD71D /* libgmpxx.a */; };
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
@@ -419,8 +417,6 @@
5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = "<group>"; };
5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
@@ -438,11 +434,6 @@
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkedDeletedItemView.swift; sourceTree = "<group>"; };
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
6462EF752A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a"; sourceTree = "<group>"; };
6462EF762A8F4448003B2EAF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
6462EF772A8F4448003B2EAF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
6462EF782A8F4448003B2EAF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
6462EF792A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a"; sourceTree = "<group>"; };
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = "<group>"; };
@@ -450,6 +441,11 @@
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
64A3530B2A4C84CE007CD71D /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64A3530C2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a"; sourceTree = "<group>"; };
64A3530D2A4C84CE007CD71D /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64A3530E2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a"; sourceTree = "<group>"; };
64A3530F2A4C84CE007CD71D /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
@@ -502,12 +498,12 @@
buildActionMask = 2147483647;
files = (
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
6462EF7D2A8F4448003B2EAF /* libffi.a in Frameworks */,
6462EF7C2A8F4448003B2EAF /* libgmpxx.a in Frameworks */,
6462EF7A2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a in Frameworks */,
6462EF7E2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a in Frameworks */,
6462EF7B2A8F4448003B2EAF /* libgmp.a in Frameworks */,
64A353132A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a in Frameworks */,
64A353102A4C84CE007CD71D /* libgmp.a in Frameworks */,
64A353112A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a in Frameworks */,
64A353122A4C84CE007CD71D /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
64A353142A4C84CE007CD71D /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -568,11 +564,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
6462EF782A8F4448003B2EAF /* libffi.a */,
6462EF762A8F4448003B2EAF /* libgmp.a */,
6462EF772A8F4448003B2EAF /* libgmpxx.a */,
6462EF752A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a */,
6462EF792A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a */,
64A3530D2A4C84CE007CD71D /* libffi.a */,
64A3530B2A4C84CE007CD71D /* libgmp.a */,
64A3530F2A4C84CE007CD71D /* libgmpxx.a */,
64A3530E2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a */,
64A3530C2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -623,7 +619,6 @@
18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */,
64466DCB29FFE3E800E3D48D /* MailView.swift */,
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */,
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */,
);
path = Helpers;
sourceTree = "<group>";
@@ -746,7 +741,6 @@
5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */,
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */,
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */,
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */,
);
path = UserSettings;
sourceTree = "<group>";
@@ -1148,7 +1142,6 @@
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */,
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */,
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */,
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */,
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
@@ -1197,7 +1190,6 @@
5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */,
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */,
5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */,
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */,
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */,
@@ -1478,7 +1470,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 168;
CURRENT_PROJECT_VERSION = 151;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1499,7 +1491,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.3;
MARKETING_VERSION = 5.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1520,7 +1512,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 168;
CURRENT_PROJECT_VERSION = 151;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1541,7 +1533,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.3;
MARKETING_VERSION = 5.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1600,7 +1592,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 168;
CURRENT_PROJECT_VERSION = 151;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1613,7 +1605,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3;
MARKETING_VERSION = 5.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1632,7 +1624,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 168;
CURRENT_PROJECT_VERSION = 151;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1645,7 +1637,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3;
MARKETING_VERSION = 5.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1664,7 +1656,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 167;
CURRENT_PROJECT_VERSION = 71;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1688,7 +1680,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 5.3;
MARKETING_VERSION = 4.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1710,7 +1702,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 167;
CURRENT_PROJECT_VERSION = 71;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1734,7 +1726,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 5.3;
MARKETING_VERSION = 4.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;

View File

@@ -41,7 +41,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"

View File

@@ -206,7 +206,7 @@ public func responseError(_ err: Error) -> String {
switch r {
case let .chatCmdError(_, chatError): return chatErrorString(chatError)
case let .chatError(_, chatError): return chatErrorString(chatError)
default: return "\(String(describing: r.responseType)), details: \(String(describing: r.details))"
default: return String(describing: r)
}
} else {
return String(describing: err)

View File

@@ -17,9 +17,6 @@ public enum ChatCommand {
case createActiveUser(profile: Profile?, sameServers: Bool, pastTimestamp: Bool)
case listUsers
case apiSetActiveUser(userId: Int64, viewPwd: String?)
case setAllContactReceipts(enable: Bool)
case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings)
case apiSetUserGroupReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings)
case apiHideUser(userId: Int64, viewPwd: String)
case apiUnhideUser(userId: Int64, viewPwd: String)
case apiMuteUser(userId: Int64)
@@ -32,6 +29,7 @@ public enum ChatCommand {
case setTempFolder(tempFolder: String)
case setFilesFolder(filesFolder: String)
case apiSetXFTPConfig(config: XFTPFileConfig?)
case setIncognito(incognito: Bool)
case apiExportArchive(config: ArchiveConfig)
case apiImportArchive(config: ArchiveConfig)
case apiDeleteStorage
@@ -68,7 +66,6 @@ public enum ChatCommand {
case apiGetChatItemTTL(userId: Int64)
case apiSetNetworkConfig(networkConfig: NetCfg)
case apiGetNetworkConfig
case reconnectAllServers
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
case apiContactInfo(contactId: Int64)
case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64)
@@ -76,15 +73,12 @@ public enum ChatCommand {
case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
case apiAbortSwitchContact(contactId: Int64)
case apiAbortSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
case apiSyncContactRatchet(contactId: Int64, force: Bool)
case apiSyncGroupMemberRatchet(groupId: Int64, groupMemberId: Int64, force: Bool)
case apiGetContactCode(contactId: Int64)
case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64)
case apiVerifyContact(contactId: Int64, connectionCode: String?)
case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?)
case apiAddContact(userId: Int64, incognito: Bool)
case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
case apiConnect(userId: Int64, incognito: Bool, connReq: String)
case apiAddContact(userId: Int64)
case apiConnect(userId: Int64, connReq: String)
case apiDeleteChat(type: ChatType, id: Int64)
case apiClearChat(type: ChatType, id: Int64)
case apiListContacts(userId: Int64)
@@ -97,7 +91,7 @@ public enum ChatCommand {
case apiShowMyAddress(userId: Int64)
case apiSetProfileAddress(userId: Int64, on: Bool)
case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?)
case apiAcceptContact(incognito: Bool, contactReqId: Int64)
case apiAcceptContact(contactReqId: Int64)
case apiRejectContact(contactReqId: Int64)
// WebRTC calls
case apiSendCallInvitation(contact: Contact, callType: CallType)
@@ -125,13 +119,6 @@ public enum ChatCommand {
return "/_create user \(encodeJSON(user))"
case .listUsers: return "/users"
case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))"
case let .setAllContactReceipts(enable): return "/set receipts all \(onOff(enable))"
case let .apiSetUserContactReceipts(userId, userMsgReceiptSettings):
let umrs = userMsgReceiptSettings
return "/_set receipts contacts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))"
case let .apiSetUserGroupReceipts(userId, userMsgReceiptSettings):
let umrs = userMsgReceiptSettings
return "/_set receipts groups \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))"
case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))"
case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))"
case let .apiMuteUser(userId): return "/_mute user \(userId)"
@@ -148,6 +135,7 @@ public enum ChatCommand {
} else {
return "/_xftp off"
}
case let .setIncognito(incognito): return "/incognito \(onOff(incognito))"
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
case .apiDeleteStorage: return "/_db delete"
@@ -188,7 +176,6 @@ public enum ChatCommand {
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
case .apiGetNetworkConfig: return "/network"
case .reconnectAllServers: return "/reconnect"
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)"
@@ -196,25 +183,14 @@ public enum ChatCommand {
case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)"
case let .apiAbortSwitchContact(contactId): return "/_abort switch @\(contactId)"
case let .apiAbortSwitchGroupMember(groupId, groupMemberId): return "/_abort switch #\(groupId) \(groupMemberId)"
case let .apiSyncContactRatchet(contactId, force): if force {
return "/_sync @\(contactId) force=on"
} else {
return "/_sync @\(contactId)"
}
case let .apiSyncGroupMemberRatchet(groupId, groupMemberId, force): if force {
return "/_sync #\(groupId) \(groupMemberId) force=on"
} else {
return "/_sync #\(groupId) \(groupMemberId)"
}
case let .apiGetContactCode(contactId): return "/_get code @\(contactId)"
case let .apiGetGroupMemberCode(groupId, groupMemberId): return "/_get code #\(groupId) \(groupMemberId)"
case let .apiVerifyContact(contactId, .some(connectionCode)): return "/_verify code @\(contactId) \(connectionCode)"
case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)"
case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)"
case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)"
case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))"
case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))"
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)"
case let .apiAddContact(userId): return "/_connect \(userId)"
case let .apiConnect(userId, connReq): return "/_connect \(userId) \(connReq)"
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
case let .apiListContacts(userId): return "/_contacts \(userId)"
@@ -227,7 +203,7 @@ public enum ChatCommand {
case let .apiShowMyAddress(userId): return "/_show_address \(userId)"
case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))"
case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))"
case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)"
case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)"
case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))"
case let .apiRejectCall(contact): return "/_call reject @\(contact.apiId)"
@@ -259,9 +235,6 @@ public enum ChatCommand {
case .createActiveUser: return "createActiveUser"
case .listUsers: return "listUsers"
case .apiSetActiveUser: return "apiSetActiveUser"
case .setAllContactReceipts: return "setAllContactReceipts"
case .apiSetUserContactReceipts: return "apiSetUserContactReceipts"
case .apiSetUserGroupReceipts: return "apiSetUserGroupReceipts"
case .apiHideUser: return "apiHideUser"
case .apiUnhideUser: return "apiUnhideUser"
case .apiMuteUser: return "apiMuteUser"
@@ -274,6 +247,7 @@ public enum ChatCommand {
case .setTempFolder: return "setTempFolder"
case .setFilesFolder: return "setFilesFolder"
case .apiSetXFTPConfig: return "apiSetXFTPConfig"
case .setIncognito: return "setIncognito"
case .apiExportArchive: return "apiExportArchive"
case .apiImportArchive: return "apiImportArchive"
case .apiDeleteStorage: return "apiDeleteStorage"
@@ -310,7 +284,6 @@ public enum ChatCommand {
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
case .apiSetNetworkConfig: return "apiSetNetworkConfig"
case .apiGetNetworkConfig: return "apiGetNetworkConfig"
case .reconnectAllServers: return "reconnectAllServers"
case .apiSetChatSettings: return "apiSetChatSettings"
case .apiContactInfo: return "apiContactInfo"
case .apiGroupMemberInfo: return "apiGroupMemberInfo"
@@ -318,14 +291,11 @@ public enum ChatCommand {
case .apiSwitchGroupMember: return "apiSwitchGroupMember"
case .apiAbortSwitchContact: return "apiAbortSwitchContact"
case .apiAbortSwitchGroupMember: return "apiAbortSwitchGroupMember"
case .apiSyncContactRatchet: return "apiSyncContactRatchet"
case .apiSyncGroupMemberRatchet: return "apiSyncGroupMemberRatchet"
case .apiGetContactCode: return "apiGetContactCode"
case .apiGetGroupMemberCode: return "apiGetGroupMemberCode"
case .apiVerifyContact: return "apiVerifyContact"
case .apiVerifyGroupMember: return "apiVerifyGroupMember"
case .apiAddContact: return "apiAddContact"
case .apiSetConnectionIncognito: return "apiSetConnectionIncognito"
case .apiConnect: return "apiConnect"
case .apiDeleteChat: return "apiDeleteChat"
case .apiClearChat: return "apiClearChat"
@@ -437,23 +407,13 @@ public enum ChatResponse: Decodable, Error {
case groupMemberSwitchStarted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
case contactSwitchAborted(user: User, contact: Contact, connectionStats: ConnectionStats)
case groupMemberSwitchAborted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
case contactSwitch(user: User, contact: Contact, switchProgress: SwitchProgress)
case groupMemberSwitch(user: User, groupInfo: GroupInfo, member: GroupMember, switchProgress: SwitchProgress)
case contactRatchetSyncStarted(user: User, contact: Contact, connectionStats: ConnectionStats)
case groupMemberRatchetSyncStarted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
case contactRatchetSync(user: User, contact: Contact, ratchetSyncProgress: RatchetSyncProgress)
case groupMemberRatchetSync(user: User, groupInfo: GroupInfo, member: GroupMember, ratchetSyncProgress: RatchetSyncProgress)
case contactVerificationReset(user: User, contact: Contact)
case groupMemberVerificationReset(user: User, groupInfo: GroupInfo, member: GroupMember)
case contactCode(user: User, contact: Contact, connectionCode: String)
case groupMemberCode(user: User, groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
case connectionVerified(user: User, verified: Bool, expectedCode: String)
case invitation(user: User, connReqInvitation: String, connection: PendingContactConnection)
case connectionIncognitoUpdated(user: User, toConnection: PendingContactConnection)
case invitation(user: User, connReqInvitation: String)
case sentConfirmation(user: User)
case sentInvitation(user: User)
case contactAlreadyExists(user: User, contact: Contact)
case contactRequestAlreadyAccepted(user: User, contact: Contact)
case contactDeleted(user: User, contact: Contact)
case chatCleared(user: User, chatInfo: ChatInfo)
case userProfileNoChange(user: User)
@@ -483,7 +443,6 @@ public enum ChatResponse: Decodable, Error {
case newChatItem(user: User, chatItem: AChatItem)
case chatItemStatusUpdated(user: User, chatItem: AChatItem)
case chatItemUpdated(user: User, chatItem: AChatItem)
case chatItemNotChanged(user: User, chatItem: AChatItem)
case chatItemReaction(user: User, added: Bool, reaction: ACIReaction)
case chatItemDeleted(user: User, deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool)
case contactsList(user: User, contacts: [Contact])
@@ -571,23 +530,13 @@ public enum ChatResponse: Decodable, Error {
case .groupMemberSwitchStarted: return "groupMemberSwitchStarted"
case .contactSwitchAborted: return "contactSwitchAborted"
case .groupMemberSwitchAborted: return "groupMemberSwitchAborted"
case .contactSwitch: return "contactSwitch"
case .groupMemberSwitch: return "groupMemberSwitch"
case .contactRatchetSyncStarted: return "contactRatchetSyncStarted"
case .groupMemberRatchetSyncStarted: return "groupMemberRatchetSyncStarted"
case .contactRatchetSync: return "contactRatchetSync"
case .groupMemberRatchetSync: return "groupMemberRatchetSync"
case .contactVerificationReset: return "contactVerificationReset"
case .groupMemberVerificationReset: return "groupMemberVerificationReset"
case .contactCode: return "contactCode"
case .groupMemberCode: return "groupMemberCode"
case .connectionVerified: return "connectionVerified"
case .invitation: return "invitation"
case .connectionIncognitoUpdated: return "connectionIncognitoUpdated"
case .sentConfirmation: return "sentConfirmation"
case .sentInvitation: return "sentInvitation"
case .contactAlreadyExists: return "contactAlreadyExists"
case .contactRequestAlreadyAccepted: return "contactRequestAlreadyAccepted"
case .contactDeleted: return "contactDeleted"
case .chatCleared: return "chatCleared"
case .userProfileNoChange: return "userProfileNoChange"
@@ -617,7 +566,6 @@ public enum ChatResponse: Decodable, Error {
case .newChatItem: return "newChatItem"
case .chatItemStatusUpdated: return "chatItemStatusUpdated"
case .chatItemUpdated: return "chatItemUpdated"
case .chatItemNotChanged: return "chatItemNotChanged"
case .chatItemReaction: return "chatItemReaction"
case .chatItemDeleted: return "chatItemDeleted"
case .contactsList: return "contactsList"
@@ -704,23 +652,13 @@ public enum ChatResponse: Decodable, Error {
case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
case let .groupMemberSwitchAborted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
case let .contactSwitch(u, contact, switchProgress): return withUser(u, "contact: \(String(describing: contact))\nswitchProgress: \(String(describing: switchProgress))")
case let .groupMemberSwitch(u, groupInfo, member, switchProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nswitchProgress: \(String(describing: switchProgress))")
case let .contactRatchetSyncStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
case let .groupMemberRatchetSyncStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
case let .contactRatchetSync(u, contact, ratchetSyncProgress): return withUser(u, "contact: \(String(describing: contact))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))")
case let .groupMemberRatchetSync(u, groupInfo, member, ratchetSyncProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))")
case let .contactVerificationReset(u, contact): return withUser(u, "contact: \(String(describing: contact))")
case let .groupMemberVerificationReset(u, groupInfo, member): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))")
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
case let .invitation(u, connReqInvitation, _): return withUser(u, connReqInvitation)
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
case let .invitation(u, connReqInvitation): return withUser(u, connReqInvitation)
case .sentConfirmation: return noDetails
case .sentInvitation: return noDetails
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
case let .contactRequestAlreadyAccepted(u, contact): return withUser(u, String(describing: contact))
case let .contactDeleted(u, contact): return withUser(u, String(describing: contact))
case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo))
case .userProfileNoChange: return noDetails
@@ -750,7 +688,6 @@ public enum ChatResponse: Decodable, Error {
case let .newChatItem(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemStatusUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))")
case let .chatItemDeleted(u, deletedChatItem, toChatItem, byUser): return withUser(u, "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)")
case let .contactsList(u, contacts): return withUser(u, String(describing: contacts))
@@ -824,14 +761,6 @@ public enum ChatResponse: Decodable, Error {
}
}
public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? {
switch chatResponse {
case let .chatCmdError(_, .error(error)): return error
case let .chatError(_, .error(error)): return error
default: return nil
}
}
struct NewUser: Encodable {
var profile: Profile?
var sameServers: Bool
@@ -1065,7 +994,6 @@ public struct NetCfg: Codable, Equatable {
public var sessionMode: TransportSessionMode
public var tcpConnectTimeout: Int // microseconds
public var tcpTimeout: Int // microseconds
public var tcpTimeoutPerKb: Int // microseconds
public var tcpKeepAlive: KeepAliveOpts?
public var smpPingInterval: Int // microseconds
public var smpPingCount: Int // times
@@ -1074,9 +1002,8 @@ public struct NetCfg: Codable, Equatable {
public static let defaults: NetCfg = NetCfg(
socksProxy: nil,
sessionMode: TransportSessionMode.user,
tcpConnectTimeout: 15_000_000,
tcpTimeout: 10_000_000,
tcpTimeoutPerKb: 20_000,
tcpConnectTimeout: 10_000_000,
tcpTimeout: 7_000_000,
tcpKeepAlive: KeepAliveOpts.defaults,
smpPingInterval: 1200_000_000,
smpPingCount: 3,
@@ -1086,9 +1013,8 @@ public struct NetCfg: Codable, Equatable {
public static let proxyDefaults: NetCfg = NetCfg(
socksProxy: nil,
sessionMode: TransportSessionMode.user,
tcpConnectTimeout: 30_000_000,
tcpTimeout: 20_000_000,
tcpTimeoutPerKb: 40_000,
tcpConnectTimeout: 20_000_000,
tcpTimeout: 15_000_000,
tcpKeepAlive: KeepAliveOpts.defaults,
smpPingInterval: 1200_000_000,
smpPingCount: 3,
@@ -1164,42 +1090,19 @@ public struct KeepAliveOpts: Codable, Equatable {
public struct ChatSettings: Codable {
public var enableNtfs: Bool
public var sendRcpts: Bool?
public var favorite: Bool
public init(enableNtfs: Bool, sendRcpts: Bool?, favorite: Bool) {
public init(enableNtfs: Bool, favorite: Bool) {
self.enableNtfs = enableNtfs
self.sendRcpts = sendRcpts
self.favorite = favorite
}
public static let defaults: ChatSettings = ChatSettings(enableNtfs: true, sendRcpts: nil, favorite: false)
public static let defaults: ChatSettings = ChatSettings(enableNtfs: true, favorite: false)
}
public struct UserMsgReceiptSettings: Codable {
public var enable: Bool
public var clearOverrides: Bool
public init(enable: Bool, clearOverrides: Bool) {
self.enable = enable
self.clearOverrides = clearOverrides
}
}
public struct ConnectionStats: Decodable {
public var connAgentVersion: Int
public struct ConnectionStats: Codable {
public var rcvQueuesInfo: [RcvQueueInfo]
public var sndQueuesInfo: [SndQueueInfo]
public var ratchetSyncState: RatchetSyncState
public var ratchetSyncSupported: Bool
public var ratchetSyncAllowed: Bool {
ratchetSyncSupported && [.allowed, .required].contains(ratchetSyncState)
}
public var ratchetSyncSendProhibited: Bool {
[.required, .started, .agreed].contains(ratchetSyncState)
}
}
public struct RcvQueueInfo: Codable {
@@ -1225,30 +1128,6 @@ public enum SndSwitchStatus: String, Codable {
case sendingQTEST = "sending_qtest"
}
public enum QueueDirection: String, Decodable {
case rcv
case snd
}
public struct SwitchProgress: Decodable {
public var queueDirection: QueueDirection
public var switchPhase: SwitchPhase
public var connectionStats: ConnectionStats
}
public struct RatchetSyncProgress: Decodable {
public var ratchetSyncStatus: RatchetSyncState
public var connectionStats: ConnectionStats
}
public enum RatchetSyncState: String, Decodable {
case ok
case allowed
case required
case started
case agreed
}
public struct UserContactLink: Decodable {
public var connReqContact: String
public var autoAccept: AutoAccept?
@@ -1384,32 +1263,14 @@ public enum ChatError: Decodable {
public enum ChatErrorType: Decodable {
case noActiveUser
case noConnectionUser(agentConnId: String)
case noSndFileUser(agentSndFileId: String)
case noRcvFileUser(agentRcvFileId: String)
case userUnknown
case activeUserExists
case userExists
case differentActiveUser(commandUserId: Int64, activeUserId: Int64)
case cantDeleteActiveUser(userId: Int64)
case cantDeleteLastUser(userId: Int64)
case cantHideLastUser(userId: Int64)
case hiddenUserAlwaysMuted(userId: Int64)
case emptyUserPassword(userId: Int64)
case userAlreadyHidden(userId: Int64)
case userNotHidden(userId: Int64)
case differentActiveUser
case chatNotStarted
case chatNotStopped
case chatStoreChanged
case invalidConnReq
case invalidChatMessage(connection: Connection, message: String)
case invalidChatMessage(message: String)
case contactNotReady(contact: Contact)
case contactDisabled(contact: Contact)
case connectionDisabled(connection: Connection)
case groupUserRole(groupInfo: GroupInfo, requiredRole: GroupMemberRole)
case groupMemberInitialRole(groupInfo: GroupInfo, initialRole: GroupMemberRole)
case contactIncognitoCantInvite
case groupIncognitoCantInvite
case groupUserRole
case groupContactRole(contactName: ContactName)
case groupDuplicateMember(contactName: ContactName)
case groupDuplicateMemberId
@@ -1421,50 +1282,23 @@ public enum ChatErrorType: Decodable {
case groupCantResendInvitation(groupInfo: GroupInfo, contactName: ContactName)
case groupInternal(message: String)
case fileNotFound(message: String)
case fileSize(filePath: String)
case fileAlreadyReceiving(message: String)
case fileCancelled(message: String)
case fileCancel(fileId: Int64, message: String)
case fileAlreadyExists(filePath: String)
case fileRead(filePath: String, message: String)
case fileWrite(filePath: String, message: String)
case fileSend(fileId: Int64, agentError: String)
case fileRcvChunk(message: String)
case fileInternal(message: String)
case fileImageType(filePath: String)
case fileImageSize(filePath: String)
case fileNotReceived(fileId: Int64)
// case xFTPRcvFile
// case xFTPSndFile
case fallbackToSMPProhibited(fileId: Int64)
case inlineFileProhibited(fileId: Int64)
case invalidQuote
case invalidChatItemUpdate
case invalidChatItemDelete
case hasCurrentCall
case noCurrentCall
case callContact(contactId: Int64)
case callState
case directMessagesProhibited(contact: Contact)
case agentVersion
case agentNoSubResult(agentConnId: String)
case commandError(message: String)
case serverProtocol
case agentCommandError(message: String)
case invalidFileDescription(message: String)
case connectionIncognitoChangeProhibited
case internalError(message: String)
case exception(message: String)
}
public enum StoreError: Decodable {
case duplicateName
case userNotFound(userId: Int64)
case userNotFoundByName(contactName: ContactName)
case userNotFoundByContactId(contactId: Int64)
case userNotFoundByGroupId(groupId: Int64)
case userNotFoundByFileId(fileId: Int64)
case userNotFoundByContactRequestId(contactRequestId: Int64)
case contactNotFound(contactId: Int64)
case contactNotFoundByName(contactName: ContactName)
case contactNotReady(contactName: ContactName)
@@ -1474,9 +1308,6 @@ public enum StoreError: Decodable {
case contactRequestNotFoundByName(contactName: ContactName)
case groupNotFound(groupId: Int64)
case groupNotFoundByName(groupName: GroupName)
case groupMemberNameNotFound(groupId: Int64, groupMemberName: ContactName)
case groupMemberNotFound(groupMemberId: Int64)
case groupMemberNotFoundByMemberId(memberId: String)
case groupWithoutUser
case duplicateGroupMember
case groupAlreadyJoined
@@ -1484,16 +1315,9 @@ public enum StoreError: Decodable {
case sndFileNotFound(fileId: Int64)
case sndFileInvalid(fileId: Int64)
case rcvFileNotFound(fileId: Int64)
case rcvFileDescrNotFound(fileId: Int64)
case fileNotFound(fileId: Int64)
case rcvFileInvalid(fileId: Int64)
case rcvFileInvalidDescrPart
case sharedMsgIdNotFoundByFileId(fileId: Int64)
case fileIdNotFoundBySharedMsgId(sharedMsgId: String)
case sndFileNotFoundXFTP(agentSndFileId: String)
case rcvFileNotFoundXFTP(agentRcvFileId: String)
case connectionNotFound(agentConnId: String)
case connectionNotFoundById(connId: Int64)
case pendingConnectionNotFound(connId: Int64)
case introNotFound
case uniqueID
@@ -1501,16 +1325,11 @@ public enum StoreError: Decodable {
case noMsgDelivery(connId: Int64, agentMsgId: String)
case badChatItem(itemId: Int64)
case chatItemNotFound(itemId: Int64)
case chatItemNotFoundByText(text: String)
case quotedChatItemNotFound
case chatItemSharedMsgIdNotFound(sharedMsgId: String)
case chatItemNotFoundByFileId(fileId: Int64)
case chatItemNotFoundByGroupId(groupId: Int64)
case profileNotFound(profileId: Int64)
case duplicateGroupLink(groupInfo: GroupInfo)
case groupLinkNotFound(groupInfo: GroupInfo)
case hostMemberIdNotFound(groupId: Int64)
case contactNotFoundByFileId(fileId: Int64)
case noGroupSndStatus(itemId: Int64, groupMemberId: Int64)
}
public enum DatabaseError: Decodable {
@@ -1530,12 +1349,11 @@ public enum AgentErrorType: Decodable {
case CMD(cmdErr: CommandErrorType)
case CONN(connErr: ConnectionErrorType)
case SMP(smpErr: ProtocolErrorType)
case NTF(ntfErr: ProtocolErrorType)
case XFTP(xftpErr: XFTPErrorType)
case NTF(ntfErr: ProtocolErrorType)
case BROKER(brokerAddress: String, brokerErr: BrokerErrorType)
case AGENT(agentErr: SMPAgentError)
case INTERNAL(internalErr: String)
case INACTIVE
}
public enum CommandErrorType: Decodable {
@@ -1555,10 +1373,9 @@ public enum ConnectionErrorType: Decodable {
}
public enum BrokerErrorType: Decodable {
case RESPONSE(smpErr: String)
case RESPONSE(smpErr: ProtocolErrorType)
case UNEXPECTED
case NETWORK
case HOST
case TRANSPORT(transportErr: ProtocolTransportError)
case TIMEOUT
}
@@ -1592,7 +1409,6 @@ public enum XFTPErrorType: Decodable {
public enum ProtocolCommandError: Decodable {
case UNKNOWN
case SYNTAX
case PROHIBITED
case NO_AUTH
case HAS_AUTH
case NO_ENTITY
@@ -1615,9 +1431,7 @@ public enum SMPAgentError: Decodable {
case A_MESSAGE
case A_PROHIBITED
case A_VERSION
case A_CRYPTO
case A_DUPLICATE
case A_QUEUE(queueErr: String)
case A_ENCRYPTION
}
public enum ArchiveError: Decodable {

View File

@@ -13,8 +13,6 @@ let GROUP_DEFAULT_APP_STATE = "appState"
let GROUP_DEFAULT_DB_CONTAINER = "dbContainer"
public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart"
let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode"
public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal"
public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic"
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
@@ -22,14 +20,13 @@ let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout"
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout"
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb"
let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval"
let GROUP_DEFAULT_NETWORK_SMP_PING_COUNT = "networkSMPPingCount"
let GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE = "networkEnableKeepAlive"
let GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE = "networkTCPKeepIdle"
let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl"
let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt"
public let GROUP_DEFAULT_INCOGNITO = "incognito"
let GROUP_DEFAULT_INCOGNITO = "incognito"
let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase"
let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase"
public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades"
@@ -41,13 +38,10 @@ public let groupDefaults = UserDefaults(suiteName: APP_GROUP_NAME)!
public func registerGroupDefaults() {
groupDefaults.register(defaults: [
GROUP_DEFAULT_NTF_ENABLE_LOCAL: false,
GROUP_DEFAULT_NTF_ENABLE_PERIODIC: false,
GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue,
GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.user.rawValue,
GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout,
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout,
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb,
GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval,
GROUP_DEFAULT_NETWORK_SMP_PING_COUNT: NetCfg.defaults.smpPingCount,
GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE: NetCfg.defaults.enableKeepAlive,
@@ -107,10 +101,6 @@ public let ntfPreviewModeGroupDefault = EnumDefault<NotificationPreviewMode>(
public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INCOGNITO)
public let ntfEnableLocalGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_ENABLE_LOCAL)
public let ntfEnablePeriodicGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_ENABLE_PERIODIC)
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
public let privacyTransferImagesInlineGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE)
@@ -219,7 +209,6 @@ public func getNetCfg() -> NetCfg {
let sessionMode = networkSessionModeGroupDefault.get()
let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL)
let smpPingCount = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT)
let enableKeepAlive = groupDefaults.bool(forKey: GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE)
@@ -238,7 +227,6 @@ public func getNetCfg() -> NetCfg {
sessionMode: sessionMode,
tcpConnectTimeout: tcpConnectTimeout,
tcpTimeout: tcpTimeout,
tcpTimeoutPerKb: tcpTimeoutPerKb,
tcpKeepAlive: tcpKeepAlive,
smpPingInterval: smpPingInterval,
smpPingCount: smpPingCount,
@@ -251,7 +239,6 @@ public func setNetCfg(_ cfg: NetCfg) {
networkSessionModeGroupDefault.set(cfg.sessionMode)
groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL)
groupDefaults.set(cfg.smpPingCount, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT)
if let tcpKeepAlive = cfg.tcpKeepAlive {

View File

@@ -23,8 +23,6 @@ public struct User: Decodable, NamedChat, Identifiable {
public var localAlias: String { get { "" } }
public var showNtfs: Bool
public var sendRcptsContacts: Bool
public var sendRcptsSmallGroups: Bool
public var viewPwdHash: UserPwdHash?
public var id: Int64 { userId }
@@ -46,9 +44,7 @@ public struct User: Decodable, NamedChat, Identifiable {
profile: LocalProfile.sampleData,
fullPreferences: FullPreferences.sampleData,
activeUser: true,
showNtfs: true,
sendRcptsContacts: true,
sendRcptsSmallGroups: false
showNtfs: true
)
}
@@ -1200,13 +1196,6 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
public var groupInfo: GroupInfo? {
switch self {
case let .group(groupInfo): return groupInfo
default: return nil
}
}
// this works for features that are common for contacts and groups
public func featureEnabled(_ feature: ChatFeature) -> Bool {
switch self {
@@ -1364,7 +1353,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
public var id: ChatId { get { "@\(contactId)" } }
public var apiId: Int64 { get { contactId } }
public var ready: Bool { get { activeConn.connStatus == .ready } }
public var sendMsgEnabled: Bool { get { !(activeConn.connectionStats?.ratchetSyncSendProhibited ?? false) } }
public var sendMsgEnabled: Bool { get { true } }
public var displayName: String { localAlias == "" ? profile.displayName : localAlias }
public var fullName: String { get { profile.fullName } }
public var image: String? { get { profile.image } }
@@ -1437,12 +1426,6 @@ public struct Connection: Decodable {
public var customUserProfileId: Int64?
public var connectionCode: SecurityCode?
public var connectionStats: ConnectionStats? = nil
private enum CodingKeys: String, CodingKey {
case connId, agentConnId, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode
}
public var id: ChatId { get { ":\(connId)" } }
static let sampleData = Connection(
@@ -1466,8 +1449,6 @@ public struct SecurityCode: Decodable, Equatable {
public struct UserContact: Decodable {
public var userContactLinkId: Int64
// public var connReqContact: String
public var groupId: Int64?
public init(userContactLinkId: Int64) {
self.userContactLinkId = userContactLinkId
@@ -1929,16 +1910,6 @@ public enum ConnectionEntity: Decodable {
return nil
}
}
public var ntfsEnabled: Bool {
switch self {
case let .rcvDirectMsgConnection(contact): return contact?.chatSettings.enableNtfs ?? false
case let .rcvGroupMsgConnection(groupInfo, _): return groupInfo.chatSettings.enableNtfs
case .sndFileConnection: return false
case .rcvFileConnection: return false
case let .userContactConnection(userContact): return userContact.groupId == nil
}
}
}
public struct NtfMsgInfo: Decodable {
@@ -2021,17 +1992,6 @@ public struct ChatItem: Identifiable, Decodable {
}
}
public var memberConnected: GroupMember? {
switch chatDir {
case .groupRcv(let groupMember):
switch content {
case .rcvGroupEvent(rcvGroupEvent: .memberConnected): return groupMember
default: return nil
}
default: return nil
}
}
private var showNtfDir: Bool {
return !chatDir.sent
}
@@ -2094,6 +2054,14 @@ public struct ChatItem: Identifiable, Decodable {
return nil
}
public var showMutableNotification: Bool {
switch content {
case .rcvCall: return false
case .rcvChatFeature: return false
default: return showNtfDir
}
}
public var memberDisplayName: String? {
get {
if case let .groupRcv(groupMember) = chatDir {
@@ -2293,7 +2261,13 @@ public struct CIMeta: Decodable {
}
public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? {
itemStatus.statusIcon(metaColor)
switch itemStatus {
case .sndSent: return ("checkmark", metaColor)
case .sndErrorAuth: return ("multiply", .red)
case .sndError: return ("exclamationmark.triangle.fill", .yellow)
case .rcvNew: return ("circlebadge.fill", Color.accentColor)
default: return nil
}
}
public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> CIMeta {
@@ -2356,75 +2330,22 @@ private func recent(_ date: Date) -> Bool {
public enum CIStatus: Decodable {
case sndNew
case sndSent(sndProgress: SndCIStatusProgress)
case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress)
case sndSent
case sndErrorAuth
case sndError(agentError: String)
case rcvNew
case rcvRead
case invalid(text: String)
var id: String {
switch self {
case .sndNew: return "sndNew"
case .sndSent: return "sndSent"
case .sndRcvd: return "sndRcvd"
case .sndErrorAuth: return "sndErrorAuth"
case .sndError: return "sndError"
case .rcvNew: return "rcvNew"
case .sndNew: return "sndNew"
case .sndSent: return "sndSent"
case .sndErrorAuth: return "sndErrorAuth"
case .sndError: return "sndError"
case .rcvNew: return "rcvNew"
case .rcvRead: return "rcvRead"
case .invalid: return "invalid"
}
}
public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? {
switch self {
case .sndNew: return nil
case .sndSent: return ("checkmark", metaColor)
case let .sndRcvd(msgRcptStatus, _):
switch msgRcptStatus {
case .ok: return ("checkmark", metaColor)
case .badMsgHash: return ("checkmark", .red)
}
case .sndErrorAuth: return ("multiply", .red)
case .sndError: return ("exclamationmark.triangle.fill", .yellow)
case .rcvNew: return ("circlebadge.fill", Color.accentColor)
case .rcvRead: return nil
case .invalid: return ("questionmark", metaColor)
}
}
public var statusInfo: (String, String)? {
switch self {
case .sndNew: return nil
case .sndSent: return nil
case .sndRcvd: return nil
case .sndErrorAuth: return (
NSLocalizedString("Message delivery error", comment: "item status text"),
NSLocalizedString("Most likely this connection is deleted.", comment: "item status description")
)
case let .sndError(agentError): return (
NSLocalizedString("Message delivery error", comment: "item status text"),
String.localizedStringWithFormat(NSLocalizedString("Unexpected error: %@", comment: "item status description"), agentError)
)
case .rcvNew: return nil
case .rcvRead: return nil
case let .invalid(text): return (
NSLocalizedString("Invalid status", comment: "item status text"),
text
)
}
}
}
public enum MsgReceiptStatus: String, Decodable {
case ok
case badMsgHash
}
public enum SndCIStatusProgress: String, Decodable {
case partial
case complete
}
public enum CIDeleted: Decodable {
@@ -2530,43 +2451,25 @@ public enum CIContent: Decodable, ItemContent {
}
}
}
public var showMemberName: Bool {
switch self {
case .rcvMsgContent: return true
case .rcvDeleted: return true
case .rcvCall: return true
case .rcvIntegrityError: return true
case .rcvDecryptionError: return true
case .rcvGroupInvitation: return true
case .rcvModerated: return true
case .invalidJSON: return true
default: return false
}
}
}
public enum MsgDecryptError: String, Decodable {
case ratchetHeader
case tooManySkipped
case ratchetEarlier
case other
var text: String {
switch self {
case .ratchetHeader: return NSLocalizedString("Permanent decryption error", comment: "message decrypt error item")
case .tooManySkipped: return NSLocalizedString("Permanent decryption error", comment: "message decrypt error item")
case .ratchetEarlier: return NSLocalizedString("Decryption error", comment: "message decrypt error item")
case .other: return NSLocalizedString("Decryption error", comment: "message decrypt error item")
}
}
}
public struct CIQuote: Decodable, ItemContent {
public var chatDir: CIDirection?
var chatDir: CIDirection?
public var itemId: Int64?
var sharedMsgId: String? = nil
public var sentAt: Date
var sentAt: Date
public var content: MsgContent
public var formattedText: [FormattedText]?
@@ -2581,7 +2484,7 @@ public struct CIQuote: Decodable, ItemContent {
switch (chatDir) {
case .directSnd: return "you"
case .directRcv: return nil
case .groupSnd: return membership?.displayName ?? "you"
case .groupSnd: return membership?.displayName
case let .groupRcv(member): return member.displayName
case nil: return nil
}
@@ -2694,7 +2597,6 @@ public struct CIFile: Decodable {
case .rcvCancelled: return false
case .rcvComplete: return true
case .rcvError: return false
case .invalid: return false
}
}
}
@@ -2718,7 +2620,6 @@ public struct CIFile: Decodable {
case .rcvCancelled: return nil
case .rcvComplete: return nil
case .rcvError: return nil
case .invalid: return nil
}
}
}
@@ -2779,7 +2680,6 @@ public enum CIFileStatus: Decodable, Equatable {
case rcvComplete
case rcvCancelled
case rcvError
case invalid(text: String)
var id: String {
switch self {
@@ -2794,12 +2694,11 @@ public enum CIFileStatus: Decodable, Equatable {
case .rcvComplete: return "rcvComplete"
case .rcvCancelled: return "rcvCancelled"
case .rcvError: return "rcvError"
case .invalid: return "invalid"
}
}
}
public enum MsgContent: Equatable {
public enum MsgContent {
case text(String)
case link(text: String, preview: LinkPreview)
case image(text: String, image: String)
@@ -2860,19 +2759,6 @@ public enum MsgContent: Equatable {
case image
case duration
}
public static func == (lhs: MsgContent, rhs: MsgContent) -> Bool {
switch (lhs, rhs) {
case let (.text(lt), .text(rt)): return lt == rt
case let (.link(lt, lp), .link(rt, rp)): return lt == rt && lp == rp
case let (.image(lt, li), .image(rt, ri)): return lt == rt && li == ri
case let (.video(lt, li, ld), .video(rt, ri, rd)): return lt == rt && li == ri && ld == rd
case let (.voice(lt, ld), .voice(rt, rd)): return lt == rt && ld == rd
case let (.file(lf), .file(rf)): return lf == rf
case let (.unknown(lType, lt), .unknown(rType, rt)): return lType == rType && lt == rt
default: return false
}
}
}
extension MsgContent: Decodable {
@@ -3171,8 +3057,6 @@ public enum SndGroupEvent: Decodable {
public enum RcvConnEvent: Decodable {
case switchQueue(phase: SwitchPhase)
case ratchetSync(syncStatus: RatchetSyncState)
case verificationCodeReset
var text: String {
switch self {
@@ -3180,51 +3064,25 @@ public enum RcvConnEvent: Decodable {
if case .completed = phase {
return NSLocalizedString("changed address for you", comment: "chat item text")
}
return NSLocalizedString("changing address", comment: "chat item text")
case let .ratchetSync(syncStatus):
return ratchetSyncStatusToText(syncStatus)
case .verificationCodeReset:
return NSLocalizedString("security code changed", comment: "chat item text")
return NSLocalizedString("changing address...", comment: "chat item text")
}
}
}
func ratchetSyncStatusToText(_ ratchetSyncStatus: RatchetSyncState) -> String {
switch ratchetSyncStatus {
case .ok: return NSLocalizedString("encryption ok", comment: "chat item text")
case .allowed: return NSLocalizedString("encryption re-negotiation allowed", comment: "chat item text")
case .required: return NSLocalizedString("encryption re-negotiation required", comment: "chat item text")
case .started: return NSLocalizedString("agreeing encryption…", comment: "chat item text")
case .agreed: return NSLocalizedString("encryption agreed", comment: "chat item text")
}
}
public enum SndConnEvent: Decodable {
case switchQueue(phase: SwitchPhase, member: GroupMemberRef?)
case ratchetSync(syncStatus: RatchetSyncState, member: GroupMemberRef?)
var text: String {
switch self {
case let .switchQueue(phase, member):
if let name = member?.profile.profileViewName {
return phase == .completed
? String.localizedStringWithFormat(NSLocalizedString("you changed address for %@", comment: "chat item text"), name)
: String.localizedStringWithFormat(NSLocalizedString("changing address for %@", comment: "chat item text"), name)
? String.localizedStringWithFormat(NSLocalizedString("you changed address for %@", comment: "chat item text"), name)
: String.localizedStringWithFormat(NSLocalizedString("changing address for %@...", comment: "chat item text"), name)
}
return phase == .completed
? NSLocalizedString("you changed address", comment: "chat item text")
: NSLocalizedString("changing address", comment: "chat item text")
case let .ratchetSync(syncStatus, member):
if let name = member?.profile.profileViewName {
switch syncStatus {
case .ok: return String.localizedStringWithFormat(NSLocalizedString("encryption ok for %@", comment: "chat item text"), name)
case .allowed: return String.localizedStringWithFormat(NSLocalizedString("encryption re-negotiation allowed for %@", comment: "chat item text"), name)
case .required: return String.localizedStringWithFormat(NSLocalizedString("encryption re-negotiation required for %@", comment: "chat item text"), name)
case .started: return String.localizedStringWithFormat(NSLocalizedString("agreeing encryption for %@…", comment: "chat item text"), name)
case .agreed: return String.localizedStringWithFormat(NSLocalizedString("encryption agreed for %@", comment: "chat item text"), name)
}
}
return ratchetSyncStatusToText(syncStatus)
? NSLocalizedString("you changed address", comment: "chat item text")
: NSLocalizedString("changing address...", comment: "chat item text")
}
}
}
@@ -3288,7 +3146,6 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable {
public struct ChatItemInfo: Decodable {
public var itemVersions: [ChatItemVersion]
public var memberDeliveryStatuses: [MemberDeliveryStatus]?
}
public struct ChatItemVersion: Decodable {
@@ -3298,8 +3155,3 @@ public struct ChatItemVersion: Decodable {
public var itemVersionTs: Date
public var createdAt: Date
}
public struct MemberDeliveryStatus: Decodable {
public var groupMemberId: Int64
public var memberDeliveryStatus: CIStatus
}

Some files were not shown because too many files have changed in this diff Show More