diff --git a/.gitignore b/.gitignore
index e3ea5d267b..645b55ec9d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,6 +61,7 @@ website/package/generated*
# Ignore build tool output, e.g. code coverage
website/.nyc_output/
website/coverage/
+result
# Ignore API documentation
website/api-docs/
diff --git a/README.md b/README.md
index e62fb8c8b3..c2df084477 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@ You must:
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
-You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
+You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform:
@@ -83,7 +83,7 @@ There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=s
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
-[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
+[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmfiivxDKWFuowXrQOp11jsY8TuP__rBL%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAiz3pKNwvKudckFYMUfgoT0s96B0jfZ7ALHAu7rtE9HQ%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22jZeJpXGrRXQJU_-MSJ_v2A%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FJ5ES83pJimY2BRklS8fvy_iQwIU37xra%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0F0STP6UqN_12_k2cjjTrIjFgBGeWhOAmbY1qlk3pnM%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22VmUU0fqmYdCRmVCyvStvHA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FxCHBE_6PBRMqNEpm4UQDHXb9cz-mN7dd%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAetqlcM7zTCRw-iatnwCrvpJSto7lq5Yv6AsBMWv7GSM%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22foO5Xw4hhjOa_x7zET7otw%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FVXQTB0J2lLjYkgjWByhl6-1qmb5fgZHh%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAI6JaEWezfSwvcoTEkk6au-gkjrXR2ew2OqZYMYBvayk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22ORH9OEe8Duissh-hslfeVg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FqpHu0psOUdYfc11yQCzSyq5JhijrBzZT%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEACZ_7fbwlM45wl6cGif8cY47oPQ_AMdP0ATqOYLA6zHY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%229uRQRTir3ealdcSfB0zsrw%3D%3D%22%7D) (Italian-speaking).
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/Contents.json b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/Contents.json
new file mode 100644
index 0000000000..8e38b499dd
--- /dev/null
+++ b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "symbols": [
+ {
+ "filename": "checkmark.2.svg",
+ "idiom": "universal"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/checkmark.2.svg b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/checkmark.2.svg
new file mode 100644
index 0000000000..577fa1db76
--- /dev/null
+++ b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/checkmark.2.svg
@@ -0,0 +1,227 @@
+
+
\ No newline at end of file
diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/Contents.json b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/Contents.json
new file mode 100644
index 0000000000..11a91cb811
--- /dev/null
+++ b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "symbols": [
+ {
+ "filename": "checkmark.wide.svg",
+ "idiom": "universal"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/checkmark.wide.svg b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/checkmark.wide.svg
new file mode 100644
index 0000000000..b5dfc6b3de
--- /dev/null
+++ b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/checkmark.wide.svg
@@ -0,0 +1,218 @@
+
+
\ No newline at end of file
diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift
index 1f41e7c31d..6d600f33ff 100644
--- a/apps/ios/Shared/ContentView.swift
+++ b/apps/ios/Shared/ContentView.swift
@@ -29,12 +29,12 @@ struct ContentView: View {
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
- @State private var showSettings = false
@State private var showWhatsNew = false
@State private var showChooseLAMode = false
@State private var showSetPasscode = false
@State private var waitingForOrPassedAuth = true
@State private var chatListActionSheet: ChatListActionSheet? = nil
+ @State private var chatListUserPickerSheet: UserPickerSheet? = nil
private let callTopPadding: CGFloat = 40
@@ -86,7 +86,7 @@ struct ContentView: View {
callView(call)
}
- if !showSettings, let la = chatModel.laRequest {
+ if chatListUserPickerSheet == nil, let la = chatModel.laRequest {
LocalAuthView(authRequest: la)
.onDisappear {
// this flag is separate from accessAuthenticated to show initializationView while we wait for authentication
@@ -109,9 +109,6 @@ struct ContentView: View {
}
}
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
- .sheet(isPresented: $showSettings) {
- SettingsView(showSettings: $showSettings)
- }
.confirmationDialog("SimpleX Lock mode", isPresented: $showChooseLAMode, titleVisibility: .visible) {
Button("System authentication") { initialEnableLA() }
Button("Passcode entry") { showSetPasscode = true }
@@ -253,7 +250,7 @@ struct ContentView: View {
private func mainView() -> some View {
ZStack(alignment: .top) {
- ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
+ ChatListView(activeUserPickerSheet: $chatListUserPickerSheet).privacySensitive(protectScreen)
.onAppear {
requestNtfAuthorization()
// Local Authentication notice is to be shown on next start after onboarding is complete
@@ -300,9 +297,18 @@ struct ContentView: View {
if let contactId = contacts?.first?.personHandle?.value,
let chat = chatModel.getChat(contactId),
case let .direct(contact) = chat.chatInfo {
- logger.debug("callToRecentContact: schedule call")
- DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
- CallController.shared.startCall(contact, mediaType)
+ let activeCall = chatModel.activeCall
+ // This line works when a user clicks on a video button in CallKit UI while in call.
+ // The app tries to make another call to the same contact and overwite activeCall instance making its state broken
+ if let activeCall, contactId == activeCall.contact.id, mediaType == .video, !activeCall.hasVideo {
+ Task {
+ await chatModel.callCommand.processCommand(.media(source: .camera, enable: true))
+ }
+ } else if activeCall == nil {
+ logger.debug("callToRecentContact: schedule call")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+ CallController.shared.startCall(contact, mediaType)
+ }
}
}
}
diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift
index caa54887f7..8525a141e3 100644
--- a/apps/ios/Shared/Model/ChatModel.swift
+++ b/apps/ios/Shared/Model/ChatModel.swift
@@ -61,7 +61,7 @@ class ItemsModel: ObservableObject {
init() {
publisher
- .throttle(for: 0.25, scheduler: DispatchQueue.main, latest: true)
+ .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
.sink { self.objectWillChange.send() }
.store(in: &bag)
}
@@ -123,6 +123,14 @@ class NetworkModel: ObservableObject {
}
}
+/// ChatItemWithMenu can depend on previous or next item for it's appearance
+/// This dummy model is used to force an update of all chat items,
+/// when they might have changed appearance.
+class ChatItemDummyModel: ObservableObject {
+ static let shared = ChatItemDummyModel()
+ func sendUpdate() { objectWillChange.send() }
+}
+
final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage?
@Published var setDeliveryReceipts = false
@@ -427,19 +435,17 @@ final class ChatModel: ObservableObject {
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
if let i = getChatItemIndex(cItem) {
- withConditionalAnimation {
- _updateChatItem(at: i, with: cItem)
- }
+ _updateChatItem(at: i, with: cItem)
+ ChatItemDummyModel.shared.sendUpdate()
return false
} else {
- withConditionalAnimation(itemAnimation()) {
- var ci = cItem
- if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
- ci.meta.itemStatus = status
- }
- im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
- im.itemAdded = true
+ var ci = cItem
+ if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
+ ci.meta.itemStatus = status
}
+ im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
+ im.itemAdded = true
+ ChatItemDummyModel.shared.sendUpdate()
return true
}
@@ -520,7 +526,7 @@ final class ChatModel: ObservableObject {
}
func updateCurrentUserUiThemes(uiThemes: ThemeModeOverrides?) {
- guard var current = currentUser else { return }
+ guard var current = currentUser, current.uiThemes != uiThemes else { return }
current.uiThemes = uiThemes
let i = users.firstIndex(where: { $0.user.userId == current.userId })
if let i {
@@ -556,6 +562,7 @@ final class ChatModel: ObservableObject {
// update preview
_updateChat(cInfo.id) { chat in
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
+ self.updateFloatingButtons(unreadCount: 0)
chat.chatStats = ChatStats()
}
// update current chat
@@ -572,6 +579,12 @@ final class ChatModel: ObservableObject {
}
}
+ private func updateFloatingButtons(unreadCount: Int) {
+ let fbm = ChatView.FloatingButtonModel.shared
+ fbm.totalUnread = unreadCount
+ fbm.objectWillChange.send()
+ }
+
func markChatItemsRead(_ cInfo: ChatInfo, aboveItem: ChatItem? = nil) {
if let cItem = aboveItem {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
@@ -590,6 +603,7 @@ final class ChatModel: ObservableObject {
if markedCount > 0 {
chat.chatStats.unreadCount -= markedCount
self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount)
+ self.updateFloatingButtons(unreadCount: chat.chatStats.unreadCount)
}
}
}
@@ -619,19 +633,15 @@ final class ChatModel: ObservableObject {
}
}
- func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
- if chatId == cInfo.id,
- let itemIndex = getChatItemIndex(cItem),
- im.reversedChatItems[itemIndex].isRcvNew {
- await MainActor.run {
- withTransaction(Transaction()) {
- // update current chat
- markChatItemRead_(itemIndex)
- // update preview
- unreadCollector.changeUnreadCounter(cInfo.id, by: -1)
+ func markChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) {
+ if self.chatId == cInfo.id {
+ for itemId in itemIds {
+ if let i = im.reversedChatItems.firstIndex(where: { $0.id == itemId }) {
+ markChatItemRead_(i)
}
}
}
+ self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count)
}
private let unreadCollector = UnreadCollector()
@@ -657,9 +667,10 @@ final class ChatModel: ObservableObject {
}
func changeUnreadCounter(_ chatId: ChatId, by count: Int) {
- DispatchQueue.main.async {
- self.unreadCounts[chatId] = (self.unreadCounts[chatId] ?? 0) + count
+ if chatId == ChatModel.shared.chatId {
+ ChatView.FloatingButtonModel.shared.totalUnread += count
}
+ self.unreadCounts[chatId] = (self.unreadCounts[chatId] ?? 0) + count
subject.send()
}
}
@@ -881,35 +892,6 @@ final class ChatModel: ObservableObject {
_ = upsertGroupMember(groupInfo, updatedMember)
}
}
-
- func unreadChatItemCounts(itemsInView: Set) -> UnreadChatItemCounts {
- var i = 0
- var totalBelow = 0
- var unreadBelow = 0
- while i < im.reversedChatItems.count - 1 && !itemsInView.contains(im.reversedChatItems[i].viewId) {
- totalBelow += 1
- if im.reversedChatItems[i].isRcvNew {
- unreadBelow += 1
- }
- i += 1
- }
- return UnreadChatItemCounts(
- // TODO these thresholds account for the fact that items are still "visible" while
- // covered by compose area, they should be replaced with the actual height in pixels below the screen.
- isNearBottom: totalBelow < 15,
- isReallyNearBottom: totalBelow < 2,
- unreadBelow: unreadBelow
- )
- }
-
- func topItemInView(itemsInView: Set) -> ChatItem? {
- let maxIx = im.reversedChatItems.count - 1
- var i = 0
- let inView = { itemsInView.contains(self.im.reversedChatItems[$0].viewId) }
- while i < maxIx && !inView(i) { i += 1 }
- while i < maxIx && inView(i) { i += 1 }
- return im.reversedChatItems[min(i - 1, maxIx)]
- }
}
struct ShowingInvitation {
@@ -922,12 +904,6 @@ struct NTFContactRequest {
var chatId: String
}
-struct UnreadChatItemCounts: Equatable {
- var isNearBottom: Bool
- var isReallyNearBottom: Bool
- var unreadBelow: Int
-}
-
final class Chat: ObservableObject, Identifiable, ChatLike {
@Published var chatInfo: ChatInfo
@Published var chatItems: [ChatItem]
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 6c5d5504e6..a0a32156c4 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -357,17 +357,23 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
throw r
}
-func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) async -> ChatItem? {
- let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId, ttl: ttl)
+func apiPlanForwardChatItems(type: ChatType, id: Int64, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) {
+ let r = await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds))
+ if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) }
+ throw r
+}
+
+func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? {
+ let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemIds: itemIds, ttl: ttl)
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
}
-func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
- let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
+func apiSendMessages(type: ChatType, id: Int64, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
+ let cmd: ChatCommand = .apiSendMessages(type: type, id: id, live: live, ttl: ttl, composedMessages: composedMessages)
return await processSendMessageCmd(toChatType: type, cmd: cmd)
}
-private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> ChatItem? {
+private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> [ChatItem]? {
let chatModel = ChatModel.shared
let r: ChatResponse
if toChatType == .direct {
@@ -380,10 +386,13 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async
}
})
r = await chatSendCmd(cmd, bgTask: false)
- if case let .newChatItem(_, aChatItem) = r {
- cItem = aChatItem.chatItem
- chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
- return cItem
+ if case let .newChatItems(_, aChatItems) = r {
+ let cItems = aChatItems.map { $0.chatItem }
+ if let cItemLast = cItems.last {
+ cItem = cItemLast
+ chatModel.messageDelivery[cItemLast.id] = endTask
+ }
+ return cItems
}
if let networkErrorAlert = networkErrorAlert(r) {
AlertManager.shared.showAlert(networkErrorAlert)
@@ -394,18 +403,18 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async
return nil
} else {
r = await chatSendCmd(cmd, bgDelay: msgDelay)
- if case let .newChatItem(_, aChatItem) = r {
- return aChatItem.chatItem
+ if case let .newChatItems(_, aChatItems) = r {
+ return aChatItems.map { $0.chatItem }
}
sendMessageErrorAlert(r)
return nil
}
}
-func apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) async -> ChatItem? {
- let r = await chatSendCmd(.apiCreateChatItem(noteFolderId: noteFolderId, file: file, msg: msg))
- if case let .newChatItem(_, aChatItem) = r { return aChatItem.chatItem }
- createChatItemErrorAlert(r)
+func apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
+ let r = await chatSendCmd(.apiCreateChatItems(noteFolderId: noteFolderId, composedMessages: composedMessages))
+ if case let .newChatItems(_, aChatItems) = r { return aChatItems.map { $0.chatItem } }
+ createChatItemsErrorAlert(r)
return nil
}
@@ -417,8 +426,8 @@ private func sendMessageErrorAlert(_ r: ChatResponse) {
)
}
-private func createChatItemErrorAlert(_ r: ChatResponse) {
- logger.error("apiCreateChatItem error: \(String(describing: r))")
+private func createChatItemsErrorAlert(_ r: ChatResponse) {
+ logger.error("apiCreateChatItems error: \(String(describing: r))")
AlertManager.shared.showAlertMsg(
title: "Error creating message",
message: "Error: \(responseError(r))"
@@ -453,7 +462,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String
let r = chatSendCmdSync(.apiGetNtfToken)
switch r {
case let .ntfToken(token, status, ntfMode, ntfServer): return (token, status, ntfMode, ntfServer)
- case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off, nil)
+ case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED, _))): return (nil, nil, .off, nil)
default:
logger.debug("apiGetNtfToken response: \(String(describing: r))")
return (nil, nil, .off, nil)
@@ -576,12 +585,18 @@ func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profi
throw r
}
-func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) {
+func apiGroupMemberInfoSync(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) {
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) }
throw r
}
+func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupMember, ConnectionStats?) {
+ let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
+ if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) }
+ throw r
+}
+
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId))
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
@@ -636,8 +651,8 @@ func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
throw r
}
-func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, String) {
- let r = chatSendCmdSync(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
+func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupMember, String) {
+ let r = await chatSendCmd(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberCode(_, _, member, connectionCode) = r { return (member, connectionCode) }
throw r
}
@@ -673,6 +688,13 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
throw r
}
+func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection? {
+ let r = await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId))
+
+ if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection}
+ throw r
+}
+
func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
let userId = try currentUserId("apiConnectPlan")
let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq))
@@ -990,6 +1012,10 @@ func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async thr
try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange))
}
+func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws {
+ try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, itemIds: itemIds))
+}
+
func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
}
@@ -1025,77 +1051,122 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF
}
func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async {
- if let chatItem = await apiReceiveFile(
- fileId: fileId,
- userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
- encrypted: privacyEncryptLocalFilesGroupDefault.get(),
+ await receiveFiles(
+ user: user,
+ fileIds: [fileId],
+ userApprovedRelays: userApprovedRelays,
auto: auto
- ) {
- await chatItemSimpleUpdate(user, chatItem)
- }
+ )
}
-func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
- let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, 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."
+func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool = false, auto: Bool = false) async {
+ var fileIdsToApprove = [Int64]()
+ var srvsToApprove = Set()
+ var otherFileErrs = [ChatResponse]()
+
+ for fileId in fileIds {
+ let r = await chatSendCmd(
+ .receiveFile(
+ fileId: fileId,
+ userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
+ encrypted: privacyEncryptLocalFilesGroupDefault.get(),
+ inline: nil
)
+ )
+ switch r {
+ case let .rcvFileAccepted(_, chatItem):
+ await chatItemSimpleUpdate(user, chatItem)
+ default:
+ if let chatError = chatError(r) {
+ switch chatError {
+ case let .fileNotApproved(fileId, unknownServers):
+ fileIdsToApprove.append(fileId)
+ srvsToApprove.formUnion(unknownServers)
+ default:
+ otherFileErrs.append(r)
+ }
+ }
}
- } else if let networkErrorAlert = networkErrorAlert(r) {
- logger.error("apiReceiveFile network error: \(String(describing: r))")
- if !auto {
- am.showAlert(networkErrorAlert)
+ }
+
+ if !auto {
+ let otherErrsStr = if otherFileErrs.isEmpty {
+ ""
+ } else if otherFileErrs.count == 1 {
+ "\(otherFileErrs[0])"
+ } else if otherFileErrs.count == 2 {
+ "\(otherFileErrs[0])\n\(otherFileErrs[1])"
+ } else {
+ "\(otherFileErrs[0])\n\(otherFileErrs[1])\nand \(otherFileErrs.count - 2) other error(s)"
}
- } else {
- switch chatError(r) {
- case .fileCancelled:
- logger.debug("apiReceiveFile ignoring fileCancelled error")
- case .fileAlreadyReceiving:
- logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
- case let .fileNotApproved(fileId, unknownServers):
- logger.debug("apiReceiveFile fileNotApproved error")
- if !auto {
- let srvs = unknownServers.map { s in
+
+ // If there are not approved files, alert is shown the same way both in case of singular and plural files reception
+ if !fileIdsToApprove.isEmpty {
+ let srvs = srvsToApprove
+ .map { s in
if let srv = parseServerAddress(s), !srv.hostnames.isEmpty {
srv.hostnames[0]
} else {
serverHost(s)
}
}
- am.showAlert(Alert(
- title: Text("Unknown servers!"),
- message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."),
- primaryButton: .default(
- Text("Download"),
- action: {
- Task {
- logger.debug("apiReceiveFile fileNotApproved alert - in Task")
- if let user = ChatModel.shared.currentUser {
- await receiveFile(user: user, fileId: fileId, userApprovedRelays: true)
- }
+ .sorted()
+ .joined(separator: ", ")
+ let fIds = fileIdsToApprove
+ await MainActor.run {
+ showAlert(
+ title: NSLocalizedString("Unknown servers!", comment: "alert title"),
+ message: (
+ String.localizedStringWithFormat(NSLocalizedString("Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.", comment: "alert message"), srvs) +
+ (otherErrsStr != "" ? "\n\n" + String.localizedStringWithFormat(NSLocalizedString("Other file errors:\n%@", comment: "alert message"), otherErrsStr) : "")
+ ),
+ buttonTitle: NSLocalizedString("Download", comment: "alert button"),
+ buttonAction: {
+ Task {
+ logger.debug("apiReceiveFile fileNotApproved alert - in Task")
+ if let user = ChatModel.shared.currentUser {
+ await receiveFiles(user: user, fileIds: fIds, userApprovedRelays: true)
}
}
- ),
- secondaryButton: .cancel()
- ))
+ },
+ cancelButton: true
+ )
}
- default:
- logger.error("apiReceiveFile error: \(String(describing: r))")
- if !auto {
- am.showAlertMsg(
- title: "Error receiving file",
- message: "Error: \(responseError(r))"
+ } else if otherFileErrs.count == 1 { // If there is a single other error, we differentiate on it
+ let errorResponse = otherFileErrs.first!
+ switch errorResponse {
+ case let .rcvFileAcceptedSndCancelled(_, rcvFileTransfer):
+ logger.debug("receiveFiles error: sender cancelled file transfer \(rcvFileTransfer.fileId)")
+ await MainActor.run {
+ showAlert(
+ NSLocalizedString("Cannot receive file", comment: "alert title"),
+ message: NSLocalizedString("Sender cancelled file transfer.", comment: "alert message")
+ )
+ }
+ default:
+ if let chatError = chatError(errorResponse) {
+ switch chatError {
+ case .fileCancelled, .fileAlreadyReceiving:
+ logger.debug("receiveFiles ignoring FileCancelled or FileAlreadyReceiving error")
+ default:
+ await MainActor.run {
+ showAlert(
+ NSLocalizedString("Error receiving file", comment: "alert title"),
+ message: responseError(errorResponse)
+ )
+ }
+ }
+ }
+ }
+ } else if otherFileErrs.count > 1 { // If there are multiple other errors, we show general alert
+ await MainActor.run {
+ showAlert(
+ NSLocalizedString("Error receiving file", comment: "alert title"),
+ message: String.localizedStringWithFormat(NSLocalizedString("File errors:\n%@", comment: "alert message"), otherErrsStr)
)
}
}
}
- return nil
}
func cancelFile(user: User, fileId: Int64) async {
@@ -1277,11 +1348,23 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
do {
- logger.debug("apiMarkChatItemRead: \(cItem.id)")
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
- await ChatModel.shared.markChatItemRead(cInfo, cItem)
+ DispatchQueue.main.async {
+ ChatModel.shared.markChatItemsRead(cInfo, [cItem.id])
+ }
} catch {
- logger.error("apiMarkChatItemRead apiChatRead error: \(responseError(error))")
+ logger.error("apiChatRead error: \(responseError(error))")
+ }
+}
+
+func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) async {
+ do {
+ try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds)
+ DispatchQueue.main.async {
+ ChatModel.shared.markChatItemsRead(cInfo, itemIds)
+ }
+ } catch {
+ logger.error("apiChatItemsRead error: \(responseError(error))")
}
}
@@ -1775,36 +1858,45 @@ func processReceivedMsg(_ res: ChatResponse) async {
n.networkStatuses = ns
}
}
- case let .newChatItem(user, aChatItem):
- let cInfo = aChatItem.chatInfo
- let cItem = aChatItem.chatItem
- await MainActor.run {
- if active(user) {
- m.addChatItem(cInfo, cItem)
- } else if cItem.isRcvNew && cInfo.ntfsEnabled {
- m.increaseUnreadCounter(user: user)
+ case let .newChatItems(user, chatItems):
+ for chatItem in chatItems {
+ let cInfo = chatItem.chatInfo
+ let cItem = chatItem.chatItem
+ await MainActor.run {
+ 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)
}
}
- if let file = cItem.autoReceiveFile() {
- Task {
- await receiveFile(user: user, fileId: file.fileId, auto: true)
+ case let .chatItemsStatusesUpdated(user, chatItems):
+ for chatItem in chatItems {
+ let cInfo = chatItem.chatInfo
+ let cItem = chatItem.chatItem
+ if !cItem.isDeletedContent && active(user) {
+ await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
}
- }
- 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) {
- await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
- }
- if let endTask = m.messageDelivery[cItem.id] {
- switch cItem.meta.itemStatus {
- case .sndSent: endTask()
- case .sndErrorAuth: endTask()
- case .sndError: endTask()
- default: ()
+ if let endTask = m.messageDelivery[cItem.id] {
+ switch cItem.meta.itemStatus {
+ case .sndNew: ()
+ case .sndSent: endTask()
+ case .sndRcvd: endTask()
+ case .sndErrorAuth: endTask()
+ case .sndError: endTask()
+ case .sndWarning: endTask()
+ case .rcvNew: ()
+ case .rcvRead: ()
+ case .invalid: ()
+ }
}
}
case let .chatItemUpdated(user, aChatItem):
@@ -1998,7 +2090,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
await withCall(contact) { call in
await MainActor.run {
call.callState = .offerReceived
- call.peerMedia = callType.media
call.sharedKey = sharedKey
}
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
diff --git a/apps/ios/Shared/Theme/Theme.swift b/apps/ios/Shared/Theme/Theme.swift
index e2641eb8dd..de67390026 100644
--- a/apps/ios/Shared/Theme/Theme.swift
+++ b/apps/ios/Shared/Theme/Theme.swift
@@ -102,7 +102,7 @@ extension ThemeWallpaper {
public func importFromString() -> ThemeWallpaper {
if preset == nil, let image {
// Need to save image from string and to save its path
- if let parsed = UIImage(base64Encoded: image),
+ if let parsed = imageFromBase64(image),
let filename = saveWallpaperFile(image: parsed) {
var copy = self
copy.image = nil
@@ -122,7 +122,7 @@ extension ThemeWallpaper {
let preset: String? = if case let WallpaperType.preset(filename, _) = type { filename } else { nil }
let scale: Float? = if case let WallpaperType.preset(_, scale) = type { scale } else { if case let WallpaperType.image(_, scale, _) = type { scale } else { 1.0 } }
let scaleType: WallpaperScaleType? = if case let WallpaperType.image(_, _, scaleType) = type { scaleType } else { nil }
- let image: String? = if case WallpaperType.image = type, let image = type.uiImage { resizeImageToStrSize(image, maxDataSize: 5_000_000) } else { nil }
+ let image: String? = if case WallpaperType.image = type, let image = type.uiImage { resizeImageToStrSizeSync(image, maxDataSize: 5_000_000) } else { nil }
return ThemeWallpaper (
preset: preset,
scale: scale,
diff --git a/apps/ios/Shared/Theme/ThemeManager.swift b/apps/ios/Shared/Theme/ThemeManager.swift
index 9d648750d1..4166619d04 100644
--- a/apps/ios/Shared/Theme/ThemeManager.swift
+++ b/apps/ios/Shared/Theme/ThemeManager.swift
@@ -197,7 +197,7 @@ class ThemeManager {
var themeIds = currentThemeIdsDefault.get()
themeIds[nonSystemThemeName] = prevValue.themeId
currentThemeIdsDefault.set(themeIds)
- applyTheme(nonSystemThemeName)
+ applyTheme(currentThemeDefault.get())
}
static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding) -> Bool {
diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift
index d238c2dbae..2f76f1f046 100644
--- a/apps/ios/Shared/Views/Call/ActiveCallView.swift
+++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift
@@ -17,8 +17,8 @@ struct ActiveCallView: View {
@ObservedObject var call: Call
@Environment(\.scenePhase) var scenePhase
@State private var client: WebRTCClient? = nil
- @State private var activeCall: WebRTCClient.Call? = nil
@State private var localRendererAspectRatio: CGFloat? = nil
+ @State var remoteContentMode: UIView.ContentMode = .scaleAspectFill
@Binding var canConnectCall: Bool
@State var prevColorScheme: ColorScheme = .dark
@State var pipShown = false
@@ -27,24 +27,39 @@ struct ActiveCallView: View {
var body: some View {
ZStack(alignment: .topLeading) {
ZStack(alignment: .bottom) {
- if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
+ if let client = client, call.hasVideo {
GeometryReader { g in
let width = g.size.width * 0.3
ZStack(alignment: .topTrailing) {
- CallViewRemote(client: client, activeCall: $activeCall, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, pipShown: $pipShown)
- CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown)
- .cornerRadius(10)
- .frame(width: width, height: width / (localRendererAspectRatio ?? 1))
- .padding([.top, .trailing], 17)
ZStack(alignment: .center) {
// For some reason, when the view in GeometryReader and ZStack is visible, it steals clicks on a back button, so showing something on top like this with background color helps (.clear color doesn't work)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.primary.opacity(0.000001))
+
+ CallViewRemote(client: client, call: call, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, contentMode: $remoteContentMode, pipShown: $pipShown)
+ .onTapGesture {
+ remoteContentMode = remoteContentMode == .scaleAspectFill ? .scaleAspectFit : .scaleAspectFill
+ }
+
+ Group {
+ let localVideoTrack = client.activeCall?.localVideoTrack ?? client.notConnectedCall?.localCameraAndTrack?.1
+ if localVideoTrack != nil {
+ CallViewLocal(client: client, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown)
+ .onDisappear {
+ localRendererAspectRatio = nil
+ }
+ } else {
+ Rectangle().fill(.black)
+ }
+ }
+ .cornerRadius(10)
+ .frame(width: width, height: localRendererAspectRatio == nil ? (g.size.width < g.size.height ? width * 1.33 : width / 1.33) : width / (localRendererAspectRatio ?? 1))
+ .padding([.top, .trailing], 17)
}
}
}
- if let call = m.activeCall, let client = client, (!pipShown || !call.supportsVideo) {
+ if let call = m.activeCall, let client = client, (!pipShown || !call.hasVideo) {
ActiveCallOverlay(call: call, client: client)
}
}
@@ -54,6 +69,9 @@ struct ActiveCallView: View {
.onAppear {
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
AppDelegate.keepScreenOn(true)
+ Task {
+ await askRequiredPermissions()
+ }
createWebRTCClient()
dismissAllSheets()
hideKeyboard()
@@ -84,7 +102,7 @@ struct ActiveCallView: View {
private func createWebRTCClient() {
if client == nil && canConnectCall {
- client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
+ client = WebRTCClient({ msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
Task {
await m.callCommand.setClient(client)
}
@@ -99,7 +117,7 @@ struct ActiveCallView: View {
logger.debug("ActiveCallView: response \(msg.resp.respType)")
switch msg.resp {
case let .capabilities(capabilities):
- let callType = CallType(media: call.localMedia, capabilities: capabilities)
+ let callType = CallType(media: call.initialCallType, capabilities: capabilities)
Task {
do {
try await apiSendCallInvitation(call.contact, callType)
@@ -110,7 +128,7 @@ struct ActiveCallView: View {
call.callState = .invitationSent
call.localCapabilities = capabilities
}
- if call.supportsVideo && !AVAudioSession.sharedInstance().hasExternalAudioDevice() {
+ if call.hasVideo && !AVAudioSession.sharedInstance().hasExternalAudioDevice() {
try? AVAudioSession.sharedInstance().setCategory(.playback, options: [.allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
}
CallSoundsPlayer.shared.startConnectingCallSound()
@@ -120,7 +138,7 @@ struct ActiveCallView: View {
Task {
do {
try await apiSendCallOffer(call.contact, offer, iceCandidates,
- media: call.localMedia, capabilities: capabilities)
+ media: call.initialCallType, capabilities: capabilities)
} catch {
logger.error("apiSendCallOffer \(responseError(error))")
}
@@ -164,6 +182,9 @@ struct ActiveCallView: View {
}
if state.connectionState == "closed" {
closeCallView(client)
+ if let callUUID = m.activeCall?.callUUID {
+ CallController.shared.endCall(callUUID: callUUID)
+ }
m.activeCall = nil
m.activeCallViewIsCollapsed = false
}
@@ -182,6 +203,14 @@ struct ActiveCallView: View {
CallSoundsPlayer.shared.vibrate(long: false)
wasConnected = true
}
+ case let .peerMedia(source, enabled):
+ switch source {
+ case .mic: call.peerMediaSources.mic = enabled
+ case .camera: call.peerMediaSources.camera = enabled
+ case .screenAudio: call.peerMediaSources.screenAudio = enabled
+ case .screenVideo: call.peerMediaSources.screenVideo = enabled
+ case .unknown: ()
+ }
case .ended:
closeCallView(client)
call.callState = .ended
@@ -214,16 +243,38 @@ struct ActiveCallView: View {
ChatReceiver.shared.messagesChannel = nil
return
}
- if case let .chatItemStatusUpdated(_, msg) = msg,
- msg.chatInfo.id == call.contact.id,
- case .sndCall = msg.chatItem.content,
- case .sndRcvd = msg.chatItem.meta.itemStatus {
+ if case let .chatItemsStatusesUpdated(_, chatItems) = msg,
+ chatItems.contains(where: { ci in
+ ci.chatInfo.id == call.contact.id &&
+ ci.chatItem.content.isSndCall &&
+ ci.chatItem.meta.itemStatus.isSndRcvd
+ }) {
CallSoundsPlayer.shared.startInCallSound()
ChatReceiver.shared.messagesChannel = nil
}
}
}
+ private func askRequiredPermissions() async {
+ let mic = await WebRTCClient.isAuthorized(for: .audio)
+ await MainActor.run {
+ call.localMediaSources.mic = mic
+ }
+ let cameraAuthorized = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
+ var camera = call.initialCallType == .audio || cameraAuthorized
+ if call.initialCallType == .video && !cameraAuthorized {
+ camera = await WebRTCClient.isAuthorized(for: .video)
+ await MainActor.run {
+ if camera, let client {
+ client.setCameraEnabled(true)
+ }
+ }
+ }
+ if !mic || !camera {
+ WebRTCClient.showUnauthorizedAlert(for: !mic ? .audio : .video)
+ }
+ }
+
private func closeCallView(_ client: WebRTCClient) {
if m.activeCall != nil {
m.showCallView = false
@@ -239,44 +290,16 @@ struct ActiveCallOverlay: View {
var body: some View {
VStack {
- switch call.localMedia {
- case .video:
+ switch call.hasVideo {
+ case true:
videoCallInfoView(call)
.foregroundColor(.white)
.opacity(0.8)
- .padding()
-
- Spacer()
-
- HStack {
- toggleAudioButton()
- Spacer()
- if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) {
- toggleSpeakerButton()
- .frame(width: 40, height: 40)
- } else if call.hasMedia {
- AudioDevicePicker()
- .scaleEffect(2)
- .frame(maxWidth: 40, maxHeight: 40)
- } else {
- Color.clear.frame(width: 40, height: 40)
- }
- Spacer()
- endCallButton()
- Spacer()
- if call.videoEnabled {
- flipCameraButton()
- } else {
- Color.clear.frame(width: 40, height: 40)
- }
- Spacer()
- toggleVideoButton()
- }
- .padding(.horizontal, 20)
- .padding(.bottom, 16)
- .frame(maxWidth: .infinity, alignment: .center)
-
- case .audio:
+ .padding(.horizontal)
+ // Fixed vertical padding required for preserving position of buttons row when changing audio-to-video and back in landscape orientation.
+ // Otherwise, bigger padding is added by SwiftUI when switching call types
+ .padding(.vertical, 10)
+ case false:
ZStack(alignment: .topLeading) {
Button {
chatModel.activeCallViewIsCollapsed = true
@@ -291,35 +314,32 @@ struct ActiveCallOverlay: View {
}
.foregroundColor(.white)
.opacity(0.8)
- .padding()
+ .padding(.horizontal)
+ .padding(.vertical, 10)
.frame(maxHeight: .infinity)
}
-
- Spacer()
-
- ZStack(alignment: .bottom) {
- toggleAudioButton()
- .frame(maxWidth: .infinity, alignment: .leading)
- endCallButton()
- // Check if the only input is microphone. And in this case show toggle button,
- // If there are more inputs, it probably means something like bluetooth headphones are available
- // and in this case show iOS button for choosing different output.
- // There is no way to get available outputs, only inputs
- if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) {
- toggleSpeakerButton()
- .frame(maxWidth: .infinity, alignment: .trailing)
- } else if call.hasMedia {
- AudioDevicePicker()
- .scaleEffect(2)
- .frame(maxWidth: 50, maxHeight: 40)
- .frame(maxWidth: .infinity, alignment: .trailing)
- } else {
- Color.clear.frame(width: 50, height: 40)
- }
- }
- .padding(.bottom, 60)
- .padding(.horizontal, 48)
}
+
+ Spacer()
+
+ HStack {
+ toggleMicButton()
+ Spacer()
+ audioDeviceButton()
+ Spacer()
+ endCallButton()
+ Spacer()
+ if call.localMediaSources.camera {
+ flipCameraButton()
+ } else {
+ Color.clear.frame(width: 60, height: 60)
+ }
+ Spacer()
+ toggleCameraButton()
+ }
+ .padding(.horizontal, 20)
+ .padding(.bottom, 16)
+ .frame(maxWidth: 440, alignment: .center)
}
.frame(maxWidth: .infinity)
.onAppear {
@@ -381,36 +401,54 @@ struct ActiveCallOverlay: View {
private func endCallButton() -> some View {
let cc = CallController.shared
- return callButton("phone.down.fill", width: 60, height: 60) {
+ return callButton("phone.down.fill", .red, padding: 10) {
if let uuid = call.callUUID {
cc.endCall(callUUID: uuid)
} else {
cc.endCall(call: call) {}
}
}
- .foregroundColor(.red)
}
- private func toggleAudioButton() -> some View {
- controlButton(call, call.audioEnabled ? "mic.fill" : "mic.slash") {
+ private func toggleMicButton() -> some View {
+ controlButton(call, call.localMediaSources.mic ? "mic.fill" : "mic.slash", padding: 14) {
Task {
- client.setAudioEnabled(!call.audioEnabled)
- DispatchQueue.main.async {
- call.audioEnabled = !call.audioEnabled
- }
+ if await WebRTCClient.isAuthorized(for: .audio) {
+ client.setAudioEnabled(!call.localMediaSources.mic)
+ } else { WebRTCClient.showUnauthorizedAlert(for: .audio) }
+ }
+ }
+ }
+
+ func audioDeviceButton() -> some View {
+ // Check if the only input is microphone. And in this case show toggle button,
+ // If there are more inputs, it probably means something like bluetooth headphones are available
+ // and in this case show iOS button for choosing different output.
+ // There is no way to get available outputs, only inputs
+ Group {
+ if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) {
+ toggleSpeakerButton()
+ } else {
+ audioDevicePickerButton()
+ }
+ }
+ .onChange(of: call.localMediaSources.hasVideo) { hasVideo in
+ let current = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType
+ let speakerEnabled = current == .builtInSpeaker
+ let receiverEnabled = current == .builtInReceiver
+ // react automatically only when receiver were selected, otherwise keep an external device selected
+ if !speakerEnabled && hasVideo && receiverEnabled {
+ client.setSpeakerEnabledAndConfigureSession(!speakerEnabled, skipExternalDevice: true)
+ call.speakerEnabled = !speakerEnabled
}
}
}
private func toggleSpeakerButton() -> some View {
- controlButton(call, call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill") {
- Task {
- let speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker
- client.setSpeakerEnabledAndConfigureSession(!speakerEnabled)
- DispatchQueue.main.async {
- call.speakerEnabled = !speakerEnabled
- }
- }
+ controlButton(call, !call.peerMediaSources.mic ? "speaker.slash" : call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill", padding: !call.peerMediaSources.mic ? 16 : call.speakerEnabled ? 15 : 17) {
+ let speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker
+ client.setSpeakerEnabledAndConfigureSession(!speakerEnabled)
+ call.speakerEnabled = !speakerEnabled
}
.onAppear {
deviceManager.call = call
@@ -418,53 +456,67 @@ struct ActiveCallOverlay: View {
}
}
- private func toggleVideoButton() -> some View {
- controlButton(call, call.videoEnabled ? "video.fill" : "video.slash") {
+ private func toggleCameraButton() -> some View {
+ controlButton(call, call.localMediaSources.camera ? "video.fill" : "video.slash", padding: call.localMediaSources.camera ? 16 : 14) {
Task {
- client.setVideoEnabled(!call.videoEnabled)
- DispatchQueue.main.async {
- call.videoEnabled = !call.videoEnabled
- }
+ if await WebRTCClient.isAuthorized(for: .video) {
+ client.setCameraEnabled(!call.localMediaSources.camera)
+ } else { WebRTCClient.showUnauthorizedAlert(for: .video) }
}
}
+ .disabled(call.initialCallType == .audio && client.activeCall?.peerHasOldVersion == true)
}
@ViewBuilder private func flipCameraButton() -> some View {
- controlButton(call, "arrow.triangle.2.circlepath") {
+ controlButton(call, "arrow.triangle.2.circlepath", padding: 12) {
Task {
- client.flipCamera()
+ if await WebRTCClient.isAuthorized(for: .video) {
+ client.flipCamera()
+ }
}
}
}
- @ViewBuilder private func controlButton(_ call: Call, _ imageName: String, _ perform: @escaping () -> Void) -> some View {
- if call.hasMedia {
- callButton(imageName, width: 50, height: 38, perform)
- .foregroundColor(.white)
- .opacity(0.85)
- } else {
- Color.clear.frame(width: 50, height: 38)
- }
+ @ViewBuilder private func controlButton(_ call: Call, _ imageName: String, padding: CGFloat, _ perform: @escaping () -> Void) -> some View {
+ callButton(imageName, call.peerMediaSources.hasVideo ? Color.black.opacity(0.2) : Color.white.opacity(0.2), padding: padding, perform)
}
- private func callButton(_ imageName: String, width: CGFloat, height: CGFloat, _ perform: @escaping () -> Void) -> some View {
+ @ViewBuilder private func audioDevicePickerButton() -> some View {
+ AudioDevicePicker()
+ .opacity(0.8)
+ .scaleEffect(2)
+ .padding(10)
+ .frame(width: 60, height: 60)
+ .background(call.peerMediaSources.hasVideo ? Color.black.opacity(0.2) : Color.white.opacity(0.2))
+ .clipShape(.circle)
+ }
+
+ private func callButton(_ imageName: String, _ background: Color, padding: CGFloat, _ perform: @escaping () -> Void) -> some View {
Button {
perform()
} label: {
Image(systemName: imageName)
.resizable()
.scaledToFit()
- .frame(maxWidth: width, maxHeight: height)
+ .padding(padding)
+ .frame(width: 60, height: 60)
+ .background(background)
}
+ .foregroundColor(whiteColorWithAlpha)
+ .clipShape(.circle)
+ }
+
+ private var whiteColorWithAlpha: Color {
+ get { Color(red: 204 / 255, green: 204 / 255, blue: 204 / 255) }
}
}
struct ActiveCallOverlay_Previews: PreviewProvider {
static var previews: some View {
Group{
- ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
+ ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, initialCallType: .video), client: WebRTCClient({ _ in }, Binding.constant(nil)))
.background(.black)
- ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
+ ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, initialCallType: .audio), client: WebRTCClient({ _ in }, Binding.constant(nil)))
.background(.black)
}
}
diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift
index 36887d6184..1f28180e87 100644
--- a/apps/ios/Shared/Views/Call/CallController.swift
+++ b/apps/ios/Shared/Views/Call/CallController.swift
@@ -104,7 +104,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
- if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) {
+ if callManager.enableMedia(source: .mic, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) {
action.fulfill()
} else {
action.fail()
@@ -121,8 +121,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
RTCAudioSession.sharedInstance().isAudioEnabled = true
do {
- let supportsVideo = ChatModel.shared.activeCall?.supportsVideo == true
- if supportsVideo {
+ let hasVideo = ChatModel.shared.activeCall?.hasVideo == true
+ if hasVideo {
try audioSession.setCategory(.playAndRecord, mode: .videoChat, options: [.defaultToSpeaker, .mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
} else {
try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
@@ -133,7 +133,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
try? await Task.sleep(nanoseconds: UInt64(i) * 300_000000)
if let preferred = audioSession.preferredInputDevice() {
await MainActor.run { try? audioSession.setPreferredInput(preferred) }
- } else if supportsVideo {
+ } else if hasVideo {
await MainActor.run { try? audioSession.overrideOutputAudioPort(.speaker) }
}
}
@@ -357,11 +357,9 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
self.provider.reportCall(with: uuid, updated: update)
}
} else if callManager.startOutgoingCall(callUUID: callUUID) {
- if callManager.startOutgoingCall(callUUID: callUUID) {
- logger.debug("CallController.startCall: call started")
- } else {
- logger.error("CallController.startCall: no active call")
- }
+ logger.debug("CallController.startCall: call started")
+ } else {
+ logger.error("CallController.startCall: no active call")
}
}
diff --git a/apps/ios/Shared/Views/Call/CallManager.swift b/apps/ios/Shared/Views/Call/CallManager.swift
index f3021815af..a3e1df2301 100644
--- a/apps/ios/Shared/Views/Call/CallManager.swift
+++ b/apps/ios/Shared/Views/Call/CallManager.swift
@@ -12,7 +12,7 @@ import SimpleXChat
class CallManager {
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> String {
let uuid = UUID().uuidString.lowercased()
- let call = Call(direction: .outgoing, contact: contact, callUUID: uuid, callState: .waitCapabilities, localMedia: media)
+ let call = Call(direction: .outgoing, contact: contact, callUUID: uuid, callState: .waitCapabilities, initialCallType: media)
call.speakerEnabled = media == .video
ChatModel.shared.activeCall = call
return uuid
@@ -22,7 +22,7 @@ class CallManager {
let m = ChatModel.shared
if let call = m.activeCall, call.callUUID == callUUID {
m.showCallView = true
- Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) }
+ Task { await m.callCommand.processCommand(.capabilities(media: call.initialCallType)) }
return true
}
return false
@@ -44,7 +44,7 @@ class CallManager {
contact: invitation.contact,
callUUID: invitation.callUUID,
callState: .invitationAccepted,
- localMedia: invitation.callType.media,
+ initialCallType: invitation.callType.media,
sharedKey: invitation.sharedKey
)
call.speakerEnabled = invitation.callType.media == .video
@@ -68,10 +68,10 @@ class CallManager {
}
}
- func enableMedia(media: CallMediaType, enable: Bool, callUUID: String) -> Bool {
+ func enableMedia(source: CallMediaSource, enable: Bool, callUUID: String) -> Bool {
if let call = ChatModel.shared.activeCall, call.callUUID == callUUID {
let m = ChatModel.shared
- Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) }
+ Task { await m.callCommand.processCommand(.media(source: source, enable: enable)) }
return true
}
return false
diff --git a/apps/ios/Shared/Views/Call/CallViewRenderers.swift b/apps/ios/Shared/Views/Call/CallViewRenderers.swift
index a3201d9351..e779093a24 100644
--- a/apps/ios/Shared/Views/Call/CallViewRenderers.swift
+++ b/apps/ios/Shared/Views/Call/CallViewRenderers.swift
@@ -10,40 +10,49 @@ import AVKit
struct CallViewRemote: UIViewRepresentable {
var client: WebRTCClient
- var activeCall: Binding
+ @ObservedObject var call: Call
@State var enablePip: (Bool) -> Void = {_ in }
@Binding var activeCallViewIsCollapsed: Bool
+ @Binding var contentMode: UIView.ContentMode
@Binding var pipShown: Bool
- init(client: WebRTCClient, activeCall: Binding, activeCallViewIsCollapsed: Binding, pipShown: Binding) {
- self.client = client
- self.activeCall = activeCall
- self._activeCallViewIsCollapsed = activeCallViewIsCollapsed
- self._pipShown = pipShown
- }
-
func makeUIView(context: Context) -> UIView {
let view = UIView()
- if let call = activeCall.wrappedValue {
- let remoteRenderer = RTCMTLVideoView(frame: view.frame)
- remoteRenderer.videoContentMode = .scaleAspectFill
- client.addRemoteRenderer(call, remoteRenderer)
- addSubviewAndResize(remoteRenderer, into: view)
+ let remoteCameraRenderer = RTCMTLVideoView(frame: view.frame)
+ remoteCameraRenderer.videoContentMode = contentMode
+ remoteCameraRenderer.tag = 0
- if AVPictureInPictureController.isPictureInPictureSupported() {
- makeViewWithRTCRenderer(call, remoteRenderer, view, context)
- }
+ let screenVideo = call.peerMediaSources.screenVideo
+ let remoteScreenRenderer = RTCMTLVideoView(frame: view.frame)
+ remoteScreenRenderer.videoContentMode = contentMode
+ remoteScreenRenderer.tag = 1
+ remoteScreenRenderer.alpha = screenVideo ? 1 : 0
+
+ context.coordinator.cameraRenderer = remoteCameraRenderer
+ context.coordinator.screenRenderer = remoteScreenRenderer
+ client.addRemoteCameraRenderer(remoteCameraRenderer)
+ client.addRemoteScreenRenderer(remoteScreenRenderer)
+ if screenVideo {
+ addSubviewAndResize(remoteScreenRenderer, remoteCameraRenderer, into: view)
+ } else {
+ addSubviewAndResize(remoteCameraRenderer, remoteScreenRenderer, into: view)
+ }
+
+ if AVPictureInPictureController.isPictureInPictureSupported() {
+ makeViewWithRTCRenderer(remoteCameraRenderer, remoteScreenRenderer, view, context)
}
return view
}
- func makeViewWithRTCRenderer(_ call: WebRTCClient.Call, _ remoteRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) {
- let pipRemoteRenderer = RTCMTLVideoView(frame: view.frame)
- pipRemoteRenderer.videoContentMode = .scaleAspectFill
-
+ func makeViewWithRTCRenderer(_ remoteCameraRenderer: RTCMTLVideoView, _ remoteScreenRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) {
+ let pipRemoteCameraRenderer = RTCMTLVideoView(frame: view.frame)
+ pipRemoteCameraRenderer.videoContentMode = .scaleAspectFill
+
+ let pipRemoteScreenRenderer = RTCMTLVideoView(frame: view.frame)
+ pipRemoteScreenRenderer.videoContentMode = .scaleAspectFill
+
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
pipVideoCallViewController.preferredContentSize = CGSize(width: 1080, height: 1920)
- addSubviewAndResize(pipRemoteRenderer, into: pipVideoCallViewController.view)
let pipContentSource = AVPictureInPictureController.ContentSource(
activeVideoCallSourceView: view,
contentViewController: pipVideoCallViewController
@@ -55,7 +64,9 @@ struct CallViewRemote: UIViewRepresentable {
context.coordinator.pipController = pipController
context.coordinator.willShowHide = { show in
if show {
- client.addRemoteRenderer(call, pipRemoteRenderer)
+ client.addRemoteCameraRenderer(pipRemoteCameraRenderer)
+ client.addRemoteScreenRenderer(pipRemoteScreenRenderer)
+ context.coordinator.relayout()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
activeCallViewIsCollapsed = true
}
@@ -67,13 +78,29 @@ struct CallViewRemote: UIViewRepresentable {
}
context.coordinator.didShowHide = { show in
if show {
- remoteRenderer.isHidden = true
+ remoteCameraRenderer.isHidden = true
+ remoteScreenRenderer.isHidden = true
} else {
- client.removeRemoteRenderer(call, pipRemoteRenderer)
- remoteRenderer.isHidden = false
+ client.removeRemoteCameraRenderer(pipRemoteCameraRenderer)
+ client.removeRemoteScreenRenderer(pipRemoteScreenRenderer)
+ remoteCameraRenderer.isHidden = false
+ remoteScreenRenderer.isHidden = false
}
pipShown = show
}
+ context.coordinator.relayout = {
+ let camera = call.peerMediaSources.camera
+ let screenVideo = call.peerMediaSources.screenVideo
+ pipRemoteCameraRenderer.alpha = camera ? 1 : 0
+ pipRemoteScreenRenderer.alpha = screenVideo ? 1 : 0
+ if screenVideo {
+ addSubviewAndResize(pipRemoteScreenRenderer, pipRemoteCameraRenderer, pip: true, into: pipVideoCallViewController.view)
+ } else {
+ addSubviewAndResize(pipRemoteCameraRenderer, pipRemoteScreenRenderer, pip: true, into: pipVideoCallViewController.view)
+ }
+ (pipVideoCallViewController.view.subviews[0] as! RTCMTLVideoView).videoContentMode = contentMode
+ (pipVideoCallViewController.view.subviews[1] as! RTCMTLVideoView).videoContentMode = .scaleAspectFill
+ }
DispatchQueue.main.async {
enablePip = { enable in
if enable != pipShown /* pipController.isPictureInPictureActive */ {
@@ -88,24 +115,50 @@ struct CallViewRemote: UIViewRepresentable {
}
func makeCoordinator() -> Coordinator {
- Coordinator()
+ Coordinator(client)
}
func updateUIView(_ view: UIView, context: Context) {
logger.debug("CallView.updateUIView remote")
+ let camera = view.subviews.first(where: { $0.tag == 0 })!
+ let screen = view.subviews.first(where: { $0.tag == 1 })!
+ let screenVideo = call.peerMediaSources.screenVideo
+ if screenVideo && screen.alpha == 0 {
+ screen.alpha = 1
+ addSubviewAndResize(screen, camera, into: view)
+ } else if !screenVideo && screen.alpha == 1 {
+ screen.alpha = 0
+ addSubviewAndResize(camera, screen, into: view)
+ }
+ (view.subviews[0] as! RTCMTLVideoView).videoContentMode = contentMode
+ (view.subviews[1] as! RTCMTLVideoView).videoContentMode = .scaleAspectFill
+
+ camera.alpha = call.peerMediaSources.camera ? 1 : 0
+ screen.alpha = call.peerMediaSources.screenVideo ? 1 : 0
+
DispatchQueue.main.async {
if activeCallViewIsCollapsed != pipShown {
enablePip(activeCallViewIsCollapsed)
+ } else if pipShown {
+ context.coordinator.relayout()
}
}
}
// MARK: - Coordinator
class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
+ var cameraRenderer: RTCMTLVideoView?
+ var screenRenderer: RTCMTLVideoView?
+ var client: WebRTCClient
var pipController: AVPictureInPictureController? = nil
var willShowHide: (Bool) -> Void = { _ in }
var didShowHide: (Bool) -> Void = { _ in }
-
+ var relayout: () -> Void = {}
+
+ required init(_ client: WebRTCClient) {
+ self.client = client
+ }
+
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
willShowHide(true)
}
@@ -127,11 +180,20 @@ struct CallViewRemote: UIViewRepresentable {
}
deinit {
+ // TODO: deinit is not called when changing call type from audio to video and back,
+ // which causes many renderers can be created and added to stream (if enabling/disabling
+ // video while not yet connected in outgoing call)
pipController?.stopPictureInPicture()
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
pipController?.contentSource = nil
pipController?.delegate = nil
pipController = nil
+ if let cameraRenderer {
+ client.removeRemoteCameraRenderer(cameraRenderer)
+ }
+ if let screenRenderer {
+ client.removeRemoteScreenRenderer(screenRenderer)
+ }
}
}
@@ -148,51 +210,109 @@ struct CallViewRemote: UIViewRepresentable {
struct CallViewLocal: UIViewRepresentable {
var client: WebRTCClient
- var activeCall: Binding
var localRendererAspectRatio: Binding
@State var pipStateChanged: (Bool) -> Void = {_ in }
@Binding var pipShown: Bool
- init(client: WebRTCClient, activeCall: Binding, localRendererAspectRatio: Binding, pipShown: Binding) {
+ init(client: WebRTCClient, localRendererAspectRatio: Binding, pipShown: Binding) {
self.client = client
- self.activeCall = activeCall
self.localRendererAspectRatio = localRendererAspectRatio
self._pipShown = pipShown
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
- if let call = activeCall.wrappedValue {
- let localRenderer = RTCEAGLVideoView(frame: .zero)
- client.addLocalRenderer(call, localRenderer)
- client.startCaptureLocalVideo(call)
- addSubviewAndResize(localRenderer, into: view)
- DispatchQueue.main.async {
- pipStateChanged = { shown in
- localRenderer.isHidden = shown
- }
+ let localRenderer = RTCEAGLVideoView(frame: .zero)
+ context.coordinator.renderer = localRenderer
+ client.addLocalRenderer(localRenderer)
+ addSubviewAndResize(localRenderer, nil, into: view)
+ DispatchQueue.main.async {
+ pipStateChanged = { shown in
+ localRenderer.isHidden = shown
}
}
return view
}
+ func makeCoordinator() -> Coordinator {
+ Coordinator(client)
+ }
+
func updateUIView(_ view: UIView, context: Context) {
logger.debug("CallView.updateUIView local")
pipStateChanged(pipShown)
}
+
+ // MARK: - Coordinator
+ class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
+ var renderer: RTCEAGLVideoView?
+ var client: WebRTCClient
+
+ required init(_ client: WebRTCClient) {
+ self.client = client
+ }
+
+ deinit {
+ if let renderer {
+ client.removeLocalRenderer(renderer)
+ }
+ }
+ }
}
-private func addSubviewAndResize(_ view: UIView, into containerView: UIView) {
- containerView.addSubview(view)
- view.translatesAutoresizingMaskIntoConstraints = false
- containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|",
- options: [],
- metrics: nil,
- views: ["view": view]))
+private func addSubviewAndResize(_ fullscreen: UIView, _ end: UIView?, pip: Bool = false, into containerView: UIView) {
+ if containerView.subviews.firstIndex(of: fullscreen) == 0 && ((end == nil && containerView.subviews.count == 1) || (end != nil && containerView.subviews.firstIndex(of: end!) == 1)) {
+ // Nothing to do, elements on their places
+ return
+ }
+ containerView.removeConstraints(containerView.constraints)
+ containerView.subviews.forEach { sub in sub.removeFromSuperview()}
- containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|",
+ containerView.addSubview(fullscreen)
+ fullscreen.translatesAutoresizingMaskIntoConstraints = false
+ fullscreen.layer.cornerRadius = 0
+ fullscreen.layer.masksToBounds = false
+
+ if let end {
+ containerView.addSubview(end)
+ end.translatesAutoresizingMaskIntoConstraints = false
+ end.layer.cornerRadius = pip ? 8 : 10
+ end.layer.masksToBounds = true
+ }
+
+ let constraintFullscreenV = NSLayoutConstraint.constraints(
+ withVisualFormat: "V:|[fullscreen]|",
options: [],
metrics: nil,
- views: ["view": view]))
+ views: ["fullscreen": fullscreen]
+ )
+ let constraintFullscreenH = NSLayoutConstraint.constraints(
+ withVisualFormat: "H:|[fullscreen]|",
+ options: [],
+ metrics: nil,
+ views: ["fullscreen": fullscreen]
+ )
+
+ containerView.addConstraints(constraintFullscreenV)
+ containerView.addConstraints(constraintFullscreenH)
+
+ if let end {
+ let constraintEndWidth = NSLayoutConstraint(
+ item: end, attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: pip ? 0.5 : 0.3, constant: 0
+ )
+ let constraintEndHeight = NSLayoutConstraint(
+ item: end, attribute: .height, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: pip ? 0.5 * 1.33 : 0.3 * 1.33, constant: 0
+ )
+ let constraintEndX = NSLayoutConstraint(
+ item: end, attribute: .leading, relatedBy: .equal, toItem: containerView, attribute: .trailing, multiplier: pip ? 0.5 : 0.7, constant: pip ? -8 : -17
+ )
+ let constraintEndY = NSLayoutConstraint(
+ item: end, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: pip ? -8 : -92
+ )
+ containerView.addConstraint(constraintEndWidth)
+ containerView.addConstraint(constraintEndHeight)
+ containerView.addConstraint(constraintEndX)
+ containerView.addConstraint(constraintEndY)
+ }
containerView.layoutIfNeeded()
}
diff --git a/apps/ios/Shared/Views/Call/IncomingCallView.swift b/apps/ios/Shared/Views/Call/IncomingCallView.swift
index 4960281d72..5479a9fada 100644
--- a/apps/ios/Shared/Views/Call/IncomingCallView.swift
+++ b/apps/ios/Shared/Views/Call/IncomingCallView.swift
@@ -38,6 +38,7 @@ struct IncomingCallView: View {
}
HStack {
ProfilePreview(profileOf: invitation.contact, color: .white)
+ .padding(.vertical, 6)
Spacer()
callButton("Reject", "phone.down.fill", .red) {
diff --git a/apps/ios/Shared/Views/Call/WebRTC.swift b/apps/ios/Shared/Views/Call/WebRTC.swift
index ba990981a1..ef9135761c 100644
--- a/apps/ios/Shared/Views/Call/WebRTC.swift
+++ b/apps/ios/Shared/Views/Call/WebRTC.swift
@@ -19,14 +19,13 @@ class Call: ObservableObject, Equatable {
var direction: CallDirection
var contact: Contact
var callUUID: String?
- var localMedia: CallMediaType
+ var initialCallType: CallMediaType
+ @Published var localMediaSources: CallMediaSources
@Published var callState: CallState
@Published var localCapabilities: CallCapabilities?
- @Published var peerMedia: CallMediaType?
+ @Published var peerMediaSources: CallMediaSources = CallMediaSources()
@Published var sharedKey: String?
- @Published var audioEnabled = true
@Published var speakerEnabled = false
- @Published var videoEnabled: Bool
@Published var connectionInfo: ConnectionInfo?
@Published var connectedAt: Date? = nil
@@ -35,32 +34,33 @@ class Call: ObservableObject, Equatable {
contact: Contact,
callUUID: String?,
callState: CallState,
- localMedia: CallMediaType,
+ initialCallType: CallMediaType,
sharedKey: String? = nil
) {
self.direction = direction
self.contact = contact
self.callUUID = callUUID
self.callState = callState
- self.localMedia = localMedia
+ self.initialCallType = initialCallType
self.sharedKey = sharedKey
- self.videoEnabled = localMedia == .video
+ self.localMediaSources = CallMediaSources(
+ mic: AVCaptureDevice.authorizationStatus(for: .audio) == .authorized,
+ camera: initialCallType == .video && AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
}
var encrypted: Bool { get { localEncrypted && sharedKey != nil } }
- var localEncrypted: Bool { get { localCapabilities?.encryption ?? false } }
+ private var localEncrypted: Bool { get { localCapabilities?.encryption ?? false } }
var encryptionStatus: LocalizedStringKey {
get {
switch callState {
case .waitCapabilities: return ""
case .invitationSent: return localEncrypted ? "e2e encrypted" : "no e2e encryption"
case .invitationAccepted: return sharedKey == nil ? "contact has no e2e encryption" : "contact has e2e encryption"
- default: return !localEncrypted ? "no e2e encryption" : sharedKey == nil ? "contact has no e2e encryption" : "e2e encrypted"
+ default: return !localEncrypted ? "no e2e encryption" : sharedKey == nil ? "contact has no e2e encryption" : "e2e encrypted"
}
}
}
- var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } }
- var supportsVideo: Bool { get { peerMedia == .video || localMedia == .video } }
+ var hasVideo: Bool { get { localMediaSources.hasVideo || peerMediaSources.hasVideo } }
}
enum CallDirection {
@@ -105,18 +105,28 @@ struct WVAPIMessage: Equatable, Decodable, Encodable {
var command: WCallCommand?
}
+struct CallMediaSources: Equatable, Codable {
+ var mic: Bool = false
+ var camera: Bool = false
+ var screenAudio: Bool = false
+ var screenVideo: Bool = false
+
+ var hasVideo: Bool { get { camera || screenVideo } }
+}
+
enum WCallCommand: Equatable, Encodable, Decodable {
case capabilities(media: CallMediaType)
case start(media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
case offer(offer: String, iceCandidates: String, media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
case answer(answer: String, iceCandidates: String)
case ice(iceCandidates: String)
- case media(media: CallMediaType, enable: Bool)
+ case media(source: CallMediaSource, enable: Bool)
case end
enum CodingKeys: String, CodingKey {
case type
case media
+ case source
case aesKey
case offer
case answer
@@ -167,9 +177,9 @@ enum WCallCommand: Equatable, Encodable, Decodable {
case let .ice(iceCandidates):
try container.encode("ice", forKey: .type)
try container.encode(iceCandidates, forKey: .iceCandidates)
- case let .media(media, enable):
+ case let .media(source, enable):
try container.encode("media", forKey: .type)
- try container.encode(media, forKey: .media)
+ try container.encode(source, forKey: .media)
try container.encode(enable, forKey: .enable)
case .end:
try container.encode("end", forKey: .type)
@@ -205,9 +215,9 @@ enum WCallCommand: Equatable, Encodable, Decodable {
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
self = .ice(iceCandidates: iceCandidates)
case "media":
- let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
+ let source = try container.decode(CallMediaSource.self, forKey: CodingKeys.source)
let enable = try container.decode(Bool.self, forKey: CodingKeys.enable)
- self = .media(media: media, enable: enable)
+ self = .media(source: source, enable: enable)
case "end":
self = .end
default:
@@ -224,6 +234,7 @@ enum WCallResponse: Equatable, Decodable {
case ice(iceCandidates: String)
case connection(state: ConnectionState)
case connected(connectionInfo: ConnectionInfo)
+ case peerMedia(source: CallMediaSource, enabled: Bool)
case ended
case ok
case error(message: String)
@@ -238,6 +249,8 @@ enum WCallResponse: Equatable, Decodable {
case state
case connectionInfo
case message
+ case source
+ case enabled
}
var respType: String {
@@ -249,6 +262,7 @@ enum WCallResponse: Equatable, Decodable {
case .ice: return "ice"
case .connection: return "connection"
case .connected: return "connected"
+ case .peerMedia: return "peerMedia"
case .ended: return "ended"
case .ok: return "ok"
case .error: return "error"
@@ -283,6 +297,10 @@ enum WCallResponse: Equatable, Decodable {
case "connected":
let connectionInfo = try container.decode(ConnectionInfo.self, forKey: CodingKeys.connectionInfo)
self = .connected(connectionInfo: connectionInfo)
+ case "peerMedia":
+ let source = try container.decode(CallMediaSource.self, forKey: CodingKeys.source)
+ let enabled = try container.decode(Bool.self, forKey: CodingKeys.enabled)
+ self = .peerMedia(source: source, enabled: enabled)
case "ended":
self = .ended
case "ok":
@@ -324,6 +342,10 @@ extension WCallResponse: Encodable {
case let .connected(connectionInfo):
try container.encode("connected", forKey: .type)
try container.encode(connectionInfo, forKey: .connectionInfo)
+ case let .peerMedia(source, enabled):
+ try container.encode("peerMedia", forKey: .type)
+ try container.encode(source, forKey: .source)
+ try container.encode(enabled, forKey: .enabled)
case .ended:
try container.encode("ended", forKey: .type)
case .ok:
@@ -376,7 +398,7 @@ actor WebRTCCommandProcessor {
func shouldRunCommand(_ client: WebRTCClient, _ c: WCallCommand) -> Bool {
switch c {
case .capabilities, .start, .offer, .end: true
- default: client.activeCall.wrappedValue != nil
+ default: client.activeCall != nil
}
}
}
diff --git a/apps/ios/Shared/Views/Call/WebRTCClient.swift b/apps/ios/Shared/Views/Call/WebRTCClient.swift
index 79e0dea16c..db7910836e 100644
--- a/apps/ios/Shared/Views/Call/WebRTCClient.swift
+++ b/apps/ios/Shared/Views/Call/WebRTCClient.swift
@@ -23,15 +23,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
struct Call {
var connection: RTCPeerConnection
var iceCandidates: IceCandidates
- var localMedia: CallMediaType
var localCamera: RTCVideoCapturer?
- var localVideoSource: RTCVideoSource?
- var localStream: RTCVideoTrack?
- var remoteStream: RTCVideoTrack?
- var device: AVCaptureDevice.Position = .front
+ var localAudioTrack: RTCAudioTrack?
+ var localVideoTrack: RTCVideoTrack?
+ var remoteAudioTrack: RTCAudioTrack?
+ var remoteVideoTrack: RTCVideoTrack?
+ var remoteScreenAudioTrack: RTCAudioTrack?
+ var remoteScreenVideoTrack: RTCVideoTrack?
+ var device: AVCaptureDevice.Position
var aesKey: String?
var frameEncryptor: RTCFrameEncryptor?
var frameDecryptor: RTCFrameDecryptor?
+ var peerHasOldVersion: Bool
+ }
+
+ struct NotConnectedCall {
+ var audioTrack: RTCAudioTrack?
+ var localCameraAndTrack: (RTCVideoCapturer, RTCVideoTrack)?
+ var device: AVCaptureDevice.Position = .front
}
actor IceCandidates {
@@ -51,17 +60,20 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
private let rtcAudioSession = RTCAudioSession.sharedInstance()
private let audioQueue = DispatchQueue(label: "chat.simplex.app.audio")
private var sendCallResponse: (WVAPIMessage) async -> Void
- var activeCall: Binding
+ var activeCall: Call?
+ var notConnectedCall: NotConnectedCall?
private var localRendererAspectRatio: Binding
+ var cameraRenderers: [RTCVideoRenderer] = []
+ var screenRenderers: [RTCVideoRenderer] = []
+
@available(*, unavailable)
override init() {
fatalError("Unimplemented")
}
- required init(_ activeCall: Binding, _ sendCallResponse: @escaping (WVAPIMessage) async -> Void, _ localRendererAspectRatio: Binding) {
+ required init(_ sendCallResponse: @escaping (WVAPIMessage) async -> Void, _ localRendererAspectRatio: Binding) {
self.sendCallResponse = sendCallResponse
- self.activeCall = activeCall
self.localRendererAspectRatio = localRendererAspectRatio
rtcAudioSession.useManualAudio = CallController.useCallKit()
rtcAudioSession.isAudioEnabled = !CallController.useCallKit()
@@ -78,39 +90,45 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
connection.delegate = self
- createAudioSender(connection)
- var localStream: RTCVideoTrack? = nil
- var remoteStream: RTCVideoTrack? = nil
+ let device = notConnectedCall?.device ?? .front
var localCamera: RTCVideoCapturer? = nil
- var localVideoSource: RTCVideoSource? = nil
- if mediaType == .video {
- (localStream, remoteStream, localCamera, localVideoSource) = createVideoSender(connection)
+ var localAudioTrack: RTCAudioTrack? = nil
+ var localVideoTrack: RTCVideoTrack? = nil
+ if let localCameraAndTrack = notConnectedCall?.localCameraAndTrack {
+ (localCamera, localVideoTrack) = localCameraAndTrack
+ } else if notConnectedCall == nil && mediaType == .video {
+ (localCamera, localVideoTrack) = createVideoTrackAndStartCapture(device)
}
+ if let audioTrack = notConnectedCall?.audioTrack {
+ localAudioTrack = audioTrack
+ } else if notConnectedCall == nil {
+ localAudioTrack = createAudioTrack()
+ }
+ notConnectedCall?.localCameraAndTrack = nil
+ notConnectedCall?.audioTrack = nil
+
var frameEncryptor: RTCFrameEncryptor? = nil
var frameDecryptor: RTCFrameDecryptor? = nil
if aesKey != nil {
let encryptor = RTCFrameEncryptor.init(sizeChange: Int32(WebRTCClient.ivTagBytes))
encryptor.delegate = self
frameEncryptor = encryptor
- connection.senders.forEach { $0.setRtcFrameEncryptor(encryptor) }
let decryptor = RTCFrameDecryptor.init(sizeChange: -Int32(WebRTCClient.ivTagBytes))
decryptor.delegate = self
frameDecryptor = decryptor
- // Has no video receiver in outgoing call if applied here, see [peerConnection(_ connection: RTCPeerConnection, didChange newState]
- // connection.receivers.forEach { $0.setRtcFrameDecryptor(decryptor) }
}
return Call(
connection: connection,
iceCandidates: IceCandidates(),
- localMedia: mediaType,
localCamera: localCamera,
- localVideoSource: localVideoSource,
- localStream: localStream,
- remoteStream: remoteStream,
+ localAudioTrack: localAudioTrack,
+ localVideoTrack: localVideoTrack,
+ device: device,
aesKey: aesKey,
frameEncryptor: frameEncryptor,
- frameDecryptor: frameDecryptor
+ frameDecryptor: frameDecryptor,
+ peerHasOldVersion: false
)
}
@@ -151,18 +169,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func sendCallCommand(command: WCallCommand) async {
var resp: WCallResponse? = nil
- let pc = activeCall.wrappedValue?.connection
+ let pc = activeCall?.connection
switch command {
- case .capabilities:
+ case let .capabilities(media): // outgoing
+ let localCameraAndTrack: (RTCVideoCapturer, RTCVideoTrack)? = media == .video
+ ? createVideoTrackAndStartCapture(.front)
+ : nil
+ notConnectedCall = NotConnectedCall(audioTrack: createAudioTrack(), localCameraAndTrack: localCameraAndTrack, device: .front)
resp = .capabilities(capabilities: CallCapabilities(encryption: WebRTCClient.enableEncryption))
- case let .start(media: media, aesKey, iceServers, relay):
+ case let .start(media: media, aesKey, iceServers, relay): // incoming
logger.debug("starting incoming call - create webrtc session")
- if activeCall.wrappedValue != nil { endCall() }
+ if activeCall != nil { endCall() }
let encryption = WebRTCClient.enableEncryption
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, encryption ? aesKey : nil, relay)
- activeCall.wrappedValue = call
+ activeCall = call
+ setupLocalTracks(true, call)
let (offer, error) = await call.connection.offer()
if let offer = offer {
+ setupEncryptionForLocalTracks(call)
resp = .offer(
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: offer.type.toSdpType(), sdp: offer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates())),
@@ -172,18 +196,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
} else {
resp = .error(message: "offer error: \(error?.localizedDescription ?? "unknown error")")
}
- case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
- if activeCall.wrappedValue != nil {
+ case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay): // outgoing
+ if activeCall != nil {
resp = .error(message: "accept: call already started")
} else if !WebRTCClient.enableEncryption && aesKey != nil {
resp = .error(message: "accept: encryption is not supported")
} else if let offer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: offer)),
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) {
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
- activeCall.wrappedValue = call
+ activeCall = call
let pc = call.connection
if let type = offer.type, let sdp = offer.sdp {
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
+ setupLocalTracks(false, call)
+ setupEncryptionForLocalTracks(call)
+ pc.transceivers.forEach { transceiver in
+ transceiver.setDirection(.sendRecv, error: nil)
+ }
+ await adaptToOldVersion(pc.transceivers.count <= 2)
let (answer, error) = await pc.answer()
if let answer = answer {
self.addIceCandidates(pc, remoteIceCandidates)
@@ -200,7 +230,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
}
- case let .answer(answer, iceCandidates):
+ case let .answer(answer, iceCandidates): // incoming
if pc == nil {
resp = .error(message: "answer: call not started")
} else if pc?.localDescription == nil {
@@ -212,6 +242,9 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
let type = answer.type, let sdp = answer.sdp,
let pc = pc {
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
+ var currentDirection: RTCRtpTransceiverDirection = .sendOnly
+ pc.transceivers[2].currentDirection(¤tDirection)
+ await adaptToOldVersion(currentDirection == .sendOnly)
addIceCandidates(pc, remoteIceCandidates)
resp = .ok
} else {
@@ -226,13 +259,11 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
} else {
resp = .error(message: "ice: call not started")
}
- case let .media(media, enable):
- if activeCall.wrappedValue == nil {
+ case let .media(source, enable):
+ if activeCall == nil {
resp = .error(message: "media: call not started")
- } else if activeCall.wrappedValue?.localMedia == .audio && media == .video {
- resp = .error(message: "media: no video")
} else {
- enableMedia(media, enable)
+ await enableMedia(source, enable)
resp = .ok
}
case .end:
@@ -247,7 +278,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func getInitialIceCandidates() async -> [RTCIceCandidate] {
await untilIceComplete(timeoutMs: 750, stepMs: 150) {}
- let candidates = await activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
+ let candidates = await activeCall?.iceCandidates.getAndClear() ?? []
logger.debug("WebRTCClient: sending initial ice candidates: \(candidates.count)")
return candidates
}
@@ -255,7 +286,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func waitForMoreIceCandidates() {
Task {
await untilIceComplete(timeoutMs: 12000, stepMs: 1500) {
- let candidates = await self.activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
+ let candidates = await self.activeCall?.iceCandidates.getAndClear() ?? []
if candidates.count > 0 {
logger.debug("WebRTCClient: sending more ice candidates: \(candidates.count)")
await self.sendIceCandidates(candidates)
@@ -272,25 +303,202 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
)
}
- func enableMedia(_ media: CallMediaType, _ enable: Bool) {
- logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
- media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
+ func setupMuteUnmuteListener(_ transceiver: RTCRtpTransceiver, _ track: RTCMediaStreamTrack) {
+ // logger.log("Setting up mute/unmute listener in the call without encryption for mid = \(transceiver.mid)")
+ Task {
+ var lastBytesReceived: Int64 = 0
+ // muted initially
+ var mutedSeconds = 4
+ while let call = self.activeCall, transceiver.receiver.track?.readyState == .live {
+ let stats: RTCStatisticsReport = await call.connection.statistics(for: transceiver.receiver)
+ let stat = stats.statistics.values.first(where: { stat in stat.type == "inbound-rtp"})
+ if let stat {
+ //logger.debug("Stat \(stat.debugDescription)")
+ let bytes = stat.values["bytesReceived"] as! Int64
+ if bytes <= lastBytesReceived {
+ mutedSeconds += 1
+ if mutedSeconds == 3 {
+ await MainActor.run {
+ self.onMediaMuteUnmute(transceiver.mid, true)
+ }
+ }
+ } else {
+ if mutedSeconds >= 3 {
+ await MainActor.run {
+ self.onMediaMuteUnmute(transceiver.mid, false)
+ }
+ }
+ lastBytesReceived = bytes
+ mutedSeconds = 0
+ }
+ }
+ try? await Task.sleep(nanoseconds: 1000_000000)
+ }
+ }
}
- func addLocalRenderer(_ activeCall: Call, _ renderer: RTCEAGLVideoView) {
- activeCall.localStream?.add(renderer)
+ @MainActor
+ func onMediaMuteUnmute(_ transceiverMid: String?, _ mute: Bool) {
+ guard let activeCall = ChatModel.shared.activeCall else { return }
+ let source = mediaSourceFromTransceiverMid(transceiverMid)
+ logger.log("Mute/unmute \(source.rawValue) track = \(mute) with mid = \(transceiverMid ?? "nil")")
+ if source == .mic && activeCall.peerMediaSources.mic == mute {
+ activeCall.peerMediaSources.mic = !mute
+ } else if (source == .camera && activeCall.peerMediaSources.camera == mute) {
+ activeCall.peerMediaSources.camera = !mute
+ } else if (source == .screenAudio && activeCall.peerMediaSources.screenAudio == mute) {
+ activeCall.peerMediaSources.screenAudio = !mute
+ } else if (source == .screenVideo && activeCall.peerMediaSources.screenVideo == mute) {
+ activeCall.peerMediaSources.screenVideo = !mute
+ }
+ }
+
+ @MainActor
+ func enableMedia(_ source: CallMediaSource, _ enable: Bool) {
+ logger.debug("WebRTCClient: enabling media \(source.rawValue) \(enable)")
+ source == .camera ? setCameraEnabled(enable) : setAudioEnabled(enable)
+ }
+
+ @MainActor
+ func adaptToOldVersion(_ peerHasOldVersion: Bool) {
+ activeCall?.peerHasOldVersion = peerHasOldVersion
+ if peerHasOldVersion {
+ logger.debug("The peer has an old version. Remote audio track is nil = \(self.activeCall?.remoteAudioTrack == nil), video = \(self.activeCall?.remoteVideoTrack == nil)")
+ onMediaMuteUnmute("0", false)
+ if activeCall?.remoteVideoTrack != nil {
+ onMediaMuteUnmute("1", false)
+ }
+ if ChatModel.shared.activeCall?.localMediaSources.camera == true && ChatModel.shared.activeCall?.peerMediaSources.camera == false {
+ logger.debug("Stopping video track for the old version")
+ activeCall?.connection.senders[1].track = nil
+ ChatModel.shared.activeCall?.localMediaSources.camera = false
+ (activeCall?.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
+ activeCall?.localCamera = nil
+ activeCall?.localVideoTrack = nil
+ }
+ }
+ }
+
+ func addLocalRenderer(_ renderer: RTCEAGLVideoView) {
+ if let activeCall {
+ if let track = activeCall.localVideoTrack {
+ track.add(renderer)
+ }
+ } else if let notConnectedCall {
+ if let track = notConnectedCall.localCameraAndTrack?.1 {
+ track.add(renderer)
+ }
+ }
// To get width and height of a frame, see videoView(videoView:, didChangeVideoSize)
renderer.delegate = self
}
+ func removeLocalRenderer(_ renderer: RTCEAGLVideoView) {
+ if let activeCall {
+ if let track = activeCall.localVideoTrack {
+ track.remove(renderer)
+ }
+ } else if let notConnectedCall {
+ if let track = notConnectedCall.localCameraAndTrack?.1 {
+ track.remove(renderer)
+ }
+ }
+ renderer.delegate = nil
+ }
+
func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) {
guard size.height > 0 else { return }
localRendererAspectRatio.wrappedValue = size.width / size.height
}
+ func setupLocalTracks(_ incomingCall: Bool, _ call: Call) {
+ let pc = call.connection
+ let transceivers = call.connection.transceivers
+ let audioTrack = call.localAudioTrack
+ let videoTrack = call.localVideoTrack
+
+ if incomingCall {
+ let micCameraInit = RTCRtpTransceiverInit()
+ // streamIds required for old versions which adds tracks from stream, not from track property
+ micCameraInit.streamIds = ["micCamera"]
+
+ let screenAudioVideoInit = RTCRtpTransceiverInit()
+ screenAudioVideoInit.streamIds = ["screenAudioVideo"]
+
+ // incoming call, no transceivers yet. But they should be added in order: mic, camera, screen audio, screen video
+ // mid = 0, mic
+ if let audioTrack {
+ pc.addTransceiver(with: audioTrack, init: micCameraInit)
+ } else {
+ pc.addTransceiver(of: .audio, init: micCameraInit)
+ }
+ // mid = 1, camera
+ if let videoTrack {
+ pc.addTransceiver(with: videoTrack, init: micCameraInit)
+ } else {
+ pc.addTransceiver(of: .video, init: micCameraInit)
+ }
+ // mid = 2, screenAudio
+ pc.addTransceiver(of: .audio, init: screenAudioVideoInit)
+ // mid = 3, screenVideo
+ pc.addTransceiver(of: .video, init: screenAudioVideoInit)
+ } else {
+ // new version
+ if transceivers.count > 2 {
+ // Outgoing call. All transceivers are ready. Don't addTrack() because it will create new transceivers, replace existing (nil) tracks
+ transceivers
+ .first(where: { elem in mediaSourceFromTransceiverMid(elem.mid) == .mic })?
+ .sender.track = audioTrack
+ transceivers
+ .first(where: { elem in mediaSourceFromTransceiverMid(elem.mid) == .camera })?
+ .sender.track = videoTrack
+ } else {
+ // old version, only two transceivers
+ if let audioTrack {
+ pc.add(audioTrack, streamIds: ["micCamera"])
+ } else {
+ // it's important to have any track in order to be able to turn it on again (currently it's off)
+ let sender = pc.add(createAudioTrack(), streamIds: ["micCamera"])
+ sender?.track = nil
+ }
+ if let videoTrack {
+ pc.add(videoTrack, streamIds: ["micCamera"])
+ } else {
+ // it's important to have any track in order to be able to turn it on again (currently it's off)
+ let localVideoSource = WebRTCClient.factory.videoSource()
+ let localVideoTrack = WebRTCClient.factory.videoTrack(with: localVideoSource, trackId: "video0")
+ let sender = pc.add(localVideoTrack, streamIds: ["micCamera"])
+ sender?.track = nil
+ }
+ }
+ }
+ }
+
+ func mediaSourceFromTransceiverMid(_ mid: String?) -> CallMediaSource {
+ switch mid {
+ case "0":
+ return .mic
+ case "1":
+ return .camera
+ case "2":
+ return .screenAudio
+ case "3":
+ return .screenVideo
+ default:
+ return .unknown
+ }
+ }
+
+ // Should be called after local description set
+ func setupEncryptionForLocalTracks(_ call: Call) {
+ if let encryptor = call.frameEncryptor {
+ call.connection.senders.forEach { $0.setRtcFrameEncryptor(encryptor) }
+ }
+ }
+
func frameDecryptor(_ decryptor: RTCFrameDecryptor, mediaType: RTCRtpMediaType, withFrame encrypted: Data) -> Data? {
guard encrypted.count > 0 else { return nil }
- if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
+ if var key: [CChar] = activeCall?.aesKey?.cString(using: .utf8),
let pointer: UnsafeMutableRawPointer = malloc(encrypted.count) {
memcpy(pointer, (encrypted as NSData).bytes, encrypted.count)
let isKeyFrame = encrypted[0] & 1 == 0
@@ -304,7 +512,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func frameEncryptor(_ encryptor: RTCFrameEncryptor, mediaType: RTCRtpMediaType, withFrame unencrypted: Data) -> Data? {
guard unencrypted.count > 0 else { return nil }
- if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
+ if var key: [CChar] = activeCall?.aesKey?.cString(using: .utf8),
let pointer: UnsafeMutableRawPointer = malloc(unencrypted.count + WebRTCClient.ivTagBytes) {
memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
let isKeyFrame = unencrypted[0] & 1 == 0
@@ -327,18 +535,42 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
- func addRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
- activeCall.remoteStream?.add(renderer)
+ func addRemoteCameraRenderer(_ renderer: RTCVideoRenderer) {
+ if activeCall?.remoteVideoTrack != nil {
+ activeCall?.remoteVideoTrack?.add(renderer)
+ } else {
+ cameraRenderers.append(renderer)
+ }
}
- func removeRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
- activeCall.remoteStream?.remove(renderer)
+ func removeRemoteCameraRenderer(_ renderer: RTCVideoRenderer) {
+ if activeCall?.remoteVideoTrack != nil {
+ activeCall?.remoteVideoTrack?.remove(renderer)
+ } else {
+ cameraRenderers.removeAll(where: { $0.isEqual(renderer) })
+ }
}
- func startCaptureLocalVideo(_ activeCall: Call) {
+ func addRemoteScreenRenderer(_ renderer: RTCVideoRenderer) {
+ if activeCall?.remoteScreenVideoTrack != nil {
+ activeCall?.remoteScreenVideoTrack?.add(renderer)
+ } else {
+ screenRenderers.append(renderer)
+ }
+ }
+
+ func removeRemoteScreenRenderer(_ renderer: RTCVideoRenderer) {
+ if activeCall?.remoteScreenVideoTrack != nil {
+ activeCall?.remoteScreenVideoTrack?.remove(renderer)
+ } else {
+ screenRenderers.removeAll(where: { $0.isEqual(renderer) })
+ }
+ }
+
+ func startCaptureLocalVideo(_ device: AVCaptureDevice.Position?, _ capturer: RTCVideoCapturer?) {
#if targetEnvironment(simulator)
guard
- let capturer = activeCall.localCamera as? RTCFileVideoCapturer
+ let capturer = (activeCall?.localCamera ?? notConnectedCall?.localCameraAndTrack?.0) as? RTCFileVideoCapturer
else {
logger.error("Unable to work with a file capturer")
return
@@ -348,10 +580,10 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
capturer.startCapturing(fromFileNamed: "sounds/video.mp4")
#else
guard
- let capturer = activeCall.localCamera as? RTCCameraVideoCapturer,
- let camera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == activeCall.device })
+ let capturer = capturer as? RTCCameraVideoCapturer,
+ let camera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == device })
else {
- logger.error("Unable to find a camera")
+ logger.error("Unable to find a camera or local track")
return
}
@@ -377,19 +609,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
#endif
}
- private func createAudioSender(_ connection: RTCPeerConnection) {
- let streamId = "stream"
- let audioTrack = createAudioTrack()
- connection.add(audioTrack, streamIds: [streamId])
- }
-
- private func createVideoSender(_ connection: RTCPeerConnection) -> (RTCVideoTrack?, RTCVideoTrack?, RTCVideoCapturer?, RTCVideoSource?) {
- let streamId = "stream"
- let (localVideoTrack, localCamera, localVideoSource) = createVideoTrack()
- connection.add(localVideoTrack, streamIds: [streamId])
- return (localVideoTrack, connection.transceivers.first { $0.mediaType == .video }?.receiver.track as? RTCVideoTrack, localCamera, localVideoSource)
- }
-
private func createAudioTrack() -> RTCAudioTrack {
let audioConstrains = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
let audioSource = WebRTCClient.factory.audioSource(with: audioConstrains)
@@ -397,7 +616,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
return audioTrack
}
- private func createVideoTrack() -> (RTCVideoTrack, RTCVideoCapturer, RTCVideoSource) {
+ private func createVideoTrackAndStartCapture(_ device: AVCaptureDevice.Position) -> (RTCVideoCapturer, RTCVideoTrack) {
let localVideoSource = WebRTCClient.factory.videoSource()
#if targetEnvironment(simulator)
@@ -407,7 +626,8 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
#endif
let localVideoTrack = WebRTCClient.factory.videoTrack(with: localVideoSource, trackId: "video0")
- return (localVideoTrack, localCamera, localVideoSource)
+ startCaptureLocalVideo(device, localCamera)
+ return (localCamera, localVideoTrack)
}
func endCall() {
@@ -420,15 +640,16 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
private func _endCall() {
- guard let call = activeCall.wrappedValue else { return }
+ (notConnectedCall?.localCameraAndTrack?.0 as? RTCCameraVideoCapturer)?.stopCapture()
+ guard let call = activeCall else { return }
logger.debug("WebRTCClient: ending the call")
- activeCall.wrappedValue = nil
- (call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
call.connection.close()
call.connection.delegate = nil
call.frameEncryptor?.delegate = nil
call.frameDecryptor?.delegate = nil
+ (call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
audioSessionToDefaults()
+ activeCall = nil
}
func untilIceComplete(timeoutMs: UInt64, stepMs: UInt64, action: @escaping () async -> Void) async {
@@ -437,7 +658,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
t += stepMs
await action()
- } while t < timeoutMs && activeCall.wrappedValue?.connection.iceGatheringState != .complete
+ } while t < timeoutMs && activeCall?.connection.iceGatheringState != .complete
}
}
@@ -498,11 +719,40 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
logger.debug("Connection should negotiate")
}
+ func peerConnection(_ peerConnection: RTCPeerConnection, didStartReceivingOn transceiver: RTCRtpTransceiver) {
+ if let track = transceiver.receiver.track {
+ DispatchQueue.main.async {
+ // Doesn't work for outgoing video call (audio in video call works ok still, same as incoming call)
+// if let decryptor = self.activeCall?.frameDecryptor {
+// transceiver.receiver.setRtcFrameDecryptor(decryptor)
+// }
+ let source = self.mediaSourceFromTransceiverMid(transceiver.mid)
+ switch source {
+ case .mic: self.activeCall?.remoteAudioTrack = track as? RTCAudioTrack
+ case .camera:
+ self.activeCall?.remoteVideoTrack = track as? RTCVideoTrack
+ self.cameraRenderers.forEach({ renderer in
+ self.activeCall?.remoteVideoTrack?.add(renderer)
+ })
+ self.cameraRenderers.removeAll()
+ case .screenAudio: self.activeCall?.remoteScreenAudioTrack = track as? RTCAudioTrack
+ case .screenVideo:
+ self.activeCall?.remoteScreenVideoTrack = track as? RTCVideoTrack
+ self.screenRenderers.forEach({ renderer in
+ self.activeCall?.remoteScreenVideoTrack?.add(renderer)
+ })
+ self.screenRenderers.removeAll()
+ case .unknown: ()
+ }
+ }
+ self.setupMuteUnmuteListener(transceiver, track)
+ }
+ }
+
func peerConnection(_ connection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
debugPrint("Connection new connection state: \(newState.toString() ?? "" + newState.rawValue.description) \(connection.receivers)")
- guard let call = activeCall.wrappedValue,
- let connectionStateString = newState.toString(),
+ guard let connectionStateString = newState.toString(),
let iceConnectionStateString = connection.iceConnectionState.toString(),
let iceGatheringStateString = connection.iceGatheringState.toString(),
let signalingStateString = connection.signalingState.toString()
@@ -523,18 +773,14 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
switch newState {
case .checking:
- if let frameDecryptor = activeCall.wrappedValue?.frameDecryptor {
+ if let frameDecryptor = activeCall?.frameDecryptor {
connection.receivers.forEach { $0.setRtcFrameDecryptor(frameDecryptor) }
}
- let enableSpeaker: Bool
- switch call.localMedia {
- case .video: enableSpeaker = true
- default: enableSpeaker = false
- }
+ let enableSpeaker: Bool = ChatModel.shared.activeCall?.localMediaSources.hasVideo == true
setSpeakerEnabledAndConfigureSession(enableSpeaker)
case .connected: sendConnectedEvent(connection)
case .disconnected, .failed: endCall()
- default: do {}
+ default: ()
}
}
}
@@ -546,7 +792,7 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) {
// logger.debug("Connection generated candidate \(candidate.debugDescription)")
Task {
- await self.activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil))
+ await self.activeCall?.iceCandidates.append(candidate.toCandidate(nil, nil))
}
}
@@ -601,11 +847,42 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
}
extension WebRTCClient {
- func setAudioEnabled(_ enabled: Bool) {
- setTrackEnabled(RTCAudioTrack.self, enabled)
+ static func isAuthorized(for type: AVMediaType) async -> Bool {
+ let status = AVCaptureDevice.authorizationStatus(for: type)
+ var isAuthorized = status == .authorized
+ if status == .notDetermined {
+ isAuthorized = await AVCaptureDevice.requestAccess(for: type)
+ }
+ return isAuthorized
}
- func setSpeakerEnabledAndConfigureSession( _ enabled: Bool) {
+ static func showUnauthorizedAlert(for type: AVMediaType) {
+ if type == .audio {
+ AlertManager.shared.showAlert(Alert(
+ title: Text("No permission to record speech"),
+ message: Text("To record speech please grant permission to use Microphone."),
+ primaryButton: .default(Text("Open Settings")) {
+ DispatchQueue.main.async {
+ UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
+ }
+ },
+ secondaryButton: .cancel()
+ ))
+ } else if type == .video {
+ AlertManager.shared.showAlert(Alert(
+ title: Text("No permission to record video"),
+ message: Text("To record video please grant permission to use Camera."),
+ primaryButton: .default(Text("Open Settings")) {
+ DispatchQueue.main.async {
+ UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
+ }
+ },
+ secondaryButton: .cancel()
+ ))
+ }
+ }
+
+ func setSpeakerEnabledAndConfigureSession( _ enabled: Bool, skipExternalDevice: Bool = false) {
logger.debug("WebRTCClient: configuring session with speaker enabled \(enabled)")
audioQueue.async { [weak self] in
guard let self = self else { return }
@@ -618,7 +895,7 @@ extension WebRTCClient {
if enabled {
try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue, with: [.defaultToSpeaker, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
try self.rtcAudioSession.setMode(AVAudioSession.Mode.videoChat.rawValue)
- if hasExternalAudioDevice, let preferred = self.rtcAudioSession.session.preferredInputDevice() {
+ if hasExternalAudioDevice && !skipExternalDevice, let preferred = self.rtcAudioSession.session.preferredInputDevice() {
try self.rtcAudioSession.setPreferredInput(preferred)
} else {
try self.rtcAudioSession.overrideOutputAudioPort(.speaker)
@@ -628,7 +905,7 @@ extension WebRTCClient {
try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue)
try self.rtcAudioSession.overrideOutputAudioPort(.none)
}
- if hasExternalAudioDevice {
+ if hasExternalAudioDevice && !skipExternalDevice {
logger.debug("WebRTCClient: configuring session with external device available, skip configuring speaker")
}
try self.rtcAudioSession.setActive(true)
@@ -659,25 +936,59 @@ extension WebRTCClient {
}
}
- func setVideoEnabled(_ enabled: Bool) {
- setTrackEnabled(RTCVideoTrack.self, enabled)
+ @MainActor
+ func setAudioEnabled(_ enabled: Bool) {
+ if activeCall != nil {
+ activeCall?.localAudioTrack = enabled ? createAudioTrack() : nil
+ activeCall?.connection.transceivers.first(where: { t in mediaSourceFromTransceiverMid(t.mid) == .mic })?.sender.track = activeCall?.localAudioTrack
+ } else if notConnectedCall != nil {
+ notConnectedCall?.audioTrack = enabled ? createAudioTrack() : nil
+ }
+ ChatModel.shared.activeCall?.localMediaSources.mic = enabled
+ }
+
+ @MainActor
+ func setCameraEnabled(_ enabled: Bool) {
+ if let call = activeCall {
+ if enabled {
+ if call.localVideoTrack == nil {
+ let device = activeCall?.device ?? notConnectedCall?.device ?? .front
+ let (camera, track) = createVideoTrackAndStartCapture(device)
+ activeCall?.localCamera = camera
+ activeCall?.localVideoTrack = track
+ }
+ } else {
+ (call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
+ activeCall?.localCamera = nil
+ activeCall?.localVideoTrack = nil
+ }
+ call.connection.transceivers
+ .first(where: { t in mediaSourceFromTransceiverMid(t.mid) == .camera })?
+ .sender.track = activeCall?.localVideoTrack
+ ChatModel.shared.activeCall?.localMediaSources.camera = activeCall?.localVideoTrack != nil
+ } else if let call = notConnectedCall {
+ if enabled {
+ let device = activeCall?.device ?? notConnectedCall?.device ?? .front
+ notConnectedCall?.localCameraAndTrack = createVideoTrackAndStartCapture(device)
+ } else {
+ (call.localCameraAndTrack?.0 as? RTCCameraVideoCapturer)?.stopCapture()
+ notConnectedCall?.localCameraAndTrack = nil
+ }
+ ChatModel.shared.activeCall?.localMediaSources.camera = notConnectedCall?.localCameraAndTrack != nil
+ }
}
func flipCamera() {
- switch activeCall.wrappedValue?.device {
- case .front: activeCall.wrappedValue?.device = .back
- case .back: activeCall.wrappedValue?.device = .front
- default: ()
+ let device = activeCall?.device ?? notConnectedCall?.device
+ if activeCall != nil {
+ activeCall?.device = device == .front ? .back : .front
+ } else {
+ notConnectedCall?.device = device == .front ? .back : .front
}
- if let call = activeCall.wrappedValue {
- startCaptureLocalVideo(call)
- }
- }
-
- private func setTrackEnabled(_ type: T.Type, _ enabled: Bool) {
- activeCall.wrappedValue?.connection.transceivers
- .compactMap { $0.sender.track as? T }
- .forEach { $0.isEnabled = enabled }
+ startCaptureLocalVideo(
+ activeCall?.device ?? notConnectedCall?.device,
+ (activeCall?.localCamera ?? notConnectedCall?.localCameraAndTrack?.0) as? RTCCameraVideoCapturer
+ )
}
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift
index ef0fec5dfe..1a77b36d6f 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift
@@ -12,6 +12,7 @@ import SimpleXChat
struct CIGroupInvitationView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
+ @Environment(\.showTimestamp) var showTimestamp: Bool
@ObservedObject var chat: Chat
var chatItem: ChatItem
var groupInvitation: CIGroupInvitation
@@ -45,7 +46,7 @@ struct CIGroupInvitationView: View {
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
.font(.callout)
+ Text(" ")
- + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
+ + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
.overlay(DetermineWidth())
}
@@ -53,7 +54,7 @@ struct CIGroupInvitationView: View {
(
groupInvitationText()
+ Text(" ")
- + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
+ + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
.overlay(DetermineWidth())
}
@@ -69,7 +70,7 @@ struct CIGroupInvitationView: View {
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
- .background(chatItemFrameColor(chatItem, theme))
+ .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
.textSelection(.disabled)
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
.onChange(of: inProgress) { inProgress in
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
index 3966d7e258..b06c6df48c 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
@@ -165,9 +165,9 @@ struct CIImageView: View {
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
Image(systemName: icon)
.resizable()
+ .invertedForegroundStyle()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
- .foregroundColor(.white)
.padding(padding)
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift
index 3c864ab172..692e6bb8a6 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift
@@ -16,7 +16,7 @@ struct CILinkView: View {
var body: some View {
VStack(alignment: .center, spacing: 6) {
- if let uiImage = UIImage(base64Encoded: linkPreview.image) {
+ if let uiImage = imageFromBase64(linkPreview.image) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift
index 66b810cf2f..32249506d3 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift
@@ -12,11 +12,13 @@ import SimpleXChat
struct CIMetaView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
+ @Environment(\.showTimestamp) var showTimestamp: Bool
var chatItem: ChatItem
var metaColor: Color
var paleMetaColor = Color(UIColor.tertiaryLabel)
var showStatus = true
var showEdited = true
+ var invertedMaterial = false
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
@@ -24,93 +26,143 @@ struct CIMetaView: View {
if chatItem.isDeletedContent {
chatItem.timestampText.font(.caption).foregroundColor(metaColor)
} else {
- let meta = chatItem.meta
- let ttl = chat.chatInfo.timedMessagesTTL
- let encrypted = chatItem.encryptedFile
- switch meta.itemStatus {
- case let .sndSent(sndProgress):
- switch sndProgress {
- case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
- case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
+ ZStack {
+ ciMetaText(
+ chatItem.meta,
+ chatTTL: chat.chatInfo.timedMessagesTTL,
+ encrypted: chatItem.encryptedFile,
+ color: metaColor,
+ paleColor: paleMetaColor,
+ colorMode: invertedMaterial
+ ? .invertedMaterial
+ : .normal,
+ showStatus: showStatus,
+ showEdited: showEdited,
+ showViaProxy: showSentViaProxy,
+ showTimesamp: showTimestamp
+ ).invertedForegroundStyle(enabled: invertedMaterial)
+ if invertedMaterial {
+ ciMetaText(
+ chatItem.meta,
+ chatTTL: chat.chatInfo.timedMessagesTTL,
+ encrypted: chatItem.encryptedFile,
+ colorMode: .normal,
+ onlyOverrides: true,
+ showStatus: showStatus,
+ showEdited: showEdited,
+ showViaProxy: showSentViaProxy,
+ showTimesamp: showTimestamp
+ )
}
- case let .sndRcvd(_, sndProgress):
- switch sndProgress {
- case .complete:
- ZStack {
- ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
- ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
- }
- case .partial:
- ZStack {
- ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
- ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
- }
- }
- default:
- ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
}
}
}
}
-enum SentCheckmark {
- case sent
- case rcvd1
- case rcvd2
+enum MetaColorMode {
+ // Renders provided colours
+ case normal
+ // Fully transparent meta - used for reserving space
+ case transparent
+ // Renders white on dark backgrounds and black on light ones
+ case invertedMaterial
+
+ func resolve(_ c: Color?) -> Color? {
+ switch self {
+ case .normal: c
+ case .transparent: .clear
+ case .invertedMaterial: nil
+ }
+ }
+
+ func statusSpacer(_ sent: Bool) -> Text {
+ switch self {
+ case .normal, .transparent:
+ Text(
+ sent
+ ? Image("checkmark.wide")
+ : Image(systemName: "circlebadge.fill")
+ ).foregroundColor(.clear)
+ case .invertedMaterial: Text(" ").kerning(13)
+ }
+ }
}
func ciMetaText(
_ meta: CIMeta,
chatTTL: Int?,
encrypted: Bool?,
- color: Color = .clear,
+ color: Color = .clear, // we use this function to reserve space without rendering meta
+ paleColor: Color? = nil,
primaryColor: Color = .accentColor,
- transparent: Bool = false,
- sent: SentCheckmark? = nil,
+ colorMode: MetaColorMode = .normal,
+ onlyOverrides: Bool = false, // only render colors that differ from base
showStatus: Bool = true,
showEdited: Bool = true,
- showViaProxy: Bool
+ showViaProxy: Bool,
+ showTimesamp: Bool
) -> Text {
var r = Text("")
+ var space: Text? = nil
+ let appendSpace = {
+ if let sp = space {
+ r = r + sp
+ space = nil
+ }
+ }
+ let resolved = colorMode.resolve(color)
if showEdited, meta.itemEdited {
- r = r + statusIconText("pencil", color)
+ r = r + statusIconText("pencil", resolved)
}
if meta.disappearing {
- r = r + statusIconText("timer", color).font(.caption2)
+ r = r + statusIconText("timer", resolved).font(.caption2)
let ttl = meta.itemTimed?.ttl
if ttl != chatTTL {
- r = r + Text(shortTimeText(ttl)).foregroundColor(color)
+ r = r + colored(Text(shortTimeText(ttl)), resolved)
}
- r = r + Text(" ")
+ space = Text(" ")
}
if showViaProxy, meta.sentViaProxy == true {
- r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
+ appendSpace()
+ r = r + statusIconText("arrow.forward", resolved?.opacity(0.67)).font(.caption2)
}
if showStatus {
- if let (icon, statusColor) = meta.statusIcon(color, primaryColor) {
- 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
+ appendSpace()
+ if let (image, statusColor) = meta.itemStatus.statusIcon(color, paleColor ?? color, primaryColor) {
+ let metaColor = if onlyOverrides && statusColor == color {
+ Color.clear
+ } else {
+ colorMode.resolve(statusColor)
}
- r = r + Text(" ")
+ r = r + colored(Text(image), metaColor)
} else if !meta.disappearing {
- r = r + statusIconText("circlebadge.fill", .clear) + Text(" ")
+ r = r + colorMode.statusSpacer(meta.itemStatus.sent)
}
+ space = Text(" ")
}
if let enc = encrypted {
- r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
+ appendSpace()
+ r = r + statusIconText(enc ? "lock" : "lock.open", resolved)
+ space = Text(" ")
+ }
+ if showTimesamp {
+ appendSpace()
+ r = r + colored(meta.timestampText, resolved)
}
- r = r + meta.timestampText.foregroundColor(color)
return r.font(.caption)
}
-private func statusIconText(_ icon: String, _ color: Color) -> Text {
- Text(Image(systemName: icon)).foregroundColor(color)
+private func statusIconText(_ icon: String, _ color: Color?) -> Text {
+ colored(Text(Image(systemName: icon)), color)
+}
+
+// Applying `foregroundColor(nil)` breaks `.invertedForegroundStyle` modifier
+private func colored(_ t: Text, _ color: Color?) -> Text {
+ if let color {
+ t.foregroundColor(color)
+ } else {
+ t
+ }
}
struct CIMetaView_Previews: PreviewProvider {
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
index 1f2e16448d..693641b1d3 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
@@ -15,6 +15,7 @@ struct CIRcvDecryptionError: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
+ @Environment(\.showTimestamp) var showTimestamp: Bool
var msgDecryptError: MsgDecryptError
var msgCount: UInt32
var chatItem: ChatItem
@@ -47,7 +48,7 @@ struct CIRcvDecryptionError: View {
if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir {
do {
- let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId)
+ let (member, stats) = try apiGroupMemberInfoSync(groupInfo.apiId, groupMember.groupMemberId)
if let s = stats {
m.updateGroupMemberConnectionStats(groupInfo, member, s)
}
@@ -68,37 +69,40 @@ struct CIRcvDecryptionError: View {
}
@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) }
+ Group {
+ 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 !contactStats.ratchetSyncSupported {
- decryptionErrorItemFixButton(syncSupported: false) {
- alert = .syncNotSupportedContactAlert
+ } else if case let .group(groupInfo) = chat.chatInfo,
+ case let .groupRcv(groupMember) = chatItem.chatDir,
+ let mem = m.getGroupMember(groupMember.groupMemberId),
+ let memberStats = mem.wrapped.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()
}
- } else if case let .group(groupInfo) = chat.chatInfo,
- case let .groupRcv(groupMember) = chatItem.chatDir,
- let mem = m.getGroupMember(groupMember.groupMemberId),
- let memberStats = mem.wrapped.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()
}
+ .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
}
private func basicDecryptionErrorItem() -> some View {
@@ -122,7 +126,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
.font(.callout)
+ Text(" ")
- + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
+ + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
}
.padding(.horizontal, 12)
@@ -131,7 +135,6 @@ struct CIRcvDecryptionError: View {
}
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
- .background(Color(uiColor: .tertiarySystemGroupedBackground))
.textSelection(.disabled)
}
@@ -142,7 +145,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(.red)
.italic()
+ Text(" ")
- + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
+ + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
.padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
@@ -150,7 +153,6 @@ struct CIRcvDecryptionError: View {
}
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
- .background(Color(uiColor: .tertiarySystemGroupedBackground))
.textSelection(.disabled)
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift
index 4670fc685f..851b90bc3d 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift
@@ -292,30 +292,22 @@ struct CIVideoView: View {
.clipShape(Circle())
}
- private func durationProgress() -> some View {
- HStack {
- Text("\(durationText(videoPlaying ? progress : duration))")
- .foregroundColor(.white)
- .font(.caption)
- .padding(.vertical, 3)
- .padding(.horizontal, 6)
- .background(Color.black.opacity(0.35))
- .cornerRadius(10)
- .padding([.top, .leading], 6)
-
- if let file = chatItem.file, !videoPlaying {
- Text("\(ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary))")
- .foregroundColor(.white)
- .font(.caption)
- .padding(.vertical, 3)
- .padding(.horizontal, 6)
- .background(Color.black.opacity(0.35))
- .cornerRadius(10)
- .padding(.top, 6)
- }
+ private var fileSizeString: String {
+ if let file = chatItem.file, !videoPlaying {
+ " " + ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary)
+ } else {
+ ""
}
}
+ private func durationProgress() -> some View {
+ Text((durationText(videoPlaying ? progress : duration)) + fileSizeString)
+ .invertedForegroundStyle()
+ .font(.caption)
+ .padding(.vertical, 6)
+ .padding(.horizontal, 12)
+ }
+
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
return ZStack(alignment: .topTrailing) {
@@ -411,9 +403,9 @@ struct CIVideoView: View {
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
Image(systemName: icon)
.resizable()
+ .invertedForegroundStyle()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
- .foregroundColor(.white)
.padding(smallView ? 0 : padding)
}
@@ -428,10 +420,8 @@ struct CIVideoView: View {
private func progressCircle(_ progress: Int64, _ total: Int64) -> some View {
Circle()
.trim(from: 0, to: Double(progress) / Double(total))
- .stroke(
- Color(uiColor: .white),
- style: StrokeStyle(lineWidth: 2)
- )
+ .stroke(style: StrokeStyle(lineWidth: 2))
+ .invertedForegroundStyle()
.rotationEffect(.degrees(-90))
.frame(width: 16, height: 16)
.padding([.trailing, .top], smallView ? 0 : 11)
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
index 313ec0d419..5f2930951f 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
@@ -12,7 +12,7 @@ import SimpleXChat
struct FramedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
- @EnvironmentObject var scrollModel: ReverseListScrollModel
+ @EnvironmentObject var scrollModel: ReverseListScrollModel
@ObservedObject var chat: Chat
var chatItem: ChatItem
var preview: UIImage?
@@ -64,15 +64,20 @@ struct FramedItemView: View {
.overlay(DetermineWidth())
}
- if chatItem.content.msgContent != nil {
- CIMetaView(chat: chat, chatItem: chatItem, metaColor: useWhiteMetaColor ? Color.white : theme.colors.secondary)
- .padding(.horizontal, 12)
- .padding(.bottom, 6)
- .overlay(DetermineWidth())
- .accessibilityLabel("")
+ if let content = chatItem.content.msgContent {
+ CIMetaView(
+ chat: chat,
+ chatItem: chatItem,
+ metaColor: theme.colors.secondary,
+ invertedMaterial: useWhiteMetaColor
+ )
+ .padding(.horizontal, 12)
+ .padding(.bottom, 6)
+ .overlay(DetermineWidth())
+ .accessibilityLabel("")
}
- }
- .background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
+ }
+ .background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) }
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
if let (title, text) = chatItem.meta.itemStatus.statusInfo {
@@ -185,7 +190,7 @@ struct FramedItemView: View {
let v = ZStack(alignment: .topTrailing) {
switch (qi.content) {
case let .image(_, image):
- if let uiImage = UIImage(base64Encoded: image) {
+ if let uiImage = imageFromBase64(image) {
ciQuotedMsgView(qi)
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
Image(uiImage: uiImage)
@@ -197,7 +202,7 @@ struct FramedItemView: View {
ciQuotedMsgView(qi)
}
case let .video(_, image, _):
- if let uiImage = UIImage(base64Encoded: image) {
+ if let uiImage = imageFromBase64(image) {
ciQuotedMsgView(qi)
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
Image(uiImage: uiImage)
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift
index a80c5412b6..044ee2a26d 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift
@@ -13,7 +13,7 @@ import AVKit
struct FullScreenMediaView: View {
@EnvironmentObject var m: ChatModel
- @EnvironmentObject var scrollModel: ReverseListScrollModel
+ @EnvironmentObject var scrollModel: ReverseListScrollModel
@State var chatItem: ChatItem
@State var image: UIImage?
@State var player: AVPlayer? = nil
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift
index 822dda4d06..afeb88b05d 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift
@@ -69,7 +69,7 @@ struct CIMsgError: View {
}
.padding(.leading, 12)
.padding(.vertical, 6)
- .background(Color(uiColor: .tertiarySystemGroupedBackground))
+ .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
.textSelection(.disabled)
.onTapGesture(perform: onTap)
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift
index 25e06b9ea4..afd817357c 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift
@@ -22,7 +22,7 @@ struct MarkedDeletedItemView: View {
.foregroundColor(theme.colors.secondary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
- .background(chatItemFrameColor(chatItem, theme))
+ .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
.textSelection(.disabled)
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
index 999f99b294..63d5dc30dc 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
@@ -26,6 +26,7 @@ private func typing(_ w: Font.Weight = .light) -> Text {
struct MsgContentView: View {
@ObservedObject var chat: Chat
+ @Environment(\.showTimestamp) var showTimestamp: Bool
@EnvironmentObject var theme: AppTheme
var text: String
var formattedText: [FormattedText]? = nil
@@ -84,7 +85,7 @@ struct MsgContentView: View {
}
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
- (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
+ (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift
index 32993d1a76..587957cd5d 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift
@@ -14,12 +14,11 @@ struct ChatItemForwardingView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss
- var ci: ChatItem
+ var chatItems: [ChatItem]
var fromChatInfo: ChatInfo
@Binding var composeState: ComposeState
@State private var searchText: String = ""
- @FocusState private var searchFocused
@State private var alert: SomeAlert?
private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats)
@@ -46,8 +45,6 @@ struct ChatItemForwardingView: View {
VStack(alignment: .leading) {
if !chatsToForwardTo.isEmpty {
List {
- searchFieldView(text: $searchText, focussed: $searchFocused, theme.colors.onBackground, theme.colors.secondary)
- .padding(.leading, 2)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) }
ForEach(chats) { chat in
@@ -55,6 +52,7 @@ struct ChatItemForwardingView: View {
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
}
}
+ .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.modifier(ThemedBackground(grouped: true))
} else {
ZStack {
@@ -73,11 +71,14 @@ struct ChatItemForwardingView: View {
}
@ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View {
- let prohibited = chat.prohibitedByPref(
- hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text),
- isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false,
- isVoice: ci.content.msgContent?.isVoice ?? false
- )
+ let prohibited = chatItems.map { ci in
+ chat.prohibitedByPref(
+ hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text),
+ isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false,
+ isVoice: ci.content.msgContent?.isVoice ?? false
+ )
+ }.contains(true)
+
Button {
if prohibited {
alert = SomeAlert(
@@ -93,10 +94,10 @@ struct ChatItemForwardingView: View {
composeState = ComposeState(
message: composeState.message,
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
- contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
+ contextItem: .forwardingItems(chatItems: chatItems, fromChatInfo: fromChatInfo)
)
} else {
- composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
+ composeState = ComposeState.init(forwardingItems: chatItems, fromChatInfo: fromChatInfo)
ItemsModel.shared.loadOpenChat(chat.id)
}
}
@@ -123,7 +124,7 @@ struct ChatItemForwardingView: View {
#Preview {
ChatItemForwardingView(
- ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
+ chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")],
fromChatInfo: .direct(contact: Contact.sampleData),
composeState: Binding.constant(ComposeState(message: "hello"))
).environmentObject(CurrentColors.toAppTheme())
diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift
index f6a856dad1..62ea607d27 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift
@@ -450,20 +450,8 @@ struct ChatItemInfoView: View {
.foregroundColor(theme.colors.secondary).opacity(0.67)
}
let v = Group {
- let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary)
- switch status {
- case .rcvd:
- 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)
- }
+ let (image, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary)
+ image.foregroundColor(statusColor)
}
if let (title, text) = status.statusInfo {
diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift
index 870fe30108..bf09d15ff1 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift
@@ -9,9 +9,21 @@
import SwiftUI
import SimpleXChat
+extension EnvironmentValues {
+ struct ShowTimestamp: EnvironmentKey {
+ static let defaultValue: Bool = true
+ }
+
+ var showTimestamp: Bool {
+ get { self[ShowTimestamp.self] }
+ set { self[ShowTimestamp.self] = newValue }
+ }
+}
+
struct ChatItemView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
+ @Environment(\.showTimestamp) var showTimestamp: Bool
var chatItem: ChatItem
var maxWidth: CGFloat = .infinity
@Binding var revealed: Bool
@@ -60,7 +72,7 @@ struct ChatItemView: View {
default: nil
}
}
- .flatMap { UIImage(base64Encoded: $0) }
+ .flatMap { imageFromBase64($0) }
let adjustedMaxWidth = {
if let preview, preview.size.width <= preview.size.height {
maxWidth * 0.75
diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift
index d65fbc1ed6..b3943eb066 100644
--- a/apps/ios/Shared/Views/Chat/ChatView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatView.swift
@@ -22,8 +22,7 @@ struct ChatView: View {
@Environment(\.presentationMode) var presentationMode
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat
- @StateObject private var scrollModel = ReverseListScrollModel()
- @StateObject private var floatingButtonModel = FloatingButtonModel()
+ @StateObject private var scrollModel = ReverseListScrollModel()
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState()
@@ -43,6 +42,7 @@ struct ChatView: View {
@State private var showGroupLinkSheet: Bool = false
@State private var groupLink: String?
@State private var groupLinkMemberRole: GroupMemberRole = .member
+ @State private var forwardedChatItems: [ChatItem] = []
@State private var selectedChatItems: Set? = nil
@State private var showDeleteSelectedMessages: Bool = false
@State private var allowToDeleteSelectedMessagesForAll: Bool = false
@@ -76,7 +76,7 @@ struct ChatView: View {
VStack(spacing: 0) {
ZStack(alignment: .bottomTrailing) {
chatItemsList()
- floatingButtons(counts: floatingButtonModel.unreadChatItemCounts)
+ FloatingButtons(theme: theme, scrollModel: scrollModel, chat: chat)
}
connectingText()
if selectedChatItems == nil {
@@ -99,7 +99,8 @@ struct ChatView: View {
if case let .group(groupInfo) = chat.chatInfo {
showModerateSelectedMessagesAlert(groupInfo)
}
- }
+ },
+ forwardItems: forwardSelectedMessages
)
}
}
@@ -136,6 +137,30 @@ struct ChatView: View {
}
}
}
+ // it should be presented on top level in order to prevent a bug in SwiftUI on iOS 16 related to .focused() modifier in AddGroupMembersView's search field
+ .appSheet(isPresented: $showAddMembersSheet) {
+ Group {
+ if case let .group(groupInfo) = cInfo {
+ AddGroupMembersView(chat: chat, groupInfo: groupInfo)
+ }
+ }
+ }
+ .sheet(isPresented: Binding(
+ get: { !forwardedChatItems.isEmpty },
+ set: { isPresented in
+ if !isPresented {
+ forwardedChatItems = []
+ selectedChatItems = nil
+ }
+ }
+ )) {
+ if #available(iOS 16.0, *) {
+ ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState)
+ .presentationDetents([.fraction(0.8)])
+ } else {
+ ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState)
+ }
+ }
.onAppear {
selectedChatItems = nil
initChatView()
@@ -287,9 +312,6 @@ struct ChatView: View {
}
} else {
addMembersButton()
- .appSheet(isPresented: $showAddMembersSheet) {
- AddGroupMembersView(chat: chat, groupInfo: groupInfo)
- }
}
}
Menu {
@@ -340,6 +362,7 @@ struct ChatView: View {
await markChatUnread(chat, unreadChat: false)
}
}
+ ChatView.FloatingButtonModel.shared.totalUnread = chat.chatStats.unreadCount
}
private func searchToolbar() -> some View {
@@ -411,14 +434,9 @@ struct ChatView: View {
composeState: $composeState,
selectedMember: $selectedMember,
revealedChatItem: $revealedChatItem,
- selectedChatItems: $selectedChatItems
+ selectedChatItems: $selectedChatItems,
+ forwardedChatItems: $forwardedChatItems
)
- .onAppear {
- floatingButtonModel.appeared(viewId: ci.viewId)
- }
- .onDisappear {
- floatingButtonModel.disappeared(viewId: ci.viewId)
- }
.id(ci.id) // Required to trigger `onAppear` on iOS15
} loadPage: {
loadChatItems(cInfo)
@@ -429,13 +447,10 @@ struct ChatView: View {
.onChange(of: searchText) { _ in
Task { await loadChat(chat: chat, search: searchText) }
}
- .onChange(of: im.reversedChatItems) { _ in
- floatingButtonModel.chatItemsChanged()
- }
.onChange(of: im.itemAdded) { added in
if added {
im.itemAdded = false
- if floatingButtonModel.unreadChatItemCounts.isReallyNearBottom {
+ if FloatingButtonModel.shared.isReallyNearBottom {
scrollModel.scrollToBottom()
}
}
@@ -458,103 +473,169 @@ struct ChatView: View {
}
class FloatingButtonModel: ObservableObject {
- private enum Event {
- case appeared(String)
- case disappeared(String)
- case chatItemsChanged
- }
+ static let shared = FloatingButtonModel()
+ @Published var unreadBelow: Int = 0
+ @Published var isNearBottom: Bool = true
+ @Published var date: Date?
+ @Published var isDateVisible: Bool = false
+ var totalUnread: Int = 0
+ var isReallyNearBottom: Bool = true
+ var hideDateWorkItem: DispatchWorkItem?
- @Published var unreadChatItemCounts: UnreadChatItemCounts
-
- private let events = PassthroughSubject()
- private var bag = Set()
-
- init() {
- unreadChatItemCounts = UnreadChatItemCounts(
- isNearBottom: true,
- isReallyNearBottom: true,
- unreadBelow: 0
- )
- events
- .receive(on: DispatchQueue.global(qos: .background))
- .scan(Set()) { itemsInView, event in
- var updated = itemsInView
- switch event {
- case let .appeared(viewId): updated.insert(viewId)
- case let .disappeared(viewId): updated.remove(viewId)
- case .chatItemsChanged: ()
- }
- return updated
+ func updateOnListChange(_ listState: ListState) {
+ let im = ItemsModel.shared
+ let unreadBelow =
+ if let id = listState.bottomItemId,
+ let index = im.reversedChatItems.firstIndex(where: { $0.id == id })
+ {
+ im.reversedChatItems[.. 0 && listState.scrollOffset < 500
+ }
+
+ // set floating button indication mode
+ let nearBottom = listState.scrollOffset < 800
+ if nearBottom != self.isNearBottom {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
+ self?.isNearBottom = nearBottom
+ }
+ }
+
+ // hide Date indicator after 1 second of no scrolling
+ hideDateWorkItem?.cancel()
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let it = self else { return }
+ it.setDate(visibility: false)
+ it.hideDateWorkItem = nil
+ }
+ DispatchQueue.main.async { [weak self] in
+ self?.hideDateWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem)
+ }
}
- func appeared(viewId: String) {
- events.send(.appeared(viewId))
+ func resetDate() {
+ date = nil
+ isDateVisible = false
}
- func disappeared(viewId: String) {
- events.send(.disappeared(viewId))
+ private func setDate(visibility isVisible: Bool) {
+ if isVisible {
+ if !isNearBottom,
+ !isDateVisible,
+ let date, !Calendar.current.isDateInToday(date) {
+ withAnimation { self.isDateVisible = true }
+ }
+ } else if isDateVisible {
+ withAnimation { self.isDateVisible = false }
+ }
}
- func chatItemsChanged() {
- events.send(.chatItemsChanged)
- }
}
- private func floatingButtons(counts: UnreadChatItemCounts) -> some View {
- VStack {
- let unreadAbove = chat.chatStats.unreadCount - counts.unreadBelow
- if unreadAbove > 0 {
- circleButton {
- unreadCountText(unreadAbove)
- .font(.callout)
- .foregroundColor(theme.colors.primary)
+ private struct FloatingButtons: View {
+ let theme: AppTheme
+ let scrollModel: ReverseListScrollModel
+ let chat: Chat
+ @ObservedObject var model = FloatingButtonModel.shared
+
+ var body: some View {
+ ZStack(alignment: .top) {
+ if let date = model.date {
+ DateSeparator(date: date)
+ .padding(.vertical, 4).padding(.horizontal, 8)
+ .background(.thinMaterial)
+ .clipShape(Capsule())
+ .opacity(model.isDateVisible ? 1 : 0)
}
- .onTapGesture {
- scrollModel.scrollToNextPage()
- }
- .contextMenu {
- Button {
- Task {
- await markChatRead(chat)
+ VStack {
+ let unreadAbove = model.totalUnread - model.unreadBelow
+ if unreadAbove > 0 {
+ circleButton {
+ unreadCountText(unreadAbove)
+ .font(.callout)
+ .foregroundColor(theme.colors.primary)
}
- } label: {
- Label("Mark read", systemImage: "checkmark")
+ .onTapGesture {
+ scrollModel.scrollToNextPage()
+ }
+ .contextMenu {
+ Button {
+ Task {
+ await markChatRead(chat)
+ }
+ } label: {
+ Label("Mark read", systemImage: "checkmark")
+ }
+ }
+ }
+ Spacer()
+ if model.unreadBelow > 0 {
+ circleButton {
+ unreadCountText(model.unreadBelow)
+ .font(.callout)
+ .foregroundColor(theme.colors.primary)
+ }
+ .onTapGesture {
+ scrollModel.scrollToBottom()
+ }
+ } else if !model.isNearBottom {
+ circleButton {
+ Image(systemName: "chevron.down")
+ .foregroundColor(theme.colors.primary)
+ }
+ .onTapGesture { scrollModel.scrollToBottom() }
}
}
+ .padding()
+ .frame(maxWidth: .infinity, alignment: .trailing)
}
- Spacer()
- if counts.unreadBelow > 0 {
- circleButton {
- unreadCountText(counts.unreadBelow)
- .font(.callout)
- .foregroundColor(theme.colors.primary)
- }
- .onTapGesture {
- scrollModel.scrollToBottom()
- }
- } else if !counts.isNearBottom {
- circleButton {
- Image(systemName: "chevron.down")
- .foregroundColor(theme.colors.primary)
- }
- .onTapGesture { scrollModel.scrollToBottom() }
+ .onDisappear(perform: model.resetDate)
+ }
+
+ private func circleButton(_ content: @escaping () -> Content) -> some View {
+ ZStack {
+ Circle()
+ .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground))
+ .frame(width: 44, height: 44)
+ content()
}
}
- .padding()
}
- private func circleButton(_ content: @escaping () -> Content) -> some View {
- ZStack {
- Circle()
- .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground))
- .frame(width: 44, height: 44)
- content()
+ private struct DateSeparator: View {
+ let date: Date
+
+ var body: some View {
+ Text(String.localizedStringWithFormat(
+ NSLocalizedString("%@, %@", comment: "format for date separator in chat"),
+ date.formatted(.dateTime.weekday(.abbreviated)),
+ date.formatted(
+ Calendar.current.isDate(date, equalTo: .now, toGranularity: .year)
+ ? .dateTime.day().month(.abbreviated)
+ : .dateTime.day().month(.abbreviated).year()
+ )
+ ))
+ .font(.callout)
+ .fontWeight(.medium)
+ .foregroundStyle(.secondary)
}
}
@@ -648,6 +729,116 @@ struct ChatView: View {
}
}
+ private func forwardSelectedMessages() {
+ Task {
+ do {
+ if let selectedChatItems {
+ let (validItems, confirmation) = try await apiPlanForwardChatItems(
+ type: chat.chatInfo.chatType,
+ id: chat.chatInfo.apiId,
+ itemIds: Array(selectedChatItems)
+ )
+ if let confirmation {
+ if validItems.count > 0 {
+ showAlert(
+ String.localizedStringWithFormat(
+ NSLocalizedString("Forward %d message(s)?", comment: "alert title"),
+ validItems.count
+ ),
+ message: forwardConfirmationText(confirmation) + "\n" +
+ NSLocalizedString("Forward messages without files?", comment: "alert message")
+ ) {
+ switch confirmation {
+ case let .filesNotAccepted(fileIds):
+ [forwardAction(validItems), downloadAction(fileIds), cancelAlertAction]
+ default:
+ [forwardAction(validItems), cancelAlertAction]
+ }
+ }
+ } else {
+ showAlert(
+ NSLocalizedString("Nothing to forward!", comment: "alert title"),
+ message: forwardConfirmationText(confirmation)
+ ) {
+ switch confirmation {
+ case let .filesNotAccepted(fileIds):
+ [downloadAction(fileIds), cancelAlertAction]
+ default:
+ [okAlertAction]
+ }
+ }
+ }
+ } else {
+ await openForwardingSheet(validItems)
+ }
+ }
+ } catch {
+ logger.error("Plan forward chat items failed: \(error.localizedDescription)")
+ }
+ }
+
+ func forwardConfirmationText(_ fc: ForwardConfirmation) -> String {
+ switch fc {
+ case let .filesNotAccepted(fileIds):
+ String.localizedStringWithFormat(
+ NSLocalizedString("%d file(s) were not downloaded.", comment: "forward confirmation reason"),
+ fileIds.count
+ )
+ case let .filesInProgress(filesCount):
+ String.localizedStringWithFormat(
+ NSLocalizedString("%d file(s) are still being downloaded.", comment: "forward confirmation reason"),
+ filesCount
+ )
+ case let .filesMissing(filesCount):
+ String.localizedStringWithFormat(
+ NSLocalizedString("%d file(s) were deleted.", comment: "forward confirmation reason"),
+ filesCount
+ )
+ case let .filesFailed(filesCount):
+ String.localizedStringWithFormat(
+ NSLocalizedString("%d file(s) failed to download.", comment: "forward confirmation reason"),
+ filesCount
+ )
+ }
+ }
+
+ func forwardAction(_ items: [Int64]) -> UIAlertAction {
+ UIAlertAction(
+ title: NSLocalizedString("Forward messages", comment: "alert action"),
+ style: .default,
+ handler: { _ in Task { await openForwardingSheet(items) } }
+ )
+ }
+
+ func downloadAction(_ fileIds: [Int64]) -> UIAlertAction {
+ UIAlertAction(
+ title: NSLocalizedString("Download files", comment: "alert action"),
+ style: .default,
+ handler: { _ in
+ Task {
+ if let user = ChatModel.shared.currentUser {
+ await receiveFiles(user: user, fileIds: fileIds)
+ }
+ }
+ }
+ )
+ }
+
+ func openForwardingSheet(_ items: [Int64]) async {
+ let im = ItemsModel.shared
+ var items = Set(items)
+ var fci = [ChatItem]()
+ for reversedChatItem in im.reversedChatItems {
+ if items.contains(reversedChatItem.id) {
+ items.remove(reversedChatItem.id)
+ fci.insert(reversedChatItem, at: 0)
+ }
+ if items.isEmpty { break }
+ }
+ await MainActor.run { forwardedChatItems = fci }
+ }
+ }
+
private func loadChatItems(_ cInfo: ChatInfo) {
Task {
if loadingItems || firstPage { return }
@@ -696,6 +887,7 @@ struct ChatView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding @ObservedObject var chat: Chat
+ @ObservedObject var dummyModel: ChatItemDummyModel = .shared
let chatItem: ChatItem
let maxWidth: CGFloat
@Binding var composeState: ComposeState
@@ -708,39 +900,66 @@ struct ChatView: View {
@State private var showDeleteMessages = false
@State private var showChatItemInfoSheet: Bool = false
@State private var chatItemInfo: ChatItemInfo?
- @State private var showForwardingSheet: Bool = false
-
+ @State private var msgWidth: CGFloat = 0
+
@Binding var selectedChatItems: Set?
+ @Binding var forwardedChatItems: [ChatItem]
@State private var allowMenu: Bool = true
+ @State private var markedRead = false
var revealed: Bool { chatItem == revealedChatItem }
+ typealias ItemSeparation = (timestamp: Bool, largeGap: Bool, date: Date?)
+
+ func getItemSeparation(_ chatItem: ChatItem, at i: Int?) -> ItemSeparation {
+ let im = ItemsModel.shared
+ if let i, i > 0 && im.reversedChatItems.count >= i {
+ let nextItem = im.reversedChatItems[i - 1]
+ let largeGap = !nextItem.chatDir.sameDirection(chatItem.chatDir) || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60
+ return (
+ timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(nextItem.meta.itemTs),
+ largeGap: largeGap,
+ date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: nextItem.meta.itemTs) ? nil : nextItem.meta.itemTs
+ )
+ } else {
+ return (timestamp: true, largeGap: true, date: nil)
+ }
+ }
+
var body: some View {
- let (currIndex, _) = m.getNextChatItem(chatItem)
+ let currIndex = m.getChatItemIndex(chatItem)
let ciCategory = chatItem.mergeCategory
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
let range = itemsRange(currIndex, prevHidden)
+ let timeSeparation = getItemSeparation(chatItem, at: currIndex)
let im = ItemsModel.shared
Group {
if revealed, let range = range {
let items = Array(zip(Array(range), im.reversedChatItems[range]))
- ForEach(items.reversed(), id: \.1.viewId) { (i, ci) in
- let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
- chatItemView(ci, nil, prev)
- .overlay {
- if let selected = selectedChatItems, ci.canBeDeletedForSelf {
- Color.clear
- .contentShape(Rectangle())
- .onTapGesture {
- let checked = selected.contains(ci.id)
- selectUnselectChatItem(select: !checked, ci)
+ VStack(spacing: 0) {
+ ForEach(items.reversed(), id: \.1.viewId) { (i: Int, ci: ChatItem) in
+ let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
+ chatItemView(ci, nil, prev, getItemSeparation(ci, at: i))
+ .overlay {
+ if let selected = selectedChatItems, ci.canBeDeletedForSelf {
+ Color.clear
+ .contentShape(Rectangle())
+ .onTapGesture {
+ let checked = selected.contains(ci.id)
+ selectUnselectChatItem(select: !checked, ci)
+ }
}
- }
+ }
}
}
} else {
- chatItemView(chatItem, range, prevItem)
+ VStack(spacing: 0) {
+ chatItemView(chatItem, range, prevItem, timeSeparation)
+ if let date = timeSeparation.date {
+ DateSeparator(date: date).padding(8)
+ }
+ }
.overlay {
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
Color.clear
@@ -754,12 +973,16 @@ struct ChatView: View {
}
}
.onAppear {
+ if markedRead {
+ return
+ } else {
+ markedRead = true
+ }
if let range {
- if let items = unreadItems(range) {
+ let itemIds = unreadItemIds(range)
+ if !itemIds.isEmpty {
waitToMarkRead {
- for ci in items {
- await apiMarkChatItemRead(chat.chatInfo, ci)
- }
+ await apiMarkChatItemsRead(chat.chatInfo, itemIds)
}
}
} else if chatItem.isRcvNew {
@@ -769,29 +992,75 @@ struct ChatView: View {
}
}
}
-
- private func unreadItems(_ range: ClosedRange) -> [ChatItem]? {
+
+ private func unreadItemIds(_ range: ClosedRange) -> [ChatItem.ID] {
let im = ItemsModel.shared
- let items = range.compactMap { i in
+ return range.compactMap { i in
if i >= 0 && i < im.reversedChatItems.count {
let ci = im.reversedChatItems[i]
- return if ci.isRcvNew { ci } else { nil }
+ return if ci.isRcvNew { ci.id } else { nil }
} else {
return nil
}
}
- return if items.isEmpty { nil } else { items }
}
private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) {
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
+ Task {
+ _ = try? await Task.sleep(nanoseconds: 600_000000)
if m.chatId == chat.chatInfo.id {
- Task(operation: op)
+ await op()
}
}
}
+
- @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange?, _ prevItem: ChatItem?) -> some View {
+ @available(iOS 16.0, *)
+ struct MemberLayout: Layout {
+ let spacing: Double
+ let msgWidth: Double
+
+ private func sizes(subviews: Subviews, proposal: ProposedViewSize) -> (CGSize, CGSize) {
+ assert(subviews.count == 2, "member layout must contain exactly two subviews")
+ let roleSize = subviews[1].sizeThatFits(proposal)
+ let memberSize = subviews[0].sizeThatFits(
+ ProposedViewSize(
+ width: (proposal.width ?? msgWidth) - roleSize.width,
+ height: proposal.height
+ )
+ )
+ return (memberSize, roleSize)
+ }
+
+ func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
+ let (memberSize, roleSize) = sizes(subviews: subviews, proposal: proposal)
+ return CGSize(
+ width: min(
+ proposal.width ?? msgWidth,
+ max(msgWidth, roleSize.width + spacing + memberSize.width)
+ ),
+ height: max(memberSize.height, roleSize.height)
+ )
+ }
+
+ func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
+ let (memberSize, roleSize) = sizes(subviews: subviews, proposal: proposal)
+ subviews[0].place(
+ at: CGPoint(x: bounds.minX, y: bounds.midY - memberSize.height / 2),
+ proposal: ProposedViewSize(memberSize)
+ )
+ subviews[1].place(
+ at: CGPoint(
+ x: bounds.minX + max(memberSize.width + spacing, msgWidth - roleSize.width),
+ y: bounds.midY - roleSize.height / 2
+ ),
+ proposal: ProposedViewSize(roleSize)
+ )
+ }
+ }
+
+ @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange?, _ prevItem: ChatItem?, _ itemSeparation: ItemSeparation) -> some View {
+ let bottomPadding: Double = itemSeparation.largeGap ? 10 : 2
if case let .groupRcv(member) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
let (prevMember, memCount): (GroupMember?, Int) =
@@ -803,24 +1072,49 @@ struct ChatView: View {
if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil {
VStack(alignment: .leading, spacing: 4) {
if ci.content.showMemberName {
- let t = if memCount == 1 && member.memberRole > .member {
- Text(member.memberRole.text + " ").fontWeight(.semibold) + Text(member.displayName)
- } else {
- Text(memberNames(member, prevMember, memCount))
+ Group {
+ if memCount == 1 && member.memberRole > .member {
+ Group {
+ if #available(iOS 16.0, *) {
+ MemberLayout(spacing: 16, msgWidth: msgWidth) {
+ Text(member.chatViewName)
+ .lineLimit(1)
+ Text(member.memberRole.text)
+ .fontWeight(.semibold)
+ .lineLimit(1)
+ .padding(.trailing, 8)
+ }
+ } else {
+ HStack(spacing: 16) {
+ Text(member.chatViewName)
+ .lineLimit(1)
+ Text(member.memberRole.text)
+ .fontWeight(.semibold)
+ .lineLimit(1)
+ .layoutPriority(1)
+ }
+ }
+ }
+ .frame(
+ maxWidth: maxWidth,
+ alignment: chatItem.chatDir.sent ? .trailing : .leading
+ )
+ } else {
+ Text(memberNames(member, prevMember, memCount))
+ .lineLimit(2)
+ }
}
- t
.font(.caption)
.foregroundStyle(.secondary)
- .lineLimit(2)
.padding(.leading, memberImageSize + 14 + (selectedChatItems != nil && ci.canBeDeletedForSelf ? 12 + 24 : 0))
- .padding(.top, 7)
+ .padding(.top, 3) // this is in addition to message sequence gap
}
HStack(alignment: .center, spacing: 0) {
if selectedChatItems != nil && ci.canBeDeletedForSelf {
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
.padding(.trailing, 12)
}
- HStack(alignment: .top, spacing: 8) {
+ HStack(alignment: .top, spacing: 10) {
MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background)
.onTapGesture {
if let member = m.getGroupMember(member.groupMemberId) {
@@ -833,11 +1127,12 @@ struct ChatView: View {
}
}
}
- chatItemWithMenu(ci, range, maxWidth)
+ chatItemWithMenu(ci, range, maxWidth, itemSeparation)
+ .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
}
}
}
- .padding(.bottom, 5)
+ .padding(.bottom, bottomPadding)
.padding(.trailing)
.padding(.leading, 12)
} else {
@@ -846,11 +1141,11 @@ struct ChatView: View {
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
.padding(.leading, 12)
}
- chatItemWithMenu(ci, range, maxWidth)
+ chatItemWithMenu(ci, range, maxWidth, itemSeparation)
.padding(.trailing)
- .padding(.leading, memberImageSize + 8 + 12)
+ .padding(.leading, 10 + memberImageSize + 12)
}
- .padding(.bottom, 5)
+ .padding(.bottom, bottomPadding)
}
} else {
HStack(alignment: .center, spacing: 0) {
@@ -863,10 +1158,10 @@ struct ChatView: View {
.padding(.leading)
}
}
- chatItemWithMenu(ci, range, maxWidth)
+ chatItemWithMenu(ci, range, maxWidth, itemSeparation)
.padding(.horizontal)
}
- .padding(.bottom, 5)
+ .padding(.bottom, bottomPadding)
}
}
@@ -881,7 +1176,7 @@ struct ChatView: View {
}
}
- @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat) -> some View {
+ @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View {
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
VStack(alignment: alignment.horizontal, spacing: 3) {
ChatItemView(
@@ -891,7 +1186,8 @@ struct ChatView: View {
revealed: .constant(revealed),
allowMenu: $allowMenu
)
- .modifier(ChatItemClipped(ci))
+ .environment(\.showTimestamp, itemSeparation.timestamp)
+ .modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed)))
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
@@ -921,14 +1217,6 @@ struct ChatView: View {
}) {
ChatItemInfoView(ci: ci, chatItemInfo: $chatItemInfo)
}
- .sheet(isPresented: $showForwardingSheet) {
- if #available(iOS 16.0, *) {
- ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
- .presentationDetents([.fraction(0.8)])
- } else {
- ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
- }
- }
}
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
@@ -1069,7 +1357,7 @@ struct ChatView: View {
var forwardButton: Button {
Button {
- showForwardingSheet = true
+ forwardedChatItems = [chatItem]
} label: {
Label(
NSLocalizedString("Forward", comment: "chat item action"),
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift
index df3a8caf55..14026d79d1 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift
@@ -18,7 +18,7 @@ struct ComposeImageView: View {
var body: some View {
HStack(alignment: .center, spacing: 8) {
let imgs: [UIImage] = images.compactMap { image in
- UIImage(base64Encoded: image)
+ imageFromBase64(image)
}
if imgs.count == 0 {
ProgressView()
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift
index f7f1a89299..6c44aeea83 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift
@@ -40,7 +40,7 @@ struct ComposeLinkView: View {
private func linkPreviewView(_ linkPreview: LinkPreview) -> some View {
HStack(alignment: .center, spacing: 8) {
- if let uiImage = UIImage(base64Encoded: linkPreview.image) {
+ if let uiImage = imageFromBase64(linkPreview.image) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
index 78cae78cf5..971d088fe2 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
@@ -23,7 +23,7 @@ enum ComposeContextItem {
case noContextItem
case quotedItem(chatItem: ChatItem)
case editingItem(chatItem: ChatItem)
- case forwardingItem(chatItem: ChatItem, fromChatInfo: ChatInfo)
+ case forwardingItems(chatItems: [ChatItem], fromChatInfo: ChatInfo)
}
enum VoiceMessageRecordingState {
@@ -73,10 +73,10 @@ struct ComposeState {
}
}
- init(forwardingItem: ChatItem, fromChatInfo: ChatInfo) {
+ init(forwardingItems: [ChatItem], fromChatInfo: ChatInfo) {
self.message = ""
self.preview = .noPreview
- self.contextItem = .forwardingItem(chatItem: forwardingItem, fromChatInfo: fromChatInfo)
+ self.contextItem = .forwardingItems(chatItems: forwardingItems, fromChatInfo: fromChatInfo)
self.voiceMessageRecordingState = .noRecording
}
@@ -112,7 +112,7 @@ struct ComposeState {
var forwarding: Bool {
switch contextItem {
- case .forwardingItem: return true
+ case .forwardingItems: return true
default: return false
}
}
@@ -167,6 +167,13 @@ struct ComposeState {
}
}
+ var manyMediaPreviews: Bool {
+ switch preview {
+ case let .mediaPreviews(mediaPreviews): return mediaPreviews.count > 1
+ default: return false
+ }
+ }
+
var attachmentDisabled: Bool {
if editing || forwarding || liveMessage != nil || inProgress { return true }
switch preview {
@@ -452,7 +459,7 @@ struct ComposeView: View {
Task {
var media: [(String, UploadContent)] = []
for content in selected {
- if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
+ if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
media.append((img, content))
await MainActor.run {
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: media))
@@ -544,7 +551,7 @@ struct ComposeView: View {
}
private func addMediaContent(_ content: UploadContent) async {
- if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
+ if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
var newMedia: [(String, UploadContent?)] = []
if case var .mediaPreviews(media) = composeState.preview {
media.append((img, content))
@@ -687,7 +694,7 @@ struct ComposeView: View {
case let .quotedItem(chatItem: quotedItem):
ContextItemView(
chat: chat,
- contextItem: quotedItem,
+ contextItems: [quotedItem],
contextIcon: "arrowshape.turn.up.left",
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
)
@@ -695,18 +702,17 @@ struct ComposeView: View {
case let .editingItem(chatItem: editingItem):
ContextItemView(
chat: chat,
- contextItem: editingItem,
+ contextItems: [editingItem],
contextIcon: "pencil",
cancelContextItem: { clearState() }
)
Divider()
- case let .forwardingItem(chatItem: forwardedItem, _):
+ case let .forwardingItems(chatItems, _):
ContextItemView(
chat: chat,
- contextItem: forwardedItem,
+ contextItems: chatItems,
contextIcon: "arrowshape.turn.up.forward",
- cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) },
- showSender: false
+ cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
)
Divider()
}
@@ -730,10 +736,11 @@ struct ComposeView: View {
}
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
await sendMemberContactInvitation()
- } else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem {
- sent = await forwardItem(ci, fromChatInfo, ttl)
+ } else if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem {
+ // Composed text is send as a reply to the last forwarded item
+ sent = await forwardItems(chatItems, fromChatInfo, ttl).last
if !composeState.message.isEmpty {
- sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
+ _ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
}
} else if case let .editingItem(ci) = composeState.contextItem {
sent = await updateMessage(ci, live: live)
@@ -750,26 +757,28 @@ struct ComposeView: View {
sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl)
case .linkPreview:
sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl)
- case let .mediaPreviews(mediaPreviews: media):
+ case let .mediaPreviews(media):
let last = media.count - 1
+ var msgs: [ComposedMessage] = []
if last >= 0 {
for i in 0.. 0 {
+ // Sleep to allow `progressByTimeout` update be rendered
+ try? await Task.sleep(nanoseconds: 100_000000)
+ }
+ if let (fileSource, msgContent) = mediaContent(media[i], text: "") {
+ msgs.append(ComposedMessage(fileSource: fileSource, msgContent: msgContent))
}
- _ = try? await Task.sleep(nanoseconds: 100_000000)
}
- if case (_, .video(_, _, _)) = media[last] {
- sent = await sendVideo(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl)
- } else {
- sent = await sendImage(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl)
+ if let (fileSource, msgContent) = mediaContent(media[last], text: msgText) {
+ msgs.append(ComposedMessage(fileSource: fileSource, quotedItemId: quoted, msgContent: msgContent))
}
}
- if sent == nil {
- sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl)
+ if msgs.isEmpty {
+ msgs = [ComposedMessage(quotedItemId: quoted, msgContent: .text(msgText))]
}
+ sent = await send(msgs, live: live, ttl: ttl).last
+
case let .voicePreview(recordingFileName, duration):
stopPlayback.toggle()
let file = voiceCryptoFile(recordingFileName)
@@ -791,6 +800,20 @@ struct ComposeView: View {
}
return sent
+ func mediaContent(_ media: (String, UploadContent?), text: String) -> (CryptoFile?, MsgContent)? {
+ let (previewImage, uploadContent) = media
+ return switch uploadContent {
+ case let .simpleImage(image):
+ (saveImage(image), .image(text: text, image: previewImage))
+ case let .animatedImage(image):
+ (saveAnimImage(image), .image(text: text, image: previewImage))
+ case let .video(_, url, duration):
+ (moveTempFileFromURL(url), .video(text: text, image: previewImage, duration: duration))
+ case .none:
+ nil
+ }
+ }
+
func sending() async {
await MainActor.run { composeState.inProgress = true }
}
@@ -854,23 +877,6 @@ struct ComposeView: View {
}
}
- func sendImage(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
- let (image, data) = imageData
- if let data = data, let savedFile = saveAnyImage(data) {
- return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live, ttl: ttl)
- }
- return nil
- }
-
- func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
- let (image, data) = imageData
- if case let .video(_, url, duration) = data, let savedFile = moveTempFileFromURL(url) {
- ChatModel.shared.filesToDelete.remove(url)
- return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live, ttl: ttl)
- }
- return nil
- }
-
func voiceCryptoFile(_ fileName: String) -> CryptoFile? {
if !privacyEncryptLocalFilesGroupDefault.get() {
return CryptoFile.plain(fileName)
@@ -887,44 +893,66 @@ struct ComposeView: View {
}
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
- if let chatItem = chat.chatInfo.chatType == .local
- ? await apiCreateChatItem(noteFolderId: chat.chatInfo.apiId, file: file, msg: mc)
- : await apiSendMessage(
+ await send(
+ [ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc)],
+ live: live,
+ ttl: ttl
+ ).first
+ }
+
+ func send(_ msgs: [ComposedMessage], live: Bool, ttl: Int?) async -> [ChatItem] {
+ if let chatItems = chat.chatInfo.chatType == .local
+ ? await apiCreateChatItems(noteFolderId: chat.chatInfo.apiId, composedMessages: msgs)
+ : await apiSendMessages(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
- file: file,
- quotedItemId: quoted,
- msg: mc,
live: live,
- ttl: ttl
+ ttl: ttl,
+ composedMessages: msgs
) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
- chatModel.addChatItem(chat.chatInfo, chatItem)
+ for chatItem in chatItems {
+ chatModel.addChatItem(chat.chatInfo, chatItem)
+ }
}
- return chatItem
+ return chatItems
}
- if let file = file {
- removeFile(file.filePath)
+ for msg in msgs {
+ if let file = msg.fileSource {
+ removeFile(file.filePath)
+ }
}
- return nil
+ return []
}
- func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? {
- if let chatItem = await apiForwardChatItem(
+ func forwardItems(_ forwardedItems: [ChatItem], _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> [ChatItem] {
+ if let chatItems = await apiForwardChatItems(
toChatType: chat.chatInfo.chatType,
toChatId: chat.chatInfo.apiId,
fromChatType: fromChatInfo.chatType,
fromChatId: fromChatInfo.apiId,
- itemId: forwardedItem.id,
+ itemIds: forwardedItems.map { $0.id },
ttl: ttl
) {
await MainActor.run {
- chatModel.addChatItem(chat.chatInfo, chatItem)
+ for chatItem in chatItems {
+ chatModel.addChatItem(chat.chatInfo, chatItem)
+ }
+ if forwardedItems.count != chatItems.count {
+ showAlert(
+ String.localizedStringWithFormat(
+ NSLocalizedString("%d messages not forwarded", comment: "alert title"),
+ forwardedItems.count - chatItems.count
+ ),
+ message: NSLocalizedString("Messages were deleted after you selected them.", comment: "alert message")
+ )
+ }
}
- return chatItem
+ return chatItems
+ } else {
+ return []
}
- return nil
}
func checkLinkPreview() -> MsgContent {
@@ -941,14 +969,6 @@ struct ComposeView: View {
return .text(msgText)
}
}
-
- func saveAnyImage(_ img: UploadContent) -> CryptoFile? {
- switch img {
- case let .simpleImage(image): return saveImage(image)
- case let .animatedImage(image): return saveAnimImage(image)
- default: return nil
- }
- }
}
private func startVoiceMessageRecording() async {
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift
index 6245bbe21f..8b988f5624 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift
@@ -12,7 +12,7 @@ import SimpleXChat
struct ContextItemView: View {
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
- let contextItem: ChatItem
+ let contextItems: [ChatItem]
let contextIcon: String
let cancelContextItem: () -> Void
var showSender: Bool = true
@@ -24,13 +24,22 @@ struct ContextItemView: View {
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(theme.colors.secondary)
- if showSender, let sender = contextItem.memberDisplayName {
- VStack(alignment: .leading, spacing: 4) {
- Text(sender).font(.caption).foregroundColor(theme.colors.secondary)
- msgContentView(lines: 2)
- }
+ if let singleItem = contextItems.first, contextItems.count == 1 {
+ if showSender, let sender = singleItem.memberDisplayName {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(sender).font(.caption).foregroundColor(theme.colors.secondary)
+ msgContentView(lines: 2, contextItem: singleItem)
+ }
+ } else {
+ msgContentView(lines: 3, contextItem: singleItem)
+ }
} else {
- msgContentView(lines: 3)
+ Text(
+ chat.chatInfo.chatType == .local
+ ? "Saving \(contextItems.count) messages"
+ : "Forwarding \(contextItems.count) messages"
+ )
+ .italic()
}
Spacer()
Button {
@@ -45,23 +54,32 @@ struct ContextItemView: View {
.padding(12)
.frame(minHeight: 54)
.frame(maxWidth: .infinity)
- .background(chatItemFrameColor(contextItem, theme))
+ .background(background)
}
- private func msgContentView(lines: Int) -> some View {
- contextMsgPreview()
+ private var background: Color {
+ contextItems.first
+ .map { chatItemFrameColor($0, theme) }
+ ?? Color(uiColor: .tertiarySystemBackground)
+ }
+
+ private func msgContentView(lines: Int, contextItem: ChatItem) -> some View {
+ contextMsgPreview(contextItem)
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
.lineLimit(lines)
}
- private func contextMsgPreview() -> Text {
+ private func contextMsgPreview(_ contextItem: ChatItem) -> Text {
return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
func attachment() -> Text {
+ let isFileLoaded = if let fileSource = getLoadedFileSource(contextItem.file) {
+ FileManager.default.fileExists(atPath: getAppFilePath(fileSource.filePath).path)
+ } else { false }
switch contextItem.content.msgContent {
- case .file: return image("doc.fill")
+ case .file: return isFileLoaded ? image("doc.fill") : Text("")
case .image: return image("photo")
- case .voice: return image("play.fill")
+ case .voice: return isFileLoaded ? image("play.fill") : Text("")
default: return Text("")
}
}
@@ -75,6 +93,6 @@ struct ContextItemView: View {
struct ContextItemView_Previews: PreviewProvider {
static var previews: some View {
let contextItem: ChatItem = ChatItem.getSample(1, .directSnd, .now, "hello")
- return ContextItemView(chat: Chat.sampleData, contextItem: contextItem, contextIcon: "pencil.circle", cancelContextItem: {})
+ return ContextItemView(chat: Chat.sampleData, contextItems: [contextItem], contextIcon: "pencil.circle", cancelContextItem: {})
}
}
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift
index 9ad6e986bd..50ec8f28c1 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift
@@ -227,6 +227,7 @@ struct SendMessageView: View {
!composeState.editing {
if case .noContextItem = composeState.contextItem,
!composeState.voicePreview,
+ !composeState.manyMediaPreviews,
let send = sendLiveMessage,
let update = updateLiveMessage {
Button {
diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift
index 189ab95494..859d2dfd27 100644
--- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift
@@ -105,8 +105,10 @@ struct AddGroupMembersViewCommon: View {
.padding(.leading, 2)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let members = s == "" ? membersToAdd : membersToAdd.filter { $0.chatViewName.localizedLowercase.contains(s) }
- ForEach(members) { contact in
- contactCheckView(contact)
+ ForEach(members + [dummyContact]) { contact in
+ if contact.contactId != dummyContact.contactId {
+ contactCheckView(contact)
+ }
}
}
}
@@ -130,6 +132,14 @@ struct AddGroupMembersViewCommon: View {
.modifier(ThemedBackground(grouped: true))
}
+ // Resolves keyboard losing focus bug in iOS16 and iOS17,
+ // when there are no items inside `ForEach(memebers)` loop
+ private let dummyContact: Contact = {
+ var dummy = Contact.sampleData
+ dummy.contactId = -1
+ return dummy
+ }()
+
private func inviteMembersButton() -> some View {
Button {
inviteMembers()
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
index ddf3b8e4b9..fd72b5b515 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
@@ -18,6 +18,7 @@ struct GroupMemberInfoView: View {
var navigation: Bool = false
@State private var connectionStats: ConnectionStats? = nil
@State private var connectionCode: String? = nil
+ @State private var connectionLoaded: Bool = false
@State private var newRole: GroupMemberRole = .member
@State private var alert: GroupMemberInfoViewAlert?
@State private var sheet: PlanAndConnectActionSheet?
@@ -94,129 +95,137 @@ struct GroupMemberInfoView: View {
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
- if member.memberActive {
- Section {
- if let code = connectionCode { verifyCodeButton(code) }
- if let connStats = connectionStats,
- connStats.ratchetSyncAllowed {
- synchronizeConnectionButton()
- }
- // } else if developerTools {
- // synchronizeConnectionButtonForce()
- // }
- }
- }
+ if connectionLoaded {
- if let contactLink = member.contactLink {
- Section {
- SimpleXLinkQRCode(uri: contactLink)
- Button {
- showShareSheet(items: [simplexChatLink(contactLink)])
- } label: {
- Label("Share address", systemImage: "square.and.arrow.up")
+ if member.memberActive {
+ Section {
+ if let code = connectionCode { verifyCodeButton(code) }
+ if let connStats = connectionStats,
+ connStats.ratchetSyncAllowed {
+ synchronizeConnectionButton()
+ }
+ // } else if developerTools {
+ // synchronizeConnectionButtonForce()
+ // }
}
- if let contactId = member.memberContactId {
- if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
+ }
+
+ if let contactLink = member.contactLink {
+ Section {
+ SimpleXLinkQRCode(uri: contactLink)
+ Button {
+ showShareSheet(items: [simplexChatLink(contactLink)])
+ } label: {
+ Label("Share address", systemImage: "square.and.arrow.up")
+ }
+ if let contactId = member.memberContactId {
+ if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
+ connectViaAddressButton(contactLink)
+ }
+ } else {
connectViaAddressButton(contactLink)
}
- } else {
- connectViaAddressButton(contactLink)
+ } header: {
+ Text("Address")
+ .foregroundColor(theme.colors.secondary)
+ } footer: {
+ Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
+ .foregroundColor(theme.colors.secondary)
}
- } header: {
- Text("Address")
- .foregroundColor(theme.colors.secondary)
- } footer: {
- Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
- .foregroundColor(theme.colors.secondary)
}
- }
- Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
- infoRow("Group", groupInfo.displayName)
+ Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
+ infoRow("Group", groupInfo.displayName)
- if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
- Picker("Change role", selection: $newRole) {
- ForEach(roles) { role in
- Text(role.text)
+ if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
+ Picker("Change role", selection: $newRole) {
+ ForEach(roles) { role in
+ Text(role.text)
+ }
}
+ .frame(height: 36)
+ } else {
+ infoRow("Role", member.memberRole.text)
}
- .frame(height: 36)
- } else {
- infoRow("Role", member.memberRole.text)
}
- }
- if let connStats = connectionStats {
- Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
- // TODO network connection status
- Button("Change receiving address") {
- alert = .switchAddressAlert
- }
- .disabled(
- connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
- || connStats.ratchetSyncSendProhibited
- )
- if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
- Button("Abort changing address") {
- alert = .abortSwitchAddressAlert
+ if let connStats = connectionStats {
+ Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
+ // TODO network connection status
+ Button("Change receiving address") {
+ alert = .switchAddressAlert
}
.disabled(
- connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
+ connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
+ if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
+ Button("Abort changing address") {
+ alert = .abortSwitchAddressAlert
+ }
+ .disabled(
+ connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
+ || connStats.ratchetSyncSendProhibited
+ )
+ }
+ smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary)
+ smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary)
}
- smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary)
- smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary)
}
- }
- if groupInfo.membership.memberRole >= .admin {
- adminDestructiveSection(member)
- } else {
- nonAdminBlockSection(member)
- }
+ if groupInfo.membership.memberRole >= .admin {
+ adminDestructiveSection(member)
+ } else {
+ nonAdminBlockSection(member)
+ }
- if developerTools {
- Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
- infoRow("Local name", member.localDisplayName)
- infoRow("Database ID", "\(member.groupMemberId)")
- if let conn = member.activeConn {
- let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
- infoRow("Connection", connLevelDesc)
- }
- Button ("Debug delivery") {
- Task {
- do {
- let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId))
- await MainActor.run { alert = .queueInfo(info: info) }
- } catch let e {
- logger.error("apiContactQueueInfo error: \(responseError(e))")
- let a = getErrorAlert(e, "Error")
- await MainActor.run { alert = .error(title: a.title, error: a.message) }
+ if developerTools {
+ Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
+ infoRow("Local name", member.localDisplayName)
+ infoRow("Database ID", "\(member.groupMemberId)")
+ if let conn = member.activeConn {
+ let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
+ infoRow("Connection", connLevelDesc)
+ }
+ Button ("Debug delivery") {
+ Task {
+ do {
+ let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId))
+ await MainActor.run { alert = .queueInfo(info: info) }
+ } catch let e {
+ logger.error("apiContactQueueInfo error: \(responseError(e))")
+ let a = getErrorAlert(e, "Error")
+ await MainActor.run { alert = .error(title: a.title, error: a.message) }
+ }
}
}
}
}
+
}
}
.navigationBarHidden(true)
- .onAppear {
+ .task {
if #unavailable(iOS 16) {
// this condition prevents re-setting picker
if !justOpened { return }
}
justOpened = false
- DispatchQueue.main.async {
- newRole = member.memberRole
- do {
- let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
- let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
+ newRole = member.memberRole
+ do {
+ let (_, stats) = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
+ let (mem, code) = member.memberActive ? try await apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
+ await MainActor.run {
_ = chatModel.upsertGroupMember(groupInfo, mem)
connectionStats = stats
connectionCode = code
- } catch let error {
- logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
+ connectionLoaded = true
}
+ } catch let error {
+ await MainActor.run {
+ connectionLoaded = true
+ }
+ logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
}
}
.onChange(of: newRole) { newRole in
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift
index 18cc3f4d80..1617edd11f 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift
@@ -110,10 +110,13 @@ struct GroupProfileView: View {
}
}
.onChange(of: chosenImage) { image in
- if let image = image {
- groupProfile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
- } else {
- groupProfile.image = nil
+ Task {
+ let resized: String? = if let image {
+ await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
+ } else {
+ nil
+ }
+ await MainActor.run { groupProfile.image = resized }
}
}
.onAppear {
diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift
index 94d160e1b4..2e09909c5e 100644
--- a/apps/ios/Shared/Views/Chat/ReverseList.swift
+++ b/apps/ios/Shared/Views/Chat/ReverseList.swift
@@ -8,15 +8,16 @@
import SwiftUI
import Combine
+import SimpleXChat
/// A List, which displays it's items in reverse order - from bottom to top
-struct ReverseList: UIViewControllerRepresentable {
- let items: Array-
+struct ReverseList: UIViewControllerRepresentable {
+ let items: Array
- @Binding var scrollState: ReverseListScrollModel
- .State
+ @Binding var scrollState: ReverseListScrollModel.State
/// Closure, that returns user interface for a given item
- let content: (Item) -> Content
+ let content: (ChatItem) -> Content
let loadPage: () -> Void
@@ -25,7 +26,9 @@ struct ReverseList: UIV
}
func updateUIViewController(_ controller: Controller, context: Context) {
+ controller.representer = self
if case let .scrollingTo(destination) = scrollState, !items.isEmpty {
+ controller.view.layer.removeAllAnimations()
switch destination {
case .nextPage:
controller.scrollToNextPage()
@@ -42,9 +45,10 @@ struct ReverseList: UIV
/// Controller, which hosts SwiftUI cells
class Controller: UITableViewController {
private enum Section { case main }
- private let representer: ReverseList
- private var dataSource: UITableViewDiffableDataSource!
+ var representer: ReverseList
+ private var dataSource: UITableViewDiffableDataSource!
private var itemCount: Int = 0
+ private let updateFloatingButtons = PassthroughSubject()
private var bag = Set()
init(representer: ReverseList) {
@@ -71,7 +75,7 @@ struct ReverseList: UIV
}
// 3. Configure data source
- self.dataSource = UITableViewDiffableDataSource(
+ self.dataSource = UITableViewDiffableDataSource(
tableView: tableView
) { (tableView, indexPath, item) -> UITableViewCell? in
if indexPath.item > self.itemCount - 8, self.itemCount > 8 {
@@ -103,6 +107,15 @@ struct ReverseList: UIV
name: notificationName,
object: nil
)
+
+ updateFloatingButtons
+ .throttle(for: 0.2, scheduler: DispatchQueue.global(qos: .background), latest: true)
+ .sink {
+ if let listState = DispatchQueue.main.sync(execute: { [weak self] in self?.getListState() }) {
+ ChatView.FloatingButtonModel.shared.updateOnListChange(listState)
+ }
+ }
+ .store(in: &bag)
}
@available(*, unavailable)
@@ -171,8 +184,8 @@ struct ReverseList: UIV
Task { representer.scrollState = .atDestination }
}
- func update(items: Array
- ) {
- var snapshot = NSDiffableDataSourceSnapshot()
+ func update(items: [ChatItem]) {
+ var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.defaultRowAnimation = .none
@@ -188,6 +201,42 @@ struct ReverseList: UIV
)
}
itemCount = items.count
+ updateFloatingButtons.send()
+ }
+
+ override func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ updateFloatingButtons.send()
+ }
+
+ func getListState() -> ListState? {
+ if let visibleRows = tableView.indexPathsForVisibleRows,
+ visibleRows.last?.item ?? 0 < representer.items.count {
+ let scrollOffset: Double = tableView.contentOffset.y + InvertedTableView.inset
+ let topItemDate: Date? =
+ if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) {
+ representer.items[lastVisible.item].meta.itemTs
+ } else {
+ nil
+ }
+ let bottomItemId: ChatItem.ID? =
+ if let firstVisible = visibleRows.first(where: { isVisible(indexPath: $0) }) {
+ representer.items[firstVisible.item].id
+ } else {
+ nil
+ }
+ return (scrollOffset: scrollOffset, topItemDate: topItemDate, bottomItemId: bottomItemId)
+ }
+ return nil
+ }
+
+ private func isVisible(indexPath: IndexPath) -> Bool {
+ if let relativeFrame = tableView.superview?.convert(
+ tableView.rectForRow(at: indexPath),
+ from: tableView
+ ) {
+ relativeFrame.maxY > InvertedTableView.inset &&
+ relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
+ } else { false }
}
}
@@ -231,13 +280,19 @@ struct ReverseList: UIV
}
}
+typealias ListState = (
+ scrollOffset: Double,
+ topItemDate: Date?,
+ bottomItemId: ChatItem.ID?
+)
+
/// Manages ``ReverseList`` scrolling
-class ReverseListScrollModel: ObservableObject {
+class ReverseListScrollModel: ObservableObject {
/// Represents Scroll State of ``ReverseList``
enum State: Equatable {
enum Destination: Equatable {
case nextPage
- case item(Item.ID)
+ case item(ChatItem.ID)
case bottom
}
@@ -255,7 +310,7 @@ class ReverseListScrollModel: ObservableObject {
state = .scrollingTo(.bottom)
}
- func scrollToItem(id: Item.ID) {
+ func scrollToItem(id: ChatItem.ID) {
state = .scrollingTo(.item(id))
}
}
diff --git a/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift
index 87bc73a60e..7b185d8211 100644
--- a/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift
+++ b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift
@@ -32,13 +32,17 @@ struct SelectedItemsBottomToolbar: View {
var deleteItems: (Bool) -> Void
var moderateItems: () -> Void
//var shareItems: () -> Void
+ var forwardItems: () -> Void
@State var deleteEnabled: Bool = false
@State var deleteForEveryoneEnabled: Bool = false
@State var canModerate: Bool = false
@State var moderateEnabled: Bool = false
- @State var allButtonsDisabled = false
+ @State var forwardEnabled: Bool = false
+
+ @State var deleteCountProhibited = false
+ @State var forwardCountProhibited = false
var body: some View {
VStack(spacing: 0) {
@@ -50,10 +54,11 @@ struct SelectedItemsBottomToolbar: View {
} label: {
Image(systemName: "trash")
.resizable()
+ .scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
- .foregroundColor(!deleteEnabled || allButtonsDisabled ? theme.colors.secondary: .red)
+ .foregroundColor(!deleteEnabled || deleteCountProhibited ? theme.colors.secondary: .red)
}
- .disabled(!deleteEnabled || allButtonsDisabled)
+ .disabled(!deleteEnabled || deleteCountProhibited)
Spacer()
Button {
@@ -61,24 +66,24 @@ struct SelectedItemsBottomToolbar: View {
} label: {
Image(systemName: "flag")
.resizable()
+ .scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
- .foregroundColor(!moderateEnabled || allButtonsDisabled ? theme.colors.secondary : .red)
+ .foregroundColor(!moderateEnabled || deleteCountProhibited ? theme.colors.secondary : .red)
}
- .disabled(!moderateEnabled || allButtonsDisabled)
+ .disabled(!moderateEnabled || deleteCountProhibited)
.opacity(canModerate ? 1 : 0)
-
Spacer()
Button {
- //shareItems()
+ forwardItems()
} label: {
- Image(systemName: "square.and.arrow.up")
+ Image(systemName: "arrowshape.turn.up.forward")
.resizable()
+ .scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
- .foregroundColor(allButtonsDisabled ? theme.colors.secondary : theme.colors.primary)
+ .foregroundColor(!forwardEnabled || forwardCountProhibited ? theme.colors.secondary : theme.colors.primary)
}
- .disabled(allButtonsDisabled)
- .opacity(0)
+ .disabled(!forwardEnabled || forwardCountProhibited)
}
.frame(maxHeight: .infinity)
.padding([.leading, .trailing], 12)
@@ -101,20 +106,22 @@ struct SelectedItemsBottomToolbar: View {
private func recheckItems(_ chatInfo: ChatInfo, _ chatItems: [ChatItem], _ selectedItems: Set?) {
let count = selectedItems?.count ?? 0
- allButtonsDisabled = count == 0 || count > 20
+ deleteCountProhibited = count == 0 || count > 200
+ forwardCountProhibited = count == 0 || count > 20
canModerate = possibleToModerate(chatInfo)
if let selected = selectedItems {
let me: Bool
let onlyOwnGroupItems: Bool
- (deleteEnabled, deleteForEveryoneEnabled, me, onlyOwnGroupItems, selectedChatItems) = chatItems.reduce((true, true, true, true, [])) { (r, ci) in
+ (deleteEnabled, deleteForEveryoneEnabled, me, onlyOwnGroupItems, forwardEnabled, selectedChatItems) = chatItems.reduce((true, true, true, true, true, [])) { (r, ci) in
if selected.contains(ci.id) {
- var (de, dee, me, onlyOwnGroupItems, sel) = r
+ var (de, dee, me, onlyOwnGroupItems, fe, sel) = r
de = de && ci.canBeDeletedForSelf
dee = dee && ci.meta.deletable && !ci.localNote
onlyOwnGroupItems = onlyOwnGroupItems && ci.chatDir == .groupSnd
me = me && ci.content.msgContent != nil && ci.memberToModerate(chatInfo) != nil
+ fe = fe && ci.content.msgContent != nil && ci.meta.itemDeleted == nil && !ci.isLiveDummy
sel.insert(ci.id) // we are collecting new selected items here to account for any changes in chat items list
- return (de, dee, me, onlyOwnGroupItems, sel)
+ return (de, dee, me, onlyOwnGroupItems, fe, sel)
} else {
return r
}
diff --git a/apps/ios/Shared/Views/ChatList/ChatHelp.swift b/apps/ios/Shared/Views/ChatList/ChatHelp.swift
index c84cdb0b97..a01c81bafb 100644
--- a/apps/ios/Shared/Views/ChatList/ChatHelp.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatHelp.swift
@@ -10,7 +10,7 @@ import SwiftUI
struct ChatHelp: View {
@EnvironmentObject var chatModel: ChatModel
- @Binding var showSettings: Bool
+ let dismissSettingsSheet: DismissAction
var body: some View {
ScrollView { chatHelp() }
@@ -23,7 +23,7 @@ struct ChatHelp: View {
VStack(alignment: .leading, spacing: 0) {
Text("To ask any questions and to receive updates:")
Button("connect to SimpleX Chat developers.") {
- showSettings = false
+ dismissSettingsSheet()
DispatchQueue.main.async {
UIApplication.shared.open(simplexTeamURL)
}
@@ -61,8 +61,9 @@ struct ChatHelp: View {
}
struct ChatHelp_Previews: PreviewProvider {
+ @Environment(\.dismiss) static var mockDismiss
+
static var previews: some View {
- @State var showSettings = false
- return ChatHelp(showSettings: $showSettings)
+ ChatHelp(dismissSettingsSheet: mockDismiss)
}
}
diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift
index 8ad03236f1..a1b40aadbe 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift
@@ -9,18 +9,87 @@
import SwiftUI
import SimpleXChat
+enum UserPickerSheet: Identifiable {
+ case address
+ case chatPreferences
+ case chatProfiles
+ case currentProfile
+ case useFromDesktop
+ case settings
+
+ var id: Self { self }
+
+ var navigationTitle: LocalizedStringKey {
+ switch self {
+ case .address: "SimpleX address"
+ case .chatPreferences: "Your preferences"
+ case .chatProfiles: "Your chat profiles"
+ case .currentProfile: "Your current profile"
+ case .useFromDesktop: "Connect to desktop"
+ case .settings: "Your settings"
+ }
+ }
+}
+
+struct UserPickerSheetView: View {
+ let sheet: UserPickerSheet
+ @EnvironmentObject var chatModel: ChatModel
+ @State private var loaded = false
+
+ var body: some View {
+ NavigationView {
+ ZStack {
+ if loaded, let currentUser = chatModel.currentUser {
+ switch sheet {
+ case .address:
+ UserAddressView(shareViaProfile: currentUser.addressShared)
+ case .chatPreferences:
+ PreferencesView(
+ profile: currentUser.profile,
+ preferences: currentUser.fullPreferences,
+ currentPreferences: currentUser.fullPreferences
+ )
+ case .chatProfiles:
+ UserProfilesView()
+ case .currentProfile:
+ UserProfile()
+ case .useFromDesktop:
+ ConnectDesktopView()
+ case .settings:
+ SettingsView()
+ }
+ }
+ Color.clear // Required for list background to be rendered during loading
+ }
+ .navigationTitle(sheet.navigationTitle)
+ .navigationBarTitleDisplayMode(.large)
+ .modifier(ThemedBackground(grouped: true))
+ }
+ .overlay {
+ if let la = chatModel.laRequest {
+ LocalAuthView(authRequest: la)
+ }
+ }
+ .task {
+ withAnimation(
+ .easeOut(duration: 0.1),
+ { loaded = true }
+ )
+ }
+ }
+}
+
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
- @Binding var showSettings: Bool
+ @Binding var activeUserPickerSheet: UserPickerSheet?
@State private var searchMode = false
@FocusState private var searchFocussed
@State private var searchText = ""
@State private var searchShowingSimplexLink = false
@State private var searchChatFilteredBySimplexLink: String? = nil
- @State private var userPickerVisible = false
- @State private var showConnectDesktop = false
@State private var scrollToSearchBar = false
+ @State private var userPickerShown: Bool = false
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
@@ -46,21 +115,21 @@ struct ChatListView: View {
),
destination: chatView
) { chatListView }
- if userPickerVisible {
- Rectangle().fill(.white.opacity(0.001)).onTapGesture {
- withAnimation {
- userPickerVisible.toggle()
- }
+ }
+ .modifier(
+ Sheet(isPresented: $userPickerShown) {
+ UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet)
+ }
+ )
+ .sheet(item: $activeUserPickerSheet) {
+ UserPickerSheetView(sheet: $0)
+ }
+ .onChange(of: activeUserPickerSheet) {
+ if $0 != nil {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ userPickerShown = false
}
}
- UserPicker(
- showSettings: $showSettings,
- showConnectDesktop: $showConnectDesktop,
- userPickerVisible: $userPickerVisible
- )
- }
- .sheet(isPresented: $showConnectDesktop) {
- ConnectDesktopView()
}
}
@@ -73,7 +142,7 @@ struct ChatListView: View {
.navigationBarHidden(searchMode || oneHandUI)
}
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
- .onDisappear() { withAnimation { userPickerVisible = false } }
+ .onDisappear() { activeUserPickerSheet = nil }
.refreshable {
AlertManager.shared.showAlert(Alert(
title: Text("Reconnect servers?"),
@@ -164,7 +233,7 @@ struct ChatListView: View {
let user = chatModel.currentUser ?? User.sampleData
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel))
- .padding(.trailing, 4)
+ .padding([.top, .trailing], 3)
let allRead = chatModel.users
.filter { u in !u.user.activeUser && !u.user.hidden }
.allSatisfy { u in u.unreadCount == 0 }
@@ -173,13 +242,7 @@ struct ChatListView: View {
}
}
.onTapGesture {
- if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 {
- withAnimation {
- userPickerVisible.toggle()
- }
- } else {
- showSettings = true
- }
+ userPickerShown = true
}
}
@@ -269,7 +332,7 @@ struct ChatListView: View {
}
}
- private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
+ private func unreadBadge(size: CGFloat = 18) -> some View {
Circle()
.frame(width: size, height: size)
.foregroundColor(theme.colors.primary)
@@ -374,7 +437,7 @@ struct SubsStatusIndicator: View {
private func startTask() {
task = Task {
while !Task.isCancelled {
- if AppChatState.shared.value == .active {
+ if AppChatState.shared.value == .active, ChatModel.shared.chatRunning == true {
do {
let (subs, hasSess) = try await getAgentSubsTotal()
await MainActor.run {
@@ -516,6 +579,8 @@ func chatStoppedIcon() -> some View {
}
struct ChatListView_Previews: PreviewProvider {
+ @State static var userPickerSheet: UserPickerSheet? = .none
+
static var previews: some View {
let chatModel = ChatModel()
chatModel.updateChats([
@@ -534,9 +599,9 @@ struct ChatListView_Previews: PreviewProvider {
])
return Group {
- ChatListView(showSettings: Binding.constant(false))
+ ChatListView(activeUserPickerSheet: $userPickerSheet)
.environmentObject(chatModel)
- ChatListView(showSettings: Binding.constant(false))
+ ChatListView(activeUserPickerSheet: $userPickerSheet)
.environmentObject(ChatModel())
}
}
diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
index 9e6d3005b6..d721d546c1 100644
--- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
@@ -35,11 +35,16 @@ struct ChatPreviewView: View {
}
.padding(.leading, 4)
+ let chatTs = if let cItem {
+ cItem.meta.itemTs
+ } else {
+ chat.chatInfo.chatTs
+ }
VStack(spacing: 0) {
HStack(alignment: .top) {
chatPreviewTitle()
Spacer()
- (cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
+ (formatTimestampText(chatTs))
.font(.subheadline)
.frame(minWidth: 60, alignment: .trailing)
.foregroundColor(theme.colors.secondary)
@@ -302,7 +307,7 @@ struct ChatPreviewView: View {
case let .link(_, preview):
smallContentPreview(size: dynamicMediaSize) {
ZStack(alignment: .topTrailing) {
- Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!)
+ Image(uiImage: imageFromBase64(preview.image) ?? UIImage(systemName: "arrow.up.right")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: dynamicMediaSize, height: dynamicMediaSize)
@@ -323,13 +328,13 @@ struct ChatPreviewView: View {
}
case let .image(_, image):
smallContentPreview(size: dynamicMediaSize) {
- CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
- .environmentObject(ReverseListScrollModel())
+ CIImageView(chatItem: ci, preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
+ .environmentObject(ReverseListScrollModel())
}
case let .video(_,image, duration):
smallContentPreview(size: dynamicMediaSize) {
- CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
- .environmentObject(ReverseListScrollModel())
+ CIVideoView(chatItem: ci, preview: imageFromBase64(image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
+ .environmentObject(ReverseListScrollModel())
}
case let .voice(_, duration):
smallContentPreviewVoice(size: dynamicMediaSize) {
diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift
index 5041e093db..cfcfe851f3 100644
--- a/apps/ios/Shared/Views/ChatList/UserPicker.swift
+++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift
@@ -8,179 +8,275 @@ import SimpleXChat
struct UserPicker: View {
@EnvironmentObject var m: ChatModel
- @Environment(\.scenePhase) var scenePhase
@EnvironmentObject var theme: AppTheme
- @Binding var showSettings: Bool
- @Binding var showConnectDesktop: Bool
- @Binding var userPickerVisible: Bool
- @State var scrollViewContentSize: CGSize = .zero
- @State var disableScrolling: Bool = true
- private let menuButtonHeight: CGFloat = 68
- @State var chatViewNameWidth: CGFloat = 0
+ @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
+ @Environment(\.scenePhase) private var scenePhase: ScenePhase
+ @Environment(\.colorScheme) private var colorScheme: ColorScheme
+ @Binding var userPickerShown: Bool
+ @Binding var activeSheet: UserPickerSheet?
+ @State private var currentUser: Int64?
+ @State private var switchingProfile = false
+ @State private var frameWidth: CGFloat = 0
+ @State private var resetScroll = ResetScrollAction()
+ // Inset grouped list dimensions
+ private let imageSize: CGFloat = 44
+ private let rowPadding: CGFloat = 16
+ private let rowVerticalPadding: CGFloat = 11
+ private let sectionSpacing: CGFloat = 35
+ private var sectionHorizontalPadding: CGFloat { frameWidth > 375 ? 20 : 16 }
+ private let sectionShape = RoundedRectangle(cornerRadius: 10, style: .continuous)
var body: some View {
- VStack {
- Spacer().frame(height: 1)
+ let otherUsers: [UserInfo] = m.users
+ .filter { u in !u.user.hidden && u.user.userId != m.currentUser?.userId }
+ .sorted(using: KeyPathComparator(\.user.activeOrder, order: .reverse))
+ let sectionWidth = max(frameWidth - sectionHorizontalPadding * 2, 0)
+ let currentUserWidth = max(frameWidth - sectionHorizontalPadding - rowPadding * 2 - 14 - imageSize, 0)
+ let stopped = m.chatRunning != true
+ VStack(spacing: sectionSpacing) {
+ if let user = m.currentUser {
+ StickyScrollView(resetScroll: $resetScroll) {
+ HStack(spacing: rowPadding) {
+ HStack {
+ ProfileImage(imageStr: user.image, size: imageSize, color: Color(uiColor: .tertiarySystemGroupedBackground))
+ .padding(.trailing, 6)
+ profileName(user).lineLimit(1)
+ }
+ .padding(rowPadding)
+ .frame(width: otherUsers.isEmpty ? sectionWidth : currentUserWidth, alignment: .leading)
+ .modifier(ListRow { activeSheet = .currentProfile })
+ .clipShape(sectionShape)
+ .disabled(stopped)
+ .opacity(stopped ? 0.4 : 1)
+ ForEach(otherUsers) { u in
+ userView(u, size: imageSize)
+ .frame(maxWidth: sectionWidth * 0.618)
+ .fixedSize()
+ .disabled(stopped)
+ .opacity(stopped ? 0.4 : 1)
+ }
+ }
+ .padding(.horizontal, sectionHorizontalPadding)
+ }
+ .frame(height: 2 * rowPadding + imageSize)
+ .padding(.top, sectionSpacing)
+ .overlay(DetermineWidth())
+ .onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
+ }
VStack(spacing: 0) {
- ScrollView {
- ScrollViewReader { sp in
- let users = m.users
- .filter({ u in u.user.activeUser || !u.user.hidden })
- .sorted { u, _ in u.user.activeUser }
- VStack(spacing: 0) {
- ForEach(users) { u in
- userView(u)
- Divider()
- if u.user.activeUser { Divider() }
+ openSheetOnTap("qrcode", title: m.userAddress == nil ? "Create SimpleX address" : "Your SimpleX address", sheet: .address, disabled: stopped)
+ openSheetOnTap("switch.2", title: "Chat preferences", sheet: .chatPreferences, disabled: stopped)
+ openSheetOnTap("person.crop.rectangle.stack", title: "Your chat profiles", sheet: .chatProfiles, disabled: stopped)
+ openSheetOnTap("desktopcomputer", title: "Use from desktop", sheet: .useFromDesktop, disabled: stopped)
+ ZStack(alignment: .trailing) {
+ openSheetOnTap("gearshape", title: "Settings", sheet: .settings, showDivider: false)
+ Image(systemName: colorScheme == .light ? "sun.max" : "moon.fill")
+ .resizable()
+ .scaledToFit()
+ .symbolRenderingMode(.monochrome)
+ .foregroundColor(theme.colors.secondary)
+ .frame(maxWidth: 20, maxHeight: .infinity)
+ .padding(.horizontal, rowPadding)
+ .background(Color(.systemBackground).opacity(0.01))
+ .onTapGesture {
+ if (colorScheme == .light) {
+ ThemeManager.applyTheme(systemDarkThemeDefault.get())
+ } else {
+ ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName)
}
}
- .overlay {
- GeometryReader { geo -> Color in
- DispatchQueue.main.async {
- scrollViewContentSize = geo.size
- let scenes = UIApplication.shared.connectedScenes
- if let windowScene = scenes.first as? UIWindowScene {
- let layoutFrame = windowScene.windows[0].safeAreaLayoutGuide.layoutFrame
- disableScrolling = scrollViewContentSize.height + menuButtonHeight + 10 < layoutFrame.height
- }
- }
- return Color.clear
- }
+ .onLongPressGesture {
+ ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
}
- .onChange(of: userPickerVisible) { visible in
- if visible, let u = users.first {
- sp.scrollTo(u.id)
- }
- }
- }
- }
- .simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
- .frame(maxHeight: scrollViewContentSize.height)
-
- menuButton("Use from desktop", icon: "desktopcomputer") {
- showConnectDesktop = true
- withAnimation {
- userPickerVisible.toggle()
- }
- }
- Divider()
- menuButton("Settings", icon: "gearshape") {
- showSettings = true
- withAnimation {
- userPickerVisible.toggle()
- }
}
}
+ .clipShape(sectionShape)
+ .padding(.horizontal, sectionHorizontalPadding)
+ .padding(.bottom, sectionSpacing)
}
- .clipShape(RoundedRectangle(cornerRadius: 16))
- .background(
- Rectangle()
- .fill(theme.colors.surface)
- .cornerRadius(16)
- .shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
- )
- .onPreferenceChange(DetermineWidth.Key.self) { chatViewNameWidth = $0 }
- .frame(maxWidth: chatViewNameWidth > 0 ? min(300, chatViewNameWidth + 130) : 300)
- .padding(8)
- .opacity(userPickerVisible ? 1.0 : 0.0)
.onAppear {
- // This check prevents the call of listUsers after the app is suspended, and the database is closed.
- if case .active = scenePhase {
- Task {
- do {
- let users = try await listUsersAsync()
- await MainActor.run { m.users = users }
- } catch {
- logger.error("Error loading users \(responseError(error))")
- }
- }
- }
- }
- }
-
- private func userView(_ u: UserInfo) -> some View {
- let user = u.user
- return Button(action: {
- if user.activeUser {
- showSettings = true
- withAnimation {
- userPickerVisible.toggle()
- }
- } else {
+ // This check prevents the call of listUsers after the app is suspended, and the database is closed.
+ if case .active = scenePhase {
+ currentUser = m.currentUser?.userId
Task {
do {
- try await changeActiveUserAsync_(user.userId, viewPwd: nil)
- await MainActor.run { userPickerVisible = false }
- } catch {
+ let users = try await listUsersAsync()
await MainActor.run {
- AlertManager.shared.showAlertMsg(
- title: "Error switching profile!",
- message: "Error: \(responseError(error))"
- )
+ m.users = users
+ currentUser = m.currentUser?.userId
}
+ } catch {
+ logger.error("Error loading users \(responseError(error))")
}
}
}
- }, label: {
- HStack(spacing: 0) {
- ProfileImage(imageStr: user.image, size: 44, color: Color(uiColor: .tertiarySystemFill))
- .padding(.trailing, 12)
- Text(user.chatViewName)
- .fontWeight(user.activeUser ? .medium : .regular)
- .foregroundColor(theme.colors.onBackground)
- .overlay(DetermineWidth())
- Spacer()
- if user.activeUser {
- Image(systemName: "checkmark")
- } else if u.unreadCount > 0 {
- unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary)
- } else if !user.showNtfs {
- Image(systemName: "speaker.slash")
+ }
+ .onChange(of: userPickerShown) {
+ if !$0 { resetScroll() }
+ }
+ .modifier(ThemedBackground(grouped: true))
+ .disabled(switchingProfile)
+ }
+
+ private func userView(_ u: UserInfo, size: CGFloat) -> some View {
+ HStack {
+ ZStack(alignment: .topTrailing) {
+ ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground))
+ if (u.unreadCount > 0) {
+ unreadBadge(u).offset(x: 4, y: -4)
}
}
- .padding(.trailing)
- .padding([.leading, .vertical], 12)
- })
- .buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
- }
-
- private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
- Button(action: action) {
- HStack(spacing: 0) {
- Text(title)
- .overlay(DetermineWidth())
- Spacer()
- Image(systemName: icon)
- .symbolRenderingMode(.monochrome)
- .foregroundColor(theme.colors.secondary)
- }
- .padding(.horizontal)
- .padding(.vertical, 22)
- .frame(height: menuButtonHeight)
+ .padding(.trailing, 6)
+ Text(u.user.displayName).font(.title2).lineLimit(1)
}
- .buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
+ .padding(rowPadding)
+ .modifier(ListRow {
+ switchingProfile = true
+ Task {
+ do {
+ try await changeActiveUserAsync_(u.user.userId, viewPwd: nil)
+ await MainActor.run {
+ switchingProfile = false
+ userPickerShown = false
+ }
+ } catch {
+ await MainActor.run {
+ switchingProfile = false
+ showAlert(
+ NSLocalizedString("Error switching profile!", comment: "alertTitle"),
+ message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "alert message"), responseError(error))
+ )
+ }
+ }
+ }
+ })
+ .clipShape(sectionShape)
+ }
+
+ private func openSheetOnTap(_ icon: String, title: LocalizedStringKey, sheet: UserPickerSheet, showDivider: Bool = true, disabled: Bool = false) -> some View {
+ ZStack(alignment: .bottom) {
+ settingsRow(icon, color: theme.colors.secondary) {
+ Text(title).foregroundColor(.primary).opacity(disabled ? 0.4 : 1)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal, rowPadding)
+ .padding(.vertical, rowVerticalPadding)
+ .modifier(ListRow { activeSheet = sheet })
+ .disabled(disabled)
+ if showDivider {
+ Divider().padding(.leading, 52)
+ }
+ }
+ }
+
+ private func unreadBadge(_ u: UserInfo) -> some View {
+ let size = dynamicSize(userFont).chatInfoSize
+ return unreadCountText(u.unreadCount)
+ .font(userFont <= .xxxLarge ? .caption : .caption2)
+ .foregroundColor(.white)
+ .padding(.horizontal, dynamicSize(userFont).unreadPadding)
+ .frame(minWidth: size, minHeight: size)
+ .background(u.user.showNtfs ? theme.colors.primary : theme.colors.secondary)
+ .cornerRadius(dynamicSize(userFont).unreadCorner)
}
}
-private func unreadCounter(_ unread: Int, color: Color) -> some View {
- unreadCountText(unread)
- .font(.caption)
- .foregroundColor(.white)
- .padding(.horizontal, 4)
- .frame(minWidth: 18, minHeight: 18)
- .background(color)
- .cornerRadius(10)
+struct ListRow: ViewModifier {
+ @Environment(\.colorScheme) private var colorScheme: ColorScheme
+ @State private var touchDown = false
+ let action: () -> Void
+
+ func body(content: Content) -> some View {
+ ZStack {
+ elevatedSecondarySystemGroupedBackground
+ Color(.systemGray4).opacity(touchDown ? 1 : 0)
+ content
+ TouchOverlay(touchDown: $touchDown, action: action)
+ }
+ }
+
+ var elevatedSecondarySystemGroupedBackground: Color {
+ switch colorScheme {
+ case .dark: Color(0xFF2C2C2E)
+ default: Color(0xFFFFFFFF)
+ }
+ }
+
+ struct TouchOverlay: UIViewRepresentable {
+ @Binding var touchDown: Bool
+ let action: () -> Void
+
+ func makeUIView(context: Context) -> TouchView {
+ let touchView = TouchView()
+ let gesture = UILongPressGestureRecognizer(
+ target: touchView,
+ action: #selector(touchView.longPress(gesture:))
+ )
+ gesture.delegate = touchView
+ gesture.minimumPressDuration = 0
+ touchView.addGestureRecognizer(gesture)
+ return touchView
+ }
+
+ func updateUIView(_ touchView: TouchView, context: Context) {
+ touchView.representer = self
+ }
+
+ class TouchView: UIView, UIGestureRecognizerDelegate {
+ var representer: TouchOverlay?
+ private var startLocation: CGPoint?
+ private var task: Task?
+
+ @objc
+ func longPress(gesture: UILongPressGestureRecognizer) {
+ switch gesture.state {
+ case .began:
+ startLocation = gesture.location(in: nil)
+ task = Task {
+ do {
+ try await Task.sleep(nanoseconds: 200_000000)
+ await MainActor.run { representer?.touchDown = true }
+ } catch { }
+ }
+ case .ended:
+ if hitTest(gesture.location(in: self), with: nil) == self {
+ representer?.action()
+ }
+ task?.cancel()
+ representer?.touchDown = false
+ case .changed:
+ if let startLocation {
+ let location = gesture.location(in: nil)
+ let dx = location.x - startLocation.x
+ let dy = location.y - startLocation.y
+ if sqrt(pow(dx, 2) + pow(dy, 2)) > 10 { gesture.state = .failed }
+ }
+ case .cancelled, .failed:
+ task?.cancel()
+ representer?.touchDown = false
+ default: break
+ }
+ }
+
+ func gestureRecognizer(
+ _: UIGestureRecognizer,
+ shouldRecognizeSimultaneouslyWith: UIGestureRecognizer
+ ) -> Bool { true }
+ }
+ }
}
+
struct UserPicker_Previews: PreviewProvider {
static var previews: some View {
+ @State var activeSheet: UserPickerSheet?
+
let m = ChatModel()
m.users = [UserInfo.sampleData, UserInfo.sampleData]
return UserPicker(
- showSettings: Binding.constant(false),
- showConnectDesktop: Binding.constant(false),
- userPickerVisible: Binding.constant(true)
+ userPickerShown: .constant(true),
+ activeSheet: $activeSheet
)
.environmentObject(m)
}
diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift
index f5b5287971..804f2307ef 100644
--- a/apps/ios/Shared/Views/Database/DatabaseView.swift
+++ b/apps/ios/Shared/Views/Database/DatabaseView.swift
@@ -44,7 +44,7 @@ enum DatabaseAlert: Identifiable {
struct DatabaseView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
- @Binding var showSettings: Bool
+ let dismissSettingsSheet: DismissAction
@State private var runChat = false
@State private var alert: DatabaseAlert? = nil
@State private var showFileImporter = false
@@ -270,12 +270,12 @@ struct DatabaseView: View {
case let .archiveImportedWithErrors(errs):
return Alert(
title: Text("Chat database imported"),
- message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs)
+ message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs)
)
case let .archiveExportedWithErrors(archivePath, errs):
return Alert(
title: Text("Chat database exported"),
- message: Text("You may save the exported archive.") + Text(verbatim: "\n\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
+ message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
dismissButton: .default(Text("Continue")) {
showShareSheet(items: [archivePath])
}
@@ -439,7 +439,7 @@ struct DatabaseView: View {
private func startChat() {
if m.chatDbChanged {
- showSettings = false
+ dismissSettingsSheet()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
resetChatCtrl()
do {
@@ -533,7 +533,9 @@ func deleteChatAsync() async throws {
}
struct DatabaseView_Previews: PreviewProvider {
+ @Environment(\.dismiss) static var mockDismiss
+
static var previews: some View {
- DatabaseView(showSettings: Binding.constant(false), chatItemTTL: .none)
+ DatabaseView(dismissSettingsSheet: mockDismiss, chatItemTTL: .none)
}
}
diff --git a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift
index 477dc567eb..9aa6ac86cf 100644
--- a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift
+++ b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift
@@ -14,50 +14,55 @@ import SimpleXChat
/// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically)
/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius
struct ChatItemClipped: ViewModifier {
- struct ClipShape: Shape {
- let maxCornerRadius: Double
-
- func path(in rect: CGRect) -> Path {
- Path(
- roundedRect: rect,
- cornerRadius: min((rect.height / 2), maxCornerRadius),
- style: .circular
- )
- }
- }
-
+ @AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness
+ @AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var tailEnabled = true
+ private let chatItem: (content: CIContent, chatDir: CIDirection)?
+ private let tailVisible: Bool
+
init() {
- clipShape = ClipShape(
- maxCornerRadius: 18
- )
+ self.chatItem = nil
+ self.tailVisible = false
+ }
+
+ init(_ ci: ChatItem, tailVisible: Bool) {
+ self.chatItem = (ci.content, ci.chatDir)
+ self.tailVisible = tailVisible
}
- init(_ chatItem: ChatItem) {
- clipShape = ClipShape(
- maxCornerRadius: {
- switch chatItem.content {
- case
- .sndMsgContent,
+ private func shapeStyle() -> ChatItemShape.Style {
+ if let ci = chatItem {
+ switch ci.content {
+ case
+ .sndMsgContent,
.rcvMsgContent,
.rcvDecryptionError,
- .rcvGroupInvitation,
- .sndGroupInvitation,
- .sndDeleted,
- .rcvDeleted,
.rcvIntegrityError,
- .sndModerated,
- .rcvModerated,
- .rcvBlocked,
- .invalidJSON: 18
- default: 8
+ .invalidJSON:
+ let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty {
+ false
+ } else {
+ tailVisible
}
- }()
- )
+ return tailEnabled
+ ? .bubble(
+ padding: ci.chatDir.sent ? .trailing : .leading,
+ tailVisible: tail
+ )
+ : .roundRect(radius: msgRectMaxRadius)
+ case .rcvGroupInvitation, .sndGroupInvitation:
+ return .roundRect(radius: msgRectMaxRadius)
+ default: return .roundRect(radius: 8)
+ }
+ } else {
+ return .roundRect(radius: msgRectMaxRadius)
+ }
}
-
- private let clipShape: ClipShape
-
+
func body(content: Content) -> some View {
+ let clipShape = ChatItemShape(
+ roundness: roundness,
+ style: shapeStyle()
+ )
content
.contentShape(.dragPreview, clipShape)
.contentShape(.contextMenuPreview, clipShape)
@@ -65,4 +70,106 @@ struct ChatItemClipped: ViewModifier {
}
}
+struct ChatTailPadding: ViewModifier {
+ func body(content: Content) -> some View {
+ content.padding(.horizontal, -msgTailWidth)
+ }
+}
+private let msgRectMaxRadius: Double = 18
+private let msgBubbleMaxRadius: Double = msgRectMaxRadius * 1.2
+private let msgTailWidth: Double = 9
+private let msgTailMinHeight: Double = msgTailWidth * 1.254 // ~56deg
+private let msgTailMaxHeight: Double = msgTailWidth * 1.732 // 60deg
+
+struct ChatItemShape: Shape {
+ fileprivate enum Style {
+ case bubble(padding: HorizontalEdge, tailVisible: Bool)
+ case roundRect(radius: Double)
+ }
+
+ fileprivate let roundness: Double
+ fileprivate let style: Style
+
+ func path(in rect: CGRect) -> Path {
+ switch style {
+ case let .bubble(padding, tailVisible):
+ let w = rect.width
+ let h = rect.height
+ let rxMax = min(msgBubbleMaxRadius, w / 2)
+ let ryMax = min(msgBubbleMaxRadius, h / 2)
+ let rx = roundness * rxMax
+ let ry = roundness * ryMax
+ let tailHeight = min(msgTailMinHeight + roundness * (msgTailMaxHeight - msgTailMinHeight), h / 2)
+ var path = Path()
+ // top side
+ path.move(to: CGPoint(x: rx, y: 0))
+ path.addLine(to: CGPoint(x: w - rx, y: 0))
+ if roundness > 0 {
+ // top-right corner
+ path.addQuadCurve(to: CGPoint(x: w, y: ry), control: CGPoint(x: w, y: 0))
+ }
+ if rect.height > 2 * ry {
+ // right side
+ path.addLine(to: CGPoint(x: w, y: h - ry))
+ }
+ if roundness > 0 {
+ // bottom-right corner
+ path.addQuadCurve(to: CGPoint(x: w - rx, y: h), control: CGPoint(x: w, y: h))
+ }
+ // bottom side
+ if tailVisible {
+ path.addLine(to: CGPoint(x: -msgTailWidth, y: h))
+ if roundness > 0 {
+ // bottom-left tail
+ // distance of control point from touch point, calculated via ratios
+ let d = tailHeight - msgTailWidth * msgTailWidth / tailHeight
+ // tail control point
+ let tc = CGPoint(x: 0, y: h - tailHeight + d * sqrt(roundness))
+ // bottom-left tail curve
+ path.addQuadCurve(to: CGPoint(x: 0, y: h - tailHeight), control: tc)
+ } else {
+ path.addLine(to: CGPoint(x: 0, y: h - tailHeight))
+ }
+ if rect.height > ry + tailHeight {
+ // left side
+ path.addLine(to: CGPoint(x: 0, y: ry))
+ }
+ } else {
+ path.addLine(to: CGPoint(x: rx, y: h))
+ path.addQuadCurve(to: CGPoint(x: 0, y: h - ry), control: CGPoint(x: 0 , y: h))
+ if rect.height > 2 * ry {
+ // left side
+ path.addLine(to: CGPoint(x: 0, y: ry))
+ }
+ }
+ if roundness > 0 {
+ // top-left corner
+ path.addQuadCurve(to: CGPoint(x: rx, y: 0), control: CGPoint(x: 0, y: 0))
+ }
+ path.closeSubpath()
+ return switch padding {
+ case .leading: path
+ case .trailing: path
+ .scale(x: -1, y: 1, anchor: .center)
+ .path(in: rect)
+ }
+ case let .roundRect(radius):
+ return Path(roundedRect: rect, cornerRadius: radius * roundness)
+ }
+ }
+
+ var offset: Double? {
+ switch style {
+ case let .bubble(padding, isTailVisible):
+ if isTailVisible {
+ switch padding {
+ case .leading: -msgTailWidth
+ case .trailing: msgTailWidth
+ }
+ } else { 0 }
+ case .roundRect: 0
+ }
+ }
+
+}
diff --git a/apps/ios/Shared/Views/Helpers/InvertedForegroundStyle.swift b/apps/ios/Shared/Views/Helpers/InvertedForegroundStyle.swift
new file mode 100644
index 0000000000..dca413dafe
--- /dev/null
+++ b/apps/ios/Shared/Views/Helpers/InvertedForegroundStyle.swift
@@ -0,0 +1,21 @@
+//
+// Test.swift
+// SimpleX (iOS)
+//
+// Created by Levitating Pineapple on 31/08/2024.
+// Copyright © 2024 SimpleX Chat. All rights reserved.
+//
+
+import SwiftUI
+
+extension View {
+ @ViewBuilder
+ func invertedForegroundStyle(enabled: Bool = true) -> some View {
+ if enabled {
+ foregroundStyle(Material.ultraThin)
+ .environment(\.colorScheme, .dark)
+ .grayscale(1)
+ .contrast(-20)
+ } else { self }
+ }
+}
diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift
index 248504c59b..3eedd56441 100644
--- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift
+++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift
@@ -20,7 +20,7 @@ struct ProfileImage: View {
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
var body: some View {
- if let uiImage = UIImage(base64Encoded: imageStr) {
+ if let uiImage = imageFromBase64(imageStr) {
clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius, blurred: blurred)
} else {
let c = color.asAnotherColorFromSecondaryVariant(theme)
diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift
index 936c6cb3ab..b8de0e4ceb 100644
--- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift
+++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift
@@ -8,15 +8,63 @@
import SwiftUI
-func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
+func getTopViewController() -> UIViewController? {
let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first,
- let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController {
+ let rootViewController = keyWindow.rootViewController {
+ // Find the top-most presented view controller
+ var topController = rootViewController
+ while let presentedViewController = topController.presentedViewController {
+ topController = presentedViewController
+ }
+ return topController
+ }
+ return nil
+}
+
+func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
+ if let topController = getTopViewController() {
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
if let completed = completed {
- let handler: UIActivityViewController.CompletionWithItemsHandler = { _,_,_,_ in completed() }
- activityViewController.completionWithItemsHandler = handler
- }
- presentedViewController.present(activityViewController, animated: true)
+ activityViewController.completionWithItemsHandler = { _, _, _, _ in
+ completed()
+ }
+ }
+ topController.present(activityViewController, animated: true)
}
}
+
+func showAlert(
+ title: String,
+ message: String? = nil,
+ buttonTitle: String,
+ buttonAction: @escaping () -> Void,
+ cancelButton: Bool
+) -> Void {
+ if let topController = getTopViewController() {
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: buttonTitle, style: .default) { _ in
+ buttonAction()
+ })
+ if cancelButton {
+ alert.addAction(cancelAlertAction)
+ }
+ topController.present(alert, animated: true)
+ }
+}
+
+func showAlert(
+ _ title: String,
+ message: String? = nil,
+ actions: () -> [UIAlertAction] = { [okAlertAction] }
+) {
+ if let topController = getTopViewController() {
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ for action in actions() { alert.addAction(action) }
+ topController.present(alert, animated: true)
+ }
+}
+
+let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default)
+
+let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel)
diff --git a/apps/ios/Shared/Views/Helpers/SheetRepresentable.swift b/apps/ios/Shared/Views/Helpers/SheetRepresentable.swift
new file mode 100644
index 0000000000..841d5c7eda
--- /dev/null
+++ b/apps/ios/Shared/Views/Helpers/SheetRepresentable.swift
@@ -0,0 +1,188 @@
+//
+// SwiftUISheet.swift
+// SimpleX (iOS)
+//
+// Created by user on 23/09/2024.
+// Copyright © 2024 SimpleX Chat. All rights reserved.
+//
+
+import SwiftUI
+
+private let sheetAnimationDuration: Double = 0.35
+
+// Refrence: https://easings.net/
+private let easeOutCubic = UICubicTimingParameters(
+ controlPoint1: CGPoint(x: 0.215, y: 0.61),
+ controlPoint2: CGPoint(x: 0.355, y: 1)
+)
+
+struct Sheet: ViewModifier {
+ @Binding var isPresented: Bool
+ @ViewBuilder let sheetContent: () -> SheetContent
+
+ func body(content: Content) -> some View {
+ ZStack {
+ content
+ SheetRepresentable(isPresented: $isPresented, content: sheetContent())
+ .allowsHitTesting(isPresented)
+ .ignoresSafeArea()
+ }
+ }
+}
+
+struct SheetRepresentable: UIViewControllerRepresentable {
+ @Binding var isPresented: Bool
+ let content: Content
+
+ func makeUIViewController(context: Context) -> Controller {
+ Controller(content: content, representer: self)
+ }
+
+ func updateUIViewController(_ sheetController: Controller, context: Context) {
+ sheetController.animate(isPresented: isPresented)
+ }
+
+ class Controller: UIViewController {
+ let hostingController: UIHostingController
+ private let animator = UIViewPropertyAnimator(
+ duration: sheetAnimationDuration,
+ timingParameters: easeOutCubic
+ )
+ private let representer: SheetRepresentable
+ private var retainedFraction: CGFloat = 0
+ private var sheetHeight: Double { hostingController.view.frame.height }
+ private var task: Task?
+
+ init(content: C, representer: SheetRepresentable) {
+ self.representer = representer
+ self.hostingController = UIHostingController(rootView: content)
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError("init(coder:) missing") }
+
+ deinit {
+ animator.stopAnimation(true)
+ animator.finishAnimation(at: .current)
+ }
+
+ func animate(isPresented: Bool) {
+ let alreadyAnimating = animator.isRunning && isPresented != animator.isReversed
+ let sheetFullyDismissed = animator.fractionComplete == (animator.isReversed ? 1 : 0)
+ let sheetFullyPresented = animator.fractionComplete == (animator.isReversed ? 0 : 1)
+
+ if !isPresented && sheetFullyDismissed ||
+ isPresented && sheetFullyPresented ||
+ alreadyAnimating {
+ return
+ }
+
+ animator.pauseAnimation()
+ animator.isReversed = !isPresented
+ animator.continueAnimation(
+ withTimingParameters: isPresented
+ ? easeOutCubic
+ : UICubicTimingParameters(animationCurve: .easeIn),
+ durationFactor: 1 - animator.fractionComplete
+ )
+ handleVisibility()
+ }
+
+ func handleVisibility() {
+ if animator.isReversed {
+ task = Task {
+ do {
+ let sleepDuration = UInt64(sheetAnimationDuration * Double(NSEC_PER_SEC))
+ try await Task.sleep(nanoseconds: sleepDuration)
+ view.isHidden = true
+ } catch { }
+ }
+ } else {
+ task?.cancel()
+ task = nil
+ view.isHidden = false
+ }
+ }
+
+ override func viewDidLoad() {
+ view.isHidden = true
+ view.backgroundColor = .clear
+ view.addGestureRecognizer(
+ UITapGestureRecognizer(target: self, action: #selector(tap(gesture:)))
+ )
+ addChild(hostingController)
+ hostingController.didMove(toParent: self)
+ if let sheet = hostingController.view {
+ sheet.isHidden = true
+ sheet.clipsToBounds = true
+ sheet.layer.cornerRadius = 10
+ sheet.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
+ sheet.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(pan(gesture:))))
+ sheet.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(sheet)
+ NSLayoutConstraint.activate([
+ hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ ])
+ }
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ // Ensures animations are only setup once
+ // on some iOS version `viewDidAppear` can get called on each state change.
+ if hostingController.view.isHidden {
+ hostingController.view.transform = CGAffineTransform(translationX: 0, y: self.sheetHeight)
+ hostingController.view.isHidden = false
+ animator.pausesOnCompletion = true
+ animator.addAnimations {
+ self.hostingController.view.transform = .identity
+ self.view.backgroundColor = UIColor {
+ switch $0.userInterfaceStyle {
+ case .dark: .black.withAlphaComponent(0.290)
+ default: .black.withAlphaComponent(0.121)
+ }
+ }
+ }
+ animator.startAnimation()
+ animator.pauseAnimation()
+ }
+ }
+
+ @objc
+ func pan(gesture: UIPanGestureRecognizer) {
+ switch gesture.state {
+ case .began:
+ animator.isReversed = false
+ animator.pauseAnimation()
+ retainedFraction = animator.fractionComplete
+ case .changed:
+ animator.fractionComplete = retainedFraction - gesture.translation(in: view).y / sheetHeight
+ case .ended, .cancelled:
+ let velocity = gesture.velocity(in: view).y
+ animator.isReversed = (velocity - (animator.fractionComplete - 0.5) * 100).sign == .plus
+ let defaultVelocity = sheetHeight / sheetAnimationDuration
+ let fractionRemaining = 1 - animator.fractionComplete
+ let durationFactor = min(max(fractionRemaining / (abs(velocity) / defaultVelocity), 0.5), 1)
+ animator.continueAnimation(withTimingParameters: nil, durationFactor: durationFactor * fractionRemaining)
+ handleVisibility()
+ DispatchQueue.main.asyncAfter(deadline: .now() + sheetAnimationDuration) {
+ self.representer.isPresented = !self.animator.isReversed
+ }
+ default: break
+ }
+ }
+
+ @objc
+ func tap(gesture: UITapGestureRecognizer) {
+ switch gesture.state {
+ case .ended:
+ if gesture.location(in: view).y < view.frame.height - sheetHeight {
+ representer.isPresented = false
+ }
+ default: break
+ }
+ }
+ }
+}
diff --git a/apps/ios/Shared/Views/Helpers/StickyScrollView.swift b/apps/ios/Shared/Views/Helpers/StickyScrollView.swift
new file mode 100644
index 0000000000..5799962778
--- /dev/null
+++ b/apps/ios/Shared/Views/Helpers/StickyScrollView.swift
@@ -0,0 +1,61 @@
+//
+// StickyScrollView.swift
+// SimpleX (iOS)
+//
+// Created by user on 20/09/2024.
+// Copyright © 2024 SimpleX Chat. All rights reserved.
+//
+
+import SwiftUI
+
+struct StickyScrollView: UIViewRepresentable {
+ @Binding var resetScroll: ResetScrollAction
+ @ViewBuilder let content: () -> Content
+
+ func makeUIView(context: Context) -> UIScrollView {
+ let hc = context.coordinator.hostingController
+ hc.view.backgroundColor = .clear
+ let sv = UIScrollView()
+ sv.showsHorizontalScrollIndicator = false
+ sv.addSubview(hc.view)
+ sv.delegate = context.coordinator
+ DispatchQueue.main.async {
+ resetScroll = ResetScrollAction { sv.setContentOffset(.zero, animated: false) }
+ }
+ return sv
+ }
+
+ func updateUIView(_ scrollView: UIScrollView, context: Context) {
+ let hc = context.coordinator.hostingController
+ hc.rootView = content()
+ hc.view.frame.size = hc.view.intrinsicContentSize
+ scrollView.contentSize = hc.view.intrinsicContentSize
+ }
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(content: content())
+ }
+
+ class Coordinator: NSObject, UIScrollViewDelegate {
+ let hostingController: UIHostingController
+
+ init(content: Content) {
+ self.hostingController = UIHostingController(rootView: content)
+ }
+
+ func scrollViewWillEndDragging(
+ _ scrollView: UIScrollView,
+ withVelocity velocity: CGPoint,
+ targetContentOffset: UnsafeMutablePointer
+ ) {
+ if targetContentOffset.pointee.x < 32 {
+ targetContentOffset.pointee.x = 0
+ }
+ }
+ }
+}
+
+struct ResetScrollAction {
+ var action = { }
+ func callAsFunction() { action() }
+}
diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift
index 3c514d3529..829cea0165 100644
--- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift
+++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift
@@ -58,8 +58,6 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
struct MigrateFromDevice: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
- @Environment(\.dismiss) var dismiss: DismissAction
- @Binding var showSettings: Bool
@Binding var showProgressOnSettings: Bool
@State private var migrationState: MigrationFromState = .chatStopInProgress
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@@ -108,9 +106,6 @@ struct MigrateFromDevice: View {
finishedView(chatDeletion)
}
}
- .modifier(BackButton(label: "Back", disabled: $backDisabled) {
- dismiss()
- })
.onChange(of: migrationState) { state in
backDisabled = switch migrationState {
case .chatStopInProgress, .archiving, .linkShown, .finished: true
@@ -182,7 +177,7 @@ struct MigrateFromDevice: View {
case let .archiveExportedWithErrors(archivePath, errs):
return Alert(
title: Text("Chat database exported"),
- message: Text("You may migrate the exported database.") + Text(verbatim: "\n\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
+ message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
dismissButton: .default(Text("Continue")) {
Task { await uploadArchive(path: archivePath) }
}
@@ -607,7 +602,7 @@ struct MigrateFromDevice: View {
} catch let error {
fatalError("Error starting chat \(responseError(error))")
}
- showSettings = false
+ dismissAllSheets(animated: true)
}
} catch let error {
alert = .error(title: "Error deleting database", error: responseError(error))
@@ -630,9 +625,7 @@ struct MigrateFromDevice: View {
}
// Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered
if dismiss || m.chatDbStatus != .ok {
- await MainActor.run {
- showSettings = false
- }
+ dismissAllSheets(animated: true)
}
}
@@ -784,6 +777,6 @@ private class MigrationChatReceiver {
struct MigrateFromDevice_Previews: PreviewProvider {
static var previews: some View {
- MigrateFromDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false))
+ MigrateFromDevice(showProgressOnSettings: Binding.constant(false))
}
}
diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift
index e2df68a0e4..fe0eec609b 100644
--- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift
+++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift
@@ -571,7 +571,7 @@ struct MigrateToDevice: View {
AlertManager.shared.showAlert(
Alert(
title: Text("Error migrating settings"),
- message: Text ("Not all settings were migrated. Repeat migration if you need them.") + Text("\n\n") + Text(responseError(error)))
+ message: Text ("Some app settings were not migrated.") + Text("\n") + Text(responseError(error)))
)
}
hideView()
diff --git a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift
index 6001dff790..3a64a955c5 100644
--- a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift
+++ b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift
@@ -28,7 +28,9 @@ struct AddContactLearnMore: View {
Text("If you can't meet in person, show QR code in a video call, or share the link.")
Text("Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).")
}
+ .frame(maxWidth: .infinity, alignment: .leading)
.listRowBackground(Color.clear)
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.modifier(ThemedBackground(grouped: true))
}
diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift
index 33c187b64b..0c7f6136ff 100644
--- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift
+++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift
@@ -137,10 +137,13 @@ struct AddGroupView: View {
createInvalidNameAlert(mkValidName(profile.displayName), $profile.displayName)
}
.onChange(of: chosenImage) { image in
- if let image = image {
- profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
- } else {
- profile.image = nil
+ Task {
+ let resized: String? = if let image {
+ await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
+ } else {
+ nil
+ }
+ await MainActor.run { profile.image = resized }
}
}
.modifier(ThemedBackground(grouped: true))
diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift
index 764c2b47b3..3ca3e0e4d8 100644
--- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift
+++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift
@@ -14,9 +14,11 @@ enum ContactType: Int {
}
struct NewChatMenuButton: View {
+ // do not use chatModel here because it prevents showing AddGroupMembersView after group creation and QR code after link creation on iOS 16
+// @EnvironmentObject var chatModel: ChatModel
@State private var showNewChatSheet = false
@State private var alert: SomeAlert? = nil
- @State private var globalAlert: SomeAlert? = nil
+ @State private var pendingConnection: PendingContactConnection? = nil
var body: some View {
Button {
@@ -28,22 +30,14 @@ struct NewChatMenuButton: View {
.frame(width: 24, height: 24)
}
.appSheet(isPresented: $showNewChatSheet) {
- NewChatSheet(alert: $alert)
+ NewChatSheet(pendingConnection: $pendingConnection)
.environment(\EnvironmentValues.refresh as! WritableKeyPath, nil)
- .alert(item: $alert) { a in
- return a.alert
+ .onDisappear {
+ alert = cleanupPendingConnection(contactConnection: pendingConnection)
+ pendingConnection = nil
}
}
- // This is a workaround to show "Keep unused invitation" alert in both following cases:
- // - on going back from NewChatView to NewChatSheet,
- // - on dismissing NewChatMenuButton sheet while on NewChatView (skipping NewChatSheet)
- .onChange(of: alert?.id) { a in
- if !showNewChatSheet && alert != nil {
- globalAlert = alert
- alert = nil
- }
- }
- .alert(item: $globalAlert) { a in
+ .alert(item: $alert) { a in
return a.alert
}
}
@@ -60,7 +54,8 @@ struct NewChatSheet: View {
@State private var searchText = ""
@State private var searchShowingSimplexLink = false
@State private var searchChatFilteredBySimplexLink: String? = nil
- @Binding var alert: SomeAlert?
+ @State private var alert: SomeAlert?
+ @Binding var pendingConnection: PendingContactConnection?
// Sheet height management
@State private var isAddContactActive = false
@@ -78,6 +73,9 @@ struct NewChatSheet: View {
.navigationBarTitleDisplayMode(.large)
.navigationBarHidden(searchMode)
.modifier(ThemedBackground(grouped: true))
+ .alert(item: $alert) { a in
+ return a.alert
+ }
}
if #available(iOS 16.0, *), oneHandUI {
let sheetHeight: CGFloat = showArchive ? 575 : 500
@@ -112,7 +110,7 @@ struct NewChatSheet: View {
if (searchText.isEmpty) {
Section {
NavigationLink(isActive: $isAddContactActive) {
- NewChatView(selection: .invite, parentAlert: $alert)
+ NewChatView(selection: .invite, parentAlert: $alert, contactConnection: $pendingConnection)
.navigationTitle("New chat")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
@@ -122,7 +120,7 @@ struct NewChatSheet: View {
}
}
NavigationLink(isActive: $isScanPasteLinkActive) {
- NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert)
+ NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert, contactConnection: $pendingConnection)
.navigationTitle("New chat")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift
index c9670aa44a..4ca33e674d 100644
--- a/apps/ios/Shared/Views/NewChat/NewChatView.swift
+++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift
@@ -45,18 +45,47 @@ enum NewChatOption: Identifiable {
var id: Self { self }
}
+func cleanupPendingConnection(contactConnection: PendingContactConnection?) -> SomeAlert? {
+ var alert: SomeAlert? = nil
+
+ if !(ChatModel.shared.showingInvitation?.connChatUsed ?? true),
+ let conn = contactConnection {
+ alert = SomeAlert(
+ alert: Alert(
+ title: Text("Keep unused invitation?"),
+ message: Text("You can view invitation link again in connection details."),
+ primaryButton: .default(Text("Keep")) {},
+ secondaryButton: .destructive(Text("Delete")) {
+ Task {
+ await deleteChat(Chat(
+ chatInfo: .contactConnection(contactConnection: conn),
+ chatItems: []
+ ))
+ }
+ }
+ ),
+ id: "keepUnusedInvitation"
+ )
+ }
+
+ ChatModel.shared.showingInvitation = nil
+
+ return alert
+}
+
struct NewChatView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@State var selection: NewChatOption
@State var showQRCodeScanner = false
@State private var invitationUsed: Bool = false
- @State private var contactConnection: PendingContactConnection? = nil
@State private var connReqInvitation: String = ""
@State private var creatingConnReq = false
+ @State var choosingProfile = false
@State private var pastedLink: String = ""
@State private var alert: NewChatViewAlert?
@Binding var parentAlert: SomeAlert?
+ @Binding var contactConnection: PendingContactConnection?
var body: some View {
VStack(alignment: .leading) {
@@ -68,6 +97,11 @@ struct NewChatView: View {
}
.pickerStyle(.segmented)
.padding()
+ .onChange(of: $selection.wrappedValue) { opt in
+ if opt == NewChatOption.connect {
+ showQRCodeScanner = true
+ }
+ }
VStack {
// it seems there's a bug in iOS 15 if several views in switch (or if-else) statement have different transitions
@@ -122,26 +156,10 @@ struct NewChatView: View {
}
}
.onDisappear {
- if !(m.showingInvitation?.connChatUsed ?? true),
- let conn = contactConnection {
- parentAlert = SomeAlert(
- alert: Alert(
- title: Text("Keep unused invitation?"),
- message: Text("You can view invitation link again in connection details."),
- primaryButton: .default(Text("Keep")) {},
- secondaryButton: .destructive(Text("Delete")) {
- Task {
- await deleteChat(Chat(
- chatInfo: .contactConnection(contactConnection: conn),
- chatItems: []
- ))
- }
- }
- ),
- id: "keepUnusedInvitation"
- )
+ if !choosingProfile {
+ parentAlert = cleanupPendingConnection(contactConnection: contactConnection)
+ contactConnection = nil
}
- m.showingInvitation = nil
}
.alert(item: $alert) { a in
switch(a) {
@@ -159,7 +177,8 @@ struct NewChatView: View {
InviteView(
invitationUsed: $invitationUsed,
contactConnection: $contactConnection,
- connReqInvitation: connReqInvitation
+ connReqInvitation: $connReqInvitation,
+ choosingProfile: $choosingProfile
)
} else if creatingConnReq {
creatingLinkProgressView()
@@ -210,12 +229,22 @@ struct NewChatView: View {
}
}
+private func incognitoProfileImage() -> some View {
+ Image(systemName: "theatermasks.fill")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 30)
+ .foregroundColor(.indigo)
+}
+
private struct InviteView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var invitationUsed: Bool
@Binding var contactConnection: PendingContactConnection?
- var connReqInvitation: String
+ @Binding var connReqInvitation: String
+ @Binding var choosingProfile: Bool
+
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
var body: some View {
@@ -226,28 +255,40 @@ private struct InviteView: View {
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10))
qrCodeView()
-
- Section {
- IncognitoToggle(incognitoEnabled: $incognitoDefault)
- } footer: {
- sharedProfileInfo(incognitoDefault)
- .foregroundColor(theme.colors.secondary)
+ if let selectedProfile = chatModel.currentUser {
+ Section {
+ NavigationLink {
+ ActiveProfilePicker(
+ contactConnection: $contactConnection,
+ connReqInvitation: $connReqInvitation,
+ incognitoEnabled: $incognitoDefault,
+ choosingProfile: $choosingProfile,
+ selectedProfile: selectedProfile
+ )
+ } label: {
+ HStack {
+ if incognitoDefault {
+ incognitoProfileImage()
+ Text("Incognito")
+ } else {
+ ProfileImage(imageStr: chatModel.currentUser?.image, size: 30)
+ Text(chatModel.currentUser?.chatViewName ?? "")
+ }
+ }
+ }
+ } header: {
+ Text("Share profile").foregroundColor(theme.colors.secondary)
+ } footer: {
+ if incognitoDefault {
+ Text("A new random profile will be shared.")
+ }
+ }
}
}
.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.updateContactConnection(conn)
- }
- }
- } catch {
- logger.error("apiSetConnectionIncognito error: \(responseError(error))")
- }
- }
+ setInvitationUsed()
+ }
+ .onChange(of: chatModel.currentUser) { u in
setInvitationUsed()
}
}
@@ -270,6 +311,7 @@ private struct InviteView: View {
private func qrCodeView() -> some View {
Section(header: Text("Or show this code").foregroundColor(theme.colors.secondary)) {
SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed)
+ .id("simplex-qrcode-view-for-\(connReqInvitation)")
.padding()
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
@@ -289,6 +331,257 @@ private struct InviteView: View {
}
}
+private enum ProfileSwitchStatus {
+ case switchingUser
+ case switchingIncognito
+ case idle
+}
+
+private struct ActiveProfilePicker: View {
+ @Environment(\.dismiss) var dismiss
+ @EnvironmentObject var chatModel: ChatModel
+ @EnvironmentObject var theme: AppTheme
+ @Binding var contactConnection: PendingContactConnection?
+ @Binding var connReqInvitation: String
+ @Binding var incognitoEnabled: Bool
+ @Binding var choosingProfile: Bool
+ @State private var alert: SomeAlert?
+ @State private var profileSwitchStatus: ProfileSwitchStatus = .idle
+ @State private var switchingProfileByTimeout = false
+ @State private var lastSwitchingProfileByTimeoutCall: Double?
+ @State private var profiles: [User] = []
+ @State private var searchTextOrPassword = ""
+ @State private var showIncognitoSheet = false
+ @State private var incognitoFirst: Bool = false
+ @State var selectedProfile: User
+ var trimmedSearchTextOrPassword: String { searchTextOrPassword.trimmingCharacters(in: .whitespaces)}
+
+ var body: some View {
+ viewBody()
+ .navigationTitle("Select chat profile")
+ .searchable(text: $searchTextOrPassword, placement: .navigationBarDrawer(displayMode: .always))
+ .autocorrectionDisabled(true)
+ .navigationBarTitleDisplayMode(.large)
+ .onAppear {
+ profiles = chatModel.users
+ .map { $0.user }
+ .sorted { u, _ in u.activeUser }
+ }
+ .onChange(of: incognitoEnabled) { incognito in
+ if profileSwitchStatus != .switchingIncognito {
+ return
+ }
+
+ Task {
+ do {
+ if let contactConn = contactConnection,
+ let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
+ await MainActor.run {
+ contactConnection = conn
+ chatModel.updateContactConnection(conn)
+ profileSwitchStatus = .idle
+ dismiss()
+ }
+ }
+ } catch {
+ profileSwitchStatus = .idle
+ incognitoEnabled = !incognito
+ logger.error("apiSetConnectionIncognito error: \(responseError(error))")
+ let err = getErrorAlert(error, "Error changing to incognito!")
+
+ alert = SomeAlert(
+ alert: Alert(
+ title: Text(err.title),
+ message: Text(err.message ?? "Error: \(responseError(error))")
+ ),
+ id: "setConnectionIncognitoError"
+ )
+ }
+ }
+ }
+ .onChange(of: profileSwitchStatus) { sp in
+ if sp != .idle {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ switchingProfileByTimeout = profileSwitchStatus != .idle
+ }
+ } else {
+ switchingProfileByTimeout = false
+ }
+ }
+ .onChange(of: selectedProfile) { profile in
+ if (profileSwitchStatus != .switchingUser) {
+ return
+ }
+ Task {
+ do {
+ if let contactConn = contactConnection,
+ let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) {
+
+ await MainActor.run {
+ contactConnection = conn
+ connReqInvitation = conn.connReqInv ?? ""
+ incognitoEnabled = false
+ chatModel.updateContactConnection(conn)
+ }
+ do {
+ try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil )
+ await MainActor.run {
+ profileSwitchStatus = .idle
+ dismiss()
+ }
+ } catch {
+ await MainActor.run {
+ profileSwitchStatus = .idle
+ alert = SomeAlert(
+ alert: Alert(
+ title: Text("Error switching profile"),
+ message: Text("Your connection was moved to \(profile.chatViewName) but an unexpected error occurred while redirecting you to the profile.")
+ ),
+ id: "switchingProfileError"
+ )
+ }
+ }
+ }
+ } catch {
+ await MainActor.run {
+ profileSwitchStatus = .idle
+ if let currentUser = chatModel.currentUser {
+ selectedProfile = currentUser
+ }
+ let err = getErrorAlert(error, "Error changing connection profile")
+ alert = SomeAlert(
+ alert: Alert(
+ title: Text(err.title),
+ message: Text(err.message ?? "Error: \(responseError(error))")
+ ),
+ id: "changeConnectionUserError"
+ )
+ }
+ }
+ }
+ }
+ .alert(item: $alert) { a in
+ a.alert
+ }
+ .onAppear {
+ incognitoFirst = incognitoEnabled
+ choosingProfile = true
+ }
+ .onDisappear {
+ choosingProfile = false
+ }
+ .sheet(isPresented: $showIncognitoSheet) {
+ IncognitoHelp()
+ }
+ }
+
+
+ @ViewBuilder private func viewBody() -> some View {
+ profilePicker()
+ .allowsHitTesting(!switchingProfileByTimeout)
+ .modifier(ThemedBackground(grouped: true))
+ .overlay {
+ if switchingProfileByTimeout {
+ ProgressView()
+ .scaleEffect(2)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ }
+ }
+
+ private func filteredProfiles() -> [User] {
+ let s = trimmedSearchTextOrPassword
+ let lower = s.localizedLowercase
+
+ return profiles.filter { u in
+ if (u.activeUser || !u.hidden) && (s == "" || u.chatViewName.localizedLowercase.contains(lower)) {
+ return true
+ }
+ return correctPassword(u, s)
+ }
+ }
+
+ @ViewBuilder private func profilerPickerUserOption(_ user: User) -> some View {
+ Button {
+ if selectedProfile == user && incognitoEnabled {
+ incognitoEnabled = false
+ profileSwitchStatus = .switchingIncognito
+ } else if selectedProfile != user {
+ selectedProfile = user
+ profileSwitchStatus = .switchingUser
+ }
+ } label: {
+ HStack {
+ ProfileImage(imageStr: user.image, size: 30)
+ .padding(.trailing, 2)
+ Text(user.chatViewName)
+ .foregroundColor(theme.colors.onBackground)
+ .lineLimit(1)
+ Spacer()
+ if selectedProfile == user, !incognitoEnabled {
+ Image(systemName: "checkmark")
+ .resizable().scaledToFit().frame(width: 16)
+ .foregroundColor(theme.colors.primary)
+ }
+ }
+ }
+ }
+
+ @ViewBuilder private func profilePicker() -> some View {
+ let incognitoOption = Button {
+ if !incognitoEnabled {
+ incognitoEnabled = true
+ profileSwitchStatus = .switchingIncognito
+ }
+ } label : {
+ HStack {
+ incognitoProfileImage()
+ Text("Incognito")
+ .foregroundColor(theme.colors.onBackground)
+ Image(systemName: "info.circle")
+ .foregroundColor(theme.colors.primary)
+ .font(.system(size: 14))
+ .onTapGesture {
+ showIncognitoSheet = true
+ }
+ Spacer()
+ if incognitoEnabled {
+ Image(systemName: "checkmark")
+ .resizable().scaledToFit().frame(width: 16)
+ .foregroundColor(theme.colors.primary)
+ }
+ }
+ }
+
+ List {
+ let filteredProfiles = filteredProfiles()
+ let activeProfile = filteredProfiles.first { u in u.activeUser }
+
+ if let selectedProfile = activeProfile {
+ let otherProfiles = filteredProfiles.filter { u in u.userId != activeProfile?.userId }
+
+ if incognitoFirst {
+ incognitoOption
+ profilerPickerUserOption(selectedProfile)
+ } else {
+ profilerPickerUserOption(selectedProfile)
+ incognitoOption
+ }
+
+ ForEach(otherProfiles) { p in
+ profilerPickerUserOption(p)
+ }
+ } else {
+ incognitoOption
+ ForEach(filteredProfiles) { p in
+ profilerPickerUserOption(p)
+ }
+ }
+ }
+ .opacity(switchingProfileByTimeout ? 0.4 : 1)
+ }
+}
+
private struct ConnectView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var theme: AppTheme
@@ -404,6 +697,7 @@ struct ScannerInView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.foregroundColor(Color.clear)
switch cameraAuthorizationStatus {
+ case .authorized, nil: EmptyView()
case .restricted: Text("Camera not available")
case .denied: Label("Enable camera access", systemImage: "camera")
default: Label("Tap to scan", systemImage: "qrcode")
@@ -423,21 +717,26 @@ struct ScannerInView: View {
.disabled(cameraAuthorizationStatus == .restricted)
}
}
- .onAppear {
+ .task {
let status = AVCaptureDevice.authorizationStatus(for: .video)
cameraAuthorizationStatus = status
if showQRCodeScanner {
switch status {
- case .notDetermined: askCameraAuthorization()
+ case .notDetermined: await askCameraAuthorizationAsync()
case .restricted: showQRCodeScanner = false
case .denied: showQRCodeScanner = false
case .authorized: ()
- @unknown default: askCameraAuthorization()
+ @unknown default: await askCameraAuthorizationAsync()
}
}
}
}
+ func askCameraAuthorizationAsync() async {
+ await AVCaptureDevice.requestAccess(for: .video)
+ cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
+ }
+
func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
AVCaptureDevice.requestAccess(for: .video) { allowed in
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
@@ -976,10 +1275,12 @@ func connReqSentAlert(_ type: ConnReqType) -> Alert {
struct NewChatView_Previews: PreviewProvider {
static var previews: some View {
@State var parentAlert: SomeAlert?
+ @State var contactConnection: PendingContactConnection? = nil
NewChatView(
selection: .invite,
- parentAlert: $parentAlert
+ parentAlert: $parentAlert,
+ contactConnection: $contactConnection
)
}
}
diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift
index e3bae9287a..bc1dc4b5bc 100644
--- a/apps/ios/Shared/Views/NewChat/QRCode.swift
+++ b/apps/ios/Shared/Views/NewChat/QRCode.swift
@@ -49,34 +49,34 @@ struct QRCode: View {
ZStack {
if let image = image {
qrCodeImage(image)
- }
- GeometryReader { geo in
- ZStack {
- if withLogo {
- let w = geo.size.width
- Image("icon-light")
- .resizable()
- .scaledToFit()
- .frame(width: w * 0.16, height: w * 0.16)
- .frame(width: w * 0.165, height: w * 0.165)
- .background(.white)
- .clipShape(Circle())
+ GeometryReader { geo in
+ ZStack {
+ if withLogo {
+ let w = geo.size.width
+ Image("icon-light")
+ .resizable()
+ .scaledToFit()
+ .frame(width: w * 0.16, height: w * 0.16)
+ .frame(width: w * 0.165, height: w * 0.165)
+ .background(.white)
+ .clipShape(Circle())
+ }
}
- }
- .onAppear {
- makeScreenshotFunc = {
- let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
- showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])
- onShare?()
+ .onAppear {
+ makeScreenshotFunc = {
+ let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
+ showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])
+ onShare?()
+ }
}
+ .frame(width: geo.size.width, height: geo.size.height)
}
- .frame(width: geo.size.width, height: geo.size.height)
+ } else {
+ Color.clear.aspectRatio(1, contentMode: .fit)
}
}
.onTapGesture(perform: makeScreenshotFunc)
- .onAppear {
- image = image ?? generateImage(uri, tintColor: tintColor)
- }
+ .task { image = await generateImage(uri, tintColor: tintColor) }
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@@ -89,7 +89,7 @@ private func qrCodeImage(_ image: UIImage) -> some View {
.textSelection(.enabled)
}
-private func generateImage(_ uri: String, tintColor: UIColor) -> UIImage? {
+private func generateImage(_ uri: String, tintColor: UIColor) async -> UIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(uri.utf8)
diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
index ed3adcfe7d..2ae4aa8c2b 100644
--- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
+++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
@@ -467,6 +467,39 @@ private let versionDescriptions: [VersionDescription] = [
)
]
),
+ VersionDescription(
+ version: "v6.1",
+ post: URL(string: "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"),
+ features: [
+ FeatureDescription(
+ icon: "checkmark.shield",
+ title: "Better security ✅",
+ description: "SimpleX protocols reviewed by Trail of Bits."
+ ),
+ FeatureDescription(
+ icon: "video",
+ title: "Better calls",
+ description: "Switch audio and video during the call."
+ ),
+ FeatureDescription(
+ icon: "bolt",
+ title: "Better notifications",
+ description: "Improved delivery, reduced traffic usage.\nMore improvements are coming soon!"
+ ),
+ FeatureDescription(
+ icon: nil,
+ title: "Better user experience",
+ description: nil,
+ subfeatures: [
+ ("link", "Switch chat profile for 1-time invitations."),
+ ("message", "Customizable message shape."),
+ ("calendar", "Better message dates."),
+ ("arrowshape.turn.up.right", "Forward up to 20 messages at once."),
+ ("flag", "Delete or moderate up to 200 messages.")
+ ]
+ ),
+ ]
+ ),
]
private let lastVersion = versionDescriptions.last!.version
diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift
index be063334d3..b99c054abb 100644
--- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift
+++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift
@@ -14,7 +14,6 @@ struct ConnectDesktopView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
- var viaSettings = false
@AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name
@AppStorage(DEFAULT_CONFIRM_REMOTE_SESSIONS) private var confirmRemoteSessions = false
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = true
@@ -57,23 +56,6 @@ struct ConnectDesktopView: View {
}
var body: some View {
- if viaSettings {
- viewBody
- .modifier(BackButton(label: "Back", disabled: Binding.constant(false)) {
- if m.activeRemoteCtrl {
- alert = .disconnectDesktop(action: .back)
- } else {
- dismiss()
- }
- })
- } else {
- NavigationView {
- viewBody
- }
- }
- }
-
- var viewBody: some View {
Group {
let discovery = m.remoteCtrlSession?.discovery
if discovery == true || (discovery == nil && !showConnectScreen) {
diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift
index d209ced128..36c05ed43d 100644
--- a/apps/ios/Shared/Views/TerminalView.swift
+++ b/apps/ios/Shared/Views/TerminalView.swift
@@ -160,7 +160,7 @@ struct TerminalView_Previews: PreviewProvider {
let chatModel = ChatModel()
chatModel.terminalItems = [
.resp(.now, ChatResponse.response(type: "contactSubscribed", json: "{}")),
- .resp(.now, ChatResponse.response(type: "newChatItem", json: "{}"))
+ .resp(.now, ChatResponse.response(type: "newChatItems", json: "{}"))
]
return NavigationView {
TerminalView()
diff --git a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift
index 9884c6e877..754ca3cf6b 100644
--- a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift
@@ -196,11 +196,14 @@ struct AdvancedNetworkSettings: View {
if developerTools {
Section {
Picker("Transport isolation", selection: $netCfg.sessionMode) {
- ForEach(TransportSessionMode.values, id: \.self) { Text($0.text) }
+ let modes = TransportSessionMode.values.contains(netCfg.sessionMode)
+ ? TransportSessionMode.values
+ : TransportSessionMode.values + [netCfg.sessionMode]
+ ForEach(modes, id: \.self) { Text($0.text) }
}
.frame(height: 36)
} footer: {
- Text(sessionModeInfo(netCfg.sessionMode))
+ sessionModeInfo(netCfg.sessionMode)
.foregroundColor(theme.colors.secondary)
}
}
@@ -353,10 +356,13 @@ struct AdvancedNetworkSettings: View {
}
}
- private func sessionModeInfo(_ mode: TransportSessionMode) -> LocalizedStringKey {
- switch mode {
- case .user: return "A separate TCP connection will be used **for each chat profile you have in the app**."
- case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail."
+ private func sessionModeInfo(_ mode: TransportSessionMode) -> Text {
+ let userMode = Text("A separate TCP connection will be used **for each chat profile you have in the app**.")
+ return switch mode {
+ case .user: userMode
+ case .session: userMode + Text("\n") + Text("New SOCKS credentials will be used every time you start the app.")
+ case .server: userMode + Text("\n") + Text("New SOCKS credentials will be used for each server.")
+ case .entity: Text("A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.")
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift
index 19260ce573..aa7f885ac6 100644
--- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift
@@ -58,6 +58,8 @@ extension AppSettings {
profileImageCornerRadiusGroupDefault.set(val)
def.setValue(val, forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
}
+ if let val = uiChatItemRoundness { def.setValue(val, forKey: DEFAULT_CHAT_ITEM_ROUNDNESS)}
+ if let val = uiChatItemTail { def.setValue(val, forKey: DEFAULT_CHAT_ITEM_TAIL)}
if let val = uiColorScheme { currentThemeDefault.set(val) }
if let val = uiDarkColorScheme { systemDarkThemeDefault.set(val) }
if let val = uiCurrentThemeIds { currentThemeIdsDefault.set(val) }
@@ -91,6 +93,8 @@ extension AppSettings {
c.iosCallKitEnabled = callKitEnabledGroupDefault.get()
c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
c.uiProfileImageCornerRadius = def.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
+ c.uiChatItemRoundness = def.double(forKey: DEFAULT_CHAT_ITEM_ROUNDNESS)
+ c.uiChatItemTail = def.bool(forKey: DEFAULT_CHAT_ITEM_TAIL)
c.uiColorScheme = currentThemeDefault.get()
c.uiDarkColorScheme = systemDarkThemeDefault.get()
c.uiCurrentThemeIds = currentThemeIdsDefault.get()
diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift
index 73a789f108..ab3388bfce 100644
--- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift
@@ -33,6 +33,8 @@ struct AppearanceSettings: View {
}()
@State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.DARK.themeName
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
+ @AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var chatItemRoundness = defaultChatItemRoundness
+ @AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var chatItemTail = true
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
@@ -179,6 +181,14 @@ struct AppearanceSettings: View {
}
}
+ Section(header: Text("Message shape").foregroundColor(theme.colors.secondary)) {
+ HStack {
+ Text("Corner")
+ Slider(value: $chatItemRoundness, in: 0...1, step: 0.05)
+ }
+ Toggle("Tail", isOn: $chatItemTail)
+ }
+
Section(header: Text("Profile images").foregroundColor(theme.colors.secondary)) {
HStack(spacing: 16) {
if let img = m.currentUser?.image, img != "" {
@@ -358,20 +368,21 @@ struct ChatThemePreview: View {
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
HStack {
ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false))
- .modifier(ChatItemClipped())
+ .modifier(ChatItemClipped(alice, tailVisible: true))
Spacer()
}
HStack {
Spacer()
ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false))
- .modifier(ChatItemClipped())
+ .modifier(ChatItemClipped(bob, tailVisible: true))
.frame(alignment: .trailing)
}
} else {
Rectangle().fill(.clear)
}
}
- .padding(10)
+ .padding(.vertical, 10)
+ .padding(.horizontal, 16)
.frame(maxWidth: .infinity)
if let wallpaperType, let wallpaperImage = wallpaperType.image, let backgroundColor, let tintColor {
@@ -797,7 +808,7 @@ struct ThemeDestinationPicker: View {
@Binding var customizeThemeIsOpen: Bool
var body: some View {
- let values = [(nil, "All profiles")] + m.users.filter { $0.user.activeUser }.map { ($0.user.userId, $0.user.chatViewName)}
+ let values = [(nil, NSLocalizedString("All profiles", comment: "profile dropdown"))] + m.users.filter { $0.user.activeUser }.map { ($0.user.userId, $0.user.chatViewName)}
if values.contains(where: { (userId, text) in userId == themeUserDestination?.0 }) {
Picker("Apply to", selection: $themeUserDest) {
diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift
index a0250afddf..d9862aaac8 100644
--- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift
+++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift
@@ -26,6 +26,7 @@ struct IncognitoHelp: View {
Text("Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).")
}
.listRowBackground(Color.clear)
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.modifier(ThemedBackground())
}
diff --git a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift
index 0c10da2103..bd8171623a 100644
--- a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift
+++ b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift
@@ -32,6 +32,17 @@ struct PreferencesView: View {
.disabled(currentPreferences == preferences)
}
}
+ .onDisappear {
+ if currentPreferences != preferences {
+ showAlert(
+ title: NSLocalizedString("Your chat preferences", comment: "alert title"),
+ message: NSLocalizedString("Chat preferences were changed.", comment: "alert message"),
+ buttonTitle: NSLocalizedString("Save", comment: "alert button"),
+ buttonAction: savePreferences,
+ cancelButton: true
+ )
+ }
+ }
}
private func featureSection(_ feature: ChatFeature, _ allowFeature: Binding) -> some View {
diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
index 5fb44e1ef8..a7938b86c3 100644
--- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
@@ -47,6 +47,8 @@ let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" // deprecated, only used for
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" // deprecated, only used for migration
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle" // deprecated, only used for migration
let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
+let DEFAULT_CHAT_ITEM_ROUNDNESS = "chatItemRoundness"
+let DEFAULT_CHAT_ITEM_TAIL = "chatItemTail"
let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown"
let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial"
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
@@ -77,6 +79,8 @@ let DEFAULT_NETWORK_PROXY = "networkProxy"
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
+let defaultChatItemRoundness: Double = 0.75
+
let appDefaults: [String: Any] = [
DEFAULT_SHOW_LA_NOTICE: false,
DEFAULT_LA_NOTICE_SHOWN: false,
@@ -100,6 +104,8 @@ let appDefaults: [String: Any] = [
DEFAULT_DEVELOPER_TOOLS: false,
DEFAULT_ENCRYPTION_STARTED: false,
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
+ DEFAULT_CHAT_ITEM_ROUNDNESS: defaultChatItemRoundness,
+ DEFAULT_CHAT_ITEM_TAIL: true,
DEFAULT_ONE_HAND_UI_CARD_SHOWN: false,
DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial,
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
@@ -251,10 +257,10 @@ let networkProxyDefault: CodableDefault = CodableDefault(defaults:
struct SettingsView: View {
@Environment(\.colorScheme) var colorScheme
+ @Environment(\.dismiss) var dismiss
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var sceneDelegate: SceneDelegate
@EnvironmentObject var theme: AppTheme
- @Binding var showSettings: Bool
@State private var showProgress: Bool = false
var body: some View {
@@ -263,71 +269,12 @@ struct SettingsView: View {
if showProgress {
progressView()
}
- if let la = chatModel.laRequest {
- LocalAuthView(authRequest: la)
- }
}
}
@ViewBuilder func settingsView() -> some View {
let user = chatModel.currentUser
- NavigationView {
List {
- Section(header: Text("You").foregroundColor(theme.colors.secondary)) {
- if let user = user {
- NavigationLink {
- UserProfile()
- .navigationTitle("Your current profile")
- .modifier(ThemedBackground())
- } label: {
- ProfilePreview(profileOf: user)
- .padding(.leading, -8)
- }
- }
-
- NavigationLink {
- UserProfilesView(showSettings: $showSettings)
- } label: {
- settingsRow("person.crop.rectangle.stack", color: theme.colors.secondary) { Text("Your chat profiles") }
- }
-
-
- if let user = user {
- NavigationLink {
- UserAddressView(shareViaProfile: user.addressShared)
- .navigationTitle("SimpleX address")
- .modifier(ThemedBackground(grouped: true))
- .navigationBarTitleDisplayMode(.large)
- } label: {
- settingsRow("qrcode", color: theme.colors.secondary) { Text("Your SimpleX address") }
- }
-
- NavigationLink {
- PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences)
- .navigationTitle("Your preferences")
- .modifier(ThemedBackground(grouped: true))
- } label: {
- settingsRow("switch.2", color: theme.colors.secondary) { Text("Chat preferences") }
- }
- }
-
- NavigationLink {
- ConnectDesktopView(viaSettings: true)
- } label: {
- settingsRow("desktopcomputer", color: theme.colors.secondary) { Text("Use from desktop") }
- }
-
- NavigationLink {
- MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
- .navigationTitle("Migrate device")
- .modifier(ThemedBackground(grouped: true))
- .navigationBarTitleDisplayMode(.large)
- } label: {
- settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") }
- }
- }
- .disabled(chatModel.chatRunning != true)
-
Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) {
NavigationLink {
NotificationsView()
@@ -378,14 +325,24 @@ struct SettingsView: View {
}
.disabled(chatModel.chatRunning != true)
}
-
- chatDatabaseRow()
}
+ Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) {
+ chatDatabaseRow()
+ NavigationLink {
+ MigrateFromDevice(showProgressOnSettings: $showProgress)
+ .navigationTitle("Migrate device")
+ .modifier(ThemedBackground(grouped: true))
+ .navigationBarTitleDisplayMode(.large)
+ } label: {
+ settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") }
+ }
+ }
+
Section(header: Text("Help").foregroundColor(theme.colors.secondary)) {
if let user = user {
NavigationLink {
- ChatHelp(showSettings: $showSettings)
+ ChatHelp(dismissSettingsSheet: dismiss)
.navigationTitle("Welcome \(user.displayName)!")
.modifier(ThemedBackground())
.frame(maxHeight: .infinity, alignment: .top)
@@ -410,7 +367,7 @@ struct SettingsView: View {
}
settingsRow("number", color: theme.colors.secondary) {
Button("Send questions and ideas") {
- showSettings = false
+ dismiss()
DispatchQueue.main.async {
UIApplication.shared.open(simplexTeamURL)
}
@@ -459,16 +416,15 @@ struct SettingsView: View {
}
.navigationTitle("Your settings")
.modifier(ThemedBackground(grouped: true))
- }
- .onDisappear {
- chatModel.showingTerminal = false
- chatModel.terminalItems = []
- }
+ .onDisappear {
+ chatModel.showingTerminal = false
+ chatModel.terminalItems = []
+ }
}
private func chatDatabaseRow() -> some View {
NavigationLink {
- DatabaseView(showSettings: $showSettings, chatItemTTL: chatModel.chatItemTTL)
+ DatabaseView(dismissSettingsSheet: dismiss, chatItemTTL: chatModel.chatItemTTL)
.navigationTitle("Your chat database")
.modifier(ThemedBackground(grouped: true))
} label: {
@@ -546,26 +502,25 @@ struct ProfilePreview: View {
HStack {
ProfileImage(imageStr: profileOf.image, size: 44, color: color)
.padding(.trailing, 6)
- .padding(.vertical, 6)
- VStack(alignment: .leading) {
- Text(profileOf.displayName)
- .fontWeight(.bold)
- .font(.title2)
- if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName {
- Text(profileOf.fullName)
- }
- }
+ profileName(profileOf).lineLimit(1)
}
}
}
+func profileName(_ profileOf: NamedChat) -> Text {
+ var t = Text(profileOf.displayName).fontWeight(.semibold).font(.title2)
+ if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName {
+ t = t + Text(" (" + profileOf.fullName + ")")
+// .font(.callout)
+ }
+ return t
+}
+
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.currentUser = User.sampleData
- @State var showSettings = false
-
- return SettingsView(showSettings: $showSettings)
+ return SettingsView()
.environmentObject(chatModel)
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
index fa95c51d36..2469dc59db 100644
--- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
@@ -14,7 +14,6 @@ struct UserAddressView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject private var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
- @State var viaCreateLinkView = false
@State var shareViaProfile = false
@State private var aas = AutoAcceptState()
@State private var savedAAS = AutoAcceptState()
@@ -22,7 +21,6 @@ struct UserAddressView: View {
@State private var showMailView = false
@State private var mailViewResult: Result? = nil
@State private var alert: UserAddressAlert?
- @State private var showSaveDialogue = false
@State private var progressIndicator = false
@FocusState private var keyboardVisible: Bool
@@ -44,26 +42,19 @@ struct UserAddressView: View {
var body: some View {
ZStack {
- if viaCreateLinkView {
- userAddressScrollView()
- } else {
- userAddressScrollView()
- .modifier(BackButton(disabled: Binding.constant(false)) {
- if savedAAS == aas {
- dismiss()
- } else {
- keyboardVisible = false
- showSaveDialogue = true
- }
- })
- .confirmationDialog("Save settings?", isPresented: $showSaveDialogue) {
- Button("Save auto-accept settings") {
- saveAAS()
- dismiss()
- }
- Button("Exit without saving") { dismiss() }
+ userAddressScrollView()
+ .onDisappear {
+ if savedAAS != aas {
+ showAlert(
+ title: NSLocalizedString("Auto-accept settings", comment: "alert title"),
+ message: NSLocalizedString("Settings were changed.", comment: "alert message"),
+ buttonTitle: NSLocalizedString("Save", comment: "alert button"),
+ buttonAction: saveAAS,
+ cancelButton: true
+ )
}
- }
+ }
+
if progressIndicator {
ZStack {
if chatModel.userAddress != nil {
@@ -342,7 +333,7 @@ struct UserAddressView: View {
}
}
}
-
+
private struct AutoAcceptState: Equatable {
var enable = false
var incognito = false
@@ -447,6 +438,8 @@ struct UserAddressView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")
+
+
return Group {
UserAddressView()
.environmentObject(chatModel)
diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift
index 198fd495bd..8a70efbe82 100644
--- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift
@@ -11,8 +11,11 @@ import SimpleXChat
struct UserProfile: View {
@EnvironmentObject var chatModel: ChatModel
+ @EnvironmentObject var theme: AppTheme
+ @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
@State private var profile = Profile(displayName: "", fullName: "")
- @State private var editProfile = false
+ @State private var currentProfileHash: Int?
+ // Modals
@State private var showChooseSource = false
@State private var showImagePicker = false
@State private var showTakePhoto = false
@@ -21,85 +24,86 @@ struct UserProfile: View {
@FocusState private var focusDisplayName
var body: some View {
- let user: User = chatModel.currentUser!
-
- return VStack(alignment: .leading) {
- Text("Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile.")
- .padding(.bottom)
-
- if editProfile {
- ZStack(alignment: .center) {
- ZStack(alignment: .topTrailing) {
+ List {
+ Group {
+ if profile.image != nil {
+ ZStack(alignment: .bottomTrailing) {
+ ZStack(alignment: .topTrailing) {
+ profileImageView(profile.image)
+ .onTapGesture { showChooseSource = true }
+ overlayButton("multiply", edge: .top) { profile.image = nil }
+ }
+ overlayButton("camera", edge: .bottom) { showChooseSource = true }
+ }
+ } else {
+ ZStack(alignment: .center) {
profileImageView(profile.image)
- if user.image != nil {
- Button {
- profile.image = nil
- } label: {
- Image(systemName: "multiply")
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(width: 12)
- }
+ editImageButton { showChooseSource = true }
+ }
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .center)
+ .listRowBackground(Color.clear)
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+ .padding(.top)
+ .contentShape(Rectangle())
+
+ Section {
+ HStack {
+ TextField("Enter your name…", text: $profile.displayName)
+ .focused($focusDisplayName)
+ if !validDisplayName(profile.displayName) {
+ Button {
+ alert = .invalidNameError(validName: mkValidName(profile.displayName))
+ } label: {
+ Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
}
-
- editImageButton { showChooseSource = true }
}
- .frame(maxWidth: .infinity, alignment: .center)
-
- VStack(alignment: .leading) {
- ZStack(alignment: .leading) {
- if !validNewProfileName(user) {
- Button {
- alert = .invalidNameError(validName: mkValidName(profile.displayName))
- } label: {
- Image(systemName: "exclamationmark.circle").foregroundColor(.red)
- }
- } else {
- Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
- }
- profileNameTextEdit("Profile name", $profile.displayName)
- .focused($focusDisplayName)
- }
- .padding(.bottom)
- if showFullName(user) {
- profileNameTextEdit("Full name (optional)", $profile.fullName)
- .padding(.bottom)
- }
- HStack(spacing: 20) {
- Button("Cancel") { editProfile = false }
- Button("Save (and notify contacts)") { saveProfile() }
- .disabled(!canSaveProfile(user))
- }
+ if let user = chatModel.currentUser, showFullName(user) {
+ TextField("Full name (optional)", text: $profile.fullName)
}
- .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
- } else {
- ZStack(alignment: .center) {
- profileImageView(user.image)
- .onTapGesture { startEditingImage(user) }
+ } footer: {
+ Text("Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.")
+ }
- if user.image == nil {
- editImageButton { startEditingImage(user) }
- }
+ Section {
+ Button(action: getCurrentProfile) {
+ Text("Reset")
}
- .frame(maxWidth: .infinity, alignment: .center)
-
- VStack(alignment: .leading) {
- profileNameView("Profile name:", user.profile.displayName)
- if showFullName(user) {
- profileNameView("Full name:", user.profile.fullName)
- }
- Button("Edit") {
- profile = fromLocalProfile(user.profile)
- editProfile = true
- focusDisplayName = true
- }
+ .disabled(currentProfileHash == profile.hashValue)
+ Button(action: saveProfile) {
+ Text("Save (and notify contacts)")
}
- .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
+ .disabled(!canSaveProfile)
}
}
- .padding()
- .frame(maxHeight: .infinity, alignment: .top)
+ // Lifecycle
+ .onAppear {
+ getCurrentProfile()
+ }
+ .onDisappear {
+ if canSaveProfile {
+ showAlert(
+ title: NSLocalizedString("Save your profile?", comment: "alert title"),
+ message: NSLocalizedString("Your profile was changed. If you save it, the updated profile will be sent to all your contacts.", comment: "alert message"),
+ buttonTitle: NSLocalizedString("Save (and notify contacts)", comment: "alert button"),
+ buttonAction: saveProfile,
+ cancelButton: true
+ )
+ }
+ }
+ .onChange(of: chosenImage) { image in
+ Task {
+ let resized: String? = if let image {
+ await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
+ } else {
+ nil
+ }
+ await MainActor.run { profile.image = resized }
+ }
+ }
+ // Modals
.confirmationDialog("Profile image", isPresented: $showChooseSource, titleVisibility: .visible) {
Button("Take picture") {
showTakePhoto = true
@@ -126,57 +130,49 @@ struct UserProfile: View {
}
}
}
- .onChange(of: chosenImage) { image in
- if let image = image {
- profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
- } else {
- profile.image = nil
- }
- }
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
}
- func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding) -> some View {
- TextField(label, text: name)
- .padding(.leading, 32)
- }
-
- func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View {
- HStack {
- Text(label)
- Text(name).fontWeight(.bold)
- }
- .padding(.bottom)
- }
-
- func startEditingImage(_ user: User) {
- profile = fromLocalProfile(user.profile)
- editProfile = true
- showChooseSource = true
- }
-
- private func validNewProfileName(_ user: User) -> Bool {
- profile.displayName == user.profile.displayName || validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces))
+ @ViewBuilder
+ private func overlayButton(
+ _ systemName: String,
+ edge: Edge.Set,
+ action: @escaping () -> Void
+ ) -> some View {
+ Image(systemName: systemName)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(height: 12)
+ .foregroundColor(theme.colors.primary)
+ .padding(6)
+ .frame(width: 36, height: 36, alignment: .center)
+ .background(radius >= 20 ? Color.clear : theme.colors.background.opacity(0.5))
+ .clipShape(Circle())
+ .contentShape(Circle())
+ .padding([.trailing, edge], -12)
+ .onTapGesture(perform: action)
}
private func showFullName(_ user: User) -> Bool {
user.profile.fullName != "" && user.profile.fullName != user.profile.displayName
}
-
- private func canSaveProfile(_ user: User) -> Bool {
- profile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName(user)
+
+ private var canSaveProfile: Bool {
+ currentProfileHash != profile.hashValue &&
+ profile.displayName.trimmingCharacters(in: .whitespaces) != "" &&
+ validDisplayName(profile.displayName)
}
- func saveProfile() {
+ private func saveProfile() {
+ focusDisplayName = false
Task {
do {
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
if let (newProfile, _) = try await apiUpdateProfile(profile: profile) {
- DispatchQueue.main.async {
+ await MainActor.run {
chatModel.updateCurrentUser(newProfile)
- profile = newProfile
+ getCurrentProfile()
}
- editProfile = false
} else {
alert = .duplicateUserError
}
@@ -185,6 +181,13 @@ struct UserProfile: View {
}
}
}
+
+ private func getCurrentProfile() {
+ if let user = chatModel.currentUser {
+ profile = fromLocalProfile(user.profile)
+ currentProfileHash = profile.hashValue
+ }
+ }
}
func profileImageView(_ imageStr: String?) -> some View {
@@ -201,19 +204,3 @@ func editImageButton(action: @escaping () -> Void) -> some View {
.frame(width: 48)
}
}
-
-struct UserProfile_Previews: PreviewProvider {
- static var previews: some View {
- let chatModel1 = ChatModel()
- chatModel1.currentUser = User.sampleData
- let chatModel2 = ChatModel()
- chatModel2.currentUser = User.sampleData
- chatModel2.currentUser?.profile.image = "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBMRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAAqACAAQAAAABAAAAgKADAAQAAAABAAAAgAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/+ICNElDQ19QUk9GSUxFAAEBAAACJGFwcGwEAAAAbW50clJHQiBYWVogB+EABwAHAA0AFgAgYWNzcEFQUEwAAAAAQVBQTAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1hcHBsyhqVgiV/EE04mRPV0eoVggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKZGVzYwAAAPwAAABlY3BydAAAAWQAAAAjd3RwdAAAAYgAAAAUclhZWgAAAZwAAAAUZ1hZWgAAAbAAAAAUYlhZWgAAAcQAAAAUclRSQwAAAdgAAAAgY2hhZAAAAfgAAAAsYlRSQwAAAdgAAAAgZ1RSQwAAAdgAAAAgZGVzYwAAAAAAAAALRGlzcGxheSBQMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0ZXh0AAAAAENvcHlyaWdodCBBcHBsZSBJbmMuLCAyMDE3AABYWVogAAAAAAAA81EAAQAAAAEWzFhZWiAAAAAAAACD3wAAPb////+7WFlaIAAAAAAAAEq/AACxNwAACrlYWVogAAAAAAAAKDgAABELAADIuXBhcmEAAAAAAAMAAAACZmYAAPKnAAANWQAAE9AAAApbc2YzMgAAAAAAAQxCAAAF3v//8yYAAAeTAAD9kP//+6L///2jAAAD3AAAwG7/wAARCACAAIADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9sAQwABAQEBAQECAQECAwICAgMEAwMDAwQGBAQEBAQGBwYGBgYGBgcHBwcHBwcHCAgICAgICQkJCQkLCwsLCwsLCwsL/9sAQwECAgIDAwMFAwMFCwgGCAsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsL/90ABAAI/9oADAMBAAIRAxEAPwD4N1TV59SxpunRtBb/APPP/lo+eMsf4R+uKyxNa6Y32a3UTzjoi8Ip9/8AOfYV0tx4d1a8VlsojaWo6uThj+Pb6Cs2CCGyP2LQ4xPIMBpGIVVz7ngV+Ap31P2C1iSDQbnWXRtVYyMT8kSDkZ9B29zXXReD7ZVOkX0QlLgg2ycjBH8ZHXPoOK9O8L6LpljZidWMjyqMzAdc/wB3PJ+p4qjrPiuxs1a38LwLJIn35ScoP94jlm9hxW8ZKJm1fY/Gv4yeA/E37L3xf07xz4GuH0260+7i1bRLpDkwzQOHVfQ+WwAI7r1zmv7fv2Nv2nfCv7YH7PHh346+FwkD6nEYtRs1OTZ6jBhbiA98K/zJnrGynvX8u3x3+G6fFvwXcadcOZNTQebZyN1EgH3QB91W6H657VD/AMEYP2qdQ/Zb/aRuPgN8RpjZeFviJcJabJztWy1tPkgkOeFE3+ok9zGTwtfY5Nj1Vjyt6nzuZ4XlfMj+zamH5TupVYnhhgjsaRyMYNe8eEMC7jxxU+1SMYFQFyaevPWgRqaeuSVFb0SDgAZI/SsLS9w4kxux1HTNdTEAMDvQJst20UitvA4rotMh8ycbuAv6k1Rs3UgcHjrXc6Xb2iTKVIJPQEcZ96qKMW7nWabpNmzRyEE9wOlegtplzFCLiMbEcfKw5/XP51l6ZPK6b2SJsdd64A/Kr0t5fyRsqsPLU5baNo49P0q2I//Q8iuPD17eeTpVy32u2ufls5lAC5P8MmOA2O/Q/XIrHl+GWn+CGN7qyC9ugxkSID92nvz1+pwK/TKb9j34t3Pw/PjXXrpdR165L3F7pkiDz5RISzHzFIUzliXKBQCTgMGwD8P6zompRzR2V2xuLWV9sE7ggo4yPLlBxhgRgE8k8cHivyPPMl9g3iMMrw6r+X/gH6PlmZ+1tRrP3uj7/wDBPnjXdR1rXWDao5jtm4S3h43gf3jwSPyH1rW0Xw9f6uyw2MYSNAAT/Ag/qa9ii+GTWEv2nV8nfztH3m/+t/nirMsVtMPscGIYYuCqjj8fWvmo+9qz227aI5O38NeH/DeJIGE079ZW9fQf/W/Ovyx/ba+C1x/aR+K/h6FoLa5dUvDH8rRzj7kgI+7ux253DPev1yuINKtF3XriOMDlm+83+6O1eNePZoPH2h3ngWC032N7E0UhI7HuPcdQfWvQweJdKakjkxFFTjZn6+f8Eu/2yE/a+/Zss9R8TXCyeMvCpTSfECZ+eSZF/dXWPS5jG4n/AJ6Bx2r9JGbd0r+GX9jD476z/wAE5v20IL3xPM7eGdUZdK8QBeUewmYGO6A7tbviT127171/cfaXdve28d1aSJNFKqukiHcjqwyGUjggggg9xX6Dhq6q01JM+NxVF05tdCyRQCOvakY4GRTFYd66DmN2xk2sK6eE5+YVxlo5EwB4rrLZiTyePWgmSOmsAThCcZPFdxZ5KruJyprgrWQ5G3tXS21+FABzVrYyZ6ZZTTSqCR8vQ4rUudWgW1e3QMrBScj1/D+tcpp1+UXaOn09fWtKP7OAzNjK+tNiP//R/oYjkSW9NgqsWVA7HHyrk4AJ9Tzx6CvjL9qz4M+FrbRrn4q2s0Fjcs6R3ttKdsd+ZCFBUf8APx0xj/WAYOCA1fVF58Y/hbb/AAwPxlXWIH8OCHzhdKc57bAv3vM3fLsxu3cYzX58eGdH8f8A7b/xIHi/xOs2k+DNGkK28AOCgPVQejXMg++/IiU7RyefmI+Z79+qPl++0JpR/wATG7Z9M4WOQfeVv7srdT/snp+NeWa9bfZXez8KxCZQcGVhiJT/AOzH6fnX7K/Fn9mfwzf6N9r+GmnwWV3DF5UlmBiC8iAxtbPAkx0c/e6N/eH5s+IvDcuj2jWcUTJYwsYXDrtktHXgxuvBxngE9Oh9/is6yVUr4nDL3Oq7enl+R9Plmac9qNZ+90ff/gnybLoheT7XrM3nMo5JH8h2HtXJa9/aGoMYbAC0gTqwH7x1H8hXsHiWGDRUboqr/Eeck+nrXj9/d3twWmlzbQHnn77e/tXzaqXXuntuNtz4z/ay+Eul+NPAf9u+H4TLq2kqzEAfNLAeXU/T7w/Ed6/XL/giD+2n/wALr+Ck37Nnjq78zxV8PYkW0Z2+a60VjthbJ5LWzfuW/wBjyz3NfCGuJLLm30tSsT8OT/U1+b1v4w8VfsE/tXeHf2kfhqjz2Vvcl5rdDiO4tZflu7Q+zoSUz0baeq19RkWMUZexk/Q8LNMLzx51uf3yIxPXvTQuTkVw3wz+IfhH4seBNG+JngS7W+0XX7OG/sp1P34ZlDLn0Izhh2YEGu+LAHFfXo+XJ4P9cp6YNdbCWHFcerFSCK6OGcMBk0wOmtZMVswurDNcnHKB7VqxXbDGKaZEoncRXpt4iy8fWlN44XdM5+bGPauWbUAI9p5NeH/E39oTwF8OAdO1W6+06kfuWVuQ0vtvOcIPdiPalOrGC5pOyHToym7RV2f/0nXmiaPrF/ceJvC1hrUnhC11EyFGZsIN2Mtg+QLjy+A5GQcZI6V/QP8ABrWvhd4i+GmnXXwZeI6DAnkxRxgq0LL95JFb5hJnO7dyTz3qt4f8EeCPC3g5Pht4csYItKt4fKNngMpjfOd4PJLckk8k18FeKvBXj79kHxu/xW+ECte+F711XUtNdiVC54VvQj/lnL2+63FfNNqWh7rVtT9JdItdaitpV8QSxyy+a5VowVURE/KDnuB1PQ9a/OD4yfEbwv8AEP4rx6F8JNIfXb4QyQXMlqAwvmQgEBThSkQBUysQpyFBOBjE+NH7WWu/HtrH4QfACxvYpNZHl3bSr5M7kjLQqc/JGo5ml/u8DrX2X+z38A9C+B3hzyQUvNbvVX7dehcA7ekUQ/hiT+Fe/U81m1bVj1Px/wDiX4FXQ4b7WNItJXitXZLq3nU+fpzjqpQ87PQ88eowa+JdanuvP+03JzG3Kk87voP8a/pi+NPwStfiAo8V+GDHaeI7aPYsjj91dxj/AJYzjuOyv1X6V+Mfxk+By6eL7xPodhLE9kzDUNJYfvbSXqWUd4z147cjivjc3ybkviMMtOq7eaPo8tzXmtRrvXo/8z4aaC/1a3drrbDbr6nCgepPc+36V4T8Z/A/h7xz4KvPB8uGmcb4LhhxHKv3WUeh6HPY17TrMuo3dysUA3p0VUGEArCudFt7aH7bqjguOQP6V89SquLUk9T26lNNWZ7L/wAEJv2vNQ8L6xq/7BPxZma3ureafUPDHnHvy93Zg/X9/EO+XA7Cv6fFwRnNfwWftIWHi/wL4u0T9pX4Vu2ma74buobpJY+GEkDBo5CO4B+Vx3U4PFf2VfshftPeFf2tv2e/Dvx18LbYhq0G29tQcm0vovluID/uPkr6oVPev0TLsWq9FT69T43MMN7KpdbM+q1kA+WtuF8qCa5H7SD0qvrnjbw34L0KTxD4qvobCyhBLzTuFUY7DPU+wya7nNJXZwxu3ZHoqyqq5JxXnPxL+Nvw3+EemjUPHmqxWIbPlxcvNIR2WNcsfrjFflz8cf8AgpDJMZ/DvwKgwOVOq3S/rFGf0LV8MaZp/jf4j603ibxTdT3U053PdXRLu+eflB7fkK8PFZ5TheNHV/h/wT2cLlFSfvVNF+J+hnxI/ba8cfEa5fQfhnG+h6e5KCY/NeTD6jIjH0yfcV514W8HX2plrjUiWLEtIWbcSSOS7dST/k1x2g2PhrwdZhpyFbHzEnLk+5/oK6eDxRq2soYdPH2S0xjjh2H9K+erY+pVlzTdz3aWEhSjaCsf/9P+gafwFajxovjGKeVJSqrJEPuOVUoD7ZBGR32ivgn9pz9pHUfGOvP+zb8BIDrGr6kZLO/nhwUXH34UY/LwP9bJ91BxndxXyp41/ab/AGivht4c1D9mf+0La7vrOY6f/asUpe4WP7vlRzEhRnIHmMNyAkcEcfpB+zB+zBo37O/hQ3moBL3xLfxA312gyFA5EEOeRGp79Xb5j2x8wfQHyHZ/CP41fsg6lZ/GHT3tvEVvDC0WqxwIU8uGUqXXnnaCoIlHQj5vlOR+lPwv+Lngv4v+Gk8UeC7oTRBvLnib5ZYJcZKSL1B9D0YcgkU/QfEkXitbuzuLR7S5tGCTwS4bAfO3kcEEA5B/lg1+Yn7Qdtbfsd/E/TPiT8IdShs21jzDc6HIf3TRIQWyB0hYnCE8xt9044Ckr7k7H7AiUEf4V438U/hZa+O0TXNGkWy120XbDcEfJKn/ADxmA+8h7Hqp5HpWN8Efjv4N+OvhFfFHhOTy5otqXlnIR51tKRnaw7g9VccMOnOQPXZ71Yo2mdgiqMsWOAAOufasXoyrXPw++NX7P9zHdX174Q0wWOqW/wC81DSjjMe7J86HHDxtgnC5zzjkEV+Z3iOS20u7PlZupiT+9YYQH/ZWv6hvjRp3grXPAJ8c3t6lldabGZLC/j5be3KxY/jSUgAp+IwRkfzs/tYan4Vi+LM8nhzyo5bq2gnu4Iukd04PmDI6ZGGIHc18hnmW06K+s09LvVefkfRZTjZ1H7Cetlo/8z5d1bQk1m1ng1OMTRXCGOVX+7tbg5+tQf8ABPL9o/xV/wAE9vi/r3gDxhYahrPw18WSrMJbGMzvZXcYwkyxjn5k/dyr1OFI6VqBpJ8LdPiM9gOv0FWFTzJBFbJtzgADliT0H515uAzKphpNxV0z0sVhIVo8sj9rviP/AMFJPhxpuhJ/wqm2n1rUbhcqbmJreKLP95T8zEeg/GvzP8Y/Eb4vftA+Ije+Kb2XUWU/JCDstoAewH3Rj8TXmOi+HrJYTd63MII1OPLB+d8diev4DtXtWjeIrPTNNENtD9mjx8kY+V2H0/hH60YzNK2IdpPTsthYXL6VHWK17s2/C3gHQvDCLqPiKRZ7hei/wKfYdz7mu9/4TGa5lEGjREA8Z7/5+lec2Ntf65KLm+IjhXkZ4UCunt9X0zTONN56gu39K4k2dtlueh6Xpdxcz/a9UfMi84J4X+grv7fxNaaehi0oCWUDDSH7o+leNW99f30fls3l2+eT0z61oDVFgiEOngtgY3Y/kP61pEln/9T74+Ff/BPn4e6R8MnsPieWvfFF+haS+gkbbZM3RIQeHA/jLjMhznAwBufCz42+Mf2bPEsHwM/aNlMmiONmj6+cmIRg4Cuxz+7GQMn5oicNlcGvWf2ffiB418d/Dfwn4tvR9st9StTb3IVVUxSw8NK7E5O4qRgeo46msH9tXx78JfAfwS1CL4oQx30l8ki6XZ5Ama7VTtkQ9UWPIMjdNvynO4A/NHvnqP7Rn7Q/gX9nLwY3iXVGiudR1BS2n2aOA102PvkjpEowWfpjgcmviz9nH9njxT8afFEn7SX7TkJvJL8+bp+mXSfIUP3JJIyPljUf6qI9vmPOK+DfgboFl4V+LfhHxt+1DpWoW/he7iL6bJfRt9mLpgwOwbOYIyd23sSrFdvX+iZ7n7bY+fpkqHzU3RSj50IYZVuDhh34PIqG7bBufnr8Zv2fvF3wa8Vf8L8/ZgQ20sAJ1DR4lLRPF1fbGPvRHGWjHKn5kxjFe8fDD9qX4Q/FL4cXni/V7uHS2sIv+JpYXLgyQE/3RwZEc8Rso+bpwcive/E/irQPBOgXfizxTeJYafp8ZmnnkOFRR+pJPAA5J4GTX8uP7Uf7R3hHWPilqfjDwNpo02HVZ8wWqL84jAAaVlHAeUguVHAY/Unnq1oU6bnVdkuv6GtOlKclCmtWfQn7X37bl7qEqaB4HRbaCyXytOssgiBTgedL281hzg9Onrn8xl1eNpJNQ1C4M00zGSSV23M7HqST1Oa5K7Np44uf7Psmkubp3M0hCjcG9ZGzjn1r3fwR8LrDRokvNaIlmABw3IU/l1/yBXwWZY+eJnzS0itl/XU+tweEjh4WW73ZmaHpev8AiNhJCjW9vjh2+8w9hXqVnpukeGoFe4cqVIJdjyT2/X86W+8U2ljG1rpCiRxxu6jNeO+IrbX9amEzuwERy3rz9eB/M15jdztSPQhr7ahrEt/b/Ky8bXHIz0bn1HPP4CvW/CsEUKNqOqybQ3zZb77n2z/OvnvS2khv4r5wZLiLAUADbx6jvjtmvWNGinvbn7TqjlyRnGcjNNR0DmPTZtYuNSxb2KlY+w7fX3rd063toHDTAzSj+H/H0+lYulwz3Moislx2yOD+n9KzvF3xX8C/DCIwXbi+1NvuWsJzhj/fPRRxVRRV7ntNlp91eRm61F1hgUZOTtVawtT+JGiaQDYeF4hf3J+Uyn/VqT6dya+GNb+M3j74i339n3rx2ttG2PItwwT2yxALH6ce9e3eGLXyLFcofN24wf6nsPYU9gP/1fof9kb9uf4LeBf2QYLjxVctDrujNcIdJAImuJHkYoIiRjaejFsbMHI6Zf8As+/BTxt+1l4/X9qT9pSPdpW4NoukOCIpI0OYyUPS3Q8qDzK3zNkdfkv/AIJ4/s0ah+0xZWv7Q3xmjik8PCZvstqgwuoSQnYC3cwJtwSeZmBz8uc/vtp3iPQrm+k0LT50M9oMNCo27QuFIXgAheAdudp4ODXzeyPfbIviJ4C8I/FLwnceCPHFmLvTrkdOjxOPuyRt/A69iPocgkV+dehfEbxr+wf4ot/hz8W5ZtZ+Hd+7DS9VRCz2h67CvoM/PFnK/eTK5FfpHrviHR/DejXXiDxBdRWNhYxNPcXEzBI4o0GWZieAAK/mw/bP/bF1n9pvxTH4a8DxvD4X0mZjYRSAo88pBQ3Uw6jKkiOP+FSc/MxxhUqQpwc6jtFFU6cqk1GCu2W/26f269Y+Nutnwv4KElv4cs5M2ds/ytcOOPtE2O/9xP4R7kmvz00L4e614kvTqniKR087qf429h/dH616Zofg/S/D+dW16Xz7k/MXbr9AO3+ea2W1q8v/AN1pqeTE3AYj5iPb/P4V8DmWZzxU9NILZfq/M+uwWCjh495dWa2jWPh7wZaC10+FFfsqD5ifUnrn3/WpbibUtVI+0Psj/uA449z/AErPjtrTTI/tepybc8kE5Ymse78UXV0fL0hPIjHG89fw9K8u3c7W7Grd38WjOEt0Blx95v4c+i/41iW5ur+VmvHIG7IHTmqscK2ymaY5dhnLck/Qf41sWlqyqZp3EWevrRZCu2bdgoUiCIYOeT3zXp2hrp+nRfb9VmWCFerP1PsB3NeNz+K9O0eApYr58q/xN0B9f/1VzZ1q/wBQv/td07Mw6lvT2HRR+pockhpHp3jv4q6pdwnR/CObKBxgyf8ALZx7dxXz5p+i6tPqryW8WXYHLSgso7/Oe59s16Np9rNdXTG0Uh24Z++Pr2H5n6V6LZ22k+HoFudVcBs/LHjv7L1J9z+lRzGyiM8IeCI7fZfXKguFUGRjkcDnaD/WvQrrxNYaQo0rSYzLMR25wfUn/P0rift2ueJG2RB7S3PRV/1jD3PRRj/9ddh4b0C1iJKAY/MZPv8AxH9KhS1Lt3P/1v0M/YPkRP2ZNBhiARY3uVCqMAAStwAOwr6budO8L6Fe3PjW/dbUQRySzTSSlII12jzJGBIRTtQbnwOBya+Lf+CevizRdf8A2VNH1vS7lJbQT3hMmcBQshJ3Z+7t75xivy7/AG6/27G+OWpy/CP4WXTL4OgfE9wmQ2qyIeG7H7MrfcU48w4Y8bRXy9ScYRc5uyW59BGEpT5YrUs/tq/tm6r+0x4gPw3+G9xJa+CdPmDM/KNqMiHiVxwfKB5ijPX77c4C/GVlc2eip9h0SLz5z94noD/tH/J9hXJaTZXUkGxT5MA5YZxnPdm9/QV1j3WmeHoFkuPk4+Vf4mHsP4R7n8q+DzTMpYufLHSC2/zZ9XgcFHDxu/iZaj0i6uZDqGtThtvJzwoqrdeJY7RzbaYuSRw7Dt7f5xXE6h4kvNamG/5YgcqmcLj1Pc/X8qtLAwQGPDyPzk9B/n0ryuXsdzkW5LyS4k8+/kLsx4X/AB/wFdFYxXVwyxW6gMe55Ix6Cm6Z4et7JTqevzCJj1Zu/wBBUepeNba3t2svDcflL/FPJyT9BSsuormlcPYaJGHuGM0zcjJrk7vUbvUZwJD8vO1Rwo/Dv+Ncvda3AP3s7FpHOSzHLE+w7Utm+q6uTFZDyo8/Mx6/WomWkb+baDDTPlj0ReSPqRnFdBpukXeptv2iK3Xl3Y4RQPU1mWkFhpOQF+0XAwCO+TnAJ6L9OvtViJNV8RShdTcC2j5ESfLEvufU/Xn0rNstRPQI9QtwgsfCyiYr/wAvLjEQP+yv8X1P610mj+H0WcXWpO1xeMOWbl8fyQU3RbbMSiyG1EH+sbjgf3R2+tdbamytrc3KnbErANM3OWPOAP4iR0qGzdGotg2xbNBktjKJk/p1P48fSuziOn6DBtuj5twekYP3Sf7xH8q8/ttbvriUw6eGgSTv/wAtZB65/hH0P49qll1PS9FJF0RLP2jU5xn1qLiP/9k="
- return Group {
- UserProfile()
- .environmentObject(chatModel1)
- UserProfile()
- .environmentObject(chatModel2)
- }
- }
-}
diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
index 160130bccc..330ce56e0b 100644
--- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
@@ -9,7 +9,6 @@ import SimpleXChat
struct UserProfilesView: View {
@EnvironmentObject private var m: ChatModel
@EnvironmentObject private var theme: AppTheme
- @Binding var showSettings: Bool
@Environment(\.editMode) private var editMode
@AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true
@AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true
@@ -96,8 +95,7 @@ struct UserProfilesView: View {
} label: {
Label("Add profile", systemImage: "plus")
}
- .frame(height: 44)
- .padding(.vertical, 4)
+ .frame(height: 38)
}
} footer: {
Text("Tap to activate profile.")
@@ -285,7 +283,7 @@ struct UserProfilesView: View {
await MainActor.run {
onboardingStageDefault.set(.step1_SimpleXInfo)
m.onboardingStage = .step1_SimpleXInfo
- showSettings = false
+ dismissAllSheets()
}
}
} else {
@@ -308,14 +306,14 @@ struct UserProfilesView: View {
Task {
do {
try await changeActiveUserAsync_(user.userId, viewPwd: userViewPassword(user))
+ dismissAllSheets()
} catch {
await MainActor.run { alert = .activateUserError(error: responseError(error)) }
}
}
} label: {
HStack {
- ProfileImage(imageStr: user.image, size: 44)
- .padding(.vertical, 4)
+ ProfileImage(imageStr: user.image, size: 38)
.padding(.trailing, 12)
Text(user.chatViewName)
Spacer()
@@ -406,8 +404,15 @@ public func chatPasswordHash(_ pwd: String, _ salt: String) -> String {
return hash
}
+public func correctPassword(_ user: User, _ pwd: String) -> Bool {
+ if let ph = user.viewPwdHash {
+ return pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash
+ }
+ return false
+}
+
struct UserProfilesView_Previews: PreviewProvider {
static var previews: some View {
- UserProfilesView(showSettings: Binding.constant(true))
+ UserProfilesView()
}
}
diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
index 419f0ae864..1a40820dce 100644
--- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
+++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
@@ -137,6 +137,10 @@
%@ иска да се свърже!
notification title
+
+ %1$@, %2$@
+ format for date separator in chat
+
%@, %@ and %lld members
%@, %@ и %lld членове
@@ -157,11 +161,31 @@
%d дни
time interval
+
+ %d file(s) are still being downloaded.
+ forward confirmation reason
+
+
+ %d file(s) failed to download.
+ forward confirmation reason
+
+
+ %d file(s) were deleted.
+ forward confirmation reason
+
+
+ %d file(s) were not downloaded.
+ forward confirmation reason
+
%d hours
%d часа
time interval
+
+ %d messages not forwarded
+ alert title
+
%d min
%d мин.
@@ -713,7 +737,7 @@
All profiles
- No comment provided by engineer.
+ profile dropdown
All your contacts will remain connected.
@@ -901,6 +925,10 @@
Кода за достъп до приложение се заменя с код за самоунищожение.
No comment provided by engineer.
+
+ App session
+ No comment provided by engineer.
+
App version
Версия на приложението
@@ -1003,6 +1031,10 @@
Автоматично приемане на изображения
No comment provided by engineer.
+
+ Auto-accept settings
+ alert title
+
Back
Назад
@@ -1027,11 +1059,19 @@
Лош хеш на съобщението
No comment provided by engineer.
+
+ Better calls
+ No comment provided by engineer.
+
Better groups
По-добри групи
No comment provided by engineer.
+
+ Better message dates.
+ No comment provided by engineer.
+
Better messages
По-добри съобщения
@@ -1041,6 +1081,18 @@
Better networking
No comment provided by engineer.
+
+ Better notifications
+ No comment provided by engineer.
+
+
+ Better security ✅
+ No comment provided by engineer.
+
+
+ Better user experience
+ No comment provided by engineer.
+
Black
No comment provided by engineer.
@@ -1167,7 +1219,7 @@
Cancel
Отказ
- No comment provided by engineer.
+ alert button
Cancel migration
@@ -1186,7 +1238,7 @@
Cannot receive file
Файлът не може да бъде получен
- No comment provided by engineer.
+ alert title
Capacity exceeded - recipient did not receive previously sent messages.
@@ -1310,6 +1362,15 @@
Чат настройки
No comment provided by engineer.
+
+ Chat preferences were changed.
+ alert message
+
+
+ Chat profile
+ Потребителски профил
+ No comment provided by engineer.
+
Chat theme
No comment provided by engineer.
@@ -1685,6 +1746,10 @@ This is your own one-time link!
Версия на ядрото: v%@
No comment provided by engineer.
+
+ Corner
+ No comment provided by engineer.
+
Correct name to %@?
Поправи име на %@?
@@ -1808,6 +1873,10 @@ This is your own one-time link!
Персонализирано време
No comment provided by engineer.
+
+ Customizable message shape.
+ No comment provided by engineer.
+
Customize theme
No comment provided by engineer.
@@ -2097,6 +2166,10 @@ This is your own one-time link!
Изтрий старата база данни?
No comment provided by engineer.
+
+ Delete or moderate up to 200 messages.
+ No comment provided by engineer.
+
Delete pending connection?
Изтрий предстоящата връзка?
@@ -2334,6 +2407,10 @@ This is your own one-time link!
Не изпращай история на нови членове.
No comment provided by engineer.
+
+ Do not use credentials with proxy.
+ No comment provided by engineer.
+
Don't create address
Не създавай адрес
@@ -2357,7 +2434,8 @@ This is your own one-time link!
Download
Изтегли
- chat item action
+ alert button
+ chat item action
Download errors
@@ -2373,6 +2451,10 @@ This is your own one-time link!
Свали файл
server test step
+
+ Download files
+ alert action
+
Downloaded
No comment provided by engineer.
@@ -2645,6 +2727,10 @@ This is your own one-time link!
Грешка при промяна на адреса
No comment provided by engineer.
+
+ Error changing connection profile
+ No comment provided by engineer.
+
Error changing role
Грешка при промяна на ролята
@@ -2655,6 +2741,10 @@ This is your own one-time link!
Грешка при промяна на настройката
No comment provided by engineer.
+
+ Error changing to incognito!
+ No comment provided by engineer.
+
Error connecting to forwarding server %@. Please try later.
No comment provided by engineer.
@@ -2773,6 +2863,10 @@ This is your own one-time link!
Грешка при зареждане на %@ сървъри
No comment provided by engineer.
+
+ Error migrating settings
+ No comment provided by engineer.
+
Error opening chat
Грешка при отваряне на чата
@@ -2781,7 +2875,7 @@ This is your own one-time link!
Error receiving file
Грешка при получаване на файл
- No comment provided by engineer.
+ alert title
Error reconnecting server
@@ -2870,10 +2964,14 @@ This is your own one-time link!
Грешка при спиране на чата
No comment provided by engineer.
+
+ Error switching profile
+ No comment provided by engineer.
+
Error switching profile!
Грешка при смяна на профил!
- No comment provided by engineer.
+ alertTitle
Error synchronizing connection
@@ -2918,8 +3016,7 @@ This is your own one-time link!
Error: %@
Грешка: %@
- file error text
- snd error text
+ alert message
Error: URL is invalid
@@ -3003,6 +3100,11 @@ This is your own one-time link!
File error
No comment provided by engineer.
+
+ File errors:
+%@
+ alert message
+
File not found - most likely file was deleted or cancelled.
file error text
@@ -3133,11 +3235,27 @@ This is your own one-time link!
Препрати
chat item action
+
+ Forward %d message(s)?
+ alert title
+
Forward and save messages
Препращане и запазване на съобщения
No comment provided by engineer.
+
+ Forward messages
+ alert action
+
+
+ Forward messages without files?
+ alert message
+
+
+ Forward up to 20 messages at once.
+ No comment provided by engineer.
+
Forwarded
Препратено
@@ -3148,6 +3266,10 @@ This is your own one-time link!
Препратено от
No comment provided by engineer.
+
+ Forwarding %lld messages
+ No comment provided by engineer.
+
Forwarding server %@ failed to connect to destination server %@. Please try later.
No comment provided by engineer.
@@ -3190,11 +3312,6 @@ Error: %2$@
Пълно име (незадължително)
No comment provided by engineer.
-
- Full name:
- Пълно име:
- No comment provided by engineer.
-
Fully decentralized – visible only to members.
Напълно децентрализирана – видима е само за членовете.
@@ -3438,6 +3555,10 @@ Error: %2$@
ICE сървъри (по един на ред)
No comment provided by engineer.
+
+ IP address
+ No comment provided by engineer.
+
If you can't meet in person, show QR code in a video call, or share the link.
Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка.
@@ -3512,6 +3633,11 @@ Error: %2$@
Импортиране на архив
No comment provided by engineer.
+
+ Improved delivery, reduced traffic usage.
+More improvements are coming soon!
+ No comment provided by engineer.
+
Improved message delivery
Подобрена доставка на съобщения
@@ -4069,6 +4195,10 @@ This is your link for group %@!
Message servers
No comment provided by engineer.
+
+ Message shape
+ No comment provided by engineer.
+
Message source remains private.
Източникът на съобщението остава скрит.
@@ -4115,6 +4245,10 @@ This is your link for group %@!
Messages sent
No comment provided by engineer.
+
+ Messages were deleted after you selected them.
+ alert message
+
Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.
Съобщенията, файловете и разговорите са защитени чрез **криптиране от край до край** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом.
@@ -4269,6 +4403,14 @@ This is your link for group %@!
Нов kод за достъп
No comment provided by engineer.
+
+ New SOCKS credentials will be used every time you start the app.
+ No comment provided by engineer.
+
+
+ New SOCKS credentials will be used for each server.
+ No comment provided by engineer.
+
New chat
Нов чат
@@ -4385,6 +4527,14 @@ This is your link for group %@!
Няма мрежова връзка
No comment provided by engineer.
+
+ No permission to record speech
+ No comment provided by engineer.
+
+
+ No permission to record video
+ No comment provided by engineer.
+
No permission to record voice message
Няма разрешение за запис на гласово съобщение
@@ -4404,6 +4554,10 @@ This is your link for group %@!
Nothing selected
No comment provided by engineer.
+
+ Nothing to forward!
+ alert title
+
Notifications
Известия
@@ -4436,7 +4590,7 @@ This is your link for group %@!
Ok
Ок
- No comment provided by engineer.
+ alert button
Old database
@@ -4624,6 +4778,11 @@ Requires compatible VPN.
Other %@ servers
No comment provided by engineer.
+
+ Other file errors:
+%@
+ alert message
+
PING count
PING бройка
@@ -4659,6 +4818,10 @@ Requires compatible VPN.
Кодът за достъп е зададен!
No comment provided by engineer.
+
+ Password
+ No comment provided by engineer.
+
Password to show
Парола за показване
@@ -4803,6 +4966,10 @@ Error: %@
Полски интерфейс
No comment provided by engineer.
+
+ Port
+ No comment provided by engineer.
+
Possibly, certificate fingerprint in server address is incorrect
Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен
@@ -4883,16 +5050,6 @@ Error: %@
Профилни изображения
No comment provided by engineer.
-
- Profile name
- Име на профила
- No comment provided by engineer.
-
-
- Profile name:
- Име на профила:
- No comment provided by engineer.
-
Profile password
Профилна парола
@@ -4989,6 +5146,10 @@ Enable in *Network & servers* settings.
Proxied servers
No comment provided by engineer.
+
+ Proxy requires password
+ No comment provided by engineer.
+
Push notifications
Push известия
@@ -5200,6 +5361,10 @@ Enable in *Network & servers* settings.
Премахване
No comment provided by engineer.
+
+ Remove archive?
+ No comment provided by engineer.
+
Remove image
No comment provided by engineer.
@@ -5373,6 +5538,10 @@ Enable in *Network & servers* settings.
SMP server
No comment provided by engineer.
+
+ SOCKS proxy
+ No comment provided by engineer.
+
Safely receive files
No comment provided by engineer.
@@ -5385,12 +5554,13 @@ Enable in *Network & servers* settings.
Save
Запази
- chat item action
+ alert button
+ chat item action
Save (and notify contacts)
Запази (и уведоми контактите)
- No comment provided by engineer.
+ alert button
Save and notify contact
@@ -5416,11 +5586,6 @@ Enable in *Network & servers* settings.
Запази архив
No comment provided by engineer.
-
- Save auto-accept settings
- Запази настройките за автоматично приемане
- No comment provided by engineer.
-
Save group profile
Запази профила на групата
@@ -5456,16 +5621,15 @@ Enable in *Network & servers* settings.
Запази сървърите?
No comment provided by engineer.
-
- Save settings?
- Запази настройките?
- No comment provided by engineer.
-
Save welcome message?
Запази съобщението при посрещане?
No comment provided by engineer.
+
+ Save your profile?
+ alert title
+
Saved
Запазено
@@ -5486,6 +5650,10 @@ Enable in *Network & servers* settings.
Запазено съобщение
message info title
+
+ Saving %lld messages
+ No comment provided by engineer.
+
Scale
No comment provided by engineer.
@@ -5562,6 +5730,10 @@ Enable in *Network & servers* settings.
Избери
chat item action
+
+ Select chat profile
+ No comment provided by engineer.
+
Selected %lld
No comment provided by engineer.
@@ -5674,7 +5846,7 @@ Enable in *Network & servers* settings.
Sender cancelled file transfer.
Подателят отмени прехвърлянето на файла.
- No comment provided by engineer.
+ alert message
Sender may have deleted the connection request.
@@ -5766,6 +5938,10 @@ Enable in *Network & servers* settings.
Sent via proxy
No comment provided by engineer.
+
+ Server
+ No comment provided by engineer.
+
Server address
No comment provided by engineer.
@@ -5877,6 +6053,10 @@ Enable in *Network & servers* settings.
Настройки
No comment provided by engineer.
+
+ Settings were changed.
+ alert message
+
Shape profile images
Променете формата на профилните изображения
@@ -5911,6 +6091,10 @@ Enable in *Network & servers* settings.
Сподели линк
No comment provided by engineer.
+
+ Share profile
+ No comment provided by engineer.
+
Share this 1-time invite link
Сподели този еднократен линк за връзка
@@ -6041,6 +6225,10 @@ Enable in *Network & servers* settings.
Еднократна покана за SimpleX
simplex link type
+
+ SimpleX protocols reviewed by Trail of Bits.
+ No comment provided by engineer.
+
Simplified incognito mode
Опростен режим инкогнито
@@ -6069,6 +6257,10 @@ Enable in *Network & servers* settings.
Soft
blur media
+
+ Some app settings were not migrated.
+ No comment provided by engineer.
+
Some file(s) were not exported:
No comment provided by engineer.
@@ -6201,6 +6393,14 @@ Enable in *Network & servers* settings.
Подкрепете SimpleX Chat
No comment provided by engineer.
+
+ Switch audio and video during the call.
+ No comment provided by engineer.
+
+
+ Switch chat profile for 1-time invitations.
+ No comment provided by engineer.
+
System
Системен
@@ -6235,6 +6435,10 @@ Enable in *Network & servers* settings.
TCP_KEEPINTVL
No comment provided by engineer.
+
+ Tail
+ No comment provided by engineer.
+
Take picture
Направи снимка
@@ -6423,6 +6627,10 @@ It can happen because of some bug or when the connection is compromised.Текстът, който поставихте, не е SimpleX линк за връзка.
No comment provided by engineer.
+
+ The uploaded database archive will be permanently removed from the servers.
+ No comment provided by engineer.
+
Themes
No comment provided by engineer.
@@ -6546,6 +6754,14 @@ You will be prompted to complete authentication before this feature is enabled.<
Ще бъдете подканени да извършите идентификация, преди тази функция да бъде активирана.
No comment provided by engineer.
+
+ To record speech please grant permission to use Microphone.
+ No comment provided by engineer.
+
+
+ To record video please grant permission to use Camera.
+ No comment provided by engineer.
+
To record voice message please grant permission to use Microphone.
За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон.
@@ -6694,7 +6910,7 @@ You will be prompted to complete authentication before this feature is enabled.<
Unknown servers!
- No comment provided by engineer.
+ alert title
Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.
@@ -6804,6 +7020,10 @@ To connect, please ask your contact to create another connection link and check
Използвай .onion хостове
No comment provided by engineer.
+
+ Use SOCKS proxy
+ No comment provided by engineer.
+
Use SimpleX Chat servers?
Използвай сървърите на SimpleX Chat?
@@ -6866,15 +7086,14 @@ To connect, please ask your contact to create another connection link and check
Use the app with one hand.
No comment provided by engineer.
-
- User profile
- Потребителски профил
- No comment provided by engineer.
-
User selection
No comment provided by engineer.
+
+ Username
+ No comment provided by engineer.
+
Using SimpleX Chat servers.
Използват се сървърите на SimpleX Chat.
@@ -7104,7 +7323,7 @@ To connect, please ask your contact to create another connection link and check
Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.
- No comment provided by engineer.
+ alert message
Wrong database passphrase
@@ -7128,11 +7347,6 @@ To connect, please ask your contact to create another connection link and check
XFTP server
No comment provided by engineer.
-
- You
- Вие
- No comment provided by engineer.
-
You **must not** use the same database on two devices.
**Не трябва** да използвате една и съща база данни на две устройства.
@@ -7475,11 +7689,19 @@ Repeat connection request?
Вашата чат база данни не е криптирана - задайте парола, за да я криптирате.
No comment provided by engineer.
+
+ Your chat preferences
+ alert title
+
Your chat profiles
Вашите чат профили
No comment provided by engineer.
+
+ Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.
+ No comment provided by engineer.
+
Your contact sent a file that is larger than currently supported maximum size (%@).
Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@).
@@ -7495,6 +7717,10 @@ Repeat connection request?
Вашите контакти ще останат свързани.
No comment provided by engineer.
+
+ Your credentials may be sent unencrypted.
+ No comment provided by engineer.
+
Your current chat database will be DELETED and REPLACED with the imported one.
Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната.
@@ -7525,13 +7751,15 @@ Repeat connection request?
Вашият профил **%@** ще бъде споделен.
No comment provided by engineer.
-
- Your profile is stored on your device and shared only with your contacts.
-SimpleX servers cannot see your profile.
- Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти.
-SimpleX сървърите не могат да видят вашия профил.
+
+ Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.
+ Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил.
No comment provided by engineer.
+
+ Your profile was changed. If you save it, the updated profile will be sent to all your contacts.
+ alert message
+
Your profile, contacts and delivered messages are stored on your device.
Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство.
diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff
index a02203e630..af56b6631f 100644
--- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff
+++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff
@@ -135,6 +135,10 @@
%@ se chce připojit!
notification title
+
+ %1$@, %2$@
+ format for date separator in chat
+
%@, %@ and %lld members
No comment provided by engineer.
@@ -154,11 +158,31 @@
%d dní
time interval
+
+ %d file(s) are still being downloaded.
+ forward confirmation reason
+
+
+ %d file(s) failed to download.
+ forward confirmation reason
+
+
+ %d file(s) were deleted.
+ forward confirmation reason
+
+
+ %d file(s) were not downloaded.
+ forward confirmation reason
+
%d hours
%d hodin
time interval
+
+ %d messages not forwarded
+ alert title
+
%d min
%d minuty
@@ -691,7 +715,7 @@
All profiles
- No comment provided by engineer.
+ profile dropdown
All your contacts will remain connected.
@@ -874,6 +898,10 @@
Přístupový kód aplikace je nahrazen sebedestrukčním přístupovým heslem.
No comment provided by engineer.
+
+ App session
+ No comment provided by engineer.
+
App version
Verze aplikace
@@ -973,6 +1001,10 @@
Automaticky přijímat obrázky
No comment provided by engineer.
+
+ Auto-accept settings
+ alert title
+
Back
Zpět
@@ -996,10 +1028,18 @@
Špatný hash zprávy
No comment provided by engineer.
+
+ Better calls
+ No comment provided by engineer.
+
Better groups
No comment provided by engineer.
+
+ Better message dates.
+ No comment provided by engineer.
+
Better messages
Lepší zprávy
@@ -1009,6 +1049,18 @@
Better networking
No comment provided by engineer.
+
+ Better notifications
+ No comment provided by engineer.
+
+
+ Better security ✅
+ No comment provided by engineer.
+
+
+ Better user experience
+ No comment provided by engineer.
+
Black
No comment provided by engineer.
@@ -1127,7 +1179,7 @@
Cancel
Zrušit
- No comment provided by engineer.
+ alert button
Cancel migration
@@ -1145,7 +1197,7 @@
Cannot receive file
Nelze přijmout soubor
- No comment provided by engineer.
+ alert title
Capacity exceeded - recipient did not receive previously sent messages.
@@ -1266,6 +1318,15 @@
Předvolby chatu
No comment provided by engineer.
+
+ Chat preferences were changed.
+ alert message
+
+
+ Chat profile
+ Profil uživatele
+ No comment provided by engineer.
+
Chat theme
No comment provided by engineer.
@@ -1623,6 +1684,10 @@ This is your own one-time link!
Verze jádra: v%@
No comment provided by engineer.
+
+ Corner
+ No comment provided by engineer.
+
Correct name to %@?
No comment provided by engineer.
@@ -1738,6 +1803,10 @@ This is your own one-time link!
Vlastní čas
No comment provided by engineer.
+
+ Customizable message shape.
+ No comment provided by engineer.
+
Customize theme
No comment provided by engineer.
@@ -2024,6 +2093,10 @@ This is your own one-time link!
Smazat starou databázi?
No comment provided by engineer.
+
+ Delete or moderate up to 200 messages.
+ No comment provided by engineer.
+
Delete pending connection?
Smazat čekající připojení?
@@ -2255,6 +2328,10 @@ This is your own one-time link!
Do not send history to new members.
No comment provided by engineer.
+
+ Do not use credentials with proxy.
+ No comment provided by engineer.
+
Don't create address
Nevytvářet adresu
@@ -2277,7 +2354,8 @@ This is your own one-time link!
Download
- chat item action
+ alert button
+ chat item action
Download errors
@@ -2292,6 +2370,10 @@ This is your own one-time link!
Stáhnout soubor
server test step
+
+ Download files
+ alert action
+
Downloaded
No comment provided by engineer.
@@ -2552,6 +2634,10 @@ This is your own one-time link!
Chuba změny adresy
No comment provided by engineer.
+
+ Error changing connection profile
+ No comment provided by engineer.
+
Error changing role
Chyba při změně role
@@ -2562,6 +2648,10 @@ This is your own one-time link!
Chyba změny nastavení
No comment provided by engineer.
+
+ Error changing to incognito!
+ No comment provided by engineer.
+
Error connecting to forwarding server %@. Please try later.
No comment provided by engineer.
@@ -2678,6 +2768,10 @@ This is your own one-time link!
Chyba načítání %@ serverů
No comment provided by engineer.
+
+ Error migrating settings
+ No comment provided by engineer.
+
Error opening chat
No comment provided by engineer.
@@ -2685,7 +2779,7 @@ This is your own one-time link!
Error receiving file
Chyba při příjmu souboru
- No comment provided by engineer.
+ alert title
Error reconnecting server
@@ -2772,10 +2866,14 @@ This is your own one-time link!
Chyba při zastavení chatu
No comment provided by engineer.
+
+ Error switching profile
+ No comment provided by engineer.
+
Error switching profile!
Chyba při přepínání profilu!
- No comment provided by engineer.
+ alertTitle
Error synchronizing connection
@@ -2818,8 +2916,7 @@ This is your own one-time link!
Error: %@
Chyba: %@
- file error text
- snd error text
+ alert message
Error: URL is invalid
@@ -2900,6 +2997,11 @@ This is your own one-time link!
File error
No comment provided by engineer.
+
+ File errors:
+%@
+ alert message
+
File not found - most likely file was deleted or cancelled.
file error text
@@ -3026,10 +3128,26 @@ This is your own one-time link!
Forward
chat item action
+
+ Forward %d message(s)?
+ alert title
+
Forward and save messages
No comment provided by engineer.
+
+ Forward messages
+ alert action
+
+
+ Forward messages without files?
+ alert message
+
+
+ Forward up to 20 messages at once.
+ No comment provided by engineer.
+
Forwarded
No comment provided by engineer.
@@ -3038,6 +3156,10 @@ This is your own one-time link!
Forwarded from
No comment provided by engineer.
+
+ Forwarding %lld messages
+ No comment provided by engineer.
+
Forwarding server %@ failed to connect to destination server %@. Please try later.
No comment provided by engineer.
@@ -3079,11 +3201,6 @@ Error: %2$@
Celé jméno (volitelně)
No comment provided by engineer.
-
- Full name:
- Celé jméno:
- No comment provided by engineer.
-
Fully decentralized – visible only to members.
No comment provided by engineer.
@@ -3321,6 +3438,10 @@ Error: %2$@
Servery ICE (jeden na řádek)
No comment provided by engineer.
+
+ IP address
+ No comment provided by engineer.
+
If you can't meet in person, show QR code in a video call, or share the link.
Pokud se nemůžete setkat osobně, zobrazte QR kód ve videohovoru nebo sdílejte odkaz.
@@ -3393,6 +3514,11 @@ Error: %2$@
Importing archive
No comment provided by engineer.
+
+ Improved delivery, reduced traffic usage.
+More improvements are coming soon!
+ No comment provided by engineer.
+
Improved message delivery
No comment provided by engineer.
@@ -3928,6 +4054,10 @@ This is your link for group %@!
Message servers
No comment provided by engineer.
+
+ Message shape
+ No comment provided by engineer.
+
Message source remains private.
No comment provided by engineer.
@@ -3971,6 +4101,10 @@ This is your link for group %@!
Messages sent
No comment provided by engineer.
+
+ Messages were deleted after you selected them.
+ alert message
+
Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.
No comment provided by engineer.
@@ -4113,6 +4247,14 @@ This is your link for group %@!
Nové heslo
No comment provided by engineer.
+
+ New SOCKS credentials will be used every time you start the app.
+ No comment provided by engineer.
+
+
+ New SOCKS credentials will be used for each server.
+ No comment provided by engineer.
+
New chat
No comment provided by engineer.
@@ -4227,6 +4369,14 @@ This is your link for group %@!
No network connection
No comment provided by engineer.
+
+ No permission to record speech
+ No comment provided by engineer.
+
+
+ No permission to record video
+ No comment provided by engineer.
+
No permission to record voice message
Nemáte oprávnění nahrávat hlasové zprávy
@@ -4245,6 +4395,10 @@ This is your link for group %@!
Nothing selected
No comment provided by engineer.
+
+ Nothing to forward!
+ alert title
+
Notifications
Oznámení
@@ -4276,7 +4430,7 @@ This is your link for group %@!
Ok
Ok
- No comment provided by engineer.
+ alert button
Old database
@@ -4456,6 +4610,11 @@ Vyžaduje povolení sítě VPN.
Other %@ servers
No comment provided by engineer.
+
+ Other file errors:
+%@
+ alert message
+
PING count
Počet PING
@@ -4491,6 +4650,10 @@ Vyžaduje povolení sítě VPN.
Heslo nastaveno!
No comment provided by engineer.
+
+ Password
+ No comment provided by engineer.
+
Password to show
Heslo k zobrazení
@@ -4627,6 +4790,10 @@ Error: %@
Polské rozhraní
No comment provided by engineer.
+
+ Port
+ No comment provided by engineer.
+
Possibly, certificate fingerprint in server address is incorrect
Je možné, že otisk certifikátu v adrese serveru je nesprávný
@@ -4705,14 +4872,6 @@ Error: %@
Profile images
No comment provided by engineer.
-
- Profile name
- No comment provided by engineer.
-
-
- Profile name:
- No comment provided by engineer.
-
Profile password
Heslo profilu
@@ -4808,6 +4967,10 @@ Enable in *Network & servers* settings.
Proxied servers
No comment provided by engineer.
+
+ Proxy requires password
+ No comment provided by engineer.
+
Push notifications
Nabízená oznámení
@@ -5014,6 +5177,10 @@ Enable in *Network & servers* settings.
Odstranit
No comment provided by engineer.
+
+ Remove archive?
+ No comment provided by engineer.
+
Remove image
No comment provided by engineer.
@@ -5181,6 +5348,10 @@ Enable in *Network & servers* settings.
SMP server
No comment provided by engineer.
+
+ SOCKS proxy
+ No comment provided by engineer.
+
Safely receive files
No comment provided by engineer.
@@ -5192,12 +5363,13 @@ Enable in *Network & servers* settings.
Save
Uložit
- chat item action
+ alert button
+ chat item action
Save (and notify contacts)
Uložit (a informovat kontakty)
- No comment provided by engineer.
+ alert button
Save and notify contact
@@ -5223,11 +5395,6 @@ Enable in *Network & servers* settings.
Uložit archiv
No comment provided by engineer.
-
- Save auto-accept settings
- Uložit nastavení automatického přijímání
- No comment provided by engineer.
-
Save group profile
Uložení profilu skupiny
@@ -5263,16 +5430,15 @@ Enable in *Network & servers* settings.
Uložit servery?
No comment provided by engineer.
-
- Save settings?
- Uložit nastavení?
- No comment provided by engineer.
-
Save welcome message?
Uložit uvítací zprávu?
No comment provided by engineer.
+
+ Save your profile?
+ alert title
+
Saved
No comment provided by engineer.
@@ -5290,6 +5456,10 @@ Enable in *Network & servers* settings.
Saved message
message info title
+
+ Saving %lld messages
+ No comment provided by engineer.
+
Scale
No comment provided by engineer.
@@ -5363,6 +5533,10 @@ Enable in *Network & servers* settings.
Vybrat
chat item action
+
+ Select chat profile
+ No comment provided by engineer.
+
Selected %lld
No comment provided by engineer.
@@ -5474,7 +5648,7 @@ Enable in *Network & servers* settings.
Sender cancelled file transfer.
Odesílatel zrušil přenos souboru.
- No comment provided by engineer.
+ alert message
Sender may have deleted the connection request.
@@ -5566,6 +5740,10 @@ Enable in *Network & servers* settings.
Sent via proxy
No comment provided by engineer.
+
+ Server
+ No comment provided by engineer.
+
Server address
No comment provided by engineer.
@@ -5675,6 +5853,10 @@ Enable in *Network & servers* settings.
Nastavení
No comment provided by engineer.
+
+ Settings were changed.
+ alert message
+
Shape profile images
No comment provided by engineer.
@@ -5708,6 +5890,10 @@ Enable in *Network & servers* settings.
Sdílet odkaz
No comment provided by engineer.
+
+ Share profile
+ No comment provided by engineer.
+
Share this 1-time invite link
No comment provided by engineer.
@@ -5834,6 +6020,10 @@ Enable in *Network & servers* settings.
Jednorázová pozvánka SimpleX
simplex link type
+
+ SimpleX protocols reviewed by Trail of Bits.
+ No comment provided by engineer.
+
Simplified incognito mode
Zjednodušený inkognito režim
@@ -5862,6 +6052,10 @@ Enable in *Network & servers* settings.
Soft
blur media
+
+ Some app settings were not migrated.
+ No comment provided by engineer.
+
Some file(s) were not exported:
No comment provided by engineer.
@@ -5990,6 +6184,14 @@ Enable in *Network & servers* settings.
Podpořte SimpleX Chat
No comment provided by engineer.
+
+ Switch audio and video during the call.
+ No comment provided by engineer.
+
+
+ Switch chat profile for 1-time invitations.
+ No comment provided by engineer.
+
System
Systém
@@ -6024,6 +6226,10 @@ Enable in *Network & servers* settings.
TCP_KEEPINTVL
No comment provided by engineer.
+
+ Tail
+ No comment provided by engineer.
+
Take picture
Vyfotit
@@ -6207,6 +6413,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
The text you pasted is not a SimpleX link.
No comment provided by engineer.
+
+ The uploaded database archive will be permanently removed from the servers.
+ No comment provided by engineer.
+
Themes
No comment provided by engineer.
@@ -6323,6 +6533,14 @@ You will be prompted to complete authentication before this feature is enabled.<
Před zapnutím této funkce budete vyzváni k dokončení ověření.
No comment provided by engineer.
+
+ To record speech please grant permission to use Microphone.
+ No comment provided by engineer.
+
+
+ To record video please grant permission to use Camera.
+ No comment provided by engineer.
+
To record voice message please grant permission to use Microphone.
Chcete-li nahrávat hlasové zprávy, udělte povolení k použití mikrofonu.
@@ -6465,7 +6683,7 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.
Unknown servers!
- No comment provided by engineer.
+ alert title
Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.
@@ -6570,6 +6788,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
Použít hostitele .onion
No comment provided by engineer.
+
+ Use SOCKS proxy
+ No comment provided by engineer.
+
Use SimpleX Chat servers?
Používat servery SimpleX Chat?
@@ -6629,15 +6851,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
Use the app with one hand.
No comment provided by engineer.
-
- User profile
- Profil uživatele
- No comment provided by engineer.
-
User selection
No comment provided by engineer.
+
+ Username
+ No comment provided by engineer.
+
Using SimpleX Chat servers.
Používat servery SimpleX Chat.
@@ -6850,7 +7071,7 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.
- No comment provided by engineer.
+ alert message
Wrong database passphrase
@@ -6874,11 +7095,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
XFTP server
No comment provided by engineer.
-
- You
- Vy
- No comment provided by engineer.
-
You **must not** use the same database on two devices.
No comment provided by engineer.
@@ -7204,11 +7420,19 @@ Repeat connection request?
Vaše chat databáze není šifrována – nastavte přístupovou frázi pro její šifrování.
No comment provided by engineer.
+
+ Your chat preferences
+ alert title
+
Your chat profiles
Vaše chat profily
No comment provided by engineer.
+
+ Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.
+ No comment provided by engineer.
+
Your contact sent a file that is larger than currently supported maximum size (%@).
Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@).
@@ -7224,6 +7448,10 @@ Repeat connection request?
Vaše kontakty zůstanou připojeny.
No comment provided by engineer.
+
+ Your credentials may be sent unencrypted.
+ No comment provided by engineer.
+
Your current chat database will be DELETED and REPLACED with the imported one.
Vaše aktuální chat databáze bude ODSTRANĚNA a NAHRAZENA importovanou.
@@ -7253,13 +7481,15 @@ Repeat connection request?
Váš profil **%@** bude sdílen.
No comment provided by engineer.
-
- Your profile is stored on your device and shared only with your contacts.
-SimpleX servers cannot see your profile.
- Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty.
-Servery SimpleX nevidí váš profil.
+
+ Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.
+ Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil.
No comment provided by engineer.
+
+ Your profile was changed. If you save it, the updated profile will be sent to all your contacts.
+ alert message
+
Your profile, contacts and delivered messages are stored on your device.
Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.
diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff
index 29adfbabf0..7ab1e3a588 100644
--- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff
+++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff
@@ -137,6 +137,11 @@
%@ will sich mit Ihnen verbinden!
notification title
+
+ %1$@, %2$@
+ %1$@, %2$@
+ format for date separator in chat
+
%@, %@ and %lld members
%@, %@ und %lld Mitglieder
@@ -157,11 +162,36 @@
%d Tage
time interval
+
+ %d file(s) are still being downloaded.
+ %d Datei(en) wird/werden immer noch heruntergeladen.
+ forward confirmation reason
+
+
+ %d file(s) failed to download.
+ Bei %d Datei(en) ist das Herunterladen fehlgeschlagen.
+ forward confirmation reason
+
+
+ %d file(s) were deleted.
+ %d Datei(en) wurde(n) gelöscht.
+ forward confirmation reason
+
+
+ %d file(s) were not downloaded.
+ %d Datei(en) wurde(n) nicht heruntergeladen.
+ forward confirmation reason
+
%d hours
%d Stunden
time interval
+
+ %d messages not forwarded
+ %d Nachrichten wurden nicht weitergeleitet
+ alert title
+
%d min
%d min
@@ -723,7 +753,7 @@
All profiles
Alle Profile
- No comment provided by engineer.
+ profile dropdown
All your contacts will remain connected.
@@ -915,6 +945,11 @@
App-Zugangscode wurde durch den Selbstzerstörungs-Zugangscode ersetzt.
No comment provided by engineer.
+
+ App session
+ App-Sitzung
+ No comment provided by engineer.
+
App version
App Version
@@ -1020,6 +1055,11 @@
Bilder automatisch akzeptieren
No comment provided by engineer.
+
+ Auto-accept settings
+ Einstellungen automatisch akzeptieren
+ alert title
+
Back
Zurück
@@ -1045,11 +1085,21 @@
Ungültiger Nachrichten-Hash
No comment provided by engineer.
+
+ Better calls
+ Verbesserte Anrufe
+ No comment provided by engineer.
+
Better groups
Bessere Gruppen
No comment provided by engineer.
+
+ Better message dates.
+ Verbesserte Nachrichten-Datumsinformation
+ No comment provided by engineer.
+
Better messages
Verbesserungen bei Nachrichten
@@ -1060,6 +1110,21 @@
Kontrollieren Sie Ihr Netzwerk
No comment provided by engineer.
+
+ Better notifications
+ Verbesserte Benachrichtigungen
+ No comment provided by engineer.
+
+
+ Better security ✅
+ Verbesserte Sicherheit ✅
+ No comment provided by engineer.
+
+
+ Better user experience
+ Verbesserte Nutzer-Erfahrung
+ No comment provided by engineer.
+
Black
Schwarz
@@ -1193,7 +1258,7 @@
Cancel
Abbrechen
- No comment provided by engineer.
+ alert button
Cancel migration
@@ -1213,7 +1278,7 @@
Cannot receive file
Datei kann nicht empfangen werden
- No comment provided by engineer.
+ alert title
Capacity exceeded - recipient did not receive previously sent messages.
@@ -1341,6 +1406,16 @@
Chat-Präferenzen
No comment provided by engineer.
+
+ Chat preferences were changed.
+ Die Chat-Präferenzen wurden geändert.
+ alert message
+
+
+ Chat profile
+ Benutzerprofil
+ No comment provided by engineer.
+
Chat theme
Chat-Design
@@ -1393,22 +1468,22 @@
Clear
- Löschen
+ Entfernen
swipe action
Clear conversation
- Chatinhalte löschen
+ Chat-Inhalte entfernen
No comment provided by engineer.
Clear conversation?
- Unterhaltung löschen?
+ Chat-Inhalte entfernen?
No comment provided by engineer.
Clear private notes?
- Private Notizen löschen?
+ Private Notizen entfernen?
No comment provided by engineer.
@@ -1722,7 +1797,7 @@ Das ist Ihr eigener Einmal-Link!
Conversation deleted!
- Unterhaltung gelöscht!
+ Chat-Inhalte entfernt!
No comment provided by engineer.
@@ -1740,6 +1815,11 @@ Das ist Ihr eigener Einmal-Link!
Core Version: v%@
No comment provided by engineer.
+
+ Corner
+ Abrundung Ecken
+ No comment provided by engineer.
+
Correct name to %@?
Richtiger Name für %@?
@@ -1865,6 +1945,11 @@ Das ist Ihr eigener Einmal-Link!
Zeit anpassen
No comment provided by engineer.
+
+ Customizable message shape.
+ Anpassbares Format des Nachrichtenfelds
+ No comment provided by engineer.
+
Customize theme
Design anpassen
@@ -2159,6 +2244,11 @@ Das ist Ihr eigener Einmal-Link!
Alte Datenbank löschen?
No comment provided by engineer.
+
+ Delete or moderate up to 200 messages.
+ Bis zu 200 Nachrichten löschen oder moderieren
+ No comment provided by engineer.
+
Delete pending connection?
Ausstehende Verbindung löschen?
@@ -2409,6 +2499,11 @@ Das ist Ihr eigener Einmal-Link!
Den Nachrichtenverlauf nicht an neue Mitglieder senden.
No comment provided by engineer.
+
+ Do not use credentials with proxy.
+ Verwenden Sie keine Anmeldeinformationen mit einem Proxy.
+ No comment provided by engineer.
+
Don't create address
Keine Adresse erstellt
@@ -2432,7 +2527,8 @@ Das ist Ihr eigener Einmal-Link!
Download
Herunterladen
- chat item action
+ alert button
+ chat item action
Download errors
@@ -2449,6 +2545,11 @@ Das ist Ihr eigener Einmal-Link!
Datei herunterladen
server test step
+
+ Download files
+ Dateien herunterladen
+ alert action
+
Downloaded
Heruntergeladen
@@ -2724,6 +2825,11 @@ Das ist Ihr eigener Einmal-Link!
Fehler beim Wechseln der Empfängeradresse
No comment provided by engineer.
+
+ Error changing connection profile
+ Fehler beim Wechseln des Verbindungs-Profils
+ No comment provided by engineer.
+
Error changing role
Fehler beim Ändern der Rolle
@@ -2734,6 +2840,11 @@ Das ist Ihr eigener Einmal-Link!
Fehler beim Ändern der Einstellung
No comment provided by engineer.
+
+ Error changing to incognito!
+ Fehler beim Wechseln zum Inkognito-Profil!
+ No comment provided by engineer.
+
Error connecting to forwarding server %@. Please try later.
Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut.
@@ -2854,6 +2965,11 @@ Das ist Ihr eigener Einmal-Link!
Fehler beim Laden von %@ Servern
No comment provided by engineer.
+
+ Error migrating settings
+ Fehler beim Migrieren der Einstellungen
+ No comment provided by engineer.
+
Error opening chat
Fehler beim Öffnen des Chats
@@ -2862,7 +2978,7 @@ Das ist Ihr eigener Einmal-Link!
Error receiving file
Fehler beim Empfangen der Datei
- No comment provided by engineer.
+ alert title
Error reconnecting server
@@ -2954,10 +3070,15 @@ Das ist Ihr eigener Einmal-Link!
Fehler beim Beenden des Chats
No comment provided by engineer.
+
+ Error switching profile
+ Fehler beim Wechseln des Profils
+ No comment provided by engineer.
+
Error switching profile!
Fehler beim Umschalten des Profils!
- No comment provided by engineer.
+ alertTitle
Error synchronizing connection
@@ -3002,8 +3123,7 @@ Das ist Ihr eigener Einmal-Link!
Error: %@
Fehler: %@
- file error text
- snd error text
+ alert message
Error: URL is invalid
@@ -3090,6 +3210,13 @@ Das ist Ihr eigener Einmal-Link!
Datei-Fehler
No comment provided by engineer.
+
+ File errors:
+%@
+ Datei-Fehler:
+%@
+ alert message
+
File not found - most likely file was deleted or cancelled.
Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen.
@@ -3225,11 +3352,31 @@ Das ist Ihr eigener Einmal-Link!
Weiterleiten
chat item action
+
+ Forward %d message(s)?
+ %d Nachricht(en) weiterleiten?
+ alert title
+
Forward and save messages
Nachrichten weiterleiten und speichern
No comment provided by engineer.
+
+ Forward messages
+ Nachrichten weiterleiten
+ alert action
+
+
+ Forward messages without files?
+ Nachrichten ohne Dateien weiterleiten?
+ alert message
+
+
+ Forward up to 20 messages at once.
+ Bis zu 20 Nachrichten auf einmal weiterleiten
+ No comment provided by engineer.
+
Forwarded
Weitergeleitet
@@ -3240,6 +3387,11 @@ Das ist Ihr eigener Einmal-Link!
Weitergeleitet aus
No comment provided by engineer.
+
+ Forwarding %lld messages
+ %lld Nachricht(en) wird/werden weitergeleitet
+ No comment provided by engineer.
+
Forwarding server %@ failed to connect to destination server %@. Please try later.
Weiterleitungsserver %@ konnte sich nicht mit dem Zielserver %@ verbinden. Bitte versuchen Sie es später erneut.
@@ -3289,11 +3441,6 @@ Fehler: %2$@
Vollständiger Name (optional)
No comment provided by engineer.
-
- Full name:
- Vollständiger Name:
- No comment provided by engineer.
-
Fully decentralized – visible only to members.
Vollständig dezentralisiert – nur für Mitglieder sichtbar.
@@ -3451,7 +3598,7 @@ Fehler: %2$@
Group will be deleted for you - this cannot be undone!
- Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!
+ Die Gruppe wird nur bei Ihnen gelöscht. Dies kann nicht rückgängig gemacht werden!
No comment provided by engineer.
@@ -3539,6 +3686,11 @@ Fehler: %2$@
ICE-Server (einer pro Zeile)
No comment provided by engineer.
+
+ IP address
+ IP-Adresse
+ No comment provided by engineer.
+
If you can't meet in person, show QR code in a video call, or share the link.
Falls Sie sich nicht persönlich treffen können, zeigen Sie den QR-Code in einem Videoanruf oder teilen Sie den Link.
@@ -3614,6 +3766,13 @@ Fehler: %2$@
Archiv wird importiert
No comment provided by engineer.
+
+ Improved delivery, reduced traffic usage.
+More improvements are coming soon!
+ Verbesserte Nachrichten-Auslieferung und verringerter Datenverbrauch.
+Weitere Verbesserungen sind bald verfügbar!
+ No comment provided by engineer.
+
Improved message delivery
Verbesserte Zustellung von Nachrichten
@@ -3906,7 +4065,7 @@ Das ist Ihr Link für die Gruppe %@!
Keep conversation
- Unterhaltung behalten
+ Chat-Inhalte beibehalten
No comment provided by engineer.
@@ -4184,6 +4343,11 @@ Das ist Ihr Link für die Gruppe %@!
Nachrichten-Server
No comment provided by engineer.
+
+ Message shape
+ Nachrichten-Form
+ No comment provided by engineer.
+
Message source remains private.
Die Nachrichtenquelle bleibt privat.
@@ -4234,6 +4398,11 @@ Das ist Ihr Link für die Gruppe %@!
Gesendete Nachrichten
No comment provided by engineer.
+
+ Messages were deleted after you selected them.
+ Die Nachrichten wurden gelöscht, nachdem Sie sie ausgewählt hatten.
+ alert message
+
Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.
Nachrichten, Dateien und Anrufe sind durch **Ende-zu-Ende-Verschlüsselung** mit Perfect Forward Secrecy, Ablehnung und Einbruchs-Wiederherstellung geschützt.
@@ -4389,6 +4558,16 @@ Das ist Ihr Link für die Gruppe %@!
Neuer Zugangscode
No comment provided by engineer.
+
+ New SOCKS credentials will be used every time you start the app.
+ Jedes Mal wenn Sie die App starten, werden neue SOCKS-Anmeldeinformationen genutzt
+ No comment provided by engineer.
+
+
+ New SOCKS credentials will be used for each server.
+ Für jeden Server werden neue SOCKS-Anmeldeinformationen genutzt
+ No comment provided by engineer.
+
New chat
Neuer Chat
@@ -4509,6 +4688,16 @@ Das ist Ihr Link für die Gruppe %@!
Keine Netzwerkverbindung
No comment provided by engineer.
+
+ No permission to record speech
+ Keine Genehmigung für Sprach-Aufnahmen
+ No comment provided by engineer.
+
+
+ No permission to record video
+ Keine Genehmigung für Video-Aufnahmen
+ No comment provided by engineer.
+
No permission to record voice message
Keine Berechtigung für das Aufnehmen von Sprachnachrichten
@@ -4529,6 +4718,11 @@ Das ist Ihr Link für die Gruppe %@!
Nichts ausgewählt
No comment provided by engineer.
+
+ Nothing to forward!
+ Es gibt nichts zum Weiterleiten!
+ alert title
+
Notifications
Benachrichtigungen
@@ -4561,7 +4755,7 @@ Das ist Ihr Link für die Gruppe %@!
Ok
Ok
- No comment provided by engineer.
+ alert button
Old database
@@ -4604,7 +4798,7 @@ Dies erfordert die Aktivierung eines VPNs.
Only delete conversation
- Nur die Unterhaltung löschen
+ Nur die Chat-Inhalte löschen
No comment provided by engineer.
@@ -4752,6 +4946,13 @@ Dies erfordert die Aktivierung eines VPNs.
Andere %@ Server
No comment provided by engineer.
+
+ Other file errors:
+%@
+ Andere(r) Datei-Fehler:
+%@
+ alert message
+
PING count
PING-Zähler
@@ -4787,6 +4988,11 @@ Dies erfordert die Aktivierung eines VPNs.
Zugangscode eingestellt!
No comment provided by engineer.
+
+ Password
+ Passwort
+ No comment provided by engineer.
+
Password to show
Passwort anzeigen
@@ -4936,6 +5142,11 @@ Fehler: %@
Polnische Bedienoberfläche
No comment provided by engineer.
+
+ Port
+ Port
+ No comment provided by engineer.
+
Possibly, certificate fingerprint in server address is incorrect
Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig
@@ -5021,16 +5232,6 @@ Fehler: %@
Profil-Bilder
No comment provided by engineer.
-
- Profile name
- Profilname
- No comment provided by engineer.
-
-
- Profile name:
- Profilname:
- No comment provided by engineer.
-
Profile password
Passwort für Profil
@@ -5133,6 +5334,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Proxy-Server
No comment provided by engineer.
+
+ Proxy requires password
+ Der Proxy benötigt ein Passwort
+ No comment provided by engineer.
+
Push notifications
Push-Benachrichtigungen
@@ -5155,7 +5361,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Reachable chat toolbar
- Erreichbare Chat-Symbolleiste
+ Chat-Symbolleiste unten
No comment provided by engineer.
@@ -5354,6 +5560,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Entfernen
No comment provided by engineer.
+
+ Remove archive?
+ Archiv entfernen?
+ No comment provided by engineer.
+
Remove image
Bild entfernen
@@ -5534,6 +5745,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
SMP-Server
No comment provided by engineer.
+
+ SOCKS proxy
+ SOCKS-Proxy
+ No comment provided by engineer.
+
Safely receive files
Dateien sicher empfangen
@@ -5547,12 +5763,13 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Save
Speichern
- chat item action
+ alert button
+ chat item action
Save (and notify contacts)
Speichern (und Kontakte benachrichtigen)
- No comment provided by engineer.
+ alert button
Save and notify contact
@@ -5579,11 +5796,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Archiv speichern
No comment provided by engineer.
-
- Save auto-accept settings
- Einstellungen von "Automatisch akzeptieren" speichern
- No comment provided by engineer.
-
Save group profile
Gruppenprofil speichern
@@ -5619,16 +5831,16 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Alle Server speichern?
No comment provided by engineer.
-
- Save settings?
- Einstellungen speichern?
- No comment provided by engineer.
-
Save welcome message?
Begrüßungsmeldung speichern?
No comment provided by engineer.
+
+ Save your profile?
+ Ihr Profil speichern?
+ alert title
+
Saved
Abgespeichert
@@ -5649,6 +5861,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Gespeicherte Nachricht
message info title
+
+ Saving %lld messages
+ Es wird/werden %lld Nachricht(en) gesichert
+ No comment provided by engineer.
+
Scale
Skalieren
@@ -5696,7 +5913,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Search or paste SimpleX link
- Suchen oder fügen Sie den SimpleX-Link ein
+ Suchen oder SimpleX-Link einfügen
No comment provided by engineer.
@@ -5729,6 +5946,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Auswählen
chat item action
+
+ Select chat profile
+ Chat-Profil auswählen
+ No comment provided by engineer.
+
Selected %lld
%lld ausgewählt
@@ -5847,7 +6069,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Sender cancelled file transfer.
Der Absender hat die Dateiübertragung abgebrochen.
- No comment provided by engineer.
+ alert message
Sender may have deleted the connection request.
@@ -5944,6 +6166,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Über einen Proxy gesendet
No comment provided by engineer.
+
+ Server
+ Server
+ No comment provided by engineer.
+
Server address
Server-Adresse
@@ -6064,6 +6291,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Einstellungen
No comment provided by engineer.
+
+ Settings were changed.
+ Die Einstellungen wurden geändert.
+ alert message
+
Shape profile images
Form der Profil-Bilder
@@ -6099,6 +6331,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Link teilen
No comment provided by engineer.
+
+ Share profile
+ Profil teilen
+ No comment provided by engineer.
+
Share this 1-time invite link
Teilen Sie diesen Einmal-Einladungslink
@@ -6234,6 +6471,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
SimpleX-Einmal-Einladung
simplex link type
+
+ SimpleX protocols reviewed by Trail of Bits.
+ Die SimpleX-Protokolle wurden von Trail of Bits überprüft.
+ No comment provided by engineer.
+
Simplified incognito mode
Vereinfachter Inkognito-Modus
@@ -6264,6 +6506,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Weich
blur media
+
+ Some app settings were not migrated.
+ Einige App-Einstellungen wurden nicht migriert.
+ No comment provided by engineer.
+
Some file(s) were not exported:
Einzelne Datei(en) wurde(n) nicht exportiert:
@@ -6404,6 +6651,16 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
Unterstützung von SimpleX Chat
No comment provided by engineer.
+
+ Switch audio and video during the call.
+ Während des Anrufs zwischen Audio und Video wechseln
+ No comment provided by engineer.
+
+
+ Switch chat profile for 1-time invitations.
+ Das Chat-Profil für Einmal-Einladungen wechseln
+ No comment provided by engineer.
+
System
System
@@ -6439,6 +6696,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.
TCP_KEEPINTVL
No comment provided by engineer.
+
+ Tail
+ Sprechblasen-Format
+ No comment provided by engineer.
+
Take picture
Machen Sie ein Foto
@@ -6588,7 +6850,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
The messages will be deleted for all members.
- Die Nachrichten werden für alle Mitglieder gelöscht werden.
+ Die Nachrichten werden für alle Gruppenmitglieder gelöscht.
No comment provided by engineer.
@@ -6631,6 +6893,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
Der von Ihnen eingefügte Text ist kein SimpleX-Link.
No comment provided by engineer.
+
+ The uploaded database archive will be permanently removed from the servers.
+ Das hochgeladene Datenbank-Archiv wird dauerhaft von den Servern entfernt.
+ No comment provided by engineer.
+
Themes
Design
@@ -6758,6 +7025,16 @@ You will be prompted to complete authentication before this feature is enabled.<
Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird.
No comment provided by engineer.
+
+ To record speech please grant permission to use Microphone.
+ Bitte erteilen Sie für Sprach-Aufnahmen die Genehmigung das Mikrofon zu nutzen.
+ No comment provided by engineer.
+
+
+ To record video please grant permission to use Camera.
+ Bitte erteilen Sie für Video-Aufnahmen die Genehmigung die Kamera zu nutzen.
+ No comment provided by engineer.
+
To record voice message please grant permission to use Microphone.
Bitte erlauben Sie die Nutzung des Mikrofons, um Sprachnachrichten aufnehmen zu können.
@@ -6911,7 +7188,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
Unknown servers!
Unbekannte Server!
- No comment provided by engineer.
+ alert title
Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.
@@ -7025,6 +7302,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
Verwende .onion-Hosts
No comment provided by engineer.
+
+ Use SOCKS proxy
+ SOCKS-Proxy nutzen
+ No comment provided by engineer.
+
Use SimpleX Chat servers?
Verwenden Sie SimpleX-Chat-Server?
@@ -7087,12 +7369,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
Use the app with one hand.
- Die App mit einer Hand nutzen.
- No comment provided by engineer.
-
-
- User profile
- Benutzerprofil
+ Die App mit einer Hand bedienen.
No comment provided by engineer.
@@ -7100,6 +7377,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
Benutzer-Auswahl
No comment provided by engineer.
+
+ Username
+ Benutzername
+ No comment provided by engineer.
+
Using SimpleX Chat servers.
Verwendung von SimpleX-Chat-Servern.
@@ -7333,7 +7615,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.
Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Relais sichtbar sein: %@.
- No comment provided by engineer.
+ alert message
Wrong database passphrase
@@ -7360,11 +7642,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
XFTP-Server
No comment provided by engineer.
-
- You
- Profil
- No comment provided by engineer.
-
You **must not** use the same database on two devices.
Sie dürfen die selbe Datenbank **nicht** auf zwei Geräten nutzen.
@@ -7519,7 +7796,7 @@ Verbindungsanfrage wiederholen?
You can still view conversation with %@ in the list of chats.
- Sie können in der Chatliste weiterhin die Unterhaltung mit %@ einsehen.
+ Sie können in der Chat-Liste weiterhin die Unterhaltung mit %@ einsehen.
No comment provided by engineer.
@@ -7714,11 +7991,21 @@ Verbindungsanfrage wiederholen?
Ihre Chat-Datenbank ist nicht verschlüsselt. Bitte legen Sie ein Passwort fest, um sie zu schützen.
No comment provided by engineer.
+
+ Your chat preferences
+ Ihre Chat-Präferenzen
+ alert title
+
Your chat profiles
Ihre Chat-Profile
No comment provided by engineer.
+
+ Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.
+ Ihre Verbindung wurde auf %@ verschoben. Während Sie auf das Profil weitergeleitet wurden trat aber ein unerwarteter Fehler auf.
+ No comment provided by engineer.
+
Your contact sent a file that is larger than currently supported maximum size (%@).
Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@).
@@ -7734,6 +8021,11 @@ Verbindungsanfrage wiederholen?
Ihre Kontakte bleiben weiterhin verbunden.
No comment provided by engineer.
+
+ Your credentials may be sent unencrypted.
+ Ihre Anmeldeinformationen können unverschlüsselt versendet werden.
+ No comment provided by engineer.
+
Your current chat database will be DELETED and REPLACED with the imported one.
Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT.
@@ -7764,13 +8056,16 @@ Verbindungsanfrage wiederholen?
Ihr Profil **%@** wird geteilt.
No comment provided by engineer.
-
- Your profile is stored on your device and shared only with your contacts.
-SimpleX servers cannot see your profile.
- Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.
-SimpleX-Server können Ihr Profil nicht einsehen.
+
+ Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.
+ Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. SimpleX-Server können Ihr Profil nicht einsehen.
No comment provided by engineer.
+
+ Your profile was changed. If you save it, the updated profile will be sent to all your contacts.
+ Ihr Profil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an alle Ihre Kontakte gesendet.
+ alert message
+
Your profile, contacts and delivered messages are stored on your device.
Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert.
@@ -8208,7 +8503,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.
expired
- abgelaufen
+ Abgelaufen
No comment provided by engineer.
@@ -8440,7 +8735,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.
other
- andere
+ Andere
No comment provided by engineer.
diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff
index b8432a33b6..799c61b448 100644
--- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff
+++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff
@@ -4200,7 +4200,7 @@ SimpleX servers cannot see your profile.
## In reply to
- ## Ως απαντηση σε
+ ## Ως απάντηση σε
copied message info
diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff
index c217793f03..321b430a3f 100644
--- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff
+++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff
@@ -137,6 +137,11 @@
%@ wants to connect!
notification title
+
+ %1$@, %2$@
+ %1$@, %2$@
+ format for date separator in chat
+
%@, %@ and %lld members
%@, %@ and %lld members
@@ -157,11 +162,36 @@
%d days
time interval
+
+ %d file(s) are still being downloaded.
+ %d file(s) are still being downloaded.
+ forward confirmation reason
+
+
+ %d file(s) failed to download.
+ %d file(s) failed to download.
+ forward confirmation reason
+
+
+ %d file(s) were deleted.
+ %d file(s) were deleted.
+ forward confirmation reason
+
+
+ %d file(s) were not downloaded.
+ %d file(s) were not downloaded.
+ forward confirmation reason
+
%d hours
%d hours
time interval
+
+ %d messages not forwarded
+ %d messages not forwarded
+ alert title
+
%d min
%d min
@@ -723,7 +753,7 @@
All profiles
All profiles
- No comment provided by engineer.
+ profile dropdown
All your contacts will remain connected.
@@ -915,6 +945,11 @@
App passcode is replaced with self-destruct passcode.
No comment provided by engineer.
+
+ App session
+ App session
+ No comment provided by engineer.
+
App version
App version
@@ -1020,6 +1055,11 @@
Auto-accept images
No comment provided by engineer.
+
+ Auto-accept settings
+ Auto-accept settings
+ alert title
+
Back
Back
@@ -1045,11 +1085,21 @@
Bad message hash
No comment provided by engineer.
+
+ Better calls
+ Better calls
+ No comment provided by engineer.
+
Better groups
Better groups
No comment provided by engineer.
+
+ Better message dates.
+ Better message dates.
+ No comment provided by engineer.
+
Better messages
Better messages
@@ -1060,6 +1110,21 @@
Better networking
No comment provided by engineer.
+
+ Better notifications
+ Better notifications
+ No comment provided by engineer.
+
+
+ Better security ✅
+ Better security ✅
+ No comment provided by engineer.
+
+
+ Better user experience
+ Better user experience
+ No comment provided by engineer.
+
Black
Black
@@ -1193,7 +1258,7 @@
Cancel
Cancel
- No comment provided by engineer.
+ alert button
Cancel migration
@@ -1213,7 +1278,7 @@
Cannot receive file
Cannot receive file
- No comment provided by engineer.
+ alert title
Capacity exceeded - recipient did not receive previously sent messages.
@@ -1341,6 +1406,16 @@
Chat preferences
No comment provided by engineer.
+
+ Chat preferences were changed.
+ Chat preferences were changed.
+ alert message
+
+
+ Chat profile
+ Chat profile
+ No comment provided by engineer.
+
Chat theme
Chat theme
@@ -1740,6 +1815,11 @@ This is your own one-time link!
Core version: v%@
No comment provided by engineer.
+
+ Corner
+ Corner
+ No comment provided by engineer.
+
Correct name to %@?
Correct name to %@?
@@ -1865,6 +1945,11 @@ This is your own one-time link!
Custom time
No comment provided by engineer.
+
+ Customizable message shape.
+ Customizable message shape.
+ No comment provided by engineer.
+
Customize theme
Customize theme
@@ -2159,6 +2244,11 @@ This is your own one-time link!
Delete old database?
No comment provided by engineer.
+
+ Delete or moderate up to 200 messages.
+ Delete or moderate up to 200 messages.
+ No comment provided by engineer.
+
Delete pending connection?
Delete pending connection?
@@ -2409,6 +2499,11 @@ This is your own one-time link!
Do not send history to new members.
No comment provided by engineer.
+
+ Do not use credentials with proxy.
+ Do not use credentials with proxy.
+ No comment provided by engineer.
+
Don't create address
Don't create address
@@ -2432,7 +2527,8 @@ This is your own one-time link!
Download
Download
- chat item action
+ alert button
+ chat item action
Download errors
@@ -2449,6 +2545,11 @@ This is your own one-time link!
Download file
server test step
+
+ Download files
+ Download files
+ alert action
+
Downloaded
Downloaded
@@ -2724,6 +2825,11 @@ This is your own one-time link!
Error changing address
No comment provided by engineer.
+
+ Error changing connection profile
+ Error changing connection profile
+ No comment provided by engineer.
+
Error changing role
Error changing role
@@ -2734,6 +2840,11 @@ This is your own one-time link!
Error changing setting
No comment provided by engineer.
+
+ Error changing to incognito!
+ Error changing to incognito!
+ No comment provided by engineer.
+
Error connecting to forwarding server %@. Please try later.
Error connecting to forwarding server %@. Please try later.
@@ -2854,6 +2965,11 @@ This is your own one-time link!
Error loading %@ servers
No comment provided by engineer.
+
+ Error migrating settings
+ Error migrating settings
+ No comment provided by engineer.
+
Error opening chat
Error opening chat
@@ -2862,7 +2978,7 @@ This is your own one-time link!
Error receiving file
Error receiving file
- No comment provided by engineer.
+ alert title
Error reconnecting server
@@ -2954,10 +3070,15 @@ This is your own one-time link!
Error stopping chat
No comment provided by engineer.
+
+ Error switching profile
+ Error switching profile
+ No comment provided by engineer.
+
Error switching profile!
Error switching profile!
- No comment provided by engineer.
+ alertTitle
Error synchronizing connection
@@ -3002,8 +3123,7 @@ This is your own one-time link!
Error: %@
Error: %@
- file error text
- snd error text
+ alert message
Error: URL is invalid
@@ -3090,6 +3210,13 @@ This is your own one-time link!
File error
No comment provided by engineer.
+
+ File errors:
+%@
+ File errors:
+%@
+ alert message
+
File not found - most likely file was deleted or cancelled.
File not found - most likely file was deleted or cancelled.
@@ -3225,11 +3352,31 @@ This is your own one-time link!
Forward
chat item action
+
+ Forward %d message(s)?
+ Forward %d message(s)?
+ alert title
+
Forward and save messages
Forward and save messages
No comment provided by engineer.
+
+ Forward messages
+ Forward messages
+ alert action
+
+
+ Forward messages without files?
+ Forward messages without files?
+ alert message
+
+
+ Forward up to 20 messages at once.
+ Forward up to 20 messages at once.
+ No comment provided by engineer.
+
Forwarded
Forwarded
@@ -3240,6 +3387,11 @@ This is your own one-time link!
Forwarded from
No comment provided by engineer.
+
+ Forwarding %lld messages
+ Forwarding %lld messages
+ No comment provided by engineer.
+
Forwarding server %@ failed to connect to destination server %@. Please try later.
Forwarding server %@ failed to connect to destination server %@. Please try later.
@@ -3289,11 +3441,6 @@ Error: %2$@
Full name (optional)
No comment provided by engineer.
-
- Full name:
- Full name:
- No comment provided by engineer.
-
Fully decentralized – visible only to members.
Fully decentralized – visible only to members.
@@ -3539,6 +3686,11 @@ Error: %2$@
ICE servers (one per line)
No comment provided by engineer.
+
+ IP address
+ IP address
+ No comment provided by engineer.
+
If you can't meet in person, show QR code in a video call, or share the link.
If you can't meet in person, show QR code in a video call, or share the link.
@@ -3614,6 +3766,13 @@ Error: %2$@
Importing archive
No comment provided by engineer.
+
+ Improved delivery, reduced traffic usage.
+More improvements are coming soon!
+ Improved delivery, reduced traffic usage.
+More improvements are coming soon!
+ No comment provided by engineer.
+
Improved message delivery
Improved message delivery
@@ -4184,6 +4343,11 @@ This is your link for group %@!
Message servers
No comment provided by engineer.
+
+ Message shape
+ Message shape
+ No comment provided by engineer.
+
Message source remains private.
Message source remains private.
@@ -4234,6 +4398,11 @@ This is your link for group %@!
Messages sent
No comment provided by engineer.
+
+ Messages were deleted after you selected them.
+ Messages were deleted after you selected them.
+ alert message
+
Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.
Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.
@@ -4389,6 +4558,16 @@ This is your link for group %@!
New Passcode
No comment provided by engineer.
+
+ New SOCKS credentials will be used every time you start the app.
+ New SOCKS credentials will be used every time you start the app.
+ No comment provided by engineer.
+
+
+ New SOCKS credentials will be used for each server.
+ New SOCKS credentials will be used for each server.
+ No comment provided by engineer.
+
New chat
New chat
@@ -4509,6 +4688,16 @@ This is your link for group %@!
No network connection
No comment provided by engineer.
+
+ No permission to record speech
+ No permission to record speech
+ No comment provided by engineer.
+
+
+ No permission to record video
+ No permission to record video
+ No comment provided by engineer.
+
No permission to record voice message
No permission to record voice message
@@ -4529,6 +4718,11 @@ This is your link for group %@!
Nothing selected
No comment provided by engineer.
+
+ Nothing to forward!
+ Nothing to forward!
+ alert title
+
Notifications
Notifications
@@ -4561,7 +4755,7 @@ This is your link for group %@!
Ok
Ok
- No comment provided by engineer.
+ alert button
Old database
@@ -4752,6 +4946,13 @@ Requires compatible VPN.
Other %@ servers
No comment provided by engineer.
+
+ Other file errors:
+%@
+ Other file errors:
+%@
+ alert message
+
PING count
PING count
@@ -4787,6 +4988,11 @@ Requires compatible VPN.
Passcode set!
No comment provided by engineer.
+
+ Password
+ Password
+ No comment provided by engineer.
+
Password to show
Password to show
@@ -4936,6 +5142,11 @@ Error: %@
Polish interface
No comment provided by engineer.
+
+ Port
+ Port
+ No comment provided by engineer.
+
Possibly, certificate fingerprint in server address is incorrect
Possibly, certificate fingerprint in server address is incorrect
@@ -5021,16 +5232,6 @@ Error: %@
Profile images
No comment provided by engineer.
-
- Profile name
- Profile name
- No comment provided by engineer.
-
-
- Profile name:
- Profile name:
- No comment provided by engineer.
-
Profile password
Profile password
@@ -5133,6 +5334,11 @@ Enable in *Network & servers* settings.
Proxied servers
No comment provided by engineer.
+
+ Proxy requires password
+ Proxy requires password
+ No comment provided by engineer.
+
Push notifications
Push notifications
@@ -5354,6 +5560,11 @@ Enable in *Network & servers* settings.
Remove
No comment provided by engineer.
+
+ Remove archive?
+ Remove archive?
+ No comment provided by engineer.
+
Remove image
Remove image
@@ -5534,6 +5745,11 @@ Enable in *Network & servers* settings.
SMP server
No comment provided by engineer.
+
+ SOCKS proxy
+ SOCKS proxy
+ No comment provided by engineer.
+
Safely receive files
Safely receive files
@@ -5547,12 +5763,13 @@ Enable in *Network & servers* settings.
Save
Save
- chat item action
+ alert button
+ chat item action
Save (and notify contacts)
Save (and notify contacts)
- No comment provided by engineer.
+ alert button
Save and notify contact
@@ -5579,11 +5796,6 @@ Enable in *Network & servers* settings.
Save archive
No comment provided by engineer.
-
- Save auto-accept settings
- Save auto-accept settings
- No comment provided by engineer.
-
Save group profile
Save group profile
@@ -5619,16 +5831,16 @@ Enable in *Network & servers* settings.
Save servers?
No comment provided by engineer.
-
- Save settings?
- Save settings?
- No comment provided by engineer.
-
Save welcome message?
Save welcome message?
No comment provided by engineer.
+
+ Save your profile?
+ Save your profile?
+ alert title
+
Saved
Saved
@@ -5649,6 +5861,11 @@ Enable in *Network & servers* settings.
Saved message
message info title
+
+ Saving %lld messages
+ Saving %lld messages
+ No comment provided by engineer.
+
Scale
Scale
@@ -5729,6 +5946,11 @@ Enable in *Network & servers* settings.
Select
chat item action
+
+ Select chat profile
+ Select chat profile
+ No comment provided by engineer.
+
Selected %lld
Selected %lld
@@ -5847,7 +6069,7 @@ Enable in *Network & servers* settings.
Sender cancelled file transfer.
Sender cancelled file transfer.
- No comment provided by engineer.
+ alert message
Sender may have deleted the connection request.
@@ -5944,6 +6166,11 @@ Enable in *Network & servers* settings.
Sent via proxy
No comment provided by engineer.
+
+ Server
+ Server
+ No comment provided by engineer.
+
Server address
Server address
@@ -6064,6 +6291,11 @@ Enable in *Network & servers* settings.
Settings
No comment provided by engineer.
+
+ Settings were changed.
+ Settings were changed.
+ alert message
+
Shape profile images
Shape profile images
@@ -6099,6 +6331,11 @@ Enable in *Network & servers* settings.
Share link
No comment provided by engineer.
+
+ Share profile
+ Share profile
+ No comment provided by engineer.
+
Share this 1-time invite link
Share this 1-time invite link
@@ -6234,6 +6471,11 @@ Enable in *Network & servers* settings.
SimpleX one-time invitation
simplex link type
+
+ SimpleX protocols reviewed by Trail of Bits.
+ SimpleX protocols reviewed by Trail of Bits.
+ No comment provided by engineer.
+
Simplified incognito mode
Simplified incognito mode
@@ -6264,6 +6506,11 @@ Enable in *Network & servers* settings.
Soft
blur media
+
+ Some app settings were not migrated.
+ Some app settings were not migrated.
+ No comment provided by engineer.
+
Some file(s) were not exported:
Some file(s) were not exported:
@@ -6404,6 +6651,16 @@ Enable in *Network & servers* settings.
Support SimpleX Chat
No comment provided by engineer.
+
+ Switch audio and video during the call.
+ Switch audio and video during the call.
+ No comment provided by engineer.
+
+
+ Switch chat profile for 1-time invitations.
+ Switch chat profile for 1-time invitations.
+ No comment provided by engineer.
+
System
System
@@ -6439,6 +6696,11 @@ Enable in *Network & servers* settings.
TCP_KEEPINTVL
No comment provided by engineer.
+
+ Tail
+ Tail
+ No comment provided by engineer.
+
Take picture
Take picture
@@ -6631,6 +6893,11 @@ It can happen because of some bug or when the connection is compromised.The text you pasted is not a SimpleX link.
No comment provided by engineer.
+
+ The uploaded database archive will be permanently removed from the servers.
+ The uploaded database archive will be permanently removed from the servers.
+ No comment provided by engineer.
+
Themes
Themes
@@ -6758,6 +7025,16 @@ You will be prompted to complete authentication before this feature is enabled.<
You will be prompted to complete authentication before this feature is enabled.
No comment provided by engineer.
+
+ To record speech please grant permission to use Microphone.
+ To record speech please grant permission to use Microphone.
+ No comment provided by engineer.
+
+
+ To record video please grant permission to use Camera.
+ To record video please grant permission to use Camera.
+ No comment provided by engineer.
+
To record voice message please grant permission to use Microphone.
To record voice message please grant permission to use Microphone.
@@ -6911,7 +7188,7 @@ You will be prompted to complete authentication before this feature is enabled.<
Unknown servers!
Unknown servers!
- No comment provided by engineer.
+ alert title
Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.
@@ -7025,6 +7302,11 @@ To connect, please ask your contact to create another connection link and check
Use .onion hosts
No comment provided by engineer.
+
+ Use SOCKS proxy
+ Use SOCKS proxy
+ No comment provided by engineer.
+
Use SimpleX Chat servers?
Use SimpleX Chat servers?
@@ -7090,16 +7372,16 @@ To connect, please ask your contact to create another connection link and check
Use the app with one hand.
No comment provided by engineer.
-
- User profile
- User profile
- No comment provided by engineer.
-
User selection
User selection
No comment provided by engineer.
+
+ Username
+ Username
+ No comment provided by engineer.
+
Using SimpleX Chat servers.
Using SimpleX Chat servers.
@@ -7333,7 +7615,7 @@ To connect, please ask your contact to create another connection link and check
Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.
Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.
- No comment provided by engineer.
+ alert message
Wrong database passphrase
@@ -7360,11 +7642,6 @@ To connect, please ask your contact to create another connection link and check
XFTP server
No comment provided by engineer.
-
- You
- You
- No comment provided by engineer.
-
You **must not** use the same database on two devices.
You **must not** use the same database on two devices.
@@ -7714,11 +7991,21 @@ Repeat connection request?
Your chat database is not encrypted - set passphrase to encrypt it.
No comment provided by engineer.
+
+ Your chat preferences
+ Your chat preferences
+ alert title
+
Your chat profiles
Your chat profiles
No comment provided by engineer.
+
+ Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.
+ Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.
+ No comment provided by engineer.
+
Your contact sent a file that is larger than currently supported maximum size (%@).
Your contact sent a file that is larger than currently supported maximum size (%@).
@@ -7734,6 +8021,11 @@ Repeat connection request?
Your contacts will remain connected.
No comment provided by engineer.
+
+ Your credentials may be sent unencrypted.
+ Your credentials may be sent unencrypted.
+ No comment provided by engineer.
+
Your current chat database will be DELETED and REPLACED with the imported one.
Your current chat database will be DELETED and REPLACED with the imported one.
@@ -7764,13 +8056,16 @@ Repeat connection request?
Your profile **%@** will be shared.
No comment provided by engineer.
-
- Your profile is stored on your device and shared only with your contacts.
-SimpleX servers cannot see your profile.
- Your profile is stored on your device and shared only with your contacts.
-SimpleX servers cannot see your profile.
+
+ Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.
+ Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.
No comment provided by engineer.
+
+ Your profile was changed. If you save it, the updated profile will be sent to all your contacts.
+ Your profile was changed. If you save it, the updated profile will be sent to all your contacts.
+ alert message
+
Your profile, contacts and delivered messages are stored on your device.
Your profile, contacts and delivered messages are stored on your device.
diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff
index 6e5ec0e85a..21cd9919db 100644
--- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff
+++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff
@@ -137,6 +137,11 @@
¡ %@ quiere contactar!
notification title
+
+ %1$@, %2$@
+ %1$@, %2$@
+ format for date separator in chat
+
%@, %@ and %lld members
%@, %@ y %lld miembro(s) más
@@ -157,11 +162,36 @@
%d días
time interval
+
+ %d file(s) are still being downloaded.
+ %d archivo(s) se está(n) descargando todavía.
+ forward confirmation reason
+
+
+ %d file(s) failed to download.
+ La descarga ha fallado para %d archivo(s).
+ forward confirmation reason
+
+
+ %d file(s) were deleted.
+ %d archivo(s) ha(n) sido eliminado(s).
+ forward confirmation reason
+
+
+ %d file(s) were not downloaded.
+ %d archivo(s) no se ha(n) descargado.
+ forward confirmation reason
+
%d hours
%d horas
time interval
+
+ %d messages not forwarded
+ %d mensajes no enviados
+ alert title
+
%d min
%d minutos
@@ -507,7 +537,7 @@
A new random profile will be shared.
- Se compartirá un perfil nuevo aleatorio.
+ Compartirás un perfil nuevo aleatorio.
No comment provided by engineer.
@@ -723,7 +753,7 @@
All profiles
Todos los perfiles
- No comment provided by engineer.
+ profile dropdown
All your contacts will remain connected.
@@ -915,6 +945,11 @@
El código de acceso será reemplazado por código de autodestrucción.
No comment provided by engineer.
+
+ App session
+ Sesión de aplicación
+ No comment provided by engineer.
+
App version
Versión de la aplicación
@@ -1020,6 +1055,11 @@
Aceptar imágenes automáticamente
No comment provided by engineer.
+
+ Auto-accept settings
+ Auto aceptar configuración
+ alert title
+
Back
Volver
@@ -1045,11 +1085,21 @@
Hash de mensaje incorrecto
No comment provided by engineer.
+
+ Better calls
+ Llamadas mejoradas
+ No comment provided by engineer.
+
Better groups
Grupos mejorados
No comment provided by engineer.
+
+ Better message dates.
+ Sistema de fechas mejorado.
+ No comment provided by engineer.
+
Better messages
Mensajes mejorados
@@ -1060,6 +1110,21 @@
Uso de red mejorado
No comment provided by engineer.
+
+ Better notifications
+ Notificaciones mejoradas
+ No comment provided by engineer.
+
+
+ Better security ✅
+ Seguridad mejorada ✅
+ No comment provided by engineer.
+
+
+ Better user experience
+ Experiencia de usuario mejorada
+ No comment provided by engineer.
+
Black
Negro
@@ -1193,7 +1258,7 @@
Cancel
Cancelar
- No comment provided by engineer.
+ alert button
Cancel migration
@@ -1213,7 +1278,7 @@
Cannot receive file
No se puede recibir el archivo
- No comment provided by engineer.
+ alert title
Capacity exceeded - recipient did not receive previously sent messages.
@@ -1341,6 +1406,16 @@
Preferencias de Chat
No comment provided by engineer.
+
+ Chat preferences were changed.
+ Las preferencias del chat han sido modificadas.
+ alert message
+
+
+ Chat profile
+ Perfil de usuario
+ No comment provided by engineer.
+
Chat theme
Tema de chat
@@ -1493,7 +1568,7 @@
Confirm that you remember database passphrase to migrate it.
- Para migrar confirma que recuerdas la frase de contraseña de la base de datos.
+ Para migrar la base de datos confirma que recuerdas la frase de contraseña.
No comment provided by engineer.
@@ -1740,6 +1815,11 @@ This is your own one-time link!
Versión Core: v%@
No comment provided by engineer.
+
+ Corner
+ Esquina
+ No comment provided by engineer.
+
Correct name to %@?
¿Corregir el nombre a %@?
@@ -1865,6 +1945,11 @@ This is your own one-time link!
Tiempo personalizado
No comment provided by engineer.
+
+ Customizable message shape.
+ Forma personalizable de los mensajes.
+ No comment provided by engineer.
+
Customize theme
Personalizar tema
@@ -1946,7 +2031,7 @@ This is your own one-time link!
Database passphrase is different from saved in the keychain.
- La contraseña es distinta a la almacenada en Keychain.
+ La contraseña es diferente a la almacenada en Keychain.
No comment provided by engineer.
@@ -2159,6 +2244,11 @@ This is your own one-time link!
¿Eliminar base de datos antigua?
No comment provided by engineer.
+
+ Delete or moderate up to 200 messages.
+ Borra o modera hasta 200 mensajes a la vez.
+ No comment provided by engineer.
+
Delete pending connection?
¿Eliminar conexión pendiente?
@@ -2371,7 +2461,7 @@ This is your own one-time link!
Disconnect desktop?
- ¿Desconectar ordenador?
+ ¿Desconectar del ordenador?
No comment provided by engineer.
@@ -2409,6 +2499,11 @@ This is your own one-time link!
No se envía el historial a los miembros nuevos.
No comment provided by engineer.
+
+ Do not use credentials with proxy.
+ No uses credenciales con proxy.
+ No comment provided by engineer.
+
Don't create address
No crear dirección SimpleX
@@ -2421,7 +2516,7 @@ This is your own one-time link!
Don't show again
- No mostrar de nuevo
+ No volver a mostrar
No comment provided by engineer.
@@ -2432,7 +2527,8 @@ This is your own one-time link!
Download
Descargar
- chat item action
+ alert button
+ chat item action
Download errors
@@ -2449,6 +2545,11 @@ This is your own one-time link!
Descargar archivo
server test step
+
+ Download files
+ Descargar archivos
+ alert action
+
Downloaded
Descargado
@@ -2724,6 +2825,11 @@ This is your own one-time link!
Error al cambiar servidor
No comment provided by engineer.
+
+ Error changing connection profile
+ Error al cambiar el perfil de conexión
+ No comment provided by engineer.
+
Error changing role
Error al cambiar rol
@@ -2734,6 +2840,11 @@ This is your own one-time link!
Error cambiando configuración
No comment provided by engineer.
+
+ Error changing to incognito!
+ ¡Error al cambiar a incógnito!
+ No comment provided by engineer.
+
Error connecting to forwarding server %@. Please try later.
Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde.
@@ -2854,6 +2965,11 @@ This is your own one-time link!
Error al cargar servidores %@
No comment provided by engineer.
+
+ Error migrating settings
+ Error al migrar la configuración
+ No comment provided by engineer.
+
Error opening chat
Error al abrir chat
@@ -2862,7 +2978,7 @@ This is your own one-time link!
Error receiving file
Error al recibir archivo
- No comment provided by engineer.
+ alert title
Error reconnecting server
@@ -2954,10 +3070,15 @@ This is your own one-time link!
Error al parar SimpleX
No comment provided by engineer.
+
+ Error switching profile
+ Error al cambiar perfil
+ No comment provided by engineer.
+
Error switching profile!
¡Error al cambiar perfil!
- No comment provided by engineer.
+ alertTitle
Error synchronizing connection
@@ -3002,8 +3123,7 @@ This is your own one-time link!
Error: %@
Error: %@
- file error text
- snd error text
+ alert message
Error: URL is invalid
@@ -3090,6 +3210,13 @@ This is your own one-time link!
Error de archivo
No comment provided by engineer.
+
+ File errors:
+%@
+ Error(es) de archivo
+%@
+ alert message
+
File not found - most likely file was deleted or cancelled.
Archivo no encontrado, probablemente haya sido borrado o cancelado.
@@ -3225,11 +3352,31 @@ This is your own one-time link!
Reenviar
chat item action
+
+ Forward %d message(s)?
+ ¿Reenviar %d mensaje(s)?
+ alert title
+
Forward and save messages
Reenviar y guardar mensajes
No comment provided by engineer.
+
+ Forward messages
+ Reenviar mensajes
+ alert action
+
+
+ Forward messages without files?
+ ¿Reenviar mensajes sin los archivos?
+ alert message
+
+
+ Forward up to 20 messages at once.
+ Desplazamiento de hasta 20 mensajes.
+ No comment provided by engineer.
+
Forwarded
Reenviado
@@ -3240,6 +3387,11 @@ This is your own one-time link!
Reenviado por
No comment provided by engineer.
+
+ Forwarding %lld messages
+ Reenviando %lld mensajes
+ No comment provided by engineer.
+
Forwarding server %@ failed to connect to destination server %@. Please try later.
El servidor de reenvío %@ no ha podido conectarse al servidor de destino %@. Por favor, intentalo más tarde.
@@ -3289,19 +3441,14 @@ Error: %2$@
Nombre completo (opcional)
No comment provided by engineer.
-
- Full name:
- Nombre completo:
- No comment provided by engineer.
-
Fully decentralized – visible only to members.
- Completamente descentralizado y sólo visible para los miembros.
+ Totalmente descentralizado. Visible sólo para los miembros.
No comment provided by engineer.
Fully re-implemented - work in background!
- Completamente reimplementado: ¡funciona en segundo plano!
+ Totalmente revisado. ¡Funciona en segundo plano!
No comment provided by engineer.
@@ -3539,6 +3686,11 @@ Error: %2$@
Servidores ICE (uno por línea)
No comment provided by engineer.
+
+ IP address
+ Dirección IP
+ No comment provided by engineer.
+
If you can't meet in person, show QR code in a video call, or share the link.
Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace.
@@ -3614,6 +3766,13 @@ Error: %2$@
Importando archivo
No comment provided by engineer.
+
+ Improved delivery, reduced traffic usage.
+More improvements are coming soon!
+ Reducción del tráfico y entrega mejorada.
+¡Pronto habrá nuevas mejoras!
+ No comment provided by engineer.
+
Improved message delivery
Entrega de mensajes mejorada
@@ -4016,7 +4175,7 @@ This is your link for group %@!
Local profile data only
- Sólo datos del perfil local
+ Eliminar sólo el perfil
No comment provided by engineer.
@@ -4184,6 +4343,11 @@ This is your link for group %@!
Servidores de mensajes
No comment provided by engineer.
+
+ Message shape
+ Forma del mensaje
+ No comment provided by engineer.
+
Message source remains private.
El autor del mensaje se mantiene privado.
@@ -4234,6 +4398,11 @@ This is your link for group %@!
Mensajes enviados
No comment provided by engineer.
+
+ Messages were deleted after you selected them.
+ Los mensajes han sido borrados después de seleccionarlos.
+ alert message
+
Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.
Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo** con secreto perfecto hacía adelante, repudio y recuperación tras ataque.
@@ -4389,6 +4558,16 @@ This is your link for group %@!
Código Nuevo
No comment provided by engineer.
+
+ New SOCKS credentials will be used every time you start the app.
+ Se usarán credenciales SOCKS nuevas cada vez que inicies la aplicación.
+ No comment provided by engineer.
+
+
+ New SOCKS credentials will be used for each server.
+ Se usarán credenciales SOCKS nuevas por cada servidor.
+ No comment provided by engineer.
+
New chat
Nuevo chat
@@ -4509,6 +4688,16 @@ This is your link for group %@!
Sin conexión de red
No comment provided by engineer.
+
+ No permission to record speech
+ Sin permiso para grabación de voz
+ No comment provided by engineer.
+
+
+ No permission to record video
+ Sin permiso para grabación de vídeo
+ No comment provided by engineer.
+
No permission to record voice message
Sin permiso para grabar mensajes de voz
@@ -4529,6 +4718,11 @@ This is your link for group %@!
Nada seleccionado
No comment provided by engineer.
+
+ Nothing to forward!
+ ¡Nada para reenviar!
+ alert title
+
Notifications
Notificaciones
@@ -4561,7 +4755,7 @@ This is your link for group %@!
Ok
Ok
- No comment provided by engineer.
+ alert button
Old database
@@ -4752,6 +4946,13 @@ Requiere activación de la VPN.
Otros servidores %@
No comment provided by engineer.
+
+ Other file errors:
+%@
+ Otro(s) error(es) de archivo.
+%@
+ alert message
+
PING count
Contador PING
@@ -4787,6 +4988,11 @@ Requiere activación de la VPN.
¡Código de acceso guardado!
No comment provided by engineer.
+
+ Password
+ Contraseña
+ No comment provided by engineer.
+
Password to show
Contraseña para hacerlo visible
@@ -4936,9 +5142,14 @@ Error: %@
Interfaz en polaco
No comment provided by engineer.
+
+ Port
+ Puerto
+ No comment provided by engineer.
+
Possibly, certificate fingerprint in server address is incorrect
- Posiblemente la huella digital del certificado en la dirección del servidor es incorrecta
+ Posiblemente la huella del certificado en la dirección del servidor es incorrecta
server test error
@@ -5008,7 +5219,7 @@ Error: %@
Profile and server connections
- Datos del perfil y conexiones
+ Eliminar perfil y conexiones
No comment provided by engineer.
@@ -5021,16 +5232,6 @@ Error: %@
Forma de los perfiles
No comment provided by engineer.
-
- Profile name
- Nombre del perfil
- No comment provided by engineer.
-
-
- Profile name:
- Nombre del perfil:
- No comment provided by engineer.
-
Profile password
Contraseña del perfil
@@ -5133,6 +5334,11 @@ Actívalo en ajustes de *Servidores y Redes*.
Servidores con proxy
No comment provided by engineer.
+
+ Proxy requires password
+ El proxy requiere contraseña
+ No comment provided by engineer.
+
Push notifications
Notificaciones automáticas
@@ -5354,6 +5560,11 @@ Actívalo en ajustes de *Servidores y Redes*.
Eliminar
No comment provided by engineer.
+
+ Remove archive?
+ ¿Eliminar archivo?
+ No comment provided by engineer.
+
Remove image
Eliminar imagen
@@ -5534,6 +5745,11 @@ Actívalo en ajustes de *Servidores y Redes*.
Servidor SMP
No comment provided by engineer.
+
+ SOCKS proxy
+ Proxy SOCKS
+ No comment provided by engineer.
+
Safely receive files
Recibe archivos de forma segura
@@ -5547,12 +5763,13 @@ Actívalo en ajustes de *Servidores y Redes*.
Save
Guardar
- chat item action
+ alert button
+ chat item action
Save (and notify contacts)
Guardar (y notificar contactos)
- No comment provided by engineer.
+ alert button
Save and notify contact
@@ -5579,11 +5796,6 @@ Actívalo en ajustes de *Servidores y Redes*.
Guardar archivo
No comment provided by engineer.
-
- Save auto-accept settings
- Guardar configuración de auto aceptar
- No comment provided by engineer.
-
Save group profile
Guardar perfil de grupo
@@ -5619,16 +5831,16 @@ Actívalo en ajustes de *Servidores y Redes*.
¿Guardar servidores?
No comment provided by engineer.
-
- Save settings?
- ¿Guardar configuración?
- No comment provided by engineer.
-
Save welcome message?
¿Guardar mensaje de bienvenida?
No comment provided by engineer.
+
+ Save your profile?
+ ¿Guardar tu perfil?
+ alert title
+
Saved
Guardado
@@ -5649,6 +5861,11 @@ Actívalo en ajustes de *Servidores y Redes*.
Mensaje guardado
message info title
+
+ Saving %lld messages
+ Guardando %lld mensajes
+ No comment provided by engineer.
+
Scale
Escala
@@ -5729,6 +5946,11 @@ Actívalo en ajustes de *Servidores y Redes*.
Seleccionar
chat item action
+
+ Select chat profile
+ Selecciona perfil de chat
+ No comment provided by engineer.
+
Selected %lld
Seleccionados %lld
@@ -5776,7 +5998,7 @@ Actívalo en ajustes de *Servidores y Redes*.
Send direct message to connect
- Envia un mensaje para conectar
+ Envía un mensaje para conectar
No comment provided by engineer.
@@ -5847,7 +6069,7 @@ Actívalo en ajustes de *Servidores y Redes*.
Sender cancelled file transfer.
El remitente ha cancelado la transferencia de archivos.
- No comment provided by engineer.
+ alert message
Sender may have deleted the connection request.
@@ -5944,6 +6166,11 @@ Actívalo en ajustes de *Servidores y Redes*.
Mediante proxy
No comment provided by engineer.
+
+ Server
+ Servidor
+ No comment provided by engineer.
+
Server address
Dirección del servidor
@@ -5971,7 +6198,7 @@ Actívalo en ajustes de *Servidores y Redes*.
Server test failed!
- ¡Error en prueba del servidor!
+ ¡Prueba no superada!
No comment provided by engineer.
@@ -6064,6 +6291,11 @@ Actívalo en ajustes de *Servidores y Redes*.
Configuración
No comment provided by engineer.
+
+ Settings were changed.
+ La configuración ha sido modificada.
+ alert message
+
Shape profile images
Dar forma a las imágenes de perfil
@@ -6099,6 +6331,11 @@ Actívalo en ajustes de *Servidores y Redes*.
Compartir enlace
No comment provided by engineer.
+
+ Share profile
+ Comparte perfil
+ No comment provided by engineer.
+
Share this 1-time invite link
Comparte este enlace de un solo uso
@@ -6234,6 +6471,11 @@ Actívalo en ajustes de *Servidores y Redes*.
Invitación SimpleX de un uso
simplex link type
+
+ SimpleX protocols reviewed by Trail of Bits.
+ Protocolos de SimpleX auditados por Trail of Bits.
+ No comment provided by engineer.
+
Simplified incognito mode
Modo incógnito simplificado
@@ -6264,6 +6506,11 @@ Actívalo en ajustes de *Servidores y Redes*.
Suave
blur media
+
+ Some app settings were not migrated.
+ Algunas configuraciones de la app no han sido migradas.
+ No comment provided by engineer.
+
Some file(s) were not exported:
Algunos archivos no han sido exportados:
@@ -6404,6 +6651,16 @@ Actívalo en ajustes de *Servidores y Redes*.
Soporte SimpleX Chat
No comment provided by engineer.
+
+ Switch audio and video during the call.
+ Intercambia audio y video durante la llamada.
+ No comment provided by engineer.
+
+
+ Switch chat profile for 1-time invitations.
+ Cambia el perfil de chat para invitaciones de un solo uso.
+ No comment provided by engineer.
+
System
Sistema
@@ -6439,6 +6696,11 @@ Actívalo en ajustes de *Servidores y Redes*.
TCP_KEEPINTVL
No comment provided by engineer.
+
+ Tail
+ Cola
+ No comment provided by engineer.
+
Take picture
Tomar foto
@@ -6486,7 +6748,7 @@ Actívalo en ajustes de *Servidores y Redes*.
Test failed at step %@.
- La prueba ha fallado en el paso %@.
+ Prueba no superada en el paso %@.
server test failure
@@ -6501,7 +6763,7 @@ Actívalo en ajustes de *Servidores y Redes*.
Tests failed!
- ¡Pruebas fallidas!
+ ¡Pruebas no superadas!
No comment provided by engineer.
@@ -6631,6 +6893,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.
El texto pegado no es un enlace SimpleX.
No comment provided by engineer.
+
+ The uploaded database archive will be permanently removed from the servers.
+ El archivo de bases de datos subido será eliminado permanentemente de los servidores.
+ No comment provided by engineer.
+
Themes
Temas
@@ -6758,6 +7025,16 @@ You will be prompted to complete authentication before this feature is enabled.<
Se te pedirá que completes la autenticación antes de activar esta función.
No comment provided by engineer.
+
+ To record speech please grant permission to use Microphone.
+ Para grabación de voz, por favor concede el permiso para usar el micrófono.
+ No comment provided by engineer.
+
+
+ To record video please grant permission to use Camera.
+ Para grabación de vídeo, por favor concede el permiso para usar la cámara.
+ No comment provided by engineer.
+
To record voice message please grant permission to use Microphone.
Para grabar el mensaje de voz concede permiso para usar el micrófono.
@@ -6911,7 +7188,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.
Unknown servers!
¡Servidores desconocidos!
- No comment provided by engineer.
+ alert title
Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.
@@ -7025,6 +7302,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión
Usar hosts .onion
No comment provided by engineer.
+
+ Use SOCKS proxy
+ Usar proxy SOCKS
+ No comment provided by engineer.
+
Use SimpleX Chat servers?
¿Usar servidores SimpleX Chat?
@@ -7090,16 +7372,16 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión
Usa la aplicación con una sola mano.
No comment provided by engineer.
-
- User profile
- Perfil de usuario
- No comment provided by engineer.
-
User selection
Selección de usuarios
No comment provided by engineer.
+
+ Username
+ Nombre de usuario
+ No comment provided by engineer.
+
Using SimpleX Chat servers.
Usar servidores SimpleX Chat.
@@ -7333,7 +7615,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión
Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.
Sin Tor o VPN, tu dirección IP será visible para estos servidores XFTP: %@.
- No comment provided by engineer.
+ alert message
Wrong database passphrase
@@ -7360,11 +7642,6 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión
Servidor XFTP
No comment provided by engineer.
-
- You
- Tú
- No comment provided by engineer.
-
You **must not** use the same database on two devices.
**No debes** usar la misma base de datos en dos dispositivos.
@@ -7714,11 +7991,21 @@ Repeat connection request?
La base de datos no está cifrada - establece una contraseña para cifrarla.
No comment provided by engineer.
+
+ Your chat preferences
+ Tus preferencias de chat
+ alert title
+
Your chat profiles
Mis perfiles
No comment provided by engineer.
+
+ Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.
+ Tu conexión ha sido trasladada a %@ pero ha ocurrido un error inesperado al redirigirte al perfil.
+ No comment provided by engineer.
+
Your contact sent a file that is larger than currently supported maximum size (%@).
El contacto ha enviado un archivo mayor al máximo admitido (%@).
@@ -7734,6 +8021,11 @@ Repeat connection request?
Tus contactos permanecerán conectados.
No comment provided by engineer.
+
+ Your credentials may be sent unencrypted.
+ Tus credenciales podrían ser enviadas sin cifrar.
+ No comment provided by engineer.
+
Your current chat database will be DELETED and REPLACED with the imported one.
La base de datos actual será ELIMINADA y SUSTITUIDA por la importada.
@@ -7764,13 +8056,16 @@ Repeat connection request?
El perfil **%@** será compartido.
No comment provided by engineer.
-
- Your profile is stored on your device and shared only with your contacts.
-SimpleX servers cannot see your profile.
- Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos.
-Los servidores SimpleX no pueden ver tu perfil.
+
+ Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.
+ Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil.
No comment provided by engineer.
+
+ Your profile was changed. If you save it, the updated profile will be sent to all your contacts.
+ Tu perfil ha sido modificado. Si lo guardas la actualización será enviada a todos tus contactos.
+ alert message
+
Your profile, contacts and delivered messages are stored on your device.
Tu perfil, contactos y mensajes se almacenan en tu dispositivo.
@@ -7788,7 +8083,7 @@ Los servidores SimpleX no pueden ver tu perfil.
Your server address
- Dirección de tu servidor
+ Dirección del servidor
No comment provided by engineer.
@@ -8500,7 +8795,7 @@ Los servidores SimpleX no pueden ver tu perfil.
removed profile picture
- imagen de perfil eliminada
+ ha eliminado la imagen del perfil
profile update event chat item
@@ -8564,7 +8859,7 @@ last received msg: %2$@
set new profile picture
- nueva imagen de perfil
+ tiene nueva imagen del perfil
profile update event chat item
@@ -8912,7 +9207,7 @@ last received msg: %2$@
Database passphrase is different from saved in the keychain.
- La contraseña de la base de datos es distinta a la almacenada en keychain.
+ La contraseña de la base de datos es diferente a la almacenada en keychain.
No comment provided by engineer.
diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff
index 211e512a1e..4b384842b6 100644
--- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff
+++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff
@@ -133,6 +133,10 @@
%@ haluaa muodostaa yhteyden!
notification title
+
+ %1$@, %2$@
+ format for date separator in chat
+
%@, %@ and %lld members
No comment provided by engineer.
@@ -152,11 +156,31 @@
%d päivää
time interval
+
+ %d file(s) are still being downloaded.
+ forward confirmation reason
+
+
+ %d file(s) failed to download.
+ forward confirmation reason
+
+
+ %d file(s) were deleted.
+ forward confirmation reason
+
+
+ %d file(s) were not downloaded.
+ forward confirmation reason
+
%d hours
%d tuntia
time interval
+
+ %d messages not forwarded
+ alert title
+
%d min
%d min
@@ -686,7 +710,7 @@
All profiles
- No comment provided by engineer.
+ profile dropdown
All your contacts will remain connected.
@@ -868,6 +892,10 @@
Sovelluksen pääsykoodi korvataan itsetuhoutuvalla pääsykoodilla.
No comment provided by engineer.
+
+ App session
+ No comment provided by engineer.
+
App version
Sovellusversio
@@ -967,6 +995,10 @@
Hyväksy kuvat automaattisesti
No comment provided by engineer.
+
+ Auto-accept settings
+ alert title
+
Back
Takaisin
@@ -990,10 +1022,18 @@
Virheellinen viestin tarkiste
No comment provided by engineer.
+
+ Better calls
+ No comment provided by engineer.
+
Better groups
No comment provided by engineer.
+
+ Better message dates.
+ No comment provided by engineer.
+
Better messages
Parempia viestejä
@@ -1003,6 +1043,18 @@
Better networking
No comment provided by engineer.
+
+ Better notifications
+ No comment provided by engineer.
+
+
+ Better security ✅
+ No comment provided by engineer.
+
+
+ Better user experience
+ No comment provided by engineer.
+
Black
No comment provided by engineer.
@@ -1120,7 +1172,7 @@
Cancel
Peruuta
- No comment provided by engineer.
+ alert button
Cancel migration
@@ -1138,7 +1190,7 @@
Cannot receive file
Tiedostoa ei voi vastaanottaa
- No comment provided by engineer.
+ alert title
Capacity exceeded - recipient did not receive previously sent messages.
@@ -1259,6 +1311,15 @@
Chat-asetukset
No comment provided by engineer.
+
+ Chat preferences were changed.
+ alert message
+
+
+ Chat profile
+ Käyttäjäprofiili
+ No comment provided by engineer.
+
Chat theme
No comment provided by engineer.
@@ -1616,6 +1677,10 @@ This is your own one-time link!
Ydinversio: v%@
No comment provided by engineer.
+
+ Corner
+ No comment provided by engineer.
+
Correct name to %@?
No comment provided by engineer.
@@ -1731,6 +1796,10 @@ This is your own one-time link!
Mukautettu aika
No comment provided by engineer.
+
+ Customizable message shape.
+ No comment provided by engineer.
+
Customize theme
No comment provided by engineer.
@@ -2017,6 +2086,10 @@ This is your own one-time link!
Poista vanha tietokanta?
No comment provided by engineer.
+
+ Delete or moderate up to 200 messages.
+ No comment provided by engineer.
+
Delete pending connection?
Poistetaanko odottava yhteys?
@@ -2248,6 +2321,10 @@ This is your own one-time link!
Do not send history to new members.
No comment provided by engineer.
+
+ Do not use credentials with proxy.
+ No comment provided by engineer.
+
Don't create address
Älä luo osoitetta
@@ -2270,7 +2347,8 @@ This is your own one-time link!
Download
- chat item action
+ alert button
+ chat item action
Download errors
@@ -2285,6 +2363,10 @@ This is your own one-time link!
Lataa tiedosto
server test step
+
+ Download files
+ alert action
+
Downloaded
No comment provided by engineer.
@@ -2544,6 +2626,10 @@ This is your own one-time link!
Virhe osoitteenvaihdossa
No comment provided by engineer.
+
+ Error changing connection profile
+ No comment provided by engineer.
+
Error changing role
Virhe roolin vaihdossa
@@ -2554,6 +2640,10 @@ This is your own one-time link!
Virhe asetuksen muuttamisessa
No comment provided by engineer.
+
+ Error changing to incognito!
+ No comment provided by engineer.
+
Error connecting to forwarding server %@. Please try later.
No comment provided by engineer.
@@ -2669,6 +2759,10 @@ This is your own one-time link!
Virhe %@-palvelimien lataamisessa
No comment provided by engineer.
+
+ Error migrating settings
+ No comment provided by engineer.
+
Error opening chat
No comment provided by engineer.
@@ -2676,7 +2770,7 @@ This is your own one-time link!
Error receiving file
Virhe tiedoston vastaanottamisessa
- No comment provided by engineer.
+ alert title
Error reconnecting server
@@ -2762,10 +2856,14 @@ This is your own one-time link!
Virhe keskustelun lopettamisessa
No comment provided by engineer.
+
+ Error switching profile
+ No comment provided by engineer.
+
Error switching profile!
Virhe profiilin vaihdossa!
- No comment provided by engineer.
+ alertTitle
Error synchronizing connection
@@ -2808,8 +2906,7 @@ This is your own one-time link!
Error: %@
Virhe: %@
- file error text
- snd error text
+ alert message
Error: URL is invalid
@@ -2890,6 +2987,11 @@ This is your own one-time link!
File error
No comment provided by engineer.
+
+ File errors:
+%@
+ alert message
+
File not found - most likely file was deleted or cancelled.
file error text
@@ -3016,10 +3118,26 @@ This is your own one-time link!
Forward
chat item action
+
+ Forward %d message(s)?
+ alert title
+
Forward and save messages
No comment provided by engineer.
+
+ Forward messages
+ alert action
+
+
+ Forward messages without files?
+ alert message
+
+
+ Forward up to 20 messages at once.
+ No comment provided by engineer.
+
Forwarded
No comment provided by engineer.
@@ -3028,6 +3146,10 @@ This is your own one-time link!
Forwarded from
No comment provided by engineer.
+
+ Forwarding %lld messages
+ No comment provided by engineer.
+
Forwarding server %@ failed to connect to destination server %@. Please try later.
No comment provided by engineer.
@@ -3069,11 +3191,6 @@ Error: %2$@
Koko nimi (valinnainen)
No comment provided by engineer.
-
- Full name:
- Koko nimi:
- No comment provided by engineer.
-
Fully decentralized – visible only to members.
No comment provided by engineer.
@@ -3311,6 +3428,10 @@ Error: %2$@
ICE-palvelimet (yksi per rivi)
No comment provided by engineer.
+
+ IP address
+ No comment provided by engineer.
+
If you can't meet in person, show QR code in a video call, or share the link.
Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki.
@@ -3383,6 +3504,11 @@ Error: %2$@
Importing archive
No comment provided by engineer.
+
+ Improved delivery, reduced traffic usage.
+More improvements are coming soon!
+ No comment provided by engineer.
+
Improved message delivery
No comment provided by engineer.
@@ -3918,6 +4044,10 @@ This is your link for group %@!
Message servers
No comment provided by engineer.
+
+ Message shape
+ No comment provided by engineer.
+
Message source remains private.
No comment provided by engineer.
@@ -3961,6 +4091,10 @@ This is your link for group %@!
Messages sent
No comment provided by engineer.
+
+ Messages were deleted after you selected them.
+ alert message
+
Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.
No comment provided by engineer.
@@ -4103,6 +4237,14 @@ This is your link for group %@!
Uusi pääsykoodi
No comment provided by engineer.
+
+ New SOCKS credentials will be used every time you start the app.
+ No comment provided by engineer.
+
+
+ New SOCKS credentials will be used for each server.
+ No comment provided by engineer.
+
New chat
No comment provided by engineer.
@@ -4216,6 +4358,14 @@ This is your link for group %@!
No network connection
No comment provided by engineer.
+
+ No permission to record speech
+ No comment provided by engineer.
+
+
+ No permission to record video
+ No comment provided by engineer.
+
No permission to record voice message
Ei lupaa ääniviestin tallentamiseen
@@ -4234,6 +4384,10 @@ This is your link for group %@!
Nothing selected
No comment provided by engineer.
+
+ Nothing to forward!
+ alert title
+
Notifications
Ilmoitukset
@@ -4265,7 +4419,7 @@ This is your link for group %@!
Ok
Ok
- No comment provided by engineer.
+ alert button
Old database
@@ -4444,6 +4598,11 @@ Edellyttää VPN:n sallimista.
Other %@ servers
No comment provided by engineer.
+
+ Other file errors:
+%@
+ alert message
+
PING count
PING-määrä
@@ -4479,6 +4638,10 @@ Edellyttää VPN:n sallimista.
Pääsykoodi asetettu!
No comment provided by engineer.
+
+ Password
+ No comment provided by engineer.
+
Password to show
Salasana näytettäväksi
@@ -4615,6 +4778,10 @@ Error: %@
Puolalainen käyttöliittymä
No comment provided by engineer.
+
+ Port
+ No comment provided by engineer.
+
Possibly, certificate fingerprint in server address is incorrect
Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen
@@ -4693,14 +4860,6 @@ Error: %@
Profile images
No comment provided by engineer.
-
- Profile name
- No comment provided by engineer.
-
-
- Profile name:
- No comment provided by engineer.
-
Profile password
Profiilin salasana
@@ -4796,6 +4955,10 @@ Enable in *Network & servers* settings.
Proxied servers
No comment provided by engineer.
+
+ Proxy requires password
+ No comment provided by engineer.
+
Push notifications
Push-ilmoitukset
@@ -5002,6 +5165,10 @@ Enable in *Network & servers* settings.
Poista
No comment provided by engineer.
+
+ Remove archive?
+ No comment provided by engineer.
+
Remove image
No comment provided by engineer.
@@ -5169,6 +5336,10 @@ Enable in *Network & servers* settings.
SMP server
No comment provided by engineer.
+
+ SOCKS proxy
+ No comment provided by engineer.
+
Safely receive files
No comment provided by engineer.
@@ -5180,12 +5351,13 @@ Enable in *Network & servers* settings.
Save
Tallenna
- chat item action
+ alert button
+ chat item action
Save (and notify contacts)
Tallenna (ja ilmoita kontakteille)
- No comment provided by engineer.
+ alert button
Save and notify contact
@@ -5211,11 +5383,6 @@ Enable in *Network & servers* settings.
Tallenna arkisto
No comment provided by engineer.
-
- Save auto-accept settings
- Tallenna automaattisen hyväksynnän asetukset
- No comment provided by engineer.
-
Save group profile
Tallenna ryhmäprofiili
@@ -5251,16 +5418,15 @@ Enable in *Network & servers* settings.
Tallenna palvelimet?
No comment provided by engineer.
-
- Save settings?
- Tallenna asetukset?
- No comment provided by engineer.
-
Save welcome message?
Tallenna tervetuloviesti?
No comment provided by engineer.
+
+ Save your profile?
+ alert title
+
Saved
No comment provided by engineer.
@@ -5278,6 +5444,10 @@ Enable in *Network & servers* settings.
Saved message
message info title
+
+ Saving %lld messages
+ No comment provided by engineer.
+
Scale
No comment provided by engineer.
@@ -5351,6 +5521,10 @@ Enable in *Network & servers* settings.
Valitse
chat item action
+
+ Select chat profile
+ No comment provided by engineer.
+
Selected %lld
No comment provided by engineer.
@@ -5461,7 +5635,7 @@ Enable in *Network & servers* settings.
Sender cancelled file transfer.
Lähettäjä peruutti tiedoston siirron.
- No comment provided by engineer.
+ alert message
Sender may have deleted the connection request.
@@ -5553,6 +5727,10 @@ Enable in *Network & servers* settings.
Sent via proxy
No comment provided by engineer.
+
+ Server
+ No comment provided by engineer.
+
Server address
No comment provided by engineer.
@@ -5662,6 +5840,10 @@ Enable in *Network & servers* settings.
Asetukset
No comment provided by engineer.
+
+ Settings were changed.
+ alert message
+
Shape profile images
No comment provided by engineer.
@@ -5695,6 +5877,10 @@ Enable in *Network & servers* settings.
Jaa linkki
No comment provided by engineer.
+
+ Share profile
+ No comment provided by engineer.
+
Share this 1-time invite link
No comment provided by engineer.
@@ -5821,6 +6007,10 @@ Enable in *Network & servers* settings.
SimpleX-kertakutsu
simplex link type
+
+ SimpleX protocols reviewed by Trail of Bits.
+ No comment provided by engineer.
+
Simplified incognito mode
No comment provided by engineer.
@@ -5848,6 +6038,10 @@ Enable in *Network & servers* settings.
Soft
blur media
+
+ Some app settings were not migrated.
+ No comment provided by engineer.
+
Some file(s) were not exported:
No comment provided by engineer.
@@ -5976,6 +6170,14 @@ Enable in *Network & servers* settings.
SimpleX Chat tuki
No comment provided by engineer.
+
+ Switch audio and video during the call.
+ No comment provided by engineer.
+
+
+ Switch chat profile for 1-time invitations.
+ No comment provided by engineer.
+
System
Järjestelmä
@@ -6010,6 +6212,10 @@ Enable in *Network & servers* settings.
TCP_KEEPINTVL
No comment provided by engineer.
+
+ Tail
+ No comment provided by engineer.
+
Take picture
Ota kuva
@@ -6193,6 +6399,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.The text you pasted is not a SimpleX link.
No comment provided by engineer.
+
+ The uploaded database archive will be permanently removed from the servers.
+ No comment provided by engineer.
+
Themes
No comment provided by engineer.
@@ -6309,6 +6519,14 @@ You will be prompted to complete authentication before this feature is enabled.<
Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus otetaan käyttöön.
No comment provided by engineer.
+
+ To record speech please grant permission to use Microphone.
+ No comment provided by engineer.
+
+
+ To record video please grant permission to use Camera.
+ No comment provided by engineer.
+
To record voice message please grant permission to use Microphone.
Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia.
@@ -6450,7 +6668,7 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote
Unknown servers!
- No comment provided by engineer.
+ alert title
Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.
@@ -6555,6 +6773,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
Käytä .onion-isäntiä
No comment provided by engineer.
+
+ Use SOCKS proxy
+ No comment provided by engineer.
+
Use SimpleX Chat servers?
Käytä SimpleX Chat palvelimia?
@@ -6614,15 +6836,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
Use the app with one hand.
No comment provided by engineer.
-
- User profile
- Käyttäjäprofiili
- No comment provided by engineer.
-
User selection
No comment provided by engineer.
+
+ Username
+ No comment provided by engineer.
+
Using SimpleX Chat servers.
Käyttää SimpleX Chat -palvelimia.
@@ -6835,7 +7056,7 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.
- No comment provided by engineer.
+ alert message
Wrong database passphrase
@@ -6859,11 +7080,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
XFTP server
No comment provided by engineer.
-
- You
- Sinä
- No comment provided by engineer.
-
You **must not** use the same database on two devices.
No comment provided by engineer.
@@ -7189,11 +7405,19 @@ Repeat connection request?
Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi.
No comment provided by engineer.
+
+ Your chat preferences
+ alert title
+
Your chat profiles
Keskusteluprofiilisi
No comment provided by engineer.
+
+ Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.
+ No comment provided by engineer.
+
Your contact sent a file that is larger than currently supported maximum size (%@).
Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@).
@@ -7209,6 +7433,10 @@ Repeat connection request?
Kontaktisi pysyvät yhdistettyinä.
No comment provided by engineer.
+
+ Your credentials may be sent unencrypted.
+ No comment provided by engineer.
+
Your current chat database will be DELETED and REPLACED with the imported one.
Nykyinen keskustelut-tietokantasi poistetaan ja korvataan tuodulla tietokannalla.
@@ -7238,13 +7466,15 @@ Repeat connection request?
Profiilisi **%@** jaetaan.
No comment provided by engineer.
-
- Your profile is stored on your device and shared only with your contacts.
-SimpleX servers cannot see your profile.
- Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa.
-SimpleX-palvelimet eivät näe profiiliasi.
+
+ Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.
+ Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. SimpleX-palvelimet eivät näe profiiliasi.
No comment provided by engineer.
+
+ Your profile was changed. If you save it, the updated profile will be sent to all your contacts.
+ alert message
+
Your profile, contacts and delivered messages are stored on your device.
Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi.
diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff
index c05098980e..b672bebc8c 100644
--- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff
+++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff
@@ -137,6 +137,10 @@
%@ veut se connecter !
notification title
+
+ %1$@, %2$@
+ format for date separator in chat
+
%@, %@ and %lld members
%@, %@ et %lld membres
@@ -157,11 +161,31 @@
%d jours
time interval
+
+ %d file(s) are still being downloaded.
+ forward confirmation reason
+
+
+ %d file(s) failed to download.
+ forward confirmation reason
+
+
+ %d file(s) were deleted.
+ forward confirmation reason
+
+
+ %d file(s) were not downloaded.
+ forward confirmation reason
+
%d hours
%d heures
time interval
+
+ %d messages not forwarded
+ alert title
+
%d min
%d min
@@ -723,7 +747,7 @@
All profiles
Tous les profiles
- No comment provided by engineer.
+ profile dropdown
All your contacts will remain connected.
@@ -915,6 +939,10 @@
Le code d'accès de l'application est remplacé par un code d'autodestruction.
No comment provided by engineer.
+
+ App session
+ No comment provided by engineer.
+
App version
Version de l'app
@@ -1020,6 +1048,10 @@
Images auto-acceptées
No comment provided by engineer.
+
+ Auto-accept settings
+ alert title
+
Back
Retour
@@ -1045,11 +1077,19 @@
Mauvais hash de message
No comment provided by engineer.
+
+ Better calls
+ No comment provided by engineer.
+
Better groups
Des groupes plus performants
No comment provided by engineer.
+
+ Better message dates.
+ No comment provided by engineer.
+
Better messages
Meilleurs messages
@@ -1060,6 +1100,18 @@
Meilleure gestion de réseau
No comment provided by engineer.
+
+ Better notifications
+ No comment provided by engineer.
+
+
+ Better security ✅
+ No comment provided by engineer.
+
+
+ Better user experience
+ No comment provided by engineer.
+
Black
Noir
@@ -1193,7 +1245,7 @@
Cancel
Annuler
- No comment provided by engineer.
+ alert button
Cancel migration
@@ -1213,7 +1265,7 @@
Cannot receive file
Impossible de recevoir le fichier
- No comment provided by engineer.
+ alert title
Capacity exceeded - recipient did not receive previously sent messages.
@@ -1341,6 +1393,15 @@
Préférences de chat
No comment provided by engineer.
+
+ Chat preferences were changed.
+ alert message
+
+
+ Chat profile
+ Profil d'utilisateur
+ No comment provided by engineer.
+
Chat theme
Thème de chat
@@ -1740,6 +1801,10 @@ Il s'agit de votre propre lien unique !
Version du cœur : v%@
No comment provided by engineer.
+
+ Corner
+ No comment provided by engineer.
+
Correct name to %@?
Corriger le nom pour %@ ?
@@ -1865,6 +1930,10 @@ Il s'agit de votre propre lien unique !
Délai personnalisé
No comment provided by engineer.
+
+ Customizable message shape.
+ No comment provided by engineer.
+
Customize theme
Personnaliser le thème
@@ -2159,6 +2228,10 @@ Il s'agit de votre propre lien unique !
Supprimer l'ancienne base de données ?
No comment provided by engineer.
+
+ Delete or moderate up to 200 messages.
+ No comment provided by engineer.
+
Delete pending connection?
Supprimer la connexion en attente ?
@@ -2409,6 +2482,10 @@ Il s'agit de votre propre lien unique !
Ne pas envoyer d'historique aux nouveaux membres.
No comment provided by engineer.
+
+ Do not use credentials with proxy.
+ No comment provided by engineer.
+
Don't create address
Ne pas créer d'adresse
@@ -2432,7 +2509,8 @@ Il s'agit de votre propre lien unique !
Download
Télécharger
- chat item action
+ alert button
+ chat item action
Download errors
@@ -2449,6 +2527,10 @@ Il s'agit de votre propre lien unique !
Télécharger le fichier
server test step
+
+ Download files
+ alert action
+
Downloaded
Téléchargé
@@ -2724,6 +2806,10 @@ Il s'agit de votre propre lien unique !
Erreur de changement d'adresse
No comment provided by engineer.
+
+ Error changing connection profile
+ No comment provided by engineer.
+
Error changing role
Erreur lors du changement de rôle
@@ -2734,6 +2820,10 @@ Il s'agit de votre propre lien unique !
Erreur de changement de paramètre
No comment provided by engineer.
+
+ Error changing to incognito!
+ No comment provided by engineer.
+
Error connecting to forwarding server %@. Please try later.
Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard.
@@ -2854,6 +2944,10 @@ Il s'agit de votre propre lien unique !
Erreur lors du chargement des serveurs %@
No comment provided by engineer.
+
+ Error migrating settings
+ No comment provided by engineer.
+
Error opening chat
Erreur lors de l'ouverture du chat
@@ -2862,7 +2956,7 @@ Il s'agit de votre propre lien unique !
Error receiving file
Erreur lors de la réception du fichier
- No comment provided by engineer.
+ alert title
Error reconnecting server
@@ -2954,10 +3048,14 @@ Il s'agit de votre propre lien unique !
Erreur lors de l'arrêt du chat
No comment provided by engineer.
+
+ Error switching profile
+ No comment provided by engineer.
+
Error switching profile!
Erreur lors du changement de profil !
- No comment provided by engineer.
+ alertTitle
Error synchronizing connection
@@ -3002,8 +3100,7 @@ Il s'agit de votre propre lien unique !
Error: %@
Erreur : %@
- file error text
- snd error text
+ alert message
Error: URL is invalid
@@ -3090,6 +3187,11 @@ Il s'agit de votre propre lien unique !
Erreur de fichier
No comment provided by engineer.
+
+ File errors:
+%@
+ alert message
+
File not found - most likely file was deleted or cancelled.
Fichier introuvable - le fichier a probablement été supprimé ou annulé.
@@ -3225,11 +3327,27 @@ Il s'agit de votre propre lien unique !
Transférer
chat item action
+
+ Forward %d message(s)?
+ alert title
+
Forward and save messages
Transférer et sauvegarder des messages
No comment provided by engineer.
+
+ Forward messages
+ alert action
+
+
+ Forward messages without files?
+ alert message
+
+
+ Forward up to 20 messages at once.
+ No comment provided by engineer.
+
Forwarded
Transféré
@@ -3240,6 +3358,10 @@ Il s'agit de votre propre lien unique !
Transféré depuis
No comment provided by engineer.
+
+ Forwarding %lld messages
+ No comment provided by engineer.
+
Forwarding server %@ failed to connect to destination server %@. Please try later.
Le serveur de redirection %@ n'a pas réussi à se connecter au serveur de destination %@. Veuillez réessayer plus tard.
@@ -3289,11 +3411,6 @@ Erreur : %2$@
Nom complet (optionnel)
No comment provided by engineer.
-
- Full name:
- Nom complet :
- No comment provided by engineer.
-
Fully decentralized – visible only to members.
Entièrement décentralisé – visible que par ses membres.
@@ -3539,6 +3656,10 @@ Erreur : %2$@
Serveurs ICE (un par ligne)
No comment provided by engineer.
+
+ IP address
+ No comment provided by engineer.
+
If you can't meet in person, show QR code in a video call, or share the link.
Si vous ne pouvez pas vous rencontrer en personne, montrez le code QR lors d'un appel vidéo ou partagez le lien.
@@ -3614,6 +3735,11 @@ Erreur : %2$@
Importation de l'archive
No comment provided by engineer.
+
+ Improved delivery, reduced traffic usage.
+More improvements are coming soon!
+ No comment provided by engineer.
+
Improved message delivery
Amélioration de la transmission des messages
@@ -4184,6 +4310,10 @@ Voici votre lien pour le groupe %@ !
Serveurs de messages
No comment provided by engineer.
+
+ Message shape
+ No comment provided by engineer.
+
Message source remains private.
La source du message reste privée.
@@ -4234,6 +4364,10 @@ Voici votre lien pour le groupe %@ !
Messages envoyés
No comment provided by engineer.
+
+ Messages were deleted after you selected them.
+ alert message
+
Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.
Les messages, fichiers et appels sont protégés par un chiffrement **de bout en bout** avec une confidentialité persistante, une répudiation et une récupération en cas d'effraction.
@@ -4389,6 +4523,14 @@ Voici votre lien pour le groupe %@ !
Nouveau code d'accès
No comment provided by engineer.
+
+ New SOCKS credentials will be used every time you start the app.
+ No comment provided by engineer.
+
+
+ New SOCKS credentials will be used for each server.
+ No comment provided by engineer.
+
New chat
Nouveau chat
@@ -4509,6 +4651,14 @@ Voici votre lien pour le groupe %@ !
Pas de connexion au réseau
No comment provided by engineer.
+
+ No permission to record speech
+ No comment provided by engineer.
+
+
+ No permission to record video
+ No comment provided by engineer.
+
No permission to record voice message
Pas l'autorisation d'enregistrer un message vocal
@@ -4529,6 +4679,10 @@ Voici votre lien pour le groupe %@ !
Aucune sélection
No comment provided by engineer.
+
+ Nothing to forward!
+ alert title
+
Notifications
Notifications
@@ -4561,7 +4715,7 @@ Voici votre lien pour le groupe %@ !
Ok
Ok
- No comment provided by engineer.
+ alert button
Old database
@@ -4752,6 +4906,11 @@ Nécessite l'activation d'un VPN.
Autres serveurs %@
No comment provided by engineer.
+
+ Other file errors:
+%@
+ alert message
+
PING count
Nombre de PING
@@ -4787,6 +4946,10 @@ Nécessite l'activation d'un VPN.
Code d'accès défini !
No comment provided by engineer.
+
+ Password
+ No comment provided by engineer.
+
Password to show
Mot de passe à entrer
@@ -4936,6 +5099,10 @@ Erreur : %@
Interface en polonais
No comment provided by engineer.
+
+ Port
+ No comment provided by engineer.
+
Possibly, certificate fingerprint in server address is incorrect
Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte
@@ -5021,16 +5188,6 @@ Erreur : %@
Images de profil
No comment provided by engineer.
-
- Profile name
- Nom du profil
- No comment provided by engineer.
-
-
- Profile name:
- Nom du profil :
- No comment provided by engineer.
-
Profile password
Mot de passe de profil
@@ -5133,6 +5290,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Serveurs routés via des proxy
No comment provided by engineer.
+
+ Proxy requires password
+ No comment provided by engineer.
+
Push notifications
Notifications push
@@ -5354,6 +5515,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Supprimer
No comment provided by engineer.
+
+ Remove archive?
+ No comment provided by engineer.
+
Remove image
Enlever l'image
@@ -5534,6 +5699,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Serveur SMP
No comment provided by engineer.
+
+ SOCKS proxy
+ No comment provided by engineer.
+
Safely receive files
Réception de fichiers en toute sécurité
@@ -5547,12 +5716,13 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Save
Enregistrer
- chat item action
+ alert button
+ chat item action
Save (and notify contacts)
Enregistrer (et en informer les contacts)
- No comment provided by engineer.
+ alert button
Save and notify contact
@@ -5579,11 +5749,6 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Enregistrer l'archive
No comment provided by engineer.
-
- Save auto-accept settings
- Enregistrer les paramètres de validation automatique
- No comment provided by engineer.
-
Save group profile
Enregistrer le profil du groupe
@@ -5619,16 +5784,15 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Enregistrer les serveurs ?
No comment provided by engineer.
-
- Save settings?
- Enregistrer les paramètres ?
- No comment provided by engineer.
-
Save welcome message?
Enregistrer le message d'accueil ?
No comment provided by engineer.
+
+ Save your profile?
+ alert title
+
Saved
Enregistré
@@ -5649,6 +5813,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Message enregistré
message info title
+
+ Saving %lld messages
+ No comment provided by engineer.
+
Scale
Échelle
@@ -5729,6 +5897,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Choisir
chat item action
+
+ Select chat profile
+ No comment provided by engineer.
+
Selected %lld
%lld sélectionné(s)
@@ -5847,7 +6019,7 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Sender cancelled file transfer.
L'expéditeur a annulé le transfert de fichiers.
- No comment provided by engineer.
+ alert message
Sender may have deleted the connection request.
@@ -5944,6 +6116,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Envoyé via le proxy
No comment provided by engineer.
+
+ Server
+ No comment provided by engineer.
+
Server address
Adresse du serveur
@@ -6064,6 +6240,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Paramètres
No comment provided by engineer.
+
+ Settings were changed.
+ alert message
+
Shape profile images
Images de profil modelable
@@ -6099,6 +6279,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Partager le lien
No comment provided by engineer.
+
+ Share profile
+ No comment provided by engineer.
+
Share this 1-time invite link
Partager ce lien d'invitation unique
@@ -6234,6 +6418,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Invitation unique SimpleX
simplex link type
+
+ SimpleX protocols reviewed by Trail of Bits.
+ No comment provided by engineer.
+
Simplified incognito mode
Mode incognito simplifié
@@ -6264,6 +6452,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Léger
blur media
+
+ Some app settings were not migrated.
+ No comment provided by engineer.
+
Some file(s) were not exported:
Certains fichiers n'ont pas été exportés :
@@ -6404,6 +6596,14 @@ Activez-le dans les paramètres *Réseau et serveurs*.
Supporter SimpleX Chat
No comment provided by engineer.
+
+ Switch audio and video during the call.
+ No comment provided by engineer.
+
+
+ Switch chat profile for 1-time invitations.
+ No comment provided by engineer.
+
System
Système
@@ -6439,6 +6639,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.
TCP_KEEPINTVL
No comment provided by engineer.
+
+ Tail
+ No comment provided by engineer.
+
Take picture
Prendre une photo
@@ -6631,6 +6835,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise.
Le texte collé n'est pas un lien SimpleX.
No comment provided by engineer.
+
+ The uploaded database archive will be permanently removed from the servers.
+ No comment provided by engineer.
+
Themes
Thèmes
@@ -6758,6 +6966,14 @@ You will be prompted to complete authentication before this feature is enabled.<
Vous serez invité à confirmer l'authentification avant que cette fonction ne soit activée.
No comment provided by engineer.
+
+ To record speech please grant permission to use Microphone.
+ No comment provided by engineer.
+
+
+ To record video please grant permission to use Camera.
+ No comment provided by engineer.
+
To record voice message please grant permission to use Microphone.
Pour enregistrer un message vocal, veuillez accorder la permission d'utiliser le microphone.
@@ -6911,7 +7127,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s
Unknown servers!
Serveurs inconnus !
- No comment provided by engineer.
+ alert title
Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.
@@ -7025,6 +7241,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien
Utiliser les hôtes .onions
No comment provided by engineer.
+
+ Use SOCKS proxy
+ No comment provided by engineer.
+
Use SimpleX Chat servers?
Utiliser les serveurs SimpleX Chat ?
@@ -7090,16 +7310,15 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien
Utiliser l'application d'une main.
No comment provided by engineer.
-
- User profile
- Profil d'utilisateur
- No comment provided by engineer.
-
User selection
Sélection de l'utilisateur
No comment provided by engineer.
+
+ Username
+ No comment provided by engineer.
+
Using SimpleX Chat servers.
Vous utilisez les serveurs SimpleX.
@@ -7333,7 +7552,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien
Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.
Sans Tor ni VPN, votre adresse IP sera visible par ces relais XFTP : %@.
- No comment provided by engineer.
+ alert message
Wrong database passphrase
@@ -7360,11 +7579,6 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien
Serveur XFTP
No comment provided by engineer.
-
- You
- Vous
- No comment provided by engineer.
-
You **must not** use the same database on two devices.
Vous **ne devez pas** utiliser la même base de données sur deux appareils.
@@ -7714,11 +7928,19 @@ Répéter la demande de connexion ?
Votre base de données de chat n'est pas chiffrée - définisez une phrase secrète.
No comment provided by engineer.
+
+ Your chat preferences
+ alert title
+
Your chat profiles
Vos profils de chat
No comment provided by engineer.
+
+ Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.
+ No comment provided by engineer.
+
Your contact sent a file that is larger than currently supported maximum size (%@).
Votre contact a envoyé un fichier plus grand que la taille maximale supportée actuellement(%@).
@@ -7734,6 +7956,10 @@ Répéter la demande de connexion ?
Vos contacts resteront connectés.
No comment provided by engineer.
+
+ Your credentials may be sent unencrypted.
+ No comment provided by engineer.
+
Your current chat database will be DELETED and REPLACED with the imported one.
Votre base de données de chat actuelle va être SUPPRIMEE et REMPLACEE par celle importée.
@@ -7764,13 +7990,15 @@ Répéter la demande de connexion ?
Votre profil **%@** sera partagé.
No comment provided by engineer.
-
- Your profile is stored on your device and shared only with your contacts.
-SimpleX servers cannot see your profile.
- Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts.
-Les serveurs SimpleX ne peuvent pas voir votre profil.
+
+ Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.
+ Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts. Les serveurs SimpleX ne peuvent pas voir votre profil.
No comment provided by engineer.
+
+ Your profile was changed. If you save it, the updated profile will be sent to all your contacts.
+ alert message
+
Your profile, contacts and delivered messages are stored on your device.
Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil.
diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff
index f7328eed91..0c8c1635a5 100644
--- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff
+++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff
@@ -99,7 +99,7 @@
%1$@ at %2$@:
- %1$@ ekkor: %2$@
+ %1$@ ekkor: %2$@:
copied message info, <sender> at <time>
@@ -119,12 +119,12 @@
%@ is not verified
- %@ nem ellenőrzött
+ %@ nem hitelesített
No comment provided by engineer.
%@ is verified
- %@ ellenőrizve
+ %@ hitelesítve
No comment provided by engineer.
@@ -137,6 +137,11 @@
%@ kapcsolódni szeretne!
notification title
+
+ %1$@, %2$@
+ %1$@, %2$@
+ format for date separator in chat
+
%@, %@ and %lld members
%@, %@ és további %lld tag
@@ -157,11 +162,36 @@
%d nap
time interval
+
+ %d file(s) are still being downloaded.
+ %d fájl letöltése még folyamatban van.
+ forward confirmation reason
+
+
+ %d file(s) failed to download.
+ %d fájlt nem sikerült letölteni.
+ forward confirmation reason
+
+
+ %d file(s) were deleted.
+ %d fájl törölve lett.
+ forward confirmation reason
+
+
+ %d file(s) were not downloaded.
+ %d fájl nem lett letöltve.
+ forward confirmation reason
+
%d hours
%d óra
time interval
+
+ %d messages not forwarded
+ %d üzenet nem lett továbbítva
+ alert title
+
%d min
%d perc
@@ -224,7 +254,7 @@
%lld messages blocked by admin
- %lld üzenet letiltva az admin által
+ %lld üzenetet letiltott az admin
No comment provided by engineer.
@@ -234,7 +264,7 @@
%lld messages moderated by %@
- %lld üzenet moderálva lett %@ által
+ %@ %lld üzenetet moderált
No comment provided by engineer.
@@ -324,52 +354,52 @@
**Add contact**: to create a new invitation link, or connect via a link you received.
- **Ismerős hozzáadása**: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz.
+ **Ismerős hozzáadása:** új meghívó-hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz.
No comment provided by engineer.
**Add new contact**: to create your one-time QR Code or link for your contact.
- **Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása az ismerőse számára.
+ **Új ismerős hozzáadása:** egyszer használható QR-kód vagy hivatkozás létrehozása az ismerőse számára.
No comment provided by engineer.
**Create group**: to create a new group.
- **Csoport létrehozása**: új csoport létrehozásához.
+ **Csoport létrehozása:** új csoport létrehozásához.
No comment provided by engineer.
**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.
- **Privátabb**: 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken megosztásra kerül a SimpleX Chat kiszolgálóval, de az nem, hogy hány ismerőse vagy üzenete van.
+ **Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken megosztásra kerül a SimpleX Chat-kiszolgálóval, de az nem, hogy hány ismerőse vagy üzenete van.
No comment provided by engineer.
**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).
- **Legprivátabb**: ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást).
+ **Legprivátabb:** ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást).
No comment provided by engineer.
**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.
- **Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését.
+ **Megjegyzés:** ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését.
No comment provided by engineer.
**Please note**: you will NOT be able to recover or change passphrase if you lose it.
- **Figyelem**: NEM tudja visszaállítani vagy megváltoztatni jelmondatát, ha elveszíti azt.
+ **Megjegyzés:** NEM tudja visszaállítani vagy megváltoztatni jelmondatát, ha elveszíti azt.
No comment provided by engineer.
**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.
- **Javasolt**: az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik.
+ **Megjegyzés:** az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik.
No comment provided by engineer.
**Warning**: Instant push notifications require passphrase saved in Keychain.
- **Figyelmeztetés**: Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges.
+ **Figyelmeztetés:** Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges.
No comment provided by engineer.
**Warning**: the archive will be removed.
- **Figyelem**: az archívum törlésre kerül.
+ **Figyelmeztetés:** az archívum eltávolításra kerül.
No comment provided by engineer.
@@ -414,8 +444,8 @@
- optionally notify deleted contacts.
- profile names with spaces.
- and more!
- - opcionális értesítés a törölt kapcsolatokról.
-- profilnevek szóközökkel.
+ - értesíti az ismerősöket a törlésről (nem kötelező)
+- profil nevek szóközökkel
- és még sok más!
No comment provided by engineer.
@@ -423,8 +453,8 @@
- voice messages up to 5 minutes.
- custom time to disappear.
- editing history.
- - hangüzenetek legfeljebb 5 perces időtartamig.
-- egyedi eltűnési időhatár megadása.
+ - 5 perc hosszúságú hangüzenetek.
+- egyedi üzenet-eltűnési időkorlát.
- előzmények szerkesztése.
No comment provided by engineer.
@@ -492,7 +522,7 @@
<p>Hi!</p>
<p><a href="%@">Connect to me via SimpleX Chat</a></p>
<p>Üdvözlöm!</p>
-<p><a href=„%@”>Csatlakozzon hozzám a SimpleX Chaten</a></p>
+<p><a href="%@">Csatlakozzon hozzám a SimpleX Chaten keresztül</a></p>
email text
@@ -518,8 +548,8 @@
A separate TCP connection will be used **for each contact and group member**.
**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.
- A rendszer külön TCP-kapcsolatot fog használni **minden ismerőshöz és csoporttaghoz**.
-**Figyelem**: sok kapcsolódás esetén, az akkumulátor- és adatforgalom fogyasztás jelentősen megnőhet, és egyes kapcsolatok meghiúsulhatnak.
+ **Minden egyes kapcsolathoz és csoporttaghoz** külön TCP-kapcsolat lesz használva.
+**Megjegyzés:** ha sok kapcsolata van, az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet.
No comment provided by engineer.
@@ -539,17 +569,17 @@
About SimpleX
- A SimpleX-ről
+ A SimpleXről
No comment provided by engineer.
About SimpleX Chat
- A SimpleX Chat-ről
+ A SimpleX Chatről
No comment provided by engineer.
About SimpleX address
- A SimpleX címről
+ A SimpleX-címről
No comment provided by engineer.
@@ -566,17 +596,17 @@
Accept connection request?
- Kapcsolódási kérelem elfogadása?
+ Kapcsolatkérés elfogadása?
No comment provided by engineer.
Accept contact request from %@?
- Elfogadja %@ kapcsolat kérését?
+ Elfogadja %@ kapcsolatkérését?
notification body
Accept incognito
- Fogadás inkognítóban
+ Fogadás inkognitóban
accept contact request via notification
swipe action
@@ -632,7 +662,7 @@
Add welcome message
- Üdvözlő üzenet hozzáadása
+ Üdvözlőüzenet hozzáadása
No comment provided by engineer.
@@ -677,7 +707,7 @@
Advanced settings
- Haladó beállítások
+ Speciális beállítások
No comment provided by engineer.
@@ -697,7 +727,7 @@
All data is private to your device.
- Minden adat biztonságban van a készülékén.
+ Minden adat biztonságban van az eszközén.
No comment provided by engineer.
@@ -712,7 +742,7 @@
All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.
- Minden üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az ön számára törlődnek.
+ Minden üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek.
No comment provided by engineer.
@@ -723,21 +753,21 @@
All profiles
Minden profil
- No comment provided by engineer.
+ profile dropdown
All your contacts will remain connected.
- Minden ismerős kapcsolódva marad.
+ Minden ismerősével kapcsolatban marad.
No comment provided by engineer.
All your contacts will remain connected. Profile update will be sent to your contacts.
- Ismerőseivel kapcsolatban marad. A profil változtatások frissítésre kerülnek az ismerősöknél.
+ Az ismerőseivel kapcsolatban marad. A profil-változtatások frissítésre kerülnek az ismerősöknél.
No comment provided by engineer.
All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.
- Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP átjátszókra.
+ Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra.
No comment provided by engineer.
@@ -747,7 +777,7 @@
Allow calls only if your contact allows them.
- A hívások kezdeményezése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi.
+ A hívások kezdeményezése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi.
No comment provided by engineer.
@@ -757,7 +787,7 @@
Allow disappearing messages only if your contact allows it to you.
- Az eltűnő üzenetek küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi az ön számára.
+ Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi az Ön számára.
No comment provided by engineer.
@@ -767,12 +797,12 @@
Allow irreversible message deletion only if your contact allows it to you. (24 hours)
- Az üzenetek végleges törlése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. (24 óra)
+ Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. (24 óra)
No comment provided by engineer.
Allow message reactions only if your contact allows them.
- Az üzenetreakciók küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi.
+ Az üzenetreakciók küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi.
No comment provided by engineer.
@@ -802,12 +832,12 @@
Allow to send SimpleX links.
- A SimpleX hivatkozások küldése engedélyezve van.
+ A SimpleX-hivatkozások küldése engedélyezve van.
No comment provided by engineer.
Allow to send files and media.
- Fájlok és médiatartalom küldésének engedélyezése.
+ Fájlok és médiatartalmak küldésének engedélyezése.
No comment provided by engineer.
@@ -817,7 +847,7 @@
Allow voice messages only if your contact allows them.
- A hangüzenetek küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi.
+ A hangüzenetek küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi.
No comment provided by engineer.
@@ -872,7 +902,7 @@
Always use relay
- Mindig használjon átjátszó kiszolgálót
+ Mindig használjon közvetítő-kiszolgálót
No comment provided by engineer.
@@ -915,6 +945,11 @@
Az alkalmazás jelkód helyettesítésre kerül egy önmegsemmisítő jelkóddal.
No comment provided by engineer.
+
+ App session
+ Alkalmazás munkamenete
+ No comment provided by engineer.
+
App version
Alkalmazás verzió
@@ -947,7 +982,7 @@
Archive contacts to chat later.
- Ismerősök archiválása a későbbi csevegéshez.
+ Az ismerősök archiválása a későbbi csevegéshez.
No comment provided by engineer.
@@ -1012,7 +1047,7 @@
Auto-accept contact requests
- Kapcsolódási kérelmek automatikus elfogadása
+ Kapcsolatkérések automatikus elfogadása
No comment provided by engineer.
@@ -1020,6 +1055,11 @@
Képek automatikus elfogadása
No comment provided by engineer.
+
+ Auto-accept settings
+ Beállítások automatikus elfogadása
+ alert title
+
Back
Vissza
@@ -1042,7 +1082,12 @@
Bad message hash
- Hibás az üzenet ellenőrzőösszege
+ Hibás az üzenet hasító értéke
+ No comment provided by engineer.
+
+
+ Better calls
+ Továbbfejlesztett hívásélmény
No comment provided by engineer.
@@ -1050,6 +1095,11 @@
Javított csoportok
No comment provided by engineer.
+
+ Better message dates.
+ Továbbfejlesztett üzenetdátumok.
+ No comment provided by engineer.
+
Better messages
Jobb üzenetek
@@ -1060,6 +1110,21 @@
Jobb hálózatkezelés
No comment provided by engineer.
+
+ Better notifications
+ Továbbfejlesztett értesítések
+ No comment provided by engineer.
+
+
+ Better security ✅
+ Továbbfejlesztett biztonság ✅
+ No comment provided by engineer.
+
+
+ Better user experience
+ Továbbfejlesztett felhasználói élmény
+ No comment provided by engineer.
+
Black
Fekete
@@ -1097,7 +1162,7 @@
Blocked by admin
- Letiltva az admin által
+ Az admin letiltotta
No comment provided by engineer.
@@ -1142,7 +1207,7 @@
By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).
- Csevegési profil (alapértelmezett) vagy [kapcsolat alapján] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA).
+ A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA).
No comment provided by engineer.
@@ -1177,7 +1242,7 @@
Can't invite contact!
- Ismerősök meghívása le van tiltva!
+ Ismerős meghívása nem lehetséges!
No comment provided by engineer.
@@ -1193,7 +1258,7 @@
Cancel
Mégse
- No comment provided by engineer.
+ alert button
Cancel migration
@@ -1202,7 +1267,7 @@
Cannot access keychain to save database password
- Nem lehet hozzáférni a kulcstartóhoz az adatbázis jelszavának mentéséhez
+ Nem lehet hozzáférni a kulcstartóhoz az adatbázisjelszó mentéséhez
No comment provided by engineer.
@@ -1213,7 +1278,7 @@
Cannot receive file
Nem lehet fogadni a fájlt
- No comment provided by engineer.
+ alert title
Capacity exceeded - recipient did not receive previously sent messages.
@@ -1232,7 +1297,7 @@
Change database passphrase?
- Adatbázis jelmondat megváltoztatása?
+ Adatbázis-jelmondat megváltoztatása?
No comment provided by engineer.
@@ -1341,6 +1406,16 @@
Csevegési beállítások
No comment provided by engineer.
+
+ Chat preferences were changed.
+ A csevegési beállítások megváltoztak.
+ alert message
+
+
+ Chat profile
+ Csevegési profil
+ No comment provided by engineer.
+
Chat theme
Csevegés témája
@@ -1363,7 +1438,7 @@
Choose _Migrate from another device_ on the new device and scan QR code.
- Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközön és olvassa be a QR-kódot.
+ Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközén és olvassa be a QR-kódot.
No comment provided by engineer.
@@ -1433,7 +1508,7 @@
Compare security codes with your contacts.
- Biztonsági kódok összehasonlítása az ismerősökkel.
+ Biztonsági kódok összehasonlítása az ismerősökével.
No comment provided by engineer.
@@ -1443,7 +1518,7 @@
Configure ICE servers
- ICE kiszolgálók beállítása
+ ICE-kiszolgálók beállítása
No comment provided by engineer.
@@ -1493,7 +1568,7 @@
Confirm that you remember database passphrase to migrate it.
- Erősítse meg, hogy emlékszik az adatbázis jelmondatára az átköltöztetéshez.
+ Az átköltöztetéshez erősítse meg, hogy emlékszik az adatbázis jelmondatára.
No comment provided by engineer.
@@ -1518,7 +1593,7 @@
Connect to desktop
- Kapcsolódás számítógéphez
+ Társítás számítógéppel
No comment provided by engineer.
@@ -1535,14 +1610,14 @@
Connect to yourself?
This is your own SimpleX address!
Kapcsolódás saját magához?
-Ez az ön SimpleX címe!
+Ez az Ön SimpleX-címe!
No comment provided by engineer.
Connect to yourself?
This is your own one-time link!
Kapcsolódás saját magához?
-Ez az egyszer használatos hivatkozása!
+Ez az Ön egyszer használható hivatkozása!
No comment provided by engineer.
@@ -1557,7 +1632,7 @@ Ez az egyszer használatos hivatkozása!
Connect via one-time link
- Kapcsolódás egyszer használatos hivatkozáson keresztül
+ Kapcsolódás egyszer használható hivatkozáson keresztül
No comment provided by engineer.
@@ -1572,7 +1647,7 @@ Ez az egyszer használatos hivatkozása!
Connected desktop
- Csatlakoztatott számítógép
+ Társított számítógép
No comment provided by engineer.
@@ -1597,7 +1672,7 @@ Ez az egyszer használatos hivatkozása!
Connecting to server… (error: %@)
- Kapcsolódás a kiszolgálóhoz... (hiba: %@)
+ Kapcsolódás a kiszolgálóhoz… (hiba: %@)
No comment provided by engineer.
@@ -1637,7 +1712,7 @@ Ez az egyszer használatos hivatkozása!
Connection request sent!
- Kapcsolódási kérés elküldve!
+ Kapcsolatkérés elküldve!
No comment provided by engineer.
@@ -1647,7 +1722,7 @@ Ez az egyszer használatos hivatkozása!
Connection timeout
- Kapcsolat időtúllépés
+ Időtúllépés kapcsolódáskor
No comment provided by engineer.
@@ -1667,7 +1742,7 @@ Ez az egyszer használatos hivatkozása!
Contact already exists
- Létező ismerős
+ Az ismerős már létezik
No comment provided by engineer.
@@ -1692,7 +1767,7 @@ Ez az egyszer használatos hivatkozása!
Contact name
- Ismerős neve
+ Csak név
No comment provided by engineer.
@@ -1712,7 +1787,7 @@ Ez az egyszer használatos hivatkozása!
Contacts can mark messages for deletion; you will be able to view them.
- Az ismerősei törlésre jelölhetnek üzeneteket; ön majd meg tudja nézni azokat.
+ Az ismerősei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat.
No comment provided by engineer.
@@ -1740,6 +1815,11 @@ Ez az egyszer használatos hivatkozása!
Alapverziószám: v%@
No comment provided by engineer.
+
+ Corner
+ Sarok
+ No comment provided by engineer.
+
Correct name to %@?
Név javítása erre: %@?
@@ -1752,7 +1832,7 @@ Ez az egyszer használatos hivatkozása!
Create SimpleX address
- SimpleX cím létrehozása
+ SimpleX-cím létrehozása
No comment provided by engineer.
@@ -1762,7 +1842,7 @@ Ez az egyszer használatos hivatkozása!
Create an address to let people connect with you.
- Cím létrehozása, hogy az emberek kapcsolatba léphessenek önnel.
+ Cím létrehozása, hogy az emberek kapcsolatba léphessenek Önnel.
No comment provided by engineer.
@@ -1777,7 +1857,7 @@ Ez az egyszer használatos hivatkozása!
Create group link
- Csoportos hivatkozás létrehozása
+ Csoporthivatkozás létrehozása
No comment provided by engineer.
@@ -1787,7 +1867,7 @@ Ez az egyszer használatos hivatkozása!
Create new profile in [desktop app](https://simplex.chat/downloads/). 💻
- Új profil létrehozása az [asztali kliensben](https://simplex.chat/downloads/). 💻
+ Új profil létrehozása a [számítógép-alkalmazásban](https://simplex.chat/downloads/). 💻
No comment provided by engineer.
@@ -1797,7 +1877,7 @@ Ez az egyszer használatos hivatkozása!
Create queue
- Várólista létrehozása
+ Sorbaállítás létrehozása
server test step
@@ -1832,7 +1912,7 @@ Ez az egyszer használatos hivatkozása!
Creating archive link
- Archív hivatkozás létrehozása
+ Archívum hivatkozás létrehozása
No comment provided by engineer.
@@ -1865,6 +1945,11 @@ Ez az egyszer használatos hivatkozása!
Személyreszabott idő
No comment provided by engineer.
+
+ Customizable message shape.
+ Testreszabható üzenetbuborékok.
+ No comment provided by engineer.
+
Customize theme
Téma személyre szabása
@@ -1882,17 +1967,17 @@ Ez az egyszer használatos hivatkozása!
Database ID
- Adatbázis ID
+ Adatbázis-azonosító
No comment provided by engineer.
Database ID: %d
- Adatbázis azonosító: %d
+ Adatbázis-azonosító: %d
copied message info
Database IDs and Transport isolation option.
- Adatbázis azonosítók és átviteli izolációs beállítások.
+ Adatbázis-azonosítók és átvitel-izolációs beállítások.
No comment provided by engineer.
@@ -1908,40 +1993,40 @@ Ez az egyszer használatos hivatkozása!
Database encryption passphrase will be updated and stored in the keychain.
- Az adatbázis titkosítási jelmondata frissül és tárolódik a kulcstartóban.
+ Az adatbázis titkosítási jelmondata frissítve lesz és a kulcstartóban kerül tárolásra.
No comment provided by engineer.
Database encryption passphrase will be updated.
- Adatbázis titkosítási jelmondat frissítve lesz.
+ Az datbázis titkosítási jelmondata frissítve lesz.
No comment provided by engineer.
Database error
- Adatbázis hiba
+ Adatbázishiba
No comment provided by engineer.
Database is encrypted using a random passphrase, you can change it.
- Az adatbázis egy véletlenszerű jelmondattal van titkosítva, megváltoztatható.
+ Az adatbázis egy véletlenszerű jelmondattal van titkosítva, ami megváltoztatható.
No comment provided by engineer.
Database is encrypted using a random passphrase. Please change it before exporting.
- Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtti módosítás szükséges.
+ Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt változtassa meg.
No comment provided by engineer.
Database passphrase
- Adatbázis jelmondat
+ Adatbázis-jelmondat
No comment provided by engineer.
Database passphrase & export
- Adatbázis jelmondat és exportálás
+ Adatbázis-jelmondat és -exportálás
No comment provided by engineer.
@@ -1962,7 +2047,7 @@ Ez az egyszer használatos hivatkozása!
Database will be encrypted and the passphrase stored in the keychain.
- Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstartóban lesz tárolva.
+ Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstartóban kerül tárolásra.
No comment provided by engineer.
@@ -1975,7 +2060,7 @@ Ez az egyszer használatos hivatkozása!
Database will be migrated when the app restarts
- Az adatbázis az alkalmazás újraindításakor migrálásra kerül
+ Az adatbázis az alkalmazás újraindításakor átköltöztetésre kerül
No comment provided by engineer.
@@ -2159,9 +2244,14 @@ Ez az egyszer használatos hivatkozása!
Régi adatbázis törlése?
No comment provided by engineer.
+
+ Delete or moderate up to 200 messages.
+ Legfeljebb 200 üzenet egyszerre való törlése, vagy moderálása.
+ No comment provided by engineer.
+
Delete pending connection?
- Függő kapcsolatfelvételi kérések törlése?
+ Függőben lévő ismerőskérelem törlése?
No comment provided by engineer.
@@ -2171,12 +2261,12 @@ Ez az egyszer használatos hivatkozása!
Delete queue
- Várólista törlése
+ Sorbaállítás törlése
server test step
Delete up to 20 messages at once.
- Legfeljebb 20 üzenet törlése egyszerre.
+ Legfeljebb 20 üzenet egyszerre való törlése.
No comment provided by engineer.
@@ -2221,7 +2311,7 @@ Ez az egyszer használatos hivatkozása!
Delivery receipts!
- Üzenet kézbesítési jelentések!
+ Kézbesítési jelentések!
No comment provided by engineer.
@@ -2236,7 +2326,7 @@ Ez az egyszer használatos hivatkozása!
Desktop app version %@ is not compatible with this app.
- Az asztali kliens verziója %@ nem kompatibilis ezzel az alkalmazással.
+ A számítógép-alkalmazás verziója %@ nem kompatibilis ezzel az alkalmazással.
No comment provided by engineer.
@@ -2266,7 +2356,7 @@ Ez az egyszer használatos hivatkozása!
Details
- Részletek
+ További részletek
No comment provided by engineer.
@@ -2291,17 +2381,17 @@ Ez az egyszer használatos hivatkozása!
Device authentication is disabled. Turning off SimpleX Lock.
- A készüléken nincs beállítva a képernyőzár. A SimpleX zár ki van kapcsolva.
+ Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár ki van kapcsolva.
No comment provided by engineer.
Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.
- A készüléken nincs beállítva a képernyőzár. A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén.
+ Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén.
No comment provided by engineer.
Different names, avatars and transport isolation.
- Különböző nevek, avatarok és átviteli izoláció.
+ Különböző nevek, profilképek és átvitel-izoláció.
No comment provided by engineer.
@@ -2321,7 +2411,7 @@ Ez az egyszer használatos hivatkozása!
Disable SimpleX Lock
- SimpleX zár kikapcsolása
+ SimpleX-zár kikapcsolása
authentication reason
@@ -2386,7 +2476,7 @@ Ez az egyszer használatos hivatkozása!
Do NOT send messages directly, even if your or destination server does not support private routing.
- Ne küldjön üzeneteket közvetlenül, még akkor sem, ha az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.
+ Ne küldjön üzeneteket közvetlenül, még akkor sem, ha az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.
No comment provided by engineer.
@@ -2401,7 +2491,7 @@ Ez az egyszer használatos hivatkozása!
Do it later
- Későbbre halaszt
+ Befejezés később
No comment provided by engineer.
@@ -2409,6 +2499,11 @@ Ez az egyszer használatos hivatkozása!
Az előzmények ne kerüljenek elküldésre az új tagok számára.
No comment provided by engineer.
+
+ Do not use credentials with proxy.
+ Ne használja a hitelesítőadatokat proxyval.
+ No comment provided by engineer.
+
Don't create address
Ne hozzon létre címet
@@ -2432,7 +2527,8 @@ Ez az egyszer használatos hivatkozása!
Download
Letöltés
- chat item action
+ alert button
+ chat item action
Download errors
@@ -2449,6 +2545,11 @@ Ez az egyszer használatos hivatkozása!
Fájl letöltése
server test step
+
+ Download files
+ Fájlok letöltése
+ alert action
+
Downloaded
Letöltve
@@ -2471,7 +2572,7 @@ Ez az egyszer használatos hivatkozása!
Duplicate display name!
- Duplikált megjelenítési név!
+ Duplikált megjelenített név!
No comment provided by engineer.
@@ -2501,7 +2602,7 @@ Ez az egyszer használatos hivatkozása!
Enable SimpleX Lock
- SimpleX zár bekapcsolása
+ SimpleX-zár bekapcsolása
authentication reason
@@ -2586,7 +2687,7 @@ Ez az egyszer használatos hivatkozása!
Encrypt stored files & media
- Tárolt fájlok és médiatartalmak titkosítása
+ A tárolt fájlok- és a médiatartalmak titkosítása
No comment provided by engineer.
@@ -2606,12 +2707,12 @@ Ez az egyszer használatos hivatkozása!
Encrypted message: database error
- Titkosított üzenet: adatbázis hiba
+ Titkosított üzenet: adatbázishiba
notification
Encrypted message: database migration error
- Titkosított üzenet: adatbázis-átköltöztetés hiba
+ Titkosított üzenet: adatbázis-átköltöztetési hiba
notification
@@ -2646,7 +2747,7 @@ Ez az egyszer használatos hivatkozása!
Enter correct passphrase.
- Helyes jelmondat bevitele.
+ Adja meg a helyes jelmondatot.
No comment provided by engineer.
@@ -2676,17 +2777,17 @@ Ez az egyszer használatos hivatkozása!
Enter this device name…
- Eszköznév megadása…
+ Adja meg ennek az eszköznek a nevét…
No comment provided by engineer.
Enter welcome message…
- Üdvözlő üzenet megadása…
+ Üdvözlőüzenet megadása…
placeholder
Enter welcome message… (optional)
- Üdvözlő üzenet megadása… (opcionális)
+ Üdvözlőüzenet megadása… (nem kötelező)
placeholder
@@ -2706,7 +2807,7 @@ Ez az egyszer használatos hivatkozása!
Error accepting contact request
- Hiba történt a kapcsolatfelvételi kérelem elfogadásakor
+ Hiba történt a kapcsolatkérés elfogadásakor
No comment provided by engineer.
@@ -2716,7 +2817,7 @@ Ez az egyszer használatos hivatkozása!
Error adding member(s)
- Hiba a tag(-ok) hozzáadásakor
+ Hiba a tag(ok) hozzáadásakor
No comment provided by engineer.
@@ -2724,6 +2825,11 @@ Ez az egyszer használatos hivatkozása!
Hiba a cím megváltoztatásakor
No comment provided by engineer.
+
+ Error changing connection profile
+ Hiba a kapcsolati profilra való váltáskor
+ No comment provided by engineer.
+
Error changing role
Hiba a szerepkör megváltoztatásakor
@@ -2734,6 +2840,11 @@ Ez az egyszer használatos hivatkozása!
Hiba a beállítás megváltoztatásakor
No comment provided by engineer.
+
+ Error changing to incognito!
+ Hiba az inkognitóprofilra való váltáskor!
+ No comment provided by engineer.
+
Error connecting to forwarding server %@. Please try later.
Hiba a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később.
@@ -2751,7 +2862,7 @@ Ez az egyszer használatos hivatkozása!
Error creating group link
- Hiba a csoport hivatkozásának létrehozásakor
+ Hiba a csoporthivatkozás létrehozásakor
No comment provided by engineer.
@@ -2806,7 +2917,7 @@ Ez az egyszer használatos hivatkozása!
Error deleting user profile
- Hiba a felhasználói profil törlésekor
+ Hiba a felhasználó-profil törlésekor
No comment provided by engineer.
@@ -2851,7 +2962,12 @@ Ez az egyszer használatos hivatkozása!
Error loading %@ servers
- Hiba a %@ kiszolgálók betöltésekor
+ Hiba a(z) %@ -kiszolgálók betöltésekor
+ No comment provided by engineer.
+
+
+ Error migrating settings
+ Hiba a beallítások átköltöztetésekor
No comment provided by engineer.
@@ -2862,7 +2978,7 @@ Ez az egyszer használatos hivatkozása!
Error receiving file
Hiba a fájl fogadásakor
- No comment provided by engineer.
+ alert title
Error reconnecting server
@@ -2886,12 +3002,12 @@ Ez az egyszer használatos hivatkozása!
Error saving %@ servers
- Hiba történt a %@ kiszolgálók mentése közben
+ Hiba történt a(z) %@ -kiszolgálók mentésekor
No comment provided by engineer.
Error saving ICE servers
- Hiba az ICE kiszolgálók mentésekor
+ Hiba az ICE-kiszolgálók mentésekor
No comment provided by engineer.
@@ -2901,7 +3017,7 @@ Ez az egyszer használatos hivatkozása!
Error saving passcode
- Hiba a jelkód mentése közben
+ Hiba a jelkód mentésekor
No comment provided by engineer.
@@ -2916,12 +3032,12 @@ Ez az egyszer használatos hivatkozása!
Error saving user password
- Hiba a felhasználó jelszavának mentésekor
+ Hiba a felhasználói jelszó mentésekor
No comment provided by engineer.
Error scanning code: %@
- Hiba a kód beolvasása közben: %@
+ Hiba a kód beolvasásakor: %@
No comment provided by engineer.
@@ -2941,7 +3057,7 @@ Ez az egyszer használatos hivatkozása!
Error setting delivery receipts!
- Hiba történt a kézbesítési igazolások beállításakor!
+ Hiba történt a kézbesítési jelentések beállításakor!
No comment provided by engineer.
@@ -2954,19 +3070,24 @@ Ez az egyszer használatos hivatkozása!
Hiba a csevegés megállításakor
No comment provided by engineer.
+
+ Error switching profile
+ Hiba a profilváltáskor
+ No comment provided by engineer.
+
Error switching profile!
- Hiba a profil váltásakor!
- No comment provided by engineer.
+ Hiba a profilváltásakor!
+ alertTitle
Error synchronizing connection
- Hiba a kapcsolat szinkronizálása közben
+ Hiba a kapcsolat szinkronizálásakor
No comment provided by engineer.
Error updating group link
- Hiba a csoport hivatkozás frissítésekor
+ Hiba a csoporthivatkozás frissítésekor
No comment provided by engineer.
@@ -2981,7 +3102,7 @@ Ez az egyszer használatos hivatkozása!
Error updating user privacy
- Hiba a felhasználói beállítások frissítésekor
+ Hiba a felhasználói adatvédelem frissítésekor
No comment provided by engineer.
@@ -2991,7 +3112,7 @@ Ez az egyszer használatos hivatkozása!
Error verifying passphrase:
- Hiba a jelmondat ellenőrzésekor:
+ Hiba a jelmondat hitelesítésekor:
No comment provided by engineer.
@@ -3002,17 +3123,16 @@ Ez az egyszer használatos hivatkozása!
Error: %@
Hiba: %@
- file error text
- snd error text
+ alert message
Error: URL is invalid
- Hiba: az URL érvénytelen
+ Hiba: a webcím érvénytelen
No comment provided by engineer.
Error: no database file
- Hiba: nincs adatbázis fájl
+ Hiba: nincs adatbázisfájl
No comment provided by engineer.
@@ -3062,7 +3182,7 @@ Ez az egyszer használatos hivatkozása!
Exporting database archive…
- Adatbázis archívum exportálása…
+ Adatbázis-archívum exportálása…
No comment provided by engineer.
@@ -3077,7 +3197,7 @@ Ez az egyszer használatos hivatkozása!
Faster joining and more reliable messages.
- Gyorsabb csatlakozás és megbízhatóbb üzenet kézbesítés.
+ Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés.
No comment provided by engineer.
@@ -3090,6 +3210,13 @@ Ez az egyszer használatos hivatkozása!
Fájlhiba
No comment provided by engineer.
+
+ File errors:
+%@
+ Fájlhiba:
+%@
+ alert message
+
File not found - most likely file was deleted or cancelled.
A fájl nem található - valószínűleg a fájlt törölték vagy visszavonták.
@@ -3137,27 +3264,27 @@ Ez az egyszer használatos hivatkozása!
Files & media
- Fájlok és média
+ Fájlok és médiatartalmak
No comment provided by engineer.
Files and media
- Fájlok és médiatartalom
+ Fájlok és médiatartalmak
chat feature
Files and media are prohibited in this group.
- A fájlok- és a médiatartalom küldése le van tiltva ebben a csoportban.
+ A fájlok- és a médiatartalmak le vannak tiltva ebben a csoportban.
No comment provided by engineer.
Files and media not allowed
- Fájlok és média tartalom küldése le van tiltva
+ A fájlok- és médiatartalmak nincsenek engedélyezve
No comment provided by engineer.
Files and media prohibited!
- A fájlok- és a médiatartalom küldése le van tiltva!
+ A fájlok- és a médiatartalmak küldése le van tiltva!
No comment provided by engineer.
@@ -3225,11 +3352,31 @@ Ez az egyszer használatos hivatkozása!
Továbbítás
chat item action
+
+ Forward %d message(s)?
+ %d üzenet továbbítása?
+ alert title
+
Forward and save messages
Üzenetek továbbítása és mentése
No comment provided by engineer.
+
+ Forward messages
+ Üzenetek továbbítása
+ alert action
+
+
+ Forward messages without files?
+ Üzenetek továbbítása fájlok nélkül?
+ alert message
+
+
+ Forward up to 20 messages at once.
+ Legfeljebb 20 üzenet egyszerre való továbbítása.
+ No comment provided by engineer.
+
Forwarded
Továbbított
@@ -3240,32 +3387,37 @@ Ez az egyszer használatos hivatkozása!
Továbbítva innen:
No comment provided by engineer.
+
+ Forwarding %lld messages
+ %lld üzenet továbbítása
+ No comment provided by engineer.
+
Forwarding server %@ failed to connect to destination server %@. Please try later.
- A(z) %@ továbbító kiszolgáló nem tudott csatlakozni a(z) %@ célkiszolgálóhoz. Próbálja meg később.
+ A(z) %@ továbbító-kiszolgáló nem tudott csatlakozni a(z) %@ célkiszolgálóhoz. Próbálja meg később.
No comment provided by engineer.
Forwarding server address is incompatible with network settings: %@.
- A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@.
+ A továbbító-kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@.
No comment provided by engineer.
Forwarding server version is incompatible with network settings: %@.
- A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@.
+ A továbbító-kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@.
No comment provided by engineer.
Forwarding server: %1$@
Destination server error: %2$@
- Továbbító kiszolgáló: %1$@
+ Továbbító-kiszolgáló: %1$@
Célkiszolgáló hiba:%2$@
snd error text
Forwarding server: %1$@
Error: %2$@
- Továbbító kiszolgáló: %1$@
+ Továbbító-kiszolgáló: %1$@
Hiba: %2$@
snd error text
@@ -3286,17 +3438,12 @@ Hiba: %2$@
Full name (optional)
- Teljes név (opcionális)
- No comment provided by engineer.
-
-
- Full name:
- Teljes név:
+ Teljes név (nem kötelező)
No comment provided by engineer.
Fully decentralized – visible only to members.
- Teljesen decentralizált - kizárólag tagok számára látható.
+ Teljesen decentralizált - csak a tagok számára látható.
No comment provided by engineer.
@@ -3306,7 +3453,7 @@ Hiba: %2$@
Further reduced battery usage
- Tovább csökkentett akkumulátor használat
+ Tovább csökkentett akkumulátor-használat
No comment provided by engineer.
@@ -3346,37 +3493,37 @@ Hiba: %2$@
Group full name (optional)
- Csoport teljes neve (opcionális)
+ Csoport teljes neve (nem kötelező)
No comment provided by engineer.
Group image
- Csoportkép
+ Csoport profilképe
No comment provided by engineer.
Group invitation
- Csoportos meghívó
+ Csoportmeghívó
No comment provided by engineer.
Group invitation expired
- A csoport meghívó lejárt
+ A csoportmeghívó lejárt
No comment provided by engineer.
Group invitation is no longer valid, it was removed by sender.
- A csoport meghívó már nem érvényes, a küldője törölte.
+ A csoportmeghívó már nem érvényes, a küldője eltávolította.
No comment provided by engineer.
Group link
- Csoport hivatkozás
+ Csoporthivatkozás
No comment provided by engineer.
Group links
- Csoport hivatkozások
+ Csoporthivatkozások
No comment provided by engineer.
@@ -3391,7 +3538,7 @@ Hiba: %2$@
Group members can send SimpleX links.
- A csoport tagjai küldhetnek SimpleX hivatkozásokat.
+ A csoport tagjai küldhetnek SimpleX-hivatkozásokat.
No comment provided by engineer.
@@ -3426,7 +3573,7 @@ Hiba: %2$@
Group preferences
- Csoport beállítások
+ Csoportbeállítások
No comment provided by engineer.
@@ -3441,17 +3588,17 @@ Hiba: %2$@
Group welcome message
- Csoport üdvözlő üzenete
+ A csoport üdvözlőüzenete
No comment provided by engineer.
Group will be deleted for all members - this cannot be undone!
- Csoport törlésre kerül minden tag számára - ez a művelet nem vonható vissza!
+ A csoport törlésre kerül minden tag számára - ez a művelet nem vonható vissza!
No comment provided by engineer.
Group will be deleted for you - this cannot be undone!
- A csoport törlésre kerül az ön számára - ez a művelet nem vonható vissza!
+ A csoport törlésre kerül az Ön számára - ez a művelet nem vonható vissza!
No comment provided by engineer.
@@ -3461,7 +3608,7 @@ Hiba: %2$@
Hidden
- Rejtett
+ Se név, se üzenet
No comment provided by engineer.
@@ -3471,12 +3618,12 @@ Hiba: %2$@
Hidden profile password
- Rejtett profil jelszó
+ Rejtett profiljelszó
No comment provided by engineer.
Hide
- Elrejt
+ Összecsukás
chat item action
@@ -3491,7 +3638,7 @@ Hiba: %2$@
Hide:
- Elrejt:
+ Elrejtés:
No comment provided by engineer.
@@ -3526,7 +3673,7 @@ Hiba: %2$@
How to use your servers
- Kiszolgálók használata
+ Saját kiszolgálók használata
No comment provided by engineer.
@@ -3539,6 +3686,11 @@ Hiba: %2$@
ICE-kiszolgálók (soronként egy)
No comment provided by engineer.
+
+ IP address
+ IP-cím
+ No comment provided by engineer.
+
If you can't meet in person, show QR code in a video call, or share the link.
Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás közben, vagy ossza meg a hivatkozást.
@@ -3546,7 +3698,7 @@ Hiba: %2$@
If you enter this passcode when opening the app, all app data will be irreversibly removed!
- Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen törlődik!
+ Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen eltávolításra kerül!
No comment provided by engineer.
@@ -3556,12 +3708,12 @@ Hiba: %2$@
If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).
- Ha most kell használnia a csevegést, koppintson a ** Csináld később** elemre (az alkalmazás újraindításakor felajánlásra kerül az adatbázis áttelepítése).
+ Ha most kell használnia a csevegést, koppintson alább a **Befejezés később** lehetőségre (az alkalmazás újraindításakor felajánlásra kerül az adatbázis átköltöztetése).
No comment provided by engineer.
Ignore
- Figyelmen kívül hagyás
+ Mellőzés
No comment provided by engineer.
@@ -3614,9 +3766,16 @@ Hiba: %2$@
Archívum importálása
No comment provided by engineer.
+
+ Improved delivery, reduced traffic usage.
+More improvements are coming soon!
+ Továbbfejlesztett kézbesítés, csökkentett adatforgalom-használat.
+További fejlesztések hamarosan!
+ No comment provided by engineer.
+
Improved message delivery
- Továbbfejlesztett üzenetküldés
+ Továbbfejlesztett üzenetkézbesítés
No comment provided by engineer.
@@ -3651,12 +3810,12 @@ Hiba: %2$@
Incognito groups
- Inkognitó csoportok
+ Inkognitócsoportok
No comment provided by engineer.
Incognito mode
- Inkognitó mód
+ Inkognitómód
No comment provided by engineer.
@@ -3681,7 +3840,7 @@ Hiba: %2$@
Incompatible database version
- Nem kompatibilis adatbázis verzió
+ Nem kompatibilis adatbázis-verzió
No comment provided by engineer.
@@ -3717,7 +3876,7 @@ Hiba: %2$@
Instant push notifications will be hidden!
- Az azonnali push értesítések elrejtésre kerülnek!
+ Az azonnali push-értesítések elrejtésre kerülnek!
No comment provided by engineer.
@@ -3743,12 +3902,12 @@ Hiba: %2$@
Invalid connection link
- Érvénytelen kapcsolati hivatkozás
+ Érvénytelen kapcsolattartási hivatkozás
No comment provided by engineer.
Invalid display name!
- Érvénytelen megjelenítendő felhaszálónév!
+ Érvénytelen megjelenítendő név!
No comment provided by engineer.
@@ -3823,7 +3982,7 @@ Hiba: %2$@
It can happen when you or your connection used the old database backup.
- Ez akkor fordulhat elő, ha ön vagy az ismerőse régi adatbázis biztonsági mentést használt.
+ Ez akkor fordulhat elő, ha Ön vagy az ismerőse régi adatbázis biztonsági mentést használt.
No comment provided by engineer.
@@ -3839,7 +3998,7 @@ Hiba: %2$@
It protects your IP address and connections.
- Védi az IP-címét és a kapcsolatokat.
+ Védi az IP-címét és a kapcsolatait.
No comment provided by engineer.
@@ -3891,7 +4050,7 @@ Hiba: %2$@
Join your group?
This is your link for group %@!
Csatlakozik a csoportjához?
-Ez az ön hivatkozása a(z) %@ csoporthoz!
+Ez az Ön hivatkozása a(z) %@ csoporthoz!
No comment provided by engineer.
@@ -3926,12 +4085,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
KeyChain error
- Kulcstartó hiba
+ Kulcstartóhiba
No comment provided by engineer.
Keychain error
- Kulcstartó hiba
+ Kulcstartóhiba
No comment provided by engineer.
@@ -3966,7 +4125,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Let's talk in SimpleX Chat
- Beszélgessünk a SimpleX Chat-ben
+ Beszélgessünk a SimpleX Chatben
email subject
@@ -3981,17 +4140,17 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Link mobile and desktop apps! 🔗
- Társítsa össze a mobil és az asztali alkalmazásokat! 🔗
+ Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗
No comment provided by engineer.
Linked desktop options
- Összekapcsolt számítógép beállítások
+ Társított számítógép beállítások
No comment provided by engineer.
Linked desktops
- Összekapcsolt számítógépek
+ Társított számítógépek
No comment provided by engineer.
@@ -4046,7 +4205,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).
- Győződjön meg arról, hogy a %@ kiszolgálócímek megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva (%@).
+ Győződjön meg arról, hogy a(z) %@ kiszolgálócímek megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva (%@).
No comment provided by engineer.
@@ -4056,7 +4215,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*
- Sokan kérdezték: *ha a SimpleX-nek nincsenek felhasználói azonosítói, akkor hogyan tud üzeneteket kézbesíteni?*
+ Sokan kérdezték: *ha a SimpleX Chatnek nincsenek felhasználó-azonosítói, akkor hogyan tud üzeneteket kézbesíteni?*
No comment provided by engineer.
@@ -4131,12 +4290,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Message delivery receipts!
- Üzenet kézbesítési jelentések!
+ Üzenetkézbesítési jelentések!
No comment provided by engineer.
Message delivery warning
- Üzenet kézbesítési figyelmeztetés
+ Üzenetkézbesítési figyelmeztetés
item status text
@@ -4156,7 +4315,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Message queue info
- Üzenet-várakoztatási információ
+ Üzenet-sorbaállítási információ
No comment provided by engineer.
@@ -4176,7 +4335,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Message reception
- Üzenet kézbesítési jelentés
+ Üzenetjelentés
No comment provided by engineer.
@@ -4184,6 +4343,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Üzenetkiszolgálók
No comment provided by engineer.
+
+ Message shape
+ Üzenetbuborék formája
+ No comment provided by engineer.
+
Message source remains private.
Az üzenet forrása titokban marad.
@@ -4201,7 +4365,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Message text
- Üzenet szövege
+ Név és üzenet
No comment provided by engineer.
@@ -4234,6 +4398,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Elküldött üzenetek
No comment provided by engineer.
+
+ Messages were deleted after you selected them.
+ Az üzeneteket törölték miután kiválasztotta őket.
+ alert message
+
Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.
Az üzeneteket, fájlokat és hívásokat **végpontok közötti titkosítással**, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi.
@@ -4276,7 +4445,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Migrating database archive…
- Adatbázis archívum migrálása…
+ Adatbázis-archívum átköltöztetése…
No comment provided by engineer.
@@ -4291,7 +4460,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).
- Sikertelen átköltöztetés. Koppintson a **Kihagyás** lehetőségre az aktuális adatbázis használatának folytatásához. Jelentse a problémát az alkalmazás fejlesztőinek csevegésben vagy e-mailben [chat@simplex.chat](mailto:chat@simplex.chat).
+ Sikertelen átköltöztetés. Koppintson a **Kihagyás** lehetőségre a jelenlegi adatbázis használatának folytatásához. Jelentse a problémát az alkalmazás fejlesztőinek csevegésben vagy e-mailben [chat@simplex.chat](mailto:chat@simplex.chat).
No comment provided by engineer.
@@ -4311,12 +4480,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Moderated at
- Moderálva lett ekkor:
+ Moderálva ekkor:
No comment provided by engineer.
Moderated at: %@
- Moderálva lett ekkor: %@
+ Moderálva ekkor: %@
copied message info
@@ -4389,6 +4558,16 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Új jelkód
No comment provided by engineer.
+
+ New SOCKS credentials will be used every time you start the app.
+ Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítő-adatokat fog használni.
+ No comment provided by engineer.
+
+
+ New SOCKS credentials will be used for each server.
+ Minden egyes kiszolgálóhoz új SOCKS-hitelesítő-adatok legyenek használva.
+ No comment provided by engineer.
+
New chat
Új beszélgetés
@@ -4401,7 +4580,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
New contact request
- Új kapcsolattartási kérelem
+ Új kapcsolatkérés
notification
@@ -4416,7 +4595,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
New desktop app!
- Új asztali alkalmazás!
+ Új számítógép-alkalmazás!
No comment provided by engineer.
@@ -4461,7 +4640,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
No contacts selected
- Nem kerültek ismerősök kiválasztásra
+ Nincs kiválasztva ismerős
No comment provided by engineer.
@@ -4476,7 +4655,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
No device token!
- Nincs eszköztoken!
+ Nincs kiszüléktoken!
No comment provided by engineer.
@@ -4509,6 +4688,16 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Nincs hálózati kapcsolat
No comment provided by engineer.
+
+ No permission to record speech
+ Nincs jogosultság megadva a beszéd rögzítéséhez
+ No comment provided by engineer.
+
+
+ No permission to record video
+ Nincs jogosultság megadva a videó rögzítéséhez
+ No comment provided by engineer.
+
No permission to record voice message
Nincs engedély a hangüzenet rögzítésére
@@ -4526,9 +4715,14 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Nothing selected
- Semmi sincs kiválasztva
+ Nincs kiválasztva semmi
No comment provided by engineer.
+
+ Nothing to forward!
+ Nincs mit továbbítani!
+ alert title
+
Notifications
Értesítések
@@ -4561,7 +4755,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Ok
Rendben
- No comment provided by engineer.
+ alert button
Old database
@@ -4570,36 +4764,36 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!
Old database archive
- Régi adatbázis archívum
+ Régi adatbázis-archívum
No comment provided by engineer.
One-time invitation link
- Egyszer használatos meghívó hivatkozás
+ Egyszer használható meghívó-hivatkozás
No comment provided by engineer.
Onion hosts will be **required** for connection.
Requires compatible VPN.
- A kapcsolódáshoz Onion kiszolgálókra lesz szükség.
-VPN engedélyezése szükséges.
+ Onion-kiszolgálók **szükségesek** a kapcsolódáshoz.
+Kompatibilis VPN szükséges.
No comment provided by engineer.
Onion hosts will be used when available.
Requires compatible VPN.
- Onion kiszolgálók használata, ha azok rendelkezésre állnak.
+ Onion-kiszolgálók használata, ha azok rendelkezésre állnak.
VPN engedélyezése szükséges.
No comment provided by engineer.
Onion hosts will not be used.
- Onion kiszolgálók nem lesznek használva.
+ Onion-kiszolgálók nem lesznek használva.
No comment provided by engineer.
Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.
- Csak a klienseszközök tárolják a felhasználói profilokat, névjegyeket, csoportokat és a **2 rétegű végponttól-végpontig titkosítással** küldött üzeneteket.
+ Csak az eszközök alkalmazásai tárolják a felhasználó-profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket.
No comment provided by engineer.
@@ -4624,42 +4818,42 @@ VPN engedélyezése szükséges.
Only you can add message reactions.
- Csak ön adhat hozzá üzenetreakciókat.
+ Csak Ön adhat hozzá üzenetreakciókat.
No comment provided by engineer.
Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)
- Véglegesen csak ön törölhet üzeneteket (ismerőse csak törlésre jelölheti meg őket ). (24 óra)
+ Véglegesen csak Ön törölhet üzeneteket (ismerőse csak törlésre jelölheti meg őket ). (24 óra)
No comment provided by engineer.
Only you can make calls.
- Csak ön tud hívásokat indítani.
+ Csak Ön tud hívásokat indítani.
No comment provided by engineer.
Only you can send disappearing messages.
- Csak ön tud eltűnő üzeneteket küldeni.
+ Csak Ön tud eltűnő üzeneteket küldeni.
No comment provided by engineer.
Only you can send voice messages.
- Csak ön tud hangüzeneteket küldeni.
+ Csak Ön tud hangüzeneteket küldeni.
No comment provided by engineer.
Only your contact can add message reactions.
- Csak az ismerős tud üzeneteakciókat adni.
+ Csak az ismerőse tud üzenetreakciókat küldeni.
No comment provided by engineer.
Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)
- Csak az ismerőse tudja az üzeneteket véglegesen törölni (ön csak törlésre jelölheti meg azokat). (24 óra)
+ Csak az ismerőse tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra)
No comment provided by engineer.
Only your contact can make calls.
- Csak az ismerős tud hívást indítani.
+ Csak az ismerőse tud hívást indítani.
No comment provided by engineer.
@@ -4669,7 +4863,7 @@ VPN engedélyezése szükséges.
Only your contact can send voice messages.
- Csak az ismerős tud hangüzeneteket küldeni.
+ Csak az ismerőse tud hangüzeneteket küldeni.
No comment provided by engineer.
@@ -4704,12 +4898,12 @@ VPN engedélyezése szükséges.
Open server settings
- Kiszolgáló beállításainak megnyitása
+ Kiszolgáló-beállítások megnyitása
No comment provided by engineer.
Open user profiles
- Felhasználói profilok megnyitása
+ Felhasználó-profilok megnyitása
authentication reason
@@ -4734,7 +4928,7 @@ VPN engedélyezése szükséges.
Or securely share this file link
- Vagy a fájl hivítkozásának biztonságos megosztása
+ Vagy ossza meg biztonságosan ezt a fájlhivatkozást
No comment provided by engineer.
@@ -4752,14 +4946,21 @@ VPN engedélyezése szükséges.
További %@ kiszolgálók
No comment provided by engineer.
+
+ Other file errors:
+%@
+ Egyéb fájlhiba:
+%@
+ alert message
+
PING count
- PING számláló
+ PING-ek száma
No comment provided by engineer.
PING interval
- PING időköze
+ Időtartam a PING-ek között
No comment provided by engineer.
@@ -4787,6 +4988,11 @@ VPN engedélyezése szükséges.
A jelkód beállítva!
No comment provided by engineer.
+
+ Password
+ Jelszó
+ No comment provided by engineer.
+
Password to show
Jelszó megjelenítése
@@ -4794,7 +5000,7 @@ VPN engedélyezése szükséges.
Past member %@
- Már nem tag - %@
+ (Már nem tag) %@
past/unknown group member
@@ -4814,17 +5020,17 @@ VPN engedélyezése szükséges.
Paste the link you received
- Fogadott hivatkozás beillesztése
+ Kapott hivatkozás beillesztése
No comment provided by engineer.
Pending
- Függő
+ Függőben
No comment provided by engineer.
People can connect to you only via the links you share.
- Az emberek csak az ön által megosztott hivatkozáson keresztül kapcsolódhatnak.
+ Az emberek csak az Ön által megosztott hivatkozáson keresztül kapcsolódhatnak.
No comment provided by engineer.
@@ -4854,34 +5060,34 @@ VPN engedélyezése szükséges.
Please ask your contact to enable sending voice messages.
- Ismerős felkérése, hogy engedélyezze a hangüzenetek küldését.
+ Kérje meg az ismerősét, hogy engedélyezze a hangüzenetek küldését.
No comment provided by engineer.
Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.
Please share any other issues with the developers.
- Ellenőrizze, hogy a mobil és az asztali számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint az asztali számítógép tűzfalában engedélyezve van-e a kapcsolat.
+ Ellenőrizze, hogy a hordozható eszköz és a számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint a számítógép tűzfalában engedélyezve van-e a kapcsolat.
Minden további problémát osszon meg a fejlesztőkkel.
No comment provided by engineer.
Please check that you used the correct link or ask your contact to send you another one.
- Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg ismerősét, hogy küldjön egy másikat.
+ Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg az ismerősét, hogy küldjön egy másikat.
No comment provided by engineer.
Please check your network connection with %@ and try again.
- Ellenőrizze hálózati kapcsolatát a(z) %@ segítségével, és próbálja újra.
+ Ellenőrizze a hálózati kapcsolatát a(z) %@ segítségével, és próbálja újra.
No comment provided by engineer.
Please check yours and your contact preferences.
- Ellenőrizze az ön és ismerőse beállításait.
+ Ellenőrizze a saját- és az ismerőse beállításait.
No comment provided by engineer.
Please confirm that network settings are correct for this device.
- Ellenőrizze, hogy a hálózati beállítások megfelelőek-e ehhez az eszközhöz.
+ Ellenőrizze, hogy a hálózati beállítások megfelelők-e ehhez az eszközhöz.
No comment provided by engineer.
@@ -4898,7 +5104,7 @@ Hiba: %@
Please enter correct current passphrase.
- Adja meg a helyes aktuális jelmondatát.
+ Adja meg a helyes, jelenlegi jelmondatát.
No comment provided by engineer.
@@ -4918,7 +5124,7 @@ Hiba: %@
Please restart the app and migrate the database to enable push notifications.
- Indítsa újra az alkalmazást az adatbázis-átköltöztetéséhez szükséges push értesítések engedélyezéséhez.
+ Indítsa újra az alkalmazást az adatbázis-átköltöztetéséhez szükséges push-értesítések engedélyezéséhez.
No comment provided by engineer.
@@ -4936,6 +5142,11 @@ Hiba: %@
Lengyel kezelőfelület
No comment provided by engineer.
+
+ Port
+ Port
+ No comment provided by engineer.
+
Possibly, certificate fingerprint in server address is incorrect
Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen
@@ -4978,17 +5189,17 @@ Hiba: %@
Private filenames
- Privát fájl nevek
+ Privát fájlnevek
No comment provided by engineer.
Private message routing
- Privát üzenet útválasztás
+ Privát üzenet-útválasztás
No comment provided by engineer.
Private message routing 🚀
- Privát üzenet útválasztás 🚀
+ Privát üzenet-útválasztás 🚀
No comment provided by engineer.
@@ -5003,7 +5214,7 @@ Hiba: %@
Private routing error
- Privát útválasztási hiba
+ Privát útválasztáshiba
No comment provided by engineer.
@@ -5021,16 +5232,6 @@ Hiba: %@
Profilképek
No comment provided by engineer.
-
- Profile name
- Profilnév
- No comment provided by engineer.
-
-
- Profile name:
- Profil neve:
- No comment provided by engineer.
-
Profile password
Profiljelszó
@@ -5068,7 +5269,7 @@ Hiba: %@
Prohibit sending SimpleX links.
- A SimpleX hivatkozások küldése le van tiltva.
+ A SimpleX-hivatkozások küldése le van tiltva.
No comment provided by engineer.
@@ -5083,7 +5284,7 @@ Hiba: %@
Prohibit sending files and media.
- Fájlok- és a médiatartalom küldés letiltása.
+ Fájlok- és a médiatartalmak küldésének letiltása.
No comment provided by engineer.
@@ -5104,23 +5305,23 @@ Hiba: %@
Protect your IP address from the messaging relays chosen by your contacts.
Enable in *Network & servers* settings.
- Védje IP-címét az ismerősei által kiválasztott üzenetküldő átjátszókkal szemben.
-Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
+ Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben.
+Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben.
No comment provided by engineer.
Protect your chat profiles with a password!
- Csevegési profiljok védelme jelszóval!
+ Védje meg a csevegési profiljait egy jelszóval!
No comment provided by engineer.
Protocol timeout
- Protokoll időtúllépés
+ Protokoll időtúllépése
No comment provided by engineer.
Protocol timeout per KB
- Protokoll időkorlát KB-onként
+ Protokoll időtúllépése KB-onként
No comment provided by engineer.
@@ -5133,14 +5334,19 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Proxyzott kiszolgálók
No comment provided by engineer.
+
+ Proxy requires password
+ A proxy jelszót igényel
+ No comment provided by engineer.
+
Push notifications
- Push értesítések
+ Push-értesítések
No comment provided by engineer.
Push server
- Push kiszolgáló
+ Push-kiszolgáló
No comment provided by engineer.
@@ -5200,7 +5406,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Receipts are disabled
- Üzenet kézbesítési jelentés letiltva
+ A kézbesítési jelentések le vannak tiltva
No comment provided by engineer.
@@ -5220,12 +5426,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Received file event
- Fogadott fájl esemény
+ Fogadott fájlesemény
notification
Received message
- Fogadott üzenet
+ Fogadott üzenetbuborék színe
message info title
@@ -5235,12 +5441,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Received reply
- Fogadott válasz
+ Fogadott válaszüzenet-buborék színe
No comment provided by engineer.
Received total
- Összes fogadott
+ Összes fogadott üzenet
No comment provided by engineer.
@@ -5270,7 +5476,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Recipients see updates as you type them.
- A címzettek a beírás közben látják a frissítéseket.
+ A címzettek a beírás közben látják a szövegváltozásokat.
No comment provided by engineer.
@@ -5280,7 +5486,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Reconnect all connected servers to force message delivery. It uses additional traffic.
- Újrakapcsolódás az összes kiszolgálóhoz az üzenetek kézbesítésének kikényszerítéséhez. Ez további forgalmat használ.
+ Az összes kiszolgálóhoz való újrakapcsolódás az üzenetkézbesítési jelentések kikényszerítéséhez. Ez további adatforgalmat használ.
No comment provided by engineer.
@@ -5295,7 +5501,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Reconnect server to force message delivery. It uses additional traffic.
- A kiszolgálóhoz való újrakapcsolódás az üzenet kézbesítésének kikényszerítéséhez. Ez további adatforgalmat használ.
+ A kiszolgálóhoz való újrakapcsolódás az üzenetkézbesítési jelentések kikényszerítéséhez. Ez további adatforgalmat használ.
No comment provided by engineer.
@@ -5320,7 +5526,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Reduced battery usage
- Csökkentett akkumulátorhasználat
+ Csökkentett akkumulátor-használat
No comment provided by engineer.
@@ -5336,17 +5542,17 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Reject contact request
- Kapcsolatfelvételi kérelem elutasítása
+ Kapcsolatkérés elutasítása
No comment provided by engineer.
Relay server is only used if necessary. Another party can observe your IP address.
- Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címet.
+ A közvetítő-kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címet.
No comment provided by engineer.
Relay server protects your IP address, but it can observe the duration of the call.
- Az átjátszó kiszolgáló megvédi az IP-címet, de megfigyelheti a hívás időtartamát.
+ A közvetítő-kiszolgáló megvédi az IP-címet, de megfigyelheti a hívás időtartamát.
No comment provided by engineer.
@@ -5354,6 +5560,11 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Eltávolítás
No comment provided by engineer.
+
+ Remove archive?
+ Archívum eltávolítása?
+ No comment provided by engineer.
+
Remove image
Kép eltávolítása
@@ -5391,7 +5602,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Repeat connection request?
- Kapcsolódási kérés megismétlése?
+ Kapcsolatkérés megismétlése?
No comment provided by engineer.
@@ -5406,7 +5617,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Repeat join request?
- Csatlakozási kérés megismétlése?
+ Csatlakozáskérés megismétlése?
No comment provided by engineer.
@@ -5426,7 +5637,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Reset
- Alaphelyzetbe állítás
+ Visszaállítás
No comment provided by engineer.
@@ -5446,7 +5657,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Reset colors
- Színek alaphelyzetbe állítása
+ Színek visszaállítása
No comment provided by engineer.
@@ -5456,7 +5667,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Reset to defaults
- Alaphelyzetbe állítás
+ Visszaállítás alapértelmezettre
No comment provided by engineer.
@@ -5534,6 +5745,11 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
SMP-kiszolgáló
No comment provided by engineer.
+
+ SOCKS proxy
+ SOCKS proxy
+ No comment provided by engineer.
+
Safely receive files
Fájlok biztonságos fogadása
@@ -5547,16 +5763,17 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Save
Mentés
- chat item action
+ alert button
+ chat item action
Save (and notify contacts)
- Mentés (és az ismerősök értesítése)
- No comment provided by engineer.
+ Mentés és az ismerősök értesítése
+ alert button
Save and notify contact
- Mentés és ismerős értesítése
+ Mentés és az ismerős értesítése
No comment provided by engineer.
@@ -5571,7 +5788,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Save and update group profile
- Mentés és csoportprofil frissítése
+ Mentés és a csoportprofil frissítése
No comment provided by engineer.
@@ -5579,19 +5796,14 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Archívum mentése
No comment provided by engineer.
-
- Save auto-accept settings
- Automatikus elfogadási beállítások mentése
- No comment provided by engineer.
-
Save group profile
- Csoportprofil elmentése
+ Csoportprofil mentése
No comment provided by engineer.
Save passphrase and open chat
- Jelmondat elmentése és csevegés megnyitása
+ Jelmondat mentése és a csevegés megnyitása
No comment provided by engineer.
@@ -5606,7 +5818,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Save profile password
- Felhasználói fiók jelszavának mentése
+ Profiljelszó mentése
No comment provided by engineer.
@@ -5619,16 +5831,16 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Kiszolgálók mentése?
No comment provided by engineer.
-
- Save settings?
- Beállítások mentése?
- No comment provided by engineer.
-
Save welcome message?
- Üdvözlőszöveg mentése?
+ Üdvözlőüzenet mentése?
No comment provided by engineer.
+
+ Save your profile?
+ Profil mentése?
+ alert title
+
Saved
Mentett
@@ -5636,7 +5848,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Saved WebRTC ICE servers will be removed
- A mentett WebRTC ICE kiszolgálók eltávolításra kerülnek
+ A mentett WebRTC ICE-kiszolgálók eltávolításra kerülnek
No comment provided by engineer.
@@ -5649,6 +5861,11 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Mentett üzenet
message info title
+
+ Saving %lld messages
+ %lld üzenet mentése
+ No comment provided by engineer.
+
Scale
Méretezés
@@ -5676,7 +5893,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Scan security code from your contact's app.
- Biztonsági kód beolvasása ismerős általi alkalmazásból.
+ Biztonsági kód beolvasása az ismerősének alkalmazásából.
No comment provided by engineer.
@@ -5691,12 +5908,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Search bar accepts invitation links.
- A keresősáv elfogadja a meghívó hivatkozásokat.
+ A keresősáv elfogadja a meghívó-hivatkozásokat.
No comment provided by engineer.
Search or paste SimpleX link
- Keresés, vagy SimpleX hivatkozás beillesztése
+ Keresés, vagy SimpleX-hivatkozás beillesztése
No comment provided by engineer.
@@ -5706,7 +5923,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Secure queue
- Biztonságos várólista
+ Biztonságos sorbaállítás
server test step
@@ -5726,9 +5943,14 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Select
- Választás
+ Kiválasztás
chat item action
+
+ Select chat profile
+ Csevegési profil kiválasztása
+ No comment provided by engineer.
+
Selected %lld
%lld kiválasztva
@@ -5806,12 +6028,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Send messages directly when IP address is protected and your or destination server does not support private routing.
- Közvetlen üzenetküldés, ha az IP-cím védett és az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.
+ Közvetlen üzenetküldés, ha az IP-cím védett és az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.
No comment provided by engineer.
Send messages directly when your or destination server does not support private routing.
- Közvetlen üzenetküldés, ha az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.
+ Közvetlen üzenetküldés, ha az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.
No comment provided by engineer.
@@ -5831,7 +6053,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Send receipts
- Üzenet kézbesítési jelentések
+ Kézbesítési jelentések küldése
No comment provided by engineer.
@@ -5847,11 +6069,11 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Sender cancelled file transfer.
A fájl küldője visszavonta az átvitelt.
- No comment provided by engineer.
+ alert message
Sender may have deleted the connection request.
- A küldő törölhette a kapcsolódási kérelmet.
+ A küldő törölhette a kapcsolatkérést.
No comment provided by engineer.
@@ -5861,7 +6083,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Sending delivery receipts will be enabled for all contacts.
- A kézbesítési jelentés küldése minden ismerős számára engedélyezésre kerül.
+ A kézbesítési jelentés küldése minden ismerőse számára engedélyezésre kerül.
No comment provided by engineer.
@@ -5871,22 +6093,22 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Sending receipts is disabled for %lld contacts
- A kézbesítési jelentések küldése le van tiltva %lld ismerősnél
+ A kézbesítési jelentések le vannak tiltva %lld ismerősnél
No comment provided by engineer.
Sending receipts is disabled for %lld groups
- A kézbesítési jelentések küldése le van tiltva %lld csoportban
+ A kézbesítési jelentések le vannak tiltva %lld csoportban
No comment provided by engineer.
Sending receipts is enabled for %lld contacts
- A kézbesítési jelentések küldése engedélyezve van %lld ismerős számára
+ A kézbesítési jelentések engedélyezve vannak %lld ismerősnél
No comment provided by engineer.
Sending receipts is enabled for %lld groups
- A kézbesítési jelentések küldése engedélyezve van %lld csoportban
+ A kézbesítési jelentések engedélyezve vannak %lld csoportban
No comment provided by engineer.
@@ -5911,12 +6133,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Sent file event
- Elküldött fájl esemény
+ Elküldött fájlesemény
notification
Sent message
- Elküldött üzenet
+ Üzenetbuborék színe
message info title
@@ -5931,12 +6153,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Sent reply
- Elküldött válasz
+ Válaszüzenet-buborék színe
No comment provided by engineer.
Sent total
- Összes elküldött
+ Összes elküldött üzenet
No comment provided by engineer.
@@ -5944,6 +6166,11 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Proxyn keresztül küldve
No comment provided by engineer.
+
+ Server
+ Kiszolgáló
+ No comment provided by engineer.
+
Server address
Kiszolgáló címe
@@ -5961,7 +6188,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Server requires authorization to create queues, check password
- A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát
+ A kiszolgálónak engedélyre van szüksége a sorbaállítás létrehozásához, ellenőrizze jelszavát
server test error
@@ -5996,12 +6223,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Servers info
- információk a kiszolgálókról
+ Információk a kiszolgálókról
No comment provided by engineer.
Servers statistics will be reset - this cannot be undone!
- A kiszolgálók statisztikái visszaállnak - ez nem vonható vissza!
+ A kiszolgálók statisztikái visszaállnak - ez a művelet nem vonható vissza!
No comment provided by engineer.
@@ -6064,6 +6291,11 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Beállítások
No comment provided by engineer.
+
+ Settings were changed.
+ A beállítások megváltoztak.
+ alert message
+
Shape profile images
Profilkép alakzat
@@ -6076,7 +6308,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Share 1-time link
- Egyszer használatos hivatkozás megosztása
+ Egyszer használható hivatkozás megosztása
No comment provided by engineer.
@@ -6099,9 +6331,14 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Hivatkozás megosztása
No comment provided by engineer.
+
+ Share profile
+ Profil megosztása
+ No comment provided by engineer.
+
Share this 1-time invite link
- Egyszer használatos meghívó hivatkozás megosztása
+ Egyszer használható meghívó-hivatkozás megosztása
No comment provided by engineer.
@@ -6131,12 +6368,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Show last messages
- Utolsó üzenetek megjelenítése
+ Szobák utolsó üzeneteinek megjelenítése a listanézetben
No comment provided by engineer.
Show message status
- Üzenet állapot megjelenítése
+ Üzenetállapot megjelenítése
No comment provided by engineer.
@@ -6146,7 +6383,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Show preview
- Előnézet megjelenítése
+ Értesítés előnézete
No comment provided by engineer.
@@ -6166,7 +6403,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
SimpleX Address
- SimpleX cím
+ SimpleX-cím
No comment provided by engineer.
@@ -6176,7 +6413,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
SimpleX Lock
- SimpleX zár
+ SimpleX-zár
No comment provided by engineer.
@@ -6186,17 +6423,17 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
SimpleX Lock not enabled!
- A SimpleX zár nincs bekapcsolva!
+ A SimpleX-zár nincs bekapcsolva!
No comment provided by engineer.
SimpleX Lock turned on
- SimpleX zár bekapcsolva
+ SimpleX-zár bekapcsolva
No comment provided by engineer.
SimpleX address
- SimpleX cím
+ SimpleX-cím
No comment provided by engineer.
@@ -6211,32 +6448,37 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
SimpleX group link
- SimpleX csoport hivatkozás
+ SimpleX csoporthivatkozás
simplex link type
SimpleX links
- SimpleX hivatkozások
+ SimpleX-hivatkozások
chat feature
SimpleX links are prohibited in this group.
- A SimpleX hivatkozások küldése le van tiltva ebben a csoportban.
+ A SimpleX-hivatkozások küldése le van tiltva ebben a csoportban.
No comment provided by engineer.
SimpleX links not allowed
- A SimpleX hivatkozások küldése le van tiltva
+ A SimpleX-hivatkozások küldése le van tiltva
No comment provided by engineer.
SimpleX one-time invitation
- SimpleX egyszer használatos meghívó
+ Egyszer használható SimpleX-meghívó
simplex link type
+
+ SimpleX protocols reviewed by Trail of Bits.
+ A SimpleX Chat biztonsága a Trail of Bits által lett újraauditálva.
+ No comment provided by engineer.
+
Simplified incognito mode
- Egyszerűsített inkognító mód
+ Egyszerűsített inkognitómód
No comment provided by engineer.
@@ -6264,6 +6506,11 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Enyhe
blur media
+
+ Some app settings were not migrated.
+ Egyes alkalmazásbeállítások nem lettek átköltöztetve.
+ No comment provided by engineer.
+
Some file(s) were not exported:
Néhány fájl nem került exportálásra:
@@ -6271,12 +6518,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Some non-fatal errors occurred during import - you may see Chat console for more details.
- Néhány nem végzetes hiba történt az importálás közben – további részletekért a csevegési konzolban olvashat.
+ Néhány nem végzetes hiba történt az importáláskor – további részleteket a csevegési konzolban olvashat.
No comment provided by engineer.
Some non-fatal errors occurred during import:
- Néhány nem végzetes hiba történt az importálás közben:
+ Néhány nem végzetes hiba történt az importáláskor:
No comment provided by engineer.
@@ -6336,7 +6583,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.
- A csevegés megállítása a csevegő adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni.
+ A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni.
No comment provided by engineer.
@@ -6351,12 +6598,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Stop receiving file?
- Fájl fogadás megállítása?
+ Fájlfogadás megállítása?
No comment provided by engineer.
Stop sending file?
- Fájl küldés megállítása?
+ Fájlküldés megállítása?
No comment provided by engineer.
@@ -6396,12 +6643,22 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Subscriptions ignored
- Elutasított feliratkozások
+ Mellőzött feliratkozások
No comment provided by engineer.
Support SimpleX Chat
- Támogassa a SimpleX Chatet
+ SimpleX Chat támogatása
+ No comment provided by engineer.
+
+
+ Switch audio and video during the call.
+ Hang/Videó váltása hívás közben.
+ No comment provided by engineer.
+
+
+ Switch chat profile for 1-time invitations.
+ Csevegési profilváltás az egyszer használható meghívókhoz.
No comment provided by engineer.
@@ -6421,7 +6678,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
TCP connection timeout
- TCP kapcsolat időtúllépés
+ TCP kapcsolat időtúllépése
No comment provided by engineer.
@@ -6439,6 +6696,11 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
TCP_KEEPINTVL
No comment provided by engineer.
+
+ Tail
+ Farok
+ No comment provided by engineer.
+
Take picture
Kép készítése
@@ -6451,7 +6713,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Tap to Connect
- Koppintson a kapcsolódáshoz
+ Koppintson ide a kapcsolódáshoz
No comment provided by engineer.
@@ -6461,27 +6723,27 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Tap to join
- Koppintson a csatlakozáshoz
+ Koppintson ide a csatlakozáshoz
No comment provided by engineer.
Tap to join incognito
- Koppintson az inkognitóban való csatlakozáshoz
+ Koppintson ide az inkognitóban való csatlakozáshoz
No comment provided by engineer.
Tap to paste link
- Koppintson a hivatkozás beillesztéséhez
+ Koppintson ide a hivatkozás beillesztéséhez
No comment provided by engineer.
Tap to scan
- Koppintson a beolvasáshoz
+ Koppintson ide a QR-kód beolvasáshoz
No comment provided by engineer.
Temporary file error
- Ideiglenes fájlhiba
+ Ideiglenesfájl-hiba
No comment provided by engineer.
@@ -6516,12 +6778,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.
Thanks to the users – contribute via Weblate!
- Köszönet a felhasználóknak - hozzájárulás a Weblaten!
+ Köszönet a felhasználóknak - hozzájárulás a Weblate-en!
No comment provided by engineer.
The 1st platform without any user identifiers – private by design.
- Az első csevegési rendszer bármiféle felhasználó azonosító nélkül - privátra lett tervezre.
+ Az első csevegési rendszer bármiféle felhasználó-azonosító nélkül - privátra lett tervezre.
No comment provided by engineer.
@@ -6533,7 +6795,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
The app can notify you when you receive messages or contact requests - please open settings to enable.
- Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatfelvételi kéréseket kap – beállítások megnyitása az engedélyezéshez.
+ Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatkéréseket kap – beállítások megnyitása az engedélyezéshez.
No comment provided by engineer.
@@ -6548,12 +6810,12 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
The code you scanned is not a SimpleX link QR code.
- A beolvasott kód nem egy SimpleX hivatkozás QR-kód.
+ A beolvasott QR-kód nem egy SimpleX QR-kód hivatkozás.
No comment provided by engineer.
The connection you accepted will be cancelled!
- Az ön által elfogadott kapcsolat vissza lesz vonva!
+ Az Ön által elfogadott kérelem vissza lesz vonva!
No comment provided by engineer.
@@ -6563,7 +6825,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
The created archive is available via app Settings / Database / Old database archive.
- A létrehozott archívum a Beállítások / Adatbázis / Régi adatbázis-archívum menüben érhető el.
+ A létrehozott archívum a „Beállítások / Adatbázis / Régi adatbázis-archívum” menüben érhető el.
No comment provided by engineer.
@@ -6573,7 +6835,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
The hash of the previous message is different.
- Az előző üzenet ellenőrzőösszege különbözik.
+ Az előző üzenet hasító értéke különbözik.
No comment provided by engineer.
@@ -6603,12 +6865,12 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
The old database was not removed during the migration, it can be deleted.
- A régi adatbázis nem került eltávolításra az átköltöztetés közben, így törölhető.
+ A régi adatbázis nem került eltávolításra az átköltöztetéskor, így törölhető.
No comment provided by engineer.
The profile is only shared with your contacts.
- Profilja csak az ismerőseivel kerül megosztásra.
+ A profilja csak az ismerőseivel kerül megosztásra.
No comment provided by engineer.
@@ -6623,12 +6885,17 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
The servers for new connections of your current chat profile **%@**.
- Jelenlegi profil új ismerőseinek kiszolgálói **%@**.
+ A jelenlegi csevegési profilhoz tartozó új kapcsolatok kiszolgálói **%@**.
No comment provided by engineer.
The text you pasted is not a SimpleX link.
- A beillesztett szöveg nem egy SimpleX hivatkozás.
+ A beillesztett szöveg nem egy SimpleX-hivatkozás.
+ No comment provided by engineer.
+
+
+ The uploaded database archive will be permanently removed from the servers.
+ A feltöltött adatbázis-archívum véglegesen eltávolításra kerül a kiszolgálókról.
No comment provided by engineer.
@@ -6643,12 +6910,12 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
They can be overridden in contact and group settings.
- Ezek felülbírálhatóak az ismerős- és csoportbeállításokban.
+ Ezek felülbírálhatók az ismerős- és csoportbeállításokban.
No comment provided by engineer.
This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.
- Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású képek viszont megmaradnak.
+ Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalmakkal együtt törlésre kerül. Az alacsony felbontású képek viszont megmaradnak.
No comment provided by engineer.
@@ -6678,7 +6945,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
This display name is invalid. Please choose another name.
- Ez a megjelenített felhasználónév érvénytelen. Válasszon egy másik nevet.
+ Ez a megjelenített név érvénytelen. Válasszon egy másik nevet.
No comment provided by engineer.
@@ -6693,17 +6960,17 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
This is your own SimpleX address!
- Ez az ön SimpleX címe!
+ Ez az Ön SimpleX-címe!
No comment provided by engineer.
This is your own one-time link!
- Ez az egyszer használatos hivatkozása!
+ Ez az Ön egyszer használható hivatkozása!
No comment provided by engineer.
This link was used with another mobile device, please create a new link on the desktop.
- Ezt a hivatkozást egy másik mobilleszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén.
+ Ezt a hivatkozást egy másik hordozható eszközön már használták, hozzon létre egy új hivatkozást a számítógépén.
No comment provided by engineer.
@@ -6738,7 +7005,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.
- Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználói azonosítók helyett, a SimpleX üzenetsorokhoz rendel azonosítókat, minden egyes ismerőshöz egy különbözőt.
+ Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználó-azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt.
No comment provided by engineer.
@@ -6748,16 +7015,26 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.
To protect your IP address, private routing uses your SMP servers to deliver messages.
- Az IP-cím védelmének érdekében a privát útválasztás az SMP kiszolgálókat használja az üzenetek kézbesítéséhez.
+ Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez.
No comment provided by engineer.
To protect your information, turn on SimpleX Lock.
You will be prompted to complete authentication before this feature is enabled.
- A biztonsága érdekében kapcsolja be a SimpleX zár funkciót.
+ A biztonsága érdekében kapcsolja be a SimpleX-zár funkciót.
A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén.
No comment provided by engineer.
+
+ To record speech please grant permission to use Microphone.
+ A beszéd rögzítéséhez adjon engedélyt a Mikrofon használatára.
+ No comment provided by engineer.
+
+
+ To record video please grant permission to use Camera.
+ A videó rögzítéséhez adjon engedélyt a Kamera használatára.
+ No comment provided by engineer.
+
To record voice message please grant permission to use Microphone.
Hangüzenet rögzítéséhez adjon engedélyt a mikrofon használathoz.
@@ -6770,12 +7047,12 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll
To support instant push notifications the chat database has to be migrated.
- Az azonnali push értesítések támogatásához a csevegési adatbázis migrálása szükséges.
+ Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges.
No comment provided by engineer.
To verify end-to-end encryption with your contact compare (or scan) the code on your devices.
- A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) az ismerőse eszközén lévő kóddal.
+ A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) az ismerőse eszközén lévő kóddal.
No comment provided by engineer.
@@ -6785,7 +7062,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll
Toggle incognito when connecting.
- Inkognitó mód kapcsolódáskor.
+ Inkognitómód használata kapcsolódáskor.
No comment provided by engineer.
@@ -6795,12 +7072,12 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll
Total
- Összesen
+ Összes kapcsolat
No comment provided by engineer.
Transport isolation
- Kapcsolat izolációs mód
+ Átvitel-izoláció módja
No comment provided by engineer.
@@ -6810,12 +7087,12 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll
Trying to connect to the server used to receive messages from this contact (error: %@).
- Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál (hiba: %@).
+ Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál (hiba: %@).
No comment provided by engineer.
Trying to connect to the server used to receive messages from this contact.
- Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál.
+ Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál.
No comment provided by engineer.
@@ -6870,7 +7147,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll
Unfav.
- Nem kedvelt.
+ Kedvenc megszüntetése
swipe action
@@ -6911,7 +7188,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll
Unknown servers!
Ismeretlen kiszolgálók!
- No comment provided by engineer.
+ alert title
Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.
@@ -6921,8 +7198,8 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll
Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.
To connect, please ask your contact to create another connection link and check that you have a stable network connection.
- Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, lehet hogy ez egy hiba – jelentse a problémát.
-A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e.
+ Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát.
+A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e.
No comment provided by engineer.
@@ -6947,7 +7224,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Unmute
- Némítás feloldása
+ Némítás megszüntetése
swipe action
@@ -6967,7 +7244,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Update database passphrase
- Adatbázis jelmondat megváltoztatása
+ Adatbázis-jelmondat megváltoztatása
No comment provided by engineer.
@@ -7022,12 +7299,17 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Use .onion hosts
- Tor .onion kiszolgálók használata
+ Onion-kiszolgálók használata
+ No comment provided by engineer.
+
+
+ Use SOCKS proxy
+ SOCKS proxy használata
No comment provided by engineer.
Use SimpleX Chat servers?
- SimpleX Chat kiszolgálók használata?
+ SimpleX Chat-kiszolgálók használata?
No comment provided by engineer.
@@ -7047,7 +7329,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Use from desktop
- Használat számítógépről
+ Társítás számítógéppel
No comment provided by engineer.
@@ -7057,7 +7339,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Use new incognito profile
- Az új inkognító profil használata
+ Új inkognitóprofil használata
No comment provided by engineer.
@@ -7067,7 +7349,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Use private routing with unknown servers when IP address is not protected.
- Privát útválasztás használata ismeretlen kiszolgálókkal, ha az IP-cím nem védett.
+ Használjon privát útválasztást ismeretlen kiszolgálókkal, ha az IP-cím nem védett.
No comment provided by engineer.
@@ -7090,54 +7372,54 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Használja az alkalmazást egy kézzel.
No comment provided by engineer.
-
- User profile
- Felhasználói profil
- No comment provided by engineer.
-
User selection
Felhasználó kiválasztása
No comment provided by engineer.
+
+ Username
+ Felhasználónév
+ No comment provided by engineer.
+
Using SimpleX Chat servers.
- SimpleX Chat kiszolgálók használatban.
+ SimpleX Chat-kiszolgálók használatban.
No comment provided by engineer.
Verify code with desktop
- Kód ellenőrzése a számítógépen
+ Kód hitelesítése a számítógépen
No comment provided by engineer.
Verify connection
- Kapcsolat ellenőrzése
+ Kapcsolat hitelesítése
No comment provided by engineer.
Verify connection security
- Kapcsolat biztonságának ellenőrzése
+ Biztonságos kapcsolat hitelesítése
No comment provided by engineer.
Verify connections
- Kapcsolatok ellenőrzése
+ Kapcsolatok hitelesítése
No comment provided by engineer.
Verify database passphrase
- Adatbázis jelmondatának ellenőrzése
+ Az adatbázis jelmondatának hitelesítése
No comment provided by engineer.
Verify passphrase
- Jelmondat ellenőrzése
+ Jelmondat hitelesítése
No comment provided by engineer.
Verify security code
- Biztonsági kód ellenőrzése
+ Biztonsági kód hitelesítése
No comment provided by engineer.
@@ -7147,7 +7429,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Via secure quantum resistant protocol.
- Biztonságos kvantum ellenálló protokoll által.
+ Biztonságos kvantumrezisztens-protokollon keresztül.
No comment provided by engineer.
@@ -7212,22 +7494,22 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Waiting for desktop...
- Várakozás az asztali kliensre...
+ Várakozás a számítógép-alkalmazásra…
No comment provided by engineer.
Waiting for file
- Fájlra várakozás
+ Várakozás a fájlra
No comment provided by engineer.
Waiting for image
- Képre várakozás
+ Várakozás a képre
No comment provided by engineer.
Waiting for video
- Videóra várakozás
+ Várakozás a videóra
No comment provided by engineer.
@@ -7252,7 +7534,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
WebRTC ICE servers
- WebRTC ICE kiszolgálók
+ WebRTC ICE-kiszolgálók
No comment provided by engineer.
@@ -7262,12 +7544,12 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Welcome message
- Üdvözlő üzenet
+ Üdvözlőüzenet
No comment provided by engineer.
Welcome message is too long
- Az üdvözlő üzenet túl hosszú
+ Az üdvözlőüzenet túl hosszú
No comment provided by engineer.
@@ -7287,7 +7569,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
When people request to connect, you can accept or reject it.
- Amikor az emberek kapcsolódást kérelmeznek, ön elfogadhatja vagy elutasíthatja azokat.
+ Amikor az emberek kapcsolatot kérnek, Ön elfogadhatja vagy elutasíthatja azokat.
No comment provided by engineer.
@@ -7312,17 +7594,17 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
With encrypted files and media.
- Titkosított fájlokkal és médiatartalommal.
+ Titkosított fájlokkal és médiatartalmakkal.
No comment provided by engineer.
With optional welcome message.
- Opcionális üdvözlő üzenettel.
+ Nem kötelező üdvözlőüzenettel.
No comment provided by engineer.
With reduced battery usage.
- Csökkentett akkumulátorhasználattal.
+ Csökkentett akkumulátor-használattal.
No comment provided by engineer.
@@ -7332,12 +7614,12 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.
- Tor vagy VPN nélkül az IP-címe látható lesz ezen XFTP átjátszók számára: %@.
- No comment provided by engineer.
+ Tor vagy VPN nélkül az IP-címe látható lesz a következő XFTP-közvetítő-kiszolgálók számára: %@.
+ alert message
Wrong database passphrase
- Hibás adatbázis jelmondat
+ Hibás adatbázis-jelmondat
No comment provided by engineer.
@@ -7360,11 +7642,6 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
XFTP-kiszolgáló
No comment provided by engineer.
-
- You
- Ön
- No comment provided by engineer.
-
You **must not** use the same database on two devices.
**Nem szabad** ugyanazt az adatbázist használni egyszerre két eszközön.
@@ -7387,7 +7664,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
You are already connected to %@.
- Már kapcsolódva van hozzá: %@.
+ Ön már kapcsolódva van ehhez: %@.
No comment provided by engineer.
@@ -7397,7 +7674,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
You are already connecting via this one-time link!
- A kapcsolódás már folyamatban van ezen az egyszer használatos hivatkozáson keresztül!
+ A kapcsolódás már folyamatban van ezen az egyszer használható hivatkozáson keresztül!
No comment provided by engineer.
@@ -7407,7 +7684,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
You are already joining the group %@.
- A csatlakozás már folyamatban van a(z) %@ csoporthoz.
+ A csatlakozás már folyamatban van a(z) %@ nevű csoporthoz.
No comment provided by engineer.
@@ -7424,12 +7701,12 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol
You are already joining the group!
Repeat join request?
Csatlakozás folyamatban!
-Csatlakozási kérés megismétlése?
+Csatlakozáskérés megismétlése?
No comment provided by engineer.
You are connected to the server used to receive messages from this contact.
- Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál.
+ Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál.
No comment provided by engineer.
@@ -7444,7 +7721,7 @@ Csatlakozási kérés megismétlése?
You can accept calls from lock screen, without device and app authentication.
- Hívásokat fogadhat a lezárási képernyőről, eszköz- és alkalmazáshitelesítés nélkül.
+ Hívásokat fogadhat a lezárási képernyőről, eszköz- és alkalmazás-hitelesítés nélkül.
No comment provided by engineer.
@@ -7459,12 +7736,12 @@ Csatlakozási kérés megismétlése?
You can enable later via Settings
- Később engedélyezheti a Beállításokban
+ Később engedélyezheti a „Beállításokban”
No comment provided by engineer.
You can enable them later via app Privacy & Security settings.
- Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüben.
+ Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüjében.
No comment provided by engineer.
@@ -7474,12 +7751,12 @@ Csatlakozási kérés megismétlése?
You can hide or mute a user profile - swipe it to the right.
- Elrejtheti vagy lenémíthatja a felhasználó profiljait - csúsztassa jobbra a profilt.
+ Elrejtheti vagy lenémíthatja a felhasználó -profiljait - csúsztassa jobbra a profilt.
No comment provided by engineer.
You can make it visible to your SimpleX contacts via Settings.
- Láthatóvá teheti SimpleX ismerősök számára a Beállításokban.
+ Láthatóvá teheti a SimpleXbeli ismerősei számára a „Beállításokban”.
No comment provided by engineer.
@@ -7504,17 +7781,17 @@ Csatlakozási kérés megismétlése?
You can share this address with your contacts to let them connect with **%@**.
- Megoszthatja ezt a címet az ismerőseivel, hogy kapcsolatba léphessenek önnel a(z) **%@** nevű profilján keresztül.
+ Megoszthatja ezt a címet az ismerőseivel, hogy kapcsolatba léphessenek Önnel a(z) **%@** nevű profilján keresztül.
No comment provided by engineer.
You can share your address as a link or QR code - anybody can connect to you.
- Megoszthatja a címét egy hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz.
+ Megoszthatja a címét egy hivatkozásként vagy QR-kódként – így bárki kapcsolódhat Önhöz.
No comment provided by engineer.
You can start chat via app Settings / Database or by restarting the app
- A csevegést az alkalmazás Beállítások / Adatbázis menüben vagy az alkalmazás újraindításával indíthatja el
+ A csevegést az alkalmazás „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával indíthatja el
No comment provided by engineer.
@@ -7524,7 +7801,7 @@ Csatlakozási kérés megismétlése?
You can turn on SimpleX Lock via Settings.
- A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be.
+ A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be.
No comment provided by engineer.
@@ -7534,7 +7811,7 @@ Csatlakozási kérés megismétlése?
You can view invitation link again in connection details.
- A meghívó hivatkozást újra megtekintheti a kapcsolat részleteinél.
+ A meghívó-hivatkozást újra megtekintheti a kapcsolat részleteinél.
No comment provided by engineer.
@@ -7549,19 +7826,19 @@ Csatlakozási kérés megismétlése?
You could not be verified; please try again.
- Nem lehetett ellenőrizni; próbálja meg újra.
+ Nem sikerült hitelesíteni; próbálja meg újra.
No comment provided by engineer.
You have already requested connection via this address!
- Már kért egy kapcsolódási kérelmet ezen a címen keresztül!
+ Már küldött egy kapcsolatkérést ezen a címen keresztül!
No comment provided by engineer.
You have already requested connection!
Repeat connection request?
- Már kért egy kapcsolódási kérelmet!
-Kapcsolódási kérés megismétlése?
+ Már küldött egy kapcsolódási kérelmet!
+Kapcsolatkérés megismétlése?
No comment provided by engineer.
@@ -7596,7 +7873,7 @@ Kapcsolódási kérés megismétlése?
You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.
- A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi ismerőstől.
+ A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi ismerősétől.
No comment provided by engineer.
@@ -7606,17 +7883,17 @@ Kapcsolódási kérés megismétlése?
You need to allow your contact to send voice messages to be able to send them.
- Hangüzeneteket küldéséhez engedélyeznie kell azok küldését az ismerősök számára.
+ Engedélyeznie kell a hangüzenetek küldését az ismerőse számára, hogy hangüzeneteket küldhessenek egymásnak.
No comment provided by engineer.
You rejected group invitation
- Csoport meghívó elutasítva
+ Csoportmeghívó elutasítva
No comment provided by engineer.
You sent group invitation
- Csoport meghívó elküldve
+ Csoportmeghívó elküldve
No comment provided by engineer.
@@ -7626,17 +7903,17 @@ Kapcsolódási kérés megismétlése?
You will be connected when group link host's device is online, please wait or check later!
- Akkor lesz kapcsolódva, amikor a csoportos hivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!
+ Akkor lesz kapcsolódva, amikor a csoporthivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!
No comment provided by engineer.
You will be connected when your connection request is accepted, please wait or check later!
- Akkor lesz kapcsolódva, ha a kapcsolódási kérelme elfogadásra kerül, várjon, vagy ellenőrizze később!
+ Akkor lesz kapcsolódva, ha a kapcsolatkérése elfogadásra kerül, várjon, vagy ellenőrizze később!
No comment provided by engineer.
You will be connected when your contact's device is online, please wait or check later!
- Akkor le kapcsolódva, amikor az ismerőse eszköze online lesz, várjon, vagy ellenőrizze később!
+ Akkor lesz kapcsolódva, amikor az ismerősének eszköze online lesz, várjon, vagy ellenőrizze később!
No comment provided by engineer.
@@ -7666,12 +7943,12 @@ Kapcsolódási kérés megismétlése?
You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile
- Egy olyan ismerőst próbál meghívni, akivel inkognító profilt osztott meg abban a csoportban, amelyben saját fő profilja van használatban
+ Egy olyan ismerősét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a saját fő profilja van használatban
No comment provided by engineer.
You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed
- Inkognító profilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében meghívók küldése tiltott
+ Inkognitóprofilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva
No comment provided by engineer.
@@ -7681,22 +7958,22 @@ Kapcsolódási kérés megismétlése?
Your ICE servers
- ICE kiszolgálók
+ Saját ICE-kiszolgálók
No comment provided by engineer.
Your SMP servers
- SMP kiszolgálók
+ Saját SMP-kiszolgálók
No comment provided by engineer.
Your SimpleX address
- Az ön SimpleX címe
+ Profil SimpleX-címe
No comment provided by engineer.
Your XFTP servers
- XFTP kiszolgálók
+ Saját XFTP-kiszolgálók
No comment provided by engineer.
@@ -7714,19 +7991,29 @@ Kapcsolódási kérés megismétlése?
A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz.
No comment provided by engineer.
+
+ Your chat preferences
+ Csevegési beállítások
+ alert title
+
Your chat profiles
Csevegési profilok
No comment provided by engineer.
+
+ Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.
+ A kapcsolata át lett helyezve ide: %@, de egy váratlan hiba történt a profilra való átirányításkor.
+ No comment provided by engineer.
+
Your contact sent a file that is larger than currently supported maximum size (%@).
- Ismerőse olyan fájlt küldött, amely meghaladja a jelenleg támogatott maximális méretet (%@).
+ Az ismerőse olyan fájlt küldött, amely meghaladja a jelenleg támogatott maximális méretet (%@).
No comment provided by engineer.
Your contacts can allow full message deletion.
- Ismerősök engedélyezhetik a teljes üzenet törlést.
+ Az ismerősei engedélyezhetik a teljes üzenet törlést.
No comment provided by engineer.
@@ -7734,6 +8021,11 @@ Kapcsolódási kérés megismétlése?
Az ismerősei továbbra is kapcsolódva maradnak.
No comment provided by engineer.
+
+ Your credentials may be sent unencrypted.
+ A hitelesítőadatai titkosítatlanul is elküldhetők.
+ No comment provided by engineer.
+
Your current chat database will be DELETED and REPLACED with the imported one.
A jelenlegi csevegési adatbázis TÖRLŐDNI FOG, és a HELYÉRE az importált adatbázis kerül.
@@ -7764,16 +8056,19 @@ Kapcsolódási kérés megismétlése?
A(z) **%@** nevű profilja megosztásra fog kerülni.
No comment provided by engineer.
-
- Your profile is stored on your device and shared only with your contacts.
-SimpleX servers cannot see your profile.
- Profilja az eszközön van tárolva, és csak az ismerősökkel kerül megosztásra.
-A SimpleX kiszolgálók nem látjhatják profilját.
+
+ Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.
+ A profilja az eszközön van tárolva és csak az ismerőseivel kerül megosztásra. A SimpleX-kiszolgálók nem láthatják a profilját.
No comment provided by engineer.
+
+ Your profile was changed. If you save it, the updated profile will be sent to all your contacts.
+ A profilja megváltozott. Ha elmenti, a frissített profil elküldésre kerül az összes ismerősének.
+ alert message
+
Your profile, contacts and delivered messages are stored on your device.
- Profilja, ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra.
+ A profilja, az ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra.
No comment provided by engineer.
@@ -7783,12 +8078,12 @@ A SimpleX kiszolgálók nem látjhatják profilját.
Your server
- Saját kiszolgáló
+ Saját SMP-kiszolgáló
No comment provided by engineer.
Your server address
- Saját kiszolgáló cím
+ Saját SMP-kiszolgálójának címe
No comment provided by engineer.
@@ -7808,7 +8103,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
[Star on GitHub](https://github.com/simplex-chat/simplex-chat)
- [Csillag a GitHubon](https://github.com/simplex-chat/simplex-chat)
+ [Csillagozás a GitHubon](https://github.com/simplex-chat/simplex-chat)
No comment provided by engineer.
@@ -7843,7 +8138,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
agreeing encryption for %@…
- titkosítás jóváhagyása %@ számára…
+ titkosítás elfogadása %@ számára…
chat item text
@@ -7888,7 +8183,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
bad message hash
- hibás az üzenet ellenőrzőösszege
+ hibás az üzenet hasító értéke
integrity error chat item
@@ -7918,7 +8213,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
call error
- hiba a hívásban
+ híváshiba
call status
@@ -7948,7 +8243,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
changed your role to %@
- megváltoztatta a szerepkörét erre: %@
+ megváltoztatta az Ön szerepkörét erre: %@
rcv group event chat item
@@ -7973,7 +8268,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
connect to SimpleX Chat developers.
- Kapcsolódás a SimpleX Chat fejlesztőkhöz.
+ kapcsolódás a SimpleX Chat fejlesztőkhöz.
No comment provided by engineer.
@@ -7983,7 +8278,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
connected directly
- közvetlenül kapcsolódva
+ közvetlenül kapcsolódott
rcv group event chat item
@@ -8008,12 +8303,12 @@ A SimpleX kiszolgálók nem látjhatják profilját.
connecting (introduction invitation)
- kapcsolódás (bemutatkozó meghívó)
+ kapcsolódás (bemutatkozó-meghívó)
No comment provided by engineer.
connecting call…
- hívás kapcsolódik…
+ kapcsolódási hívás…
call status
@@ -8143,12 +8438,12 @@ A SimpleX kiszolgálók nem látjhatják profilját.
enabled for you
- engedélyezve az ön számára
+ engedélyezve az Ön számára
enabled status
encryption agreed
- titkosítás egyeztetve
+ titkosítás elfogadva
chat item text
@@ -8248,17 +8543,17 @@ A SimpleX kiszolgálók nem látjhatják profilját.
incognito via contact address link
- inkognitó a kapcsolattartási hivatkozáson keresztül
+ inkognitó a kapcsolattartási címhivatkozáson keresztül
chat list item description
incognito via group link
- inkognitó a csoportos hivatkozáson keresztül
+ inkognitó a csoporthivatkozáson keresztül
chat list item description
incognito via one-time link
- inkognitó az egyszer használatos hivatkozáson keresztül
+ inkognitó egy egyszer használható hivatkozáson keresztül
chat list item description
@@ -8273,7 +8568,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
invalid chat data
- érvénytelen csevegés adat
+ érvénytelen csevegésadat
No comment provided by engineer.
@@ -8308,7 +8603,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
invited via your group link
- meghíva az ön csoport hivatkozásán keresztül
+ meghíva az Ön csoporthivatkozásán keresztül
rcv group event chat item
@@ -8328,7 +8623,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
marked deleted
- töröltnek jelölve
+ törlésre jelölve
marked deleted chat item preview text
@@ -8393,7 +8688,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
new message
- új üzenet
+ Rejtett üzenet
notification
@@ -8470,7 +8765,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
received answer…
- fogadott válasz…
+ válasz fogadása…
No comment provided by engineer.
@@ -8495,17 +8790,17 @@ A SimpleX kiszolgálók nem látjhatják profilját.
removed contact address
- törölt kapcsolattartási cím
+ eltávolította a kapcsolattartási címet
profile update event chat item
removed profile picture
- törölte a profilképét
+ eltávolította a profilképét
profile update event chat item
removed you
- eltávolítottak
+ eltávolította Önt
rcv group event chat item
@@ -8552,7 +8847,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
server queue info: %1$@
last received msg: %2$@
- kiszolgáló üzenet-várakotatási információ: %1$@
+ a kiszolgáló üzenet-sorbaállítási információi: %1$@
utoljára fogadott üzenet: %2$@
queue info
@@ -8604,12 +8899,12 @@ utoljára fogadott üzenet: %2$@
unknown status
- ismeretlen státusz
+ ismeretlen állapot
No comment provided by engineer.
unmute
- némítás feloldása
+ némítás megszüntetése
No comment provided by engineer.
@@ -8644,17 +8939,17 @@ utoljára fogadott üzenet: %2$@
via group link
- csoport hivatkozáson keresztül
+ a csoporthivatkozáson keresztül
chat list item description
via one-time link
- egyszer használatos hivatkozáson keresztül
+ egyszer használható hivatkozáson keresztül
chat list item description
via relay
- átjátszón keresztül
+ közvetítő-kiszolgálón keresztül
No comment provided by engineer.
@@ -8679,7 +8974,7 @@ utoljára fogadott üzenet: %2$@
wants to connect to you!
- kapcsolatba akar lépni önnel!
+ kapcsolatba akar lépni Önnel!
No comment provided by engineer.
@@ -8699,7 +8994,7 @@ utoljára fogadott üzenet: %2$@
you
- ön
+ Ön
No comment provided by engineer.
@@ -8714,7 +9009,7 @@ utoljára fogadott üzenet: %2$@
you blocked %@
- ön letiltotta %@-t
+ Ön letiltotta őt: %@
snd group event chat item
@@ -8729,42 +9024,42 @@ utoljára fogadott üzenet: %2$@
you changed role for yourself to %@
- saját szerepkör megváltoztatva erre: %@
+ saját szerepköre megváltozott erre: %@
snd group event chat item
you changed role of %1$@ to %2$@
- %1$@ szerepkörét megváltoztatta erre: %@
+ Ön megváltoztatta %1$@ szerepkörét erre: %@
snd group event chat item
you left
- elhagyta a csoportot
+ Ön elhagyta a csoportot
snd group event chat item
you removed %@
- eltávolította őt: %@
+ Ön eltávolította őt: %@
snd group event chat item
you shared one-time link
- egyszer használatos hivatkozást osztott meg
+ egyszer használható hivatkozást osztott meg
chat list item description
you shared one-time link incognito
- egyszer használatos hivatkozást osztott meg inkognitóban
+ egyszer használható hivatkozást osztott meg inkognitóban
chat list item description
you unblocked %@
- ön feloldotta %@ letiltását
+ Ön feloldotta %@ letiltását
snd group event chat item
you:
- ön:
+ Ön:
No comment provided by engineer.
@@ -8786,7 +9081,7 @@ utoljára fogadott üzenet: %2$@
SimpleX needs camera access to scan QR codes to connect to other users and for video calls.
- A SimpleX-nek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz.
+ A SimpleXnek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz.
Privacy - Camera Usage Description
@@ -8801,12 +9096,12 @@ utoljára fogadott üzenet: %2$@
SimpleX needs microphone access for audio and video calls, and to record voice messages.
- A SimpleX-nek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez.
+ A SimpleXnek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez.
Privacy - Microphone Usage Description
SimpleX needs access to Photo Library for saving captured and received media
- A SimpleX-nek hozzáférésre van szüksége a Galériához a rögzített és fogadott média mentéséhez
+ A SimpleXnek galéria-hozzáférésre van szüksége a rögzített és fogadott média mentéséhez
Privacy - Photo Library Additions Usage Description