Compare commits

...

25 Commits

Author SHA1 Message Date
Levitating Pineapple dea7327610 cleanup 2024-08-28 19:31:55 +03:00
Levitating Pineapple e9dd18a98c ios: add floating date separator 2024-08-28 19:28:56 +03:00
Levitating Pineapple 5406aabdc6 revert id 2024-08-28 16:10:24 +03:00
Levitating Pineapple cc5bc9c2cf account for inset; remove old on appear/disappear blocks 2024-08-28 16:09:51 +03:00
Levitating Pineapple 4153ea7ddc Merge branch 'master' into lp/fix-visible-items 2024-08-28 14:54:13 +03:00
Evgeny 8cc075eda8 ios: show correct message times (#4779) 2024-08-27 22:13:20 +01:00
Diogo 05e7f35037 core: fix associated agent user for recreated connections (#4771)
* core: fix associated user for recreated connections

* fix test for connection recreation
2024-08-27 22:12:55 +01:00
Levitating Pineapple fbe26ed7ff ios: fix inaccurate floating unread counters in chat message view 2024-08-27 23:00:34 +03:00
Diogo e582d2d742 android, desktop: allow for chat profile selection on new chat screen (#4741)
* add api and types

* basic ui

* add search on profiles

* profile images on select chat profile

* incognito adjustments

* basic api connection

* handling errors

* add loading state

* header to scroll

* selected profile on top (profile or incognito)

* adjust share profile copy

* avoid list moving around on selection commit

* bigger profile pick

* info icon interactive area

* thumbs to match contacts list size

* incognito sizes matching icons

* title to section padding

* add chevron

* align borders and other chevron icon

* prevent click on self

* only prevent selection

* update

* selectable item area

* no need for oninfo to be composable

* simplify

* wrap apis in try

* remove redundant derivedStateOf

* closure fns capital naming

* simplify current user null check

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-27 14:32:54 +01:00
Arturs Krumins 043a4ed915 ios: add chat message tail and roundness settings; date separators (#4764)
* ios: add chat message tail and roundness settings

* cleanup

* minor

* rename

* date separator

* revert max roundness to pills

* increase default roundness to 1

* minor

* out of bounds tails, style date separator

* formatting

* hardcode tail growth

* revert

* different shape (WIP)

* tail

* rename

* square

* only show tail for the last message

* remove func

* capture less

* variable tail height

* export localizations

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-27 14:30:07 +01:00
Evgeny Poberezkin 8a6bf05773 Merge branch 'stable' 2024-08-26 20:06:28 +01:00
Stanislav Dmitrenko 0118e64ab4 android, desktop: items padding and min height (#4767) 2024-08-26 16:59:57 +01:00
Arturs Krumins 0477b1aad3 ios: time based message grouping (#4743)
* ios: time based message grouping

* cleanup

* hide timestamp

* fix chat item not getting updated

* round to minute

* separate by minute

* chat dir

* time separation struct

* add date logic

* cleanup

* fix groups

* simplify timestamp logic; remove shape

* cleanup

* cleanup

* refactor, add type

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-25 19:21:24 +01:00
Evgeny Poberezkin ae850c8ce8 ios: update core library 2024-08-25 18:00:46 +01:00
Evgeny Poberezkin 94ae571ec3 Merge branch 'stable' 2024-08-25 14:33:19 +01:00
Evgeny 4552860345 ios: remove unnecessary protocols (#4763) 2024-08-25 14:33:06 +01:00
Evgeny a1579810bb ios: remove unnecessary protocols (#4763) 2024-08-25 14:31:26 +01:00
Evgeny Poberezkin 4574198990 Merge branch 'stable' 2024-08-24 19:14:28 +01:00
Evgeny 4d18174b11 ui: fix Debug delivery (#4757) 2024-08-24 19:10:30 +01:00
Evgeny Poberezkin defd095a4f 6.0.3: ios 235, android 235, desktop 64 2024-08-24 16:32:32 +01:00
Evgeny Poberezkin ed60f28e56 ios: update core library 2024-08-24 15:44:08 +01:00
Evgeny Poberezkin f0b889ffcf core: 6.0.3.0 (simplexmq 6.0.2.0) 2024-08-24 15:07:32 +01:00
Stanislav Dmitrenko c07df9e05f android: target API level 34 (Android 14) (#4697) 2024-08-24 15:00:56 +01:00
Diogo efe8ed1739 ios: fix possible race between incognito set and profile change in conn profile picker (#4752)
* ios: fix possible race between incognito set and profile change in conn profile picker

* typo

* fix swithcing incognito on same profile
2024-08-24 14:59:50 +01:00
spaced4ndy bcd50019be core: add more multi send api tests (#4750) 2024-08-23 21:05:37 +04:00
82 changed files with 2419 additions and 475 deletions
+16 -10
View File
@@ -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
@@ -428,19 +436,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
}
+2 -2
View File
@@ -585,13 +585,13 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (Gro
throw r
}
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
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) }
throw r
}
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
throw r
@@ -942,7 +942,7 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al
)
}
func queueInfoText(_ info: (RcvMsgInfo?, QueueInfo)) -> String {
func queueInfoText(_ info: (RcvMsgInfo?, ServerQueueInfo)) -> String {
let (rcvMsgInfo, qInfo) = info
var msgInfo: String
if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" }
@@ -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, transparent: true, 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, transparent: true, 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
@@ -12,6 +12,7 @@ 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)
@@ -30,24 +31,24 @@ struct CIMetaView: View {
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)
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, 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)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
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)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
}
default:
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
}
}
@@ -69,7 +70,8 @@ func ciMetaText(
sent: SentCheckmark? = nil,
showStatus: Bool = true,
showEdited: Bool = true,
showViaProxy: Bool
showViaProxy: Bool,
showTimesamp: Bool
) -> Text {
var r = Text("")
if showEdited, meta.itemEdited {
@@ -105,7 +107,9 @@ func ciMetaText(
if let enc = encrypted {
r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
}
r = r + meta.timestampText.foregroundColor(color)
if showTimesamp {
r = r + meta.timestampText.foregroundColor(color)
}
return r.font(.caption)
}
@@ -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
@@ -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, transparent: true, 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, transparent: true, 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)
}
@@ -12,7 +12,7 @@ import SimpleXChat
struct FramedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
@EnvironmentObject var scrollModel: ReverseListScrollModel
@ObservedObject var chat: Chat
var chatItem: ChatItem
var preview: UIImage?
@@ -71,8 +71,8 @@ struct FramedItemView: View {
.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 {
@@ -13,7 +13,7 @@ import AVKit
struct FullScreenMediaView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
@EnvironmentObject var scrollModel: ReverseListScrollModel
@State var chatItem: ChatItem
@State var image: UIImage?
@State var player: AVPlayer? = nil
@@ -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)
}
@@ -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)
}
@@ -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, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
}
@@ -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
+120 -63
View File
@@ -22,8 +22,8 @@ struct ChatView: View {
@Environment(\.presentationMode) var presentationMode
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat
@StateObject private var scrollModel = ReverseListScrollModel<ChatItem>()
@StateObject private var floatingButtonModel = FloatingButtonModel()
@StateObject private var scrollModel = ReverseListScrollModel()
@StateObject private var floatingButtonModel: FloatingButtonModel = .shared
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState()
@@ -74,9 +74,12 @@ struct ChatView: View {
)
}
VStack(spacing: 0) {
ZStack(alignment: .bottomTrailing) {
ZStack {
chatItemsList()
floatingButtons(counts: floatingButtonModel.unreadChatItemCounts)
.frame(maxWidth: .infinity, alignment: .trailing)
floatingDate
.frame(maxHeight: .infinity, alignment: .top)
}
connectingText()
if selectedChatItems == nil {
@@ -413,12 +416,6 @@ struct ChatView: View {
revealedChatItem: $revealedChatItem,
selectedChatItems: $selectedChatItems
)
.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,9 +426,6 @@ 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
@@ -458,15 +452,13 @@ struct ChatView: View {
}
class FloatingButtonModel: ObservableObject {
private enum Event {
case appeared(String)
case disappeared(String)
case chatItemsChanged
}
static let shared = FloatingButtonModel()
@Published var unreadChatItemCounts: UnreadChatItemCounts
@Published var date: Date?
@Published var isDateVisible: Bool = false
private let events = PassthroughSubject<Event, Never>()
let visibleItems = PassthroughSubject<[String], Never>()
private var bag = Set<AnyCancellable>()
init() {
@@ -475,34 +467,58 @@ struct ChatView: View {
isReallyNearBottom: true,
unreadBelow: 0
)
events
// Unread item counts
visibleItems
.receive(on: DispatchQueue.global(qos: .background))
.scan(Set<String>()) { 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
}
.map { ChatModel.shared.unreadChatItemCounts(itemsInView: $0) }
.map { ChatModel.shared.unreadChatItemCounts(itemsInView: Set($0)) }
.removeDuplicates()
.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: true)
.assign(to: \.unreadChatItemCounts, on: self)
.store(in: &bag)
}
func appeared(viewId: String) {
events.send(.appeared(viewId))
// Floating date
visibleItems
.receive(on: DispatchQueue.global(qos: .background))
.map { visibleItems -> Date? in
if let viewId = visibleItems.last {
ItemsModel.shared.reversedChatItems
.first { $0.viewId == viewId }
.map { Calendar.current.startOfDay(for: $0.meta.itemTs) }
} else { nil }
}
.removeDuplicates()
.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: true)
.assign(to: \.date, on: self)
.store(in: &bag)
// Floating date visibility
visibleItems
.removeDuplicates()
.receive(on: DispatchQueue.main)
.map { _ in
if !self.isDateVisible {
withAnimation { self.isDateVisible = true }
}
}
.debounce(for: 1, scheduler: DispatchQueue.main)
.sink {
if self.isDateVisible {
withAnimation { self.isDateVisible = false }
}
}
.store(in: &bag)
}
}
func disappeared(viewId: String) {
events.send(.disappeared(viewId))
}
func chatItemsChanged() {
events.send(.chatItemsChanged)
@ViewBuilder
var floatingDate: some View {
if let date = floatingButtonModel.date {
DateSeparator(date: date)
.padding(.vertical, 4).padding(.horizontal, 8)
.background(ToolbarMaterial.material(toolbarMaterial))
.clipShape(Capsule())
.opacity(floatingButtonModel.isDateVisible ? 1 : 0)
}
}
@@ -692,10 +708,26 @@ struct ChatView: View {
VoiceItemState.chatView = [:]
}
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(.dateTime.day().month(.abbreviated))
))
.font(.callout)
.fontWeight(.medium)
.foregroundStyle(.secondary)
}
}
private struct ChatItemWithMenu: 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
@@ -716,31 +748,54 @@ struct ChatView: View {
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 || formatTimestampText(chatItem.meta.itemTs) != formatTimestampText(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
@@ -791,7 +846,8 @@ struct ChatView: View {
}
}
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?) -> some View {
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ 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) =
@@ -813,14 +869,14 @@ struct ChatView: View {
.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 +889,11 @@ struct ChatView: View {
}
}
}
chatItemWithMenu(ci, range, maxWidth)
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
}
}
}
.padding(.bottom, 5)
.padding(.bottom, bottomPadding)
.padding(.trailing)
.padding(.leading, 12)
} else {
@@ -846,11 +902,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 +919,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 +937,7 @@ struct ChatView: View {
}
}
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat) -> some View {
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View {
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
VStack(alignment: alignment.horizontal, spacing: 3) {
ChatItemView(
@@ -891,7 +947,8 @@ struct ChatView: View {
revealed: .constant(revealed),
allowMenu: $allowMenu
)
.modifier(ChatItemClipped(ci))
.environment(\.showTimestamp, itemSeparation.timestamp)
.modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap))
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
+38 -12
View File
@@ -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<Item: Identifiable & Hashable & Sendable, Content: View>: UIViewControllerRepresentable {
let items: Array<Item>
struct ReverseList<Content: View>: UIViewControllerRepresentable {
let items: Array<ChatItem>
@Binding var scrollState: ReverseListScrollModel<Item>.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,6 +26,7 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
}
func updateUIViewController(_ controller: Controller, context: Context) {
controller.representer = self
if case let .scrollingTo(destination) = scrollState, !items.isEmpty {
switch destination {
case .nextPage:
@@ -42,8 +44,8 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
/// Controller, which hosts SwiftUI cells
class Controller: UITableViewController {
private enum Section { case main }
private let representer: ReverseList
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
var representer: ReverseList
private var dataSource: UITableViewDiffableDataSource<Section, ChatItem>!
private var itemCount: Int = 0
private var bag = Set<AnyCancellable>()
@@ -71,7 +73,7 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
}
// 3. Configure data source
self.dataSource = UITableViewDiffableDataSource<Section, Item>(
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
tableView: tableView
) { (tableView, indexPath, item) -> UITableViewCell? in
if indexPath.item > self.itemCount - 8, self.itemCount > 8 {
@@ -171,8 +173,8 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
Task { representer.scrollState = .atDestination }
}
func update(items: Array<Item>) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
func update(items: [ChatItem]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, ChatItem>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.defaultRowAnimation = .none
@@ -188,6 +190,30 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
)
}
itemCount = items.count
updateVisibleItems()
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateVisibleItems()
}
private func updateVisibleItems() {
ChatView.FloatingButtonModel.shared.visibleItems.send(
(tableView.indexPathsForVisibleRows ?? [])
.compactMap { indexPath -> String? in
let relativeFrame = tableView.superview!.convert(
tableView.rectForRow(at: indexPath),
from: tableView
)
// Checks that the cell is visible accounting for the added insets
let isVisible =
relativeFrame.maxY > InvertedTableView.inset &&
relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
return indexPath.item < representer.items.count && isVisible
? representer.items[indexPath.item].viewId
: nil
}
)
}
}
@@ -232,12 +258,12 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
}
/// Manages ``ReverseList`` scrolling
class ReverseListScrollModel<Item: Identifiable>: 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 +281,7 @@ class ReverseListScrollModel<Item: Identifiable>: ObservableObject {
state = .scrollingTo(.bottom)
}
func scrollToItem(id: Item.ID) {
func scrollToItem(id: ChatItem.ID) {
state = .scrollingTo(.item(id))
}
}
@@ -324,12 +324,12 @@ 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<ChatItem>())
.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<ChatItem>())
.environmentObject(ReverseListScrollModel())
}
case let .voice(_, duration):
smallContentPreviewVoice(size: dynamicMediaSize) {
@@ -14,50 +14,60 @@ 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,
.sndDeleted,
.rcvDeleted,
.rcvIntegrityError,
.sndModerated,
.rcvModerated,
.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)
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 +75,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
}
}
}
+23 -15
View File
@@ -327,6 +327,12 @@ 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
@@ -336,7 +342,7 @@ private struct ActiveProfilePicker: View {
@Binding var incognitoEnabled: Bool
@Binding var choosingProfile: Bool
@State private var alert: SomeAlert?
@State private var switchingProfile = false
@State private var profileSwitchStatus: ProfileSwitchStatus = .idle
@State private var switchingProfileByTimeout = false
@State private var lastSwitchingProfileByTimeoutCall: Double?
@State private var profiles: [User] = []
@@ -358,7 +364,7 @@ private struct ActiveProfilePicker: View {
.sorted { u, _ in u.activeUser }
}
.onChange(of: incognitoEnabled) { incognito in
if !switchingProfile {
if profileSwitchStatus != .switchingIncognito {
return
}
@@ -369,12 +375,12 @@ private struct ActiveProfilePicker: View {
await MainActor.run {
contactConnection = conn
chatModel.updateContactConnection(conn)
switchingProfile = false
profileSwitchStatus = .idle
dismiss()
}
}
} catch {
switchingProfile = false
profileSwitchStatus = .idle
incognitoEnabled = !incognito
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
let err = getErrorAlert(error, "Error changing to incognito!")
@@ -389,39 +395,39 @@ private struct ActiveProfilePicker: View {
}
}
}
.onChange(of: switchingProfile) { sp in
if sp {
.onChange(of: profileSwitchStatus) { sp in
if sp != .idle {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
switchingProfileByTimeout = switchingProfile
switchingProfileByTimeout = profileSwitchStatus != .idle
}
} else {
switchingProfileByTimeout = false
}
}
.onChange(of: selectedProfile) { profile in
if (profile == chatModel.currentUser) {
if (profileSwitchStatus != .switchingUser) {
return
}
Task {
do {
switchingProfile = true
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 {
switchingProfile = false
profileSwitchStatus = .idle
dismiss()
}
} catch {
await MainActor.run {
switchingProfile = false
profileSwitchStatus = .idle
alert = SomeAlert(
alert: Alert(
title: Text("Error switching profile"),
@@ -434,7 +440,7 @@ private struct ActiveProfilePicker: View {
}
} catch {
await MainActor.run {
switchingProfile = false
profileSwitchStatus = .idle
if let currentUser = chatModel.currentUser {
selectedProfile = currentUser
}
@@ -493,10 +499,12 @@ private struct ActiveProfilePicker: View {
@ViewBuilder private func profilerPickerUserOption(_ user: User) -> some View {
Button {
if selectedProfile != user || incognitoEnabled {
switchingProfile = true
if selectedProfile == user && incognitoEnabled {
incognitoEnabled = false
profileSwitchStatus = .switchingIncognito
} else if selectedProfile != user {
selectedProfile = user
profileSwitchStatus = .switchingUser
}
} label: {
HStack {
@@ -519,7 +527,7 @@ private struct ActiveProfilePicker: View {
let incognitoOption = Button {
if !incognitoEnabled {
incognitoEnabled = true
switchingProfile = true
profileSwitchStatus = .switchingIncognito
}
} label : {
HStack {
@@ -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 {
@@ -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"
@@ -75,6 +77,8 @@ let DEFAULT_THEME_OVERRIDES = "themeOverrides"
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,
@@ -98,6 +102,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,
@@ -137,6 +137,10 @@
<target>%@ иска да се свърже!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ и %lld членове</target>
@@ -1685,6 +1689,10 @@ This is your own one-time link!</source>
<target>Версия на ядрото: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Поправи име на %@?</target>
@@ -2645,6 +2653,10 @@ This is your own one-time link!</source>
<target>Грешка при промяна на адреса</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Грешка при промяна на ролята</target>
@@ -2655,6 +2667,10 @@ This is your own one-time link!</source>
<target>Грешка при промяна на настройката</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2870,6 +2886,10 @@ This is your own one-time link!</source>
<target>Грешка при спиране на чата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Грешка при смяна на профил!</target>
@@ -4069,6 +4089,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Източникът на съобщението остава скрит.</target>
@@ -5562,6 +5586,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Избери</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5911,6 +5939,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Сподели линк</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Сподели този еднократен линк за връзка</target>
@@ -6235,6 +6267,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Направи снимка</target>
@@ -7480,6 +7516,10 @@ Repeat connection request?</source>
<target>Вашите чат профили</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@).</target>
@@ -135,6 +135,10 @@
<target>%@ se chce připojit!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<note>No comment provided by engineer.</note>
@@ -1623,6 +1627,10 @@ This is your own one-time link!</source>
<target>Verze jádra: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
@@ -2552,6 +2560,10 @@ This is your own one-time link!</source>
<target>Chuba změny adresy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Chyba při změně role</target>
@@ -2562,6 +2574,10 @@ This is your own one-time link!</source>
<target>Chyba změny nastavení</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2772,6 +2788,10 @@ This is your own one-time link!</source>
<target>Chyba při zastavení chatu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Chyba při přepínání profilu!</target>
@@ -3928,6 +3948,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<note>No comment provided by engineer.</note>
@@ -5363,6 +5387,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Vybrat</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5708,6 +5736,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Sdílet odkaz</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
@@ -6024,6 +6056,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Vyfotit</target>
@@ -7209,6 +7245,10 @@ Repeat connection request?</source>
<target>Vaše chat profily</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@).</target>
@@ -137,6 +137,10 @@
<target>%@ will sich mit Ihnen verbinden!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ und %lld Mitglieder</target>
@@ -1740,6 +1744,10 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Core Version: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Richtiger Name für %@?</target>
@@ -2724,6 +2732,10 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Fehler beim Wechseln der Empfängeradresse</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Fehler beim Ändern der Rolle</target>
@@ -2734,6 +2746,10 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Fehler beim Ändern der Einstellung</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut.</target>
@@ -2954,6 +2970,10 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Fehler beim Beenden des Chats</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Fehler beim Umschalten des Profils!</target>
@@ -4184,6 +4204,10 @@ Das ist Ihr Link für die Gruppe %@!</target>
<target>Nachrichten-Server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Die Nachrichtenquelle bleibt privat.</target>
@@ -5729,6 +5753,10 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Auswählen</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>%lld ausgewählt</target>
@@ -6099,6 +6127,10 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Link teilen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Teilen Sie diesen Einmal-Einladungslink</target>
@@ -6439,6 +6471,10 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Machen Sie ein Foto</target>
@@ -7719,6 +7755,10 @@ Verbindungsanfrage wiederholen?</target>
<target>Ihre Chat-Profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@).</target>
@@ -137,6 +137,11 @@
<target>%@ wants to connect!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<target>%1$@, %2$@</target>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ and %lld members</target>
@@ -1740,6 +1745,11 @@ This is your own one-time link!</target>
<target>Core version: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<target>Corner</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Correct name to %@?</target>
@@ -2724,6 +2734,11 @@ This is your own one-time link!</target>
<target>Error changing address</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<target>Error changing connection profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Error changing role</target>
@@ -2734,6 +2749,11 @@ This is your own one-time link!</target>
<target>Error changing setting</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<target>Error changing to incognito!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Error connecting to forwarding server %@. Please try later.</target>
@@ -2954,6 +2974,11 @@ This is your own one-time link!</target>
<target>Error stopping chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<target>Error switching profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Error switching profile!</target>
@@ -4184,6 +4209,11 @@ This is your link for group %@!</target>
<target>Message servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<target>Message shape</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Message source remains private.</target>
@@ -5729,6 +5759,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Select</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<target>Select chat profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>Selected %lld</target>
@@ -6099,6 +6134,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Share link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<target>Share profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Share this 1-time invite link</target>
@@ -6439,6 +6479,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<target>Tail</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Take picture</target>
@@ -7719,6 +7764,11 @@ Repeat connection request?</target>
<target>Your chat profiles</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<target>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Your contact sent a file that is larger than currently supported maximum size (%@).</target>
@@ -137,6 +137,10 @@
<target>¡ %@ quiere contactar!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ y %lld miembro(s) más</target>
@@ -1740,6 +1744,10 @@ This is your own one-time link!</source>
<target>Versión Core: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>¿Corregir el nombre a %@?</target>
@@ -2724,6 +2732,10 @@ This is your own one-time link!</source>
<target>Error al cambiar servidor</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Error al cambiar rol</target>
@@ -2734,6 +2746,10 @@ This is your own one-time link!</source>
<target>Error cambiando configuración</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde.</target>
@@ -2954,6 +2970,10 @@ This is your own one-time link!</source>
<target>Error al parar SimpleX</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>¡Error al cambiar perfil!</target>
@@ -4184,6 +4204,10 @@ This is your link for group %@!</source>
<target>Servidores de mensajes</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>El autor del mensaje se mantiene privado.</target>
@@ -5729,6 +5753,10 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Seleccionar</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>Seleccionados %lld</target>
@@ -6099,6 +6127,10 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Compartir enlace</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Comparte este enlace de un solo uso</target>
@@ -6439,6 +6471,10 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Tomar foto</target>
@@ -7719,6 +7755,10 @@ Repeat connection request?</source>
<target>Mis perfiles</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>El contacto ha enviado un archivo mayor al máximo admitido (%@).</target>
@@ -133,6 +133,10 @@
<target>%@ haluaa muodostaa yhteyden!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<note>No comment provided by engineer.</note>
@@ -1616,6 +1620,10 @@ This is your own one-time link!</source>
<target>Ydinversio: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
@@ -2544,6 +2552,10 @@ This is your own one-time link!</source>
<target>Virhe osoitteenvaihdossa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Virhe roolin vaihdossa</target>
@@ -2554,6 +2566,10 @@ This is your own one-time link!</source>
<target>Virhe asetuksen muuttamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2762,6 +2778,10 @@ This is your own one-time link!</source>
<target>Virhe keskustelun lopettamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Virhe profiilin vaihdossa!</target>
@@ -3918,6 +3938,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<note>No comment provided by engineer.</note>
@@ -5351,6 +5375,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Valitse</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5695,6 +5723,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Jaa linkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
@@ -6010,6 +6042,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Ota kuva</target>
@@ -7194,6 +7230,10 @@ Repeat connection request?</source>
<target>Keskusteluprofiilisi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@).</target>
@@ -137,6 +137,10 @@
<target>%@ veut se connecter !</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ et %lld membres</target>
@@ -1740,6 +1744,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Version du cœur : v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Corriger le nom pour %@ ?</target>
@@ -2724,6 +2732,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Erreur de changement d'adresse</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Erreur lors du changement de rôle</target>
@@ -2734,6 +2746,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Erreur de changement de paramètre</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard.</target>
@@ -2954,6 +2970,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Erreur lors de l'arrêt du chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Erreur lors du changement de profil !</target>
@@ -4184,6 +4204,10 @@ Voici votre lien pour le groupe %@ !</target>
<target>Serveurs de messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>La source du message reste privée.</target>
@@ -5729,6 +5753,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Choisir</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>%lld sélectionné(s)</target>
@@ -6099,6 +6127,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Partager le lien</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Partager ce lien d'invitation unique</target>
@@ -6439,6 +6471,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Prendre une photo</target>
@@ -7719,6 +7755,10 @@ Répéter la demande de connexion ?</target>
<target>Vos profils de chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Votre contact a envoyé un fichier plus grand que la taille maximale supportée actuellement(%@).</target>
@@ -137,6 +137,10 @@
<target>%@ kapcsolódni szeretne!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ és további %lld tag</target>
@@ -1740,6 +1744,10 @@ Ez az egyszer használatos hivatkozása!</target>
<target>Alapverziószám: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Név javítása erre: %@?</target>
@@ -2724,6 +2732,10 @@ Ez az egyszer használatos hivatkozása!</target>
<target>Hiba a cím megváltoztatásakor</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Hiba a szerepkör megváltoztatásakor</target>
@@ -2734,6 +2746,10 @@ Ez az egyszer használatos hivatkozása!</target>
<target>Hiba a beállítás megváltoztatásakor</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Hiba a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később.</target>
@@ -2954,6 +2970,10 @@ Ez az egyszer használatos hivatkozása!</target>
<target>Hiba a csevegés megállításakor</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Hiba a profil váltásakor!</target>
@@ -4184,6 +4204,10 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!</target>
<target>Üzenetkiszolgálók</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Az üzenet forrása titokban marad.</target>
@@ -5729,6 +5753,10 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.</target>
<target>Választás</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>%lld kiválasztva</target>
@@ -6099,6 +6127,10 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.</target>
<target>Hivatkozás megosztása</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Egyszer használatos meghívó hivatkozás megosztása</target>
@@ -6439,6 +6471,10 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Kép készítése</target>
@@ -7719,6 +7755,10 @@ Kapcsolódási kérés megismétlése?</target>
<target>Csevegési profilok</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Ismerőse olyan fájlt küldött, amely meghaladja a jelenleg támogatott maximális méretet (%@).</target>
@@ -137,6 +137,10 @@
<target>%@ si vuole connettere!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ e %lld membri</target>
@@ -1740,6 +1744,10 @@ Questo è il tuo link una tantum!</target>
<target>Versione core: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Correggere il nome a %@?</target>
@@ -2724,6 +2732,10 @@ Questo è il tuo link una tantum!</target>
<target>Errore nella modifica dell'indirizzo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Errore nel cambio di ruolo</target>
@@ -2734,6 +2746,10 @@ Questo è il tuo link una tantum!</target>
<target>Errore nella modifica dell'impostazione</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Errore di connessione al server di inoltro %@. Riprova più tardi.</target>
@@ -2954,6 +2970,10 @@ Questo è il tuo link una tantum!</target>
<target>Errore nell'interruzione della chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Errore nel cambio di profilo!</target>
@@ -4184,6 +4204,10 @@ Questo è il tuo link per il gruppo %@!</target>
<target>Server dei messaggi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>La fonte del messaggio resta privata.</target>
@@ -5729,6 +5753,10 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Seleziona</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>%lld selezionato</target>
@@ -6099,6 +6127,10 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Condividi link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Condividi questo link di invito una tantum</target>
@@ -6439,6 +6471,10 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Scatta foto</target>
@@ -7719,6 +7755,10 @@ Ripetere la richiesta di connessione?</target>
<target>I tuoi profili di chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Il tuo contatto ha inviato un file più grande della dimensione massima attualmente supportata (%@).</target>
@@ -137,6 +137,10 @@
<target>%@ が接続を希望しています!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@や%@など%lld人のメンバー</target>
@@ -1640,6 +1644,10 @@ This is your own one-time link!</source>
<target>コアのバージョン: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
@@ -2569,6 +2577,10 @@ This is your own one-time link!</source>
<target>アドレス変更にエラー発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>役割変更にエラー発生</target>
@@ -2579,6 +2591,10 @@ This is your own one-time link!</source>
<target>設定変更にエラー発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2787,6 +2803,10 @@ This is your own one-time link!</source>
<target>チャット停止にエラー発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>プロフィール切り替えにエラー発生!</target>
@@ -3942,6 +3962,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<note>No comment provided by engineer.</note>
@@ -5376,6 +5400,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>選択</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5713,6 +5741,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>リンクを送る</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
@@ -6029,6 +6061,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>写真を撮影</target>
@@ -7212,6 +7248,10 @@ Repeat connection request?</source>
<target>あなたのチャットプロフィール</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。</target>
@@ -137,6 +137,10 @@
<target>%@ wil verbinding maken!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ en %lld leden</target>
@@ -1740,6 +1744,10 @@ Dit is uw eigen eenmalige link!</target>
<target>Core versie: v% @</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Juiste naam voor %@?</target>
@@ -2724,6 +2732,10 @@ Dit is uw eigen eenmalige link!</target>
<target>Fout bij wijzigen van adres</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Fout bij wisselen van rol</target>
@@ -2734,6 +2746,10 @@ Dit is uw eigen eenmalige link!</target>
<target>Fout bij wijzigen van instelling</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Fout bij het verbinden met doorstuurserver %@. Probeer het later opnieuw.</target>
@@ -2954,6 +2970,10 @@ Dit is uw eigen eenmalige link!</target>
<target>Fout bij het stoppen van de chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Fout bij wisselen van profiel!</target>
@@ -4184,6 +4204,10 @@ Dit is jouw link voor groep %@!</target>
<target>Berichtservers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Berichtbron blijft privé.</target>
@@ -5729,6 +5753,10 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
<target>Selecteer</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>%lld geselecteerd</target>
@@ -6099,6 +6127,10 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
<target>Deel link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Deel deze eenmalige uitnodigingslink</target>
@@ -6439,6 +6471,10 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Foto nemen</target>
@@ -7719,6 +7755,10 @@ Verbindingsverzoek herhalen?</target>
<target>Uw chat profielen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@).</target>
@@ -137,6 +137,10 @@
<target>%@ chce się połączyć!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ i %lld członków</target>
@@ -1740,6 +1744,10 @@ To jest twój jednorazowy link!</target>
<target>Wersja rdzenia: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Poprawić imię na %@?</target>
@@ -2724,6 +2732,10 @@ To jest twój jednorazowy link!</target>
<target>Błąd zmiany adresu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Błąd zmiany roli</target>
@@ -2734,6 +2746,10 @@ To jest twój jednorazowy link!</target>
<target>Błąd zmiany ustawienia</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Błąd połączenia z serwerem przekierowania %@. Spróbuj ponownie później.</target>
@@ -2954,6 +2970,10 @@ To jest twój jednorazowy link!</target>
<target>Błąd zatrzymania czatu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Błąd przełączania profilu!</target>
@@ -4184,6 +4204,10 @@ To jest twój link do grupy %@!</target>
<target>Serwery wiadomości</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Źródło wiadomości pozostaje prywatne.</target>
@@ -5729,6 +5753,10 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
<target>Wybierz</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>Zaznaczono %lld</target>
@@ -6099,6 +6127,10 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
<target>Udostępnij link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Udostępnij ten jednorazowy link</target>
@@ -6439,6 +6471,10 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Zrób zdjęcie</target>
@@ -7719,6 +7755,10 @@ Powtórzyć prośbę połączenia?</target>
<target>Twoje profile czatu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Twój kontakt wysłał plik, który jest większy niż obecnie obsługiwany maksymalny rozmiar (%@).</target>
@@ -137,6 +137,10 @@
<target>%@ хочет соединиться!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ и %lld членов группы</target>
@@ -1740,6 +1744,10 @@ This is your own one-time link!</source>
<target>Версия ядра: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Исправить имя на %@?</target>
@@ -2724,6 +2732,10 @@ This is your own one-time link!</source>
<target>Ошибка при изменении адреса</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Ошибка при изменении роли</target>
@@ -2734,6 +2746,10 @@ This is your own one-time link!</source>
<target>Ошибка при изменении настройки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Ошибка подключения к пересылающему серверу %@. Попробуйте позже.</target>
@@ -2954,6 +2970,10 @@ This is your own one-time link!</source>
<target>Ошибка при остановке чата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Ошибка выбора профиля!</target>
@@ -4184,6 +4204,10 @@ This is your link for group %@!</source>
<target>Серверы сообщений</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Источник сообщения остаётся конфиденциальным.</target>
@@ -5729,6 +5753,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Выбрать</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>Выбрано %lld</target>
@@ -6099,6 +6127,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Поделиться ссылкой</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Поделиться одноразовой ссылкой-приглашением</target>
@@ -6439,6 +6471,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Сделать фото</target>
@@ -7719,6 +7755,10 @@ Repeat connection request?</source>
<target>Ваши профили чата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Ваш контакт отправил файл, размер которого превышает максимальный размер (%@).</target>
@@ -129,6 +129,10 @@
<target>%@ อยากเชื่อมต่อ!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<note>No comment provided by engineer.</note>
@@ -1606,6 +1610,10 @@ This is your own one-time link!</source>
<target>รุ่นหลัก: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
@@ -2530,6 +2538,10 @@ This is your own one-time link!</source>
<target>เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>เกิดข้อผิดพลาดในการเปลี่ยนบทบาท</target>
@@ -2540,6 +2552,10 @@ This is your own one-time link!</source>
<target>เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2747,6 +2763,10 @@ This is your own one-time link!</source>
<target>เกิดข้อผิดพลาดในการหยุดแชท</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>เกิดข้อผิดพลาดในการเปลี่ยนโปรไฟล์!</target>
@@ -3901,6 +3921,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<note>No comment provided by engineer.</note>
@@ -5328,6 +5352,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>เลือก</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5670,6 +5698,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>แชร์ลิงก์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
@@ -5983,6 +6015,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>ถ่ายภาพ</target>
@@ -7163,6 +7199,10 @@ Repeat connection request?</source>
<target>โปรไฟล์แชทของคุณ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)</target>
@@ -137,6 +137,10 @@
<target>%@ bağlanmak istiyor!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ ve %lld üyeleri</target>
@@ -1689,6 +1693,10 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
<target>Çekirdek sürümü: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>İsim %@ olarak düzeltilsin mi?</target>
@@ -2653,6 +2661,10 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
<target>Adres değiştirilirken hata oluştu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Rol değiştirilirken hata oluştu</target>
@@ -2663,6 +2675,10 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
<target>Ayar değiştirilirken hata oluştu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2878,6 +2894,10 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
<target>Sohbet durdurulurken hata oluştu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Profil değiştirilirken hata oluştu!</target>
@@ -4084,6 +4104,10 @@ Bu senin grup için bağlantın %@!</target>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Mesaj kaynağı gizli kalır.</target>
@@ -5585,6 +5609,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Seç</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5938,6 +5966,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Bağlantıyı paylaş</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Bu tek kullanımlık bağlantı davetini paylaş</target>
@@ -6264,6 +6296,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_TVLDEKAL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Fotoğraf çek</target>
@@ -7517,6 +7553,10 @@ Bağlantı isteği tekrarlansın mı?</target>
<target>Sohbet profillerin</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Kişiniz şu anda desteklenen maksimum boyuttan (%@) daha büyük bir dosya gönderdi.</target>
@@ -137,6 +137,10 @@
<target>%@ хоче підключитися!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ та %lld учасників</target>
@@ -1740,6 +1744,10 @@ This is your own one-time link!</source>
<target>Основна версія: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Виправити ім'я на %@?</target>
@@ -2724,6 +2732,10 @@ This is your own one-time link!</source>
<target>Помилка зміни адреси</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Помилка зміни ролі</target>
@@ -2734,6 +2746,10 @@ This is your own one-time link!</source>
<target>Помилка зміни налаштування</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Помилка підключення до сервера переадресації %@. Спробуйте пізніше.</target>
@@ -2954,6 +2970,10 @@ This is your own one-time link!</source>
<target>Помилка зупинки чату</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Помилка перемикання профілю!</target>
@@ -4184,6 +4204,10 @@ This is your link for group %@!</source>
<target>Сервери повідомлень</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Джерело повідомлення залишається приватним.</target>
@@ -5729,6 +5753,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Виберіть</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>Вибрано %lld</target>
@@ -6099,6 +6127,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Поділіться посиланням</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Поділіться цим одноразовим посиланням-запрошенням</target>
@@ -6439,6 +6471,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Сфотографуйте</target>
@@ -7719,6 +7755,10 @@ Repeat connection request?</source>
<target>Ваші профілі чату</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@).</target>
@@ -135,6 +135,10 @@
<target>%@ 要连接!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ 和 %lld 成员</target>
@@ -1665,6 +1669,10 @@ This is your own one-time link!</source>
<target>核心版本: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
@@ -2618,6 +2626,10 @@ This is your own one-time link!</source>
<target>更改地址错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>更改角色错误</target>
@@ -2628,6 +2640,10 @@ This is your own one-time link!</source>
<target>更改设置错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2841,6 +2857,10 @@ This is your own one-time link!</source>
<target>停止聊天错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>切换资料错误!</target>
@@ -4033,6 +4053,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>消息来源保持私密。</target>
@@ -5512,6 +5536,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>选择</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5861,6 +5889,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>分享链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>分享此一次性邀请链接</target>
@@ -6185,6 +6217,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>拍照</target>
@@ -7417,6 +7453,10 @@ Repeat connection request?</source>
<target>您的聊天资料</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>您的联系人发送的文件大于当前支持的最大大小 (%@)。</target>
+40 -40
View File
@@ -214,11 +214,11 @@
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
E51ED5802C78BF95009F2C7C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57B2C78BF95009F2C7C /* libgmpxx.a */; };
E51ED5812C78BF95009F2C7C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57C2C78BF95009F2C7C /* libgmp.a */; };
E51ED5822C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57D2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a */; };
E51ED5832C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57E2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a */; };
E51ED5842C78BF95009F2C7C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57F2C78BF95009F2C7C /* libffi.a */; };
E51ED5942C7B9983009F2C7C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED58F2C7B9983009F2C7C /* libgmpxx.a */; };
E51ED5952C7B9983009F2C7C /* libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5902C7B9983009F2C7C /* libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx-ghc9.6.3.a */; };
E51ED5962C7B9983009F2C7C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5912C7B9983009F2C7C /* libgmp.a */; };
E51ED5972C7B9983009F2C7C /* libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5922C7B9983009F2C7C /* libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx.a */; };
E51ED5982C7B9983009F2C7C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5932C7B9983009F2C7C /* libffi.a */; };
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
@@ -550,11 +550,11 @@
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
E51ED57B2C78BF95009F2C7C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E51ED57C2C78BF95009F2C7C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E51ED57D2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a"; sourceTree = "<group>"; };
E51ED57E2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a"; sourceTree = "<group>"; };
E51ED57F2C78BF95009F2C7C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E51ED58F2C7B9983009F2C7C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E51ED5902C7B9983009F2C7C /* libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx-ghc9.6.3.a"; sourceTree = "<group>"; };
E51ED5912C7B9983009F2C7C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E51ED5922C7B9983009F2C7C /* libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx.a"; sourceTree = "<group>"; };
E51ED5932C7B9983009F2C7C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -645,14 +645,14 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E51ED5802C78BF95009F2C7C /* libgmpxx.a in Frameworks */,
E51ED5812C78BF95009F2C7C /* libgmp.a in Frameworks */,
E51ED5822C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a in Frameworks */,
E51ED5842C78BF95009F2C7C /* libffi.a in Frameworks */,
E51ED5942C7B9983009F2C7C /* libgmpxx.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
E51ED5982C7B9983009F2C7C /* libffi.a in Frameworks */,
E51ED5972C7B9983009F2C7C /* libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx.a in Frameworks */,
E51ED5952C7B9983009F2C7C /* libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx-ghc9.6.3.a in Frameworks */,
E51ED5962C7B9983009F2C7C /* libgmp.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
E51ED5832C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -729,11 +729,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
E51ED57F2C78BF95009F2C7C /* libffi.a */,
E51ED57C2C78BF95009F2C7C /* libgmp.a */,
E51ED57B2C78BF95009F2C7C /* libgmpxx.a */,
E51ED57E2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a */,
E51ED57D2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a */,
E51ED5932C7B9983009F2C7C /* libffi.a */,
E51ED5912C7B9983009F2C7C /* libgmp.a */,
E51ED58F2C7B9983009F2C7C /* libgmpxx.a */,
E51ED5902C7B9983009F2C7C /* libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx-ghc9.6.3.a */,
E51ED5922C7B9983009F2C7C /* libHSsimplex-chat-6.1.0.0-2HbUlAtNXgRGMjFy4vK7lx.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1879,7 +1879,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 234;
CURRENT_PROJECT_VERSION = 235;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1904,7 +1904,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 6.0.2;
MARKETING_VERSION = 6.0.3;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1928,7 +1928,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 234;
CURRENT_PROJECT_VERSION = 235;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1953,7 +1953,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.2;
MARKETING_VERSION = 6.0.3;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1969,11 +1969,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 234;
CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.0.2;
MARKETING_VERSION = 6.0.3;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -1989,11 +1989,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 234;
CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.0.2;
MARKETING_VERSION = 6.0.3;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2014,7 +2014,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 234;
CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2029,7 +2029,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.2;
MARKETING_VERSION = 6.0.3;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2051,7 +2051,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 234;
CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2066,7 +2066,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.2;
MARKETING_VERSION = 6.0.3;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2088,7 +2088,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 234;
CURRENT_PROJECT_VERSION = 235;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2114,7 +2114,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.2;
MARKETING_VERSION = 6.0.3;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2139,7 +2139,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 234;
CURRENT_PROJECT_VERSION = 235;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2165,7 +2165,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.2;
MARKETING_VERSION = 6.0.3;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2190,7 +2190,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 234;
CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2205,7 +2205,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.0.2;
MARKETING_VERSION = 6.0.3;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2224,7 +2224,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 234;
CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2239,7 +2239,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.0.2;
MARKETING_VERSION = 6.0.3;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
+48 -39
View File
@@ -540,7 +540,7 @@ public enum ChatResponse: Decodable, Error {
case networkConfig(networkConfig: NetCfg)
case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?)
case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: QueueInfo)
case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: ServerQueueInfo)
case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
@@ -1102,12 +1102,12 @@ public enum GroupLinkPlan: Decodable, Hashable {
case known(groupInfo: GroupInfo)
}
struct NewUser: Encodable, Hashable {
struct NewUser: Encodable {
var profile: Profile?
var pastTimestamp: Bool
}
public enum ChatPagination: Hashable {
public enum ChatPagination {
case last(count: Int)
case after(chatItemId: Int64, count: Int)
case before(chatItemId: Int64, count: Int)
@@ -1329,7 +1329,7 @@ public struct ServerAddress: Decodable {
)
}
public struct NetCfg: Codable, Equatable, Hashable {
public struct NetCfg: Codable, Equatable {
public var socksProxy: String? = nil
var socksMode: SocksMode = .always
public var hostMode: HostMode = .publicHost
@@ -1383,18 +1383,18 @@ public struct NetCfg: Codable, Equatable, Hashable {
public var enableKeepAlive: Bool { tcpKeepAlive != nil }
}
public enum HostMode: String, Codable, Hashable {
public enum HostMode: String, Codable {
case onionViaSocks
case onionHost = "onion"
case publicHost = "public"
}
public enum SocksMode: String, Codable, Hashable {
public enum SocksMode: String, Codable {
case always = "always"
case onion = "onion"
}
public enum SMPProxyMode: String, Codable, Hashable, SelectableItem {
public enum SMPProxyMode: String, Codable, SelectableItem {
case always = "always"
case unknown = "unknown"
case unprotected = "unprotected"
@@ -1414,7 +1414,7 @@ public enum SMPProxyMode: String, Codable, Hashable, SelectableItem {
public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never]
}
public enum SMPProxyFallback: String, Codable, Hashable, SelectableItem {
public enum SMPProxyFallback: String, Codable, SelectableItem {
case allow = "allow"
case allowProtected = "allowProtected"
case prohibit = "prohibit"
@@ -1432,7 +1432,7 @@ public enum SMPProxyFallback: String, Codable, Hashable, SelectableItem {
public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit]
}
public enum OnionHosts: String, Identifiable, Hashable {
public enum OnionHosts: String, Identifiable {
case no
case prefer
case require
@@ -1466,7 +1466,7 @@ public enum OnionHosts: String, Identifiable, Hashable {
public static let values: [OnionHosts] = [.no, .prefer, .require]
}
public enum TransportSessionMode: String, Codable, Identifiable, Hashable {
public enum TransportSessionMode: String, Codable, Identifiable {
case user
case entity
@@ -1482,7 +1482,7 @@ public enum TransportSessionMode: String, Codable, Identifiable, Hashable {
public static let values: [TransportSessionMode] = [.user, .entity]
}
public struct KeepAliveOpts: Codable, Equatable, Hashable {
public struct KeepAliveOpts: Codable, Equatable {
public var keepIdle: Int // seconds
public var keepIntvl: Int // seconds
public var keepCnt: Int // times
@@ -1490,7 +1490,7 @@ public struct KeepAliveOpts: Codable, Equatable, Hashable {
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
}
public enum NetworkStatus: Decodable, Equatable, Hashable {
public enum NetworkStatus: Decodable, Equatable {
case unknown
case connected
case disconnected
@@ -1528,7 +1528,7 @@ public enum NetworkStatus: Decodable, Equatable, Hashable {
}
}
public struct ConnNetworkStatus: Decodable, Hashable {
public struct ConnNetworkStatus: Decodable {
public var agentConnId: String
public var networkStatus: NetworkStatus
}
@@ -1553,7 +1553,7 @@ public enum MsgFilter: String, Codable, Hashable {
case mentions
}
public struct UserMsgReceiptSettings: Codable, Hashable {
public struct UserMsgReceiptSettings: Codable {
public var enable: Bool
public var clearOverrides: Bool
@@ -1602,7 +1602,7 @@ public enum SndSwitchStatus: String, Codable, Hashable {
case sendingQTEST = "sending_qtest"
}
public enum QueueDirection: String, Decodable, Hashable {
public enum QueueDirection: String, Decodable {
case rcv
case snd
}
@@ -1657,12 +1657,12 @@ public struct AutoAccept: Codable, Hashable {
}
}
public protocol SelectableItem: Hashable, Identifiable {
public protocol SelectableItem: Identifiable, Equatable {
var label: LocalizedStringKey { get }
static var values: [Self] { get }
}
public struct DeviceToken: Decodable, Hashable {
public struct DeviceToken: Decodable {
var pushProvider: PushProvider
var token: String
@@ -1676,12 +1676,12 @@ public struct DeviceToken: Decodable, Hashable {
}
}
public enum PushEnvironment: String, Hashable {
public enum PushEnvironment: String {
case development
case production
}
public enum PushProvider: String, Decodable, Hashable {
public enum PushProvider: String, Decodable {
case apns_dev
case apns_prod
@@ -1695,7 +1695,7 @@ public enum PushProvider: String, Decodable, Hashable {
// This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable,
// and .local for periodic background checks
public enum NotificationsMode: String, Decodable, SelectableItem, Hashable {
public enum NotificationsMode: String, Decodable, SelectableItem {
case off = "OFF"
case periodic = "PERIODIC"
case instant = "INSTANT"
@@ -1713,7 +1713,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem, Hashable {
public static var values: [NotificationsMode] = [.instant, .periodic, .off]
}
public enum NotificationPreviewMode: String, SelectableItem, Codable, Hashable {
public enum NotificationPreviewMode: String, SelectableItem, Codable {
case hidden
case contact
case message
@@ -1731,7 +1731,7 @@ public enum NotificationPreviewMode: String, SelectableItem, Codable, Hashable {
public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden]
}
public struct RemoteCtrlInfo: Decodable, Hashable {
public struct RemoteCtrlInfo: Decodable {
public var remoteCtrlId: Int64
public var ctrlDeviceName: String
public var sessionState: RemoteCtrlSessionState?
@@ -1741,7 +1741,7 @@ public struct RemoteCtrlInfo: Decodable, Hashable {
}
}
public enum RemoteCtrlSessionState: Decodable, Hashable {
public enum RemoteCtrlSessionState: Decodable {
case starting
case searching
case connecting
@@ -1756,17 +1756,17 @@ public enum RemoteCtrlStopReason: Decodable {
case disconnected
}
public struct CtrlAppInfo: Decodable, Hashable {
public struct CtrlAppInfo: Decodable {
public var appVersionRange: AppVersionRange
public var deviceName: String
}
public struct AppVersionRange: Decodable, Hashable {
public struct AppVersionRange: Decodable {
public var minVersion: String
public var maxVersion: String
}
public struct CoreVersionInfo: Decodable, Hashable {
public struct CoreVersionInfo: Decodable {
public var version: String
public var simplexmqVersion: String
public var simplexmqCommit: String
@@ -2104,14 +2104,14 @@ public enum RemoteCtrlError: Decodable, Hashable {
case protocolError
}
public struct MigrationFileLinkData: Codable, Hashable {
public struct MigrationFileLinkData: Codable {
let networkConfig: NetworkConfig?
public init(networkConfig: NetworkConfig) {
self.networkConfig = networkConfig
}
public struct NetworkConfig: Codable, Hashable {
public struct NetworkConfig: Codable {
let socksProxy: String?
let hostMode: HostMode?
let requiredHostMode: Bool?
@@ -2143,7 +2143,7 @@ public struct MigrationFileLinkData: Codable, Hashable {
}
}
public struct AppSettings: Codable, Equatable, Hashable {
public struct AppSettings: Codable, Equatable {
public var networkConfig: NetCfg? = nil
public var privacyEncryptLocalFiles: Bool? = nil
public var privacyAskToApproveRelays: Bool? = nil
@@ -2238,7 +2238,7 @@ public struct AppSettings: Codable, Equatable, Hashable {
}
}
public enum AppSettingsNotificationMode: String, Codable, Hashable {
public enum AppSettingsNotificationMode: String, Codable {
case off
case periodic
case instant
@@ -2266,13 +2266,13 @@ public enum AppSettingsNotificationMode: String, Codable, Hashable {
// case message
//}
public enum AppSettingsLockScreenCalls: String, Codable, Hashable {
public enum AppSettingsLockScreenCalls: String, Codable {
case disable
case show
case accept
}
public struct UserNetworkInfo: Codable, Equatable, Hashable {
public struct UserNetworkInfo: Codable, Equatable {
public let networkType: UserNetworkType
public let online: Bool
@@ -2282,7 +2282,7 @@ public struct UserNetworkInfo: Codable, Equatable, Hashable {
}
}
public enum UserNetworkType: String, Codable, Hashable {
public enum UserNetworkType: String, Codable {
case none
case cellular
case wifi
@@ -2300,7 +2300,7 @@ public enum UserNetworkType: String, Codable, Hashable {
}
}
public struct RcvMsgInfo: Codable, Hashable {
public struct RcvMsgInfo: Codable {
var msgId: Int64
var msgDeliveryId: Int64
var msgDeliveryStatus: String
@@ -2308,7 +2308,16 @@ public struct RcvMsgInfo: Codable, Hashable {
var agentMsgMeta: String
}
public struct QueueInfo: Codable, Hashable {
public struct ServerQueueInfo: Codable {
var server: String
var rcvId: String
var sndId: String
var ntfId: String?
var status: String
var info: QueueInfo
}
public struct QueueInfo: Codable {
var qiSnd: Bool
var qiNtf: Bool
var qiSub: QSub?
@@ -2316,25 +2325,25 @@ public struct QueueInfo: Codable, Hashable {
var qiMsg: MsgInfo?
}
public struct QSub: Codable, Hashable {
public struct QSub: Codable {
var qSubThread: QSubThread
var qDelivered: String?
}
public enum QSubThread: String, Codable, Hashable {
public enum QSubThread: String, Codable {
case noSub
case subPending
case subThread
case prohibitSub
}
public struct MsgInfo: Codable, Hashable {
public struct MsgInfo: Codable {
var msgId: String
var msgTs: Date
var msgType: MsgType
}
public enum MsgType: String, Codable, Hashable {
public enum MsgType: String, Codable {
case message
case quota
}
+8 -19
View File
@@ -2688,6 +2688,13 @@ public enum CIDirection: Decodable, Hashable {
}
}
}
public func sameDirection(_ dir: CIDirection) -> Bool {
switch (self, dir) {
case let (.groupRcv(m1), .groupRcv(m2)): m1.groupMemberId == m2.groupMemberId
default: sent == dir.sent
}
}
}
public struct CIMeta: Decodable, Hashable {
@@ -2758,26 +2765,8 @@ public struct CITimed: Decodable, Hashable {
public var deleteAt: Date?
}
let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute()
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
public func formatTimestampText(_ date: Date) -> Text {
return Text(date, format: recent(date) ? msgTimeFormat : msgDateFormat)
}
private func recent(_ date: Date) -> Bool {
let now = Date()
let calendar = Calendar.current
guard let previousDay = calendar.date(byAdding: DateComponents(day: -1), to: now),
let previousDay18 = calendar.date(bySettingHour: 18, minute: 0, second: 0, of: previousDay),
let currentDay00 = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: now),
let currentDay12 = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: now) else {
return false
}
let isSameDay = calendar.isDate(date, inSameDayAs: now)
return isSameDay || (now < currentDay12 && date >= previousDay18 && date < currentDay00)
Text(verbatim: date.formatted(date: .omitted, time: .shortened))
}
public enum CIStatus: Decodable, Hashable {
+11 -11
View File
@@ -15,7 +15,7 @@ android {
namespace = "chat.simplex.app"
minSdk = 26
//noinspection OldTargetApi
targetSdk = 33
targetSdk = 34
// !!!
// skip version code after release to F-Droid, as it uses two version codes
versionCode = (extra["android.version_code"] as String).toInt()
@@ -126,29 +126,29 @@ android {
dependencies {
implementation(project(":common"))
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.core:core-ktx:1.13.1")
//implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}")
//implementation("androidx.compose.material:material:$compose_version")
//implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-process:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
val workVersion = "2.9.0"
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
implementation("androidx.lifecycle:lifecycle-process:2.8.4")
implementation("androidx.activity:activity-compose:1.9.1")
val workVersion = "2.9.1"
implementation("androidx.work:work-runtime-ktx:$workVersion")
implementation("androidx.work:work-multiprocess:$workVersion")
implementation("com.jakewharton:process-phoenix:2.2.0")
implementation("com.jakewharton:process-phoenix:3.0.0")
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.23.0")
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
//implementation("androidx.compose.ui:ui-util:$compose_version")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
//androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
debugImplementation("androidx.compose.ui:ui-tooling:1.6.4")
}
@@ -21,6 +21,12 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- Requirements that allows to specify foreground service types -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application
android:name="SimplexApp"
android:allowBackup="false"
@@ -133,7 +139,9 @@
android:name=".SimplexService"
android:enabled="true"
android:exported="false"
android:stopWithTask="false"></service>
android:stopWithTask="false"
android:foregroundServiceType="remoteMessaging"
/>
<!-- SimplexService restart on reboot -->
@@ -141,7 +149,9 @@
android:name=".CallService"
android:enabled="true"
android:exported="false"
android:stopWithTask="false"/>
android:stopWithTask="false"
android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging"
/>
<receiver
android:name=".CallService$CallActionReceiver"
@@ -2,17 +2,18 @@ package chat.simplex.app
import android.app.*
import android.content.*
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.*
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import chat.simplex.app.model.NtfManager.EndCallAction
import chat.simplex.app.views.call.CallActivity
import chat.simplex.common.model.NotificationPreviewMode
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.CallState
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.datetime.Instant
@@ -34,7 +35,7 @@ class CallService: Service() {
} else {
Log.d(TAG, "null intent. Probably restarted by the system.")
}
startForeground(CALL_SERVICE_ID, serviceNotification)
ServiceCompat.startForeground(this, CALL_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
return START_STICKY
}
@@ -42,8 +43,7 @@ class CallService: Service() {
super.onCreate()
Log.d(TAG, "Call service created")
notificationManager = createNotificationChannel()
updateNotification()
startForeground(CALL_SERVICE_ID, serviceNotification)
ServiceCompat.startForeground(this, CALL_SERVICE_ID, updateNotification(), foregroundServiceType())
}
override fun onDestroy() {
@@ -69,7 +69,14 @@ class CallService: Service() {
}
}
fun updateNotification() {
private fun createNotificationIfNeeded(): Notification {
val ntf = serviceNotification
if (ntf != null) return ntf
return updateNotification()
}
fun updateNotification(): Notification {
val call = chatModel.activeCall.value
val previewMode = appPreferences.notificationPreviewMode.get()
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
@@ -83,8 +90,31 @@ class CallService: Service() {
else
base64ToBitmap(image).asAndroidBitmap()
serviceNotification = createNotification(title, text, largeIcon, call?.connectedAt)
startForeground(CALL_SERVICE_ID, serviceNotification)
val ntf = createNotification(title, text, largeIcon, call?.connectedAt)
serviceNotification = ntf
ServiceCompat.startForeground(this, CALL_SERVICE_ID, ntf, foregroundServiceType())
return ntf
}
private fun foregroundServiceType(): Int {
val call = chatModel.activeCall.value
return if (call == null) {
if (Build.VERSION.SDK_INT >= 34) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
} else {
0
}
} else if (Build.VERSION.SDK_INT >= 30) {
if (call.supportsVideo()) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
} else {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
}
} else if (Build.VERSION.SDK_INT >= 29) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
} else {
0
}
}
private fun createNotificationChannel(): NotificationManager? {
@@ -54,7 +54,7 @@ class MainActivity: FragmentActivity() {
SimplexApp.context.schedulePeriodicWakeUp()
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
processIntent(intent)
processExternalIntent(intent)
@@ -120,7 +120,10 @@ class SimplexApp: Application(), LifecycleEventObserver {
* */
if (chatModel.chatRunning.value != false &&
chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete &&
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE &&
// New installation passes all checks above and tries to start the service which is not needed at all
// because preferred notification type is not yet chosen. So, check that the user has initialized db already
appPrefs.newDatabaseInitialized.get()
) {
SimplexService.start()
}
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.*
import android.content.*
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.net.Uri
import android.os.*
import android.os.SystemClock
@@ -15,8 +16,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import chat.simplex.app.model.NtfManager
import chat.simplex.common.AppLock
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.ChatController
@@ -52,18 +55,15 @@ class SimplexService: Service() {
} else {
Log.d(TAG, "null intent. Probably restarted by the system.")
}
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
return START_STICKY // to restart if killed
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Simplex service created")
val title = generalGetString(MR.strings.simplex_service_notification_title)
val text = generalGetString(MR.strings.simplex_service_notification_text)
notificationManager = createNotificationChannel()
serviceNotification = createNotification(title, text)
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
createNotificationIfNeeded()
ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
/**
* The reason [stopAfterStart] exists is because when the service is not called [startForeground] yet, and
* we call [stopSelf] on the same service, [ForegroundServiceDidNotStartInTimeException] will be thrown.
@@ -103,6 +103,26 @@ class SimplexService: Service() {
super.onDestroy()
}
private fun createNotificationIfNeeded(): Notification {
val ntf = serviceNotification
if (ntf != null) return ntf
val title = generalGetString(MR.strings.simplex_service_notification_title)
val text = generalGetString(MR.strings.simplex_service_notification_text)
notificationManager = createNotificationChannel()
val newNtf = createNotification(title, text)
serviceNotification = newNtf
return newNtf
}
private fun foregroundServiceType(): Int {
return if (Build.VERSION.SDK_INT >= 34) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
} else {
0
}
}
private fun startService() {
Log.d(TAG, "SimplexService startService")
if (wakeLock != null || isCheckingNewMessages) return
@@ -292,6 +312,10 @@ class SimplexService: Service() {
}
private suspend fun serviceAction(action: Action) {
if (!NtfManager.areNotificationsEnabledInSystem()) {
Log.d(TAG, "SimplexService serviceAction: ${action.name}. Notifications are not enabled in OS yet, not starting service")
return
}
Log.d(TAG, "SimplexService serviceAction: ${action.name}")
withContext(Dispatchers.IO) {
Intent(androidAppContext, SimplexService::class.java).also {
@@ -53,7 +53,7 @@ object NtfManager {
private val msgNtfTimeoutMs = 30000L
init {
if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert()
if (areNotificationsEnabledInSystem()) createNtfChannelsMaybeShowAlert()
}
private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel {
@@ -287,6 +287,8 @@ object NtfManager {
}
}
fun areNotificationsEnabledInSystem() = manager.areNotificationsEnabled()
/**
* This function creates notifications channels. On Android 13+ calling it for the first time will trigger system alert,
* The alert asks a user to allow or disallow to show notifications for the app. That's why it should be called only when the user
@@ -120,6 +120,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
return grantedAudio && grantedCamera
}
@Deprecated("Was deprecated in OS")
override fun onBackPressed() {
if (isOnLockScreenNow()) {
super.onBackPressed()
@@ -139,6 +140,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
// On Android 12+ PiP is enabled automatically when a user hides the app
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
enterPictureInPictureMode()
@@ -248,6 +250,9 @@ fun CallActivityView() {
)
if (permissionsState.allPermissionsGranted) {
ActiveCallView()
LaunchedEffect(Unit) {
activity.startServiceAndBind()
}
} else {
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
withBGApi { chatModel.callManager.endCall(call) }
@@ -285,11 +290,6 @@ fun CallActivityView() {
AlertManager.shared.showInView()
}
}
LaunchedEffect(call == null) {
if (call != null) {
activity.startServiceAndBind()
}
}
LaunchedEffect(invitation, call, switchingCall, showCallView) {
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
Log.d(TAG, "CallActivityView: finishing activity")
@@ -2,6 +2,5 @@
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="highOrLowLight">#8b8786</color>
<color name="window_background_dark">#121212</color>
</resources>
+8 -8
View File
@@ -61,8 +61,8 @@ kotlin {
val androidMain by getting {
kotlin.srcDir("build/generated/moko/androidMain/src")
dependencies {
implementation("androidx.activity:activity-compose:1.8.2")
val workVersion = "2.9.0"
implementation("androidx.activity:activity-compose:1.9.1")
val workVersion = "2.9.1"
implementation("androidx.work:work-runtime-ktx:$workVersion")
implementation("com.google.accompanist:accompanist-insets:0.30.1")
@@ -78,22 +78,22 @@ kotlin {
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
implementation("androidx.webkit:webkit:1.10.0")
implementation("androidx.webkit:webkit:1.11.0")
// GIFs support
implementation("io.coil-kt:coil-compose:2.6.0")
implementation("io.coil-kt:coil-gif:2.6.0")
implementation("com.jakewharton:process-phoenix:2.2.0")
implementation("com.jakewharton:process-phoenix:3.0.0")
val cameraXVersion = "1.3.2"
val cameraXVersion = "1.3.4"
implementation("androidx.camera:camera-core:${cameraXVersion}")
implementation("androidx.camera:camera-camera2:${cameraXVersion}")
implementation("androidx.camera:camera-lifecycle:${cameraXVersion}")
implementation("androidx.camera:camera-view:${cameraXVersion}")
// Calls lifecycle listener
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
implementation("androidx.lifecycle:lifecycle-process:2.8.4")
}
}
val desktopMain by getting {
@@ -119,8 +119,8 @@ android {
defaultConfig {
minSdk = 26
}
testOptions.targetSdk = 33
lint.targetSdk = 33
testOptions.targetSdk = 34
lint.targetSdk = 34
val isAndroid = gradle.startParameter.taskNames.find {
val lower = it.lowercase()
lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install")
@@ -14,13 +14,11 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@@ -33,7 +31,6 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.compose.painterResource
@@ -82,7 +79,7 @@ fun AppearanceScope.AppearanceLayout(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.appearance_settings))
SectionView(stringResource(MR.strings.settings_section_title_interface), padding = PaddingValues()) {
SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) {
val context = LocalContext.current
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// SectionItemWithValue(
@@ -123,7 +120,7 @@ fun AppearanceScope.AppearanceLayout(
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
SectionView(stringResource(MR.strings.settings_section_title_icon), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
LazyRow {
items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index ->
val item = AppIcon.values()[index]
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/highOrLowLight" />
<solid android:color="#8b8786" />
<size android:width="1dp" />
</shape>
@@ -784,7 +784,8 @@ object ChatModel {
data class ShowingInvitation(
val connId: String,
val connReq: String,
val connChatUsed: Boolean
val connChatUsed: Boolean,
val conn: PendingContactConnection
)
enum class ChatType(val type: String) {
@@ -985,6 +986,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val fullName get() = contact.fullName
override val image get() = contact.image
override val localAlias: String get() = contact.localAlias
override fun anyNameContains(searchAnyCase: String): Boolean = contact.anyNameContains(searchAnyCase)
companion object {
val sampleData = Direct(Contact.sampleData)
@@ -1219,6 +1221,12 @@ data class Contact(
override val localAlias get() = profile.localAlias
val verified get() = activeConn?.connectionCode != null
override fun anyNameContains(searchAnyCase: String): Boolean {
val s = searchAnyCase.trim().lowercase()
return profile.chatViewName.lowercase().contains(s) || profile.displayName.lowercase().contains(s) || profile.fullName.lowercase().contains(s)
}
val directOrUsed: Boolean get() =
if (activeConn != null) {
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
@@ -1030,14 +1030,14 @@ object ChatController {
return null
}
suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair<RcvMsgInfo?, QueueInfo>? {
suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? {
val r = sendCmd(rh, CC.APIContactQueueInfo(contactId))
if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo)
apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r)
return null
}
suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair<RcvMsgInfo?, QueueInfo>? {
suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? {
val r = sendCmd(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId))
if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo)
apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r)
@@ -1132,9 +1132,30 @@ object ChatController {
suspend fun apiSetConnectionIncognito(rh: Long?, connId: Long, incognito: Boolean): PendingContactConnection? {
val r = sendCmd(rh, CC.ApiSetConnectionIncognito(connId, incognito))
if (r is CR.ConnectionIncognitoUpdated) return r.toConnection
Log.e(TAG, "apiSetConnectionIncognito bad response: ${r.responseType} ${r.details}")
return null
return when (r) {
is CR.ConnectionIncognitoUpdated -> r.toConnection
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiSetConnectionIncognito", generalGetString(MR.strings.error_sending_message), r)
}
null
}
}
}
suspend fun apiChangeConnectionUser(rh: Long?, connId: Long, userId: Long): PendingContactConnection? {
val r = sendCmd(rh, CC.ApiChangeConnectionUser(connId, userId))
return when (r) {
is CR.ConnectionUserChanged -> r.toConnection
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r)
}
null
}
}
}
suspend fun apiConnectPlan(rh: Long?, connReq: String): ConnectionPlan? {
@@ -2916,6 +2937,7 @@ sealed class CC {
class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC()
class APIAddContact(val userId: Long, val incognito: Boolean): CC()
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC()
class APIConnectPlan(val userId: Long, val connReq: String): CC()
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
@@ -3071,6 +3093,7 @@ sealed class CC {
is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else ""
is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}"
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
is ApiChangeConnectionUser -> "/_set conn user :$connId $userId"
is APIConnectPlan -> "/_connect plan $userId $connReq"
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
@@ -3213,6 +3236,7 @@ sealed class CC {
is APIVerifyGroupMember -> "apiVerifyGroupMember"
is APIAddContact -> "apiAddContact"
is ApiSetConnectionIncognito -> "apiSetConnectionIncognito"
is ApiChangeConnectionUser -> "apiChangeConnectionUser"
is APIConnectPlan -> "apiConnectPlan"
is APIConnect -> "apiConnect"
is ApiConnectContactViaAddress -> "apiConnectContactViaAddress"
@@ -4739,7 +4763,7 @@ sealed class CR {
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
@Serializable @SerialName("contactInfo") class ContactInfo(val user: UserRef, val contact: Contact, val connectionStats_: ConnectionStats? = null, val customUserProfile: Profile? = null): CR()
@Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats? = null): CR()
@Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: QueueInfo): CR()
@Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: ServerQueueInfo): CR()
@Serializable @SerialName("contactSwitchStarted") class ContactSwitchStarted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR()
@@ -4757,6 +4781,7 @@ sealed class CR {
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR()
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
@Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR()
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR()
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR()
@@ -4935,6 +4960,7 @@ sealed class CR {
is ConnectionVerified -> "connectionVerified"
is Invitation -> "invitation"
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
is ConnectionUserChanged -> "ConnectionUserChanged"
is CRConnectionPlan -> "connectionPlan"
is SentConfirmation -> "sentConfirmation"
is SentInvitation -> "sentInvitation"
@@ -5103,6 +5129,7 @@ sealed class CR {
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection")
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" )
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
is SentConfirmation -> withUser(user, json.encodeToString(connection))
is SentInvitation -> withUser(user, json.encodeToString(connection))
@@ -5553,6 +5580,7 @@ sealed class ChatErrorType {
is AgentCommandError -> "agentCommandError"
is InvalidFileDescription -> "invalidFileDescription"
is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited"
is ConnectionUserChangeProhibited -> "connectionUserChangeProhibited"
is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible"
is InternalError -> "internalError"
is CEException -> "exception $message"
@@ -5630,6 +5658,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType()
@Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType()
@Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType()
@Serializable @SerialName("connectionUserChangeProhibited") object ConnectionUserChangeProhibited: ChatErrorType()
@Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType()
@Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType()
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
@@ -6414,6 +6443,16 @@ data class RcvMsgInfo (
val agentMsgMeta: String
)
@Serializable
data class ServerQueueInfo (
val server: String,
val rcvId: String,
val sndId: String,
val ntfId: String? = null,
val status: String,
val info: QueueInfo
)
@Serializable
data class QueueInfo (
val qiSnd: Boolean,
@@ -1268,7 +1268,7 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
)
}
fun queueInfoText(info: Pair<RcvMsgInfo?, QueueInfo>): String {
fun queueInfoText(info: Pair<RcvMsgInfo?, ServerQueueInfo>): String {
val (rcvMsgInfo, qInfo) = info
val msgInfo: String = if (rcvMsgInfo != null) json.encodeToString(rcvMsgInfo) else generalGetString(MR.strings.message_queue_info_none)
return generalGetString(MR.strings.message_queue_info_server_info).format(json.encodeToString(qInfo), msgInfo)
@@ -282,14 +282,14 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val versions = ciInfo.itemVersions
if (versions.isNotEmpty()) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
versions.forEachIndexed { i, ciVersion ->
ItemVersionView(ciVersion, current = i == 0)
}
}
} else {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(MR.strings.no_history), color = MaterialTheme.colors.secondary)
}
@@ -304,7 +304,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.in_reply_to), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
QuotedMsgView(qi)
}
@@ -381,14 +381,14 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
if (mss.isNotEmpty()) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
mss.forEach { (member, status, sentViaProxy) ->
MemberDeliveryStatusView(member, status, sentViaProxy)
}
}
} else {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(MR.strings.no_info_on_delivery), color = MaterialTheme.colors.secondary)
}
@@ -509,11 +509,11 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -
verticalAlignment = Alignment.CenterVertically
) {
Row(
Modifier.weight(1f).padding(end = DEFAULT_PADDING),
Modifier.weight(1f).padding(top = 8.dp, end = DEFAULT_PADDING, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
MemberProfileImage(size = DEFAULT_MIN_SECTION_ITEM_HEIGHT, member)
MemberProfileImage(size = 42.dp, member)
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -80,7 +80,7 @@ private fun ShareListNavLinkLayout(
click: () -> Unit,
stopped: Boolean,
) {
SectionItemView(minHeight = 50.dp, click = click, disabled = stopped) {
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = 8.dp), click = click, disabled = stopped) {
chatLinkPreview()
}
Divider(Modifier.padding(horizontal = 8.dp))
@@ -98,9 +98,11 @@ private fun SharePreviewView(chat: Chat, disabled: Boolean) {
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
if (chat.chatInfo is ChatInfo.Local) {
ProfileImage(size = 46.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor)
ProfileImage(size = 42.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor)
} else if (chat.chatInfo is ChatInfo.Group) {
ProfileImage(size = 42.dp, chat.chatInfo.image, icon = MR.images.ic_supervised_user_circle_filled)
} else {
ProfileImage(size = 46.dp, chat.chatInfo.image)
ProfileImage(size = 42.dp, chat.chatInfo.image)
}
Text(
chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
@@ -94,10 +94,17 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
}
if (appPlatform.isAndroid) {
tryOrShowError("UserPicker", error = {}) {
UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = {
chatModel.sharedContent.value = null
userPickerState.value = AnimatedViewState.GONE
})
UserPicker(
chatModel,
userPickerState,
showSettings = false,
showCancel = true,
contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart,
cancelClicked = {
chatModel.sharedContent.value = null
userPickerState.value = AnimatedViewState.GONE
}
)
}
}
}
@@ -5,9 +5,7 @@ import SectionSpacer
import SectionView
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -66,7 +64,7 @@ fun DatabaseErrorView(
Modifier.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
style = MaterialTheme.typography.h1
)
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content)
SectionView(null, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content = content)
}
@Composable
@@ -85,7 +85,8 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
Text(
title.value,
fontWeight = FontWeight.SemiBold,
maxLines = 1
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
} else {
@@ -21,15 +21,15 @@ import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
import chat.simplex.res.MR
@Composable
fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(), content: (@Composable ColumnScope.() -> Unit)) {
fun SectionView(title: String? = null, contentPadding: PaddingValues = PaddingValues(), headerBottomPadding: Dp = DEFAULT_PADDING, content: (@Composable ColumnScope.() -> Unit)) {
Column {
if (title != null) {
Text(
title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = DEFAULT_PADDING), fontSize = 12.sp
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = headerBottomPadding), fontSize = 12.sp
)
}
Column(Modifier.padding(padding).fillMaxWidth()) { content() }
Column(Modifier.padding(contentPadding).fillMaxWidth()) { content() }
}
}
@@ -39,7 +39,7 @@ fun ContactConnectionInfoView(
) {
LaunchedEffect(connReqInvitation) {
if (connReqInvitation != null) {
chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false)
chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false, conn = contactConnection)
}
}
/** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation].
@@ -2,20 +2,25 @@ package chat.simplex.common.views.newchat
import SectionBottomSpacer
import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import TextIconSpaced
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.TextStyle
@@ -32,6 +37,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.net.URI
@@ -43,7 +49,7 @@ enum class NewChatOption {
fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, close: () -> Unit) {
val selection = remember { stateGetOrPut("selection") { selection } }
val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } }
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) }
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) }
val connReqInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connReq ?: "" } }
val creatingConnReq = rememberSaveable { mutableStateOf(false) }
val pastedLink = rememberSaveable { mutableStateOf("") }
@@ -177,6 +183,15 @@ private fun CreatingLinkProgressView() {
DefaultProgressView(stringResource(MR.strings.creating_link))
}
private fun updateShownConnection(conn: PendingContactConnection) {
chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy(
conn = conn,
connId = conn.id,
connReq = conn.connReqInv ?: "",
connChatUsed = true
)
}
@Composable
private fun RetryButton(onClick: () -> Unit) {
Column(
@@ -192,35 +207,326 @@ private fun RetryButton(onClick: () -> Unit) {
}
}
@Composable
private fun ProfilePickerOption(
title: String,
selected: Boolean,
disabled: Boolean,
onSelected: () -> Unit,
image: @Composable () -> Unit,
onInfo: (() -> Unit)? = null
) {
Row(
Modifier
.fillMaxWidth()
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp)
.clickable(enabled = !disabled, onClick = onSelected)
.padding(horizontal = DEFAULT_PADDING, vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
image()
TextIconSpaced(false)
Text(title, modifier = Modifier.align(Alignment.CenterVertically))
if (onInfo != null) {
Spacer(Modifier.padding(6.dp))
Column(Modifier
.size(48.dp)
.clip(CircleShape)
.clickable(
enabled = !disabled,
onClick = { ModalManager.start.showModal { IncognitoView() } }
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
painterResource(MR.images.ic_info),
stringResource(MR.strings.incognito),
tint = MaterialTheme.colors.primary
)
}
}
Spacer(Modifier.weight(1f))
if (selected) {
Icon(
painterResource(
MR.images.ic_check
),
title,
Modifier.size(20.dp),
tint = MaterialTheme.colors.primary,
)
}
}
Divider(
Modifier.padding(
start = DEFAULT_PADDING_HALF,
end = DEFAULT_PADDING_HALF,
)
)
}
private fun filteredProfiles(users: List<User>, searchTextOrPassword: String): List<User> {
val s = searchTextOrPassword.trim()
val lower = s.lowercase()
return users.filter { u ->
if ((u.activeUser || !u.hidden) && (s == "" || u.anyNameContains(lower))) {
true
} else {
correctPassword(u, s)
}
}
}
@Composable
private fun ActiveProfilePicker(
search: MutableState<String>,
contactConnection: PendingContactConnection?,
close: () -> Unit,
rhId: Long?
) {
val switchingProfile = remember { mutableStateOf(false) }
val incognito = remember {
chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get()
}
val selectedProfile by remember { chatModel.currentUser }
val searchTextOrPassword = rememberSaveable { search }
val profiles = remember {
chatModel.users.map { it.user }.sortedBy { !it.activeUser }
}
val filteredProfiles by remember {
derivedStateOf { filteredProfiles(profiles, searchTextOrPassword.value) }
}
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(switchingProfile.value) {
progressByTimeout = if (switchingProfile.value) {
delay(500)
switchingProfile.value
} else {
false
}
}
@Composable
fun ProfilePickerUserOption(user: User) {
val selected = selectedProfile?.userId == user.userId && !incognito
ProfilePickerOption(
title = user.chatViewName,
disabled = switchingProfile.value || selected,
selected = selected,
onSelected = {
switchingProfile.value = true
withApi {
try {
if (contactConnection != null) {
val conn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId)
if (conn != null) {
withChats {
updateContactConnection(rhId, conn)
updateShownConnection(conn)
}
controller.changeActiveUser_(
rhId = user.remoteHostId,
toUserId = user.userId,
viewPwd = if (user.hidden) searchTextOrPassword.value else null
)
if (chatModel.currentUser.value?.userId != user.userId) {
AlertManager.shared.showAlertMsg(generalGetString(
MR.strings.switching_profile_error_title),
String.format(generalGetString(MR.strings.switching_profile_error_message), user.chatViewName)
)
}
withChats {
updateContactConnection(user.remoteHostId, conn)
}
close.invoke()
}
}
} finally {
switchingProfile.value = false
}
}
},
image = { ProfileImage(size = 42.dp, image = user.image) }
)
}
@Composable
fun IncognitoUserOption() {
ProfilePickerOption(
disabled = switchingProfile.value,
title = stringResource(MR.strings.incognito),
selected = incognito,
onSelected = {
if (!incognito) {
switchingProfile.value = true
withApi {
try {
if (contactConnection != null) {
val conn = controller.apiSetConnectionIncognito(rhId, contactConnection.pccConnId, true)
if (conn != null) {
withChats {
updateContactConnection(rhId, conn)
updateShownConnection(conn)
}
close.invoke()
}
}
} finally {
switchingProfile.value = false
}
}
}
},
image = {
Spacer(Modifier.width(8.dp))
Icon(
painterResource(MR.images.ic_theater_comedy_filled),
contentDescription = stringResource(MR.strings.incognito),
Modifier.size(32.dp),
tint = Indigo,
)
Spacer(Modifier.width(2.dp))
},
onInfo = { ModalManager.start.showModal { IncognitoView() } },
)
}
BoxWithConstraints {
Column(
Modifier
.fillMaxSize()
.alpha(if (progressByTimeout) 0.6f else 1f)
) {
LazyColumnWithScrollBar(userScrollEnabled = !switchingProfile.value) {
item {
AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(rhId), bottomPadding = DEFAULT_PADDING)
}
val activeProfile = filteredProfiles.firstOrNull { it.activeUser }
if (activeProfile != null) {
val otherProfiles = filteredProfiles.filter { it.userId != activeProfile.userId }
if (incognito) {
item {
IncognitoUserOption()
}
item {
ProfilePickerUserOption(activeProfile)
}
} else {
item {
ProfilePickerUserOption(activeProfile)
}
item {
IncognitoUserOption()
}
}
itemsIndexed(otherProfiles) { _, p ->
ProfilePickerUserOption(p)
}
} else {
item {
IncognitoUserOption()
}
itemsIndexed(filteredProfiles) { _, p ->
ProfilePickerUserOption(p)
}
}
}
}
if (progressByTimeout) {
DefaultProgressView("")
}
}
}
@Composable
private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection: MutableState<PendingContactConnection?>) {
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase()) {
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) {
LinkTextView(connReqInvitation, true)
}
Spacer(Modifier.height(10.dp))
SectionView(stringResource(MR.strings.or_show_this_qr_code).uppercase()) {
SectionView(stringResource(MR.strings.or_show_this_qr_code).uppercase(), headerBottomPadding = 5.dp) {
SimpleXLinkQRCode(connReqInvitation, onShare = { chatModel.markShowingInvitationUsed() })
}
Spacer(Modifier.height(10.dp))
val incognito = remember { mutableStateOf(controller.appPrefs.incognito.get()) }
IncognitoToggle(controller.appPrefs.incognito, incognito) {
ModalManager.start.showModal { IncognitoView() }
Spacer(Modifier.height(DEFAULT_PADDING))
val incognito by remember(chatModel.showingInvitation.value?.conn?.incognito, controller.appPrefs.incognito.get()) {
derivedStateOf {
chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get()
}
}
KeyChangeEffect(incognito.value) {
withBGApi {
val contactConn = contactConnection.value ?: return@withBGApi
val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi
withChats {
contactConnection.value = conn
updateContactConnection(rhId, conn)
val currentUser = remember { chatModel.currentUser }.value
if (currentUser != null) {
SectionView(stringResource(MR.strings.new_chat_share_profile).uppercase(), headerBottomPadding = 5.dp) {
SectionItemView(
padding = PaddingValues(
top = 0.dp,
bottom = 0.dp,
start = 16.dp,
end = 16.dp
),
click = {
ModalManager.start.showCustomModal { close ->
val search = rememberSaveable { mutableStateOf("") }
ModalView(
{ close() },
endButtons = {
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
},
content = {
ActiveProfilePicker(
search = search,
close = close,
rhId = rhId,
contactConnection = contactConnection.value
)
})
}
}
) {
if (incognito) {
Spacer(Modifier.width(8.dp))
Icon(
painterResource(MR.images.ic_theater_comedy_filled),
contentDescription = stringResource(MR.strings.incognito),
tint = Indigo,
modifier = Modifier.size(32.dp)
)
Spacer(Modifier.width(2.dp))
} else {
ProfileImage(size = 42.dp, image = currentUser.image)
}
TextIconSpaced(false)
Text(
text = if (incognito) stringResource(MR.strings.incognito) else currentUser.chatViewName,
color = MaterialTheme.colors.onBackground
)
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.End) {
Icon(
painter = painterResource(MR.images.ic_arrow_forward_ios),
contentDescription = stringResource(MR.strings.new_chat_share_profile),
tint = MaterialTheme.colors.secondary,
)
}
}
}
chatModel.markShowingInvitationUsed()
if (incognito) {
SectionTextFooter(generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared))
}
}
SectionTextFooter(sharedProfileInfo(chatModel, incognito.value))
}
@Composable
@@ -242,14 +548,14 @@ fun AddContactLearnMoreButton() {
@Composable
private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState<Boolean>, pastedLink: MutableState<String>, close: () -> Unit) {
SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase()) {
SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) {
PasteLinkView(rhId, pastedLink, showQRCodeScanner, close)
}
if (appPlatform.isAndroid) {
Spacer(Modifier.height(10.dp))
SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase()) {
SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) {
QRCodeScanner(showQRCodeScanner) { text ->
withBGApi {
val res = verify(rhId, text, close)
@@ -366,7 +672,7 @@ private fun createInvitation(
if (r != null) {
withChats {
updateContactConnection(rhId, r.second)
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false)
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false, conn = r.second)
contactConnection.value = r.second
}
} else {
@@ -8,9 +8,7 @@ import SectionSpacer
import SectionView
import TextIconSpaced
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -149,7 +147,7 @@ private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList<Re
@Composable
private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
AppBarTitle(stringResource(MR.strings.connecting_to_desktop))
SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
@@ -199,7 +197,7 @@ private fun SearchingDesktop(deviceName: String, remoteCtrls: SnapshotStateList<
}
}
SectionDividerSpaced()
SectionView(stringResource(MR.strings.found_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.waiting_for_desktop), fontStyle = FontStyle.Italic)
}
SectionSpacer()
@@ -226,7 +224,7 @@ private fun FoundDesktop(
}
}
SectionDividerSpaced()
SectionView(stringResource(MR.strings.found_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
CtrlDeviceVersionText(session)
if (!compatible) {
@@ -258,7 +256,7 @@ private fun FoundDesktop(
@Composable
private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessCode: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
AppBarTitle(stringResource(MR.strings.verify_connection))
SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
@@ -313,7 +311,7 @@ private fun CtrlDeviceVersionText(session: RemoteCtrlSession) {
@Composable
private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: () -> Unit) {
AppBarTitle(stringResource(MR.strings.connected_to_desktop))
SectionView(stringResource(MR.strings.connected_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.connected_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(rc.deviceViewName)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
@@ -53,7 +53,7 @@ expect fun AppearanceView(m: ChatModel)
object AppearanceScope {
@Composable
fun ProfileImageSection() {
SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
val image = remember { chatModel.currentUser }.value?.image
Row(Modifier.padding(top = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
val size = 60
@@ -86,7 +86,7 @@ object AppearanceScope {
@Composable
fun FontScaleSection() {
val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) }
SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(60.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
@@ -1,15 +1,12 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionItemView
import SectionItemViewSpaceBetween
import SectionItemViewWithoutMinPadding
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -64,7 +61,7 @@ private fun HiddenProfileLayout(
.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.hide_profile))
SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
UserProfileRow(user)
}
SectionSpacer()
@@ -22,7 +22,6 @@ import androidx.compose.ui.text.input.*
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller
@@ -264,7 +263,7 @@ fun SocksProxySettings(
.fillMaxWidth()
) {
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
DefaultConfigurableTextField(
hostUnsaved,
stringResource(MR.strings.host_verb),
@@ -8,7 +8,6 @@ import SectionItemViewWithoutMinPadding
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -270,7 +269,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (
@Composable fun ActionHeader(title: StringResource) {
AppBarTitle(stringResource(title))
SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
UserProfileRow(user)
}
SectionSpacer()
@@ -304,7 +303,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (
}
}
private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User> {
fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User> {
val s = searchTextOrPassword.trim()
val lower = s.lowercase()
return m.users.filter { u ->
@@ -318,7 +317,7 @@ private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User
private fun visibleUsersCount(m: ChatModel): Int = m.users.filter { u -> !u.user.hidden }.size
private fun correctPassword(user: User, pwd: String): Boolean {
fun correctPassword(user: User, pwd: String): Boolean {
val ph = user.viewPwdHash
return ph != null && pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash
}
@@ -665,6 +665,10 @@
<string name="one_time_link_short">1-time link</string>
<string name="simplex_address">SimpleX address</string>
<string name="or_show_this_qr_code">Or show this code</string>
<string name="new_chat_share_profile">Share profile</string>
<string name="select_chat_profile">Select chat profile</string>
<string name="switching_profile_error_title">Error switching profile</string>
<string name="switching_profile_error_message">Your connection was moved to %s but an unexpected error occurred while redirecting you to the profile.</string>
<string name="or_scan_qr_code">Or scan QR code</string>
<string name="keep_unused_invitation_question">Keep unused invitation?</string>
<string name="you_can_view_invitation_link_again">You can view invitation link again in connection details.</string>
@@ -25,7 +25,6 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import java.util.Locale
import kotlin.math.roundToInt
@Composable
actual fun AppearanceView(m: ChatModel) {
@@ -44,7 +43,7 @@ fun AppearanceScope.AppearanceLayout(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.appearance_settings))
SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) {
SectionView(stringResource(MR.strings.settings_section_title_language), contentPadding = PaddingValues()) {
val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") }
LangSelector(state) {
state.value = it
@@ -79,7 +78,7 @@ fun AppearanceScope.AppearanceLayout(
@Composable
fun DensityScaleSection() {
val localDensityScale = remember { mutableStateOf(appPrefs.densityScale.get()) }
SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(60.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.0.2
android.version_code=234
android.version_name=6.0.3
android.version_code=235
desktop.version_name=6.0.2
desktop.version_code=63
desktop.version_name=6.0.3
desktop.version_code=64
kotlin.version=1.9.23
gradle.plugin.version=8.2.0
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 1d22608f860636f21a5557f1b3fab4a7da09c5cc
tag: 56986f82c89b04beae84a61208db8b55eb0098e3
source-repository-package
type: git
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."1d22608f860636f21a5557f1b3fab4a7da09c5cc" = "16kmc05avzdyd6kpj83nyqkyjks5kim5j351397f6p3yvm7iydwz";
"https://github.com/simplex-chat/simplexmq.git"."56986f82c89b04beae84a61208db8b55eb0098e3" = "0vqvdnm560xrfq7kjsghdbpk67vn4hcdpp58dfqgh9l2c9f79bin";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+5 -1
View File
@@ -1709,7 +1709,7 @@ processChatCommand' vr = \case
pure conn'
recreateConn user conn@PendingContactConnection {customUserProfileId} newUser = do
subMode <- chatReadVar subscriptionMode
(agConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOn subMode
(agConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation Nothing IKPQOn subMode
conn' <- withFastStore' $ \db -> do
deleteConnectionRecord db user connId
forM_ customUserProfileId $ \profileId ->
@@ -2906,6 +2906,7 @@ processChatCommand' vr = \case
(msgContainers, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_
msgs_ <- sendDirectContactMessages user ct $ L.map XMsgNew msgContainers
let itemsData = prepareSndItemsData msgs_ cmrs ciFiles_ quotedItems_
when (length itemsData /= length cmrs) $ logError "sendContactContentMessages: cmrs and itemsData length mismatch"
(errs, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) itemsData timed_ live
unless (null errs) $ toView $ CRChatErrors (Just user) errs
forM_ (timed_ >>= timedDeleteAt') $ \deleteAt ->
@@ -2969,6 +2970,7 @@ processChatCommand' vr = \case
(msgs_, gsr) <- sendGroupMessages user gInfo ms $ L.map XMsgNew msgContainers
let itemsData = prepareSndItemsData (L.toList msgs_) cmrs ciFiles_ quotedItems_
cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData timed_ live
when (length itemsData /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch"
createMemberSndStatuses cis_ msgs_ gsr
let (errs, cis) = partitionEithers cis_
unless (null errs) $ toView $ CRChatErrors (Just user) errs
@@ -7038,6 +7040,7 @@ batchSendConnMessagesB _user conn msgFlags msgs_ = do
delivered <- deliverMessagesB msgReqs
let msgs' = concat $ L.zipWith flattenMsgs batched' delivered
pqEnc = findLastPQEnc delivered
when (length msgs' /= length msgs_) $ logError "batchSendConnMessagesB: msgs_ and msgs' length mismatch"
pure (msgs', pqEnc)
Nothing -> pure ([], Nothing)
where
@@ -7190,6 +7193,7 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do
-- Save as pending for toPending members
let (pendingMemIds, pendingReqs) = preparePending sndMsgs_ toPending
stored <- lift $ withStoreBatch (\db -> map (bindRight $ createPendingMsg db) pendingReqs)
when (length stored /= length pendingMemIds) $ logError "sendGroupMessages_: pendingMemIds and stored length mismatch"
-- Zip for easier access to results
let sentTo = zipWith3 (\mId mReq r -> (mId, fmap (\(_, _, _, msgIds) -> msgIds) mReq, r)) sendToMemIds msgReqs delivered
pending = zipWith3 (\mId pReq r -> (mId, fmap snd pReq, r)) pendingMemIds pendingReqs stored
+3
View File
@@ -336,6 +336,9 @@ aChatItemId (AChatItem _ _ _ ci) = chatItemId' ci
aChatItemTs :: AChatItem -> UTCTime
aChatItemTs (AChatItem _ _ _ ci) = chatItemTs' ci
aChatItemDir :: AChatItem -> MsgDirection
aChatItemDir (AChatItem _ sMsgDir _ _) = toMsgDirection sMsgDir
updateFileStatus :: forall c d. ChatItem c d -> CIFileStatus d -> ChatItem c d
updateFileStatus ci@ChatItem {file} status = case file of
Just f -> ci {file = Just (f :: CIFile d) {fileStatus = status}}
+11 -4
View File
@@ -2,6 +2,7 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
@@ -120,10 +121,16 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView
CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView
CRNewChatItems u chatItems ->
concatMap
(\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item)
chatItems
CRNewChatItems u chatItems
| length chatItems > 20 ->
if
| all (\aci -> aChatItemDir aci == MDRcv) chatItems -> ttyUser u [sShow (length chatItems) <> " new messages"]
| all (\aci -> aChatItemDir aci == MDSnd) chatItems -> ttyUser u [sShow (length chatItems) <> " messages sent"]
| otherwise -> ttyUser u [sShow (length chatItems) <> " new messages created"]
| otherwise ->
concatMap
(\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item)
chatItems
CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems
CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz
CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId]
+114 -9
View File
@@ -17,6 +17,7 @@ import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.List (intercalate)
import qualified Data.Text as T
import Database.SQLite.Simple (Only (..))
import Simplex.Chat.AppSettings (defaultAppSettings)
import qualified Simplex.Chat.AppSettings as AS
import Simplex.Chat.Call
@@ -25,6 +26,7 @@ import Simplex.Chat.Options (ChatOpts (..))
import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificationCode, verificationCode, pattern VersionChat)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Util (safeDecodeUtf8)
import Simplex.Messaging.Version
@@ -52,7 +54,11 @@ chatDirectTests = do
it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact
it "should send multiline message" testMultilineMessage
it "send large message" testLargeMessage
describe "batch send messages" $ do
it "send multiple messages api" testSendMulti
it "send multiple timed messages" testSendMultiTimed
it "send multiple messages, including quote" testSendMultiWithQuote
it "send multiple messages (many chat batches)" testSendMultiManyBatches
describe "duplicate contacts" $ do
it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate
it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate
@@ -716,22 +722,27 @@ testDirectMessageDeleteMultipleManyBatches =
\alice bob -> do
connectUsers alice bob
alice #> "@bob message 0"
bob <# "alice> message 0"
msgIdFirst <- lastItemId alice
msgIdZero <- lastItemId alice
forM_ [(1 :: Int) .. 300] $ \i -> do
alice #> ("@bob message " <> show i)
bob <# ("alice> message " <> show i)
let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}"
cms = intercalate ", " (map cm [1 .. 300 :: Int])
alice `send` ("/_send @2 json [" <> cms <> "]")
_ <- getTermLine alice
alice <## "300 messages sent"
msgIdLast <- lastItemId alice
let mIdFirst = read msgIdFirst :: Int
forM_ [(1 :: Int) .. 300] $ \i -> do
bob <# ("alice> message " <> show i)
let mIdFirst = (read msgIdZero :: Int) + 1
mIdLast = read msgIdLast :: Int
deleteIds = intercalate "," (map show [mIdFirst .. mIdLast])
alice `send` ("/_delete item @2 " <> deleteIds <> " broadcast")
_ <- getTermLine alice
alice <## "301 messages deleted"
forM_ [(0 :: Int) .. 300] $ \i -> do
alice <## "300 messages deleted"
forM_ [(1 :: Int) .. 300] $ \i -> do
bob <# ("alice> [marked deleted] message " <> show i)
testDirectLiveMessage :: HasCallStack => FilePath -> IO ()
@@ -852,6 +863,100 @@ testSendMulti =
bob <# "alice> test 1"
bob <# "alice> test 2"
testSendMultiTimed :: HasCallStack => FilePath -> IO ()
testSendMultiTimed =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice ##> "/_send @2 ttl=1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "@bob test 1"
alice <# "@bob test 2"
bob <# "alice> test 1"
bob <# "alice> test 2"
alice
<### [ "timed message deleted: test 1",
"timed message deleted: test 2"
]
bob
<### [ "timed message deleted: test 1",
"timed message deleted: test 2"
]
testSendMultiWithQuote :: HasCallStack => FilePath -> IO ()
testSendMultiWithQuote =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice #> "@bob hello"
bob <# "alice> hello"
msgId1 <- lastItemId alice
threadDelay 1000000
bob #> "@alice hi"
alice <# "bob> hi"
msgId2 <- lastItemId alice
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message 1\"}}"
cm2 = "{\"quotedItemId\": " <> msgId1 <> ", \"msgContent\": {\"type\": \"text\", \"text\": \"message 2\"}}"
cm3 = "{\"quotedItemId\": " <> msgId2 <> ", \"msgContent\": {\"type\": \"text\", \"text\": \"message 3\"}}"
alice ##> ("/_send @2 json [" <> cm1 <> ", " <> cm2 <> ", " <> cm3 <> "]")
alice <## "bad chat command: invalid multi send: live and more than one quote not supported"
alice ##> ("/_send @2 json [" <> cm1 <> ", " <> cm2 <> "]")
alice <# "@bob message 1"
alice <# "@bob >> hello"
alice <## " message 2"
bob <# "alice> message 1"
bob <# "alice> >> hello"
bob <## " message 2"
alice ##> ("/_send @2 json [" <> cm3 <> ", " <> cm1 <> "]")
alice <# "@bob > hi"
alice <## " message 3"
alice <# "@bob message 1"
bob <# "alice> > hi"
bob <## " message 3"
bob <# "alice> message 1"
testSendMultiManyBatches :: HasCallStack => FilePath -> IO ()
testSendMultiManyBatches =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
threadDelay 1000000
msgIdAlice <- lastItemId alice
msgIdBob <- lastItemId bob
let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}"
cms = intercalate ", " (map cm [1 .. 300 :: Int])
alice `send` ("/_send @2 json [" <> cms <> "]")
_ <- getTermLine alice
alice <## "300 messages sent"
forM_ [(1 :: Int) .. 300] $ \i ->
bob <# ("alice> message " <> show i)
aliceItemsCount <- withCCTransaction alice $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]]
aliceItemsCount `shouldBe` [[300]]
bobItemsCount <- withCCTransaction bob $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]]
bobItemsCount `shouldBe` [[300]]
testGetSetSMPServers :: HasCallStack => FilePath -> IO ()
testGetSetSMPServers =
testChat2 aliceProfile bobProfile $
+163
View File
@@ -36,6 +36,9 @@ chatFileTests = do
it "send and receive image with text and quote" testSendImageWithTextAndQuote
it "send and receive image to group" testGroupSendImage
it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote
describe "batch send messages with files" $ do
it "with files folder: send multiple files to contact" testSendMultiFilesDirect
it "with files folder: send multiple files to group" testSendMultiFilesGroup
describe "file transfer over XFTP" $ do
it "round file description count" $ const testXFTPRoundFDCount
it "send and receive file" testXFTPFileTransfer
@@ -406,6 +409,166 @@ testGroupSendImageWithTextAndQuote =
cath #$> ("/_get chat #1 count=2", chat'', [((0, "hi team"), Nothing, Nothing), ((0, "hey bob"), Just (0, "hi team"), Just "./tests/tmp/test_1.jpg")])
cath @@@ [("#team", "hey bob"), ("@alice", "received invitation to join group team as admin")]
testSendMultiFilesDirect :: HasCallStack => FilePath -> IO ()
testSendMultiFilesDirect =
testChat2 aliceProfile bobProfile $ \alice bob -> do
withXFTPServer $ do
connectUsers alice bob
alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok")
copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg"
copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf"
bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok")
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}"
cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}"
cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}"
alice ##> ("/_send @2 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]")
alice <# "@bob message without file"
alice <# "@bob sending file 1"
alice <# "/f @bob test.jpg"
alice <## "use /fc 1 to cancel sending"
alice <# "@bob sending file 2"
alice <# "/f @bob test.pdf"
alice <## "use /fc 2 to cancel sending"
bob <# "alice> message without file"
bob <# "alice> sending file 1"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob <# "alice> sending file 2"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
alice <## "completed uploading file 1 (test.jpg) for bob"
alice <## "completed uploading file 2 (test.pdf) for bob"
bob ##> "/fr 1"
bob
<### [ "saving file 1 from alice to test.jpg",
"started receiving file 1 (test.jpg) from alice"
]
bob <## "completed receiving file 1 (test.jpg) from alice"
bob ##> "/fr 2"
bob
<### [ "saving file 2 from alice to test.pdf",
"started receiving file 2 (test.pdf) from alice"
]
bob <## "completed receiving file 2 (test.pdf) from alice"
src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg"
dest1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg"
dest1 `shouldBe` src1
src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf"
dest2 <- B.readFile "./tests/tmp/bob_app_files/test.pdf"
dest2 `shouldBe` src2
alice #$> ("/_get chat @2 count=3", chatF, [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")])
bob #$> ("/_get chat @2 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")])
testSendMultiFilesGroup :: HasCallStack => FilePath -> IO ()
testSendMultiFilesGroup =
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
withXFTPServer $ do
createGroup3 "team" alice bob cath
threadDelay 1000000
alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok")
copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg"
copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf"
bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok")
cath #$> ("/_files_folder ./tests/tmp/cath_app_files", id, "ok")
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}"
cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}"
cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}"
alice ##> ("/_send #1 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]")
alice <# "#team message without file"
alice <# "#team sending file 1"
alice <# "/f #team test.jpg"
alice <## "use /fc 1 to cancel sending"
alice <# "#team sending file 2"
alice <# "/f #team test.pdf"
alice <## "use /fc 2 to cancel sending"
bob <# "#team alice> message without file"
bob <# "#team alice> sending file 1"
bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob <# "#team alice> sending file 2"
bob <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
cath <# "#team alice> message without file"
cath <# "#team alice> sending file 1"
cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
cath <# "#team alice> sending file 2"
cath <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
cath <## "use /fr 2 [<dir>/ | <path>] to receive it"
alice <## "completed uploading file 1 (test.jpg) for #team"
alice <## "completed uploading file 2 (test.pdf) for #team"
bob ##> "/fr 1"
bob
<### [ "saving file 1 from alice to test.jpg",
"started receiving file 1 (test.jpg) from alice"
]
bob <## "completed receiving file 1 (test.jpg) from alice"
bob ##> "/fr 2"
bob
<### [ "saving file 2 from alice to test.pdf",
"started receiving file 2 (test.pdf) from alice"
]
bob <## "completed receiving file 2 (test.pdf) from alice"
cath ##> "/fr 1"
cath
<### [ "saving file 1 from alice to test.jpg",
"started receiving file 1 (test.jpg) from alice"
]
cath <## "completed receiving file 1 (test.jpg) from alice"
cath ##> "/fr 2"
cath
<### [ "saving file 2 from alice to test.pdf",
"started receiving file 2 (test.pdf) from alice"
]
cath <## "completed receiving file 2 (test.pdf) from alice"
src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg"
dest1_1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg"
dest1_2 <- B.readFile "./tests/tmp/cath_app_files/test.jpg"
dest1_1 `shouldBe` src1
dest1_2 `shouldBe` src1
src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf"
dest2_1 <- B.readFile "./tests/tmp/bob_app_files/test.pdf"
dest2_2 <- B.readFile "./tests/tmp/cath_app_files/test.pdf"
dest2_1 `shouldBe` src2
dest2_2 `shouldBe` src2
alice #$> ("/_get chat #1 count=3", chatF, [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")])
bob #$> ("/_get chat #1 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")])
cath #$> ("/_get chat #1 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")])
testXFTPRoundFDCount :: Expectation
testXFTPRoundFDCount = do
roundedFDCount (-100) `shouldBe` 4
+187
View File
@@ -35,6 +35,8 @@ chatForwardTests = do
it "with relative paths: from notes to group" testForwardFileNotesToGroup
describe "multi forward api" $ do
it "from contact to contact" testForwardContactToContactMulti
it "from group to group" testForwardGroupToGroupMulti
it "with relative paths: multiple files from contact to contact" testMultiForwardFiles
testForwardContactToContact :: HasCallStack => FilePath -> IO ()
testForwardContactToContact =
@@ -620,3 +622,188 @@ testForwardContactToContactMulti =
cath <## " hi"
cath <# "alice> -> forwarded"
cath <## " hey"
testForwardGroupToGroupMulti :: HasCallStack => FilePath -> IO ()
testForwardGroupToGroupMulti =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice bob
createGroup2 "club" alice cath
threadDelay 1000000
alice #> "#team hi"
bob <# "#team alice> hi"
msgId1 <- lastItemId alice
threadDelay 1000000
bob #> "#team hey"
alice <# "#team bob> hey"
msgId2 <- lastItemId alice
alice ##> ("/_forward #2 #1 " <> msgId1 <> "," <> msgId2)
alice <# "#club <- you #team"
alice <## " hi"
alice <# "#club <- #team"
alice <## " hey"
cath <# "#club alice> -> forwarded"
cath <## " hi"
cath <# "#club alice> -> forwarded"
cath <## " hey"
-- read chat
alice ##> "/tail #club 2"
alice <# "#club <- you #team"
alice <## " hi"
alice <# "#club <- #team"
alice <## " hey"
cath ##> "/tail #club 2"
cath <# "#club alice> -> forwarded"
cath <## " hi"
cath <# "#club alice> -> forwarded"
cath <## " hey"
testMultiForwardFiles :: HasCallStack => FilePath -> IO ()
testMultiForwardFiles =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
setRelativePaths alice "./tests/tmp/alice_app_files" "./tests/tmp/alice_xftp"
copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg"
copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf"
setRelativePaths bob "./tests/tmp/bob_app_files" "./tests/tmp/bob_xftp"
setRelativePaths cath "./tests/tmp/cath_app_files" "./tests/tmp/cath_xftp"
connectUsers alice bob
connectUsers bob cath
threadDelay 1000000
msgIdZero <- lastItemId bob
bob #> "@alice hi"
alice <# "bob> hi"
-- send original files
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}"
cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}"
cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}"
alice ##> ("/_send @2 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]")
alice <# "@bob message without file"
alice <# "@bob sending file 1"
alice <# "/f @bob test.jpg"
alice <## "use /fc 1 to cancel sending"
alice <# "@bob sending file 2"
alice <# "/f @bob test.pdf"
alice <## "use /fc 2 to cancel sending"
bob <# "alice> message without file"
bob <# "alice> sending file 1"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob <# "alice> sending file 2"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
alice <## "completed uploading file 1 (test.jpg) for bob"
alice <## "completed uploading file 2 (test.pdf) for bob"
bob ##> "/fr 1"
bob
<### [ "saving file 1 from alice to test.jpg",
"started receiving file 1 (test.jpg) from alice"
]
bob <## "completed receiving file 1 (test.jpg) from alice"
bob ##> "/fr 2"
bob
<### [ "saving file 2 from alice to test.pdf",
"started receiving file 2 (test.pdf) from alice"
]
bob <## "completed receiving file 2 (test.pdf) from alice"
src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg"
dest1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg"
dest1 `shouldBe` src1
src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf"
dest2 <- B.readFile "./tests/tmp/bob_app_files/test.pdf"
dest2 `shouldBe` src2
-- forward file
let msgId1 = (read msgIdZero :: Int) + 1
bob ##> ("/_forward @3 @2 " <> show msgId1 <> "," <> show (msgId1 + 1) <> "," <> show (msgId1 + 2) <> "," <> show (msgId1 + 3))
-- messages printed for bob
bob <# "@cath <- you @alice"
bob <## " hi"
bob <# "@cath <- @alice"
bob <## " message without file"
bob <# "@cath <- @alice"
bob <## " sending file 1"
bob <# "/f @cath test_1.jpg"
bob <## "use /fc 3 to cancel sending"
bob <# "@cath <- @alice"
bob <## " sending file 2"
bob <# "/f @cath test_1.pdf"
bob <## "use /fc 4 to cancel sending"
-- messages printed for cath
cath <# "bob> -> forwarded"
cath <## " hi"
cath <# "bob> -> forwarded"
cath <## " message without file"
cath <# "bob> -> forwarded"
cath <## " sending file 1"
cath <# "bob> sends file test_1.jpg (136.5 KiB / 139737 bytes)"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
cath <# "bob> -> forwarded"
cath <## " sending file 2"
cath <# "bob> sends file test_1.pdf (266.0 KiB / 272376 bytes)"
cath <## "use /fr 2 [<dir>/ | <path>] to receive it"
-- file transfer
bob <## "completed uploading file 3 (test_1.jpg) for cath"
bob <## "completed uploading file 4 (test_1.pdf) for cath"
cath ##> "/fr 1"
cath
<### [ "saving file 1 from bob to test_1.jpg",
"started receiving file 1 (test_1.jpg) from bob"
]
cath <## "completed receiving file 1 (test_1.jpg) from bob"
cath ##> "/fr 2"
cath
<### [ "saving file 2 from bob to test_1.pdf",
"started receiving file 2 (test_1.pdf) from bob"
]
cath <## "completed receiving file 2 (test_1.pdf) from bob"
src1B <- B.readFile "./tests/tmp/bob_app_files/test_1.jpg"
src1B `shouldBe` dest1
dest1C <- B.readFile "./tests/tmp/cath_app_files/test_1.jpg"
dest1C `shouldBe` src1B
src2B <- B.readFile "./tests/tmp/bob_app_files/test_1.pdf"
src2B `shouldBe` dest2
dest2C <- B.readFile "./tests/tmp/cath_app_files/test_1.pdf"
dest2C `shouldBe` src2B
-- deleting original file doesn't delete forwarded file
checkActionDeletesFile "./tests/tmp/bob_app_files/test.jpg" $ do
bob ##> "/clear alice"
bob <## "alice: all messages are removed locally ONLY"
fwdFileExists <- doesFileExist "./tests/tmp/bob_app_files/test_1.jpg"
fwdFileExists `shouldBe` True
+93 -12
View File
@@ -14,6 +14,7 @@ import Control.Monad (forM_, void, when)
import qualified Data.ByteString.Char8 as B
import Data.List (intercalate, isInfixOf)
import qualified Data.Text as T
import Database.SQLite.Simple (Only (..))
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Options
import Simplex.Chat.Protocol (supportedChatVRange)
@@ -64,7 +65,10 @@ chatGroupTests = do
it "moderate message of another group member (full delete)" testGroupModerateFullDelete
it "moderate message that arrives after the event of moderation" testGroupDelayedModeration
it "moderate message that arrives after the event of moderation (full delete)" testGroupDelayedModerationFullDelete
describe "batch send messages" $ do
it "send multiple messages api" testSendMulti
it "send multiple timed messages" testSendMultiTimed
it "send multiple messages (many chat batches)" testSendMultiManyBatches
describe "async group connections" $ do
xit "create and join group when clients go offline" testGroupAsync
describe "group links" $ do
@@ -1305,26 +1309,29 @@ testGroupMessageDeleteMultipleManyBatches =
cath ##> "/set receipts all off"
cath <## "ok"
alice #> "#team message 0"
concurrently_
(bob <# "#team alice> message 0")
(cath <# "#team alice> message 0")
msgIdFirst <- lastItemId alice
msgIdZero <- lastItemId alice
let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}"
cms = intercalate ", " (map cm [1 .. 300 :: Int])
alice `send` ("/_send #1 json [" <> cms <> "]")
_ <- getTermLine alice
alice <## "300 messages sent"
forM_ [(1 :: Int) .. 300] $ \i -> do
alice #> ("#team message " <> show i)
concurrently_
(bob <# ("#team alice> message " <> show i))
(cath <# ("#team alice> message " <> show i))
msgIdLast <- lastItemId alice
let mIdFirst = read msgIdFirst :: Int
let mIdFirst = (read msgIdZero :: Int) + 1
mIdLast = read msgIdLast :: Int
deleteIds = intercalate "," (map show [mIdFirst .. mIdLast])
alice `send` ("/_delete item #1 " <> deleteIds <> " broadcast")
_ <- getTermLine alice
alice <## "301 messages deleted"
forM_ [(0 :: Int) .. 300] $ \i ->
alice <## "300 messages deleted"
forM_ [(1 :: Int) .. 300] $ \i ->
concurrently_
(bob <# ("#team alice> [marked deleted] message " <> show i))
(cath <# ("#team alice> [marked deleted] message " <> show i))
@@ -1821,15 +1828,89 @@ testGroupDelayedModerationFullDelete tmp = do
testSendMulti :: HasCallStack => FilePath -> IO ()
testSendMulti =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "#team test 1"
alice <# "#team test 2"
bob <# "#team alice> test 1"
bob <# "#team alice> test 2"
cath <# "#team alice> test 1"
cath <# "#team alice> test 2"
testSendMultiTimed :: HasCallStack => FilePath -> IO ()
testSendMultiTimed =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice ##> "/set disappear #team on 1"
alice <## "updated group preferences:"
alice <## "Disappearing messages: on (1 sec)"
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "Disappearing messages: on (1 sec)"
cath <## "alice updated group #team:"
cath <## "updated group preferences:"
cath <## "Disappearing messages: on (1 sec)"
alice ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "#team test 1"
alice <# "#team test 2"
bob <# "#team alice> test 1"
bob <# "#team alice> test 2"
cath <# "#team alice> test 1"
cath <# "#team alice> test 2"
alice
<### [ "timed message deleted: test 1",
"timed message deleted: test 2"
]
bob
<### [ "timed message deleted: test 1",
"timed message deleted: test 2"
]
cath
<### [ "timed message deleted: test 1",
"timed message deleted: test 2"
]
testSendMultiManyBatches :: HasCallStack => FilePath -> IO ()
testSendMultiManyBatches =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
msgIdAlice <- lastItemId alice
msgIdBob <- lastItemId bob
msgIdCath <- lastItemId cath
let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}"
cms = intercalate ", " (map cm [1 .. 300 :: Int])
alice `send` ("/_send #1 json [" <> cms <> "]")
_ <- getTermLine alice
alice <## "300 messages sent"
forM_ [(1 :: Int) .. 300] $ \i -> do
concurrently_
(bob <# ("#team alice> message " <> show i))
(cath <# ("#team alice> message " <> show i))
aliceItemsCount <- withCCTransaction alice $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]]
aliceItemsCount `shouldBe` [[300]]
bobItemsCount <- withCCTransaction bob $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]]
bobItemsCount `shouldBe` [[300]]
cathItemsCount <- withCCTransaction cath $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdCath) :: IO [[Int]]
cathItemsCount `shouldBe` [[300]]
testGroupAsync :: HasCallStack => FilePath -> IO ()
testGroupAsync tmp = do
+36 -9
View File
@@ -17,12 +17,14 @@ chatLocalChatsTests :: SpecWith FilePath
chatLocalChatsTests = do
describe "note folders" $ do
it "create folders, add notes, read, search" testNotes
it "create multiple messages api" testCreateMulti
it "switch users" testUserNotes
it "preview pagination for notes" testPreviewsPagination
it "chat pagination" testChatPagination
it "stores files" testFiles
it "deleting files does not interfere with other chat types" testOtherFiles
describe "batch create messages" $ do
it "create multiple messages api" testCreateMulti
it "create multiple messages with files" testCreateMultiFiles
testNotes :: FilePath -> IO ()
testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
@@ -53,14 +55,6 @@ testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
alice ##> "/tail *"
alice <# "* Greetings."
testCreateMulti :: FilePath -> IO ()
testCreateMulti tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
createCCNoteFolder alice
alice ##> "/_create *1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "* test 1"
alice <# "* test 2"
testUserNotes :: FilePath -> IO ()
testUserNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
createCCNoteFolder alice
@@ -197,3 +191,36 @@ testOtherFiles =
doesFileExist "./tests/tmp/test.jpg" `shouldReturn` True
where
cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}}
testCreateMulti :: FilePath -> IO ()
testCreateMulti tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
createCCNoteFolder alice
alice ##> "/_create *1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "* test 1"
alice <# "* test 2"
testCreateMultiFiles :: FilePath -> IO ()
testCreateMultiFiles tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
createCCNoteFolder alice
alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok")
copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg"
copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf"
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}"
cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}"
cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}"
alice ##> ("/_create *1 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]")
alice <# "* message without file"
alice <# "* sending file 1"
alice <# "* file 1 (test.jpg)"
alice <# "* sending file 2"
alice <# "* file 2 (test.pdf)"
doesFileExist "./tests/tmp/alice_app_files/test.jpg" `shouldReturn` True
doesFileExist "./tests/tmp/alice_app_files/test.pdf" `shouldReturn` True
alice ##> "/_get chat *1 count=3"
r <- chatF <$> getTermLine alice
r `shouldBe` [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")]
+39 -28
View File
@@ -1,6 +1,7 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-}
{-# LANGUAGE TypeApplications #-}
module ChatTests.Profiles where
@@ -18,6 +19,8 @@ import Simplex.Chat.Types (ConnStatus (..), Profile (..))
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import Simplex.Messaging.Server.Env.STM hiding (subscriptions)
import Simplex.Messaging.Transport
import Simplex.Messaging.Util (encodeJSON)
import System.Directory (copyFile, createDirectoryIfMissing)
import Test.Hspec hiding (it)
@@ -1653,34 +1656,42 @@ testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $
]
testChangePCCUserDiffSrv :: HasCallStack => FilePath -> IO ()
testChangePCCUserDiffSrv = testChat2 aliceProfile bobProfile $
\alice bob -> do
-- Create a new invite
alice ##> "/connect"
_ <- getInvitation alice
alice ##> "/_set incognito :1 on"
alice <## "connection 1 changed to incognito"
-- Create new user with different servers
alice ##> "/create user alisa"
showActiveUser alice "alisa"
alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok")
alice ##> "/user alice"
showActiveUser alice "alice (Alice)"
-- Change connection to newly created user and use the newly created connection
alice ##> "/_set conn user :1 2"
alice <## "connection 1 changed from user alice to user alisa, new link:"
alice <## ""
inv <- getTermLine alice
alice <## ""
alice `hasContactProfiles` ["alice"]
alice ##> "/user alisa"
showActiveUser alice "alisa"
-- Connect
bob ##> ("/connect " <> inv)
bob <## "confirmation sent!"
concurrently_
(alice <## "bob (Bob): contact is connected")
(bob <## "alisa: contact is connected")
testChangePCCUserDiffSrv tmp = do
withSmpServer' serverCfg' $ do
withNewTestChatCfgOpts tmp testCfg testOpts "alice" aliceProfile $ \alice -> do
withNewTestChatCfgOpts tmp testCfg testOpts "bob" bobProfile $ \bob -> do
-- Create a new invite
alice ##> "/connect"
_ <- getInvitation alice
alice ##> "/_set incognito :1 on"
alice <## "connection 1 changed to incognito"
-- Create new user with different servers
alice ##> "/create user alisa"
showActiveUser alice "alisa"
alice #$> ("/smp smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003", id, "ok")
alice ##> "/user alice"
showActiveUser alice "alice (Alice)"
-- Change connection to newly created user and use the newly created connection
alice ##> "/_set conn user :1 2"
alice <## "connection 1 changed from user alice to user alisa, new link:"
alice <## ""
inv <- getTermLine alice
alice <## ""
alice `hasContactProfiles` ["alice"]
alice ##> "/user alisa"
showActiveUser alice "alisa"
-- Connect
bob ##> ("/connect " <> inv)
bob <## "confirmation sent!"
concurrently_
(alice <## "bob (Bob): contact is connected")
(bob <## "alisa: contact is connected")
where
serverCfg' =
smpServerCfg
{ transports = [("7003", transport @TLS), ("7002", transport @TLS)],
msgQueueQuota = 2
}
testSetConnectionAlias :: HasCallStack => FilePath -> IO ()
testSetConnectionAlias = testChat2 aliceProfile bobProfile $