Compare commits

..

3 Commits

Author SHA1 Message Date
Diogo Cunha 387faa0c27 android: implemented one hand ui for chat list screen 2024-07-12 23:02:14 +01:00
Diogo Cunha aa990da17c android, desktop: added setting for one hand ui 2024-07-11 21:25:03 +01:00
Diogo Cunha a48c82f4a1 android, desktop: added action buttons and delete to contact card, added toolbar 2024-07-10 22:23:43 +01:00
251 changed files with 4048 additions and 10988 deletions
+15 -35
View File
@@ -74,7 +74,6 @@ final class ChatModel: ObservableObject {
@Published var chatToTop: String?
@Published var groupMembers: [GMember] = []
@Published var groupMembersIndexes: Dictionary<Int64, Int> = [:] // groupMemberId to index in groupMembers list
@Published var membersLoaded = false
// items in the terminal view
@Published var showingTerminal = false
@Published var terminalItems: [TerminalItem] = []
@@ -196,18 +195,6 @@ final class ChatModel: ObservableObject {
return nil
}
func loadGroupMembers(_ groupInfo: GroupInfo, updateView: @escaping () -> Void = {}) async {
let groupMembers = await apiListMembers(groupInfo.groupId)
await MainActor.run {
if chatId == groupInfo.id {
self.groupMembers = groupMembers.map { GMember.init($0) }
self.populateGroupMembersIndexes()
self.membersLoaded = true
updateView()
}
}
}
private func getChatIndex(_ id: String) -> Int? {
chats.firstIndex(where: { $0.id == id })
}
@@ -354,9 +341,6 @@ 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
}
@@ -406,8 +390,8 @@ final class ChatModel: ObservableObject {
}
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
if cItem.isRcvNew, let chatIndex = getChatIndex(cInfo.id) {
decreaseUnreadCounter(chatIndex)
if cItem.isRcvNew {
decreaseUnreadCounter(cInfo)
}
// update previews
if let chat = getChat(cInfo.id) {
@@ -423,7 +407,6 @@ final class ChatModel: ObservableObject {
}
}
}
VoiceItemState.stopVoiceInChatView(cInfo, cItem)
}
func nextChatItemData<T>(_ chatItemId: Int64, previous: Bool, map: @escaping (ChatItem) -> T?) -> T? {
@@ -553,18 +536,13 @@ final class ChatModel: ObservableObject {
}
}
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
if chatId == cInfo.id,
let itemIndex = getChatItemIndex(cItem),
let chatIndex = getChatIndex(cInfo.id),
reversedChatItems[itemIndex].isRcvNew {
await MainActor.run {
withTransaction(Transaction()) {
// update current chat
markChatItemRead_(itemIndex)
// update preview
decreaseUnreadCounter(chatIndex)
}
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
if reversedChatItems[i].isRcvNew {
// update current chat
markChatItemRead_(i)
// update preview
decreaseUnreadCounter(cInfo)
}
}
}
@@ -580,9 +558,11 @@ final class ChatModel: ObservableObject {
}
}
func decreaseUnreadCounter(_ chatIndex: Int) {
chats[chatIndex].chatStats.unreadCount = chats[chatIndex].chatStats.unreadCount - 1
decreaseUnreadCounter(user: currentUser!)
func decreaseUnreadCounter(_ cInfo: ChatInfo) {
if let i = getChatIndex(cInfo.id) {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1
decreaseUnreadCounter(user: currentUser!)
}
}
func increaseUnreadCounter(user: any UserLike) {
@@ -778,7 +758,7 @@ struct UnreadChatItemCounts: Equatable {
var unreadBelow: Int
}
final class Chat: ObservableObject, Identifiable, ChatLike {
final class Chat: ObservableObject, Identifiable {
@Published var chatInfo: ChatInfo
@Published var chatItems: [ChatItem]
@Published var chatStats: ChatStats
@@ -7,19 +7,18 @@
//
import Foundation
import SimpleXChat
import SwiftUI
import AVKit
import SwiftyGif
import LinkPresentation
public func getLoadedFileSource(_ file: CIFile?) -> CryptoFile? {
func getLoadedFileSource(_ file: CIFile?) -> CryptoFile? {
if let file = file, file.loaded {
return file.fileSource
}
return nil
}
public func getLoadedImage(_ file: CIFile?) -> UIImage? {
func getLoadedImage(_ file: CIFile?) -> UIImage? {
if let fileSource = getLoadedFileSource(file) {
let filePath = getAppFilePath(fileSource.filePath)
do {
@@ -38,7 +37,7 @@ public func getLoadedImage(_ file: CIFile?) -> UIImage? {
return nil
}
public func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data {
func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data {
if let cfArgs = cfArgs {
return try readCryptoFile(path: path.path, cryptoArgs: cfArgs)
} else {
@@ -46,7 +45,7 @@ public func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data {
}
}
public func getLoadedVideo(_ file: CIFile?) -> URL? {
func getLoadedVideo(_ file: CIFile?) -> URL? {
if let fileSource = getLoadedFileSource(file) {
let filePath = getAppFilePath(fileSource.filePath)
if FileManager.default.fileExists(atPath: filePath.path) {
@@ -56,13 +55,13 @@ public func getLoadedVideo(_ file: CIFile?) -> URL? {
return nil
}
public func saveAnimImage(_ image: UIImage) -> CryptoFile? {
func saveAnimImage(_ image: UIImage) -> CryptoFile? {
let fileName = generateNewFileName("IMG", "gif")
guard let imageData = image.imageData else { return nil }
return saveFile(imageData, fileName, encrypted: privacyEncryptLocalFilesGroupDefault.get())
}
public func saveImage(_ uiImage: UIImage) -> CryptoFile? {
func saveImage(_ uiImage: UIImage) -> CryptoFile? {
let hasAlpha = imageHasAlpha(uiImage)
let ext = hasAlpha ? "png" : "jpg"
if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: MAX_IMAGE_SIZE, hasAlpha: hasAlpha) {
@@ -72,7 +71,7 @@ public func saveImage(_ uiImage: UIImage) -> CryptoFile? {
return nil
}
public func cropToSquare(_ image: UIImage) -> UIImage {
func cropToSquare(_ image: UIImage) -> UIImage {
let size = image.size
let side = min(size.width, size.height)
let newSize = CGSize(width: side, height: side)
@@ -85,7 +84,7 @@ public func cropToSquare(_ image: UIImage) -> UIImage {
return resizeImage(image, newBounds: CGRect(origin: .zero, size: newSize), drawIn: CGRect(origin: origin, size: size), hasAlpha: imageHasAlpha(image))
}
public func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) -> Data? {
func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) -> Data? {
var img = image
var data = hasAlpha ? img.pngData() : img.jpegData(compressionQuality: 0.85)
var dataSize = data?.count ?? 0
@@ -100,7 +99,7 @@ public func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha
return data
}
public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
var img = image
let hasAlpha = imageHasAlpha(image)
var str = compressImageStr(img, hasAlpha: hasAlpha)
@@ -116,7 +115,7 @@ public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String
return str
}
public func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85, hasAlpha: Bool) -> String? {
func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85, hasAlpha: Bool) -> String? {
let ext = hasAlpha ? "png" : "jpg"
if let data = hasAlpha ? image.pngData() : image.jpegData(compressionQuality: compressionQuality) {
return "data:image/\(ext);base64,\(data.base64EncodedString())"
@@ -139,7 +138,7 @@ private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect, ha
}
}
public func imageHasAlpha(_ img: UIImage) -> Bool {
func imageHasAlpha(_ img: UIImage) -> Bool {
if let cgImage = img.cgImage {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
@@ -159,35 +158,7 @@ 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? {
func saveFileFromURL(_ url: URL) -> CryptoFile? {
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
let savedFile: CryptoFile?
if url.startAccessingSecurityScopedResource() {
@@ -213,7 +184,7 @@ public func saveFileFromURL(_ url: URL) -> CryptoFile? {
return savedFile
}
public func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
do {
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
let fileName = uniqueCombine(url.lastPathComponent)
@@ -226,6 +197,7 @@ public func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName))
savedFile = CryptoFile.plain(fileName)
}
ChatModel.shared.filesToDelete.remove(url)
return savedFile
} catch {
logger.error("ImageUtils.moveTempFileFromURL error: \(error.localizedDescription)")
@@ -233,7 +205,7 @@ public func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
}
}
public func saveWallpaperFile(url: URL) -> String? {
func saveWallpaperFile(url: URL) -> String? {
let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", "jpg", fullPath: true))
do {
try FileManager.default.copyItem(atPath: url.path, toPath: destFile.path)
@@ -244,7 +216,7 @@ public func saveWallpaperFile(url: URL) -> String? {
}
}
public func saveWallpaperFile(image: UIImage) -> String? {
func saveWallpaperFile(image: UIImage) -> String? {
let hasAlpha = imageHasAlpha(image)
let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", hasAlpha ? "png" : "jpg", fullPath: true))
let dataResized = resizeImageToDataSize(image, maxDataSize: 5_000_000, hasAlpha: hasAlpha)
@@ -257,7 +229,7 @@ public func saveWallpaperFile(image: UIImage) -> String? {
}
}
public func removeWallpaperFile(fileName: String? = nil) {
func removeWallpaperFile(fileName: String? = nil) {
do {
try FileManager.default.contentsOfDirectory(atPath: getWallpaperDirectory().path).forEach {
if URL(fileURLWithPath: $0).lastPathComponent == fileName { try FileManager.default.removeItem(atPath: $0) }
@@ -270,7 +242,7 @@ public func removeWallpaperFile(fileName: String? = nil) {
}
}
public func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String {
func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String {
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath)
}
@@ -302,7 +274,7 @@ private func getTimestamp() -> String {
return df.string(from: Date())
}
public func dropImagePrefix(_ s: String) -> String {
func dropImagePrefix(_ s: String) -> String {
dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,")
}
@@ -310,23 +282,8 @@ 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)? {
func generatePreview() -> (UIImage, Int)? {
let generator = AVAssetImageGenerator(asset: self)
generator.appliesPreferredTrackTransform = true
var actualTime = CMTimeMake(value: 0, timescale: 0)
@@ -338,7 +295,7 @@ extension AVAsset {
}
extension UIImage {
public func replaceColor(_ from: UIColor, _ to: UIColor) -> UIImage {
func replaceColor(_ from: UIColor, _ to: UIColor) -> UIImage {
if let cgImage = cgImage {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
@@ -383,48 +340,4 @@ extension UIImage {
}
return self
}
public convenience init?(base64Encoded: String?) {
if let base64Encoded, let data = Data(base64Encoded: dropImagePrefix(base64Encoded)) {
self.init(data: data)
} else {
return nil
}
}
}
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) }
}
}
+27 -52
View File
@@ -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, enableSndFiles: true), ctrl)
let r = chatSendCmdSync(.startChat(mainApp: true), ctrl)
switch r {
case .chatStarted: return true
case .chatRunning: return false
@@ -225,15 +225,6 @@ func apiStartChat(ctrl: chat_ctrl? = nil) throws -> Bool {
}
}
func apiCheckChatRunning() throws -> Bool {
let r = chatSendCmdSync(.checkChatRunning)
switch r {
case .chatRunning: return true
case .chatStopped: return false
default: throw r
}
}
func apiStopChat() async throws {
let r = await chatSendCmd(.apiStopChat)
switch r {
@@ -406,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: \(responseError(r))"
message: "Error: \(String(describing: r))"
)
}
@@ -414,7 +405,7 @@ private func createChatItemErrorAlert(_ r: ChatResponse) {
logger.error("apiCreateChatItem error: \(String(describing: r))")
AlertManager.shared.showAlertMsg(
title: "Error creating message",
message: "Error: \(responseError(r))"
message: "Error: \(String(describing: r))"
)
}
@@ -708,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."
@@ -741,7 +732,7 @@ private func connectionErrorAlert(_ r: ChatResponse) -> Alert {
} else {
return mkAlert(
title: "Connection error",
message: "Error: \(responseError(r))"
message: "Error: \(String(describing: r))"
)
}
}
@@ -907,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."
@@ -918,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: \(responseError(r))"
message: "Error: \(String(describing: r))"
)
}
return nil
@@ -944,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, responseError(r))
return (nil, String(describing: r))
}
}
@@ -954,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, responseError(r))
return (nil, String(describing: r))
}
}
@@ -1034,7 +1025,7 @@ func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, in
if !auto {
am.showAlertMsg(
title: "Error receiving file",
message: "Error: \(responseError(r))"
message: "Error: \(String(describing: r))"
)
}
}
@@ -1101,9 +1092,18 @@ func deleteRemoteCtrl(_ rcId: Int64) async throws {
}
func networkErrorAlert(_ r: ChatResponse) -> Alert? {
if let alert = getNetworkErrorAlert(r) {
return mkAlert(title: alert.title, message: alert.message)
} else {
switch r {
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
return mkAlert(
title: "Connection timeout",
message: "Please check your network connection with \(serverHostname(addr)) and try again."
)
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
return mkAlert(
title: "Connection error",
message: "Please check your network connection with \(serverHostname(addr)) and try again."
)
default:
return nil
}
}
@@ -1111,10 +1111,7 @@ 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)
ChatModel.shared.setContactNetworkStatus(contact, .connected)
}
DispatchQueue.main.async { ChatModel.shared.replaceChat(contactRequest.id, chat) }
}
}
@@ -1210,7 +1207,7 @@ func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
do {
logger.debug("apiMarkChatItemRead: \(cItem.id)")
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
await ChatModel.shared.markChatItemRead(cInfo, cItem)
await MainActor.run { ChatModel.shared.markChatItemRead(cInfo, cItem) }
} catch {
logger.error("apiMarkChatItemRead apiChatRead error: \(responseError(error))")
}
@@ -1251,7 +1248,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
}
@@ -1357,14 +1354,6 @@ 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)
@@ -1448,16 +1437,15 @@ func startChat(refreshInvitations: Bool = true) throws {
logger.debug("startChat")
let m = ChatModel.shared
try setNetworkConfig(getNetCfg())
let chatRunning = try apiCheckChatRunning()
let justStarted = try apiStartChat()
m.users = try listUsers()
if !chatRunning {
if justStarted {
try getUserChatData()
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
if (refreshInvitations) {
try refreshCallInvitations()
}
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
_ = try apiStartChat()
// deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called,
// when it is called before startChat
if let token = m.deviceToken {
@@ -1629,19 +1617,6 @@ 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)
-12
View File
@@ -36,18 +36,6 @@ 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)
+2 -1
View File
@@ -102,7 +102,8 @@ extension ThemeWallpaper {
public func importFromString() -> ThemeWallpaper {
if preset == nil, let image {
// Need to save image from string and to save its path
if let parsed = UIImage(base64Encoded: image),
if let data = Data(base64Encoded: dropImagePrefix(image)),
let parsed = UIImage(data: data),
let filename = saveWallpaperFile(image: parsed) {
var copy = self
copy.image = nil
@@ -28,17 +28,15 @@ struct ChatInfoToolbar: View {
color: Color(uiColor: .tertiaryLabel)
)
.padding(.trailing, 4)
let t = Text(cInfo.displayName).font(.headline)
(cInfo.contact?.verified == true ? contactVerifiedShield + t : t)
.lineLimit(1)
.if (cInfo.fullName != "" && cInfo.displayName != cInfo.fullName) { v in
VStack(spacing: 0) {
v
Text(cInfo.fullName).font(.subheadline)
.lineLimit(1)
.padding(.top, -2)
}
VStack {
let t = Text(cInfo.displayName).font(.headline)
(cInfo.contact?.verified == true ? contactVerifiedShield + t : t)
.lineLimit(1)
if cInfo.fullName != "" && cInfo.displayName != cInfo.fullName {
Text(cInfo.fullName).font(.subheadline)
.lineLimit(1)
}
}
}
.foregroundColor(theme.colors.onBackground)
.frame(width: 220)
+12 -14
View File
@@ -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,25 +155,23 @@ struct ChatInfoView: View {
}
Section {
Group {
if let code = connectionCode { verifyCodeButton(code) }
contactPreferencesButton()
sendReceiptsOption()
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
}
// } else if developerTools {
// synchronizeConnectionButtonForce()
// }
if let code = connectionCode { verifyCodeButton(code) }
contactPreferencesButton()
sendReceiptsOption()
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
}
.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 {
@@ -273,7 +271,7 @@ struct ChatInfoView: View {
}
}
.actionSheet(isPresented: $showDeleteContactActionSheet) {
if contact.sndReady && contact.active {
if contact.ready && contact.active {
return ActionSheet(
title: Text("Delete contact?\nThis cannot be undone!"),
buttons: [
@@ -9,7 +9,6 @@ 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)
@@ -19,12 +18,11 @@ class AnimatedImageView: UIView {
fatalError("Not implemented")
}
convenience init(image: UIImage, contentMode: UIView.ContentMode) {
convenience init(image: UIImage) {
self.init()
self.image = image
self.cMode = contentMode
imageView = UIImageView(gifImage: image)
imageView!.contentMode = contentMode
imageView!.contentMode = .scaleAspectFit
self.addSubview(imageView!)
}
@@ -37,7 +35,7 @@ class AnimatedImageView: UIView {
if let subview = self.subviews.first as? UIImageView {
if image.imageData != subview.gifImage?.imageData {
imageView = UIImageView(gifImage: image)
imageView!.contentMode = contentMode
imageView!.contentMode = .scaleAspectFit
self.addSubview(imageView!)
subview.removeFromSuperview()
}
@@ -49,15 +47,13 @@ class AnimatedImageView: UIView {
struct SwiftyGif: UIViewRepresentable {
private let image: UIImage
private let contentMode: UIView.ContentMode
init(image: UIImage, contentMode: UIView.ContentMode = .scaleAspectFit) {
init(image: UIImage) {
self.image = image
self.contentMode = contentMode
}
func makeUIView(context: Context) -> AnimatedImageView {
AnimatedImageView(image: image, contentMode: contentMode)
AnimatedImageView(image: image)
}
func updateUIView(_ imageView: AnimatedImageView, context: Context) {
@@ -14,45 +14,39 @@ struct CIFileView: View {
@EnvironmentObject var theme: AppTheme
let file: CIFile?
let edited: Bool
var smallViewSize: CGFloat?
var body: some View {
if smallViewSize != nil {
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)
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)
}
.padding(.top, 4)
.padding(.bottom, 6)
.padding(.leading, 10)
.padding(.trailing, 12)
}
.disabled(!itemInteractive)
.padding(.top, 4)
.padding(.bottom, 6)
.padding(.leading, 10)
.padding(.trailing, 12)
}
.disabled(!itemInteractive)
}
private var itemInteractive: Bool {
@@ -201,22 +195,21 @@ struct CIFileView: View {
}
private func fileIcon(_ icon: String, color: Color = Color(uiColor: .tertiaryLabel), innerIcon: String? = nil, innerIconSize: CGFloat? = nil) -> some View {
let size = smallViewSize ?? 30
return ZStack(alignment: .center) {
ZStack(alignment: .center) {
Image(systemName: icon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.frame(width: 30, height: 30)
.foregroundColor(color)
if let innerIcon = innerIcon,
let innerIconSize = innerIconSize, (smallViewSize == nil || file?.showStatusIconInSmallView == true) {
let innerIconSize = innerIconSize {
Image(systemName: innerIcon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxHeight: 16)
.frame(width: innerIconSize, height: innerIconSize)
.foregroundColor(.white)
.padding(.top, size / 2.5)
.padding(.top, 12)
}
}
}
@@ -15,33 +15,22 @@ struct CIImageView: View {
var preview: UIImage?
let maxWidth: CGFloat
var imgWidth: CGFloat?
var smallView: Bool = false
@Binding var showFullScreenImage: Bool
@State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0
@State private var showFullScreenImage = false
var body: some View {
let file = chatItem.file
VStack(alignment: .center, spacing: 6) {
if let uiImage = getLoadedImage(file) {
Group { if smallView { smallViewImageView(uiImage) } else { imageView(uiImage) } }
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 {
Group {
if smallView {
smallViewImageView(preview)
} else {
imageView(preview).modifier(PrivacyBlur(blurred: $blurred))
}
}
imageView(preview)
.onTapGesture {
if let file = file {
switch file.fileStatus {
@@ -94,9 +83,6 @@ struct CIImageView: View {
}
}
}
.onDisappear {
showFullScreenImage = false
}
}
private func imageView(_ img: UIImage) -> some View {
@@ -112,26 +98,7 @@ struct CIImageView: View {
.frame(width: w, height: w * img.size.height / img.size.width)
.scaledToFit()
}
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()
}
loadingIndicator()
}
}
@@ -178,12 +145,4 @@ struct CIImageView: View {
.tint(.white)
.padding(8)
}
private func showDownloadButton(_ fileStatus: CIFileStatus?) -> Bool {
switch fileStatus {
case .rcvInvitation: true
case .rcvAborted: true
default: false
}
}
}
@@ -12,15 +12,14 @@ 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) {
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
if let data = Data(base64Encoded: dropImagePrefix(linkPreview.image)),
let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.modifier(PrivacyBlur(blurred: $blurred))
}
VStack(alignment: .leading, spacing: 6) {
Text(linkPreview.title)
@@ -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 mkAlert(title: title, message: error)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
}
}
}
@@ -20,44 +20,45 @@ 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
@Binding private var showFullScreenPlayer: Bool
@State private var showFullScreenPlayer = false
@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?, smallView: Bool = false, showFullscreenPlayer: Binding<Bool>) {
init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?) {
self.chatItem = chatItem
self.preview = preview
self._duration = State(initialValue: duration)
self.maxWidth = maxWidth
self.videoWidth = videoWidth
self.smallView = smallView
self._showFullScreenPlayer = showFullscreenPlayer
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)
}
}
var body: some View {
let file = chatItem.file
ZStack(alignment: smallView ? .topLeading : .center) {
ZStack {
ZStack(alignment: .topLeading) {
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 {
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, let file {
Group { if smallView { smallViewImageView(preview, file) } else { imageView(preview) } }
.onTapGesture {
} else if let preview {
imageView(preview)
.onTapGesture {
if let file = file {
switch file.fileStatus {
case .rcvInvitation, .rcvAborted:
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
@@ -81,64 +82,21 @@ struct CIVideoView: View {
default: ()
}
}
}
if !smallView {
durationProgress()
}
}
if !blurred, let file, showDownloadButton(file.fileStatus) {
if !smallView {
Button {
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
} label: {
playPauseIcon("play.fill")
}
} else if !file.showStatusIconInSmallView {
}
durationProgress()
}
if let file = file, showDownloadButton(file.fileStatus) {
Button {
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
} label: {
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 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 {
private func showDownloadButton(_ fileStatus: CIFileStatus) -> Bool {
switch fileStatus {
case .rcvInvitation: true
case .rcvAborted: true
@@ -151,6 +109,11 @@ 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
@@ -159,22 +122,20 @@ struct CIVideoView: View {
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenPlayer = false
}
if !blurred {
if !decryptionInProgress {
Button {
decrypt(file: file) {
if urlDecrypted != nil {
videoPlaying = true
player?.play()
}
if !decryptionInProgress {
Button {
decrypt(file: file) {
if urlDecrypted != nil {
videoPlaying = true
player?.play()
}
} label: {
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
}
.disabled(!canBePlayed)
} else {
videoDecryptionProgress()
} label: {
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
}
.disabled(!canBePlayed)
} else {
videoDecryptionProgress()
}
}
}
@@ -193,7 +154,9 @@ struct CIVideoView: View {
videoPlaying = false
}
}
.modifier(PrivacyBlur(enabled: !videoPlaying, blurred: $blurred))
.fullScreenCover(isPresented: $showFullScreenPlayer) {
fullScreenPlayer(url)
}
.onTapGesture {
switch player.timeControlStatus {
case .playing:
@@ -209,7 +172,7 @@ struct CIVideoView: View {
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenPlayer = false
}
if !videoPlaying && !blurred {
if !videoPlaying {
Button {
m.stopPreviousRecPlay = url
player.play()
@@ -231,53 +194,14 @@ 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: smallView ? 12 * sizeMultiplier * 1.6 : 12, height: smallView ? 12 * sizeMultiplier * 1.6 : 12)
.frame(width: 12, height: 12)
.foregroundColor(color)
.padding(.leading, smallView ? 0 : 4)
.frame(width: 40 * sizeMultiplier, height: 40 * sizeMultiplier)
.padding(.leading, 4)
.frame(width: 40, height: 40)
.background(Color.black.opacity(0.35))
.clipShape(Circle())
}
@@ -285,9 +209,9 @@ struct CIVideoView: View {
private func videoDecryptionProgress(_ color: Color = .white) -> some View {
ProgressView()
.progressViewStyle(.circular)
.frame(width: smallView ? 12 * sizeMultiplier : 12, height: smallView ? 12 * sizeMultiplier : 12)
.frame(width: 12, height: 12)
.tint(color)
.frame(width: smallView ? 40 * sizeMultiplier * 0.9 : 40, height: smallView ? 40 * sizeMultiplier * 0.9 : 40)
.frame(width: 40, height: 40)
.background(Color.black.opacity(0.35))
.clipShape(Circle())
}
@@ -323,23 +247,7 @@ struct CIVideoView: View {
.resizable()
.scaledToFit()
.frame(width: w)
.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)
}
fileStatusIcon()
}
}
@@ -414,7 +322,7 @@ struct CIVideoView: View {
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.foregroundColor(.white)
.padding(smallView ? 0 : padding)
.padding(padding)
}
private func progressView() -> some View {
@@ -422,7 +330,7 @@ struct CIVideoView: View {
.progressViewStyle(.circular)
.frame(width: 16, height: 16)
.tint(.white)
.padding(smallView ? 0 : 11)
.padding(11)
}
private func progressCircle(_ progress: Int64, _ total: Int64) -> some View {
@@ -434,7 +342,7 @@ struct CIVideoView: View {
)
.rotationEffect(.degrees(-90))
.frame(width: 16, height: 16)
.padding([.trailing, .top], smallView ? 0 : 11)
.padding([.trailing, .top], 11)
}
// TODO encrypt: where file size is checked?
@@ -474,8 +382,7 @@ struct CIVideoView: View {
)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now()) {
// Prevent feedback loop - setting `ChatModel`s property causes `onAppear` to be called on iOS17+
if m.stopPreviousRecPlay != url { m.stopPreviousRecPlay = url }
m.stopPreviousRecPlay = url
if let player = fullPlayer {
player.play()
var played = false
@@ -512,12 +419,10 @@ struct CIVideoView: View {
urlDecrypted = await file.fileSource?.decryptedGetOrCreate(&ChatModel.shared.filesToDelete)
await MainActor.run {
if let decrypted = urlDecrypted {
if !smallView {
player = VideoPlayerView.getOrCreatePlayer(decrypted, false)
}
player = VideoPlayerView.getOrCreatePlayer(decrypted, false)
fullPlayer = AVPlayer(url: decrypted)
}
decryptionInProgress = false
decryptionInProgress = true
completed?()
}
}
@@ -15,26 +15,15 @@ struct CIVoiceView: View {
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 audioPlayer: AudioPlayer?
@Binding var playbackState: VoiceMessagePlaybackState
@Binding var playbackTime: TimeInterval?
@Binding var allowMenu: Bool
var smallViewSize: CGFloat?
@State private var seek: (TimeInterval) -> Void = { _ in }
var body: some View {
Group {
if smallViewSize != nil {
HStack(spacing: 10) {
player()
playerTime()
.allowsHitTesting(false)
if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu {
playbackSlider()
}
}
} else if chatItem.chatDir.sent {
if chatItem.chatDir.sent {
VStack (alignment: .trailing, spacing: 6) {
HStack {
if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu {
@@ -65,13 +54,7 @@ struct CIVoiceView: View {
}
private func player() -> some View {
let sizeMultiplier: CGFloat = if let sz = smallViewSize {
voiceMessageSizeBasedOnSquareSize(sz) / 56
} else {
1
}
return VoiceMessagePlayer(
chat: chat,
VoiceMessagePlayer(
chatItem: chatItem,
recordingFile: recordingFile,
recordingTime: TimeInterval(duration),
@@ -80,8 +63,7 @@ struct CIVoiceView: View {
audioPlayer: $audioPlayer,
playbackState: $playbackState,
playbackTime: $playbackTime,
allowMenu: $allowMenu,
sizeMultiplier: sizeMultiplier
allowMenu: $allowMenu
)
}
@@ -137,7 +119,6 @@ struct VoiceMessagePlayerTime: View {
}
struct VoiceMessagePlayer: View {
@ObservedObject var chat: Chat
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
@@ -149,9 +130,7 @@ 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 {
@@ -211,113 +190,49 @@ 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) }
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?.onTimer = { playbackTime = $0 }
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) {
stopPlayback()
audioPlayer?.stop()
playbackState = .noPlayback
playbackTime = TimeInterval(0)
}
}
.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 {
if sizeMultiplier != 1 {
switch playbackState {
case .noPlayback:
switch playbackState {
case .noPlayback:
Button {
if let recordingSource = getLoadedFileSource(recordingFile) {
startPlayback(recordingSource)
}
} label: {
playPauseIcon("play.fill", theme.colors.primary)
.onTapGesture {
if let recordingSource = getLoadedFileSource(recordingFile) {
startPlayback(recordingSource)
}
}
case .playing:
playPauseIcon("pause.fill", theme.colors.primary)
.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)
}
case .playing:
Button {
audioPlayer?.pause()
playbackState = .paused
} label: {
playPauseIcon("pause.fill", theme.colors.primary)
}
case .paused:
Button {
audioPlayer?.play()
playbackState = .playing
} label: {
playPauseIcon("play.fill", theme.colors.primary)
}
}
}
@@ -327,49 +242,28 @@ struct VoiceMessagePlayer: View {
Image(systemName: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20 * sizeMultiplier, height: 20 * sizeMultiplier)
.frame(width: 20, height: 20)
.foregroundColor(color)
.padding(.leading, image == "play.fill" ? 4 : 0)
.frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier)
.frame(width: 56, height: 56)
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
.clipShape(Circle())
if recordingTime > 0 {
ProgressCircle(length: recordingTime, progress: $playbackTime)
.frame(width: 53 * sizeMultiplier, height: 53 * sizeMultiplier) // this + ProgressCircle lineWidth = background circle diameter
.frame(width: 53, height: 53) // this + ProgressCircle lineWidth = background circle diameter
}
}
}
private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View {
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)
Button {
Task {
if let user = chatModel.currentUser {
await receiveFile(user: user, fileId: recordingFile.fileId)
}
}
}
}
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)
} label: {
playPauseIcon(icon, theme.colors.primary)
}
}
@@ -394,96 +288,33 @@ struct VoiceMessagePlayer: View {
Image(systemName: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: size * sizeMultiplier, height: size * sizeMultiplier)
.frame(width: size, height: size)
.foregroundColor(Color(uiColor: .tertiaryLabel))
.frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier)
.frame(width: 56, height: 56)
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
.clipShape(Circle())
}
private func loadingIcon() -> some View {
ProgressView()
.frame(width: 30 * sizeMultiplier, height: 30 * sizeMultiplier)
.frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier)
.frame(width: 30, height: 30)
.frame(width: 56, height: 56)
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
.clipShape(Circle())
}
private func startPlayback(_ recordingSource: CryptoFile) {
let audioPath = getAppFilePath(recordingSource.filePath)
let chatId = chatModel.chatId
let userId = chatModel.currentUser?.userId
chatModel.stopPreviousRecPlay = audioPath
chatModel.stopPreviousRecPlay = getAppFilePath(recordingSource.filePath)
audioPlayer = 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 chatModel.stopPreviousRecPlay != audioPath || chatModel.chatId != chatId || chatModel.currentUser?.userId != userId {
stopPlayback()
}
},
onTimer: { playbackTime = $0 },
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 {
@@ -508,12 +339,15 @@ 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))
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))
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))
}
.previewLayout(.fixed(width: 360, height: 360))
}
@@ -13,23 +13,21 @@ 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),
@@ -38,8 +36,7 @@ struct FramedCIVoiceView: View {
audioPlayer: $audioPlayer,
playbackState: $playbackState,
playbackTime: $playbackTime,
allowMenu: $allowMenu,
sizeMultiplier: 1
allowMenu: $allowMenu
)
VoiceMessagePlayerTime(
recordingTime: TimeInterval(duration),
File diff suppressed because one or more lines are too long
@@ -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(chats: ChatModel.shared.chats)
private let chatsToForwardTo = filterChatsToForwardTo()
var body: some View {
NavigationView {
@@ -67,6 +67,22 @@ 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)
@@ -146,6 +162,27 @@ 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"),
@@ -153,4 +190,3 @@ struct ChatItemForwardingView: View {
composeState: Binding.constant(ComposeState(message: "hello"))
).environmentObject(CurrentColors.toAppTheme())
}
+18 -5
View File
@@ -16,20 +16,28 @@ 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<Bool>,
allowMenu: Binding<Bool> = .constant(false)
allowMenu: Binding<Bool> = .constant(false),
audioPlayer: Binding<AudioPlayer?> = .constant(nil),
playbackState: Binding<VoiceMessagePlaybackState> = .constant(.noPlayback),
playbackTime: Binding<TimeInterval?> = .constant(nil)
) {
self.chat = chat
self.chatItem = chatItem
self.maxWidth = maxWidth
_revealed = revealed
_allowMenu = allowMenu
_audioPlayer = audioPlayer
_playbackState = playbackState
_playbackTime = playbackTime
}
var body: some View {
@@ -40,7 +48,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, allowMenu: $allowMenu)
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, 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 {
@@ -60,7 +68,9 @@ struct ChatItemView: View {
default: nil
}
}
.flatMap { UIImage(base64Encoded: $0) }
.map { dropImagePrefix($0) }
.flatMap { Data(base64Encoded: $0) }
.flatMap { UIImage(data: $0) }
let adjustedMaxWidth = {
if let preview, preview.size.width <= preview.size.height {
maxWidth * 0.75
@@ -76,7 +86,10 @@ struct ChatItemView: View {
maxWidth: maxWidth,
imgWidth: adjustedMaxWidth,
videoWidth: adjustedMaxWidth,
allowMenu: $allowMenu
allowMenu: $allowMenu,
audioPlayer: $audioPlayer,
playbackState: $playbackState,
playbackTime: $playbackTime
)
}
}
+67 -65
View File
@@ -37,6 +37,7 @@ struct ChatView: View {
@State private var searchText: String = ""
@FocusState private var searchFocussed
// opening GroupMemberInfoView on member icon
@State private var membersLoaded = false
@State private var selectedMember: GMember? = nil
// opening GroupLinkView on link button (incognito)
@State private var showGroupLinkSheet: Bool = false
@@ -45,14 +46,9 @@ struct ChatView: View {
var body: some View {
if #available(iOS 16.0, *) {
let v = viewBody
viewBody
.scrollDismissesKeyboard(.immediately)
.keyboardPadding()
if (searchMode) {
v.toolbarBackground(.thinMaterial, for: .navigationBar)
} else {
v.toolbarBackground(.visible, for: .navigationBar)
}
} else {
viewBody
}
@@ -86,6 +82,7 @@ struct ChatView: View {
)
.disabled(!cInfo.sendMsgEnabled)
}
.padding(.top, 1)
.navigationTitle(cInfo.chatViewName)
.background(theme.colors.background)
.navigationBarTitleDisplayMode(.inline)
@@ -96,7 +93,6 @@ struct ChatView: View {
}
.onChange(of: chatModel.chatId) { cId in
showChatInfoSheet = false
stopAudioPlayer()
if let cId {
if let c = chatModel.getChat(cId) {
chat = c
@@ -118,7 +114,6 @@ 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) {
@@ -127,7 +122,7 @@ struct ChatView: View {
chatModel.reversedChatItems = []
chatModel.groupMembers = []
chatModel.groupMembersIndexes.removeAll()
chatModel.membersLoaded = false
membersLoaded = false
}
}
}
@@ -169,7 +164,7 @@ struct ChatView: View {
}
} else if case let .group(groupInfo) = cInfo {
Button {
Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
Task { await loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
} label: {
ChatInfoToolbar(chat: chat)
.tint(theme.colors.primary)
@@ -255,7 +250,19 @@ struct ChatView: View {
}
}
}
private func loadGroupMembers(_ groupInfo: GroupInfo, updateView: @escaping () -> Void = {}) async {
let groupMembers = await apiListMembers(groupInfo.groupId)
await MainActor.run {
if chatModel.chatId == groupInfo.id {
chatModel.groupMembers = groupMembers.map { GMember.init($0) }
chatModel.populateGroupMembersIndexes()
membersLoaded = true
updateView()
}
}
}
private func initChatView() {
let cInfo = chat.chatInfo
// This check prevents the call to apiContactInfo after the app is suspended, and the database is closed.
@@ -316,9 +323,8 @@ struct ChatView: View {
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(.thinMaterial)
}
private func voiceWithoutFrame(_ ci: ChatItem) -> Bool {
ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil && ci.meta.itemForwarded == nil
}
@@ -380,7 +386,7 @@ struct ChatView: View {
@ViewBuilder private func connectingText() -> some View {
if case let .direct(contact) = chat.chatInfo,
!contact.sndReady,
!contact.ready,
contact.active,
!contact.nextSendGrpInv {
Text("connecting…")
@@ -471,7 +477,9 @@ struct ChatView: View {
.foregroundColor(theme.colors.primary)
}
.onTapGesture {
scrollModel.scrollToBottom()
if let latestUnreadItem = filtered(chatModel.reversedChatItems).last(where: { $0.isRcvNew }) {
scrollModel.scrollToItem(id: latestUnreadItem.id)
}
}
} else if !counts.isNearBottom {
circleButton {
@@ -526,7 +534,7 @@ struct ChatView: View {
private func addMembersButton() -> some View {
Button {
if case let .group(gInfo) = chat.chatInfo {
Task { await chatModel.loadGroupMembers(gInfo) { showAddMembersSheet = true } }
Task { await loadGroupMembers(gInfo) { showAddMembersSheet = true } }
}
} label: {
Image(systemName: "person.crop.circle.badge.plus")
@@ -591,19 +599,16 @@ 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,
chatItem: ci,
maxWidth: maxWidth,
itemWidth: maxWidth,
composeState: $composeState,
selectedMember: $selectedMember,
revealedChatItem: $revealedChatItem
revealedChatItem: $revealedChatItem,
chatView: self
)
}
@@ -611,11 +616,13 @@ struct ChatView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
let chatItem: ChatItem
let maxWidth: CGFloat
var chatItem: ChatItem
var maxWidth: CGFloat
@State var itemWidth: CGFloat
@Binding var composeState: ComposeState
@Binding var selectedMember: GMember?
@Binding var revealedChatItem: ChatItem?
var chatView: ChatView
@State private var deletingItem: ChatItem? = nil
@State private var showDeleteMessage = false
@@ -627,6 +634,10 @@ 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 {
@@ -646,38 +657,23 @@ struct ChatView: View {
}
}
.onAppear {
if let range {
if let items = unreadItems(range) {
waitToMarkRead {
for ci in items {
await apiMarkChatItemRead(chat.chatInfo, ci)
}
}
}
} else if chatItem.isRcvNew {
waitToMarkRead {
await apiMarkChatItemRead(chat.chatInfo, chatItem)
}
}
markRead(
chatItems: range.flatMap { m.reversedChatItems[$0] }
?? [chatItem]
)
}
}
private func unreadItems(_ range: ClosedRange<Int>) -> [ChatItem]? {
let items = range.compactMap { i in
if i >= 0 && i < m.reversedChatItems.count {
let ci = m.reversedChatItems[i]
return if ci.isRcvNew { ci } else { nil }
} else {
return nil
}
}
return if items.isEmpty { nil } else { items }
}
private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) {
private func markRead(chatItems: Array<ChatItem>.SubSequence) {
let unreadItems = chatItems.filter { $0.isRcvNew }
if unreadItems.isEmpty { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
if m.chatId == chat.chatInfo.id {
Task(operation: op)
Task {
for unreadItem in unreadItems {
await apiMarkChatItemRead(chat.chatInfo, unreadItem)
}
}
}
}
}
@@ -694,12 +690,7 @@ struct ChatView: View {
if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil {
VStack(alignment: .leading, spacing: 4) {
if ci.content.showMemberName {
let t = if memCount == 1 && member.memberRole > .member {
Text(member.memberRole.text + " ").fontWeight(.semibold) + Text(member.displayName)
} else {
Text(memberNames(member, prevMember, memCount))
}
t
Text(memberNames(member, prevMember, memCount))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
@@ -709,11 +700,11 @@ struct ChatView: View {
HStack(alignment: .top, spacing: 8) {
ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize, backgroundColor: theme.colors.background)
.onTapGesture {
if m.membersLoaded {
if chatView.membersLoaded {
selectedMember = m.getGroupMember(member.groupMemberId)
} else {
Task {
await m.loadGroupMembers(groupInfo) {
await chatView.loadGroupMembers(groupInfo) {
selectedMember = m.getGroupMember(member.groupMemberId)
}
}
@@ -725,19 +716,19 @@ struct ChatView: View {
chatItemWithMenu(ci, range, maxWidth)
}
}
.padding(.bottom, 5)
.padding(.top, 5)
.padding(.trailing)
.padding(.leading, 12)
} else {
chatItemWithMenu(ci, range, maxWidth)
.padding(.bottom, 5)
.padding(.top, 5)
.padding(.trailing)
.padding(.leading, memberImageSize + 8 + 12)
}
} else {
chatItemWithMenu(ci, range, maxWidth)
.padding(.horizontal)
.padding(.bottom, 5)
.padding(.top, 5)
}
}
@@ -760,7 +751,10 @@ struct ChatView: View {
chatItem: ci,
maxWidth: maxWidth,
revealed: .constant(revealed),
allowMenu: $allowMenu
allowMenu: $allowMenu,
audioPlayer: $audioPlayer,
playbackState: $playbackState,
playbackTime: $playbackTime
)
.modifier(ChatItemClipped(ci))
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
@@ -787,6 +781,14 @@ 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
}) {
@@ -1097,7 +1099,7 @@ struct ChatView: View {
chatItemInfo = ciInfo
}
if case let .group(gInfo) = chat.chatInfo {
await m.loadGroupMembers(gInfo)
await chatView.loadGroupMembers(gInfo)
}
} catch let error {
logger.error("apiGetChatItemInfo error: \(responseError(error))")
@@ -32,8 +32,9 @@ struct ComposeFileView: View {
}
.padding(.vertical, 1)
.padding(.trailing, 12)
.frame(height: 54)
.frame(height: 50)
.background(theme.appColors.sentMessage)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
}
@@ -18,7 +18,10 @@ struct ComposeImageView: View {
var body: some View {
HStack(alignment: .center, spacing: 8) {
let imgs: [UIImage] = images.compactMap { image in
UIImage(base64Encoded: image)
if let data = Data(base64Encoded: dropImagePrefix(image)) {
return UIImage(data: data)
}
return nil
}
if imgs.count == 0 {
ProgressView()
@@ -46,8 +49,8 @@ struct ComposeImageView: View {
.padding(.vertical, 1)
.padding(.trailing, 12)
.background(theme.appColors.sentMessage)
.frame(minHeight: 54)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
}
@@ -10,6 +10,35 @@ 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?
@@ -34,13 +63,14 @@ struct ComposeLinkView: View {
.padding(.vertical, 1)
.padding(.trailing, 12)
.background(theme.appColors.sentMessage)
.frame(minHeight: 54)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
private func linkPreviewView(_ linkPreview: LinkPreview) -> some View {
HStack(alignment: .center, spacing: 8) {
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
if let data = Data(base64Encoded: dropImagePrefix(linkPreview.image)),
let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
@@ -284,10 +284,8 @@ struct ComposeView: View {
var body: some View {
VStack(spacing: 0) {
Divider()
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
ContextInvitingContactMemberView()
Divider()
}
// preference checks should match checks in forwarding list
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
@@ -295,13 +293,10 @@ struct ComposeView: View {
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
if simplexLinkProhibited {
msgNotAllowedView("SimpleX links not allowed", icon: "link")
Divider()
} else if fileProhibited {
msgNotAllowedView("Files and media not allowed", icon: "doc")
Divider()
} else if voiceProhibited {
msgNotAllowedView("Voice messages not allowed", icon: "mic")
Divider()
}
contextItemView()
switch (composeState.editing, composeState.preview) {
@@ -364,6 +359,7 @@ struct ComposeView: View {
: theme.colors.primary
)
.padding(.trailing, 12)
.background(theme.colors.background)
.disabled(!chat.userCanSend)
if chat.userIsObserver {
@@ -381,7 +377,6 @@ struct ComposeView: View {
}
}
}
.background(.thinMaterial)
.onChange(of: composeState.message) { msg in
if composeState.linkPreviewAllowed {
if msg.count > 0 {
@@ -630,7 +625,6 @@ struct ComposeView: View {
cancelPreview: cancelLinkPreview,
cancelEnabled: !composeState.inProgress
)
Divider()
case let .mediaPreviews(mediaPreviews: media):
ComposeImageView(
images: media.map { (img, _) in img },
@@ -639,7 +633,6 @@ struct ComposeView: View {
chosenMedia = []
},
cancelEnabled: !composeState.editing && !composeState.inProgress)
Divider()
case let .voicePreview(recordingFileName, _):
ComposeVoiceView(
recordingFileName: recordingFileName,
@@ -652,7 +645,6 @@ struct ComposeView: View {
cancelEnabled: !composeState.editing && !composeState.inProgress,
stopPlayback: $stopPlayback
)
Divider()
case let .filePreview(fileName, _):
ComposeFileView(
fileName: fileName,
@@ -660,7 +652,6 @@ struct ComposeView: View {
composeState = composeState.copy(preview: .noPreview)
},
cancelEnabled: !composeState.editing && !composeState.inProgress)
Divider()
}
}
@@ -670,9 +661,10 @@ struct ComposeView: View {
Text(reason).italic()
}
.padding(12)
.frame(minHeight: 54)
.frame(minHeight: 50)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.padding(.top, 8)
}
@ViewBuilder private func contextItemView() -> some View {
@@ -686,7 +678,6 @@ struct ComposeView: View {
contextIcon: "arrowshape.turn.up.left",
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
)
Divider()
case let .editingItem(chatItem: editingItem):
ContextItemView(
chat: chat,
@@ -694,7 +685,6 @@ struct ComposeView: View {
contextIcon: "pencil",
cancelContextItem: { clearState() }
)
Divider()
case let .forwardingItem(chatItem: forwardedItem, _):
ContextItemView(
chat: chat,
@@ -703,7 +693,6 @@ struct ComposeView: View {
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) },
showSender: false
)
Divider()
}
}
@@ -860,7 +849,6 @@ struct ComposeView: View {
func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
let (image, data) = imageData
if case let .video(_, url, duration) = data, let savedFile = moveTempFileFromURL(url) {
ChatModel.shared.filesToDelete.remove(url)
return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live, ttl: ttl)
}
return nil
@@ -51,8 +51,8 @@ struct ComposeVoiceView: View {
.padding(.vertical, 1)
.frame(height: ComposeVoiceView.previewHeight)
.background(theme.appColors.sentMessage)
.frame(minHeight: 54)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
private func recordingMode() -> some View {
@@ -18,9 +18,10 @@ struct ContextInvitingContactMemberView: View {
Text("Send direct message to connect")
}
.padding(12)
.frame(minHeight: 54)
.frame(minHeight: 50)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial)
.background(theme.appColors.sentMessage)
.padding(.top, 8)
}
}
@@ -43,9 +43,10 @@ struct ContextItemView: View {
.tint(theme.colors.primary)
}
.padding(12)
.frame(minHeight: 54)
.frame(minHeight: 50)
.frame(maxWidth: .infinity)
.background(chatItemFrameColor(contextItem, theme))
.padding(.top, 8)
}
private func msgContentView(lines: Int) -> some View {
@@ -44,7 +44,6 @@ struct SendMessageView: View {
var body: some View {
ZStack {
let composeShape = RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
HStack(alignment: .bottom) {
ZStack(alignment: .leading) {
if case .voicePreview = composeState.preview {
@@ -67,6 +66,7 @@ struct SendMessageView: View {
.fixedSize(horizontal: false, vertical: true)
}
}
if progressByTimeout {
ProgressView()
.scaleEffect(1.4)
@@ -84,9 +84,10 @@ struct SendMessageView: View {
}
}
.padding(.vertical, 1)
.background(theme.colors.background)
.clipShape(composeShape)
.overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7))
.overlay(
RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)
)
}
.onChange(of: composeState.message, perform: { text in updateFont(text) })
.onChange(of: composeState.inProgress) { inProgress in
@@ -35,7 +35,7 @@ struct AddGroupMembersViewCommon: View {
private enum AddGroupMembersAlert: Identifiable {
case prohibitedToInviteIncognito
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
switch self {
@@ -122,7 +122,7 @@ struct AddGroupMembersViewCommon: View {
message: Text("You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile")
)
case let .error(title, error):
return mkAlert(title: title, message: error)
return Alert(title: Text(title), message: Text(error))
}
}
.onChange(of: selectedContacts) { _ in
@@ -40,7 +40,7 @@ struct GroupChatInfoView: View {
case blockForAllAlert(mem: GroupMember)
case unblockForAllAlert(mem: GroupMember)
case removeMemberAlert(mem: GroupMember)
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
case error(title: LocalizedStringKey, error: LocalizedStringKey)
var id: String {
switch self {
@@ -158,7 +158,7 @@ struct GroupChatInfoView: View {
case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem)
case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem)
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
case let .error(title, error): return mkAlert(title: title, message: error)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
}
}
.onAppear {
@@ -22,7 +22,7 @@ struct GroupLinkView: View {
private enum GroupLinkAlert: Identifiable {
case deleteLink
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
switch self {
@@ -113,7 +113,7 @@ struct GroupLinkView: View {
}, secondaryButton: .cancel()
)
case let .error(title, error):
return mkAlert(title: title, message: error)
return Alert(title: Text(title), message: Text(error))
}
}
.onChange(of: groupLinkMemberRole) { _ in
@@ -37,7 +37,7 @@ struct GroupMemberInfoView: View {
case syncConnectionForceAlert
case planAndConnectAlert(alert: PlanAndConnectAlert)
case queueInfo(info: String)
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
case error(title: LocalizedStringKey, error: LocalizedStringKey)
var id: String {
switch self {
@@ -76,152 +76,154 @@ struct GroupMemberInfoView: View {
private func groupMemberInfoView() -> some View {
ZStack {
let member = groupMember.wrapped
List {
groupMemberInfoHeader(member)
.listRowBackground(Color.clear)
VStack {
let member = groupMember.wrapped
List {
groupMemberInfoHeader(member)
.listRowBackground(Color.clear)
if member.memberActive {
Section {
if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) {
knownDirectChatButton(chat)
} else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
if let contactId = member.memberContactId {
newDirectChatButton(contactId)
} else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false {
createMemberContactButton()
if member.memberActive {
Section {
if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) {
knownDirectChatButton(chat)
} else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
if let contactId = member.memberContactId {
newDirectChatButton(contactId)
} else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false {
createMemberContactButton()
}
}
if let code = connectionCode { verifyCodeButton(code) }
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
}
// } else if developerTools {
// synchronizeConnectionButtonForce()
// }
}
if let code = connectionCode { verifyCodeButton(code) }
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
}
// } else if developerTools {
// synchronizeConnectionButtonForce()
// }
}
}
if let contactLink = member.contactLink {
Section {
SimpleXLinkQRCode(uri: contactLink)
Button {
showShareSheet(items: [simplexChatLink(contactLink)])
} label: {
Label("Share address", systemImage: "square.and.arrow.up")
}
if let contactId = member.memberContactId {
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
if let contactLink = member.contactLink {
Section {
SimpleXLinkQRCode(uri: contactLink)
Button {
showShareSheet(items: [simplexChatLink(contactLink)])
} label: {
Label("Share address", systemImage: "square.and.arrow.up")
}
if let contactId = member.memberContactId {
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
connectViaAddressButton(contactLink)
}
} else {
connectViaAddressButton(contactLink)
}
} else {
connectViaAddressButton(contactLink)
} header: {
Text("Address")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
.foregroundColor(theme.colors.secondary)
}
} header: {
Text("Address")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
.foregroundColor(theme.colors.secondary)
}
}
Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
infoRow("Group", groupInfo.displayName)
Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
infoRow("Group", groupInfo.displayName)
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
Picker("Change role", selection: $newRole) {
ForEach(roles) { role in
Text(role.text)
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
Picker("Change role", selection: $newRole) {
ForEach(roles) { role in
Text(role.text)
}
}
.frame(height: 36)
} else {
infoRow("Role", member.memberRole.text)
}
.frame(height: 36)
} else {
infoRow("Role", member.memberRole.text)
}
}
if let connStats = connectionStats {
Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
// TODO network connection status
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
if let connStats = connectionStats {
Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
// TODO network connection status
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|| connStats.ratchetSyncSendProhibited
)
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary)
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary)
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary)
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary)
}
}
if groupInfo.membership.memberRole >= .admin {
adminDestructiveSection(member)
} else {
nonAdminBlockSection(member)
}
if groupInfo.membership.memberRole >= .admin {
adminDestructiveSection(member)
} else {
nonAdminBlockSection(member)
}
if developerTools {
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
infoRow("Local name", member.localDisplayName)
infoRow("Database ID", "\(member.groupMemberId)")
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
}
Button ("Debug delivery") {
Task {
do {
let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId))
await MainActor.run { alert = .queueInfo(info: info) }
} catch let e {
logger.error("apiContactQueueInfo error: \(responseError(e))")
let a = getErrorAlert(e, "Error")
await MainActor.run { alert = .error(title: a.title, error: a.message) }
if developerTools {
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
infoRow("Local name", member.localDisplayName)
infoRow("Database ID", "\(member.groupMemberId)")
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
}
Button ("Debug delivery") {
Task {
do {
let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId))
await MainActor.run { alert = .queueInfo(info: info) }
} catch let e {
logger.error("apiContactQueueInfo error: \(responseError(e))")
let a = getErrorAlert(e, "Error")
await MainActor.run { alert = .error(title: a.title, error: a.message) }
}
}
}
}
}
}
}
.navigationBarHidden(true)
.onAppear {
if #unavailable(iOS 16) {
// this condition prevents re-setting picker
if !justOpened { return }
}
justOpened = false
DispatchQueue.main.async {
newRole = member.memberRole
do {
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
_ = chatModel.upsertGroupMember(groupInfo, mem)
connectionStats = stats
connectionCode = code
} catch let error {
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
.navigationBarHidden(true)
.onAppear {
if #unavailable(iOS 16) {
// this condition prevents re-setting picker
if !justOpened { return }
}
justOpened = false
DispatchQueue.main.async {
newRole = member.memberRole
do {
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
_ = chatModel.upsertGroupMember(groupInfo, mem)
connectionStats = stats
connectionCode = code
} catch let error {
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
}
}
}
}
.onChange(of: newRole) { newRole in
if newRole != member.memberRole {
alert = .changeMemberRoleAlert(mem: member, role: newRole)
.onChange(of: newRole) { newRole in
if newRole != member.memberRole {
alert = .changeMemberRoleAlert(mem: member, role: newRole)
}
}
.onChange(of: member.memberRole) { role in
newRole = role
}
}
.onChange(of: member.memberRole) { role in
newRole = role
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.alert(item: $alert) { alertItem in
@@ -237,7 +239,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 mkAlert(title: title, message: error)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
}
}
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
+1 -2
View File
@@ -129,9 +129,8 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
from: nil,
for: nil
)
NotificationCenter.default.post(name: .chatViewWillBeginScrolling, object: nil)
}
/// Scrolls up
func scrollToNextPage() {
tableView.setContentOffset(
@@ -9,34 +9,25 @@
import SwiftUI
import SimpleXChat
let dynamicSizes: [
DynamicTypeSize: (
rowHeight: CGFloat,
profileImageSize: CGFloat,
mediaSize: CGFloat,
chatInfoSize: CGFloat,
unreadCorner: CGFloat,
unreadPadding: CGFloat
)
] = [
.xSmall: (68, 55, 33, 18, 9, 3),
.small: (72, 57, 34, 18, 9, 3),
.medium: (76, 60, 36, 18, 10, 4),
.large: (80, 63, 38, 20, 10, 4),
.xLarge: (88, 67, 41, 20, 10, 4),
.xxLarge: (94, 71, 44, 22, 11, 4),
.xxxLarge: (104, 75, 48, 24, 12, 5),
.accessibility1: (104, 75, 48, 24, 12, 5),
.accessibility2: (114, 75, 48, 24, 12, 5),
.accessibility3: (124, 75, 48, 24, 12, 5),
.accessibility4: (134, 75, 48, 24, 12, 5),
.accessibility5: (144, 75, 48, 24, 12, 5)
private let rowHeights: [DynamicTypeSize: CGFloat] = [
.xSmall: 68,
.small: 72,
.medium: 76,
.large: 80,
.xLarge: 88,
.xxLarge: 94,
.xxxLarge: 104,
.accessibility1: 90,
.accessibility2: 100,
.accessibility3: 120,
.accessibility4: 130,
.accessibility5: 140
]
struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@ObservedObject var chat: Chat
@State private var showContactRequestDialog = false
@State private var showJoinGroupDialog = false
@@ -47,8 +38,6 @@ struct ChatListNavLink: View {
@State private var inProgress = false
@State private var progressByTimeout = false
var dynamicRowHeight: CGFloat { dynamicSizes[userFont]?.rowHeight ?? 80 }
var body: some View {
Group {
switch chat.chatInfo {
@@ -81,7 +70,7 @@ struct ChatListNavLink: View {
Group {
if contact.activeConn == nil && contact.profile.contactLink != nil {
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
.frame(height: dynamicRowHeight)
.frame(height: rowHeights[dynamicTypeSize])
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
showDeleteContactActionSheet = true
@@ -111,7 +100,7 @@ struct ChatListNavLink: View {
clearChatButton()
}
Button {
if contact.sndReady || !contact.active {
if contact.ready || !contact.active {
showDeleteContactActionSheet = true
} else {
AlertManager.shared.showAlert(deletePendingContactAlert(chat, contact))
@@ -121,11 +110,11 @@ struct ChatListNavLink: View {
}
.tint(.red)
}
.frame(height: dynamicRowHeight)
.frame(height: rowHeights[dynamicTypeSize])
}
}
.actionSheet(isPresented: $showDeleteContactActionSheet) {
if contact.sndReady && contact.active {
if contact.ready && contact.active {
return ActionSheet(
title: Text("Delete contact?\nThis cannot be undone!"),
buttons: [
@@ -150,7 +139,7 @@ struct ChatListNavLink: View {
switch (groupInfo.membership.memberStatus) {
case .memInvited:
ChatPreviewView(chat: chat, progressByTimeout: $progressByTimeout)
.frame(height: dynamicRowHeight)
.frame(height: rowHeights[dynamicTypeSize])
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
joinGroupButton()
if groupInfo.canDelete {
@@ -170,7 +159,7 @@ struct ChatListNavLink: View {
.disabled(inProgress)
case .memAccepted:
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
.frame(height: dynamicRowHeight)
.frame(height: rowHeights[dynamicTypeSize])
.onTapGesture {
AlertManager.shared.showAlert(groupInvitationAcceptedAlert())
}
@@ -189,7 +178,7 @@ struct ChatListNavLink: View {
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) },
disabled: !groupInfo.ready
)
.frame(height: dynamicRowHeight)
.frame(height: rowHeights[dynamicTypeSize])
.swipeActions(edge: .leading, allowsFullSwipe: true) {
markReadButton()
toggleFavoriteButton()
@@ -216,7 +205,7 @@ struct ChatListNavLink: View {
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) },
disabled: !noteFolder.ready
)
.frame(height: dynamicRowHeight)
.frame(height: rowHeights[dynamicTypeSize])
.swipeActions(edge: .leading, allowsFullSwipe: true) {
markReadButton()
}
@@ -332,7 +321,7 @@ struct ChatListNavLink: View {
}
.tint(.red)
}
.frame(height: dynamicRowHeight)
.frame(height: rowHeights[dynamicTypeSize])
.onTapGesture { showContactRequestDialog = true }
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } }
@@ -360,7 +349,7 @@ struct ChatListNavLink: View {
}
.tint(theme.colors.primary)
}
.frame(height: dynamicRowHeight)
.frame(height: rowHeights[dynamicTypeSize])
.appSheet(isPresented: $showContactConnectionInfo) {
Group {
if case let .contactConnection(contactConnection) = chat.chatInfo {
@@ -480,7 +469,7 @@ struct ChatListNavLink: View {
Text("invalid chat data")
.foregroundColor(.red)
.padding(4)
.frame(height: dynamicRowHeight)
.frame(height: rowHeights[dynamicTypeSize])
.onTapGesture { showInvalidJSON = true }
.appSheet(isPresented: $showInvalidJSON) {
invalidJSONView(json)
@@ -575,11 +564,18 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) {
}
}
struct ErrorAlert {
var title: LocalizedStringKey
var message: LocalizedStringKey
}
func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert {
if let r = error as? ChatResponse,
let alert = getNetworkErrorAlert(r) {
return alert
} else {
switch error as? ChatResponse {
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.")
default:
return ErrorAlert(title: title, message: "Error: \(responseError(error))")
}
}
@@ -21,7 +21,6 @@ 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 {
@@ -163,10 +162,6 @@ 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)
@@ -222,11 +217,6 @@ 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 }
@@ -277,25 +267,31 @@ struct ChatListView: View {
struct SubsStatusIndicator: View {
@State private var subs: SMPServerSubs = SMPServerSubs.newSMPServerSubs
@State private var hasSess: Bool = false
@State private var sess: ServerSessions = ServerSessions.newServerSessions
@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
// Constants for the intervals
let initialInterval: TimeInterval = 1.0
let regularInterval: TimeInterval = 3.0
let initialPhaseDuration: TimeInterval = 10.0 // Duration for initial phase in seconds
var body: some View {
Button {
showServersSummary = true
} label: {
HStack(spacing: 4) {
SubscriptionStatusIndicatorView(subs: subs, hasSess: hasSess)
SubscriptionStatusIndicatorView(subs: subs, sess: sess)
if showSubscriptionPercentage {
SubscriptionStatusPercentageView(subs: subs, hasSess: hasSess)
SubscriptionStatusPercentageView(subs: subs, sess: sess)
}
}
}
.onAppear {
startTimer()
startInitialTimer()
}
.onDisappear {
stopTimer()
@@ -305,24 +301,35 @@ struct SubsStatusIndicator: View {
}
}
private func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if AppChatState.shared.value == .active {
getSubsTotal()
private func startInitialTimer() {
timer = Timer.scheduledTimer(withTimeInterval: initialInterval, repeats: true) { _ in
getServersSummary()
timerCounter += 1
// Switch to the regular timer after the initial phase
if timerCounter * Int(initialInterval) >= Int(initialPhaseDuration) {
switchToRegularTimer()
}
}
}
func switchToRegularTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: regularInterval, repeats: true) { _ in
getServersSummary()
}
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
private func getSubsTotal() {
private func getServersSummary() {
do {
(subs, hasSess) = try getAgentSubsTotal()
let summ = try getAgentServersSummary()
(subs, sess) = (summ.allUsersSMP.smpTotals.subs, summ.allUsersSMP.smpTotals.sessions)
} catch let error {
logger.error("getSubsTotal error: \(responseError(error))")
logger.error("getAgentServersSummary error: \(responseError(error))")
}
}
}
@@ -12,24 +12,18 @@ import SimpleXChat
struct ChatPreviewView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
@ObservedObject var chat: Chat
@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
var dynamicMediaSize: CGFloat { dynamicSizes[userFont]?.mediaSize ?? 36 }
var dynamicChatInfoSize: CGFloat { dynamicSizes[userFont]?.chatInfoSize ?? 18 }
var body: some View {
let cItem = chat.chatItems.last
return HStack(spacing: 8) {
ZStack(alignment: .bottomTrailing) {
ChatInfoImage(chat: chat, size: dynamicSizes[userFont]?.profileImageSize ?? 63)
ChatInfoImage(chat: chat, size: 63)
chatPreviewImageOverlayIcon()
.padding([.bottom, .trailing], 1)
}
@@ -49,38 +43,11 @@ struct ChatPreviewView: View {
.padding(.horizontal, 8)
ZStack(alignment: .topTrailing) {
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)
}
chatMessagePreview(cItem)
chatStatusImage()
.padding(.top, 26)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.trailing, 8)
Spacer()
@@ -90,33 +57,6 @@ 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
}
}
}
@@ -173,50 +113,39 @@ struct ChatPreviewView: View {
.kerning(-2)
}
private func chatPreviewLayout(_ text: Text?, draft: Bool = false, _ hasFilePreview: Bool = false) -> some View {
private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View {
ZStack(alignment: .topTrailing) {
let t = text
.lineLimit(userFont <= .xxxLarge ? 2 : 1)
.lineLimit(2)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.leading, hasFilePreview ? 0 : 8)
.padding(.trailing, hasFilePreview ? 38 : 36)
.offset(x: hasFilePreview ? -2 : 0)
.fixedSize(horizontal: false, vertical: true)
.padding(.leading, 8)
.padding(.trailing, 36)
if !showChatPreviews && !draft {
t.privacySensitive(true).redacted(reason: .privacy)
} else {
t
}
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(userFont <= .xxxLarge ? .caption : .caption2)
.foregroundColor(.white)
.padding(.horizontal, dynamicSizes[userFont]?.unreadPadding ?? 4)
.frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize)
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary)
.cornerRadius(dynamicSizes[userFont]?.unreadCorner ?? 10)
} else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local {
Image(systemName: "speaker.slash.fill")
.resizable()
.scaledToFill()
.frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize)
.foregroundColor(theme.colors.secondary)
} else if chat.chatInfo.chatSettings?.favorite ?? false {
Image(systemName: "star.fill")
.resizable()
.scaledToFill()
.frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize)
.padding(.trailing, 1)
.foregroundColor(theme.colors.secondary.opacity(0.65))
} else {
Color.clear.frame(width: 0)
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))
}
}
}
@@ -243,7 +172,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: nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), 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
@@ -267,18 +196,18 @@ struct ChatPreviewView: View {
}
}
@ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?, _ hasFilePreview: Bool = false) -> some View {
@ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View {
if chatModel.draftChatId == chat.id, let draft = chatModel.draft {
chatPreviewLayout(messageDraft(draft), draft: true, hasFilePreview)
chatPreviewLayout(messageDraft(draft), draft: true)
} else if let cItem = cItem {
chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem), hasFilePreview)
chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem))
} 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.sndReady && contact.activeConn != nil {
} else if !contact.ready && contact.activeConn != nil {
if contact.nextSendGrpInv {
chatPreviewInfoText("send direct message")
} else if contact.active {
@@ -296,54 +225,6 @@ struct ChatPreviewView: View {
}
}
@ViewBuilder func chatItemContentPreview(_ chat: Chat, _ ci: ChatItem) -> some View {
let mc = ci.content.msgContent
switch mc {
case let .link(_, preview):
smallContentPreview(size: dynamicMediaSize) {
ZStack(alignment: .topTrailing) {
Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: dynamicMediaSize, height: dynamicMediaSize)
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(size: dynamicMediaSize) {
CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
.environmentObject(ReverseListScrollModel<ChatItem>())
}
case let .video(_,image, duration):
smallContentPreview(size: dynamicMediaSize) {
CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
.environmentObject(ReverseListScrollModel<ChatItem>())
}
case let .voice(_, duration):
smallContentPreviewVoice(size: dynamicMediaSize) {
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: Binding.constant(true), smallViewSize: dynamicMediaSize)
}
case .file:
smallContentPreviewFile(size: dynamicMediaSize) {
CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallViewSize: dynamicMediaSize)
}
default: EmptyView()
}
}
@ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View {
groupInfo.membership.memberIncognito
? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)")
@@ -413,45 +294,10 @@ struct ChatPreviewView: View {
}
}
func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View {
view()
.frame(width: size, height: size)
.cornerRadius(8)
.overlay(RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))
.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true))
.padding(.vertical, size / 6)
.padding(.leading, 3)
.offset(x: 6)
}
func smallContentPreviewVoice(size: CGFloat, _ view: @escaping () -> some View) -> some View {
view()
.frame(height: voiceMessageSizeBasedOnSquareSize(size))
.padding(.vertical, size / 6)
.padding(.leading, 8)
}
func smallContentPreviewFile(size: CGFloat, _ view: @escaping () -> some View) -> some View {
view()
.frame(width: size, height: size)
.padding(.vertical, size / 7)
.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 {
@@ -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 mkAlert(title: title, message: error)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
}
}
.onAppear {
@@ -12,13 +12,12 @@ import SimpleXChat
struct ContactRequestView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
var contactRequest: UserContactRequest
@ObservedObject var chat: Chat
var body: some View {
HStack(spacing: 8) {
ChatInfoImage(chat: chat, size: dynamicSizes[userFont]?.profileImageSize ?? 63)
ChatInfoImage(chat: chat, size: 63)
.padding(.leading, 4)
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top) {
@@ -11,7 +11,6 @@ import SimpleXChat
struct ServersSummaryView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@State private var serversSummary: PresentedServersSummary? = nil
@State private var selectedUserCategory: PresentedUserCategory = .allUsers
@State private var selectedServerType: PresentedServerType = .smp
@@ -59,21 +58,11 @@ struct ServersSummaryView: View {
private func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if AppChatState.shared.value == .active {
getServersSummary()
}
getServersSummary()
}
}
private func getServersSummary() {
do {
serversSummary = try getAgentServersSummary()
} catch let error {
logger.error("getAgentServersSummary error: \(responseError(error))")
}
}
private func stopTimer() {
func stopTimer() {
timer?.invalidate()
timer = nil
}
@@ -102,8 +91,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 profiles").tag(PresentedUserCategory.allUsers)
Text("Current profile").tag(PresentedUserCategory.currentUser)
Text("All users").tag(PresentedUserCategory.allUsers)
Text("Current user").tag(PresentedUserCategory.currentUser)
}
.pickerStyle(.segmented)
}
@@ -194,22 +183,19 @@ struct ServersSummaryView: View {
}
} else {
Text("No info, try to reload")
.foregroundColor(theme.colors.secondary)
.background(theme.colors.background)
}
}
private func smpSubsSection(_ totals: SMPTotals) -> some View {
Section {
infoRow("Active connections", numOrDash(totals.subs.ssActive))
infoRow("Connections subscribed", numOrDash(totals.subs.ssActive))
infoRow("Total", numOrDash(totals.subs.total))
Toggle("Show percentage", isOn: $showSubscriptionPercentage)
} header: {
HStack {
Text("Message reception")
SubscriptionStatusIndicatorView(subs: totals.subs, hasSess: totals.sessions.hasSess)
Text("Message subscriptions")
SubscriptionStatusIndicatorView(subs: totals.subs, sess: totals.sessions)
if showSubscriptionPercentage {
SubscriptionStatusPercentageView(subs: totals.subs, hasSess: totals.sessions.hasSess)
SubscriptionStatusPercentageView(subs: totals.subs, sess: totals.sessions)
}
}
}
@@ -287,9 +273,9 @@ struct ServersSummaryView: View {
if let subs = srvSumm.subs {
Spacer()
if showSubscriptionPercentage {
SubscriptionStatusPercentageView(subs: subs, hasSess: srvSumm.sessionsOrNew.hasSess)
SubscriptionStatusPercentageView(subs: subs, sess: srvSumm.sessionsOrNew)
}
SubscriptionStatusIndicatorView(subs: subs, hasSess: srvSumm.sessionsOrNew.hasSess)
SubscriptionStatusIndicatorView(subs: subs, sess: srvSumm.sessionsOrNew)
} else if let sess = srvSumm.sessions {
Spacer()
Image(systemName: "arrow.up.circle")
@@ -403,16 +389,24 @@ struct ServersSummaryView: View {
Text("Reset all statistics")
}
}
private func getServersSummary() {
do {
serversSummary = try getAgentServersSummary()
} catch let error {
logger.error("getAgentServersSummary error: \(responseError(error))")
}
}
}
struct SubscriptionStatusIndicatorView: View {
@EnvironmentObject var m: ChatModel
var subs: SMPServerSubs
var hasSess: Bool
var sess: ServerSessions
var body: some View {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess)
if #available(iOS 16.0, *) {
Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue)
.foregroundColor(color)
@@ -426,18 +420,18 @@ struct SubscriptionStatusIndicatorView: View {
struct SubscriptionStatusPercentageView: View {
@EnvironmentObject var m: ChatModel
var subs: SMPServerSubs
var hasSess: Bool
var sess: ServerSessions
var body: some View {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess)
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
.foregroundColor(.secondary)
.font(.caption)
}
}
func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ hasSess: Bool) -> (Color, Double, Double, Double) {
func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ sess: ServerSessions) -> (Color, Double, Double, Double) {
func roundedToQuarter(_ n: Double) -> Double {
n >= 1 ? 1
: n <= 0 ? 0
@@ -452,12 +446,12 @@ func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHos
? (
subs.ssActive == 0
? (
hasSess ? (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) : noConnColorAndPercent
sess.ssConnected == 0 ? noConnColorAndPercent : (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
)
: ( // ssActive > 0
hasSess
? (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
: (.orange, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) // This would mean implementation error
sess.ssConnected == 0
? (.orange, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) // This would mean implementation error
: (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
)
)
: noConnColorAndPercent
@@ -503,16 +497,16 @@ struct SMPServerSummaryView: View {
private func smpSubsSection(_ subs: SMPServerSubs) -> some View {
Section {
infoRow("Active connections", numOrDash(subs.ssActive))
infoRow("Connections subscribed", numOrDash(subs.ssActive))
infoRow("Pending", numOrDash(subs.ssPending))
infoRow("Total", numOrDash(subs.total))
reconnectButton()
} header: {
HStack {
Text("Message reception")
SubscriptionStatusIndicatorView(subs: subs, hasSess: summary.sessionsOrNew.hasSess)
Text("Message subscriptions")
SubscriptionStatusIndicatorView(subs: subs, sess: summary.sessionsOrNew)
if showSubscriptionPercentage {
SubscriptionStatusPercentageView(subs: subs, hasSess: summary.sessionsOrNew.hasSess)
SubscriptionStatusPercentageView(subs: subs, sess: summary.sessionsOrNew)
}
}
}
@@ -617,7 +611,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("Connections") {
Section {
infoRow("Created", numOrDash(stats._connCreated))
infoRow("Secured", numOrDash(stats._connCreated))
infoRow("Completed", numOrDash(stats._connCompleted))
@@ -626,12 +620,8 @@ 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("Connection notifications")
Text("Connections")
} footer: {
Text("Starting from \(localTimestamp(statsStartedAt)).")
}
@@ -64,7 +64,7 @@ struct DatabaseErrorView: View {
case let .migrationError(mtrError):
titleText("Incompatible database version")
fileNameText(dbFile)
Text("Error: ") + Text(mtrErrorDescription(mtrError))
Text("Error: ") + Text(DatabaseErrorView.mtrErrorDescription(mtrError))
}
case let .errorSQL(dbFile, migrationSQLError):
titleText("Database error")
@@ -105,6 +105,15 @@ struct DatabaseErrorView: View {
Text("Migrations: \(ms.joined(separator: ", "))")
}
static func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey {
switch err {
case let .noDown(dbMigrations):
return "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))"
case let .different(appMigration, dbMigration):
return "different migration in the app/database: \(appMigration) / \(dbMigration)"
}
}
private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View {
PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit)
}
@@ -16,10 +16,18 @@ 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: chatIconName(chat.chatInfo),
iconName: iconName,
size: size,
color: iconColor
)
@@ -23,10 +23,8 @@ 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 * 2.5 // scale wallpaper for iOS
let scale = imageScale * 1.57 // for some reason a wallpaper on iOS looks smaller than on Android
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)
@@ -21,7 +21,9 @@ struct ProfileImage: View {
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
var body: some View {
if let uiImage = UIImage(base64Encoded: imageStr) {
if let image = imageStr,
let data = Data(base64Encoded: dropImagePrefix(image)),
let uiImage = UIImage(data: data) {
clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius)
} else {
let c = color.asAnotherColorFromSecondaryVariant(theme)
@@ -0,0 +1,26 @@
//
// 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
}
@@ -17,37 +17,3 @@ 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
}
}
}
@@ -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(responseError(error)))
alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(String(describing: error)))
}
}
}
@@ -331,7 +331,7 @@ struct MigrateToDevice: View {
case let .migrationError(mtrError):
("Incompatible database version",
nil,
"\(NSLocalizedString("Error: ", comment: "")) \(mtrErrorDescription(mtrError))",
"\(NSLocalizedString("Error: ", comment: "")) \(DatabaseErrorView.mtrErrorDescription(mtrError))",
nil)
}
default: ("Error", nil, "Unknown error", nil)
@@ -37,7 +37,7 @@ struct ConnectDesktopView: View {
case badInvitationError
case badVersionError(version: String?)
case desktopDisconnectedError
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
switch self {
@@ -160,7 +160,7 @@ struct ConnectDesktopView: View {
case .desktopDisconnectedError:
Alert(title: Text("Connection terminated"))
case let .error(title, error):
mkAlert(title: title, message: error)
Alert(title: Text(title), message: Text(error))
}
}
.interactiveDismissDisabled(m.activeRemoteCtrl)
@@ -32,7 +32,6 @@ 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) }
@@ -63,7 +62,6 @@ 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)
@@ -32,6 +32,7 @@ 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
@@ -61,6 +62,8 @@ 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) }
}
@@ -22,7 +22,6 @@ 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
@@ -114,22 +113,6 @@ 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)
}
@@ -15,6 +15,7 @@ struct ProtocolServerView: View {
let serverProtocol: ServerProtocol
@Binding var server: ServerCfg
@State var serverToEdit: ServerCfg
@State var serverEnabled: Bool
@State private var showTestFailure = false
@State private var testing = false
@State private var testFailure: ProtocolTestFailure?
@@ -112,10 +113,10 @@ struct ProtocolServerView: View {
Spacer()
showTestStatus(server: serverToEdit)
}
let useForNewDisabled = serverToEdit.tested != true && !serverToEdit.preset
Toggle("Use for new connections", isOn: $serverToEdit.enabled)
.disabled(useForNewDisabled)
.foregroundColor(useForNewDisabled ? theme.colors.secondary : theme.colors.onBackground)
Toggle("Use for new connections", isOn: $serverEnabled)
.onChange(of: serverEnabled) { enabled in
serverToEdit.enabled = enabled ? .enabled : .disabled
}
}
}
}
@@ -175,12 +176,17 @@ func testServerConnection(server: Binding<ServerCfg>) async -> ProtocolTestFailu
}
}
func serverHostname(_ srv: String) -> String {
parseServerAddress(srv)?.hostnames.first ?? srv
}
struct ProtocolServerView_Previews: PreviewProvider {
static var previews: some View {
ProtocolServerView(
serverProtocol: .smp,
server: Binding.constant(ServerCfg.sampleData.custom),
serverToEdit: ServerCfg.sampleData.custom
serverToEdit: ServerCfg.sampleData.custom,
serverEnabled: true
)
}
}
@@ -18,9 +18,8 @@ struct ProtocolServersView: View {
@Environment(\.editMode) private var editMode
let serverProtocol: ServerProtocol
@State private var currServers: [ServerCfg] = []
@State private var presetServers: [ServerCfg] = []
@State private var configuredServers: [ServerCfg] = []
@State private var otherServers: [ServerCfg] = []
@State private var presetServers: [String] = []
@State private var servers: [ServerCfg] = []
@State private var selectedServer: String? = nil
@State private var showAddServer = false
@State private var showScanProtoServer = false
@@ -54,53 +53,31 @@ struct ProtocolServersView: View {
private func protocolServersView() -> some View {
List {
if !configuredServers.isEmpty {
Section {
ForEach($configuredServers) { srv in
protocolServerView(srv)
}
.onMove { indexSet, offset in
configuredServers.move(fromOffsets: indexSet, toOffset: offset)
}
.onDelete { indexSet in
configuredServers.remove(atOffsets: indexSet)
}
} header: {
Text("Configured \(proto) servers")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.")
.foregroundColor(theme.colors.secondary)
.lineLimit(10)
}
}
if !otherServers.isEmpty {
Section {
ForEach($otherServers) { srv in
protocolServerView(srv)
}
.onMove { indexSet, offset in
otherServers.move(fromOffsets: indexSet, toOffset: offset)
}
.onDelete { indexSet in
otherServers.remove(atOffsets: indexSet)
}
} header: {
Text("Other \(proto) servers")
.foregroundColor(theme.colors.secondary)
}
}
Section {
Button("Add server") {
ForEach($servers) { srv in
protocolServerView(srv)
}
.onMove { indexSet, offset in
servers.move(fromOffsets: indexSet, toOffset: offset)
}
.onDelete { indexSet in
servers.remove(atOffsets: indexSet)
}
Button("Add server…") {
showAddServer = true
}
} header: {
Text("\(proto) servers")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.")
.foregroundColor(theme.colors.secondary)
.lineLimit(10)
}
Section {
Button("Reset") { partitionServers(currServers) }
.disabled(Set(allServers) == Set(currServers) || testing)
Button("Reset") { servers = currServers }
.disabled(servers == currServers || testing)
Button("Test servers", action: testServers)
.disabled(testing || allServersDisabled)
Button("Save servers", action: saveServers)
@@ -109,17 +86,17 @@ struct ProtocolServersView: View {
}
}
.toolbar { EditButton() }
.confirmationDialog("Add server", isPresented: $showAddServer, titleVisibility: .hidden) {
.confirmationDialog("Add server", isPresented: $showAddServer, titleVisibility: .hidden) {
Button("Enter server manually") {
otherServers.append(ServerCfg.empty)
selectedServer = allServers.last?.id
servers.append(ServerCfg.empty)
selectedServer = servers.last?.id
}
Button("Scan server QR code") { showScanProtoServer = true }
Button("Add preset servers", action: addAllPresets)
.disabled(hasAllPresets())
}
.sheet(isPresented: $showScanProtoServer) {
ScanProtocolServer(servers: $otherServers)
ScanProtocolServer(servers: $servers)
.modifier(ThemedBackground(grouped: true))
}
.modifier(BackButton(disabled: Binding.constant(false)) {
@@ -156,39 +133,27 @@ struct ProtocolServersView: View {
}
.onAppear {
// this condition is needed to prevent re-setting the servers when exiting single server view
if justOpened {
do {
let r = try getUserProtoServers(serverProtocol)
currServers = r.protoServers
presetServers = r.presetServers
partitionServers(currServers)
} catch let error {
alert = .error(
title: "Error loading \(proto) servers",
error: "Error: \(responseError(error))"
)
}
justOpened = false
} else {
partitionServers(allServers)
if !justOpened { return }
do {
let r = try getUserProtoServers(serverProtocol)
currServers = r.protoServers
presetServers = r.presetServers
servers = currServers
} catch let error {
alert = .error(
title: "Error loading \(proto) servers",
error: "Error: \(responseError(error))"
)
}
justOpened = false
}
}
private func partitionServers(_ servers: [ServerCfg]) {
configuredServers = servers.filter { $0.preset || $0.enabled }
otherServers = servers.filter { !($0.preset || $0.enabled) }
}
private var allServers: [ServerCfg] {
configuredServers + otherServers
}
private var saveDisabled: Bool {
allServers.isEmpty ||
Set(allServers) == Set(currServers) ||
servers.isEmpty ||
servers == currServers ||
testing ||
!allServers.allSatisfy { srv in
!servers.allSatisfy { srv in
if let address = parseServerAddress(srv.server) {
return uniqueAddress(srv, address)
}
@@ -198,7 +163,7 @@ struct ProtocolServersView: View {
}
private var allServersDisabled: Bool {
allServers.allSatisfy { !$0.enabled }
servers.allSatisfy { $0.enabled != .enabled }
}
private func protocolServerView(_ server: Binding<ServerCfg>) -> some View {
@@ -207,7 +172,8 @@ struct ProtocolServersView: View {
ProtocolServerView(
serverProtocol: serverProtocol,
server: server,
serverToEdit: srv
serverToEdit: srv,
serverEnabled: srv.enabled == .enabled
)
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
.modifier(ThemedBackground(grouped: true))
@@ -221,7 +187,7 @@ struct ProtocolServersView: View {
invalidServer()
} else if !uniqueAddress(srv, address) {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
} else if !srv.enabled {
} else if srv.enabled != .enabled {
Image(systemName: "slash.circle").foregroundColor(theme.colors.secondary)
} else {
showTestStatus(server: srv)
@@ -234,7 +200,7 @@ struct ProtocolServersView: View {
.padding(.trailing, 4)
let v = Text(address?.hostnames.first ?? srv.server).lineLimit(1)
if srv.enabled {
if srv.enabled == .enabled {
v
} else {
v.foregroundColor(theme.colors.secondary)
@@ -261,7 +227,7 @@ struct ProtocolServersView: View {
}
private func uniqueAddress(_ s: ServerCfg, _ address: ServerAddress) -> Bool {
allServers.allSatisfy { srv in
servers.allSatisfy { srv in
address.hostnames.allSatisfy { host in
srv.id == s.id || !srv.server.contains(host)
}
@@ -275,13 +241,13 @@ struct ProtocolServersView: View {
private func addAllPresets() {
for srv in presetServers {
if !hasPreset(srv) {
configuredServers.append(srv)
servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: .enabled))
}
}
}
private func hasPreset(_ srv: ServerCfg) -> Bool {
allServers.contains(where: { $0.server == srv.server })
private func hasPreset(_ srv: String) -> Bool {
servers.contains(where: { $0.server == srv })
}
private func testServers() {
@@ -299,31 +265,19 @@ struct ProtocolServersView: View {
}
private func resetTestStatus() {
for i in 0..<configuredServers.count {
if configuredServers[i].enabled {
configuredServers[i].tested = nil
}
}
for i in 0..<otherServers.count {
if otherServers[i].enabled {
otherServers[i].tested = nil
for i in 0..<servers.count {
if servers[i].enabled == .enabled {
servers[i].tested = nil
}
}
}
private func runServersTest() async -> [String: ProtocolTestFailure] {
var fs: [String: ProtocolTestFailure] = [:]
for i in 0..<configuredServers.count {
if configuredServers[i].enabled {
if let f = await testServerConnection(server: $configuredServers[i]) {
fs[serverHostname(configuredServers[i].server)] = f
}
}
}
for i in 0..<otherServers.count {
if otherServers[i].enabled {
if let f = await testServerConnection(server: $otherServers[i]) {
fs[serverHostname(otherServers[i].server)] = f
for i in 0..<servers.count {
if servers[i].enabled == .enabled {
if let f = await testServerConnection(server: $servers[i]) {
fs[serverHostname(servers[i].server)] = f
}
}
}
@@ -333,9 +287,9 @@ struct ProtocolServersView: View {
func saveServers() {
Task {
do {
try await setUserProtoServers(serverProtocol, servers: allServers)
try await setUserProtoServers(serverProtocol, servers: servers)
await MainActor.run {
currServers = allServers
currServers = servers
editMode?.wrappedValue = .inactive
}
} catch let error {
@@ -40,7 +40,7 @@ struct ScanProtocolServer: View {
switch resp {
case let .success(r):
if parseServerAddress(r.string) != nil {
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: false))
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: .enabled))
dismiss()
} else {
showAddressError = true
@@ -34,7 +34,6 @@ 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"
@@ -88,7 +87,6 @@ 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,
@@ -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 mkAlert(title: title, message: error)
return Alert(title: Text(title), message: Text(error))
}
}
}
@@ -30,7 +30,7 @@ struct UserProfilesView: View {
case hiddenProfilesNotice
case muteProfileAlert
case activateUserError(error: String)
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
switch self {
@@ -172,7 +172,7 @@ struct UserProfilesView: View {
message: Text(err)
)
case let .error(title, error):
return mkAlert(title: title, message: error)
return Alert(title: Text(title), message: Text(error))
}
}
}
@@ -367,8 +367,8 @@
<source>Add servers by scanning QR codes.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
@@ -127,6 +127,11 @@
<target>%@ е потвърдено</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ сървъри</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<target>%@ качено</target>
@@ -585,10 +590,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти.</target>
@@ -609,16 +610,16 @@
<target>Добави профил</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Добави сървър</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Добави сървъри чрез сканиране на QR кодове.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Добави сървър…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Добави към друго устройство</target>
@@ -709,8 +710,8 @@
<target>Всички нови съобщения от %@ ще бъдат скрити!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1350,10 +1351,6 @@
<target>Конфигурирай ICE сървъри</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Потвърди</target>
@@ -1532,6 +1529,10 @@ This is your own one-time link!</source>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>Контактът позволява</target>
@@ -1705,8 +1706,8 @@ This is your own one-time link!</source>
<target>Текуща парола…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -3925,10 +3926,6 @@ This is your link for group %@!</source>
<target>Реакциите на съобщения са забранени в тази група.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<note>No comment provided by engineer.</note>
@@ -3950,6 +3947,10 @@ This is your link for group %@!</source>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Текст на съобщението</target>
@@ -4468,10 +4469,6 @@ This is your link for group %@!</source>
<target>Други</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING бройка</target>
@@ -4638,10 +4635,6 @@ Error: %@</source>
<target>Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Полски интерфейс</target>
@@ -4708,10 +4701,6 @@ Error: %@</source>
<source>Private routing</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Профилни и сървърни връзки</target>
@@ -5612,10 +5601,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server address is incompatible with network settings.</source>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>Сървърът изисква оторизация за създаване на опашки, проверете паролата</target>
@@ -5639,14 +5624,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server version is incompatible with network settings.</source>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Сървъри</target>
@@ -5783,10 +5760,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Show message status</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Показване на визуализация</target>
@@ -6010,6 +5983,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -7064,8 +7041,8 @@ Repeat join request?</source>
<target>Можете да го направите видим за вашите контакти в SimpleX чрез Настройки.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Вече можете да изпращате съобщения до %@</target>
<note>notification body</note>
</trans-unit>
@@ -7472,7 +7449,7 @@ SimpleX сървърите не могат да видят вашия профи
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>блокиран от админ</target>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -386,8 +386,8 @@
<source>Add servers by scanning QR codes.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
@@ -126,6 +126,11 @@
<target>%@ je ověřený</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ servery</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<note>No comment provided by engineer.</note>
@@ -567,10 +572,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům.</target>
@@ -590,16 +591,16 @@
<target>Přidat profil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Přidat server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Přidejte servery skenováním QR kódů.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Přidat server…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Přidat do jiného zařízení</target>
@@ -687,8 +688,8 @@
<source>All new messages from %@ will be hidden!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1304,10 +1305,6 @@
<target>Konfigurace serverů ICE</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Potvrdit</target>
@@ -1470,6 +1467,10 @@ This is your own one-time link!</source>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>Kontakt povolil</target>
@@ -1635,8 +1636,8 @@ This is your own one-time link!</source>
<target>Aktuální přístupová fráze…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -3782,10 +3783,6 @@ This is your link for group %@!</source>
<target>Reakce na zprávy jsou v této skupině zakázány.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<note>No comment provided by engineer.</note>
@@ -3806,6 +3803,10 @@ This is your link for group %@!</source>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Text zprávy</target>
@@ -4298,10 +4299,6 @@ This is your link for group %@!</source>
<source>Other</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Počet PING</target>
@@ -4460,10 +4457,6 @@ Error: %@</source>
<target>Heslo uložte bezpečně, v případě jeho ztráty jej NEBUDE možné změnit.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Polské rozhraní</target>
@@ -4529,10 +4522,6 @@ Error: %@</source>
<source>Private routing</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Profil a připojení k serveru</target>
@@ -5410,10 +5399,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server address is incompatible with network settings.</source>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo</target>
@@ -5437,14 +5422,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server version is incompatible with network settings.</source>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Servery</target>
@@ -5576,10 +5553,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Show message status</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Zobrazení náhledu</target>
@@ -5797,6 +5770,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -6797,8 +6774,8 @@ Repeat join request?</source>
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Nyní můžete posílat zprávy %@</target>
<note>notification body</note>
</trans-unit>
@@ -7191,7 +7168,7 @@ Servery SimpleX nevidí váš profil.</target>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
File diff suppressed because it is too large Load Diff
@@ -336,8 +336,8 @@ Available in v5.1</source>
<source>Add servers by scanning QR codes.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
@@ -127,6 +127,11 @@
<target>%@ is verified</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<target>%@ uploaded</target>
@@ -588,11 +593,6 @@
<target>Acknowledgement errors</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<target>Active connections</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</target>
@@ -613,16 +613,16 @@
<target>Add profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Add server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Add servers by scanning QR codes.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Add server…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Add to another device</target>
@@ -718,9 +718,9 @@
<target>All new messages from %@ will be hidden!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<target>All profiles</target>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<target>All users</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1374,11 +1374,6 @@
<target>Configure ICE servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<target>Configured %@ servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Confirm</target>
@@ -1563,6 +1558,11 @@ This is your own one-time link!</target>
<target>Connections</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<target>Connections subscribed</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>Contact allows</target>
@@ -1738,9 +1738,9 @@ This is your own one-time link!</target>
<target>Current passphrase…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<target>Current profile</target>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<target>Current user</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -3998,11 +3998,6 @@ This is your link for group %@!</target>
<target>Message reactions are prohibited in this group.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<target>Message reception</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<target>Message routing fallback</target>
@@ -4028,6 +4023,11 @@ This is your link for group %@!</target>
<target>Message status: %@</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<target>Message subscriptions</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Message text</target>
@@ -4552,11 +4552,6 @@ This is your link for group %@!</target>
<target>Other</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<target>Other %@ servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING count</target>
@@ -4726,11 +4721,6 @@ Error: %@</target>
<target>Please store passphrase securely, you will NOT be able to change it if you lose it.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<target>Please try later.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Polish interface</target>
@@ -4801,11 +4791,6 @@ Error: %@</target>
<target>Private routing</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<target>Private routing error</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Profile and server connections</target>
@@ -5743,11 +5728,6 @@ Enable in *Network &amp; servers* settings.</target>
<target>Server address is incompatible with network settings.</target>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<target>Server address is incompatible with network settings: %@.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>Server requires authorization to create queues, check password</target>
@@ -5773,16 +5753,6 @@ Enable in *Network &amp; servers* settings.</target>
<target>Server version is incompatible with network settings.</target>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<target>Server version is incompatible with network settings: %@.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<target>Server version is incompatible with your app: %@.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Servers</target>
@@ -5923,11 +5893,6 @@ Enable in *Network &amp; servers* settings.</target>
<target>Show message status</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<target>Show percentage</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Show preview</target>
@@ -6158,6 +6123,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Subscription errors</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<target>Subscription percentage</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<target>Subscriptions ignored</target>
@@ -7236,9 +7206,9 @@ Repeat join request?</target>
<target>You can make it visible to your SimpleX contacts via Settings.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<target>You can now chat with %@</target>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>You can now send messages to %@</target>
<note>notification body</note>
</trans-unit>
<trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve">
@@ -7645,7 +7615,7 @@ SimpleX servers cannot see your profile.</target>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>blocked by admin</target>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -127,6 +127,11 @@
<target>%@ está verificado</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>Servidores %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<target>%@ subido</target>
@@ -585,10 +590,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Añade la dirección a tu perfil para que tus contactos puedan compartirla con otros. La actualización del perfil se enviará a tus contactos.</target>
@@ -609,16 +610,16 @@
<target>Añadir perfil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Añadir servidor</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Añadir servidores mediante el escaneo de códigos QR.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Añadir servidor…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Añadir a otro dispositivo</target>
@@ -709,8 +710,8 @@
<target>¡Los mensajes nuevos de %@ estarán ocultos!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1353,10 +1354,6 @@
<target>Configure servidores ICE</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Confirmar</target>
@@ -1510,7 +1507,7 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Connection error (AUTH)" xml:space="preserve">
<source>Connection error (AUTH)</source>
<target>Error de conexión (Autenticación)</target>
<target>Error conexión (Autenticación)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection request sent!" xml:space="preserve">
@@ -1525,7 +1522,7 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source>
<target>Tiempo de conexión agotado</target>
<target>Tiempo de conexión expirado</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection with desktop stopped" xml:space="preserve">
@@ -1536,6 +1533,10 @@ This is your own one-time link!</source>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>El contacto permite</target>
@@ -1709,8 +1710,8 @@ This is your own one-time link!</source>
<target>Contraseña actual…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -1836,7 +1837,6 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Debug delivery" xml:space="preserve">
<source>Debug delivery</source>
<target>Informe debug</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Decentralized" xml:space="preserve">
@@ -3333,7 +3333,7 @@ Error: %2$@</target>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace.</target>
<target>Si no puedes reunirte en persona, muestra el código QR por videollamada, o comparte el enlace.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
@@ -3922,7 +3922,6 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="Message queue info" xml:space="preserve">
<source>Message queue info</source>
<target>Información cola de mensajes</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reactions" xml:space="preserve">
@@ -3940,10 +3939,6 @@ This is your link for group %@!</source>
<target>Las reacciones a los mensajes no están permitidas en este grupo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<target>Enrutamiento de mensajes alternativo</target>
@@ -3967,6 +3962,10 @@ This is your link for group %@!</source>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Contacto y texto</target>
@@ -4324,7 +4323,7 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="One-time invitation link" xml:space="preserve">
<source>One-time invitation link</source>
<target>Enlace de invitación de un solo uso</target>
<target>Enlace único de invitación de un uso</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve">
@@ -4468,7 +4467,7 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<target>O escanea el código QR</target>
<target>O escanear código QR</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or securely share this file link" xml:space="preserve">
@@ -4478,7 +4477,7 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<target>O muestra este código QR</target>
<target>O mostrar este código</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other" xml:space="preserve">
@@ -4486,10 +4485,6 @@ This is your link for group %@!</source>
<target>Otro</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Contador PING</target>
@@ -4552,7 +4547,7 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<target>Pega el enlace recibido</target>
<target>Pegar el enlace recibido</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Pending" xml:space="preserve">
@@ -4566,7 +4561,7 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="Periodically" xml:space="preserve">
<source>Periodically</source>
<target>Periódicamente</target>
<target>Periódico</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Permanent decryption error" xml:space="preserve">
@@ -4656,10 +4651,6 @@ Error: %@</target>
<target>Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Interfaz en polaco</target>
@@ -4729,10 +4720,6 @@ Error: %@</target>
<target>Enrutamiento privado</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Datos del perfil y conexiones</target>
@@ -4841,12 +4828,12 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit>
<trans-unit id="Protocol timeout" xml:space="preserve">
<source>Protocol timeout</source>
<target>Timeout protocolo</target>
<target>Tiempo de espera del protocolo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Protocol timeout per KB" xml:space="preserve">
<source>Protocol timeout per KB</source>
<target>Timeout protocolo por KB</target>
<target>Límite de espera del protocolo por KB</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxied" xml:space="preserve">
@@ -4889,32 +4876,32 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit>
<trans-unit id="Read more" xml:space="preserve">
<source>Read more</source>
<target>Conoce más</target>
<target>Saber más</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source>
<target>Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target>
<target>Saber más en el [Manual del Usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<target>Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target>
<target>Saber más en [Guía de Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target>
<target>Saber más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in our GitHub repository." xml:space="preserve">
<source>Read more in our GitHub repository.</source>
<target>Conoce más en nuestro repositorio GitHub.</target>
<target>Saber más en nuestro repositorio GitHub.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve">
<source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source>
<target>Conoce más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target>
<target>Saber más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receipts are disabled" xml:space="preserve">
@@ -5640,10 +5627,6 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>La dirección del servidor es incompatible con la configuración de la red.</target>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>El servidor requiere autorización para crear colas, comprueba la contraseña</target>
@@ -5668,14 +5651,6 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>La versión del servidor es incompatible con la configuración de red.</target>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Servidores</target>
@@ -5780,7 +5755,7 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Comparte este enlace de un solo uso</target>
<target>Compartir este enlace de un uso</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -5813,10 +5788,6 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Estado del mensaje</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Mostrar vista previa</target>
@@ -6041,6 +6012,10 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -6062,7 +6037,7 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit>
<trans-unit id="TCP connection timeout" xml:space="preserve">
<source>TCP connection timeout</source>
<target>Timeout de la conexión TCP</target>
<target>Tiempo de espera de la conexión TCP agotado</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="TCP_KEEPCNT" xml:space="preserve">
@@ -6112,7 +6087,7 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<target>Pulsa para pegar el enlacePulsa para pegar enlace</target>
<target>Pulsa para pegar enlace</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
@@ -6380,7 +6355,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target>
</trans-unit>
<trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve">
<source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source>
<target>Para proteger tu dirección IP, el enrutamiento privado usa tu lista de servidores SMP para enviar mensajes.</target>
<target>Para proteger tu dirección IP, el enrutamiento privado usa tus servidores SMP para enviar mensajes.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To protect your information, turn on SimpleX Lock.&#10;You will be prompted to complete authentication before this feature is enabled." xml:space="preserve">
@@ -6541,8 +6516,9 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t
<trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.&#10;To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve">
<source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.
To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source>
<target>A menos que tu contacto haya eliminado la conexión o el enlace haya sido usado, podría ser un error. Por favor, notifícalo.
Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red.</target>
<target>A menos que tu contacto haya eliminado la conexión o
que este enlace ya se haya usado, podría ser un error. Por favor, notifícalo.
Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueba que tienes buena conexión de red.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
@@ -6694,12 +6670,12 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión
</trans-unit>
<trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve">
<source>Use private routing with unknown servers when IP address is not protected.</source>
<target>Usar enrutamiento privado con servidores desconocidos cuando tu dirección IP no está protegida.</target>
<target>Usar enrutamiento privado con servidores desconocidos cuando la dirección IP no está protegida.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use private routing with unknown servers." xml:space="preserve">
<source>Use private routing with unknown servers.</source>
<target>Usar enrutamiento privado con servidores de retransmisión desconocidos.</target>
<target>Usar enrutamiento privado con servidores desconocidos.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
@@ -7103,8 +7079,8 @@ Repeat join request?</source>
<target>Puedes hacerlo visible para tus contactos de SimpleX en Configuración.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Ya puedes enviar mensajes a %@</target>
<note>notification body</note>
</trans-unit>
@@ -7242,7 +7218,7 @@ Repeat connection request?</source>
</trans-unit>
<trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve">
<source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source>
<target>Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano.</target>
<target>Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You will connect to all group members." xml:space="preserve">
@@ -7375,8 +7351,8 @@ Puedes cancelarla y eliminar el contacto (e intentarlo más tarde con un enlace
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos.
Los servidores SimpleX no pueden ver tu perfil.</target>
<target>Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos.
Los servidores de SimpleX no pueden ver tu perfil.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
@@ -7511,7 +7487,7 @@ Los servidores SimpleX no pueden ver tu perfil.</target>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>bloqueado por administrador</target>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -8128,9 +8104,6 @@ Los servidores SimpleX no pueden ver tu perfil.</target>
<source>server queue info: %1$@
last received msg: %2$@</source>
<target>información cola del servidor: %1$@
último mensaje recibido: %2$@</target>
<note>queue info</note>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
@@ -8175,7 +8148,7 @@ last received msg: %2$@</source>
</trans-unit>
<trans-unit id="unknown relays" xml:space="preserve">
<source>unknown relays</source>
<target>con servidores desconocidos</target>
<target>servidor de retransmisión desconocido</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="unknown status" xml:space="preserve">
@@ -8185,7 +8158,7 @@ last received msg: %2$@</source>
</trans-unit>
<trans-unit id="unprotected" xml:space="preserve">
<source>unprotected</source>
<target>con IP desprotegida</target>
<target>desprotegido</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="updated group profile" xml:space="preserve">
@@ -124,6 +124,11 @@
<target>%@ on vahvistettu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ palvelimet</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<note>No comment provided by engineer.</note>
@@ -562,10 +567,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi.</target>
@@ -585,16 +586,16 @@
<target>Lisää profiili</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Lisää palvelin</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Lisää palvelimia skannaamalla QR-koodeja.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Lisää palvelin…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Lisää toiseen laitteeseen</target>
@@ -682,8 +683,8 @@
<source>All new messages from %@ will be hidden!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1297,10 +1298,6 @@
<target>Määritä ICE-palvelimet</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Vahvista</target>
@@ -1463,6 +1460,10 @@ This is your own one-time link!</source>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>Kontakti sallii</target>
@@ -1628,8 +1629,8 @@ This is your own one-time link!</source>
<target>Nykyinen tunnuslause…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -3772,10 +3773,6 @@ This is your link for group %@!</source>
<target>Viestireaktiot ovat kiellettyjä tässä ryhmässä.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<note>No comment provided by engineer.</note>
@@ -3796,6 +3793,10 @@ This is your link for group %@!</source>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Viestin teksti</target>
@@ -4286,10 +4287,6 @@ This is your link for group %@!</source>
<source>Other</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING-määrä</target>
@@ -4448,10 +4445,6 @@ Error: %@</source>
<target>Säilytä tunnuslause turvallisesti, ET voi muuttaa sitä, jos kadotat sen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Puolalainen käyttöliittymä</target>
@@ -4517,10 +4510,6 @@ Error: %@</source>
<source>Private routing</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Profiili- ja palvelinyhteydet</target>
@@ -5397,10 +5386,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server address is incompatible with network settings.</source>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana</target>
@@ -5424,14 +5409,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server version is incompatible with network settings.</source>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Palvelimet</target>
@@ -5563,10 +5540,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Show message status</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Näytä esikatselu</target>
@@ -5783,6 +5756,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -6782,8 +6759,8 @@ Repeat join request?</source>
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Voit nyt lähettää viestejä %@:lle</target>
<note>notification body</note>
</trans-unit>
@@ -7176,7 +7153,7 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -127,6 +127,11 @@
<target>%@ est vérifié·e</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>Serveurs %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<target>%@ envoyé</target>
@@ -585,10 +590,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Ajoutez une adresse à votre profil, afin que vos contacts puissent la partager avec d'autres personnes. La mise à jour du profil sera envoyée à vos contacts.</target>
@@ -609,16 +610,16 @@
<target>Ajouter un profil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Ajouter un serveur</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Ajoutez des serveurs en scannant des codes QR.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Ajouter un serveur…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Ajouter à un autre appareil</target>
@@ -709,8 +710,8 @@
<target>Tous les nouveaux messages de %@ seront cachés !</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1353,10 +1354,6 @@
<target>Configurer les serveurs ICE</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Confirmer</target>
@@ -1536,6 +1533,10 @@ Il s'agit de votre propre lien unique !</target>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>Votre contact autorise</target>
@@ -1709,8 +1710,8 @@ Il s'agit de votre propre lien unique !</target>
<target>Phrase secrète actuelle…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -1836,7 +1837,6 @@ Il s'agit de votre propre lien unique !</target>
</trans-unit>
<trans-unit id="Debug delivery" xml:space="preserve">
<source>Debug delivery</source>
<target>Livraison de débogage</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Decentralized" xml:space="preserve">
@@ -3922,7 +3922,6 @@ Voici votre lien pour le groupe %@ !</target>
</trans-unit>
<trans-unit id="Message queue info" xml:space="preserve">
<source>Message queue info</source>
<target>Informations sur la file d'attente des messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reactions" xml:space="preserve">
@@ -3940,10 +3939,6 @@ Voici votre lien pour le groupe %@ !</target>
<target>Les réactions aux messages sont interdites dans ce groupe.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<target>Rabattement du routage des messages</target>
@@ -3967,6 +3962,10 @@ Voici votre lien pour le groupe %@ !</target>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Texte du message</target>
@@ -4486,10 +4485,6 @@ Voici votre lien pour le groupe %@ !</target>
<target>Autres</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Nombre de PING</target>
@@ -4656,10 +4651,6 @@ Erreur: %@</target>
<target>Veuillez conserver votre phrase secrète en lieu sûr, vous NE pourrez PAS la changer si vous la perdez.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Interface en polonais</target>
@@ -4729,10 +4720,6 @@ Erreur: %@</target>
<target>Routage privé</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Profil et connexions au serveur</target>
@@ -5640,10 +5627,6 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>L'adresse du serveur est incompatible avec les paramètres du réseau.</target>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe</target>
@@ -5668,14 +5651,6 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>La version du serveur est incompatible avec les paramètres du réseau.</target>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Serveurs</target>
@@ -5813,10 +5788,6 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Afficher le statut du message</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Afficher l'aperçu</target>
@@ -6041,6 +6012,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -7103,8 +7078,8 @@ Répéter la demande d'adhésion ?</target>
<target>Vous pouvez le rendre visible à vos contacts SimpleX via Paramètres.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Vous pouvez maintenant envoyer des messages à %@</target>
<note>notification body</note>
</trans-unit>
@@ -7511,7 +7486,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>bloqué par l'administrateur</target>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -8128,9 +8103,6 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
<source>server queue info: %1$@
last received msg: %2$@</source>
<target>info sur la file d'attente du serveur: %1$@
dernier message reçu: %2$@</target>
<note>queue info</note>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
@@ -403,9 +403,9 @@ Available in v5.1</source>
<target state="translated">הוספת שרתים על ידי סריקת קוד QR.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve" approved="no">
<source>Add server</source>
<target state="translated">הוסף שרת</target>
<trans-unit id="Add server" xml:space="preserve" approved="no">
<source>Add server</source>
<target state="translated">הוסף שרת</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve" approved="no">
@@ -300,8 +300,8 @@
<source>Add servers by scanning QR codes.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
@@ -367,8 +367,8 @@
<source>Add servers by scanning QR codes.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -127,6 +127,11 @@
<target>%@ は検証されています</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ サーバー</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<target>%@ アップロード済</target>
@@ -579,10 +584,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。</target>
@@ -602,16 +603,16 @@
<target>プロフィールを追加</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>サーバを追加</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>QRコードでサーバを追加する。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>サーバを追加…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>別の端末に追加</target>
@@ -699,8 +700,8 @@
<source>All new messages from %@ will be hidden!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1321,10 +1322,6 @@
<target>ICEサーバを設定</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>確認</target>
@@ -1487,6 +1484,10 @@ This is your own one-time link!</source>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>連絡先の許可</target>
@@ -1652,8 +1653,8 @@ This is your own one-time link!</source>
<target>現在の暗証フレーズ…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -3796,10 +3797,6 @@ This is your link for group %@!</source>
<target>このグループではメッセージへのリアクションは禁止されています。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<note>No comment provided by engineer.</note>
@@ -3820,6 +3817,10 @@ This is your link for group %@!</source>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>メッセージ内容</target>
@@ -4312,10 +4313,6 @@ This is your link for group %@!</source>
<source>Other</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING回数</target>
@@ -4474,10 +4471,6 @@ Error: %@</source>
<target>パスフレーズを失くさないように保管してください。失くすと変更できなくなります。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>ポーランド語UI</target>
@@ -4543,10 +4536,6 @@ Error: %@</source>
<source>Private routing</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>プロフィールとサーバ接続</target>
@@ -5415,10 +5404,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server address is incompatible with network settings.</source>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>キューを作成するにはサーバーの認証が必要です。パスワードを確認してください</target>
@@ -5442,14 +5427,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server version is incompatible with network settings.</source>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>サーバ</target>
@@ -5581,10 +5558,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Show message status</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>プレビューを表示</target>
@@ -5802,6 +5775,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -6800,8 +6777,8 @@ Repeat join request?</source>
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>%@ にメッセージを送信できるようになりました</target>
<note>notification body</note>
</trans-unit>
@@ -7194,7 +7171,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -5,11 +5,9 @@
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="&#10;" xml:space="preserve" approved="no">
<trans-unit id="&#10;" xml:space="preserve">
<source>
</source>
<target state="translated">
</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" " xml:space="preserve">
@@ -302,8 +300,8 @@
<source>Add servers by scanning QR codes.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
@@ -486,62 +484,53 @@
<source>Can't delete user profile!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't invite contact!" xml:space="preserve" approved="no">
<trans-unit id="Can't invite contact!" xml:space="preserve">
<source>Can't invite contact!</source>
<target state="translated">주소를 초대할 수 없습니다.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't invite contacts!" xml:space="preserve">
<source>Can't invite contacts!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Cancel" xml:space="preserve" approved="no">
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target state="translated">취소</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve" approved="no">
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
<source>Cannot access keychain to save database password</source>
<target state="translated">데이터베이스 암호를 저장하는 키체인에 접근 할 수 없습니다</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Cannot receive file" xml:space="preserve" approved="no">
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target state="translated">파일을 받을 수 없습니다</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change" xml:space="preserve" approved="no">
<trans-unit id="Change" xml:space="preserve">
<source>Change</source>
<target state="translated">변경</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change database passphrase?" xml:space="preserve">
<source>Change database passphrase?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change member role?" xml:space="preserve" approved="no">
<trans-unit id="Change member role?" xml:space="preserve">
<source>Change member role?</source>
<target state="translated">멤버 역할을 변경하시겠습니까?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change receiving address" xml:space="preserve" approved="no">
<trans-unit id="Change receiving address" xml:space="preserve">
<source>Change receiving address</source>
<target state="translated">수신 주소 변경</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change receiving address?" xml:space="preserve" approved="no">
<source>Change receiving address?</source>
<target state="translated">수신 주소를 변경하시겠습니까?</target>
<target state="translated">修改接收地址?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change role" xml:space="preserve" approved="no">
<trans-unit id="Change role" xml:space="preserve">
<source>Change role</source>
<target state="translated">역할 변경</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat archive" xml:space="preserve" approved="no">
<trans-unit id="Chat archive" xml:space="preserve">
<source>Chat archive</source>
<target state="translated">채팅 기록 보관함</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat console" xml:space="preserve">
@@ -556,9 +545,8 @@
<source>Chat database deleted</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat database imported" xml:space="preserve" approved="no">
<trans-unit id="Chat database imported" xml:space="preserve">
<source>Chat database imported</source>
<target state="translated">채팅 데이터베이스를 가져옴</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is running" xml:space="preserve">
@@ -2409,29 +2397,24 @@ We will be adding server redundancy to prevent lost messages.</source>
<source>Send live message</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send notifications" xml:space="preserve" approved="no">
<trans-unit id="Send notifications" xml:space="preserve">
<source>Send notifications</source>
<target state="translated">알림 전송</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send notifications:" xml:space="preserve" approved="no">
<trans-unit id="Send notifications:" xml:space="preserve">
<source>Send notifications:</source>
<target state="translated">알림 전송:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send questions and ideas" xml:space="preserve" approved="no">
<trans-unit id="Send questions and ideas" xml:space="preserve">
<source>Send questions and ideas</source>
<target state="translated">질문이나 아이디어 보내기</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve" approved="no">
<trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve">
<source>Send them from gallery or custom keyboards.</source>
<target state="needs-translation">갤러리 또는 사용자 정의 키보드에서 그들을 보내십시오.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve" approved="no">
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target state="translated">상대방이 파일 전송을 취소했습니다.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
@@ -3772,26 +3755,6 @@ SimpleX servers cannot see your profile.</source>
<source>\~strike~</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change passcode" xml:space="preserve" approved="no">
<source>Change passcode</source>
<target state="translated">패스코드 변경</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Cellular" xml:space="preserve" approved="no">
<source>Cellular</source>
<target state="translated">셀룰러</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve" approved="no">
<source>Send messages directly when your or destination server does not support private routing.</source>
<target state="needs-translation">이 서버 또는 도착 서버가 비밀 라우팅을 지원하지 않을 때 직통 메시지 보내기.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve" approved="no">
<source>Send up to 100 last messages to new members.</source>
<target state="translated">새로운 멤버에게 최대 100개의 마지막 메시지 보내기.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ko" datatype="plaintext">
@@ -3815,9 +3778,8 @@ SimpleX servers cannot see your profile.</source>
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<note>Privacy - Microphone Usage Description</note>
</trans-unit>
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve" approved="no">
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
<source>SimpleX needs access to Photo Library for saving captured and received media</source>
<target state="needs-translation">SimpleX는 캡처 및 수신 된 미디어를 저장하기 위해 사진 라이브러리에 접근이 필요합니다</target>
<note>Privacy - Photo Library Additions Usage Description</note>
</trans-unit>
</body>
@@ -3831,9 +3793,8 @@ SimpleX servers cannot see your profile.</source>
<source>SimpleX NSE</source>
<note>Bundle display name</note>
</trans-unit>
<trans-unit id="CFBundleName" xml:space="preserve" approved="no">
<trans-unit id="CFBundleName" xml:space="preserve">
<source>SimpleX NSE</source>
<target state="translated">SimpleX NSE</target>
<note>Bundle name</note>
</trans-unit>
<trans-unit id="NSHumanReadableCopyright" xml:space="preserve">
@@ -329,9 +329,9 @@
<target state="translated">Pridėti serverius skenuojant QR kodus.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve" approved="no">
<source>Add server</source>
<target state="translated">Pridėti serverį</target>
<trans-unit id="Add server" xml:space="preserve" approved="no">
<source>Add server</source>
<target state="translated">Pridėti serverį</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
@@ -367,8 +367,8 @@
<source>Add servers by scanning QR codes.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
@@ -127,6 +127,11 @@
<target>%@ is geverifieerd</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<target>%@ geüpload</target>
@@ -585,10 +590,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Voeg een adres toe aan uw profiel, zodat uw contacten het met andere mensen kunnen delen. Profiel update wordt naar uw contacten verzonden.</target>
@@ -609,16 +610,16 @@
<target>Profiel toevoegen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Server toevoegen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Servers toevoegen door QR-codes te scannen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Server toevoegen…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Toevoegen aan een ander apparaat</target>
@@ -709,8 +710,8 @@
<target>Alle nieuwe berichten van %@ worden verborgen!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1353,10 +1354,6 @@
<target>ICE servers configureren</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Bevestigen</target>
@@ -1536,6 +1533,10 @@ Dit is uw eigen eenmalige link!</target>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>Contact maakt het mogelijk</target>
@@ -1709,8 +1710,8 @@ Dit is uw eigen eenmalige link!</target>
<target>Huidige wachtwoord…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -1836,7 +1837,6 @@ Dit is uw eigen eenmalige link!</target>
</trans-unit>
<trans-unit id="Debug delivery" xml:space="preserve">
<source>Debug delivery</source>
<target>Foutopsporing bezorging</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Decentralized" xml:space="preserve">
@@ -3922,7 +3922,6 @@ Dit is jouw link voor groep %@!</target>
</trans-unit>
<trans-unit id="Message queue info" xml:space="preserve">
<source>Message queue info</source>
<target>Informatie over berichtenwachtrij</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reactions" xml:space="preserve">
@@ -3940,10 +3939,6 @@ Dit is jouw link voor groep %@!</target>
<target>Reacties op berichten zijn verboden in deze groep.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<target>Terugval op berichtroutering</target>
@@ -3967,6 +3962,10 @@ Dit is jouw link voor groep %@!</target>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Bericht tekst</target>
@@ -4486,10 +4485,6 @@ Dit is jouw link voor groep %@!</target>
<target>Ander</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING count</target>
@@ -4656,10 +4651,6 @@ Fout: %@</target>
<target>Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u het kwijtraakt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Poolse interface</target>
@@ -4729,10 +4720,6 @@ Fout: %@</target>
<target>Privéroutering</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Profiel- en serververbindingen</target>
@@ -5640,10 +5627,6 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
<target>Serveradres is niet compatibel met netwerkinstellingen.</target>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord</target>
@@ -5668,14 +5651,6 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
<target>Serverversie is incompatibel met netwerkinstellingen.</target>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Servers</target>
@@ -5813,10 +5788,6 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
<target>Toon berichtstatus</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Toon voorbeeld</target>
@@ -6041,6 +6012,10 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -7103,8 +7078,8 @@ Deelnameverzoek herhalen?</target>
<target>Je kunt het via Instellingen zichtbaar maken voor je SimpleX contacten.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Je kunt nu berichten sturen naar %@</target>
<note>notification body</note>
</trans-unit>
@@ -7511,7 +7486,7 @@ SimpleX servers kunnen uw profiel niet zien.</target>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>geblokkeerd door beheerder</target>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -8128,9 +8103,6 @@ SimpleX servers kunnen uw profiel niet zien.</target>
<source>server queue info: %1$@
last received msg: %2$@</source>
<target>informatie over serverwachtrij: %1$@
laatst ontvangen bericht: %2$@</target>
<note>queue info</note>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
@@ -127,6 +127,11 @@
<target>%@ jest zweryfikowany</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ serwery</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<target>%@ wgrane</target>
@@ -585,10 +590,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów.</target>
@@ -609,16 +610,16 @@
<target>Dodaj profil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Dodaj serwer</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Dodaj serwery, skanując kody QR.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Dodaj serwer…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Dodaj do innego urządzenia</target>
@@ -709,8 +710,8 @@
<target>Wszystkie nowe wiadomości z %@ zostaną ukryte!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1353,10 +1354,6 @@
<target>Skonfiguruj serwery ICE</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Potwierdź</target>
@@ -1536,6 +1533,10 @@ To jest twój jednorazowy link!</target>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>Kontakt pozwala</target>
@@ -1709,8 +1710,8 @@ To jest twój jednorazowy link!</target>
<target>Obecne hasło…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -1836,7 +1837,6 @@ To jest twój jednorazowy link!</target>
</trans-unit>
<trans-unit id="Debug delivery" xml:space="preserve">
<source>Debug delivery</source>
<target>Dostarczenie debugowania</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Decentralized" xml:space="preserve">
@@ -3922,7 +3922,6 @@ To jest twój link do grupy %@!</target>
</trans-unit>
<trans-unit id="Message queue info" xml:space="preserve">
<source>Message queue info</source>
<target>Informacje kolejki wiadomości</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reactions" xml:space="preserve">
@@ -3940,10 +3939,6 @@ To jest twój link do grupy %@!</target>
<target>Reakcje wiadomości są zabronione w tej grupie.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<target>Rezerwowe trasowania wiadomości</target>
@@ -3967,6 +3962,10 @@ To jest twój link do grupy %@!</target>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Tekst wiadomości</target>
@@ -4486,10 +4485,6 @@ To jest twój link do grupy %@!</target>
<target>Inne</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Liczba PINGÓW</target>
@@ -4656,10 +4651,6 @@ Błąd: %@</target>
<target>Przechowuj kod dostępu w bezpieczny sposób, w przypadku jego utraty NIE będzie można go zmienić.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Polski interfejs</target>
@@ -4729,10 +4720,6 @@ Błąd: %@</target>
<target>Prywatne trasowanie</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Profil i połączenia z serwerem</target>
@@ -5640,10 +5627,6 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
<target>Adres serwera jest niekompatybilny z ustawieniami sieciowymi.</target>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło</target>
@@ -5668,14 +5651,6 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
<target>Wersja serwera jest niekompatybilna z ustawieniami sieciowymi.</target>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Serwery</target>
@@ -5813,10 +5788,6 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
<target>Pokaż status wiadomości</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Pokaż podgląd</target>
@@ -6041,6 +6012,10 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -7103,8 +7078,8 @@ Powtórzyć prośbę dołączenia?</target>
<target>Możesz ustawić go jako widoczny dla swoich kontaktów SimpleX w Ustawieniach.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Możesz teraz wysyłać wiadomości do %@</target>
<note>notification body</note>
</trans-unit>
@@ -7511,7 +7486,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>zablokowany przez admina</target>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -8128,9 +8103,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
<source>server queue info: %1$@
last received msg: %2$@</source>
<target>Informacje kolejki serwera: %1$@
ostatnia otrzymana wiadomość: %2$@</target>
<note>queue info</note>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
@@ -374,9 +374,9 @@
<target state="translated">Adicione servidores escaneando o QR code.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve" approved="no">
<source>Add server</source>
<target state="translated">Adicionar servidor</target>
<trans-unit id="Add server" xml:space="preserve" approved="no">
<source>Add server</source>
<target state="translated">Adicionar servidor</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve" approved="no">
@@ -5,11 +5,9 @@
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="&#10;" xml:space="preserve" approved="no">
<trans-unit id="&#10;" xml:space="preserve">
<source>
</source>
<target state="needs-translation">
</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="&#10;Available in v5.1" xml:space="preserve">
@@ -52,19 +50,16 @@ Available in v5.1</source>
<target state="translated">#secreto#</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@" xml:space="preserve" approved="no">
<trans-unit id="%@" xml:space="preserve">
<source>%@</source>
<target state="needs-translation">%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ %@" xml:space="preserve" approved="no">
<trans-unit id="%@ %@" xml:space="preserve">
<source>%@ %@</source>
<target state="needs-translation">%@ %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ / %@" xml:space="preserve" approved="no">
<trans-unit id="%@ / %@" xml:space="preserve">
<source>%@ / %@</source>
<target state="needs-translation">%@ / %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve" approved="no">
@@ -122,14 +117,12 @@ Available in v5.1</source>
<target state="translated">%d mensagem(s) ignorada(s)</target>
<note>integrity error chat item</note>
</trans-unit>
<trans-unit id="%lld" xml:space="preserve" approved="no">
<trans-unit id="%lld" xml:space="preserve">
<source>%lld</source>
<target state="needs-translation">%lld</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld %@" xml:space="preserve" approved="no">
<trans-unit id="%lld %@" xml:space="preserve">
<source>%lld %@</source>
<target state="needs-translation">%lld %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no">
@@ -162,29 +155,24 @@ Available in v5.1</source>
<target state="translated">%lld segundos</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lldd" xml:space="preserve" approved="no">
<trans-unit id="%lldd" xml:space="preserve">
<source>%lldd</source>
<target state="needs-translation">%lldd</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lldh" xml:space="preserve" approved="no">
<trans-unit id="%lldh" xml:space="preserve">
<source>%lldh</source>
<target state="needs-translation">%lldh</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lldk" xml:space="preserve" approved="no">
<trans-unit id="%lldk" xml:space="preserve">
<source>%lldk</source>
<target state="needs-translation">%lldk</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lldm" xml:space="preserve" approved="no">
<trans-unit id="%lldm" xml:space="preserve">
<source>%lldm</source>
<target state="needs-translation">%lldm</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lldmth" xml:space="preserve" approved="no">
<trans-unit id="%lldmth" xml:space="preserve">
<source>%lldmth</source>
<target state="needs-translation">%lldmth</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%llds" xml:space="preserve">
@@ -205,9 +193,8 @@ Available in v5.1</source>
<target state="translated">%u mensagens ignoradas.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(" xml:space="preserve" approved="no">
<trans-unit id="(" xml:space="preserve">
<source>(</source>
<target state="needs-translation">(</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve">
@@ -372,8 +359,8 @@ Available in v5.1</source>
<source>Add servers by scanning QR codes.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
@@ -4553,31 +4540,6 @@ SimpleX servers cannot see your profile.</source>
<target state="translated">Confirmar envio</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ downloaded" xml:space="preserve" approved="no">
<source>%@ downloaded</source>
<target state="translated">%@ baixado</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="# %@" xml:space="preserve" approved="no">
<source># %@</source>
<target state="needs-translation"># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="%@:" xml:space="preserve" approved="no">
<source>%@:</source>
<target state="needs-translation">%@:</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@ (current)" xml:space="preserve" approved="no">
<source>%@ (current)</source>
<target state="translated">%@(atual)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ (current):" xml:space="preserve" approved="no">
<source>%@ (current):</source>
<target state="translated">%@ (atual):</target>
<note>copied message info</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pt" datatype="plaintext">
@@ -127,6 +127,11 @@
<target>%@ подтверждён</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ серверы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<target>%@ загружено</target>
@@ -585,10 +590,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам.</target>
@@ -609,16 +610,16 @@
<target>Добавить профиль</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Добавить сервер</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Добавить серверы через QR код.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Добавить сервер…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Добавить на другое устройство</target>
@@ -709,8 +710,8 @@
<target>Все новые сообщения от %@ будут скрыты!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1353,10 +1354,6 @@
<target>Настройка ICE серверов</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Подтвердить</target>
@@ -1536,6 +1533,10 @@ This is your own one-time link!</source>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>Контакт разрешает</target>
@@ -1709,8 +1710,8 @@ This is your own one-time link!</source>
<target>Текущий пароль…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -3938,10 +3939,6 @@ This is your link for group %@!</source>
<target>Реакции на сообщения запрещены в этой группе.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<target>Прямая доставка сообщений</target>
@@ -3965,6 +3962,10 @@ This is your link for group %@!</source>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Текст сообщения</target>
@@ -4484,10 +4485,6 @@ This is your link for group %@!</source>
<target>Другaя сеть</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Количество PING</target>
@@ -4654,10 +4651,6 @@ Error: %@</source>
<target>Пожалуйста, надежно сохраните пароль, Вы НЕ сможете его поменять, если потеряете.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Польский интерфейс</target>
@@ -4727,10 +4720,6 @@ Error: %@</source>
<target>Конфиденциальная доставка</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Профиль и соединения на сервере</target>
@@ -5638,10 +5627,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Адрес сервера несовместим с настройками сети.</target>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>Сервер требует авторизации для создания очередей, проверьте пароль</target>
@@ -5666,14 +5651,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Версия сервера несовместима с настройками сети.</target>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Серверы</target>
@@ -5811,10 +5788,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Показать статус сообщения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Показывать уведомления</target>
@@ -6039,6 +6012,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -7101,9 +7078,9 @@ Repeat join request?</source>
<target>Вы можете сделать его видимым для ваших контактов в SimpleX через Настройки.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<target>Вы теперь можете общаться с %@</target>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Вы теперь можете отправлять сообщения %@</target>
<note>notification body</note>
</trans-unit>
<trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve">
@@ -7509,7 +7486,7 @@ SimpleX серверы не могут получить доступ к Ваше
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>заблокировано администратором</target>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -120,6 +120,11 @@
<target>%@ ได้รับการตรวจสอบแล้ว</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ เซิร์ฟเวอร์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<note>No comment provided by engineer.</note>
@@ -554,10 +559,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ</target>
@@ -577,16 +578,16 @@
<target>เพิ่มโปรไฟล์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>เพิ่มเซิร์ฟเวอร์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>เพิ่มเซิร์ฟเวอร์…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>เพิ่มเข้าไปในอุปกรณ์อื่น</target>
@@ -674,8 +675,8 @@
<source>All new messages from %@ will be hidden!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1289,10 +1290,6 @@
<target>กำหนดค่าเซิร์ฟเวอร์ ICE</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>ยืนยัน</target>
@@ -1453,6 +1450,10 @@ This is your own one-time link!</source>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>ผู้ติดต่ออนุญาต</target>
@@ -1617,8 +1618,8 @@ This is your own one-time link!</source>
<target>รหัสผ่านปัจจุบัน…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -3755,10 +3756,6 @@ This is your link for group %@!</source>
<target>ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<note>No comment provided by engineer.</note>
@@ -3779,6 +3776,10 @@ This is your link for group %@!</source>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>ข้อความ</target>
@@ -4267,10 +4268,6 @@ This is your link for group %@!</source>
<source>Other</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>จํานวน PING</target>
@@ -4429,10 +4426,6 @@ Error: %@</source>
<target>โปรดจัดเก็บรหัสผ่านอย่างปลอดภัย คุณจะไม่สามารถเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>อินเตอร์เฟซภาษาโปแลนด์</target>
@@ -4498,10 +4491,6 @@ Error: %@</source>
<source>Private routing</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>การเชื่อมต่อโปรไฟล์และเซิร์ฟเวอร์</target>
@@ -5374,10 +5363,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server address is incompatible with network settings.</source>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน</target>
@@ -5401,14 +5386,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server version is incompatible with network settings.</source>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>เซิร์ฟเวอร์</target>
@@ -5539,10 +5516,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Show message status</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>แสดงตัวอย่าง</target>
@@ -5758,6 +5731,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -6754,8 +6731,8 @@ Repeat join request?</source>
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>ตอนนี้คุณสามารถส่งข้อความถึง %@</target>
<note>notification body</note>
</trans-unit>
@@ -7146,7 +7123,7 @@ SimpleX servers cannot see your profile.</source>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -127,6 +127,11 @@
<target>%@ onaylandı</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ sunucuları</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<target>%@ yüklendi</target>
@@ -585,10 +590,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Kişilerinizin başkalarıyla paylaşabilmesi için profilinize adres ekleyin. Profil güncellemesi kişilerinize gönderilecek.</target>
@@ -609,16 +610,16 @@
<target>Profil ekle</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Sunucu ekle</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Karekod taratarak sunucuları ekleyin.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Sunucu ekle…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Başka bir cihaza ekle</target>
@@ -709,8 +710,8 @@
<target>%@ 'den gelen bütün yeni mesajlar saklı olacak!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1353,10 +1354,6 @@
<target>ICE sunucularını ayarla</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Onayla</target>
@@ -1536,6 +1533,10 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>Kişi izin veriyor</target>
@@ -1709,8 +1710,8 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
<target>Şu anki parola…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -1836,7 +1837,6 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
</trans-unit>
<trans-unit id="Debug delivery" xml:space="preserve">
<source>Debug delivery</source>
<target>Hata ayıklama teslimatı</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Decentralized" xml:space="preserve">
@@ -3922,7 +3922,6 @@ Bu senin grup için bağlantın %@!</target>
</trans-unit>
<trans-unit id="Message queue info" xml:space="preserve">
<source>Message queue info</source>
<target>Mesaj kuyruğu bilgisi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reactions" xml:space="preserve">
@@ -3940,10 +3939,6 @@ Bu senin grup için bağlantın %@!</target>
<target>Mesaj tepkileri bu grupta yasaklandı.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<target>Mesaj yönlendirme yedeklemesi</target>
@@ -3967,6 +3962,10 @@ Bu senin grup için bağlantın %@!</target>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Mesaj yazısı</target>
@@ -4486,10 +4485,6 @@ Bu senin grup için bağlantın %@!</target>
<target>Diğer</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING sayısı</target>
@@ -4656,10 +4651,6 @@ Hata: %@</target>
<target>Lütfen parolayı güvenli bir şekilde saklayın, kaybederseniz parolayı DEĞİŞTİREMEZSİNİZ.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Lehçe arayüz</target>
@@ -4729,10 +4720,6 @@ Hata: %@</target>
<target>Gizli yönlendirme</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Profil ve sunucu bağlantıları</target>
@@ -5640,10 +5627,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Sunucu adresi ağ ayarlarıyla uyumlu değil.</target>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin</target>
@@ -5668,14 +5651,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Sunucu sürümü ağ ayarlarıyla uyumlu değil.</target>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Sunucular</target>
@@ -5813,10 +5788,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Mesaj durumunu göster</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Ön gösterimi göser</target>
@@ -6041,6 +6012,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -7103,8 +7078,8 @@ Katılma isteği tekrarlansın mı?</target>
<target>Ayarlardan SimpleX kişilerinize görünür yapabilirsiniz.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Artık %@ adresine mesaj gönderebilirsin</target>
<note>notification body</note>
</trans-unit>
@@ -7511,7 +7486,7 @@ SimpleX sunucuları profilinizi göremez.</target>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>yönetici tarafından engellendi</target>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -8128,9 +8103,6 @@ SimpleX sunucuları profilinizi göremez.</target>
<source>server queue info: %1$@
last received msg: %2$@</source>
<target>sunucu kuyruk bilgisi: %1$@
son alınan msj: %2$@</target>
<note>queue info</note>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
@@ -127,6 +127,11 @@
<target>%@ перевірено</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ сервери</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<target>%@ завантажено</target>
@@ -585,10 +590,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам.</target>
@@ -609,16 +610,16 @@
<target>Додати профіль</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>Додати сервер</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>Додайте сервери, відсканувавши QR-код.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>Додати сервер…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>Додати до іншого пристрою</target>
@@ -709,8 +710,8 @@
<target>Всі нові повідомлення від %@ будуть приховані!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1353,10 +1354,6 @@
<target>Налаштування серверів ICE</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>Підтвердити</target>
@@ -1536,6 +1533,10 @@ This is your own one-time link!</source>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>Контакт дозволяє</target>
@@ -1709,8 +1710,8 @@ This is your own one-time link!</source>
<target>Поточна парольна фраза…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -1836,7 +1837,6 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Debug delivery" xml:space="preserve">
<source>Debug delivery</source>
<target>Доставка налагодження</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Decentralized" xml:space="preserve">
@@ -3922,7 +3922,6 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="Message queue info" xml:space="preserve">
<source>Message queue info</source>
<target>Інформація про чергу повідомлень</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reactions" xml:space="preserve">
@@ -3940,10 +3939,6 @@ This is your link for group %@!</source>
<target>Реакції на повідомлення в цій групі заборонені.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<target>Запасний варіант маршрутизації повідомлень</target>
@@ -3967,6 +3962,10 @@ This is your link for group %@!</source>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>Текст повідомлення</target>
@@ -4486,10 +4485,6 @@ This is your link for group %@!</source>
<target>Інше</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Кількість PING</target>
@@ -4656,10 +4651,6 @@ Error: %@</source>
<target>Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>Польський інтерфейс</target>
@@ -4729,10 +4720,6 @@ Error: %@</source>
<target>Приватна маршрутизація</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>З'єднання профілю та сервера</target>
@@ -5640,10 +5627,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Адреса сервера несумісна з налаштуваннями мережі.</target>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>Сервер вимагає авторизації для створення черг, перевірте пароль</target>
@@ -5668,14 +5651,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Серверна версія несумісна з мережевими налаштуваннями.</target>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>Сервери</target>
@@ -5813,10 +5788,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Показати статус повідомлення</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>Показати попередній перегляд</target>
@@ -6041,6 +6012,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -7103,8 +7078,8 @@ Repeat join request?</source>
<target>Ви можете зробити його видимим для ваших контактів у SimpleX за допомогою налаштувань.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Тепер ви можете надсилати повідомлення на адресу %@</target>
<note>notification body</note>
</trans-unit>
@@ -7511,7 +7486,7 @@ SimpleX servers cannot see your profile.</source>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>заблоковано адміністратором</target>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -8128,9 +8103,6 @@ SimpleX servers cannot see your profile.</source>
<source>server queue info: %1$@
last received msg: %2$@</source>
<target>інформація про чергу на сервері: %1$@
останнє отримане повідомлення: %2$@</target>
<note>queue info</note>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
@@ -126,6 +126,11 @@
<target>%@ 已认证</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<source>%@ servers</source>
<target>%@ 服务器</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve">
<source>%@ uploaded</source>
<note>No comment provided by engineer.</note>
@@ -573,10 +578,6 @@
<source>Acknowledgement errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve">
<source>Active connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
<target>将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。</target>
@@ -597,16 +598,16 @@
<target>添加个人资料</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve">
<source>Add server</source>
<target>添加服务器</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target>扫描二维码来添加服务器。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target>添加服务器…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<target>添加另一设备</target>
@@ -696,8 +697,8 @@
<source>All new messages from %@ will be hidden!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<trans-unit id="All users" xml:space="preserve">
<source>All users</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -1336,10 +1337,6 @@
<target>配置 ICE 服务器</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Configured %@ servers" xml:space="preserve">
<source>Configured %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm" xml:space="preserve">
<source>Confirm</source>
<target>确认</target>
@@ -1512,6 +1509,10 @@ This is your own one-time link!</source>
<source>Connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connections subscribed" xml:space="preserve">
<source>Connections subscribed</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact allows" xml:space="preserve">
<source>Contact allows</source>
<target>联系人允许</target>
@@ -1683,8 +1684,8 @@ This is your own one-time link!</source>
<target>现有密码……</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current profile" xml:space="preserve">
<source>Current profile</source>
<trans-unit id="Current user" xml:space="preserve">
<source>Current user</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
@@ -3887,10 +3888,6 @@ This is your link for group %@!</source>
<target>该群组禁用了消息回应。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reception" xml:space="preserve">
<source>Message reception</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message routing fallback" xml:space="preserve">
<source>Message routing fallback</source>
<note>No comment provided by engineer.</note>
@@ -3912,6 +3909,10 @@ This is your link for group %@!</source>
<source>Message status: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Message subscriptions" xml:space="preserve">
<source>Message subscriptions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<source>Message text</source>
<target>消息正文</target>
@@ -4425,10 +4426,6 @@ This is your link for group %@!</source>
<target>其他</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other %@ servers" xml:space="preserve">
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING 次数</target>
@@ -4592,10 +4589,6 @@ Error: %@</source>
<target>请安全地保存密码,如果您丢失了密码,您将无法更改它。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please try later." xml:space="preserve">
<source>Please try later.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Polish interface" xml:space="preserve">
<source>Polish interface</source>
<target>波兰语界面</target>
@@ -4662,10 +4655,6 @@ Error: %@</source>
<source>Private routing</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private routing error" xml:space="preserve">
<source>Private routing error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>资料和服务器连接</target>
@@ -5562,10 +5551,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server address is incompatible with network settings.</source>
<note>srv error text.</note>
</trans-unit>
<trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve">
<source>Server address is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
<source>Server requires authorization to create queues, check password</source>
<target>服务器需要授权才能创建队列,检查密码</target>
@@ -5589,14 +5574,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Server version is incompatible with network settings.</source>
<note>srv error text</note>
</trans-unit>
<trans-unit id="Server version is incompatible with network settings: %@." xml:space="preserve">
<source>Server version is incompatible with network settings: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve">
<source>Server version is incompatible with your app: %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Servers" xml:space="preserve">
<source>Servers</source>
<target>服务器</target>
@@ -5733,10 +5710,6 @@ Enable in *Network &amp; servers* settings.</source>
<source>Show message status</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show percentage" xml:space="preserve">
<source>Show percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
<source>Show preview</source>
<target>显示预览</target>
@@ -5960,6 +5933,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Subscription errors</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscription percentage" xml:space="preserve">
<source>Subscription percentage</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Subscriptions ignored" xml:space="preserve">
<source>Subscriptions ignored</source>
<note>No comment provided by engineer.</note>
@@ -7004,8 +6981,8 @@ Repeat join request?</source>
<target>你可以通过设置让它对你的 SimpleX 联系人可见。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now chat with %@" xml:space="preserve">
<source>You can now chat with %@</source>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>您现在可以给 %@ 发送消息</target>
<note>notification body</note>
</trans-unit>
@@ -7407,7 +7384,7 @@ SimpleX 服务器无法看到您的资料。</target>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>由管理员封禁</target>
<note>marked deleted chat item preview text</note>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
@@ -358,9 +358,9 @@
<target state="translated">使用二維碼掃描以新增伺服器。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server" xml:space="preserve" approved="no">
<source>Add server</source>
<target state="translated">新增伺服器</target>
<trans-unit id="Add server" xml:space="preserve" approved="no">
<source>Add server</source>
<target state="translated">新增伺服器</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve" approved="no">
+14 -22
View File
@@ -119,7 +119,7 @@ class NotificationService: UNNotificationServiceExtension {
var threadId: UUID? = NSEThreads.shared.newThread()
var notificationInfo: NtfMessages?
var receiveEntityId: String?
var expectedMessage: String?
var expectedMessages: Set<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.ntfMessage_ == nil ? 0 : 1))")
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count))")
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
expectedMessage = ntfInfo.ntfMessage_.flatMap { $0.msgId }
expectedMessages = Set(ntfInfo.ntfMessages.map { $0.msgId })
shouldProcessNtf = true
return
}
@@ -224,10 +224,12 @@ class NotificationService: UNNotificationServiceExtension {
self.setBestAttemptNtf(.empty)
}
if case let .msgInfo(info) = ntf {
if info.msgId == expectedMessage {
expectedMessage = nil
logger.debug("NotificationService processNtf: msgInfo")
self.deliverBestAttemptNtf()
let found = expectedMessages.remove(info.msgId)
if found != nil {
logger.debug("NotificationService processNtf: msgInfo, last: \(self.expectedMessages.isEmpty)")
if expectedMessages.isEmpty {
self.deliverBestAttemptNtf()
}
return true
} else if info.msgTs > msgTs {
logger.debug("NotificationService processNtf: unexpected msgInfo, let other instance to process it, stopping this one")
@@ -390,16 +392,6 @@ 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)
@@ -644,7 +636,7 @@ func apiGetActiveUser() -> User? {
}
func apiStartChat() throws -> Bool {
let r = sendSimpleXCmd(.startChat(mainApp: false, enableSndFiles: false))
let r = sendSimpleXCmd(.startChat(mainApp: false))
switch r {
case .chatStarted: return true
case .chatRunning: return false
@@ -685,9 +677,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, ntfMessage_) = r, let user = user {
logger.debug("apiGetNtfMessage response ntfMessages: \(ntfMessage_ == nil ? 0 : 1)")
return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessage_: ntfMessage_)
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)
} else if case let .chatCmdError(_, error) = r {
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
} else {
@@ -734,7 +726,7 @@ struct NtfMessages {
var user: User
var connEntity_: ConnectionEntity?
var msgTs: Date?
var ntfMessage_: NtfMsgInfo?
var ntfMessages: [NtfMsgInfo]
var ntfsEnabled: Bool {
user.showNotifications && (connEntity_?.ntfsEnabled ?? false)
-35
View File
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
<integer>0</integer>
<key>NSExtensionActivationSupportsAttachmentsWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>ShareViewController</string>
</dict>
</dict>
</plist>
-39
View File
@@ -1,39 +0,0 @@
//
// 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
}
}
}
-115
View File
@@ -1,115 +0,0 @@
//
// ShareAPI.swift
// SimpleX SE
//
// Created by User on 15/07/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import OSLog
import Foundation
import SimpleXChat
let logger = Logger()
func apiGetActiveUser() throws -> User? {
let r = sendSimpleXCmd(.showActiveUser)
switch r {
case let .activeUser(user): return user
case .chatCmdError(_, .error(.noActiveUser)): return nil
default: throw r
}
}
func apiStartChat() throws -> Bool {
let r = sendSimpleXCmd(.startChat(mainApp: false, enableSndFiles: true))
switch r {
case .chatStarted: return true
case .chatRunning: return false
default: throw r
}
}
func apiSetNetworkConfig(_ cfg: NetCfg) throws {
let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg))
if case .cmdOk = r { return }
throw r
}
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws {
let r = sendSimpleXCmd(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder))
if case .cmdOk = r { return }
throw r
}
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable))
if case .cmdOk = r { return }
throw r
}
func apiGetChats(userId: User.ID) throws -> Array<ChatData> {
let r = sendSimpleXCmd(.apiGetChats(userId: userId))
if case let .apiChats(user: _, chats: chats) = r { return chats }
throw r
}
func apiSendMessage(
chatInfo: ChatInfo,
cryptoFile: CryptoFile?,
msgContent: MsgContent
) throws -> AChatItem {
let r = sendSimpleXCmd(
chatInfo.chatType == .local
? .apiCreateChatItem(
noteFolderId: chatInfo.apiId,
file: cryptoFile,
msg: msgContent
)
: .apiSendMessage(
type: chatInfo.chatType,
id: chatInfo.apiId,
file: cryptoFile,
quotedItemId: nil,
msg: msgContent,
live: false,
ttl: nil
)
)
if case let .newChatItem(_, chatItem) = r {
return chatItem
} else {
if let filePath = cryptoFile?.filePath { removeFile(filePath) }
throw r
}
}
func apiActivateChat() throws {
chatReopenStore()
let r = sendSimpleXCmd(.apiActivateChat(restoreChat: false))
if case .cmdOk = r { return }
throw r
}
func apiSuspendChat(expired: Bool) {
let r = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: expired ? 0 : 3_000000))
// Block until `chatSuspended` received or 3 seconds has passed
var suspended = false
if case .cmdOk = r, !expired {
let startTime = CFAbsoluteTimeGetCurrent()
while CFAbsoluteTimeGetCurrent() - startTime < 3 {
switch recvSimpleXMsg(messageTimeout: 3_500000) {
case .chatSuspended:
suspended = false
break
default: continue
}
}
}
if !suspended {
_ = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: 0))
}
logger.debug("close store")
chatCloseStore()
SEChatState.shared.set(.inactive)
}
-500
View File
@@ -1,500 +0,0 @@
//
// ShareModel.swift
// SimpleX SE
//
// Created by Levitating Pineapple on 09/07/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import UniformTypeIdentifiers
import AVFoundation
import SwiftUI
import SimpleXChat
/// Maximum size of hex encoded media previews
private let MAX_DATA_SIZE: Int64 = 14000
/// Maximum dimension (width or height) of an image, before passed for processing
private let MAX_DOWNSAMPLE_SIZE: Int64 = 2000
class ShareModel: ObservableObject {
@Published var sharedContent: SharedContent?
@Published var chats = Array<ChatData>()
@Published var profileImages = Dictionary<ChatInfo.ID, UIImage>()
@Published var search = String()
@Published var comment = String()
@Published var selected: ChatData?
@Published var isLoaded = false
@Published var bottomBar: BottomBar = .loadingSpinner
@Published var errorAlert: ErrorAlert?
enum BottomBar {
case sendButton
case loadingSpinner
case loadingBar(progress: Double)
var isLoading: Bool {
switch self {
case .sendButton: false
case .loadingSpinner: true
case .loadingBar: true
}
}
}
var completion: () -> Void = {
fatalError("completion has not been set")
}
private var itemProvider: NSItemProvider?
var isSendDisbled: Bool { sharedContent == nil || selected == nil }
var filteredChats: Array<ChatData> {
search.isEmpty
? filterChatsToForwardTo(chats: chats)
: filterChatsToForwardTo(chats: chats)
.filter { foundChat($0, search.localizedLowercase) }
}
func setup(context: NSExtensionContext) {
if let item = context.inputItems.first as? NSExtensionItem,
let itemProvider = item.attachments?.first {
self.itemProvider = itemProvider
self.completion = {
ShareModel.CompletionHandler.isEventLoopEnabled = false
context.completeRequest(returningItems: [item]) {
apiSuspendChat(expired: $0)
}
}
// Init Chat
Task {
if let e = initChat() {
await MainActor.run { errorAlert = e }
} else {
// Load Chats
Task {
switch fetchChats() {
case let .success(chats):
// Decode base64 images on background thread
let profileImages = chats.reduce(into: Dictionary<ChatInfo.ID, UIImage>()) { dict, chatData in
if let profileImage = chatData.chatInfo.image,
let uiImage = UIImage(base64Encoded: profileImage) {
dict[chatData.id] = uiImage
}
}
await MainActor.run {
self.chats = chats
self.profileImages = profileImages
withAnimation { isLoaded = true }
}
case let .failure(error):
await MainActor.run { errorAlert = error }
}
}
// Process Attachment
Task {
switch await self.itemProvider!.sharedContent() {
case let .success(chatItemContent):
await MainActor.run {
self.sharedContent = chatItemContent
self.bottomBar = .sendButton
if case let .text(string) = chatItemContent { comment = string }
}
case let .failure(errorAlert):
await MainActor.run { self.errorAlert = errorAlert }
}
}
}
}
}
}
func send() {
if let sharedContent, let selected {
Task {
await MainActor.run { self.bottomBar = .loadingSpinner }
do {
SEChatState.shared.set(.sendingMessage)
await waitForOtherProcessesToSuspend()
let ci = try apiSendMessage(
chatInfo: selected.chatInfo,
cryptoFile: sharedContent.cryptoFile,
msgContent: sharedContent.msgContent(comment: self.comment)
)
if selected.chatInfo.chatType == .local {
completion()
} else {
await MainActor.run { self.bottomBar = .loadingBar(progress: .zero) }
if let e = await handleEvents(
isGroupChat: ci.chatInfo.chatType == .group,
isWithoutFile: sharedContent.cryptoFile == nil,
chatItemId: ci.chatItem.id
) {
await MainActor.run { errorAlert = e }
} else {
completion()
}
}
} catch {
if let e = error as? ErrorAlert {
await MainActor.run { errorAlert = e }
}
}
}
}
}
private func initChat() -> ErrorAlert? {
do {
if hasChatCtrl() {
try apiActivateChat()
} else {
registerGroupDefaults()
haskell_init_se()
let (_, result) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation())
if let e = migrationError(result) { return e }
try apiSetAppFilePaths(
filesFolder: getAppFilesDirectory().path,
tempFolder: getTempFilesDirectory().path,
assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path
)
let isRunning = try apiStartChat()
logger.log(level: .debug, "chat started, running: \(isRunning)")
}
try apiSetNetworkConfig(getNetCfg())
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
} catch { return ErrorAlert(error) }
return nil
}
private func migrationError(_ r: DBMigrationResult) -> ErrorAlert? {
let useKeychain = storeDBPassphraseGroupDefault.get()
let storedDBKey = kcDatabasePassword.get()
// This switch duplicates DatabaseErrorView.
// TODO allow entering passphrase and make messages the same as in DatabaseErrorView.
return switch r {
case .errorNotADatabase:
if useKeychain && storedDBKey != nil && storedDBKey != "" {
ErrorAlert(
title: "Wrong database passphrase",
message: "Database passphrase is different from saved in the keychain."
)
} else {
ErrorAlert(
title: "Encrypted database",
message: "Sharing is not supported when passphrase is not stored in KeyChain."
)
}
case let .errorMigration(_, migrationError):
switch migrationError {
case .upgrade:
ErrorAlert(
title: "Database upgrade required",
message: "Open the app to upgrade the database."
)
case .downgrade:
ErrorAlert(
title: "Database downgrade required",
message: "Open the app to downgrade the database."
)
case let .migrationError(mtrError):
ErrorAlert(
title: "Incompatible database version",
message: mtrErrorDescription(mtrError)
)
}
case let .errorSQL(_, migrationSQLError):
ErrorAlert(
title: "Database error",
message: "Error: \(migrationSQLError)"
)
case .errorKeychain:
ErrorAlert(
title: "Keychain error",
message: "Cannot access keychain to save database password"
)
case .invalidConfirmation:
ErrorAlert("Invalid migration confirmation")
case let .unknown(json):
ErrorAlert(
title: "Database error",
message: "Unknown database error: \(json)"
)
case .ok: nil
}
}
private func fetchChats() -> Result<Array<ChatData>, ErrorAlert> {
do {
guard let user = try apiGetActiveUser() else {
return .failure(
ErrorAlert(
title: "No active profile",
message: "Please create a profile in the SimpleX app"
)
)
}
return .success(try apiGetChats(userId: user.id))
} catch {
return .failure(ErrorAlert(error))
}
}
actor CompletionHandler {
static var isEventLoopEnabled = false
private var fileCompleted = false
private var messageCompleted = false
func completeFile() { fileCompleted = true }
func completeMessage() { messageCompleted = true }
var isRunning: Bool {
Self.isEventLoopEnabled && !(fileCompleted && messageCompleted)
}
}
/// Polls and processes chat events
/// Returns when message sending has completed optionally returning and error.
private func handleEvents(isGroupChat: Bool, isWithoutFile: Bool, chatItemId: ChatItem.ID) async -> ErrorAlert? {
func isMessage(for item: AChatItem?) -> Bool {
item.map { $0.chatItem.id == chatItemId } ?? false
}
CompletionHandler.isEventLoopEnabled = true
let ch = CompletionHandler()
if isWithoutFile { await ch.completeFile() }
var networkTimeout = CFAbsoluteTimeGetCurrent()
while await ch.isRunning {
if CFAbsoluteTimeGetCurrent() - networkTimeout > 30 {
networkTimeout = CFAbsoluteTimeGetCurrent()
await MainActor.run {
self.errorAlert = ErrorAlert(title: "No network connection") {
Button("Keep Trying", role: .cancel) { }
Button("Dismiss Sheet", role: .destructive) { self.completion() }
}
}
}
switch recvSimpleXMsg(messageTimeout: 1_000_000) {
case let .sndFileProgressXFTP(_, ci, _, sentSize, totalSize):
guard isMessage(for: ci) else { continue }
networkTimeout = CFAbsoluteTimeGetCurrent()
await MainActor.run {
withAnimation {
let progress = Double(sentSize) / Double(totalSize)
bottomBar = .loadingBar(progress: progress)
}
}
case let .sndFileCompleteXFTP(_, ci, _):
guard isMessage(for: ci) else { continue }
if isGroupChat {
await MainActor.run { bottomBar = .loadingSpinner }
}
await ch.completeFile()
if await !ch.isRunning { break }
case let .chatItemStatusUpdated(_, ci):
guard isMessage(for: ci) else { continue }
if let (title, message) = ci.chatItem.meta.itemStatus.statusInfo {
// `title` and `message` already localized and interpolated
return ErrorAlert(
title: "\(title)",
message: "\(message)"
)
} else if case let .sndSent(sndProgress) = ci.chatItem.meta.itemStatus {
switch sndProgress {
case .complete:
await ch.completeMessage()
case .partial:
if isGroupChat {
Task {
try? await Task.sleep(nanoseconds: 5 * NSEC_PER_SEC)
await ch.completeMessage()
}
}
}
}
case let .sndFileError(_, ci, _, errorMessage):
guard isMessage(for: ci) else { continue }
if let ci { cleanupFile(ci) }
return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)")
case let .sndFileWarning(_, ci, _, errorMessage):
guard isMessage(for: ci) else { continue }
if let ci { cleanupFile(ci) }
return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)")
case let .chatError(_, chatError):
return ErrorAlert(chatError)
case let .chatCmdError(_, chatError):
return ErrorAlert(chatError)
default: continue
}
}
return nil
}
private func fileErrorInfo(_ ci: AChatItem?) -> String? {
switch ci?.chatItem.file?.fileStatus {
case let .sndError(e): e.errorInfo
case let .sndWarning(e): e.errorInfo
default: nil
}
}
}
/// Chat Item Content extracted from `NSItemProvider` without the comment
enum SharedContent {
case image(preview: String, cryptoFile: CryptoFile)
case movie(preview: String, duration: Int, cryptoFile: CryptoFile)
case url(preview: LinkPreview)
case text(string: String)
case data(cryptoFile: CryptoFile)
var cryptoFile: CryptoFile? {
switch self {
case let .image(_, cryptoFile): cryptoFile
case let .movie(_, _, cryptoFile): cryptoFile
case .url: nil
case .text: nil
case let .data(cryptoFile): cryptoFile
}
}
func msgContent(comment: String) -> MsgContent {
switch self {
case let .image(preview, _): .image(text: comment, image: preview)
case let .movie(preview, duration, _): .video(text: comment, image: preview, duration: duration)
case let .url(preview): .link(text: comment, preview: preview)
case .text: .text(comment)
case .data: .file(comment)
}
}
}
extension NSItemProvider {
fileprivate func sharedContent() async -> Result<SharedContent, ErrorAlert> {
if let type = firstMatching(of: [.image, .movie, .fileURL, .url, .text]) {
switch type {
// Prepare Image message
case .image:
// Animated
return if hasItemConformingToTypeIdentifier(UTType.gif.identifier) {
if let url = try? await inPlaceUrl(type: type),
let data = try? Data(contentsOf: url),
let image = UIImage(data: data),
let cryptoFile = saveFile(data, generateNewFileName("IMG", "gif"), encrypted: privacyEncryptLocalFilesGroupDefault.get()),
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) {
.success(.image(preview: preview, cryptoFile: cryptoFile))
} else { .failure(ErrorAlert("Error preparing message")) }
// Static
} else {
if let url = try? await inPlaceUrl(type: type),
let image = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE),
let cryptoFile = saveImage(image),
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) {
.success(.image(preview: preview, cryptoFile: cryptoFile))
} else { .failure(ErrorAlert("Error preparing message")) }
}
// Prepare Movie message
case .movie:
if let url = try? await inPlaceUrl(type: type),
let trancodedUrl = await transcodeVideo(from: url),
let (image, duration) = AVAsset(url: trancodedUrl).generatePreview(),
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE),
let cryptoFile = moveTempFileFromURL(trancodedUrl) {
try? FileManager.default.removeItem(at: trancodedUrl)
return .success(.movie(preview: preview, duration: duration, cryptoFile: cryptoFile))
} else { return .failure(ErrorAlert("Error preparing message")) }
// Prepare Data message
case .fileURL:
if let url = try? await inPlaceUrl(type: .data) {
if isFileTooLarge(for: url) {
let sizeString = ByteCountFormatter.string(
fromByteCount: Int64(getMaxFileSize(.xftp)),
countStyle: .binary
)
return .failure(
ErrorAlert(
title: "Large file!",
message: "Currently maximum supported file size is \(sizeString)."
)
)
}
if let file = saveFileFromURL(url) {
return .success(.data(cryptoFile: file))
}
}
return .failure(ErrorAlert("Error preparing file"))
// Prepare Link message
case .url:
if let url = try? await loadItem(forTypeIdentifier: type.identifier) as? URL {
let content: SharedContent =
// Option to disable previews needs to be taken into account
// if let linkPreview = await getLinkPreview(for: url) {
// .url(preview: linkPreview)
// } else {
.text(string: url.absoluteString)
// }
return .success(content)
} else { return .failure(ErrorAlert("Error preparing message")) }
// Prepare Text message
case .text:
return if let text = try? await loadItem(forTypeIdentifier: type.identifier) as? String {
.success(.text(string: text))
} else { .failure(ErrorAlert("Error preparing message")) }
default: return .failure(ErrorAlert("Unsupported format"))
}
} else {
return .failure(ErrorAlert("Unsupported format"))
}
}
private func inPlaceUrl(type: UTType) async throws -> URL {
try await withCheckedThrowingContinuation { cont in
let _ = loadInPlaceFileRepresentation(forTypeIdentifier: type.identifier) { url, bool, error in
if let url = url {
cont.resume(returning: url)
} else if let error = error {
cont.resume(throwing: error)
} else {
fatalError("Either `url` or `error` must be present")
}
}
}
}
private func firstMatching(of types: Array<UTType>) -> UTType? {
for type in types {
if hasItemConformingToTypeIdentifier(type.identifier) { return type }
}
return nil
}
}
fileprivate func transcodeVideo(from input: URL) async -> URL? {
let outputUrl = URL(
fileURLWithPath: generateNewFileName(
getTempFilesDirectory().path + "/" + "video", "mp4",
fullPath: true
)
)
if await makeVideoQualityLower(input, outputUrl: outputUrl) {
return outputUrl
} else {
try? FileManager.default.removeItem(at: outputUrl)
return nil
}
}
fileprivate func isFileTooLarge(for url: URL) -> Bool {
fileSize(url)
.map { $0 > getMaxFileSize(.xftp) }
?? false
}
-180
View File
@@ -1,180 +0,0 @@
//
// ShareView.swift
// SimpleX SE
//
// Created by Levitating Pineapple on 09/07/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ShareView: View {
@ObservedObject var model: ShareModel
@Environment(\.colorScheme) var colorScheme
var body: some View {
NavigationView {
ZStack(alignment: .bottom) {
if model.isLoaded {
List(model.filteredChats) { chat in
HStack {
profileImage(
chatInfoId: chat.chatInfo.id,
systemFallback: chatIconName(chat.chatInfo),
size: 30
)
Text(chat.chatInfo.displayName)
Spacer()
radioButton(selected: chat == model.selected)
}
.contentShape(Rectangle())
.onTapGesture { model.selected = model.selected == chat ? nil : chat }
.tag(chat)
}
} else {
ProgressView().frame(maxHeight: .infinity)
}
}
.navigationTitle("Share")
.safeAreaInset(edge: .bottom) {
switch model.bottomBar {
case .sendButton:
compose(isLoading: false)
case .loadingSpinner:
compose(isLoading: true)
case .loadingBar(let progress):
loadingBar(progress: progress)
}
}
}
.searchable(
text: $model.search,
placement: .navigationBarDrawer(displayMode: .always)
)
.alert($model.errorAlert) { alert in
Button("Ok") { model.completion() }
}
}
private func compose(isLoading: Bool) -> some View {
VStack(spacing: .zero) {
Divider()
if let content = model.sharedContent {
itemPreview(content)
}
HStack {
Group {
if #available(iOSApplicationExtension 16.0, *) {
TextField("Comment", text: $model.comment, axis: .vertical)
} else {
TextField("Comment", text: $model.comment)
}
}
.contentShape(Rectangle())
.disabled(isLoading)
.padding(.horizontal, 12)
.padding(.vertical, 4)
Group {
if isLoading {
ProgressView()
} else {
Button(action: model.send) {
Image(systemName: "arrow.up.circle.fill")
.resizable()
}
.disabled(model.isSendDisbled)
}
}
.frame(width: 28, height: 28)
.padding(6)
}
.background(Color(.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(
RoundedRectangle(cornerRadius: 20)
.strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7)
)
.padding(8)
}
.background(.thinMaterial)
}
@ViewBuilder private func itemPreview(_ content: SharedContent) -> some View {
switch content {
case let .image(preview, _): imagePreview(preview)
case let .movie(preview, _, _): imagePreview(preview)
case let .url(linkPreview): imagePreview(linkPreview.image)
case let .data(cryptoFile):
previewArea {
Image(systemName: "doc.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30)
.foregroundColor(Color(uiColor: .tertiaryLabel))
.padding(.leading, 4)
Text(cryptoFile.filePath)
}
case .text: EmptyView()
}
}
@ViewBuilder private func imagePreview(_ img: String) -> some View {
if let img = UIImage(base64Encoded: img) {
previewArea {
Image(uiImage: img)
.resizable()
.scaledToFit()
.frame(minHeight: 40, maxHeight: 60)
}
} else {
EmptyView()
}
}
@ViewBuilder private func previewArea<V: View>(@ViewBuilder content: @escaping () -> V) -> some View {
HStack(alignment: .center, spacing: 8) {
content()
Spacer()
}
.padding(.vertical, 1)
.frame(minHeight: 54)
.background {
switch colorScheme {
case .light: LightColorPaletteApp.sentMessage
case .dark: DarkColorPaletteApp.sentMessage
@unknown default: Color(.tertiarySystemBackground)
}
}
Divider()
}
private func loadingBar(progress: Double) -> some View {
VStack {
Text("Sending File")
ProgressView(value: progress)
}
.padding()
.background(Material.ultraThin)
}
private func profileImage(chatInfoId: ChatInfo.ID, systemFallback: String, size: Double) -> some View {
Group {
if let uiImage = model.profileImages[chatInfoId] {
Image(uiImage: uiImage).resizable()
} else {
Image(systemName: systemFallback).resizable()
}
}
.foregroundStyle(Color(.tertiaryLabel))
.frame(width: size, height: size)
.clipShape(RoundedRectangle(cornerRadius: size * 0.225, style: .continuous))
}
private func radioButton(selected: Bool) -> some View {
Image(systemName: selected ? "checkmark.circle.fill" : "circle")
.imageScale(.large)
.foregroundStyle(selected ? Color.accentColor : Color(.tertiaryLabel))
}
}
@@ -1,46 +0,0 @@
//
// ShareViewController.swift
// SimpleX SE
//
// Created by Levitating Pineapple on 08/07/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import UIKit
import SwiftUI
import SimpleXChat
/// Extension Entry point
/// System will create this controller each time share sheet is invoked
/// using `NSExtensionPrincipalClass` in the info.plist
@objc(ShareViewController)
class ShareViewController: UIHostingController<ShareView> {
private let model = ShareModel()
// Assuming iOS continues to only allow single share sheet to be presented at once
static var isVisible: Bool = false
@objc init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(rootView: ShareView(model: model))
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) { fatalError() }
override func viewDidLoad() {
ShareModel.CompletionHandler.isEventLoopEnabled = false
model.setup(context: extensionContext!)
}
override func viewWillAppear(_ animated: Bool) {
logger.debug("ShareSheet will appear")
super.viewWillAppear(animated)
Self.isVisible = true
}
override func viewWillDisappear(_ animated: Bool) {
logger.debug("ShareSheet will dissappear")
super.viewWillDisappear(animated)
ShareModel.CompletionHandler.isEventLoopEnabled = false
Self.isVisible = false
}
}
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.chat.simplex.app</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)chat.simplex.app</string>
</array>
</dict>
</plist>
+38 -233
View File
@@ -100,6 +100,7 @@
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; };
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; };
5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */; };
5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */; };
5CBD285C29575B8E00EC2CF4 /* WhatsNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD285B29575B8E00EC2CF4 /* WhatsNewView.swift */; };
5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */; };
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */; };
@@ -178,6 +179,7 @@
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 */; };
@@ -193,17 +195,7 @@
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, ); }; };
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 */; };
@@ -211,13 +203,12 @@
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 */; };
E5DCF8D52C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D02C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a */; };
E5DCF8D62C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D12C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a */; };
E5DCF8D72C56F7EF007928CC /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D22C56F7EF007928CC /* libffi.a */; };
E5DCF8D82C56F7EF007928CC /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D32C56F7EF007928CC /* libgmp.a */; };
E5DCF8D92C56F7EF007928CC /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D42C56F7EF007928CC /* libgmpxx.a */; };
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -249,20 +240,6 @@
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 */
@@ -283,7 +260,6 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */,
5CE2BA9D284555F500EC33A6 /* SimpleX NSE.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
@@ -512,6 +488,7 @@
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = "<group>"; };
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
8C74C3EB2C1B92A900039E77 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaper.swift; sourceTree = "<group>"; };
@@ -526,27 +503,17 @@
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = "<group>"; };
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = "<group>"; };
CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = "<group>"; };
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = "<group>"; };
CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = "<group>"; };
CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX SE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
CEE723AE2C3BD3D70009AE93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX SE.entitlements"; sourceTree = "<group>"; };
CEE723EF2C3D25C70009AE93 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = "<group>"; };
CEE723F12C3D25ED0009AE93 /* ShareModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareModel.swift; sourceTree = "<group>"; };
CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseList.swift; sourceTree = "<group>"; };
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
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; };
E5DCF8D02C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a"; sourceTree = "<group>"; };
E5DCF8D12C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a"; sourceTree = "<group>"; };
E5DCF8D22C56F7EF007928CC /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E5DCF8D32C56F7EF007928CC /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E5DCF8D42C56F7EF007928CC /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E50580FB2C3DDD7F009C3F71 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E50580FC2C3DDD7F009C3F71 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
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 = "<group>"; };
E50580FE2C3DDD7F009C3F71 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E50580FF2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -585,23 +552,14 @@
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 */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
E5DCF8D82C56F7EF007928CC /* libgmp.a in Frameworks */,
E50581022C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a in Frameworks */,
E50581062C3DDD9D009C3F71 /* Yams in Frameworks */,
E5DCF8D62C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a in Frameworks */,
E5DCF8D72C56F7EF007928CC /* libffi.a in Frameworks */,
E5DCF8D92C56F7EF007928CC /* libgmpxx.a in Frameworks */,
E5DCF8D52C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
E5DCF8DA2C56FABA007928CC /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */,
E50581042C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -668,11 +626,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
E5DCF8D22C56F7EF007928CC /* libffi.a */,
E5DCF8D32C56F7EF007928CC /* libgmp.a */,
E5DCF8D42C56F7EF007928CC /* libgmpxx.a */,
E5DCF8D02C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a */,
E5DCF8D12C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a */,
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 */,
);
path = Libraries;
sourceTree = "<group>";
@@ -700,6 +658,7 @@
5CF937212B25034A00E1D781 /* NSESubscriber.swift */,
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */,
5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */,
5CBD2859295711D700EC2CF4 /* ImageUtils.swift */,
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */,
);
path = Model;
@@ -724,6 +683,7 @@
64466DCB29FFE3E800E3D48D /* MailView.swift */,
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */,
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */,
8C05382D2B39887E006436DC /* VideoUtils.swift */,
8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */,
8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */,
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */,
@@ -742,7 +702,6 @@
5C764E5C279C70B7000C6508 /* Libraries */,
5CA059C2279559F40002BEB4 /* Shared */,
5CDCAD462818589900503DA2 /* SimpleX NSE */,
CEE723A82C3BD3D70009AE93 /* SimpleX SE */,
5CA059DA279559F40002BEB4 /* Tests iOS */,
5CE2BA692845308900EC33A6 /* SimpleXChat */,
5CA059CB279559F40002BEB4 /* Products */,
@@ -773,7 +732,6 @@
5CA059D7279559F40002BEB4 /* Tests iOS.xctest */,
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */,
5CE2BA682845308900EC33A6 /* SimpleXChat.framework */,
CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -897,12 +855,9 @@
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 */,
@@ -1017,20 +972,6 @@
path = Theme;
sourceTree = "<group>";
};
CEE723A82C3BD3D70009AE93 /* SimpleX SE */ = {
isa = PBXGroup;
children = (
CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */,
CEE723AE2C3BD3D70009AE93 /* Info.plist */,
CEDE70212C48FD9500233B1F /* SEChatState.swift */,
CE1EB0E32C459A660099D896 /* ShareAPI.swift */,
CEE723F12C3D25ED0009AE93 /* ShareModel.swift */,
CEE723EF2C3D25C70009AE93 /* ShareView.swift */,
CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */,
);
path = "SimpleX SE";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -1062,7 +1003,6 @@
dependencies = (
5CE2BA6F2845308900EC33A6 /* PBXTargetDependency */,
5CE2BA9F284555F500EC33A6 /* PBXTargetDependency */,
CEE723B02C3BD3D70009AE93 /* PBXTargetDependency */,
);
name = "SimpleX (iOS)";
packageProductDependencies = (
@@ -1128,30 +1068,11 @@
name = SimpleXChat;
packageProductDependencies = (
E50581052C3DDD9D009C3F71 /* Yams */,
CE38A29B2C3FCD72005ED185 /* SwiftyGif */,
);
productName = SimpleXChat;
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 */,
E5DCF8DA2C56FABA007928CC /* 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 */
@@ -1159,7 +1080,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1540;
LastSwiftUpdateCheck = 1330;
LastUpgradeCheck = 1340;
ORGANIZATIONNAME = "SimpleX Chat";
TargetAttributes = {
@@ -1179,9 +1100,6 @@
CreatedOnToolsVersion = 13.3;
LastSwiftMigration = 1330;
};
CEE723A62C3BD3D70009AE93 = {
CreatedOnToolsVersion = 15.4;
};
};
};
buildConfigurationList = 5CA059C1279559F40002BEB4 /* Build configuration list for PBXProject "SimpleX" */;
@@ -1223,7 +1141,6 @@
5CA059C9279559F40002BEB4 /* SimpleX (iOS) */,
5CA059D6279559F40002BEB4 /* Tests iOS */,
5CDCAD442818589900503DA2 /* SimpleX NSE */,
CEE723A62C3BD3D70009AE93 /* SimpleX SE */,
5CE2BA672845308900EC33A6 /* SimpleXChat */,
);
};
@@ -1263,13 +1180,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
CEE723A52C3BD3D70009AE93 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -1295,6 +1205,7 @@
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */,
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */,
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */,
5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */,
8C74C3EC2C1B92A900039E77 /* Theme.swift in Sources */,
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */,
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
@@ -1386,6 +1297,7 @@
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 */,
@@ -1454,7 +1366,6 @@
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 */,
@@ -1465,29 +1376,15 @@
5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */,
5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */,
5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */,
CE38A29A2C3FCA54005ED185 /* ImageUtils.swift in Sources */,
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */,
5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */,
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 */
@@ -1512,16 +1409,6 @@
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 */
@@ -1728,7 +1615,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 228;
CURRENT_PROJECT_VERSION = 227;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1777,7 +1664,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 228;
CURRENT_PROJECT_VERSION = 227;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1863,7 +1750,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 228;
CURRENT_PROJECT_VERSION = 227;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -1900,7 +1787,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 228;
CURRENT_PROJECT_VERSION = 227;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -1937,7 +1824,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 228;
CURRENT_PROJECT_VERSION = 227;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1988,7 +1875,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 228;
CURRENT_PROJECT_VERSION = 227;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2032,74 +1919,6 @@
};
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 */
@@ -2148,15 +1967,6 @@
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 */
@@ -2164,8 +1974,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/twostraws/CodeScanner";
requirement = {
kind = exactVersion;
version = 2.1.1;
kind = upToNextMajorVersion;
minimumVersion = 2.0.0;
};
};
8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */ = {
@@ -2188,8 +1998,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kirualex/SwiftyGif";
requirement = {
kind = revision;
revision = 5e8619335d394901379c9add5c4c1c2f420b3800;
branch = master;
kind = branch;
};
};
D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */ = {
@@ -2213,11 +2023,6 @@
package = 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */;
productName = Yams;
};
CE38A29B2C3FCD72005ED185 /* SwiftyGif */ = {
isa = XCSwiftPackageProductDependency;
package = D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */;
productName = SwiftyGif;
};
D7197A1729AE89660055C05A /* WebRTC */ = {
isa = XCSwiftPackageProductDependency;
package = D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */;
@@ -1,97 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CEE723A62C3BD3D70009AE93"
BuildableName = "SimpleX SE.appex"
BlueprintName = "SimpleX SE"
ReferencedContainer = "container:SimpleX.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5CA059C9279559F40002BEB4"
BuildableName = "SimpleX.app"
BlueprintName = "SimpleX (iOS)"
ReferencedContainer = "container:SimpleX.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5CA059C9279559F40002BEB4"
BuildableName = "SimpleX.app"
BlueprintName = "SimpleX (iOS)"
ReferencedContainer = "container:SimpleX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5CA059C9279559F40002BEB4"
BuildableName = "SimpleX.app"
BlueprintName = "SimpleX (iOS)"
ReferencedContainer = "container:SimpleX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+10 -18
View File
@@ -117,10 +117,10 @@ public func sendSimpleXCmd(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) -> Chat
}
// in microseconds
public let MESSAGE_TIMEOUT: Int32 = 15_000_000
let MESSAGE_TIMEOUT: Int32 = 15_000_000
public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> ChatResponse? {
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout) {
public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil) -> ChatResponse? {
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), MESSAGE_TIMEOUT) {
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(serializeJSON(jChat, options: .prettyPrinted) ?? "")
return ChatData.invalidJSON(prettyJSON(jChat) ?? "")
}
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: errorJson(jError) ?? ""))
return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? ""))
}
} else if type == "chatError" {
if let jError = jResp["chatError"] as? NSDictionary {
return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? ""))
return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? ""))
}
}
}
json = serializeJSON(j, options: .prettyPrinted)
json = prettyJSON(j)
}
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
}
@@ -239,14 +239,6 @@ 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"]!)
@@ -259,7 +251,7 @@ func parseChatData(_ jChat: Any) throws -> ChatData {
return ChatItem.invalidJSON(
chatDir: decodeProperty(jCI, "chatDir"),
meta: decodeProperty(jCI, "meta"),
json: serializeJSON(jCI, options: .prettyPrinted) ?? ""
json: prettyJSON(jCI) ?? ""
)
}
return ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats)
@@ -276,8 +268,8 @@ func decodeProperty<T: Decodable>(_ obj: Any, _ prop: NSString) -> T? {
return nil
}
func serializeJSON(_ obj: Any, options: JSONSerialization.WritingOptions = []) -> String? {
if let d = try? JSONSerialization.data(withJSONObject: obj, options: options) {
func prettyJSON(_ obj: Any) -> String? {
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) {
return String(decoding: d, as: UTF8.self)
}
return nil
+37 -69
View File
@@ -26,8 +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, enableSndFiles: Bool)
case checkChatRunning
case startChat(mainApp: Bool)
case apiStopChat
case apiActivateChat(restoreChat: Bool)
case apiSuspendChat(timeoutMicroseconds: Int)
@@ -147,7 +146,6 @@ public enum ChatCommand {
case apiStandaloneFileInfo(url: String)
// misc
case showVersion
case getAgentSubsTotal(userId: Int64)
case getAgentServersSummary(userId: Int64)
case resetAgentServersStats
case string(String)
@@ -173,8 +171,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, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))"
case .checkChatRunning: return "/_check running"
case let .startChat(mainApp): return "/_start main=\(onOff(mainApp))"
case .apiStopChat: return "/_stop"
case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
@@ -312,7 +309,6 @@ 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
@@ -336,7 +332,6 @@ public enum ChatCommand {
case .apiUnmuteUser: return "apiUnmuteUser"
case .apiDeleteUser: return "apiDeleteUser"
case .startChat: return "startChat"
case .checkChatRunning: return "checkChatRunning"
case .apiStopChat: return "apiStopChat"
case .apiActivateChat: return "apiActivateChat"
case .apiSuspendChat: return "apiSuspendChat"
@@ -452,7 +447,6 @@ 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"
@@ -581,7 +575,6 @@ 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)
@@ -668,7 +661,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?, ntfMessage_: NtfMsgInfo?)
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo)
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
case contactDisabled(user: UserRef, contact: Contact)
@@ -684,7 +677,6 @@ 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)
@@ -750,7 +742,6 @@ 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"
@@ -846,7 +837,6 @@ 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"
@@ -917,7 +907,6 @@ 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
@@ -1013,7 +1002,6 @@ 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))
@@ -1134,20 +1122,20 @@ public struct ProtoServersConfig: Codable {
public struct UserProtoServers: Decodable {
public var serverProtocol: ServerProtocol
public var protoServers: [ServerCfg]
public var presetServers: [ServerCfg]
public var presetServers: [String]
}
public struct ServerCfg: Identifiable, Equatable, Codable, Hashable {
public struct ServerCfg: Identifiable, Equatable, Codable {
public var server: String
public var preset: Bool
public var tested: Bool?
public var enabled: Bool
public var enabled: ServerEnabled
var createdAt = Date()
// public var sendEnabled: Bool // can we potentially want to prevent sending on the servers we use to receive?
// Even if we don't see the use case, it's probably better to allow it in the model
// In any case, "trusted/known" servers are out of scope of this change
public init(server: String, preset: Bool, tested: Bool?, enabled: Bool) {
public init(server: String, preset: Bool, tested: Bool?, enabled: ServerEnabled) {
self.server = server
self.preset = preset
self.tested = tested
@@ -1160,7 +1148,7 @@ public struct ServerCfg: Identifiable, Equatable, Codable, Hashable {
public var id: String { "\(server) \(createdAt)" }
public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: false)
public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: .enabled)
public var isEmpty: Bool {
server.trimmingCharacters(in: .whitespaces) == ""
@@ -1177,19 +1165,19 @@ public struct ServerCfg: Identifiable, Equatable, Codable, Hashable {
server: "smp://abcd@smp8.simplex.im",
preset: true,
tested: true,
enabled: true
enabled: .enabled
),
custom: ServerCfg(
server: "smp://abcd@smp9.simplex.im",
preset: false,
tested: false,
enabled: false
enabled: .disabled
),
untested: ServerCfg(
server: "smp://abcd@smp10.simplex.im",
preset: false,
tested: nil,
enabled: true
enabled: .enabled
)
)
@@ -1201,6 +1189,12 @@ public struct ServerCfg: Identifiable, Equatable, Codable, Hashable {
}
}
public enum ServerEnabled: String, Codable {
case disabled
case enabled
case known
}
public enum ProtocolTestStep: String, Decodable, Equatable {
case connect
case disconnect
@@ -1240,7 +1234,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")
@@ -1299,32 +1293,42 @@ public struct NetCfg: Codable, Equatable, Hashable {
var socksMode: SocksMode = .always
public var hostMode: HostMode = .publicHost
public var requiredHostMode = true
public var sessionMode = TransportSessionMode.user
public var smpProxyMode: SMPProxyMode = .unknown
public var smpProxyFallback: SMPProxyFallback = .allowProtected
public var sessionMode: TransportSessionMode
public var smpProxyMode: SMPProxyMode = .never
public var smpProxyFallback: SMPProxyFallback = .allow
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? = KeepAliveOpts.defaults
public var tcpKeepAlive: KeepAliveOpts?
public var smpPingInterval: Int // microseconds
public var smpPingCount: Int = 3 // times
public var logTLSErrors: Bool = false
public var smpPingCount: Int // times
public var logTLSErrors: Bool
public static let defaults: NetCfg = NetCfg(
socksProxy: nil,
sessionMode: TransportSessionMode.user,
tcpConnectTimeout: 25_000_000,
tcpTimeout: 15_000_000,
tcpTimeoutPerKb: 10_000,
rcvConcurrency: 12,
smpPingInterval: 1200_000_000
tcpKeepAlive: KeepAliveOpts.defaults,
smpPingInterval: 1200_000_000,
smpPingCount: 3,
logTLSErrors: false
)
public static let proxyDefaults: NetCfg = NetCfg(
socksProxy: nil,
sessionMode: TransportSessionMode.user,
tcpConnectTimeout: 35_000_000,
tcpTimeout: 20_000_000,
tcpTimeoutPerKb: 15_000,
rcvConcurrency: 8,
smpPingInterval: 1200_000_000
tcpKeepAlive: KeepAliveOpts.defaults,
smpPingInterval: 1200_000_000,
smpPingCount: 3,
logTLSErrors: false
)
public var enableKeepAlive: Bool { tcpKeepAlive != nil }
@@ -1899,10 +1903,9 @@ public enum SQLiteError: Decodable, Hashable {
public enum AgentErrorType: Decodable, Hashable {
case CMD(cmdErr: CommandErrorType)
case CONN(connErr: ConnectionErrorType)
case SMP(serverAddress: String, smpErr: ProtocolErrorType)
case SMP(smpErr: ProtocolErrorType)
case NTF(ntfErr: ProtocolErrorType)
case XFTP(xftpErr: XFTPErrorType)
case PROXY(proxyServer: String, relayServer: String, proxyErr: ProxyClientError)
case RCP(rcpErr: RCErrorType)
case BROKER(brokerAddress: String, brokerErr: BrokerErrorType)
case AGENT(agentErr: SMPAgentError)
@@ -1940,23 +1943,13 @@ public enum ProtocolErrorType: Decodable, Hashable {
case BLOCK
case SESSION
case CMD(cmdErr: ProtocolCommandError)
indirect case PROXY(proxyErr: ProxyError)
case AUTH
case CRYPTO
case QUOTA
case NO_MSG
case LARGE_MSG
case EXPIRED
case INTERNAL
}
public enum ProxyError: Decodable, Hashable {
case PROTOCOL(protocolErr: ProtocolErrorType)
case BROKER(brokerErr: BrokerErrorType)
case BASIC_AUTH
case NO_SESSION
}
public enum XFTPErrorType: Decodable, Hashable {
case BLOCK
case SESSION
@@ -1974,12 +1967,6 @@ public enum XFTPErrorType: Decodable, Hashable {
case INTERNAL
}
public enum ProxyClientError: Decodable, Hashable {
case protocolError(protocolErr: ProtocolErrorType)
case unexpectedResponse(responseStr: String)
case responseError(responseErr: ProtocolErrorType)
}
public enum RCErrorType: Decodable, Hashable {
case `internal`(internalErr: String)
case identity
@@ -2009,7 +1996,6 @@ public enum ProtocolCommandError: Decodable, Hashable {
public enum ProtocolTransportError: Decodable, Hashable {
case badBlock
case version
case largeMsg
case badSession
case noServerAuth
@@ -2099,7 +2085,6 @@ 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
@@ -2129,7 +2114,6 @@ 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 }
@@ -2160,7 +2144,6 @@ public struct AppSettings: Codable, Equatable, Hashable {
privacyShowChatPreviews: true,
privacySaveLastDraft: true,
privacyProtectScreen: false,
privacyMediaBlurRadius: 0,
notificationMode: AppSettingsNotificationMode.instant,
notificationPreviewMode: NotificationPreviewMode.message,
webrtcPolicyRelay: true,
@@ -2336,8 +2319,6 @@ public struct ServerSessions: Codable {
ssErrors: 0,
ssConnecting: 0
)
public var hasSess: Bool { ssConnected > 0 }
}
public struct SMPServerSubs: Codable {
@@ -2391,10 +2372,6 @@ 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 {
@@ -2434,12 +2411,3 @@ 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
}
-12
View File
@@ -13,7 +13,6 @@ 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"
@@ -137,11 +136,6 @@ public enum NSEState: String, Codable {
}
}
public enum SEState: String, Codable {
case inactive
case sendingMessage
}
public enum DBContainer: String {
case documents
case group
@@ -161,12 +155,6 @@ public let nseStateGroupDefault = EnumDefault<NSEState>(
withDefault: .suspended // so that NSE that was never launched does not delay the app from resuming
)
public let seStateGroupDefault = EnumDefault<SEState>(
defaults: groupDefaults,
forKey: GROUP_DEFAULT_SE_STATE,
withDefault: .inactive
)
// inactive app states do not include "stopped" state
public func allowBackgroundRefresh() -> Bool {
appStateGroupDefault.get().inactive && nseStateGroupDefault.get().inactive
+3 -26
View File
@@ -1463,7 +1463,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
)
}
public struct ChatData: Decodable, Identifiable, Hashable, ChatLike {
public struct ChatData: Decodable, Identifiable, Hashable {
public var chatInfo: ChatInfo
public var chatItems: [ChatItem]
public var chatStats: ChatStats
@@ -1512,11 +1512,10 @@ 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 {
(
sndReady
ready
&& active
&& !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false)
&& !(activeConn?.connDisabled ?? true)
@@ -1825,7 +1824,7 @@ public enum ConnStatus: String, Decodable, Hashable {
case .joined: return false
case .requested: return true
case .accepted: return true
case .sndReady: return nil
case .sndReady: return false
case .ready: return nil
case .deleted: return nil
}
@@ -3294,28 +3293,6 @@ 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 {
-62
View File
@@ -1,62 +0,0 @@
//
// ChatUtils.swift
// SimpleXChat
//
// Created by Levitating Pineapple on 15/07/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import Foundation
public protocol ChatLike {
var chatInfo: ChatInfo { get}
var chatItems: [ChatItem] { get }
var chatStats: ChatStats { get }
}
public func filterChatsToForwardTo<C: ChatLike>(chats: [C]) -> [C] {
var filteredChats = chats.filter { c in
c.chatInfo.chatType != .local && canForwardToChat(c.chatInfo)
}
if let privateNotes = chats.first(where: { $0.chatInfo.chatType == .local }) {
filteredChats.insert(privateNotes, at: 0)
}
return filteredChats
}
public func foundChat(_ chat: ChatLike, _ searchStr: String) -> Bool {
let cInfo = chat.chatInfo
return switch cInfo {
case let .direct(contact):
viewNameContains(cInfo, searchStr) ||
contact.profile.displayName.localizedLowercase.contains(searchStr) ||
contact.fullName.localizedLowercase.contains(searchStr)
default:
viewNameContains(cInfo, searchStr)
}
func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool {
cInfo.chatViewName.localizedLowercase.contains(s)
}
}
private func canForwardToChat(_ cInfo: ChatInfo) -> Bool {
switch cInfo {
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
case let .group(groupInfo): groupInfo.sendMsgEnabled
case let .local(noteFolder): noteFolder.sendMsgEnabled
case .contactRequest: false
case .contactConnection: false
case .invalidJSON: false
}
}
public func chatIconName(_ cInfo: ChatInfo) -> String {
switch cInfo {
case .direct: "person.crop.circle.fill"
case .group: "person.2.circle.fill"
case .local: "folder.circle.fill"
case .contactRequest: "person.crop.circle.fill"
default: "circle.fill"
}
}

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