diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 7dae94dfcd..3d5f238122 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -354,6 +354,9 @@ final class ChatModel: ObservableObject { addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) res = true } + if cItem.isDeletedContent || cItem.meta.itemDeleted != nil { + VoiceItemState.stopVoiceInChatView(cInfo, cItem) + } // update current chat return chatId == cInfo.id ? _upsertChatItem(cInfo, cItem) : res } @@ -420,6 +423,7 @@ final class ChatModel: ObservableObject { } } } + VoiceItemState.stopVoiceInChatView(cInfo, cItem) } func nextChatItemData(_ chatItemId: Int64, previous: Bool, map: @escaping (ChatItem) -> T?) -> T? { @@ -774,7 +778,7 @@ struct UnreadChatItemCounts: Equatable { var unreadBelow: Int } -final class Chat: ObservableObject, Identifiable { +final class Chat: ObservableObject, Identifiable, ChatLike { @Published var chatInfo: ChatInfo @Published var chatItems: [ChatItem] @Published var chatStats: ChatStats diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index dd0610e48c..fb727b494e 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -217,7 +217,7 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn } func apiStartChat(ctrl: chat_ctrl? = nil) throws -> Bool { - let r = chatSendCmdSync(.startChat(mainApp: true), ctrl) + let r = chatSendCmdSync(.startChat(mainApp: true, enableSndFiles: true), ctrl) switch r { case .chatStarted: return true case .chatRunning: return false @@ -397,7 +397,7 @@ private func sendMessageErrorAlert(_ r: ChatResponse) { logger.error("send message error: \(String(describing: r))") AlertManager.shared.showAlertMsg( title: "Error sending message", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r))" ) } @@ -405,7 +405,7 @@ private func createChatItemErrorAlert(_ r: ChatResponse) { logger.error("apiCreateChatItem error: \(String(describing: r))") AlertManager.shared.showAlertMsg( title: "Error creating message", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r))" ) } @@ -699,7 +699,7 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi message: "Please check that you used the correct link or ask your contact to send you another one." ) return (nil, alert) - case .chatCmdError(_, .errorAgent(.SMP(.AUTH))): + case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))): let alert = mkAlert( title: "Connection error (AUTH)", message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." @@ -732,7 +732,7 @@ private func connectionErrorAlert(_ r: ChatResponse) -> Alert { } else { return mkAlert( title: "Connection error", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r))" ) } } @@ -898,7 +898,7 @@ func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Cont let am = AlertManager.shared if case let .acceptingContactRequest(_, contact) = r { return contact } - if case .chatCmdError(_, .errorAgent(.SMP(.AUTH))) = r { + if case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))) = r { am.showAlertMsg( title: "Connection error (AUTH)", message: "Sender may have deleted the connection request." @@ -909,7 +909,7 @@ func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Cont logger.error("apiAcceptContactRequest error: \(String(describing: r))") am.showAlertMsg( title: "Error accepting contact request", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r))" ) } return nil @@ -935,7 +935,7 @@ func uploadStandaloneFile(user: any UserLike, file: CryptoFile, ctrl: chat_ctrl? return (fileTransferMeta, nil) } else { logger.error("uploadStandaloneFile error: \(String(describing: r))") - return (nil, String(describing: r)) + return (nil, responseError(r)) } } @@ -945,7 +945,7 @@ func downloadStandaloneFile(user: any UserLike, url: String, file: CryptoFile, c return (rcvFileTransfer, nil) } else { logger.error("downloadStandaloneFile error: \(String(describing: r))") - return (nil, String(describing: r)) + return (nil, responseError(r)) } } @@ -1025,7 +1025,7 @@ func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, in if !auto { am.showAlertMsg( title: "Error receiving file", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r))" ) } } @@ -1091,47 +1091,6 @@ func deleteRemoteCtrl(_ rcId: Int64) async throws { try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId)) } -struct ErrorAlert { - var title: LocalizedStringKey - var message: LocalizedStringKey -} - -func getNetworkErrorAlert(_ r: ChatResponse) -> ErrorAlert? { - switch r { - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))): - return ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))): - return ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.") - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .HOST))): - return ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).") - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TRANSPORT(.version)))): - return ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).") - case let .chatCmdError(_, .errorAgent(.SMP(.PROXY(proxyErr)))): - return proxyErrorAlert(proxyErr) - case let .chatCmdError(_, .errorAgent(.PROXY(_, _, .protocolError(.PROXY(proxyErr))))): - return proxyErrorAlert(proxyErr) - default: - return nil - } -} - -private func proxyErrorAlert(_ proxyErr: ProxyError) -> ErrorAlert? { - switch proxyErr { - case .BROKER(brokerErr: .TIMEOUT): - return ErrorAlert(title: "Private routing error", message: "Please try later.") - case .BROKER(brokerErr: .NETWORK): - return ErrorAlert(title: "Private routing error", message: "Please try later.") - case .NO_SESSION: - return ErrorAlert(title: "Private routing error", message: "Please try later.") - case .BROKER(brokerErr: .HOST): - return ErrorAlert(title: "Private routing error", message: "Server address is incompatible with network settings.") - case .BROKER(brokerErr: .TRANSPORT(.version)): - return ErrorAlert(title: "Private routing error", message: "Server version is incompatible with network settings.") - default: - return nil - } -} - func networkErrorAlert(_ r: ChatResponse) -> Alert? { if let alert = getNetworkErrorAlert(r) { return mkAlert(title: alert.title, message: alert.message) @@ -1143,7 +1102,10 @@ func networkErrorAlert(_ r: ChatResponse) -> Alert? { func acceptContactRequest(incognito: Bool, contactRequest: UserContactRequest) async { if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequest.apiId) { let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: []) - DispatchQueue.main.async { ChatModel.shared.replaceChat(contactRequest.id, chat) } + DispatchQueue.main.async { + ChatModel.shared.replaceChat(contactRequest.id, chat) + ChatModel.shared.setContactNetworkStatus(contact, .connected) + } } } @@ -1280,7 +1242,7 @@ func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult { let r = await chatSendCmd(.apiJoinGroup(groupId: groupId)) switch r { case let .userAcceptedGroupSent(_, groupInfo, _): return .joined(groupInfo: groupInfo) - case .chatCmdError(_, .errorAgent(.SMP(.AUTH))): return .invitationRemoved + case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))): return .invitationRemoved case .chatCmdError(_, .errorStore(.groupNotFound)): return .groupNotFound default: throw r } @@ -1386,6 +1348,14 @@ func apiGetVersion() throws -> CoreVersionInfo { throw r } +func getAgentSubsTotal() throws -> (SMPServerSubs, Bool) { + let userId = try currentUserId("getAgentSubsTotal") + let r = chatSendCmdSync(.getAgentSubsTotal(userId: userId), log: false) + if case let .agentSubsTotal(_, subsTotal, hasSession) = r { return (subsTotal, hasSession) } + logger.error("getAgentSubsTotal error: \(String(describing: r))") + throw r +} + func getAgentServersSummary() throws -> PresentedServersSummary { let userId = try currentUserId("getAgentServersSummary") let r = chatSendCmdSync(.getAgentServersSummary(userId: userId), log: false) @@ -1649,6 +1619,19 @@ func processReceivedMsg(_ res: ChatResponse) async { } } } + case let .contactSndReady(user, contact): + if active(user) && contact.directOrUsed { + await MainActor.run { + m.updateContact(contact) + if let conn = contact.activeConn { + m.dismissConnReqView(conn.id) + m.removeChat(conn.id) + } + } + } + await MainActor.run { + m.setContactNetworkStatus(contact, .connected) + } case let .receivedContactRequest(user, contactRequest): if active(user) { let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest) diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift index 4494adc0e8..92bcdcac53 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -36,6 +36,18 @@ private func _suspendChat(timeout: Int) { } } +let seSubscriber = seMessageSubscriber { + switch $0 { + case let .state(state): + switch state { + case .inactive: + if AppChatState.shared.value.inactive { activateChat() } + case .sendingMessage: + if AppChatState.shared.value.canSuspend { suspendChat() } + } + } +} + func suspendChat() { suspendLockQueue.sync { _suspendChat(timeout: appSuspendTimeout) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index ed589ec083..4d45db4700 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -112,7 +112,7 @@ struct ChatInfoView: View { case abortSwitchAddressAlert case syncConnectionForceAlert case queueInfo(info: String) - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -155,23 +155,25 @@ struct ChatInfoView: View { } Section { - if let code = connectionCode { verifyCodeButton(code) } - contactPreferencesButton() - sendReceiptsOption() - if let connStats = connectionStats, - connStats.ratchetSyncAllowed { - synchronizeConnectionButton() + Group { + if let code = connectionCode { verifyCodeButton(code) } + contactPreferencesButton() + sendReceiptsOption() + if let connStats = connectionStats, + connStats.ratchetSyncAllowed { + synchronizeConnectionButton() + } + // } else if developerTools { + // synchronizeConnectionButtonForce() + // } } + .disabled(!contact.ready || !contact.active) NavigationLink { ChatWallpaperEditorSheet(chat: chat) } label: { Label("Chat theme", systemImage: "photo") } -// } else if developerTools { -// synchronizeConnectionButtonForce() -// } } - .disabled(!contact.ready || !contact.active) if let conn = contact.activeConn { Section { @@ -271,7 +273,7 @@ struct ChatInfoView: View { } } .actionSheet(isPresented: $showDeleteContactActionSheet) { - if contact.ready && contact.active { + if contact.sndReady && contact.active { return ActionSheet( title: Text("Delete contact?\nThis cannot be undone!"), buttons: [ diff --git a/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift index bcdeb7fd9c..30f5e7a589 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift @@ -9,6 +9,7 @@ import SwiftUI class AnimatedImageView: UIView { var image: UIImage? = nil var imageView: UIImageView? = nil + var cMode: UIView.ContentMode = .scaleAspectFit override init(frame: CGRect) { super.init(frame: frame) @@ -18,11 +19,12 @@ class AnimatedImageView: UIView { fatalError("Not implemented") } - convenience init(image: UIImage) { + convenience init(image: UIImage, contentMode: UIView.ContentMode) { self.init() self.image = image + self.cMode = contentMode imageView = UIImageView(gifImage: image) - imageView!.contentMode = .scaleAspectFit + imageView!.contentMode = contentMode self.addSubview(imageView!) } @@ -35,7 +37,7 @@ class AnimatedImageView: UIView { if let subview = self.subviews.first as? UIImageView { if image.imageData != subview.gifImage?.imageData { imageView = UIImageView(gifImage: image) - imageView!.contentMode = .scaleAspectFit + imageView!.contentMode = contentMode self.addSubview(imageView!) subview.removeFromSuperview() } @@ -47,13 +49,15 @@ class AnimatedImageView: UIView { struct SwiftyGif: UIViewRepresentable { private let image: UIImage + private let contentMode: UIView.ContentMode - init(image: UIImage) { + init(image: UIImage, contentMode: UIView.ContentMode = .scaleAspectFit) { self.image = image + self.contentMode = contentMode } func makeUIView(context: Context) -> AnimatedImageView { - AnimatedImageView(image: image) + AnimatedImageView(image: image, contentMode: contentMode) } func updateUIView(_ imageView: AnimatedImageView, context: Context) { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 414da5371a..57123d74ba 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -14,39 +14,45 @@ struct CIFileView: View { @EnvironmentObject var theme: AppTheme let file: CIFile? let edited: Bool + var smallView: Bool = false var body: some View { - let metaReserve = edited - ? " " - : " " - Button(action: fileAction) { - HStack(alignment: .bottom, spacing: 6) { - fileIndicator() - .padding(.top, 5) - .padding(.bottom, 3) - if let file = file { - let prettyFileSize = ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary) - VStack(alignment: .leading, spacing: 2) { - Text(file.fileName) - .lineLimit(1) - .multilineTextAlignment(.leading) - .foregroundColor(theme.colors.onBackground) - Text(prettyFileSize + metaReserve) - .font(.caption) - .lineLimit(1) - .multilineTextAlignment(.leading) - .foregroundColor(theme.colors.secondary) + if smallView { + fileIndicator() + .onTapGesture(perform: fileAction) + } else { + let metaReserve = edited + ? " " + : " " + Button(action: fileAction) { + HStack(alignment: .bottom, spacing: 6) { + fileIndicator() + .padding(.top, 5) + .padding(.bottom, 3) + if let file = file { + let prettyFileSize = ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary) + VStack(alignment: .leading, spacing: 2) { + Text(file.fileName) + .lineLimit(1) + .multilineTextAlignment(.leading) + .foregroundColor(theme.colors.onBackground) + Text(prettyFileSize + metaReserve) + .font(.caption) + .lineLimit(1) + .multilineTextAlignment(.leading) + .foregroundColor(theme.colors.secondary) + } + } else { + Text(metaReserve) } - } else { - Text(metaReserve) } + .padding(.top, 4) + .padding(.bottom, 6) + .padding(.leading, 10) + .padding(.trailing, 12) } - .padding(.top, 4) - .padding(.bottom, 6) - .padding(.leading, 10) - .padding(.trailing, 12) + .disabled(!itemInteractive) } - .disabled(!itemInteractive) } private var itemInteractive: Bool { @@ -199,17 +205,17 @@ struct CIFileView: View { Image(systemName: icon) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) + .frame(width: smallView ? 36 : 30, height: smallView ? 36 : 30) .foregroundColor(color) if let innerIcon = innerIcon, - let innerIconSize = innerIconSize { + let innerIconSize = innerIconSize, (!smallView || file?.showStatusIconInSmallView == true) { Image(systemName: innerIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(maxHeight: 16) .frame(width: innerIconSize, height: innerIconSize) .foregroundColor(.white) - .padding(.top, 12) + .padding(.top, smallView ? 15 : 12) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index 9c12653343..3966d7e258 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -15,22 +15,33 @@ struct CIImageView: View { var preview: UIImage? let maxWidth: CGFloat var imgWidth: CGFloat? - @State private var showFullScreenImage = false + var smallView: Bool = false + @Binding var showFullScreenImage: Bool + @State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0 var body: some View { let file = chatItem.file VStack(alignment: .center, spacing: 6) { if let uiImage = getLoadedImage(file) { - imageView(uiImage) + Group { if smallView { smallViewImageView(uiImage) } else { imageView(uiImage) } } .fullScreenCover(isPresented: $showFullScreenImage) { FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage) } + .if(!smallView) { view in + view.modifier(PrivacyBlur(blurred: $blurred)) + } .onTapGesture { showFullScreenImage = true } .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenImage = false } } else if let preview { - imageView(preview) + Group { + if smallView { + smallViewImageView(preview) + } else { + imageView(preview).modifier(PrivacyBlur(blurred: $blurred)) + } + } .onTapGesture { if let file = file { switch file.fileStatus { @@ -83,6 +94,9 @@ struct CIImageView: View { } } } + .onDisappear { + showFullScreenImage = false + } } private func imageView(_ img: UIImage) -> some View { @@ -98,7 +112,26 @@ struct CIImageView: View { .frame(width: w, height: w * img.size.height / img.size.width) .scaledToFit() } - loadingIndicator() + if !blurred || !showDownloadButton(chatItem.file?.fileStatus) { + loadingIndicator() + } + } + } + + private func smallViewImageView(_ img: UIImage) -> some View { + ZStack(alignment: .topTrailing) { + if img.imageData == nil { + Image(uiImage: img) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: maxWidth, height: maxWidth) + } else { + SwiftyGif(image: img, contentMode: .scaleAspectFill) + .frame(width: maxWidth, height: maxWidth) + } + if chatItem.file?.showStatusIconInSmallView == true { + loadingIndicator() + } } } @@ -145,4 +178,12 @@ struct CIImageView: View { .tint(.white) .padding(8) } + + private func showDownloadButton(_ fileStatus: CIFileStatus?) -> Bool { + switch fileStatus { + case .rcvInvitation: true + case .rcvAborted: true + default: false + } + } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index 3e6ef4abff..3c864ab172 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -12,6 +12,7 @@ import SimpleXChat struct CILinkView: View { @EnvironmentObject var theme: AppTheme let linkPreview: LinkPreview + @State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0 var body: some View { VStack(alignment: .center, spacing: 6) { @@ -19,6 +20,7 @@ struct CILinkView: View { Image(uiImage: uiImage) .resizable() .scaledToFit() + .modifier(PrivacyBlur(blurred: $blurred)) } VStack(alignment: .leading, spacing: 6) { Text(linkPreview.title) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index 7023449e9f..1f2e16448d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -27,7 +27,7 @@ struct CIRcvDecryptionError: View { case syncNotSupportedContactAlert case syncNotSupportedMemberAlert case decryptionErrorAlert - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -62,7 +62,7 @@ struct CIRcvDecryptionError: View { case .syncNotSupportedContactAlert: return Alert(title: Text("Fix not supported by contact"), message: message()) case .syncNotSupportedMemberAlert: return Alert(title: Text("Fix not supported by group member"), message: message()) case .decryptionErrorAlert: return Alert(title: Text("Decryption error"), message: message()) - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .error(title, error): return mkAlert(title: title, message: error) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index c31c4a0da9..4670fc685f 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -20,45 +20,44 @@ struct CIVideoView: View { @State private var videoPlaying: Bool = false private let maxWidth: CGFloat private var videoWidth: CGFloat? + private let smallView: Bool @State private var player: AVPlayer? @State private var fullPlayer: AVPlayer? @State private var url: URL? @State private var urlDecrypted: URL? @State private var decryptionInProgress: Bool = false - @State private var showFullScreenPlayer = false + @Binding private var showFullScreenPlayer: Bool @State private var timeObserver: Any? = nil @State private var fullScreenTimeObserver: Any? = nil @State private var publisher: AnyCancellable? = nil + private var sizeMultiplier: CGFloat { smallView ? 0.38 : 1 } + @State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0 - init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?) { + init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?, smallView: Bool = false, showFullscreenPlayer: Binding) { self.chatItem = chatItem self.preview = preview self._duration = State(initialValue: duration) self.maxWidth = maxWidth self.videoWidth = videoWidth - if let url = getLoadedVideo(chatItem.file) { - let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet() - self._urlDecrypted = State(initialValue: decrypted) - if let decrypted = decrypted { - self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(decrypted, false)) - self._fullPlayer = State(initialValue: AVPlayer(url: decrypted)) - } - self._url = State(initialValue: url) - } + self.smallView = smallView + self._showFullScreenPlayer = showFullscreenPlayer } var body: some View { let file = chatItem.file - ZStack { + ZStack(alignment: smallView ? .topLeading : .center) { ZStack(alignment: .topLeading) { - if let file = file, let preview = preview, let player = player, let decrypted = urlDecrypted { + if let file = file, let preview = preview, let decrypted = urlDecrypted, smallView { + smallVideoView(decrypted, file, preview) + } else if let file = file, let preview = preview, let player = player, let decrypted = urlDecrypted { videoView(player, decrypted, file, preview, duration) + } else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil, smallView { + smallVideoViewEncrypted(file, defaultPreview) } else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil { videoViewEncrypted(file, defaultPreview, duration) - } else if let preview { - imageView(preview) - .onTapGesture { - if let file = file { + } else if let preview, let file { + Group { if smallView { smallViewImageView(preview, file) } else { imageView(preview) } } + .onTapGesture { switch file.fileStatus { case .rcvInvitation, .rcvAborted: receiveFileIfValidSize(file: file, receiveFile: receiveFile) @@ -82,21 +81,64 @@ struct CIVideoView: View { default: () } } - } } - durationProgress() + if !smallView { + durationProgress() + } } - if let file = file, showDownloadButton(file.fileStatus) { - Button { - receiveFileIfValidSize(file: file, receiveFile: receiveFile) - } label: { + if !blurred, let file, showDownloadButton(file.fileStatus) { + if !smallView { + Button { + receiveFileIfValidSize(file: file, receiveFile: receiveFile) + } label: { + playPauseIcon("play.fill") + } + } else if !file.showStatusIconInSmallView { playPauseIcon("play.fill") + .onTapGesture { + receiveFileIfValidSize(file: file, receiveFile: receiveFile) + } } } } + .fullScreenCover(isPresented: $showFullScreenPlayer) { + if let decrypted = urlDecrypted { + fullScreenPlayer(decrypted) + } + } + .onAppear { + setupPlayer(chatItem.file) + } + .onChange(of: chatItem.file) { file in + // ChatItem can be changed in small view on chat list screen + setupPlayer(file) + } + .onDisappear { + showFullScreenPlayer = false + } } - private func showDownloadButton(_ fileStatus: CIFileStatus) -> Bool { + private func setupPlayer(_ file: CIFile?) { + let newUrl = getLoadedVideo(file) + if newUrl == url { + return + } + url = nil + urlDecrypted = nil + player = nil + fullPlayer = nil + if let newUrl { + let decrypted = file?.fileSource?.cryptoArgs == nil ? newUrl : file?.fileSource?.decryptedGet() + urlDecrypted = decrypted + if let decrypted = decrypted { + player = VideoPlayerView.getOrCreatePlayer(decrypted, false) + fullPlayer = AVPlayer(url: decrypted) + } + url = newUrl + } + } + + private func showDownloadButton(_ fileStatus: CIFileStatus?) -> Bool { switch fileStatus { case .rcvInvitation: true case .rcvAborted: true @@ -109,11 +151,6 @@ struct CIVideoView: View { ZStack(alignment: .center) { let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) imageView(defaultPreview) - .fullScreenCover(isPresented: $showFullScreenPlayer) { - if let decrypted = urlDecrypted { - fullScreenPlayer(decrypted) - } - } .onTapGesture { decrypt(file: file) { showFullScreenPlayer = urlDecrypted != nil @@ -122,20 +159,22 @@ struct CIVideoView: View { .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenPlayer = false } - if !decryptionInProgress { - Button { - decrypt(file: file) { - if urlDecrypted != nil { - videoPlaying = true - player?.play() + if !blurred { + if !decryptionInProgress { + Button { + decrypt(file: file) { + if urlDecrypted != nil { + videoPlaying = true + player?.play() + } } + } label: { + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") } - } label: { - playPauseIcon(canBePlayed ? "play.fill" : "play.slash") + .disabled(!canBePlayed) + } else { + videoDecryptionProgress() } - .disabled(!canBePlayed) - } else { - videoDecryptionProgress() } } } @@ -154,9 +193,7 @@ struct CIVideoView: View { videoPlaying = false } } - .fullScreenCover(isPresented: $showFullScreenPlayer) { - fullScreenPlayer(url) - } + .modifier(PrivacyBlur(enabled: !videoPlaying, blurred: $blurred)) .onTapGesture { switch player.timeControlStatus { case .playing: @@ -172,7 +209,7 @@ struct CIVideoView: View { .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenPlayer = false } - if !videoPlaying { + if !videoPlaying && !blurred { Button { m.stopPreviousRecPlay = url player.play() @@ -194,14 +231,53 @@ struct CIVideoView: View { } } + private func smallVideoViewEncrypted(_ file: CIFile, _ preview: UIImage) -> some View { + return ZStack(alignment: .topLeading) { + let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) + smallViewImageView(preview, file) + .onTapGesture { + decrypt(file: file) { + showFullScreenPlayer = urlDecrypted != nil + } + } + .onChange(of: m.activeCallViewIsCollapsed) { _ in + showFullScreenPlayer = false + } + if file.showStatusIconInSmallView { + // Show nothing + } else if !decryptionInProgress { + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") + } else { + videoDecryptionProgress() + } + } + } + + private func smallVideoView(_ url: URL, _ file: CIFile, _ preview: UIImage) -> some View { + return ZStack(alignment: .topLeading) { + smallViewImageView(preview, file) + .onTapGesture { + showFullScreenPlayer = true + } + .onChange(of: m.activeCallViewIsCollapsed) { _ in + showFullScreenPlayer = false + } + + if !file.showStatusIconInSmallView { + playPauseIcon("play.fill") + } + } + } + + private func playPauseIcon(_ image: String, _ color: Color = .white) -> some View { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 12, height: 12) + .frame(width: smallView ? 12 * sizeMultiplier * 1.6 : 12, height: smallView ? 12 * sizeMultiplier * 1.6 : 12) .foregroundColor(color) - .padding(.leading, 4) - .frame(width: 40, height: 40) + .padding(.leading, smallView ? 0 : 4) + .frame(width: 40 * sizeMultiplier, height: 40 * sizeMultiplier) .background(Color.black.opacity(0.35)) .clipShape(Circle()) } @@ -209,9 +285,9 @@ struct CIVideoView: View { private func videoDecryptionProgress(_ color: Color = .white) -> some View { ProgressView() .progressViewStyle(.circular) - .frame(width: 12, height: 12) + .frame(width: smallView ? 12 * sizeMultiplier : 12, height: smallView ? 12 * sizeMultiplier : 12) .tint(color) - .frame(width: 40, height: 40) + .frame(width: smallView ? 40 * sizeMultiplier * 0.9 : 40, height: smallView ? 40 * sizeMultiplier * 0.9 : 40) .background(Color.black.opacity(0.35)) .clipShape(Circle()) } @@ -247,7 +323,23 @@ struct CIVideoView: View { .resizable() .scaledToFit() .frame(width: w) - fileStatusIcon() + .modifier(PrivacyBlur(blurred: $blurred)) + if !blurred || !showDownloadButton(chatItem.file?.fileStatus) { + fileStatusIcon() + } + } + } + + private func smallViewImageView(_ img: UIImage, _ file: CIFile) -> some View { + ZStack(alignment: .center) { + Image(uiImage: img) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: maxWidth, height: maxWidth) + if file.showStatusIconInSmallView { + fileStatusIcon() + .allowsHitTesting(false) + } } } @@ -322,7 +414,7 @@ struct CIVideoView: View { .aspectRatio(contentMode: .fit) .frame(width: size, height: size) .foregroundColor(.white) - .padding(padding) + .padding(smallView ? 0 : padding) } private func progressView() -> some View { @@ -330,7 +422,7 @@ struct CIVideoView: View { .progressViewStyle(.circular) .frame(width: 16, height: 16) .tint(.white) - .padding(11) + .padding(smallView ? 0 : 11) } private func progressCircle(_ progress: Int64, _ total: Int64) -> some View { @@ -342,7 +434,7 @@ struct CIVideoView: View { ) .rotationEffect(.degrees(-90)) .frame(width: 16, height: 16) - .padding([.trailing, .top], 11) + .padding([.trailing, .top], smallView ? 0 : 11) } // TODO encrypt: where file size is checked? @@ -382,7 +474,8 @@ struct CIVideoView: View { ) .onAppear { DispatchQueue.main.asyncAfter(deadline: .now()) { - m.stopPreviousRecPlay = url + // Prevent feedback loop - setting `ChatModel`s property causes `onAppear` to be called on iOS17+ + if m.stopPreviousRecPlay != url { m.stopPreviousRecPlay = url } if let player = fullPlayer { player.play() var played = false @@ -419,10 +512,12 @@ struct CIVideoView: View { urlDecrypted = await file.fileSource?.decryptedGetOrCreate(&ChatModel.shared.filesToDelete) await MainActor.run { if let decrypted = urlDecrypted { - player = VideoPlayerView.getOrCreatePlayer(decrypted, false) + if !smallView { + player = VideoPlayerView.getOrCreatePlayer(decrypted, false) + } fullPlayer = AVPlayer(url: decrypted) } - decryptionInProgress = true + decryptionInProgress = false completed?() } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index ad15d0d342..ce4d2a8181 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -15,15 +15,26 @@ struct CIVoiceView: View { var chatItem: ChatItem let recordingFile: CIFile? let duration: Int - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? + @State var audioPlayer: AudioPlayer? = nil + @State var playbackState: VoiceMessagePlaybackState = .noPlayback + @State var playbackTime: TimeInterval? = nil + @Binding var allowMenu: Bool + var smallView: Bool = false @State private var seek: (TimeInterval) -> Void = { _ in } var body: some View { Group { - if chatItem.chatDir.sent { + if smallView { + HStack(spacing: 10) { + player() + playerTime() + .allowsHitTesting(false) + if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { + playbackSlider() + } + } + } else if chatItem.chatDir.sent { VStack (alignment: .trailing, spacing: 6) { HStack { if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { @@ -55,6 +66,7 @@ struct CIVoiceView: View { private func player() -> some View { VoiceMessagePlayer( + chat: chat, chatItem: chatItem, recordingFile: recordingFile, recordingTime: TimeInterval(duration), @@ -63,7 +75,8 @@ struct CIVoiceView: View { audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, - allowMenu: $allowMenu + allowMenu: $allowMenu, + sizeMultiplier: smallView ? voiceMessageSizeBasedOnSquareSize(36) / 56 : 1 ) } @@ -119,6 +132,7 @@ struct VoiceMessagePlayerTime: View { } struct VoiceMessagePlayer: View { + @ObservedObject var chat: Chat @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme var chatItem: ChatItem @@ -130,7 +144,9 @@ struct VoiceMessagePlayer: View { @Binding var audioPlayer: AudioPlayer? @Binding var playbackState: VoiceMessagePlaybackState @Binding var playbackTime: TimeInterval? + @Binding var allowMenu: Bool + var sizeMultiplier: CGFloat var body: some View { ZStack { @@ -190,49 +206,113 @@ struct VoiceMessagePlayer: View { } } .onAppear { + if audioPlayer == nil { + let small = sizeMultiplier != 1 + audioPlayer = small ? VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)]?.audioPlayer : VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)]?.audioPlayer + playbackState = (small ? VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)]?.playbackState : VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)]?.playbackState) ?? .noPlayback + playbackTime = small ? VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)]?.playbackTime : VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)]?.playbackTime + } seek = { to in audioPlayer?.seek(to) } - audioPlayer?.onTimer = { playbackTime = $0 } + let audioPath: URL? = if let recordingSource = getLoadedFileSource(recordingFile) { + getAppFilePath(recordingSource.filePath) + } else { + nil + } + let chatId = chatModel.chatId + let userId = chatModel.currentUser?.userId + audioPlayer?.onTimer = { + playbackTime = $0 + notifyStateChange() + // Manual check here is needed because when this view is not visible, SwiftUI don't react on stopPreviousRecPlay, chatId and current user changes and audio keeps playing when it should stop + if (audioPath != nil && chatModel.stopPreviousRecPlay != audioPath) || chatModel.chatId != chatId || chatModel.currentUser?.userId != userId { + stopPlayback() + } + } audioPlayer?.onFinishPlayback = { playbackState = .noPlayback playbackTime = TimeInterval(0) + notifyStateChange() + } + // One voice message was paused, then scrolled far from it, started to play another one, drop to stopped state + if let audioPath, chatModel.stopPreviousRecPlay != audioPath { + stopPlayback() } } .onChange(of: chatModel.stopPreviousRecPlay) { it in if let recordingFileName = getLoadedFileSource(recordingFile)?.filePath, chatModel.stopPreviousRecPlay != getAppFilePath(recordingFileName) { - audioPlayer?.stop() - playbackState = .noPlayback - playbackTime = TimeInterval(0) + stopPlayback() } } .onChange(of: playbackState) { state in allowMenu = state == .paused || state == .noPlayback + // Notify activeContentPreview in ChatPreviewView that playback is finished + if state == .noPlayback, let recordingFileName = getLoadedFileSource(recordingFile)?.filePath, + chatModel.stopPreviousRecPlay == getAppFilePath(recordingFileName) { + chatModel.stopPreviousRecPlay = nil + } + } + .onChange(of: chatModel.chatId) { _ in + stopPlayback() + } + .onDisappear { + if sizeMultiplier == 1 && chatModel.chatId == nil { + stopPlayback() + } } } @ViewBuilder private func playbackButton() -> some View { - switch playbackState { - case .noPlayback: - Button { - if let recordingSource = getLoadedFileSource(recordingFile) { - startPlayback(recordingSource) - } - } label: { + if sizeMultiplier != 1 { + switch playbackState { + case .noPlayback: playPauseIcon("play.fill", theme.colors.primary) - } - case .playing: - Button { - audioPlayer?.pause() - playbackState = .paused - } label: { + .onTapGesture { + if let recordingSource = getLoadedFileSource(recordingFile) { + startPlayback(recordingSource) + } + } + case .playing: playPauseIcon("pause.fill", theme.colors.primary) - } - case .paused: - Button { - audioPlayer?.play() - playbackState = .playing - } label: { + .onTapGesture { + audioPlayer?.pause() + playbackState = .paused + notifyStateChange() + } + case .paused: playPauseIcon("play.fill", theme.colors.primary) + .onTapGesture { + audioPlayer?.play() + playbackState = .playing + notifyStateChange() + } + } + } else { + switch playbackState { + case .noPlayback: + Button { + if let recordingSource = getLoadedFileSource(recordingFile) { + startPlayback(recordingSource) + } + } label: { + playPauseIcon("play.fill", theme.colors.primary) + } + case .playing: + Button { + audioPlayer?.pause() + playbackState = .paused + notifyStateChange() + } label: { + playPauseIcon("pause.fill", theme.colors.primary) + } + case .paused: + Button { + audioPlayer?.play() + playbackState = .playing + notifyStateChange() + } label: { + playPauseIcon("play.fill", theme.colors.primary) + } } } } @@ -242,28 +322,49 @@ struct VoiceMessagePlayer: View { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) + .frame(width: 20 * sizeMultiplier, height: 20 * sizeMultiplier) .foregroundColor(color) .padding(.leading, image == "play.fill" ? 4 : 0) - .frame(width: 56, height: 56) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) if recordingTime > 0 { ProgressCircle(length: recordingTime, progress: $playbackTime) - .frame(width: 53, height: 53) // this + ProgressCircle lineWidth = background circle diameter + .frame(width: 53 * sizeMultiplier, height: 53 * sizeMultiplier) // this + ProgressCircle lineWidth = background circle diameter } } } private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View { - Button { - Task { - if let user = chatModel.currentUser { - await receiveFile(user: user, fileId: recordingFile.fileId) + Group { + if sizeMultiplier != 1 { + playPauseIcon(icon, theme.colors.primary) + .onTapGesture { + Task { + if let user = chatModel.currentUser { + await receiveFile(user: user, fileId: recordingFile.fileId) + } + } + } + } else { + Button { + Task { + if let user = chatModel.currentUser { + await receiveFile(user: user, fileId: recordingFile.fileId) + } + } + } label: { + playPauseIcon(icon, theme.colors.primary) } } - } label: { - playPauseIcon(icon, theme.colors.primary) + } + } + + func notifyStateChange() { + if sizeMultiplier != 1 { + VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)] = VoiceItemState(audioPlayer: audioPlayer, playbackState: playbackState, playbackTime: playbackTime) + } else { + VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)] = VoiceItemState(audioPlayer: audioPlayer, playbackState: playbackState, playbackTime: playbackTime) } } @@ -288,33 +389,96 @@ struct VoiceMessagePlayer: View { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: size, height: size) + .frame(width: size * sizeMultiplier, height: size * sizeMultiplier) .foregroundColor(Color(uiColor: .tertiaryLabel)) - .frame(width: 56, height: 56) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) } private func loadingIcon() -> some View { ProgressView() - .frame(width: 30, height: 30) - .frame(width: 56, height: 56) + .frame(width: 30 * sizeMultiplier, height: 30 * sizeMultiplier) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) } private func startPlayback(_ recordingSource: CryptoFile) { - chatModel.stopPreviousRecPlay = getAppFilePath(recordingSource.filePath) + let audioPath = getAppFilePath(recordingSource.filePath) + let chatId = chatModel.chatId + let userId = chatModel.currentUser?.userId + chatModel.stopPreviousRecPlay = audioPath audioPlayer = AudioPlayer( - onTimer: { playbackTime = $0 }, + onTimer: { + playbackTime = $0 + notifyStateChange() + // Manual check here is needed because when this view is not visible, SwiftUI don't react on stopPreviousRecPlay, chatId and current user changes and audio keeps playing when it should stop + if chatModel.stopPreviousRecPlay != audioPath || chatModel.chatId != chatId || chatModel.currentUser?.userId != userId { + stopPlayback() + } + }, onFinishPlayback: { playbackState = .noPlayback playbackTime = TimeInterval(0) + notifyStateChange() } ) audioPlayer?.start(fileSource: recordingSource, at: playbackTime) playbackState = .playing + notifyStateChange() } + + private func stopPlayback() { + audioPlayer?.stop() + playbackState = .noPlayback + playbackTime = TimeInterval(0) + notifyStateChange() + } +} + +func voiceMessageSizeBasedOnSquareSize(_ squareSize: CGFloat) -> CGFloat { + let squareToCircleRatio = 0.935 + return squareSize + squareSize * (1 - squareToCircleRatio) +} + +class VoiceItemState { + var audioPlayer: AudioPlayer? + var playbackState: VoiceMessagePlaybackState + var playbackTime: TimeInterval? + + init(audioPlayer: AudioPlayer? = nil, playbackState: VoiceMessagePlaybackState, playbackTime: TimeInterval? = nil) { + self.audioPlayer = audioPlayer + self.playbackState = playbackState + self.playbackTime = playbackTime + } + + static func id(_ chat: Chat, _ chatItem: ChatItem) -> String { + "\(chat.id) \(chatItem.id)" + } + + static func id(_ chatInfo: ChatInfo, _ chatItem: ChatItem) -> String { + "\(chatInfo.id) \(chatItem.id)" + } + + static func stopVoiceInSmallView(_ chatInfo: ChatInfo, _ chatItem: ChatItem) { + let id = id(chatInfo, chatItem) + if let item = smallView[id] { + item.audioPlayer?.stop() + ChatModel.shared.stopPreviousRecPlay = nil + } + } + + static func stopVoiceInChatView(_ chatInfo: ChatInfo, _ chatItem: ChatItem) { + let id = id(chatInfo, chatItem) + if let item = chatView[id] { + item.audioPlayer?.stop() + ChatModel.shared.stopPreviousRecPlay = nil + } + } + + static var smallView: [String: VoiceItemState] = [:] + static var chatView: [String: VoiceItemState] = [:] } struct CIVoiceView_Previews: PreviewProvider { @@ -339,15 +503,12 @@ struct CIVoiceView_Previews: PreviewProvider { chatItem: ChatItem.getVoiceMsgContentSample(), recordingFile: CIFile.getSample(fileName: "voice.m4a", fileSize: 65536, fileStatus: .rcvComplete), duration: 30, - audioPlayer: .constant(nil), - playbackState: .constant(.playing), - playbackTime: .constant(TimeInterval(20)), allowMenu: Binding.constant(true) ) - ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true)) } .previewLayout(.fixed(width: 360, height: 360)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index 59fabb3901..64a7f29a25 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -13,21 +13,23 @@ import SimpleXChat struct FramedCIVoiceView: View { @EnvironmentObject var theme: AppTheme + @ObservedObject var chat: Chat var chatItem: ChatItem let recordingFile: CIFile? let duration: Int - + + @State var audioPlayer: AudioPlayer? = nil + @State var playbackState: VoiceMessagePlaybackState = .noPlayback + @State var playbackTime: TimeInterval? = nil + @Binding var allowMenu: Bool - - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? - + @State private var seek: (TimeInterval) -> Void = { _ in } var body: some View { HStack { VoiceMessagePlayer( + chat: chat, chatItem: chatItem, recordingFile: recordingFile, recordingTime: TimeInterval(duration), @@ -36,7 +38,8 @@ struct FramedCIVoiceView: View { audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, - allowMenu: $allowMenu + allowMenu: $allowMenu, + sizeMultiplier: 1 ) VoiceMessagePlayerTime( recordingTime: TimeInterval(duration), diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 595d9bf2fc..2fdd708fdb 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -26,10 +26,7 @@ struct FramedItemView: View { @Binding var allowMenu: Bool @State private var showSecrets = false @State private var showQuoteSecrets = false - - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? + @State private var showFullscreenGallery: Bool = false var body: some View { let v = ZStack(alignment: .bottomTrailing) { @@ -106,7 +103,7 @@ struct FramedItemView: View { } else { switch (chatItem.content.msgContent) { case let .image(text, _): - CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth) + CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear @@ -121,7 +118,7 @@ struct FramedItemView: View { ciMsgContentView(chatItem) } case let .video(text, _, duration): - CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth) + CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth, showFullscreenPlayer: $showFullscreenGallery) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear @@ -136,7 +133,7 @@ struct FramedItemView: View { ciMsgContentView(chatItem) } case let .voice(text, duration): - FramedCIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) + FramedCIVoiceView(chat: chat, chatItem: chatItem, recordingFile: chatItem.file, duration: duration, allowMenu: $allowMenu) .overlay(DetermineWidth()) if text != "" { ciMsgContentView(chatItem) @@ -370,14 +367,14 @@ func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color { struct FramedItemView_Previews: PreviewProvider { static var previews: some View { Group{ - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 200)) } @@ -386,16 +383,16 @@ struct FramedItemView_Previews: PreviewProvider { struct FramedItemView_Edited_Previews: PreviewProvider { static var previews: some View { Group { - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 200)) } @@ -404,16 +401,16 @@ struct FramedItemView_Edited_Previews: PreviewProvider { struct FramedItemView_Deleted_Previews: PreviewProvider { static var previews: some View { Group { - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 200)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift index 1814419623..0b7de32a88 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -22,7 +22,7 @@ struct ChatItemForwardingView: View { @FocusState private var searchFocused @State private var alert: SomeAlert? @State private var hasSimplexLink_: Bool? - private let chatsToForwardTo = filterChatsToForwardTo() + private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats) var body: some View { NavigationView { @@ -67,22 +67,6 @@ struct ChatItemForwardingView: View { } } - private func foundChat(_ chat: Chat, _ searchStr: String) -> Bool { - let cInfo = chat.chatInfo - return switch cInfo { - case let .direct(contact): - viewNameContains(cInfo, searchStr) || - contact.profile.displayName.localizedLowercase.contains(searchStr) || - contact.fullName.localizedLowercase.contains(searchStr) - default: - viewNameContains(cInfo, searchStr) - } - - func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool { - cInfo.chatViewName.localizedLowercase.contains(s) - } - } - private func prohibitedByPref(_ chat: Chat) -> Bool { // preference checks should match checks in compose view let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) @@ -162,27 +146,6 @@ struct ChatItemForwardingView: View { } } -private func filterChatsToForwardTo() -> [Chat] { - var filteredChats = ChatModel.shared.chats.filter { c in - c.chatInfo.chatType != .local && canForwardToChat(c) - } - if let privateNotes = ChatModel.shared.chats.first(where: { $0.chatInfo.chatType == .local }) { - filteredChats.insert(privateNotes, at: 0) - } - return filteredChats -} - -private func canForwardToChat(_ chat: Chat) -> Bool { - switch chat.chatInfo { - case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv - case let .group(groupInfo): groupInfo.sendMsgEnabled - case let .local(noteFolder): noteFolder.sendMsgEnabled - case .contactRequest: false - case .contactConnection: false - case .invalidJSON: false - } -} - #Preview { ChatItemForwardingView( ci: ChatItem.getSample(1, .directSnd, .now, "hello"), @@ -190,3 +153,4 @@ private func canForwardToChat(_ chat: Chat) -> Bool { composeState: Binding.constant(ComposeState(message: "hello")) ).environmentObject(CurrentColors.toAppTheme()) } + diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index d06e67e2e6..870fe30108 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -16,28 +16,20 @@ struct ChatItemView: View { var maxWidth: CGFloat = .infinity @Binding var revealed: Bool @Binding var allowMenu: Bool - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? + init( chat: Chat, chatItem: ChatItem, showMember: Bool = false, maxWidth: CGFloat = .infinity, revealed: Binding, - allowMenu: Binding = .constant(false), - audioPlayer: Binding = .constant(nil), - playbackState: Binding = .constant(.noPlayback), - playbackTime: Binding = .constant(nil) + allowMenu: Binding = .constant(false) ) { self.chat = chat self.chatItem = chatItem self.maxWidth = maxWidth _revealed = revealed _allowMenu = allowMenu - _audioPlayer = audioPlayer - _playbackState = playbackState - _playbackTime = playbackTime } var body: some View { @@ -48,7 +40,7 @@ struct ChatItemView: View { if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) { EmojiItemView(chat: chat, chatItem: ci) } else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent { - CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu) + CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu) } else if ci.content.msgContent == nil { ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case } else { @@ -84,10 +76,7 @@ struct ChatItemView: View { maxWidth: maxWidth, imgWidth: adjustedMaxWidth, videoWidth: adjustedMaxWidth, - allowMenu: $allowMenu, - audioPlayer: $audioPlayer, - playbackState: $playbackState, - playbackTime: $playbackTime + allowMenu: $allowMenu ) } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 1f3c04085b..5eb7861bd2 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -96,6 +96,7 @@ struct ChatView: View { } .onChange(of: chatModel.chatId) { cId in showChatInfoSheet = false + stopAudioPlayer() if let cId { if let c = chatModel.getChat(cId) { chat = c @@ -117,6 +118,7 @@ struct ChatView: View { .environmentObject(scrollModel) .onDisappear { VideoPlayerView.players.removeAll() + stopAudioPlayer() if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented { chatModel.chatId = nil DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { @@ -378,7 +380,7 @@ struct ChatView: View { @ViewBuilder private func connectingText() -> some View { if case let .direct(contact) = chat.chatInfo, - !contact.ready, + !contact.sndReady, contact.active, !contact.nextSendGrpInv { Text("connecting…") @@ -589,6 +591,11 @@ struct ChatView: View { } } + func stopAudioPlayer() { + VoiceItemState.chatView.values.forEach { $0.audioPlayer?.stop() } + VoiceItemState.chatView = [:] + } + @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { ChatItemWithMenu( chat: chat, @@ -620,10 +627,6 @@ struct ChatView: View { @State private var allowMenu: Bool = true - @State private var audioPlayer: AudioPlayer? - @State private var playbackState: VoiceMessagePlaybackState = .noPlayback - @State private var playbackTime: TimeInterval? - var revealed: Bool { chatItem == revealedChatItem } var body: some View { @@ -742,10 +745,7 @@ struct ChatView: View { chatItem: ci, maxWidth: maxWidth, revealed: .constant(revealed), - allowMenu: $allowMenu, - audioPlayer: $audioPlayer, - playbackState: $playbackState, - playbackTime: $playbackTime + allowMenu: $allowMenu ) .modifier(ChatItemClipped(ci)) .contextMenu { menu(ci, range, live: composeState.liveMessage != nil) } @@ -772,14 +772,6 @@ struct ChatView: View { } .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) - .onDisappear { - if ci.content.msgContent?.isVoice == true { - allowMenu = true - audioPlayer?.stop() - playbackState = .noPlayback - playbackTime = TimeInterval(0) - } - } .sheet(isPresented: $showChatItemInfoSheet, onDismiss: { chatItemInfo = nil }) { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift index 0fb48033d5..66cb9edcf8 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift @@ -10,35 +10,6 @@ import SwiftUI import LinkPresentation import SimpleXChat -func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { - logger.debug("getLinkMetadata: fetching URL preview") - LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in - if let e = error { - logger.error("Error retrieving link metadata: \(e.localizedDescription)") - } - if let metadata = metadata, - let imageProvider = metadata.imageProvider, - imageProvider.canLoadObject(ofClass: UIImage.self) { - imageProvider.loadObject(ofClass: UIImage.self){ object, error in - var linkPreview: LinkPreview? = nil - if let error = error { - logger.error("Couldn't load image preview from link metadata with error: \(error.localizedDescription)") - } else { - if let image = object as? UIImage, - let resized = resizeImageToStrSize(image, maxDataSize: 14000), - let title = metadata.title, - let uri = metadata.originalURL { - linkPreview = LinkPreview(uri: uri, title: title, image: resized) - } - } - cb(linkPreview) - } - } else { - cb(nil) - } - } -} - struct ComposeLinkView: View { @EnvironmentObject var theme: AppTheme let linkPreview: LinkPreview? diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index a720c3aaaf..a776ebf0dd 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -67,7 +67,6 @@ struct SendMessageView: View { .fixedSize(horizontal: false, vertical: true) } } - if progressByTimeout { ProgressView() .scaleEffect(1.4) @@ -87,7 +86,7 @@ struct SendMessageView: View { .padding(.vertical, 1) .background(theme.colors.background) .clipShape(composeShape) - .overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)) + .overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7)) } .onChange(of: composeState.message, perform: { text in updateFont(text) }) .onChange(of: composeState.inProgress) { inProgress in diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 49239c8fa5..dc867b026f 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -35,7 +35,7 @@ struct AddGroupMembersViewCommon: View { private enum AddGroupMembersAlert: Identifiable { case prohibitedToInviteIncognito - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -122,7 +122,7 @@ struct AddGroupMembersViewCommon: View { message: Text("You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile") ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } .onChange(of: selectedContacts) { _ in diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 59a21d2330..f89009f93f 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -40,7 +40,7 @@ struct GroupChatInfoView: View { case blockForAllAlert(mem: GroupMember) case unblockForAllAlert(mem: GroupMember) case removeMemberAlert(mem: GroupMember) - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -158,7 +158,7 @@ struct GroupChatInfoView: View { case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem) case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem) case let .removeMemberAlert(mem): return removeMemberAlert(mem) - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .error(title, error): return mkAlert(title: title, message: error) } } .onAppear { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index adf5f998a4..93a8be04f4 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -22,7 +22,7 @@ struct GroupLinkView: View { private enum GroupLinkAlert: Identifiable { case deleteLink - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -113,7 +113,7 @@ struct GroupLinkView: View { }, secondaryButton: .cancel() ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } .onChange(of: groupLinkMemberRole) { _ in diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 3e4c3c9f6e..12b5bd5a98 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -37,7 +37,7 @@ struct GroupMemberInfoView: View { case syncConnectionForceAlert case planAndConnectAlert(alert: PlanAndConnectAlert) case queueInfo(info: String) - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -237,7 +237,7 @@ struct GroupMemberInfoView: View { case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true) case let .queueInfo(info): return queueInfoAlert(info) - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .error(title, error): return mkAlert(title: title, message: error) } } .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) } diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift index ae6d900eb6..a3f485cb5e 100644 --- a/apps/ios/Shared/Views/Chat/ReverseList.swift +++ b/apps/ios/Shared/Views/Chat/ReverseList.swift @@ -129,8 +129,9 @@ struct ReverseList: UIV from: nil, for: nil ) + NotificationCenter.default.post(name: .chatViewWillBeginScrolling, object: nil) } - + /// Scrolls up func scrollToNextPage() { tableView.setContentOffset( diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index c0cc42bd8e..35cb5b3861 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -100,7 +100,7 @@ struct ChatListNavLink: View { clearChatButton() } Button { - if contact.ready || !contact.active { + if contact.sndReady || !contact.active { showDeleteContactActionSheet = true } else { AlertManager.shared.showAlert(deletePendingContactAlert(chat, contact)) @@ -114,7 +114,7 @@ struct ChatListNavLink: View { } } .actionSheet(isPresented: $showDeleteContactActionSheet) { - if contact.ready && contact.active { + if contact.sndReady && contact.active { return ActionSheet( title: Text("Delete contact?\nThis cannot be undone!"), buttons: [ diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 16f87f0c13..82d322d6fd 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -21,6 +21,7 @@ struct ChatListView: View { @State private var newChatMenuOption: NewChatMenuOption? = nil @State private var userPickerVisible = false @State private var showConnectDesktop = false + @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false var body: some View { @@ -162,6 +163,10 @@ struct ChatListView: View { chatModel.chatToTop = nil chatModel.popChat(chatId) } + stopAudioPlayer() + } + .onChange(of: chatModel.currentUser?.userId) { _ in + stopAudioPlayer() } if cs.isEmpty && !chatModel.chats.isEmpty { Text("No filtered chats").foregroundColor(theme.colors.secondary) @@ -217,6 +222,11 @@ struct ChatListView: View { } } + func stopAudioPlayer() { + VoiceItemState.smallView.values.forEach { $0.audioPlayer?.stop() } + VoiceItemState.smallView = [:] + } + private func filteredChats() -> [Chat] { if let linkChatId = searchChatFilteredBySimplexLink { return chatModel.chats.filter { $0.id == linkChatId } @@ -266,9 +276,9 @@ struct ChatListView: View { } struct SubsStatusIndicator: View { - @State private var serversSummary: PresentedServersSummary? + @State private var subs: SMPServerSubs = SMPServerSubs.newSMPServerSubs + @State private var hasSess: Bool = false @State private var timer: Timer? = nil - @State private var timerCounter = 0 @State private var showServersSummary = false @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false @@ -277,12 +287,10 @@ struct SubsStatusIndicator: View { Button { showServersSummary = true } label: { - let subs = serversSummary?.allUsersSMP.smpTotals.subs ?? SMPServerSubs.newSMPServerSubs - let sess = serversSummary?.allUsersSMP.smpTotals.sessions ?? ServerSessions.newServerSessions HStack(spacing: 4) { - SubscriptionStatusIndicatorView(subs: subs, sess: sess) + SubscriptionStatusIndicatorView(subs: subs, hasSess: hasSess) if showSubscriptionPercentage { - SubscriptionStatusPercentageView(subs: subs, sess: sess) + SubscriptionStatusPercentageView(subs: subs, hasSess: hasSess) } } } @@ -293,14 +301,14 @@ struct SubsStatusIndicator: View { stopTimer() } .sheet(isPresented: $showServersSummary) { - ServersSummaryView(serversSummary: $serversSummary) + ServersSummaryView() } } private func startTimer() { timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in if AppChatState.shared.value == .active { - getServersSummary() + getSubsTotal() } } } @@ -310,11 +318,11 @@ struct SubsStatusIndicator: View { timer = nil } - private func getServersSummary() { + private func getSubsTotal() { do { - serversSummary = try getAgentServersSummary() + (subs, hasSess) = try getAgentSubsTotal() } catch let error { - logger.error("getAgentServersSummary error: \(responseError(error))") + logger.error("getSubsTotal error: \(responseError(error))") } } } diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index c1156225d8..ce638e8a0a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -16,6 +16,8 @@ struct ChatPreviewView: View { @Binding var progressByTimeout: Bool @State var deleting: Bool = false var darkGreen = Color(red: 0, green: 0.5, blue: 0) + @State private var activeContentPreview: ActiveContentPreview? = nil + @State private var showFullscreenGallery: Bool = false @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @@ -43,11 +45,38 @@ struct ChatPreviewView: View { .padding(.horizontal, 8) ZStack(alignment: .topTrailing) { - chatMessagePreview(cItem) + let chat = activeContentPreview?.chat ?? chat + let ci = activeContentPreview?.ci ?? chat.chatItems.last + let mc = ci?.content.msgContent + HStack(alignment: .top) { + let deleted = ci?.isDeletedContent == true || ci?.meta.itemDeleted != nil + let showContentPreview = (showChatPreviews && chatModel.draftChatId != chat.id && !deleted) || activeContentPreview != nil + if let ci, showContentPreview { + chatItemContentPreview(chat, ci) + } + let mcIsVoice = switch mc { case .voice: true; default: false } + if !mcIsVoice || !showContentPreview || mc?.text != "" || chatModel.draftChatId == chat.id { + let hasFilePreview = if case .file = mc { true } else { false } + chatMessagePreview(cItem, hasFilePreview) + } else { + Spacer() + chatInfoIcon(chat).frame(minWidth: 37, alignment: .trailing) + } + } + .onChange(of: chatModel.stopPreviousRecPlay?.path) { _ in + checkActiveContentPreview(chat, ci, mc) + } + .onChange(of: activeContentPreview) { _ in + checkActiveContentPreview(chat, ci, mc) + } + .onChange(of: showFullscreenGallery) { _ in + checkActiveContentPreview(chat, ci, mc) + } chatStatusImage() .padding(.top, 26) .frame(maxWidth: .infinity, alignment: .trailing) } + .frame(maxWidth: .infinity, alignment: .leading) .padding(.trailing, 8) Spacer() @@ -57,6 +86,33 @@ struct ChatPreviewView: View { .padding(.bottom, -8) .onChange(of: chatModel.deletedChats.contains(chat.chatInfo.id)) { contains in deleting = contains + // Stop voice when deleting the chat + if contains, let ci = activeContentPreview?.ci { + VoiceItemState.stopVoiceInSmallView(chat.chatInfo, ci) + } + } + + func checkActiveContentPreview(_ chat: Chat, _ ci: ChatItem?, _ mc: MsgContent?) { + let playing = chatModel.stopPreviousRecPlay + if case .voice = activeContentPreview?.mc, playing == nil { + activeContentPreview = nil + } else if activeContentPreview == nil { + if case .image = mc, let ci, let mc, showFullscreenGallery { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + if case .video = mc, let ci, let mc, showFullscreenGallery { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + if case .voice = mc, let ci, let mc, let fileSource = ci.file?.fileSource, playing?.path.hasSuffix(fileSource.filePath) == true { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + } else if case .voice = activeContentPreview?.mc { + if let playing, let fileSource = ci?.file?.fileSource, !playing.path.hasSuffix(fileSource.filePath) { + activeContentPreview = nil + } + } else if !showFullscreenGallery { + activeContentPreview = nil + } } } @@ -113,39 +169,47 @@ struct ChatPreviewView: View { .kerning(-2) } - private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View { + private func chatPreviewLayout(_ text: Text?, draft: Bool = false, _ hasFilePreview: Bool = false) -> some View { ZStack(alignment: .topTrailing) { let t = text .lineLimit(2) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .topLeading) - .padding(.leading, 8) - .padding(.trailing, 36) + .padding(.leading, hasFilePreview ? 0 : 8) + .padding(.trailing, hasFilePreview ? 38 : 36) + .offset(x: hasFilePreview ? -2 : 0) + .fixedSize(horizontal: false, vertical: true) if !showChatPreviews && !draft { t.privacySensitive(true).redacted(reason: .privacy) } else { t } - let s = chat.chatStats - if s.unreadCount > 0 || s.unreadChat { - unreadCountText(s.unreadCount) - .font(.caption) - .foregroundColor(.white) - .padding(.horizontal, 4) - .frame(minWidth: 18, minHeight: 18) - .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary) - .cornerRadius(10) - } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local { - Image(systemName: "speaker.slash.fill") - .foregroundColor(theme.colors.secondary) - } else if chat.chatInfo.chatSettings?.favorite ?? false { - Image(systemName: "star.fill") - .resizable() - .scaledToFill() - .frame(width: 18, height: 18) - .padding(.trailing, 1) - .foregroundColor(.secondary.opacity(0.65)) - } + chatInfoIcon(chat).frame(minWidth: 37, alignment: .trailing) + } + } + + @ViewBuilder private func chatInfoIcon(_ chat: Chat) -> some View { + let s = chat.chatStats + if s.unreadCount > 0 || s.unreadChat { + unreadCountText(s.unreadCount) + .font(.caption) + .foregroundColor(.white) + .padding(.horizontal, 4) + .frame(minWidth: 18, minHeight: 18) + .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary) + .cornerRadius(10) + } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local { + Image(systemName: "speaker.slash.fill") + .foregroundColor(theme.colors.secondary) + } else if chat.chatInfo.chatSettings?.favorite ?? false { + Image(systemName: "star.fill") + .resizable() + .scaledToFill() + .frame(width: 18, height: 18) + .padding(.trailing, 1) + .foregroundColor(.secondary.opacity(0.65)) + } else { + Color.clear.frame(width: 0) } } @@ -172,7 +236,7 @@ struct ChatPreviewView: View { func chatItemPreview(_ cItem: ChatItem) -> Text { let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText() let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil - return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false, secondaryColor: theme.colors.secondary) + return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary) // same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey; // can be refactored into a single function if functions calling these are changed to return same type @@ -196,18 +260,18 @@ struct ChatPreviewView: View { } } - @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View { + @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?, _ hasFilePreview: Bool = false) -> some View { if chatModel.draftChatId == chat.id, let draft = chatModel.draft { - chatPreviewLayout(messageDraft(draft), draft: true) + chatPreviewLayout(messageDraft(draft), draft: true, hasFilePreview) } else if let cItem = cItem { - chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem)) + chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem), hasFilePreview) } else { switch (chat.chatInfo) { case let .direct(contact): if contact.activeConn == nil && contact.profile.contactLink != nil { chatPreviewInfoText("Tap to Connect") .foregroundColor(theme.colors.primary) - } else if !contact.ready && contact.activeConn != nil { + } else if !contact.sndReady && contact.activeConn != nil { if contact.nextSendGrpInv { chatPreviewInfoText("send direct message") } else if contact.active { @@ -225,6 +289,54 @@ struct ChatPreviewView: View { } } + @ViewBuilder func chatItemContentPreview(_ chat: Chat, _ ci: ChatItem) -> some View { + let mc = ci.content.msgContent + switch mc { + case let .link(_, preview): + smallContentPreview( + ZStack(alignment: .topTrailing) { + Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 36, height: 36) + ZStack { + Image(systemName: "arrow.up.right") + .resizable() + .foregroundColor(Color.white) + .font(.system(size: 15, weight: .black)) + .frame(width: 8, height: 8) + } + .frame(width: 16, height: 16) + .background(Color.black.opacity(0.25)) + .cornerRadius(8) + } + .onTapGesture { + UIApplication.shared.open(preview.uri) + } + ) + case let .image(_, image): + smallContentPreview( + CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: 36, smallView: true, showFullScreenImage: $showFullscreenGallery) + .environmentObject(ReverseListScrollModel()) + ) + case let .video(_,image, duration): + smallContentPreview( + CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: 36, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery) + .environmentObject(ReverseListScrollModel()) + ) + case let .voice(_, duration): + smallContentPreviewVoice( + CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: Binding.constant(true), smallView: true) + ) + case .file: + smallContentPreviewFile( + CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallView: true) + ) + default: EmptyView() + } + } + + @ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View { groupInfo.membership.memberIncognito ? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)") @@ -294,10 +406,50 @@ struct ChatPreviewView: View { } } +func smallContentPreview(_ view: some View) -> some View { + ZStack { + view + .frame(width: 36, height: 36) + } + .cornerRadius(8) + .overlay(RoundedRectangle(cornerSize: CGSize(width: 8, height: 8)) + .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)) + .padding([.top, .leading], 3) + .offset(x: 6) +} + +func smallContentPreviewVoice(_ view: some View) -> some View { + ZStack { + view + .frame(height: voiceMessageSizeBasedOnSquareSize(36)) + } + .padding(.leading, 8) + .padding(.top, 6) +} + +func smallContentPreviewFile(_ view: some View) -> some View { + ZStack { + view + .frame(width: 36, height: 36) + } + .padding(.top, 2) + .padding(.leading, 5) +} + func unreadCountText(_ n: Int) -> Text { Text(n > 999 ? "\(n / 1000)k" : n > 0 ? "\(n)" : "") } +private struct ActiveContentPreview: Equatable { + var chat: Chat + var ci: ChatItem + var mc: MsgContent + + static func == (lhs: ActiveContentPreview, rhs: ActiveContentPreview) -> Bool { + lhs.chat.id == rhs.chat.id && lhs.ci.id == rhs.ci.id && lhs.mc == rhs.mc + } +} + struct ChatPreviewView_Previews: PreviewProvider { static var previews: some View { Group { diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index b7e641a338..0f64b632dc 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -21,7 +21,7 @@ struct ContactConnectionInfo: View { enum CCInfoAlert: Identifiable { case deleteInvitationAlert - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -102,7 +102,7 @@ struct ContactConnectionInfo: View { } success: { dismiss() } - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .error(title, error): return mkAlert(title: title, message: error) } } .onAppear { diff --git a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift index f210809a09..2e0dd9d9e4 100644 --- a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift +++ b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift @@ -12,11 +12,12 @@ import SimpleXChat struct ServersSummaryView: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme - @Binding var serversSummary: PresentedServersSummary? + @State private var serversSummary: PresentedServersSummary? = nil @State private var selectedUserCategory: PresentedUserCategory = .allUsers @State private var selectedServerType: PresentedServerType = .smp @State private var selectedSMPServer: String? = nil @State private var selectedXFTPServer: String? = nil + @State private var timer: Timer? = nil @State private var alert: SomeAlert? @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false @@ -47,10 +48,36 @@ struct ServersSummaryView: View { if m.users.filter({ u in u.user.activeUser || !u.user.hidden }).count == 1 { selectedUserCategory = .currentUser } + getServersSummary() + startTimer() + } + .onDisappear { + stopTimer() } .alert(item: $alert) { $0.alert } } + private func startTimer() { + timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + if AppChatState.shared.value == .active { + getServersSummary() + } + } + } + + private func getServersSummary() { + do { + serversSummary = try getAgentServersSummary() + } catch let error { + logger.error("getAgentServersSummary error: \(responseError(error))") + } + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } + private func shareButton() -> some View { Button { if let serversSummary = serversSummary { @@ -75,8 +102,8 @@ struct ServersSummaryView: View { Group { if m.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 { Picker("User selection", selection: $selectedUserCategory) { - Text("All users").tag(PresentedUserCategory.allUsers) - Text("Current user").tag(PresentedUserCategory.currentUser) + Text("All profiles").tag(PresentedUserCategory.allUsers) + Text("Current profile").tag(PresentedUserCategory.currentUser) } .pickerStyle(.segmented) } @@ -176,12 +203,13 @@ struct ServersSummaryView: View { Section { infoRow("Active connections", numOrDash(totals.subs.ssActive)) infoRow("Total", numOrDash(totals.subs.total)) + Toggle("Show percentage", isOn: $showSubscriptionPercentage) } header: { HStack { Text("Message reception") - SubscriptionStatusIndicatorView(subs: totals.subs, sess: totals.sessions) + SubscriptionStatusIndicatorView(subs: totals.subs, hasSess: totals.sessions.hasSess) if showSubscriptionPercentage { - SubscriptionStatusPercentageView(subs: totals.subs, sess: totals.sessions) + SubscriptionStatusPercentageView(subs: totals.subs, hasSess: totals.sessions.hasSess) } } } @@ -259,9 +287,9 @@ struct ServersSummaryView: View { if let subs = srvSumm.subs { Spacer() if showSubscriptionPercentage { - SubscriptionStatusPercentageView(subs: subs, sess: srvSumm.sessionsOrNew) + SubscriptionStatusPercentageView(subs: subs, hasSess: srvSumm.sessionsOrNew.hasSess) } - SubscriptionStatusIndicatorView(subs: subs, sess: srvSumm.sessionsOrNew) + SubscriptionStatusIndicatorView(subs: subs, hasSess: srvSumm.sessionsOrNew.hasSess) } else if let sess = srvSumm.sessions { Spacer() Image(systemName: "arrow.up.circle") @@ -355,6 +383,7 @@ struct ServersSummaryView: View { Task { do { try await resetAgentServersStats() + getServersSummary() } catch let error { alert = SomeAlert( alert: mkAlert( @@ -379,11 +408,11 @@ struct ServersSummaryView: View { struct SubscriptionStatusIndicatorView: View { @EnvironmentObject var m: ChatModel var subs: SMPServerSubs - var sess: ServerSessions + var hasSess: Bool var body: some View { let onionHosts = networkUseOnionHostsGroupDefault.get() - let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess) + let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess) if #available(iOS 16.0, *) { Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue) .foregroundColor(color) @@ -397,18 +426,18 @@ struct SubscriptionStatusIndicatorView: View { struct SubscriptionStatusPercentageView: View { @EnvironmentObject var m: ChatModel var subs: SMPServerSubs - var sess: ServerSessions + var hasSess: Bool var body: some View { let onionHosts = networkUseOnionHostsGroupDefault.get() - let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess) + let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess) Text(verbatim: "\(Int(floor(statusPercent * 100)))%") .foregroundColor(.secondary) .font(.caption) } } -func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ sess: ServerSessions) -> (Color, Double, Double, Double) { +func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ hasSess: Bool) -> (Color, Double, Double, Double) { func roundedToQuarter(_ n: Double) -> Double { n >= 1 ? 1 : n <= 0 ? 0 @@ -423,12 +452,12 @@ func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHos ? ( subs.ssActive == 0 ? ( - sess.ssConnected == 0 ? noConnColorAndPercent : (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + hasSess ? (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) : noConnColorAndPercent ) : ( // ssActive > 0 - sess.ssConnected == 0 - ? (.orange, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) // This would mean implementation error - : (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + hasSess + ? (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + : (.orange, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) // This would mean implementation error ) ) : noConnColorAndPercent @@ -481,9 +510,9 @@ struct SMPServerSummaryView: View { } header: { HStack { Text("Message reception") - SubscriptionStatusIndicatorView(subs: subs, sess: summary.sessionsOrNew) + SubscriptionStatusIndicatorView(subs: subs, hasSess: summary.sessionsOrNew.hasSess) if showSubscriptionPercentage { - SubscriptionStatusPercentageView(subs: subs, sess: summary.sessionsOrNew) + SubscriptionStatusPercentageView(subs: subs, hasSess: summary.sessionsOrNew.hasSess) } } } @@ -588,7 +617,7 @@ struct DetailedSMPStatsView: View { infoRow(Text(verbatim: "NO_MSG errors"), numOrDash(stats._ackNoMsgErrs)).padding(.leading, 24) infoRow("other errors", numOrDash(stats._ackOtherErrs)).padding(.leading, 24) } - Section { + Section("Connections") { infoRow("Created", numOrDash(stats._connCreated)) infoRow("Secured", numOrDash(stats._connCreated)) infoRow("Completed", numOrDash(stats._connCompleted)) @@ -597,8 +626,12 @@ struct DetailedSMPStatsView: View { infoRowTwoValues("Subscribed", "attempts", stats._connSubscribed, stats._connSubAttempts) infoRow("Subscriptions ignored", numOrDash(stats._connSubIgnored)) infoRow("Subscription errors", numOrDash(stats._connSubErrs)) + } + Section { + infoRowTwoValues("Enabled", "attempts", stats._ntfKey, stats._ntfKeyAttempts) + infoRowTwoValues("Disabled", "attempts", stats._ntfKeyDeleted, stats._ntfKeyDeleteAttempts) } header: { - Text("Connections") + Text("Connection notifications") } footer: { Text("Starting from \(localTimestamp(statsStartedAt)).") } @@ -711,7 +744,5 @@ struct DetailedXFTPStatsView: View { } #Preview { - ServersSummaryView( - serversSummary: Binding.constant(nil) - ) + ServersSummaryView() } diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift index f8d282a6d1..9d71e2a788 100644 --- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -64,7 +64,7 @@ struct DatabaseErrorView: View { case let .migrationError(mtrError): titleText("Incompatible database version") fileNameText(dbFile) - Text("Error: ") + Text(DatabaseErrorView.mtrErrorDescription(mtrError)) + Text("Error: ") + Text(mtrErrorDescription(mtrError)) } case let .errorSQL(dbFile, migrationSQLError): titleText("Database error") @@ -105,15 +105,6 @@ struct DatabaseErrorView: View { Text("Migrations: \(ms.joined(separator: ", "))") } - static func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey { - switch err { - case let .noDown(dbMigrations): - return "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))" - case let .different(appMigration, dbMigration): - return "different migration in the app/database: \(appMigration) / \(dbMigration)" - } - } - private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View { PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit) } diff --git a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift index 844b5ab4d3..40d62e009b 100644 --- a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift +++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift @@ -16,18 +16,10 @@ struct ChatInfoImage: View { var color = Color(uiColor: .tertiarySystemGroupedBackground) var body: some View { - var iconName: String - switch chat.chatInfo { - case .direct: iconName = "person.crop.circle.fill" - case .group: iconName = "person.2.circle.fill" - case .local: iconName = "folder.circle.fill" - case .contactRequest: iconName = "person.crop.circle.fill" - default: iconName = "circle.fill" - } let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color } return ProfileImage( imageStr: chat.chatInfo.image, - iconName: iconName, + iconName: chatIconName(chat.chatInfo), size: size, color: iconColor ) diff --git a/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift b/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift index 6eef843d37..d8ddc1c306 100644 --- a/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift +++ b/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift @@ -23,8 +23,10 @@ struct ChatViewBackground: ViewModifier { var image = context.resolve(image) let rect = CGRectMake(0, 0, size.width, size.height) func repeatDraw(_ imageScale: CGFloat) { + // Prevent range bounds crash and dividing by zero + if size.height == 0 || size.width == 0 || image.size.height == 0 || image.size.width == 0 { return } image.shading = .color(tint) - let scale = imageScale * 1.57 // for some reason a wallpaper on iOS looks smaller than on Android + let scale = imageScale * 2.5 // scale wallpaper for iOS for h in 0 ... Int(size.height / image.size.height / scale) { for w in 0 ... Int(size.width / image.size.width / scale) { let rect = CGRectMake(CGFloat(w) * image.size.width * scale, CGFloat(h) * image.size.height * scale, image.size.width * scale, image.size.height * scale) diff --git a/apps/ios/Shared/Views/Helpers/VideoUtils.swift b/apps/ios/Shared/Views/Helpers/VideoUtils.swift deleted file mode 100644 index e13893de6e..0000000000 --- a/apps/ios/Shared/Views/Helpers/VideoUtils.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// VideoUtils.swift -// SimpleX (iOS) -// -// Created by Avently on 25.12.2023. -// Copyright © 2023 SimpleX Chat. All rights reserved. -// - -import AVFoundation -import Foundation -import SimpleXChat - -func makeVideoQualityLower(_ input: URL, outputUrl: URL) async -> Bool { - let asset: AVURLAsset = AVURLAsset(url: input, options: nil) - if let s = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset640x480) { - s.outputURL = outputUrl - s.outputFileType = .mp4 - s.metadataItemFilter = AVMetadataItemFilter.forSharing() - await s.export() - if let err = s.error { - logger.error("Failed to export video with error: \(err)") - } - return s.status == .completed - } - return false -} diff --git a/apps/ios/Shared/Views/Helpers/ViewModifiers.swift b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift index 7e2655f4f7..fee0f262cb 100644 --- a/apps/ios/Shared/Views/Helpers/ViewModifiers.swift +++ b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift @@ -17,3 +17,37 @@ extension View { } } } + +extension Notification.Name { + static let chatViewWillBeginScrolling = Notification.Name("chatWillBeginScrolling") +} + +struct PrivacyBlur: ViewModifier { + var enabled: Bool = true + @Binding var blurred: Bool + @AppStorage(DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) private var blurRadius: Int = 0 + + func body(content: Content) -> some View { + if blurRadius > 0 { + // parallel ifs are necessary here because otherwise some views flicker, + // e.g. when playing video + content + .blur(radius: blurred && enabled ? CGFloat(blurRadius) * 0.5 : 0) + .overlay { + if (blurred && enabled) { + Color.clear.contentShape(Rectangle()) + .onTapGesture { + blurred = false + } + } + } + .onReceive(NotificationCenter.default.publisher(for: .chatViewWillBeginScrolling)) { _ in + if !blurred { + blurred = true + } + } + } else { + content + } + } +} diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift index ec2ce883c5..028a6d179f 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -662,7 +662,7 @@ private struct PassphraseConfirmationView: View { if case .chatCmdError(_, .errorDatabase(.errorOpen(.errorNotADatabase))) = error as? ChatResponse { showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert) } else { - alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(String(describing: error))) + alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(responseError(error))) } } } diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index 107785e336..67ea1008cd 100644 --- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -331,7 +331,7 @@ struct MigrateToDevice: View { case let .migrationError(mtrError): ("Incompatible database version", nil, - "\(NSLocalizedString("Error: ", comment: "")) \(DatabaseErrorView.mtrErrorDescription(mtrError))", + "\(NSLocalizedString("Error: ", comment: "")) \(mtrErrorDescription(mtrError))", nil) } default: ("Error", nil, "Unknown error", nil) diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 30d200b6e3..be063334d3 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -37,7 +37,7 @@ struct ConnectDesktopView: View { case badInvitationError case badVersionError(version: String?) case desktopDisconnectedError - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -160,7 +160,7 @@ struct ConnectDesktopView: View { case .desktopDisconnectedError: Alert(title: Text("Connection terminated")) case let .error(title, error): - Alert(title: Text(title), message: Text(error)) + mkAlert(title: title, message: error) } } .interactiveDismissDisabled(m.activeRemoteCtrl) diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift index 8c68d70526..ac81c42b2e 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -32,6 +32,7 @@ extension AppSettings { if let val = privacyShowChatPreviews { def.setValue(val, forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) } if let val = privacySaveLastDraft { def.setValue(val, forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) } if let val = privacyProtectScreen { def.setValue(val, forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) } + if let val = privacyMediaBlurRadius { def.setValue(val, forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) } if let val = notificationMode { ChatModel.shared.notificationMode = val.toNotificationsMode() } if let val = notificationPreviewMode { ntfPreviewModeGroupDefault.set(val) } if let val = webrtcPolicyRelay { def.setValue(val, forKey: DEFAULT_WEBRTC_POLICY_RELAY) } @@ -62,6 +63,7 @@ extension AppSettings { c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) c.privacySaveLastDraft = def.bool(forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) c.privacyProtectScreen = def.bool(forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) + c.privacyMediaBlurRadius = def.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) c.notificationMode = AppSettingsNotificationMode.from(ChatModel.shared.notificationMode) c.notificationPreviewMode = ntfPreviewModeGroupDefault.get() c.webrtcPolicyRelay = def.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY) diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift index b07f0f6a13..3d93a92e08 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -32,7 +32,6 @@ struct NetworkAndServers: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false - @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false @State private var cfgLoaded = false @State private var currentNetCfg = NetCfg.defaults @State private var netCfg = NetCfg.defaults @@ -62,8 +61,6 @@ struct NetworkAndServers: View { Text("XFTP servers") } - Toggle("Subscription percentage", isOn: $showSubscriptionPercentage) - Picker("Use .onion hosts", selection: $onionHosts) { ForEach(OnionHosts.values, id: \.self) { Text($0.text) } } diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 879ee301f2..6b1a619c18 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -22,6 +22,7 @@ struct PrivacySettings: View { @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @State private var currentLAMode = privacyLocalAuthModeDefault.get() + @AppStorage(DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) private var privacyMediaBlurRadius: Int = 0 @State private var contactReceipts = false @State private var contactReceiptsReset = false @State private var contactReceiptsOverrides = 0 @@ -113,6 +114,22 @@ struct PrivacySettings: View { privacyAcceptImagesGroupDefault.set($0) } } + settingsRow("circle.rectangle.filled.pattern.diagonalline", color: theme.colors.secondary) { + Picker("Blur media", selection: $privacyMediaBlurRadius) { + let values = [0, 12, 24, 48] + ([0, 12, 24, 48].contains(privacyMediaBlurRadius) ? [] : [privacyMediaBlurRadius]) + ForEach(values, id: \.self) { radius in + let text: String = switch radius { + case 0: NSLocalizedString("Off", comment: "blur media") + case 12: NSLocalizedString("Soft", comment: "blur media") + case 24: NSLocalizedString("Medium", comment: "blur media") + case 48: NSLocalizedString("Strong", comment: "blur media") + default: "\(radius)" + } + Text(text) + } + } + } + .frame(height: 36) settingsRow("network.badge.shield.half.filled", color: theme.colors.secondary) { Toggle("Protect IP address", isOn: $askToApproveRelays) } diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift index 6433168810..da29dfac29 100644 --- a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift @@ -175,10 +175,6 @@ func testServerConnection(server: Binding) async -> ProtocolTestFailu } } -func serverHostname(_ srv: String) -> String { - parseServerAddress(srv)?.hostnames.first ?? srv -} - struct ProtocolServerView_Previews: PreviewProvider { static var previews: some View { ProtocolServerView( diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index b195efb985..0a83db1e5c 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -34,6 +34,7 @@ let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews" let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft" let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen" let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet" +let DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS = "privacyMediaBlurRadius" let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls" let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName" let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime" @@ -87,6 +88,7 @@ let appDefaults: [String: Any] = [ DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true, DEFAULT_PRIVACY_PROTECT_SCREEN: false, DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false, + DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS: 0, DEFAULT_EXPERIMENTAL_CALLS: false, DEFAULT_CHAT_V3_DB_MIGRATION: V3DBMigrationState.offer.rawValue, DEFAULT_DEVELOPER_TOOLS: false, diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index a22a10cd9c..fa95c51d36 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -30,7 +30,7 @@ struct UserAddressView: View { case deleteAddress case profileAddress(on: Bool) case shareOnCreate - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -185,7 +185,7 @@ struct UserAddressView: View { }, secondaryButton: .cancel() ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index 13b9b2b097..160130bccc 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -30,7 +30,7 @@ struct UserProfilesView: View { case hiddenProfilesNotice case muteProfileAlert case activateUserError(error: String) - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -172,7 +172,7 @@ struct UserProfilesView: View { message: Text(err) ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } } diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index c17ffcd571..9ac4d8ced4 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -709,8 +709,8 @@ Всички нови съобщения от %@ ще бъдат скрити! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1705,8 +1705,8 @@ This is your own one-time link! Текуща парола… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5639,6 +5639,10 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5779,6 +5783,10 @@ Enable in *Network & servers* settings. Show message status No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Показване на визуализация @@ -6002,10 +6010,6 @@ Enable in *Network & servers* settings. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -7060,8 +7064,8 @@ Repeat join request? Можете да го направите видим за вашите контакти в SimpleX чрез Настройки. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Вече можете да изпращате съобщения до %@ notification body @@ -7468,7 +7472,7 @@ SimpleX сървърите не могат да видят вашия профи blocked by admin блокиран от админ - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 9cc8d12f74..c37ee1038e 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -687,8 +687,8 @@ All new messages from %@ will be hidden! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1635,8 +1635,8 @@ This is your own one-time link! Aktuální přístupová fráze… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5437,6 +5437,10 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5572,6 +5576,10 @@ Enable in *Network & servers* settings. Show message status No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Zobrazení náhledu @@ -5789,10 +5797,6 @@ Enable in *Network & servers* settings. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -6793,8 +6797,8 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Nyní můžete posílat zprávy %@ notification body @@ -7187,7 +7191,7 @@ Servery SimpleX nevidí váš profil. blocked by admin - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 341839d246..e3d36cf09e 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -554,6 +554,7 @@ Accent + Akzent No comment provided by engineer. @@ -579,10 +580,12 @@ Acknowledged + Bestätigt No comment provided by engineer. Acknowledgement errors + Fehler bei der Bestätigung No comment provided by engineer. @@ -631,14 +634,17 @@ Additional accent + Erste Akzentfarbe No comment provided by engineer. Additional accent 2 + Zusätzlicher Akzent 2 No comment provided by engineer. Additional secondary + Zweite Akzentfarbe No comment provided by engineer. @@ -668,6 +674,7 @@ Advanced settings + Erweiterte Einstellungen No comment provided by engineer. @@ -677,7 +684,7 @@ All chats and messages will be deleted - this cannot be undone! - Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden! + Alle Chats und Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -687,6 +694,7 @@ All data is private to your device. + Alle Daten sind auf Ihrem Gerät geschützt. No comment provided by engineer. @@ -696,12 +704,12 @@ All messages will be deleted - this cannot be undone! - Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden! + Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. - Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. + Alle Nachrichten werden gelöscht . Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. No comment provided by engineer. @@ -709,8 +717,9 @@ Von %@ werden alle neuen Nachrichten ausgeblendet! No comment provided by engineer. - - All users + + All profiles + Alle Profile No comment provided by engineer. @@ -915,6 +924,7 @@ Apply to + Anwenden auf No comment provided by engineer. @@ -994,6 +1004,7 @@ Background + Hintergrund-Farbe No comment provided by engineer. @@ -1023,6 +1034,7 @@ Black + Schwarz No comment provided by engineer. @@ -1137,6 +1149,7 @@ Cannot forward message + Die Nachricht kann nicht weitergeleitet werden No comment provided by engineer. @@ -1212,6 +1225,7 @@ Chat colors + Chat-Farben No comment provided by engineer. @@ -1261,6 +1275,7 @@ Chat theme + Chat-Design No comment provided by engineer. @@ -1295,14 +1310,17 @@ Chunks deleted + Daten-Pakete gelöscht No comment provided by engineer. Chunks downloaded + Daten-Pakete heruntergeladen No comment provided by engineer. Chunks uploaded + Daten-Pakete hochgeladen No comment provided by engineer. @@ -1332,6 +1350,7 @@ Color mode + Farbvariante No comment provided by engineer. @@ -1346,6 +1365,7 @@ Completed + Abgeschlossen No comment provided by engineer. @@ -1463,6 +1483,7 @@ Das ist Ihr eigener Einmal-Link! Connected + Verbunden No comment provided by engineer. @@ -1472,6 +1493,7 @@ Das ist Ihr eigener Einmal-Link! Connected servers + Verbundene Server No comment provided by engineer. @@ -1481,6 +1503,7 @@ Das ist Ihr eigener Einmal-Link! Connecting + Verbinden No comment provided by engineer. @@ -1530,10 +1553,12 @@ Das ist Ihr eigener Einmal-Link! Connection with desktop stopped + Die Verbindung mit dem Desktop wurde gestoppt No comment provided by engineer. Connections + Verbindungen No comment provided by engineer. @@ -1593,6 +1618,7 @@ Das ist Ihr eigener Einmal-Link! Copy error + Fehlermeldung kopieren No comment provided by engineer. @@ -1672,6 +1698,7 @@ Das ist Ihr eigener Einmal-Link! Created + Erstellt No comment provided by engineer. @@ -1709,8 +1736,9 @@ Das ist Ihr eigener Einmal-Link! Aktuelles Passwort… No comment provided by engineer. - - Current user + + Current profile + Aktueller Profil No comment provided by engineer. @@ -1725,6 +1753,7 @@ Das ist Ihr eigener Einmal-Link! Customize theme + Design anpassen No comment provided by engineer. @@ -1734,6 +1763,7 @@ Das ist Ihr eigener Einmal-Link! Dark mode colors + Farben für die dunkle Variante No comment provided by engineer. @@ -1923,7 +1953,7 @@ Das ist Ihr eigener Einmal-Link! Delete contact? This cannot be undone! Kontakt löschen? -Das kann nicht rückgängig gemacht werden! +Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -2023,7 +2053,7 @@ Das kann nicht rückgängig gemacht werden! Delete pending connection? - Die ausstehende Verbindung löschen? + Ausstehende Verbindung löschen? No comment provided by engineer. @@ -2043,6 +2073,7 @@ Das kann nicht rückgängig gemacht werden! Deleted + Gelöscht No comment provided by engineer. @@ -2057,6 +2088,7 @@ Das kann nicht rückgängig gemacht werden! Deletion errors + Fehler beim Löschen No comment provided by engineer. @@ -2101,10 +2133,12 @@ Das kann nicht rückgängig gemacht werden! Detailed statistics + Detaillierte Statistiken No comment provided by engineer. Details + Details No comment provided by engineer. @@ -2264,6 +2298,7 @@ Das kann nicht rückgängig gemacht werden! Download errors + Fehler beim Herunterladen No comment provided by engineer. @@ -2278,10 +2313,12 @@ Das kann nicht rückgängig gemacht werden! Downloaded + Heruntergeladen No comment provided by engineer. Downloaded files + Heruntergeladene Dateien No comment provided by engineer. @@ -2656,6 +2693,7 @@ Das kann nicht rückgängig gemacht werden! Error exporting theme: %@ + Fehler beim Exportieren des Designs: %@ No comment provided by engineer. @@ -2685,10 +2723,12 @@ Das kann nicht rückgängig gemacht werden! Error reconnecting server + Fehler beim Wiederherstellen der Verbindung zum Server No comment provided by engineer. Error reconnecting servers + Fehler beim Wiederherstellen der Verbindungen zu den Servern No comment provided by engineer. @@ -2698,6 +2738,7 @@ Das kann nicht rückgängig gemacht werden! Error resetting statistics + Fehler beim Zurücksetzen der Statistiken No comment provided by engineer. @@ -2833,6 +2874,7 @@ Das kann nicht rückgängig gemacht werden! Errors + Fehler No comment provided by engineer. @@ -2862,6 +2904,7 @@ Das kann nicht rückgängig gemacht werden! Export theme + Design exportieren No comment provided by engineer. @@ -2901,22 +2944,27 @@ Das kann nicht rückgängig gemacht werden! File error + Datei-Fehler No comment provided by engineer. File not found - most likely file was deleted or cancelled. + Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen. file error text File server error: %@ + Datei-Server Fehler: %@ file error text File status + Datei-Status No comment provided by engineer. File status: %@ + Datei-Status: %@ copied message info @@ -3110,10 +3158,12 @@ Fehler: %2$@ Good afternoon! + Guten Nachmittag! message preview Good morning! + Guten Morgen! message preview @@ -3238,12 +3288,12 @@ Fehler: %2$@ Group will be deleted for all members - this cannot be undone! - Die Gruppe wird für alle Mitglieder gelöscht - dies kann nicht rückgängig gemacht werden! + Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. Group will be deleted for you - this cannot be undone! - Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden! + Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -3398,6 +3448,7 @@ Fehler: %2$@ Import theme + Design importieren No comment provided by engineer. @@ -3524,6 +3575,7 @@ Fehler: %2$@ Interface colors + Interface-Farben No comment provided by engineer. @@ -3871,6 +3923,7 @@ Das ist Ihr Link für die Gruppe %@! Member inactive + Mitglied inaktiv item status text @@ -3885,11 +3938,12 @@ Das ist Ihr Link für die Gruppe %@! Member will be removed from group - this cannot be undone! - Das Mitglied wird aus der Gruppe entfernt - dies kann nicht rückgängig gemacht werden! + Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. Menus + Menüs No comment provided by engineer. @@ -3914,10 +3968,12 @@ Das ist Ihr Link für die Gruppe %@! Message forwarded + Nachricht weitergeleitet item status text Message may be delivered later if member becomes active. + Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird. item status description @@ -3961,10 +4017,12 @@ Das ist Ihr Link für die Gruppe %@! Message status + Nachrichten-Status No comment provided by engineer. Message status: %@ + Nachrichten-Status: %@ copied message info @@ -3994,10 +4052,12 @@ Das ist Ihr Link für die Gruppe %@! Messages received + Empfangene Nachrichten No comment provided by engineer. Messages sent + Gesendete Nachrichten No comment provided by engineer. @@ -4237,6 +4297,7 @@ Das ist Ihr Link für die Gruppe %@! No direct connection yet, message is forwarded by admin. + Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet. item status description @@ -4256,6 +4317,7 @@ Das ist Ihr Link für die Gruppe %@! No info, try to reload + Keine Information - es wird versucht neu zu laden No comment provided by engineer. @@ -4444,6 +4506,7 @@ Das ist Ihr Link für die Gruppe %@! Open server settings + Server-Einstellungen öffnen No comment provided by engineer. @@ -4557,6 +4620,7 @@ Das ist Ihr Link für die Gruppe %@! Pending + Ausstehend No comment provided by engineer. @@ -4587,6 +4651,8 @@ Das ist Ihr Link für die Gruppe %@! Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers. + Bitte überprüfen Sie, ob sich das Mobiltelefon und die Desktop-App im gleichen lokalen Netzwerk befinden, und die Desktop-Firewall die Verbindung erlaubt. +Bitte teilen Sie weitere mögliche Probleme den Entwicklern mit. No comment provided by engineer. @@ -4692,6 +4758,7 @@ Fehler: %@ Previously connected servers + Bisher verbundene Server No comment provided by engineer. @@ -4765,6 +4832,7 @@ Fehler: %@ Profile theme + Profil-Design No comment provided by engineer. @@ -4851,10 +4919,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Proxied + Proxy No comment provided by engineer. Proxied servers + Proxy-Server No comment provided by engineer. @@ -4924,6 +4994,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Receive errors + Fehler beim Empfang No comment provided by engineer. @@ -4948,14 +5019,17 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Received messages + Empfangene Nachrichten No comment provided by engineer. Received reply + Empfangene Antwort No comment provided by engineer. Received total + Summe aller empfangenen Nachrichten No comment provided by engineer. @@ -4990,6 +5064,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Reconnect + Neu verbinden No comment provided by engineer. @@ -4999,18 +5074,22 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Reconnect all servers + Alle Server neu verbinden No comment provided by engineer. Reconnect all servers? + Alle Server neu verbinden? No comment provided by engineer. Reconnect server to force message delivery. It uses additional traffic. + Um die Auslieferung von Nachrichten zu erzwingen, wird der Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt. No comment provided by engineer. Reconnect server? + Server neu verbinden? No comment provided by engineer. @@ -5065,6 +5144,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Remove image + Bild entfernen No comment provided by engineer. @@ -5139,10 +5219,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Reset all statistics + Alle Statistiken zurücksetzen No comment provided by engineer. Reset all statistics? + Alle Statistiken zurücksetzen? No comment provided by engineer. @@ -5152,6 +5234,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Reset to app theme + Auf das App-Design zurücksetzen No comment provided by engineer. @@ -5161,6 +5244,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Reset to user theme + Auf das Benutzer-spezifische Design zurücksetzen No comment provided by engineer. @@ -5235,6 +5319,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. SMP server + SMP-Server No comment provided by engineer. @@ -5354,10 +5439,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Scale + Skalieren No comment provided by engineer. Scan / Paste link + Link scannen / einfügen No comment provided by engineer. @@ -5402,6 +5489,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Secondary + Zweite Farbe No comment provided by engineer. @@ -5411,6 +5499,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Secured + Abgesichert No comment provided by engineer. @@ -5430,6 +5519,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Selected chat preferences prohibit this message. + Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt. No comment provided by engineer. @@ -5484,6 +5574,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Send errors + Fehler beim Senden No comment provided by engineer. @@ -5598,6 +5689,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Sent directly + Direkt gesendet No comment provided by engineer. @@ -5612,6 +5704,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Sent messages + Gesendete Nachrichten No comment provided by engineer. @@ -5621,18 +5714,22 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Sent reply + Gesendete Antwort No comment provided by engineer. Sent total + Summe aller gesendeten Nachrichten No comment provided by engineer. Sent via proxy + Über einen Proxy gesendet No comment provided by engineer. Server address + Server-Adresse No comment provided by engineer. @@ -5661,6 +5758,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Server type + Server-Typ No comment provided by engineer. @@ -5668,6 +5766,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Die Server-Version ist nicht mit den Netzwerk-Einstellungen kompatibel. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5679,10 +5781,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Servers info + Server-Informationen No comment provided by engineer. Servers statistics will be reset - this cannot be undone! + Die Serverstatistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -5702,6 +5806,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Set default theme + Default-Design einstellen No comment provided by engineer. @@ -5809,6 +5914,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Nachrichtenstatus anzeigen No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Vorschau anzeigen @@ -5826,6 +5935,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. SimpleX + SimpleX No comment provided by engineer. @@ -5905,6 +6015,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Size + Größe No comment provided by engineer. @@ -5954,10 +6065,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Starting from %@. + Beginnend mit %@. No comment provided by engineer. Statistics + Statistiken No comment provided by engineer. @@ -6027,18 +6140,17 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Subscribed + Abonniert No comment provided by engineer. Subscription errors - No comment provided by engineer. - - - Subscription percentage + Fehler beim Abonnieren No comment provided by engineer. Subscriptions ignored + Nicht beachtete Abonnements No comment provided by engineer. @@ -6123,6 +6235,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Temporary file error + Temporärer Datei-Fehler No comment provided by engineer. @@ -6264,6 +6377,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Themes + Design No comment provided by engineer. @@ -6333,6 +6447,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro This link was used with another mobile device, please create a new link on the desktop. + Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App. No comment provided by engineer. @@ -6342,6 +6457,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Title + Bezeichnung No comment provided by engineer. @@ -6413,6 +6529,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Total + Summe aller Abonnements No comment provided by engineer. @@ -6422,6 +6539,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Transport sessions + Transport-Sitzungen No comment provided by engineer. @@ -6618,6 +6736,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Upload errors + Fehler beim Hochladen No comment provided by engineer. @@ -6632,10 +6751,12 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Uploaded + Hochgeladen No comment provided by engineer. Uploaded files + Hochgeladene Dateien No comment provided by engineer. @@ -6715,6 +6836,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s User selection + Benutzer-Auswahl No comment provided by engineer. @@ -6854,10 +6976,12 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Wallpaper accent + Wallpaper-Akzent No comment provided by engineer. Wallpaper background + Wallpaper-Hintergrund No comment provided by engineer. @@ -6967,6 +7091,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Wrong key or unknown file chunk address - most likely file is deleted. + Falscher Schlüssel oder unbekannte Daten-Paketadresse der Datei - höchstwahrscheinlich wurde die Datei gelöscht. file error text @@ -6976,6 +7101,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s XFTP server + XFTP-Server No comment provided by engineer. @@ -7062,6 +7188,7 @@ Verbindungsanfrage wiederholen? You are not connected to these servers. Private routing is used to deliver messages to them. + Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt. No comment provided by engineer. @@ -7099,8 +7226,8 @@ Verbindungsanfrage wiederholen? Sie können sie über Einstellungen für Ihre SimpleX-Kontakte sichtbar machen. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Sie können nun Nachrichten an %@ versenden notification body @@ -7472,6 +7599,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. attempts + Versuche No comment provided by engineer. @@ -7507,7 +7635,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. blocked by admin wurde vom Administrator blockiert - blocked chat item + marked deleted chat item preview text bold @@ -7666,6 +7794,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. decryption errors + Entschlüsselungs-Fehler No comment provided by engineer. @@ -7720,6 +7849,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. duplicates + Duplikate No comment provided by engineer. @@ -7804,6 +7934,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. expired + abgelaufen No comment provided by engineer. @@ -7838,6 +7969,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. inactive + Inaktiv No comment provided by engineer. @@ -8019,10 +8151,12 @@ SimpleX-Server können Ihr Profil nicht einsehen. other + andere No comment provided by engineer. other errors + Andere Fehler No comment provided by engineer. @@ -8348,7 +8482,7 @@ Zuletzt empfangene Nachricht: %2$@ SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Nutzern zu scannen und Videoanrufe durchzuführen. + SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Benutzern zu scannen und Videoanrufe durchzuführen. Privacy - Camera Usage Description diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 7bdf67cc94..a0ec9aca4a 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -718,9 +718,9 @@ All new messages from %@ will be hidden! No comment provided by engineer. - - All users - All users + + All profiles + All profiles No comment provided by engineer. @@ -1738,9 +1738,9 @@ This is your own one-time link! Current passphrase… No comment provided by engineer. - - Current user - Current user + + Current profile + Current profile No comment provided by engineer. @@ -5773,6 +5773,11 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. srv error text + + Server version is incompatible with network settings: %@. + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. Server version is incompatible with your app: %@. @@ -5918,6 +5923,11 @@ Enable in *Network & servers* settings. Show message status No comment provided by engineer. + + Show percentage + Show percentage + No comment provided by engineer. + Show preview Show preview @@ -6148,11 +6158,6 @@ Enable in *Network & servers* settings. Subscription errors No comment provided by engineer. - - Subscription percentage - Subscription percentage - No comment provided by engineer. - Subscriptions ignored Subscriptions ignored @@ -7231,9 +7236,9 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ - You can now send messages to %@ + + You can now chat with %@ + You can now chat with %@ notification body @@ -7640,7 +7645,7 @@ SimpleX servers cannot see your profile. blocked by admin blocked by admin - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index eb4933e69a..837ea06e96 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -709,8 +709,8 @@ ¡Los mensajes nuevos de %@ estarán ocultos! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1709,8 +1709,8 @@ This is your own one-time link! Contraseña actual… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5668,6 +5668,10 @@ Actívalo en ajustes de *Servidores y Redes*. La versión del servidor es incompatible con la configuración de red. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5809,6 +5813,10 @@ Actívalo en ajustes de *Servidores y Redes*. Estado del mensaje No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Mostrar vista previa @@ -6033,10 +6041,6 @@ Actívalo en ajustes de *Servidores y Redes*. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -7099,8 +7103,8 @@ Repeat join request? Puedes hacerlo visible para tus contactos de SimpleX en Configuración. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Ya puedes enviar mensajes a %@ notification body @@ -7507,7 +7511,7 @@ Los servidores SimpleX no pueden ver tu perfil. blocked by admin bloqueado por administrador - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 4959324895..07673d911c 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -682,8 +682,8 @@ All new messages from %@ will be hidden! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1628,8 +1628,8 @@ This is your own one-time link! Nykyinen tunnuslause… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5424,6 +5424,10 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5559,6 +5563,10 @@ Enable in *Network & servers* settings. Show message status No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Näytä esikatselu @@ -5775,10 +5783,6 @@ Enable in *Network & servers* settings. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -6778,8 +6782,8 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Voit nyt lähettää viestejä %@:lle notification body @@ -7172,7 +7176,7 @@ SimpleX-palvelimet eivät näe profiiliasi. blocked by admin - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index a3c5bd8c65..5c23f58e9c 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -709,8 +709,8 @@ Tous les nouveaux messages de %@ seront cachés ! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1709,8 +1709,8 @@ Il s'agit de votre propre lien unique ! Phrase secrète actuelle… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5668,6 +5668,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. La version du serveur est incompatible avec les paramètres du réseau. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5809,6 +5813,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Afficher le statut du message No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Afficher l'aperçu @@ -6033,10 +6041,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -7099,8 +7103,8 @@ Répéter la demande d'adhésion ? Vous pouvez le rendre visible à vos contacts SimpleX via Paramètres. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Vous pouvez maintenant envoyer des messages à %@ notification body @@ -7507,7 +7511,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. blocked by admin bloqué par l'administrateur - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 5241ae89c0..2dbd206209 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -554,6 +554,7 @@ Accent + Kiemelés No comment provided by engineer. @@ -579,10 +580,12 @@ Acknowledged + Nyugtázva No comment provided by engineer. Acknowledgement errors + Nyugtázott hibák No comment provided by engineer. @@ -631,14 +634,17 @@ Additional accent + További kiemelés No comment provided by engineer. Additional accent 2 + További kiemelés 2 No comment provided by engineer. Additional secondary + További másodlagos No comment provided by engineer. @@ -668,6 +674,7 @@ Advanced settings + Haladó beállítások No comment provided by engineer. @@ -687,6 +694,7 @@ All data is private to your device. + Minden adat biztonságban van a készülékén. No comment provided by engineer. @@ -709,8 +717,9 @@ Minden új üzenet elrejtésre kerül tőle: %@! No comment provided by engineer. - - All users + + All profiles + Minden profil No comment provided by engineer. @@ -915,6 +924,7 @@ Apply to + Alkalmazás erre No comment provided by engineer. @@ -994,6 +1004,7 @@ Background + Háttér No comment provided by engineer. @@ -1023,6 +1034,7 @@ Black + Fekete No comment provided by engineer. @@ -1137,6 +1149,7 @@ Cannot forward message + Nem lehet továbbítani az üzenetet No comment provided by engineer. @@ -1212,6 +1225,7 @@ Chat colors + Csevegés színei No comment provided by engineer. @@ -1261,6 +1275,7 @@ Chat theme + Csevegés témája No comment provided by engineer. @@ -1295,14 +1310,17 @@ Chunks deleted + Törölt fájltöredékek No comment provided by engineer. Chunks downloaded + Letöltött fájltöredékek No comment provided by engineer. Chunks uploaded + Feltöltött fájltöredékek No comment provided by engineer. @@ -1332,6 +1350,7 @@ Color mode + Színmód No comment provided by engineer. @@ -1346,6 +1365,7 @@ Completed + Elkészült No comment provided by engineer. @@ -1463,6 +1483,7 @@ Ez az egyszer használatos hivatkozása! Connected + Kapcsolódva No comment provided by engineer. @@ -1472,6 +1493,7 @@ Ez az egyszer használatos hivatkozása! Connected servers + Kapcsolódott kiszolgálók No comment provided by engineer. @@ -1481,6 +1503,7 @@ Ez az egyszer használatos hivatkozása! Connecting + Kapcsolódás No comment provided by engineer. @@ -1530,10 +1553,12 @@ Ez az egyszer használatos hivatkozása! Connection with desktop stopped + A kapcsolat a számítógéppel megszakadt No comment provided by engineer. Connections + Kapcsolatok No comment provided by engineer. @@ -1593,6 +1618,7 @@ Ez az egyszer használatos hivatkozása! Copy error + Másolási hiba No comment provided by engineer. @@ -1672,6 +1698,7 @@ Ez az egyszer használatos hivatkozása! Created + Létrehozva No comment provided by engineer. @@ -1709,8 +1736,9 @@ Ez az egyszer használatos hivatkozása! Jelenlegi jelmondat… No comment provided by engineer. - - Current user + + Current profile + Jelenlegi profil No comment provided by engineer. @@ -1725,6 +1753,7 @@ Ez az egyszer használatos hivatkozása! Customize theme + Téma személyre szabása No comment provided by engineer. @@ -1734,6 +1763,7 @@ Ez az egyszer használatos hivatkozása! Dark mode colors + Sötét mód színei No comment provided by engineer. @@ -2043,6 +2073,7 @@ Ez a művelet nem vonható vissza! Deleted + Törölve No comment provided by engineer. @@ -2057,6 +2088,7 @@ Ez a művelet nem vonható vissza! Deletion errors + Törlési hibák No comment provided by engineer. @@ -2101,10 +2133,12 @@ Ez a művelet nem vonható vissza! Detailed statistics + Részletes statisztikák No comment provided by engineer. Details + Részletek No comment provided by engineer. @@ -2264,6 +2298,7 @@ Ez a művelet nem vonható vissza! Download errors + Letöltési hibák No comment provided by engineer. @@ -2278,10 +2313,12 @@ Ez a művelet nem vonható vissza! Downloaded + Letöltve No comment provided by engineer. Downloaded files + Letöltött fájlok No comment provided by engineer. @@ -2656,6 +2693,7 @@ Ez a művelet nem vonható vissza! Error exporting theme: %@ + Hiba a téma exportálásakor: %@ No comment provided by engineer. @@ -2685,10 +2723,12 @@ Ez a művelet nem vonható vissza! Error reconnecting server + Hiba a kiszolgálóhoz való újrakapcsolódáskor No comment provided by engineer. Error reconnecting servers + Hiba a kiszolgálókhoz való újrakapcsolódáskor No comment provided by engineer. @@ -2698,6 +2738,7 @@ Ez a művelet nem vonható vissza! Error resetting statistics + Hiba a statisztikák visszaállításakor No comment provided by engineer. @@ -2833,6 +2874,7 @@ Ez a művelet nem vonható vissza! Errors + Hibák No comment provided by engineer. @@ -2862,6 +2904,7 @@ Ez a művelet nem vonható vissza! Export theme + Téma exportálása No comment provided by engineer. @@ -2901,22 +2944,27 @@ Ez a művelet nem vonható vissza! File error + Fájlhiba No comment provided by engineer. File not found - most likely file was deleted or cancelled. + A fájl nem található - valószínűleg a fájlt törölték vagy visszavonták. file error text File server error: %@ + Fájlkiszolgáló hiba: %@ file error text File status + Fájlállapot No comment provided by engineer. File status: %@ + Fájlállapot: %@ copied message info @@ -3110,10 +3158,12 @@ Hiba: %2$@ Good afternoon! + Jó napot! message preview Good morning! + Jó reggelt! message preview @@ -3398,6 +3448,7 @@ Hiba: %2$@ Import theme + Téma importálása No comment provided by engineer. @@ -3524,6 +3575,7 @@ Hiba: %2$@ Interface colors + Kezelőfelület színei No comment provided by engineer. @@ -3871,6 +3923,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Member inactive + Inaktív tag item status text @@ -3890,6 +3943,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Menus + Menük No comment provided by engineer. @@ -3914,10 +3968,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Message forwarded + Továbbított üzenet item status text Message may be delivered later if member becomes active. + Az üzenet később is kézbesíthető, ha a tag aktívvá válik. item status description @@ -3961,10 +4017,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Message status + Üzenetállapot No comment provided by engineer. Message status: %@ + Üzenetállapot: %@ copied message info @@ -3994,10 +4052,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Messages received + Fogadott üzenetek No comment provided by engineer. Messages sent + Elküldött üzenetek No comment provided by engineer. @@ -4237,6 +4297,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! No direct connection yet, message is forwarded by admin. + Még nincs közvetlen kapcsolat, az üzenetet az admin továbbítja. item status description @@ -4256,6 +4317,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! No info, try to reload + Nincs információ, próbálja meg újratölteni No comment provided by engineer. @@ -4444,6 +4506,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Open server settings + Kiszolgáló beállításainak megnyitása No comment provided by engineer. @@ -4557,6 +4620,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Pending + Függő No comment provided by engineer. @@ -4587,6 +4651,8 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers. + Ellenőrizze, hogy a mobil és az asztali számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint az asztali számítógép tűzfalában engedélyezve van-e a kapcsolat. +Minden további problémát osszon meg a fejlesztőkkel. No comment provided by engineer. @@ -4692,6 +4758,7 @@ Hiba: %@ Previously connected servers + Korábban kapcsolódott kiszolgálók No comment provided by engineer. @@ -4765,6 +4832,7 @@ Hiba: %@ Profile theme + Profiltéma No comment provided by engineer. @@ -4851,10 +4919,12 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Proxied + Proxyzott No comment provided by engineer. Proxied servers + Proxyzott kiszolgálók No comment provided by engineer. @@ -4924,6 +4994,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Receive errors + Üzenetfogadási hibák No comment provided by engineer. @@ -4948,14 +5019,17 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Received messages + Fogadott üzenetek No comment provided by engineer. Received reply + Fogadott válasz No comment provided by engineer. Received total + Összes fogadott No comment provided by engineer. @@ -4990,6 +5064,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Reconnect + Újrakapcsolás No comment provided by engineer. @@ -4999,18 +5074,22 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Reconnect all servers + Újrakapcsolódás minden kiszolgálóhoz No comment provided by engineer. Reconnect all servers? + Újrakapcsolódás minden kiszolgálóhoz? No comment provided by engineer. Reconnect server to force message delivery. It uses additional traffic. + A kiszolgálóhoz való újrakapcsolódás az üzenet kézbesítésének kikényszerítéséhez. Ez további adatforgalmat használ. No comment provided by engineer. Reconnect server? + Újrakapcsolódás a kiszolgálóhoz? No comment provided by engineer. @@ -5065,6 +5144,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Remove image + Kép eltávolítása No comment provided by engineer. @@ -5139,10 +5219,12 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Reset all statistics + Minden statisztika visszaállítása No comment provided by engineer. Reset all statistics? + Minden statisztika visszaállítása? No comment provided by engineer. @@ -5152,6 +5234,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Reset to app theme + Alkalmazás témájának visszaállítása No comment provided by engineer. @@ -5161,6 +5244,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Reset to user theme + Felhasználó által létrehozott téma visszaállítása No comment provided by engineer. @@ -5235,6 +5319,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< SMP server + SMP-kiszolgáló No comment provided by engineer. @@ -5354,10 +5439,12 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Scale + Méretezés No comment provided by engineer. Scan / Paste link + Hivatkozás beolvasása / beillesztése No comment provided by engineer. @@ -5402,6 +5489,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Secondary + Másodlagos No comment provided by engineer. @@ -5411,6 +5499,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Secured + Biztosítva No comment provided by engineer. @@ -5430,6 +5519,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Selected chat preferences prohibit this message. + A kiválasztott csevegési beállítások tiltják ezt az üzenetet. No comment provided by engineer. @@ -5484,6 +5574,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Send errors + Üzenetküldési hibák No comment provided by engineer. @@ -5598,6 +5689,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Sent directly + Közvetlenül küldött No comment provided by engineer. @@ -5612,6 +5704,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Sent messages + Elküldött üzenetek No comment provided by engineer. @@ -5621,18 +5714,22 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Sent reply + Elküldött válasz No comment provided by engineer. Sent total + Összes elküldött No comment provided by engineer. Sent via proxy + Proxyn keresztül küldve No comment provided by engineer. Server address + Kiszolgáló címe No comment provided by engineer. @@ -5661,6 +5758,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Server type + Kiszolgáló típusa No comment provided by engineer. @@ -5668,6 +5766,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5679,10 +5781,12 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Servers info + információk a kiszolgálókról No comment provided by engineer. Servers statistics will be reset - this cannot be undone! + A kiszolgálók statisztikái visszaállnak - ez nem vonható vissza! No comment provided by engineer. @@ -5702,6 +5806,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Set default theme + Alapértelmezett téma beállítása No comment provided by engineer. @@ -5809,6 +5914,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Üzenet állapot megjelenítése No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Előnézet megjelenítése @@ -5826,6 +5935,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< SimpleX + SimpleX No comment provided by engineer. @@ -5905,6 +6015,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Size + Méret No comment provided by engineer. @@ -5954,10 +6065,12 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Starting from %@. + Kezdve ettől %@. No comment provided by engineer. Statistics + Statisztikák No comment provided by engineer. @@ -6027,18 +6140,17 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Subscribed + Feliratkozva No comment provided by engineer. Subscription errors - No comment provided by engineer. - - - Subscription percentage + Feliratkozási hibák No comment provided by engineer. Subscriptions ignored + Elutasított feliratkozások No comment provided by engineer. @@ -6123,6 +6235,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Temporary file error + Ideiglenes fájlhiba No comment provided by engineer. @@ -6264,6 +6377,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Themes + Témák No comment provided by engineer. @@ -6333,6 +6447,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This link was used with another mobile device, please create a new link on the desktop. + Ezt a hivatkozást egy másik mobilleszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén. No comment provided by engineer. @@ -6342,6 +6457,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Title + Cím No comment provided by engineer. @@ -6413,6 +6529,7 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Total + Összesen No comment provided by engineer. @@ -6422,6 +6539,7 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Transport sessions + Munkamenetek átvitele No comment provided by engineer. @@ -6618,6 +6736,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Upload errors + Feltöltési hibák No comment provided by engineer. @@ -6632,10 +6751,12 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Uploaded + Feltöltve No comment provided by engineer. Uploaded files + Feltöltött fájlok No comment provided by engineer. @@ -6715,6 +6836,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol User selection + Felhasználó kiválasztása No comment provided by engineer. @@ -6854,10 +6976,12 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Wallpaper accent + Háttérkép kiemelés No comment provided by engineer. Wallpaper background + Háttérkép háttérszíne No comment provided by engineer. @@ -6967,6 +7091,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Wrong key or unknown file chunk address - most likely file is deleted. + Hibás kulcs vagy ismeretlen fájltöredék cím - valószínűleg a fájl törlődött. file error text @@ -6976,6 +7101,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol XFTP server + XFTP-kiszolgáló No comment provided by engineer. @@ -7062,6 +7188,7 @@ Csatlakozási kérés megismétlése? You are not connected to these servers. Private routing is used to deliver messages to them. + Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál. No comment provided by engineer. @@ -7099,8 +7226,8 @@ Csatlakozási kérés megismétlése? Láthatóvá teheti SimpleX ismerősök számára a Beállításokban. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Mostantól küldhet üzeneteket %@ számára notification body @@ -7472,6 +7599,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. attempts + próbálkozások No comment provided by engineer. @@ -7507,7 +7635,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. blocked by admin letiltva az admin által - blocked chat item + marked deleted chat item preview text bold @@ -7666,6 +7794,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. decryption errors + visszafejtési hibák No comment provided by engineer. @@ -7720,6 +7849,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. duplicates + duplikációk No comment provided by engineer. @@ -7804,6 +7934,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. expired + lejárt No comment provided by engineer. @@ -7838,6 +7969,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. inactive + inaktív No comment provided by engineer. @@ -8019,10 +8151,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. other + egyéb No comment provided by engineer. other errors + egyéb hibák No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 2146b0115d..bb2241c336 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -554,6 +554,7 @@ Accent + Principale No comment provided by engineer. @@ -579,10 +580,12 @@ Acknowledged + Riconosciuto No comment provided by engineer. Acknowledgement errors + Errori di riconoscimento No comment provided by engineer. @@ -631,14 +634,17 @@ Additional accent + Principale aggiuntivo No comment provided by engineer. Additional accent 2 + Principale aggiuntivo 2 No comment provided by engineer. Additional secondary + Secondario aggiuntivo No comment provided by engineer. @@ -668,6 +674,7 @@ Advanced settings + Impostazioni avanzate No comment provided by engineer. @@ -687,6 +694,7 @@ All data is private to your device. + Tutti i dati sono privati, nel tuo dispositivo. No comment provided by engineer. @@ -709,8 +717,9 @@ Tutti i nuovi messaggi da %@ verrranno nascosti! No comment provided by engineer. - - All users + + All profiles + Tutti gli profili No comment provided by engineer. @@ -915,6 +924,7 @@ Apply to + Applica a No comment provided by engineer. @@ -994,6 +1004,7 @@ Background + Sfondo No comment provided by engineer. @@ -1023,6 +1034,7 @@ Black + Nero No comment provided by engineer. @@ -1137,6 +1149,7 @@ Cannot forward message + Impossibile inoltrare il messaggio No comment provided by engineer. @@ -1212,6 +1225,7 @@ Chat colors + Colori della chat No comment provided by engineer. @@ -1261,6 +1275,7 @@ Chat theme + Tema della chat No comment provided by engineer. @@ -1295,14 +1310,17 @@ Chunks deleted + Blocchi eliminati No comment provided by engineer. Chunks downloaded + Blocchi scaricati No comment provided by engineer. Chunks uploaded + Blocchi inviati No comment provided by engineer. @@ -1332,6 +1350,7 @@ Color mode + Modalità di colore No comment provided by engineer. @@ -1346,6 +1365,7 @@ Completed + Completato No comment provided by engineer. @@ -1463,6 +1483,7 @@ Questo è il tuo link una tantum! Connected + Connesso No comment provided by engineer. @@ -1472,6 +1493,7 @@ Questo è il tuo link una tantum! Connected servers + Server connessi No comment provided by engineer. @@ -1481,6 +1503,7 @@ Questo è il tuo link una tantum! Connecting + In connessione No comment provided by engineer. @@ -1530,10 +1553,12 @@ Questo è il tuo link una tantum! Connection with desktop stopped + Connessione con il desktop fermata No comment provided by engineer. Connections + Connessioni No comment provided by engineer. @@ -1593,6 +1618,7 @@ Questo è il tuo link una tantum! Copy error + Copia errore No comment provided by engineer. @@ -1672,6 +1698,7 @@ Questo è il tuo link una tantum! Created + Creato No comment provided by engineer. @@ -1709,8 +1736,9 @@ Questo è il tuo link una tantum! Password attuale… No comment provided by engineer. - - Current user + + Current profile + Profilo attuale No comment provided by engineer. @@ -1725,6 +1753,7 @@ Questo è il tuo link una tantum! Customize theme + Personalizza il tema No comment provided by engineer. @@ -1734,6 +1763,7 @@ Questo è il tuo link una tantum! Dark mode colors + Colori modalità scura No comment provided by engineer. @@ -2043,6 +2073,7 @@ Non è reversibile! Deleted + Eliminato No comment provided by engineer. @@ -2057,6 +2088,7 @@ Non è reversibile! Deletion errors + Errori di eliminazione No comment provided by engineer. @@ -2101,10 +2133,12 @@ Non è reversibile! Detailed statistics + Statistiche dettagliate No comment provided by engineer. Details + Dettagli No comment provided by engineer. @@ -2264,6 +2298,7 @@ Non è reversibile! Download errors + Errori di scaricamento No comment provided by engineer. @@ -2278,10 +2313,12 @@ Non è reversibile! Downloaded + Scaricato No comment provided by engineer. Downloaded files + File scaricati No comment provided by engineer. @@ -2656,6 +2693,7 @@ Non è reversibile! Error exporting theme: %@ + Errore di esportazione del tema: %@ No comment provided by engineer. @@ -2685,10 +2723,12 @@ Non è reversibile! Error reconnecting server + Errore di riconnessione al server No comment provided by engineer. Error reconnecting servers + Errore di riconnessione ai server No comment provided by engineer. @@ -2698,6 +2738,7 @@ Non è reversibile! Error resetting statistics + Errore di azzeramento statistiche No comment provided by engineer. @@ -2833,6 +2874,7 @@ Non è reversibile! Errors + Errori No comment provided by engineer. @@ -2862,6 +2904,7 @@ Non è reversibile! Export theme + Esporta tema No comment provided by engineer. @@ -2901,22 +2944,27 @@ Non è reversibile! File error + Errore del file No comment provided by engineer. File not found - most likely file was deleted or cancelled. + File non trovato - probabilmente è stato eliminato o annullato. file error text File server error: %@ + Errore del server dei file: %@ file error text File status + Stato del file No comment provided by engineer. File status: %@ + Stato del file: %@ copied message info @@ -3110,10 +3158,12 @@ Errore: %2$@ Good afternoon! + Buon pomeriggio! message preview Good morning! + Buongiorno! message preview @@ -3398,6 +3448,7 @@ Errore: %2$@ Import theme + Importa tema No comment provided by engineer. @@ -3524,6 +3575,7 @@ Errore: %2$@ Interface colors + Colori dell'interfaccia No comment provided by engineer. @@ -3871,6 +3923,7 @@ Questo è il tuo link per il gruppo %@! Member inactive + Membro inattivo item status text @@ -3890,6 +3943,7 @@ Questo è il tuo link per il gruppo %@! Menus + Menu No comment provided by engineer. @@ -3914,10 +3968,12 @@ Questo è il tuo link per il gruppo %@! Message forwarded + Messaggio inoltrato item status text Message may be delivered later if member becomes active. + Il messaggio può essere consegnato più tardi se il membro diventa attivo. item status description @@ -3961,10 +4017,12 @@ Questo è il tuo link per il gruppo %@! Message status + Stato del messaggio No comment provided by engineer. Message status: %@ + Stato del messaggio: %@ copied message info @@ -3994,10 +4052,12 @@ Questo è il tuo link per il gruppo %@! Messages received + Messaggi ricevuti No comment provided by engineer. Messages sent + Messaggi inviati No comment provided by engineer. @@ -4237,6 +4297,7 @@ Questo è il tuo link per il gruppo %@! No direct connection yet, message is forwarded by admin. + Ancora nessuna connessione diretta, il messaggio viene inoltrato dall'amministratore. item status description @@ -4256,6 +4317,7 @@ Questo è il tuo link per il gruppo %@! No info, try to reload + Nessuna informazione, prova a ricaricare No comment provided by engineer. @@ -4444,6 +4506,7 @@ Questo è il tuo link per il gruppo %@! Open server settings + Apri impostazioni server No comment provided by engineer. @@ -4557,6 +4620,7 @@ Questo è il tuo link per il gruppo %@! Pending + In attesa No comment provided by engineer. @@ -4587,6 +4651,8 @@ Questo è il tuo link per il gruppo %@! Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers. + Controlla che mobile e desktop siano collegati alla stessa rete locale e che il firewall del desktop consenta la connessione. +Si prega di condividere qualsiasi altro problema con gli sviluppatori. No comment provided by engineer. @@ -4692,6 +4758,7 @@ Errore: %@ Previously connected servers + Server precedentemente connessi No comment provided by engineer. @@ -4765,6 +4832,7 @@ Errore: %@ Profile theme + Tema del profilo No comment provided by engineer. @@ -4851,10 +4919,12 @@ Attivalo nelle impostazioni *Rete e server*. Proxied + Via proxy No comment provided by engineer. Proxied servers + Server via proxy No comment provided by engineer. @@ -4924,6 +4994,7 @@ Attivalo nelle impostazioni *Rete e server*. Receive errors + Errori di ricezione No comment provided by engineer. @@ -4948,14 +5019,17 @@ Attivalo nelle impostazioni *Rete e server*. Received messages + Messaggi ricevuti No comment provided by engineer. Received reply + Risposta ricevuta No comment provided by engineer. Received total + Totale ricevuto No comment provided by engineer. @@ -4990,6 +5064,7 @@ Attivalo nelle impostazioni *Rete e server*. Reconnect + Riconnetti No comment provided by engineer. @@ -4999,18 +5074,22 @@ Attivalo nelle impostazioni *Rete e server*. Reconnect all servers + Riconnetti tutti i server No comment provided by engineer. Reconnect all servers? + Riconnettere tutti i server? No comment provided by engineer. Reconnect server to force message delivery. It uses additional traffic. + Riconnetti il server per forzare la consegna dei messaggi. Usa traffico aggiuntivo. No comment provided by engineer. Reconnect server? + Riconnettere il server? No comment provided by engineer. @@ -5065,6 +5144,7 @@ Attivalo nelle impostazioni *Rete e server*. Remove image + Rimuovi immagine No comment provided by engineer. @@ -5139,10 +5219,12 @@ Attivalo nelle impostazioni *Rete e server*. Reset all statistics + Azzera tutte le statistiche No comment provided by engineer. Reset all statistics? + Azzerare tutte le statistiche? No comment provided by engineer. @@ -5152,6 +5234,7 @@ Attivalo nelle impostazioni *Rete e server*. Reset to app theme + Ripristina al tema dell'app No comment provided by engineer. @@ -5161,6 +5244,7 @@ Attivalo nelle impostazioni *Rete e server*. Reset to user theme + Ripristina al tema dell'utente No comment provided by engineer. @@ -5235,6 +5319,7 @@ Attivalo nelle impostazioni *Rete e server*. SMP server + Server SMP No comment provided by engineer. @@ -5354,10 +5439,12 @@ Attivalo nelle impostazioni *Rete e server*. Scale + Scala No comment provided by engineer. Scan / Paste link + Scansiona / Incolla link No comment provided by engineer. @@ -5402,6 +5489,7 @@ Attivalo nelle impostazioni *Rete e server*. Secondary + Secondario No comment provided by engineer. @@ -5411,6 +5499,7 @@ Attivalo nelle impostazioni *Rete e server*. Secured + Protetto No comment provided by engineer. @@ -5430,6 +5519,7 @@ Attivalo nelle impostazioni *Rete e server*. Selected chat preferences prohibit this message. + Le preferenze della chat selezionata vietano questo messaggio. No comment provided by engineer. @@ -5484,6 +5574,7 @@ Attivalo nelle impostazioni *Rete e server*. Send errors + Errori di invio No comment provided by engineer. @@ -5598,6 +5689,7 @@ Attivalo nelle impostazioni *Rete e server*. Sent directly + Inviato direttamente No comment provided by engineer. @@ -5612,6 +5704,7 @@ Attivalo nelle impostazioni *Rete e server*. Sent messages + Messaggi inviati No comment provided by engineer. @@ -5621,18 +5714,22 @@ Attivalo nelle impostazioni *Rete e server*. Sent reply + Risposta inviata No comment provided by engineer. Sent total + Totale inviato No comment provided by engineer. Sent via proxy + Inviato via proxy No comment provided by engineer. Server address + Indirizzo server No comment provided by engineer. @@ -5661,6 +5758,7 @@ Attivalo nelle impostazioni *Rete e server*. Server type + Tipo server No comment provided by engineer. @@ -5668,6 +5766,10 @@ Attivalo nelle impostazioni *Rete e server*. La versione del server non è compatibile con le impostazioni di rete. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5679,10 +5781,12 @@ Attivalo nelle impostazioni *Rete e server*. Servers info + Info dei server No comment provided by engineer. Servers statistics will be reset - this cannot be undone! + Le statistiche dei server verranno azzerate - è irreversibile! No comment provided by engineer. @@ -5702,6 +5806,7 @@ Attivalo nelle impostazioni *Rete e server*. Set default theme + Imposta tema predefinito No comment provided by engineer. @@ -5809,6 +5914,10 @@ Attivalo nelle impostazioni *Rete e server*. Mostra stato del messaggio No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Mostra anteprima @@ -5826,6 +5935,7 @@ Attivalo nelle impostazioni *Rete e server*. SimpleX + SimpleX No comment provided by engineer. @@ -5905,6 +6015,7 @@ Attivalo nelle impostazioni *Rete e server*. Size + Dimensione No comment provided by engineer. @@ -5954,10 +6065,12 @@ Attivalo nelle impostazioni *Rete e server*. Starting from %@. + Inizio da %@. No comment provided by engineer. Statistics + Statistiche No comment provided by engineer. @@ -6027,18 +6140,17 @@ Attivalo nelle impostazioni *Rete e server*. Subscribed + Iscritto No comment provided by engineer. Subscription errors - No comment provided by engineer. - - - Subscription percentage + Errori di iscrizione No comment provided by engineer. Subscriptions ignored + Iscrizioni ignorate No comment provided by engineer. @@ -6123,6 +6235,7 @@ Attivalo nelle impostazioni *Rete e server*. Temporary file error + Errore del file temporaneo No comment provided by engineer. @@ -6264,6 +6377,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. Themes + Temi No comment provided by engineer. @@ -6333,6 +6447,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. This link was used with another mobile device, please create a new link on the desktop. + Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop. No comment provided by engineer. @@ -6342,6 +6457,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. Title + Titoli No comment provided by engineer. @@ -6413,6 +6529,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Total + Totale No comment provided by engineer. @@ -6422,6 +6539,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Transport sessions + Sessioni di trasporto No comment provided by engineer. @@ -6618,6 +6736,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Upload errors + Errori di invio No comment provided by engineer. @@ -6632,10 +6751,12 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Uploaded + Inviato No comment provided by engineer. Uploaded files + File inviati No comment provided by engineer. @@ -6715,6 +6836,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e User selection + Selezione utente No comment provided by engineer. @@ -6854,10 +6976,12 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Wallpaper accent + Tinta dello sfondo No comment provided by engineer. Wallpaper background + Retro dello sfondo No comment provided by engineer. @@ -6967,6 +7091,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Wrong key or unknown file chunk address - most likely file is deleted. + Chiave sbagliata o indirizzo sconosciuto per frammento del file - probabilmente il file è stato eliminato. file error text @@ -6976,6 +7101,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e XFTP server + Server XFTP No comment provided by engineer. @@ -7062,6 +7188,7 @@ Ripetere la richiesta di ingresso? You are not connected to these servers. Private routing is used to deliver messages to them. + Non sei connesso/a a questi server. L'instradamento privato è usato per consegnare loro i messaggi. No comment provided by engineer. @@ -7099,8 +7226,8 @@ Ripetere la richiesta di ingresso? Puoi renderlo visibile ai tuoi contatti SimpleX nelle impostazioni. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Ora puoi inviare messaggi a %@ notification body @@ -7472,6 +7599,7 @@ I server di SimpleX non possono vedere il tuo profilo. attempts + tentativi No comment provided by engineer. @@ -7507,7 +7635,7 @@ I server di SimpleX non possono vedere il tuo profilo. blocked by admin bloccato dall'amministratore - blocked chat item + marked deleted chat item preview text bold @@ -7666,6 +7794,7 @@ I server di SimpleX non possono vedere il tuo profilo. decryption errors + errori di decifrazione No comment provided by engineer. @@ -7720,6 +7849,7 @@ I server di SimpleX non possono vedere il tuo profilo. duplicates + doppi No comment provided by engineer. @@ -7804,6 +7934,7 @@ I server di SimpleX non possono vedere il tuo profilo. expired + scaduto No comment provided by engineer. @@ -7838,6 +7969,7 @@ I server di SimpleX non possono vedere il tuo profilo. inactive + inattivo No comment provided by engineer. @@ -8019,10 +8151,12 @@ I server di SimpleX non possono vedere il tuo profilo. other + altro No comment provided by engineer. other errors + altri errori No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 2e23ad7423..2f94e9c141 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -699,8 +699,8 @@ All new messages from %@ will be hidden! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1652,8 +1652,8 @@ This is your own one-time link! 現在の暗証フレーズ… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5442,6 +5442,10 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5577,6 +5581,10 @@ Enable in *Network & servers* settings. Show message status No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview プレビューを表示 @@ -5794,10 +5802,6 @@ Enable in *Network & servers* settings. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -6796,8 +6800,8 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ %@ にメッセージを送信できるようになりました notification body @@ -7190,7 +7194,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 blocked by admin - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index e3da8c8d41..1f7adaeca4 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -709,8 +709,8 @@ Alle nieuwe berichten van %@ worden verborgen! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1709,8 +1709,8 @@ Dit is uw eigen eenmalige link! Huidige wachtwoord… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5668,6 +5668,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Serverversie is incompatibel met netwerkinstellingen. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5809,6 +5813,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Toon berichtstatus No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Toon voorbeeld @@ -6033,10 +6041,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -7099,8 +7103,8 @@ Deelnameverzoek herhalen? Je kunt het via Instellingen zichtbaar maken voor je SimpleX contacten. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Je kunt nu berichten sturen naar %@ notification body @@ -7507,7 +7511,7 @@ SimpleX servers kunnen uw profiel niet zien. blocked by admin geblokkeerd door beheerder - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index ce4fe705a1..731095dea8 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -709,8 +709,8 @@ Wszystkie nowe wiadomości z %@ zostaną ukryte! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1709,8 +1709,8 @@ To jest twój jednorazowy link! Obecne hasło… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5668,6 +5668,10 @@ Włącz w ustawianiach *Sieć i serwery* . Wersja serwera jest niekompatybilna z ustawieniami sieciowymi. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5809,6 +5813,10 @@ Włącz w ustawianiach *Sieć i serwery* . Pokaż status wiadomości No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Pokaż podgląd @@ -6033,10 +6041,6 @@ Włącz w ustawianiach *Sieć i serwery* . Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -7099,8 +7103,8 @@ Powtórzyć prośbę dołączenia? Możesz ustawić go jako widoczny dla swoich kontaktów SimpleX w Ustawieniach. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Możesz teraz wysyłać wiadomości do %@ notification body @@ -7507,7 +7511,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. blocked by admin zablokowany przez admina - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index b5da149546..2fe659c6d8 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -709,8 +709,8 @@ Все новые сообщения от %@ будут скрыты! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1709,8 +1709,8 @@ This is your own one-time link! Текущий пароль… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5666,6 +5666,10 @@ Enable in *Network & servers* settings. Версия сервера несовместима с настройками сети. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5807,6 +5811,10 @@ Enable in *Network & servers* settings. Показать статус сообщения No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Показывать уведомления @@ -6031,10 +6039,6 @@ Enable in *Network & servers* settings. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -7097,9 +7101,9 @@ Repeat join request? Вы можете сделать его видимым для ваших контактов в SimpleX через Настройки. No comment provided by engineer. - - You can now send messages to %@ - Вы теперь можете отправлять сообщения %@ + + You can now chat with %@ + Вы теперь можете общаться с %@ notification body @@ -7505,7 +7509,7 @@ SimpleX серверы не могут получить доступ к Ваше blocked by admin заблокировано администратором - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 605a630f10..2fb86f9a76 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -674,8 +674,8 @@ All new messages from %@ will be hidden! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1617,8 +1617,8 @@ This is your own one-time link! รหัสผ่านปัจจุบัน… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5401,6 +5401,10 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5535,6 +5539,10 @@ Enable in *Network & servers* settings. Show message status No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview แสดงตัวอย่าง @@ -5750,10 +5758,6 @@ Enable in *Network & servers* settings. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -6750,8 +6754,8 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ ตอนนี้คุณสามารถส่งข้อความถึง %@ notification body @@ -7142,7 +7146,7 @@ SimpleX servers cannot see your profile. blocked by admin - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 8a09e1f21a..4b4c804fa6 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -709,8 +709,8 @@ %@ 'den gelen bütün yeni mesajlar saklı olacak! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1709,8 +1709,8 @@ Bu senin kendi tek kullanımlık bağlantın! Şu anki parola… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5668,6 +5668,10 @@ Enable in *Network & servers* settings. Sunucu sürümü ağ ayarlarıyla uyumlu değil. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5809,6 +5813,10 @@ Enable in *Network & servers* settings. Mesaj durumunu göster No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Ön gösterimi göser @@ -6033,10 +6041,6 @@ Enable in *Network & servers* settings. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -7099,8 +7103,8 @@ Katılma isteği tekrarlansın mı? Ayarlardan SimpleX kişilerinize görünür yapabilirsiniz. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Artık %@ adresine mesaj gönderebilirsin notification body @@ -7507,7 +7511,7 @@ SimpleX sunucuları profilinizi göremez. blocked by admin yönetici tarafından engellendi - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index f210d39a78..8056f0753f 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -709,8 +709,8 @@ Всі нові повідомлення від %@ будуть приховані! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1709,8 +1709,8 @@ This is your own one-time link! Поточна парольна фраза… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5668,6 +5668,10 @@ Enable in *Network & servers* settings. Серверна версія несумісна з мережевими налаштуваннями. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5809,6 +5813,10 @@ Enable in *Network & servers* settings. Показати статус повідомлення No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview Показати попередній перегляд @@ -6033,10 +6041,6 @@ Enable in *Network & servers* settings. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -7099,8 +7103,8 @@ Repeat join request? Ви можете зробити його видимим для ваших контактів у SimpleX за допомогою налаштувань. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Тепер ви можете надсилати повідомлення на адресу %@ notification body @@ -7507,7 +7511,7 @@ SimpleX servers cannot see your profile. blocked by admin заблоковано адміністратором - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index bc61b16ce0..0a55e89f1a 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -696,8 +696,8 @@ All new messages from %@ will be hidden! No comment provided by engineer. - - All users + + All profiles No comment provided by engineer. @@ -1683,8 +1683,8 @@ This is your own one-time link! 现有密码…… No comment provided by engineer. - - Current user + + Current profile No comment provided by engineer. @@ -5589,6 +5589,10 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. srv error text + + Server version is incompatible with network settings: %@. + No comment provided by engineer. + Server version is incompatible with your app: %@. No comment provided by engineer. @@ -5729,6 +5733,10 @@ Enable in *Network & servers* settings. Show message status No comment provided by engineer. + + Show percentage + No comment provided by engineer. + Show preview 显示预览 @@ -5952,10 +5960,6 @@ Enable in *Network & servers* settings. Subscription errors No comment provided by engineer. - - Subscription percentage - No comment provided by engineer. - Subscriptions ignored No comment provided by engineer. @@ -7000,8 +7004,8 @@ Repeat join request? 你可以通过设置让它对你的 SimpleX 联系人可见。 No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ 您现在可以给 %@ 发送消息 notification body @@ -7403,7 +7407,7 @@ SimpleX 服务器无法看到您的资料。 blocked by admin 由管理员封禁 - blocked chat item + marked deleted chat item preview text bold diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 764415b1aa..1a2a27ba9b 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -119,7 +119,7 @@ class NotificationService: UNNotificationServiceExtension { var threadId: UUID? = NSEThreads.shared.newThread() var notificationInfo: NtfMessages? var receiveEntityId: String? - var expectedMessages: Set = [] + var expectedMessage: String? // return true if the message is taken - it prevents sending it to another NotificationService instance for processing var shouldProcessNtf = false var appSubscriber: AppSubscriber? @@ -191,7 +191,7 @@ class NotificationService: UNNotificationServiceExtension { let dbStatus = startChat() if case .ok = dbStatus, let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count))") + logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessage_ == nil ? 0 : 1))") if let connEntity = ntfInfo.connEntity_ { setBestAttemptNtf( ntfInfo.ntfsEnabled @@ -201,7 +201,7 @@ class NotificationService: UNNotificationServiceExtension { if let id = connEntity.id, ntfInfo.msgTs != nil { notificationInfo = ntfInfo receiveEntityId = id - expectedMessages = Set(ntfInfo.ntfMessages.map { $0.msgId }) + expectedMessage = ntfInfo.ntfMessage_.flatMap { $0.msgId } shouldProcessNtf = true return } @@ -224,12 +224,10 @@ class NotificationService: UNNotificationServiceExtension { self.setBestAttemptNtf(.empty) } if case let .msgInfo(info) = ntf { - let found = expectedMessages.remove(info.msgId) - if found != nil { - logger.debug("NotificationService processNtf: msgInfo, last: \(self.expectedMessages.isEmpty)") - if expectedMessages.isEmpty { - self.deliverBestAttemptNtf() - } + if info.msgId == expectedMessage { + expectedMessage = nil + logger.debug("NotificationService processNtf: msgInfo") + self.deliverBestAttemptNtf() return true } else if info.msgTs > msgTs { logger.debug("NotificationService processNtf: unexpected msgInfo, let other instance to process it, stopping this one") @@ -392,6 +390,16 @@ func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber } } +let seSubscriber = seMessageSubscriber { + switch $0 { + case let .state(state): + if state == .sendingMessage && NSEChatState.shared.value.canSuspend { + logger.debug("NotificationService: seSubscriber app state \(state.rawValue), suspending") + suspendChat(fastNSESuspendSchedule.timeout) + } + } +} + var receiverStarted = false let startLock = DispatchSemaphore(value: 1) let suspendLock = DispatchSemaphore(value: 1) @@ -636,7 +644,7 @@ func apiGetActiveUser() -> User? { } func apiStartChat() throws -> Bool { - let r = sendSimpleXCmd(.startChat(mainApp: false)) + let r = sendSimpleXCmd(.startChat(mainApp: false, enableSndFiles: false)) switch r { case .chatStarted: return true case .chatRunning: return false @@ -677,9 +685,9 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { return nil } let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) - if case let .ntfMessages(user, connEntity_, msgTs, ntfMessages) = r, let user = user { - logger.debug("apiGetNtfMessage response ntfMessages: \(ntfMessages.count)") - return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages) + if case let .ntfMessages(user, connEntity_, msgTs, ntfMessage_) = r, let user = user { + logger.debug("apiGetNtfMessage response ntfMessages: \(ntfMessage_ == nil ? 0 : 1)") + return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessage_: ntfMessage_) } else if case let .chatCmdError(_, error) = r { logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))") } else { @@ -726,7 +734,7 @@ struct NtfMessages { var user: User var connEntity_: ConnectionEntity? var msgTs: Date? - var ntfMessages: [NtfMsgInfo] + var ntfMessage_: NtfMsgInfo? var ntfsEnabled: Bool { user.showNotifications && (connEntity_?.ntfsEnabled ?? false) diff --git a/apps/ios/SimpleX SE/Info.plist b/apps/ios/SimpleX SE/Info.plist new file mode 100644 index 0000000000..2ce1f45040 --- /dev/null +++ b/apps/ios/SimpleX SE/Info.plist @@ -0,0 +1,35 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsAttachmentsWithMinCount + 0 + NSExtensionActivationSupportsAttachmentsWithMaxCount + 1 + NSExtensionActivationSupportsWebPageWithMaxCount + 1 + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + NSExtensionActivationSupportsFileWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 1 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + ShareViewController + + + diff --git a/apps/ios/SimpleX SE/SEChatState.swift b/apps/ios/SimpleX SE/SEChatState.swift new file mode 100644 index 0000000000..581bff894a --- /dev/null +++ b/apps/ios/SimpleX SE/SEChatState.swift @@ -0,0 +1,39 @@ +// +// SEChatState.swift +// SimpleX SE +// +// Created by User on 18/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// SEStateGroupDefault must not be used in the share extension directly, only via this singleton +class SEChatState { + static let shared = SEChatState() + private var value_ = seStateGroupDefault.get() + + var value: SEState { + value_ + } + + func set(_ state: SEState) { + seStateGroupDefault.set(state) + sendSEState(state) + value_ = state + } +} + +/// Waits for other processes to set their state to suspended +/// Will wait for maximum of two seconds, since they might not be running +func waitForOtherProcessesToSuspend() async { + let startTime = CFAbsoluteTimeGetCurrent() + while CFAbsoluteTimeGetCurrent() - startTime < 2 { + try? await Task.sleep(nanoseconds: 100 * NSEC_PER_MSEC) + if appStateGroupDefault.get() == .suspended && + nseStateGroupDefault.get() == .suspended { + break + } + } +} diff --git a/apps/ios/SimpleX SE/ShareAPI.swift b/apps/ios/SimpleX SE/ShareAPI.swift new file mode 100644 index 0000000000..47e072ae78 --- /dev/null +++ b/apps/ios/SimpleX SE/ShareAPI.swift @@ -0,0 +1,115 @@ +// +// ShareAPI.swift +// SimpleX SE +// +// Created by User on 15/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import OSLog +import Foundation +import SimpleXChat + +let logger = Logger() + +func apiGetActiveUser() throws -> User? { + let r = sendSimpleXCmd(.showActiveUser) + switch r { + case let .activeUser(user): return user + case .chatCmdError(_, .error(.noActiveUser)): return nil + default: throw r + } +} + +func apiStartChat() throws -> Bool { + let r = sendSimpleXCmd(.startChat(mainApp: false, enableSndFiles: true)) + switch r { + case .chatStarted: return true + case .chatRunning: return false + default: throw r + } +} + +func apiSetNetworkConfig(_ cfg: NetCfg) throws { + let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg)) + if case .cmdOk = r { return } + throw r +} + +func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws { + let r = sendSimpleXCmd(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder)) + if case .cmdOk = r { return } + throw r +} + +func apiSetEncryptLocalFiles(_ enable: Bool) throws { + let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable)) + if case .cmdOk = r { return } + throw r +} + +func apiGetChats(userId: User.ID) throws -> Array { + let r = sendSimpleXCmd(.apiGetChats(userId: userId)) + if case let .apiChats(user: _, chats: chats) = r { return chats } + throw r +} + +func apiSendMessage( + chatInfo: ChatInfo, + cryptoFile: CryptoFile?, + msgContent: MsgContent +) throws -> AChatItem { + let r = sendSimpleXCmd( + chatInfo.chatType == .local + ? .apiCreateChatItem( + noteFolderId: chatInfo.apiId, + file: cryptoFile, + msg: msgContent + ) + : .apiSendMessage( + type: chatInfo.chatType, + id: chatInfo.apiId, + file: cryptoFile, + quotedItemId: nil, + msg: msgContent, + live: false, + ttl: nil + ) + ) + if case let .newChatItem(_, chatItem) = r { + return chatItem + } else { + if let filePath = cryptoFile?.filePath { removeFile(filePath) } + throw r + } +} + +func apiActivateChat() throws { + chatReopenStore() + let r = sendSimpleXCmd(.apiActivateChat(restoreChat: false)) + if case .cmdOk = r { return } + throw r +} + +func apiSuspendChat(expired: Bool) { + let r = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: expired ? 0 : 3_000000)) + // Block until `chatSuspended` received or 3 seconds has passed + var suspended = false + if case .cmdOk = r, !expired { + let startTime = CFAbsoluteTimeGetCurrent() + while CFAbsoluteTimeGetCurrent() - startTime < 3 { + switch recvSimpleXMsg(messageTimeout: 3_500000) { + case .chatSuspended: + suspended = false + break + default: continue + } + } + } + if !suspended { + _ = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: 0)) + } + logger.debug("close store") + chatCloseStore() + SEChatState.shared.set(.inactive) +} diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift new file mode 100644 index 0000000000..35d26dea35 --- /dev/null +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -0,0 +1,500 @@ +// +// ShareModel.swift +// SimpleX SE +// +// Created by Levitating Pineapple on 09/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import UniformTypeIdentifiers +import AVFoundation +import SwiftUI +import SimpleXChat + +/// Maximum size of hex encoded media previews +private let MAX_DATA_SIZE: Int64 = 14000 + +/// Maximum dimension (width or height) of an image, before passed for processing +private let MAX_DOWNSAMPLE_SIZE: Int64 = 2000 + +class ShareModel: ObservableObject { + @Published var sharedContent: SharedContent? + @Published var chats = Array() + @Published var profileImages = Dictionary() + @Published var search = String() + @Published var comment = String() + @Published var selected: ChatData? + @Published var isLoaded = false + @Published var bottomBar: BottomBar = .loadingSpinner + @Published var errorAlert: ErrorAlert? + + enum BottomBar { + case sendButton + case loadingSpinner + case loadingBar(progress: Double) + + var isLoading: Bool { + switch self { + case .sendButton: false + case .loadingSpinner: true + case .loadingBar: true + } + } + } + + var completion: () -> Void = { + fatalError("completion has not been set") + } + + private var itemProvider: NSItemProvider? + + var isSendDisbled: Bool { sharedContent == nil || selected == nil } + + var filteredChats: Array { + search.isEmpty + ? filterChatsToForwardTo(chats: chats) + : filterChatsToForwardTo(chats: chats) + .filter { foundChat($0, search.localizedLowercase) } + } + + func setup(context: NSExtensionContext) { + if let item = context.inputItems.first as? NSExtensionItem, + let itemProvider = item.attachments?.first { + self.itemProvider = itemProvider + self.completion = { + ShareModel.CompletionHandler.isEventLoopEnabled = false + context.completeRequest(returningItems: [item]) { + apiSuspendChat(expired: $0) + } + } + // Init Chat + Task { + if let e = initChat() { + await MainActor.run { errorAlert = e } + } else { + // Load Chats + Task { + switch fetchChats() { + case let .success(chats): + // Decode base64 images on background thread + let profileImages = chats.reduce(into: Dictionary()) { dict, chatData in + if let profileImage = chatData.chatInfo.image, + let uiImage = UIImage(base64Encoded: profileImage) { + dict[chatData.id] = uiImage + } + } + await MainActor.run { + self.chats = chats + self.profileImages = profileImages + withAnimation { isLoaded = true } + } + case let .failure(error): + await MainActor.run { errorAlert = error } + } + } + // Process Attachment + Task { + switch await self.itemProvider!.sharedContent() { + case let .success(chatItemContent): + await MainActor.run { + self.sharedContent = chatItemContent + self.bottomBar = .sendButton + if case let .text(string) = chatItemContent { comment = string } + } + case let .failure(errorAlert): + await MainActor.run { self.errorAlert = errorAlert } + } + } + } + } + } + } + + func send() { + if let sharedContent, let selected { + Task { + await MainActor.run { self.bottomBar = .loadingSpinner } + do { + SEChatState.shared.set(.sendingMessage) + await waitForOtherProcessesToSuspend() + let ci = try apiSendMessage( + chatInfo: selected.chatInfo, + cryptoFile: sharedContent.cryptoFile, + msgContent: sharedContent.msgContent(comment: self.comment) + ) + if selected.chatInfo.chatType == .local { + completion() + } else { + await MainActor.run { self.bottomBar = .loadingBar(progress: .zero) } + if let e = await handleEvents( + isGroupChat: ci.chatInfo.chatType == .group, + isWithoutFile: sharedContent.cryptoFile == nil, + chatItemId: ci.chatItem.id + ) { + await MainActor.run { errorAlert = e } + } else { + completion() + } + } + } catch { + if let e = error as? ErrorAlert { + await MainActor.run { errorAlert = e } + } + } + } + } + } + + private func initChat() -> ErrorAlert? { + do { + if hasChatCtrl() { + try apiActivateChat() + } else { + registerGroupDefaults() + haskell_init_se() + let (_, result) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation()) + if let e = migrationError(result) { return e } + try apiSetAppFilePaths( + filesFolder: getAppFilesDirectory().path, + tempFolder: getTempFilesDirectory().path, + assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path + ) + let isRunning = try apiStartChat() + logger.log(level: .debug, "chat started, running: \(isRunning)") + } + try apiSetNetworkConfig(getNetCfg()) + try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) + } catch { return ErrorAlert(error) } + return nil + } + + private func migrationError(_ r: DBMigrationResult) -> ErrorAlert? { + let useKeychain = storeDBPassphraseGroupDefault.get() + let storedDBKey = kcDatabasePassword.get() + // This switch duplicates DatabaseErrorView. + // TODO allow entering passphrase and make messages the same as in DatabaseErrorView. + return switch r { + case .errorNotADatabase: + if useKeychain && storedDBKey != nil && storedDBKey != "" { + ErrorAlert( + title: "Wrong database passphrase", + message: "Database passphrase is different from saved in the keychain." + ) + } else { + ErrorAlert( + title: "Encrypted database", + message: "Sharing is not supported when passphrase is not stored in KeyChain." + ) + } + case let .errorMigration(_, migrationError): + switch migrationError { + case .upgrade: + ErrorAlert( + title: "Database upgrade required", + message: "Open the app to upgrade the database." + ) + case .downgrade: + ErrorAlert( + title: "Database downgrade required", + message: "Open the app to downgrade the database." + ) + case let .migrationError(mtrError): + ErrorAlert( + title: "Incompatible database version", + message: mtrErrorDescription(mtrError) + ) + } + case let .errorSQL(_, migrationSQLError): + ErrorAlert( + title: "Database error", + message: "Error: \(migrationSQLError)" + ) + case .errorKeychain: + ErrorAlert( + title: "Keychain error", + message: "Cannot access keychain to save database password" + ) + case .invalidConfirmation: + ErrorAlert("Invalid migration confirmation") + case let .unknown(json): + ErrorAlert( + title: "Database error", + message: "Unknown database error: \(json)" + ) + case .ok: nil + } + } + + private func fetchChats() -> Result, ErrorAlert> { + do { + guard let user = try apiGetActiveUser() else { + return .failure( + ErrorAlert( + title: "No active profile", + message: "Please create a profile in the SimpleX app" + ) + ) + } + return .success(try apiGetChats(userId: user.id)) + } catch { + return .failure(ErrorAlert(error)) + } + } + + actor CompletionHandler { + static var isEventLoopEnabled = false + private var fileCompleted = false + private var messageCompleted = false + + func completeFile() { fileCompleted = true } + + func completeMessage() { messageCompleted = true } + + var isRunning: Bool { + Self.isEventLoopEnabled && !(fileCompleted && messageCompleted) + } + } + + /// Polls and processes chat events + /// Returns when message sending has completed optionally returning and error. + private func handleEvents(isGroupChat: Bool, isWithoutFile: Bool, chatItemId: ChatItem.ID) async -> ErrorAlert? { + func isMessage(for item: AChatItem?) -> Bool { + item.map { $0.chatItem.id == chatItemId } ?? false + } + + CompletionHandler.isEventLoopEnabled = true + let ch = CompletionHandler() + if isWithoutFile { await ch.completeFile() } + var networkTimeout = CFAbsoluteTimeGetCurrent() + while await ch.isRunning { + if CFAbsoluteTimeGetCurrent() - networkTimeout > 30 { + networkTimeout = CFAbsoluteTimeGetCurrent() + await MainActor.run { + self.errorAlert = ErrorAlert(title: "No network connection") { + Button("Keep Trying", role: .cancel) { } + Button("Dismiss Sheet", role: .destructive) { self.completion() } + } + } + } + switch recvSimpleXMsg(messageTimeout: 1_000_000) { + case let .sndFileProgressXFTP(_, ci, _, sentSize, totalSize): + guard isMessage(for: ci) else { continue } + networkTimeout = CFAbsoluteTimeGetCurrent() + await MainActor.run { + withAnimation { + let progress = Double(sentSize) / Double(totalSize) + bottomBar = .loadingBar(progress: progress) + } + } + case let .sndFileCompleteXFTP(_, ci, _): + guard isMessage(for: ci) else { continue } + if isGroupChat { + await MainActor.run { bottomBar = .loadingSpinner } + } + await ch.completeFile() + if await !ch.isRunning { break } + case let .chatItemStatusUpdated(_, ci): + guard isMessage(for: ci) else { continue } + if let (title, message) = ci.chatItem.meta.itemStatus.statusInfo { + // `title` and `message` already localized and interpolated + return ErrorAlert( + title: "\(title)", + message: "\(message)" + ) + } else if case let .sndSent(sndProgress) = ci.chatItem.meta.itemStatus { + switch sndProgress { + case .complete: + await ch.completeMessage() + case .partial: + if isGroupChat { + Task { + try? await Task.sleep(nanoseconds: 5 * NSEC_PER_SEC) + await ch.completeMessage() + } + } + } + } + case let .sndFileError(_, ci, _, errorMessage): + guard isMessage(for: ci) else { continue } + if let ci { cleanupFile(ci) } + return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)") + case let .sndFileWarning(_, ci, _, errorMessage): + guard isMessage(for: ci) else { continue } + if let ci { cleanupFile(ci) } + return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)") + case let .chatError(_, chatError): + return ErrorAlert(chatError) + case let .chatCmdError(_, chatError): + return ErrorAlert(chatError) + default: continue + } + } + return nil + } + + private func fileErrorInfo(_ ci: AChatItem?) -> String? { + switch ci?.chatItem.file?.fileStatus { + case let .sndError(e): e.errorInfo + case let .sndWarning(e): e.errorInfo + default: nil + } + } +} + +/// Chat Item Content extracted from `NSItemProvider` without the comment +enum SharedContent { + case image(preview: String, cryptoFile: CryptoFile) + case movie(preview: String, duration: Int, cryptoFile: CryptoFile) + case url(preview: LinkPreview) + case text(string: String) + case data(cryptoFile: CryptoFile) + + var cryptoFile: CryptoFile? { + switch self { + case let .image(_, cryptoFile): cryptoFile + case let .movie(_, _, cryptoFile): cryptoFile + case .url: nil + case .text: nil + case let .data(cryptoFile): cryptoFile + } + } + + func msgContent(comment: String) -> MsgContent { + switch self { + case let .image(preview, _): .image(text: comment, image: preview) + case let .movie(preview, duration, _): .video(text: comment, image: preview, duration: duration) + case let .url(preview): .link(text: comment, preview: preview) + case .text: .text(comment) + case .data: .file(comment) + } + } +} + +extension NSItemProvider { + fileprivate func sharedContent() async -> Result { + if let type = firstMatching(of: [.image, .movie, .fileURL, .url, .text]) { + switch type { + // Prepare Image message + case .image: + + // Animated + return if hasItemConformingToTypeIdentifier(UTType.gif.identifier) { + if let url = try? await inPlaceUrl(type: type), + let data = try? Data(contentsOf: url), + let image = UIImage(data: data), + let cryptoFile = saveFile(data, generateNewFileName("IMG", "gif"), encrypted: privacyEncryptLocalFilesGroupDefault.get()), + let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { + .success(.image(preview: preview, cryptoFile: cryptoFile)) + } else { .failure(ErrorAlert("Error preparing message")) } + + // Static + } else { + if let url = try? await inPlaceUrl(type: type), + let image = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE), + let cryptoFile = saveImage(image), + let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { + .success(.image(preview: preview, cryptoFile: cryptoFile)) + } else { .failure(ErrorAlert("Error preparing message")) } + } + + // Prepare Movie message + case .movie: + if let url = try? await inPlaceUrl(type: type), + let trancodedUrl = await transcodeVideo(from: url), + let (image, duration) = AVAsset(url: trancodedUrl).generatePreview(), + let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE), + let cryptoFile = moveTempFileFromURL(trancodedUrl) { + try? FileManager.default.removeItem(at: trancodedUrl) + return .success(.movie(preview: preview, duration: duration, cryptoFile: cryptoFile)) + } else { return .failure(ErrorAlert("Error preparing message")) } + + // Prepare Data message + case .fileURL: + if let url = try? await inPlaceUrl(type: .data) { + if isFileTooLarge(for: url) { + let sizeString = ByteCountFormatter.string( + fromByteCount: Int64(getMaxFileSize(.xftp)), + countStyle: .binary + ) + return .failure( + ErrorAlert( + title: "Large file!", + message: "Currently maximum supported file size is \(sizeString)." + ) + ) + } + if let file = saveFileFromURL(url) { + return .success(.data(cryptoFile: file)) + } + } + return .failure(ErrorAlert("Error preparing file")) + + // Prepare Link message + case .url: + if let url = try? await loadItem(forTypeIdentifier: type.identifier) as? URL { + let content: SharedContent = +// Option to disable previews needs to be taken into account +// if let linkPreview = await getLinkPreview(for: url) { +// .url(preview: linkPreview) +// } else { + .text(string: url.absoluteString) +// } + return .success(content) + } else { return .failure(ErrorAlert("Error preparing message")) } + + // Prepare Text message + case .text: + return if let text = try? await loadItem(forTypeIdentifier: type.identifier) as? String { + .success(.text(string: text)) + } else { .failure(ErrorAlert("Error preparing message")) } + default: return .failure(ErrorAlert("Unsupported format")) + } + } else { + return .failure(ErrorAlert("Unsupported format")) + } + } + + private func inPlaceUrl(type: UTType) async throws -> URL { + try await withCheckedThrowingContinuation { cont in + let _ = loadInPlaceFileRepresentation(forTypeIdentifier: type.identifier) { url, bool, error in + if let url = url { + cont.resume(returning: url) + } else if let error = error { + cont.resume(throwing: error) + } else { + fatalError("Either `url` or `error` must be present") + } + } + } + } + + private func firstMatching(of types: Array) -> UTType? { + for type in types { + if hasItemConformingToTypeIdentifier(type.identifier) { return type } + } + return nil + } +} + + +fileprivate func transcodeVideo(from input: URL) async -> URL? { + let outputUrl = URL( + fileURLWithPath: generateNewFileName( + getTempFilesDirectory().path + "/" + "video", "mp4", + fullPath: true + ) + ) + if await makeVideoQualityLower(input, outputUrl: outputUrl) { + return outputUrl + } else { + try? FileManager.default.removeItem(at: outputUrl) + return nil + } +} + +fileprivate func isFileTooLarge(for url: URL) -> Bool { + fileSize(url) + .map { $0 > getMaxFileSize(.xftp) } + ?? false +} + diff --git a/apps/ios/SimpleX SE/ShareView.swift b/apps/ios/SimpleX SE/ShareView.swift new file mode 100644 index 0000000000..20e6450b99 --- /dev/null +++ b/apps/ios/SimpleX SE/ShareView.swift @@ -0,0 +1,180 @@ +// +// ShareView.swift +// SimpleX SE +// +// Created by Levitating Pineapple on 09/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ShareView: View { + @ObservedObject var model: ShareModel + @Environment(\.colorScheme) var colorScheme + + var body: some View { + NavigationView { + ZStack(alignment: .bottom) { + if model.isLoaded { + List(model.filteredChats) { chat in + HStack { + profileImage( + chatInfoId: chat.chatInfo.id, + systemFallback: chatIconName(chat.chatInfo), + size: 30 + ) + Text(chat.chatInfo.displayName) + Spacer() + radioButton(selected: chat == model.selected) + } + .contentShape(Rectangle()) + .onTapGesture { model.selected = model.selected == chat ? nil : chat } + .tag(chat) + } + } else { + ProgressView().frame(maxHeight: .infinity) + } + } + .navigationTitle("Share") + .safeAreaInset(edge: .bottom) { + switch model.bottomBar { + case .sendButton: + compose(isLoading: false) + case .loadingSpinner: + compose(isLoading: true) + case .loadingBar(let progress): + loadingBar(progress: progress) + } + } + } + .searchable( + text: $model.search, + placement: .navigationBarDrawer(displayMode: .always) + ) + .alert($model.errorAlert) { alert in + Button("Ok") { model.completion() } + } + } + + private func compose(isLoading: Bool) -> some View { + VStack(spacing: .zero) { + Divider() + if let content = model.sharedContent { + itemPreview(content) + } + HStack { + Group { + if #available(iOSApplicationExtension 16.0, *) { + TextField("Comment", text: $model.comment, axis: .vertical) + } else { + TextField("Comment", text: $model.comment) + } + } + .contentShape(Rectangle()) + .disabled(isLoading) + .padding(.horizontal, 12) + .padding(.vertical, 4) + Group { + if isLoading { + ProgressView() + } else { + Button(action: model.send) { + Image(systemName: "arrow.up.circle.fill") + .resizable() + } + .disabled(model.isSendDisbled) + } + } + .frame(width: 28, height: 28) + .padding(6) + + } + .background(Color(.systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay( + RoundedRectangle(cornerRadius: 20) + .strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7) + ) + .padding(8) + } + .background(.thinMaterial) + } + + @ViewBuilder private func itemPreview(_ content: SharedContent) -> some View { + switch content { + case let .image(preview, _): imagePreview(preview) + case let .movie(preview, _, _): imagePreview(preview) + case let .url(linkPreview): imagePreview(linkPreview.image) + case let .data(cryptoFile): + previewArea { + Image(systemName: "doc.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .padding(.leading, 4) + Text(cryptoFile.filePath) + } + case .text: EmptyView() + } + } + + @ViewBuilder private func imagePreview(_ img: String) -> some View { + if let img = UIImage(base64Encoded: img) { + previewArea { + Image(uiImage: img) + .resizable() + .scaledToFit() + .frame(minHeight: 40, maxHeight: 60) + } + } else { + EmptyView() + } + } + + @ViewBuilder private func previewArea(@ViewBuilder content: @escaping () -> V) -> some View { + HStack(alignment: .center, spacing: 8) { + content() + Spacer() + } + .padding(.vertical, 1) + .frame(minHeight: 54) + .background { + switch colorScheme { + case .light: LightColorPaletteApp.sentMessage + case .dark: DarkColorPaletteApp.sentMessage + @unknown default: Color(.tertiarySystemBackground) + } + } + Divider() + } + + private func loadingBar(progress: Double) -> some View { + VStack { + Text("Sending File") + ProgressView(value: progress) + } + .padding() + .background(Material.ultraThin) + } + + private func profileImage(chatInfoId: ChatInfo.ID, systemFallback: String, size: Double) -> some View { + Group { + if let uiImage = model.profileImages[chatInfoId] { + Image(uiImage: uiImage).resizable() + } else { + Image(systemName: systemFallback).resizable() + } + } + .foregroundStyle(Color(.tertiaryLabel)) + .frame(width: size, height: size) + .clipShape(RoundedRectangle(cornerRadius: size * 0.225, style: .continuous)) + } + + private func radioButton(selected: Bool) -> some View { + Image(systemName: selected ? "checkmark.circle.fill" : "circle") + .imageScale(.large) + .foregroundStyle(selected ? Color.accentColor : Color(.tertiaryLabel)) + } +} diff --git a/apps/ios/SimpleX SE/ShareViewController.swift b/apps/ios/SimpleX SE/ShareViewController.swift new file mode 100644 index 0000000000..bf22f44a3b --- /dev/null +++ b/apps/ios/SimpleX SE/ShareViewController.swift @@ -0,0 +1,46 @@ +// +// ShareViewController.swift +// SimpleX SE +// +// Created by Levitating Pineapple on 08/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import UIKit +import SwiftUI +import SimpleXChat + +/// Extension Entry point +/// System will create this controller each time share sheet is invoked +/// using `NSExtensionPrincipalClass` in the info.plist +@objc(ShareViewController) +class ShareViewController: UIHostingController { + private let model = ShareModel() + // Assuming iOS continues to only allow single share sheet to be presented at once + static var isVisible: Bool = false + + @objc init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(rootView: ShareView(model: model)) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { fatalError() } + + override func viewDidLoad() { + ShareModel.CompletionHandler.isEventLoopEnabled = false + model.setup(context: extensionContext!) + } + + override func viewWillAppear(_ animated: Bool) { + logger.debug("ShareSheet will appear") + super.viewWillAppear(animated) + Self.isVisible = true + } + + override func viewWillDisappear(_ animated: Bool) { + logger.debug("ShareSheet will dissappear") + super.viewWillDisappear(animated) + ShareModel.CompletionHandler.isEventLoopEnabled = false + Self.isVisible = false + } +} diff --git a/apps/ios/SimpleX SE/SimpleX SE.entitlements b/apps/ios/SimpleX SE/SimpleX SE.entitlements new file mode 100644 index 0000000000..51dea2c806 --- /dev/null +++ b/apps/ios/SimpleX SE/SimpleX SE.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.chat.simplex.app + + keychain-access-groups + + $(AppIdentifierPrefix)chat.simplex.app + + + diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index e8f2159efd..f8c342b0fb 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -175,6 +175,11 @@ 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; + 64BAC45E2C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BAC4592C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a */; }; + 64BAC45F2C495205008D3995 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BAC45A2C495205008D3995 /* libffi.a */; }; + 64BAC4602C495205008D3995 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BAC45B2C495205008D3995 /* libgmpxx.a */; }; + 64BAC4612C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BAC45C2C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a */; }; + 64BAC4622C495205008D3995 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BAC45D2C495205008D3995 /* libgmp.a */; }; 64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; }; 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; @@ -183,7 +188,6 @@ 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; }; 64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; }; 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; }; - 8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; }; 8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; }; 8C74C3E52C1B900600039E77 /* ThemeTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */; }; 8C74C3E72C1B901900039E77 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C852B072C1086D100BA61E8 /* Color.swift */; }; @@ -199,9 +203,18 @@ 8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */; }; 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; }; 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; }; + CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; }; + CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */; }; + CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */; }; CE38A29A2C3FCA54005ED185 /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */; }; CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = CE38A29B2C3FCD72005ED185 /* SwiftyGif */; }; CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */; }; + CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; }; + CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; }; + CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + CEE723D02C3C21C90009AE93 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CEE723F02C3D25C70009AE93 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723EF2C3D25C70009AE93 /* ShareView.swift */; }; + CEE723F22C3D25ED0009AE93 /* ShareModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723F12C3D25ED0009AE93 /* ShareModel.swift */; }; CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */; }; D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; }; D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; }; @@ -209,11 +222,6 @@ D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; }; D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; - E50581002C3DDD7F009C3F71 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FB2C3DDD7F009C3F71 /* libffi.a */; }; - E50581012C3DDD7F009C3F71 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FC2C3DDD7F009C3F71 /* libgmp.a */; }; - E50581022C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FD2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a */; }; - E50581032C3DDD7F009C3F71 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FE2C3DDD7F009C3F71 /* libgmpxx.a */; }; - E50581042C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FF2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a */; }; E50581062C3DDD9D009C3F71 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = E50581052C3DDD9D009C3F71 /* Yams */; }; /* End PBXBuildFile section */ @@ -246,6 +254,20 @@ remoteGlobalIDString = 5CE2BA672845308900EC33A6; remoteInfo = SimpleXChat; }; + CEE723AF2C3BD3D70009AE93 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5CA059BE279559F40002BEB4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = CEE723A62C3BD3D70009AE93; + remoteInfo = "SimpleX SE"; + }; + CEE723D12C3C21C90009AE93 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5CA059BE279559F40002BEB4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5CE2BA672845308900EC33A6; + remoteInfo = SimpleXChat; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -266,11 +288,23 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */, 5CE2BA9D284555F500EC33A6 /* SimpleX NSE.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; }; + CEE723D32C3C21C90009AE93 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + CEE723D02C3C21C90009AE93 /* SimpleXChat.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -490,6 +524,11 @@ 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = ""; }; 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = ""; }; + 64BAC4592C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a"; sourceTree = ""; }; + 64BAC45A2C495205008D3995 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 64BAC45B2C495205008D3995 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 64BAC45C2C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a"; sourceTree = ""; }; + 64BAC45D2C495205008D3995 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = ""; }; 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; @@ -499,7 +538,6 @@ 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = ""; }; 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = ""; }; 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = ""; }; - 8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = ""; }; 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; 8C74C3EB2C1B92A900039E77 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaper.swift; sourceTree = ""; }; @@ -514,17 +552,22 @@ 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = ""; }; 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = ""; }; 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = ""; }; + CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = ""; }; + CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = ""; }; + CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = ""; }; CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = ""; }; + CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = ""; }; + CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX SE.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + CEE723AE2C3BD3D70009AE93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX SE.entitlements"; sourceTree = ""; }; + CEE723EF2C3D25C70009AE93 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = ""; }; + CEE723F12C3D25ED0009AE93 /* ShareModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareModel.swift; sourceTree = ""; }; CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseList.swift; sourceTree = ""; }; D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = ""; }; D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; 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; }; - E50580FB2C3DDD7F009C3F71 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - E50580FC2C3DDD7F009C3F71 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - E50580FD2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a"; sourceTree = ""; }; - E50580FE2C3DDD7F009C3F71 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - E50580FF2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -563,15 +606,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E50581032C3DDD7F009C3F71 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - E50581002C3DDD7F009C3F71 /* libffi.a in Frameworks */, - E50581012C3DDD7F009C3F71 /* libgmp.a in Frameworks */, + 64BAC4622C495205008D3995 /* libgmp.a in Frameworks */, + 64BAC45F2C495205008D3995 /* libffi.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - E50581022C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a in Frameworks */, E50581062C3DDD9D009C3F71 /* Yams in Frameworks */, - E50581042C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, + 64BAC4612C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a in Frameworks */, + 64BAC45E2C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a in Frameworks */, + 64BAC4602C495205008D3995 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -638,11 +681,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - E50580FB2C3DDD7F009C3F71 /* libffi.a */, - E50580FC2C3DDD7F009C3F71 /* libgmp.a */, - E50580FE2C3DDD7F009C3F71 /* libgmpxx.a */, - E50580FD2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a */, - E50580FF2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a */, + 64BAC45A2C495205008D3995 /* libffi.a */, + 64BAC45D2C495205008D3995 /* libgmp.a */, + 64BAC45B2C495205008D3995 /* libgmpxx.a */, + 64BAC45C2C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a */, + 64BAC4592C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a */, ); path = Libraries; sourceTree = ""; @@ -694,7 +737,6 @@ 64466DCB29FFE3E800E3D48D /* MailView.swift */, 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */, 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */, - 8C05382D2B39887E006436DC /* VideoUtils.swift */, 8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */, 8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */, 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */, @@ -713,6 +755,7 @@ 5C764E5C279C70B7000C6508 /* Libraries */, 5CA059C2279559F40002BEB4 /* Shared */, 5CDCAD462818589900503DA2 /* SimpleX NSE */, + CEE723A82C3BD3D70009AE93 /* SimpleX SE */, 5CA059DA279559F40002BEB4 /* Tests iOS */, 5CE2BA692845308900EC33A6 /* SimpleXChat */, 5CA059CB279559F40002BEB4 /* Products */, @@ -743,6 +786,7 @@ 5CA059D7279559F40002BEB4 /* Tests iOS.xctest */, 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */, 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */, + CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */, ); name = Products; sourceTree = ""; @@ -866,10 +910,12 @@ 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */, 5CDCAD7428188D2900503DA2 /* APITypes.swift */, 5C5E5D3C282447AB00B0488A /* CallTypes.swift */, + CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */, 5C9FD96A27A56D4D0075386C /* JSON.swift */, 5CDCAD7D2818941F00503DA2 /* API.swift */, 5CDCAD80281A7E2700503DA2 /* Notifications.swift */, 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */, + CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */, 64DAE1502809D9F5000DA960 /* FileUtils.swift */, 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */, 5C00168028C4FE760094D739 /* KeyChain.swift */, @@ -984,6 +1030,20 @@ path = Theme; sourceTree = ""; }; + CEE723A82C3BD3D70009AE93 /* SimpleX SE */ = { + isa = PBXGroup; + children = ( + CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */, + CEE723AE2C3BD3D70009AE93 /* Info.plist */, + CEDE70212C48FD9500233B1F /* SEChatState.swift */, + CE1EB0E32C459A660099D896 /* ShareAPI.swift */, + CEE723F12C3D25ED0009AE93 /* ShareModel.swift */, + CEE723EF2C3D25C70009AE93 /* ShareView.swift */, + CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */, + ); + path = "SimpleX SE"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1015,6 +1075,7 @@ dependencies = ( 5CE2BA6F2845308900EC33A6 /* PBXTargetDependency */, 5CE2BA9F284555F500EC33A6 /* PBXTargetDependency */, + CEE723B02C3BD3D70009AE93 /* PBXTargetDependency */, ); name = "SimpleX (iOS)"; packageProductDependencies = ( @@ -1086,6 +1147,24 @@ productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; productType = "com.apple.product-type.framework"; }; + CEE723A62C3BD3D70009AE93 /* SimpleX SE */ = { + isa = PBXNativeTarget; + buildConfigurationList = CEE723B42C3BD3D70009AE93 /* Build configuration list for PBXNativeTarget "SimpleX SE" */; + buildPhases = ( + CEE723A32C3BD3D70009AE93 /* Sources */, + CEE723A52C3BD3D70009AE93 /* Resources */, + CEE723D32C3C21C90009AE93 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + CEE723D22C3C21C90009AE93 /* PBXTargetDependency */, + ); + name = "SimpleX SE"; + productName = "SimpleX SE"; + productReference = CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1093,7 +1172,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1330; + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1340; ORGANIZATIONNAME = "SimpleX Chat"; TargetAttributes = { @@ -1113,6 +1192,9 @@ CreatedOnToolsVersion = 13.3; LastSwiftMigration = 1330; }; + CEE723A62C3BD3D70009AE93 = { + CreatedOnToolsVersion = 15.4; + }; }; }; buildConfigurationList = 5CA059C1279559F40002BEB4 /* Build configuration list for PBXProject "SimpleX" */; @@ -1154,6 +1236,7 @@ 5CA059C9279559F40002BEB4 /* SimpleX (iOS) */, 5CA059D6279559F40002BEB4 /* Tests iOS */, 5CDCAD442818589900503DA2 /* SimpleX NSE */, + CEE723A62C3BD3D70009AE93 /* SimpleX SE */, 5CE2BA672845308900EC33A6 /* SimpleXChat */, ); }; @@ -1193,6 +1276,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEE723A52C3BD3D70009AE93 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1309,7 +1399,6 @@ 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */, 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */, 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */, - 8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */, 5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */, 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */, 5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */, @@ -1378,6 +1467,7 @@ buildActionMask = 2147483647; files = ( 5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */, + CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */, 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */, 5CE2BA97284537A800EC33A6 /* dummy.m in Sources */, 5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */, @@ -1394,10 +1484,23 @@ 8C74C3E52C1B900600039E77 /* ThemeTypes.swift in Sources */, 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */, 8C74C3E82C1B905B00039E77 /* ChatWallpaperTypes.swift in Sources */, + CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */, 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + CEE723A32C3BD3D70009AE93 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */, + CEE723F02C3D25C70009AE93 /* ShareView.swift in Sources */, + CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */, + CEE723F22C3D25ED0009AE93 /* ShareModel.swift in Sources */, + CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1422,6 +1525,16 @@ target = 5CE2BA672845308900EC33A6 /* SimpleXChat */; targetProxy = 5CE2BAA82845617C00EC33A6 /* PBXContainerItemProxy */; }; + CEE723B02C3BD3D70009AE93 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CEE723A62C3BD3D70009AE93 /* SimpleX SE */; + targetProxy = CEE723AF2C3BD3D70009AE93 /* PBXContainerItemProxy */; + }; + CEE723D22C3C21C90009AE93 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5CE2BA672845308900EC33A6 /* SimpleXChat */; + targetProxy = CEE723D12C3C21C90009AE93 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -1628,7 +1741,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 227; + CURRENT_PROJECT_VERSION = 228; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1677,7 +1790,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 227; + CURRENT_PROJECT_VERSION = 228; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1763,7 +1876,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 227; + CURRENT_PROJECT_VERSION = 228; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1800,7 +1913,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 227; + CURRENT_PROJECT_VERSION = 228; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1837,7 +1950,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 227; + CURRENT_PROJECT_VERSION = 228; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1888,7 +2001,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 227; + CURRENT_PROJECT_VERSION = 228; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1932,6 +2045,74 @@ }; name = Release; }; + CEE723B22C3BD3D70009AE93 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5NN7GUYB6T; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "SimpleX SE/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "SimpleX SE"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 SimpleX Chat. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CEE723B32C3BD3D70009AE93 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5NN7GUYB6T; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "SimpleX SE/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "SimpleX SE"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 SimpleX Chat. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1980,6 +2161,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CEE723B42C3BD3D70009AE93 /* Build configuration list for PBXNativeTarget "SimpleX SE" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CEE723B22C3BD3D70009AE93 /* Debug */, + CEE723B32C3BD3D70009AE93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -1987,8 +2177,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/twostraws/CodeScanner"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.0.0; + kind = exactVersion; + version = 2.1.1; }; }; 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */ = { @@ -2011,8 +2201,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kirualex/SwiftyGif"; requirement = { - branch = master; - kind = branch; + kind = revision; + revision = 5e8619335d394901379c9add5c4c1c2f420b3800; }; }; D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */ = { diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme new file mode 100644 index 0000000000..a2639eb263 --- /dev/null +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index e2f4adc60f..987f7f3d41 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -117,10 +117,10 @@ public func sendSimpleXCmd(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) -> Chat } // in microseconds -let MESSAGE_TIMEOUT: Int32 = 15_000_000 +public let MESSAGE_TIMEOUT: Int32 = 15_000_000 -public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil) -> ChatResponse? { - if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), MESSAGE_TIMEOUT) { +public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> ChatResponse? { + if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout) { let s = fromCString(cjson) return s == "" ? nil : chatResponse(s) } @@ -205,7 +205,7 @@ public func chatResponse(_ s: String) -> ChatResponse { if let chatData = try? parseChatData(jChat) { return chatData } - return ChatData.invalidJSON(prettyJSON(jChat) ?? "") + return ChatData.invalidJSON(serializeJSON(jChat, options: .prettyPrinted) ?? "") } return .apiChats(user: user, chats: chats) } @@ -218,15 +218,15 @@ public func chatResponse(_ s: String) -> ChatResponse { } } else if type == "chatCmdError" { if let jError = jResp["chatCmdError"] as? NSDictionary { - return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? "")) + return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? "")) } } else if type == "chatError" { if let jError = jResp["chatError"] as? NSDictionary { - return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? "")) + return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? "")) } } } - json = prettyJSON(j) + json = serializeJSON(j, options: .prettyPrinted) } return ChatResponse.response(type: type ?? "invalid", json: json ?? s) } @@ -239,6 +239,14 @@ private func decodeUser_(_ jDict: NSDictionary) -> UserRef? { } } +private func errorJson(_ jDict: NSDictionary) -> String? { + if let chatError = jDict["chatError"] { + serializeJSON(chatError) + } else { + serializeJSON(jDict) + } +} + func parseChatData(_ jChat: Any) throws -> ChatData { let jChatDict = jChat as! NSDictionary let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!) @@ -251,7 +259,7 @@ func parseChatData(_ jChat: Any) throws -> ChatData { return ChatItem.invalidJSON( chatDir: decodeProperty(jCI, "chatDir"), meta: decodeProperty(jCI, "meta"), - json: prettyJSON(jCI) ?? "" + json: serializeJSON(jCI, options: .prettyPrinted) ?? "" ) } return ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats) @@ -268,8 +276,8 @@ func decodeProperty(_ obj: Any, _ prop: NSString) -> T? { return nil } -func prettyJSON(_ obj: Any) -> String? { - if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) { +func serializeJSON(_ obj: Any, options: JSONSerialization.WritingOptions = []) -> String? { + if let d = try? JSONSerialization.data(withJSONObject: obj, options: options) { return String(decoding: d, as: UTF8.self) } return nil diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index d5cd9c8f3b..c1263f26e2 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -26,7 +26,7 @@ public enum ChatCommand { case apiMuteUser(userId: Int64) case apiUnmuteUser(userId: Int64) case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?) - case startChat(mainApp: Bool) + case startChat(mainApp: Bool, enableSndFiles: Bool) case apiStopChat case apiActivateChat(restoreChat: Bool) case apiSuspendChat(timeoutMicroseconds: Int) @@ -146,6 +146,7 @@ public enum ChatCommand { case apiStandaloneFileInfo(url: String) // misc case showVersion + case getAgentSubsTotal(userId: Int64) case getAgentServersSummary(userId: Int64) case resetAgentServersStats case string(String) @@ -171,7 +172,7 @@ public enum ChatCommand { case let .apiMuteUser(userId): return "/_mute user \(userId)" case let .apiUnmuteUser(userId): return "/_unmute user \(userId)" case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))" - case let .startChat(mainApp): return "/_start main=\(onOff(mainApp))" + case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))" case .apiStopChat: return "/_stop" case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))" case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" @@ -309,6 +310,7 @@ public enum ChatCommand { case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)" case let .apiStandaloneFileInfo(link): return "/_download info \(link)" case .showVersion: return "/version" + case let .getAgentSubsTotal(userId): return "/get subs total \(userId)" case let .getAgentServersSummary(userId): return "/get servers summary \(userId)" case .resetAgentServersStats: return "/reset servers stats" case let .string(str): return str @@ -447,6 +449,7 @@ public enum ChatCommand { case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile" case .apiStandaloneFileInfo: return "apiStandaloneFileInfo" case .showVersion: return "showVersion" + case .getAgentSubsTotal: return "getAgentSubsTotal" case .getAgentServersSummary: return "getAgentServersSummary" case .resetAgentServersStats: return "resetAgentServersStats" case .string: return "console command" @@ -575,6 +578,7 @@ public enum ChatResponse: Decodable, Error { case userContactLinkDeleted(user: User) case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?) case contactConnecting(user: UserRef, contact: Contact) + case contactSndReady(user: UserRef, contact: Contact) case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest) case acceptingContactRequest(user: UserRef, contact: Contact) case contactRequestRejected(user: UserRef) @@ -661,7 +665,7 @@ public enum ChatResponse: Decodable, Error { case callInvitations(callInvitations: [RcvCallInvitation]) case ntfTokenStatus(status: NtfTknStatus) case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String) - case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) + case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessage_: NtfMsgInfo?) case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo) case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) case contactDisabled(user: UserRef, contact: Contact) @@ -677,6 +681,7 @@ public enum ChatResponse: Decodable, Error { // misc case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) case cmdOk(user: UserRef?) + case agentSubsTotal(user: UserRef, subsTotal: SMPServerSubs, hasSession: Bool) case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary) case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs) case chatCmdError(user_: UserRef?, chatError: ChatError) @@ -742,6 +747,7 @@ public enum ChatResponse: Decodable, Error { case .userContactLinkDeleted: return "userContactLinkDeleted" case .contactConnected: return "contactConnected" case .contactConnecting: return "contactConnecting" + case .contactSndReady: return "contactSndReady" case .receivedContactRequest: return "receivedContactRequest" case .acceptingContactRequest: return "acceptingContactRequest" case .contactRequestRejected: return "contactRequestRejected" @@ -837,6 +843,7 @@ public enum ChatResponse: Decodable, Error { case .contactPQEnabled: return "contactPQEnabled" case .versionInfo: return "versionInfo" case .cmdOk: return "cmdOk" + case .agentSubsTotal: return "agentSubsTotal" case .agentServersSummary: return "agentServersSummary" case .agentSubsSummary: return "agentSubsSummary" case .chatCmdError: return "chatCmdError" @@ -907,6 +914,7 @@ public enum ChatResponse: Decodable, Error { case .userContactLinkDeleted: return noDetails case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact)) case let .contactConnecting(u, contact): return withUser(u, String(describing: contact)) + case let .contactSndReady(u, contact): return withUser(u, String(describing: contact)) case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest)) case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact)) case .contactRequestRejected: return noDetails @@ -1002,6 +1010,7 @@ public enum ChatResponse: Decodable, Error { case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)") case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))" case .cmdOk: return noDetails + case let .agentSubsTotal(u, subsTotal, hasSession): return withUser(u, "subsTotal: \(String(describing: subsTotal))\nhasSession: \(hasSession)") case let .agentServersSummary(u, serversSummary): return withUser(u, String(describing: serversSummary)) case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary)) case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) @@ -1228,7 +1237,7 @@ public struct ProtocolTestFailure: Decodable, Error, Equatable { public var localizedDescription: String { let err = String.localizedStringWithFormat(NSLocalizedString("Test failed at step %@.", comment: "server test failure"), testStep.text) switch testError { - case .SMP(.AUTH): + case .SMP(_, .AUTH): return err + " " + NSLocalizedString("Server requires authorization to create queues, check password", comment: "server test error") case .XFTP(.AUTH): return err + " " + NSLocalizedString("Server requires authorization to upload, check password", comment: "server test error") @@ -1287,42 +1296,32 @@ public struct NetCfg: Codable, Equatable, Hashable { var socksMode: SocksMode = .always public var hostMode: HostMode = .publicHost public var requiredHostMode = true - public var sessionMode: TransportSessionMode - public var smpProxyMode: SMPProxyMode = .never - public var smpProxyFallback: SMPProxyFallback = .allow + public var sessionMode = TransportSessionMode.user + public var smpProxyMode: SMPProxyMode = .unknown + public var smpProxyFallback: SMPProxyFallback = .allowProtected public var tcpConnectTimeout: Int // microseconds public var tcpTimeout: Int // microseconds public var tcpTimeoutPerKb: Int // microseconds public var rcvConcurrency: Int // pool size - public var tcpKeepAlive: KeepAliveOpts? + public var tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults public var smpPingInterval: Int // microseconds - public var smpPingCount: Int // times - public var logTLSErrors: Bool + public var smpPingCount: Int = 3 // times + public var logTLSErrors: Bool = false public static let defaults: NetCfg = NetCfg( - socksProxy: nil, - sessionMode: TransportSessionMode.user, tcpConnectTimeout: 25_000_000, tcpTimeout: 15_000_000, tcpTimeoutPerKb: 10_000, rcvConcurrency: 12, - tcpKeepAlive: KeepAliveOpts.defaults, - smpPingInterval: 1200_000_000, - smpPingCount: 3, - logTLSErrors: false + smpPingInterval: 1200_000_000 ) public static let proxyDefaults: NetCfg = NetCfg( - socksProxy: nil, - sessionMode: TransportSessionMode.user, tcpConnectTimeout: 35_000_000, tcpTimeout: 20_000_000, tcpTimeoutPerKb: 15_000, rcvConcurrency: 8, - tcpKeepAlive: KeepAliveOpts.defaults, - smpPingInterval: 1200_000_000, - smpPingCount: 3, - logTLSErrors: false + smpPingInterval: 1200_000_000 ) public var enableKeepAlive: Bool { tcpKeepAlive != nil } @@ -1897,7 +1896,7 @@ public enum SQLiteError: Decodable, Hashable { public enum AgentErrorType: Decodable, Hashable { case CMD(cmdErr: CommandErrorType) case CONN(connErr: ConnectionErrorType) - case SMP(smpErr: ProtocolErrorType) + case SMP(serverAddress: String, smpErr: ProtocolErrorType) case NTF(ntfErr: ProtocolErrorType) case XFTP(xftpErr: XFTPErrorType) case PROXY(proxyServer: String, relayServer: String, proxyErr: ProxyClientError) @@ -2097,6 +2096,7 @@ public struct AppSettings: Codable, Equatable, Hashable { public var privacyShowChatPreviews: Bool? = nil public var privacySaveLastDraft: Bool? = nil public var privacyProtectScreen: Bool? = nil + public var privacyMediaBlurRadius: Int? = nil public var notificationMode: AppSettingsNotificationMode? = nil public var notificationPreviewMode: NotificationPreviewMode? = nil public var webrtcPolicyRelay: Bool? = nil @@ -2126,6 +2126,7 @@ public struct AppSettings: Codable, Equatable, Hashable { if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews } if privacySaveLastDraft != def.privacySaveLastDraft { empty.privacySaveLastDraft = privacySaveLastDraft } if privacyProtectScreen != def.privacyProtectScreen { empty.privacyProtectScreen = privacyProtectScreen } + if privacyMediaBlurRadius != def.privacyMediaBlurRadius { empty.privacyMediaBlurRadius = privacyMediaBlurRadius } if notificationMode != def.notificationMode { empty.notificationMode = notificationMode } if notificationPreviewMode != def.notificationPreviewMode { empty.notificationPreviewMode = notificationPreviewMode } if webrtcPolicyRelay != def.webrtcPolicyRelay { empty.webrtcPolicyRelay = webrtcPolicyRelay } @@ -2156,6 +2157,7 @@ public struct AppSettings: Codable, Equatable, Hashable { privacyShowChatPreviews: true, privacySaveLastDraft: true, privacyProtectScreen: false, + privacyMediaBlurRadius: 0, notificationMode: AppSettingsNotificationMode.instant, notificationPreviewMode: NotificationPreviewMode.message, webrtcPolicyRelay: true, @@ -2331,6 +2333,8 @@ public struct ServerSessions: Codable { ssErrors: 0, ssConnecting: 0 ) + + public var hasSess: Bool { ssConnected > 0 } } public struct SMPServerSubs: Codable { @@ -2384,6 +2388,10 @@ public struct AgentSMPServerStatsData: Codable { public var _connSubAttempts: Int public var _connSubIgnored: Int public var _connSubErrs: Int + public var _ntfKey: Int + public var _ntfKeyAttempts: Int + public var _ntfKeyDeleted: Int + public var _ntfKeyDeleteAttempts: Int } public struct XFTPServersSummary: Codable { @@ -2423,3 +2431,12 @@ public struct AgentXFTPServerStatsData: Codable { public var _deleteAttempts: Int public var _deleteErrs: Int } + +public struct AgentNtfServerStatsData: Codable { + public var _ntfCreated: Int + public var _ntfCreateAttempts: Int + public var _ntfChecked: Int + public var _ntfCheckAttempts: Int + public var _ntfDeleted: Int + public var _ntfDelAttempts: Int +} diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 90ac403999..6f9ad3b68e 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -13,6 +13,7 @@ public let appSuspendTimeout: Int = 15 // seconds let GROUP_DEFAULT_APP_STATE = "appState" let GROUP_DEFAULT_NSE_STATE = "nseState" +let GROUP_DEFAULT_SE_STATE = "seState" let GROUP_DEFAULT_DB_CONTAINER = "dbContainer" public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart" public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun" @@ -136,6 +137,11 @@ public enum NSEState: String, Codable { } } +public enum SEState: String, Codable { + case inactive + case sendingMessage +} + public enum DBContainer: String { case documents case group @@ -155,6 +161,12 @@ public let nseStateGroupDefault = EnumDefault( withDefault: .suspended // so that NSE that was never launched does not delay the app from resuming ) +public let seStateGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_SE_STATE, + withDefault: .inactive +) + // inactive app states do not include "stopped" state public func allowBackgroundRefresh() -> Bool { appStateGroupDefault.get().inactive && nseStateGroupDefault.get().inactive diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index b84e4bb3a0..54e8a80332 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1463,7 +1463,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { ) } -public struct ChatData: Decodable, Identifiable, Hashable { +public struct ChatData: Decodable, Identifiable, Hashable, ChatLike { public var chatInfo: ChatInfo public var chatItems: [ChatItem] public var chatStats: ChatStats @@ -1512,10 +1512,11 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var id: ChatId { get { "@\(contactId)" } } public var apiId: Int64 { get { contactId } } public var ready: Bool { get { activeConn?.connStatus == .ready } } + public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } } public var active: Bool { get { contactStatus == .active } } public var sendMsgEnabled: Bool { get { ( - ready + sndReady && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false) && !(activeConn?.connDisabled ?? true) @@ -1824,7 +1825,7 @@ public enum ConnStatus: String, Decodable, Hashable { case .joined: return false case .requested: return true case .accepted: return true - case .sndReady: return false + case .sndReady: return nil case .ready: return nil case .deleted: return nil } @@ -3293,6 +3294,28 @@ public struct CIFile: Decodable, Hashable { } } } + + public var showStatusIconInSmallView: Bool { + get { + switch fileStatus { + case .sndStored: fileProtocol != .local + case .sndTransfer: true + case .sndComplete: false + case .sndCancelled: true + case .sndError: true + case .sndWarning: true + case .rcvInvitation: false + case .rcvAccepted: true + case .rcvTransfer: true + case .rcvAborted: true + case .rcvCancelled: true + case .rcvComplete: false + case .rcvError: true + case .rcvWarning: true + case .invalid: true + } + } + } } public struct CryptoFile: Codable, Hashable { diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift new file mode 100644 index 0000000000..a37b6babf7 --- /dev/null +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -0,0 +1,62 @@ +// +// ChatUtils.swift +// SimpleXChat +// +// Created by Levitating Pineapple on 15/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation + +public protocol ChatLike { + var chatInfo: ChatInfo { get} + var chatItems: [ChatItem] { get } + var chatStats: ChatStats { get } +} + +public func filterChatsToForwardTo(chats: [C]) -> [C] { + var filteredChats = chats.filter { c in + c.chatInfo.chatType != .local && canForwardToChat(c.chatInfo) + } + if let privateNotes = chats.first(where: { $0.chatInfo.chatType == .local }) { + filteredChats.insert(privateNotes, at: 0) + } + return filteredChats +} + +public func foundChat(_ chat: ChatLike, _ searchStr: String) -> Bool { + let cInfo = chat.chatInfo + return switch cInfo { + case let .direct(contact): + viewNameContains(cInfo, searchStr) || + contact.profile.displayName.localizedLowercase.contains(searchStr) || + contact.fullName.localizedLowercase.contains(searchStr) + default: + viewNameContains(cInfo, searchStr) + } + + func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool { + cInfo.chatViewName.localizedLowercase.contains(s) + } +} + +private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { + switch cInfo { + case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv + case let .group(groupInfo): groupInfo.sendMsgEnabled + case let .local(noteFolder): noteFolder.sendMsgEnabled + case .contactRequest: false + case .contactConnection: false + case .invalidJSON: false + } +} + +public func chatIconName(_ cInfo: ChatInfo) -> String { + switch cInfo { + case .direct: "person.crop.circle.fill" + case .group: "person.2.circle.fill" + case .local: "folder.circle.fill" + case .contactRequest: "person.crop.circle.fill" + default: "circle.fill" + } +} diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift new file mode 100644 index 0000000000..65ed4c6717 --- /dev/null +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -0,0 +1,159 @@ +// +// ErrorAlert.swift +// SimpleXChat +// +// Created by Levitating Pineapple on 20/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +public struct ErrorAlert: Error { + public let title: LocalizedStringKey + public let message: LocalizedStringKey? + public let actions: Optional<() -> AnyView> + + public init( + title: LocalizedStringKey, + message: LocalizedStringKey? = nil + ) { + self.title = title + self.message = message + self.actions = nil + } + + public init( + title: LocalizedStringKey, + message: LocalizedStringKey? = nil, + @ViewBuilder actions: @escaping () -> A + ) { + self.title = title + self.message = message + self.actions = { AnyView(actions()) } + } + + public init(_ title: LocalizedStringKey) { + self = ErrorAlert(title: title) + } + + public init(_ error: any Error) { + self = if let chatResponse = error as? ChatResponse { + ErrorAlert(chatResponse) + } else { + ErrorAlert(LocalizedStringKey(error.localizedDescription)) + } + } + + public init(_ chatError: ChatError) { + self = ErrorAlert("\(chatErrorString(chatError))") + } + + public init(_ chatResponse: ChatResponse) { + self = if let networkErrorAlert = getNetworkErrorAlert(chatResponse) { + networkErrorAlert + } else { + ErrorAlert("\(responseError(chatResponse))") + } + } +} + +extension LocalizedStringKey: @unchecked Sendable { } + +extension View { + /// Bridges ``ErrorAlert`` to the generic alert API. + /// - Parameters: + /// - errorAlert: Binding to the Error, which is rendered in the alert + /// - actions: View Builder containing action buttons. + /// System defaults to `Ok` dismiss error action, when no actions are provided. + /// System implicitly adds `Cancel` action, if a destructive action is present + /// + /// - Returns: View, which displays ErrorAlert?, when set. + @ViewBuilder public func alert( + _ errorAlert: Binding, + @ViewBuilder actions: (ErrorAlert) -> A = { _ in EmptyView() } + ) -> some View { + if let alert = errorAlert.wrappedValue { + self.alert( + alert.title, + isPresented: Binding( + get: { errorAlert.wrappedValue != nil }, + set: { if !$0 { errorAlert.wrappedValue = nil } } + ), + actions: { + if let actions_ = alert.actions { + actions_() + } else { + actions(alert) + } + }, + message: { + if let message = alert.message { Text(message) } + } + ) + } else { self } + } +} + +public func getNetworkErrorAlert(_ r: ChatResponse) -> ErrorAlert? { + switch r { + case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))): + return ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))): + return ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + case let .chatCmdError(_, .errorAgent(.BROKER(addr, .HOST))): + return ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).") + case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TRANSPORT(.version)))): + return ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).") + case let .chatCmdError(_, .errorAgent(.SMP(serverAddress, .PROXY(proxyErr)))): + return smpProxyErrorAlert(proxyErr, serverAddress) + case let .chatCmdError(_, .errorAgent(.PROXY(proxyServer, relayServer, .protocolError(.PROXY(proxyErr))))): + return proxyDestinationErrorAlert(proxyErr, proxyServer, relayServer) + default: + return nil + } +} + +private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> ErrorAlert? { + switch proxyErr { + case .BROKER(brokerErr: .TIMEOUT): + return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + case .BROKER(brokerErr: .NETWORK): + return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + case .BROKER(brokerErr: .HOST): + return ErrorAlert(title: "Private routing error", message: "Forwarding server address is incompatible with network settings: \(serverHostname(srvAddr)).") + case .BROKER(brokerErr: .TRANSPORT(.version)): + return ErrorAlert(title: "Private routing error", message: "Forwarding server version is incompatible with network settings: \(serverHostname(srvAddr)).") + default: + return nil + } +} + +private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> ErrorAlert? { + switch proxyErr { + case .BROKER(brokerErr: .TIMEOUT): + return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + case .BROKER(brokerErr: .NETWORK): + return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + case .NO_SESSION: + return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + case .BROKER(brokerErr: .HOST): + return ErrorAlert(title: "Private routing error", message: "Destination server address of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)) settings.") + case .BROKER(brokerErr: .TRANSPORT(.version)): + return ErrorAlert(title: "Private routing error", message: "Destination server version of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)).") + default: + return nil + } +} + +public func serverHostname(_ srv: String) -> String { + parseServerAddress(srv)?.hostnames.first ?? srv +} + +public func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey { + switch err { + case let .noDown(dbMigrations): + "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))" + case let .different(appMigration, dbMigration): + "different migration in the app/database: \(appMigration) / \(dbMigration)" + } +} diff --git a/apps/ios/SimpleXChat/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift index fd6d951f48..c387c84aaa 100644 --- a/apps/ios/SimpleXChat/ImageUtils.swift +++ b/apps/ios/SimpleXChat/ImageUtils.swift @@ -10,6 +10,7 @@ import Foundation import SwiftUI import AVKit import SwiftyGif +import LinkPresentation public func getLoadedFileSource(_ file: CIFile?) -> CryptoFile? { if let file = file, file.loaded { @@ -158,6 +159,34 @@ public func imageHasAlpha(_ img: UIImage) -> Bool { return false } +/// Reduces image size, while consuming less RAM +/// +/// Used by ShareExtension to downsize large images +/// before passing them to regular image processing pipeline +/// to avoid exceeding 120MB memory +/// +/// - Parameters: +/// - url: Location of the image data +/// - size: Maximum dimension (width or height) +/// - Returns: Downsampled image or `nil`, if the image can't be located +public func downsampleImage(at url: URL, to size: Int64) -> UIImage? { + autoreleasepool { + if let source = CGImageSourceCreateWithURL(url as CFURL, nil) { + CGImageSourceCreateThumbnailAtIndex( + source, + Int.zero, + [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: String(size) as CFString + ] as CFDictionary + ) + .map { UIImage(cgImage: $0) } + } else { nil } + } +} + public func saveFileFromURL(_ url: URL) -> CryptoFile? { let encrypted = privacyEncryptLocalFilesGroupDefault.get() let savedFile: CryptoFile? @@ -281,6 +310,21 @@ private func dropPrefix(_ s: String, _ prefix: String) -> String { s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s } +public func makeVideoQualityLower(_ input: URL, outputUrl: URL) async -> Bool { + let asset: AVURLAsset = AVURLAsset(url: input, options: nil) + if let s = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset640x480) { + s.outputURL = outputUrl + s.outputFileType = .mp4 + s.metadataItemFilter = AVMetadataItemFilter.forSharing() + await s.export() + if let err = s.error { + logger.error("Failed to export video with error: \(err)") + } + return s.status == .completed + } + return false +} + extension AVAsset { public func generatePreview() -> (UIImage, Int)? { let generator = AVAssetImageGenerator(asset: self) @@ -348,3 +392,39 @@ extension UIImage { } } } + +public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { + logger.debug("getLinkMetadata: fetching URL preview") + LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in + if let e = error { + logger.error("Error retrieving link metadata: \(e.localizedDescription)") + } + if let metadata = metadata, + let imageProvider = metadata.imageProvider, + imageProvider.canLoadObject(ofClass: UIImage.self) { + imageProvider.loadObject(ofClass: UIImage.self){ object, error in + var linkPreview: LinkPreview? = nil + if let error = error { + logger.error("Couldn't load image preview from link metadata with error: \(error.localizedDescription)") + } else { + if let image = object as? UIImage, + let resized = resizeImageToStrSize(image, maxDataSize: 14000), + let title = metadata.title, + let uri = metadata.originalURL { + linkPreview = LinkPreview(uri: uri, title: title, image: resized) + } + } + cb(linkPreview) + } + } else { + logger.error("Could not load link preview image") + cb(nil) + } + } +} + +public func getLinkPreview(for url: URL) async -> LinkPreview? { + await withCheckedContinuation { cont in + getLinkPreview(url: url) { cont.resume(returning: $0) } + } +} diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index bc959cb34b..4b43595372 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -47,7 +47,7 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact) hideContent ? NSLocalizedString("A new contact", comment: "notification title") : contact.displayName ), body: String.localizedStringWithFormat( - NSLocalizedString("You can now send messages to %@", comment: "notification body"), + NSLocalizedString("You can now chat with %@", comment: "notification body"), hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName ), targetContentIdentifier: contact.id, diff --git a/apps/ios/SimpleXChat/SharedFileSubscriber.swift b/apps/ios/SimpleXChat/SharedFileSubscriber.swift index f496e6999e..bf5997f40b 100644 --- a/apps/ios/SimpleXChat/SharedFileSubscriber.swift +++ b/apps/ios/SimpleXChat/SharedFileSubscriber.swift @@ -12,6 +12,8 @@ public typealias AppSubscriber = SharedFileSubscriber> +public typealias SESubscriber = SharedFileSubscriber> + public class SharedFileSubscriber: NSObject, NSFilePresenter { var fileURL: URL public var presentedItemURL: URL? @@ -57,6 +59,8 @@ let appMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent( let nseMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent("chat.simplex.app.SimpleX-NSE.messages", isDirectory: false) +let seMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent("chat.simplex.app.SimpleX-SE.messages", isDirectory: false) + public struct ProcessMessage: Codable { var createdAt: Date = Date.now var message: Message @@ -70,6 +74,10 @@ public enum NSEProcessMessage: Codable { case state(state: NSEState) } +public enum SEProcessMessage: Codable { + case state(state: SEState) +} + public func sendAppProcessMessage(_ message: AppProcessMessage) { SharedFileSubscriber.notify(url: appMessagesSharedFile, message: ProcessMessage(message: message)) } @@ -78,6 +86,10 @@ public func sendNSEProcessMessage(_ message: NSEProcessMessage) { SharedFileSubscriber.notify(url: nseMessagesSharedFile, message: ProcessMessage(message: message)) } +public func sendSEProcessMessage(_ message: SEProcessMessage) { + SharedFileSubscriber.notify(url: seMessagesSharedFile, message: ProcessMessage(message: message)) +} + public func appMessageSubscriber(onMessage: @escaping (AppProcessMessage) -> Void) -> AppSubscriber { SharedFileSubscriber(fileURL: appMessagesSharedFile) { (msg: ProcessMessage) in onMessage(msg.message) @@ -90,6 +102,12 @@ public func nseMessageSubscriber(onMessage: @escaping (NSEProcessMessage) -> Voi } } +public func seMessageSubscriber(onMessage: @escaping (SEProcessMessage) -> Void) -> SESubscriber { + SharedFileSubscriber(fileURL: seMessagesSharedFile) { (msg: ProcessMessage) in + onMessage(msg.message) + } +} + public func sendAppState(_ state: AppState) { sendAppProcessMessage(.state(state: state)) } @@ -97,3 +115,7 @@ public func sendAppState(_ state: AppState) { public func sendNSEState(_ state: NSEState) { sendNSEProcessMessage(.state(state: state)) } + +public func sendSEState(_ state: SEState) { + sendSEProcessMessage(.state(state: state)) +} diff --git a/apps/ios/SimpleXChat/hs_init.c b/apps/ios/SimpleXChat/hs_init.c index adacd57310..4731e7b829 100644 --- a/apps/ios/SimpleXChat/hs_init.c +++ b/apps/ios/SimpleXChat/hs_init.c @@ -39,3 +39,19 @@ void haskell_init_nse(void) { char **pargv = argv; hs_init_with_rtsopts(&argc, &pargv); } + +void haskell_init_se(void) { + int argc = 7; + char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A1m", // chunk size for new allocations + "-H1m", // initial heap size + "-F0.5", // heap growth triggering GC + "-Fd1", // memory return + "-c", // compacting garbage collector + 0 + }; + char **pargv = argv; + hs_init_with_rtsopts(&argc, &pargv); +} diff --git a/apps/ios/SimpleXChat/hs_init.h b/apps/ios/SimpleXChat/hs_init.h index a732fd7113..40be4fc263 100644 --- a/apps/ios/SimpleXChat/hs_init.h +++ b/apps/ios/SimpleXChat/hs_init.h @@ -13,4 +13,6 @@ void haskell_init(void); void haskell_init_nse(void); +void haskell_init_se(void); + #endif /* hs_init_h */ diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 9748a26730..142baa5cbe 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ е потвърдено"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ сървъри"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ качено"; @@ -644,7 +641,7 @@ /* rcv group event chat item */ "blocked %@" = "блокиран %@"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "блокиран от админ"; /* No comment provided by engineer. */ @@ -4297,7 +4294,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Можете да го направите видим за вашите контакти в SimpleX чрез Настройки."; /* notification body */ -"You can now send messages to %@" = "Вече можете да изпращате съобщения до %@"; +"You can now chat with %@" = "Вече можете да изпращате съобщения до %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Можете да зададете визуализация на известията на заключен екран през настройките."; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 95c79efd8b..1a83061c8c 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -130,9 +130,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ je ověřený"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ servery"; - /* notification title */ "%@ wants to connect!" = "%@ se chce připojit!"; @@ -3439,7 +3436,7 @@ "You can hide or mute a user profile - swipe it to the right." = "Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava."; /* notification body */ -"You can now send messages to %@" = "Nyní můžete posílat zprávy %@"; +"You can now chat with %@" = "Nyní můžete posílat zprávy %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Náhled oznámení na zamykací obrazovce můžete změnit v nastavení."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 7464e096c4..d46f3243d4 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ wurde erfolgreich überprüft"; -/* No comment provided by engineer. */ -"%@ servers" = "%@-Server"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ hochgeladen"; @@ -337,6 +334,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "Danach die gewünschte Aktion auswählen:"; +/* No comment provided by engineer. */ +"Accent" = "Akzent"; + /* accept contact request via notification accept incoming call via notification */ "Accept" = "Annehmen"; @@ -353,6 +353,12 @@ /* call status */ "accepted call" = "Anruf angenommen"; +/* No comment provided by engineer. */ +"Acknowledged" = "Bestätigt"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Fehler bei der Bestätigung"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet."; @@ -377,6 +383,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Begrüßungsmeldung hinzufügen"; +/* No comment provided by engineer. */ +"Additional accent" = "Erste Akzentfarbe"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Zusätzlicher Akzent 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Zweite Akzentfarbe"; + /* No comment provided by engineer. */ "Address" = "Adresse"; @@ -398,6 +413,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Erweiterte Netzwerkeinstellungen"; +/* No comment provided by engineer. */ +"Advanced settings" = "Erweiterte Einstellungen"; + /* chat item text */ "agreeing encryption for %@…" = "Verschlüsselung von %@ zustimmen…"; @@ -408,11 +426,14 @@ "All app data is deleted." = "Werden die App-Daten komplett gelöscht."; /* No comment provided by engineer. */ -"All chats and messages will be deleted - this cannot be undone!" = "Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!"; +"All chats and messages will be deleted - this cannot be undone!" = "Alle Chats und Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ "All data is erased when it is entered." = "Alle Daten werden gelöscht, sobald dieser eingegeben wird."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Alle Daten sind auf Ihrem Gerät geschützt."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden."; @@ -420,14 +441,17 @@ "all members" = "Alle Mitglieder"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!"; +"All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht."; +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht . Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht."; /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Von %@ werden alle neuen Nachrichten ausgeblendet!"; +/* No comment provided by engineer. */ +"All profiles" = "Alle Profile"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Alle Ihre Kontakte bleiben verbunden."; @@ -554,6 +578,9 @@ /* No comment provided by engineer. */ "Apply" = "Anwenden"; +/* No comment provided by engineer. */ +"Apply to" = "Anwenden auf"; + /* No comment provided by engineer. */ "Archive and upload" = "Archivieren und Hochladen"; @@ -563,6 +590,9 @@ /* No comment provided by engineer. */ "Attach" = "Anhängen"; +/* No comment provided by engineer. */ +"attempts" = "Versuche"; + /* No comment provided by engineer. */ "Audio & video calls" = "Audio- & Videoanrufe"; @@ -605,6 +635,9 @@ /* No comment provided by engineer. */ "Back" = "Zurück"; +/* No comment provided by engineer. */ +"Background" = "Hintergrund-Farbe"; + /* No comment provided by engineer. */ "Bad desktop address" = "Falsche Desktop-Adresse"; @@ -626,6 +659,9 @@ /* No comment provided by engineer. */ "Better messages" = "Verbesserungen bei Nachrichten"; +/* No comment provided by engineer. */ +"Black" = "Schwarz"; + /* No comment provided by engineer. */ "Block" = "Blockieren"; @@ -650,7 +686,7 @@ /* rcv group event chat item */ "blocked %@" = "%@ wurde blockiert"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "wurde vom Administrator blockiert"; /* No comment provided by engineer. */ @@ -716,6 +752,9 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Die App kann nicht auf den Schlüsselbund zugreifen, um das Datenbank-Passwort zu speichern"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Die Nachricht kann nicht weitergeleitet werden"; + /* No comment provided by engineer. */ "Cannot receive file" = "Datei kann nicht empfangen werden"; @@ -774,6 +813,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Datenbank Archiv"; +/* No comment provided by engineer. */ +"Chat colors" = "Chat-Farben"; + /* No comment provided by engineer. */ "Chat console" = "Chat-Konsole"; @@ -801,6 +843,9 @@ /* No comment provided by engineer. */ "Chat preferences" = "Chat-Präferenzen"; +/* No comment provided by engineer. */ +"Chat theme" = "Chat-Design"; + /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -819,6 +864,15 @@ /* No comment provided by engineer. */ "Choose from library" = "Aus dem Fotoalbum auswählen"; +/* No comment provided by engineer. */ +"Chunks deleted" = "Daten-Pakete gelöscht"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Daten-Pakete heruntergeladen"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Daten-Pakete hochgeladen"; + /* No comment provided by engineer. */ "Clear" = "Löschen"; @@ -834,6 +888,9 @@ /* No comment provided by engineer. */ "Clear verification" = "Überprüfung zurücknehmen"; +/* No comment provided by engineer. */ +"Color mode" = "Farbvariante"; + /* No comment provided by engineer. */ "colored" = "farbig"; @@ -846,6 +903,9 @@ /* No comment provided by engineer. */ "complete" = "vollständig"; +/* No comment provided by engineer. */ +"Completed" = "Abgeschlossen"; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICE-Server konfigurieren"; @@ -915,18 +975,27 @@ /* No comment provided by engineer. */ "connected" = "Verbunden"; +/* No comment provided by engineer. */ +"Connected" = "Verbunden"; + /* No comment provided by engineer. */ "Connected desktop" = "Verbundener Desktop"; /* rcv group event chat item */ "connected directly" = "Direkt miteinander verbunden"; +/* No comment provided by engineer. */ +"Connected servers" = "Verbundene Server"; + /* No comment provided by engineer. */ "Connected to desktop" = "Mit dem Desktop verbunden"; /* No comment provided by engineer. */ "connecting" = "verbinde"; +/* No comment provided by engineer. */ +"Connecting" = "Verbinden"; + /* No comment provided by engineer. */ "connecting (accepted)" = "Verbindung (angenommen)"; @@ -975,9 +1044,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Verbindungszeitüberschreitung"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Die Verbindung mit dem Desktop wurde gestoppt"; + /* connection information */ "connection:%@" = "Verbindung:%@"; +/* No comment provided by engineer. */ +"Connections" = "Verbindungen"; + /* profile update event chat item */ "contact %@ changed to %@" = "Der Kontaktname wurde von %1$@ auf %2$@ geändert"; @@ -1020,6 +1095,9 @@ /* No comment provided by engineer. */ "Copy" = "Kopieren"; +/* No comment provided by engineer. */ +"Copy error" = "Fehlermeldung kopieren"; + /* No comment provided by engineer. */ "Core version: v%@" = "Core Version: v%@"; @@ -1065,6 +1143,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Erstellen Sie Ihr Profil"; +/* No comment provided by engineer. */ +"Created" = "Erstellt"; + /* No comment provided by engineer. */ "Created at" = "Erstellt um"; @@ -1089,6 +1170,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Aktuelles Passwort…"; +/* No comment provided by engineer. */ +"Current profile" = "Aktueller Profil"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Die derzeit maximal unterstützte Dateigröße beträgt %@."; @@ -1098,9 +1182,15 @@ /* No comment provided by engineer. */ "Custom time" = "Zeit anpassen"; +/* No comment provided by engineer. */ +"Customize theme" = "Design anpassen"; + /* No comment provided by engineer. */ "Dark" = "Dunkel"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Farben für die dunkle Variante"; + /* No comment provided by engineer. */ "Database downgrade" = "Datenbank auf alte Version herabstufen"; @@ -1170,6 +1260,9 @@ /* message decrypt error item */ "Decryption error" = "Entschlüsselungsfehler"; +/* No comment provided by engineer. */ +"decryption errors" = "Entschlüsselungs-Fehler"; + /* pref value */ "default (%@)" = "Voreinstellung (%@)"; @@ -1222,7 +1315,7 @@ "Delete Contact" = "Kontakt löschen"; /* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Kontakt löschen?\nDas kann nicht rückgängig gemacht werden!"; +"Delete contact?\nThis cannot be undone!" = "Kontakt löschen?\nDies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ "Delete database" = "Datenbank löschen"; @@ -1282,7 +1375,7 @@ "Delete pending connection" = "Ausstehende Verbindung löschen"; /* No comment provided by engineer. */ -"Delete pending connection?" = "Die ausstehende Verbindung löschen?"; +"Delete pending connection?" = "Ausstehende Verbindung löschen?"; /* No comment provided by engineer. */ "Delete profile" = "Profil löschen"; @@ -1296,6 +1389,9 @@ /* deleted chat item */ "deleted" = "Gelöscht"; +/* No comment provided by engineer. */ +"Deleted" = "Gelöscht"; + /* No comment provided by engineer. */ "Deleted at" = "Gelöscht um"; @@ -1308,6 +1404,9 @@ /* rcv group event chat item */ "deleted group" = "Gruppe gelöscht"; +/* No comment provided by engineer. */ +"Deletion errors" = "Fehler beim Löschen"; + /* No comment provided by engineer. */ "Delivery" = "Zustellung"; @@ -1332,6 +1431,12 @@ /* snd error text */ "Destination server error: %@" = "Zielserver-Fehler: %@"; +/* No comment provided by engineer. */ +"Detailed statistics" = "Detaillierte Statistiken"; + +/* No comment provided by engineer. */ +"Details" = "Details"; + /* No comment provided by engineer. */ "Develop" = "Entwicklung"; @@ -1434,12 +1539,21 @@ /* chat item action */ "Download" = "Herunterladen"; +/* No comment provided by engineer. */ +"Download errors" = "Fehler beim Herunterladen"; + /* No comment provided by engineer. */ "Download failed" = "Herunterladen fehlgeschlagen"; /* server test step */ "Download file" = "Datei herunterladen"; +/* No comment provided by engineer. */ +"Downloaded" = "Heruntergeladen"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Heruntergeladene Dateien"; + /* No comment provided by engineer. */ "Downloading archive" = "Archiv wird heruntergeladen"; @@ -1452,6 +1566,9 @@ /* integrity error chat item */ "duplicate message" = "Doppelte Nachricht"; +/* No comment provided by engineer. */ +"duplicates" = "Duplikate"; + /* No comment provided by engineer. */ "Duration" = "Dauer"; @@ -1710,6 +1827,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Fehler beim Exportieren der Chat-Datenbank"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Fehler beim Exportieren des Designs: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Fehler beim Importieren der Chat-Datenbank"; @@ -1725,9 +1845,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Fehler beim Empfangen der Datei"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Fehler beim Wiederherstellen der Verbindung zum Server"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Fehler beim Wiederherstellen der Verbindungen zu den Servern"; + /* No comment provided by engineer. */ "Error removing member" = "Fehler beim Entfernen des Mitglieds"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Fehler beim Zurücksetzen der Statistiken"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Fehler beim Speichern der %@-Server"; @@ -1807,6 +1936,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Fehler: URL ist ungültig"; +/* No comment provided by engineer. */ +"Errors" = "Fehler"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Auch wenn sie im Chat deaktiviert sind."; @@ -1819,12 +1951,18 @@ /* chat item action */ "Expand" = "Erweitern"; +/* No comment provided by engineer. */ +"expired" = "abgelaufen"; + /* No comment provided by engineer. */ "Export database" = "Datenbank exportieren"; /* No comment provided by engineer. */ "Export error:" = "Fehler beim Export:"; +/* No comment provided by engineer. */ +"Export theme" = "Design exportieren"; + /* No comment provided by engineer. */ "Exported database archive." = "Exportiertes Datenbankarchiv."; @@ -1846,6 +1984,21 @@ /* No comment provided by engineer. */ "Favorite" = "Favorit"; +/* No comment provided by engineer. */ +"File error" = "Datei-Fehler"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen."; + +/* file error text */ +"File server error: %@" = "Datei-Server Fehler: %@"; + +/* No comment provided by engineer. */ +"File status" = "Datei-Status"; + +/* copied message info */ +"File status: %@" = "Datei-Status: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Die Datei wird von den Servern gelöscht."; @@ -1960,6 +2113,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs und Sticker"; +/* message preview */ +"Good afternoon!" = "Guten Nachmittag!"; + +/* message preview */ +"Good morning!" = "Guten Morgen!"; + /* No comment provided by engineer. */ "Group" = "Gruppe"; @@ -2039,10 +2198,10 @@ "Group welcome message" = "Gruppen-Begrüßungsmeldung"; /* No comment provided by engineer. */ -"Group will be deleted for all members - this cannot be undone!" = "Die Gruppe wird für alle Mitglieder gelöscht - dies kann nicht rückgängig gemacht werden!"; +"Group will be deleted for all members - this cannot be undone!" = "Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ -"Group will be deleted for you - this cannot be undone!" = "Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden!"; +"Group will be deleted for you - this cannot be undone!" = "Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ "Help" = "Hilfe"; @@ -2137,6 +2296,9 @@ /* No comment provided by engineer. */ "Import failed" = "Import ist fehlgeschlagen"; +/* No comment provided by engineer. */ +"Import theme" = "Design importieren"; + /* No comment provided by engineer. */ "Importing archive" = "Archiv wird importiert"; @@ -2158,6 +2320,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Klingeltöne"; +/* No comment provided by engineer. */ +"inactive" = "Inaktiv"; + /* No comment provided by engineer. */ "Incognito" = "Inkognito"; @@ -2221,6 +2386,9 @@ /* No comment provided by engineer. */ "Interface" = "Schnittstelle"; +/* No comment provided by engineer. */ +"Interface colors" = "Interface-Farben"; + /* invalid chat data */ "invalid chat" = "Ungültiger Chat"; @@ -2473,6 +2641,9 @@ /* rcv group event chat item */ "member connected" = "ist der Gruppe beigetreten"; +/* item status text */ +"Member inactive" = "Mitglied inaktiv"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Die Mitgliederrolle wird auf \"%@\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt."; @@ -2480,7 +2651,10 @@ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Die Mitgliederrolle wird auf \"%@\" geändert. Das Mitglied wird eine neue Einladung erhalten."; /* No comment provided by engineer. */ -"Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt - dies kann nicht rückgängig gemacht werden!"; +"Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!"; + +/* No comment provided by engineer. */ +"Menus" = "Menüs"; /* item status text */ "Message delivery error" = "Fehler bei der Nachrichtenzustellung"; @@ -2494,6 +2668,12 @@ /* No comment provided by engineer. */ "Message draft" = "Nachrichtenentwurf"; +/* item status text */ +"Message forwarded" = "Nachricht weitergeleitet"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird."; + /* No comment provided by engineer. */ "Message queue info" = "Nachrichten-Warteschlangen-Information"; @@ -2518,6 +2698,12 @@ /* No comment provided by engineer. */ "Message source remains private." = "Die Nachrichtenquelle bleibt privat."; +/* No comment provided by engineer. */ +"Message status" = "Nachrichten-Status"; + +/* copied message info */ +"Message status: %@" = "Nachrichten-Status: %@"; + /* No comment provided by engineer. */ "Message text" = "Nachrichtentext"; @@ -2533,6 +2719,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Die Nachrichten von %@ werden angezeigt!"; +/* No comment provided by engineer. */ +"Messages received" = "Empfangene Nachrichten"; + +/* No comment provided by engineer. */ +"Messages sent" = "Gesendete Nachrichten"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Nachrichten, Dateien und Anrufe sind durch **Ende-zu-Ende-Verschlüsselung** mit Perfect Forward Secrecy, Ablehnung und Einbruchs-Wiederherstellung geschützt."; @@ -2698,6 +2890,9 @@ /* No comment provided by engineer. */ "No device token!" = "Kein Geräte-Token!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet."; + /* No comment provided by engineer. */ "no e2e encryption" = "Keine E2E-Verschlüsselung"; @@ -2710,6 +2905,9 @@ /* No comment provided by engineer. */ "No history" = "Kein Nachrichtenverlauf"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Keine Information - es wird versucht neu zu laden"; + /* No comment provided by engineer. */ "No network connection" = "Keine Netzwerkverbindung"; @@ -2835,6 +3033,9 @@ /* authentication reason */ "Open migration to another device" = "Migration auf ein anderes Gerät öffnen"; +/* No comment provided by engineer. */ +"Open server settings" = "Server-Einstellungen öffnen"; + /* No comment provided by engineer. */ "Open Settings" = "Geräte-Einstellungen öffnen"; @@ -2859,9 +3060,15 @@ /* No comment provided by engineer. */ "Or show this code" = "Oder diesen QR-Code anzeigen"; +/* No comment provided by engineer. */ +"other" = "andere"; + /* No comment provided by engineer. */ "Other" = "Andere"; +/* No comment provided by engineer. */ +"other errors" = "Andere Fehler"; + /* member role */ "owner" = "Eigentümer"; @@ -2904,6 +3111,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "Peer-to-Peer"; +/* No comment provided by engineer. */ +"Pending" = "Ausstehend"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen."; @@ -2925,6 +3135,9 @@ /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Bitten Sie Ihren Kontakt darum, das Senden von Sprachnachrichten zu aktivieren."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Bitte überprüfen Sie, ob sich das Mobiltelefon und die Desktop-App im gleichen lokalen Netzwerk befinden, und die Desktop-Firewall die Verbindung erlaubt.\nBitte teilen Sie weitere mögliche Probleme den Entwicklern mit."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt nochmal darum, Ihnen einen Link zuzusenden."; @@ -2982,6 +3195,9 @@ /* No comment provided by engineer. */ "Preview" = "Vorschau"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Bisher verbundene Server"; + /* No comment provided by engineer. */ "Privacy & security" = "Datenschutz & Sicherheit"; @@ -3021,6 +3237,9 @@ /* No comment provided by engineer. */ "Profile password" = "Passwort für Profil"; +/* No comment provided by engineer. */ +"Profile theme" = "Profil-Design"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "Profil-Aktualisierung wird an Ihre Kontakte gesendet."; @@ -3069,6 +3288,12 @@ /* No comment provided by engineer. */ "Protocol timeout per KB" = "Protokollzeitüberschreitung pro kB"; +/* No comment provided by engineer. */ +"Proxied" = "Proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Proxy-Server"; + /* No comment provided by engineer. */ "Push notifications" = "Push-Benachrichtigungen"; @@ -3111,6 +3336,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Bestätigungen sind deaktiviert"; +/* No comment provided by engineer. */ +"Receive errors" = "Fehler beim Empfang"; + /* No comment provided by engineer. */ "received answer…" = "Antwort erhalten…"; @@ -3129,6 +3357,15 @@ /* message info title */ "Received message" = "Empfangene Nachricht"; +/* No comment provided by engineer. */ +"Received messages" = "Empfangene Nachrichten"; + +/* No comment provided by engineer. */ +"Received reply" = "Empfangene Antwort"; + +/* No comment provided by engineer. */ +"Received total" = "Summe aller empfangenen Nachrichten"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist."; @@ -3147,9 +3384,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben."; +/* No comment provided by engineer. */ +"Reconnect" = "Neu verbinden"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Alle verbundenen Server werden neu verbunden, um die Zustellung der Nachricht zu erzwingen. Dies verursacht zusätzlichen Datenverkehr."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Alle Server neu verbinden"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Alle Server neu verbinden?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Um die Auslieferung von Nachrichten zu erzwingen, wird der Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Server neu verbinden?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Die Server neu verbinden?"; @@ -3183,6 +3435,9 @@ /* No comment provided by engineer. */ "Remove" = "Entfernen"; +/* No comment provided by engineer. */ +"Remove image" = "Bild entfernen"; + /* No comment provided by engineer. */ "Remove member" = "Mitglied entfernen"; @@ -3240,12 +3495,24 @@ /* No comment provided by engineer. */ "Reset" = "Zurücksetzen"; +/* No comment provided by engineer. */ +"Reset all statistics" = "Alle Statistiken zurücksetzen"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Alle Statistiken zurücksetzen?"; + /* No comment provided by engineer. */ "Reset colors" = "Farben zurücksetzen"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Auf das App-Design zurücksetzen"; + /* No comment provided by engineer. */ "Reset to defaults" = "Auf Voreinstellungen zurücksetzen"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Auf das Benutzer-spezifische Design zurücksetzen"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Um ein neues Chat-Profil zu erstellen, starten Sie die App neu"; @@ -3360,6 +3627,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Gespeicherte WebRTC ICE-Server werden entfernt"; +/* No comment provided by engineer. */ +"Scale" = "Skalieren"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Link scannen / einfügen"; + /* No comment provided by engineer. */ "Scan code" = "Code scannen"; @@ -3387,6 +3660,9 @@ /* network option */ "sec" = "sek"; +/* No comment provided by engineer. */ +"Secondary" = "Zweite Farbe"; + /* time unit */ "seconds" = "Sekunden"; @@ -3396,6 +3672,9 @@ /* server test step */ "Secure queue" = "Sichere Warteschlange"; +/* No comment provided by engineer. */ +"Secured" = "Abgesichert"; + /* No comment provided by engineer. */ "Security assessment" = "Sicherheits-Gutachten"; @@ -3408,6 +3687,9 @@ /* No comment provided by engineer. */ "Select" = "Auswählen"; +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt."; + /* No comment provided by engineer. */ "Self-destruct" = "Selbstzerstörung"; @@ -3441,6 +3723,9 @@ /* No comment provided by engineer. */ "Send disappearing message" = "Verschwindende Nachricht senden"; +/* No comment provided by engineer. */ +"Send errors" = "Fehler beim Senden"; + /* No comment provided by engineer. */ "Send link previews" = "Link-Vorschau senden"; @@ -3507,15 +3792,33 @@ /* copied message info */ "Sent at: %@" = "Gesendet um: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Direkt gesendet"; + /* notification */ "Sent file event" = "Datei-Ereignis wurde gesendet"; /* message info title */ "Sent message" = "Gesendete Nachricht"; +/* No comment provided by engineer. */ +"Sent messages" = "Gesendete Nachrichten"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Gesendete Nachrichten werden nach der eingestellten Zeit gelöscht."; +/* No comment provided by engineer. */ +"Sent reply" = "Gesendete Antwort"; + +/* No comment provided by engineer. */ +"Sent total" = "Summe aller gesendeten Nachrichten"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Über einen Proxy gesendet"; + +/* No comment provided by engineer. */ +"Server address" = "Server-Adresse"; + /* srv error text. */ "Server address is incompatible with network settings." = "Die Server-Adresse ist nicht mit den Netzwerk-Einstellungen kompatibel."; @@ -3531,12 +3834,21 @@ /* No comment provided by engineer. */ "Server test failed!" = "Server Test ist fehlgeschlagen!"; +/* No comment provided by engineer. */ +"Server type" = "Server-Typ"; + /* srv error text */ "Server version is incompatible with network settings." = "Die Server-Version ist nicht mit den Netzwerk-Einstellungen kompatibel."; /* No comment provided by engineer. */ "Servers" = "Server"; +/* No comment provided by engineer. */ +"Servers info" = "Server-Informationen"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Die Serverstatistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden!"; + /* No comment provided by engineer. */ "Session code" = "Sitzungscode"; @@ -3546,6 +3858,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Kontaktname festlegen…"; +/* No comment provided by engineer. */ +"Set default theme" = "Default-Design einstellen"; + /* No comment provided by engineer. */ "Set group preferences" = "Gruppen-Präferenzen einstellen"; @@ -3624,6 +3939,9 @@ /* No comment provided by engineer. */ "Show:" = "Anzeigen:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "SimpleX-Adresse"; @@ -3669,6 +3987,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Vereinfachter Inkognito-Modus"; +/* No comment provided by engineer. */ +"Size" = "Größe"; + /* No comment provided by engineer. */ "Skip" = "Überspringen"; @@ -3678,6 +3999,9 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Kleine Gruppen (max. 20)"; +/* No comment provided by engineer. */ +"SMP server" = "SMP-Server"; + /* No comment provided by engineer. */ "SMP servers" = "SMP-Server"; @@ -3702,9 +4026,15 @@ /* No comment provided by engineer. */ "Start migration" = "Starten Sie die Migration"; +/* No comment provided by engineer. */ +"Starting from %@." = "Beginnend mit %@."; + /* No comment provided by engineer. */ "starting…" = "Verbindung wird gestartet…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistiken"; + /* No comment provided by engineer. */ "Stop" = "Beenden"; @@ -3747,6 +4077,15 @@ /* No comment provided by engineer. */ "Submit" = "Bestätigen"; +/* No comment provided by engineer. */ +"Subscribed" = "Abonniert"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Fehler beim Abonnieren"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Nicht beachtete Abonnements"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Unterstützung von SimpleX Chat"; @@ -3795,6 +4134,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Temporärer Datei-Fehler"; + /* server test failure */ "Test failed at step %@." = "Der Test ist beim Schritt %@ fehlgeschlagen."; @@ -3876,6 +4218,9 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Der von Ihnen eingefügte Text ist kein SimpleX-Link."; +/* No comment provided by engineer. */ +"Themes" = "Design"; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Diese Einstellungen betreffen Ihr aktuelles Profil **%@**."; @@ -3918,9 +4263,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Das ist Ihre eigene SimpleX-Adresse!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil **%@**."; +/* No comment provided by engineer. */ +"Title" = "Bezeichnung"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Um Fragen zu stellen und aktuelle Informationen zu erhalten:"; @@ -3960,9 +4311,15 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Inkognito beim Verbinden einschalten."; +/* No comment provided by engineer. */ +"Total" = "Summe aller Abonnements"; + /* No comment provided by engineer. */ "Transport isolation" = "Transport-Isolation"; +/* No comment provided by engineer. */ +"Transport sessions" = "Transport-Sitzungen"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %@)."; @@ -4098,12 +4455,21 @@ /* No comment provided by engineer. */ "Upgrade and open chat" = "Aktualisieren und den Chat öffnen"; +/* No comment provided by engineer. */ +"Upload errors" = "Fehler beim Hochladen"; + /* No comment provided by engineer. */ "Upload failed" = "Hochladen fehlgeschlagen"; /* server test step */ "Upload file" = "Datei hochladen"; +/* No comment provided by engineer. */ +"Uploaded" = "Hochgeladen"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Hochgeladene Dateien"; + /* No comment provided by engineer. */ "Uploading archive" = "Archiv wird hochgeladen"; @@ -4149,6 +4515,9 @@ /* No comment provided by engineer. */ "User profile" = "Benutzerprofil"; +/* No comment provided by engineer. */ +"User selection" = "Benutzer-Auswahl"; + /* No comment provided by engineer. */ "Using .onion hosts requires compatible VPN provider." = "Für die Nutzung von .onion-Hosts sind kompatible VPN-Anbieter erforderlich."; @@ -4257,6 +4626,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Auf das Video warten"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Wallpaper-Akzent"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Wallpaper-Hintergrund"; + /* No comment provided by engineer. */ "wants to connect to you!" = "möchte sich mit Ihnen verbinden!"; @@ -4329,9 +4704,15 @@ /* snd error text */ "Wrong key or unknown connection - most likely this connection is deleted." = "Falscher Schlüssel oder unbekannte Verbindung - höchstwahrscheinlich ist diese Verbindung gelöscht worden."; +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Falscher Schlüssel oder unbekannte Daten-Paketadresse der Datei - höchstwahrscheinlich wurde die Datei gelöscht."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Falsches Passwort!"; +/* No comment provided by engineer. */ +"XFTP server" = "XFTP-Server"; + /* No comment provided by engineer. */ "XFTP servers" = "XFTP-Server"; @@ -4389,6 +4770,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Sie sind zu der Gruppe eingeladen"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt."; + /* No comment provided by engineer. */ "you are observer" = "Sie sind Beobachter"; @@ -4417,7 +4801,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Sie können sie über Einstellungen für Ihre SimpleX-Kontakte sichtbar machen."; /* notification body */ -"You can now send messages to %@" = "Sie können nun Nachrichten an %@ versenden"; +"You can now chat with %@" = "Sie können nun Nachrichten an %@ versenden"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben."; diff --git a/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings index 5fe2ef2d09..0dee85ad95 100644 --- a/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings @@ -2,7 +2,7 @@ "CFBundleName" = "SimpleX"; /* Privacy - Camera Usage Description */ -"NSCameraUsageDescription" = "SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Nutzern zu scannen und Videoanrufe durchzuführen."; +"NSCameraUsageDescription" = "SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Benutzern zu scannen und Videoanrufe durchzuführen."; /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "Face ID wird von SimpleX für die lokale Authentifizierung genutzt"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 54316b092f..1a8835c976 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ está verificado"; -/* No comment provided by engineer. */ -"%@ servers" = "Servidores %@"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ subido"; @@ -650,7 +647,7 @@ /* rcv group event chat item */ "blocked %@" = "ha bloqueado a %@"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "bloqueado por administrador"; /* No comment provided by engineer. */ @@ -4417,7 +4414,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Puedes hacerlo visible para tus contactos de SimpleX en Configuración."; /* notification body */ -"You can now send messages to %@" = "Ya puedes enviar mensajes a %@"; +"You can now chat with %@" = "Ya puedes enviar mensajes a %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 1405f14694..84613e6a54 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -121,9 +121,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ on vahvistettu"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ palvelimet"; - /* notification title */ "%@ wants to connect!" = "%@ haluaa muodostaa yhteyden!"; @@ -3397,7 +3394,7 @@ "You can hide or mute a user profile - swipe it to the right." = "Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle."; /* notification body */ -"You can now send messages to %@" = "Voit nyt lähettää viestejä %@:lle"; +"You can now chat with %@" = "Voit nyt lähettää viestejä %@:lle"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista."; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index aa9bfef3e5..c2777fac75 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ est vérifié·e"; -/* No comment provided by engineer. */ -"%@ servers" = "Serveurs %@"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ envoyé"; @@ -650,7 +647,7 @@ /* rcv group event chat item */ "blocked %@" = "%@ bloqué"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "bloqué par l'administrateur"; /* No comment provided by engineer. */ @@ -4417,7 +4414,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Vous pouvez le rendre visible à vos contacts SimpleX via Paramètres."; /* notification body */ -"You can now send messages to %@" = "Vous pouvez maintenant envoyer des messages à %@"; +"You can now chat with %@" = "Vous pouvez maintenant envoyer des messages à %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Vous pouvez configurer l'aperçu des notifications sur l'écran de verrouillage via les paramètres."; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 44b3447ef8..ab732754c9 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ ellenőrizve"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ kiszolgáló"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ feltöltve"; @@ -337,6 +334,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "gombra fent, majd válassza ki:"; +/* No comment provided by engineer. */ +"Accent" = "Kiemelés"; + /* accept contact request via notification accept incoming call via notification */ "Accept" = "Elfogadás"; @@ -353,6 +353,12 @@ /* call status */ "accepted call" = "elfogadott hívás"; +/* No comment provided by engineer. */ +"Acknowledged" = "Nyugtázva"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Nyugtázott hibák"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Cím hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára."; @@ -377,6 +383,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Üdvözlő üzenet hozzáadása"; +/* No comment provided by engineer. */ +"Additional accent" = "További kiemelés"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "További kiemelés 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "További másodlagos"; + /* No comment provided by engineer. */ "Address" = "Cím"; @@ -398,6 +413,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Speciális hálózati beállítások"; +/* No comment provided by engineer. */ +"Advanced settings" = "Haladó beállítások"; + /* chat item text */ "agreeing encryption for %@…" = "titkosítás jóváhagyása %@ számára…"; @@ -413,6 +431,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "A jelkód megadása után minden adat törlésre kerül."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Minden adat biztonságban van a készülékén."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Minden csoporttag kapcsolódva marad."; @@ -428,6 +449,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Minden új üzenet elrejtésre kerül tőle: %@!"; +/* No comment provided by engineer. */ +"All profiles" = "Minden profil"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Minden ismerős kapcsolódva marad."; @@ -554,6 +578,9 @@ /* No comment provided by engineer. */ "Apply" = "Alkalmaz"; +/* No comment provided by engineer. */ +"Apply to" = "Alkalmazás erre"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiválás és feltöltés"; @@ -563,6 +590,9 @@ /* No comment provided by engineer. */ "Attach" = "Csatolás"; +/* No comment provided by engineer. */ +"attempts" = "próbálkozások"; + /* No comment provided by engineer. */ "Audio & video calls" = "Hang- és videóhívások"; @@ -605,6 +635,9 @@ /* No comment provided by engineer. */ "Back" = "Vissza"; +/* No comment provided by engineer. */ +"Background" = "Háttér"; + /* No comment provided by engineer. */ "Bad desktop address" = "Hibás számítógép cím"; @@ -626,6 +659,9 @@ /* No comment provided by engineer. */ "Better messages" = "Jobb üzenetek"; +/* No comment provided by engineer. */ +"Black" = "Fekete"; + /* No comment provided by engineer. */ "Block" = "Blokkolás"; @@ -650,7 +686,7 @@ /* rcv group event chat item */ "blocked %@" = "letiltotta őt: %@"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "letiltva az admin által"; /* No comment provided by engineer. */ @@ -716,6 +752,9 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Nem lehet hozzáférni a kulcstartóhoz az adatbázis jelszavának mentéséhez"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Nem lehet továbbítani az üzenetet"; + /* No comment provided by engineer. */ "Cannot receive file" = "Nem lehet fogadni a fájlt"; @@ -774,6 +813,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Csevegési archívum"; +/* No comment provided by engineer. */ +"Chat colors" = "Csevegés színei"; + /* No comment provided by engineer. */ "Chat console" = "Csevegési konzol"; @@ -801,6 +843,9 @@ /* No comment provided by engineer. */ "Chat preferences" = "Csevegési beállítások"; +/* No comment provided by engineer. */ +"Chat theme" = "Csevegés témája"; + /* No comment provided by engineer. */ "Chats" = "Csevegések"; @@ -819,6 +864,15 @@ /* No comment provided by engineer. */ "Choose from library" = "Választás a könyvtárból"; +/* No comment provided by engineer. */ +"Chunks deleted" = "Törölt fájltöredékek"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Letöltött fájltöredékek"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Feltöltött fájltöredékek"; + /* No comment provided by engineer. */ "Clear" = "Kiürítés"; @@ -834,6 +888,9 @@ /* No comment provided by engineer. */ "Clear verification" = "Hitelesítés törlése"; +/* No comment provided by engineer. */ +"Color mode" = "Színmód"; + /* No comment provided by engineer. */ "colored" = "színes"; @@ -846,6 +903,9 @@ /* No comment provided by engineer. */ "complete" = "befejezett"; +/* No comment provided by engineer. */ +"Completed" = "Elkészült"; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICE kiszolgálók beállítása"; @@ -915,18 +975,27 @@ /* No comment provided by engineer. */ "connected" = "kapcsolódva"; +/* No comment provided by engineer. */ +"Connected" = "Kapcsolódva"; + /* No comment provided by engineer. */ "Connected desktop" = "Csatlakoztatott számítógép"; /* rcv group event chat item */ "connected directly" = "közvetlenül kapcsolódva"; +/* No comment provided by engineer. */ +"Connected servers" = "Kapcsolódott kiszolgálók"; + /* No comment provided by engineer. */ "Connected to desktop" = "Kapcsolódva a számítógéphez"; /* No comment provided by engineer. */ "connecting" = "kapcsolódás"; +/* No comment provided by engineer. */ +"Connecting" = "Kapcsolódás"; + /* No comment provided by engineer. */ "connecting (accepted)" = "kapcsolódás (elfogadva)"; @@ -975,9 +1044,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Kapcsolat időtúllépés"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "A kapcsolat a számítógéppel megszakadt"; + /* connection information */ "connection:%@" = "kapcsolat: %@"; +/* No comment provided by engineer. */ +"Connections" = "Kapcsolatok"; + /* profile update event chat item */ "contact %@ changed to %@" = "%1$@ megváltoztatta a nevét erre: %2$@"; @@ -1020,6 +1095,9 @@ /* No comment provided by engineer. */ "Copy" = "Másolás"; +/* No comment provided by engineer. */ +"Copy error" = "Másolási hiba"; + /* No comment provided by engineer. */ "Core version: v%@" = "Alapverziószám: v%@"; @@ -1065,6 +1143,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Saját profil létrehozása"; +/* No comment provided by engineer. */ +"Created" = "Létrehozva"; + /* No comment provided by engineer. */ "Created at" = "Létrehozva ekkor:"; @@ -1089,6 +1170,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Jelenlegi jelmondat…"; +/* No comment provided by engineer. */ +"Current profile" = "Jelenlegi profil"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Jelenleg a maximális támogatott fájlméret %@."; @@ -1098,9 +1182,15 @@ /* No comment provided by engineer. */ "Custom time" = "Személyreszabott idő"; +/* No comment provided by engineer. */ +"Customize theme" = "Téma személyre szabása"; + /* No comment provided by engineer. */ "Dark" = "Sötét"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Sötét mód színei"; + /* No comment provided by engineer. */ "Database downgrade" = "Visszatérés a korábbi adatbázis verzióra"; @@ -1170,6 +1260,9 @@ /* message decrypt error item */ "Decryption error" = "Titkosítás visszafejtési hiba"; +/* No comment provided by engineer. */ +"decryption errors" = "visszafejtési hibák"; + /* pref value */ "default (%@)" = "alapértelmezett (%@)"; @@ -1296,6 +1389,9 @@ /* deleted chat item */ "deleted" = "törölve"; +/* No comment provided by engineer. */ +"Deleted" = "Törölve"; + /* No comment provided by engineer. */ "Deleted at" = "Törölve ekkor:"; @@ -1308,6 +1404,9 @@ /* rcv group event chat item */ "deleted group" = "törölt csoport"; +/* No comment provided by engineer. */ +"Deletion errors" = "Törlési hibák"; + /* No comment provided by engineer. */ "Delivery" = "Kézbesítés"; @@ -1332,6 +1431,12 @@ /* snd error text */ "Destination server error: %@" = "Célkiszolgáló hiba: %@"; +/* No comment provided by engineer. */ +"Detailed statistics" = "Részletes statisztikák"; + +/* No comment provided by engineer. */ +"Details" = "Részletek"; + /* No comment provided by engineer. */ "Develop" = "Fejlesztés"; @@ -1434,12 +1539,21 @@ /* chat item action */ "Download" = "Letöltés"; +/* No comment provided by engineer. */ +"Download errors" = "Letöltési hibák"; + /* No comment provided by engineer. */ "Download failed" = "Sikertelen letöltés"; /* server test step */ "Download file" = "Fájl letöltése"; +/* No comment provided by engineer. */ +"Downloaded" = "Letöltve"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Letöltött fájlok"; + /* No comment provided by engineer. */ "Downloading archive" = "Archívum letöltése"; @@ -1452,6 +1566,9 @@ /* integrity error chat item */ "duplicate message" = "duplikált üzenet"; +/* No comment provided by engineer. */ +"duplicates" = "duplikációk"; + /* No comment provided by engineer. */ "Duration" = "Időtartam"; @@ -1710,6 +1827,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Hiba a csevegési adatbázis exportálásakor"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Hiba a téma exportálásakor: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Hiba a csevegési adatbázis importálásakor"; @@ -1725,9 +1845,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Hiba a fájl fogadásakor"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Hiba a kiszolgálóhoz való újrakapcsolódáskor"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Hiba a kiszolgálókhoz való újrakapcsolódáskor"; + /* No comment provided by engineer. */ "Error removing member" = "Hiba a tag eltávolításakor"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Hiba a statisztikák visszaállításakor"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Hiba történt a %@ kiszolgálók mentése közben"; @@ -1807,6 +1936,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Hiba: az URL érvénytelen"; +/* No comment provided by engineer. */ +"Errors" = "Hibák"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Akkor is, ha le van tiltva a beszélgetésben."; @@ -1819,12 +1951,18 @@ /* chat item action */ "Expand" = "Kibontás"; +/* No comment provided by engineer. */ +"expired" = "lejárt"; + /* No comment provided by engineer. */ "Export database" = "Adatbázis exportálása"; /* No comment provided by engineer. */ "Export error:" = "Exportálási hiba:"; +/* No comment provided by engineer. */ +"Export theme" = "Téma exportálása"; + /* No comment provided by engineer. */ "Exported database archive." = "Exportált adatbázis-archívum."; @@ -1846,6 +1984,21 @@ /* No comment provided by engineer. */ "Favorite" = "Kedvenc"; +/* No comment provided by engineer. */ +"File error" = "Fájlhiba"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "A fájl nem található - valószínűleg a fájlt törölték vagy visszavonták."; + +/* file error text */ +"File server error: %@" = "Fájlkiszolgáló hiba: %@"; + +/* No comment provided by engineer. */ +"File status" = "Fájlállapot"; + +/* copied message info */ +"File status: %@" = "Fájlállapot: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "A fájl törölve lesz a kiszolgálóról."; @@ -1960,6 +2113,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-ek és matricák"; +/* message preview */ +"Good afternoon!" = "Jó napot!"; + +/* message preview */ +"Good morning!" = "Jó reggelt!"; + /* No comment provided by engineer. */ "Group" = "Csoport"; @@ -2137,6 +2296,9 @@ /* No comment provided by engineer. */ "Import failed" = "Sikertelen importálás"; +/* No comment provided by engineer. */ +"Import theme" = "Téma importálása"; + /* No comment provided by engineer. */ "Importing archive" = "Archívum importálása"; @@ -2158,6 +2320,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Bejövő hívás csengőhangja"; +/* No comment provided by engineer. */ +"inactive" = "inaktív"; + /* No comment provided by engineer. */ "Incognito" = "Inkognitó"; @@ -2221,6 +2386,9 @@ /* No comment provided by engineer. */ "Interface" = "Felület"; +/* No comment provided by engineer. */ +"Interface colors" = "Kezelőfelület színei"; + /* invalid chat data */ "invalid chat" = "érvénytelen csevegés"; @@ -2473,6 +2641,9 @@ /* rcv group event chat item */ "member connected" = "kapcsolódott"; +/* item status text */ +"Member inactive" = "Inaktív tag"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: „%@”. A csoport minden tagja értesítést kap róla."; @@ -2482,6 +2653,9 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "A tag eltávolítása a csoportból - ez a művelet nem vonható vissza!"; +/* No comment provided by engineer. */ +"Menus" = "Menük"; + /* item status text */ "Message delivery error" = "Üzenetkézbesítési hiba"; @@ -2494,6 +2668,12 @@ /* No comment provided by engineer. */ "Message draft" = "Üzenetvázlat"; +/* item status text */ +"Message forwarded" = "Továbbított üzenet"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Az üzenet később is kézbesíthető, ha a tag aktívvá válik."; + /* No comment provided by engineer. */ "Message queue info" = "Üzenet-várakoztatási információ"; @@ -2518,6 +2698,12 @@ /* No comment provided by engineer. */ "Message source remains private." = "Az üzenet forrása titokban marad."; +/* No comment provided by engineer. */ +"Message status" = "Üzenetállapot"; + +/* copied message info */ +"Message status: %@" = "Üzenetállapot: %@"; + /* No comment provided by engineer. */ "Message text" = "Üzenet szövege"; @@ -2533,6 +2719,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "A(z) %@ által írt üzenetek megjelennek!"; +/* No comment provided by engineer. */ +"Messages received" = "Fogadott üzenetek"; + +/* No comment provided by engineer. */ +"Messages sent" = "Elküldött üzenetek"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzeneteket, fájlokat és hívásokat **végpontok közötti titkosítással**, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi."; @@ -2698,6 +2890,9 @@ /* No comment provided by engineer. */ "No device token!" = "Nincs eszköztoken!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Még nincs közvetlen kapcsolat, az üzenetet az admin továbbítja."; + /* No comment provided by engineer. */ "no e2e encryption" = "nincs e2e titkosítás"; @@ -2710,6 +2905,9 @@ /* No comment provided by engineer. */ "No history" = "Nincsenek előzmények"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Nincs információ, próbálja meg újratölteni"; + /* No comment provided by engineer. */ "No network connection" = "Nincs hálózati kapcsolat"; @@ -2835,6 +3033,9 @@ /* authentication reason */ "Open migration to another device" = "Átköltöztetés megkezdése egy másik eszközre"; +/* No comment provided by engineer. */ +"Open server settings" = "Kiszolgáló beállításainak megnyitása"; + /* No comment provided by engineer. */ "Open Settings" = "Beállítások megnyitása"; @@ -2859,9 +3060,15 @@ /* No comment provided by engineer. */ "Or show this code" = "Vagy mutassa meg ezt a kódot"; +/* No comment provided by engineer. */ +"other" = "egyéb"; + /* No comment provided by engineer. */ "Other" = "További"; +/* No comment provided by engineer. */ +"other errors" = "egyéb hibák"; + /* member role */ "owner" = "tulajdonos"; @@ -2904,6 +3111,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "ponttól-pontig"; +/* No comment provided by engineer. */ +"Pending" = "Függő"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Az emberek csak az ön által megosztott hivatkozáson keresztül kapcsolódhatnak."; @@ -2925,6 +3135,9 @@ /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Ismerős felkérése, hogy engedélyezze a hangüzenetek küldését."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Ellenőrizze, hogy a mobil és az asztali számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint az asztali számítógép tűzfalában engedélyezve van-e a kapcsolat.\nMinden további problémát osszon meg a fejlesztőkkel."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg ismerősét, hogy küldjön egy másikat."; @@ -2982,6 +3195,9 @@ /* No comment provided by engineer. */ "Preview" = "Előnézet"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Korábban kapcsolódott kiszolgálók"; + /* No comment provided by engineer. */ "Privacy & security" = "Adatvédelem és biztonság"; @@ -3021,6 +3237,9 @@ /* No comment provided by engineer. */ "Profile password" = "Profiljelszó"; +/* No comment provided by engineer. */ +"Profile theme" = "Profiltéma"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "A profilfrissítés elküldésre került az ismerősök számára."; @@ -3069,6 +3288,12 @@ /* No comment provided by engineer. */ "Protocol timeout per KB" = "Protokoll időkorlát KB-onként"; +/* No comment provided by engineer. */ +"Proxied" = "Proxyzott"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Proxyzott kiszolgálók"; + /* No comment provided by engineer. */ "Push notifications" = "Push értesítések"; @@ -3111,6 +3336,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Üzenet kézbesítési jelentés letiltva"; +/* No comment provided by engineer. */ +"Receive errors" = "Üzenetfogadási hibák"; + /* No comment provided by engineer. */ "received answer…" = "fogadott válasz…"; @@ -3129,6 +3357,15 @@ /* message info title */ "Received message" = "Fogadott üzenet"; +/* No comment provided by engineer. */ +"Received messages" = "Fogadott üzenetek"; + +/* No comment provided by engineer. */ +"Received reply" = "Fogadott válasz"; + +/* No comment provided by engineer. */ +"Received total" = "Összes fogadott"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be."; @@ -3147,9 +3384,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "A címzettek a beírás közben látják a frissítéseket."; +/* No comment provided by engineer. */ +"Reconnect" = "Újrakapcsolás"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Újrakapcsolódás az összes kiszolgálóhoz az üzenetek kézbesítésének kikényszerítéséhez. Ez további forgalmat használ."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Újrakapcsolódás minden kiszolgálóhoz"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Újrakapcsolódás minden kiszolgálóhoz?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "A kiszolgálóhoz való újrakapcsolódás az üzenet kézbesítésének kikényszerítéséhez. Ez további adatforgalmat használ."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Újrakapcsolódás a kiszolgálóhoz?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Újrakapcsolódás a kiszolgálókhoz?"; @@ -3183,6 +3435,9 @@ /* No comment provided by engineer. */ "Remove" = "Eltávolítás"; +/* No comment provided by engineer. */ +"Remove image" = "Kép eltávolítása"; + /* No comment provided by engineer. */ "Remove member" = "Eltávolítás"; @@ -3240,12 +3495,24 @@ /* No comment provided by engineer. */ "Reset" = "Alaphelyzetbe állítás"; +/* No comment provided by engineer. */ +"Reset all statistics" = "Minden statisztika visszaállítása"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Minden statisztika visszaállítása?"; + /* No comment provided by engineer. */ "Reset colors" = "Színek alaphelyzetbe állítása"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Alkalmazás témájának visszaállítása"; + /* No comment provided by engineer. */ "Reset to defaults" = "Alaphelyzetbe állítás"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Felhasználó által létrehozott téma visszaállítása"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Új csevegési profil létrehozásához indítsa újra az alkalmazást"; @@ -3360,6 +3627,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "A mentett WebRTC ICE kiszolgálók eltávolításra kerülnek"; +/* No comment provided by engineer. */ +"Scale" = "Méretezés"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Hivatkozás beolvasása / beillesztése"; + /* No comment provided by engineer. */ "Scan code" = "Beolvasás"; @@ -3387,6 +3660,9 @@ /* network option */ "sec" = "mp"; +/* No comment provided by engineer. */ +"Secondary" = "Másodlagos"; + /* time unit */ "seconds" = "másodperc"; @@ -3396,6 +3672,9 @@ /* server test step */ "Secure queue" = "Biztonságos várólista"; +/* No comment provided by engineer. */ +"Secured" = "Biztosítva"; + /* No comment provided by engineer. */ "Security assessment" = "Biztonsági kiértékelés"; @@ -3408,6 +3687,9 @@ /* No comment provided by engineer. */ "Select" = "Választás"; +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "A kiválasztott csevegési beállítások tiltják ezt az üzenetet."; + /* No comment provided by engineer. */ "Self-destruct" = "Önmegsemmisítés"; @@ -3441,6 +3723,9 @@ /* No comment provided by engineer. */ "Send disappearing message" = "Eltűnő üzenet küldése"; +/* No comment provided by engineer. */ +"Send errors" = "Üzenetküldési hibák"; + /* No comment provided by engineer. */ "Send link previews" = "Hivatkozás előnézetek küldése"; @@ -3507,15 +3792,33 @@ /* copied message info */ "Sent at: %@" = "Elküldve ekkor: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Közvetlenül küldött"; + /* notification */ "Sent file event" = "Elküldött fájl esemény"; /* message info title */ "Sent message" = "Elküldött üzenet"; +/* No comment provided by engineer. */ +"Sent messages" = "Elküldött üzenetek"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Az elküldött üzenetek törlésre kerülnek a beállított idő után."; +/* No comment provided by engineer. */ +"Sent reply" = "Elküldött válasz"; + +/* No comment provided by engineer. */ +"Sent total" = "Összes elküldött"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Proxyn keresztül küldve"; + +/* No comment provided by engineer. */ +"Server address" = "Kiszolgáló címe"; + /* srv error text. */ "Server address is incompatible with network settings." = "A kiszolgáló címe nem kompatibilis a hálózati beállításokkal."; @@ -3531,12 +3834,21 @@ /* No comment provided by engineer. */ "Server test failed!" = "Sikertelen kiszolgáló-teszt!"; +/* No comment provided by engineer. */ +"Server type" = "Kiszolgáló típusa"; + /* srv error text */ "Server version is incompatible with network settings." = "A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal."; /* No comment provided by engineer. */ "Servers" = "Kiszolgálók"; +/* No comment provided by engineer. */ +"Servers info" = "információk a kiszolgálókról"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "A kiszolgálók statisztikái visszaállnak - ez nem vonható vissza!"; + /* No comment provided by engineer. */ "Session code" = "Munkamenet kód"; @@ -3546,6 +3858,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Ismerős nevének beállítása…"; +/* No comment provided by engineer. */ +"Set default theme" = "Alapértelmezett téma beállítása"; + /* No comment provided by engineer. */ "Set group preferences" = "Csoportbeállítások megadása"; @@ -3624,6 +3939,9 @@ /* No comment provided by engineer. */ "Show:" = "Megjelenítés:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "SimpleX cím"; @@ -3669,6 +3987,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Egyszerűsített inkognító mód"; +/* No comment provided by engineer. */ +"Size" = "Méret"; + /* No comment provided by engineer. */ "Skip" = "Kihagyás"; @@ -3678,6 +3999,9 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Kis csoportok (max. 20 tag)"; +/* No comment provided by engineer. */ +"SMP server" = "SMP-kiszolgáló"; + /* No comment provided by engineer. */ "SMP servers" = "SMP kiszolgálók"; @@ -3702,9 +4026,15 @@ /* No comment provided by engineer. */ "Start migration" = "Átköltöztetés indítása"; +/* No comment provided by engineer. */ +"Starting from %@." = "Kezdve ettől %@."; + /* No comment provided by engineer. */ "starting…" = "indítás…"; +/* No comment provided by engineer. */ +"Statistics" = "Statisztikák"; + /* No comment provided by engineer. */ "Stop" = "Megállítás"; @@ -3747,6 +4077,15 @@ /* No comment provided by engineer. */ "Submit" = "Elküldés"; +/* No comment provided by engineer. */ +"Subscribed" = "Feliratkozva"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Feliratkozási hibák"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Elutasított feliratkozások"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Támogassa a SimpleX Chatet"; @@ -3795,6 +4134,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Ideiglenes fájlhiba"; + /* server test failure */ "Test failed at step %@." = "A teszt sikertelen volt a(z) %@ lépésnél."; @@ -3876,6 +4218,9 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "A beillesztett szöveg nem egy SimpleX hivatkozás."; +/* No comment provided by engineer. */ +"Themes" = "Témák"; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Ezek a beállítások a jelenlegi **%@** profiljára vonatkoznak."; @@ -3918,9 +4263,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Ez az ön SimpleX címe!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Ezt a hivatkozást egy másik mobilleszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Ez a beállítás a jelenlegi **%@** profiljában lévő üzenetekre érvényes."; +/* No comment provided by engineer. */ +"Title" = "Cím"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Bármilyen kérdés feltevéséhez és a frissítésekért:"; @@ -3960,9 +4311,15 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Inkognitó mód kapcsolódáskor."; +/* No comment provided by engineer. */ +"Total" = "Összesen"; + /* No comment provided by engineer. */ "Transport isolation" = "Kapcsolat izolációs mód"; +/* No comment provided by engineer. */ +"Transport sessions" = "Munkamenetek átvitele"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál (hiba: %@)."; @@ -4098,12 +4455,21 @@ /* No comment provided by engineer. */ "Upgrade and open chat" = "A csevegés frissítése és megnyitása"; +/* No comment provided by engineer. */ +"Upload errors" = "Feltöltési hibák"; + /* No comment provided by engineer. */ "Upload failed" = "Sikertelen feltöltés"; /* server test step */ "Upload file" = "Fájl feltöltése"; +/* No comment provided by engineer. */ +"Uploaded" = "Feltöltve"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Feltöltött fájlok"; + /* No comment provided by engineer. */ "Uploading archive" = "Archívum feltöltése"; @@ -4149,6 +4515,9 @@ /* No comment provided by engineer. */ "User profile" = "Felhasználói profil"; +/* No comment provided by engineer. */ +"User selection" = "Felhasználó kiválasztása"; + /* No comment provided by engineer. */ "Using .onion hosts requires compatible VPN provider." = "A .onion kiszolgálók használatához kompatibilis VPN szolgáltatóra van szükség."; @@ -4257,6 +4626,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Videóra várakozás"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Háttérkép kiemelés"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Háttérkép háttérszíne"; + /* No comment provided by engineer. */ "wants to connect to you!" = "kapcsolatba akar lépni önnel!"; @@ -4329,9 +4704,15 @@ /* snd error text */ "Wrong key or unknown connection - most likely this connection is deleted." = "Rossz kulcs vagy ismeretlen kapcsolat - valószínűleg ez a kapcsolat törlődött."; +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Hibás kulcs vagy ismeretlen fájltöredék cím - valószínűleg a fájl törlődött."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Téves jelmondat!"; +/* No comment provided by engineer. */ +"XFTP server" = "XFTP-kiszolgáló"; + /* No comment provided by engineer. */ "XFTP servers" = "XFTP kiszolgálók"; @@ -4389,6 +4770,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Meghívást kapott a csoportba"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál."; + /* No comment provided by engineer. */ "you are observer" = "megfigyelő szerep"; @@ -4417,7 +4801,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Láthatóvá teheti SimpleX ismerősök számára a Beállításokban."; /* notification body */ -"You can now send messages to %@" = "Mostantól küldhet üzeneteket %@ számára"; +"You can now chat with %@" = "Mostantól küldhet üzeneteket %@ számára"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "A beállításokon keresztül beállíthatja a lezárási képernyő értesítési előnézetét."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index addb026ab7..24b604ecc0 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ è verificato/a"; -/* No comment provided by engineer. */ -"%@ servers" = "Server %@"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ caricati"; @@ -337,6 +334,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "sopra, quindi scegli:"; +/* No comment provided by engineer. */ +"Accent" = "Principale"; + /* accept contact request via notification accept incoming call via notification */ "Accept" = "Accetta"; @@ -353,6 +353,12 @@ /* call status */ "accepted call" = "chiamata accettata"; +/* No comment provided by engineer. */ +"Acknowledged" = "Riconosciuto"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Errori di riconoscimento"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti."; @@ -377,6 +383,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Aggiungi messaggio di benvenuto"; +/* No comment provided by engineer. */ +"Additional accent" = "Principale aggiuntivo"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Principale aggiuntivo 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Secondario aggiuntivo"; + /* No comment provided by engineer. */ "Address" = "Indirizzo"; @@ -398,6 +413,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Impostazioni di rete avanzate"; +/* No comment provided by engineer. */ +"Advanced settings" = "Impostazioni avanzate"; + /* chat item text */ "agreeing encryption for %@…" = "concordando la crittografia per %@…"; @@ -413,6 +431,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Tutti i dati vengono cancellati quando inserito."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Tutti i dati sono privati, nel tuo dispositivo."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Tutti i membri del gruppo resteranno connessi."; @@ -428,6 +449,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Tutti i nuovi messaggi da %@ verrranno nascosti!"; +/* No comment provided by engineer. */ +"All profiles" = "Tutti gli profili"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Tutti i tuoi contatti resteranno connessi."; @@ -554,6 +578,9 @@ /* No comment provided by engineer. */ "Apply" = "Applica"; +/* No comment provided by engineer. */ +"Apply to" = "Applica a"; + /* No comment provided by engineer. */ "Archive and upload" = "Archivia e carica"; @@ -563,6 +590,9 @@ /* No comment provided by engineer. */ "Attach" = "Allega"; +/* No comment provided by engineer. */ +"attempts" = "tentativi"; + /* No comment provided by engineer. */ "Audio & video calls" = "Chiamate audio e video"; @@ -605,6 +635,9 @@ /* No comment provided by engineer. */ "Back" = "Indietro"; +/* No comment provided by engineer. */ +"Background" = "Sfondo"; + /* No comment provided by engineer. */ "Bad desktop address" = "Indirizzo desktop errato"; @@ -626,6 +659,9 @@ /* No comment provided by engineer. */ "Better messages" = "Messaggi migliorati"; +/* No comment provided by engineer. */ +"Black" = "Nero"; + /* No comment provided by engineer. */ "Block" = "Blocca"; @@ -650,7 +686,7 @@ /* rcv group event chat item */ "blocked %@" = "ha bloccato %@"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "bloccato dall'amministratore"; /* No comment provided by engineer. */ @@ -716,6 +752,9 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Impossibile accedere al portachiavi per salvare la password del database"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Impossibile inoltrare il messaggio"; + /* No comment provided by engineer. */ "Cannot receive file" = "Impossibile ricevere il file"; @@ -774,6 +813,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Archivio chat"; +/* No comment provided by engineer. */ +"Chat colors" = "Colori della chat"; + /* No comment provided by engineer. */ "Chat console" = "Console della chat"; @@ -801,6 +843,9 @@ /* No comment provided by engineer. */ "Chat preferences" = "Preferenze della chat"; +/* No comment provided by engineer. */ +"Chat theme" = "Tema della chat"; + /* No comment provided by engineer. */ "Chats" = "Chat"; @@ -819,6 +864,15 @@ /* No comment provided by engineer. */ "Choose from library" = "Scegli dalla libreria"; +/* No comment provided by engineer. */ +"Chunks deleted" = "Blocchi eliminati"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Blocchi scaricati"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Blocchi inviati"; + /* No comment provided by engineer. */ "Clear" = "Svuota"; @@ -834,6 +888,9 @@ /* No comment provided by engineer. */ "Clear verification" = "Annulla la verifica"; +/* No comment provided by engineer. */ +"Color mode" = "Modalità di colore"; + /* No comment provided by engineer. */ "colored" = "colorato"; @@ -846,6 +903,9 @@ /* No comment provided by engineer. */ "complete" = "completo"; +/* No comment provided by engineer. */ +"Completed" = "Completato"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Configura server ICE"; @@ -915,18 +975,27 @@ /* No comment provided by engineer. */ "connected" = "connesso/a"; +/* No comment provided by engineer. */ +"Connected" = "Connesso"; + /* No comment provided by engineer. */ "Connected desktop" = "Desktop connesso"; /* rcv group event chat item */ "connected directly" = "si è connesso/a direttamente"; +/* No comment provided by engineer. */ +"Connected servers" = "Server connessi"; + /* No comment provided by engineer. */ "Connected to desktop" = "Connesso al desktop"; /* No comment provided by engineer. */ "connecting" = "in connessione"; +/* No comment provided by engineer. */ +"Connecting" = "In connessione"; + /* No comment provided by engineer. */ "connecting (accepted)" = "in connessione (accettato)"; @@ -975,9 +1044,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Connessione scaduta"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Connessione con il desktop fermata"; + /* connection information */ "connection:%@" = "connessione:% @"; +/* No comment provided by engineer. */ +"Connections" = "Connessioni"; + /* profile update event chat item */ "contact %@ changed to %@" = "contatto %1$@ cambiato in %2$@"; @@ -1020,6 +1095,9 @@ /* No comment provided by engineer. */ "Copy" = "Copia"; +/* No comment provided by engineer. */ +"Copy error" = "Copia errore"; + /* No comment provided by engineer. */ "Core version: v%@" = "Versione core: v%@"; @@ -1065,6 +1143,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Crea il tuo profilo"; +/* No comment provided by engineer. */ +"Created" = "Creato"; + /* No comment provided by engineer. */ "Created at" = "Creato il"; @@ -1089,6 +1170,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Password attuale…"; +/* No comment provided by engineer. */ +"Current profile" = "Profilo attuale"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Attualmente la dimensione massima supportata è di %@."; @@ -1098,9 +1182,15 @@ /* No comment provided by engineer. */ "Custom time" = "Tempo personalizzato"; +/* No comment provided by engineer. */ +"Customize theme" = "Personalizza il tema"; + /* No comment provided by engineer. */ "Dark" = "Scuro"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Colori modalità scura"; + /* No comment provided by engineer. */ "Database downgrade" = "Downgrade del database"; @@ -1170,6 +1260,9 @@ /* message decrypt error item */ "Decryption error" = "Errore di decifrazione"; +/* No comment provided by engineer. */ +"decryption errors" = "errori di decifrazione"; + /* pref value */ "default (%@)" = "predefinito (%@)"; @@ -1296,6 +1389,9 @@ /* deleted chat item */ "deleted" = "eliminato"; +/* No comment provided by engineer. */ +"Deleted" = "Eliminato"; + /* No comment provided by engineer. */ "Deleted at" = "Eliminato il"; @@ -1308,6 +1404,9 @@ /* rcv group event chat item */ "deleted group" = "gruppo eliminato"; +/* No comment provided by engineer. */ +"Deletion errors" = "Errori di eliminazione"; + /* No comment provided by engineer. */ "Delivery" = "Consegna"; @@ -1332,6 +1431,12 @@ /* snd error text */ "Destination server error: %@" = "Errore del server di destinazione: %@"; +/* No comment provided by engineer. */ +"Detailed statistics" = "Statistiche dettagliate"; + +/* No comment provided by engineer. */ +"Details" = "Dettagli"; + /* No comment provided by engineer. */ "Develop" = "Sviluppa"; @@ -1434,12 +1539,21 @@ /* chat item action */ "Download" = "Scarica"; +/* No comment provided by engineer. */ +"Download errors" = "Errori di scaricamento"; + /* No comment provided by engineer. */ "Download failed" = "Scaricamento fallito"; /* server test step */ "Download file" = "Scarica file"; +/* No comment provided by engineer. */ +"Downloaded" = "Scaricato"; + +/* No comment provided by engineer. */ +"Downloaded files" = "File scaricati"; + /* No comment provided by engineer. */ "Downloading archive" = "Scaricamento archivio"; @@ -1452,6 +1566,9 @@ /* integrity error chat item */ "duplicate message" = "messaggio duplicato"; +/* No comment provided by engineer. */ +"duplicates" = "doppi"; + /* No comment provided by engineer. */ "Duration" = "Durata"; @@ -1710,6 +1827,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Errore nell'esportazione del database della chat"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Errore di esportazione del tema: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Errore nell'importazione del database della chat"; @@ -1725,9 +1845,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Errore nella ricezione del file"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Errore di riconnessione al server"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Errore di riconnessione ai server"; + /* No comment provided by engineer. */ "Error removing member" = "Errore nella rimozione del membro"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Errore di azzeramento statistiche"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Errore nel salvataggio dei server %@"; @@ -1807,6 +1936,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Errore: l'URL non è valido"; +/* No comment provided by engineer. */ +"Errors" = "Errori"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Anche quando disattivato nella conversazione."; @@ -1819,12 +1951,18 @@ /* chat item action */ "Expand" = "Espandi"; +/* No comment provided by engineer. */ +"expired" = "scaduto"; + /* No comment provided by engineer. */ "Export database" = "Esporta database"; /* No comment provided by engineer. */ "Export error:" = "Errore di esportazione:"; +/* No comment provided by engineer. */ +"Export theme" = "Esporta tema"; + /* No comment provided by engineer. */ "Exported database archive." = "Archivio database esportato."; @@ -1846,6 +1984,21 @@ /* No comment provided by engineer. */ "Favorite" = "Preferito"; +/* No comment provided by engineer. */ +"File error" = "Errore del file"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "File non trovato - probabilmente è stato eliminato o annullato."; + +/* file error text */ +"File server error: %@" = "Errore del server dei file: %@"; + +/* No comment provided by engineer. */ +"File status" = "Stato del file"; + +/* copied message info */ +"File status: %@" = "Stato del file: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Il file verrà eliminato dai server."; @@ -1960,6 +2113,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF e adesivi"; +/* message preview */ +"Good afternoon!" = "Buon pomeriggio!"; + +/* message preview */ +"Good morning!" = "Buongiorno!"; + /* No comment provided by engineer. */ "Group" = "Gruppo"; @@ -2137,6 +2296,9 @@ /* No comment provided by engineer. */ "Import failed" = "Importazione fallita"; +/* No comment provided by engineer. */ +"Import theme" = "Importa tema"; + /* No comment provided by engineer. */ "Importing archive" = "Importazione archivio"; @@ -2158,6 +2320,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Suoni nelle chiamate"; +/* No comment provided by engineer. */ +"inactive" = "inattivo"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -2221,6 +2386,9 @@ /* No comment provided by engineer. */ "Interface" = "Interfaccia"; +/* No comment provided by engineer. */ +"Interface colors" = "Colori dell'interfaccia"; + /* invalid chat data */ "invalid chat" = "chat non valida"; @@ -2473,6 +2641,9 @@ /* rcv group event chat item */ "member connected" = "si è connesso/a"; +/* item status text */ +"Member inactive" = "Membro inattivo"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Tutti i membri del gruppo verranno avvisati."; @@ -2482,6 +2653,9 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Il membro verrà rimosso dal gruppo, non è reversibile!"; +/* No comment provided by engineer. */ +"Menus" = "Menu"; + /* item status text */ "Message delivery error" = "Errore di recapito del messaggio"; @@ -2494,6 +2668,12 @@ /* No comment provided by engineer. */ "Message draft" = "Bozza dei messaggi"; +/* item status text */ +"Message forwarded" = "Messaggio inoltrato"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Il messaggio può essere consegnato più tardi se il membro diventa attivo."; + /* No comment provided by engineer. */ "Message queue info" = "Info coda messaggi"; @@ -2518,6 +2698,12 @@ /* No comment provided by engineer. */ "Message source remains private." = "La fonte del messaggio resta privata."; +/* No comment provided by engineer. */ +"Message status" = "Stato del messaggio"; + +/* copied message info */ +"Message status: %@" = "Stato del messaggio: %@"; + /* No comment provided by engineer. */ "Message text" = "Testo del messaggio"; @@ -2533,6 +2719,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "I messaggi da %@ verranno mostrati!"; +/* No comment provided by engineer. */ +"Messages received" = "Messaggi ricevuti"; + +/* No comment provided by engineer. */ +"Messages sent" = "Messaggi inviati"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "I messaggi, i file e le chiamate sono protetti da **crittografia end-to-end** con perfect forward secrecy, ripudio e recupero da intrusione."; @@ -2698,6 +2890,9 @@ /* No comment provided by engineer. */ "No device token!" = "Nessun token del dispositivo!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Ancora nessuna connessione diretta, il messaggio viene inoltrato dall'amministratore."; + /* No comment provided by engineer. */ "no e2e encryption" = "nessuna crittografia e2e"; @@ -2710,6 +2905,9 @@ /* No comment provided by engineer. */ "No history" = "Nessuna cronologia"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Nessuna informazione, prova a ricaricare"; + /* No comment provided by engineer. */ "No network connection" = "Nessuna connessione di rete"; @@ -2835,6 +3033,9 @@ /* authentication reason */ "Open migration to another device" = "Apri migrazione ad un altro dispositivo"; +/* No comment provided by engineer. */ +"Open server settings" = "Apri impostazioni server"; + /* No comment provided by engineer. */ "Open Settings" = "Apri le impostazioni"; @@ -2859,9 +3060,15 @@ /* No comment provided by engineer. */ "Or show this code" = "O mostra questo codice"; +/* No comment provided by engineer. */ +"other" = "altro"; + /* No comment provided by engineer. */ "Other" = "Altro"; +/* No comment provided by engineer. */ +"other errors" = "altri errori"; + /* member role */ "owner" = "proprietario"; @@ -2904,6 +3111,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; +/* No comment provided by engineer. */ +"Pending" = "In attesa"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Le persone possono connettersi a te solo tramite i link che condividi."; @@ -2925,6 +3135,9 @@ /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Chiedi al tuo contatto di attivare l'invio dei messaggi vocali."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Controlla che mobile e desktop siano collegati alla stessa rete locale e che il firewall del desktop consenta la connessione.\nSi prega di condividere qualsiasi altro problema con gli sviluppatori."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Controlla di aver usato il link giusto o chiedi al tuo contatto di inviartene un altro."; @@ -2982,6 +3195,9 @@ /* No comment provided by engineer. */ "Preview" = "Anteprima"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Server precedentemente connessi"; + /* No comment provided by engineer. */ "Privacy & security" = "Privacy e sicurezza"; @@ -3021,6 +3237,9 @@ /* No comment provided by engineer. */ "Profile password" = "Password del profilo"; +/* No comment provided by engineer. */ +"Profile theme" = "Tema del profilo"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "L'aggiornamento del profilo verrà inviato ai tuoi contatti."; @@ -3069,6 +3288,12 @@ /* No comment provided by engineer. */ "Protocol timeout per KB" = "Scadenza del protocollo per KB"; +/* No comment provided by engineer. */ +"Proxied" = "Via proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Server via proxy"; + /* No comment provided by engineer. */ "Push notifications" = "Notifiche push"; @@ -3111,6 +3336,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Le ricevute sono disattivate"; +/* No comment provided by engineer. */ +"Receive errors" = "Errori di ricezione"; + /* No comment provided by engineer. */ "received answer…" = "risposta ricevuta…"; @@ -3129,6 +3357,15 @@ /* message info title */ "Received message" = "Messaggio ricevuto"; +/* No comment provided by engineer. */ +"Received messages" = "Messaggi ricevuti"; + +/* No comment provided by engineer. */ +"Received reply" = "Risposta ricevuta"; + +/* No comment provided by engineer. */ +"Received total" = "Totale ricevuto"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'indirizzo di ricezione verrà cambiato in un server diverso. La modifica dell'indirizzo verrà completata dopo che il mittente sarà in linea."; @@ -3147,9 +3384,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "I destinatari vedono gli aggiornamenti mentre li digiti."; +/* No comment provided by engineer. */ +"Reconnect" = "Riconnetti"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Riconnetti tutti i server connessi per imporre il recapito dei messaggi. Utilizza traffico aggiuntivo."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Riconnetti tutti i server"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Riconnettere tutti i server?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Riconnetti il server per forzare la consegna dei messaggi. Usa traffico aggiuntivo."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Riconnettere il server?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Riconnettere i server?"; @@ -3183,6 +3435,9 @@ /* No comment provided by engineer. */ "Remove" = "Rimuovi"; +/* No comment provided by engineer. */ +"Remove image" = "Rimuovi immagine"; + /* No comment provided by engineer. */ "Remove member" = "Rimuovi membro"; @@ -3240,12 +3495,24 @@ /* No comment provided by engineer. */ "Reset" = "Ripristina"; +/* No comment provided by engineer. */ +"Reset all statistics" = "Azzera tutte le statistiche"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Azzerare tutte le statistiche?"; + /* No comment provided by engineer. */ "Reset colors" = "Ripristina i colori"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Ripristina al tema dell'app"; + /* No comment provided by engineer. */ "Reset to defaults" = "Ripristina i predefiniti"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Ripristina al tema dell'utente"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Riavvia l'app per creare un nuovo profilo di chat"; @@ -3360,6 +3627,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "I server WebRTC ICE salvati verranno rimossi"; +/* No comment provided by engineer. */ +"Scale" = "Scala"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Scansiona / Incolla link"; + /* No comment provided by engineer. */ "Scan code" = "Scansiona codice"; @@ -3387,6 +3660,9 @@ /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secondario"; + /* time unit */ "seconds" = "secondi"; @@ -3396,6 +3672,9 @@ /* server test step */ "Secure queue" = "Coda sicura"; +/* No comment provided by engineer. */ +"Secured" = "Protetto"; + /* No comment provided by engineer. */ "Security assessment" = "Valutazione della sicurezza"; @@ -3408,6 +3687,9 @@ /* No comment provided by engineer. */ "Select" = "Seleziona"; +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Le preferenze della chat selezionata vietano questo messaggio."; + /* No comment provided by engineer. */ "Self-destruct" = "Autodistruzione"; @@ -3441,6 +3723,9 @@ /* No comment provided by engineer. */ "Send disappearing message" = "Invia messaggio a tempo"; +/* No comment provided by engineer. */ +"Send errors" = "Errori di invio"; + /* No comment provided by engineer. */ "Send link previews" = "Invia anteprime dei link"; @@ -3507,15 +3792,33 @@ /* copied message info */ "Sent at: %@" = "Inviato il: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Inviato direttamente"; + /* notification */ "Sent file event" = "Evento file inviato"; /* message info title */ "Sent message" = "Messaggio inviato"; +/* No comment provided by engineer. */ +"Sent messages" = "Messaggi inviati"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "I messaggi inviati verranno eliminati dopo il tempo impostato."; +/* No comment provided by engineer. */ +"Sent reply" = "Risposta inviata"; + +/* No comment provided by engineer. */ +"Sent total" = "Totale inviato"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Inviato via proxy"; + +/* No comment provided by engineer. */ +"Server address" = "Indirizzo server"; + /* srv error text. */ "Server address is incompatible with network settings." = "L'indirizzo del server non è compatibile con le impostazioni di rete."; @@ -3531,12 +3834,21 @@ /* No comment provided by engineer. */ "Server test failed!" = "Test del server fallito!"; +/* No comment provided by engineer. */ +"Server type" = "Tipo server"; + /* srv error text */ "Server version is incompatible with network settings." = "La versione del server non è compatibile con le impostazioni di rete."; /* No comment provided by engineer. */ "Servers" = "Server"; +/* No comment provided by engineer. */ +"Servers info" = "Info dei server"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Le statistiche dei server verranno azzerate - è irreversibile!"; + /* No comment provided by engineer. */ "Session code" = "Codice di sessione"; @@ -3546,6 +3858,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Imposta nome del contatto…"; +/* No comment provided by engineer. */ +"Set default theme" = "Imposta tema predefinito"; + /* No comment provided by engineer. */ "Set group preferences" = "Imposta le preferenze del gruppo"; @@ -3624,6 +3939,9 @@ /* No comment provided by engineer. */ "Show:" = "Mostra:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Indirizzo SimpleX"; @@ -3669,6 +3987,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Modalità incognito semplificata"; +/* No comment provided by engineer. */ +"Size" = "Dimensione"; + /* No comment provided by engineer. */ "Skip" = "Salta"; @@ -3678,6 +3999,9 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Piccoli gruppi (max 20)"; +/* No comment provided by engineer. */ +"SMP server" = "Server SMP"; + /* No comment provided by engineer. */ "SMP servers" = "Server SMP"; @@ -3702,9 +4026,15 @@ /* No comment provided by engineer. */ "Start migration" = "Avvia la migrazione"; +/* No comment provided by engineer. */ +"Starting from %@." = "Inizio da %@."; + /* No comment provided by engineer. */ "starting…" = "avvio…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistiche"; + /* No comment provided by engineer. */ "Stop" = "Ferma"; @@ -3747,6 +4077,15 @@ /* No comment provided by engineer. */ "Submit" = "Invia"; +/* No comment provided by engineer. */ +"Subscribed" = "Iscritto"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Errori di iscrizione"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Iscrizioni ignorate"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Supporta SimpleX Chat"; @@ -3795,6 +4134,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Errore del file temporaneo"; + /* server test failure */ "Test failed at step %@." = "Test fallito al passo %@."; @@ -3876,6 +4218,9 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Il testo che hai incollato non è un link SimpleX."; +/* No comment provided by engineer. */ +"Themes" = "Temi"; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Queste impostazioni sono per il tuo profilo attuale **%@**."; @@ -3918,9 +4263,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Questo è il tuo indirizzo SimpleX!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Questa impostazione si applica ai messaggi del profilo di chat attuale **%@**."; +/* No comment provided by engineer. */ +"Title" = "Titoli"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Per porre domande e ricevere aggiornamenti:"; @@ -3960,9 +4311,15 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Attiva/disattiva l'incognito quando ti colleghi."; +/* No comment provided by engineer. */ +"Total" = "Totale"; + /* No comment provided by engineer. */ "Transport isolation" = "Isolamento del trasporto"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sessioni di trasporto"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %@)."; @@ -4098,12 +4455,21 @@ /* No comment provided by engineer. */ "Upgrade and open chat" = "Aggiorna e apri chat"; +/* No comment provided by engineer. */ +"Upload errors" = "Errori di invio"; + /* No comment provided by engineer. */ "Upload failed" = "Invio fallito"; /* server test step */ "Upload file" = "Invia file"; +/* No comment provided by engineer. */ +"Uploaded" = "Inviato"; + +/* No comment provided by engineer. */ +"Uploaded files" = "File inviati"; + /* No comment provided by engineer. */ "Uploading archive" = "Invio dell'archivio"; @@ -4149,6 +4515,9 @@ /* No comment provided by engineer. */ "User profile" = "Profilo utente"; +/* No comment provided by engineer. */ +"User selection" = "Selezione utente"; + /* No comment provided by engineer. */ "Using .onion hosts requires compatible VPN provider." = "L'uso di host .onion richiede un fornitore di VPN compatibile."; @@ -4257,6 +4626,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "In attesa del video"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Tinta dello sfondo"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Retro dello sfondo"; + /* No comment provided by engineer. */ "wants to connect to you!" = "vuole connettersi con te!"; @@ -4329,9 +4704,15 @@ /* snd error text */ "Wrong key or unknown connection - most likely this connection is deleted." = "Chiave sbagliata o connessione sconosciuta - molto probabilmente questa connessione è stata eliminata."; +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Chiave sbagliata o indirizzo sconosciuto per frammento del file - probabilmente il file è stato eliminato."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Password sbagliata!"; +/* No comment provided by engineer. */ +"XFTP server" = "Server XFTP"; + /* No comment provided by engineer. */ "XFTP servers" = "Server XFTP"; @@ -4389,6 +4770,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Sei stato/a invitato/a al gruppo"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Non sei connesso/a a questi server. L'instradamento privato è usato per consegnare loro i messaggi."; + /* No comment provided by engineer. */ "you are observer" = "sei un osservatore"; @@ -4417,7 +4801,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Puoi renderlo visibile ai tuoi contatti SimpleX nelle impostazioni."; /* notification body */ -"You can now send messages to %@" = "Ora puoi inviare messaggi a %@"; +"You can now chat with %@" = "Ora puoi inviare messaggi a %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Puoi impostare l'anteprima della notifica nella schermata di blocco tramite le impostazioni."; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index d66f5bee3d..0f5ccc2b8c 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -148,9 +148,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ は検証されています"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ サーバー"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ アップロード済"; @@ -3451,7 +3448,7 @@ "You can hide or mute a user profile - swipe it to the right." = "ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。"; /* notification body */ -"You can now send messages to %@" = "%@ にメッセージを送信できるようになりました"; +"You can now chat with %@" = "%@ にメッセージを送信できるようになりました"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "設定からロック画面の通知プレビューを設定できます。"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index c361c2c64e..9cd3c078b3 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ is geverifieerd"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ servers"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ geüpload"; @@ -650,7 +647,7 @@ /* rcv group event chat item */ "blocked %@" = "geblokkeerd %@"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "geblokkeerd door beheerder"; /* No comment provided by engineer. */ @@ -4417,7 +4414,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Je kunt het via Instellingen zichtbaar maken voor je SimpleX contacten."; /* notification body */ -"You can now send messages to %@" = "Je kunt nu berichten sturen naar %@"; +"You can now chat with %@" = "Je kunt nu berichten sturen naar %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "U kunt een voorbeeld van een melding op het vergrendeld scherm instellen via instellingen."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 5860ed1868..6807e4f240 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ jest zweryfikowany"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ serwery"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ wgrane"; @@ -650,7 +647,7 @@ /* rcv group event chat item */ "blocked %@" = "zablokowany %@"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "zablokowany przez admina"; /* No comment provided by engineer. */ @@ -4417,7 +4414,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Możesz ustawić go jako widoczny dla swoich kontaktów SimpleX w Ustawieniach."; /* notification body */ -"You can now send messages to %@" = "Możesz teraz wysyłać wiadomości do %@"; +"You can now chat with %@" = "Możesz teraz wysyłać wiadomości do %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Podgląd powiadomień na ekranie blokady można ustawić w ustawieniach."; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 450a6147b7..255ce8a6b5 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ подтверждён"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ серверы"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ загружено"; @@ -650,7 +647,7 @@ /* rcv group event chat item */ "blocked %@" = "%@ заблокирован"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "заблокировано администратором"; /* No comment provided by engineer. */ @@ -4408,7 +4405,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Вы можете сделать его видимым для ваших контактов в SimpleX через Настройки."; /* notification body */ -"You can now send messages to %@" = "Вы теперь можете отправлять сообщения %@"; +"You can now chat with %@" = "Вы теперь можете общаться с %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Вы можете установить просмотр уведомлений на экране блокировки в настройках."; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 5ce2e40e14..22b707f886 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -109,9 +109,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ ได้รับการตรวจสอบแล้ว"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ เซิร์ฟเวอร์"; - /* notification title */ "%@ wants to connect!" = "%@ อยากเชื่อมต่อ!"; @@ -3304,7 +3301,7 @@ "You can hide or mute a user profile - swipe it to the right." = "คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา"; /* notification body */ -"You can now send messages to %@" = "ตอนนี้คุณสามารถส่งข้อความถึง %@"; +"You can now chat with %@" = "ตอนนี้คุณสามารถส่งข้อความถึง %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 65496a82bf..644fa5fdbe 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ onaylandı"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ sunucuları"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ yüklendi"; @@ -650,7 +647,7 @@ /* rcv group event chat item */ "blocked %@" = "engellendi %@"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "yönetici tarafından engellendi"; /* No comment provided by engineer. */ @@ -4417,7 +4414,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Ayarlardan SimpleX kişilerinize görünür yapabilirsiniz."; /* notification body */ -"You can now send messages to %@" = "Artık %@ adresine mesaj gönderebilirsin"; +"You can now chat with %@" = "Artık %@ adresine mesaj gönderebilirsin"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Kilit ekranı bildirim önizlemesini ayarlar üzerinden ayarlayabilirsiniz."; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 69aac64ab5..d41e32efc9 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ перевірено"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ сервери"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ завантажено"; @@ -650,7 +647,7 @@ /* rcv group event chat item */ "blocked %@" = "заблоковано %@"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "заблоковано адміністратором"; /* No comment provided by engineer. */ @@ -4417,7 +4414,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Ви можете зробити його видимим для ваших контактів у SimpleX за допомогою налаштувань."; /* notification body */ -"You can now send messages to %@" = "Тепер ви можете надсилати повідомлення на адресу %@"; +"You can now chat with %@" = "Тепер ви можете надсилати повідомлення на адресу %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань."; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 94588646c8..9d990ab609 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -133,9 +133,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ 已认证"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ 服务器"; - /* notification title */ "%@ wants to connect!" = "%@ 要连接!"; @@ -602,7 +599,7 @@ /* rcv group event chat item */ "blocked %@" = "已封禁 %@"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "由管理员封禁"; /* No comment provided by engineer. */ @@ -4114,7 +4111,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "你可以通过设置让它对你的 SimpleX 联系人可见。"; /* notification body */ -"You can now send messages to %@" = "您现在可以给 %@ 发送消息"; +"You can now chat with %@" = "您现在可以给 %@ 发送消息"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "您可以通过设置来设置锁屏通知预览。"; diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt index b103367fe8..2ff2a3e021 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt @@ -28,3 +28,5 @@ actual fun Modifier.desktopOnExternalDrag( actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this actual fun Modifier.desktopPointerHoverIconHand(): Modifier = this + +actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier = Modifier diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt index 4dbc9bd9a9..e5dda23f0f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt @@ -126,16 +126,11 @@ actual object AudioPlayer: AudioPlayerInterface { .build() ) } - // Filepath: String, onProgressUpdate - private val currentlyPlaying: MutableState Unit>?> = mutableStateOf(null) + override val currentlyPlaying: MutableState = mutableStateOf(null) private var progressJob: Job? = null - enum class TrackState { - PLAYING, PAUSED, REPLACED - } - // Returns real duration of the track - private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { + private fun start(fileSource: CryptoFile, smallView: Boolean, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) if (!File(absoluteFilePath).exists()) { Log.e(TAG, "No such file: ${fileSource.filePath}") @@ -145,7 +140,7 @@ actual object AudioPlayer: AudioPlayerInterface { VideoPlayerHolder.stopAll() RecorderInterface.stopRecording?.invoke() val current = currentlyPlaying.value - if (current == null || current.first != fileSource.filePath) { + if (current == null || current.fileSource.filePath != fileSource.filePath || smallView != current.smallView) { stopListener() player.reset() runCatching { @@ -168,7 +163,7 @@ actual object AudioPlayer: AudioPlayerInterface { } if (seek != null) player.seekTo(seek) player.start() - currentlyPlaying.value = fileSource.filePath to onProgressUpdate + currentlyPlaying.value = CurrentlyPlayingState(fileSource, onProgressUpdate, smallView) progressJob = CoroutineScope(Dispatchers.Default).launch { onProgressUpdate(player.currentPosition, TrackState.PLAYING) while(isActive && player.isPlaying) { @@ -192,6 +187,10 @@ actual object AudioPlayer: AudioPlayerInterface { } keepScreenOn(false) onProgressUpdate(null, TrackState.PAUSED) + + if (smallView && isActive) { + stopListener() + } } return player.duration } @@ -215,7 +214,7 @@ actual object AudioPlayer: AudioPlayerInterface { // FileName or filePath are ok override fun stop(fileName: String?) { - if (fileName != null && currentlyPlaying.value?.first?.endsWith(fileName) == true) { + if (fileName != null && currentlyPlaying.value?.fileSource?.filePath?.endsWith(fileName) == true) { stop() } } @@ -223,7 +222,7 @@ actual object AudioPlayer: AudioPlayerInterface { private fun stopListener() { val afterCoroutineCancel: CompletionHandler = { // Notify prev audio listener about stop - currentlyPlaying.value?.second?.invoke(null, TrackState.REPLACED) + currentlyPlaying.value?.onProgressUpdate?.invoke(null, TrackState.REPLACED) currentlyPlaying.value = null } /** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be: @@ -244,11 +243,12 @@ actual object AudioPlayer: AudioPlayerInterface { progress: MutableState, duration: MutableState, resetOnEnd: Boolean, + smallView: Boolean, ) { if (progress.value == duration.value) { progress.value = 0 } - val realDuration = start(fileSource, progress.value) { pro, state -> + val realDuration = start(fileSource, smallView, progress.value) { pro, state -> if (pro != null) { progress.value = pro } @@ -274,7 +274,7 @@ actual object AudioPlayer: AudioPlayerInterface { override fun seekTo(ms: Int, pro: MutableState, filePath: String?) { pro.value = ms - if (currentlyPlaying.value?.first == filePath) { + if (currentlyPlaying.value?.fileSource?.filePath == filePath) { player.seekTo(ms) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 5fd813f09f..025f722734 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1085,9 +1085,10 @@ data class Contact( override val id get() = "@$contactId" override val apiId get() = contactId override val ready get() = activeConn?.connStatus == ConnStatus.Ready + val sndReady get() = ready || activeConn?.connStatus == ConnStatus.SndReady val active get() = contactStatus == ContactStatus.Active override val sendMsgEnabled get() = ( - ready + sndReady && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false) && !(activeConn?.connDisabled ?: true) @@ -1753,7 +1754,7 @@ enum class ConnStatus { Joined -> false Requested -> true Accepted -> true - SndReady -> false + SndReady -> null Ready -> null Deleted -> null } @@ -2765,6 +2766,24 @@ data class CIFile( is CIFileStatus.Invalid -> null } + val showStatusIconInSmallView: Boolean = when (fileStatus) { + is CIFileStatus.SndStored -> fileProtocol != FileProtocol.LOCAL + is CIFileStatus.SndTransfer -> true + is CIFileStatus.SndComplete -> false + is CIFileStatus.SndCancelled -> true + is CIFileStatus.SndError -> true + is CIFileStatus.SndWarning -> true + is CIFileStatus.RcvInvitation -> false + is CIFileStatus.RcvAccepted -> true + is CIFileStatus.RcvTransfer -> true + is CIFileStatus.RcvAborted -> true + is CIFileStatus.RcvCancelled -> true + is CIFileStatus.RcvComplete -> false + is CIFileStatus.RcvError -> true + is CIFileStatus.RcvWarning -> true + is CIFileStatus.Invalid -> true + } + /** * DO NOT CALL this function in compose scope, [LaunchedEffect], [DisposableEffect] and so on. Only with [withBGApi] or [runBlocking]. * Otherwise, it will be canceled when moving to another screen/item/view, etc diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index d259aa65c8..cac6a7082d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -116,6 +116,7 @@ class AppPreferences { val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) + val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) @@ -328,6 +329,7 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" + private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" @@ -423,6 +425,16 @@ object ChatController { fun hasChatCtrl() = ctrl != -1L && ctrl != null + suspend fun getAgentSubsTotal(rh: Long?): Pair? { + val userId = currentUserId("getAgentSubsTotal") + + val r = sendCmd(rh, CC.GetAgentSubsTotal(userId), log = false) + + if (r is CR.AgentSubsTotal) return r.subsTotal to r.hasSession + Log.e(TAG, "getAgentSubsTotal bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun getAgentServersSummary(rh: Long?): PresentedServersSummary? { val userId = currentUserId("getAgentServersSummary") @@ -1837,23 +1849,27 @@ object ChatController { r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.SMP && r.chatError.agentError.smpErr is SMPErrorType.PROXY -> - proxyErrorAlert(r.chatError.agentError.smpErr.proxyErr) + smpProxyErrorAlert(r.chatError.agentError.smpErr.proxyErr, r.chatError.agentError.serverAddress) r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.PROXY && r.chatError.agentError.proxyErr is ProxyClientError.ProxyProtocolError && r.chatError.agentError.proxyErr.protocolErr is SMPErrorType.PROXY -> - proxyErrorAlert(r.chatError.agentError.proxyErr.protocolErr.proxyErr) + proxyDestinationErrorAlert( + r.chatError.agentError.proxyErr.protocolErr.proxyErr, + r.chatError.agentError.proxyServer, + r.chatError.agentError.relayServer + ) else -> false } } - private fun proxyErrorAlert(pe: ProxyError): Boolean { + private fun smpProxyErrorAlert(pe: ProxyError, srvAddr: String): Boolean { return when { pe is ProxyError.BROKER && pe.brokerErr is BrokerErrorType.TIMEOUT -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.private_routing_error), - generalGetString(MR.strings.please_try_later) + String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(srvAddr)) ) true } @@ -1861,14 +1877,7 @@ object ChatController { && pe.brokerErr is BrokerErrorType.NETWORK -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.private_routing_error), - generalGetString(MR.strings.please_try_later) - ) - true - } - pe is ProxyError.NO_SESSION -> { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.private_routing_error), - generalGetString(MR.strings.please_try_later) + String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(srvAddr)) ) true } @@ -1876,7 +1885,7 @@ object ChatController { && pe.brokerErr is BrokerErrorType.HOST -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.private_routing_error), - generalGetString(MR.strings.srv_error_host) + String.format(generalGetString(MR.strings.smp_proxy_error_broker_host), serverHostname(srvAddr)) ) true } @@ -1885,7 +1894,53 @@ object ChatController { && pe.brokerErr.transportErr is SMPTransportError.Version -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.private_routing_error), - generalGetString(MR.strings.srv_error_version) + String.format(generalGetString(MR.strings.smp_proxy_error_broker_version), serverHostname(srvAddr)) + ) + true + } + else -> false + } + } + + private fun proxyDestinationErrorAlert(pe: ProxyError, proxyServer: String, relayServer: String): Boolean { + return when { + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.TIMEOUT -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.NETWORK -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) + ) + true + } + pe is ProxyError.NO_SESSION -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.HOST -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_broker_host), serverHostname(relayServer), serverHostname(proxyServer)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.TRANSPORT + && pe.brokerErr.transportErr is SMPTransportError.Version -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_broker_version), serverHostname(relayServer), serverHostname(proxyServer)) ) true } @@ -1935,6 +1990,17 @@ object ChatController { } } } + is CR.ContactSndReady -> { + if (active(r.user) && r.contact.directOrUsed) { + chatModel.updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + chatModel.removeChat(rhId, conn.id) + } + } + chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected()) + } is CR.ReceivedContactRequest -> { val contactRequest = r.contactRequest val cInfo = ChatInfo.ContactRequest(contactRequest) @@ -2761,6 +2827,7 @@ sealed class CC { // misc class ShowVersion(): CC() class ResetAgentServersStats(): CC() + class GetAgentSubsTotal(val userId: Long): CC() class GetAgentServersSummary(val userId: Long): CC() val cmdString: String get() = when (this) { @@ -2921,6 +2988,7 @@ sealed class CC { is ApiStandaloneFileInfo -> "/_download info $url" is ShowVersion -> "/version" is ResetAgentServersStats -> "/reset servers stats" + is GetAgentSubsTotal -> "/get subs total $userId" is GetAgentServersSummary -> "/get servers summary $userId" } @@ -3054,6 +3122,7 @@ sealed class CC { is ApiStandaloneFileInfo -> "apiStandaloneFileInfo" is ShowVersion -> "showVersion" is ResetAgentServersStats -> "resetAgentServersStats" + is GetAgentSubsTotal -> "getAgentSubsTotal" is GetAgentServersSummary -> "getAgentServersSummary" } @@ -3306,18 +3375,18 @@ data class ParsedServerAddress ( data class NetCfg( val socksProxy: String?, val socksMode: SocksMode = SocksMode.Always, - val hostMode: HostMode, - val requiredHostMode: Boolean, - val sessionMode: TransportSessionMode, - val smpProxyMode: SMPProxyMode, - val smpProxyFallback: SMPProxyFallback, + val hostMode: HostMode = HostMode.OnionViaSocks, + val requiredHostMode: Boolean = false, + val sessionMode: TransportSessionMode = TransportSessionMode.User, + val smpProxyMode: SMPProxyMode = SMPProxyMode.Unknown, + val smpProxyFallback: SMPProxyFallback = SMPProxyFallback.AllowProtected, val tcpConnectTimeout: Long, // microseconds val tcpTimeout: Long, // microseconds val tcpTimeoutPerKb: Long, // microseconds val rcvConcurrency: Int, // pool size - val tcpKeepAlive: KeepAliveOpts?, + val tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults, val smpPingInterval: Long, // microseconds - val smpPingCount: Int, + val smpPingCount: Int = 3, val logTLSErrors: Boolean = false, ) { val useSocksProxy: Boolean get() = socksProxy != null @@ -3336,35 +3405,21 @@ data class NetCfg( val defaults: NetCfg = NetCfg( socksProxy = null, - hostMode = HostMode.OnionViaSocks, - requiredHostMode = false, - sessionMode = TransportSessionMode.User, - smpProxyMode = SMPProxyMode.Never, - smpProxyFallback = SMPProxyFallback.Allow, tcpConnectTimeout = 25_000_000, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 10_000, rcvConcurrency = 12, - tcpKeepAlive = KeepAliveOpts.defaults, - smpPingInterval = 1200_000_000, - smpPingCount = 3 + smpPingInterval = 1200_000_000 ) val proxyDefaults: NetCfg = NetCfg( socksProxy = ":9050", - hostMode = HostMode.OnionViaSocks, - requiredHostMode = false, - sessionMode = TransportSessionMode.User, - smpProxyMode = SMPProxyMode.Never, - smpProxyFallback = SMPProxyFallback.Allow, tcpConnectTimeout = 35_000_000, tcpTimeout = 20_000_000, tcpTimeoutPerKb = 15_000, rcvConcurrency = 8, - tcpKeepAlive = KeepAliveOpts.defaults, - smpPingInterval = 1200_000_000, - smpPingCount = 3 + smpPingInterval = 1200_000_000 ) } @@ -3598,6 +3653,9 @@ data class ServerSessions( ssConnecting = 0 ) } + + val hasSess: Boolean + get() = ssConnected > 0 } @Serializable @@ -4586,6 +4644,7 @@ sealed class CR { @Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted(val user: User): CR() @Serializable @SerialName("contactConnected") class ContactConnected(val user: UserRef, val contact: Contact, val userCustomProfile: Profile? = null): CR() @Serializable @SerialName("contactConnecting") class ContactConnecting(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("contactSndReady") class ContactSndReady(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val user: UserRef, val contactRequest: UserContactRequest): CR() @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef): CR() @@ -4702,6 +4761,7 @@ sealed class CR { @Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR() @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List): CR() @Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR() + @Serializable @SerialName("agentSubsTotal") class AgentSubsTotal(val user: UserRef, val subsTotal: SMPServerSubs, val hasSession: Boolean): CR() @Serializable @SerialName("agentServersSummary") class AgentServersSummary(val user: UserRef, val serversSummary: PresentedServersSummary): CR() // general @Serializable class Response(val type: String, val json: String): CR() @@ -4761,6 +4821,7 @@ sealed class CR { is UserContactLinkDeleted -> "userContactLinkDeleted" is ContactConnected -> "contactConnected" is ContactConnecting -> "contactConnecting" + is ContactSndReady -> "contactSndReady" is ReceivedContactRequest -> "receivedContactRequest" is AcceptingContactRequest -> "acceptingContactRequest" is ContactRequestRejected -> "contactRequestRejected" @@ -4862,6 +4923,7 @@ sealed class CR { is ContactPQAllowed -> "contactPQAllowed" is ContactPQEnabled -> "contactPQEnabled" is VersionInfo -> "versionInfo" + is AgentSubsTotal -> "agentSubsTotal" is AgentServersSummary -> "agentServersSummary" is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" @@ -4926,6 +4988,7 @@ sealed class CR { is UserContactLinkDeleted -> withUser(user, noDetails()) is ContactConnected -> withUser(user, json.encodeToString(contact)) is ContactConnecting -> withUser(user, json.encodeToString(contact)) + is ContactSndReady -> withUser(user, json.encodeToString(contact)) is ReceivedContactRequest -> withUser(user, json.encodeToString(contactRequest)) is AcceptingContactRequest -> withUser(user, json.encodeToString(contact)) is ContactRequestRejected -> withUser(user, noDetails()) @@ -5041,6 +5104,7 @@ sealed class CR { is RemoteCtrlStopped -> "rcsState: $rcsState\nrcsStopReason: $rcStopReason" is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption") is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled") + is AgentSubsTotal -> withUser(user, "subsTotal: ${subsTotal}\nhasSession: $hasSession") is AgentServersSummary -> withUser(user, json.encodeToString(serversSummary)) is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" + "chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" + @@ -5583,7 +5647,7 @@ sealed class AgentErrorType { } @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() - @Serializable @SerialName("SMP") class SMP(val smpErr: SMPErrorType): AgentErrorType() + @Serializable @SerialName("SMP") class SMP(val serverAddress: String, val smpErr: SMPErrorType): AgentErrorType() // @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType() @Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType() @Serializable @SerialName("PROXY") class PROXY(val proxyServer: String, val relayServer: String, val proxyErr: ProxyClientError): AgentErrorType() @@ -5936,6 +6000,7 @@ data class AppSettings( var privacyShowChatPreviews: Boolean? = null, var privacySaveLastDraft: Boolean? = null, var privacyProtectScreen: Boolean? = null, + var privacyMediaBlurRadius: Int? = null, var notificationMode: AppSettingsNotificationMode? = null, var notificationPreviewMode: AppSettingsNotificationPreviewMode? = null, var webrtcPolicyRelay: Boolean? = null, @@ -5965,6 +6030,7 @@ data class AppSettings( if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews } if (privacySaveLastDraft != def.privacySaveLastDraft) { empty.privacySaveLastDraft = privacySaveLastDraft } if (privacyProtectScreen != def.privacyProtectScreen) { empty.privacyProtectScreen = privacyProtectScreen } + if (privacyMediaBlurRadius != def.privacyMediaBlurRadius) { empty.privacyMediaBlurRadius = privacyMediaBlurRadius } if (notificationMode != def.notificationMode) { empty.notificationMode = notificationMode } if (notificationPreviewMode != def.notificationPreviewMode) { empty.notificationPreviewMode = notificationPreviewMode } if (webrtcPolicyRelay != def.webrtcPolicyRelay) { empty.webrtcPolicyRelay = webrtcPolicyRelay } @@ -6002,6 +6068,7 @@ data class AppSettings( privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) } privacySaveLastDraft?.let { def.privacySaveLastDraft.set(it) } privacyProtectScreen?.let { def.privacyProtectScreen.set(it) } + privacyMediaBlurRadius?.let { def.privacyMediaBlurRadius.set(it) } notificationMode?.let { def.notificationsMode.set(it.toNotificationsMode()) } notificationPreviewMode?.let { def.notificationPreviewMode.set(it.toNotificationPreviewMode().name) } webrtcPolicyRelay?.let { def.webrtcPolicyRelay.set(it) } @@ -6032,6 +6099,7 @@ data class AppSettings( privacyShowChatPreviews = true, privacySaveLastDraft = true, privacyProtectScreen = false, + privacyMediaBlurRadius = 0, notificationMode = AppSettingsNotificationMode.INSTANT, notificationPreviewMode = AppSettingsNotificationPreviewMode.MESSAGE, webrtcPolicyRelay = true, @@ -6063,6 +6131,7 @@ data class AppSettings( privacyShowChatPreviews = def.privacyShowChatPreviews.get(), privacySaveLastDraft = def.privacySaveLastDraft.get(), privacyProtectScreen = def.privacyProtectScreen.get(), + privacyMediaBlurRadius = def.privacyMediaBlurRadius.get(), notificationMode = AppSettingsNotificationMode.from(def.notificationsMode.get()), notificationPreviewMode = AppSettingsNotificationPreviewMode.from(NotificationPreviewMode.valueOf(def.notificationPreviewMode.get()!!)), webrtcPolicyRelay = def.webrtcPolicyRelay.get(), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt index 4a10027746..6683ea7d33 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt @@ -1,8 +1,17 @@ package chat.simplex.common.platform -import androidx.compose.runtime.Composable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.blur import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.views.helpers.KeyChangeEffect +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.filter import java.io.File expect fun Modifier.navigationBarsWithImePadding(): Modifier @@ -25,3 +34,68 @@ expect fun Modifier.desktopOnExternalDrag( expect fun Modifier.onRightClick(action: () -> Unit): Modifier expect fun Modifier.desktopPointerHoverIconHand(): Modifier + +expect fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier + +@Composable +fun Modifier.desktopModifyBlurredState(enabled: Boolean, blurred: MutableState, showMenu: State,): Modifier { + val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state } + if (appPlatform.isDesktop) { + KeyChangeEffect(blurRadius.value) { + blurred.value = enabled && blurRadius.value > 0 + } + } + return if (appPlatform.isDesktop && enabled && blurRadius.value > 0 && !showMenu.value) { + var job: Job = remember { Job() } + LaunchedEffect(Unit) { + // The approach here is to allow menu to show up and to not blur the view. When menu is shown and mouse is hovering, + // unhovered action is still received, but we don't need to handle it until menu closes. When it closes, it takes one frame to catch a + // hover action again and if: + // 1. mouse is still on the view, the hover action will cancel this coroutine and the view will stay unblurred + // 2. mouse is not on the view, the view will become blurred after 100 ms + job = launch { + delay(100) + blurred.value = true + } + } + this then Modifier.desktopOnHovered { hovered -> + job.cancel() + blurred.value = !hovered && !showMenu.value + } + } else { + this + } +} + +@Composable +fun Modifier.privacyBlur( + enabled: Boolean, + blurred: MutableState = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }, + scrollState: State, + onLongClick: () -> Unit = {} +): Modifier { + val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state } + return if (enabled && blurred.value) { + this then Modifier.blur( + radiusX = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp, + radiusY = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp, + edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(0.dp)) + ) + .combinedClickable( + onLongClick = onLongClick, + onClick = { + blurred.value = false + } + ) + } else if (enabled && blurRadius.value > 0 && appPlatform.isAndroid) { + LaunchedEffect(Unit) { + snapshotFlow { scrollState.value } + .filter { it } + .filter { !blurred.value } + .collect { blurred.value = true } + } + this + } else { + this + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt index 1e902b5d88..fd1824d5b6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt @@ -16,13 +16,25 @@ interface RecorderInterface { expect class RecorderNative(): RecorderInterface +enum class TrackState { + PLAYING, PAUSED, REPLACED +} + +data class CurrentlyPlayingState( + val fileSource: CryptoFile, + val onProgressUpdate: (position: Int?, state: TrackState) -> Unit, + val smallView: Boolean, +) + interface AudioPlayerInterface { + val currentlyPlaying: MutableState fun play( fileSource: CryptoFile, audioPlaying: MutableState, progress: MutableState, duration: MutableState, resetOnEnd: Boolean, + smallView: Boolean, ) fun stop() fun stop(item: ChatItem) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 48ed0570a7..838225afb8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -206,7 +206,7 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)), buttons = { Column { - if (chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) { + if (chatInfo is ChatInfo.Direct && chatInfo.contact.sndReady && chatInfo.contact.active) { // Delete and notify contact SectionItemView({ AlertManager.shared.hideAlert() @@ -330,8 +330,8 @@ fun ChatInfoLayout( SectionDividerSpaced() } - if (contact.ready && contact.active) { - SectionView { + SectionView { + if (contact.ready && contact.active) { if (connectionCode != null) { VerifyCodeButton(contact.verified, verifyClicked) } @@ -340,22 +340,22 @@ fun ChatInfoLayout( if (cStats != null && cStats.ratchetSyncAllowed) { SynchronizeConnectionButton(syncContactConnection) } + // } else if (developerTools) { + // SynchronizeConnectionButtonForce(syncContactConnectionForce) + // } + } - WallpaperButton { - ModalManager.end.showModal { - val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } } - val c = chat.value - if (c != null) { - ChatWallpaperEditorModal(c) - } + WallpaperButton { + ModalManager.end.showModal { + val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } } + val c = chat.value + if (c != null) { + ChatWallpaperEditorModal(c) } } - // } else if (developerTools) { - // SynchronizeConnectionButtonForce(syncContactConnectionForce) - // } } - SectionDividerSpaced() } + SectionDividerSpaced() val conn = contact.activeConn if (conn != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 5578180ff8..610d8d95e9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -132,7 +132,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: ) { if ( chat.chatInfo is ChatInfo.Direct - && !chat.chatInfo.contact.ready + && !chat.chatInfo.contact.sndReady && chat.chatInfo.contact.active && !chat.chatInfo.contact.nextSendGrpInv ) { @@ -1113,6 +1113,12 @@ fun BoxWithConstraintsScope.ChatItemsList( } } FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState) + LaunchedEffect(Unit) { + snapshotFlow { listState.isScrollInProgress } + .collect { + chatViewScrollState.value = it + } + } } @Composable @@ -1326,6 +1332,8 @@ private fun TopEndFloatingButton( } } +val chatViewScrollState = MutableStateFlow(false) + private fun bottomEndFloatingButton( unreadCount: Int, showButtonWithCounter: Boolean, @@ -1416,7 +1424,7 @@ sealed class ProviderMedia { data class Video(val uri: URI, val fileSource: CryptoFile?, val preview: String): ProviderMedia() } -private fun providerForGallery( +fun providerForGallery( listStateIndex: Int, chatItems: List, cItemId: Long, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt index b71d090a4e..b070dce1d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt @@ -53,7 +53,7 @@ fun ComposeVoiceView( IconButton( onClick = { if (!audioPlaying.value) { - AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, false) + AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, resetOnEnd = false, smallView = false) } else { AudioPlayer.pause(audioPlaying, progress) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index f181126b33..59643afdf4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -1,5 +1,6 @@ package chat.simplex.common.views.chat.item +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize @@ -12,10 +13,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* @@ -29,14 +29,17 @@ fun CIFileView( file: CIFile?, edited: Boolean, showMenu: MutableState, + smallView: Boolean = false, receiveFile: (Long) -> Unit ) { val saveFileLauncher = rememberSaveFileLauncher(ciFile = file) - + val sizeMultiplier = 1f + val progressSizeMultiplier = if (smallView) 0.7f else 1f @Composable fun fileIcon( innerIcon: Painter? = null, - color: Color = if (isInDarkTheme()) FileDark else FileLight + color: Color = if (isInDarkTheme()) FileDark else FileLight, + topPadding: Dp = 12.sp.toDp() ) { Box( contentAlignment = Alignment.Center @@ -52,8 +55,9 @@ fun CIFileView( innerIcon, stringResource(MR.strings.icon_descr_file), Modifier - .size(32.dp) - .padding(top = 12.dp), + .padding(top = topPadding * sizeMultiplier) + .height(20.sp.toDp() * sizeMultiplier) + .width(32.sp.toDp() * sizeMultiplier), tint = Color.White ) } @@ -132,39 +136,39 @@ fun CIFileView( fun fileIndicator() { Box( Modifier - .size(42.dp) - .clip(RoundedCornerShape(4.dp)), + .size(42.sp.toDp() * sizeMultiplier) + .clip(RoundedCornerShape(4.sp.toDp() * sizeMultiplier)), contentAlignment = Alignment.Center ) { if (file != null) { when (file.fileStatus) { is CIFileStatus.SndStored -> when (file.fileProtocol) { - FileProtocol.XFTP -> CIFileViewScope.progressIndicator() + FileProtocol.XFTP -> CIFileViewScope.progressIndicator(progressSizeMultiplier) FileProtocol.SMP -> fileIcon() FileProtocol.LOCAL -> fileIcon() } is CIFileStatus.SndTransfer -> when (file.fileProtocol) { - FileProtocol.XFTP -> CIFileViewScope.progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal) - FileProtocol.SMP -> CIFileViewScope.progressIndicator() + FileProtocol.XFTP -> CIFileViewScope.progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal, progressSizeMultiplier) + FileProtocol.SMP -> CIFileViewScope.progressIndicator(progressSizeMultiplier) FileProtocol.LOCAL -> {} } - is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) + is CIFileStatus.SndComplete -> fileIcon(innerIcon = if (!smallView) painterResource(MR.images.ic_check_filled) else null) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled)) is CIFileStatus.RcvInvitation -> if (fileSizeValid(file)) - fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary) + fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary, topPadding = 10.sp.toDp()) else fileIcon(innerIcon = painterResource(MR.images.ic_priority_high), color = WarningOrange) is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(MR.images.ic_more_horiz)) is CIFileStatus.RcvTransfer -> if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) { - CIFileViewScope.progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal) + CIFileViewScope.progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal, progressSizeMultiplier) } else { - CIFileViewScope.progressIndicator() + CIFileViewScope.progressIndicator(progressSizeMultiplier) } is CIFileStatus.RcvAborted -> fileIcon(innerIcon = painterResource(MR.images.ic_sync_problem), color = MaterialTheme.colors.primary) @@ -186,31 +190,33 @@ fun CIFileView( onClick = { fileAction() }, onLongClick = { showMenu.value = true } ) - .padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp), + .padding(if (smallView) PaddingValues() else PaddingValues(top = 4.sp.toDp(), bottom = 6.sp.toDp(), start = 6.sp.toDp(), end = 12.sp.toDp())), //Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp), verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.spacedBy(2.dp) + horizontalArrangement = Arrangement.spacedBy(2.sp.toDp()) ) { fileIndicator() - val metaReserve = if (edited) - " " - else - " " - if (file != null) { - Column { - Text( - file.fileName, - maxLines = 1 - ) - Text( - formatBytes(file.fileSize) + metaReserve, - color = MaterialTheme.colors.secondary, - fontSize = 14.sp, - maxLines = 1 - ) + if (!smallView) { + val metaReserve = if (edited) + " " + else + " " + if (file != null) { + Column { + Text( + file.fileName, + maxLines = 1 + ) + Text( + formatBytes(file.fileSize) + metaReserve, + color = MaterialTheme.colors.secondary, + fontSize = 14.sp, + maxLines = 1 + ) + } + } else { + Text(metaReserve) } - } else { - Text(metaReserve) } } } @@ -243,18 +249,18 @@ fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher = object CIFileViewScope { @Composable - fun progressIndicator() { + fun progressIndicator(sizeMultiplier: Float = 1f) { CircularProgressIndicator( - Modifier.size(32.dp), + Modifier.size(32.sp.toDp() * sizeMultiplier), color = if (isInDarkTheme()) FileDark else FileLight, - strokeWidth = 3.dp + strokeWidth = 3.sp.toDp() * sizeMultiplier ) } @Composable - fun progressCircle(progress: Long, total: Long) { + fun progressCircle(progress: Long, total: Long, sizeMultiplier: Float = 1f) { val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat() - val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } + val strokeWidth = with(LocalDensity.current) { 3.sp.toPx() } val strokeColor = if (isInDarkTheme()) FileDark else FileLight Surface( Modifier.drawRingModifier(angle, strokeColor, strokeWidth), @@ -262,7 +268,7 @@ object CIFileViewScope { shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), contentColor = LocalContentColor.current ) { - Box(Modifier.size(32.dp)) + Box(Modifier.size(32.sp.toDp() * sizeMultiplier)) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 5aa3bfab05..e234a73136 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -1,6 +1,8 @@ package chat.simplex.common.views.chat.item import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -17,8 +19,10 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH +import chat.simplex.common.views.chat.chatViewScrollState import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.runBlocking @@ -29,8 +33,10 @@ fun CIImageView( file: CIFile?, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState, + smallView: Boolean, receiveFile: (Long) -> Unit ) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } @Composable fun progressIndicator() { CircularProgressIndicator( @@ -55,7 +61,7 @@ fun CIImageView( if (file != null) { Box( Modifier - .padding(8.dp) + .padding(if (smallView) 0.dp else 8.dp) .size(20.dp), contentAlignment = Alignment.Center ) { @@ -104,8 +110,9 @@ fun CIImageView( onLongClick = { showMenu.value = true }, onClick = onClick ) - .onRightClick { showMenu.value = true }, - contentScale = ContentScale.FillWidth, + .onRightClick { showMenu.value = true } + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), + contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth, ) } @@ -127,8 +134,9 @@ fun CIImageView( onLongClick = { showMenu.value = true }, onClick = onClick ) - .onRightClick { showMenu.value = true }, - contentScale = ContentScale.FillWidth, + .onRightClick { showMenu.value = true } + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), + contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth, ) } else { Box(Modifier @@ -137,7 +145,8 @@ fun CIImageView( onLongClick = { showMenu.value = true }, onClick = {} ) - .onRightClick { showMenu.value = true }, + .onRightClick { showMenu.value = true } + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), contentAlignment = Alignment.Center ) { imageView(base64ToBitmap(image), onClick = { @@ -173,7 +182,8 @@ fun CIImageView( } Box( - Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), + Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID) + .desktopModifyBlurredState(!smallView, blurred, showMenu), contentAlignment = Alignment.TopEnd ) { val res: MutableState?> = remember { @@ -191,7 +201,7 @@ fun CIImageView( } } else { KeyChangeEffect(file) { - if (res.value == null) { + if (res.value == null || res.value!!.third != getLoadedFilePath(file)) { res.value = imageAndFilePath(file) } } @@ -255,10 +265,20 @@ fun CIImageView( } }) } - loadingIndicator() + // Do not show download icon when the view is blurred + if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) { + loadingIndicator() + } else if (smallView && file?.showStatusIconInSmallView == true) { + Box(Modifier.align(Alignment.Center)) { + loadingIndicator() + } + } } } +private fun showDownloadButton(status: CIFileStatus?): Boolean = + status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted + @Composable expect fun SimpleAndAnimatedImageView( data: ByteArray, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt similarity index 70% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt index e655b73b02..ca93349092 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt @@ -19,7 +19,9 @@ import chat.simplex.res.MR import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.chatViewScrollState import dev.icerock.moko.resources.StringResource import java.io.File import java.net.URI @@ -31,14 +33,18 @@ fun CIVideoView( file: CIFile?, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState, + smallView: Boolean = false, receiveFile: (Long) -> Unit ) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } Box( - Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), + Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID) + .desktopModifyBlurredState(!smallView, blurred, showMenu), contentAlignment = Alignment.TopEnd ) { val preview = remember(image) { base64ToBitmap(image) } val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) } + val sizeMultiplier = if (smallView) 0.38f else 1f if (chatModel.connectedToRemote()) { LaunchedEffect(file) { withLongRunningApi(slow = 600_000) { @@ -63,14 +69,18 @@ fun CIVideoView( val autoPlay = remember { mutableStateOf(false) } val uriDecrypted = remember(filePath) { mutableStateOf(if (file.fileSource?.cryptoArgs == null) uri else file.fileSource.decryptedGet()) } val decrypted = uriDecrypted.value - if (decrypted != null) { - VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen) + if (decrypted != null && smallView) { + SmallVideoView(decrypted, file, preview, duration * 1000L, autoPlay, sizeMultiplier, openFullscreen = openFullscreen) + } else if (decrypted != null) { + VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen) + } else if (smallView) { + SmallVideoViewEncrypted(uriDecrypted, file, preview, autoPlay, showMenu, sizeMultiplier, openFullscreen = openFullscreen) } else { - VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen) + VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen) } } else { Box { - VideoPreviewImageView(preview, onClick = { + VideoPreviewImageView(preview, blurred = blurred, onClick = { if (file != null) { when (file.fileStatus) { CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted -> @@ -96,18 +106,26 @@ fun CIVideoView( } } }, + smallView = smallView, onLongClick = { showMenu.value = true }) - if (file != null) { + if (file != null && !smallView) { DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) } - if (file?.fileStatus is CIFileStatus.RcvInvitation || file?.fileStatus is CIFileStatus.RcvAborted) { - PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } + if (showDownloadButton(file?.fileStatus) && !blurred.value && file != null) { + PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } } } } - fileStatusIcon(file) + // Do not show download icon when the view is blurred + if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) { + fileStatusIcon(file, false) + } else if (smallView && file?.showStatusIconInSmallView == true) { + Box(Modifier.align(Alignment.Center)) { + fileStatusIcon(file, true) + } + } } } @@ -119,16 +137,17 @@ private fun VideoViewEncrypted( defaultDuration: Long, autoPlay: MutableState, showMenu: MutableState, + blurred: MutableState, openFullscreen: () -> Unit, ) { var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) } val onLongClick = { showMenu.value = true } Box { - VideoPreviewImageView(defaultPreview, if (decryptionInProgress) {{}} else openFullscreen, onLongClick) + VideoPreviewImageView(defaultPreview, smallView = false, blurred = blurred, if (decryptionInProgress) {{}} else openFullscreen, onLongClick) if (decryptionInProgress) { - VideoDecryptionProgress(onLongClick = onLongClick) - } else { - PlayButton(false, onLongClick = onLongClick) { + VideoDecryptionProgress(1f, onLongClick = onLongClick) + } else if (!blurred.value) { + PlayButton(false, 1f, onLongClick = onLongClick) { decryptionInProgress = true withBGApi { try { @@ -145,7 +164,82 @@ private fun VideoViewEncrypted( } @Composable -private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, autoPlay: MutableState, showMenu: MutableState, openFullscreen: () -> Unit) { +private fun SmallVideoViewEncrypted( + uriUnencrypted: MutableState, + file: CIFile, + defaultPreview: ImageBitmap, + autoPlay: MutableState, + showMenu: MutableState, + sizeMultiplier: Float, + openFullscreen: () -> Unit, +) { + var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) } + val onLongClick = { showMenu.value = true } + Box { + VideoPreviewImageView(defaultPreview, smallView = true, blurred = remember { mutableStateOf(false) }, onClick = if (decryptionInProgress) {{}} else openFullscreen, onLongClick = onLongClick) + if (decryptionInProgress) { + VideoDecryptionProgress(sizeMultiplier, onLongClick = onLongClick) + } else if (!file.showStatusIconInSmallView) { + PlayButton(false, sizeMultiplier, onLongClick = onLongClick) { + decryptionInProgress = true + withBGApi { + try { + uriUnencrypted.value = file.fileSource?.decryptedGetOrCreate() + autoPlay.value = uriUnencrypted.value != null + } finally { + decryptionInProgress = false + } + } + } + } + } +} + +@Composable +private fun SmallVideoView( + uri: URI, + file: CIFile, + defaultPreview: ImageBitmap, + defaultDuration: Long, + autoPlay: MutableState, + sizeMultiplier: Float, + openFullscreen: () -> Unit +) { + val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, defaultDuration, true) } + val preview by remember { player.preview } + // val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled } + val brokenVideo by rememberSaveable(uri.path) { player.brokenVideo } + Box { + val windowWidth = LocalWindowWidth() + val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } + PlayerView( + player, + width, + onClick = openFullscreen, + onLongClick = {}, + {} + ) + VideoPreviewImageView(preview, smallView = true, blurred = remember { mutableStateOf(false) }, onClick = openFullscreen, onLongClick = {}) + if (!file.showStatusIconInSmallView) { + PlayButton(brokenVideo, sizeMultiplier, onLongClick = {}, onClick = openFullscreen) + } + } + LaunchedEffect(uri) { + if (autoPlay.value) openFullscreen() + } +} + +@Composable +private fun VideoView( + uri: URI, + file: CIFile, + defaultPreview: ImageBitmap, + defaultDuration: Long, + autoPlay: MutableState, + showMenu: MutableState, + blurred: MutableState, + openFullscreen: () -> Unit +) { val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, false, defaultPreview, defaultDuration, true) } val videoPlaying = remember(uri.path) { player.videoPlaying } val progress = remember(uri.path) { player.progress } @@ -186,9 +280,9 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau stop ) if (showPreview.value) { - VideoPreviewImageView(preview, openFullscreen, onLongClick) - if (!autoPlay.value) { - PlayButton(brokenVideo, onLongClick = onLongClick, play) + VideoPreviewImageView(preview, smallView = false, blurred = blurred, openFullscreen, onLongClick) + if (!autoPlay.value && !blurred.value) { + PlayButton(brokenVideo, onLongClick = onLongClick, onClick = play) } } DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/) @@ -199,16 +293,16 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau expect fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) @Composable -private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, onClick: () -> Unit) { +private fun BoxScope.PlayButton(error: Boolean = false, sizeMultiplier: Float = 1f, onLongClick: () -> Unit, onClick: () -> Unit) { Surface( - Modifier.align(Alignment.Center), + Modifier.align(if (sizeMultiplier != 1f) Alignment.TopStart else Alignment.Center), color = Color.Black.copy(alpha = 0.25f), shape = RoundedCornerShape(percent = 50), contentColor = LocalContentColor.current ) { Box( Modifier - .defaultMinSize(minWidth = 40.dp, minHeight = 40.dp) + .defaultMinSize(minWidth = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp(), minHeight = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp()) .combinedClickable(onClick = onClick, onLongClick = onLongClick) .onRightClick { onLongClick.invoke() }, contentAlignment = Alignment.Center @@ -216,6 +310,7 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, Icon( painterResource(MR.images.ic_play_arrow_filled), contentDescription = null, + Modifier.size(if (sizeMultiplier != 1f) 24.sp.toDp() * sizeMultiplier * 1.6f else 24.sp.toDp()), tint = if (error) WarningOrange else Color.White ) } @@ -223,25 +318,25 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, } @Composable -fun BoxScope.VideoDecryptionProgress(onLongClick: () -> Unit) { +fun BoxScope.VideoDecryptionProgress(sizeMultiplier: Float = 1f, onLongClick: () -> Unit) { Surface( - Modifier.align(Alignment.Center), + Modifier.align(if (sizeMultiplier != 1f) Alignment.TopStart else Alignment.Center), color = Color.Black.copy(alpha = 0.25f), shape = RoundedCornerShape(percent = 50), contentColor = LocalContentColor.current ) { Box( Modifier - .defaultMinSize(minWidth = 40.dp, minHeight = 40.dp) + .defaultMinSize(minWidth = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp(), minHeight = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp()) .combinedClickable(onClick = {}, onLongClick = onLongClick) .onRightClick { onLongClick.invoke() }, contentAlignment = Alignment.Center ) { CircularProgressIndicator( Modifier - .size(30.dp), + .size(if (sizeMultiplier != 1f) 30.sp.toDp() * sizeMultiplier else 30.sp.toDp()), color = Color.White, - strokeWidth = 2.5.dp + strokeWidth = 2.5.sp.toDp() * sizeMultiplier ) } } @@ -293,7 +388,13 @@ private fun DurationProgress(file: CIFile, playing: MutableState, durat } @Composable -fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) { +fun VideoPreviewImageView( + preview: ImageBitmap, + smallView: Boolean, + blurred: MutableState, + onClick: () -> Unit, + onLongClick: () -> Unit +) { val windowWidth = LocalWindowWidth() val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } Image( @@ -305,8 +406,9 @@ fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick onLongClick = onLongClick, onClick = onClick ) - .onRightClick(onLongClick), - contentScale = ContentScale.FillWidth, + .onRightClick(onLongClick) + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = onLongClick), + contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth, ) } @@ -366,11 +468,11 @@ private fun progressCircle(progress: Long, total: Long) { } @Composable -private fun fileStatusIcon(file: CIFile?) { +private fun fileStatusIcon(file: CIFile?, smallView: Boolean) { if (file != null) { Box( Modifier - .padding(8.dp) + .padding(if (smallView) 0.dp else 8.dp) .size(20.dp), contentAlignment = Alignment.Center ) { @@ -450,6 +552,9 @@ private fun fileStatusIcon(file: CIFile?) { } } +private fun showDownloadButton(status: CIFileStatus?): Boolean = + status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted + private fun fileSizeValid(file: CIFile?): Boolean { if (file != null) { return file.fileSize <= getMaxFileSize(file.fileProtocol) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index 040dd97474..5ae46ef4e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -24,6 +24,7 @@ import chat.simplex.common.platform.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import kotlinx.coroutines.flow.* +import kotlin.math.* // TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 @@ -37,11 +38,18 @@ fun CIVoiceView( ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, + smallView: Boolean = false, longClick: () -> Unit, receiveFile: (Long) -> Unit, ) { + val sizeMultiplier = if (smallView) voiceMessageSizeBasedOnSquareSize(36f) / 56f else 1f + val padding = when { + smallView -> PaddingValues() + hasText -> PaddingValues(top = 14.sp.toDp() * sizeMultiplier, bottom = 14.sp.toDp() * sizeMultiplier, start = 6.sp.toDp() * sizeMultiplier, end = 6.sp.toDp() * sizeMultiplier) + else -> PaddingValues(top = 4.sp.toDp() * sizeMultiplier, bottom = 6.sp.toDp() * sizeMultiplier, start = 0.dp, end = 0.dp) + } Row( - Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = if (hasText) 6.dp else 0.dp, end = if (hasText) 6.dp else 0.dp), + Modifier.padding(padding), verticalAlignment = Alignment.CenterVertically ) { if (file != null) { @@ -54,7 +62,7 @@ fun CIVoiceView( val play: () -> Unit = { val playIfExists = { if (fileSource.value != null) { - AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, true) + AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, resetOnEnd = true, smallView = smallView) brokenAudio = !audioPlaying.value } } @@ -69,7 +77,7 @@ fun CIVoiceView( val pause = { AudioPlayer.pause(audioPlaying, progress) } - val text = remember { + val text = remember(ci.file?.fileId, ci.file?.fileStatus) { derivedStateOf { val time = when { audioPlaying.value || progress.value != 0 -> progress.value @@ -78,11 +86,18 @@ fun CIVoiceView( durationText(time / 1000) } } - VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, play, pause, longClick, receiveFile) { + VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, sizeMultiplier, play, pause, longClick, receiveFile) { AudioPlayer.seekTo(it, progress, fileSource.value?.filePath) } + if (smallView) { + KeyChangeEffect(chatModel.chatId.value, chatModel.currentUser.value?.userId, chatModel.currentRemoteHost.value) { + AudioPlayer.stop() + } + } + } else if (smallView) { + VoiceMsgIndicator(null, false, sent, hasText, null, null, false, sizeMultiplier, {}, {}, longClick, receiveFile) } else { - VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile) + VoiceMsgIndicator(null, false, sent, hasText, null, null, false, 1f, {}, {}, longClick, receiveFile) val metaReserve = if (edited) " " else @@ -105,6 +120,7 @@ private fun VoiceLayout( hasText: Boolean, timedMessagesTTL: Int?, showViaProxy: Boolean, + sizeMultiplier: Float, play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, @@ -116,15 +132,16 @@ private fun VoiceLayout( var movedManuallyTo by rememberSaveable(file.fileId) { mutableStateOf(-1) } if (audioPlaying.value || progress.value > 0 || movedManuallyTo == progress.value) { val dp4 = with(LocalDensity.current) { 4.dp.toPx() } - val dp10 = with(LocalDensity.current) { 10.dp.toPx() } val primary = MaterialTheme.colors.primary val inactiveTrackColor = MaterialTheme.colors.primary.mixWith( backgroundColor.copy(1f).mixWith(MaterialTheme.colors.background, backgroundColor.alpha), 0.24f) val width = LocalWindowWidth() + // Built-in slider has rounded corners but we need square corners, so drawing a track manually val colors = SliderDefaults.colors( - inactiveTrackColor = inactiveTrackColor + inactiveTrackColor = Color.Transparent, + activeTrackColor = Color.Transparent ) Slider( progress.value.toFloat(), @@ -133,12 +150,12 @@ private fun VoiceLayout( movedManuallyTo = it.toInt() }, Modifier - .size(width, 48.dp) + .size(width, 48.sp.toDp()) .weight(1f) .padding(padding) .drawBehind { - drawRect(primary, Offset(0f, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4)) - drawRect(inactiveTrackColor, Offset(size.width - dp10, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4)) + drawRect(inactiveTrackColor, Offset(0f, (size.height - dp4) / 2), size = Size(size.width, dp4)) + drawRect(primary, Offset(0f, (size.height - dp4) / 2), size = Size(progress.value.toFloat() / max(0.00001f, duration.value.toFloat()) * size.width, dp4)) }, valueRange = 0f..duration.value.toFloat(), colors = colors @@ -153,13 +170,22 @@ private fun VoiceLayout( } } when { + sizeMultiplier != 1f -> { + Row(verticalAlignment = Alignment.CenterVertically) { + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, sizeMultiplier, play, pause, longClick, receiveFile) + Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically) { + DurationText(text, PaddingValues(start = 8.sp.toDp()), true) + Slider(MaterialTheme.colors.background, PaddingValues(start = 7.sp.toDp())) + } + } + } hasText -> { val sentColor = MaterialTheme.appColors.sentMessage val receivedColor = MaterialTheme.appColors.receivedMessage - Spacer(Modifier.width(6.dp)) - VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) + Spacer(Modifier.width(6.sp.toDp() * sizeMultiplier)) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile) Row(verticalAlignment = Alignment.CenterVertically) { - DurationText(text, PaddingValues(start = 12.dp)) + DurationText(text, PaddingValues(start = 12.sp.toDp() * sizeMultiplier)) Slider(if (ci.chatDir.sent) sentColor else receivedColor) } } @@ -167,13 +193,13 @@ private fun VoiceLayout( Column(horizontalAlignment = Alignment.End) { Row { Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End) { - Spacer(Modifier.height(56.dp)) + Spacer(Modifier.height(56.sp.toDp() * sizeMultiplier)) Slider(MaterialTheme.colors.background, PaddingValues(end = DEFAULT_PADDING_HALF + 3.dp)) - DurationText(text, PaddingValues(end = 12.dp)) + DurationText(text, PaddingValues(end = 12.sp.toDp() * sizeMultiplier)) } - VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile) } - Box(Modifier.padding(top = 6.dp, end = 6.dp)) { + Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier, end = 6.sp.toDp() * sizeMultiplier)) { CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) } } @@ -181,14 +207,14 @@ private fun VoiceLayout( else -> { Column(horizontalAlignment = Alignment.Start) { Row { - VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile) Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { - DurationText(text, PaddingValues(start = 12.dp)) + DurationText(text, PaddingValues(start = 12.sp.toDp() * sizeMultiplier)) Slider(MaterialTheme.colors.background, PaddingValues(start = DEFAULT_PADDING_HALF + 3.dp)) - Spacer(Modifier.height(56.dp)) + Spacer(Modifier.height(56.sp.toDp() * sizeMultiplier)) } } - Box(Modifier.padding(top = 6.dp)) { + Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier)) { CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) } } @@ -197,7 +223,7 @@ private fun VoiceLayout( } @Composable -private fun DurationText(text: State, padding: PaddingValues) { +private fun DurationText(text: State, padding: PaddingValues, smallView: Boolean = false) { val minWidth = with(LocalDensity.current) { 45.sp.toDp() } Text( text.value, @@ -205,7 +231,7 @@ private fun DurationText(text: State, padding: PaddingValues) { .padding(padding) .widthIn(min = minWidth), color = MaterialTheme.colors.secondary, - fontSize = 16.sp, + fontSize = if (smallView) 15.sp else 16.sp, maxLines = 1 ) } @@ -219,6 +245,7 @@ private fun PlayPauseButton( strokeColor: Color, enabled: Boolean, error: Boolean, + sizeMultiplier: Float = 1f, play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, @@ -234,7 +261,7 @@ private fun PlayPauseButton( ) { Box( Modifier - .defaultMinSize(minWidth = 56.dp, minHeight = 56.dp) + .defaultMinSize(minWidth = 56.sp.toDp() * sizeMultiplier, minHeight = 56.sp.toDp() * sizeMultiplier) .combinedClickable( onClick = { if (!audioPlaying) play() else pause() }, onLongClick = longClick @@ -245,7 +272,7 @@ private fun PlayPauseButton( Icon( if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(icon), contentDescription = null, - Modifier.size(36.dp), + Modifier.size(36.sp.toDp() * sizeMultiplier), tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } @@ -262,34 +289,42 @@ private fun PlayablePlayPauseButton( strokeWidth: Float, strokeColor: Color, error: Boolean, + sizeMultiplier: Float = 1f, play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, ) { val angle = 360f * (progress.value.toDouble() / duration.value).toFloat() if (hasText) { - IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) { + Box( + Modifier + .defaultMinSize(minWidth = 56.sp.toDp() * sizeMultiplier, minHeight = 56.sp.toDp() * sizeMultiplier) + .clip(MaterialTheme.shapes.small.copy(CornerSize(percent = 50))) + .combinedClickable(onClick = { if (!audioPlaying) play() else pause() } ) + .drawRingModifier(angle, strokeColor, strokeWidth), + contentAlignment = Alignment.Center + ) { Icon( if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), contentDescription = null, - Modifier.size(36.dp), + Modifier.size(36.sp.toDp() * sizeMultiplier), tint = MaterialTheme.colors.primary ) } } else { - PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick) + PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, sizeMultiplier, play, pause, longClick = longClick) } } @Composable -private fun VoiceMsgLoadingProgressIndicator() { +private fun VoiceMsgLoadingProgressIndicator(sizeMultiplier: Float) { Box( Modifier - .size(56.dp) - .clip(RoundedCornerShape(4.dp)), + .size(56.sp.toDp() * sizeMultiplier) + .clip(RoundedCornerShape(4.sp.toDp() * sizeMultiplier)), contentAlignment = Alignment.Center ) { - ProgressIndicator() + ProgressIndicator(sizeMultiplier) } } @@ -297,6 +332,7 @@ private fun VoiceMsgLoadingProgressIndicator() { private fun FileStatusIcon( sent: Boolean, icon: ImageResource, + sizeMultiplier: Float, longClick: () -> Unit, onClick: () -> Unit, ) { @@ -309,7 +345,7 @@ private fun FileStatusIcon( ) { Box( Modifier - .defaultMinSize(minWidth = 56.dp, minHeight = 56.dp) + .defaultMinSize(minWidth = 56.sp.toDp() * sizeMultiplier, minHeight = 56.sp.toDp() * sizeMultiplier) .combinedClickable( onClick = onClick, onLongClick = longClick @@ -320,7 +356,7 @@ private fun FileStatusIcon( Icon( painterResource(icon), contentDescription = null, - Modifier.size(36.dp), + Modifier.size(36.sp.toDp() * sizeMultiplier), tint = MaterialTheme.colors.secondary ) } @@ -336,26 +372,28 @@ private fun VoiceMsgIndicator( progress: State?, duration: State?, error: Boolean, + sizeMultiplier: Float, play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, receiveFile: (Long) -> Unit, ) { - val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } + val strokeWidth = with(LocalDensity.current) { 3.sp.toPx() } * sizeMultiplier val strokeColor = MaterialTheme.colors.primary when { file?.fileStatus is CIFileStatus.SndStored -> if (file.fileProtocol == FileProtocol.LOCAL && progress != null && duration != null) { - PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick) + PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, sizeMultiplier, play, pause, longClick = longClick) } else { - VoiceMsgLoadingProgressIndicator() + VoiceMsgLoadingProgressIndicator(sizeMultiplier) } file?.fileStatus is CIFileStatus.SndTransfer -> - VoiceMsgLoadingProgressIndicator() + VoiceMsgLoadingProgressIndicator(sizeMultiplier) file != null && file.fileStatus is CIFileStatus.SndError -> FileStatusIcon( sent, MR.images.ic_close, + sizeMultiplier, longClick, onClick = { AlertManager.shared.showAlertMsg( @@ -368,6 +406,7 @@ private fun VoiceMsgIndicator( FileStatusIcon( sent, MR.images.ic_warning_filled, + sizeMultiplier, longClick, onClick = { AlertManager.shared.showAlertMsg( @@ -377,15 +416,16 @@ private fun VoiceMsgIndicator( } ) file?.fileStatus is CIFileStatus.RcvInvitation -> - PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick) + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, sizeMultiplier, { receiveFile(file.fileId) }, {}, longClick = longClick) file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted -> - VoiceMsgLoadingProgressIndicator() + VoiceMsgLoadingProgressIndicator(sizeMultiplier) file?.fileStatus is CIFileStatus.RcvAborted -> - PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem) + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, sizeMultiplier, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem) file != null && file.fileStatus is CIFileStatus.RcvError -> FileStatusIcon( sent, MR.images.ic_close, + sizeMultiplier, longClick, onClick = { AlertManager.shared.showAlertMsg( @@ -398,6 +438,7 @@ private fun VoiceMsgIndicator( FileStatusIcon( sent, MR.images.ic_warning_filled, + sizeMultiplier, longClick, onClick = { AlertManager.shared.showAlertMsg( @@ -407,9 +448,9 @@ private fun VoiceMsgIndicator( } ) file != null && file.loaded && progress != null && duration != null -> - PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick) + PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, sizeMultiplier, play, pause, longClick = longClick) else -> - PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick) + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, sizeMultiplier, {}, {}, longClick) } } @@ -435,11 +476,16 @@ fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = } } +fun voiceMessageSizeBasedOnSquareSize(squareSize: Float): Float { + val squareToCircleRatio = 0.935f + return squareSize + squareSize * (1 - squareToCircleRatio) +} + @Composable -private fun ProgressIndicator() { +private fun ProgressIndicator(sizeMultiplier: Float) { CircularProgressIndicator( - Modifier.size(32.dp), + Modifier.size(32.sp.toDp() * sizeMultiplier), color = if (isInDarkTheme()) FileDark else FileLight, - strokeWidth = 4.dp + strokeWidth = 4.sp.toDp() * sizeMultiplier ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index c195a1a299..6f5cb63262 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -341,7 +341,7 @@ fun ChatItemView( if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy) } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) { - CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile) + CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) } else { framedItemView() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index b2777a7042..8a579d5289 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -176,7 +176,7 @@ fun FramedItemView( @Composable fun ciFileView(ci: ChatItem, text: String) { - CIFileView(ci.file, ci.meta.itemEdited, showMenu, receiveFile) + CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, receiveFile) if (text != "" || ci.meta.isLive) { CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy) } @@ -238,7 +238,7 @@ fun FramedItemView( } else { when (val mc = ci.content.msgContent) { is MsgContent.MCImage -> { - CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, false, receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { @@ -246,7 +246,7 @@ fun FramedItemView( } } is MsgContent.MCVideo -> { - CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, smallView = false, receiveFile = receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { @@ -254,7 +254,7 @@ fun FramedItemView( } } is MsgContent.MCVoice -> { - CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile) + CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) if (mc.text != "") { CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy) } @@ -267,7 +267,7 @@ fun FramedItemView( ciFileView(ci, mc.text) } is MsgContent.MCLink -> { - ChatItemLinkView(mc.preview) + ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true }) Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index 1dd5e4ee69..09838796c5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -16,7 +16,6 @@ import chat.simplex.common.model.CryptoFile import chat.simplex.common.platform.* import chat.simplex.common.views.chat.ProviderMedia import chat.simplex.common.views.helpers.* -import chat.simplex.res.MR import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import java.net.URI @@ -40,14 +39,17 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> ) { provider.totalMediaSize.value } + val firstValidPageBeforeScrollingToStart = remember { mutableStateOf(0) } val goBack = { provider.onDismiss(pagerState.currentPage); close() } BackHandler(onBack = goBack) // Pager doesn't ask previous page at initialization step who knows why. By not doing this, prev page is not checked and can be blank, // which makes this blank page visible for a moment. Prevent it by doing the check ourselves LaunchedEffect(Unit) { if (provider.getMedia(provider.initialIndex - 1) == null) { + firstValidPageBeforeScrollingToStart.value = provider.initialIndex provider.scrollToStart() pagerState.scrollToPage(0) + firstValidPageBeforeScrollingToStart.value = 0 } } val scope = rememberCoroutineScope() @@ -58,6 +60,9 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> @Composable fun Content(index: Int) { + // Index can be huge but in reality at that moment pager state scrolls to 0 and that page should have index 0 too if it's the first one. + // Or index 1 if it's the second page + val index = index - firstValidPageBeforeScrollingToStart.value Column( Modifier .fillMaxSize() @@ -174,7 +179,7 @@ private fun VideoViewEncrypted(uriUnencrypted: MutableState, fileSource: C } Box(contentAlignment = Alignment.Center) { VideoPreviewImageViewFullScreen(defaultPreview, {}, {}) - VideoDecryptionProgress {} + VideoDecryptionProgress() {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index d06e2ae88e..dc32bb1318 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -630,6 +630,7 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe if (contact != null && isCurrentUser && contactRequest != null) { val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) chatModel.replaceChat(rhId, contactRequest.id, chat) + chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index bf3e7774c7..43f0b7ef9b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -255,7 +255,6 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt fontWeight = FontWeight.SemiBold, ) SubscriptionStatusIndicator( - serversSummary = serversSummary, click = { ModalManager.start.closeModals() ModalManager.start.showModalCloseable( @@ -286,34 +285,33 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt } @Composable -fun SubscriptionStatusIndicator(serversSummary: MutableState, click: (() -> Unit)) { +fun SubscriptionStatusIndicator(click: (() -> Unit)) { var subs by remember { mutableStateOf(SMPServerSubs.newSMPServerSubs) } - var sess by remember { mutableStateOf(ServerSessions.newServerSessions) } + var hasSess by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - suspend fun setServersSummary() { - serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId()) - - serversSummary.value?.let { - subs = it.allUsersSMP.smpTotals.subs - sess = it.allUsersSMP.smpTotals.sessions + suspend fun setSubsTotal() { + val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId()) + if (r != null) { + subs = r.first + hasSess = r.second } } LaunchedEffect(Unit) { - setServersSummary() + setSubsTotal() scope.launch { while (isActive) { delay(1.seconds) if ((appPlatform.isDesktop || chatModel.chatId.value == null) && !ModalManager.start.hasModalsOpen() && !ModalManager.fullscreen.hasModalsOpen() && isAppVisibleAndFocused()) { - setServersSummary() + setSubsTotal() } } } } SimpleButtonFrame(click = click) { - SubscriptionStatusIndicatorView(subs = subs, sess = sess) + SubscriptionStatusIndicatorView(subs = subs, hasSess = hasSess) } } @@ -518,7 +516,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState + itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat -> val nextChatSelected = remember(chat.id, chats) { derivedStateOf { chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index c42b7e3ec3..2cf9008fc3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.chatlist -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.InlineTextContent @@ -15,19 +14,22 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chat.ComposePreview -import chat.simplex.common.views.chat.ComposeState -import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.model.GroupInfo -import chat.simplex.common.platform.appPlatform -import chat.simplex.common.platform.chatModel -import chat.simplex.common.views.chat.item.markedDeletedText +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.item.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @@ -207,7 +209,7 @@ fun ChatPreviewView( is ChatInfo.Direct -> if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) { Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary) - } else if (!cInfo.ready && cInfo.contact.activeConn != null) { + } else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null) { if (cInfo.contact.nextSendGrpInv) { Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary) } else if (cInfo.contact.active) { @@ -225,6 +227,50 @@ fun ChatPreviewView( } } + @Composable + fun chatItemContentPreview(chat: Chat, ci: ChatItem?) { + val mc = ci?.content?.msgContent + val provider by remember(chat.id, ci?.id, ci?.file?.fileStatus) { + mutableStateOf({ providerForGallery(0, chat.chatItems, ci?.id ?: 0) {} }) + } + val uriHandler = LocalUriHandler.current + when (mc) { + is MsgContent.MCLink -> SmallContentPreview { + IconButton({ uriHandler.openUriCatching(mc.preview.uri) }, Modifier.desktopPointerHoverIconHand()) { + Image(base64ToBitmap(mc.preview.image), null, contentScale = ContentScale.Crop) + } + Box(Modifier.align(Alignment.TopEnd).size(15.sp.toDp()).background(Color.Black.copy(0.25f), CircleShape), contentAlignment = Alignment.Center) { + Icon(painterResource(MR.images.ic_arrow_outward), null, Modifier.size(13.sp.toDp()), tint = Color.White) + } + } + is MsgContent.MCImage -> SmallContentPreview { + CIImageView(image = mc.image, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true) { + val user = chatModel.currentUser.value ?: return@CIImageView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + is MsgContent.MCVideo -> SmallContentPreview { + CIVideoView(image = mc.image, mc.duration, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true) { + val user = chatModel.currentUser.value ?: return@CIVideoView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + is MsgContent.MCVoice -> SmallContentPreviewVoice() { + CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, smallView = true, longClick = {}) { + val user = chatModel.currentUser.value ?: return@CIVoiceView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + is MsgContent.MCFile -> SmallContentPreviewFile { + CIFileView(ci.file, false, remember { mutableStateOf(false) }, smallView = true) { + val user = chatModel.currentUser.value ?: return@CIFileView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + else -> {} + } + } + @Composable fun progressView() { CircularProgressIndicator( @@ -279,73 +325,115 @@ fun ChatPreviewView( chatPreviewImageOverlayIcon() } } - Column( - modifier = Modifier - .padding(start = 8.dp, end = 8.sp.toDp()) - .weight(1F) - ) { - chatPreviewTitle() - Row(Modifier.heightIn(min = 46.sp.toDp()).padding(top = 3.sp.toDp())) { - chatPreviewText() + Spacer(Modifier.width(8.dp)) + Column(Modifier.weight(1f)) { + Row { + Box(Modifier.weight(1f)) { + chatPreviewTitle() + } + Spacer(Modifier.width(8.sp.toDp())) + val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs) + ChatListTimestampView(ts) } - } + Row(Modifier.heightIn(min = 46.sp.toDp()).fillMaxWidth()) { + Row(Modifier.padding(top = 3.sp.toDp()).weight(1f)) { + val activeVoicePreview: MutableState<(ActiveVoicePreview)?> = remember(chat.id) { mutableStateOf(null) } + val chat = activeVoicePreview.value?.chat ?: chat + val ci = activeVoicePreview.value?.ci ?: chat.chatItems.lastOrNull() + val mc = ci?.content?.msgContent + if ((showChatPreviews && chatModelDraftChatId != chat.id) || activeVoicePreview.value != null) { + chatItemContentPreview(chat, ci) + } + if (mc !is MsgContent.MCVoice || mc.text.isNotEmpty() || chatModelDraftChatId == chat.id) { + Box(Modifier.offset(x = if (mc is MsgContent.MCFile) -15.sp.toDp() else 0.dp)) { + chatPreviewText() + } + } + LaunchedEffect(AudioPlayer.currentlyPlaying.value, activeVoicePreview.value) { + val playing = AudioPlayer.currentlyPlaying.value + when { + playing == null -> activeVoicePreview.value = null + activeVoicePreview.value == null -> if (mc is MsgContent.MCVoice && playing.fileSource.filePath == ci.file?.fileSource?.filePath) { + activeVoicePreview.value = ActiveVoicePreview(chat, ci, mc) + } + else -> if (playing.fileSource.filePath != ci?.file?.fileSource?.filePath) { + activeVoicePreview.value = null + } + } + } + } - Box( - contentAlignment = Alignment.TopEnd - ) { - val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs) - ChatListTimestampView(ts) - val n = chat.chatStats.unreadCount - val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) - if (n > 0 || chat.chatStats.unreadChat) { - Box( - Modifier.padding(top = 24.5.sp.toDp())) { - Text( - if (n > 0) unreadCountStr(n) else "", - color = Color.White, - fontSize = 10.sp, - modifier = Modifier - .background(if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape) - .badgeLayout() - .padding(horizontal = 3.sp.toDp()) - .padding(vertical = 1.sp.toDp()) - ) + Spacer(Modifier.width(8.sp.toDp())) + + Box(Modifier.widthIn(min = 34.sp.toDp()), contentAlignment = Alignment.TopEnd) { + val n = chat.chatStats.unreadCount + val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) + if (n > 0 || chat.chatStats.unreadChat) { + Text( + if (n > 0) unreadCountStr(n) else "", + color = Color.White, + fontSize = 10.sp, + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier + .offset(y = 3.sp.toDp()) + .background(if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape) + .badgeLayout() + .padding(horizontal = 2.sp.toDp()) + .padding(vertical = 1.sp.toDp()) + ) + } else if (showNtfsIcon) { + Icon( + painterResource(MR.images.ic_notifications_off_filled), + contentDescription = generalGetString(MR.strings.notifications), + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .padding(start = 2.sp.toDp()) + .size(18.sp.toDp()) + .offset(x = 2.5.sp.toDp(), y = 2.sp.toDp()) + ) + } else if (chat.chatInfo.chatSettings?.favorite == true) { + Icon( + painterResource(MR.images.ic_star_filled), + contentDescription = generalGetString(MR.strings.favorite_chat), + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(20.sp.toDp()) + .offset(x = 2.5.sp.toDp()) + ) + } + Box( + Modifier.offset(y = 28.sp.toDp()), + contentAlignment = Alignment.Center + ) { + chatStatusImage() + } } - } else if (showNtfsIcon) { - Box( - Modifier.padding(top = 22.sp.toDp())) { - Icon( - painterResource(MR.images.ic_notifications_off_filled), - contentDescription = generalGetString(MR.strings.notifications), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(17.sp.toDp()) - .offset(x = 2.5.sp.toDp()) - ) - } - } else if (chat.chatInfo.chatSettings?.favorite == true) { - Box( - Modifier.padding(top = 20.sp.toDp())) { - Icon( - painterResource(MR.images.ic_star_filled), - contentDescription = generalGetString(MR.strings.favorite_chat), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(20.sp.toDp()) - .offset(x = 2.5.sp.toDp()) - ) - } - } - Box( - Modifier.padding(top = 46.sp.toDp()), - contentAlignment = Alignment.Center - ) { - chatStatusImage() } } } } +@Composable +private fun SmallContentPreview(content: @Composable BoxScope.() -> Unit) { + Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).size(36.sp.toDp()).border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(22)).clip(RoundedCornerShape(22))) { + content() + } +} + +@Composable +private fun SmallContentPreviewVoice(content: @Composable () -> Unit) { + Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).height(voiceMessageSizeBasedOnSquareSize(36f).sp.toDp())) { + content() + } +} + +@Composable +private fun SmallContentPreviewFile(content: @Composable () -> Unit) { + Box(Modifier.padding(top = 3.sp.toDp(), end = 8.sp.toDp()).offset(x = -8.sp.toDp(), y = -4.sp.toDp()).height(41.sp.toDp())) { + content() + } +} + @Composable fun IncognitoIcon(incognito: Boolean) { if (incognito) { @@ -390,6 +478,12 @@ fun unreadCountStr(n: Int): String { } } +private data class ActiveVoicePreview( + val chat: Chat, + val ci: ChatItem, + val mc: MsgContent.MCVoice +) + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt index 5a3f09919e..8621c73ef3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -45,21 +45,21 @@ import chat.simplex.common.model.ServerProtocol import chat.simplex.common.model.ServerSessions import chat.simplex.common.model.XFTPServerSummary import chat.simplex.common.model.localTimestamp -import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.platform.appPlatform +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.ProtocolServersView +import chat.simplex.common.views.usersettings.SettingsPreferenceItem import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.datetime.Instant import numOrDash import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.seconds enum class SubscriptionColorType { ACTIVE, ACTIVE_SOCKS_PROXY, DISCONNECTED, ACTIVE_DISCONNECTED @@ -76,7 +76,7 @@ fun subscriptionStatusColorAndPercentage( online: Boolean, socksProxy: String?, subs: SMPServerSubs, - sess: ServerSessions + hasSess: Boolean ): SubscriptionStatus { fun roundedToQuarter(n: Float): Float = when { @@ -91,16 +91,16 @@ fun subscriptionStatusColorAndPercentage( return if (online && subs.total > 0) { if (subs.ssActive == 0) { - if (sess.ssConnected == 0) - noConnColorAndPercent - else + if (hasSess) SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + else + noConnColorAndPercent } else { // ssActive > 0 - if (sess.ssConnected == 0) + if (hasSess) + SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + else // This would mean implementation error SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) - else - SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) } } else noConnColorAndPercent } @@ -116,9 +116,9 @@ private fun SubscriptionStatusIndicatorPercentage(percentageText: String) { } @Composable -fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, sess: ServerSessions, leadingPercentage: Boolean = false) { +fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, hasSess: Boolean, leadingPercentage: Boolean = false) { val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg()) - val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, sess) + val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, hasSess) val pref = remember { chatModel.controller.appPrefs.networkShowSubscriptionPercentage } val percentageText = "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%" @@ -193,7 +193,7 @@ private fun SMPServerView(srvSumm: SMPServerSummary, statsStartedAt: Instant, rh ) if (srvSumm.subs != null) { Spacer(Modifier.fillMaxWidth().weight(1f)) - SubscriptionStatusIndicatorView(subs = srvSumm.subs, sess = srvSumm.sessionsOrNew, leadingPercentage = true) + SubscriptionStatusIndicatorView(subs = srvSumm.subs, hasSess = srvSumm.sessionsOrNew.hasSess, leadingPercentage = true) } else if (srvSumm.sessions != null) { Spacer(Modifier.fillMaxWidth().weight(1f)) Icon(painterResource(MR.images.ic_arrow_upward), contentDescription = null, tint = SessIconColor(srvSumm.sessions)) @@ -334,7 +334,7 @@ private fun SMPSubscriptionsSection(totals: SMPTotals) { style = MaterialTheme.typography.body2, fontSize = 12.sp ) - SubscriptionStatusIndicatorView(totals.subs, totals.sessions) + SubscriptionStatusIndicatorView(totals.subs, totals.sessions.hasSess) } Column(Modifier.padding(PaddingValues()).fillMaxWidth()) { InfoRow( @@ -345,6 +345,7 @@ private fun SMPSubscriptionsSection(totals: SMPTotals) { generalGetString(MR.strings.servers_info_subscriptions_total), numOrDash(totals.subs.total) ) + SettingsPreferenceItem(null, stringResource(MR.strings.subscription_percentage), chatModel.controller.appPrefs.networkShowSubscriptionPercentage) } } } @@ -363,7 +364,7 @@ private fun SMPSubscriptionsSection(subs: SMPServerSubs, summary: SMPServerSumma style = MaterialTheme.typography.body2, fontSize = 12.sp ) - SubscriptionStatusIndicatorView(subs, summary.sessionsOrNew) + SubscriptionStatusIndicatorView(subs, summary.sessionsOrNew.hasSess) } Column(Modifier.padding(PaddingValues()).fillMaxWidth()) { InfoRow( @@ -717,6 +718,10 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta remember { stateGetOrPut("serverTypeSelection") { PresentedServerType.SMP } } val scope = rememberCoroutineScope() + suspend fun setServersSummary() { + serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId()) + } + LaunchedEffect(Unit) { if (chatModel.users.count { u -> u.user.activeUser || !u.user.hidden } == 1 ) { @@ -724,6 +729,29 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta } else { showUserSelection = true } + setServersSummary() + scope.launch { + while (isActive) { + delay(1.seconds) + if ((appPlatform.isDesktop || chat.simplex.common.platform.chatModel.chatId.value == null) && isAppVisibleAndFocused()) { + setServersSummary() + } + } + } + } + + fun resetStats() { + withBGApi { + val success = controller.resetAgentServersStats(rh?.remoteHostId) + if (success) { + setServersSummary() + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.servers_info_modal_error_title), + text = generalGetString(MR.strings.servers_info_reset_stats_alert_error_title) + ) + } + } } Column( @@ -768,7 +796,7 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta ) { PresentedServerType.entries.size } KeyChangeEffect(serverTypePagerState.currentPage) { - selectedServerType.value = PresentedServerType.values()[serverTypePagerState.currentPage] + selectedServerType.value = PresentedServerType.entries[serverTypePagerState.currentPage] } TabRow( selectedTabIndex = serverTypePagerState.currentPage, @@ -907,7 +935,7 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta SectionView { ReconnectAllServersButton(rh) - ResetStatisticsButton(rh) + ResetStatisticsButton(rh, resetStats = { resetStats() }) } SectionBottomSpacer() @@ -948,8 +976,8 @@ private fun reconnectAllServersAlert(rh: RemoteHostInfo?) { } @Composable -private fun ResetStatisticsButton(rh: RemoteHostInfo?) { - SectionItemView(click = { resetStatisticsAlert(rh) }) { +private fun ResetStatisticsButton(rh: RemoteHostInfo?, resetStats: () -> Unit) { + SectionItemView(click = { resetStatisticsAlert(rh, resetStats) }) { Text( stringResource(MR.strings.servers_info_reset_stats), color = MaterialTheme.colors.primary @@ -957,23 +985,12 @@ private fun ResetStatisticsButton(rh: RemoteHostInfo?) { } } -private fun resetStatisticsAlert(rh: RemoteHostInfo?) { +private fun resetStatisticsAlert(rh: RemoteHostInfo?, resetStats: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.servers_info_reset_stats_alert_title), text = generalGetString(MR.strings.servers_info_reset_stats_alert_message), confirmText = generalGetString(MR.strings.servers_info_reset_stats_alert_confirm), destructive = true, - onConfirm = { - withBGApi { - val success = controller.resetAgentServersStats(rh?.remoteHostId) - - if (!success) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.servers_info_modal_error_title), - text = generalGetString(MR.strings.servers_info_reset_stats_alert_error_title) - ) - } - } - } + onConfirm = resetStats ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt index 20b8f7c09f..cce7cf17a5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt @@ -1,12 +1,11 @@ package chat.simplex.common.views.helpers import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -15,9 +14,11 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.LinkPreview import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.chatViewScrollState import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -121,12 +122,16 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel } @Composable -fun ChatItemLinkView(linkPreview: LinkPreview) { +fun ChatItemLinkView(linkPreview: LinkPreview, showMenu: State, onLongClick: () -> Unit) { Column(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } Image( base64ToBitmap(linkPreview.image), stringResource(MR.strings.image_descr_link_preview), - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .desktopModifyBlurredState(true, blurred, showMenu) + .privacyBlur(true, blurred, chatViewScrollState.collectAsState(), onLongClick = onLongClick), contentScale = ContentScale.FillWidth, ) Column(Modifier.padding(top = 6.dp).padding(horizontal = 12.dp)) { @@ -179,7 +184,7 @@ private fun normalizeImageUri(u: URL, imageUri: String) = when { @Composable fun PreviewChatItemLinkView() { SimpleXTheme { - ChatItemLinkView(LinkPreview.sampleData) + ChatItemLinkView(LinkPreview.sampleData, remember { mutableStateOf(false) }) {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index e7033e88f4..61c8e1b75f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -42,7 +42,6 @@ fun NetworkAndServersView() { // It's not a state, just a one-time value. Shouldn't be used in any state-related situations val netCfg = remember { chatModel.controller.getNetCfg() } val networkUseSocksProxy: MutableState = remember { mutableStateOf(netCfg.useSocksProxy) } - val networkShowSubscriptionPercentage: MutableState = remember { mutableStateOf(chatModel.controller.appPrefs.networkShowSubscriptionPercentage.get()) } val developerTools = chatModel.controller.appPrefs.developerTools.get() val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } val sessionMode = remember { mutableStateOf(netCfg.sessionMode) } @@ -54,7 +53,6 @@ fun NetworkAndServersView() { currentRemoteHost = currentRemoteHost, developerTools = developerTools, networkUseSocksProxy = networkUseSocksProxy, - networkShowSubscriptionPercentage = networkShowSubscriptionPercentage, onionHosts = onionHosts, sessionMode = sessionMode, smpProxyMode = smpProxyMode, @@ -119,9 +117,6 @@ fun NetworkAndServersView() { ) } }, - toggleNetworkShowSubscriptionPercentage = { enable -> - networkShowSubscriptionPercentage.value = enable - }, useOnion = { if (onionHosts.value == it) return@NetworkAndServersLayout val prevValue = onionHosts.value @@ -235,14 +230,12 @@ fun NetworkAndServersView() { currentRemoteHost: RemoteHostInfo?, developerTools: Boolean, networkUseSocksProxy: MutableState, - networkShowSubscriptionPercentage: MutableState, onionHosts: MutableState, sessionMode: MutableState, smpProxyMode: MutableState, smpProxyFallback: MutableState, proxyPort: State, toggleSocksProxy: (Boolean) -> Unit, - toggleNetworkShowSubscriptionPercentage: (Boolean) -> Unit, useOnion: (OnionHosts) -> Unit, updateSessionMode: (TransportSessionMode) -> Unit, updateSMPProxyMode: (SMPProxyMode) -> Unit, @@ -263,7 +256,6 @@ fun NetworkAndServersView() { SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } }) if (currentRemoteHost == null) { - SettingsPreferenceItem(painterResource(MR.images.ic_radiowaves_up_forward_4_bar),stringResource(MR.strings.subscription_percentage), chatModel.controller.appPrefs.networkShowSubscriptionPercentage) UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false) UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion) if (developerTools) { @@ -685,10 +677,8 @@ fun PreviewNetworkAndServersLayout() { currentRemoteHost = null, developerTools = true, networkUseSocksProxy = remember { mutableStateOf(true) }, - networkShowSubscriptionPercentage = remember { mutableStateOf(false) }, proxyPort = remember { mutableStateOf(9050) }, toggleSocksProxy = {}, - toggleNetworkShowSubscriptionPercentage = {}, onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, sessionMode = remember { mutableStateOf(TransportSessionMode.User) }, smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index dc0760193d..88b14b6a66 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.res.MR import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.ProfileNameField import chat.simplex.common.views.helpers.* @@ -32,6 +33,8 @@ import chat.simplex.common.views.localauth.SetAppPasscodeView import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* +import kotlin.math.min +import kotlin.math.roundToInt enum class LAMode { SYSTEM, @@ -95,6 +98,9 @@ fun PrivacySettingsView( withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } }) SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) + BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) { + appPrefs.privacyMediaBlurRadius.set(it) + } SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays) } SectionCustomFooter { @@ -217,6 +223,30 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State, onS ) } +@Composable +private fun BlurRadiusOptions(state: State, onSelected: (Int) -> Unit) { + val choices = listOf(0, 12, 24, 48) + val pickerValues = choices + if (choices.contains(state.value)) emptyList() else listOf(state.value) + val values = remember { + pickerValues.map { + when (it) { + 0 -> it to generalGetString(MR.strings.privacy_media_blur_radius_off) + 12 -> it to generalGetString(MR.strings.privacy_media_blur_radius_soft) + 24 -> it to generalGetString(MR.strings.privacy_media_blur_radius_medium) + 48 -> it to generalGetString(MR.strings.privacy_media_blur_radius_strong) + else -> it to "$it" + } + } + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.privacy_media_blur_radius), + values, + state, + icon = painterResource(MR.images.ic_blur_on), + onSelected = onSelected + ) +} + @Composable expect fun PrivacyDeviceSection( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index f7c6e03811..2b87fdd91c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -295,7 +295,7 @@ المنشئ خطأ في إضافة الأعضاء خطأ في إنشاء العنوان - خطأ في حذف اتصال جهة الاتصال المعلق + خطأ في حذف اتصال جهة الاتصال المنتظر أدخل رسالة ترحيب… متصل جار الاتصال @@ -793,7 +793,7 @@ لن يتم استخدام مضيفات البصل. اسم عرض جديد: عبارة مرور جديدة… - يرجى الانتظار + قيد الانتظار كلمة المرور مطلوبة ألصِق الرابط الذي استلمته فقط مالكي المجموعة يمكنهم تفعيل الملفات والوسائط. @@ -828,9 +828,9 @@ كلمة المرور للإظهار ندّ لِندّ يمكن للناس التواصل معك فقط عبر الرابط الذي تقوم بمشاركته - مكالمة في الانتظار + مكالمة قيد الانتظار تعمية ثنائية الطبقات من بين الطريفين.]]> - إعادة تعيين الألوان + صفّر الألوان حفظ عنوان الخادم المحدد مسبقًا حفظ وإشعار أعضاء المجموعة @@ -910,7 +910,7 @@ استلمت في إزالة العضو إزالة - إعادة التعيين إلى الإعدادات الافتراضية + صفّر إلى الإعدادات الافتراضية بينج الفاصل الزمني كلمة مرور الملف الشخصي منع إرسال الرسائل التي تختفي. @@ -938,7 +938,7 @@ يرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية. العنصر النائب لصورة الملف الشخصي رمز QR - إعادة التعيين + صفّر المنفذ %d خادم محدد مسبقًا قراءة المزيد في مستودعنا على GitHub. @@ -1812,7 +1812,7 @@ تلقى الرد أزِل الصورة تكرار - إعادة تعيين اللون + صفّر اللون أرسلت رد تعيين السمة الافتراضية النظام @@ -1840,10 +1840,10 @@ اجعل محادثاتك تبدو مختلفة! تلقي الملفات بأمان واجهة المستخدم الفارسية - إعادة التعيين إلى سمة التطبيق + صفّر إلى سمة التطبيق سمة التطبيق تأكيد الملفات من خوادم غير معروفة. - إعادة التعيين إلى سمة المستخدم + صفّر إلى سمة المستخدم معلومات قائمة انتظار الخادم: %1$s \n \nآخر رسالة تم استلامها: %2$s @@ -1871,4 +1871,120 @@ \nيُرجى مشاركة أي مشاكل أُخرى مع المطورين. لا يمكن إرسال الرسالة تفضيلات الدردشة المحدّدة تحظر هذه الرسالة. + التفاصيل + بدءًا من %s. +\nجميع البيانات خاصة بجهازك. + أرسلت الإجمالي + الحجم + الملفات المرفوعة + يُرجى المحاولة لاحقا. + خطأ في التوجيه الخاص + عنوان الخادم غير متوافق مع إعدادات الشبكة: %1$s. + إصدار الخادم غير متوافق مع تطبيقك: %1$s. + العضو غير نشط + رسالة محوّلة + لا يوجد اتصال مباشر حتى الآن، يتم تحويل من قِبل المشرف. + امسح / ألصِق الرابط + خوادم SMP المهيأة + خوادم SMP أخرى + خوادم XFTP المهيأة + خوادم XFTP أخرى + نسبة الاشتراك + مُعطّل + مستقرّ + يتوفر تحديث: %s + التمس التحديثات + نزّل %s (%s) + ثُبّت بنجاح + افتح مكان الملف + يُرجى إعادة تشغيل التطبيق. + تذكر لاحقا + تخطي هذه النسخة + أُلغيت تنزيل التحديث + مُعطّل + غير نشط + معلومات الخوادم + عرض المعلومات ل + الأخطاء + الرسائل المُرسلة + الإجمالي + الخوادم المتصلة سابقًا + حدث خطأ أثناء إعادة الاتصال بالخادم + أعِد توصيل الخادم؟ + أعِد التوصيل بالخادم لفرض تسليم الرسالة. يستخدم حركة مرور إضافية. + صفّر جميع الإحصائيات + صفّر جميع الإحصائيات؟ + نُزّلت + الرسائل المُستلمة + الرسائل المُرسلة + سيتم تصفير إحصائيات الخوادم - لا يمكن التراجع عن هذا! + رُفع + صفّر + بدءًا من %s. + خادم SMP + خادم XFTP + معترف به + حُذفت القطع + نُزّلت القطع + اكتملت + الاتصالات + أُنشئت + أخطاء فك التعمية + حُذِفت + الملفات التي نُزّلت + أخطاء التنزيل + منتهية الصلاحيّة + افتح إعدادات الخادم + أخرى + موّكل + مؤمن + أرسِل الأخطاء + أُرسلت مباشرةً + مُرسَل عبر الوكيل + مشترك + أخطاء الاشتراك + رفع الأخطاء + التمس التحديثات + أخطاء معترف بها + نُزّل تحديث التطبيق + جميع المستخدمين + المحاولات + تجريبي + رُفع القطع + متصل + الخوادم المتصلة + جارِ الاتصال + الاتصالات المشتركة + المستخدم الحالي + أخطاء الحذف + إحصائيات مفصلة + عطّل + خطأ في تصفير الإحصائيات + التكرارات + خطأ + حدث خطأ أثناء إعادة الاتصال بالخوادم + جارٍ تنزيل تحديث التطبيق، لا تغلق التطبيق + الملفات + حجم الخط + ثبّت التحديث + قد يتم تسليم الرسالة لاحقًا إذا أصبح العضو نشطًا. + الرسائل المُستلمة + اشتراكات الرسائل + لا توجد معلومات، حاول إعادة التحميل + أخطاء أخرى + قيد الانتظار + أعِد التوصيل + أعِد توصيل كافة الخوادم المتصلة لفرض تسليم الرسالة. يستخدم حركة مرور إضافية. + خوادم موّكلة + تلقي الأخطاء + تلقى الإجمالي + أعِد توصيل جميع الخوادم + أعِد توصيل الخوادم؟ + عنوان الخادم + الإحصائيات + تم تجاهل الاشتراكات + جلسات النقل + لكي يتم إعلامك بالإصدارات الجديدة، شغّل الفحص الدوري للإصدارات المستقرة أو التجريبية. + أنت غير متصل بهذه الخوادم. يتم استخدام التوجيه الخاص لتسليم الرسائل إليهم. + قرّب \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 7fbb4395d6..0564d87cd3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -115,6 +115,12 @@ Server address is incompatible with network settings: %1$s. Server version is incompatible with your app: %1$s. Private routing error + Error connecting to forwarding server %1$s. Please try later. + Forwarding server address is incompatible with network settings: %1$s. + Forwarding server version is incompatible with network settings: %1$s. + Forwarding server %1$s failed to connect to destination server %2$s. Please try later. + Destination server address of %1$s is incompatible with forwarding server %2$s settings. + Destination server version of %1$s is incompatible with forwarding server %2$s. Please try later. Error sending message Error creating message @@ -699,7 +705,7 @@ XFTP servers Configured XFTP servers Other XFTP servers - Subscription percentage + Show percentage Install SimpleX Chat for terminal Star on GitHub Contribute @@ -1096,6 +1102,11 @@ Disable (keep group overrides) Enable for all groups Disable for all groups + Blur media + Off + Soft + Medium + Strong YOU @@ -2119,8 +2130,8 @@ Files No info, try to reload Showing info for - All users - Current user + All profiles + Current profile Transport sessions Connected Connecting diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index cdc5720e37..2029650eb8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -74,7 +74,7 @@ Fehler beim Löschen des Kontakts Fehler beim Löschen der Gruppe Fehler beim Löschen der Kontaktanfrage - Fehler beim Löschen der anstehenden Kontaktaufnahme + Fehler beim Löschen der ausstehenden Kontaktaufnahme Fehler beim Wechseln der Empfängeradresse Der Test ist beim Schritt %s fehlgeschlagen. Um Warteschlangen zu erzeugen, benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort. @@ -157,7 +157,7 @@ Verbergen Erlauben Die Nachricht löschen? - Nachricht wird gelöscht – dies kann nicht rückgängig gemacht werden! + Nachricht wird gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachricht wird zum Löschen markiert. Der/die Empfänger kann/können diese Nachricht aufdecken. Für mich löschen Für alle @@ -218,7 +218,7 @@ Benachrichtigungen Kontakt löschen? - Der Kontakt und alle Nachrichten werden gelöscht – dies kann nicht rückgängig gemacht werden! + Der Kontakt und alle Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! Kontakt löschen Kontaktname festlegen… Verbunden @@ -278,7 +278,7 @@ Ablehnen Chatinhalte löschen? - Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. + Alle Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. Löschen Chatinhalte löschen Chatinhalte löschen @@ -298,7 +298,8 @@ Die von Ihnen akzeptierte Verbindung wird abgebrochen! Ihr Kontakt ist noch nicht verbunden! - Ihr Kontakt muss online sein, damit die Verbindung hergestellt werden kann.\nSie können diese Verbindung abbrechen und den Kontakt entfernen (und es später nochmals mit einem neuen Link versuchen). + Ihr Kontakt muss online sein, damit die Verbindung hergestellt werden kann. +\nSie können diese Verbindung abbrechen, den Kontakt entfernen und es später nochmals mit einem neuen Link versuchen. möchte sich mit Ihnen verbinden! @@ -531,7 +532,7 @@ Lautsprecher an Kamera umdrehen - Anstehender Anruf + Ausstehender Anruf Verpasster Anruf Abgelehnter Anruf Anruf wird verbunden @@ -766,8 +767,8 @@ Sie: %1$s Gruppe löschen Gruppe löschen? - Die Gruppe wird für alle Mitglieder gelöscht – dies kann nicht rückgängig gemacht werden! - Die Gruppe wird für Sie gelöscht – dies kann nicht rückgängig gemacht werden! + Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden! + Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! Gruppe verlassen Gruppenprofil bearbeiten Gruppen-Link @@ -786,7 +787,7 @@ Mitglied entfernen Direktnachricht senden - Das Mitglied wird aus der Gruppe entfernt – dies kann nicht rückgängig gemacht werden! + Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! Entfernen MITGLIED Rolle @@ -988,7 +989,7 @@ App-Version: v%s Core Version: v%s Profil hinzufügen - Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden! + Alle Chats und Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! Chat-Profil löschen für PING-Zähler Transport-Isolations-Modus aktualisieren\? @@ -1675,7 +1676,7 @@ unbekannter Gruppenmitglieds-Status Mit verschlüsselten Dateien und Medien. Private Notizen - Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden! + Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Private Notizen löschen? %s wurde blockiert %s wurde freigegeben @@ -1940,8 +1941,8 @@ \n \nZuletzt empfangene Nachricht: %2$s Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen. - Datei-Server-Fehler: %1$s - Falscher Schlüssel oder unbekannte Datei-Chunk-Adresse - höchstwahrscheinlich wurde die Datei gelöscht. + Datei-Server Fehler: %1$s + Falscher Schlüssel oder unbekannte Daten-Paketadresse der Datei - höchstwahrscheinlich wurde die Datei gelöscht. Datei-Fehler Nachrichten-Status Nachrichten-Status: %s @@ -1954,4 +1955,120 @@ \nBitte teilen Sie weitere mögliche Probleme den Entwicklern mit. Nachricht wurde nicht gesendet Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt. + Bitte versuchen Sie es später erneut. + Fehler beim privaten Routing + Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird. + Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet. + Konfigurierte SMP-Server + Konfigurierte XFTP-Server + Andere SMP-Server + Andere XFTP-Server + Abgeschlossen + Inaktiv + Verbunden + Verbinden + Abonnierte Verbindungen + Aktueller Nutzer + Detaillierte Statistiken + Details + Heruntergeladen + Fehler + Fehler beim Wiederherstellen der Verbindungen zu den Servern + Fehler beim Zurücksetzen der Statistiken + Fehler + Empfangene Nachrichten + Nachrichten-Abonnements + Ausstehend + Bisher verbundene Server + Proxy-Server + Empfangene Nachrichten + Summe aller empfangenen Nachrichten + Fehler bei der Bestätigung + Versuche + Daten-Pakete gelöscht + Daten-Pakete hochgeladen + Verbindungen + Erstellt + Entschlüsselungs-Fehler + Fehler beim Löschen + Heruntergeladene Dateien + Fehler beim Herunterladen + Duplikate + abgelaufen + Server-Einstellungen öffnen + Andere Fehler + Proxy + Fehler beim Empfang + Neu verbinden + Deaktiviert + Beta + App-Aktualisierung wird heruntergeladen. App nicht schließen! + Heruntergeladen %s (%s) + Nach Aktualisierungen suchen + Deaktivieren + Erfolgreich installiert + Aktualisierung installieren + Bitte starten Sie die App neu. + Bestätigt + Alle Nutzer + App-Aktualisierung wurde heruntergeladen + Nach Aktualisierungen suchen + Daten-Pakete heruntergeladen + Verbundene Server + Gelöscht + deaktiviert + Fehler beim Wiederherstellen der Verbindung zum Server + Nachricht weitergeleitet + Dateien + Schriftgröße + Mitglied inaktiv + Gesendete Nachrichten + Keine Information - es wird versucht neu zu laden + Dateispeicherort öffnen + andere + Die Server-Adresse ist nicht mit den Netzwerk-Einstellungen kompatibel: %1$s. + Link scannen / einfügen + Alle Server neu verbinden + Server neu verbinden? + Alle Server neu verbinden? + Um die Auslieferung von Nachrichten zu erzwingen, werden alle Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt. + Um die Auslieferung von Nachrichten zu erzwingen, wird der Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt. + Zurücksetzen + Alle Statistiken zurücksetzen + Alle Statistiken zurücksetzen? + Gesendete Nachrichten + Summe aller gesendeten Nachrichten + Abgesichert + Fehler beim Senden + Direkt gesendet + Über einen Proxy gesendet + Server-Adresse + Später erinnern + Server-Informationen + Ihre App ist nicht kompatibel mit der Server-Version: %1$s. + Prozentualer Anteil der Abonnements + Zoom + SMP-Server + Informationen zeigen für + Beginnend mit %s. +\nAlle Daten sind auf Ihrem Gerät geschützt. + Statistiken + Transport-Sitzungen + Hochgeladen + Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt. + Summe aller Abonnements + Die Serverstatistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden! + Größe + Beginnend mit %s. + Abonniert + Fehler beim Abonnieren + Nicht beachtete Abonnements + Hochgeladene Dateien + Fehler beim Hochladen + XFTP-Server + Stabil + Diese Version überspringen + Aktualisierung verfügbar: %s + Aktivieren Sie die periodische Überprüfung auf stabile oder Beta-Versionen der App, um über neue Versionen benachrichtigt zu werden. + Herunterladen der Aktualisierung abgebrochen \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 66ea969b09..7195024ab5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -1867,4 +1867,120 @@ \nMinden további problémát osszon meg a fejlesztőkkel. Nem lehet üzenetet küldeni A kiválasztott csevegési beállítások tiltják ezt az üzenetet. + Próbálja meg később. + A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. + Inaktív tag + Továbbított üzenet + Az üzenet később is kézbesíthető, ha a tag aktívvá válik. + Még nincs közvetlen kapcsolat, az üzenetet az admin továbbítja. + Hivatkozás beolvasása / beillesztése + Beállított SMP-kiszolgálók + Egyéb SMP-kiszolgálók + Egyéb XFTP-kiszolgálók + letiltva + inaktív + Nagyítás + információk a kiszolgálókról + Kapcsolódás + Hibák + Függőben + Kezdve ettől: %s. +\nMinden adat biztonságban van a készülékén. + Elküldött üzenetek + Proxyzott kiszolgálók + Újrakapcsolódás a kiszolgálókhoz? + Újrakapcsolódás a kiszolgálóhoz? + Hiba a kiszolgálóhoz való újrakapcsolódáskor + Újrakapcsolódás minden kiszolgálóhoz + Hiba a statisztikák visszaállításakor + Visszaállítás + Minden statisztika visszaállítása + Minden statisztika visszaállítása? + A kiszolgálók statisztikái visszaállnak - ez nem vonható vissza! + Részletes statisztikák + Letöltve + lejárt + egyéb + Összes fogadott + Üzenetfogadási hibák + Újrakapcsolás + Üzenetküldési hibák + Közvetlenül küldött + Összes elküldött + Proxyn keresztül küldve + SMP-kiszolgáló + Kezdve ettől: %s. + Feltöltve + XFTP-kiszolgáló + Proxyzott + duplikációk + egyéb hibák + Kapcsolatok + Létrehozva + Biztosítva + Törlési hibák + Méret + Feltöltött fájlok + Letöltött fájltöredékek + Letöltött fájlok + Kiszolgáló beállításainak megnyitása + Kiszolgáló címe + Feltöltési hibák + Nyugtázva + Nyugtázott hibák + próbálkozások + Törölt fájltöredékek + Minden felhasználó + Feltöltött fájltöredékek + Elkészült + Kapcsolódott kiszolgálók + Beállított XFTP-kiszolgálók + Kapcsolódva + Jelenlegi felhasználó + Részletek + visszafejtési hibák + Törölve + Fogadott üzenetek + Letöltési hibák + Hiba + Hiba a kiszolgálókhoz való újrakapcsolódáskor + Fájlok + Betűméret + Nincs információ, próbálja meg újratölteni + Korábban kapcsolódott kiszolgálók + Privát útválasztási hiba + Fogadott üzenetek + Az összes kiszolgálóhoz való újrakapcsolás az üzenetek kézbesítésének kikényszerítéséhez. Ez további adatforgalmat használ. + A kiszolgálóhoz való újrakapcsolódás az üzenet kézbesítésének kikényszerítéséhez. Ez további adatforgalmat használ. + Elküldött üzenetek + Munkamenetek átvitele + Összesen + Statisztikák + Információk megjelenítése ehhez: + A kiszolgáló verziója nem kompatibilis az alkalmazással: %1$s. + Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál. + Feliratkozott kapcsolatok + Üzenet feliratkozások + Feliratkozva + Feliratkozási hibák + Elutasított feliratkozások + Feliratkozási százalék + Alkalmazásfrissítés letöltve + Frissítések keresése + Frissítések keresése + Alkalmazásfrissítés letöltése, ne zárja be az alkalmazást + Letöltés %s (%s) + Sikeresen telepítve + Frissítés telepítése + Fájl helyének megnyitása + Indítsa újra az alkalmazást. + Emlékeztessen később + Hagyja ki ezt a verziót + Ha értesítést szeretne kapni az új kiadásokról, kapcsolja be a stabil vagy béta verziók időszakos ellenőrzését. + Frissítés érhető el: %s + A frissítés letöltése megszakítva + Béta + Letiltás + Letiltva + Stabil \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_outward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_outward.svg new file mode 100644 index 0000000000..2391aba06c --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_outward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_blur_on.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_blur_on.svg new file mode 100644 index 0000000000..e8767ccc86 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_blur_on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 3cace47d15..d41069a1b2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -46,4 +46,7 @@ Selalu APLIKASI Tampilan + Tentang SimpleX Chat + Terima + Terima \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 690c826549..f2f62c1fb9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -1873,4 +1873,120 @@ Copia errore Impossibile inviare il messaggio Le preferenze della chat selezionata vietano questo messaggio. + Connessioni + Connessioni sottoscritte + Creato + errori di decifrazione + Statistiche dettagliate + doppi + Errore + Errore di riconnessione al server + Errore di riconnessione ai server + scaduto + Iscrizioni ai messaggi + altro + altri errori + In attesa + Via proxy + Server via proxy + Totale ricevuto + Errori di ricezione + Riconnetti + Riconnetti tutti i server connessi per forzare la consegna dei messaggi. Usa traffico aggiuntivo. + Riconnettere il server? + Riconnettere i server? + Riconnetti il server per forzare la consegna dei messaggi. Usa traffico aggiuntivo. + Errori di invio + Inviato direttamente + Messaggi inviati + Inviato via proxy + Le statistiche dei server verranno azzerate - è irreversibile! + Azzera tutte le statistiche + Azzerare tutte le statistiche? + Azzera + Errore di azzeramento statistiche + Server SMP + Inizio da %s. + Totale + Inviato + Server XFTP + Riconosciuto + Errori di riconoscimento + Blocchi eliminati + Blocchi scaricati + Blocchi inviati + Eliminato + Errori di eliminazione + File scaricati + Apri impostazioni server + Protetto + Indirizzo server + Dimensione + Iscritto + Errori di iscrizione + Iscrizioni ignorate + File inviati + Errori di invio + Errore di instradamento privato + La versione del server non è compatibile con la tua app: %1$s. + Membro inattivo + Il messaggio può essere consegnato più tardi se il membro diventa attivo. + Server SMP configurati + Altri server SMP + Percentuale di iscrizione + inattivo + Zoom + Connesso + In connessione + Utente attuale + Dettagli + Errori + Messaggi ricevuti + Messaggi inviati + Nessuna informazione, prova a ricaricare + Info dei server + Informazioni di + Statistiche + Sessioni di trasporto + Tutti gli utenti + tentativi + Server XFTP configurati + Completato + Server connessi + disattivato + Scaricato + Messaggi ricevuti + Errori di scaricamento + Riconnetti tutti i server + L\'indirizzo del server non è compatibile con le impostazioni di rete: %1$s. + File + Scansiona / Incolla link + Dimensione carattere + Totale inviato + Messaggio inoltrato + Inizio da %s. +\nTutti i dati sono privati, nel tuo dispositivo. + Ancora nessuna connessione diretta, il messaggio viene inoltrato dall\'amministratore. + Non sei connesso/a a questi server. L\'instradamento privato è usato per consegnare loro i messaggi. + Altri server XFTP + Server precedentemente connessi + Riprova più tardi. + Beta + Disattiva + Disattivato + Scaricamento dell\'aggiornamento, non chiudere l\'app + Scarica %s (%s) + Installato correttamente + Apri percorso file + Riavvia l\'app. + Ricordamelo più tardi + Salta questa versione + Stabile + Per essere avvisato sulle nuove versioni, attiva il controllo periodico di versioni stabili o beta. + Aggiornamento disponibile: %s + Scaricamento aggiornamento annullato + Cerca aggiornamenti + Aggiornamento dell\'app scaricato + Cerca aggiornamenti + Installa aggiornamento \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index ac3f75780e..7d97b6fa03 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -1834,4 +1834,12 @@ いいえ はい メッセージルーティングモード + フォントサイズ + ベータ + アップデートを確認 + アップデートを確認 + 完了 + SMPサーバーの構成 + 接続中 + XFTPサーバーの構成 \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index ef66bf6e10..34255ce99b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -1871,4 +1871,96 @@ Berichtstatus Kan bericht niet verzenden Geselecteerde chatvoorkeuren verbieden dit bericht. + Fout in privéroutering + Serverversie is niet compatibel met uw app: %1$s. + Bericht doorgestuurd + Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder. + Overige XFTP servers + Link scannen/plakken + Zoom + Huidige gebruiker + Bestanden + Server informatie + Informatie weergeven voor + Fouten + Statistieken + Transportsessies + Verbindingen geabonneerd + Details + Berichten ontvangen + Berichten abonnementen + Beginnend vanaf %s. +\nAlle gegevens zijn privé op uw apparaat. + Verbonden servers + in behandeling + Eerder verbonden servers + Proxied servers + Totaal + Server opnieuw verbinden? + Servers opnieuw verbinden? + Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data. + U bent niet verbonden met deze servers. Privéroutering wordt gebruikt om berichten bij hen af te leveren. + Maak opnieuw verbinding met alle servers + Reset + Reset alle statistieken + Alle statistieken resetten? + Geüpload + Gedetailleerde statistieken + Ontvangen berichten + Verzonden berichten + Totaal verzonden + Proxied + Fouten ontvangen + opnieuw verbinden + Direct verzonden + Verzonden via proxy + SMP server + XFTP server + Erkend + Bevestigingsfouten + Verbindingen + Gemaakt + decoderingsfouten + duplicaten + verlopen + overige fouten + overig + Verzend fouten + Stukken gedownload + Stukken geüpload + Verwijderd + Verwijderingsfouten + Gedownloade bestanden + Beveiligd + Maat + Geabonneerd + Geüploade bestanden + Upload fouten + Downloadfouten + Server instellingen openen + Server adres + Alle gebruikers + pogingen + Stukken verwijderd + voltooid + Verbonden + Verbinden + Fout bij opnieuw verbinding maken met de server + Letter grootte + inactief + Lid inactief + Gedownload + Fout + Fout bij opnieuw verbinden van servers + Fout bij het resetten van statistieken + Het bericht kan later worden bezorgd als het lid actief wordt. + Berichten verzonden + Geen info, probeer opnieuw te laden + Overige SMP servers + Totaal ontvangen + Probeer het later. + Maak opnieuw verbinding met alle verbonden servers om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data. + Het serveradres is niet compatibel met de netwerkinstellingen: %1$s. + Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt! + Beginnend vanaf %s. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 3bb4d1b72b..dd675791c6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -1873,4 +1873,120 @@ 复制错误 无法发送消息 选择的聊天首选项禁止此条消息。 + 请稍后尝试。 + 私密路由出错 + 已转发的消息 + 尚无直接连接,消息由管理员转发。 + 其他 SMP 服务器 + 其他 XFTP 服务器 + 扫描/粘贴链接 + 订阅百分比 + 不活跃 + 缩放 + 所有用户 + 文件 + 没有信息,试试重新加载 + 服务器信息 + 尝试 + 已连接 + 已连接的服务器 + 连接中 + 订阅的连接 + 详细统计数据 + 详情 + 已下载 + 错误 + 重连服务器出错 + 重连服务器出错 + 重设统计数据出错 + 错误 + 收到的消息 + 消息订阅 + 待连接 + 先前连接的服务器 + 已代理的服务器 + 接收到的消息 + 接收总计 + 接收错误 + 重连 + 重连服务器? + 重连服务器? + 重连服务器强制消息传输。这会使用额外流量。 + 重置所有统计数据 + 重置所有统计数据吗? + 直接发送 + 已发送消息 + 发送总计 + 通过代理发送 + 服务器统计数据将被重置。此操作无法撤销! + XFTP 服务器 + 认可出错 + 块已删除 + 块已下载 + 已完毕 + 连接数 + 已创建 + 解密出错 + 已删除 + 删除错误 + 已下载的文件 + 下载出错 + 重复 + 已过期 + 其他 + 其他错误 + 已代理 + 已受保护 + 发送错误 + 服务器地址 + 大小 + 已上传的文件 + 上传出错 + 重新连接所有已连接的服务器来强制消息传输。这会使用额外流量。 + 你没有连接到这些服务器。私密路由被用于向它们传输消息。 + 重连所有服务器 + 重置 + 服务器地址不兼容网络设置:%1$s。 + 起始自 %s。 + 起始自 %s. +\n所有数据都是设备的私有数据。 + 已订阅 + 已认可 + 服务器版本不兼容你的应用:%1$s. + 信息主体 + SMP 服务器 + 统计数据 + 订阅错误 + 总计 + 块已上传 + 订阅被忽略 + 已配置的 SMP 服务器 + 已配置的 XFTP 服务器 + 当前用户 + 传输会话 + 已上传 + 已停用 + 字体大小 + 成员不活跃 + 如果成员变得活跃,可能会在之后传输消息。 + 发送的消息 + 打开服务器设置 + 检查更新 + 检查更新 + 停用 + 已停用 + 正在下载应用更新,不要关闭应用 + 下载 %s(%s) + 打开文件位置 + 请重启应用。 + 稍后提醒 + 跳过此版本 + 稳定版 + 有更新可用:%s + 取消了更新下载 + 要接收新版本通知,请打开“定期检查稳定或测试版本”。 + 应用更新已下载 + 测试版 + 安装成功 + 安装更新 \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt index 9245f2b950..97f8bc129a 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt @@ -4,8 +4,7 @@ import androidx.compose.foundation.contextMenuOpenDetector import androidx.compose.runtime.Composable import androidx.compose.ui.* import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.input.pointer.PointerIcon -import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.input.pointer.* import java.io.File import java.net.URI @@ -40,3 +39,8 @@ onExternalDrag(enabled) { actual fun Modifier.onRightClick(action: () -> Unit): Modifier = contextMenuOpenDetector { action() } actual fun Modifier.desktopPointerHoverIconHand(): Modifier = Modifier.pointerHoverIcon(PointerIcon.Hand) + +actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier = + this then Modifier + .onPointerEvent(PointerEventType.Enter) { action(true) } + .onPointerEvent(PointerEventType.Exit) { action(false) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt index e1dba29f04..9f34891b37 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt @@ -25,18 +25,13 @@ actual class RecorderNative: RecorderInterface { } actual object AudioPlayer: AudioPlayerInterface { - val player by lazy { AudioPlayerComponent().mediaPlayer() } + private val player by lazy { AudioPlayerComponent().mediaPlayer() } - // Filepath: String, onProgressUpdate - private val currentlyPlaying: MutableState Unit>?> = mutableStateOf(null) + override val currentlyPlaying: MutableState = mutableStateOf(null) private var progressJob: Job? = null - enum class TrackState { - PLAYING, PAUSED, REPLACED - } - // Returns real duration of the track - private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { + private fun start(fileSource: CryptoFile, smallView: Boolean, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) if (!File(absoluteFilePath).exists()) { Log.e(TAG, "No such file: ${fileSource.filePath}") @@ -46,7 +41,7 @@ actual object AudioPlayer: AudioPlayerInterface { VideoPlayerHolder.stopAll() RecorderInterface.stopRecording?.invoke() val current = currentlyPlaying.value - if (current == null || current.first != fileSource || !player.status().isPlayable) { + if (current == null || current.fileSource != fileSource || !player.status().isPlayable || smallView != current.smallView) { stopListener() player.stop() runCatching { @@ -66,7 +61,7 @@ actual object AudioPlayer: AudioPlayerInterface { } if (seek != null) player.seekTo(seek) player.start() - currentlyPlaying.value = fileSource to onProgressUpdate + currentlyPlaying.value = CurrentlyPlayingState(fileSource, onProgressUpdate, smallView) progressJob = CoroutineScope(Dispatchers.Default).launch { onProgressUpdate(player.currentPosition, TrackState.PLAYING) while(isActive && (player.isPlaying || player.status().state() == State.OPENING)) { @@ -80,7 +75,11 @@ actual object AudioPlayer: AudioPlayerInterface { onProgressUpdate(player.currentPosition, TrackState.PLAYING) } onProgressUpdate(null, TrackState.PAUSED) - currentlyPlaying.value?.first?.deleteTmpFile() + currentlyPlaying.value?.fileSource?.deleteTmpFile() + // Since coroutine is still NOT canceled, means player ended (no stop/no pause). + if (smallView && isActive) { + stopListener() + } } return player.duration } @@ -103,7 +102,7 @@ actual object AudioPlayer: AudioPlayerInterface { // FileName or filePath are ok override fun stop(fileName: String?) { - if (fileName != null && currentlyPlaying.value?.first?.filePath?.endsWith(fileName) == true) { + if (fileName != null && currentlyPlaying.value?.fileSource?.filePath?.endsWith(fileName) == true) { stop() } } @@ -111,8 +110,8 @@ actual object AudioPlayer: AudioPlayerInterface { private fun stopListener() { val afterCoroutineCancel: CompletionHandler = { // Notify prev audio listener about stop - currentlyPlaying.value?.second?.invoke(null, TrackState.REPLACED) - currentlyPlaying.value?.first?.deleteTmpFile() + currentlyPlaying.value?.onProgressUpdate?.invoke(null, TrackState.REPLACED) + currentlyPlaying.value?.fileSource?.deleteTmpFile() currentlyPlaying.value = null } /** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be: @@ -133,11 +132,12 @@ actual object AudioPlayer: AudioPlayerInterface { progress: MutableState, duration: MutableState, resetOnEnd: Boolean, + smallView: Boolean, ) { if (progress.value == duration.value) { progress.value = 0 } - val realDuration = start(fileSource, progress.value) { pro, state -> + val realDuration = start(fileSource, smallView = smallView, progress.value) { pro, state -> if (pro != null) { progress.value = pro } @@ -162,7 +162,7 @@ actual object AudioPlayer: AudioPlayerInterface { override fun seekTo(ms: Int, pro: MutableState, filePath: String?) { pro.value = ms - if (currentlyPlaying.value?.first?.filePath == filePath) { + if (currentlyPlaying.value?.fileSource?.filePath == filePath) { player.seekTo(ms) } } @@ -217,7 +217,7 @@ actual object SoundPlayer: SoundPlayerInterface { playing = true scope.launch { while (playing && sound) { - AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), true) + AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), resetOnEnd = true, smallView = false) delay(3500) } } @@ -239,7 +239,7 @@ actual object CallSoundsPlayer: CallSoundsPlayerInterface { SoundPlayer::class.java.getResource(soundPath)!!.openStream()!!.use { it.copyTo(tmpFile.outputStream()) } playingJob = scope.launch { while (isActive) { - AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), true) + AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), resetOnEnd = true, smallView = false) delay(delay) } } diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 2923c39742..35bc671ccf 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -26,11 +26,11 @@ android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.0-beta.0 -android.version_code=225 +android.version_name=6.0-beta.1 +android.version_code=226 -desktop.version_name=6.0-beta.0 -desktop.version_code=56 +desktop.version_name=6.0-beta.1 +desktop.version_code=57 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 diff --git a/cabal.project b/cabal.project index e7e30afb3c..5767cbfa97 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: b40d55c358ebabeb6cf1eca8e64016e2bae09a15 + tag: 2de16cfae89661605b468df71eff8b8e8188ef86 source-repository-package type: git diff --git a/docs/TRANSPARENCY.md b/docs/TRANSPARENCY.md index 547660861b..43fdd12ac5 100644 --- a/docs/TRANSPARENCY.md +++ b/docs/TRANSPARENCY.md @@ -1,12 +1,12 @@ --- title: Transparency Reports permalink: /transparency/index.html -revision: 26.04.2024 +revision: 16.07.2024 --- # Transparency Reports -**Updated**: Apr 26, 2024 +**Updated**: Jul 16, 2024 SimpleX Chat Ltd. is a company registered in the UK – it develops communication software enabling users to operate and communicate via SimpleX network, without user profile identifiers of any kind, and without having their data hosted by any network infrastructure operators. diff --git a/flake.nix b/flake.nix index 24a5062be3..64b624e674 100644 --- a/flake.nix +++ b/flake.nix @@ -309,7 +309,7 @@ packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ - (pkgs.openssl.override { static = true; }) + ((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;})) ]; }]; }).simplex-chat.components.library.override ( diff --git a/package.yaml b/package.yaml index 1c9b372b20..02db02ece4 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 6.0.0.1 +version: 6.0.0.2 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/desktop/prepare-openssl-windows.sh b/scripts/desktop/prepare-openssl-windows.sh index 942646853f..d65d4b8e31 100644 --- a/scripts/desktop/prepare-openssl-windows.sh +++ b/scripts/desktop/prepare-openssl-windows.sh @@ -12,7 +12,7 @@ cd $root_dir if [ ! -f dist-newstyle/openssl-1.1.1w/libcrypto-1_1-x64.dll ]; then mkdir dist-newstyle 2>/dev/null || true cd dist-newstyle - curl --tlsv1.2 https://www.openssl.org/source/openssl-1.1.1w.tar.gz -o openssl.tar.gz + curl --tlsv1.2 https://www.openssl.org/source/openssl-1.1.1w.tar.gz -L -o openssl.tar.gz $WINDIR\\System32\\tar.exe -xvzf openssl.tar.gz cd openssl-1.1.1w ./Configure mingw64 diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 6eaf36e709..ceae107df5 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."b40d55c358ebabeb6cf1eca8e64016e2bae09a15" = "1ppn2yvml12yr5k5d6hjn7r7xy6a83ig9mys49jzzk034rzwxcd2"; + "https://github.com/simplex-chat/simplexmq.git"."2de16cfae89661605b468df71eff8b8e8188ef86" = "00bgpy3gygqhmcbb2r5i8kryc5vn667bdg5s3xl3lf7y9m13g047"; "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"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index a05608c5ad..9346e7fc34 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.0.0.1 +version: 6.0.0.2 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index d31c2cdeca..bcf6856c4f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -384,8 +384,9 @@ cfgServers p DefaultAgentServers {smp, xftp} = case p of SPSMP -> smp SPXFTP -> xftp -startChatController :: Bool -> CM' (Async ()) -startChatController mainApp = do +-- enableSndFiles has no effect when mainApp is True +startChatController :: Bool -> Bool -> CM' (Async ()) +startChatController mainApp enableSndFiles = do asks smpAgent >>= liftIO . resumeAgentClient unless mainApp $ chatWriteVar' subscriptionMode SMOnlyCreate users <- fromRight [] <$> runExceptT (withStore' getUsers) @@ -400,15 +401,18 @@ startChatController mainApp = do then Just <$> async (subscribeUsers False users) else pure Nothing atomically . writeTVar s $ Just (a1, a2) - when mainApp $ do - startXFTP - void $ forkIO $ startFilesToReceive users - startCleanupManager - startExpireCIs users + if mainApp + then do + startXFTP xftpStartWorkers + void $ forkIO $ startFilesToReceive users + startCleanupManager + startExpireCIs users + else + when enableSndFiles $ startXFTP xftpStartSndWorkers pure a1 - startXFTP = do + startXFTP startWorkers = do tmp <- readTVarIO =<< asks tempDirectory - runExceptT (withAgent $ \a -> xftpStartWorkers a tmp) >>= \case + runExceptT (withAgent $ \a -> startWorkers a tmp) >>= \case Left e -> liftIO $ print $ "Error starting XFTP workers: " <> show e Right _ -> pure () startCleanupManager = do @@ -617,10 +621,10 @@ processChatCommand' vr = \case checkDeleteChatUser user' withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_ - StartChat mainApp -> withUser' $ \_ -> + StartChat {mainApp, enableSndFiles} -> withUser' $ \_ -> asks agentAsync >>= readTVarIO >>= \case Just _ -> pure CRChatRunning - _ -> checkStoreNotChanged . lift $ startChatController mainApp $> CRChatStarted + _ -> checkStoreNotChanged . lift $ startChatController mainApp enableSndFiles $> CRChatStarted APIStopChat -> do ask >>= liftIO . stopChatController pure CRChatStopped @@ -1307,14 +1311,14 @@ processChatCommand' vr = \case APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token nonce code) >> ok_ APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_ APIGetNtfMessage nonce encNtfInfo -> withUser $ \_ -> do - (NotificationInfo {ntfConnId, ntfMsgMeta}, msgs) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo + (NotificationInfo {ntfConnId, ntfMsgMeta}, msg) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo let msgTs' = systemToUTCTime . (\SMP.NMsgMeta {msgTs} -> msgTs) <$> ntfMsgMeta agentConnId = AgentConnId ntfConnId user_ <- withStore' (`getUserByAConnId` agentConnId) connEntity_ <- pure user_ $>>= \user -> withStore (\db -> Just <$> getConnectionEntity db vr user agentConnId) `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing) - pure CRNtfMessages {user_, connEntity_, msgTs = msgTs', ntfMessages = map ntfMsgInfo msgs} + pure CRNtfMessages {user_, connEntity_, msgTs = msgTs', ntfMessage_ = ntfMsgInfo <$> msg} APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do cfg@ChatConfig {defaultServers} <- asks config servers <- withStore' (`getProtocolServers` user) @@ -2258,12 +2262,17 @@ processChatCommand' vr = \case CLUserContact ucId -> "UserContact " <> show ucId CLFile fId -> "File " <> show fId DebugEvent event -> toView event >> ok_ + GetAgentSubsTotal userId -> withUserId userId $ \user -> do + users <- withStore' $ \db -> getUsers db + let userIds = map aUserId $ filter (\u -> isNothing (viewPwdHash u) || aUserId u == aUserId user) users + (subsTotal, hasSession) <- lift $ withAgent' $ \a -> getAgentSubsTotal a userIds + pure $ CRAgentSubsTotal user subsTotal hasSession GetAgentServersSummary userId -> withUserId userId $ \user -> do agentServersSummary <- lift $ withAgent' getAgentServersSummary cfg <- asks config (users, smpServers, xftpServers) <- withStore' $ \db -> (,,) <$> getUsers db <*> getServers db cfg user SPSMP <*> getServers db cfg user SPXFTP - let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers + let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers pure $ CRAgentServersSummary user presentedServersSummary where getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> ChatConfig -> User -> SProtocolType p -> IO (NonEmpty (ProtocolServer p)) @@ -3319,8 +3328,9 @@ acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId inv chatV = vr `peerConnChatVersion` cReqChatVRange pqSup' = pqSup `CR.pqSupportAnd` pqSupport dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - acId <- withAgent $ \a -> acceptContact a True invId dm pqSup' subMode - withStore' $ \db -> createAcceptedContact db user acId chatV cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode pqSup' contactUsed + (acId, sqSecured) <- withAgent $ \a -> acceptContact a True invId dm pqSup' subMode + let connStatus = if sqSecured then ConnSndReady else ConnNew + withStore' $ \db -> createAcceptedContact db user acId connStatus chatV cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode pqSup' contactUsed acceptContactRequestAsync :: User -> UserContactRequest -> Maybe IncognitoProfile -> Bool -> PQSupport -> CM Contact acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile contactUsed pqSup = do @@ -3330,7 +3340,7 @@ acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvI let chatV = vr `peerConnChatVersion` cReqChatVRange (cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode pqSup chatV withStore' $ \db -> do - ct@Contact {activeConn} <- createAcceptedContact db user acId chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed + ct@Contact {activeConn} <- createAcceptedContact db user acId ConnNew chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed forM_ activeConn $ \Connection {connId} -> setCommandConnId db user cmdId connId pure ct @@ -3993,6 +4003,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = agentMsgConnStatus :: AEvent e -> Maybe ConnStatus agentMsgConnStatus = \case + JOINED True -> Just ConnSndReady CONF {} -> Just ConnRequested INFO {} -> Just ConnSndReady CON _ -> Just ConnReady @@ -4041,6 +4052,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () QCONT -> void $ continueSending connEntity conn MWARN _ err -> @@ -4160,12 +4174,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> resetContactConnInitiated db user conn' forM_ viaUserContactLink $ \userContactLinkId -> do ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let (UserContactLink {autoAccept}, groupId_, gLinkMemRole) = ucl - forM_ autoAccept $ \(AutoAccept {autoReply = mc_}) -> - forM_ mc_ $ \mc -> do - (msg, _) <- sendDirectContactMessage user ct' (XMsgNew $ MCSimple (extMsgContent mc Nothing)) - ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct') ci) + let (_, groupId_, gLinkMemRole) = ucl forM_ groupId_ $ \groupId -> do groupInfo <- withStore $ \db -> getGroupInfo db vr user groupId subMode <- chatReadVar subscriptionMode @@ -4219,6 +4228,20 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED sqSecured -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> + when (directOrUsed ct && sqSecured) $ do + lift $ setContactNetworkStatus ct NSConnected + toView $ CRContactSndReady user ct + forM_ viaUserContactLink $ \userContactLinkId -> do + ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId + let (UserContactLink {autoAccept}, _, _) = ucl + forM_ autoAccept $ \(AutoAccept {autoReply = mc_}) -> + forM_ mc_ $ \mc -> do + (msg, _) <- sendDirectContactMessage user ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) + ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) + toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) QCONT -> void $ continueSending connEntity conn MWARN msgId err -> do @@ -4616,6 +4639,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () QCONT -> do continued <- continueSending connEntity conn when continued $ sendPendingGroupMessages user m conn @@ -4708,6 +4734,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () ERR err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -4754,6 +4783,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () MERR _ err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) processConnMERR connEntity conn err @@ -7355,8 +7387,11 @@ chatCommandP = "/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)), "/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)), ("/user" <|> "/u") $> ShowActiveUser, - "/_start main=" *> (StartChat <$> onOffP), - "/_start" $> StartChat True, + "/_start " *> do + mainApp <- "main=" *> onOffP + enableSndFiles <- " snd_files=" *> onOffP <|> pure mainApp + pure StartChat {mainApp, enableSndFiles}, + "/_start" $> StartChat True True, "/_stop" $> APIStopChat, "/_app activate restore=" *> (APIActivateChat <$> onOffP), "/_app activate" $> APIActivateChat True, @@ -7642,6 +7677,7 @@ chatCommandP = ("/version" <|> "/v") $> ShowVersion, "/debug locks" $> DebugLocks, "/debug event " *> (DebugEvent <$> jsonP), + "/get subs total " *> (GetAgentSubsTotal <$> A.decimal), "/get servers summary " *> (GetAgentServersSummary <$> A.decimal), "/reset servers stats" $> ResetAgentServersStats, "/get subs" $> GetAgentSubs, diff --git a/src/Simplex/Chat/AppSettings.hs b/src/Simplex/Chat/AppSettings.hs index 2b8b531dc3..6d23a19ba1 100644 --- a/src/Simplex/Chat/AppSettings.hs +++ b/src/Simplex/Chat/AppSettings.hs @@ -35,6 +35,7 @@ data AppSettings = AppSettings privacyShowChatPreviews :: Maybe Bool, privacySaveLastDraft :: Maybe Bool, privacyProtectScreen :: Maybe Bool, + privacyMediaBlurRadius :: Maybe Int, notificationMode :: Maybe NotificationMode, notificationPreviewMode :: Maybe NotificationPreviewMode, webrtcPolicyRelay :: Maybe Bool, @@ -68,6 +69,7 @@ defaultAppSettings = privacyShowChatPreviews = Just True, privacySaveLastDraft = Just True, privacyProtectScreen = Just False, + privacyMediaBlurRadius = Just 0, notificationMode = Just NMInstant, notificationPreviewMode = Just NPMMessage, webrtcPolicyRelay = Just True, @@ -100,6 +102,7 @@ defaultParseAppSettings = privacyShowChatPreviews = Nothing, privacySaveLastDraft = Nothing, privacyProtectScreen = Nothing, + privacyMediaBlurRadius = Nothing, notificationMode = Nothing, notificationPreviewMode = Nothing, webrtcPolicyRelay = Nothing, @@ -132,6 +135,7 @@ combineAppSettings platformDefaults storedSettings = privacyShowChatPreviews = p privacyShowChatPreviews, privacySaveLastDraft = p privacySaveLastDraft, privacyProtectScreen = p privacyProtectScreen, + privacyMediaBlurRadius = p privacyMediaBlurRadius, notificationMode = p notificationMode, notificationPreviewMode = p notificationPreviewMode, webrtcPolicyRelay = p webrtcPolicyRelay, @@ -176,6 +180,7 @@ instance FromJSON AppSettings where privacyShowChatPreviews <- p "privacyShowChatPreviews" privacySaveLastDraft <- p "privacySaveLastDraft" privacyProtectScreen <- p "privacyProtectScreen" + privacyMediaBlurRadius <- p "privacyMediaBlurRadius" notificationMode <- p "notificationMode" notificationPreviewMode <- p "notificationPreviewMode" webrtcPolicyRelay <- p "webrtcPolicyRelay" @@ -205,6 +210,7 @@ instance FromJSON AppSettings where privacyShowChatPreviews, privacySaveLastDraft, privacyProtectScreen, + privacyMediaBlurRadius, notificationMode, notificationPreviewMode, webrtcPolicyRelay, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index e08c8a287a..66a4aca95d 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -69,7 +69,7 @@ import Simplex.Chat.Types.UITheme import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) -import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, ServerQueueInfo, UserNetworkInfo) +import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, ServerQueueInfo, SMPServerSubs, UserNetworkInfo) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol @@ -263,7 +263,7 @@ data ChatCommand | UnmuteUser | APIDeleteUser UserId Bool (Maybe UserPwd) | DeleteUser UserName Bool (Maybe UserPwd) - | StartChat {mainApp :: Bool} + | StartChat {mainApp :: Bool, enableSndFiles :: Bool} -- enableSndFiles has no effect when mainApp is True | APIStopChat | APIActivateChat {restoreChat :: Bool} | APISuspendChat {suspendTimeout :: Int} @@ -505,6 +505,7 @@ data ChatCommand | ShowVersion | DebugLocks | DebugEvent ChatResponse + | GetAgentSubsTotal UserId | GetAgentServersSummary UserId | ResetAgentServersStats | GetAgentSubs @@ -675,6 +676,7 @@ data ChatResponse | CRContactPrefsUpdated {user :: User, fromContact :: Contact, toContact :: Contact} | CRContactConnecting {user :: User, contact :: Contact} | CRContactConnected {user :: User, contact :: Contact, userCustomProfile :: Maybe Profile} + | CRContactSndReady {user :: User, contact :: Contact} | CRContactAnotherClient {user :: User, contact :: Contact} | CRSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity} | CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]} @@ -734,7 +736,7 @@ data ChatResponse | CRUserContactLinkSubError {chatError :: ChatError} -- TODO delete | CRNtfTokenStatus {status :: NtfTknStatus} | CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer} - | CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]} + | CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessage_ :: Maybe NtfMsgInfo} | CRNtfMessage {user :: User, connEntity :: ConnectionEntity, ntfMessage :: NtfMsgInfo} | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRRemoteHostList {remoteHosts :: [RemoteHostInfo]} @@ -755,6 +757,7 @@ data ChatResponse | CRSQLResult {rows :: [Text]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} | CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks} + | CRAgentSubsTotal {user :: User, subsTotal :: SMPServerSubs, hasSession :: Bool} | CRAgentServersSummary {user :: User, serversSummary :: PresentedServersSummary} | CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails} | CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary} diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index a8580746d1..07fac82677 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -54,7 +54,7 @@ runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController runSimplexChat ChatOpts {maintenance} u cc chat | maintenance = wait =<< async (chat u cc) | otherwise = do - a1 <- runReaderT (startChatController True) cc + a1 <- runReaderT (startChatController True True) cc a2 <- async $ chat u cc waitEither_ a1 a2 diff --git a/src/Simplex/Chat/Stats.hs b/src/Simplex/Chat/Stats.hs index e1d0372080..6dd5c79ab1 100644 --- a/src/Simplex/Chat/Stats.hs +++ b/src/Simplex/Chat/Stats.hs @@ -6,6 +6,7 @@ module Simplex.Chat.Stats where import qualified Data.Aeson.TH as J +import Data.List (partition) import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M @@ -22,8 +23,10 @@ data PresentedServersSummary = PresentedServersSummary { statsStartedAt :: UTCTime, allUsersSMP :: SMPServersSummary, allUsersXFTP :: XFTPServersSummary, + allUsersNtf :: NtfServersSummary, currentUserSMP :: SMPServersSummary, - currentUserXFTP :: XFTPServersSummary + currentUserXFTP :: XFTPServersSummary, + currentUserNtf :: NtfServersSummary } deriving (Show) @@ -102,52 +105,86 @@ data XFTPServerSummary = XFTPServerSummary } deriving (Show) +data NtfServersSummary = NtfServersSummary + { ntfTotals :: NtfTotals, + currentlyUsedNtfServers :: [NtfServerSummary], + previouslyUsedNtfServers :: [NtfServerSummary] + } + deriving (Show) + +data NtfTotals = NtfTotals + { sessions :: ServerSessions, + stats :: AgentNtfServerStatsData + } + deriving (Show) + +data NtfServerSummary = NtfServerSummary + { ntfServer :: NtfServer, + known :: Maybe Bool, + sessions :: Maybe ServerSessions, + stats :: Maybe AgentNtfServerStatsData + } + deriving (Show) + -- Maps AgentServersSummary to PresentedServersSummary: -- - currentUserServers is for currentUser; -- - users are passed to exclude hidden users from totalServersSummary; -- - if currentUser is hidden, it should be accounted in totalServersSummary; -- - known is set only in user level summaries based on passed userSMPSrvs and userXFTPSrvs -toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> NonEmpty SMPServer -> NonEmpty XFTPServer -> PresentedServersSummary -toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs = do +toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> NonEmpty SMPServer -> NonEmpty XFTPServer -> [NtfServer] -> PresentedServersSummary +toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs userNtfSrvs = do let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries - (userSMPTotals, allSMPTotals) = (accSMPTotals userSMPSrvsSumms, accSMPTotals allSMPSrvsSumms) (userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms (allSMPCurr, allSMPPrev, allSMPProx) = smpSummsIntoCategories allSMPSrvsSumms - (userXFTPSrvsSumms, allXFTPSrvsSumms) = accXFTPSrvsSummaries - (userXFTPTotals, allXFTPTotals) = (accXFTPTotals userXFTPSrvsSumms, accXFTPTotals allXFTPSrvsSumms) + let (userXFTPSrvsSumms, allXFTPSrvsSumms) = accXFTPSrvsSummaries (userXFTPCurr, userXFTPPrev) = xftpSummsIntoCategories userXFTPSrvsSumms (allXFTPCurr, allXFTPPrev) = xftpSummsIntoCategories allXFTPSrvsSumms + let (userNtfSrvsSumms, allNtfSrvsSumms) = accNtfSrvsSummaries + (userNtfCurr, userNtfPrev) = ntfSummsIntoCategories userNtfSrvsSumms + (allNtfCurr, allNtfPrev) = ntfSummsIntoCategories allNtfSrvsSumms PresentedServersSummary { statsStartedAt, allUsersSMP = SMPServersSummary - { smpTotals = allSMPTotals, + { smpTotals = accSMPTotals allSMPSrvsSumms, currentlyUsedSMPServers = allSMPCurr, previouslyUsedSMPServers = allSMPPrev, onlyProxiedSMPServers = allSMPProx }, allUsersXFTP = XFTPServersSummary - { xftpTotals = allXFTPTotals, + { xftpTotals = accXFTPTotals allXFTPSrvsSumms, currentlyUsedXFTPServers = allXFTPCurr, previouslyUsedXFTPServers = allXFTPPrev }, + allUsersNtf = + NtfServersSummary + { ntfTotals = accNtfTotals allNtfSrvsSumms, + currentlyUsedNtfServers = allNtfCurr, + previouslyUsedNtfServers = allNtfPrev + }, currentUserSMP = SMPServersSummary - { smpTotals = userSMPTotals, + { smpTotals = accSMPTotals userSMPSrvsSumms, currentlyUsedSMPServers = userSMPCurr, previouslyUsedSMPServers = userSMPPrev, onlyProxiedSMPServers = userSMPProx }, currentUserXFTP = XFTPServersSummary - { xftpTotals = userXFTPTotals, + { xftpTotals = accXFTPTotals userXFTPSrvsSumms, currentlyUsedXFTPServers = userXFTPCurr, previouslyUsedXFTPServers = userXFTPPrev + }, + currentUserNtf = + NtfServersSummary + { ntfTotals = accNtfTotals userNtfSrvsSumms, + currentlyUsedNtfServers = userNtfCurr, + previouslyUsedNtfServers = userNtfPrev } } where - AgentServersSummary {statsStartedAt, smpServersSessions, smpServersSubs, smpServersStats, xftpServersSessions, xftpServersStats, xftpRcvInProgress, xftpSndInProgress, xftpDelInProgress} = agentSummary + AgentServersSummary {statsStartedAt, smpServersSessions, smpServersSubs, smpServersStats, xftpServersSessions, xftpServersStats, xftpRcvInProgress, xftpSndInProgress, xftpDelInProgress, ntfServersSessions, ntfServersStats} = agentSummary countUserInAll auId = countUserInAllStats (AgentUserId auId) currentUser users accSMPTotals :: Map SMPServer SMPServerSummary -> SMPTotals accSMPTotals = M.foldr' addTotals initialTotals @@ -168,10 +205,19 @@ toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrv { sessions = maybe accSess (accSess `addServerSessions`) sessions, stats = maybe accStats (accStats `addXFTPStatsData`) stats } - smpSummsIntoCategories :: Map SMPServer SMPServerSummary -> ([SMPServerSummary], [SMPServerSummary], [SMPServerSummary]) - smpSummsIntoCategories = M.foldr' partitionSummary ([], [], []) + accNtfTotals :: Map NtfServer NtfServerSummary -> NtfTotals + accNtfTotals = M.foldr' addTotals initialTotals where - partitionSummary srvSumm (curr, prev, prox) + initialTotals = NtfTotals {sessions = ServerSessions 0 0 0, stats = newAgentNtfServerStatsData} + addTotals NtfServerSummary {sessions, stats} NtfTotals {sessions = accSess, stats = accStats} = + NtfTotals + { sessions = maybe accSess (accSess `addServerSessions`) sessions, + stats = maybe accStats (accStats `addNtfStatsData`) stats + } + smpSummsIntoCategories :: Map SMPServer SMPServerSummary -> ([SMPServerSummary], [SMPServerSummary], [SMPServerSummary]) + smpSummsIntoCategories = M.foldr' addSummary ([], [], []) + where + addSummary srvSumm (curr, prev, prox) | isCurrentlyUsed srvSumm = (srvSumm : curr, prev, prox) | isPreviouslyUsed srvSumm = (curr, srvSumm : prev, prox) | otherwise = (curr, prev, srvSumm : prox) @@ -183,42 +229,29 @@ toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrv Just AgentSMPServerStatsData {_sentDirect, _sentProxied, _sentDirectAttempts, _sentProxiedAttempts, _recvMsgs, _connCreated, _connSecured, _connSubscribed, _connSubAttempts} -> _sentDirect > 0 || _sentProxied > 0 || _sentDirectAttempts > 0 || _sentProxiedAttempts > 0 || _recvMsgs > 0 || _connCreated > 0 || _connSecured > 0 || _connSubscribed > 0 || _connSubAttempts > 0 xftpSummsIntoCategories :: Map XFTPServer XFTPServerSummary -> ([XFTPServerSummary], [XFTPServerSummary]) - xftpSummsIntoCategories = M.foldr' partitionSummary ([], []) + xftpSummsIntoCategories = partition isCurrentlyUsed . M.elems where - partitionSummary srvSumm (curr, prev) - | isCurrentlyUsed srvSumm = (srvSumm : curr, prev) - | otherwise = (curr, srvSumm : prev) isCurrentlyUsed XFTPServerSummary {sessions, rcvInProgress, sndInProgress, delInProgress} = isJust sessions || rcvInProgress || sndInProgress || delInProgress + ntfSummsIntoCategories :: Map NtfServer NtfServerSummary -> ([NtfServerSummary], [NtfServerSummary]) + ntfSummsIntoCategories = partition isCurrentlyUsed . M.elems + where + isCurrentlyUsed NtfServerSummary {sessions} = isJust sessions accSMPSrvsSummaries :: (Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary) accSMPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs2 smpServersStats where summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) smpServersSessions summs2 = M.foldrWithKey' (addServerData addSubs) summs1 smpServersSubs - addServerData :: - (a -> SMPServerSummary -> SMPServerSummary) -> - (UserId, SMPServer) -> - a -> - (Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary) -> - (Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary) - addServerData addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms') - where - userSumms' - | userId == aUserId currentUser = alterSumms newUserSummary userSumms - | otherwise = userSumms - allUsersSumms' - | countUserInAll userId = alterSumms newSummary allUsersSumms - | otherwise = allUsersSumms - alterSumms n = M.alter (Just . addData d . fromMaybe n) srv - newUserSummary = (newSummary :: SMPServerSummary) {known = Just $ srv `elem` userSMPSrvs} - newSummary = - SMPServerSummary - { smpServer = srv, - known = Nothing, - sessions = Nothing, - subs = Nothing, - stats = Nothing - } + addServerData = addServerData_ newSummary newUserSummary + newUserSummary srv = (newSummary srv :: SMPServerSummary) {known = Just $ srv `elem` userSMPSrvs} + newSummary srv = + SMPServerSummary + { smpServer = srv, + known = Nothing, + sessions = Nothing, + subs = Nothing, + stats = Nothing + } addSessions :: ServerSessions -> SMPServerSummary -> SMPServerSummary addSessions s summ@SMPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions} addSubs :: SMPServerSubs -> SMPServerSummary -> SMPServerSummary @@ -229,36 +262,56 @@ toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrv accXFTPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs1 xftpServersStats where summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) xftpServersSessions - addServerData :: - (a -> XFTPServerSummary -> XFTPServerSummary) -> - (UserId, XFTPServer) -> - a -> - (Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary) -> - (Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary) - addServerData addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms') - where - userSumms' - | userId == aUserId currentUser = alterSumms newUserSummary userSumms - | otherwise = userSumms - allUsersSumms' - | countUserInAll userId = alterSumms newSummary allUsersSumms - | otherwise = allUsersSumms - alterSumms n = M.alter (Just . addData d . fromMaybe n) srv - newUserSummary = (newSummary :: XFTPServerSummary) {known = Just $ srv `elem` userXFTPSrvs} - newSummary = - XFTPServerSummary - { xftpServer = srv, - known = Nothing, - sessions = Nothing, - stats = Nothing, - rcvInProgress = srv `elem` xftpRcvInProgress, - sndInProgress = srv `elem` xftpSndInProgress, - delInProgress = srv `elem` xftpDelInProgress - } + addServerData = addServerData_ newSummary newUserSummary addSessions :: ServerSessions -> XFTPServerSummary -> XFTPServerSummary addSessions s summ@XFTPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions} addStats :: AgentXFTPServerStatsData -> XFTPServerSummary -> XFTPServerSummary addStats s summ@XFTPServerSummary {stats} = summ {stats = Just $ maybe s (s `addXFTPStatsData`) stats} + newUserSummary srv = (newSummary srv :: XFTPServerSummary) {known = Just $ srv `elem` userXFTPSrvs} + newSummary srv = + XFTPServerSummary + { xftpServer = srv, + known = Nothing, + sessions = Nothing, + stats = Nothing, + rcvInProgress = srv `elem` xftpRcvInProgress, + sndInProgress = srv `elem` xftpSndInProgress, + delInProgress = srv `elem` xftpDelInProgress + } + accNtfSrvsSummaries :: (Map NtfServer NtfServerSummary, Map NtfServer NtfServerSummary) + accNtfSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs1 ntfServersStats + where + summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) ntfServersSessions + addServerData = addServerData_ newSummary newUserSummary + addSessions :: ServerSessions -> NtfServerSummary -> NtfServerSummary + addSessions s summ@NtfServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions} + addStats :: AgentNtfServerStatsData -> NtfServerSummary -> NtfServerSummary + addStats s summ@NtfServerSummary {stats} = summ {stats = Just $ maybe s (s `addNtfStatsData`) stats} + newUserSummary srv = (newSummary srv :: NtfServerSummary) {known = Just $ srv `elem` userNtfSrvs} + newSummary srv = + NtfServerSummary + { ntfServer = srv, + known = Nothing, + sessions = Nothing, + stats = Nothing + } + addServerData_ :: + (ProtocolServer p -> s) -> + (ProtocolServer p -> s) -> + (a -> s -> s) -> + (UserId, ProtocolServer p) -> + a -> + (Map (ProtocolServer p) s, Map (ProtocolServer p) s) -> + (Map (ProtocolServer p) s, Map (ProtocolServer p) s) + addServerData_ newSummary newUserSummary addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms') + where + userSumms' + | userId == aUserId currentUser = alterSumms (newUserSummary srv) userSumms + | otherwise = userSumms + allUsersSumms' + | countUserInAll userId = alterSumms (newSummary srv) allUsersSumms + | otherwise = allUsersSumms + alterSumms n = M.alter (Just . addData d . fromMaybe n) srv addServerSessions :: ServerSessions -> ServerSessions -> ServerSessions addServerSessions ss1 ss2 = ServerSessions @@ -292,4 +345,10 @@ $(J.deriveJSON defaultJSON ''XFTPServerSummary) $(J.deriveJSON defaultJSON ''XFTPServersSummary) +$(J.deriveJSON defaultJSON ''NtfTotals) + +$(J.deriveJSON defaultJSON ''NtfServerSummary) + +$(J.deriveJSON defaultJSON ''NtfServersSummary) + $(J.deriveJSON defaultJSON ''PresentedServersSummary) diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 1145c2494b..508a992543 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -752,8 +752,8 @@ deleteContactRequest db User {userId} contactRequestId = do (userId, userId, contactRequestId, userId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO Contact -createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do +createAcceptedContact :: DB.Connection -> User -> ConnId -> ConnStatus -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO Contact +createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connStatus connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) createdAt <- getCurrentTime customUserProfileId <- forM incognitoProfile $ \case @@ -765,7 +765,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences} "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?)" (userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId, contactUsed) contactId <- insertedRowId db - conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup + conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connStatus connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn pure $ Contact diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index d70bbb8970..d1da081cee 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -455,7 +455,7 @@ lookupChatRefByFileId db User {userId} fileId = createSndFileConnection_ :: DB.Connection -> VersionRangeChat -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection createSndFileConnection_ db vr userId fileId agentConnId subMode = do currentTs <- getCurrentTime - createConnection_ db userId ConnSndFile (Just fileId) agentConnId (minVersion vr) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff + createConnection_ db userId ConnSndFile (Just fileId) agentConnId ConnNew (minVersion vr) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO () updateSndFileStatus db SndFileTransfer {fileId, connId} status = do diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 42637d4169..55847114ca 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -191,7 +191,7 @@ createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} "INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" (userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff getGroupLinkConnection :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ExceptT StoreError IO Connection getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} = @@ -914,7 +914,7 @@ createAcceptedMemberConnection groupMemberId subMode = do createdAt <- liftIO getCurrentTime - Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff + Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff setCommandConnId db user cmdId connId getContactViaMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> ExceptT StoreError IO Contact @@ -1250,7 +1250,7 @@ createIntroReMember currentTs <- liftIO getCurrentTime newMember <- case directConnIds of Just (directCmdId, directAgentConnId) -> do - Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId chatV mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff + Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId ConnNew chatV mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff liftIO $ setCommandConnId db user directCmdId directConnId (localDisplayName, contactId, memProfileId) <- createContact_ db userId memberProfile "" (Just groupId) currentTs False liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, directConnId) @@ -1271,7 +1271,7 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId chatV mcvr viaContactId cLevel currentTs subMode setCommandConnId db user groupCmdId groupConnId forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do - Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId chatV mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff + Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId ConnNew chatV mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff setCommandConnId db user directCmdId directConnId contactId <- createMemberContact_ directConnId currentTs updateMember_ contactId currentTs @@ -1303,7 +1303,7 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionChat -> VersionRangeChat -> Maybe Int64 -> Int -> UTCTime -> SubscriptionMode -> IO Connection createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange viaContact connLevel currentTs subMode = - createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff + createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff getViaGroupMember :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index cad06448e6..fb87662c27 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -328,7 +328,7 @@ createUserContactLink db User {userId} agentConnId cReq subMode = "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" (userId, cReq, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff getUserAddressConnections :: DB.Connection -> VersionRangeChat -> User -> ExceptT StoreError IO [Connection] getUserAddressConnections db vr User {userId} = do diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index c364cf10c2..b80e2ce805 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -208,8 +208,8 @@ toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, quotaErrCounter, connChatVersion, minVer, maxVer)) toMaybeConnection _ _ = Nothing -createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionChat -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection -createConnection_ db userId connType entityId acId connChatVersion peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode pqSup = do +createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> ConnStatus -> VersionChat -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection +createConnection_ db userId connType entityId acId connStatus connChatVersion peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode pqSup = do viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId -> maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId) let viaGroupLink = isJust viaLinkGroupId @@ -222,7 +222,7 @@ createConnection_ db userId connType entityId acId connChatVersion peerChatVRang conn_chat_version, peer_chat_min_version, peer_chat_max_version, to_subscribe, pq_support, pq_encryption ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType) + ( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, connStatus, connType) :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) :. (connChatVersion, minV, maxV, subMode == SMOnlyCreate, pqSup, pqSup) ) @@ -242,7 +242,7 @@ createConnection_ db userId connType entityId acId connChatVersion peerChatVRang groupLinkId = Nothing, customUserProfileId, connLevel, - connStatus = ConnNew, + connStatus, localAlias = "", createdAt = currentTs, connectionCode = Nothing, diff --git a/src/Simplex/Chat/Terminal/Main.hs b/src/Simplex/Chat/Terminal/Main.hs index 5d684d7283..a946ba3483 100644 --- a/src/Simplex/Chat/Terminal/Main.hs +++ b/src/Simplex/Chat/Terminal/Main.hs @@ -23,20 +23,24 @@ import System.Terminal (withTerminal) simplexChatCLI :: ChatConfig -> Maybe (ServiceName -> ChatConfig -> ChatOpts -> IO ()) -> IO () simplexChatCLI cfg server_ = do appDir <- getAppUserDataDirectory "simplex" - opts@ChatOpts {chatCmd, chatServerPort} <- getChatOpts appDir "simplex_v1" + opts <- getChatOpts appDir "simplex_v1" + simplexChatCLI' cfg opts server_ + +simplexChatCLI' :: ChatConfig -> ChatOpts -> Maybe (ServiceName -> ChatConfig -> ChatOpts -> IO ()) -> IO () +simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServerPort} server_ = do if null chatCmd then case chatServerPort of Just chatPort -> case server_ of Just server -> server chatPort cfg opts Nothing -> putStrLn "Not allowed to run as a WebSockets server" >> exitFailure - _ -> runCLI opts - else simplexChatCore cfg opts $ runCommand opts + _ -> runCLI + else simplexChatCore cfg opts runCommand where - runCLI opts = do + runCLI = do welcome cfg opts t <- withTerminal pure simplexChatTerminal cfg opts t - runCommand ChatOpts {chatCmd, chatCmdLog, chatCmdDelay} user cc = do + runCommand user cc = do when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do (_, _, r') <- atomically . readTBQueue $ outputQ cc case r' of diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index be8aa12cfe..40f14a10de 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -189,6 +189,8 @@ responseNotification t@ChatTerminal {sendNotification} cc = \case CRContactConnected u ct _ -> when (contactNtf u ct False) $ do whenCurrUser cc u $ setActiveContact t ct sendNtf (viewContactName ct <> "> ", "connected") + CRContactSndReady u ct -> + whenCurrUser cc u $ setActiveContact t ct CRContactAnotherClient u ct -> do whenCurrUser cc u $ unsetActiveContact t ct when (contactNtf u ct False) $ sendNtf (viewContactName ct <> "> ", "connected to another client") diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 04cc2bcc6f..93820365b0 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -1377,7 +1377,7 @@ data ConnStatus ConnRequested | -- | initiating party accepted connection with agent LET command (to be renamed to ACPT) (allowConnection) ConnAccepted - | -- | connection can be sent messages to (after joining party received INFO notification) + | -- | connection can be sent messages to (after joining party received INFO notification, or after securing snd queue on join) ConnSndReady | -- | connection is ready for both parties to send and receive messages ConnReady @@ -1588,9 +1588,9 @@ commandExpectedResponse = \case CFCreateConnGrpInv -> t INV_ CFCreateConnFileInvDirect -> t INV_ CFCreateConnFileInvGroup -> t INV_ - CFJoinConn -> t OK_ + CFJoinConn -> t JOINED_ CFAllowConn -> t OK_ - CFAcceptContact -> t OK_ + CFAcceptContact -> t JOINED_ CFAckMessage -> t OK_ CFDeleteConn -> t OK_ where diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index b0d3032ed3..2ac29be60d 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -176,7 +176,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"] CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"] CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo - CRAcceptingContactRequest u c -> ttyUser u [ttyFullContact c <> ": accepting contact request..."] + CRAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"] CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] CRUserContactLinkCreated u cReq -> ttyUser u $ connReqContact_ "Your new chat address is created!" cReq @@ -231,6 +231,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_ CRContactConnecting u _ -> ttyUser u [] CRContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView + CRContactSndReady u ct -> ttyUser u [ttyFullContact ct <> ": you can send messages to contact"] CRContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"] CRSubscriptionEnd u acEntity -> let Connection {connId} = entityConnection acEntity @@ -365,6 +366,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe "chat entity locks: " <> viewJSON chatEntityLocks, "agent locks: " <> viewJSON agentLocks ] + CRAgentSubsTotal u subsTotal _ -> ttyUser u ["total subscriptions: " <> sShow subsTotal] CRAgentServersSummary u serversSummary -> ttyUser u ["agent servers summary: " <> viewJSON serversSummary] CRAgentSubs {activeSubs, pendingSubs, removedSubs} -> [plain $ "Subscriptions: active = " <> show (sum activeSubs) <> ", pending = " <> show (sum pendingSubs) <> ", removed = " <> show (sum $ M.map length removedSubs)] @@ -406,7 +408,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] CRAppSettings as -> ["app settings: " <> viewJSON as] CRTimedAction _ _ -> [] - CRCustomChatResponse u r -> ttyUser' u $ [plain r] + CRCustomChatResponse u r -> ttyUser' u $ map plain $ T.lines r where ttyUser :: User -> [StyledString] -> [StyledString] ttyUser user@User {showNtfs, activeUser} ss @@ -963,6 +965,11 @@ viewSentInvitation incognitoProfile testView = message = ["connection request sent incognito!"] Nothing -> ["connection request sent!"] +viewAcceptingContactRequest :: Contact -> [StyledString] +viewAcceptingContactRequest ct + | contactReady ct = [ttyFullContact ct <> ": accepting contact request, you can send messages to contact"] + | otherwise = [ttyFullContact ct <> ": accepting contact request..."] + viewReceivedContactRequest :: ContactName -> Profile -> [StyledString] viewReceivedContactRequest c Profile {fullName} = [ ttyFullName c fullName <> " wants to connect to you!", diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index ca5b92e04e..ffea6a3529 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -224,7 +224,10 @@ testCfgCreateGroupDirect = mkCfgCreateGroupDirect testCfg mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig -mkCfgCreateGroupDirect cfg = cfg {chatVRange = groupCreateDirectVRange} +mkCfgCreateGroupDirect cfg = cfg { + chatVRange = groupCreateDirectVRange, + agentConfig = testAgentCfgSlow +} groupCreateDirectVRange :: VersionRangeChat groupCreateDirectVRange = mkVersionRange (VersionChat 1) (VersionChat 1) diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index aaf91af910..2bbcf87d5b 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -1150,7 +1150,7 @@ testSubscribeAppNSE tmp = alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -1376,7 +1376,7 @@ testMultipleUserAddresses = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -1394,7 +1394,7 @@ testMultipleUserAddresses = alice <#? bob alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", ""), ("*", "")]) alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alisa: contact is connected") (alice <## "bob (Bob): contact is connected") @@ -1425,7 +1425,7 @@ testMultipleUserAddresses = showActiveUser alice "alisa" alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alisa: contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -2649,7 +2649,7 @@ testConnReqChatVRange ct1VRange ct2VRange tmp = bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 6f1ba20246..67d1bfeae5 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -307,28 +307,6 @@ testGroupShared alice bob cath checkMessages directConnections = do alice ##> "/d bob" alice <## "bob: contact is deleted" bob <## "alice (Alice) deleted contact with you" - alice `send` "@bob hey" - if directConnections - then - alice - <### [ "@bob hey", - "member #team bob does not have direct connection, creating", - "peer chat protocol version range incompatible" - ] - else do - alice - <### [ WithTime "@bob hey", - "member #team bob does not have direct connection, creating", - "contact for member #team bob is created", - "sent invitation to connect directly to member #team bob", - "bob (Bob): contact is connected" - ] - bob - <### [ "#team alice is creating direct contact alice with you", - WithTime "alice> hey", - "alice: security code changed", - "alice (Alice): contact is connected" - ] when checkMessages $ threadDelay 1000000 alice #> "#team checking connection" bob <# "#team alice> checking connection" @@ -818,6 +796,7 @@ testGroupDeleteInvitedContact = WithTime "alice> hey", "alice: security code changed" ] + bob <## "alice (Alice): you can send messages to contact" concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") @@ -1907,7 +1886,7 @@ testGroupLink = cath ##> ("/c " <> cLink) alice <#? cath alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -3965,6 +3944,7 @@ testMemberContactMessage = <### [ "#team alice is creating direct contact alice with you", WithTime "alice> hi" ] + bob <## "alice (Alice): you can send messages to contact" concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") @@ -3998,6 +3978,7 @@ testMemberContactMessage = <### [ "#team bob is creating direct contact bob with you", WithTime "bob> hi" ] + cath <## "bob (Bob): you can send messages to contact" concurrently_ (bob <## "cath (Catherine): contact is connected") (cath <## "bob (Bob): contact is connected") @@ -4018,6 +3999,7 @@ testMemberContactNoMessage = bob ##> "/_invite member contact @3" bob <## "sent invitation to connect directly to member #team cath" cath <## "#team bob is creating direct contact bob with you" + cath <## "bob (Bob): you can send messages to contact" concurrently_ (bob <## "cath (Catherine): contact is connected") (cath <## "bob (Bob): contact is connected") @@ -4058,6 +4040,7 @@ testMemberContactProhibitedRepeatInv = <### [ "#team bob is creating direct contact bob with you", WithTime "bob> hi" ] + cath <## "bob (Bob): you can send messages to contact" concurrently_ (bob <## "cath (Catherine): contact is connected") (cath <## "bob (Bob): contact is connected") @@ -4087,6 +4070,7 @@ testMemberContactInvitedConnectionReplaced tmp = do WithTime "alice> hi", "alice: security code changed" ] + bob <## "alice (Alice): you can send messages to contact" concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") @@ -4196,6 +4180,7 @@ testMemberContactIncognito = <### [ ConsoleString ("#team " <> bobIncognito <> " is creating direct contact " <> bobIncognito <> " with you"), WithTime ("i " <> bobIncognito <> "> hi") ] + cath <## (bobIncognito <> ": you can send messages to contact") _ <- getTermLine bob _ <- getTermLine cath concurrentlyN_ @@ -4267,6 +4252,7 @@ testMemberContactProfileUpdate = <### [ "#team bob is creating direct contact bob with you", WithTime "bob> hi" ] + cath <## "bob (Bob): you can send messages to contact" concurrentlyN_ [ do bob <## "contact cath changed to kate (Kate)" diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index a3352c7f4f..6971898ddf 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -198,6 +198,7 @@ testMultiWordProfileNames = ] cath <## "#'Our Team' 'Alice Jones' is creating direct contact 'Alice Jones' with you" cath <# "'Alice Jones'> hello" + cath <## "'Alice Jones': you can send messages to contact" cath <## "'Alice Jones': contact is connected" alice <## "'Cath Johnson': contact is connected" cath ##> "/p 'Cath J'" @@ -224,7 +225,7 @@ testUserContactLink = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -236,7 +237,7 @@ testUserContactLink = alice <#? cath alice @@@ [("<@cath", ""), ("@bob", "hey")] alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -254,7 +255,7 @@ testProfileLink = bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -269,7 +270,7 @@ testProfileLink = cath ##> ("/c " <> cLink) alice <#? cath alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -336,7 +337,7 @@ testUserContactLinkAutoAccept = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -350,6 +351,7 @@ testUserContactLinkAutoAccept = cath ##> ("/c " <> cLink) cath <## "connection request sent!" alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -364,7 +366,7 @@ testUserContactLinkAutoAccept = alice <#? dan alice @@@ [("<@dan", ""), ("@cath", "hey"), ("@bob", "hey")] alice ##> "/ac dan" - alice <## "dan (Daniel): accepting contact request..." + alice <## "dan (Daniel): accepting contact request, you can send messages to contact" concurrently_ (dan <## "alice (Alice): contact is connected") (alice <## "dan (Daniel): contact is connected") @@ -391,7 +393,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ bob @@@! [(":3", "", Just ConnJoined), (":2", "", Just ConnJoined), (":1", "", Just ConnJoined)] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -423,7 +425,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ alice <#? cath alice @@@ [("<@cath", ""), ("@bob", "hey")] alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -465,7 +467,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile alice ##> "/ac bob" alice <## "no contact request from bob" alice ##> "/ac robert" - alice <## "robert (Robert): accepting contact request..." + alice <## "robert (Robert): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "robert (Robert): contact is connected") @@ -500,7 +502,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile alice <#? cath alice @@@ [("<@cath", ""), ("@robert", "hey")] alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -566,13 +568,13 @@ testAutoReplyMessage = testChat2 aliceProfile bobProfile $ bob ##> ("/c " <> cLink) bob <## "connection request sent!" alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): you can send messages to contact" + alice <# "@bob hello!" concurrentlyN_ [ do - bob <## "alice (Alice): contact is connected" - bob <# "alice> hello!", - do - alice <## "bob (Bob): contact is connected" - alice <# "@bob hello!" + bob <# "alice> hello!" + bob <## "alice (Alice): contact is connected", + alice <## "bob (Bob): contact is connected" ] testAutoReplyMessageInIncognito :: HasCallStack => FilePath -> IO () @@ -588,17 +590,16 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $ bob ##> ("/c " <> cLink) bob <## "connection request sent!" alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): you can send messages to contact" + alice <# "i @bob hello!" aliceIncognito <- getTermLine alice concurrentlyN_ [ do - bob <## (aliceIncognito <> ": contact is connected") - bob <# (aliceIncognito <> "> hello!"), + bob <# (aliceIncognito <> "> hello!") + bob <## (aliceIncognito <> ": contact is connected"), do alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) - alice - <### [ "use /i bob to print out this incognito profile again", - WithTime "i @bob hello!" - ] + alice <## "use /i bob to print out this incognito profile again" ] testPlanAddressOkKnown :: HasCallStack => FilePath -> IO () @@ -615,7 +616,7 @@ testPlanAddressOkKnown = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -654,7 +655,7 @@ testPlanAddressOwn tmp = alice <## "to reject: /rc alice_1 (the sender will NOT be notified)" alice @@@ [("<@alice_1", ""), (":2", "")] alice ##> "/ac alice_1" - alice <## "alice_1 (Alice): accepting contact request..." + alice <## "alice_1 (Alice): accepting contact request, you can send messages to contact" alice <### [ "alice_1 (Alice): contact is connected", "alice_2 (Alice): contact is connected" @@ -705,7 +706,7 @@ testPlanAddressConnecting tmp = do alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" withTestChat tmp "bob" $ \bob -> do threadDelay 500000 bob <## "alice (Alice): contact is connected" @@ -772,7 +773,7 @@ testPlanAddressContactDeletedReconnected = bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -803,7 +804,7 @@ testPlanAddressContactDeletedReconnected = alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice_1 (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -877,7 +878,7 @@ testPlanAddressContactViaAddress = alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -971,7 +972,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ alice <## ("to accept: /ac " <> bobIncognito) alice <## ("to reject: /rc " <> bobIncognito <> " (the sender will NOT be notified)") alice ##> ("/ac " <> bobIncognito) - alice <## (bobIncognito <> ": accepting contact request...") + alice <## (bobIncognito <> ": accepting contact request, you can send messages to contact") _ <- getTermLine bob concurrentlyN_ [ do @@ -1005,7 +1006,7 @@ testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfil bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/accept incognito bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" aliceIncognitoBob <- getTermLine alice concurrentlyN_ [ bob <## (aliceIncognitoBob <> ": contact is connected"), @@ -1033,7 +1034,7 @@ testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfil cath ##> ("/c " <> cLink) alice <#? cath alice ##> "/_accept incognito=on 1" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" aliceIncognitoCath <- getTermLine alice concurrentlyN_ [ cath <## (aliceIncognitoCath <> ": contact is connected"), @@ -2036,6 +2037,7 @@ testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danP <### [ "#team alice is creating direct contact alice with you", WithTime "alice> hello dan" ] + dan <## "alice (Alice): you can send messages to contact" concurrently_ (alice <## "dan (Daniel): contact is connected") (dan <## "alice (Alice): contact is connected")