diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift index 7237711a2a..f90653534c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -19,6 +19,9 @@ struct ChatItemForwardingView: View { @State private var searchText: String = "" @FocusState private var searchFocused + @State private var alert: SomeAlert? + @State private var hasSimplexLink_: Bool? + private let chatsToForwardTo = filterChatsToForwardTo() var body: some View { NavigationView { @@ -35,47 +38,29 @@ struct ChatItemForwardingView: View { } } } + .alert(item: $alert) { $0.alert } } @ViewBuilder private func forwardListView() -> some View { VStack(alignment: .leading) { - let chatsToForwardTo = filterChatsToForwardTo() if !chatsToForwardTo.isEmpty { - ScrollView { - LazyVStack(alignment: .leading, spacing: 8) { - searchFieldView(text: $searchText, focussed: $searchFocused) - .padding(.leading, 2) - let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase - let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { filterChatSearched($0, s) } - ForEach(chats) { chat in - Divider() - forwardListNavLinkView(chat) - .disabled(chatModel.deletedChats.contains(chat.chatInfo.id)) - } + List { + searchFieldView(text: $searchText, focussed: $searchFocused) + .padding(.leading, 2) + let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase + let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) } + ForEach(chats) { chat in + forwardListChatView(chat) + .disabled(chatModel.deletedChats.contains(chat.chatInfo.id)) } - .padding(.horizontal) - .padding(.vertical, 8) - .background(Color(uiColor: .systemBackground)) - .cornerRadius(12) - .padding(.horizontal) } - .background(Color(.systemGroupedBackground)) } else { emptyList() } } } - private func filterChatsToForwardTo() -> [Chat] { - var filteredChats = chatModel.chats.filter({ canForwardToChat($0) }) - if let index = filteredChats.firstIndex(where: { $0.chatInfo.chatType == .local }) { - let privateNotes = filteredChats.remove(at: index) - filteredChats.insert(privateNotes, at: 0) - } - return filteredChats - } - - private func filterChatSearched(_ chat: Chat, _ searchStr: String) -> Bool { + private func foundChat(_ chat: Chat, _ searchStr: String) -> Bool { let cInfo = chat.chatInfo return switch cInfo { case let .direct(contact): @@ -91,42 +76,70 @@ struct ChatItemForwardingView: View { } } - 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 + private func prohibitedByPref(_ chat: Chat) -> Bool { + // preference checks should match checks in compose view + let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) + let fileProhibited = (ci.content.msgContent?.isMediaOrFileAttachment ?? false) && !chat.groupFeatureEnabled(.files) + let voiceProhibited = (ci.content.msgContent?.isVoice ?? false) && !chat.chatInfo.featureEnabled(.voice) + return switch chat.chatInfo { + case .direct: voiceProhibited + case .group: simplexLinkProhibited || fileProhibited || voiceProhibited + case .local: false case .contactRequest: false case .contactConnection: false case .invalidJSON: false } } + private var hasSimplexLink: Bool { + if let hasSimplexLink_ { return hasSimplexLink_ } + let r = + if let mcText = ci.content.msgContent?.text, + let parsedMsg = parseSimpleXMarkdown(mcText) { + parsedMsgHasSimplexLink(parsedMsg) + } else { + false + } + hasSimplexLink_ = r + return r + } + private func emptyList() -> some View { Text("No filtered chats") .foregroundColor(.secondary) .frame(maxWidth: .infinity) } - - @ViewBuilder private func forwardListNavLinkView(_ chat: Chat) -> some View { + + @ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View { + let prohibited = prohibitedByPref(chat) Button { - dismiss() - if chat.id == fromChatInfo.id { - composeState = ComposeState( - message: composeState.message, - preview: composeState.linkPreview != nil ? composeState.preview : .noPreview, - contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo) - ) + if prohibited { + alert = SomeAlert( + alert: mkAlert( + title: "Cannot forward message", + message: "Selected chat preferences prohibit this message." + ), + id: "forward prohibited by preferences" + ) } else { - composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo) - chatModel.chatId = chat.id + dismiss() + if chat.id == fromChatInfo.id { + composeState = ComposeState( + message: composeState.message, + preview: composeState.linkPreview != nil ? composeState.preview : .noPreview, + contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo) + ) + } else { + composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo) + chatModel.chatId = chat.id + } } } label: { HStack { ChatInfoImage(chat: chat, size: 30) .padding(.trailing, 2) Text(chat.chatInfo.chatViewName) - .foregroundColor(.primary) + .foregroundColor(prohibited ? .secondary : .primary) .lineLimit(1) if chat.chatInfo.incognito { Spacer() @@ -142,6 +155,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"), diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 3f1824cd6a..8a0ad9a023 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -286,6 +286,7 @@ struct ComposeView: View { if chat.chatInfo.contact?.nextSendGrpInv ?? false { ContextInvitingContactMemberView() } + // preference checks should match checks in forwarding list let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files) let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice) @@ -1065,7 +1066,7 @@ struct ComposeView: View { } else { nil } - let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false }) + let simplexLink = parsedMsgHasSimplexLink(parsedMsg) return (url, simplexLink) } @@ -1105,6 +1106,10 @@ struct ComposeView: View { } } +func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool { + parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false }) +} + struct ComposeView_Previews: PreviewProvider { static var previews: some View { let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 442f933ace..4b1f72345a 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -11,14 +11,9 @@ import SimpleXChat import CodeScanner import AVFoundation -enum SomeAlert: Identifiable { - case someAlert(alert: Alert, id: String) - - var id: String { - switch self { - case let .someAlert(_, id): return id - } - } +struct SomeAlert: Identifiable { + var alert: Alert + var id: String } private enum NewChatViewAlert: Identifiable { @@ -142,8 +137,8 @@ struct NewChatView: View { switch(a) { case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" }) - case let .newChatSomeAlert(.someAlert(alert, _)): - return alert + case let .newChatSomeAlert(a): + return a.alert } } } @@ -181,7 +176,7 @@ struct NewChatView: View { await MainActor.run { creatingConnReq = false if let apiAlert = apiAlert { - alert = .newChatSomeAlert(alert: .someAlert(alert: apiAlert, id: "createInvitation error")) + alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error")) } } } @@ -315,7 +310,7 @@ private struct ConnectView: View { // showQRCodeScanner = false connect(pastedLink) } else { - alert = .newChatSomeAlert(alert: .someAlert( + alert = .newChatSomeAlert(alert: SomeAlert( alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."), id: "pasteLinkView: code is not a SimpleX link" )) @@ -338,14 +333,14 @@ private struct ConnectView: View { if strIsSimplexLink(r.string) { connect(link) } else { - alert = .newChatSomeAlert(alert: .someAlert( + alert = .newChatSomeAlert(alert: SomeAlert( alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."), id: "processQRCode: code is not a SimpleX link" )) } case let .failure(e): logger.error("processQRCode QR code error: \(e.localizedDescription)") - alert = .newChatSomeAlert(alert: .someAlert( + alert = .newChatSomeAlert(alert: SomeAlert( alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"), id: "processQRCode: failure" )) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index a1e2eeb66b..8907938f52 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -24,11 +24,6 @@ 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; }; - 5C0EA13B2C0B176B00AD2E5E /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1362C0B176B00AD2E5E /* libgmp.a */; }; - 5C0EA13C2C0B176B00AD2E5E /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1372C0B176B00AD2E5E /* libgmpxx.a */; }; - 5C0EA13D2C0B176B00AD2E5E /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1382C0B176B00AD2E5E /* libffi.a */; }; - 5C0EA13E2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1392C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a */; }; - 5C0EA13F2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA13A2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a */; }; 5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */; }; 5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */; }; 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; @@ -197,6 +192,11 @@ 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 */; }; + E5D68D3F2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */; }; + E5D68D402C22D78C00CBA347 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3B2C22D78C00CBA347 /* libffi.a */; }; + E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */; }; + E5D68D422C22D78C00CBA347 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3D2C22D78C00CBA347 /* libgmp.a */; }; + E5D68D432C22D78C00CBA347 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -273,11 +273,6 @@ 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = ""; }; - 5C0EA1362C0B176B00AD2E5E /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C0EA1372C0B176B00AD2E5E /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5C0EA1382C0B176B00AD2E5E /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C0EA1392C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a"; sourceTree = ""; }; - 5C0EA13A2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a"; sourceTree = ""; }; 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = ""; }; 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaView.swift; sourceTree = ""; }; 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = ""; }; @@ -492,6 +487,11 @@ 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; }; + E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; sourceTree = ""; }; + E5D68D3B2C22D78C00CBA347 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a"; sourceTree = ""; }; + E5D68D3D2C22D78C00CBA347 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -529,13 +529,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C0EA13C2C0B176B00AD2E5E /* libgmpxx.a in Frameworks */, + E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C0EA13B2C0B176B00AD2E5E /* libgmp.a in Frameworks */, - 5C0EA13D2C0B176B00AD2E5E /* libffi.a in Frameworks */, - 5C0EA13F2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5C0EA13E2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a in Frameworks */, + E5D68D3F2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */, + E5D68D422C22D78C00CBA347 /* libgmp.a in Frameworks */, + E5D68D402C22D78C00CBA347 /* libffi.a in Frameworks */, + E5D68D432C22D78C00CBA347 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -601,11 +601,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C0EA1382C0B176B00AD2E5E /* libffi.a */, - 5C0EA1362C0B176B00AD2E5E /* libgmp.a */, - 5C0EA1372C0B176B00AD2E5E /* libgmpxx.a */, - 5C0EA1392C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a */, - 5C0EA13A2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a */, + E5D68D3B2C22D78C00CBA347 /* libffi.a */, + E5D68D3D2C22D78C00CBA347 /* libgmp.a */, + E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */, + E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */, + E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */, ); path = Libraries; sourceTree = ""; @@ -1552,7 +1552,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 224; + CURRENT_PROJECT_VERSION = 225; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1577,7 +1577,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 5.8; + MARKETING_VERSION = 5.8.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1601,7 +1601,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 224; + CURRENT_PROJECT_VERSION = 225; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1626,7 +1626,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.8; + MARKETING_VERSION = 5.8.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1687,7 +1687,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 224; + CURRENT_PROJECT_VERSION = 225; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1702,7 +1702,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.8; + MARKETING_VERSION = 5.8.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1724,7 +1724,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 224; + CURRENT_PROJECT_VERSION = 225; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1739,7 +1739,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.8; + MARKETING_VERSION = 5.8.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1761,7 +1761,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 224; + CURRENT_PROJECT_VERSION = 225; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1787,7 +1787,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.8; + MARKETING_VERSION = 5.8.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1812,7 +1812,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 224; + CURRENT_PROJECT_VERSION = 225; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1838,7 +1838,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.8; + MARKETING_VERSION = 5.8.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 4c62fe4c8b..1f58ee2363 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -3438,6 +3438,15 @@ public enum MsgContent: Equatable { } } + public var isMediaOrFileAttachment: Bool { + switch self { + case .image: true + case .video: true + case .file: true + default: false + } + } + var cmdString: String { "json \(encodeJSON(self))" } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 89af46edb5..0ebe77e524 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2925,6 +2925,20 @@ sealed class MsgContent { @Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() + val isVoice: Boolean get() = + when (this) { + is MCVoice -> true + else -> false + } + + val isMediaOrFileAttachment: Boolean get() = + when (this) { + is MCImage -> true + is MCVideo -> true + is MCFile -> true + else -> false + } + val cmdString: String get() = if (this is MCUnknown) "json $json" else "json ${json.encodeToString(this)}" } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt index 1de1e40afb..91dffeb7c8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt @@ -9,30 +9,55 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import chat.simplex.common.views.helpers.ProfileImage import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable -fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) { +fun ShareListNavLinkView( + chat: Chat, + chatModel: ChatModel, + isMediaOrFileAttachment: Boolean, + isVoice: Boolean, + hasSimplexLink: Boolean +) { val stopped = chatModel.chatRunning.value == false when (chat.chatInfo) { - is ChatInfo.Direct -> + is ChatInfo.Direct -> { + val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice) ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat) }, - click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) }, + chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited) }, + click = { + if (voiceProhibited) { + showForwardProhibitedByPrefAlert() + } else { + directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) + } + }, stopped ) - is ChatInfo.Group -> + } + is ChatInfo.Group -> { + val simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks) + val fileProhibited = isMediaOrFileAttachment && !chat.groupFeatureEnabled(GroupFeature.Files) + val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice) + val prohibitedByPref = simplexLinkProhibited || fileProhibited || voiceProhibited ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat) }, - click = { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) }, + chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref) }, + click = { + if (prohibitedByPref) { + showForwardProhibitedByPrefAlert() + } else { + groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) + } + }, stopped ) + } is ChatInfo.Local -> ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat) }, + chatLinkPreview = { SharePreviewView(chat, disabled = false) }, click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) }, stopped ) @@ -40,6 +65,13 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) { } } +private fun showForwardProhibitedByPrefAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cannot_share_message_alert_title), + text = generalGetString(MR.strings.cannot_share_message_alert_text), + ) +} + @Composable private fun ShareListNavLinkLayout( chatLinkPreview: @Composable () -> Unit, @@ -53,7 +85,7 @@ private fun ShareListNavLinkLayout( } @Composable -private fun SharePreviewView(chat: Chat) { +private fun SharePreviewView(chat: Chat, disabled: Boolean) { Row( Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween, @@ -70,7 +102,7 @@ private fun SharePreviewView(chat: Chat) { } Text( chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = if (chat.chatInfo.incognito) Indigo else Color.Unspecified + color = if (disabled) MaterialTheme.colors.secondary else if (chat.chatInfo.incognito) Indigo else Color.Unspecified ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index a36930f5ce..69382d9fde 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -31,13 +31,44 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe scaffoldState = scaffoldState, topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } }, ) { + val sharedContent = chatModel.sharedContent.value + var isMediaOrFileAttachment = false + var isVoice = false + var hasSimplexLink = false + when (sharedContent) { + is SharedContent.Text -> + hasSimplexLink = hasSimplexLink(sharedContent.text) + is SharedContent.Media -> { + isMediaOrFileAttachment = true + hasSimplexLink = hasSimplexLink(sharedContent.text) + } + is SharedContent.File -> { + isMediaOrFileAttachment = true + hasSimplexLink = hasSimplexLink(sharedContent.text) + } + is SharedContent.Forward -> { + val mc = sharedContent.chatItem.content.msgContent + if (mc != null) { + isMediaOrFileAttachment = mc.isMediaOrFileAttachment + isVoice = mc.isVoice + hasSimplexLink = hasSimplexLink(mc.text) + } + } + null -> {} + } Box(Modifier.padding(it)) { Column( modifier = Modifier .fillMaxSize() ) { if (chatModel.chats.isNotEmpty()) { - ShareList(chatModel, search = searchInList) + ShareList( + chatModel, + search = searchInList, + isMediaOrFileAttachment = isMediaOrFileAttachment, + isVoice = isVoice, + hasSimplexLink = hasSimplexLink + ) } else { EmptyList() } @@ -54,6 +85,11 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe } } +private fun hasSimplexLink(msg: String): Boolean { + val parsedMsg = parseToMarkdown(msg) ?: return false + return parsedMsg.any { ft -> ft.format is Format.SimplexLink } +} + @Composable private fun EmptyList() { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -141,7 +177,13 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState } @Composable -private fun ShareList(chatModel: ChatModel, search: String) { +private fun ShareList( + chatModel: ChatModel, + search: String, + isMediaOrFileAttachment: Boolean, + isVoice: Boolean, + hasSimplexLink: Boolean +) { val chats by remember(search) { derivedStateOf { val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local } @@ -156,7 +198,13 @@ private fun ShareList(chatModel: ChatModel, search: String) { modifier = Modifier.fillMaxWidth() ) { items(chats) { chat -> - ShareListNavLinkView(chat, chatModel) + ShareListNavLinkView( + chat, + chatModel, + isMediaOrFileAttachment = isMediaOrFileAttachment, + isVoice = isVoice, + hasSimplexLink = hasSimplexLink + ) } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index b896c4e980..996ecb11da 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -358,6 +358,8 @@ Share media… Share file… Forward message… + Cannot send message + Selected chat preferences prohibit this message. Attach diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 66178aa557..ecf038c8a5 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -26,11 +26,11 @@ android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=5.8 -android.version_code=219 +android.version_name=5.8.1 +android.version_code=221 -desktop.version_name=5.8 -desktop.version_code=53 +desktop.version_name=5.8.1 +desktop.version_code=54 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 diff --git a/cabal.project b/cabal.project index a3d8c16890..5a607b257d 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: bb1d31e459337f5d2de05f4495ff50d0a8788dff + tag: 8a3b72458f917e9867f4e3640dda0fa1827ff6cf source-repository-package type: git diff --git a/docs/protocol/simplex-chat.md b/docs/protocol/simplex-chat.md index e3d8e88ae0..efa4edb959 100644 --- a/docs/protocol/simplex-chat.md +++ b/docs/protocol/simplex-chat.md @@ -17,18 +17,23 @@ SimpleX Chat Protocol is a protocol used by SimpleX Chat clients to exchange mes The scope of SimpleX Chat Protocol is application level messages, both for chat functionality, related to the conversations between the clients, and extensible for any other application functions. Currently supported chat functions: - direct and group messages, -- message replies (quoting), forwarded messages and message deletions, -- message attachments: images and files, +- message replies (quoting), message editing, forwarded messages and message deletions, +- message attachments: images, videos, voice messages and files, - creating and managing chat groups, - invitation and signalling for audio/video WebRTC calls. ## General message format -SimpleX Chat protocol supports two message formats: +SimpleX Chat protocol supports these message formats: - JSON-based format for chat and application messages. +- compressed format for adapting larger messages to reduced size of message envelope, caused by addition of PQ encryption keys to SMP agent message envelope. - binary format for sending files or any other binary data. +JSON-based message format supports batching inside a single container message, by encoding list of messages as JSON array. + +Current implementation of chat protocol in SimpleX Chat uses SimpleX File Transfer Protocol (XFTP) for file transfer, with passing file description as chat protocol messages, instead passing files in binary format via SMP connections. + ### JSON format for chat and application messages This document uses JTD schemas [RFC 8927](https://www.rfc-editor.org/rfc/rfc8927.html) to define the properties of chat messages, with some additional restrictions on message properties included in metadata member of JTD schemas. In case of any contradiction between JSON examples and JTD schema the latter MUST be considered correct. @@ -77,8 +82,22 @@ For example, this message defines a simple text message `"hello!"`: `params` property includes message data, depending on `event`, as defined below and in [JTD schema](./simplex-chat.schema.json). +### Compressed format + +The syntax of compressed message is defined by the following ABNF notation: + +```abnf +compressedMessage = %s"X" 1*15780 OCTET; compressed message data +``` + +Compressed message is required to fit into 13388 bytes, accounting for agent overhead (see Protocol's maxCompressedMsgLength). + +The actual JSON message is required to fit into 15610 bytes, accounting for group message forwarding (x.grp.msg.forward) overhead (see Protocol's maxEncodedMsgLength). + ### Binary format for sending files +> Note: Planned to be deprecated. No longer used for file transfer in SimpleX Chat implementation of chat protocol. + SimpleX Chat clients use separate connections to send files using a binary format. File chunk size send in each message MUST NOT be bigger than 15,780 bytes to fit into 16kb (16384 bytes) transport block. The syntax of each message used to send files is defined by the following ABNF notation: @@ -117,7 +136,9 @@ SimpleX Chat Protocol supports the following message types passed in `event` pro - `x.contact` - contact profile and additional data sent as part of contact request to a long-term contact address. - `x.info*` - messages to send, update and de-duplicate contact profiles. - `x.msg.*` - messages to create, update and delete content chat items. +- `x.msg.file.descr` - message to transfer XFTP file description. - `x.file.*` - messages to accept and cancel sending files (see files sub-protocol). +- `x.direct.del` - message to notify about contact deletion. - `x.grp.*` - messages used to manage groups and group members (see group sub-protocol). - `x.call.*` - messages to invite to WebRTC calls and send signalling messages. - `x.ok` - message sent during connection handshake. @@ -155,6 +176,8 @@ Message content can be one of four types: - `text` - no file attachment is expected for this format, `text` property MUST be non-empty. - `file` - attached file is required, `text` property MAY be empty. - `image` - attached file is required, `text` property MAY be empty. +- `video` - attached file is required, `text` property MAY be empty. +- `voice` - attached file is required, `text` property MAY be empty. - `link` - no file attachment is expected, `text` property MUST be non-empty. `preview` property contains information about link preview. See `/definition/msgContent` in [JTD schema](./simplex-chat.schema.json) for message container format. @@ -181,6 +204,8 @@ File attachment can optionally include connection address to receive the file - `x.file.cancel` message is sent to notify the recipient that sending of the file was cancelled. It is sent in response to accepting the file with `x.file.acpt.inv` message. It is sent in the same connection where the file was offered. +`x.msg.file.descr` message is used to send XFTP file description. File descriptions that don't fit into a single chat protocol message are sent in parts, with messages including part number (`fileDescrPartNo`) and description completion marker (`fileDescrComplete`). Recipient client accumulates description parts and starts file download upon completing file description. + ## Sub-protocol for chat groups ### Decentralized design for chat groups @@ -197,6 +222,8 @@ The diagram below shows the sequence of messages sent between the users' clients ![Adding member to the group](./diagrams/group.svg) +While introduced members establish connection inside group, inviting member forwards messages between them by sending `x.grp.msg.forward` messages. When introduced members finalize connection, they notify inviting member to stop forwarding via `x.grp.mem.con` message. + ### Member roles Currently members can have one of three roles - `owner`, `admin` and `member`. The user that created the group is self-assigned owner role, the new members are assigned role by the member who adds them - only `owner` and `admin` members can add new members; only `owner` members can add members with `owner` role. @@ -207,6 +234,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T `x.grp.acpt` message is sent as part of group member connection handshake, only to the inviting user. +`x.grp.link.inv` message is sent as part of connection handshake to member joining via group link, and contains group profile and initial information about inviting and joining member. + +`x.grp.link.mem` message is sent as part of connection handshake to member joining via group link, and contains remaining information about inviting member. + `x.grp.mem.new` message is sent by the inviting user to all connected members (and scheduled as pending to all announced but not yet connected members) to announce a new member to the existing members. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with `member` role. `x.grp.mem.intro` messages are sent by the inviting user to the invited member, via their group member connection, one message for each existing member. When this message is sent by any other member than the one who invited the recipient it MUST be ignored. @@ -219,6 +250,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T `x.grp.mem.role` message is sent to update group member role - it is sent to all members by the member who updated the role of the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with role less than `admin`. +`x.grp.mem.restrict` message is sent to group members to communicate group member restrictions, such as member being blocked for sending messages. + +`x.grp.mem.con` message is sent by members connecting inside group to inviting member, to notify the inviting member they have completed the connection and no longer require forwarding messages between them. + `x.grp.mem.del` message is sent to delete a member - it is sent to all members by the member who deletes the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with `member` role. `x.grp.leave` message is sent to all members by the member leaving the group. If the only group `owner` leaves the group, it will not be possible to delete it with `x.grp.del` message - but all members can still leave the group with `x.grp.leave` message and then delete a local copy of the group. @@ -227,6 +262,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T `x.grp.info` message is sent to all members by the member who updated group profile. Only group owners can update group profiles. Clients MAY implement some conflict resolution strategy - it is currently not implemented by SimpleX Chat client. This message MUST only be sent by members with `owner` role. Receiving clients MUST ignore this message if it is received from member other than with `owner` role. +`x.grp.direct.inv` message is sent to a group member to propose establishing a direct connection between members, thus creating a contact with another member. + +`x.grp.msg.forward` message is sent by inviting member to forward messages between introduced members, while they are connecting. + ## Sub-protocol for WebRTC audio/video calls This sub-protocol is used to send call invitations and to negotiate end-to-end encryption keys and pass WebRTC signalling information. diff --git a/package.yaml b/package.yaml index 2e73355d6f..15f9c5bfa1 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.8.0.5 +version: 5.8.1.0 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/android/compress-and-sign-apk.sh b/scripts/android/compress-and-sign-apk.sh index e46b8a54f1..74d59203c4 100755 --- a/scripts/android/compress-and-sign-apk.sh +++ b/scripts/android/compress-and-sign-apk.sh @@ -47,7 +47,7 @@ for ORIG_NAME in "${ORIG_NAMES[@]}"; do #(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc) ALL_TOOLS=("$sdk_dir"/build-tools/*/) - BIN_DIR="${ALL_TOOLS[1]}" + BIN_DIR="${ALL_TOOLS[${#ALL_TOOLS[@]}-1]}" "$BIN_DIR"/zipalign -p -f 4 "$ORIG_NAME" "$ORIG_NAME"-2 diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index b3858de700..fc8833ee31 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."bb1d31e459337f5d2de05f4495ff50d0a8788dff" = "1nbd80anzh2k5hnf330vlcda0zdn2lzlcnjmi3qz2jdkmmzcc3b6"; + "https://github.com/simplex-chat/simplexmq.git"."8a3b72458f917e9867f4e3640dda0fa1827ff6cf" = "1mmxdaj563kjmlkacxdnq62n6mzw9khampzaqghnk6iiwzdig0qy"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 3e942a1bcb..930ec3c1ff 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 5.8.0.5 +version: 5.8.1.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 045d1b9deb..205da79ca6 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -104,7 +104,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations -import Simplex.Messaging.Client (ProxyClientError (..), NetworkConfig (..), defaultNetworkConfig) +import Simplex.Messaging.Client (NetworkConfig (..), ProxyClientError (..), defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -218,11 +218,12 @@ newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Boo newChatController ChatDatabase {chatStore, agentStore} user - cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote} - ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} + cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote, confirmMigrations} + ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} - config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable} + confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations + config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} firstTime = dbNew chatStore currentUser <- newTVarIO user currentRemoteHost <- newTVarIO Nothing @@ -762,28 +763,31 @@ processChatCommand' vr = \case _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTGroup -> withGroupLock "updateChatItem" chatId $ do - Group gInfo@GroupInfo {groupId} ms <- withStore $ \db -> getGroup db vr user chatId + Group gInfo@GroupInfo {groupId, membership} ms <- withStore $ \db -> getGroup db vr user chatId assertUserGroupRole gInfo GRAuthor - cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId - case cci of - CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do - case (ciContent, itemSharedMsgId, editable) of - (CISndMsgContent oldMC, Just itemSharedMId, True) -> do - let changed = mc /= oldMC - if changed || fromMaybe False itemLive - then do - (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) - ci' <- withStore' $ \db -> do - currentTs <- liftIO getCurrentTime - when changed $ - addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) - let edited = itemLive /= Just True - updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId - startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' - pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') - else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) - _ -> throwChatError CEInvalidChatItemUpdate - CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate + if prohibitedSimplexLinks gInfo membership mc + then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks)) + else do + cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId + case cci of + CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do + case (ciContent, itemSharedMsgId, editable) of + (CISndMsgContent oldMC, Just itemSharedMId, True) -> do + let changed = mc /= oldMC + if changed || fromMaybe False itemLive + then do + (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) + ci' <- withStore' $ \db -> do + currentTs <- liftIO getCurrentTime + when changed $ + addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) + let edited = itemLive /= Just True + updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId + startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' + pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') + else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + _ -> throwChatError CEInvalidChatItemUpdate + CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTLocal -> do (nf@NoteFolder {noteFolderId}, cci) <- withStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId case cci of @@ -1357,6 +1361,9 @@ processChatCommand' vr = \case pure $ CRNetworkConfig cfg APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_ ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_ + ReconnectServer userId srv -> withUserId userId $ \user -> do + lift (withAgent' $ \a -> reconnectSMPServer a (aUserId user) srv) + ok_ APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of CTDirect -> do ct <- withStore $ \db -> do @@ -2918,9 +2925,17 @@ prohibitedGroupContent :: GroupInfo -> GroupMember -> MsgContent -> Maybe f -> M prohibitedGroupContent gInfo m mc file_ | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice | not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles - | not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) && containsFormat isSimplexLink (parseMarkdown $ msgContentText mc) = Just GFSimplexLinks + | prohibitedSimplexLinks gInfo m mc = Just GFSimplexLinks | otherwise = Nothing +prohibitedSimplexLinks :: GroupInfo -> GroupMember -> MsgContent -> Bool +prohibitedSimplexLinks gInfo m mc = + not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) + && maybe False (any ftIsSimplexLink) (parseMaybeMarkdownList $ msgContentText mc) + where + ftIsSimplexLink :: FormattedText -> Bool + ftIsSimplexLink FormattedText {format} = maybe False isSimplexLink format + roundedFDCount :: Int -> Int roundedFDCount n | n <= 0 = 4 @@ -3214,7 +3229,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci throwChatError $ CEFileNotApproved fileId unknownSrvs -getNetworkConfig :: CM' NetworkConfig +getNetworkConfig :: CM' NetworkConfig getNetworkConfig = withAgent' $ liftIO . getNetworkConfig' resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem) @@ -5223,18 +5238,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMsgToView gInfo ci' {reactions} groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> CM () - groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} brokerTs ttl_ live_ = - updateRcvChatItem `catchCINotFound` \_ -> do - -- This patches initial sharedMsgId into chat item when locally deleted chat item - -- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete). - -- Chat item and update message which created it will have different sharedMsgId in this case... - let timed_ = rcvGroupCITimed gInfo ttl_ - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs content Nothing timed_ live - ci' <- withStore' $ \db -> do - createChatItemVersion db (chatItemId' ci) brokerTs mc - ci' <- updateGroupChatItem db user groupId ci content True live Nothing - blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci' - toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') + groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} brokerTs ttl_ live_ + | prohibitedSimplexLinks gInfo m mc = + messageWarning $ "x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks + | otherwise = do + updateRcvChatItem `catchCINotFound` \_ -> do + -- This patches initial sharedMsgId into chat item when locally deleted chat item + -- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete). + -- Chat item and update message which created it will have different sharedMsgId in this case... + let timed_ = rcvGroupCITimed gInfo ttl_ + ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs content Nothing timed_ live + ci' <- withStore' $ \db -> do + createChatItemVersion db (chatItemId' ci) brokerTs mc + ci' <- updateGroupChatItem db user groupId ci content True live Nothing + blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci' + toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') where content = CIRcvMsgContent mc live = fromMaybe False live_ @@ -7383,6 +7401,7 @@ chatCommandP = "/_network " *> (APISetNetworkConfig <$> jsonP), ("/network " <|> "/net ") *> (SetNetworkConfig <$> netCfgP), ("/network" <|> "/net") $> APIGetNetworkConfig, + "/reconnect " *> (ReconnectServer <$> A.decimal <* A.space <*> strP), "/reconnect" $> ReconnectAllServers, "/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP), "/_member settings #" *> (APISetMemberSettings <$> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 0a7ef021e8..b87551978d 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -355,6 +355,7 @@ data ChatCommand | SetNetworkConfig SimpleNetCfg | APISetNetworkInfo UserNetworkInfo | ReconnectAllServers + | ReconnectServer UserId SMPServer | APISetChatSettings ChatRef ChatSettings | APISetMemberSettings GroupId GroupMemberId GroupMemberSettings | APIContactInfo ContactId diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index d3b9ea52f1..d7c6d31fc8 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -144,10 +144,6 @@ markdownToList (m1 :|: m2) = markdownToList m1 <> markdownToList m2 parseMarkdown :: Text -> Markdown parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInput) s -containsFormat :: (Format -> Bool) -> Markdown -> Bool -containsFormat p (Markdown f _) = maybe False p f -containsFormat p (m1 :|: m2) = containsFormat p m1 || containsFormat p m2 - isSimplexLink :: Format -> Bool isSimplexLink = \case SimplexLink {} -> True; diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 486e0d62f3..57b0ee6c17 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -198,7 +198,8 @@ mobileChatOpts dbFilePrefix = logAgent = Nothing, logFile = Nothing, tbqSize = 1024, - highlyAvailable = False + highlyAvailable = False, + yesToUpMigrations = False }, deviceName = Nothing, chatCmd = "", diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index f441afd2b3..14e5603976 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -62,7 +62,8 @@ data CoreChatOpts = CoreChatOpts logAgent :: Maybe LogLevel, logFile :: Maybe FilePath, tbqSize :: Natural, - highlyAvailable :: Bool + highlyAvailable :: Bool, + yesToUpMigrations :: Bool } data ChatCmdLog = CCLAll | CCLMessages | CCLNone @@ -204,6 +205,12 @@ coreChatOptsP appDir defaultDbFileName = do ( long "ha" <> help "Run as a highly available client (this may increase traffic in groups)" ) + yesToUpMigrations <- + switch + ( long "--yes-migrate" + <> short 'y' + <> help "Automatically confirm \"up\" database migrations" + ) pure CoreChatOpts { dbFilePrefix, @@ -217,7 +224,8 @@ coreChatOptsP appDir defaultDbFileName = do logAgent = if logAgent || logLevel == CLLDebug then Just $ agentLogLevel logLevel else Nothing, logFile, tbqSize, - highlyAvailable + highlyAvailable, + yesToUpMigrations } where useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index ceb3988ff9..efca493002 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -101,7 +101,8 @@ testCoreOpts = logAgent = Nothing, logFile = Nothing, tbqSize = 16, - highlyAvailable = False + highlyAvailable = False, + yesToUpMigrations = False } getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 3d92a73313..c1388072ab 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -2032,10 +2032,19 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil threadDelay 1000000 bob ##> "/c" inv <- getInvitation bob - bob ##> ("#team " <> inv) + bob ##> ("#team \"" <> inv <> "\\ntest\"") + bob <## "bad chat command: feature not allowed SimpleX links" + bob ##> ("/_send #1 json {\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}") bob <## "bad chat command: feature not allowed SimpleX links" (alice inv <> "\\ntest\"") + bob <# ("@alice " <> inv) + bob <## "test" + alice <# ("bob> " <> inv) + alice <## "test" + bob ##> "#team <- @alice https://simplex.chat" + bob <## "bad chat command: feature not allowed SimpleX links" alice #> ("#team " <> inv) bob <# ("#team alice> " <> inv) cath <# ("#team alice> " <> inv) diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index d2d15dc166..a279201bea 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -210,3 +210,10 @@ multilineMarkdownList = describe "multiline markdown" do parseMaybeMarkdownList "http://simplex.chat\ntext 1\ntext 2\nhttp://app.simplex.chat" `shouldBe` Just [uri' "http://simplex.chat", "\ntext 1\ntext 2\n", uri' "http://app.simplex.chat"] it "no markdown" do parseMaybeMarkdownList "not a\nmarkdown" `shouldBe` Nothing + let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + it "multiline with simplex link" do + parseMaybeMarkdownList ("https://simplex.chat" <> inv <> "\ntext") + `shouldBe` Just + [ FormattedText (Just $ SimplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"]) ("https://simplex.chat" <> inv), + "\ntext" + ] diff --git a/website/langs/ar.json b/website/langs/ar.json index 3551ffd31f..956b55e72c 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -31,7 +31,7 @@ "simplex-explained-tab-2-p-1": "لكل اتصال، تستخدم قائمتي انتظار منفصلتين للمُراسلة لإرسال واستلام الرسائل عبر خوادم مختلفة.", "simplex-explained-tab-2-p-2": "تقوم الخوادم بتمرير الرسائل في اتجاه واحد فقط، دون الحصول على الصورة الكاملة لمُحادثات المستخدم أو اتصالاته.", "simplex-explained-tab-3-p-1": "تحتوي الخوادم على بيانات اعتماد مجهولة منفصلة لكل قائمة انتظار، ولا تعرف المستخدمين الذين ينتمون إليهم.", - "copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2023", + "copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2024", "simplex-chat-protocol": "بروتوكول دردشة SimpleX", "developers": "المطورين", "hero-subheader": "أول نظام مُراسلة
دون معرّفات مُستخدم", diff --git a/website/langs/bg.json b/website/langs/bg.json index b70a6882e4..963f850e53 100644 --- a/website/langs/bg.json +++ b/website/langs/bg.json @@ -21,7 +21,7 @@ "smp-protocol": "СМП Протокол", "chat-protocol": "Чат протокол", "donate": "Дарете", - "copyright-label": "© 2020-2023 SimpleX | Проект с отворен код", + "copyright-label": "© 2020-2024 SimpleX | Проект с отворен код", "simplex-chat-protocol": "SimpleX Чат протокол", "terminal-cli": "Системна конзола", "terms-and-privacy-policy": "Условия и политика за поверителност", diff --git a/website/langs/cs.json b/website/langs/cs.json index b8777eea38..674351f63a 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -25,7 +25,7 @@ "smp-protocol": "SMP protokol", "chat-protocol": "Chat protokol", "donate": "Darovat", - "copyright-label": "© 2020-2023 SimpleX | Projekt s otevřeným zdrojovým kódem", + "copyright-label": "© 2020-2024 SimpleX | Projekt s otevřeným zdrojovým kódem", "simplex-chat-protocol": "SimpleX Chat protokol", "terminal-cli": "Terminálové rozhraní příkazového řádku", "terms-and-privacy-policy": "Podmínky a zásady ochrany osobních údajů", diff --git a/website/langs/de.json b/website/langs/de.json index 0972423887..7dec07e8e9 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -21,7 +21,7 @@ "smp-protocol": "SMP Protokoll", "chat-bot-example": "Beispiel für einen Chatbot", "donate": "Spenden", - "copyright-label": "© 2020-2023 SimpleX | Open-Source Projekt", + "copyright-label": "© 2020-2024 SimpleX | Open-Source Projekt", "chat-protocol": "Chat Protokoll", "simplex-chat-protocol": "SimpleX Chat Protokoll", "terminal-cli": "Terminal Kommandozeilen-Schnittstelle", diff --git a/website/langs/en.json b/website/langs/en.json index 4e0b04bcbd..89bd17f5d4 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -21,7 +21,7 @@ "smp-protocol": "SMP protocol", "chat-protocol": "Chat protocol", "donate": "Donate", - "copyright-label": "© 2020-2023 SimpleX | Open-Source Project", + "copyright-label": "© 2020-2024 SimpleX | Open-Source Project", "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Privacy Policy", diff --git a/website/langs/es.json b/website/langs/es.json index d8f9c29c26..f91ab5a92a 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -10,7 +10,7 @@ "simplex-explained-tab-3-p-2": "El usuario puede mejorar aún más la privacidad de sus metadatos haciendo uso de la red Tor para acceder a los servidores, evitando así la correlación por dirección IP.", "smp-protocol": "Protocolo SMP", "donate": "Donación", - "copyright-label": "© 2020-2023 SimpleX | Proyecto de Código Abierto", + "copyright-label": "© 2020-2024 SimpleX | Proyecto de Código Abierto", "simplex-chat-protocol": "Protocolo de SimpleX Chat", "terms-and-privacy-policy": "Términos y Política de Privacidad", "hero-header": "Privacidad redefinida", diff --git a/website/langs/fr.json b/website/langs/fr.json index 7ead7882db..7a59ead53f 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -21,7 +21,7 @@ "smp-protocol": "Protocole SMP", "chat-protocol": "Protocole de chat", "donate": "Faire un don", - "copyright-label": "© 2020-2023 SimpleX | Projet Open-Source", + "copyright-label": "© 2020-2024 SimpleX | Projet Open-Source", "simplex-chat-protocol": "Protocole SimpleX Chat", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Politique de confidentialité", diff --git a/website/langs/hu.json b/website/langs/hu.json index 1c860fa023..b4aa44f12e 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -20,7 +20,7 @@ "smp-protocol": "SMP protokoll", "chat-protocol": "Csevegés protokoll", "donate": "Támogatás", - "copyright-label": "© 2020-2023 SimpleX | Nyílt forráskódú projekt", + "copyright-label": "© 2020-2024 SimpleX | Nyílt forráskódú projekt", "simplex-chat-protocol": "SimpleX Chat protokoll", "terminal-cli": "Terminál CLI", "terms-and-privacy-policy": "Adatvédelmi irányelvek", @@ -256,4 +256,4 @@ "simplex-chat-via-f-droid": "SimpleX Chat az F-Droidon keresztül", "simplex-chat-repo": "SimpleX Chat tároló", "stable-and-beta-versions-built-by-developers": "A fejlesztők által készített stabil és béta verziók" -} \ No newline at end of file +} diff --git a/website/langs/it.json b/website/langs/it.json index 64d3b4b8a4..ffbb28903a 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -10,7 +10,7 @@ "simplex-explained-tab-3-p-1": "I server hanno credenziali anonime separate per ogni coda e non sanno a quali utenti appartengano.", "chat-protocol": "Protocollo di chat", "donate": "Dona", - "copyright-label": "© 2020-2023 SimpleX | Progetto Open-Source", + "copyright-label": "© 2020-2024 SimpleX | Progetto Open-Source", "simplex-chat-protocol": "Protocollo di SimpleX Chat", "terminal-cli": "Terminale CLI", "terms-and-privacy-policy": "Informativa sulla privacy", diff --git a/website/langs/ja.json b/website/langs/ja.json index 4b30104615..48aaa41493 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -52,7 +52,7 @@ "chat-protocol": "チャットプロトコル", "chat-bot-example": "チャットボットの例", "donate": "寄付", - "copyright-label": "© 2020-2023 SimpleX | Open-Source Project", + "copyright-label": "© 2020-2024 SimpleX | Open-Source Project", "hero-p-1": "他のアプリにはユーザー ID があります: Signal、Matrix、Session、Briar、Jami、Cwtch など。
SimpleX にはありません。乱数さえもありません
これにより、プライバシーが大幅に向上します。", "copy-the-command-below-text": "以下のコマンドをコピーしてチャットで使用します:", "simplex-private-card-9-point-1": "各メッセージ キューは、異なる送信アドレスと受信アドレスを使用してメッセージを一方向に渡します。", diff --git a/website/langs/nl.json b/website/langs/nl.json index 4f6724e3ff..1d6dc4c2df 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -17,7 +17,7 @@ "chat-bot-example": "Chatbot voorbeeld", "smp-protocol": "SMP protocol", "donate": "Doneer", - "copyright-label": "© 2020-2023 SimpleX | Open-sourceproject", + "copyright-label": "© 2020-2024 SimpleX | Open-sourceproject", "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Privacybeleid", diff --git a/website/langs/pl.json b/website/langs/pl.json index 510915831d..e976be8295 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -15,7 +15,7 @@ "smp-protocol": "Protokół SMP", "chat-protocol": "Protokół czatu", "donate": "Darowizna", - "copyright-label": "© 2020-2023 SimpleX | Projekt Open-Source", + "copyright-label": "© 2020-2024 SimpleX | Projekt Open-Source", "simplex-chat-protocol": "Protokół SimpleX Chat", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Polityka prywatności", diff --git a/website/langs/pt_BR.json b/website/langs/pt_BR.json index 1bb21372e6..784e9e7ca0 100644 --- a/website/langs/pt_BR.json +++ b/website/langs/pt_BR.json @@ -25,7 +25,7 @@ "smp-protocol": "Protocolo SMP", "chat-protocol": "Protocolo de bate-papo", "donate": "Doar", - "copyright-label": "© 2020-2023 SimpleX | Projeto de Código Livre", + "copyright-label": "© 2020-2024 SimpleX | Projeto de Código Livre", "simplex-chat-protocol": "Protocolo Chat SimpleX", "terminal-cli": "CLI Terminal", "hero-header": "Privacidade redefinida", diff --git a/website/langs/ru.json b/website/langs/ru.json index 4a9100c76d..c9a435409d 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -1,6 +1,6 @@ { "copy-the-command-below-text": "скопируйте приведенную ниже команду и используйте ее в чате:", - "copyright-label": "© 2020-2023 SimpleX | Проект с открытым исходным кодом", + "copyright-label": "© 2020-2024 SimpleX | Проект с открытым исходным кодом", "chat-bot-example": "Пример Чат бота", "simplex-private-card-9-point-1": "Каждая очередь сообщений передает сообщения в одном направлении с разными адресами отправки и получения.", "simplex-private-card-1-point-2": "Криптобокс NaCL в каждой очереди для предотвращения корреляции трафика между очередями сообщений, в случае компрометации TLS.", diff --git a/website/src/blog.html b/website/src/blog.html index 11f057ce70..571b240451 100644 --- a/website/src/blog.html +++ b/website/src/blog.html @@ -58,7 +58,7 @@ active_blog: true
-

+

{{ blog.data.title | safe }}

diff --git a/website/src/blogs-atom-feed.njk b/website/src/blogs-atom-feed.njk index 11e1d72a7a..68f396ae40 100644 --- a/website/src/blogs-atom-feed.njk +++ b/website/src/blogs-atom-feed.njk @@ -11,28 +11,31 @@ metadata: email: chat@simplex.chat --- - + + {{ metadata.url }} + + {{ metadata.title }} {{ metadata.subtitle }} - - {{ collections.blogs | getNewestCollectionItemDate | dateToRfc3339 }} - {{ metadata.url }} {{ metadata.author.name }} {{ metadata.author.email }} {%- for blog in collections.blogs | reverse %} {%- if not blog.data.draft %} - {%- set absolutePostUrl = blog.url | absoluteUrl(metadata.url) %} + {%- set absolutePostUrl = blog.data.permalink | absoluteUrl(metadata.url) %} + {{ blog.data.permalink | absoluteUrl(metadata.url) }} + + {{ blog.data.date | dateToRfc3339 }} + {{ blog.data.title }} - - {# {{ blog.date | dateToRfc3339 }} #} - {{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }} - {{ absolutePostUrl }} - {{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }} - {# {{ blog.templateContent | striptags | truncate(200) }} #} + {{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }} + + {{ metadata.author.name }} + {{ metadata.author.email }} + {%- endif %} {%- endfor %} diff --git a/website/src/blogs-rss-feed.njk b/website/src/blogs-rss-feed.njk index 54b63e088c..a2e675ec40 100644 --- a/website/src/blogs-rss-feed.njk +++ b/website/src/blogs-rss-feed.njk @@ -26,8 +26,8 @@ metadata: {{ absolutePostUrl }} {{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }} {# {{ blog.templateContent | striptags | truncate(200) }} #} - {# {{ blog.data.date | dateToRfc822 }} #} - {{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }} + {{ blog.data.date | dateToRfc822 }} + {# {{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }} #} {{ metadata.author.name }} {{ absolutePostUrl }} diff --git a/website/src/css/style.css b/website/src/css/style.css index 71eb56fa67..475eddc404 100644 --- a/website/src/css/style.css +++ b/website/src/css/style.css @@ -46,6 +46,10 @@ img{ -ms-user-select: none; /* For Internet Explorer and Edge */ } +a{ + word-wrap: break-word; +} + /* #comparison::before { display: block; content: " ";