diff --git a/.gitignore b/.gitignore index 2f4af38cca..7bd3d04e59 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ website/src/images/ website/src/js/lottie.min.js website/src/js/ethers* website/src/file-assets/ +website/src/link-images/ website/src/privacy.md # Generated files website/package/generated* diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 547c2b7000..b459f36c9d 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -2122,6 +2122,7 @@ struct AppSettings: Codable, Equatable { var privacyAskToApproveRelays: Bool? = nil var privacyAcceptImages: Bool? = nil var privacyLinkPreviews: Bool? = nil + var privacySanitizeLinks: Bool? = nil var privacyShowChatPreviews: Bool? = nil var privacySaveLastDraft: Bool? = nil var privacyProtectScreen: Bool? = nil @@ -2157,6 +2158,7 @@ struct AppSettings: Codable, Equatable { if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages } if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews } + if privacySanitizeLinks != def.privacySanitizeLinks { empty.privacySanitizeLinks = privacySanitizeLinks } if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews } if privacySaveLastDraft != def.privacySaveLastDraft { empty.privacySaveLastDraft = privacySaveLastDraft } if privacyProtectScreen != def.privacyProtectScreen { empty.privacyProtectScreen = privacyProtectScreen } @@ -2193,6 +2195,7 @@ struct AppSettings: Codable, Equatable { privacyAskToApproveRelays: true, privacyAcceptImages: true, privacyLinkPreviews: true, + privacySanitizeLinks: false, privacyShowChatPreviews: true, privacySaveLastDraft: true, privacyProtectScreen: false, diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 5c57a46129..5242923258 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -742,7 +742,7 @@ struct ComposeView: View { (relay, chatModel.groupMembers.first(where: { $0.wrapped.groupMemberId == relay.groupMemberId })?.wrapped) } let removedCount = relayMembers.filter { (_, m) in relayMemberRemoved(m?.memberStatus) }.count - let activeCount = relayMembers.filter { (relay, m) in !relayMemberRemoved(m?.memberStatus) && relay.relayStatus == .rsActive && m?.activeConn?.connFailedErr == nil }.count + let activeCount = relayMembers.filter { (relay, m) in !relayMemberRemoved(m?.memberStatus) && relay.relayStatus == .active && m?.activeConn?.connFailedErr == nil }.count let failedCount = relayMembers.filter { (_, m) in !relayMemberRemoved(m?.memberStatus) && m?.activeConn?.connFailedErr != nil }.count let noActiveRelays = activeCount == 0 && (failedCount + removedCount) == relays.count return (relays, activeCount, failedCount, removedCount, noActiveRelays) diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift index 6600cec47b..27935768e3 100644 --- a/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift +++ b/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift @@ -37,7 +37,9 @@ struct ChannelRelaysView: View { } // TODO [relays] re-enable when relay management ships // .sheet(isPresented: $showAddRelay) { - // let existingRelayIds = Set(groupRelays.filter { $0.relayStatus != .rsInactive }.compactMap { $0.userChatRelay.chatRelayId }) + // // Backend gate (APIAddGroupRelays) rejects any chatRelayId already in group_relays + // // regardless of relayStatus, so all current rows must be excluded from the add list. + // let existingRelayIds = Set(groupRelays.compactMap { $0.userChatRelay.chatRelayId }) // AddGroupRelayView(groupInfo: groupInfo, existingRelayIds: existingRelayIds) { // Task { await chatModel.loadGroupMembers(groupInfo) } // } @@ -112,7 +114,10 @@ struct ChannelRelaysView: View { } private func ownerRelayStatusText(_ member: GroupMember) -> LocalizedStringKey { - if [.memLeft, .memRemoved, .memGroupDeleted].contains(member.memberStatus) { + let relayStatus = groupRelays.first(where: { $0.groupMemberId == member.groupMemberId })?.relayStatus + return if relayStatus == .rejected { + "rejected" + } else if [.memLeft, .memRemoved, .memGroupDeleted].contains(member.memberStatus) { relayConnStatus(member).text } else if case .failed = member.activeConn?.connStatus { "failed" @@ -121,8 +126,7 @@ struct ChannelRelaysView: View { } else if member.activeConn?.connInactive ?? false { "inactive" } else { - groupRelays.first(where: { $0.groupMemberId == member.groupMemberId })?.relayStatus.text - ?? relayConnStatus(member).text + relayStatus?.text ?? relayConnStatus(member).text } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 883a768d97..dc14c7520b 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -199,6 +199,9 @@ struct GroupMemberInfoView: View { Label("Share relay address", systemImage: "square.and.arrow.up") } } + if groupRelay?.relayStatus == .rejected { + infoRow("Status", "rejected by relay operator") + } } header: { Text(channelMemberSectionHeader).foregroundColor(theme.colors.secondary) } footer: { diff --git a/apps/ios/Shared/Views/NewChat/AddChannelView.swift b/apps/ios/Shared/Views/NewChat/AddChannelView.swift index 32d6e7fe2c..7d1e5ce827 100644 --- a/apps/ios/Shared/Views/NewChat/AddChannelView.swift +++ b/apps/ios/Shared/Views/NewChat/AddChannelView.swift @@ -281,7 +281,7 @@ struct AddChannelView: View { private func progressStepView(_ gInfo: GroupInfo) -> some View { let failedCount = groupRelays.filter { relayMemberConnFailed($0) != nil }.count - let activeCount = groupRelays.filter { $0.relayStatus == .rsActive && relayMemberConnFailed($0) == nil }.count + let activeCount = groupRelays.filter { $0.relayStatus == .active && relayMemberConnFailed($0) == nil }.count let total = groupRelays.count return List { Group { @@ -376,7 +376,7 @@ struct AddChannelView: View { .onChange(of: channelRelaysModel.groupRelays) { relays in guard channelRelaysModel.groupId == gInfo.groupId else { return } groupRelays = relays.sorted { relayDisplayName($0) < relayDisplayName($1) } - if relays.allSatisfy({ $0.relayStatus == .rsActive && relayMemberConnFailed($0) == nil }) { + if relays.allSatisfy({ $0.relayStatus == .active && relayMemberConnFailed($0) == nil }) { showLinkStep = true channelRelaysModel.reset() } @@ -433,7 +433,7 @@ struct AddChannelView: View { } private func showCancelChannelAlert(_ gInfo: GroupInfo) { - let activeCount = groupRelays.filter { $0.relayStatus == .rsActive && relayMemberConnFailed($0) == nil }.count + let activeCount = groupRelays.filter { $0.relayStatus == .active && relayMemberConnFailed($0) == nil }.count let total = groupRelays.count showAlert( NSLocalizedString("Cancel creating channel?", comment: "alert title"), @@ -486,8 +486,14 @@ func chatRelayDisplayName(_ relay: UserChatRelay) -> String { func relayStatusIndicator(_ status: RelayStatus, connFailed: Bool = false, memberStatus: GroupMemberStatus? = nil) -> some View { let removed = memberStatus.map { [.memLeft, .memRemoved, .memGroupDeleted].contains($0) } ?? false - let color: Color = connFailed || removed ? .red : (status == .rsActive ? .green : .yellow) - let text: LocalizedStringKey = connFailed ? "failed" : memberStatus == .memLeft ? "removed by operator" : removed ? "removed" : status.text + let isRejected = status == .rejected + let color: Color = connFailed || removed || isRejected ? .red : (status == .active ? .green : .yellow) + let text: LocalizedStringKey = + connFailed ? "failed" + : isRejected ? "rejected" + : memberStatus == .memLeft ? "removed by operator" + : removed ? "removed" + : status.text return HStack(spacing: 4) { Circle() .fill(color) diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift index 8be0798fb1..3554ce720f 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -38,6 +38,7 @@ extension AppSettings { privacyLinkPreviewsGroupDefault.set(val) def.setValue(val, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) } + if let val = privacySanitizeLinks { privacySanitizeLinksGroupDefault.set(val) } 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) } @@ -77,6 +78,7 @@ extension AppSettings { c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get() c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get() c.privacyLinkPreviews = privacyLinkPreviewsGroupDefault.get() + c.privacySanitizeLinks = privacySanitizeLinksGroupDefault.get() 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) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index e77ad6cb82..d8543735b0 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -237,6 +237,8 @@ public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDef public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS) +public let privacySanitizeLinksGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS) + public let profileImageCornerRadiusGroupDefault = Default(defaults: groupDefaults, forKey: GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index a5a35ba5c0..594f90c4e4 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2635,11 +2635,12 @@ public struct GroupShortLinkData: Codable, Hashable { } public enum RelayStatus: String, Decodable, Equatable, Hashable { - case rsNew = "new" - case rsInvited = "invited" - case rsAccepted = "accepted" - case rsActive = "active" - case rsInactive = "inactive" + case new + case invited + case accepted + case active + case inactive + case rejected } public struct RelayProfile: Codable, Equatable, Hashable { @@ -2708,11 +2709,12 @@ public struct GroupRelay: Identifiable, Decodable, Equatable, Hashable { extension RelayStatus { public var text: LocalizedStringKey { switch self { - case .rsNew: "new" - case .rsInvited: "invited" - case .rsAccepted: "accepted" - case .rsActive: "active" - case .rsInactive: "inactive" + case .new: "new" + case .invited: "invited" + case .accepted: "accepted" + case .active: "active" + case .inactive: "inactive" + case .rejected: "rejected" } } } diff --git a/apps/ios/product/concepts.md b/apps/ios/product/concepts.md index 3fa722d47a..6d63ee2faf 100644 --- a/apps/ios/product/concepts.md +++ b/apps/ios/product/concepts.md @@ -49,7 +49,7 @@ This document provides a structured mapping between product-level concepts, thei | 28 | Chat Tags | [views/chat-list.md](views/chat-list.md) | [spec/state.md](../spec/state.md) | `ChatList/TagListView.swift`, `ChatListView.swift` | `Types.hs` (`ChatTag`), `Controller.hs` | | 29 | User Address | [views/settings.md](views/settings.md) | [spec/api.md](../spec/api.md) | `UserSettings/UserAddressView.swift`, `Onboarding/AddressCreationCard.swift` | `Controller.hs` (`APICreateMyAddress`) | | 30 | Member Support Chat | [views/group-info.md](views/group-info.md) | [spec/api.md](../spec/api.md) | `Group/MemberSupportView.swift`, `MemberAdmissionView.swift` | `Messages.hs` (`GroupChatScope`), `Controller.hs` | -| 31 | Channels (Relays) | [glossary.md](glossary.md), [views/chat.md](views/chat.md) | [spec/api.md](../spec/api.md), [spec/state.md](../spec/state.md), [spec/client/chat-view.md](../spec/client/chat-view.md), [spec/client/compose.md](../spec/client/compose.md) | `SimpleXChat/ChatTypes.swift` (`RelayStatus`, `RelayStatus.text`, `GroupRelay`, `GroupMemberRole.relay`, `CIDirection.channelRcv`, `GroupInfo.chatIconName`, `userCantSendReason`), `Shared/Views/Chat/ChatView.swift` (channel message rendering), `Shared/Views/Chat/ComposeMessage/ComposeView.swift` (`sendAsGroup`, Broadcast placeholder), `Shared/Views/Chat/Group/GroupChatInfoView.swift` (channel info adaptations), `Shared/Views/Chat/Group/ChannelMembersView.swift`, `Shared/Views/Chat/Group/ChannelRelaysView.swift`, `Shared/Model/AppAPITypes.swift` (`GroupShortLinkInfo`, `UserChatRelay`), `Shared/Model/SimpleXAPI.swift` (`apiNewPublicGroup`), `SimpleX SE/ShareAPI.swift` (channel `sendAsGroup`) | `Controller.hs` (`APINewPublicGroup`) | +| 31 | Channels (Relays) | [glossary.md](glossary.md), [views/chat.md](views/chat.md), [views/group-info.md](views/group-info.md) | [spec/api.md](../spec/api.md), [spec/state.md](../spec/state.md), [spec/client/chat-view.md](../spec/client/chat-view.md), [spec/client/compose.md](../spec/client/compose.md) | `SimpleXChat/ChatTypes.swift` (`RelayStatus` incl. `.rsRejected`, `RelayStatus.text`, `GroupRelay`, `GroupMemberRole.relay`, `GroupMemberStatus.memRejected`, `CIDirection.channelRcv`, `GroupInfo.chatIconName`, `userCantSendReason`), `Shared/Views/Chat/ChatView.swift` (channel message rendering), `Shared/Views/Chat/ComposeMessage/ComposeView.swift` (`sendAsGroup`, Broadcast placeholder), `Shared/Views/Chat/Group/GroupChatInfoView.swift` (channel info adaptations), `Shared/Views/Chat/Group/GroupMemberInfoView.swift` (rejected-status row), `Shared/Views/Chat/Group/ChannelMembersView.swift`, `Shared/Views/Chat/Group/ChannelRelaysView.swift`, `Shared/Views/NewChat/AddChannelView.swift` (`relayStatusIndicator` rejected branch), `Shared/Model/AppAPITypes.swift` (`GroupShortLinkInfo`, `UserChatRelay`), `Shared/Model/SimpleXAPI.swift` (`apiNewPublicGroup`), `SimpleX SE/ShareAPI.swift` (channel `sendAsGroup`) | `Controller.hs` (`APINewPublicGroup`, `APIAllowRelayGroup`, `XGrpRelayReject` CONF handler) | --- diff --git a/apps/ios/product/views/group-info.md b/apps/ios/product/views/group-info.md index ee0c449c68..bfc9acfa71 100644 --- a/apps/ios/product/views/group-info.md +++ b/apps/ios/product/views/group-info.md @@ -188,6 +188,7 @@ New view accessible from channel info, showing relay members (role == `.relay`): | Relay list | Filtered from `chatModel.groupMembers` by `.relay` role | | Relay row | Profile image, relay display name, status text (`RelayStatus` or connection status) | | Relay tap | NavigationLink to `GroupMemberInfoView` with `groupRelay:` parameter | +| Add relay sheet | Owner-only "Add relay" button opens `AddGroupRelayView`; the available-to-add list excludes any `chatRelayId` already present in `groupRelays` (regardless of `relayStatus`), so inactive or rejected relays cannot be re-added without first removing them via the row's swipe action | | Empty state | "No chat relays" | | Footer | "Chat relays forward messages to channel subscribers." | @@ -221,6 +222,7 @@ Owner sees relay status from `apiGetGroupRelays`; non-owner sees connection stat | "Unblock for all?" alert | "Unblock subscriber for all?" | | Relay link info row | Shown when `member.relayLink` exists, displays `hostFromRelayLink(link)` | | Relay address info row | Shown when `groupRelay?.userChatRelay.address` exists, with "Share relay address" button | +| Status row (rejected) | Shown when `groupRelay?.relayStatus == .rsRejected`: "Status: rejected by relay operator". The relay rejected the invitation to rejoin this channel after a prior `/leave`; the owner-side `GroupMember.memberStatus` is also set to `.memLeft` so the relay renders identically to one that explicitly left. Clearable only by the relay operator running `/group allow #`. | | Relay footer | Owner: "Subscribers use relay link to connect to the channel. Relay address was used to set up this relay for the channel." Non-owner: "You connected to the channel via this relay link." | ## Related Specs diff --git a/apps/ios/spec/api.md b/apps/ios/spec/api.md index 45a06c371f..f9a3c35917 100644 --- a/apps/ios/spec/api.md +++ b/apps/ios/spec/api.md @@ -415,6 +415,7 @@ Event processing entry point: [`processReceivedMsg`](../Shared/Model/SimpleXAPI. | `groupUpdated` | `user, toGroup: GroupInfo` | Group profile changed | [L1106](../Shared/Model/AppAPITypes.swift#L1106) | | `groupLinkRelaysUpdated` | `user, groupInfo, groupLink, groupRelays: [GroupRelay]` | Channel relay configuration changed | [L1107](../Shared/Model/AppAPITypes.swift#L1107) | | `groupMemberUpdated` | `user, groupInfo, fromMember, toMember` | Member info updated | [L1081](../Shared/Model/AppAPITypes.swift#L1081) | +| `groupRelayUpdated` | `user, groupInfo, member, groupRelay` | Owner-side: a relay's `relayStatus` and/or the member's status changed. Fires on `XGrpRelayReject` with `relayStatus = .rsRejected` and `member.memberStatus = .memRejected` — final until cleared by the relay operator's `/group allow ` (no event emitted to the owner for that clear). | Controller.hs (`CEvtGroupRelayUpdated`) | ### File Transfer Events diff --git a/apps/ios/spec/client/chat-view.md b/apps/ios/spec/client/chat-view.md index 182e7b7ce9..afe656ed04 100644 --- a/apps/ios/spec/client/chat-view.md +++ b/apps/ios/spec/client/chat-view.md @@ -350,8 +350,13 @@ Groups use separate [`groupLinkButton()`](../../Shared/Views/Chat/Group/GroupCha ### [`channelRelaysButton()`](../../Shared/Views/Chat/Group/GroupChatInfoView.swift#L639) → [`ChannelRelaysView`](../../Shared/Views/Chat/Group/ChannelRelaysView.swift) Navigates to relay list view with role-based branches: -- **Owner**: loads `[GroupRelay]` via [`apiGetGroupRelays`](../../Shared/Model/SimpleXAPI.swift#L1839) (owner-only API, guarded by `assertUserGroupRole GROwner` on backend). Joins with `chatModel.groupMembers` by `groupMemberId` for display names. Shows status indicators (colored circle + `RelayStatus.text`). +- **Owner**: loads `[GroupRelay]` via [`apiGetGroupRelays`](../../Shared/Model/SimpleXAPI.swift#L1839) (owner-only API, guarded by `assertUserGroupRole GROwner` on backend). Joins with `chatModel.groupMembers` by `groupMemberId` for display names. Shows status indicators (colored circle + `RelayStatus.text`). When `relayStatus == .rsRejected` the indicator dot is red and the text reads "rejected", matching the `connFailed`/`removed` rendering. - **Member**: filters `chatModel.groupMembers` by `.memberRole == .relay`. Shows relay member display names only (no status data). +- **Add relay sheet**: `existingRelayIds` excludes every `chatRelayId` present in `groupRelays` regardless of `relayStatus`, so an already-listed relay (including `.rsInactive` and `.rsRejected`) cannot be re-added from the sheet. This mirrors the backend gate at `APIAddGroupRelays` (`existingRelayIds`), which rejects duplicate `chatRelayId`s; operator must remove the relay first via the swipe action. + +### Relay Rejection Surface + +When a relay operator runs `/leave #channel`, the relay sends `x.grp.relay.reject` over the owner-relay direct contact channel. Owner-side handling: the corresponding `GroupRelay.relayStatus` transitions `RSInvited → RSRejected`; the relay's `GroupMember.memberStatus` is set to `.memLeft` so the owner UI renders the rejected relay identically to one that explicitly ran `/leave` (`.memRejected` is reserved for the knocking-admission flow). The transition surfaces through `CEvtGroupRelayUpdated`. In `GroupMemberInfoView`, an additional `Status: rejected by relay operator` info row appears when `groupRelay?.relayStatus == .rsRejected`. The status is final on the owner side — clearable only by the relay operator running `/group allow #`, which has no owner-facing event. ### Leave Button Logic diff --git a/apps/ios/spec/impact.md b/apps/ios/spec/impact.md index eaf646e7f4..74acec789e 100644 --- a/apps/ios/spec/impact.md +++ b/apps/ios/spec/impact.md @@ -61,7 +61,7 @@ | Shared/Views/Chat/Group/ChannelRelaysView.swift | PC31 | Medium | Channel relay status list | | Shared/Views/Chat/Group/AddGroupMembersView.swift | PC14, PC16 | Medium | Member invitation flow | | Shared/Views/Chat/Group/GroupLinkView.swift | PC15 | Low | Group link creation/sharing | -| Shared/Views/Chat/Group/GroupMemberInfoView.swift | PC3, PC14, PC16, PC30 | Medium | Member details and role management | +| Shared/Views/Chat/Group/GroupMemberInfoView.swift | PC3, PC14, PC16, PC30, PC31 | Medium | Member details and role management; rejected-by-operator status row for relay members | | Shared/Views/NewChat/NewChatView.swift | PC12, PC31 | High | New connection creation — onramp for all contacts and channels | | Shared/Views/NewChat/QRCode.swift | PC12 | Low | QR code display/scanning utility | | Shared/Views/Call/ActiveCallView.swift | PC17 | Medium | Call UI rendering | diff --git a/apps/ios/spec/state.md b/apps/ios/spec/state.md index 6dda4ba275..db16aa2936 100644 --- a/apps/ios/spec/state.md +++ b/apps/ios/spec/state.md @@ -390,8 +390,8 @@ A **channel** is a group with `groupInfo.useRelays == true`. These types support | Type | Kind | Description | Line | |------|------|-------------|------| -| `RelayStatus` | `enum` | Relay lifecycle: `.rsNew`, `.rsInvited`, `.rsAccepted`, `.rsActive` | [L2506](../SimpleXChat/ChatTypes.swift#L2506) | -| `RelayStatus.text` | `extension` | Localized display text: New/Invited/Accepted/Active | [L2565](../SimpleXChat/ChatTypes.swift#L2565) | +| `RelayStatus` | `enum` | Relay lifecycle: `.rsNew`, `.rsInvited`, `.rsAccepted`, `.rsActive`, `.rsInactive`, `.rsRejected` | [L2506](../SimpleXChat/ChatTypes.swift#L2506) | +| `RelayStatus.text` | `extension` | Localized display text: New/Invited/Accepted/Active/Inactive/Rejected | [L2565](../SimpleXChat/ChatTypes.swift#L2565) | | `GroupRelay` | `struct` | Relay instance for a group (ID, member ID, relay status). Fetched at runtime via `apiGetGroupRelays` (owner only) | [L2555](../SimpleXChat/ChatTypes.swift#L2555) | | `UserChatRelay` | `struct` | User's chat relay configuration (ID, SMP address, name, domains, preset/tested/enabled/deleted flags) | [L2513](../SimpleXChat/ChatTypes.swift#L2513) | 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 1b1d403521..3c9ece9dce 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 @@ -2286,18 +2286,20 @@ data class GroupShortLinkData ( @Serializable enum class RelayStatus { - @SerialName("new") RsNew, - @SerialName("invited") RsInvited, - @SerialName("accepted") RsAccepted, - @SerialName("active") RsActive, - @SerialName("inactive") RsInactive; + @SerialName("new") New, + @SerialName("invited") Invited, + @SerialName("accepted") Accepted, + @SerialName("active") Active, + @SerialName("inactive") Inactive, + @SerialName("rejected") Rejected; val text: String get() = when (this) { - RsNew -> generalGetString(MR.strings.relay_status_new) - RsInvited -> generalGetString(MR.strings.relay_status_invited) - RsAccepted -> generalGetString(MR.strings.relay_status_accepted) - RsActive -> generalGetString(MR.strings.relay_status_active) - RsInactive -> generalGetString(MR.strings.relay_status_inactive) + New -> generalGetString(MR.strings.relay_status_new) + Invited -> generalGetString(MR.strings.relay_status_invited) + Accepted -> generalGetString(MR.strings.relay_status_accepted) + Active -> generalGetString(MR.strings.relay_status_active) + Inactive -> generalGetString(MR.strings.relay_status_inactive) + Rejected -> generalGetString(MR.strings.relay_status_rejected) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index e23b76b025..a31dc145a3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -8042,6 +8042,7 @@ data class AppSettings( var privacyAskToApproveRelays: Boolean? = null, var privacyAcceptImages: Boolean? = null, var privacyLinkPreviews: Boolean? = null, + var privacySanitizeLinks: Boolean? = null, var privacyChatListOpenLinks: PrivacyChatListOpenLinksMode? = null, var privacyShowChatPreviews: Boolean? = null, var privacySaveLastDraft: Boolean? = null, @@ -8078,6 +8079,7 @@ data class AppSettings( if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages } if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews } + if (privacySanitizeLinks != def.privacySanitizeLinks) { empty.privacySanitizeLinks = privacySanitizeLinks } if (privacyChatListOpenLinks != def.privacyChatListOpenLinks) { empty.privacyChatListOpenLinks = privacyChatListOpenLinks } if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews } if (privacySaveLastDraft != def.privacySaveLastDraft) { empty.privacySaveLastDraft = privacySaveLastDraft } @@ -8125,6 +8127,7 @@ data class AppSettings( privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) } privacyAcceptImages?.let { def.privacyAcceptImages.set(it) } privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) } + privacySanitizeLinks?.let { def.privacySanitizeLinks.set(it) } privacyChatListOpenLinks?.let { def.privacyChatListOpenLinks.set(it) } privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) } privacySaveLastDraft?.let { def.privacySaveLastDraft.set(it) } @@ -8162,6 +8165,7 @@ data class AppSettings( privacyAskToApproveRelays = true, privacyAcceptImages = true, privacyLinkPreviews = true, + privacySanitizeLinks = false, privacyChatListOpenLinks = PrivacyChatListOpenLinksMode.ASK, privacyShowChatPreviews = true, privacySaveLastDraft = true, @@ -8200,6 +8204,7 @@ data class AppSettings( privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(), privacyAcceptImages = def.privacyAcceptImages.get(), privacyLinkPreviews = def.privacyLinkPreviews.get(), + privacySanitizeLinks = def.privacySanitizeLinks.get(), privacyChatListOpenLinks = def.privacyChatListOpenLinks.get(), privacyShowChatPreviews = def.privacyShowChatPreviews.get(), privacySaveLastDraft = def.privacySaveLastDraft.get(), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index d0782f6bb4..d874079238 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -2011,7 +2011,7 @@ private fun ownerRelayState(chat: Chat, chatModel: ChatModel): OwnerRelayState? relay to chatModel.groupMembers.value.firstOrNull { it.groupMemberId == relay.groupMemberId } } val removedCount = relayMembers.count { (_, m) -> relayMemberRemoved(m?.memberStatus) } - val activeCount = relayMembers.count { (relay, m) -> !relayMemberRemoved(m?.memberStatus) && relay.relayStatus == RelayStatus.RsActive && m?.activeConn?.connFailedErr == null } + val activeCount = relayMembers.count { (relay, m) -> !relayMemberRemoved(m?.memberStatus) && relay.relayStatus == RelayStatus.Active && m?.activeConn?.connFailedErr == null } val failedCount = relayMembers.count { (_, m) -> !relayMemberRemoved(m?.memberStatus) && m?.activeConn?.connFailedErr != null } val noActiveRelays = activeCount == 0 && (failedCount + removedCount) == relays.size return OwnerRelayState(relays, activeCount, failedCount, removedCount, noActiveRelays) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt index 891753aed8..cfe9f0472d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt @@ -114,7 +114,9 @@ private fun ChannelRelaysLayout( if (groupInfo.isOwner) { SectionView { SectionItemView(click = { - val existingRelayIds = groupRelays.filter { it.relayStatus != RelayStatus.RsInactive }.mapNotNull { it.userChatRelay.chatRelayId }.toSet() + // Backend gate (APIAddGroupRelays) rejects any chatRelayId already in group_relays + // regardless of relayStatus, so all current rows must be excluded from the add list. + val existingRelayIds = groupRelays.mapNotNull { it.userChatRelay.chatRelayId }.toSet() ModalManager.end.showModalCloseable(true) { close -> AddGroupRelayView( groupInfo = groupInfo, @@ -179,7 +181,10 @@ private fun subscriberRelayStatusText(member: GroupMember): String { } private fun ownerRelayStatusText(member: GroupMember, groupRelays: List): String { - return if (member.memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted)) { + val relayStatus = groupRelays.firstOrNull { it.groupMemberId == member.groupMemberId }?.relayStatus + return if (relayStatus == RelayStatus.Rejected) { + generalGetString(MR.strings.relay_status_rejected) + } else if (member.memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted)) { relayConnStatus(member).first } else if (member.activeConn?.connStatus is ConnStatus.Failed) { generalGetString(MR.strings.relay_conn_status_failed) @@ -188,8 +193,7 @@ private fun ownerRelayStatusText(member: GroupMember, groupRelays: List Unit ) { val failedCount = groupRelays.value.count { relayMemberConnFailed(chatModel, it) != null } - val activeCount = groupRelays.value.count { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null } + val activeCount = groupRelays.value.count { it.relayStatus == RelayStatus.Active && relayMemberConnFailed(chatModel, it) == null } val total = groupRelays.value.size fun showCancelAlert() { - val active = groupRelays.value.count { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null } + val active = groupRelays.value.count { it.relayStatus == RelayStatus.Active && relayMemberConnFailed(chatModel, it) == null } val tot = groupRelays.value.size AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.cancel_creating_channel_question), @@ -394,7 +394,7 @@ private fun ProgressStepView( .collect { relays -> if (ChannelRelaysModel.groupId.value != gInfo.groupId) return@collect groupRelays.value = relays.sortedBy { relayDisplayName(it) } - if (relays.all { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null }) { + if (relays.all { it.relayStatus == RelayStatus.Active && relayMemberConnFailed(chatModel, it) == null }) { onLinkReady() ChannelRelaysModel.reset() } @@ -596,8 +596,14 @@ fun chatRelayDisplayName(relay: UserChatRelay): String { @Composable fun RelayStatusIndicator(status: RelayStatus, connFailed: Boolean = false, memberStatus: GroupMemberStatus? = null) { val removed = memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) - val color = if (connFailed || removed) Color.Red else if (status == RelayStatus.RsActive) Color.Green else WarningYellow - val text = if (connFailed) generalGetString(MR.strings.relay_status_failed) else if (memberStatus == GroupMemberStatus.MemLeft) generalGetString(MR.strings.relay_conn_status_removed_by_operator) else if (removed) generalGetString(MR.strings.relay_conn_status_removed) else status.text + val isRejected = status == RelayStatus.Rejected + val color = if (connFailed || removed || isRejected) Color.Red else if (status == RelayStatus.Active) Color.Green else WarningYellow + val text = + if (connFailed) generalGetString(MR.strings.relay_status_failed) + else if (isRejected) generalGetString(MR.strings.relay_status_rejected) + else if (memberStatus == GroupMemberStatus.MemLeft) generalGetString(MR.strings.relay_conn_status_removed_by_operator) + else if (removed) generalGetString(MR.strings.relay_conn_status_removed) + else status.text Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) 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 7304625945..375edecd44 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -27,6 +27,8 @@ Invalid file path You shared an invalid file path. Report the issue to the app developers. View crashed + App is already running + Another app instance may be running or did not exit properly. Start anyway? connected @@ -2994,6 +2996,9 @@ accepted active inactive + rejected + Status + rejected by relay operator All relays removed diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 2ae4aed8e2..ba8901793f 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -23,6 +23,7 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* +import java.awt.Frame import java.awt.event.WindowEvent import java.awt.event.WindowFocusListener import java.io.File @@ -241,10 +242,10 @@ private fun ApplicationScope.handleCloseRequest(closedByError: MutableState exitApplication() - CloseBehavior.MinimizeToTray -> if (trayIsAvailable) { + CloseBehavior.MinimizeToTray -> if (trayIsAvailable && singleInstanceLock) { simplexWindowState.windowVisible.value = false } else exitApplication() - CloseBehavior.Ask -> if (trayIsAvailable) { + CloseBehavior.Ask -> if (trayIsAvailable && singleInstanceLock) { requestCloseBehavior() } else { // Tray unavailable — Minimize is not a real option; remember Quit and exit. @@ -254,6 +255,17 @@ private fun ApplicationScope.handleCloseRequest(closedByError: MutableState Unit>() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopTray.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopTray.kt index 3f35c10c9c..9f75e481f4 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopTray.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopTray.kt @@ -47,12 +47,6 @@ val trayIsAvailable: Boolean by lazy { } } -fun showWindow() { - simplexWindowState.windowVisible.value = true - simplexWindowState.window?.toFront() - simplexWindowState.window?.requestFocus() -} - @Composable fun ApplicationScope.SimplexTray() { if (!trayIsAvailable) return diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/SingleInstance.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/SingleInstance.kt new file mode 100644 index 0000000000..19cb7aea91 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/SingleInstance.kt @@ -0,0 +1,133 @@ +package chat.simplex.common + +import chat.simplex.common.platform.Log +import chat.simplex.common.platform.TAG +import chat.simplex.common.platform.dataDir +import java.io.IOException +import java.nio.channels.FileChannel +import java.nio.channels.FileLock +import java.nio.channels.OverlappingFileLockException +import java.nio.file.* +import java.nio.file.StandardOpenOption.CREATE +import java.nio.file.StandardOpenOption.READ +import java.nio.file.StandardOpenOption.WRITE +import javax.swing.SwingUtilities +import kotlin.concurrent.thread + +private var lockHandle: FileLock? = null +private var watcher: WatchService? = null + +private val lockPath get() = dataDir.resolve("simplex.started").toPath() +private val showPath get() = dataDir.resolve("simplex.show").toPath() + +var singleInstanceLock = false + private set + +private sealed interface LockResult { + class Acquired(val lock: FileLock) : LockResult + object Taken : LockResult + object Failed : LockResult +} + +fun acquireSingleInstance(): Boolean { + dataDir.mkdirs() + when (val result = tryAcquireLock()) { + is LockResult.Acquired -> { + lockHandle = result.lock + singleInstanceLock = true + deleteShowFile() + startShowFileWatcher() + return true + } + LockResult.Failed -> { + return true + } + LockResult.Taken -> { + // Ensure the signal file exists (createShowFile is a no-op if it does) + // and wait up to 1s for the primary's watcher to consume it. If still + // there after the wait, the primary is hung — let the user decide. + createShowFile() + val deadline = System.currentTimeMillis() + 1000 + while (Files.exists(showPath) && System.currentTimeMillis() < deadline) { + try { Thread.sleep(50) } catch (_: InterruptedException) { break } + } + if (!Files.exists(showPath)) return false + val start = showSingleInstanceAlert() + if (start) deleteShowFile() + return start + } + } +} + +private fun tryAcquireLock(): LockResult { + val channel = try { + FileChannel.open(lockPath, READ, WRITE, CREATE) + } catch (e: IOException) { + Log.w(TAG, "single-instance: cannot open lock file: ${e.message}") + return LockResult.Failed + } + return try { + val lock = channel.tryLock(0L, 1L, false) + if (lock != null) { + LockResult.Acquired(lock) + } else { + channel.close() + LockResult.Taken + } + } catch (_: OverlappingFileLockException) { + Log.w(TAG, "single-instance: overlapping lock in same JVM") + LockResult.Failed + } catch (e: IOException) { + Log.w(TAG, "single-instance: tryLock failed: ${e.message}") + channel.close(); LockResult.Failed + } +} + +private fun deleteShowFile() { + try { Files.deleteIfExists(showPath) } catch (e: IOException) { + Log.w(TAG, "single-instance: cannot delete show file: ${e.message}") + } +} + +private fun createShowFile() { + try { Files.createFile(showPath) } catch (_: FileAlreadyExistsException) { + // Another duplicate already signalled; primary will pick it up. + } catch (e: IOException) { + Log.w(TAG, "single-instance: cannot create show file: ${e.message}") + } +} + +private fun showSingleInstanceAlert(): Boolean { + val title = chat.simplex.common.views.helpers.generalGetString(chat.simplex.res.MR.strings.another_instance_title) + val message = chat.simplex.common.views.helpers.generalGetString(chat.simplex.res.MR.strings.another_instance_not_responding) + val result = javax.swing.JOptionPane.showConfirmDialog( + null, message, title, + javax.swing.JOptionPane.YES_NO_OPTION, + javax.swing.JOptionPane.WARNING_MESSAGE + ) + return result == javax.swing.JOptionPane.YES_OPTION +} + +private fun startShowFileWatcher() { + if (watcher != null) return + val ws = try { + dataDir.toPath().fileSystem.newWatchService() + } catch (e: IOException) { + Log.w(TAG, "single-instance: WatchService failed: ${e.message}") + return + } + dataDir.toPath().register(ws, StandardWatchEventKinds.ENTRY_CREATE) + watcher = ws + thread(name = "simplex-single-instance", isDaemon = true) { + while (true) { + val key = try { ws.take() } catch (_: ClosedWatchServiceException) { return@thread } catch (_: InterruptedException) { return@thread } + for (event in key.pollEvents()) { + if ((event.context() as? Path)?.fileName?.toString() == "simplex.show") { + deleteShowFile() + SwingUtilities.invokeLater { showWindow() } + } + } + if (!key.reset()) return@thread + } + } +} diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SingleInstanceTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SingleInstanceTest.kt new file mode 100644 index 0000000000..1b495c1774 --- /dev/null +++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SingleInstanceTest.kt @@ -0,0 +1,56 @@ +package chat.simplex.app + +import java.nio.channels.FileChannel +import java.nio.channels.OverlappingFileLockException +import java.nio.file.Files +import java.nio.file.StandardOpenOption.CREATE +import java.nio.file.StandardOpenOption.READ +import java.nio.file.StandardOpenOption.WRITE +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull + +class SingleInstanceTest { + @Test + fun overlappingLockOnSameRegionThrowsWithinOneJvm() = withTempDir { dir -> + val lockPath = dir.resolve("simplex.started") + val first = FileChannel.open(lockPath, READ, WRITE, CREATE) + val firstLock = first.tryLock(0L, 1L, false) + assertNotNull(firstLock, "first acquirer must get the lock") + + val second = FileChannel.open(lockPath, READ, WRITE, CREATE) + assertFailsWith { + second.tryLock(0L, 1L, false) + } + second.close() + firstLock.release() + first.close() + } + + @Test + fun releasedLockCanBeReacquired() = withTempDir { dir -> + val lockPath = dir.resolve("simplex.started") + val first = FileChannel.open(lockPath, READ, WRITE, CREATE) + val firstLock = first.tryLock(0L, 1L, false) + assertNotNull(firstLock) + firstLock.release() + first.close() + + val second = FileChannel.open(lockPath, READ, WRITE, CREATE) + val secondLock = second.tryLock(0L, 1L, false) + assertNotNull(secondLock, "after release, a fresh acquirer must succeed") + secondLock.release() + second.close() + } + + private fun withTempDir(block: (java.nio.file.Path) -> Unit) { + val tmp = Files.createTempDirectory("simplex-singleinstance-test") + try { + block(tmp) + } finally { + Files.walk(tmp).sorted(Comparator.reverseOrder()).forEach { + try { Files.delete(it) } catch (_: java.io.IOException) {} + } + } + } +} diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index 0e8a452e08..338660b746 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.* +import chat.simplex.common.acquireSingleInstance import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.size import chat.simplex.common.platform.* @@ -19,6 +20,7 @@ import kotlinx.coroutines.* import java.io.File fun main() { + if (!acquireSingleInstance()) return // Disable hardware acceleration //System.setProperty("skiko.renderApi", "SOFTWARE") initHaskell() diff --git a/apps/multiplatform/product/concepts.md b/apps/multiplatform/product/concepts.md index da33bf11d7..5d707cf832 100644 --- a/apps/multiplatform/product/concepts.md +++ b/apps/multiplatform/product/concepts.md @@ -49,6 +49,7 @@ This document provides a structured mapping between product-level concepts, thei | PC28 | Chat Tags | [README.md](README.md) (Navigation Map) | [spec/state.md](../spec/state.md) | `common/.../views/chatlist/TagListView.kt`, `ChatListView.kt` | `Types.hs` (`ChatTag`), `Controller.hs` | | PC29 | User Address | [README.md](README.md) (Contacts, User Management) | [spec/api.md](../spec/api.md) | `common/.../views/usersettings/UserAddressView.kt`, `UserAddressLearnMore.kt` | `Controller.hs` (`APICreateMyAddress`) | | PC30 | Member Support Chat | [README.md](README.md) (Groups) | [spec/api.md](../spec/api.md) | `common/.../views/chat/group/MemberSupportView.kt`, `MemberSupportChatView.kt`, `MemberAdmission.kt` | `Messages.hs` (`GroupChatScope`), `Controller.hs` | +| PC31 | Channels (Relays) | [views/group-info.md](views/group-info.md) | [spec/client/chat-view.md](../spec/client/chat-view.md), [spec/state.md](../spec/state.md) | `common/.../model/ChatModel.kt` (`RelayStatus` incl. `RsRejected`, `GroupRelay`, `GroupMemberRole.Relay`, `GroupMemberStatus.MemRejected`), `common/.../views/chat/group/ChannelRelaysView.kt`, `GroupMemberInfoView.kt` (rejected-status row), `common/.../views/newchat/AddChannelView.kt` (`RelayStatusIndicator` rejected branch), `common/.../views/chat/group/AddGroupRelayView.kt` | `Controller.hs` (`APIAddGroupRelays`, `APIAllowRelayGroup`, `XGrpRelayReject` CONF handler) | **Legend for abbreviated paths:** - `common/.../` expands to `common/src/commonMain/kotlin/chat/simplex/common/` diff --git a/apps/multiplatform/product/views/group-info.md b/apps/multiplatform/product/views/group-info.md index 65b068adc8..2335de7178 100644 --- a/apps/multiplatform/product/views/group-info.md +++ b/apps/multiplatform/product/views/group-info.md @@ -130,6 +130,30 @@ Shown when `developerTools` preference is enabled: Business chats use alternative labels: "Delete chat" instead of "Delete group". +### Channel Relays View (`ChannelRelaysView`) + +Accessible from channel info; shows relay members (role == `Relay`): + +| Element | Description | +|---|---| +| Relay list | Filtered from `chatModel.groupMembers` by `Relay` role; excludes `MemRemoved` and `MemGroupDeleted` | +| Relay row | Profile image, relay display name, status text (`RelayStatus.text` or connection status via `relayConnStatus`) | +| Relay tap | Navigates to `GroupMemberInfoView` with `groupRelay:` parameter | +| Add relay entry | Owner-only "Add relay" action opens `AddGroupRelayView`; the available-to-add list excludes any `chatRelayId` already present in `groupRelays` (regardless of `relayStatus`), so inactive or rejected relays cannot be re-added without first removing them via the row's long-press menu | +| Long-press menu | Owner-only "Remove relay" action for relays that can be removed | +| Empty state | "No chat relays" | +| Footer | "Chat relays forward messages to channel subscribers." | + +Owner sees relay status from `apiGetGroupRelays`; non-owner sees connection status only. + +#### Channel Member Info — relay surface (in `GroupMemberInfoView`) + +| Element | Description | +|---|---| +| Relay link info row | Shown when `member.relayLink` exists, displays `hostFromRelayLink(link)` | +| Relay address info row | Shown when `groupRelay?.userChatRelay.address` exists, with "Share relay address" button | +| Status row (rejected) | Shown when `groupRelay?.relayStatus == RelayStatus.RsRejected`: "Status: rejected by relay operator". The relay rejected the invitation to rejoin this channel after a prior `/leave`; the owner-side `GroupMember.memberStatus` is also set to `MemLeft` so the relay renders identically to one that explicitly left. Clearable only by the relay operator running `/group allow #`. | + ## Source Files | File | Path | @@ -143,3 +167,6 @@ Business chats use alternative labels: "Delete chat" instead of "Delete group". | `WelcomeMessageView.kt` | `views/chat/group/WelcomeMessageView.kt` | | `MemberAdmission.kt` | `views/chat/group/MemberAdmission.kt` | | `MemberSupportView.kt` | `views/chat/group/MemberSupportView.kt` | +| `ChannelRelaysView.kt` | `views/chat/group/ChannelRelaysView.kt` | +| `AddGroupRelayView.kt` | `views/chat/group/AddGroupRelayView.kt` | +| `AddChannelView.kt` (`RelayStatusIndicator`) | `views/newchat/AddChannelView.kt` | diff --git a/apps/multiplatform/spec/api.md b/apps/multiplatform/spec/api.md index 15d5e141a0..4114e9de4f 100644 --- a/apps/multiplatform/spec/api.md +++ b/apps/multiplatform/spec/api.md @@ -352,6 +352,7 @@ Events handled in `processReceivedMsg` include: | `DeletedMember` / `DeletedMemberUser` | A member was removed | | `LeftMember` | A member left voluntarily | | `GroupUpdated` | Group profile changed | +| `GroupRelayUpdated` | Owner-side: a relay's `relayStatus` and/or the member's status changed. Fires on `XGrpRelayReject` with `relayStatus = RsRejected` and `GroupMember.memberStatus = MemLeft` — final on owner side until cleared by the relay operator's `/group allow #` (no event emitted to the owner for that clear). | | `MemberRole` | A member's role changed | | `MemberBlockedForAll` | A member was blocked for all | | `RcvFileStart` / `RcvFileComplete` / `RcvFileError` | File receive progress | diff --git a/apps/multiplatform/spec/client/chat-view.md b/apps/multiplatform/spec/client/chat-view.md index 2819b1e751..728ace4936 100644 --- a/apps/multiplatform/spec/client/chat-view.md +++ b/apps/multiplatform/spec/client/chat-view.md @@ -322,3 +322,11 @@ Key sections: group profile, group link, member list with roles, group preferenc | `MemberSupportChatView.kt` | Member support chat (scoped context) | | `MemberSupportView.kt` | Support chat list for moderators | | `WelcomeMessageView.kt` | Group welcome message editor | +| `ChannelRelaysView.kt` | Channel relay list. Owner-only Add relay entry opens `AddGroupRelayView` with `existingRelayIds = groupRelays.mapNotNull { it.userChatRelay.chatRelayId }.toSet()` — every relay currently in `groupRelays` is excluded regardless of `relayStatus`, mirroring the backend `APIAddGroupRelays` gate. Long-press menu offers Remove relay for relays that can be removed. | +| `AddGroupRelayView.kt` | Sheet to pick relays to add to a channel | + +### Relay Rejection Surface + +When a relay operator runs `/leave #channel`, the relay sends `x.grp.relay.reject` over the owner-relay direct contact channel. Owner-side handling: the corresponding `GroupRelay.relayStatus` transitions `RSInvited → RSRejected`; the relay's `GroupMember.memberStatus` is set to `MemLeft` so the owner UI renders the rejected relay identically to one that explicitly ran `/leave` (`MemRejected` is reserved for the knocking-admission flow). In `GroupMemberInfoView`, an additional "Status: rejected by relay operator" `InfoRow` appears when `groupRelay?.relayStatus == RelayStatus.RsRejected`. The status is final on the owner side — clearable only by the relay operator running `/group allow #`, which has no owner-facing event. + +The `RelayStatusIndicator` composable in `AddChannelView.kt` renders `RsRejected` with a red dot and "rejected" text, matching the `connFailed`/`removed` rendering. diff --git a/apps/multiplatform/spec/impact.md b/apps/multiplatform/spec/impact.md index cd0f836585..f808cf31ba 100644 --- a/apps/multiplatform/spec/impact.md +++ b/apps/multiplatform/spec/impact.md @@ -40,6 +40,7 @@ | PC28 | Chat Tags | | PC29 | User Address | | PC30 | Member Support Chat | +| PC31 | Channels (Relays) | --- @@ -51,13 +52,13 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `App.kt` | PC1 through PC30 | High | Root composable — navigation scaffold for all features | +| `App.kt` | PC1 through PC31 | High | Root composable — navigation scaffold for all features | | `AppLock.kt` | PC22 | Medium | App lock state and authorization lifecycle | -| `model/ChatModel.kt` | PC1 through PC30 | High | Central state object — every feature reads or writes here | -| `model/SimpleXAPI.kt` | PC1 through PC30 | High | FFI bridge to Haskell core — all commands and responses | +| `model/ChatModel.kt` | PC1 through PC31 | High | Central state object — every feature reads or writes here | +| `model/SimpleXAPI.kt` | PC1 through PC31 | High | FFI bridge to Haskell core — all commands and responses | | `model/CryptoFile.kt` | PC10, PC23 | Medium | Encrypted file read/write helpers | -| `platform/Core.kt` | PC1 through PC30 | High | Native FFI declarations (`chatMigrateInit`, `chatSendCmd`, etc.) — all API traffic | -| `platform/AppCommon.kt` | PC1 through PC30 | Medium | Shared app initialization logic | +| `platform/Core.kt` | PC1 through PC31 | High | Native FFI declarations (`chatMigrateInit`, `chatSendCmd`, etc.) — all API traffic | +| `platform/AppCommon.kt` | PC1 through PC31 | Medium | Shared app initialization logic | | `platform/Files.kt` | PC10, PC23, PC26 | Medium | File path resolution, temp dirs, encryption utilities | | `platform/NtfManager.kt` | PC18 | High | Notification manager expect declarations | | `platform/Notifications.kt` | PC18 | Medium | Notification channel and permission abstractions | @@ -67,7 +68,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | `platform/Cryptor.kt` | PC23 | Medium | Keystore encryption expect declarations | | `platform/Share.kt` | PC10, PC12 | Low | Share sheet abstractions | | `platform/Images.kt` | PC10, PC19 | Low | Image processing utilities | -| `platform/Platform.kt` | PC1 through PC30 | Low | Platform detection and capability flags | +| `platform/Platform.kt` | PC1 through PC31 | Low | Platform detection and capability flags | | `platform/PlatformTextField.kt` | PC4 | Low | Native text input expect declarations | | `platform/Back.kt` | PC1 | Low | Back navigation handling | | `platform/UI.kt` | PC24 | Low | UI density and locale helpers | @@ -160,7 +161,9 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` |-------------|--------------------------|------------|-------| | `views/chat/group/GroupChatInfoView.kt` | PC3, PC14, PC15, PC16, PC30 | High | Group management hub | | `views/chat/group/AddGroupMembersView.kt` | PC14, PC16 | Medium | Member invitation flow | -| `views/chat/group/GroupMemberInfoView.kt` | PC3, PC14, PC16, PC30 | Medium | Member details and role management | +| `views/chat/group/GroupMemberInfoView.kt` | PC3, PC14, PC16, PC30, PC31 | Medium | Member details and role management; relay-address + rejected-status info rows | +| `views/chat/group/ChannelRelaysView.kt` | PC31 | Medium | Channel relay list, add/remove entries | +| `views/chat/group/AddGroupRelayView.kt` | PC31 | Low | Add relay sheet | | `views/chat/group/GroupProfileView.kt` | PC3, PC14 | Medium | Group profile editing | | `views/chat/group/GroupLinkView.kt` | PC15 | Low | Group link creation and sharing | | `views/chat/group/GroupPreferences.kt` | PC3, PC8, PC14 | Medium | Group feature toggles | @@ -189,6 +192,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | `views/newchat/NewChatSheet.kt` | PC12 | Medium | Bottom sheet with connection options | | `views/newchat/ConnectPlan.kt` | PC12, PC15 | Medium | Link parsing and connection plan resolution | | `views/newchat/AddGroupView.kt` | PC3, PC14 | Medium | New group creation flow | +| `views/newchat/AddChannelView.kt` | PC31 | Medium | Public channel creation, channel link card, `RelayStatusIndicator` | | `views/newchat/ContactConnectionInfoView.kt` | PC12 | Low | Pending connection details | | `views/newchat/AddContactLearnMore.kt` | PC12 | Low | Educational content | | `views/newchat/QRCode.kt` | PC12 | Low | QR code display | @@ -264,9 +268,9 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `views/helpers/AlertManager.kt` | PC1 through PC30 | Medium | Modal alert system used across all features | -| `views/helpers/ModalView.kt` | PC1 through PC30 | Medium | Modal navigation stack | -| `views/helpers/Utils.kt` | PC1 through PC30 | Low | Shared formatting, clipboard, and utility functions | +| `views/helpers/AlertManager.kt` | PC1 through PC31 | Medium | Modal alert system used across all features | +| `views/helpers/ModalView.kt` | PC1 through PC31 | Medium | Modal navigation stack | +| `views/helpers/Utils.kt` | PC1 through PC31 | Low | Shared formatting, clipboard, and utility functions | | `views/helpers/DatabaseUtils.kt` | PC23 | Medium | Keystore passphrase and database helpers | | `views/helpers/LinkPreviews.kt` | PC11 | Medium | Link preview fetching and rendering | | `views/helpers/LocalAuthentication.kt` | PC22 | Medium | Biometric/passcode authentication expect | @@ -319,8 +323,8 @@ Path prefix: `android/src/main/java/chat/simplex/app/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `SimplexApp.kt` | PC1 through PC30 | High | Application class — initializes core, preferences, and notification channels | -| `MainActivity.kt` | PC1 through PC30 | High | Single-activity host — intent handling, lifecycle, deep links | +| `SimplexApp.kt` | PC1 through PC31 | High | Application class — initializes core, preferences, and notification channels | +| `MainActivity.kt` | PC1 through PC31 | High | Single-activity host — intent handling, lifecycle, deep links | | `SimplexService.kt` | PC18 | High | Foreground service — keeps message receiver alive | | `CallService.kt` | PC17 | Medium | Foreground service for active calls | | `MessagesFetcherWorker.kt` | PC18 | Medium | WorkManager periodic message fetch | @@ -334,7 +338,7 @@ Path prefix: `common/src/androidMain/kotlin/chat/simplex/common/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `platform/AppCommon.android.kt` | PC1 through PC30 | Medium | Android app initialization actual declarations | +| `platform/AppCommon.android.kt` | PC1 through PC31 | Medium | Android app initialization actual declarations | | `platform/SimplexService.android.kt` | PC18 | Medium | Android foreground service actual implementation | | `platform/Files.android.kt` | PC10, PC23, PC26 | Medium | Android file paths and content-URI resolution | | `platform/Cryptor.android.kt` | PC23 | Medium | Android Keystore encryption actual implementation | @@ -400,7 +404,7 @@ Path prefix: `desktop/src/jvmMain/kotlin/chat/simplex/desktop/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `Main.kt` | PC1 through PC30 | High | JVM entry point — Haskell init, migrations, app launch | +| `Main.kt` | PC1 through PC31 | High | JVM entry point — Haskell init, migrations, app launch | ### 3.2 Desktop Platform Implementations (desktopMain) @@ -411,7 +415,7 @@ Path prefix: `common/src/desktopMain/kotlin/chat/simplex/common/` | `DesktopApp.kt` | PC1, PC2, PC3 | High | Desktop Compose window — window lifecycle, crash recovery | | `StoreWindowState.kt` | — | Low | Window position/size persistence | | `model/NtfManager.desktop.kt` | PC18 | Medium | Desktop system tray notification display | -| `platform/AppCommon.desktop.kt` | PC1 through PC30 | Medium | Desktop app initialization actual declarations | +| `platform/AppCommon.desktop.kt` | PC1 through PC31 | Medium | Desktop app initialization actual declarations | | `platform/SimplexService.desktop.kt` | PC18 | Low | Desktop background receiver (no foreground service) | | `platform/Files.desktop.kt` | PC10, PC23, PC26 | Medium | Desktop file path resolution | | `platform/Cryptor.desktop.kt` | PC23 | Medium | Desktop keystore encryption actual implementation | @@ -473,13 +477,13 @@ The Haskell core is compiled as a shared native library (`libsimplex.so` / `libs | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `src/Simplex/Chat.hs` | PC1 through PC30 | High | Main chat module — top-level orchestration | -| `src/Simplex/Chat/Controller.hs` | PC1 through PC30 | High | Command processor — all API commands dispatched here | -| `src/Simplex/Chat/Types.hs` | PC1 through PC30 | High | Core data types shared across all features | -| `src/Simplex/Chat/Core.hs` | PC1 through PC30 | High | Chat engine lifecycle (start, stop, subscribe) | -| `src/Simplex/Chat/Library/Commands.hs` | PC1 through PC30 | High | API command handler implementations | -| `src/Simplex/Chat/Library/Internal.hs` | PC1 through PC30 | High | Internal helpers for command processing | -| `src/Simplex/Chat/Library/Subscriber.hs` | PC1 through PC30 | High | Event subscriber — incoming message routing | +| `src/Simplex/Chat.hs` | PC1 through PC31 | High | Main chat module — top-level orchestration | +| `src/Simplex/Chat/Controller.hs` | PC1 through PC31 | High | Command processor — all API commands dispatched here | +| `src/Simplex/Chat/Types.hs` | PC1 through PC31 | High | Core data types shared across all features | +| `src/Simplex/Chat/Core.hs` | PC1 through PC31 | High | Chat engine lifecycle (start, stop, subscribe) | +| `src/Simplex/Chat/Library/Commands.hs` | PC1 through PC31 | High | API command handler implementations | +| `src/Simplex/Chat/Library/Internal.hs` | PC1 through PC31 | High | Internal helpers for command processing | +| `src/Simplex/Chat/Library/Subscriber.hs` | PC1 through PC31 | High | Event subscriber — incoming message routing | | `src/Simplex/Chat/Protocol.hs` | PC2, PC3, PC4, PC5, PC6, PC7 | High | Chat-level message protocol (x-events) | | `src/Simplex/Chat/Messages.hs` | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9 | High | Message types and content | | `src/Simplex/Chat/Messages/CIContent.hs` | PC4, PC5, PC6, PC7, PC8, PC9, PC11 | Medium | Chat item content variants | @@ -489,8 +493,8 @@ The Haskell core is compiled as a shared native library (`libsimplex.so` / `libs | `src/Simplex/Chat/Files.hs` | PC10 | Medium | File transfer orchestration | | `src/Simplex/Chat/Delivery.hs` | PC2, PC3 | Medium | Message delivery engine | | `src/Simplex/Chat/Markdown.hs` | PC4 | Low | Markdown parsing for message formatting | -| `src/Simplex/Chat/Store.hs` | PC1 through PC30 | High | Database store interface | -| `src/Simplex/Chat/Store/Shared.hs` | PC1 through PC30 | Medium | Shared store utilities | +| `src/Simplex/Chat/Store.hs` | PC1 through PC31 | High | Database store interface | +| `src/Simplex/Chat/Store/Shared.hs` | PC1 through PC31 | Medium | Shared store utilities | | `src/Simplex/Chat/Store/Messages.hs` | PC4, PC5, PC6, PC7, PC8 | High | Message persistence | | `src/Simplex/Chat/Store/Groups.hs` | PC3, PC14, PC15, PC16, PC30 | High | Group persistence | | `src/Simplex/Chat/Store/Direct.hs` | PC2, PC12, PC13 | High | Contact persistence | @@ -519,11 +523,11 @@ The Haskell core is compiled as a shared native library (`libsimplex.so` / `libs | `src/Simplex/Chat/Operators/Presets.hs` | PC25 | Low | Preset server operators | | `src/Simplex/Chat/Operators/Conditions.hs` | PC25 | Low | Operator usage conditions | | `src/Simplex/Chat/AppSettings.hs` | PC25 | Low | App settings sync types | -| `src/Simplex/Chat/Mobile.hs` | PC1 through PC30 | High | C FFI exports — JNI bridge target | +| `src/Simplex/Chat/Mobile.hs` | PC1 through PC31 | High | C FFI exports — JNI bridge target | | `src/Simplex/Chat/Mobile/File.hs` | PC10 | Medium | Mobile file read/write FFI | -| `src/Simplex/Chat/Mobile/Shared.hs` | PC1 through PC30 | Medium | Shared FFI helpers | +| `src/Simplex/Chat/Mobile/Shared.hs` | PC1 through PC31 | Medium | Shared FFI helpers | | `src/Simplex/Chat/Mobile/WebRTC.hs` | PC17 | Low | WebRTC FFI helpers | -| `src/Simplex/Chat/View.hs` | PC1 through PC30 | Low | Terminal view rendering (not used by mobile/desktop UI) | +| `src/Simplex/Chat/View.hs` | PC1 through PC31 | Low | Terminal view rendering (not used by mobile/desktop UI) | | `src/Simplex/Chat/Stats.hs` | PC25 | Low | Server statistics tracking | | `src/Simplex/Chat/Util.hs` | — | Low | General Haskell utilities | | `src/Simplex/Chat/Styled.hs` | — | Low | Terminal styled text (not used by mobile/desktop UI) | diff --git a/apps/multiplatform/spec/state.md b/apps/multiplatform/spec/state.md index 900d6593ab..09457c4dd3 100644 --- a/apps/multiplatform/spec/state.md +++ b/apps/multiplatform/spec/state.md @@ -300,6 +300,21 @@ data class ChatStats( | `ChatInfo.ContactConnection` | `"contactConnection"` | `contactConnection: PendingContactConnection` | | `ChatInfo.InvalidJSON` | `"invalidJSON"` | `json: String` | +### RelayStatus (Channels) + +`RelayStatus` is an `enum class` at [`ChatModel.kt line 2288`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L2288) modelling a relay's lifecycle for a channel on the owner's side. Serialized as a lowercase string via `@SerialName`. + +| Case | SerialName | Meaning | +|---|---|---| +| `RsNew` | `"new"` | Allocated locally; not yet sent | +| `RsInvited` | `"invited"` | `XGrpRelayInv` sent, awaiting `XGrpRelayAcpt` | +| `RsAccepted` | `"accepted"` | Accepted, link-data update pending | +| `RsActive` | `"active"` | Listed in channel link data; forwarding | +| `RsInactive` | `"inactive"` | No longer in link data or backend reports it removed | +| `RsRejected` | `"rejected"` | Relay sent `XGrpRelayReject` for the channel link; final on the owner side. Clearable only by the relay operator running `/group allow #`. The owner-side `GroupMember.memberStatus` is also set to `MemLeft` so the relay renders identically to one that explicitly left (`MemRejected` is reserved for the knocking-admission flow). | + +The `text` extension on the enum returns the localized status string (resource key `relay_status_*`, with `relay_status_rejected` = "rejected"). + --- diff --git a/apps/simplex-support-bot/package.json b/apps/simplex-support-bot/package.json index 8541056aa5..97caee2278 100644 --- a/apps/simplex-support-bot/package.json +++ b/apps/simplex-support-bot/package.json @@ -8,10 +8,10 @@ "start": "node dist/index.js" }, "dependencies": { - "@simplex-chat/types": "^0.6.0", + "@simplex-chat/types": "^0.7.0", "async-mutex": "^0.5.0", "commander": "^14.0.3", - "simplex-chat": "^6.5.1", + "simplex-chat": "^6.5.2", "yaml": "^2.8.4" }, "devDependencies": { diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index 2d804ccaa9..d14435cabd 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -33,6 +33,7 @@ This file is generated automatically. - [APINewPublicGroup](#apinewpublicgroup) - [APIGetGroupRelays](#apigetgrouprelays) - [APIAddGroupRelays](#apiaddgrouprelays) +- [APIAllowRelayGroup](#apiallowrelaygroup) - [APIUpdateGroupProfile](#apiupdategroupprofile) [Group link commands](#group-link-commands) @@ -1080,6 +1081,43 @@ ChatCmdError: Command error (only used in WebSockets API). --- +### APIAllowRelayGroup + +Clear relay rejection for a channel (relay operator). + +*Network usage*: background. + +**Parameters**: +- groupId: int64 + +**Syntax**: + +``` +/_relay allow # +``` + +```javascript +'/_relay allow #' + groupId // JavaScript +``` + +```python +'/_relay allow #' + str(groupId) # Python +``` + +**Responses**: + +RelayGroupAllowed: Relay rejection cleared for a channel. +- type: "relayGroupAllowed" +- user: [User](./TYPES.md#user) +- groupInfo: [GroupInfo](./TYPES.md#groupinfo) + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + ### APIUpdateGroupProfile Update group profile. diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 4bd924dc31..b4edb9bd22 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -3350,6 +3350,7 @@ ParseError: - "accepted" - "active" - "inactive" +- "rejected" --- diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index b3eaf96837..8894609758 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -120,6 +120,7 @@ chatCommandsDocsData = ("APINewPublicGroup", [], "Create public group.", ["CRPublicGroupCreated", "CRPublicGroupCreationFailed", "CRChatCmdError"], [], Just UNInteractive, "/_public group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Join ',' "relayIds" <> " " <> Json "groupProfile"), ("APIGetGroupRelays", [], "Get group relays.", ["CRGroupRelays", "CRChatCmdError"], [], Nothing, "/_get relays #" <> Param "groupId"), ("APIAddGroupRelays", [], "Add relays to group.", ["CRGroupRelaysAdded", "CRGroupRelaysAddFailed", "CRChatCmdError"], [], Just UNInteractive, "/_add relays #" <> Param "groupId" <> " " <> Join ',' "relayIds"), + ("APIAllowRelayGroup", [], "Clear relay rejection for a channel (relay operator).", ["CRRelayGroupAllowed", "CRChatCmdError"], [], Just UNBackground, "/_relay allow #" <> Param "groupId"), ("APIUpdateGroupProfile", [], "Update group profile.", ["CRGroupUpdated", "CRChatCmdError"], [], Just UNBackground, "/_group_profile #" <> Param "groupId" <> " " <> Json "groupProfile") ] ), @@ -203,6 +204,7 @@ cliCommands = "AcceptMember", "AddContact", "AddMember", + "AllowRelayGroup", "BlockForAll", "ChatHelp", "ClearContact", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 55f12f0a0a..ddd127241b 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -73,6 +73,7 @@ chatResponsesDocsData = ("CRGroupRelays", ""), ("CRGroupRelaysAdded", ""), ("CRGroupRelaysAddFailed", ""), + ("CRRelayGroupAllowed", "Relay rejection cleared for a channel"), ("CRGroupMembers", ""), ("CRGroupUpdated", ""), ("CRGroupsList", "Groups"), diff --git a/cabal.project b/cabal.project index d059c07ab1..7ee797e621 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 1f173abf6d6fccb617be1e7994629c405983c431 + tag: f03cec7a58ed13a39a52886888c74bcefdb64479 source-repository-package type: git diff --git a/docs/LINKS.md b/docs/LINKS.md new file mode 100644 index 0000000000..46de314a75 --- /dev/null +++ b/docs/LINKS.md @@ -0,0 +1,4174 @@ +# Links to Community Publications + +## SimpleX Chat: Product Showcase - Removing User Identifiers From Messaging + +Help Net Security + +Review + +Help Net Security showcases SimpleX Chat as a free, private, open-source messenger that eliminates traditional user identifiers and stores data locally on devices. The article highlights end-to-end encrypted communications, contact addition through one-time links or QR codes, encrypted audio/video calls via WebRTC with hidden IP addresses, and security verification through comparable security codes between contacts. + +Image: help-net-security-product-showcase.jpg + +Language: English + +Date: Apr 29, 2026 + +https://www.helpnetsecurity.com/2026/04/29/product-showcase-simplex-chat-secure-messaging/ + +## Best Secure Messaging Apps: Signal vs Session vs SimpleX vs Briar + +State of Surveillance + +Comparison + +This secure messaging comparison guide evaluates Signal, Session, SimpleX, Briar, WhatsApp, and Telegram across criteria including phone number requirements, architecture, and metadata protection. SimpleX is highlighted as requiring no phone number, using a decentralized architecture with no metadata collection, and being best suited for maximum privacy, though the guide notes its small user base as a practical limitation. + +Image: state-of-surveillance-comparison.jpg + +Language: English + +Date: May 2026 + +https://stateofsurveillance.org/guides/basic/secure-messaging-comparison/ + +## Evgeny Poberezkin on SimpleX Private Chat + +Citadel Dispatch + +Podcast + +This Citadel Dispatch podcast episode features Evgeny Poberezkin discussing SimpleX Chat's radically different approach to user identity, where addresses are assigned to connections rather than endpoints. The conversation covers critiques of the MLS protocol, upcoming scalable channels to rival Telegram, and a sustainability model where large channels fund network operations. + +Image: citadel-dispatch-cd196.jpg + +Language: English + +Date: Mar 20, 2026 + +https://podcasts.apple.com/is/podcast/cd196-evgeny-poberezkin-simplex-private-chat/id1546393840?i=1000756411661 + +## The Messaging App With No User IDs + +(SimpleX Interview) + +Techlore + +Podcast + +Image: techlore-talks-simplex-interview.jpg + +Language: English + +Date: Jan 24, 2026 + +https://www.youtube.com/watch?v=hfzf0t8ZCK4 + +## Which Encrypted Messenger Is Best Secured? Signal, Session and SimpleX + +(Quelle messagerie chiffree est la mieux securisee? Signal, Session et SimpleX mais pas Whatsapp!) + +Nicolas Forcet + +Comparison + +This French-language article positions SimpleX between Signal and Session in a security hierarchy, noting its protocol design prevents servers from mapping social connections by using separate channels for incoming and outgoing messages. SimpleX is praised for stronger privacy protections including app-level locking, ephemeral messages, and Tor/VPN compatibility, though the author notes lower adoption rates may be a practical drawback. + +Image: nicolas-forcet-comparison.jpg + +Language: French + +Date: Jan 2, 2026 + +https://nicolasforcet.com/messagerie-chiffree-2026-signal-simplex-session/ + +## SimpleX Chat Review 2026: Open-Source Secure Messaging + +Darwin Dynamic + +Review + +This 2026 review describes SimpleX Chat as a strong privacy option that eliminates unique user IDs and features end-to-end encryption with decentralized routing. The reviewer notes a learning curve and ongoing development issues as drawbacks, but ultimately recommends it for privacy-conscious individuals willing to navigate a more complex interface than mainstream alternatives. + +Image: darwin-dynamic-review-2026.jpg + +Language: English + +Date: 2026 + +https://darwindynamic.com/simplex-chat-review-2026/ + +## The 5 Best Private Chat and Message Apps for 2026 + +Darwin Dynamic + +Comparison + +SimpleX Chat ranks third among five recommended private messaging apps for 2026. The article emphasizes that it requires no user identification, making it highly secure and anonymous, while noting that initial configuration can be challenging and occasional technical issues may occur. + +Image: darwin-dynamic-top5-2026.jpg + +Language: English + +Date: 2026 + +https://darwindynamic.com/5-best-private-chat-message-apps-for-2026/ + +## Best Decentralized Private Messengers in 2026 + +Factually + +Comparison + +This product comparison ranks SimpleX as the best choice for maximal anonymity and minimal metadata, describing its peer-to-peer design that avoids server-mediated trust and emphasizes anonymity. The review notes SimpleX's UI is minimal and focused on anonymity features, but acknowledges the app is younger, less battle-tested, and has a smaller user base compared to Signal and Matrix. + +Image: factually-decentralized-2026.jpg + +Language: English + +Date: 2026 + +https://factually.co/product-reviews/electronics-tech/best-decentralized-private-messengers-2026-signal-session-simplex-matrix-a6216a + +## 8 Best Secure Messaging Apps for Encrypted Chats in 2026 + +CloudSEK + +Comparison + +CloudSEK positions SimpleX as the best phone-number-free messaging app, highlighting its relay-based routing, zero metadata approach, and private invitation links instead of identity-based profiles. The article notes a trade-off between SimpleX's strong metadata protection and its smaller user community compared to more established platforms like Signal or Telegram. + +Image: cloudsek-best-secure-2026.jpg + +Language: English + +Date: 2026 + +https://www.cloudsek.com/knowledge-base/best-secure-messaging-apps + +## 10 Best Secure Messaging Apps You Should Check Out in 2026 + +Beebom + +Comparison + +Beebom lists SimpleX Chat third among the best secure messaging apps, calling it the "Best Minimal Secure Messaging App." The article highlights its lack of user IDs, incognito mode with random usernames, live message typing preview, screenshot blocking, and contact verification, noting it requires no email or phone number to use. + +Image: beebom-best-secure-2026.jpg + +Language: English + +Date: 2026 + +https://beebom.com/best-secure-messaging-apps/ + +## Decentralized Messengers: 8 WhatsApp and Telegram Alternatives 2026 + +(Децентрализованные мессенджеры: 8 альтернатив WhatsApp и Telegram в 2026 году) + +itforprof.com + +Comparison + +This Russian-language article describes SimpleX Chat as a decentralized platform that uses cryptographic keys instead of accounts for identification and employs Double Ratchet encryption with quantum-resistant extensions. The article notes that servers cannot access metadata about who communicates with whom, but warns that SimpleX is blocked by Roskomnadzor in Russia and requires a VPN to access. + +Image: itforprof-alternatives-2026.jpg + +Language: Russian + +Date: 2026 + +https://itforprof.com/blog/decentralizovannye-messendzhery/ + +## Release of SimpleX Chat 6.5 + +(Выпуск SimpleX Chat 6.5, ориентированный на консорциум и краудфандинг для независимости) + +OpenNet.ru + +News + +This Russian tech news site covers the SimpleX Chat 6.5 release, highlighting the introduction of channels with stateful messaging capabilities and the establishment of the SimpleX Network Consortium for network independence and governance. The update also brings improved web access features and SOCKS proxy support. + +Image: opennet-simplex-65.jpg + +Language: Russian + +Date: 2026 + +https://opennet.ru/65337/ + +## Vitalik Buterin Donates $765K in Ethereum to Privacy Messaging Apps + +Yahoo Finance + +News + +Yahoo Finance reports that Vitalik Buterin donated approximately $765,000 in Ethereum to privacy messaging apps Session and SimpleX. Buterin praised both apps for advancing permissionless account creation and metadata privacy, while acknowledging neither is perfect and both need improvements in user experience and security. + +Image: yahoo-finance-buterin.jpg + +Language: English + +Date: Nov 2025 + +https://finance.yahoo.com/news/vitalik-buterin-donates-765k-ethereum-190102367.html + +## Vitalik Buterin Supports Privacy-Focused Messaging Platforms With Significant Ethereum Donation + +Bitcoin.com News + +News + +Bitcoin.com reports that Vitalik Buterin donated 128 ETH (approximately $256,000) to SimpleX Chat and an equal amount to Session, totaling 256 ETH. Buterin emphasized the importance of permissionless account creation and metadata privacy, aiming to advance truly private messaging technologies that protect users from surveillance. + +Image: bitcoin-com-buterin.jpg + +Language: English + +Date: Nov 2025 + +https://news.bitcoin.com/vitalik-buterin-supports-privacy-focused-messaging-platforms-with-significant-ethereum-donation/ + +## Inside Vitalik's 256 ETH Grants: When Ethereum Falls, Privacy Rises + +CryptoSlate + +News + +CryptoSlate covers Vitalik Buterin's 256 ETH total grants to Session and SimpleX Chat, framing it as a signal that privacy infrastructure deserves funding when designed as a foundational architectural feature. The article highlights Buterin's support for metadata-resistant communication systems that operate entirely outside Ethereum's blockchain. + +Image: cryptoslate-buterin-analysis.jpg + +Language: English + +Date: Dec 2, 2025 + +https://cryptoslate.com/inside-vitaliks-256-eth-grants-when-eth-falls-privacy-rises/ + +## SimpleX Chat: The First Messaging App with No User Identifiers - Privacy by Design + +BrightCoding + +Review + +Bright Coding presents SimpleX Chat as a privacy-by-design platform that eliminates user identifiers entirely, using pairwise temporary identifiers for each conversation and end-to-end encryption with the double ratchet algorithm. The article highlights decentralized communication through user-hosted relay servers and spam prevention through opt-in contact invitations. + +Image: brightcoding-privacy-by-design.jpg + +Language: English + +Date: Sep 18, 2025 + +https://www.blog.brightcoding.dev/2025/09/18/simplex-chat-the-first-messaging-app-with-no-user-identifiers-privacy-by-design/ + +## SimpleX Chat 6.4.3: New Features and Jack Dorsey's Support + +(SimpleX Chat publie sa version 6.4.3 : une messagerie privee toujours plus aboutie) + +SysKB + +News + +This French tech site covers SimpleX Chat version 6.4.3, noting new features including bot support via commands, hypertext links in Markdown, and automatic removal of link tracking parameters. The article also mentions the project's $1.3 million funding round backed by Jack Dorsey in August 2024. + +Image: syskb-simplex-643.jpg + +Language: French + +Date: Aug 20, 2025 + +https://syskb.com/simplex-chat-6-4-3-nouveautes/ + +## Discover the Best Secure Messaging Apps in 2025 + +(Decouvrez les Meilleures Applications de Messagerie Securisee en 2025) + +Ca Marche Ca Fonctionne + +Comparison + +This French-language guide on secure messaging apps describes SimpleX Chat as minimalist while offering advanced features, particularly its incognito mode that generates random identifiers for each conversation. SimpleX receives less detailed coverage than Signal, which is positioned as the top security leader in the article. + +Image: camarchecafonctionne-secure-2025.jpg + +Language: French + +Date: Aug 26, 2025 + +https://www.camarchecafonctionne.com/decouvrez-les-meilleures-applications-de-messagerie-securisee-en-2025/ + +## SimpleX Chat Review - Secure and Private Messaging? + +HTR + +Review, Video + +Image: htr-simplex-review.jpg + +Language: English + +Date: Feb 10, 2025 + +https://www.youtube.com/watch?v=rGrF1M7x0Nk + +## Improving SimpleX With Evgeny From SimpleX and Daniel Keller From Flux + +Opt Out Podcast + +Podcast + +This Opt Out Podcast episode features Evgeny from SimpleX and Dan Keller from Flux discussing improvements to SimpleX Chat, including quantum-resistant encryption and privacy-preserving content moderation. The conversation also covers a new chat relay approach developed collaboratively between SimpleX and Flux, along with SimpleX's network operator monetization plans. + +Image: optout-improving-simplex.jpg + +Language: English + +Date: Jan 24, 2025 + +https://optoutpod.com/episodes/improving-simplex/ + +## Best WhatsApp Alternatives 2025 + +Tuta Blog + +Comparison + +Tuta designates SimpleX as the "best decentralized" WhatsApp alternative, highlighting that it requires no phone number or ID for registration and uses a decentralized network where each chat creates unique fingerprints to prevent connection mapping. The article calls it a must-try for those whose personal well-being and safety demand a greater amount of privacy, while noting its smaller user base compared to competitors. + +Image: tuta-whatsapp-alternatives.jpg + +Language: English + +Date: 2025 + +https://tuta.com/blog/best-whatsapp-alternatives-privacy + +## Best WhatsApp Alternatives for Privacy + +Proton Blog + +Comparison + +Proton's blog highlights SimpleX Chat's radical privacy approach of requiring no phone number, email, or username to create an account, using one-time invitation links instead of a central user directory. The article notes that SimpleX encrypts both messages and metadata and has undergone independent security audits, but faces limitations including a small user base and reports of misuse by extremist groups. + +Image: proton-whatsapp-alternatives.jpg + +Language: English + +Date: 2025 + +https://proton.me/blog/whatsapp-alternatives + +## What Is SimpleX Chat? + +(Qu'est-ce que SimpleX Chat) + +No Trust Verify + +Article + +This French-language article from NoTrustVerify describes SimpleX Chat as the first messaging platform with no identifiers that respects privacy by default. It explains the decentralized architecture using message queues instead of user accounts, and highlights features like Incognito Mode, Live Messages, and separate Chat Profiles for different conversations. + +Image: notrustverify-simplex-explainer.jpg + +Language: French + +Date: May 5, 2025 + +https://blog.notrustverify.ch/quest-ce-que-simplex-chat + +## SimpleX Chat: Privacy-Friendly Messenger for Maximum Privacy + +(SimpleX Chat - Datenschutzfreundlicher Messenger fur maximale Privatsphare) + +Digital Unplug Schweiz + +Guide + +This German-language privacy site describes SimpleX Chat as an open-source messenger that completely eliminates user IDs and stores data locally on devices. It details the Double-Ratchet Protocol encryption, temporary connection links, decentralized proxy server routing with optional Tor integration, and cross-platform availability across iOS, Android, Windows, macOS, and Linux. + +Image: digitalunplug-simplex-guide.jpg + +Language: German + +Date: 2025 (estimated) + +https://digitalunplug.ch/simplex.php + +## SimpleX Chat - Next Level Private Messaging + +Mental Outlaw + +Review, Video + +Image: mental-outlaw-simplex-review.jpg + +Language: English + +Date: Oct 1, 2024 + +https://www.youtube.com/watch?v=0cRu98XSap0 + +## Why We Recommend SimpleX Now + +Techlore + +Review, Video + +Image: techlore-recommend-simplex.jpg + +Language: English + +Date: Oct 7, 2024 + +https://www.youtube.com/watch?v=DVKe8U-n8fU + +## Open-Source SimpleX Chat Succeeds Where Telegram Failed + +Notebookcheck + +News + +Notebookcheck argues that SimpleX Chat addresses privacy failures inherent in Telegram, highlighting that SimpleX requires no phone number, uses end-to-end encryption with onion routing, and allows users to select their own servers. The article notes that SimpleX operates in incognito mode by default and that even SimpleX itself cannot determine where messages originate. + +Image: notebookcheck-simplex-succeeds.jpg + +Language: English + +Date: Oct 2, 2024 + +https://www.notebookcheck.net/Open-source-SimpleX-Chat-succeeds-where-Telegram-failed.896988.0.html + +## SimpleX Chat Group Chat Tested in Practice + +(SimpleX: Gruppenchat-Funktion im Praxistest) + +Kuketz IT-Security Blog + +Review + +German privacy blogger Mike Kuketz documents significant performance issues with SimpleX's group chat functionality, including substantial message delays where one message sent at 19:39 arrived the next day at 11:50. The founder acknowledged that groups were never designed for more than 50 users, and the article concludes SimpleX is unsuitable for group chats exceeding that limit. + +Image: kuketz-group-chat-test.jpg + +Language: German + +Date: Oct 14, 2024 + +https://www.kuketz-blog.de/simplex-gruppenchat-funktion-im-praxistest/ + +## My Experience With SimpleX Chat: Is It the Ultimate Open Source Private Messaging App? + +It's FOSS + +Review + +It's FOSS presents a highly positive experience with SimpleX Chat, praising its no-ID signup, quantum-resistant encryption, and intuitive messaging features. The main drawback noted was buggy video calls with audio and quality issues, but overall the reviewer concludes SimpleX sets a new standard for secure communication. + +Image: itsfoss-simplex-review.jpg + +Language: English + +Date: Dec 2024 + +https://itsfoss.com/news/simplex-chat/ + +## SimpleX Chat: A Decentralized Messaging App + +(SimpleX Chat, un'app di messaggistica decentralizzata) + +Le Alternative + +Review + +This Italian article reviews SimpleX Chat as a completely decentralized messaging app emphasizing anonymity and privacy, with no user IDs and end-to-end encrypted messages that remain on servers only until delivery. It notes support for audio/video calls, message editing, and self-hosted servers, while flagging drawbacks like high battery consumption and account recovery difficulties without backups. + +Image: lealternative-simplex-review.jpg + +Language: Italian + +Date: Sep 18, 2024 + +https://blog.lealternative.net/2024/09/18/simplex-chat-unapp-di-messaggistica-decentralizzata/ + +## SimpleX: The Revolution of Private Messaging + +(SimpleX: La Rivoluzione della Messaggistica Privata) + +aiutocomputerhelp.it + +Review + +This Italian article describes SimpleX as a radical messaging platform that eliminates permanent user identities entirely. Communication occurs through encrypted invitation links creating isolated, non-traceable channels, with no central server knowing anything about users and the ability to self-host relay servers. + +Image: aiutocomputerhelp-simplex-revolution.jpg + +Language: Italian + +Date: Jul 16, 2025 + +https://www.aiutocomputerhelp.it/simplex-la-rivoluzione-della-messaggistica-privata/ + +## SimpleX: Messaging With Total Privacy + +(SimpleX: mensajeria con total privacidad) + +VeraSoul + +Review + +This Spanish-language article presents SimpleX Chat as a highly secure messaging app that requires no user IDs, unlike Telegram or Signal. It highlights the app's use of its own SMP protocol, end-to-end encryption, decentralized architecture, and the option for users to choose between SimpleX servers or self-host alternatives. + +Image: verasoul-simplex-review.jpg + +Language: Spanish + +Date: Nov 19, 2024 + +https://verasoul.com/simplex-mensajeria-con-total-privacidad.html + +## SimpleX Chat: The Messaging App Every Privacy Enthusiast Should Use + +(SimpleX Chat la aplicacion de mensajeria que todo entusiasta de la privacidad deberia utilizar) + +GatoOscuro + +Review + +This Spanish-language article recommends SimpleX Chat as an exceptional privacy platform that eliminates metadata surveillance risks present in alternatives like Signal. The author highlights out-of-band key exchange making man-in-the-middle attacks practically impossible, anonymous peer identifiers, self-destructing messages, and a decentralized client-centric architecture. + +Image: gatooscuro-simplex-review.jpg + +Language: Spanish + +Date: 2022 (estimated) + +https://gatooscuro.xyz/simplex-chat-la-aplicacion-de-mensajeria-que-todo-entusiasta-de-la-privacidad-deberia-utilizar/ + +## Interview With the Author of SimpleX Chat: The Most Secure Messaging by Design + +(Entrevista con el autor de SimpleX Chat: la mensajeria mas segura por diseno) + +GatoOscuro + +Interview + +This Spanish-language interview with SimpleX founder Evgeny Poberezkin covers the project's approach to eliminating user identifiers, addresses concerns about government surveillance and backdoors, and discusses funding from Village Global without compromising independence. Poberezkin emphasizes that adoption, word-of-mouth promotion, and user donations are essential for the project's survival. + +Image: gatooscuro-simplex-interview.jpg + +Language: Spanish + +Date: 2024 + +https://gatooscuro.xyz/entrevista-con-el-autor-de-simplex-chat-la-mensajeria-mas-segura-por-diseno/ + +## SimpleX Chat Messaging App + +(App de mensajeria SimpleX Chat) + +Alt43.es + +Review + +This Spanish-language Medium article reviews SimpleX Chat's security features including end-to-end encryption, open-source code, and the ability to register without a phone number or email. It explains that users exchange temporary anonymous identifiers via QR codes or one-time links, with all data stored only on client devices using an encrypted, portable database format. + +Image: burp-simplex-review.jpg + +Language: Spanish + +Date: Aug 1, 2023 + +https://medium.com/@burp.es/app-de-mensajer%C3%ADa-simplex-chat-c0ad46b50f1f + +## SimpleX Is a Revolutionary Messaging Platform That Redefines Privacy + +(O SimpleXchat e uma plataforma de mensagens revolucionarias que redefinem a privacidade) + +Alex Emidio + +Review + +This Portuguese-language Medium article describes SimpleX Chat as a revolutionary privacy platform that eliminates user IDs entirely, requiring no phone numbers or email addresses. It highlights end-to-end encrypted messages, group chats, file sharing, disappearing messages, and encrypted audio/video calls, all operating on decentralized servers using unidirectional message queues. + +Image: alex-emidio-simplex-review.jpg + +Language: Portuguese + +Date: Oct 10, 2023 + +https://medium.com/@alexemidio/o-simplexchat-%C3%A9-uma-plataforma-de-mensagens-revolucion%C3%A1rias-que-redefinem-a-privacidade-sendo-o-4690f2a1b2d4 + +## SimpleX: The Chat Network That Preserves Metadata Privacy + +(SimpleX, a rede de bate-papo que preserva a privacidade de metadados) + +Edivaldo Brito + +Review + +This Portuguese article describes SimpleX as a decentralized, open-source chat network that preserves metadata privacy by using disposable relay nodes and assigning no user identifiers for message routing. It explains that SimpleX employs two layers of end-to-end encryption - double ratchet for forward secrecy and a second layer to protect metadata - along with unidirectional (simplex) message queues that combine the advantages of P2P and server-based architectures. + +Image: edivaldo-brito-simplex-review.jpg + +Language: Portuguese + +Date: 2024 (estimated) + +https://www.edivaldobrito.com.br/simplex-a-rede-de-bate-papo-que-preserva-a-privacidade-de-metadados/ + +## SimpleX Is an Alternative to Telegram Focused on Privacy + +(SimpleX e uma alternativa ao Telegram com foco na privacidade) + +Midia Segura + +Review + +This Portuguese-language article presents SimpleX as a privacy-focused alternative to Telegram, noting increased user migration following Pavel Durov's arrest and Telegram's policy changes. It highlights SimpleX as the first messaging app without a user ID, email, or phone number, with backing from Jack Dorsey. + +Image: midia-segura-simplex-review.jpg + +Language: Portuguese + +Date: 2024 (estimated) + +https://midiasegura.com/simplex-e-uma-alternativa-ao-telegram-com-foco-na-privacidade/ + +## SimpleX Chat: The First Messenger Without User ID + +(Simplex Chat - O primeiro mensageiro sem ID de usuario) + +Rafael Mesquita / TabNews + +Article + +This Portuguese-language article highlights SimpleX Chat as a privacy-focused messenger that uses temporary anonymous pairwise message queue identifiers instead of user IDs, phone numbers, or usernames. It explains that this design prevents metadata correlation attacks and that user profiles are stored only on devices, with dual-layer end-to-end encryption and optional Tor connections. + +Image: tabnews-simplex-first-messenger.jpg + +Language: Portuguese + +Date: 2024 (estimated) + +https://www.tabnews.com.br/RafaelMesquita/simplex-chat-o-primeiro-mensageiro-sem-id-de-usuario + +## SimpleX Chat: A Revolutionary Tool for Private and Even Anonymous Communications + +(SimpleX Chat: A Ferramenta Revolucionaria para Comunicacoes Privadas e ate Anonimas) + +Coach De Osasco + +Review, Video + +Image: portuguese-simplex-revolutionary.jpg + +Language: Portuguese + +Date: 2024 (estimated) + +https://www.youtube.com/watch?v=fUwPGhSYlLY + +## Exploring SimpleX Chat: A Scriptable, Decentralized Messaging App + +Rick Carlino + +Article + +The author explores SimpleX Chat's bot-building capabilities using its CLI application with WebSocket support, finding the process remarkably straightforward and reminiscent of writing IRC scripts with a better UX. The article includes a functional TypeScript example demonstrating how to build chatbots through JSON commands, requiring minimal setup of just downloading an executable and connecting via WebSocket. + +Image: rickcarlino-simplex-bots.jpg + +Language: English + +Date: 2024 + +https://rickcarlino.com/2024/simplex-chat-bots.html + +## Session and SimpleX: Encrypted Messenger Comparison + +FreedomNode + +Comparison + +FreedomNode compares SimpleX and Session, noting that SimpleX takes privacy further by eliminating user identifiers entirely and using temporary anonymous message queue identifiers for each conversation. The article notes SimpleX represents a more experimental privacy frontier while Session provides greater stability and cross-device compatibility, with SimpleX having limitations around desktop support and requiring both parties to be online to establish connections. + +Image: freedomnode-session-simplex.jpg + +Language: English + +Date: Oct 29, 2023 + +https://freedomnode.com/blog/session-and-simplex-encrypted-messenger-comparison/ + +## SimpleX: The First Messenger Without User Identifiers + +(SimpleX - первый мессенджер без идентификаторов пользователей) + +Habr / Privacy Accelerator + +Article + +This Russian-language Habr article introduces SimpleX Chat as a privacy-focused messenger that operates without user identifiers, using separate message queue identifiers for each contact. It highlights the SimpleX Messaging Protocol with end-to-end encryption, Tor routing support, open-source code, and a professional security audit by Trail of Bits. + +Image: habr-simplex-first-messenger.jpg + +Language: Russian + +Date: Dec 2022 + +https://habr.com/ru/companies/privacyaccelerator/articles/705778/ + +## An Anonymous Messenger: A Mandatory Standard for Every Person + +(Анонимный мессенджер - обязательный стандарт для каждого человека) + +Habr + +Article + +This Russian-language article argues that anonymous messaging with strong encryption should be a standard necessity for everyone, as governments increasingly surveil communications. SimpleX Chat is highlighted as innovative for being the first messenger without user identifiers, using temporary anonymous paired identifiers for each connection that make it impossible to correlate anonymous profiles with real identities. + +Image: habr-anonymous-standard.jpg + +Language: Russian + +Date: Nov 2024 + +https://habr.com/ru/articles/851866/ + +## Open-Source SimpleX Chat Succeeds Where Telegram Failed + +(Чат SimpleX с открытым исходным кодом преуспел там, где Telegram потерпел неудачу) + +Notebookcheck Russia + +News + +This Russian-language Notebookcheck article highlights how SimpleX Chat addresses privacy concerns that Telegram failed to handle, including not requiring phone numbers, using one-way onion routing, allowing users to select their own servers, and providing end-to-end encryption with local device encryption. Unlike Telegram, which made changes after government pressure, SimpleX maintains stronger anonymity protections. + +Image: notebookcheck-ru-simplex.jpg + +Language: Russian + +Date: Oct 3, 2024 + +https://www.notebookcheck-ru.com/CHat-SimpleX-s-otkrytym-iskhodnym-kodom-preuspel-tam-gde-Telegram-poterpel-neudachu.897103.0.html + +## Messenger for Paranoids: Configuring SimpleX Chat + +(Мессенджер для параноиков. Настраиваем SimpleX Chat) + +Первый отдел + +Guide, Video + +Image: russian-paranoid-messenger-tutorial.jpg + +Language: Russian + +Date: Nov 3, 2024 + +https://www.youtube.com/watch?v=3UcejFJ3TY0 + +## SimpleX Chat: A Damn Private Messaging Service + +(SimpleX Chat une messagerie sacrement privee) + +Siksik + +Review + +Image: siksik-simplex-review.jpg + +Language: French + +Date: 2024 (estimated) + +https://siksik.org/simplex-chat-une-messagerie-sacrement-privee/ + +## SimpleX Chat: A New Instant Messaging Application + +(SimpleX Chat: Une Nouvelle Application de Messagerie Instantanee) + +AEKONE + +Review + +Image: aekone-simplex-review.jpg + +Language: French + +Date: 2024 (estimated) + +https://aek.one/simplex-chat-une-nouvelle-application-de-messagerie-instantannee/ + +## SimpleX Chat Review + +Freedom.Tech + +Review + +Freedom.tech's review praises SimpleX Chat's beautiful UI, metadata protection through its peer-to-relay architecture, double ratchet encryption, and incognito mode for pseudonymous communication. The review identifies key weaknesses including intermittent notifications on both iOS and Android and flaky audio/video calls in beta, but concludes it is a fantastic tool ideal for activists, journalists, and privacy-focused users. + +Image: freedom-tech-simplex-review.jpg + +Language: English + +Date: Sep 26, 2023 + +https://freedom.tech/simplex-chat-review/ + +## SimpleX Raises $1.3M From Jack Dorsey and Asymmetric VC, Releases Chat v6.0 + +NoBs Bitcoin + +News + +No BS Bitcoin reports that SimpleX Chat secured $1.3 million in pre-seed investment led by Jack Dorsey and Asymmetric Capital Partners. The v6.0 release includes protocol improvements reducing the messages needed for users to connect by half, a redesigned mobile interface, image blur options, and improved moderation tools supporting up to 20 message selections. + +Image: nobsbitcoin-funding-v60.jpg + +Language: English + +Date: Aug 2024 + +https://www.nobsbitcoin.com/simplex-chat-v6-0/ + +## SimpleX Chat v6.1: Better Calls, iOS Notifications, UX Improvements + +NoBs Bitcoin + +News + +No BS Bitcoin covers SimpleX Chat v6.1, which enhances call functionality with camera activation and screen sharing from the desktop app during voice calls. The release also resolves iOS notification delivery problems, improves connectivity, and features a redesigned conversation layout with faster message operations. + +Image: nobsbitcoin-v61.jpg + +Language: English + +Date: Oct 2024 + +https://www.nobsbitcoin.com/simplex-chat-v6-1-0/ + +## SimpleX Chat v5.7: Quantum Resistant End-to-End Encryption + +NoBs Bitcoin + +News + +No BS Bitcoin reports that SimpleX Chat v5.7 introduced quantum-resistant end-to-end encryption for all contacts and message forwarding without revealing the source. The article notes planned third-party security audits and the team's request for community donations to help cover audit costs exceeding $50,000. + +Image: nobsbitcoin-v57-quantum.jpg + +Language: English + +Date: Apr 2024 + +https://www.nobsbitcoin.com/simplex-chat-v5-7-0/ + +## SimpleX Chat v5.8: Private Message Routing + +NoBs Bitcoin + +News + +No BS Bitcoin covers SimpleX Chat v5.8's introduction of private message routing, described as a 2-hop onion routing protocol inspired by Tor that protects both users' IP addresses and transport sessions. The update also adds file reception protections from unknown servers, chat themes with wallpapers, and improved group permissions for admins. + +Image: nobsbitcoin-v58-routing.jpg + +Language: English + +Date: Jun 2024 + +https://www.nobsbitcoin.com/simplex-chat-v5-8/ + +## SimpleX Launches Quantum-Resistant Encryption in Beta + +Reclaim the Net + +News + +Reclaim The Net reports that SimpleX Chat launched a beta version of quantum-resistant encryption, implementing post-quantum cryptography with an improved quantum-resistant double ratchet algorithm. The article notes the use of fixed 16kb block sizes and lossless compression to prevent traffic analysis, with the feature currently optional for users. + +Image: reclaimthenet-quantum-beta.jpg + +Language: English + +Date: Apr 2, 2024 + +https://reclaimthenet.org/simplex-chat-launches-quantum-resistant-encryption-in-beta + +## SimpleX Introduces Enhanced IP Privacy Measures + +Reclaim the Net + +News + +Reclaim The Net covers SimpleX Chat v5.8's private message routing protocol where the forwarding relay is chosen by the sender and the second by the recipient, ensuring neither party can observe the other's IP address. The developers rejected embedding Tor due to latency and metadata correlation issues, instead building their own solution that provides IP protection by default. + +Image: reclaimthenet-ip-privacy.jpg + +Language: English + +Date: Jun 12, 2024 + +https://reclaimthenet.org/simplex-introduces-enhanced-ip-privacy-measures + +## We Found the Most Incognito iPhone Messaging App + +(On a trouve l'app de messagerie iPhone la plus incognito) + +iPhon.fr + +Article + +This French iPhone blog highlights SimpleX Chat as the most confidential messaging application on iOS, featuring no user identification requirements, encrypted metadata and profile hiding, and decentralized infrastructure allowing users to run it on personal servers. Contacts are established via unique QR codes rather than phone numbers or usernames. + +Image: iphon-fr-most-incognito.jpg + +Language: French + +Date: Jul 15, 2023 + +https://www.iphon.fr/post/on-a-trouve-lapp-de-messagerie-iphone-la-plus-incognito + +## SimpleX: Secure Messaging With Self-Hosted Server + +(Simplex - Messagerie securisee avec serveur auto-heberge) + +Paf LeGeek + +Guide, Video + +Image: paflegeek-simplex-selfhost.jpg + +Language: French + +Date: Jun 19, 2023 + +https://www.youtube.com/watch?v=66URjJ1RkeM + +## SimpleX Chat and How Privacy Aligns With the Future of Computing + +Opt Out Podcast + +Podcast + +This Opt Out Podcast episode features SimpleX Chat founder Evgeny discussing how privacy aligns with the future of computing. The conversation covers data sovereignty, SimpleX's decentralized architecture, and the project's emphasis on user control, with resources provided on running independent servers. + +Image: optout-simplex-s3e02.jpg + +Language: English + +Date: Feb 27, 2023 + +https://optoutpod.com/episodes/s3e02-simplexchat/ + +## SimpleX Chat: Messenger Without UserID With Maximum Protection + +(SimpleX Chat: мессенджер без UserID с максимальной защитой переписки) + +Теплица социальных технологий + +Review, Video + +Image: russian-simplex-max-protection.jpg + +Language: Russian + +Date: Aug 8, 2023 + +https://www.youtube.com/watch?v=g9HGLNCkEyk + +## SimpleX Chat: Overview of Main Functions + +(SimpleX Chat #2: обзор основных функций) + +Теплица социальных технологий + +Review, Video + +Image: russian-simplex-overview-functions.jpg + +Language: Russian + +Date: Aug 10, 2023 + +https://www.youtube.com/watch?v=fp1QUPNkxKI + +## SimpleX Chat: Anonymous Messenger Without ID + +(SimpleX CHAT - анонимный мессенджер без ID) + +Чёрный Треугольник + +Review, Video + +Image: russian-simplex-anonymous-no-id.jpg + +Language: Russian + +Date: Aug 25, 2023 + +https://www.youtube.com/watch?v=Ecx5jGUn-hQ + +## SimpleX Chat: The Ultra-Private Messaging App Almost Nobody Knows + +(SimpleX Chat la app de mensajeria ultra privada que casi nadie conoce) + +RD CIPHER + +Review, Video + +Image: spanish-simplex-ultra-private.jpg + +Language: Spanish + +Date: 2024 (estimated) + +https://www.youtube.com/watch?v=5vroKXgVZ3Q + +## SimpleX: Messaging WITHOUT Identifiers + +(SimpleX: mensajeria SIN identificadores) + +Elurk Informatica + +Review, Video + +Image: spanish-simplex-sin-identificadores.jpg + +Language: Spanish + +Date: 2024 (estimated) + +https://www.youtube.com/watch?v=Uit79EFxTAs + +## An Overview of Privacy-Focused, Decentralized Instant Messengers + +Marius + +Article + +This privacy-focused blog describes SimpleX as an open-source, decentralized instant messenger that lacks fixed user identifiers, requiring neither a phone number nor a username. However, the author withdrew their recommendation after SimpleX received venture capital funding from Jack Dorsey in August 2024, citing concerns about the investment's influence. + +Image: marius-privacy-messengers-overview.jpg + +Language: English + +Date: 2024 (estimated) + +https://xn--gckvb8fzb.com/an-overview-of-privacy-focused-decentralized-instant-messengers/ + +## What Is SimpleX Chat? + +No Trust Verify / Medium + +Article + +NoTrustVerify describes SimpleX Chat as the first messaging platform that requires no login and respects privacy by default, operating through a decentralized architecture using one-way message queues rather than centralized servers. The article discusses incognito mode, live message typing indicators, and separate chat profiles, while noting challenges around multi-device synchronization and large group management. + +Image: notrustverify-what-is-simplex.jpg + +Language: English + +Date: Jun 2023 + +https://medium.com/notrustverify/what-is-simplex-chat-11124d39a318 + +## SimpleX Chat v5.4: Link Mobile and Desktop Apps via Quantum-Resistant Protocol + +NoBs Bitcoin + +News + +No BS Bitcoin covers SimpleX Chat v5.4, which introduced the ability to link mobile and desktop apps via a secure quantum-resistant protocol on local networks. The update also improved group functionality with faster joining, incognito profile support for groups, and added screen sharing for video calls on desktop. + +Image: nobsbitcoin-v54-desktop.jpg + +Language: English + +Date: Nov 2023 + +https://www.nobsbitcoin.com/simplex-chat-v5-4/ + +## SimpleX Chat v5.3: Desktop App, Local File Encryption and Improved Groups + +NoBs Bitcoin + +News + +No BS Bitcoin reports on SimpleX Chat v5.3, which introduced a new desktop app, a directory service with group improvements, and encrypted local files and media with forward secrecy. The release also achieved a 40% reduction in memory usage and added new privacy controls for message visibility. + +Image: nobsbitcoin-v53-desktop.jpg + +Language: English + +Date: Sep 2023 + +https://www.nobsbitcoin.com/simplex-chat-v5-3/ + +## Haskell in Production: SimpleX + +Serokell + +Interview + +Serokell interviews SimpleX Chat creator Evgeny Poberezkin about building the platform in Haskell, chosen for its strength in highly concurrent communication applications with features like green threads and transactional memory. The article discusses challenges of compiling Haskell to mobile devices and the project's exploration of monetization through optional subscriptions while remaining fully open-source. + +Image: serokell-haskell-simplex.jpg + +Language: English + +Date: May 17, 2022 + +https://serokell.io/blog/haskell-in-production-simplex + +## SimpleX: Impressions From the Messenger Without Identifiers + +(SimpleX: Eindrucke vom Messenger ohne Identifier) + +Kuketz IT-Security Blog + +Review + +German privacy blogger Mike Kuketz rates SimpleX as "conditionally recommended," praising its innovative no-identifier design and Tor support. He identifies weaknesses including the absence of contact verification, high battery consumption, and client instability, though notes the developer indicated these issues are being addressed. + +Image: kuketz-simplex-review.jpg + +Language: German + +Date: Dec 2, 2022 + +https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/ + +## Decentralized, Anonymous, Encrypted: SimpleX Messenger Now Also for Smartphones + +(Dezentral, anonym, verschlusselt: SimpleX-Messenger jetzt auch furs Smartphone) + +Heise Online + +News + +Heise reports on SimpleX Chat releasing smartphone apps for iPhone and Android after initially being available only as a command-line application. The article highlights the decentralized architecture where servers cannot know who communicated with whom, with users sharing QR codes to establish encrypted connections. + +Image: heise-simplex-smartphone.jpg + +Language: German + +Date: Mar 9, 2022 + +https://www.heise.de/news/Dezentral-anonym-verschluesselt-SimpleX-Messenger-jetzt-auch-fuers-Smartphone-6544488.html + +## So Apple Won't Read Along: SimpleX Chat Updated to Version 3 + +(Damit Apple nichts mitliest: SimpleX-Chat aktualisiert auf Version 3) + +Heise Online + +News + +Heise covers SimpleX Chat version 3's privacy-focused iOS push notifications that contain no information about contacts or chat content, preventing any data from reaching Apple's servers. The update also introduced database export/import functionality and faster message transmission while maintaining backward compatibility. + +Image: heise-simplex-v3-apple.jpg + +Language: German + +Date: Jul 12, 2022 + +https://www.heise.de/news/Damit-Apple-nichts-mitliest-SimpleX-Chat-aktualisiert-auf-Version-3-7170288.html + +## Open-Source Messenger SimpleX: Anonymous and Now Also Private Chatting + +(Open-Source-Messenger SimpleX: Anonym und jetzt auch privat chatten) + +Heise Online + +News + +Heise reports on SimpleX Chat version 4.0 introducing private server authentication through password protection, allowing server operators to control who can receive messages while maintaining the platform's anonymous design. Previously all chat servers were public, but administrators can now restrict access by sharing passwords with intended users. + +Image: heise-simplex-v4-private.jpg + +Language: German + +Date: Nov 29, 2022 + +https://www.heise.de/news/Open-Source-Messenger-SimpleX-anonym-und-jetzt-auch-privat-chatten-7359889.html + +## SimpleX 1.0.0: Decentralized, Privacy-Respecting and Encrypted Chat + +(SimpleX 1.0.0: Dezentraler, Privatsphare achtender und verschlusselter Chat) + +Heise Online + +News + +Heise covers SimpleX Chat reaching version 1.0.0, marking its protocol as stable with guaranteed compatibility for future releases. The article explains the dual-layer end-to-end encryption using a double-ratchet mechanism that changes keys for each message, with decentralized servers routing messages without storing user data or metadata. + +Image: heise-simplex-100-release.jpg + +Language: German + +Date: Jan 13, 2022 + +https://www.heise.de/news/SimpleX-1-0-0-Dezentraler-Privatsphaere-achtender-und-verschluesselter-Chat-6325990.html + +## SimpleX Chat Wants to Offer Complete Privacy + +(SimpleX Chat will vollstandige Privatsphare bieten) + +iphone-ticker.de + +News + +German iPhone blog iphone-ticker covers SimpleX Chat's privacy approach of establishing direct connections through shared QR codes or links for end-to-end encrypted messaging. The article highlights version 3.0's push notifications that reveal nothing about chat content or contacts, and the addition of encrypted audio and video calling. + +Image: iphone-ticker-simplex-privacy.jpg + +Language: German + +Date: Jul 13, 2022 + +https://www.iphone-ticker.de/simplex-chat-will-vollstaendige-privatsphaere-bieten-194389/ + +## SimpleX Chat Now Also for Smartphones + +(SimpleX-Chat jetzt auch fur Smartphones) + +GNU/Linux.ch + +News + +This Swiss GNU/Linux community site reports on SimpleX Chat's release of smartphone applications for iPhone and Android, expanding beyond its command-line interface. The article emphasizes end-to-end encryption, decentralized architecture, and QR code-based peer-to-peer connections without routing data through SimpleX servers. + +Image: gnulinux-ch-simplex-smartphones.jpg + +Language: German + +Date: Mar 10, 2022 + +https://gnulinux.ch/simplex-chat-smartphones + +## SimpleX Chat: A Star on the Open-Source Messenger Horizon + +(SimpleX Chat - Frohlockender Stern am Open-Source Messenger Himmel) + +hackspoiler.de + +Review + +This German security blog highlights SimpleX Chat as a privacy-focused open-source messenger requiring no user ID, with end-to-end encryption for all communications including voice messages, audio/video calls, and self-destructing messages. The article positions it as a secure alternative to proprietary platforms, comparing it to Signal and Session under the AGPL V3 license. + +Image: hackspoiler-simplex-star.jpg + +Language: German + +Date: 2023 (estimated) + +https://hackspoiler.de/simplex-chat-verschluesselter-opensource-messenger/ + +## Good Messengers Instead of WhatsApp + +(Gute Messenger statt WhatsApp) + +Digitalcourage + +Guide + +German digital rights organization Digitalcourage describes SimpleX as a relatively new open-source messenger launched in 2022 that uses distributed servers with temporary connection identifiers rather than phone numbers or usernames. While it functions reliably and supports voice messages, file transfers, and video calls, the organization only conditionally recommends it due to its youth as a project and limited track record. + +Image: digitalcourage-simplex-recommendation.jpg + +Language: German + +Date: 2023 (estimated) + +https://digitalcourage.de/digitale-selbstverteidigung/messenger + +## Messenger SimpleX Protects Privacy and Is Open Source + +(Messenger SimpleX schutzt Privatsphare und ist Open Source) + +Linux-Magazin + +News + +German Linux Magazin reports on SimpleX as an anonymous, encrypted open-source messenger (AGPLv3) that uses no central server to track connections - servers only relay messages, and anyone can self-host a message broker using SimpleXMQ. The article highlights SimpleX's double ratchet end-to-end encryption with forward secrecy and metadata concealment, noting that at the time of writing only a command-line client was available with mobile apps in development. + +Image: linux-magazin-simplex-privacy.jpg + +Language: German + +Date: 2022 (estimated) + +https://www.linux-magazin.de/news/messenger-simplex-schuetzt-privatsphaere-und-ist-open-source/ + +## SimpleX + +freie-messenger.de + +Article + +This German free messenger comparison site provides a comprehensive overview of SimpleX Chat, noting its distinctive lack of user identifiers and end-to-end encryption with data stored only on client devices. The assessment concludes that while SimpleX is an innovative privacy-focused messenger suitable for individuals and activists, it has limitations for business use and lacks interoperability with standard protocols like XMPP. + +Image: freie-messenger-simplex.jpg + +Language: German + +Date: updated regularly + +https://www.freie-messenger.de/simplex/ + +## The Best Private Instant Messengers + +Privacy Guides + +Review + +Privacy Guides recommends SimpleX Chat as an instant messenger that does not depend on any unique identifiers such as phone numbers or usernames. The page notes its double ratchet encryption with quantum resistance, metadata protection through unidirectional message delivery queues, and independent security audits conducted in July 2024 and October 2022. + +Image: privacy-guides-recommendation.jpg + +Language: English + +Date: updated regularly + +https://www.privacyguides.org/en/real-time-communication/ + +## SimpleX + +Whonix Wiki + +Review + +The Whonix wiki describes SimpleX as a general-purpose instant messaging client with both client and server released as Freedom Software under GNU AGPLv3. It highlights mandatory end-to-end encryption, quantum-resistant encryption by default for one-on-one chats, and incognito mode, while providing detailed Flatpak installation instructions and warning that users must export their chat database before shutdown to avoid losing all data. + +Image: whonix-simplex-recommendation.jpg + +Language: English + +Date: updated regularly + +https://www.whonix.org/wiki/SimpleX + +## Private Messaging: Wikilibriste Recommendations + +(Les messageries privees) + +Wikilibriste + +Review + +This French privacy guide recommends SimpleX for users seeking a strict, intentionally confidential messaging model. It highlights SimpleX's unique reception addresses per contact, Double Ratchet encryption, incognito mode, and the absence of user identifiers, while noting that user IP remains visible to relay servers unless Tor is used. + +Image: wikilibriste-simplex-recommendation.jpg + +Language: French + +Date: updated regularly + +https://www.wikilibriste.fr/messagerie-vie-privee/ + +## Messenger Matrix + +Kuketz IT-Security Blog + +Comparison + +This comprehensive German messenger comparison table rates SimpleX on dozens of technical criteria. It notes SimpleX's decentralized architecture, post-quantum encryption, NaCl/Signal Protocol cryptography, and Trail of Bits security audit, but gives it a "very limited" recommendation and categorizes it as targeting advanced users rather than beginners. + +Image: kuketz-messenger-matrix.jpg + +Language: German + +Date: updated regularly + +https://www.messenger-matrix.de/messenger-matrix.html + +## Communicate Online in Complete Anonymity + +(SimpleX Chat: comunicare online in totale anonimato) + +Web Apps Magazine + +Article + +This Italian article presents SimpleX as a paradigm shift in private messaging, highlighting its lack of phone number requirements, decentralized relay system, and metadata protection. It positions the app for journalists, activists, and privacy-conscious users, while honestly noting the tradeoff of losing all contacts if the phone is lost without backups. + +Image: webappsmagazine-simplex-anonymity.jpg + +Language: Italian + +Date: Feb 15, 2026 + +https://webappsmagazine.blogspot.com/2026/02/simplex-chat-comunicare-online-in.html + + +## SimpleX Chat: A Privacy-Optimized Chat App - Why Telegram Users Are Leaving + +(SimpleX Chat, 개인 정보 보호에 최적화된 채팅 앱, 텔레그램 사용자들이 떠나는 이유) + +Billionnapkin + +Review + +This article frames SimpleX as a privacy-optimized alternative to Telegram, arguing users are migrating due to Telegram's changed data access policies following CEO Pavel Durov's legal issues. The tone is promotional but balanced, acknowledging concerns about potential misuse while endorsing SimpleX for journalists, activists, and privacy-conscious professionals. + +Image: billionnapkin-simplex-review.jpg + +Language: Korean + +Date: Oct 8, 2024 + +https://billionnapkin.com/simplex-chat/ + +## Telegram Alternative Apps: Stronger Anonymous Messenger Recommendations + +(텔레그램 대체 앱: 더 강력한 익명 메신저 추천) + +NetXHack + +Guide + +This Korean-language comparison of Telegram alternatives describes SimpleX as an identifier-free distributed framework that requires no phone numbers or account IDs. The article notes SimpleX's relay servers remain ignorant of senders and receivers, but acknowledges it prioritizes anonymity over user experience and is better suited for small groups needing secure communication. + +Image: netxhack-telegram-alternatives.jpg + +Language: Korean + +Date: Dec 17, 2025 (updated) + +https://netxhack.com/apps/foss/alternatives-to-telegram/ + +## Users Flocking to Telegram Alternative Apps + +(텔레그램 대안 앱으로 몰려가는 이용자들) + +eKoreaNews + +News + +This Korean news article reports on users flocking to Telegram alternatives, positioning SimpleX as the first messenger without user IDs. It notes SimpleX's decentralized architecture and mentions its early funding support from Jack Dorsey, while contextualizing the migration trend against Telegram's policy changes allowing authorities access to user data. + +Image: ekoreanews-telegram-alternatives.jpg + +Language: Korean + +Date: Oct 11, 2024 + +https://www.ekoreanews.co.kr/news/articleView.html?idxno=75746 + +## Vitalik Donates 128 ETH Each to Privacy Messaging Apps Session and SimpleX Chat + +(ヴィタリック、プライバシー重視のメッセージアプリ「Session」「SimpleX Chat」に各128ETHを寄付) + +New Economy / Gentosha + +News + +This Japanese article reports on Vitalik Buterin's donation of 128 ETH each to Session and SimpleX Chat on November 27, 2025. It quotes Buterin saying encrypted messaging is critical for digital privacy, and notes he identified permissionless account creation and metadata privacy as key development priorities, while acknowledging both apps have not yet achieved ideal user experience. + +Image: neweconomy-buterin-simplex.jpg + +Language: Japanese + +Date: Nov 2025 + +https://www.neweconomy.jp/posts/521981 + +## Ethereum Founder Makes Huge Donation to Messaging App + +(イーサリアム創設者、メッセージングアプリに巨額寄付) + +CRYPTO TIMES + +News + +This Japanese crypto news outlet reports on Vitalik Buterin's combined 256 ETH donation to Session and SimpleX Chat. It highlights SimpleX's approach of eliminating permanent user identifiers entirely, using QR codes and invitation links instead, with servers functioning as data conduits that maintain no information about who communicates with whom. + +Image: cryptotimes-buterin-simplex.jpg + +Language: Japanese + +Date: Dec 3, 2025 + +https://crypto-times.jp/news-ethereum-founder-makes-huge-donation-to-messaging-app/ + +## Anonymous Messaging App SimpleX Chat: Setup and Usage Guide + +(ID不要の匿名メッセージアプリ「SimpleX Chat」の使い方) + +VPN Taizen + +Guide + +This Japanese setup guide describes SimpleX as having the highest privacy protection among messaging apps, explaining its encrypted queue system where servers cannot know who communicates with whom. The reviewer provides detailed instructions for profiles, contact management, and Tor integration, but criticizes the interface as "terrible" with confusing design and iOS stability issues. + +Image: vpn-taizen-simplex-guide.jpg + +Language: Japanese + +Date: 2025 (updated) + +https://vpn-taizen.com/how_to_use_anonymous_messaging_app_simplex_chat_that_doesnt_require_id/ + +## Privacy-Focused Chat Tool SimpleX: First Impressions + +(プライバシー特化のチャットツール「SimpleX」を利用してみる) + +Kusaimara Blog + +Review + +This Japanese blog post from September 2022 shares first impressions of SimpleX as a privacy-focused chat tool available on mobile. The author finds it usable despite being in development and expresses cautious optimism, noting SimpleX later gained Japanese language support by June 2023. + +Image: kusaimara-simplex-first-impressions.jpg + +Language: Japanese + +Date: Sep 2022 + +https://kusaimara.net/2022/09/20 + +## Anonymous Chat Showdown: Session vs SimpleX + +(匿名チャット対決!Session対SimpleX) + +Kusaimara Blog + +Comparison + +This Japanese blog post compares Session and SimpleX across recognition, usability, and anonymity. It notes SimpleX avoids user IDs entirely using unique URLs for connections but requires manual Tor activation. The author concludes both tools suffer from limited adoption, arguing that messaging apps require network effects to be practical. + +Image: kusaimara-session-vs-simplex.jpg + +Language: Japanese + +Date: Jul 2024 + +https://kusaimara.net/2024/07/758 + +## Why I Recommend SimpleX Over Signal and Session + +(巷で話題のSignalやSessionではなくSimpleXをおすすめする理由) + +vpn53049 / Ameblo + +Review + +This Japanese blog post advocates for SimpleX over Signal and Session, citing its per-connection one-time IDs, forward secrecy, and post-quantum encryption. The author criticizes Session for lacking forward secrecy and using fixed permanent IDs, and questions Signal's leadership integrity, concluding SimpleX addresses privacy gaps present in both alternatives. + +Image: ameblo-vpn53049-simplex-recommend.jpg + +Language: Japanese + +Date: May 19, 2024 + +https://ameblo.jp/vpn53049/entry-12852836589.html + +## SimpleX's Revolutionary Idea + +(SimpleXの革命的なアイデアに心を打たれた) + +vpn53049 / Ameblo + +Article + +This Japanese blog post explains SimpleX's key innovation: generating unique IDs per conversation rather than using persistent anonymous IDs. The author argues this eliminates the social graph exposure risk inherent in competing apps and solves the practical frustration of managing multiple devices or accounts to maintain anonymity across different social contexts. + +Image: ameblo-vpn53049-simplex-revolutionary.jpg + +Language: Japanese + +Date: Sep 5, 2022 + +https://ameblo.jp/vpn53049/entry-12786598980.html + +## The Next Anonymous Messengers After Signal: Session and SimpleX Chat + +(Signalの次に代わる匿名メッセージアプリ・SessionとSimpleX Chat) + +renaro / note.com + +Comparison + +This Japanese article positions Session and SimpleX as the next anonymous messengers after Signal, whose mandatory phone number requirement is its main weakness. SimpleX is presented as the most anonymous option due to having no user ID whatsoever, though it requires the additional Orbot app for maximum privacy, making Session more convenient for baseline anonymity. + +Image: renaro-signal-session-simplex.jpg + +Language: Japanese + +Date: Nov 21, 2025 + +https://note.com/renaro/n/n8b3c9af1899f + +## SimpleX Chat: Next-Generation Secure Communication Tool Without Identity Verification + +(SimpleX Chat:无需身份识别的下一代安全通讯工具) + +Huluohu Blog + +Review + +This Chinese article introduces SimpleX Chat as a uniquely private messaging platform that requires no phone number, email, or username of any kind, using shared links instead to establish connections. It highlights SimpleX's core advantages: true anonymity where even the servers cannot know who is talking to whom, end-to-end encryption, decentralized server architecture, open-source transparency, and innovative use of unidirectional message queues with rotating random receive addresses to resist network analysis. + +Image: huluohu-simplex-review.jpg + +Language: Chinese + +Date: 2024 (estimated) + +https://www.huluohu.com/posts/1221/ + +## SimpleX Chat: A Private and Encrypted Open-Source Communication Tool Without Any User IDs + +(SimpleX Chat:一个私人且加密的开源通讯工具,没有任何用户 ID) + +Ababtools + +Review + +This article describes SimpleX Chat as an open-source messaging platform that does not rely on any user identifier, not even random numbers. It highlights Double Ratchet encryption, multiple chat profiles including hidden ones, encrypted voice messages and audio/video calls, secret group chats, and portable encrypted databases, with availability across Android, iOS, and desktop. + +Image: ababtools-simplex-review.jpg + +Language: Chinese + +Date: Jan 7, 2024 + +https://ababtools.com/?post=955 + +## Open-Source SimpleX Chat Succeeds Where Telegram Failed + +(开源 SimpleX Chat 成功弥补了 Telegram 的不足) + +Notebookcheck China + +News + +This Chinese Notebookcheck article argues SimpleX succeeds where Telegram failed on privacy. It highlights that SimpleX requires no phone numbers, uses incognito mode with auto-generated usernames, employs one-way onion routing, and allows users to set up their own servers. The article notes unlimited group sizes but acknowledges SimpleX currently lacks widespread adoption. + +Image: notebookcheck-cn-simplex.jpg + +Language: Chinese + +Date: Oct 3, 2024 + +https://www.notebookcheck-cn.com/SimpleX-Chat-Telegram.897088.0.html + +## Open-Source SimpleX Chat Succeeds Where Telegram Failed + +(O SimpleX Chat de codigo aberto tem sucesso onde o Telegram falhou) + +Notebookcheck Portugal + +News + +This Portuguese Notebookcheck article contrasts SimpleX with Telegram, emphasizing that SimpleX does not require phone numbers for registration, uses incognito mode and one-way onion routing, and offers end-to-end plus local device encryption. It positions SimpleX's open-source, decentralized approach against Telegram's conflicts with government agencies. + +Image: notebookcheck-pt-simplex.jpg + +Language: Portuguese + +Date: Oct 2024 + +https://www.notebookcheck.info/O-SimpleX-Chat-de-codigo-aberto-tem-sucesso-onde-o-Telegram-falhou.897026.0.html + +## Anonymous Messaging Apps + +(App di messaggistica anonime) + +Le Alternative + +Guide + +This Italian blog about alternative apps describes SimpleX Chat as an open-source, end-to-end encrypted messenger that has received an independent security audit. It emphasizes that the only way to add a contact is through QR code or link, requiring neither phone number nor email address, and notes availability on F-Droid, Play Store, and iOS. + +Image: lealternative-anonymous-apps.jpg + +Language: Italian + +Date: Dec 14, 2022 + +https://blog.lealternative.net/2022/12/14/app-di-messaggistica-anonime/ + +## SimpleX Chat + +Freeonline.org + +Review + +This Italian review awards SimpleX Chat "Site of the Day" status, describing it as a messaging platform that eliminates user identifiers entirely. It highlights advanced end-to-end encryption, local-only data storage, Tor network access, and availability across all major platforms, while noting the experience is more technical than competing services. + +Image: freeonline-simplex-review.jpg + +Language: Italian + +Date: Feb 17, 2026 (updated) + +https://www.freeonline.org/simplex-chat/ + +## 10 Most Secure Messaging Apps of 2024 + +(Le 10 app di messaggistica piu sicure del 2024) + +Moyens I/O Italy + +Comparison + +Image: moyens-io-secure-apps-2024.jpg + +Language: Italian + +Date: 2024 + +https://it.moyens.net/app/app-messaggistica-piu-sicure-del-signal-session-simplex/ + +## SimpleX Chat: The Hidden Portal of Privacy + +(SimpleX Chat - O Portal Oculto da Privacidade) + +Paranoia + +Review, Video + +Image: portuguese-simplex-hidden-portal.jpg + +Language: Portuguese + +Date: Oct 2024 + +https://www.youtube.com/watch?v=CCB9m0T7RIM + +## Anonymous Chat Without Phone and Email: Online Privacy With Ultra Metadata Annihilation + +(SIMPLEX: CHAT ANONIMO SEM TELEFONE E EMAIL / PRIVACIDADE ONLINE COM ULTRA ANIQUILACAO DE METADADOS) + +Leandroibov + +Review, Video + +Image: portuguese-simplex-ultra-annihilation.jpg + +Language: Portuguese + +Date: 2024 (estimated) + +https://www.youtube.com/watch?v=gvVfpPB1srI + +## How to Use SimpleX Private Chat Without Identification + +(Como usar o SimpleX chat privado sem identificacao) + +Prometheus HODL + +Guide, Video + +Image: portuguese-simplex-tutorial.jpg + +Language: Portuguese + +Date: 2024 (estimated) + +https://www.youtube.com/watch?v=JMPxptujnaQ + +## SimpleX Chat: Messaging Meets Perfect Privacy + +The Digital Prepper + +Review, Video + +Image: simplex-messaging-perfect-privacy.jpg + +Language: English + +Date: 2024 (estimated) + +https://www.youtube.com/watch?v=aKRfDch_WBQ + +## SimpleX Chat: Simple Messaging With Unusually Good Privacy + +Remember Lads Subscribe to Big Bear + +Review, Video + +Image: simplex-unusually-good-privacy.jpg + +Language: English + +Date: Dec 19, 2023 + +https://www.youtube.com/watch?v=zkEY9m2E-Y4 + +## Self-Hosted SimpleX Chat + +Simple Messaging With Unusually Good Privacy + +Guide, Video + +Image: selfhosted-simplex-tutorial.jpg + +Language: English + +Date: Feb 8, 2024 + +https://www.youtube.com/watch?v=1zMAGzYBgJY + +## Setting Up SimpleX As Your Private Messenger + +Daniel's Blog + +Guide + +This blog post walks through self-hosting a SimpleX SMP server with Traefik and Docker after the author abandoned WhatsApp over privacy concerns. It highlights SimpleX's invite-link-based connections and decentralized relays that do not reveal participant information, while honestly noting the desktop app requires an active smartphone connection and lacks a web interface. + +Image: xfuture-blog-simplex-setup.jpg + +Language: English + +Date: 2025 + +https://xfuture-blog.com/posts/setting-up-simplex-as-your-private-messenger/ + +## Showdown: Signal, Session, SimpleX, Matrix, XMPP, vs Briar + +Simplified Privacy + +Comparison + +This messenger showdown compares Signal, Session, SimpleX, Matrix, XMPP, and Briar, positioning SimpleX as "most likely to grow" due to its corporate APIs. It recommends SimpleX specifically for users who want to hide their communications, while noting it is rated as most vulnerable to psychological phishing among the messengers compared. + +Image: simplified-privacy-showdown.jpg + +Language: English + +Date: 2024 (estimated) + +https://simplifiedprivacy.com/messengers/ + +## SimpleX Network: Power to the People + +SimpleX Chat + +Livestream, Video + +Image: simplex-power-to-people-livestream.jpg + +Language: English + +Date: Feb 11, 2025 + +https://www.youtube.com/watch?v=Uez2mfVGU7s + +## Every Thing You Need to Know About SimpleX Chat + +Libre Self Hosted + +Guide + +This self-hosting directory describes SimpleX as the most private and secure chat platform, using pairwise per-queue identifiers instead of persistent user IDs. It notes the Trail of Bits security audit, AGPL-3.0 licensing, Haskell implementation, and Tor support, emphasizing that SimpleX protocols are and will remain open and in the public domain. + +Image: libreselfhosted-simplex-overview.jpg + +Language: English + +Date: 2024 (estimated) + +https://www.libreselfhosted.com/project/simplex-chat/ + +## Privacy First Steps + +Seth For Privacy + +Guide + +In this privacy guide by Seth for Privacy, SimpleX is recommended as a key step in the privacy journey for its protection of both message contents and metadata. The author notes SimpleX has become his preferred messenger over Signal, primarily because it eliminates the phone number requirement, and he references a detailed podcast interview with SimpleX founder Evgeny. + +Image: sethforprivacy-privacy-steps.jpg + +Language: English + +Date: 2024 (estimated) + +https://github.com/sethforprivacy/sethforprivacy.com/blob/master/content/posts/privacy-first-steps.md + +## Degoogle Your Private Life: Real-Time Messaging + +iode Blog + +Guide + +This iode tech blog article on degoogling covers SimpleX as a decentralized messaging alternative using unidirectional simplex queues. It praises SimpleX's open-source end-to-end encryption and censorship resistance, but identifies significant limitations: a very small user base, no remote contact discovery, slower message delivery, no multi-device support, and total data loss if the device is lost without backups. + +Image: iode-degoogle-messaging.jpg + +Language: English + +Date: 2024 (estimated) + +https://blog.iode.tech/degoogle-your-private-life-4-instant-messaging/ + +## SimpleX Chat v5.2: Message Delivery Receipts + +NoBs Bitcoin + +News + +This release announcement covers SimpleX Chat v5.2.0, highlighting new message delivery receipts with per-contact opt-out, conversation filtering by favorites and unread status, and group improvements including full context for replied messages. The developers emphasized their commitment to keeping SimpleX protocols open and in the public domain. + +Image: nobsbitcoin-v52-receipts.jpg + +Language: English + +Date: 2023 + +https://www.nobsbitcoin.com/simplex-chat-v5-2-0/ + +## SimpleX Server Now Available for StartOS + +NoBs Bitcoin + +News + +This article announces SimpleX Server availability on StartOS through the Start9 Registry. It explains SimpleX's server architecture where servers act as simple relays that do not store profiles, contacts, or groups, and each conversation typically uses two different servers - one chosen by each participant - with Tor server support built in. + +Image: nobsbitcoin-startos.jpg + +Language: English + +Date: 2023 + +https://www.nobsbitcoin.com/simplex-server-now-available-for-startos/ + +## SimpleX Chat + +Blog de Joselito + +Comparison + +This Spanish blog post acknowledges SimpleX offers superior privacy features over XMPP, including metadata protection and automatic encryption, but argues XMPP is not obsolete. The author identifies a vulnerability in SimpleX's group key distribution relying on administrators, and views SimpleX as promising but unproven long-term, using both platforms with XMPP as his primary messenger. + +Image: joselito-simplex-vs-xmpp.jpg + +Language: Spanish + +Date: Dec 5, 2023 + +https://joselito.mataroa.blog/blog/simplex-chat/ + +## SimpleX Chat Part 2 + +Blog de Joselito + +Review + +In this follow-up Spanish post, the author details SimpleX's technical strengths including metadata protection through unidirectional message queues, double-layer encryption, and automatic message deletion from servers after receipt. While primarily an XMPP user, the author calls SimpleX "an excellent messaging platform" and appreciates that robust security works automatically without user effort. + +Image: joselito-simplex-part2.jpg + +Language: Spanish + +Date: Dec 7, 2023 + +https://joselito.mataroa.blog/blog/simplex-chat-parte-2/ + +## 14 Best Secure Messengers of 2026 + +(14 лучших безопасных мессенджеров 2026) + +pro32.com + +Comparison + +This Russian article listing 14 secure messengers highlights SimpleX's unique architecture without user identifiers, where contacts are added via QR codes or invitation links. It identifies SimpleX as one of the most promising options for bypassing blockades, suitable for users prioritizing anonymity and censorship resistance. + +Image: pro32-best-messengers-2026.jpg + +Language: Russian + +Date: 2026 + +https://pro32.com/ru/article/14-samykh-bezopasnykh-messendzherov-kakoy-vybrat-dlya-lichnoy-i-rabochey-perepiski/ + +## Comparative Review of Protected Messengers + +(Сравнительный обзор защищенных мессенджеров) + +SecurityLab.ru + +Comparison + +This Russian comparative review of protected messengers describes SimpleX as a decentralized platform targeting privacy-conscious users who prioritize anonymity. It notes SimpleX's strengths in avoiding phone numbers and tracking, but identifies fewer features compared to Telegram or Signal, a potentially confusing interface, and a smaller user community as drawbacks. + +Image: securitylab-messenger-comparison.jpg + +Language: Russian + +Date: 2024 (estimated) + +https://www.securitylab.ru/blog/personal/SimlpeHacker/354153.php + +## SimpleX Chat Overview: What Is It? + +(Обзор SimpleX Chat: Что это такое?) + +DDPA.ru + +Review + +This Russian overview describes SimpleX as a privacy-focused messenger that assigns users no identifiers of any kind, ensuring complete anonymity. It highlights the hybrid P2P and federated architecture, end-to-end encryption resistant to relay server compromise, and positions SimpleX as ideal for journalists, activists, and cybersecurity enthusiasts. + +Image: ddpa-simplex-overview.jpg + +Language: Russian + +Date: 2024 (estimated) + +https://ddpa.ru/p/simplex-chat + +## Group P2P Chats and the First Messenger Without ID + +(Групповые P2P-чаты и первый мессенджер без ID) + +Habr / GlobalSign + +Article + +This Habr article examines group P2P messaging and SimpleX as the first messenger without user IDs, using temporary anonymous paired message queue identifiers unique to each connection. It details incognito mode, decentralized storage, dual-layer encryption, and Tor compatibility, contrasting SimpleX's approach with all existing messengers that rely on some form of user identification. + +Image: habr-globalsign-p2p-chats.jpg + +Language: Russian + +Date: Feb 2024 + +https://habr.com/ru/companies/globalsign/articles/792986/ + +## Safer Than Signal or Telegram? SimpleX Offers Absolute Privacy Without Creating a Personal ID + +(Bezpecnejsi nez Signal nebo Telegram? SimpleX nabidne absolutni soukromi bez vytvareni osobniho ID) + +Cnews.cz + +News + +This Czech tech news article presents SimpleX as safer than Signal or Telegram, highlighting its elimination of permanent user IDs in favor of temporary anonymous message identifiers deleted immediately after sending. It notes the project originated in 2020 and gained attention when Jack Dorsey endorsed it as potentially more secure than Signal or Telegram. + +Image: cnews-cz-simplex-privacy.jpg + +Language: Czech + +Date: May 27, 2023 + +https://www.cnews.cz/bezpecnejsi-nez-signal-nebo-telegram-simplex-nabidne-absolutni-soukromi-bez-nutnosti-vytvareni-osobniho-id/ + +## SimpleX Chat Is a Revolution in Encrypted Communication + +(SimpleX Chat je revoluci v sifrovane komunikaci) + +Kryptoanarchista.cz + +Review + +This Czech crypto-anarchist publication calls SimpleX a revolution in encrypted communication that surpasses Signal and Threema. It praises the temporary anonymous pairwise identifiers that prevent correlation attacks, and the phone-number-free setup via QR codes, while noting limitations in desktop availability and the need for an alternative channel to initiate contact. + +Image: kryptoanarchista-simplex-revolution.jpg + +Language: Czech + +Date: Aug 17, 2023 + +https://kryptoanarchista.cz/simplex-chat-je-revoluci-v-sifrovane-komunikaci/ + +## Overview and Comparison of Encrypted Communication Tools + +(Prehlad a porovnanie sifrovanych komunikacnych nastrojov - messengerov) + +Juraj Bednar + +Comparison + +In this Slovak overview of encrypted communication tools, Juraj Bednar describes SimpleX as the youngest application in the comparison, using asymmetric connections via QR codes with no user identifiers. He notes basic functionality including audio and video calls works, but advises waiting before using it for family or business communication due to ongoing development and some technical issues. + +Image: bednar-encrypted-messengers.jpg + +Language: Slovak + +Date: Apr 5, 2022 + +https://juraj.bednar.io/blog/2022/04/05/sifrovane-komunikacne-nastroje-prehlad/ + +## Encrypted Communication Between Programs and Mobile Devices + +Juraj Bednar + +Guide + +Juraj Bednar chose SimpleX as his preferred solution for sending encrypted notifications between programs and mobile devices, valuing its lack of account creation requirements and absence of spam. He uses helper functions to send messages and files from scripts, while noting SimpleX still has bugs and does not guarantee delivery if the program finishes before transmission completes. + +Image: bednar-encrypted-notifications.jpg + +Language: English + +Date: Nov 24, 2024 + +https://juraj.bednar.io/en/blog-en/2024/11/24/encrypted-communication-between-programs-and-mobile-devices/ + +## Session and SimpleX: Encrypted Messenger Comparison + +Michal Kodnar + +Comparison + +This comparison characterizes SimpleX as the first identifier-free messenger, using temporary anonymous paired message queues instead of user IDs. The author finds both Session and SimpleX are interesting projects but notes SimpleX remains buggy and incomplete, requiring QR codes for new connections and lacking disappearing messages at the time of writing. + +Image: kodnar-session-simplex.jpg + +Language: English + +Date: Oct 2023 + +https://michalkodnar.xyz/blog-en/culture-en/session-and-simlex-encrypted-messenger-comparison/ + +## Secure and Decentralized Chat Without Phone Number or Accounts + +(Chat securizat si descentralizat, fara numar de telefon sau conturi, cu aplicatia de mesagerie SimpleX Chat) + +Romica Prihor + +Guide + +This Romanian blog post promotes SimpleX as a revolution in secure and decentralized messaging without phone numbers or accounts. It provides installation instructions for Android, iOS, and desktop, and covers group management through invitation links with moderation features, positioning SimpleX as ideal for those prioritizing communication privacy without compromises. + +Image: prihor-simplex-guide.jpg + +Language: Romanian + +Date: Feb 2025 + +https://romicaprihor.blogspot.com/2025/02/simplex-chat-revolutia-mesageriei.html + +## New Super-Secure Messenger SimpleX Chat: Advantages and Disadvantages + +(Novyi super-zakhyshchenyi mesendzhder SimpleX Chat: perevahy y nedoliky) + +Kostiantyn Korsun / Censor.net + +Review + +This Ukrainian blog post reviews SimpleX Chat's advantages and disadvantages, noting it requires no phone number, email, or any user identifier - a feature rare among messengers. The author highlights that SimpleX stores all data locally with no cloud storage, supports one-time profile links, an incognito mode with random display names per contact, and invitation-only group chats, while also being fundamentally different from Signal, WhatsApp, Threema, Wire, and Session which all rely on some form of user ID. + +Image: korsun-simplex-expert-review.jpg + +Language: Ukrainian + +Date: 2023 + +https://censor.net/ua/blogs/3525466/novyyi-super-zahyschenyyi-mesendjer-simplex-chat-perevagy-yi-nedoliky + +## In Search of a Secure Messenger + +(U poshukakh bezpechnoho mesendzhera) + +KR. Labs Research + +Guide + +This Ukrainian guide to secure messaging describes SimpleX as a decentralized P2P messenger where users own their own servers, requiring no phone numbers or usernames for registration. It highlights end-to-end encryption by default with no metadata collection, positioning SimpleX as an advanced privacy solution appealing to activists and journalists. + +Image: kr-labs-secure-messenger.jpg + +Language: Ukrainian + +Date: Oct 24, 2023 + +https://research.kr-labs.com.ua/secure-and-privacy-messaging-apps-guide/ + +## SimpleX.Chat Is a Chat Network That Preserves Metadata Privacy + +(SimpleX.Chat e chat mrezha, koyato zapazva poveritelnostta na metadannite) + +Hristo Hristov / Medium + +Article + +This Bulgarian Medium article explains SimpleX as a decentralized client-server network that routes messages through disposable nodes while maintaining sender and receiver anonymity. It details the dual end-to-end encryption layers, DNS independence, and the absence of any global user identities, noting that network connections can only be discovered through observation of IP packet timing. + +Image: hristov-simplex-metadata.jpg + +Language: Bulgarian + +Date: Jun 5, 2022 + +https://hristo-hristov.medium.com/simplex-chat-%D0%B5-%D1%87%D0%B0%D1%82-%D0%BC%D1%80%D0%B5%D0%B6%D0%B0-%D0%BA%D0%BE%D1%8F%D1%82%D0%BE-%D0%B7%D0%B0%D0%BF%D0%B0%D0%B7%D0%B2%D0%B0-%D0%BF%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D0%B5%D0%BB%D0%BD%D0%BE%D1%81%D1%82%D1%82%D0%B0-%D0%BD%D0%B0-%D0%BC%D0%B5%D1%82%D0%B0%D0%B4%D0%B0%D0%BD%D0%BD%D0%B8%D1%82%D0%B5-eb31243435a6 + +## Best Encrypted Chat Apps for 2025 + +(Nay-dobrite prilozheniya za kriptiran chat na 2025) + +Questona.com + +Comparison + +This Bulgarian article on encrypted chat apps describes SimpleX as suitable for additional privacy, claiming greater anonymity than Briar because it uses no ID numbers. It notes user names change constantly in group chats and supports disappearing messages, but flags an extremely small user base and server reliability issues as significant practical drawbacks. + +Image: questona-encrypted-chat.jpg + +Language: Bulgarian + +Date: Jan 29, 2025 + +https://questona.com/kriptiran-chat/ + +## SimpleX: Communication Client for Truly Secure and Anonymous Communication + +(SimpleX - komunikacijski klijent za stvarno sigurnu i anonimnu komunikaciju) + +Bug.hr + +Review + +This Croatian tech publication describes SimpleX as a highly secure, decentralized messaging app using Double Ratchet encryption with no user identifiers and local-only data storage. It highlights anonymous profiles, self-destructing messages, voice and video calls, and self-hosting capability, with one commenter noting it is "really top for privacy." + +Image: bug-hr-app-of-day.jpg + +Language: Croatian + +Date: Feb 2, 2024 + +https://www.bug.hr/appdana/simplex-komunikacijski-klijent-za-stvarno-sigurnu-i-anonimnu-komunikaciju-38082 + +## SimpleX Chat: An Open and Secure Chat + +(SimpleX Chat, en oppen och saker chatt) + +Oppet Moln + +Article + +This Swedish article introduces SimpleX as an open and secure chat that requires no global identity such as phone number, username, or IP address. It claims superior security compared to Signal, XMPP/Matrix, and other P2P protocols, while acknowledging that users must evaluate the security claims themselves and that usability tradeoffs exist. + +Image: oppet-moln-simplex.jpg + +Language: Swedish + +Date: May 12, 2022 + +https://oppetmoln.se/20220512/simplex-chat-en-oppen-och-saker-chatt/ + +## Top 10 Secure Messaging Platforms to Replace Telegram in Vietnam 2025 + +(Top 10 Nen Tang Nhan Tin Bao Mat Thay The Telegram Tai Viet Nam 2025) + +TuDongChat + +Article + +This Vietnamese article lists SimpleX as the first decentralized, open-source messaging platform that eliminates user identification entirely, requiring no phone number, email, or personal identifier. It positions SimpleX as the highest-security option among Telegram alternatives for Vietnamese users, ideal for journalists, activists, and anyone requiring no digital footprint. + +Image: tudongchat-simplex-vietnam.jpg + +Language: Vietnamese + +Date: May 26, 2025 + +https://tudongchat.com/blog/nen-tang-nhan-tin-bao-mat/ + +## SimpleX: The Safest Instant Messaging App? + +Free.com.tw + +Review + +This Taiwanese article introduces SimpleX as a privacy-focused messaging app launched in 2020 that requires no phone number or email, using decentralized networking instead of a single server. It notes the spam prevention benefit of link-based contact sharing and mentions the desktop version requires smartphone pairing, while flagging the small user base and lack of Traditional Chinese localization. + +Image: free-com-tw-simplex.jpg + +Language: Chinese (Tr.) + +Date: Mar 26, 2025 + +https://free.com.tw/simplex/ + +## SimpleX: Comparison of Secure Messaging Platforms + +FutaGuard + +Review + +This Chinese comparison of secure messaging platforms gives SimpleX the most favorable assessment, noting it supports message deletion, disappearing messages, channels, and bots. The author calls it "currently the only one with some promise" among privacy-focused alternatives to Telegram, praising its customizable relay servers while noting ongoing cross-device synchronization challenges. + +Image: futa-gg-simplex-comparison.jpg + +Language: Chinese (Tr.) + +Date: 2024 + +https://blog.futa.gg/1/simple-x/ + +## SimpleX Chat: Privacy Without Compromise + +(SimpleX Chat - prywatnosc bez kompromisow) + +opentech.guru + +Review + +This Polish article describes SimpleX as a fully open-source, decentralized messenger that eliminates user identifiers entirely, using separate one-way message queues per contact. It highlights double ratchet and post-quantum key exchange encryption, incognito mode, Tor connectivity, and availability across all major platforms including CLI, calling the servers "dumb pipes" with no knowledge of who connects with whom. + +Image: opentech-guru-simplex.jpg + +Language: Polish + +Date: Feb 2026 + +https://opentech.guru/simplex-chat-prywatnosc-bez-kompromisow/ + +## SimpleX and Matrix Are the Best Messengers. Period. + +(SimpleX i Matrix to najlepsze komunikatory. Kropka.) + +Programista Dla Pasji + +Comparison + +This Polish article evaluates multiple messengers, praising SimpleX for its decentralization where servers function merely as message relays, requiring no personal data. However, the author ultimately chooses Matrix over SimpleX for daily use due to Matrix's superior multi-device support, while acknowledging SimpleX's elegant simplicity and decentralized design. + +Image: programista-pasji-simplex.jpg + +Language: Polish + +Date: Sep 5, 2024 + +https://programistadlapasji.pl/simplex-i-matrix-to-najlepsze-komunikatory-kropka/ + +## Privacy Redefined: First Messenger Without User IDs + +(Prywatnosc zdefiniowana na nowo - Pierwszy komunikator bez identyfikatorow uzytkownikow) + +CONEA + +Article + +Image: conea-simplex-privacy.jpg + +Language: Polish + +Date: 2024 (estimated) + +http://conea.pl/aktualnosci/Prywatnosc-zdefiniowana-na-nowo---Pierwszy-komunikator-bez-identyfikator%C3%B3w-uzytkownik%C3%B3w-(ID)_158 + +## The Only TRULY Anonymous Chat: SimpleX + +(L'unica chat VERAMENTE anonima | SimpleX) + +rdwei + +Review, Video + +Image: italian-youtube-anonymous-chat.jpg + +Language: Italian + +Date: Apr 2026 + +https://www.youtube.com/watch?v=Eamu0Ys63l4 + +## SimpleX Chat Video Review + +PeerTube Uno Italia + +Review, Video + +Video review of SimpleX Chat v5.4 on Italian federated PeerTube instance. Covers connecting mobile and desktop apps via quantum-resistant protocol and improved group features. + +Image: peertube-uno-simplex.jpg + +Language: Italian + +Date: Mar 8, 2026 + +https://peertube.uno/w/ecD1N1HjNC4SBvmWusnTC2 + +## Security in a Box: Communication Tools + +(Ilgili Araclar - Communication Tools) + +Security in a Box / Front Line Defenders + +Guide + +This Turkish-language page from Security in a Box, a digital security guide, lists SimpleX Chat as a free, open-source secure messaging application. It notes SimpleX's decentralized network, lack of phone number requirements, absence of fixed user identifiers, end-to-end encryption by default, disappearing messages, and the Trail of Bits security assessment. + +Image: securityinabox-simplex-turkish.jpg + +Language: Turkish + +Date: 2024 + +https://securityinabox.org/tr/communication/tools/ + +## SimpleX for Iran + +Paskoocheh / ASL19 + +Review + +Paskoocheh, a platform providing circumvention tools for Iranian users, offers SimpleX Chat for Android download. The listing describes SimpleX as a privacy-focused messenger with end-to-end encryption, decentralized architecture, and anonymous communication capabilities, noting it is particularly useful for civil activists, journalists, and anyone requiring confidentiality. + +Image: paskoocheh-simplex-iran.jpg + +Language: Farsi + +Date: May 20, 2025 + +https://paskoocheh.com/tools/839/android.html + +## Most Secure Messaging Apps in the World + +Plaza.ir + +Comparison + +Image: plaza-ir-secure-messaging.jpg + +Language: Farsi + +Date: 2024 + +https://www.plaza.ir/241420/best-encrypted-messaging-apps + +## Vitalik Donates 128 ETH Each to Session and SimpleX, Supporting Privacy Communication Development + +(Vitalik jin chen xuan bu xiang Session he SimpleX ge juan zeng 128 ETH) + +TechFlow + +News + +This Chinese crypto newsletter reports on Vitalik Buterin donating 128 ETH each to Session and SimpleX Chat, emphasizing that encrypted communication is crucial for protecting digital privacy. It notes Vitalik identified permissionless account creation and metadata privacy as key priorities, while acknowledging that decentralization, multi-device support, and Sybil/DoS resistance remain significant technical challenges. + +Image: techflow-vitalik-simplex.jpg + +Language: Chinese + +Date: Nov 27, 2025 + +https://www.techflowpost.com/newsletter/detail_106673.html + +## Donating 256 ETH: Vitalik's Bet on Privacy Communication - Why Session and SimpleX? + +(Juan zeng 256 ETH, Vitalik ya zhu yin si tong xun: wei shen me shi Session he SimpleX?) + +BlockBeats + +News + +This Chinese crypto outlet analyzes Vitalik's 256 ETH donation to Session and SimpleX, explaining SimpleX's radical approach of using one-directional message queues with no global user IDs. It contrasts Session's Web3 token model with SimpleX's rejection of tokenization, and notes the donation was timed one day after the EU's Chat Control proposal threatening end-to-end encryption. + +Image: blockbeats-vitalik-simplex.jpg + +Language: Chinese + +Date: Nov 2025 + +https://www.theblockbeats.info/news/60368 + +## I Used Anonymous Chat Tools for a Week: These 3 Are Truly Safe + +(Wo yong le yi zhou ni ming liao tian gong ju, fa xian zhe 3 ge cai shi zhen zheng de an quan) + +Zhihu + +Review + +This Chinese article reviews three anonymous messaging tools - Session, SimpleX Chat, and TWT - from a week-long hands-on test. SimpleX is praised for its zero-metadata design where even the server cannot know who is communicating, with support for text, voice, and groups, but criticized for requiring manual connection-code sharing and a developer-oriented interface that makes onboarding friends difficult. + +Image: zhihu-anonymous-chat-tools.jpg + +Language: Chinese + +Date: 2025 + +https://zhuanlan.zhihu.com/p/1916814606019584862 + +## Truly Secure and Anonymous Social Communication Tools + +极客小白 + +Guide, Video + +Image: chinese-youtube-secure-tools.jpg + +Language: Chinese + +Date: 2024 + +https://www.youtube.com/watch?v=UXH6wUOqnfk + +## SimpleX Chat: Decentralized Privacy Messaging App Without User Identifiers + +(SimpleX Chat - wu xu yong hu biao shi fu de qu zhong xin hua yin si xiao xi ying yong) + +Kaiyuanapp.cn + +Review + +This Chinese open-source app directory describes SimpleX as a decentralized privacy messaging app that operates without any form of user identifiers. It highlights the SimpleX Messaging Protocol with relay servers that only store encrypted messages temporarily, self-hosting capability, disappearing messages, and anonymous group chats, while noting limitations in voice/video calling, occasional messaging delays, and high Android battery consumption. + +Image: kaiyuanapp-simplex.jpg + +Language: Chinese + +Date: Apr 22, 2025 + +https://kaiyuanapp.cn/simplex-chat-%E6%97%A0%E9%9C%80%E7%94%A8%E6%88%B7%E6%A0%87%E8%AF%86%E7%AC%A6%E7%9A%84%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E9%9A%90%E7%A7%81%E6%B6%88%E6%81%AF%E5%BA%94%E7%94%A8/ + +## Pavol Luptak on Censorship, Security, and Nomadism + +(SP21 Pavol Luptak o cenzure, bezpecnosti, nomadstvi) + +Stackuj.cz Podcast + +Podcast + +Image: stackuj-luptak-podcast.jpg + +Language: Czech + +Date: May 22, 2022 + +https://www.youtube.com/watch?v=N0prtSOyeUU + +## Kostiantyn Korsun: Zaluzhnyi and Messengers + +(Kostyantyn Korsun: Zaluzhnyy i mesendzhery) + +Tverezo.info + +Article + +This Ukrainian article, written by Kostyantyn Korsun, discusses General Zaluzhny's essay on technology in modern warfare and the Ukrainian military's widespread reliance on Signal for encrypted communications despite formal prohibitions. While focused on Signal's role in military contexts and the US Defense Secretary's controversy over using Signal for classified data, the article addresses the broader topic of encrypted messengers in sensitive operational environments. + +Image: tverezo-korsun-zaluzhnyi.jpg + +Language: Ukrainian + +Date: 2025 + +https://tverezo.info/post/205151 + +## Top 10 Most Secure Messaging Apps in 2024 + +(Top 10 mest sikre besked-apps i 2024) + +Moyens I/O Denmark + +Comparison + +Image: moyens-dk-secure-apps.jpg + +Language: Danish + +Date: 2024 + +https://dk.moyens.net/apps/top-mest-sikre-besked-apps-signal-session-simplex/ + +## SimpleX Chat Tutorial Part 2: Fun Features + +(SimpleX Chat jian yi jiao cheng di er dan) + +BHB Community + +Guide + +This Chinese tutorial covers SimpleX Chat setup and advanced features including post-quantum encryption, database password creation, server provider selection, security code verification, and group management. It notes SimpleX has no message recall function except for group admins, voice/video calls require VPN access, and file sizes are limited to 1GB. + +Image: bhb-simplex-tutorial-2.jpg + +Language: Chinese + +Date: Mar 20, 2025 + +https://boyshelpboys.com/thread-6844.htm + +## Vitalik Donated 256 ETH to 2 Chat Apps You've Never Heard Of - What's He Betting On? + +(Vitalik juan le 256 ge ETH gei 2 ge ni mei ting guo de liao tian ruan jian, dao di zai ya zhu shen me?) + +BlockWeeks + +News + +This Chinese crypto article analyzes Vitalik's donation of 128 ETH each to Session and SimpleX, timed strategically one day after the EU's Chat Control proposal. It contrasts SimpleX's rejection of tokenization with Session's Web3 SESH token model, and notes Session's token surged over 450% following the announcement while SimpleX views speculation as counterproductive to privacy goals. + +Image: blockweeks-vitalik-simplex.jpg + +Language: Chinese + +Date: Nov 28, 2025 + +https://blockweeks.com/article/189510 + +## Looking Back at 2025: Top 10 Influential Figures in the Crypto Industry + +(Hui wang 2025: ying xiang jia mi hang ye de shi da nian du feng yun ren wu) + +Tencent News + +News + +This Chinese article reviewing the top 10 influential figures in the crypto industry for 2025 mentions SimpleX briefly in the context of Vitalik Buterin donating 128 ETH each to Session and SimpleX Chat. It quotes Vitalik emphasizing that digital privacy protection through encrypted messaging is crucial, citing permissionless account creation and metadata privacy as key development directions. + +Image: tencent-news-crypto-2025.jpg + +Language: Chinese + +Date: Dec 22, 2025 + +https://news.qq.com/rain/a/20251222A01LTC00 + +## SimpleX Chat: Next-Generation Secure Communication Tool + +Zhousa.com + +Review + +This Chinese article presents SimpleX as a next-generation secure communication tool with an identity-free design requiring no phone number, email, or username. It explains the single-direction message queue architecture with rotating receiver addresses, and positions the platform for ordinary users, journalists, activists, and anyone valuing privacy. + +Image: zhousa-simplex-review.jpg + +Language: Chinese + +Date: 2024 + +https://www.zhousa.com/archives/60915.html + +## From Privacy to Social: Web3 Needs an All-in-One Encrypted Social Application + +(Cong yin si dao she jiao: Web3 xu yao yi zhan shi jia mi she jiao ying yong) + +Golden Finance / KasTop + +News + +This Chinese article about Web3 encrypted social applications positions SimpleX as an important advancement in decentralized private communication, noting Vitalik's 128 ETH donation. While praising SimpleX's end-to-end encryption and lack of user identifiers, the article argues that comprehensive platforms combining privacy with full Web3 social features represent the next evolutionary stage beyond SimpleX. + +Image: golden-finance-web3-privacy.jpg + +Language: Chinese + +Date: Dec 17, 2025 + +https://www.kastop.com/Item/1995.aspx + +## SimpleX in Test + +(SimpleX im Test) + +Bitcoinlighthouse.de + +Review + +This German review tests SimpleX and explains how it fundamentally differs from other messengers by using no user IDs at all - not even random numbers - to protect metadata privacy. The article describes how SimpleX uses per-contact message queue identifiers instead of user identifiers, and highlights the incognito mode which assigns a different display name for each contact, preventing even contacts from proving they communicate with the same person. + +Image: bitcoinlighthouse-simplex-test.jpg + +Language: German + +Date: 2024 + +https://bitcoinlighthouse.de/privacy/simplex-im-test/ + +## German Language Pack for Mobile Privacy Messaging Service SimpleX + +(Jetzt auch auf Deutsch anonym chatten: Update fuer Messenger SimpleX) + +Heise Online + +News + +Heise, a major German tech publication, reports on SimpleX version 4.0 adding a German language pack. The update also introduced encryption of received and stored messages using SQLCipher, a TypeScript SDK for chatbot integration, and self-hosted WebRTC ICE server support. The article notes the founder was seeking donations for a $20,000 independent security audit. + +Image: heise-german-language-simplex.jpg + +Language: German + +Date: 2022 + +https://www.heise.de/news/Deutsches-Sprachpaket-fuer-den-mobilen-Privatsphaere-Nachrichtendienst-SimpleX-7278902.html + +## SimpleX 1.0.0: Decentralized, Privacy-Respecting and Encrypted Chat + +(SimpleX 1.0.0: Dezentraler, Privatsphaere achtender und verschluesselter Chat) + +Tarnkappe.info + +Community + +This German security forum post announces SimpleX's stable 1.0.0 release, describing its two-layer end-to-end encryption with double-ratchet algorithm and unidirectional simplex queues with unique encryption keys per queue. It emphasizes that establishing secure channels requires exchanging encryption keys through QR codes or invitations, and notes the terminal client's availability plus a DigitalOcean one-click server deployment option. + +Image: tarnkappe-simplex-1-0.jpg + +Language: German + +Date: Jan 13, 2022 + +https://tarnkappe.info/forum/t/simplex-1-0-0-dezentraler-privatsphaere-achtender-und-verschluesselter-chat/9812 + +## SimpleX Chat Overview + +GNU/Linux.ch + +Article + +This GNU/Linux-focused Swiss German article explains SimpleX's architecture as a decentralized network using one-way nodes for asynchronous message forwarding, avoiding any form of identity for message routing. It details two layers of end-to-end encryption with forward secrecy, and notes that servers do not retain user records, do not communicate with each other, and have no way to obtain a complete list of participating servers. + +Image: gnulinux-ch-simplex-overview.jpg + +Language: German + +Date: 2022 + +https://gnulinux.ch/simplex-chat + +## New Service: SimpleX Chat Server + +AdminForge + +Service + +AdminForge, a German community infrastructure provider, announces hosting a new SimpleX Chat server at simplex.adminforge.de. It describes SimpleX's Double-Ratchet encryption, multiple profile support, anonymous group participation, and explains that SMP servers function only as relays holding messages until recipients reconnect while XFTP file transfer servers retain uploads temporarily. + +Image: adminforge-simplex-server.jpg + +Language: German + +Date: Oct 4, 2023 + +https://adminforge.de/tools/neuer-service-simplex-chat-server/ + +## Privacy Handbook: SimpleX + +(Datenschutz-Handbuch) + +Privacy-Handbuch.de + +Guide + +This German privacy handbook describes SimpleX as using an innovative metadata-avoidance approach with no account IDs, where encrypted sessions are established directly between clients and servers merely route data packets. It notes SimpleX is particularly suited for concealing contact with specific individuals, though adoption remains limited for everyday use. + +Image: privacy-handbuch-simplex.jpg + +Language: German + +Date: 2024 + +https://www.privacy-handbuch.de/handbuch_89.htm + +## Signal's Brothers: Choosing From Five Most Private and Protected Messengers + +(Bratya Signal. Vybiraem iz pyati naibolee privatnykh i zashchishchyonnykh messendzherov) + +Xakep.ru + +Review + +This Russian security magazine calls SimpleX "the most interesting messenger in this selection, and also the most mysterious." The article highlights its anonymous registration requiring no phone number, federated architecture allowing user-hosted relay servers, and modern functionality including audio/video calls and disappearing messages. + +Image: xakep-simplex-signal-brothers.jpg + +Language: Russian + +Date: Aug 27, 2024 + +https://xakep.ru/2024/08/27/5-private-messengers/ + +## Decentralized Messengers: Choosing the Most Secure Way to Communicate + +(Detsentralizovannyye messendzhery: vybiraem samyj bezopasnyj sposob obshcheniya) + +SecurityLab.ru + +Guide + +This Russian security analysis categorizes SimpleX Chat as a decentralized and anonymous messenger prioritizing minimalism and maximum security. It notes SimpleX provides a high level of anonymity and eliminates metadata exposure, but acknowledges its restricted functionality and relatively low user awareness. + +Image: securitylab-decentralized-messengers.jpg + +Language: Russian + +Date: Sep 1, 2024 + +https://www.securitylab.ru/analytics/551634.php + +## WhatsApp and Telegram Alternative: Full Guide to Decentralized Chats + +(Alternativa WhatsApp i Telegram: polnyj gid po detsentralizovannym chatam) + +SecurityLab.ru + +Guide + +This Russian guide to decentralized chat alternatives describes SimpleX as using temporary anonymous message queue identifiers instead of traditional accounts, with no phone numbers or emails required. It notes the unfamiliar interaction model may confuse newcomers and potential delivery delays exist, but ranks SimpleX highly for anonymity and metadata protection. + +Image: securitylab-whatsapp-alternative.jpg + +Language: Russian + +Date: Aug 14, 2025 + +https://www.securitylab.ru/analytics/562397.php + +## Classification of Secure Messengers: New Projects + +(Klassifikatsiya zashchishchyonnykh messendzherov. Novyye proyekty) + +Habr / GlobalSign + +News + +This Habr article classifies SimpleX Chat as an experimental, "extremely privacy-focused" messenger for enthusiasts, describing it as the first messenger without user IDs of any kind. It identifies SimpleX as one of only two messengers (alongside Briar) that meets all security criteria on the author's evaluation scale. + +Image: habr-globalsign-classification.jpg + +Language: Russian + +Date: Feb 27, 2023 + +https://habr.com/ru/companies/globalsign/articles/719330/ + +## Alternatives to Signal and Telegram: Which Secure Messenger to Use Now? + +(Alternativy Signal i Telegram: kakoj bezopasnyj messendzher ispol'zovat' teper'?) + +hi-tech.mail.ru + +Review + +This Russian tech review highlights SimpleX's complete anonymity through having no telephone numbers or identifiers, and describes its support for audio/video calls, file transfer, disappearing messages, and personal relay servers. It notes all information is stored exclusively on user devices, with messages only temporarily held on relay servers during delivery. + +Image: hi-tech-mail-simplex.jpg + +Language: Russian + +Date: Sep 9, 2024 + +https://hi-tech.mail.ru/review/114266-alternativy-signal-i-telegram-kakoj-bezopasnyj-messendzher-ispolzovat/ + +## SimpleX Chat Review + +te-st.org + +Review + +This review by Russian digital rights organization Te-st characterizes SimpleX Chat as one of the first messengers where the list of missing security features is notably short. It emphasizes that the absence of UserID means users cannot be identified afterward, and unlike most secure messaging apps, no phone number is required. + +Image: te-st-simplex-review.jpg + +Language: Russian + +Date: Aug 21, 2023 + +https://te-st.org/2023/08/21/simplex-chat-review/ + +## VC.ru Article on SimpleX + +VC.ru + +Article + +This Russian user review describes SimpleX as having clear, high-quality voice and video calls, but identifies significant practical limitations including slow media file delivery, compressed photos, and sluggish video uploads. The author expresses skepticism about Microsoft's involvement with the project and concludes that the messenger functions more like a calling app than a complete communication platform. + +Image: vc-ru-simplex.jpg + +Language: Russian + +Date: 2024 + +https://vc.ru/id2160811/779184 + +## RuTube: SimpleX Chat Video + +RuTube + +Review, Video + +This is the second video in a Russian-language series about SimpleX Chat, demonstrating its primary functionality including messaging, encrypted voice and video calls, automatic message deletion timers, incognito mode, and database backup capabilities. + +Image: rutube-simplex-video.jpg + +Language: Russian + +Date: 2024 + +https://rutube.ru/video/b3dcb8869291d7de55596392c05aa24c/ + +## SimpleX: Messaging Proof Against the Curious + +(SimpleX: La mensajeria a prueba de curiosos) + +Francisco Barral + +Article + +This Spanish article explains that SimpleX Chat uses no user identifiers and instead generates unique, temporary addresses for each connection. It describes the app's use of end-to-end encryption via the Signal Protocol, peer-to-peer connections where possible, and privacy features including disappearing messages, multiple chat profiles, and incognito mode. + +Image: franciscobarral-simplex.jpg + +Language: Spanish + +Date: 2024 + +https://franciscobarral.es/simplex-la-mensajeria-a-prueba-de-curiosos/ + +## SimpleX Chat: Secure Messaging and Decentralized Communities + +(SimpleX Chat: Mensajeria segura y comunidades descentralizadas) + +El Ecosistema Startup + +Article + +This Spanish startup-focused article presents SimpleX as a fully decentralized messaging platform with independent security audits completed in 2022 and 2024. It highlights practical applications for startup founders and community managers who need confidential discussions without depending on centralized platforms, with availability across iOS, Android, macOS, Linux, and Windows. + +Image: ecosistemastartup-simplex.jpg + +Language: Spanish + +Date: 2024 + +https://ecosistemastartup.com/simplex-chat-mensajeria-segura-y-comunidades-descentralizadas/ + +## Secure Messaging Apps: Complete Technical Analysis + +(Apps de Mensajeria Seguras: Analisis Tecnico Completo) + +EsGeeks + +Comparison + +This Spanish technical analysis describes SimpleX's approach to radical metadata reduction through ephemeral addresses and message queues specific to each contact, preventing reconstruction of social graphs. It rates SimpleX as suitable for users with strict privacy models and advanced technical knowledge, noting that while IP addresses remain visible to relay servers, using Tor resolves this. + +Image: esgeeks-most-secure-app.jpg + +Language: Spanish + +Date: 2024 + +https://esgeeks.com/app-mensajeria-mas-segura/ + +## Most Secure Decentralized Messengers + +(Mensajeros descentralizados mas seguros) + +EsGeeks + +Comparison + +This Spanish article on secure decentralized messengers categorizes SimpleX Chat as focusing on minimalism and maximum security, with a high level of anonymity and absence of metadata. It notes SimpleX suffers from limited functionality and low popularity compared to other messaging platforms. + +Image: esgeeks-decentralized-messengers.jpg + +Language: Spanish + +Date: 2024 + +https://esgeeks.com/mensajeros-descentralizados-mas-seguros/ + +## SimpleX Chat: The Messaging App + +(SimpleX Chat: la app de mensajeria segura sin identificadores de usuario) + +Computekni + +Review + +This Spanish article describes SimpleX Chat as the first messaging platform without user identifiers of any kind, using end-to-end encryption and QR codes for private connections. It highlights the app's availability on iOS, Android, and F-Droid, and notes its open-source codebase allows public inspection and quick resolution of security issues. + +Image: computekni-simplex-chat.jpg + +Language: Spanish + +Date: May 2023 + +https://www.computekni.com/2023/05/simplex-chat-la-app-de-mensajeria.html + +## This Is How This Secure Messaging App Works Without User Identifiers + +(Asi funciona esta app de mensajeria segura que carece de identificadores para los usuarios) + +WWWhatsnew + +News + +This Spanish tech news site explains that SimpleX Chat delivers messages without using sender or recipient identifiers, relying on the SimpleX Messaging Protocol (SMP) with persistent queues. It describes how users generate unique invitation codes for each contact, with messages stored directly on devices rather than centralized servers. + +Image: wwwhatsnew-simplex.jpg + +Language: Spanish + +Date: Aug 13, 2022 + +https://wwwhatsnew.com/2022/08/13/asi-funciona-esta-app-de-mensajeria-segura-que-carece-de-identificadores-para-los-usuarios/ + +## Telegram Privacy Alternatives + +(Conoce las alternativas a Telegram de mensajeria encriptada y segura) + +CriptoNoticias + +Article + +This Spanish crypto news article presents SimpleX Chat as a privacy-focused alternative to Telegram, noting that developers believe persistent alphanumeric identifiers compromise privacy. It highlights SimpleX's use of temporary anonymous message queue identifiers and single-use QR codes that reduce traceability. + +Image: criptonoticias-telegram-alternatives.jpg + +Language: Spanish + +Date: 2024 + +https://www.criptonoticias.com/tecnologia/alternativas-telegram-privacidad-mensajeria/ + +## SimpleX Chat Tutorial + +Bitcoin.ar + +Guide + +This tutorial from ONG Bitcoin Argentina presents SimpleX as "the first mailbox without user identification," launched in 2021. It notes that while SimpleX includes standard messaging features, its ergonomics remain less fluid than WhatsApp or Signal and can be more restrictive when adding contacts, positioning it as suitable for privacy-conscious users willing to sacrifice daily convenience. + +Image: bitcoin-ar-simplex-tutorial.jpg + +Language: Spanish + +Date: 2024 + +https://bitcoin.ar/tutoriales/simplex-chat/ + +## These Are the Most Secure Messaging Apps According to a Criminologist + +(Estas son las aplicaciones de mensajeria mas seguras segun una criminologa) + +Noticias de Navarra + +News + +This Spanish news article reports that criminologist Maria Aperador highlights SimpleX as the most secure messaging application, citing its lack of user identifiers (not even random ones), end-to-end encryption, and impossibility of data tracking. She emphasizes that SimpleX contains no metadata and the app doesn't know who you are or where messages originate. + +Image: noticiasnavarra-simplex-criminologist.jpg + +Language: Spanish + +Date: Nov 25, 2024 + +https://www.noticiasdenavarra.com/ciencia-y-tecnologia/2024/11/25/estas-son-aplicaciones-mensajeria-mas-seguras-segun-una-criminologa-8972947.html + +## Step-by-Step SimpleX Chat Guide + +(Passo a passo do aplicativo SimpleX Chat) + +Alex Emidio / Substack + +Guide + +This Portuguese step-by-step guide covers SimpleX Chat's backup and data recovery features, including database encryption with user-created passwords and export of encrypted backups. It emphasizes the local-first approach where contacts and messages are stored on the user's device, and warns that the database password cannot be changed if lost. + +Image: alexemidio-substack-simplex.jpg + +Language: Portuguese + +Date: May 26, 2024 + +https://alexemidio.substack.com/p/passo-a-passo-do-aplicativo-simplexchat + +## Ethereum Founder Donates R$4.2 Million to Privacy-Focused Messengers + +(Fundador do Ethereum doa R$ 4,2 milhoes para mensageiros focados em privacidade) + +LiveCoins + +News + +This Brazilian article reports that Vitalik Buterin donated 128 ETH (approximately R$2.1 million) to SimpleX Chat as part of a larger donation to privacy-focused messengers. It notes Buterin acknowledged SimpleX still needs to address challenges including multi-device support and defense against Sybil/DoS attacks without requiring phone numbers. + +Image: livecoins-vitalik-simplex.jpg + +Language: Portuguese + +Date: Nov 2025 + +https://livecoins.com.br/fundador-do-ethereum-doa-r-42-milhoes-para-mensageiros-focados-em-privacidade-baixe-e-use/ + +## Top Most Secure Messaging Apps: Signal, Session, SimpleX + +(Top aplicativos de mensagens mais seguros) + +Moyens I/O Portugal + +Comparison + +Image: moyens-pt-secure-apps.jpg + +Language: Portuguese + +Date: 2024 + +https://pt.moyens.net/aplicativos/top-aplicativos-mensagens-mais-seguros-signal-session-simplex/ + +## SimpleX: How to Use the World's Most Private Messaging App + +(Simplex: como usar o app de mensagens mais privado do mundo) + +Soberano News + +Guide + +This Portuguese guide describes SimpleX as designed to not know who you are, with whom you speak, or when you speak, using blind relay servers that operate as message forwarders without identifying senders or recipients. It acknowledges the app may be slower and have a less polished design than competitors, making it ideal for journalists, activists, and those concerned with digital surveillance. + +Image: soberano-simplex-guide.jpg + +Language: Portuguese + +Date: 2024 + +https://soberano.news/guias-e-ferramentas/simplex-como-usar-o-app-de-mensagens-mais-privado-do-mundo/ + +## How to Install SimpleX Chat Messenger on Linux via Flatpak + +(Como instalar o mensageiro Simplex Chat no Linux via Flatpak) + +Edivaldo Brito + +Guide + +This Portuguese tutorial explains how to install SimpleX Chat on Linux via Flatpak, describing it as a private, open-source encrypted messenger with no user IDs. It lists SimpleX's features including end-to-end encrypted messages, files, images, audio/video calls, secret group chats, instant private notifications, and portable chat profiles, while emphasizing that SimpleX uses no phone numbers or other user identifiers and stores all data on client devices. + +Image: edivaldobrito-simplex-flatpak.jpg + +Language: Portuguese + +Date: 2024 + +https://www.edivaldobrito.com.br/como-instalar-o-mensageiro-simplex-chat-no-linux-via-flatpak/ + +## Top 10 Best Messaging Apps for Secure, Anonymous and Privacy-Respecting Chat + +(Le 10 migliori app di messaggistica per chat sicure, anonime e rispettose della privacy) + +Jacopo Coccia + +Comparison + +This Italian article lists the 10 best messaging apps for secure and anonymous chats, mentioning apps like Threema, Signal, and Wire that offer end-to-end encryption and zero-log policies. SimpleX Chat is not specifically named in the available excerpt, which focuses on the general landscape of privacy-focused messaging and the growing need for data protection. + +Image: jacopococcia-simplex-top10.jpg + +Language: Italian + +Date: 2024 + +https://www.jacopococcia.com/10-migliori-app-messaggistica-per-chat-sicure-anonime-privacy/ + +## What Is SimpleX Chat? + +No Trust Verify + +Article + +This article from the NoTrustVerify blog describes SimpleX as the first messaging platform that doesn't require any login, using the SimpleX Messaging Protocol (SMP) with one-way message queues. It highlights features like incognito mode with random names per contact and live message typing visibility, while acknowledging that multi-device synchronization and large group management still need improvement. + +Image: notrustverify-blog-simplex.jpg + +Language: English + +Date: 2024 + +https://blog.notrustverify.ch/what-is-simplex-chat + +## Interview With the Author of SimpleX Chat: The Most Secure Messaging by Design + +GatoOscuro + +Interview + +This interview with SimpleX founder Evgeny Poberezkin covers the protocol's design beginning in 2020 and mobile app launch in March 2022. Poberezkin acknowledges the "100% private" claim is aspirational marketing rather than absolute truth, and explains that the fully open-source, decentralized architecture prevents government backdoors from compromising the entire network. + +Image: gatooscuro-interview-english.jpg + +Language: English + +Date: 2024 + +https://gatooscuro.xyz/interview-with-the-author-of-simplex-chat-the-most-secure-messaging-by-design/ + +## Safe and Private Messaging Apps Similar to Signal + +Factually + +Comparison + +This product review compares secure messaging apps similar to Signal, noting that Session and SimpleX prioritize privacy-first engineering over mainstream polish. SimpleX is highlighted for avoiding phone-number registration and using no persistent user IDs, offering stronger anonymity than Signal at the cost of convenience and sometimes reliability, while Session uses onion routing which can cause delays and missed notifications. + +Image: factually-signal-alternatives.jpg + +Language: English + +Date: 2026 + +https://factually.co/product-reviews/electronics-tech/safe-private-messaging-apps-similar-to-signal-84872a + +## Best Secure Messaging Apps 2025: Private Chat Reviews + +Tileris + +Comparison + +Image: youtube-best-secure-2025.jpg + +Language: English + +Date: Jul 30, 2025 + +https://www.youtube.com/watch?v=GH4u8pQLY90 + +## SimpleX: Best Private Messenger? + +Tom Spark's Reviews + +Review, Video + +Image: youtube-best-private-messenger.jpg + +Language: English + +Date: 2024 + +https://www.youtube.com/watch?v=PbYm1G-QVUc + +## SimpleX Chat Tutorial + +How to use apps? + +Guide, Video + +Image: youtube-simplex-tutorial.jpg + +Language: English + +Date: 2024 + +https://www.youtube.com/watch?v=X7CJlbBJNcc + +## SimpleX Chat Review + +QuickLearn + +Review, Video + +Image: youtube-simplex-review-2024.jpg + +Language: English + +Date: 2024 + +https://www.youtube.com/watch?v=cGGrRnXAh1w + +## SimpleX Chat: Settings and Usage Guide + +(匿名メッセージアプリ - Simple X Chat の設定と使い方) + +ひとりかくれんぼ / note.com + +Guide + +This Japanese article covers SimpleX Chat's settings and usage, including configuration of global chat settings for complete message deletion, disabling link previews, activating SimpleX Lock with a passcode, and enabling self-destruct mode. The author recommends using a VPN alongside SimpleX for genuine anonymity and stresses that message deletion requires mutual consent between users. + +Image: notecom-deeplife-simplex.jpg + +Language: Japanese + +Date: 2024 + +https://note.com/deeplife/n/n84b9e0a75ac5 + +## How to Bypass SimpleX Chat Blocking + +dept.one + +Article + +This Japanese article explains that SimpleX Chat was blocked in Russia in September 2024 due to its confidentiality and security features, and provides three methods to bypass the block: using a developer-provided proxy, a third-party SOCKS proxy, or a local proxy app like Orbot. It describes SimpleX as a highly secure messenger with no user identification, decentralized architecture, and scrambled message ordering to prevent timing attacks. + +Image: dept-one-simplex-memo.jpg + +Language: Japanese + +Date: 2024 + +https://dept.one/memo/simplex-chat/ + +## SimpleX Chat: Another Kusaimara Article + +Kusaimara + +Article + +This Japanese blog post announces that SimpleX Chat added Japanese language support in version 5.1 released in late May 2023. It notes SimpleX doesn't assign unique user IDs and supports Tor network communication, while acknowledging that account portability requires database passphrase export rather than simple login across devices. + +Image: kusaimara-simplex-3.jpg + +Language: Japanese + +Date: Jun 2023 + +https://kusaimara.net/2023/06/444 + +## Vitalik Buterin Donates ETH to Privacy Apps + +Coinspeaker + +News + +This Japanese crypto news article reports that Vitalik Buterin donated 128 ETH to SimpleX Chat as part of a privacy-focused initiative. It describes SimpleX's one-way message queue design that eliminates global identifiers and enables account creation without linking personal information like phone numbers. + +Image: coinspeaker-jp-vitalik.jpg + +Language: Japanese + +Date: Nov 2025 + +https://www.coinspeaker.com/jp/vitalik-buterin-donates-eth-privacy-apps/ + +## Yahoo Japan: Vitalik Buterin Donation Coverage + +Yahoo Japan News + +News + +Image: yahoo-japan-vitalik-simplex.jpg + +Language: Japanese + +Date: Nov 2025 + +https://news.yahoo.co.jp/articles/a55ac83411dcaca913007857948d335b0ed4d21a + +## Encrypted Messengers: Comparison + +(Encrypted messengers - comparison) + +Juraj Bednar + +Comparison + +This encrypted messenger comparison describes SimpleX as the youngest app in the review, using asymmetric connections through QR codes or URLs that make it difficult to correlate sending and receiving patterns. The reviewer notes the interface shows signs of ongoing development and some reliability issues, and suggests SimpleX suits users prioritizing anonymity for temporary rather than permanent communications. + +Image: bednar-encrypted-messengers-en.jpg + +Language: English + +Date: May 3, 2022 + +https://juraj.bednar.io/en/blog-en/2022/05/03/encrypted-messengers-comparison/ + +## SimpleX Chat on IQ.wiki + +IQ.wiki + +Review + +This IQ.wiki page provides a comprehensive overview of SimpleX Chat, covering its custom protocols (SimpleXMQ and SMP), end-to-end encryption with the Signal Double Ratchet algorithm, and post-quantum resistant encryption via CRYSTALS-Kyber. It notes key milestones including the v1 mobile release in 2022, Jack Dorsey's investment in August 2024, and planned Community Vouchers utility token system for 2026. + +Image: iqwiki-simplex-entry.jpg + +Language: English + +Date: 2024 + +https://iq.wiki/wiki/simplex-chat + +## SimpleX: How to Use the Most Private Messaging App + +(SimpleX: how to use the world's most private messaging app) + +Soberano News + +Guide + +This English guide explains SimpleX's approach of deleting metadata rather than just encrypting messages, using servers as blind relays with temporary one-way queues where servers cannot identify senders or recipients. It acknowledges the app can be slower and have a less polished interface than competitors, positioning it for journalists, activists, and those concerned about digital surveillance. + +Image: soberano-simplex-english.jpg + +Language: English + +Date: 2024 + +https://soberano.news/en/guides-and-tools/simplex-how-to-use-the-worlds-most-private-messaging-app/ + +## Libertarian Institute Article on SimpleX + +Institute for Libertarian Ideas / Japan + +Article + +This Libertarian Institute article emphasizes that SimpleX serves everyday citizens rather than just activists, providing significant privacy improvements with minimal sacrifice of convenience. It highlights identity flexibility where users can change display names per contact, hidden profiles behind password protection, and notes SimpleX functions as an anonymization network similar to Tor. + +Image: libertarian-institute-simplex.jpg + +Language: English + +Date: 2024 + +https://institute-for-libertarian.org/the-libertarian/2148/ + +## Messaging Applications on Medium + +(Mesajlasma Uygulamalari) + +Gizli Kalsin / Medium + +Article + +This Turkish Medium article lists SimpleX Chat as one of six recommended secure messaging applications, describing it as a decentralized instant messaging app that operates without phone numbers or user IDs. It notes users join conversations by scanning QR codes or clicking invite links, and the application provides complete anonymity. + +Image: gizlikalsin-medium-simplex.jpg + +Language: Turkish + +Date: Sep 3, 2023 + +https://medium.com/@gizlikalsin/mesajla%C5%9Fma-uygulamalar%C4%B1-74dc81d78737 + +## BlockTop: From Privacy to Social - Web3 and Encrypted Social Applications + +(Cong yin si dao she jiao: Web3 xu yao yi zhan shi jia mi she jiao ying yong) + +BlockTop + +News + +This Chinese article positions SimpleX alongside Session as representing truly decentralized privacy communication in the Web3 ecosystem. It highlights Vitalik Buterin's donation of 128 ETH to SimpleX and describes the platform's elimination of phone numbers, emails, or usernames in favor of the SimpleX Messaging Protocol, calling it potential "killer-level infrastructure" for the crypto industry. + +Image: blocktop-web3-privacy.jpg + +Language: Chinese + +Date: Dec 17, 2025 + +https://blocktop.cn/newsContent/1/176524 + + +## Better and More Secure Messenger Than Signal + +(Lepszy i bezpieczniejszy komunikator niz Signal) + +Selfhosty.pl + +Comparison + +This Polish article argues there are more secure messengers than Signal, noting that Signal's requirement for a phone number is a serious privacy weakness since phone numbers and IP addresses can be used to track users. The article recommends exploring alternatives that do not require a phone number for registration, positioning them as safer choices for users concerned about government surveillance and data breaches. + +Image: selfhosty-simplex-signal.jpg + +Language: Polish + +Date: 2024 + +https://selfhosty.pl/lepszy-i-bezpieczniejszy-komunikator-niz-signal/ + +## Security in Instant Messaging Services + +(Seguridad en servicios de mensajeria instantanea) + +Colectivo 406 + +Guide + +This Spanish security guide describes SimpleX Chat as using unidirectional message queues with end-to-end encryption including post-quantum algorithms, where servers act only as message bridges with minimal knowledge. It notes SimpleX is fully self-hostable unlike Signal, but acknowledges the complex changing queue addresses make maintaining long-term contacts more difficult. + +Image: colectivo-406-messaging-security.jpg + +Language: Spanish + +Date: Oct 27, 2024 + +https://406.neocities.org/a/apps_mensajeria/ + +## Anonymous Messaging Apps: Recommended List + +(Ni ming tong xin huan jing gou jian - messeji apuri osusume ichiran) + +ひとりかくれんぼ / note.com + +Review + +This Japanese article strongly recommends SimpleX Chat as the top choice for anonymous secure communication, emphasizing that it requires no phone number or email and doesn't use user identifiers or collect metadata. The author notes that user experience is mostly acceptable with only minor delays in call connections, and observes that SimpleX adoption remains relatively low in Japan. + +Image: deeplife-anonymous-apps-list.jpg + +Language: Japanese + +Date: 2024 (estimated) + +https://note.com/deeplife/n/ne7ffdd8a50cd + +## Building Your First Anonymous Digital Life: Fundamentals + +(Tokumei de ikiru tame no saisho no kankyo kochiku to kihon no hanashi) + +ひとりかくれんぼ / note.com + +Guide + +This Japanese article about building an anonymous digital life uses SimpleX Chat as the recommended contact method for readers seeking personalized guidance on privacy topics. It presents SimpleX solely as a secure, end-to-end encrypted communication channel without elaborating on its specific features. + +Image: deeplife-anonymous-life-guide.jpg + +Language: Japanese + +Date: 2024 (estimated) + +https://note.com/deeplife/n/nea6f1c4e08a7 + +## Privacy Protection: Instant Messaging + +Hacking Articles + +Guide + +This security-focused article describes SimpleX Chat as a privacy-focused messaging network that keeps profile and contact information hidden from its servers, using proprietary protocols designed with privacy as a core principle. It recommends SimpleX for lightweight, decentralized communication and anonymous use without central servers, while noting it may lack some advanced features compared to more established platforms. + +Image: hacking-articles-privacy-messaging.jpg + +Language: English + +Date: Sep 17, 2025 + +https://www.hackingarticles.in/privacy-protection-instant-messaging/ + +## SimpleX on Yggdrasil Network + +Yggdrasil Wiki + +Guide + +This Russian wiki page describes SimpleX as the only messenger that uses no user profile identifiers, not even random numbers, with fully open-source client and server code that anyone can self-host. It explains that SimpleX delivers messages using per-contact message queue identifiers rather than user IDs, with plans to automate queue rotation so that even conversations will have no long-term network-visible identifiers. + +Image: yggwiki-simplex-howto.jpg + +Language: Russian + +Date: 2024 (estimated) + +https://yggwiki.cc/social_media:simplex + +## SimpleX Chat Review + +Tests & Tips + +Review + +German review and testing site covering SimpleX Chat. Evaluates the messenger's privacy features, usability, and security properties as part of a broader mobile app testing catalog. + +Image: tests-tips-simplex-review.jpg + +Language: German + +Date: 2024 (estimated) + +https://tests.tips/en/?software/mobile-apps/simplex + +## Self-Host SimpleX Chat + +(Hospeda SimpleX Chat) + +CLASES DE LINUX + +Guide, Video + +Image: hospeda-simplex-chat.jpg + +Language: Spanish + +Date: Feb 10, 2024 + +https://www.youtube.com/watch?v=p1NF68KIt7M + +## SimpleX Chat: The Libertarian WhatsApp + +(SimpleX Chat - Whatsapp libertario) + +Paranoia + +Guide, Video + +Image: simplex-whatsapp-libertario.jpg + +Language: Portuguese + +Date: Jan 9, 2025 + +https://www.youtube.com/watch?v=4sOvaD-YZLU + +## SimpleX Chat Is a Revolution in Encrypted Communication + +Kryptoanarchista.cz + +Review + +This Czech crypto-anarchist blog presents SimpleX as an open-source encrypted messenger requiring no KYC, supporting text, voice, video calls, file sharing, and Tor connectivity. It notes the absence of persistent identities prevents tracking of user connections but acknowledges current limitations including the need for QR code exchange to start conversations and lack of cross-device synchronization. + +Image: kryptoanarchista-english-review.jpg + +Language: English + +Date: Aug 17, 2023 + +https://kryptoanarchista.cz/en/simplex-chat-is-a-revolution-in-encrypted-communication/ + +## Which Messengers Work in Russia and Which Are Blocked + +(Kakie messendzhery rabotayut v Rossii a kakie zablokirovany) + +AppVisor.ru + +Article + +This Russian article reports that SimpleX Chat was among several decentralized messengers blocked by Moscow City Court order in September 2024, alongside Briar, Session, and Verum. It notes the blocking was due to SimpleX's architecture that makes user tracking technically extremely difficult. + +Image: appvisor-blocked-messengers.jpg + +Language: Russian + +Date: Mar 6, 2026 + +https://appvisor.ru/post/news/kakie-messendzhery-rabotayut-v-rossii-a-kakie-zablokirovany/ + +## SimpleX: Ultra-Private Messaging + +(SimpleX, mensajeria ultraprivada) + +Bala Extra + +Podcast + +Image: bala-extra-simplex-podcast.jpg + +Language: Spanish + +Date: Nov 25, 2025 + +https://www.listennotes.com/podcasts/bala-extra/simplex-mensajer%C3%ADa-z3pHYeVJcA2/ + +## Protocols Not Platforms: NOSTR, BTC, SimpleX + +Closed Network Privacy Podcast + +Podcast + +Image: closed-network-protocols.jpg + +Language: English + +Date: 2024 (estimated) + +https://closednetwork.io/podcast/episode-38-protocols-not-platforms-nostr-btc-simplex/ + +## Introducing TorGuard's New Private VPN Cloud App: SimpleX Server + +TorGuard + +Service + +Image: torguard-simplex-server.jpg + +Language: English + +Date: 2025 (estimated) + +https://blog.torguard.net/introducing-torguards-new-private-vpn-cloud-app-simplex-server/ + +## SimpleX Chat Hosting and Services + +Taurix IT + +Service + +Taurix IT offers managed SimpleX Chat hosting services, including hosting their own SMP servers in separate locations and providing setup assistance for clients who want to self-host. The page emphasizes that SimpleX protects metadata which competitors like Signal and WhatsApp fail to adequately safeguard, making it particularly valuable for whistleblowers, journalists, and activists. + +Image: taurix-simplex-hosting.jpg + +Language: English + +Date: 2025 (estimated) + +https://www.taurix.net/simplex-chat/ + +## Taurix SimpleX Customer Service Bot + +Taurix IT + +Service + +This repository contains a Python-based customer service bot for SimpleX Chat that joins a specified control group and announces newly created customer chats. Control group members can then use commands to join customer conversations, automating the connection between customer service representatives and incoming inquiries. + +Image: taurix-customerservice-bot.jpg + +Language: English + +Date: 2025 (estimated) + +https://code.taurix.net/TaurixIT/simplex-customerservicebot + +## RoboSats Orderbook Alert Bot for SimpleX Chat + +TempleOfSats + +Service + +This open-source bot monitors the RoboSats peer-to-peer Bitcoin trading platform and sends alerts via SimpleX Chat when orders matching user-defined criteria (currency, premium rates, payment methods, trade amounts) are posted. Users manage alerts through simple commands, with a default 7-day alert lifetime and extension capabilities. + +Image: robosats-simplex-bot.jpg + +Language: English + +Date: 2024 (estimated) + +https://github.com/TempleOfSats/Robosats-Orderbook-Alert-Bot-for-SimpleX-Chat + +## SimpleX SMP Server Setup Guide + +Freedom Lab NYC + +Guide + +FreedomLab provides a tutorial for self-hosting a SimpleX SMP (Simple Messaging Protocol) server on a Debian/Ubuntu VPS with Tor support. The guide covers installation steps for running your own private, decentralized messaging relay server as part of the SimpleX network. + +Image: freedomlab-simplex-smp.jpg + +Language: English + +Date: 2025 (estimated) + +https://freedomlab.nyc/resources/simplex-smp/ + +## SimpleX Chat on LunarDAO Wiki + +LunarDAO + +Community + +This LunarDAO wiki page presents SimpleX as a privacy-focused federated messaging platform that requires no phone number or account creation, using one-way message queues instead of traditional accounts. It describes features including live typing indicators, voice/video calls, disappearing messages, self-destruct passcodes, and SOCKS proxy routing, while acknowledging challenges with multi-device synchronization and large group management. + +Image: lunardao-simplex-wiki.jpg + +Language: English + +Date: 2024 (estimated) + +https://wiki.lunardao.net/simplexchat.html + +## SimpleX Communities on Monerica + +Monerica + +Review + +This Monerica directory page lists several Monero-focused SimpleX Chat communities spanning multiple languages and regions, including groups for Monero discussion in Slovenian, German, Italian, and Hebrew. It includes an automated bot that sends hourly Monero price updates via SimpleX. + +Image: monerica-simplex-communities.jpg + +Language: English + +Date: 2025 (estimated) + +https://monerica.com/communities/simplex + +## SimpleX Chat: Censorship-Resistant Communication + +SplinterCon + +Review + +The SplinterCon anti-censorship project page describes SimpleX as a decentralized messenger that operates without user IDs, using end-to-end encryption for messages and file transfers. It highlights that SimpleX can run on the Tor network and allows users to deploy their own servers, with messages routed through servers that can operate without persistence. + +Image: splintercon-simplex-listing.jpg + +Language: English + +Date: 2025 (estimated) + +https://splintercon.net/project/simplex-chat/ + +## Tech Guides for Anarchists: End-to-End Encrypted Messaging + +AnarSec + +Guide + +This anarchist security guide notes SimpleX Chat uses decentralized servers (not peer-to-peer) with in-memory storage and no phone number requirement. It recommends Cwtch over SimpleX for text-only communication but suggests SimpleX as an acceptable option for voice and video calls, noting that content padding exists to frustrate correlation attacks via message size. + +Image: anarsec-e2ee-guide.jpg + +Language: English + +Date: 2024 (estimated) + +https://www.anarsec.guide/posts/e2ee/ + +## Join Beginner Privacy on SimpleX + +Beginner Privacy + +Community + +The Beginner Privacy community selected SimpleX Chat as their primary communication platform for its strong privacy features. The page provides setup instructions for beginners across Linux, Mac, Windows, iOS, and Android, emphasizing accessibility through both graphical and command-line interfaces. + +Image: beginner-privacy-simplex-group.jpg + +Language: English + +Date: 2025 (estimated) + +https://beginnerprivacy.com/about/join-simplex-group/ + +## Sofwul.cz: E-Commerce with SimpleX Contact + +Sofwul + +Service + +This Czech e-commerce site lists SimpleX as one of several encrypted messaging contact options for reaching their customer support, alongside Threema, Session, Jami, and Teleguard. + +Image: sofwul-simplex-contact.jpg + +Language: Czech + +Date: 2025 (estimated) + +https://www.sofwul.cz/kontakty-messengery + +## SimpleX Chat Server Now Available for StartOS + +Start9 + +Service + +This Stacker News post announces SimpleX Chat server availability for StartOS (Start9's operating system), describing its support for direct messages, group messages, calls, and video calls with end-to-end encryption. Commenters praise it as "very promising tech" that is "plug and play right at the initial download" and comfortable to recommend to non-technical friends. + +Image: start9-simplex-startos.jpg + +Language: English + +Date: 2023 (estimated) + +https://stacker.news/items/196092 + +## Hack Liberty: Cypherpunk Community on SimpleX + +Hack Liberty + +Community + +Image: hackliberty-simplex-community.jpg + +Language: English + +Date: 2024 (estimated) + +https://links.hackliberty.org/ + +## Nowhere.moe: Privacy Hosting with SimpleX Infrastructure + +Nowhere.moe + +Service + +Privacy-focused hosting service operates community SimpleX SMP and XFTP relay servers with both clearnet and Tor onion addresses. Runs anonymous SimpleX chatrooms and provides tutorials on privacy and self-hosting. + +Image: nowhere-moe-simplex-servers.jpg + +Language: English + +Date: 2024 (estimated) + +https://nowhere.moe/ + +## XMRBazaar: Monero Marketplace on SimpleX + +XMRBazaar + +Service + +This Monero community post announces an unofficial XMRBazaar group on SimpleX Chat, providing two join links - one described as "possibly slower but uncensorable" using direct server protocols and another via the SimpleX Directory Service described as "faster but censorable." The group migrated from Matrix to SimpleX for discussing the Monero marketplace. + +Image: xmrbazaar-simplex-community.jpg + +Language: English + +Date: 2024 (estimated) + +https://monero.town/post/4623536 + +## Simplified Privacy: Tech Groups on SimpleX + +Simplified Privacy + +Community + +Simplified Privacy lists several active SimpleX Chat groups covering privacy, security, cryptocurrency (Monero and Bitcoin/Lightning Network), Nostr protocol, and a Switzerland-based privacy community. The page positions SimpleX alongside Session and XMPP as decentralized messaging platforms for privacy-conscious technical discussions. + +Image: simplified-privacy-techgroups.jpg + +Language: English + +Date: 2024 (estimated) + +https://simplifiedprivacy.com/techgroups/ + +## NBTV Community on SimpleX + +NBTV / Ludlow Institute + +Community + +The Ludlow Institute (NBTV) offers SimpleX as one of several community communication platforms alongside Signal and Element, describing it as an end-to-end encrypted messaging app for real-time discussions with other NBTV community members. + +Image: nbtv-simplex-community.jpg + +Language: English + +Date: 2025 (estimated) + +https://ludlowinstitute.org/community + +## Taurix "tellme" Bot + +Taurix IT + +Service + +TellMe is a notification system that monitors events (completed processes, server uptime, etc.) and sends alerts through SimpleX Chat or Matrix via websockets. It supports custom messages, process monitoring, periodic command output watching, and host ping availability tracking. + +Image: taurix-tellme-bot.jpg + +Language: English + +Date: 2025 (estimated) + +https://code.taurix.net/TaurixIT/tellme + +## BitList.co Bitcointalk Notification Bot + +BitList.co + +Service + +This Bitcointalk thread presents a notification bot for SimpleX Chat that monitors the forum for merit awards, mentions, quotes, specific users, topics, and keyword phrases. The developer notes that due to SimpleX's decentralized architecture, notifications may experience delays compared to Telegram alternatives, and data is stored for retry when users aren't connected. + +Image: bitlist-bitcointalk-bot.jpg + +Language: English + +Date: 2025 (estimated) + +https://bitcointalk.org/index.php?topic=5535681.0 + +## SimpleX Without Tears: Wrapped in Onion, Served via Tor + +nemental.de + +Guide + +This guide explains how to host a SimpleX SMP server as a Tor hidden service using Docker containers, making the relay accessible only through .onion addresses. It notes that while SimpleX already has strong privacy by design, adding Tor hides the server's public IP and eliminates DNS exposure, pulling the relay further out of reach of traditional tracking. + +Image: nemental-simplex-tor-guide.jpg + +Language: English + +Date: 2025 (estimated) + +https://nemental.de/simplex-without-tears-wrapped-in-onion-served-via-tor/ + +## SimpleX Server Docker Installation Guide (SMP/XFTP) + +Hack Liberty + +Guide + +This Hack Liberty forum guide provides detailed instructions for deploying SimpleX Chat's SMP and XFTP servers using Docker Compose, with dual-network configuration for both clearnet and Tor operation. It covers port configuration, security measures including running containers as unprivileged users, and emphasizes backing up private CA keys before deletion from the server. + +Image: hackliberty-docker-guide.jpg + +Language: English + +Date: 2024 (estimated) + +https://forum.hackliberty.org/t/simplex-server-docker-installation-guide-smp-xftp/140 + +## SimpleX Theme Archive + +SimpleX-Themes + +Community + +This community-maintained archive offers over 100 downloadable SimpleX Chat themes with preview screenshots, ranging from dark modes (AMOLED Black, Dracula) to colorful designs (Aurora Sunset, Catppuccin, neon synthwave). It is an independent community project not affiliated with SimpleX Chat Ltd, and users can submit their own custom themes. + +Image: simplex-themes-archive.jpg + +Language: English + +Date: 2025 (estimated) + +https://slcw.github.io/SimpleX-Themes/ + +## Goodbye Telegram, Welcome SimpleX! + +(Viszlat Telegram, udvozollek SimpleX!) + +HUP.hu + +Community + +This Hungarian article shares a user's year-long experience with SimpleX Chat, praising that it requires no email, phone number, username, or password to register. The author describes the SimpleX address system, the Directory Bot for finding public groups, and notes the tradeoff that deleting the app without a database backup means permanently losing your profile and all contacts, while also referencing the Wired article about neo-Nazis on SimpleX and the SimpleX developer's response defending privacy. + +Image: hup-hu-goodbye-telegram.jpg + +Language: Hungarian + +Date: Oct 2024 + +https://hup.hu/node/186622 + +## Sharing Several E2EE Open-Source Chat Apps 100x Safer Than Telegram + +(Fen xiang ji ge bi dian bao Telegram an quan 100 bei de duan dui duan jia mi kai yuan liao tian ruan jian) + +V2EX + +Community + +This Chinese V2EX forum post recommends SimpleX as having the highest encryption strength among reviewed E2EE messaging apps, highlighting that unlike Session's fixed IDs, SimpleX eliminates even fixed identifiers entirely. It notes SimpleX uses temporary anonymous pairing identifiers, creates independent fingerprints per conversation, supports Tor integration, and can be self-hosted. + +Image: v2ex-simplex-telegram-100x.jpg + +Language: Chinese + +Date: Aug 26, 2024 + +https://www.v2ex.com/t/1067954 + +## Sharing E2EE Open-Source Chat Apps 100x Safer Than Telegram + +(Fen xiang ji ge bi dian bao Telegram an quan 100 bei de kai yuan liao tian ruan jian) + +Matters.town + +Article + +This Chinese article recommends several end-to-end encrypted open-source chat apps that are far more secure than Telegram, written in the context of the Telegram founder's detention in France. It explains end-to-end encryption and mentions Session and SimpleX among the recommended alternatives that protect message content from being readable even by the relay servers. + +Image: matters-simplex-telegram-comparison.jpg + +Language: Chinese + +Date: Aug 26, 2024 + +https://matters.town/a/hvb8p0wepluy + +## Comparison of Instant Messengers + +Eylenburg + +Comparison + +This comprehensive messenger comparison states that SimpleX "probably has the best privacy and anonymity of all the messengers compared here," noting it requires no accounts or IDs and users connect via one-time invitation links. It identifies the main limitation as adoption - being a relatively new app with few users makes it impractical for most people who need to communicate with existing contacts. + +Image: eylenburg-messenger-comparison.jpg + +Language: English + +Date: Apr 2026 (updated) + +https://eylenburg.github.io/im_comparison.htm + +## SimpleX VS Quiet: Privacy and Security + +Hack Forums + +Comparison + +Image: hackforums-simplex-vs-quiet.jpg + +Language: English + +Date: 2024 (estimated) + +https://hackforums.net/blog/SimpleX-VS-Quiet + + +## MoneroKon 2024: SimpleX Chat talk + +SimpleX Chat + +Conference talk + +MoneroKon 2024 conference talk by Evgeny Poberezkin on how SimpleX Chat bridges the gap between privacy-focused and mass-market messaging applications. + +Language: English + +Date: 2024 + +https://www.youtube.com/watch?v=Fl-QS0-qENw + + +## SimpleXChat on Monero + Price and More! + +Monero Talk + +Interview, Video + +Monero Talk episode 239 featuring a discussion of SimpleX Chat's integration with Monero, payment incentives for server operators, and the broader privacy messaging landscape. + +Image: monero-talk-ep239-simplex.jpg + +Language: English + +Date: 2024 (estimated) + +https://www.youtube.com/live/quk4WY3fJCc?si=IQ0rutoHQpWtF2Un&t=3812 + + +## MoneroTopia 2026: The Future of SimpleX Network + +SimpleX Chat + +Conference talk, Video + +SimpleX Chat presentation at MoneroTopia 2026 conference in Mexico City, covering the latest developments in the SimpleX network and its alignment with the Monero privacy ecosystem. + +Image: monerotopia-2026-simplex.jpg + +Language: English + +Date: Feb 15, 2026 + +https://www.youtube.com/live/Alp-hVCoF7c?si=FqOTt1mCeQJVo-cM&t=16688 + +## SimpleX Chat - Simple Messaging With Unusually Good Privacy + +Bornhack 2023 conference, Chaos Computer Club + +Conference talk, Video + +This is a spontaneous talk about the relatively new (mobile apps released 2022) open source SimpleX Chat instant messenger protocol and software, and some reasons why it's a far better choice than in particular Matrix. + +Language: English + +Date: Aug 07, 2023 + +https://media.ccc.de/v/bornhack2023-56143-simplex-chat-simple-m diff --git a/docs/links/images/3arbi4tech-simplex-comparison.jpg b/docs/links/images/3arbi4tech-simplex-comparison.jpg new file mode 100644 index 0000000000..b8b78936b7 Binary files /dev/null and b/docs/links/images/3arbi4tech-simplex-comparison.jpg differ diff --git a/docs/links/images/ababtools-simplex-review.jpg b/docs/links/images/ababtools-simplex-review.jpg new file mode 100644 index 0000000000..792a8f777a Binary files /dev/null and b/docs/links/images/ababtools-simplex-review.jpg differ diff --git a/docs/links/images/adminforge-simplex-server.jpg b/docs/links/images/adminforge-simplex-server.jpg new file mode 100644 index 0000000000..300adaa08f Binary files /dev/null and b/docs/links/images/adminforge-simplex-server.jpg differ diff --git a/docs/links/images/aiutocomputerhelp-simplex-revolution.jpg b/docs/links/images/aiutocomputerhelp-simplex-revolution.jpg new file mode 100644 index 0000000000..e6f1f3d36e Binary files /dev/null and b/docs/links/images/aiutocomputerhelp-simplex-revolution.jpg differ diff --git a/docs/links/images/alexemidio-substack-simplex.jpg b/docs/links/images/alexemidio-substack-simplex.jpg new file mode 100644 index 0000000000..54c06afef0 Binary files /dev/null and b/docs/links/images/alexemidio-substack-simplex.jpg differ diff --git a/docs/links/images/ameblo-vpn53049-simplex-recommend.jpg b/docs/links/images/ameblo-vpn53049-simplex-recommend.jpg new file mode 100644 index 0000000000..f19ab863fb Binary files /dev/null and b/docs/links/images/ameblo-vpn53049-simplex-recommend.jpg differ diff --git a/docs/links/images/ameblo-vpn53049-simplex-revolutionary.jpg b/docs/links/images/ameblo-vpn53049-simplex-revolutionary.jpg new file mode 100644 index 0000000000..f19ab863fb Binary files /dev/null and b/docs/links/images/ameblo-vpn53049-simplex-revolutionary.jpg differ diff --git a/docs/links/images/anarsec-e2ee-guide.jpg b/docs/links/images/anarsec-e2ee-guide.jpg new file mode 100644 index 0000000000..e337f160f3 Binary files /dev/null and b/docs/links/images/anarsec-e2ee-guide.jpg differ diff --git a/docs/links/images/appvisor-blocked-messengers.jpg b/docs/links/images/appvisor-blocked-messengers.jpg new file mode 100644 index 0000000000..60ddcbcb49 Binary files /dev/null and b/docs/links/images/appvisor-blocked-messengers.jpg differ diff --git a/docs/links/images/bastion-military-messengers.jpg b/docs/links/images/bastion-military-messengers.jpg new file mode 100644 index 0000000000..e7cd4d50ac Binary files /dev/null and b/docs/links/images/bastion-military-messengers.jpg differ diff --git a/docs/links/images/bednar-encrypted-messengers-en.jpg b/docs/links/images/bednar-encrypted-messengers-en.jpg new file mode 100644 index 0000000000..552e49e93b Binary files /dev/null and b/docs/links/images/bednar-encrypted-messengers-en.jpg differ diff --git a/docs/links/images/bednar-encrypted-messengers.jpg b/docs/links/images/bednar-encrypted-messengers.jpg new file mode 100644 index 0000000000..80c79dd2b1 Binary files /dev/null and b/docs/links/images/bednar-encrypted-messengers.jpg differ diff --git a/docs/links/images/bednar-encrypted-notifications.jpg b/docs/links/images/bednar-encrypted-notifications.jpg new file mode 100644 index 0000000000..98f05ba5e0 Binary files /dev/null and b/docs/links/images/bednar-encrypted-notifications.jpg differ diff --git a/docs/links/images/beebom-best-secure-2026.jpg b/docs/links/images/beebom-best-secure-2026.jpg new file mode 100644 index 0000000000..c8a9ae637f Binary files /dev/null and b/docs/links/images/beebom-best-secure-2026.jpg differ diff --git a/docs/links/images/beginner-privacy-simplex-group.jpg b/docs/links/images/beginner-privacy-simplex-group.jpg new file mode 100644 index 0000000000..18918cc90e Binary files /dev/null and b/docs/links/images/beginner-privacy-simplex-group.jpg differ diff --git a/docs/links/images/bhb-simplex-tutorial-2.jpg b/docs/links/images/bhb-simplex-tutorial-2.jpg new file mode 100644 index 0000000000..a482c3c909 Binary files /dev/null and b/docs/links/images/bhb-simplex-tutorial-2.jpg differ diff --git a/docs/links/images/billionnapkin-simplex-review.jpg b/docs/links/images/billionnapkin-simplex-review.jpg new file mode 100644 index 0000000000..4d77bebada Binary files /dev/null and b/docs/links/images/billionnapkin-simplex-review.jpg differ diff --git a/docs/links/images/bitcoin-ar-simplex-tutorial.jpg b/docs/links/images/bitcoin-ar-simplex-tutorial.jpg new file mode 100644 index 0000000000..eacc967e52 Binary files /dev/null and b/docs/links/images/bitcoin-ar-simplex-tutorial.jpg differ diff --git a/docs/links/images/bitcoinlighthouse-simplex-test.jpg b/docs/links/images/bitcoinlighthouse-simplex-test.jpg new file mode 100644 index 0000000000..a5b9b82ff7 Binary files /dev/null and b/docs/links/images/bitcoinlighthouse-simplex-test.jpg differ diff --git a/docs/links/images/blockbeats-vitalik-simplex.jpg b/docs/links/images/blockbeats-vitalik-simplex.jpg new file mode 100644 index 0000000000..1b4f926ce4 Binary files /dev/null and b/docs/links/images/blockbeats-vitalik-simplex.jpg differ diff --git a/docs/links/images/blockweeks-vitalik-simplex.jpg b/docs/links/images/blockweeks-vitalik-simplex.jpg new file mode 100644 index 0000000000..583ac57689 Binary files /dev/null and b/docs/links/images/blockweeks-vitalik-simplex.jpg differ diff --git a/docs/links/images/brightcoding-privacy-by-design.jpg b/docs/links/images/brightcoding-privacy-by-design.jpg new file mode 100644 index 0000000000..2c2a593d75 Binary files /dev/null and b/docs/links/images/brightcoding-privacy-by-design.jpg differ diff --git a/docs/links/images/bug-hr-app-of-day.jpg b/docs/links/images/bug-hr-app-of-day.jpg new file mode 100644 index 0000000000..58a9b5d889 Binary files /dev/null and b/docs/links/images/bug-hr-app-of-day.jpg differ diff --git a/docs/links/images/chinese-youtube-secure-tools.jpg b/docs/links/images/chinese-youtube-secure-tools.jpg new file mode 100644 index 0000000000..e7bc007c8f Binary files /dev/null and b/docs/links/images/chinese-youtube-secure-tools.jpg differ diff --git a/docs/links/images/citadel-dispatch-cd196.jpg b/docs/links/images/citadel-dispatch-cd196.jpg new file mode 100644 index 0000000000..0300cfc74c Binary files /dev/null and b/docs/links/images/citadel-dispatch-cd196.jpg differ diff --git a/docs/links/images/cloudsek-best-secure-2026.jpg b/docs/links/images/cloudsek-best-secure-2026.jpg new file mode 100644 index 0000000000..ba021d7d83 Binary files /dev/null and b/docs/links/images/cloudsek-best-secure-2026.jpg differ diff --git a/docs/links/images/cnews-cz-simplex-privacy.jpg b/docs/links/images/cnews-cz-simplex-privacy.jpg new file mode 100644 index 0000000000..769b5697b8 Binary files /dev/null and b/docs/links/images/cnews-cz-simplex-privacy.jpg differ diff --git a/docs/links/images/cnews-telegram-simplex-migration.jpg b/docs/links/images/cnews-telegram-simplex-migration.jpg new file mode 100644 index 0000000000..25233d0b7c Binary files /dev/null and b/docs/links/images/cnews-telegram-simplex-migration.jpg differ diff --git a/docs/links/images/codeby-simplex-free-messengers.jpg b/docs/links/images/codeby-simplex-free-messengers.jpg new file mode 100644 index 0000000000..9777e8a0fd Binary files /dev/null and b/docs/links/images/codeby-simplex-free-messengers.jpg differ diff --git a/docs/links/images/coinspeaker-jp-vitalik.jpg b/docs/links/images/coinspeaker-jp-vitalik.jpg new file mode 100644 index 0000000000..e45ec7f1cf Binary files /dev/null and b/docs/links/images/coinspeaker-jp-vitalik.jpg differ diff --git a/docs/links/images/computekni-simplex-chat.jpg b/docs/links/images/computekni-simplex-chat.jpg new file mode 100644 index 0000000000..75730fa6c6 Binary files /dev/null and b/docs/links/images/computekni-simplex-chat.jpg differ diff --git a/docs/links/images/cryptoslate-buterin-analysis.jpg b/docs/links/images/cryptoslate-buterin-analysis.jpg new file mode 100644 index 0000000000..fabbe9814e Binary files /dev/null and b/docs/links/images/cryptoslate-buterin-analysis.jpg differ diff --git a/docs/links/images/cryptotimes-buterin-simplex.jpg b/docs/links/images/cryptotimes-buterin-simplex.jpg new file mode 100644 index 0000000000..1b92ef223a Binary files /dev/null and b/docs/links/images/cryptotimes-buterin-simplex.jpg differ diff --git a/docs/links/images/cyberinsider-most-secure-2026.jpg b/docs/links/images/cyberinsider-most-secure-2026.jpg new file mode 100644 index 0000000000..34d49afcc0 Binary files /dev/null and b/docs/links/images/cyberinsider-most-secure-2026.jpg differ diff --git a/docs/links/images/datacampus-self-hosting.jpg b/docs/links/images/datacampus-self-hosting.jpg new file mode 100644 index 0000000000..cda10213f3 Binary files /dev/null and b/docs/links/images/datacampus-self-hosting.jpg differ diff --git a/docs/links/images/dcinside-vpngate-simplex-translation.jpg b/docs/links/images/dcinside-vpngate-simplex-translation.jpg new file mode 100644 index 0000000000..cd60d11659 Binary files /dev/null and b/docs/links/images/dcinside-vpngate-simplex-translation.jpg differ diff --git a/docs/links/images/dcinside-wikileaks-simplex.jpg b/docs/links/images/dcinside-wikileaks-simplex.jpg new file mode 100644 index 0000000000..1527c05b37 Binary files /dev/null and b/docs/links/images/dcinside-wikileaks-simplex.jpg differ diff --git a/docs/links/images/ddpa-simplex-overview.jpg b/docs/links/images/ddpa-simplex-overview.jpg new file mode 100644 index 0000000000..a7017f6989 Binary files /dev/null and b/docs/links/images/ddpa-simplex-overview.jpg differ diff --git a/docs/links/images/deeplife-anonymous-apps-list.jpg b/docs/links/images/deeplife-anonymous-apps-list.jpg new file mode 100644 index 0000000000..ff68796e02 Binary files /dev/null and b/docs/links/images/deeplife-anonymous-apps-list.jpg differ diff --git a/docs/links/images/deeplife-anonymous-life-guide.jpg b/docs/links/images/deeplife-anonymous-life-guide.jpg new file mode 100644 index 0000000000..c31cc4a5af Binary files /dev/null and b/docs/links/images/deeplife-anonymous-life-guide.jpg differ diff --git a/docs/links/images/dept-one-simplex-memo.jpg b/docs/links/images/dept-one-simplex-memo.jpg new file mode 100644 index 0000000000..d0d8dbac17 Binary files /dev/null and b/docs/links/images/dept-one-simplex-memo.jpg differ diff --git a/docs/links/images/dev-community-privacy-setup-2026.jpg b/docs/links/images/dev-community-privacy-setup-2026.jpg new file mode 100644 index 0000000000..a1d5d2e1d0 Binary files /dev/null and b/docs/links/images/dev-community-privacy-setup-2026.jpg differ diff --git a/docs/links/images/digitalcourage-simplex-recommendation.jpg b/docs/links/images/digitalcourage-simplex-recommendation.jpg new file mode 100644 index 0000000000..badd42c295 Binary files /dev/null and b/docs/links/images/digitalcourage-simplex-recommendation.jpg differ diff --git a/docs/links/images/diolinux-simplex-messenger.jpg b/docs/links/images/diolinux-simplex-messenger.jpg new file mode 100644 index 0000000000..bcd5fcb04c Binary files /dev/null and b/docs/links/images/diolinux-simplex-messenger.jpg differ diff --git a/docs/links/images/ecosistemastartup-simplex.jpg b/docs/links/images/ecosistemastartup-simplex.jpg new file mode 100644 index 0000000000..d947ec934e Binary files /dev/null and b/docs/links/images/ecosistemastartup-simplex.jpg differ diff --git a/docs/links/images/edivaldo-brito-simplex-review.jpg b/docs/links/images/edivaldo-brito-simplex-review.jpg new file mode 100644 index 0000000000..bf3dfac3a7 Binary files /dev/null and b/docs/links/images/edivaldo-brito-simplex-review.jpg differ diff --git a/docs/links/images/edivaldobrito-simplex-flatpak.jpg b/docs/links/images/edivaldobrito-simplex-flatpak.jpg new file mode 100644 index 0000000000..ed8fc7db1f Binary files /dev/null and b/docs/links/images/edivaldobrito-simplex-flatpak.jpg differ diff --git a/docs/links/images/ekoreanews-telegram-alternatives.jpg b/docs/links/images/ekoreanews-telegram-alternatives.jpg new file mode 100644 index 0000000000..fcc70bec8d Binary files /dev/null and b/docs/links/images/ekoreanews-telegram-alternatives.jpg differ diff --git a/docs/links/images/eksisozluk-simplex.jpg b/docs/links/images/eksisozluk-simplex.jpg new file mode 100644 index 0000000000..2410f84666 Binary files /dev/null and b/docs/links/images/eksisozluk-simplex.jpg differ diff --git a/docs/links/images/esgeeks-decentralized-messengers.jpg b/docs/links/images/esgeeks-decentralized-messengers.jpg new file mode 100644 index 0000000000..8893a74ab9 Binary files /dev/null and b/docs/links/images/esgeeks-decentralized-messengers.jpg differ diff --git a/docs/links/images/esgeeks-most-secure-app.jpg b/docs/links/images/esgeeks-most-secure-app.jpg new file mode 100644 index 0000000000..b3c0973549 Binary files /dev/null and b/docs/links/images/esgeeks-most-secure-app.jpg differ diff --git a/docs/links/images/expressvpn-most-secure-2026.jpg b/docs/links/images/expressvpn-most-secure-2026.jpg new file mode 100644 index 0000000000..b5f3499d74 Binary files /dev/null and b/docs/links/images/expressvpn-most-secure-2026.jpg differ diff --git a/docs/links/images/franciscobarral-simplex.jpg b/docs/links/images/franciscobarral-simplex.jpg new file mode 100644 index 0000000000..831cbbc3b3 Binary files /dev/null and b/docs/links/images/franciscobarral-simplex.jpg differ diff --git a/docs/links/images/free-com-tw-simplex.jpg b/docs/links/images/free-com-tw-simplex.jpg new file mode 100644 index 0000000000..9d497e34ea Binary files /dev/null and b/docs/links/images/free-com-tw-simplex.jpg differ diff --git a/docs/links/images/freedom-tech-simplex-review.jpg b/docs/links/images/freedom-tech-simplex-review.jpg new file mode 100644 index 0000000000..6753595147 Binary files /dev/null and b/docs/links/images/freedom-tech-simplex-review.jpg differ diff --git a/docs/links/images/freedomlab-simplex-smp.jpg b/docs/links/images/freedomlab-simplex-smp.jpg new file mode 100644 index 0000000000..2a30c89e52 Binary files /dev/null and b/docs/links/images/freedomlab-simplex-smp.jpg differ diff --git a/docs/links/images/freedomnode-session-simplex.jpg b/docs/links/images/freedomnode-session-simplex.jpg new file mode 100644 index 0000000000..2244d7b5c5 Binary files /dev/null and b/docs/links/images/freedomnode-session-simplex.jpg differ diff --git a/docs/links/images/freeonline-simplex-review.jpg b/docs/links/images/freeonline-simplex-review.jpg new file mode 100644 index 0000000000..dd9465f021 Binary files /dev/null and b/docs/links/images/freeonline-simplex-review.jpg differ diff --git a/docs/links/images/gatooscuro-interview-english.jpg b/docs/links/images/gatooscuro-interview-english.jpg new file mode 100644 index 0000000000..0d4dc2e319 Binary files /dev/null and b/docs/links/images/gatooscuro-interview-english.jpg differ diff --git a/docs/links/images/gatooscuro-simplex-interview.jpg b/docs/links/images/gatooscuro-simplex-interview.jpg new file mode 100644 index 0000000000..d71d4139ed Binary files /dev/null and b/docs/links/images/gatooscuro-simplex-interview.jpg differ diff --git a/docs/links/images/gatooscuro-simplex-review.jpg b/docs/links/images/gatooscuro-simplex-review.jpg new file mode 100644 index 0000000000..d71d4139ed Binary files /dev/null and b/docs/links/images/gatooscuro-simplex-review.jpg differ diff --git a/docs/links/images/gazeta-mig-simplex-telegram.jpg b/docs/links/images/gazeta-mig-simplex-telegram.jpg new file mode 100644 index 0000000000..6d92871f9b Binary files /dev/null and b/docs/links/images/gazeta-mig-simplex-telegram.jpg differ diff --git a/docs/links/images/gnulinux-ch-simplex-overview.jpg b/docs/links/images/gnulinux-ch-simplex-overview.jpg new file mode 100644 index 0000000000..463e657142 Binary files /dev/null and b/docs/links/images/gnulinux-ch-simplex-overview.jpg differ diff --git a/docs/links/images/gnulinux-ch-simplex-smartphones.jpg b/docs/links/images/gnulinux-ch-simplex-smartphones.jpg new file mode 100644 index 0000000000..2f66de7130 Binary files /dev/null and b/docs/links/images/gnulinux-ch-simplex-smartphones.jpg differ diff --git a/docs/links/images/golden-finance-web3-privacy.jpg b/docs/links/images/golden-finance-web3-privacy.jpg new file mode 100644 index 0000000000..8ca86dc822 Binary files /dev/null and b/docs/links/images/golden-finance-web3-privacy.jpg differ diff --git a/docs/links/images/habr-anonymous-messengers.jpg b/docs/links/images/habr-anonymous-messengers.jpg new file mode 100644 index 0000000000..f1d1f926d4 Binary files /dev/null and b/docs/links/images/habr-anonymous-messengers.jpg differ diff --git a/docs/links/images/habr-anonymous-standard.jpg b/docs/links/images/habr-anonymous-standard.jpg new file mode 100644 index 0000000000..d310443ec3 Binary files /dev/null and b/docs/links/images/habr-anonymous-standard.jpg differ diff --git a/docs/links/images/habr-globalsign-classification.jpg b/docs/links/images/habr-globalsign-classification.jpg new file mode 100644 index 0000000000..e8f33d3183 Binary files /dev/null and b/docs/links/images/habr-globalsign-classification.jpg differ diff --git a/docs/links/images/habr-globalsign-p2p-chats.jpg b/docs/links/images/habr-globalsign-p2p-chats.jpg new file mode 100644 index 0000000000..b21515dea0 Binary files /dev/null and b/docs/links/images/habr-globalsign-p2p-chats.jpg differ diff --git a/docs/links/images/habr-simplex-first-messenger.jpg b/docs/links/images/habr-simplex-first-messenger.jpg new file mode 100644 index 0000000000..c4fc5edef4 Binary files /dev/null and b/docs/links/images/habr-simplex-first-messenger.jpg differ diff --git a/docs/links/images/hacking-articles-privacy-messaging.jpg b/docs/links/images/hacking-articles-privacy-messaging.jpg new file mode 100644 index 0000000000..959728197b Binary files /dev/null and b/docs/links/images/hacking-articles-privacy-messaging.jpg differ diff --git a/docs/links/images/hackspoiler-simplex-star.jpg b/docs/links/images/hackspoiler-simplex-star.jpg new file mode 100644 index 0000000000..09b70de9d1 Binary files /dev/null and b/docs/links/images/hackspoiler-simplex-star.jpg differ diff --git a/docs/links/images/heise-german-language-simplex.jpg b/docs/links/images/heise-german-language-simplex.jpg new file mode 100644 index 0000000000..eedabbce76 Binary files /dev/null and b/docs/links/images/heise-german-language-simplex.jpg differ diff --git a/docs/links/images/heise-simplex-100-release.jpg b/docs/links/images/heise-simplex-100-release.jpg new file mode 100644 index 0000000000..66bfea70ea Binary files /dev/null and b/docs/links/images/heise-simplex-100-release.jpg differ diff --git a/docs/links/images/heise-simplex-smartphone.jpg b/docs/links/images/heise-simplex-smartphone.jpg new file mode 100644 index 0000000000..f21904463d Binary files /dev/null and b/docs/links/images/heise-simplex-smartphone.jpg differ diff --git a/docs/links/images/heise-simplex-v3-apple.jpg b/docs/links/images/heise-simplex-v3-apple.jpg new file mode 100644 index 0000000000..1b39a768c8 Binary files /dev/null and b/docs/links/images/heise-simplex-v3-apple.jpg differ diff --git a/docs/links/images/heise-simplex-v4-private.jpg b/docs/links/images/heise-simplex-v4-private.jpg new file mode 100644 index 0000000000..eedabbce76 Binary files /dev/null and b/docs/links/images/heise-simplex-v4-private.jpg differ diff --git a/docs/links/images/help-net-security-product-showcase.jpg b/docs/links/images/help-net-security-product-showcase.jpg new file mode 100644 index 0000000000..5eae8399f2 Binary files /dev/null and b/docs/links/images/help-net-security-product-showcase.jpg differ diff --git a/docs/links/images/hi-tech-mail-simplex.jpg b/docs/links/images/hi-tech-mail-simplex.jpg new file mode 100644 index 0000000000..89b3a2810c Binary files /dev/null and b/docs/links/images/hi-tech-mail-simplex.jpg differ diff --git a/docs/links/images/hospeda-simplex-chat.jpg b/docs/links/images/hospeda-simplex-chat.jpg new file mode 100644 index 0000000000..d534a25095 Binary files /dev/null and b/docs/links/images/hospeda-simplex-chat.jpg differ diff --git a/docs/links/images/htr-simplex-review.jpg b/docs/links/images/htr-simplex-review.jpg new file mode 100644 index 0000000000..162d9c375b Binary files /dev/null and b/docs/links/images/htr-simplex-review.jpg differ diff --git a/docs/links/images/io-tech-secure-messaging.jpg b/docs/links/images/io-tech-secure-messaging.jpg new file mode 100644 index 0000000000..272f691514 Binary files /dev/null and b/docs/links/images/io-tech-secure-messaging.jpg differ diff --git a/docs/links/images/io-tech-simplex-forum.jpg b/docs/links/images/io-tech-simplex-forum.jpg new file mode 100644 index 0000000000..a4b3ce6362 Binary files /dev/null and b/docs/links/images/io-tech-simplex-forum.jpg differ diff --git a/docs/links/images/iode-degoogle-messaging.jpg b/docs/links/images/iode-degoogle-messaging.jpg new file mode 100644 index 0000000000..aff57e2d0c Binary files /dev/null and b/docs/links/images/iode-degoogle-messaging.jpg differ diff --git a/docs/links/images/iphon-fr-most-incognito.jpg b/docs/links/images/iphon-fr-most-incognito.jpg new file mode 100644 index 0000000000..cbb270bd88 Binary files /dev/null and b/docs/links/images/iphon-fr-most-incognito.jpg differ diff --git a/docs/links/images/iphone-ticker-simplex-privacy.jpg b/docs/links/images/iphone-ticker-simplex-privacy.jpg new file mode 100644 index 0000000000..2d28fd4adf Binary files /dev/null and b/docs/links/images/iphone-ticker-simplex-privacy.jpg differ diff --git a/docs/links/images/iqwiki-simplex-entry.jpg b/docs/links/images/iqwiki-simplex-entry.jpg new file mode 100644 index 0000000000..dedcb07cd5 Binary files /dev/null and b/docs/links/images/iqwiki-simplex-entry.jpg differ diff --git a/docs/links/images/italian-youtube-anonymous-chat.jpg b/docs/links/images/italian-youtube-anonymous-chat.jpg new file mode 100644 index 0000000000..52cb930c29 Binary files /dev/null and b/docs/links/images/italian-youtube-anonymous-chat.jpg differ diff --git a/docs/links/images/itforprof-alternatives-2026.jpg b/docs/links/images/itforprof-alternatives-2026.jpg new file mode 100644 index 0000000000..60224bef5d Binary files /dev/null and b/docs/links/images/itforprof-alternatives-2026.jpg differ diff --git a/docs/links/images/itsfoss-simplex-review.jpg b/docs/links/images/itsfoss-simplex-review.jpg new file mode 100644 index 0000000000..8469de2397 Binary files /dev/null and b/docs/links/images/itsfoss-simplex-review.jpg differ diff --git a/docs/links/images/ixbt-messengers-2026.jpg b/docs/links/images/ixbt-messengers-2026.jpg new file mode 100644 index 0000000000..aeda101fb5 Binary files /dev/null and b/docs/links/images/ixbt-messengers-2026.jpg differ diff --git a/docs/links/images/jacopococcia-simplex-top10.jpg b/docs/links/images/jacopococcia-simplex-top10.jpg new file mode 100644 index 0000000000..49163a5f80 Binary files /dev/null and b/docs/links/images/jacopococcia-simplex-top10.jpg differ diff --git a/docs/links/images/jornaldebrasilia-simplex-telegram.jpg b/docs/links/images/jornaldebrasilia-simplex-telegram.jpg new file mode 100644 index 0000000000..e06a18233c Binary files /dev/null and b/docs/links/images/jornaldebrasilia-simplex-telegram.jpg differ diff --git a/docs/links/images/joselito-simplex-vs-xmpp.jpg b/docs/links/images/joselito-simplex-vs-xmpp.jpg new file mode 100644 index 0000000000..bd33bd19d4 Binary files /dev/null and b/docs/links/images/joselito-simplex-vs-xmpp.jpg differ diff --git a/docs/links/images/kaiyuanapp-simplex.jpg b/docs/links/images/kaiyuanapp-simplex.jpg new file mode 100644 index 0000000000..999c2e1d5f Binary files /dev/null and b/docs/links/images/kaiyuanapp-simplex.jpg differ diff --git a/docs/links/images/kdroidwin-simplex-hatena-2.jpg b/docs/links/images/kdroidwin-simplex-hatena-2.jpg new file mode 100644 index 0000000000..2e51cef786 Binary files /dev/null and b/docs/links/images/kdroidwin-simplex-hatena-2.jpg differ diff --git a/docs/links/images/kdroidwin-simplex-hatena.jpg b/docs/links/images/kdroidwin-simplex-hatena.jpg new file mode 100644 index 0000000000..0be91eff23 Binary files /dev/null and b/docs/links/images/kdroidwin-simplex-hatena.jpg differ diff --git a/docs/links/images/kodnar-session-simplex.jpg b/docs/links/images/kodnar-session-simplex.jpg new file mode 100644 index 0000000000..2244d7b5c5 Binary files /dev/null and b/docs/links/images/kodnar-session-simplex.jpg differ diff --git a/docs/links/images/korben-wiki-messagerie.jpg b/docs/links/images/korben-wiki-messagerie.jpg new file mode 100644 index 0000000000..992a1642f5 Binary files /dev/null and b/docs/links/images/korben-wiki-messagerie.jpg differ diff --git a/docs/links/images/kr-labs-secure-messenger.jpg b/docs/links/images/kr-labs-secure-messenger.jpg new file mode 100644 index 0000000000..77a4d8d5bd Binary files /dev/null and b/docs/links/images/kr-labs-secure-messenger.jpg differ diff --git a/docs/links/images/kryptoanarchista-english-review.jpg b/docs/links/images/kryptoanarchista-english-review.jpg new file mode 100644 index 0000000000..3dd00b90b9 Binary files /dev/null and b/docs/links/images/kryptoanarchista-english-review.jpg differ diff --git a/docs/links/images/kryptoanarchista-simplex-revolution.jpg b/docs/links/images/kryptoanarchista-simplex-revolution.jpg new file mode 100644 index 0000000000..3dd00b90b9 Binary files /dev/null and b/docs/links/images/kryptoanarchista-simplex-revolution.jpg differ diff --git a/docs/links/images/kuketz-forum-simplex-security.jpg b/docs/links/images/kuketz-forum-simplex-security.jpg new file mode 100644 index 0000000000..90e5949483 Binary files /dev/null and b/docs/links/images/kuketz-forum-simplex-security.jpg differ diff --git a/docs/links/images/kuketz-group-chat-test.jpg b/docs/links/images/kuketz-group-chat-test.jpg new file mode 100644 index 0000000000..3a761b1b3b Binary files /dev/null and b/docs/links/images/kuketz-group-chat-test.jpg differ diff --git a/docs/links/images/kuketz-messenger-matrix.jpg b/docs/links/images/kuketz-messenger-matrix.jpg new file mode 100644 index 0000000000..3b33fad709 --- /dev/null +++ b/docs/links/images/kuketz-messenger-matrix.jpg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/links/images/kuketz-simplex-review.jpg b/docs/links/images/kuketz-simplex-review.jpg new file mode 100644 index 0000000000..3a761b1b3b Binary files /dev/null and b/docs/links/images/kuketz-simplex-review.jpg differ diff --git a/docs/links/images/kusaimara-session-vs-simplex.jpg b/docs/links/images/kusaimara-session-vs-simplex.jpg new file mode 100644 index 0000000000..2faf2e7ed6 Binary files /dev/null and b/docs/links/images/kusaimara-session-vs-simplex.jpg differ diff --git a/docs/links/images/kusaimara-simplex-3.jpg b/docs/links/images/kusaimara-simplex-3.jpg new file mode 100644 index 0000000000..b61c917779 Binary files /dev/null and b/docs/links/images/kusaimara-simplex-3.jpg differ diff --git a/docs/links/images/kusaimara-simplex-first-impressions.jpg b/docs/links/images/kusaimara-simplex-first-impressions.jpg new file mode 100644 index 0000000000..17e4c05bbc Binary files /dev/null and b/docs/links/images/kusaimara-simplex-first-impressions.jpg differ diff --git a/docs/links/images/lealternative-anonymous-apps.jpg b/docs/links/images/lealternative-anonymous-apps.jpg new file mode 100644 index 0000000000..52ab3d75e5 Binary files /dev/null and b/docs/links/images/lealternative-anonymous-apps.jpg differ diff --git a/docs/links/images/lealternative-simplex-review.jpg b/docs/links/images/lealternative-simplex-review.jpg new file mode 100644 index 0000000000..dc49ce2467 Binary files /dev/null and b/docs/links/images/lealternative-simplex-review.jpg differ diff --git a/docs/links/images/lemedia05-simplex-100.jpg b/docs/links/images/lemedia05-simplex-100.jpg new file mode 100644 index 0000000000..285311a0b5 Binary files /dev/null and b/docs/links/images/lemedia05-simplex-100.jpg differ diff --git a/docs/links/images/lemedia05-smartphone.jpg b/docs/links/images/lemedia05-smartphone.jpg new file mode 100644 index 0000000000..285311a0b5 Binary files /dev/null and b/docs/links/images/lemedia05-smartphone.jpg differ diff --git a/docs/links/images/libreselfhosted-simplex-overview.jpg b/docs/links/images/libreselfhosted-simplex-overview.jpg new file mode 100644 index 0000000000..dad31c112e --- /dev/null +++ b/docs/links/images/libreselfhosted-simplex-overview.jpg @@ -0,0 +1 @@ +downloads: 1Mdownloads1M \ No newline at end of file diff --git a/docs/links/images/linkedin-pt-simplex.jpg b/docs/links/images/linkedin-pt-simplex.jpg new file mode 100644 index 0000000000..bfa148b0c9 --- /dev/null +++ b/docs/links/images/linkedin-pt-simplex.jpg @@ -0,0 +1,8 @@ + diff --git a/docs/links/images/linux-magazin-simplex-privacy.jpg b/docs/links/images/linux-magazin-simplex-privacy.jpg new file mode 100644 index 0000000000..8d0e5b67eb Binary files /dev/null and b/docs/links/images/linux-magazin-simplex-privacy.jpg differ diff --git a/docs/links/images/livecoins-vitalik-simplex.jpg b/docs/links/images/livecoins-vitalik-simplex.jpg new file mode 100644 index 0000000000..1be58f86dc Binary files /dev/null and b/docs/links/images/livecoins-vitalik-simplex.jpg differ diff --git a/docs/links/images/marius-privacy-messengers-overview.jpg b/docs/links/images/marius-privacy-messengers-overview.jpg new file mode 100644 index 0000000000..2b58527f08 Binary files /dev/null and b/docs/links/images/marius-privacy-messengers-overview.jpg differ diff --git a/docs/links/images/matters-simplex-telegram-comparison.jpg b/docs/links/images/matters-simplex-telegram-comparison.jpg new file mode 100644 index 0000000000..0b73e2b773 Binary files /dev/null and b/docs/links/images/matters-simplex-telegram-comparison.jpg differ diff --git a/docs/links/images/midia-segura-simplex-review.jpg b/docs/links/images/midia-segura-simplex-review.jpg new file mode 100644 index 0000000000..604ef75151 Binary files /dev/null and b/docs/links/images/midia-segura-simplex-review.jpg differ diff --git a/docs/links/images/monerotopia-2026-simplex.jpg b/docs/links/images/monerotopia-2026-simplex.jpg new file mode 100644 index 0000000000..d30d1cd357 Binary files /dev/null and b/docs/links/images/monerotopia-2026-simplex.jpg differ diff --git a/docs/links/images/nbtv-simplex-review.jpg b/docs/links/images/nbtv-simplex-review.jpg new file mode 100644 index 0000000000..92a58f7f53 Binary files /dev/null and b/docs/links/images/nbtv-simplex-review.jpg differ diff --git a/docs/links/images/nemental-simplex-tor-guide.jpg b/docs/links/images/nemental-simplex-tor-guide.jpg new file mode 100644 index 0000000000..a616e7367b Binary files /dev/null and b/docs/links/images/nemental-simplex-tor-guide.jpg differ diff --git a/docs/links/images/netxhack-telegram-alternatives.jpg b/docs/links/images/netxhack-telegram-alternatives.jpg new file mode 100644 index 0000000000..f97aeedc1e Binary files /dev/null and b/docs/links/images/netxhack-telegram-alternatives.jpg differ diff --git a/docs/links/images/neweconomy-buterin-simplex.jpg b/docs/links/images/neweconomy-buterin-simplex.jpg new file mode 100644 index 0000000000..ac252bd168 Binary files /dev/null and b/docs/links/images/neweconomy-buterin-simplex.jpg differ diff --git a/docs/links/images/nicolas-forcet-comparison.jpg b/docs/links/images/nicolas-forcet-comparison.jpg new file mode 100644 index 0000000000..08b1f9223f Binary files /dev/null and b/docs/links/images/nicolas-forcet-comparison.jpg differ diff --git a/docs/links/images/niebezpiecznik-simplex-mention.jpg b/docs/links/images/niebezpiecznik-simplex-mention.jpg new file mode 100644 index 0000000000..e7ef78ee71 Binary files /dev/null and b/docs/links/images/niebezpiecznik-simplex-mention.jpg differ diff --git a/docs/links/images/nobsbitcoin-funding-v60.jpg b/docs/links/images/nobsbitcoin-funding-v60.jpg new file mode 100644 index 0000000000..9edfdabf14 Binary files /dev/null and b/docs/links/images/nobsbitcoin-funding-v60.jpg differ diff --git a/docs/links/images/nobsbitcoin-startos.jpg b/docs/links/images/nobsbitcoin-startos.jpg new file mode 100644 index 0000000000..d3d5b6501e Binary files /dev/null and b/docs/links/images/nobsbitcoin-startos.jpg differ diff --git a/docs/links/images/nobsbitcoin-v52-receipts.jpg b/docs/links/images/nobsbitcoin-v52-receipts.jpg new file mode 100644 index 0000000000..0570d3d227 Binary files /dev/null and b/docs/links/images/nobsbitcoin-v52-receipts.jpg differ diff --git a/docs/links/images/nobsbitcoin-v53-desktop.jpg b/docs/links/images/nobsbitcoin-v53-desktop.jpg new file mode 100644 index 0000000000..ff5c8d1413 Binary files /dev/null and b/docs/links/images/nobsbitcoin-v53-desktop.jpg differ diff --git a/docs/links/images/nobsbitcoin-v54-desktop.jpg b/docs/links/images/nobsbitcoin-v54-desktop.jpg new file mode 100644 index 0000000000..88733181aa Binary files /dev/null and b/docs/links/images/nobsbitcoin-v54-desktop.jpg differ diff --git a/docs/links/images/nobsbitcoin-v57-quantum.jpg b/docs/links/images/nobsbitcoin-v57-quantum.jpg new file mode 100644 index 0000000000..fafa208ea5 Binary files /dev/null and b/docs/links/images/nobsbitcoin-v57-quantum.jpg differ diff --git a/docs/links/images/nobsbitcoin-v58-routing.jpg b/docs/links/images/nobsbitcoin-v58-routing.jpg new file mode 100644 index 0000000000..69a9bed4ca Binary files /dev/null and b/docs/links/images/nobsbitcoin-v58-routing.jpg differ diff --git a/docs/links/images/nobsbitcoin-v61.jpg b/docs/links/images/nobsbitcoin-v61.jpg new file mode 100644 index 0000000000..166fa11537 Binary files /dev/null and b/docs/links/images/nobsbitcoin-v61.jpg differ diff --git a/docs/links/images/notebookcheck-cn-simplex.jpg b/docs/links/images/notebookcheck-cn-simplex.jpg new file mode 100644 index 0000000000..bce52f2e94 Binary files /dev/null and b/docs/links/images/notebookcheck-cn-simplex.jpg differ diff --git a/docs/links/images/notebookcheck-pt-simplex.jpg b/docs/links/images/notebookcheck-pt-simplex.jpg new file mode 100644 index 0000000000..bce52f2e94 Binary files /dev/null and b/docs/links/images/notebookcheck-pt-simplex.jpg differ diff --git a/docs/links/images/notebookcheck-ru-simplex.jpg b/docs/links/images/notebookcheck-ru-simplex.jpg new file mode 100644 index 0000000000..edfdd31a33 Binary files /dev/null and b/docs/links/images/notebookcheck-ru-simplex.jpg differ diff --git a/docs/links/images/notebookcheck-simplex-succeeds.jpg b/docs/links/images/notebookcheck-simplex-succeeds.jpg new file mode 100644 index 0000000000..bce52f2e94 Binary files /dev/null and b/docs/links/images/notebookcheck-simplex-succeeds.jpg differ diff --git a/docs/links/images/notecom-deeplife-simplex.jpg b/docs/links/images/notecom-deeplife-simplex.jpg new file mode 100644 index 0000000000..9f3915bc7b Binary files /dev/null and b/docs/links/images/notecom-deeplife-simplex.jpg differ diff --git a/docs/links/images/noticiasnavarra-simplex-criminologist.jpg b/docs/links/images/noticiasnavarra-simplex-criminologist.jpg new file mode 100644 index 0000000000..9dae7ea2e7 Binary files /dev/null and b/docs/links/images/noticiasnavarra-simplex-criminologist.jpg differ diff --git a/docs/links/images/nowhere-moe-simplex-servers.jpg b/docs/links/images/nowhere-moe-simplex-servers.jpg new file mode 100644 index 0000000000..8db82ac277 Binary files /dev/null and b/docs/links/images/nowhere-moe-simplex-servers.jpg differ diff --git a/docs/links/images/opennet-simplex-65.jpg b/docs/links/images/opennet-simplex-65.jpg new file mode 100644 index 0000000000..3e6e6a0ded Binary files /dev/null and b/docs/links/images/opennet-simplex-65.jpg differ diff --git a/docs/links/images/opentech-guru-simplex.jpg b/docs/links/images/opentech-guru-simplex.jpg new file mode 100644 index 0000000000..756b411086 Binary files /dev/null and b/docs/links/images/opentech-guru-simplex.jpg differ diff --git a/docs/links/images/oppet-moln-simplex.jpg b/docs/links/images/oppet-moln-simplex.jpg new file mode 100644 index 0000000000..76ce1ab4f6 Binary files /dev/null and b/docs/links/images/oppet-moln-simplex.jpg differ diff --git a/docs/links/images/optout-improving-simplex.jpg b/docs/links/images/optout-improving-simplex.jpg new file mode 100644 index 0000000000..1ebff7959d Binary files /dev/null and b/docs/links/images/optout-improving-simplex.jpg differ diff --git a/docs/links/images/optout-simplex-s3e02.jpg b/docs/links/images/optout-simplex-s3e02.jpg new file mode 100644 index 0000000000..1ebff7959d Binary files /dev/null and b/docs/links/images/optout-simplex-s3e02.jpg differ diff --git a/docs/links/images/paflegeek-simplex-selfhost.jpg b/docs/links/images/paflegeek-simplex-selfhost.jpg new file mode 100644 index 0000000000..4d757dcb19 Binary files /dev/null and b/docs/links/images/paflegeek-simplex-selfhost.jpg differ diff --git a/docs/links/images/panorama-simplex-ultra-secret.jpg b/docs/links/images/panorama-simplex-ultra-secret.jpg new file mode 100644 index 0000000000..08a70ae03b Binary files /dev/null and b/docs/links/images/panorama-simplex-ultra-secret.jpg differ diff --git a/docs/links/images/paskoocheh-simplex-iran.jpg b/docs/links/images/paskoocheh-simplex-iran.jpg new file mode 100644 index 0000000000..df34741395 Binary files /dev/null and b/docs/links/images/paskoocheh-simplex-iran.jpg differ diff --git a/docs/links/images/peertube-uno-simplex.jpg b/docs/links/images/peertube-uno-simplex.jpg new file mode 100644 index 0000000000..afe8400f01 Binary files /dev/null and b/docs/links/images/peertube-uno-simplex.jpg differ diff --git a/docs/links/images/portuguese-simplex-hidden-portal.jpg b/docs/links/images/portuguese-simplex-hidden-portal.jpg new file mode 100644 index 0000000000..68315d9221 Binary files /dev/null and b/docs/links/images/portuguese-simplex-hidden-portal.jpg differ diff --git a/docs/links/images/portuguese-simplex-revolutionary.jpg b/docs/links/images/portuguese-simplex-revolutionary.jpg new file mode 100644 index 0000000000..c52c33bdd6 Binary files /dev/null and b/docs/links/images/portuguese-simplex-revolutionary.jpg differ diff --git a/docs/links/images/portuguese-simplex-tutorial.jpg b/docs/links/images/portuguese-simplex-tutorial.jpg new file mode 100644 index 0000000000..4c1462ec4f Binary files /dev/null and b/docs/links/images/portuguese-simplex-tutorial.jpg differ diff --git a/docs/links/images/portuguese-simplex-ultra-annihilation.jpg b/docs/links/images/portuguese-simplex-ultra-annihilation.jpg new file mode 100644 index 0000000000..778f78ddd7 Binary files /dev/null and b/docs/links/images/portuguese-simplex-ultra-annihilation.jpg differ diff --git a/docs/links/images/pplware-simplex-telegram.jpg b/docs/links/images/pplware-simplex-telegram.jpg new file mode 100644 index 0000000000..255dd9c7d5 Binary files /dev/null and b/docs/links/images/pplware-simplex-telegram.jpg differ diff --git a/docs/links/images/prihor-simplex-guide.jpg b/docs/links/images/prihor-simplex-guide.jpg new file mode 100644 index 0000000000..fef6aa033d Binary files /dev/null and b/docs/links/images/prihor-simplex-guide.jpg differ diff --git a/docs/links/images/privacy-guides-recommendation.jpg b/docs/links/images/privacy-guides-recommendation.jpg new file mode 100644 index 0000000000..df2c4d9785 Binary files /dev/null and b/docs/links/images/privacy-guides-recommendation.jpg differ diff --git a/docs/links/images/pro32-best-messengers-2026.jpg b/docs/links/images/pro32-best-messengers-2026.jpg new file mode 100644 index 0000000000..9c9b67688c Binary files /dev/null and b/docs/links/images/pro32-best-messengers-2026.jpg differ diff --git a/docs/links/images/programista-pasji-simplex.jpg b/docs/links/images/programista-pasji-simplex.jpg new file mode 100644 index 0000000000..82f2bf75fe Binary files /dev/null and b/docs/links/images/programista-pasji-simplex.jpg differ diff --git a/docs/links/images/questona-encrypted-chat.jpg b/docs/links/images/questona-encrypted-chat.jpg new file mode 100644 index 0000000000..0ffe1e69e7 Binary files /dev/null and b/docs/links/images/questona-encrypted-chat.jpg differ diff --git a/docs/links/images/reclaimthenet-ip-privacy.jpg b/docs/links/images/reclaimthenet-ip-privacy.jpg new file mode 100644 index 0000000000..ee5e83fbf2 Binary files /dev/null and b/docs/links/images/reclaimthenet-ip-privacy.jpg differ diff --git a/docs/links/images/reclaimthenet-quantum-beta.jpg b/docs/links/images/reclaimthenet-quantum-beta.jpg new file mode 100644 index 0000000000..6135f8209d Binary files /dev/null and b/docs/links/images/reclaimthenet-quantum-beta.jpg differ diff --git a/docs/links/images/renaro-signal-session-simplex.jpg b/docs/links/images/renaro-signal-session-simplex.jpg new file mode 100644 index 0000000000..9c04021c7d Binary files /dev/null and b/docs/links/images/renaro-signal-session-simplex.jpg differ diff --git a/docs/links/images/robosats-simplex-bot.jpg b/docs/links/images/robosats-simplex-bot.jpg new file mode 100644 index 0000000000..5d6720bd18 Binary files /dev/null and b/docs/links/images/robosats-simplex-bot.jpg differ diff --git a/docs/links/images/russian-paranoid-messenger-tutorial.jpg b/docs/links/images/russian-paranoid-messenger-tutorial.jpg new file mode 100644 index 0000000000..da219f96e3 Binary files /dev/null and b/docs/links/images/russian-paranoid-messenger-tutorial.jpg differ diff --git a/docs/links/images/russian-simplex-anonymous-no-id.jpg b/docs/links/images/russian-simplex-anonymous-no-id.jpg new file mode 100644 index 0000000000..f388896955 Binary files /dev/null and b/docs/links/images/russian-simplex-anonymous-no-id.jpg differ diff --git a/docs/links/images/russian-simplex-max-protection.jpg b/docs/links/images/russian-simplex-max-protection.jpg new file mode 100644 index 0000000000..ff9ecfe6ba Binary files /dev/null and b/docs/links/images/russian-simplex-max-protection.jpg differ diff --git a/docs/links/images/russian-simplex-overview-functions.jpg b/docs/links/images/russian-simplex-overview-functions.jpg new file mode 100644 index 0000000000..6f8f628133 Binary files /dev/null and b/docs/links/images/russian-simplex-overview-functions.jpg differ diff --git a/docs/links/images/rutube-simplex-video.jpg b/docs/links/images/rutube-simplex-video.jpg new file mode 100644 index 0000000000..aa7d416855 Binary files /dev/null and b/docs/links/images/rutube-simplex-video.jpg differ diff --git a/docs/links/images/security-nl-simplex-users.jpg b/docs/links/images/security-nl-simplex-users.jpg new file mode 100644 index 0000000000..7de10eff2d Binary files /dev/null and b/docs/links/images/security-nl-simplex-users.jpg differ diff --git a/docs/links/images/securityinabox-simplex-turkish.jpg b/docs/links/images/securityinabox-simplex-turkish.jpg new file mode 100644 index 0000000000..230b3e1463 Binary files /dev/null and b/docs/links/images/securityinabox-simplex-turkish.jpg differ diff --git a/docs/links/images/selfhosted-simplex-tutorial.jpg b/docs/links/images/selfhosted-simplex-tutorial.jpg new file mode 100644 index 0000000000..d534a25095 Binary files /dev/null and b/docs/links/images/selfhosted-simplex-tutorial.jpg differ diff --git a/docs/links/images/selfhosty-simplex-signal.jpg b/docs/links/images/selfhosty-simplex-signal.jpg new file mode 100644 index 0000000000..85d9f24b4c Binary files /dev/null and b/docs/links/images/selfhosty-simplex-signal.jpg differ diff --git a/docs/links/images/serokell-haskell-simplex.jpg b/docs/links/images/serokell-haskell-simplex.jpg new file mode 100644 index 0000000000..7ea448125c Binary files /dev/null and b/docs/links/images/serokell-haskell-simplex.jpg differ diff --git a/docs/links/images/sethforprivacy-privacy-steps.jpg b/docs/links/images/sethforprivacy-privacy-steps.jpg new file mode 100644 index 0000000000..c2210ca2fa Binary files /dev/null and b/docs/links/images/sethforprivacy-privacy-steps.jpg differ diff --git a/docs/links/images/simplex-messaging-perfect-privacy.jpg b/docs/links/images/simplex-messaging-perfect-privacy.jpg new file mode 100644 index 0000000000..d71d4139ed Binary files /dev/null and b/docs/links/images/simplex-messaging-perfect-privacy.jpg differ diff --git a/docs/links/images/simplex-power-to-people-livestream.jpg b/docs/links/images/simplex-power-to-people-livestream.jpg new file mode 100644 index 0000000000..b4a3d03db7 Binary files /dev/null and b/docs/links/images/simplex-power-to-people-livestream.jpg differ diff --git a/docs/links/images/simplex-status-bot.jpg b/docs/links/images/simplex-status-bot.jpg new file mode 100644 index 0000000000..033a8cfcc4 Binary files /dev/null and b/docs/links/images/simplex-status-bot.jpg differ diff --git a/docs/links/images/simplex-themes-archive.jpg b/docs/links/images/simplex-themes-archive.jpg new file mode 100644 index 0000000000..c6e8afda19 Binary files /dev/null and b/docs/links/images/simplex-themes-archive.jpg differ diff --git a/docs/links/images/simplex-unusually-good-privacy.jpg b/docs/links/images/simplex-unusually-good-privacy.jpg new file mode 100644 index 0000000000..9059fc7a28 Binary files /dev/null and b/docs/links/images/simplex-unusually-good-privacy.jpg differ diff --git a/docs/links/images/simplex-whatsapp-libertario.jpg b/docs/links/images/simplex-whatsapp-libertario.jpg new file mode 100644 index 0000000000..8048e590ec Binary files /dev/null and b/docs/links/images/simplex-whatsapp-libertario.jpg differ diff --git a/docs/links/images/soberano-simplex-english.jpg b/docs/links/images/soberano-simplex-english.jpg new file mode 100644 index 0000000000..08c28cd2cc Binary files /dev/null and b/docs/links/images/soberano-simplex-english.jpg differ diff --git a/docs/links/images/soberano-simplex-guide.jpg b/docs/links/images/soberano-simplex-guide.jpg new file mode 100644 index 0000000000..08c28cd2cc Binary files /dev/null and b/docs/links/images/soberano-simplex-guide.jpg differ diff --git a/docs/links/images/sofwul-simplex-contact.jpg b/docs/links/images/sofwul-simplex-contact.jpg new file mode 100644 index 0000000000..b508ba4696 Binary files /dev/null and b/docs/links/images/sofwul-simplex-contact.jpg differ diff --git a/docs/links/images/spanish-simplex-sin-identificadores.jpg b/docs/links/images/spanish-simplex-sin-identificadores.jpg new file mode 100644 index 0000000000..6294b77977 Binary files /dev/null and b/docs/links/images/spanish-simplex-sin-identificadores.jpg differ diff --git a/docs/links/images/spanish-simplex-ultra-private.jpg b/docs/links/images/spanish-simplex-ultra-private.jpg new file mode 100644 index 0000000000..8b43c390cc Binary files /dev/null and b/docs/links/images/spanish-simplex-ultra-private.jpg differ diff --git a/docs/links/images/splintercon-simplex-listing.jpg b/docs/links/images/splintercon-simplex-listing.jpg new file mode 100644 index 0000000000..2d6c790fdc Binary files /dev/null and b/docs/links/images/splintercon-simplex-listing.jpg differ diff --git a/docs/links/images/stackuj-luptak-podcast.jpg b/docs/links/images/stackuj-luptak-podcast.jpg new file mode 100644 index 0000000000..d0c8a6c5ff Binary files /dev/null and b/docs/links/images/stackuj-luptak-podcast.jpg differ diff --git a/docs/links/images/start9-simplex-startos.jpg b/docs/links/images/start9-simplex-startos.jpg new file mode 100644 index 0000000000..06933341aa Binary files /dev/null and b/docs/links/images/start9-simplex-startos.jpg differ diff --git a/docs/links/images/syskb-simplex-643.jpg b/docs/links/images/syskb-simplex-643.jpg new file mode 100644 index 0000000000..f58e1924c6 Binary files /dev/null and b/docs/links/images/syskb-simplex-643.jpg differ diff --git a/docs/links/images/tabnews-simplex-first-messenger.jpg b/docs/links/images/tabnews-simplex-first-messenger.jpg new file mode 100644 index 0000000000..430be61d03 Binary files /dev/null and b/docs/links/images/tabnews-simplex-first-messenger.jpg differ diff --git a/docs/links/images/tarnkappe-simplex-1-0.jpg b/docs/links/images/tarnkappe-simplex-1-0.jpg new file mode 100644 index 0000000000..e301c52546 Binary files /dev/null and b/docs/links/images/tarnkappe-simplex-1-0.jpg differ diff --git a/docs/links/images/taurix-simplex-hosting.jpg b/docs/links/images/taurix-simplex-hosting.jpg new file mode 100644 index 0000000000..d92596e37f Binary files /dev/null and b/docs/links/images/taurix-simplex-hosting.jpg differ diff --git a/docs/links/images/te-st-simplex-review.jpg b/docs/links/images/te-st-simplex-review.jpg new file mode 100644 index 0000000000..ed8ed65657 Binary files /dev/null and b/docs/links/images/te-st-simplex-review.jpg differ diff --git a/docs/links/images/techflow-vitalik-simplex.jpg b/docs/links/images/techflow-vitalik-simplex.jpg new file mode 100644 index 0000000000..221bcd4e77 Binary files /dev/null and b/docs/links/images/techflow-vitalik-simplex.jpg differ diff --git a/docs/links/images/techlore-recommend-simplex.jpg b/docs/links/images/techlore-recommend-simplex.jpg new file mode 100644 index 0000000000..7d7f4c4df4 Binary files /dev/null and b/docs/links/images/techlore-recommend-simplex.jpg differ diff --git a/docs/links/images/techlore-talks-simplex-interview.jpg b/docs/links/images/techlore-talks-simplex-interview.jpg new file mode 100644 index 0000000000..e8f8f2fd48 Binary files /dev/null and b/docs/links/images/techlore-talks-simplex-interview.jpg differ diff --git a/docs/links/images/tecmundo-simplex-telegram.jpg b/docs/links/images/tecmundo-simplex-telegram.jpg new file mode 100644 index 0000000000..02d83f888a Binary files /dev/null and b/docs/links/images/tecmundo-simplex-telegram.jpg differ diff --git a/docs/links/images/tencent-news-crypto-2025.jpg b/docs/links/images/tencent-news-crypto-2025.jpg new file mode 100644 index 0000000000..82176f1228 Binary files /dev/null and b/docs/links/images/tencent-news-crypto-2025.jpg differ diff --git a/docs/links/images/tudongchat-simplex-vietnam.jpg b/docs/links/images/tudongchat-simplex-vietnam.jpg new file mode 100644 index 0000000000..f905f91614 Binary files /dev/null and b/docs/links/images/tudongchat-simplex-vietnam.jpg differ diff --git a/docs/links/images/tugatech-simplex-telegram.jpg b/docs/links/images/tugatech-simplex-telegram.jpg new file mode 100644 index 0000000000..e0e938313c Binary files /dev/null and b/docs/links/images/tugatech-simplex-telegram.jpg differ diff --git a/docs/links/images/tuta-whatsapp-alternatives.jpg b/docs/links/images/tuta-whatsapp-alternatives.jpg new file mode 100644 index 0000000000..42a82d1205 Binary files /dev/null and b/docs/links/images/tuta-whatsapp-alternatives.jpg differ diff --git a/docs/links/images/v2ex-im-china.jpg b/docs/links/images/v2ex-im-china.jpg new file mode 100644 index 0000000000..eebefd1455 Binary files /dev/null and b/docs/links/images/v2ex-im-china.jpg differ diff --git a/docs/links/images/v2ex-simplex-telegram-100x.jpg b/docs/links/images/v2ex-simplex-telegram-100x.jpg new file mode 100644 index 0000000000..a5541af8d3 Binary files /dev/null and b/docs/links/images/v2ex-simplex-telegram-100x.jpg differ diff --git a/docs/links/images/vc-ru-simplex.jpg b/docs/links/images/vc-ru-simplex.jpg new file mode 100644 index 0000000000..2df513e9e3 Binary files /dev/null and b/docs/links/images/vc-ru-simplex.jpg differ diff --git a/docs/links/images/verasoul-simplex-review.jpg b/docs/links/images/verasoul-simplex-review.jpg new file mode 100644 index 0000000000..e4c609da1b Binary files /dev/null and b/docs/links/images/verasoul-simplex-review.jpg differ diff --git a/docs/links/images/vpn-taizen-simplex-guide.jpg b/docs/links/images/vpn-taizen-simplex-guide.jpg new file mode 100644 index 0000000000..6eafe62eb3 Binary files /dev/null and b/docs/links/images/vpn-taizen-simplex-guide.jpg differ diff --git a/docs/links/images/webappsmagazine-simplex-anonymity.jpg b/docs/links/images/webappsmagazine-simplex-anonymity.jpg new file mode 100644 index 0000000000..2e532baa13 Binary files /dev/null and b/docs/links/images/webappsmagazine-simplex-anonymity.jpg differ diff --git a/docs/links/images/whonix-simplex-recommendation.jpg b/docs/links/images/whonix-simplex-recommendation.jpg new file mode 100644 index 0000000000..dadc207bd9 Binary files /dev/null and b/docs/links/images/whonix-simplex-recommendation.jpg differ diff --git a/docs/links/images/wwwhatsnew-simplex.jpg b/docs/links/images/wwwhatsnew-simplex.jpg new file mode 100644 index 0000000000..5a1e7b6eb4 Binary files /dev/null and b/docs/links/images/wwwhatsnew-simplex.jpg differ diff --git a/docs/links/images/wykop-simplex-dsa.jpg b/docs/links/images/wykop-simplex-dsa.jpg new file mode 100644 index 0000000000..7884b362e7 Binary files /dev/null and b/docs/links/images/wykop-simplex-dsa.jpg differ diff --git a/docs/links/images/xakep-simplex-signal-brothers.jpg b/docs/links/images/xakep-simplex-signal-brothers.jpg new file mode 100644 index 0000000000..9ccb6b1e2e Binary files /dev/null and b/docs/links/images/xakep-simplex-signal-brothers.jpg differ diff --git a/docs/links/images/yahoo-finance-buterin.jpg b/docs/links/images/yahoo-finance-buterin.jpg new file mode 100644 index 0000000000..6c8d0bfbad Binary files /dev/null and b/docs/links/images/yahoo-finance-buterin.jpg differ diff --git a/docs/links/images/youtube-best-private-messenger.jpg b/docs/links/images/youtube-best-private-messenger.jpg new file mode 100644 index 0000000000..ff2c2e134c Binary files /dev/null and b/docs/links/images/youtube-best-private-messenger.jpg differ diff --git a/docs/links/images/youtube-best-secure-2025.jpg b/docs/links/images/youtube-best-secure-2025.jpg new file mode 100644 index 0000000000..78d379f145 Binary files /dev/null and b/docs/links/images/youtube-best-secure-2025.jpg differ diff --git a/docs/links/images/youtube-simplex-review-2024.jpg b/docs/links/images/youtube-simplex-review-2024.jpg new file mode 100644 index 0000000000..08a57214ef Binary files /dev/null and b/docs/links/images/youtube-simplex-review-2024.jpg differ diff --git a/docs/links/images/youtube-simplex-tutorial.jpg b/docs/links/images/youtube-simplex-tutorial.jpg new file mode 100644 index 0000000000..0e94c8fc58 Binary files /dev/null and b/docs/links/images/youtube-simplex-tutorial.jpg differ diff --git a/docs/links/images/zhousa-simplex-review.jpg b/docs/links/images/zhousa-simplex-review.jpg new file mode 100644 index 0000000000..6712c3bf7e Binary files /dev/null and b/docs/links/images/zhousa-simplex-review.jpg differ diff --git a/docs/protocol/channels-protocol.md b/docs/protocol/channels-protocol.md index b6b9b3ee5b..6a232ea2ff 100644 --- a/docs/protocol/channels-protocol.md +++ b/docs/protocol/channels-protocol.md @@ -72,6 +72,20 @@ When the owner adds a relay to an existing channel: The announce is an optimisation. When it does not reach a subscriber — because the channel had no subscribers at announce time, because an older client or relay sits in the path, or because of a transient network failure — the subscriber reaches the same end state on the next channel open via its relay sync against the channel's link data. +### Relay rejection + +When a relay operator removes the relay from a channel, the relay marks the channel as rejected and refuses future invitations from the same channel link: + +1. **Leave.** The relay operator runs `/leave #channel`. The relay marks the channel as rejected locally, keyed by the channel's short link. + +2. **Refuse.** When the owner later sends `x.grp.relay.inv` for the same channel link — typically from a re-invitation — the relay does not accept the invitation as a relay. Instead it replies with `x.grp.relay.reject` over the owner-relay direct contact channel, carrying a rejection reason. The current reason is `rejoin_rejected`; older relays or future reasons fall through to an unknown reason for forward compatibility. + +3. **Owner handling.** The owner marks the corresponding relay as rejected and notifies the operator UI. The owner also sets the relay member's status to `GSMemLeft` so the UI treats the rejected relay identically to one that ran `/leave`. The owner's next user-initiated relay addition for the same channel creates a fresh invitation, which the relay rejects again unless the rejection has been cleared. + +4. **Clear.** The relay operator runs `/group allow ` to clear the rejection for the channel. After the next user-initiated relay addition, the relay accepts the invitation and rejoins as a relay. + +An older owner client that does not recognise `x.grp.relay.reject` ignores the message and leaves the relay invitation in an invited state indefinitely — the same end state as a relay that does not respond. An older relay binary does not enforce rejection; in mixed-version deployments the operator can re-run `/leave` under the new binary to re-establish rejection. + ### Subscriber connection A subscriber joins a channel through the following flow: diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json index 1d5eb5197c..c929125033 100644 --- a/packages/simplex-chat-client/types/typescript/package.json +++ b/packages/simplex-chat-client/types/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@simplex-chat/types", - "version": "0.6.0", + "version": "0.7.0", "description": "TypeScript types for SimpleX Chat bot libraries", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index f8aa6e445d..d1b89ffe27 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -387,6 +387,20 @@ export namespace APIAddGroupRelays { } } +// Clear relay rejection for a channel (relay operator). +// Network usage: background. +export interface APIAllowRelayGroup { + groupId: number // int64 +} + +export namespace APIAllowRelayGroup { + export type Response = CR.RelayGroupAllowed | CR.ChatCmdError + + export function cmdString(self: APIAllowRelayGroup): string { + return '/_relay allow #' + self.groupId + } +} + // Update group profile. // Network usage: background. export interface APIUpdateGroupProfile { diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts index e4284bf87e..0fcf0e6eca 100644 --- a/packages/simplex-chat-client/types/typescript/src/responses.ts +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -32,6 +32,7 @@ export type ChatResponse = | CR.GroupRelays | CR.GroupRelaysAdded | CR.GroupRelaysAddFailed + | CR.RelayGroupAllowed | CR.GroupMembers | CR.GroupUpdated | CR.GroupsList @@ -89,6 +90,7 @@ export namespace CR { | "groupRelays" | "groupRelaysAdded" | "groupRelaysAddFailed" + | "relayGroupAllowed" | "groupMembers" | "groupUpdated" | "groupsList" @@ -293,6 +295,12 @@ export namespace CR { addRelayResults: T.AddRelayResult[] } + export interface RelayGroupAllowed extends Interface { + type: "relayGroupAllowed" + user: T.User + groupInfo: T.GroupInfo + } + export interface GroupMembers extends Interface { type: "groupMembers" user: T.User diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 64a8b49502..7e618e05c8 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -3751,6 +3751,7 @@ export enum RelayStatus { Accepted = "accepted", Active = "active", Inactive = "inactive", + Rejected = "rejected", } export enum ReportReason { diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index c5cc255722..5166283e75 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "simplex-chat", - "version": "6.5.1", + "version": "6.5.2", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ @@ -24,7 +24,7 @@ "docs": "typedoc" }, "dependencies": { - "@simplex-chat/types": "^0.6.0", + "@simplex-chat/types": "^0.7.0", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.3", "node-addon-api": "^8.5.0" diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js index 5c1b70cda0..db042d48a2 100644 --- a/packages/simplex-chat-nodejs/src/download-libs.js +++ b/packages/simplex-chat-nodejs/src/download-libs.js @@ -4,7 +4,7 @@ const path = require('path'); const extract = require('extract-zip'); const GITHUB_REPO = 'simplex-chat/simplex-chat-libs'; -const RELEASE_TAG = 'v6.5.1'; +const RELEASE_TAG = 'v6.5.2'; const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase(); if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') { diff --git a/packages/simplex-chat-python/src/simplex_chat/_version.py b/packages/simplex-chat-python/src/simplex_chat/_version.py index 0468b65dd9..bd182d0240 100644 --- a/packages/simplex-chat-python/src/simplex_chat/_version.py +++ b/packages/simplex-chat-python/src/simplex_chat/_version.py @@ -2,8 +2,8 @@ simplex-chat-libs release tag we depend on. Bump both together for normal releases. For wrapper-only fixes use a PEP 440 -post-release: __version__ = "6.5.1.post1", LIBS_VERSION unchanged. +post-release: __version__ = "6.5.2.post1", LIBS_VERSION unchanged. """ -__version__ = "6.5.1" # PEP 440 — read by hatchling for wheel metadata -LIBS_VERSION = "6.5.1" # simplex-chat-libs release tag (no 'v' prefix) +__version__ = "6.5.2" # PEP 440 — read by hatchling for wheel metadata +LIBS_VERSION = "6.5.2" # simplex-chat-libs release tag (no 'v' prefix) diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py index 9806388835..3847f44811 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py @@ -340,6 +340,18 @@ def APIAddGroupRelays_cmd_string(self: APIAddGroupRelays) -> str: APIAddGroupRelays_Response = CR.GroupRelaysAdded | CR.GroupRelaysAddFailed | CR.ChatCmdError +# Clear relay rejection for a channel (relay operator). +# Network usage: background. +class APIAllowRelayGroup(TypedDict): + groupId: int # int64 + + +def APIAllowRelayGroup_cmd_string(self: APIAllowRelayGroup) -> str: + return '/_relay allow #' + str(self['groupId']) + +APIAllowRelayGroup_Response = CR.RelayGroupAllowed | CR.ChatCmdError + + # Update group profile. # Network usage: background. class APIUpdateGroupProfile(TypedDict): diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_responses.py b/packages/simplex-chat-python/src/simplex_chat/types/_responses.py index 84d0f1c79f..e85de02c78 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_responses.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_responses.py @@ -149,6 +149,11 @@ class GroupRelaysAddFailed(TypedDict): user: "T.User" addRelayResults: list["T.AddRelayResult"] +class RelayGroupAllowed(TypedDict): + type: Literal["relayGroupAllowed"] + user: "T.User" + groupInfo: "T.GroupInfo" + class GroupMembers(TypedDict): type: Literal["groupMembers"] user: "T.User" @@ -329,6 +334,7 @@ ChatResponse = ( | GroupRelays | GroupRelaysAdded | GroupRelaysAddFailed + | RelayGroupAllowed | GroupMembers | GroupUpdated | GroupsList @@ -357,4 +363,4 @@ ChatResponse = ( | ApiChats ) -ChatResponse_Tag = Literal["acceptingContactRequest", "activeUser", "chatItemNotChanged", "chatItemReaction", "chatItemUpdated", "chatItemsDeleted", "chatRunning", "chatStarted", "chatStopped", "cmdOk", "chatCmdError", "connectionPlan", "contactAlreadyExists", "contactConnectionDeleted", "contactDeleted", "contactPrefsUpdated", "contactRequestRejected", "contactsList", "groupDeletedUser", "groupLink", "groupLinkCreated", "groupLinkDeleted", "groupCreated", "publicGroupCreated", "publicGroupCreationFailed", "groupRelays", "groupRelaysAdded", "groupRelaysAddFailed", "groupMembers", "groupUpdated", "groupsList", "invitation", "leftMemberUser", "memberAccepted", "membersBlockedForAllUser", "membersRoleUser", "newChatItems", "rcvFileAccepted", "rcvFileAcceptedSndCancelled", "rcvFileCancelled", "sentConfirmation", "sentGroupInvitation", "sentInvitation", "sndFileCancelled", "userAcceptedGroupSent", "userContactLink", "userContactLinkCreated", "userContactLinkDeleted", "userContactLinkUpdated", "userDeletedMembers", "userProfileUpdated", "userProfileNoChange", "usersList", "apiChats"] +ChatResponse_Tag = Literal["acceptingContactRequest", "activeUser", "chatItemNotChanged", "chatItemReaction", "chatItemUpdated", "chatItemsDeleted", "chatRunning", "chatStarted", "chatStopped", "cmdOk", "chatCmdError", "connectionPlan", "contactAlreadyExists", "contactConnectionDeleted", "contactDeleted", "contactPrefsUpdated", "contactRequestRejected", "contactsList", "groupDeletedUser", "groupLink", "groupLinkCreated", "groupLinkDeleted", "groupCreated", "publicGroupCreated", "publicGroupCreationFailed", "groupRelays", "groupRelaysAdded", "groupRelaysAddFailed", "relayGroupAllowed", "groupMembers", "groupUpdated", "groupsList", "invitation", "leftMemberUser", "memberAccepted", "membersBlockedForAllUser", "membersRoleUser", "newChatItems", "rcvFileAccepted", "rcvFileAcceptedSndCancelled", "rcvFileCancelled", "sentConfirmation", "sentGroupInvitation", "sentInvitation", "sndFileCancelled", "userAcceptedGroupSent", "userContactLink", "userContactLinkCreated", "userContactLinkDeleted", "userContactLinkUpdated", "userDeletedMembers", "userProfileUpdated", "userProfileNoChange", "usersList", "apiChats"] diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 8fd700a8a2..b2fc00a44c 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -2627,7 +2627,7 @@ class RelayProfile(TypedDict): shortDescr: NotRequired[str] image: NotRequired[str] -RelayStatus = Literal["new", "invited", "accepted", "active", "inactive"] +RelayStatus = Literal["new", "invited", "accepted", "active", "inactive", "rejected"] ReportReason = Literal["spam", "content", "community", "profile", "other"] diff --git a/packages/simplex-chat-python/tests/test_native_cache.py b/packages/simplex-chat-python/tests/test_native_cache.py index bd3bc58da8..30a1f43e2a 100644 --- a/packages/simplex-chat-python/tests/test_native_cache.py +++ b/packages/simplex-chat-python/tests/test_native_cache.py @@ -41,7 +41,7 @@ def test_resolve_downloads_when_missing(tmp_path, monkeypatch): monkeypatch.setattr("simplex_chat._native._download", fake_download) libs_dir = _resolve_libs_dir("sqlite") - assert libs_dir == tmp_path / "simplex-chat" / "v6.5.1" / "sqlite" + assert libs_dir == tmp_path / "simplex-chat" / "v6.5.2" / "sqlite" assert called["backend"] == "sqlite" assert (libs_dir / "libsimplex.so").exists() @@ -49,7 +49,7 @@ def test_resolve_downloads_when_missing(tmp_path, monkeypatch): def test_resolve_uses_cache_on_second_call(tmp_path, monkeypatch): monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) monkeypatch.setattr("sys.platform", "linux") - cached = tmp_path / "simplex-chat" / "v6.5.1" / "sqlite" + cached = tmp_path / "simplex-chat" / "v6.5.2" / "sqlite" cached.mkdir(parents=True) (cached / "libsimplex.so").touch() # Should NOT call _download — use the cached file. diff --git a/packages/simplex-chat-python/tests/test_native_url.py b/packages/simplex-chat-python/tests/test_native_url.py index 7b53fa3ff7..b27c3e09cf 100644 --- a/packages/simplex-chat-python/tests/test_native_url.py +++ b/packages/simplex-chat-python/tests/test_native_url.py @@ -42,7 +42,7 @@ def test_url_sqlite(_): assert ( _libs_url("sqlite") == "https://github.com/simplex-chat/simplex-chat-libs/releases/download/" - "v6.5.1/simplex-chat-libs-linux-x86_64.zip" + "v6.5.2/simplex-chat-libs-linux-x86_64.zip" ) @@ -51,5 +51,5 @@ def test_url_postgres(_): assert ( _libs_url("postgres") == "https://github.com/simplex-chat/simplex-chat-libs/releases/download/" - "v6.5.1/simplex-chat-libs-linux-x86_64-postgres.zip" + "v6.5.2/simplex-chat-libs-linux-x86_64-postgres.zip" ) diff --git a/plans/2026-05-13-desktop-single-instance.md b/plans/2026-05-13-desktop-single-instance.md new file mode 100644 index 0000000000..87c5f7c9bb --- /dev/null +++ b/plans/2026-05-13-desktop-single-instance.md @@ -0,0 +1,30 @@ +# Desktop single instance - restore on duplicate launch + +## Problem + +After tray support (#6970), the desktop app can minimize to tray. The process stays alive holding the database. When the user clicks the app launcher again (forgetting about the tray), a second process starts and either crashes on the SQLite lock or runs in a degraded state. + +## Design + +Two files in `dataDir`: `simplex.started` (lock file) and `simplex.show` (signal file). + +### Startup + +1. Try `FileChannel.tryLock(0, 1, false)` on `simplex.started`. +2. **Lock acquired**: delete stale `simplex.show` if present (leftover from crash), start a daemon `WatchService` on `dataDir` for `ENTRY_CREATE`, start the app normally. +3. **Lock taken** (another process holds it): create `simplex.show`, exit. The running instance detects it and shows its window. +4. **Lock fails** (IOException, filesystem doesn't support locks, etc.): start normally but disable minimize-to-tray. Close quits the app. No worse than before tray support existed. + +### Signal handling + +While the lock is held, the daemon watcher runs for the JVM lifetime. When `simplex.show` appears it deletes the file and posts `showWindow()` to the EDT. `showWindow()` sets `windowVisible = true`, clears `ICONIFIED`, and brings the window to front — restores from tray, from taskbar-minimize, or just raises if visible-but-behind. + +Minimize-to-tray is only available when `singleInstanceLock` is held. If the lock couldn't be acquired (case 4), close always quits - preventing the scenario where two tray'd instances fight over the database. + +### Crash recovery + +The OS releases the file lock when the process dies. `simplex.show` may be left behind but is harmless - the next startup (step 2) deletes it. + +## Scope + +Linux, Windows, macOS. Per-data-directory - separate installs with different `dataDir` run independently. diff --git a/plans/2026-05-13-fix-privacy-links-import.md b/plans/2026-05-13-fix-privacy-links-import.md new file mode 100644 index 0000000000..2596704c24 --- /dev/null +++ b/plans/2026-05-13-fix-privacy-links-import.md @@ -0,0 +1,148 @@ +# "Remove link tracking" setting does not persist across database import + +PR: [#6977](https://github.com/simplex-chat/simplex-chat/pull/6977) · branch `nd/fix-privacy-links-import` → `master` + +## 1. Problem statement + +The **Settings → Privacy & security → Remove link tracking** toggle (`privacySanitizeLinks`) is silently dropped when a user moves their chat database to another device or reinstalls. Reproduction: + +1. Device A: enable "Remove link tracking", export chat database. +2. Device B (fresh install) or same device after re-install: import the database. +3. Open Settings → Privacy & security on B: the toggle is **off**. + +All three platforms are affected (Android, desktop, iOS) and any combination of source/target. Every other v6.5 "Safe web links" privacy guarantee survives the import; only "Remove link tracking" reverts. + +## 2. Solution summary + +The preference is stored locally only (Android `SharedPreferences`, iOS `UserDefaults` group). The cross-device transport for app settings is the `AppSettings` JSON record that travels with the database via `apiGetAppSettings` / `apiSaveAppSettings`. `privacySanitizeLinks` was absent from this record in all three layers (Haskell core, Kotlin multiplatform, Swift iOS), so it had nothing to ride on. + +Fix: add `privacySanitizeLinks :: Maybe Bool` to the `AppSettings` record in each of the three layers, wired identically to the reference field `privacyAskToApproveRelays`. Default in all three layers is `false`, matching today's local default. The fix is strictly additive (`+18` lines, 5 files, no deletions); no schema change, no command/API change, no UI change. + +## 3. Detailed tech design + +### 3.1 The round-trip the fix plugs into + +``` +Device A Device B +───────── ───────── +local pref store local pref store + ↑ ↑ importIntoApp() + │ user toggles UI │ + │ AppSettings ← apiGetAppSettings(local prepareForExport) + │ ↑ + │ archive (.zip with │ +local pref → AppSettings.current chat.db) ─────┐ + → prepareForExport │ │ + → apiSaveAppSettings │ │ + → app_settings DB row ─────────┘ │ + │ + core: combineAppSettings + stored <|> platformDefaults <|> defaults +``` + +`AppSettings.current` reads every local pref; `prepareForExport` strips fields equal to their default (space optimisation); `apiSaveAppSettings` writes the JSON into the `app_settings` table of the chat DB, which travels inside the archive. On import, the receiving client runs `apiGetAppSettings(local.prepareForExport())`; the core merges stored ⟶ local-platform ⟶ hardcoded-defaults with `Alternative` (`<|>`) and returns the result; the client's `importIntoApp` applies any non-null fields to its local store. + +A field that is **absent from `AppSettings`** at any of the three layers never enters this pipeline and is therefore lost on import. `privacySanitizeLinks` was such a field. + +### 3.2 Three-layer parity + +The three `AppSettings` definitions must agree on every field name, default value, and the four operations: + +| Operation | Haskell | Kotlin | Swift | +|---|---|---|---| +| field declaration | `data AppSettings` (`src/Simplex/Chat/AppSettings.hs:28`) | `data class AppSettings` (`SimpleXAPI.kt:8038`) | `struct AppSettings` (`AppAPITypes.swift:2118`) | +| default | `defaultAppSettings` (`AppSettings.hs:79`) | `defaults` (`SimpleXAPI.kt:8157`) | `defaults` (`AppAPITypes.swift:2188`) | +| "missing key" parse default | `defaultParseAppSettings` (`AppSettings.hs:116`) | implicit `null` | implicit `nil` | +| merge fallback | `combineAppSettings` (`AppSettings.hs:153`) | n/a (only one source) | n/a | +| JSON parser | hand-written `parseJSON` (`AppSettings.hs:207`) | `@Serializable` derived | `Codable` derived | +| read-from-local | n/a (clients send it) | `AppSettings.current` (`SimpleXAPI.kt:8193`) | `AppSettings.current` (`AppSettings.swift:71`) | +| write-to-local | n/a (clients apply it) | `importIntoApp` (`SimpleXAPI.kt:8110`) | `updateIosGroupDefaults` / `init from cfg` (`AppSettings.swift:13`) | +| serialize-only-non-default | n/a | `prepareForExport` (`SimpleXAPI.kt:8072`) | `prepareForExport` (`AppAPITypes.swift:2151`) | + +The fix adds one line to every cell that exists for `privacyAskToApproveRelays`. Default value is `false` (matches `mkBoolPreference(..., false)` and the `registerGroupDefaults` entry). + +### 3.3 Round-trip correctness — case analysis + +The core's `combineAppSettings = stored <|> platformDefaults <|> defaultAppSettings` (with `Alternative` on `Maybe`) means: take the stored value if present, else what the client said its default is, else the hardcoded default. The client's `prepareForExport` only includes a field when it differs from the client's `defaults`. With both `defaults` set to `false`: + +| Case | Archive carries | Local pref before | platformDefaults sent | Merged | Result | +|---|---|---|---|---|---| +| New archive, source had on | `Just true` | false | `Nothing` (default) | `Just true` | **on** ✓ | +| New archive, source had off (default) | `Nothing` (stripped) | false | `Nothing` | `Just false` (from defaults) | **off** ✓ | +| New archive, source had off | `Nothing` | true (local toggled) | `Just true` | `Just true` | **on** (local wins, archive silent) ✓ | +| Old archive (pre-fix) | field unknown | false | `Nothing` | `Just false` | **off** (unchanged from before fix) | +| Old archive | field unknown | true | `Just true` | `Just true` | **on** (local preserved) ✓ | +| Cross-platform | `Just true` | false | `Nothing` | `Just true` | **on** ✓ | + +The only "interesting" semantic — *archive silent on the field while local has it on* — preserves local. This matches how every other field in `AppSettings` behaves and matches user intent ("I toggled it on this device, then imported some old archive — keep it on"). + +### 3.4 Edge cases verified + +- **Downgrade then upgrade.** New code → toggle on → export. Imported on *old* code: `parseJSON` ignores unknown keys, DB row is rewritten without the field. Re-upgrade: field absent, falls through to `Just false`. This is the standard "old client drops new fields" semantics for every prior AppSettings addition; not introduced by this PR. + +- **iOS `BoolDefault` before `set` is ever called.** `apps/ios/SimpleXChat/AppGroup.swift:100` already registers `GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS: false` in `registerGroupDefaults`. So `privacySanitizeLinksGroupDefault.get()` returns `false` on first read — no NaN/nil sentinel risk. + +- **JSON field ordering.** `deriveToJSON defaultJSON` uses record-field order; new field is inserted between `privacyLinkPreviews` and `privacyShowChatPreviews`, shifting subsequent keys. No external consumer compares the JSON byte-for-byte; the existing `testAppSettings` test compares `J.encode defaultAppSettings` on both sides of the wire and so is self-consistent under the addition. + +- **`omitNothingFields = True`.** The Haskell `defaultJSON` config (`Simplex.Messaging.Parsers`) strips `Nothing` fields from JSON output, so `defaultParseAppSettings` (every field `Nothing`) does not pollute archives or wire payloads when used as a fallback. + +- **iOS NSE / SE extensions.** Neither references `privacySanitizeLinks`. No additional wiring required. + +### 3.5 What was deliberately not done + +- **Flipping the *user-facing* default to `true`.** Other privacy fields in `defaultAppSettings` are `Just True` (encrypt local files, ask to approve relays). "Remove link tracking" remains `Just False` because the local pref default (`mkBoolPreference(..., false)`, iOS `registerGroupDefaults: false`) is `false`. Aligning the `AppSettings` default with the local default keeps the `prepareForExport` "differs-from-default" comparison consistent — otherwise off-by-default users would suddenly serialise `false` everywhere and on-by-default users would serialise nothing, inverting the wire shape. Whether the *product* default should be flipped to on is a separate question for a separate change. + +- **Adding `apiSaveAppSettings` on toggle.** Toggling the pref in `PrivacySettings.kt` writes only to shared prefs; the DB's `app_settings` row stays stale until a separate trigger (theme change, export, migration) syncs. The export and migration paths already call `apiSaveAppSettings(AppSettings.current.prepareForExport())` immediately before producing the archive, so every UI-initiated export captures the current value. Plugging the sync into every toggle is a broader change affecting every AppSettings field equally — out of scope. + +- **Fixing `privacyChatListOpenLinks`.** The Kotlin `AppSettings` declares it (`SimpleXAPI.kt:8046`); the Haskell record and the Swift struct do not. Same failure mode as the bug being fixed here — almost certainly does not persist across Android-to-Android imports. Out of scope; should be tracked separately. + +- **Adding a targeted test.** The existing `testAppSettings` exercises a JSON round-trip with `defaultAppSettings`, so the new field rides through implicitly. A field-specific test (`defaultAppSettings { privacySanitizeLinks = Just True }`) would tighten coverage against a future client dropping the field; recommended as a small follow-up. + +## 4. Detailed implementation plan + +### 4.1 Files touched + +| File | Δ | Purpose | +|---|---|---| +| `src/Simplex/Chat/AppSettings.hs` | +6 / 0 | record field, `defaultAppSettings`, `defaultParseAppSettings`, `combineAppSettings`, JSON parser line, record reassembly | +| `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` | +5 / 0 | data class field, `prepareForExport`, `importIntoApp`, `defaults`, `current` | +| `apps/ios/Shared/Model/AppAPITypes.swift` | +3 / 0 | struct field, `prepareForExport`, `defaults` | +| `apps/ios/SimpleXChat/AppGroup.swift` | +2 / 0 | new `privacySanitizeLinksGroupDefault: BoolDefault` next to existing privacy defaults | +| `apps/ios/Shared/Views/UserSettings/AppSettings.swift` | +2 / 0 | import side (`set`), export side (`get`) | + +Total: 5 files, +18 / 0. No deletions. + +### 4.2 Step-by-step (commit `15457a903`) + +1. **`AppSettings.hs`** — add `privacySanitizeLinks :: Maybe Bool` to the record (between `privacyLinkPreviews` and `privacyShowChatPreviews`); set `Just False` in `defaultAppSettings`; `Nothing` in `defaultParseAppSettings`; `p privacySanitizeLinks` in `combineAppSettings`; `privacySanitizeLinks <- p "privacySanitizeLinks"` in `parseJSON`; add to record reassembly. Field position consistent with name groupings. + +2. **`SimpleXAPI.kt`** — same insertions in `data class AppSettings`, `prepareForExport`, `importIntoApp`, `defaults`, `current`. Local pref already exists (`SimpleXAPI.kt:126`). + +3. **`AppAPITypes.swift`** — same insertions in `struct AppSettings`, `prepareForExport`, `defaults`. + +4. **`AppGroup.swift`** — add `public let privacySanitizeLinksGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS)`. The key constant (line 31) and registered default `false` (line 100) already exist; only the typed wrapper for non-`@AppStorage` access was missing. + +5. **`AppSettings.swift` (iOS view extension)** — import side: `if let val = privacySanitizeLinks { privacySanitizeLinksGroupDefault.set(val) }`. Export side: `c.privacySanitizeLinks = privacySanitizeLinksGroupDefault.get()`. + +### 4.3 Verification + +- Haskell `testAppSettings` (`tests/ChatTests/Direct.hs:2768`) covers the JSON round-trip through `defaultAppSettings`; the new field flows through both sides of the equality, so existing assertions hold. +- Manual test plan (in PR description): + 1. Enable on Android, export DB, import on a second Android device — toggle stays on. + 2. Enable on iOS, export, import on a second iOS device — toggle stays on. + 3. Enable on desktop, export, fresh-install + import — toggle stays on. + 4. Cross-platform: export from Android, import on iOS, and vice versa — toggle preserved. + 5. Fresh install with no archive — toggle defaults to off (unchanged). + +### 4.4 Risk and rollback + +- **Blast radius**: the `AppSettings` JSON payload. Every other field is untouched (positional inserts, no reordering of existing fields beyond the natural shift). +- **Backwards compatibility**: old clients (no field) parsing new JSON ignore the key. New clients (with field) parsing old JSON see `Nothing`, fall through to `defaultAppSettings` and the local pref is set to its default. Either direction is safe. +- **Rollback**: `git revert 15457a903`. Restores pre-fix behaviour (the field-loss bug returns). + +## 5. Why this specific shape + +- The bug has exactly one cause: a missing field in the round-trip payload. The smallest fix is to add the field. Anything larger (e.g. broadening `importIntoApp` to scan all shared prefs, or pinning the value in a side channel) would be a structural change that does not improve correctness. +- The `<|>` merge in `combineAppSettings` already gives the right behaviour for every edge case (archive-silent local-set, fresh install, downgrade) once the field exists. No new merge logic needed. +- The default `false` is forced: any other choice would either contradict the local pref default (`mkBoolPreference(..., false)`, iOS `registerGroupDefaults: false`) or invert the wire shape of `prepareForExport`. +- Final PR is 5 files, +18 / 0. Three of those files are the three `AppSettings` records; the other two are the iOS wiring the new field needs in order to read and write its group default. No other file in the codebase needed touching. diff --git a/plans/2026-05-13-relay-refuse-rejoin.md b/plans/2026-05-13-relay-refuse-rejoin.md new file mode 100644 index 0000000000..e33a525c03 --- /dev/null +++ b/plans/2026-05-13-relay-refuse-rejoin.md @@ -0,0 +1,347 @@ +Plan rewritten for conciseness with fresh-context re-evaluation; supersedes earlier revisions. + +# Plan: relay refuses to rejoin a channel it left + +## 1. Identifier + +Gating key: `GroupRelayInvitation.groupLink :: ShortLinkContact` (Types.hs:884-889). Available at `xGrpRelayInv` (Subscriber.hs:1524-1528) before any DB write or network call. The relay already stores this value on every `groups` row it processes (column `relay_request_group_link`, M20260222:38), and the existing `relay_own_status` column already carries the relay's lifecycle for the channel — refusal slots into that state machine as a new `RSRejected` variant. Lookup is a single SELECT against `groups`. Link rotation by the owner bypasses refusal; `publicGroupId` (Types.hs:790) would resist that but is only known after `getShortLinkConnReq'` — defer that gating to a follow-up. + +## 2. Storage + +No new column, no new type, no new field on `GroupInfo`. The existing `relay_own_status TEXT` (M20260222:37) is the carrier. + +`RelayStatus` (`src/Simplex/Chat/Types/Shared.hs:81-114`) gains an `RSRejected` constructor (encoded as `"rejected"`). It is reused on both sides: on the relay it is the row's own state after `APILeaveGroup`; on the owner it is the `GroupRelay.relayStatus` after `XGrpRelayReject` arrives in §5. + +State-machine slot for `RSRejected` on the relay: + +- `updateRelayOwnStatus_` (Store/Groups.hs:1593-1597) writes `relay_inactive_at = Just currentTs` only when the new status is `RSInactive`. `RSRejected` therefore correctly leaves `relay_inactive_at = NULL`, so the row is NOT eligible for `checkRelayInactiveGroups` cleanup (Commands.hs:4812-4817). +- `checkRelayServedGroups` (Commands.hs:4795-4810) iterates only `getRelayServedGroups` rows — `relay_own_status IN (RSAccepted, RSActive)` (Store/Groups.hs:1607). RSRejected rows are not iterated. +- `xGrpMemDel` writer at Subscriber.hs:3132 currently flips any non-NULL `relay_own_status` to `RSInactive` when the owner removes the relay member. That would silently regress `RSRejected → RSInactive` and let a subsequent `XGrpRelayInv` slip through (the lookup checks `'rejected'`). The write at line 3132 is tightened to skip when the row is already `RSRejected`: + +```haskell +when (maybe False (/= RSRejected) (relayOwnStatus gInfo)) $ + updateRelayOwnStatus_ db gInfo RSInactive +``` + +New migration `M20260514_relay_request_group_link_index` adds a partial index — the column is unindexed today and the new gate SELECTs on it. SQLite: + +```sql +CREATE INDEX idx_groups_relay_request_group_link + ON groups(user_id, relay_request_group_link) + WHERE relay_request_group_link IS NOT NULL; +``` + +Postgres mirror. Partial-on-`IS NOT NULL` because most rows on owner-only or p2p installs leave the column NULL. Both engines support partial indexes. Down: `DROP INDEX idx_groups_relay_request_group_link`. + +One helper, added next to the existing `relay_*` helpers in `src/Simplex/Chat/Store/Groups.hs`: + +```haskell +isRelayGroupRefused :: DB.Connection -> User -> ShortLinkContact -> IO Bool +isRelayGroupRefused db User {userId} groupLink = + fromOnly . head <$> DB.query db + [sql| + SELECT EXISTS ( + SELECT 1 FROM groups + WHERE user_id = ? + AND relay_request_group_link = ? + AND relay_own_status = ? + LIMIT 1 + ) + |] + (userId, groupLink, RSRejected) +``` + +`EXISTS … LIMIT 1` because more than one `groups` row may share `relay_request_group_link` (`createRelayRequestGroup` at Store/Groups.hs:1526 INSERTs unconditionally). If any matching row has `relay_own_status = 'rejected'`, the channel is refused. The equality check naturally excludes other states (NULL, RSInvited, RSAccepted, RSActive, RSInactive). + +All other operator-allow and leave writes reuse existing helpers `updateRelayOwnStatus_` and `updateRelayOwnStatusFromTo` (Store/Groups.hs:1587-1597). No new write helpers. + +## 3. Rejection point — `xGrpRelayInv` (Subscriber.hs:1524) + +```haskell +xGrpRelayInv :: InvitationId -> VersionRangeChat -> GroupRelayInvitation -> CM () +xGrpRelayInv invId chatVRange groupRelayInv@GroupRelayInvitation {groupLink} = do + refused <- withStore' $ \db -> isRelayGroupRefused db user groupLink + if refused + then sendRelayRejection `catchAllErrors` eToView + else do + initialDelay <- asks $ initialInterval . relayRequestRetryInterval . config + (_gInfo, _ownerMember) <- withStore $ \db -> + createRelayRequestGroup db vr user groupRelayInv invId chatVRange initialDelay + lift $ void $ getRelayRequestWorker True + where + sendRelayRejection = do + let pqSup = PQSupportOff + subMode <- chatReadVar subscriptionMode + chatVR <- chatVersionRange + let chatV = chatVR `peerConnChatVersion` chatVRange + connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) False invId pqSup + dm <- encodeConnInfoPQ pqSup chatV XGrpRelayReject + void $ withAgent $ \a -> + acceptContact a NRMBackground (aUserId user) connId False invId dm pqSup subMode + deleteAgentConnectionAsync' connId False +``` + +**Why synchronous `acceptContact` (not `acceptContactAsync`).** `acceptContactAsync` enqueues a JOIN agent command; the CONF send and the snd-queue creation happen later inside the agent's command worker (Agent.hs:1826-1830). If we immediately call `deleteAgentConnectionAsync' acId True`, `setConnDeleted` runs, `prepareDeleteConnections_` finds zero rcv queues (no JOIN yet), `deleteConn db (Just timeout) connId` finds zero `snd_message_deliveries` and calls `deleteConnRecord`. The connection record is gone before the JOIN worker can send the CONF — the rejection signal is silently dropped. + +`acceptContact` (Internal.hs:881-912 precedent; Agent.hs:1437-1442 → `joinConn` 1263 → `joinConnSrv` 1358-1369 for CRContactUri → `sendInvitation` Agent/Client.hs:1796-1799 → `sendOrProxySMPMessage` 1084-1094 → `sendSMPMessage`/`proxySMPMessage`) hands the CONF to the SMP server via a direct SMP client call. The CONF does NOT go through `snd_message_deliveries` — it is transmitted inline. Subsequent `deleteAgentConnectionAsync' connId False` is therefore safe. The cost is one SMP round-trip blocking the receive loop, which the refusal path can absorb. + +No chat-layer `Connection` row is persisted for the refused contact — the agent owns the connection state, and `deleteAgentConnectionAsync'` cleans it up. + +If `sendInvitation` throws (SMP server unreachable), `acceptContact` throws before reaching its internal `acceptInvitation` step and the agent-allocated rcv queue from `newRcvConnSrv` is left for the agent's eventual cleanup. The owner receives no rejection and falls back to the silent-degradation path (GroupRelay stuck at `RSInvited`). The outer `catchAllErrors eToView` prevents the receive loop from being held by the bubbled-up exception. + +## 4. Wire format — `XGrpRelayReject` + +Empty-payload event, owner-relay direct contact channel only. Not group-signed. Naming matches the existing `XGrpLinkReject` precedent (Protocol.hs:440, tag:985, string:1043). + +`src/Simplex/Chat/Protocol.hs`: + +- GADT constructor (after `XGrpRelayNew`, line 446): `XGrpRelayReject :: ChatMsgEvent 'Json` +- Tag GADT (after `XGrpRelayNew_`, line 991): `XGrpRelayReject_ :: CMEventTag 'Json` +- `strEncode` (line 1049): `XGrpRelayReject_ -> "x.grp.relay.reject"` +- `strDecode` (line 1108): `"x.grp.relay.reject" -> XGrpRelayReject_` +- `toCMEventTag` (line 1163): `XGrpRelayReject -> XGrpRelayReject_` +- JSON parse (line 1321): `XGrpRelayReject_ -> pure XGrpRelayReject` +- JSON encode (line 1391): `XGrpRelayReject -> JM.empty` — matches `XGrpLeave -> JM.empty` (1402) and `XDirectDel -> JM.empty` (1379). +- **No** entry in `isForwardedGroupMsg` (485-505) or `requiresSignature` (1227-1238). + +Older owner clients parse the unknown tag as `XUnknown` (default branch at 1134) and hit the CONF handler's catch-all `_ -> messageError "CONF from invited member must have x.grp.acpt"`. No state change, no crash; the GroupRelay stays at `RSInvited` — the same end state as today's "relay never responds" mode. The owner UI shows the relay as permanently "invited" with no progress; documented degradation. + +`docs/protocol/channels-protocol.md`: insert a `### Relay refusal` subsection between `### Relay addition` (61-73) and `### Subscriber connection` (75). Paragraphs: + +1. **Trigger** — relay's `APILeaveGroup` sets `relay_own_status = 'rejected'` on the relay's local `groups` row for the channel. +2. **Signal** — empty-payload `x.grp.relay.reject` over the owner-relay direct contact channel. +3. **Owner handling** — `GroupRelay` transitions `RSInvited → RSRejected`; final. Cleared only by the relay operator running `/group allow `. +4. **Limitations** — (a) older owner clients log a CONF parse error and leave their `GroupRelay` at `RSInvited` indefinitely (same UX as a relay that doesn't respond); (b) older relay binaries do not enforce refusal — mixed-version deployments where some relays are old behave asymmetrically. + +## 5. Owner-side state + +`RelayStatus` gains `RSRejected` (§2). Add to `relayStatusText`, `textEncode`, `textDecode`. + +CONF handler arm in `src/Simplex/Chat/Library/Subscriber.hs:760-773` (immediately after the existing `XGrpRelayAcpt` clause): + +```haskell +XGrpRelayReject + | memberRole' membership == GROwner && isRelay m -> do + relay <- withStore $ \db -> do + liftIO $ updateGroupMemberStatus db userId m GSMemRejected + relay <- getGroupRelayByGMId db (groupMemberId' m) + liftIO $ updateRelayStatusFromTo db relay RSInvited RSRejected + let m' = m {memberStatus = GSMemRejected} + deleteMemberConnection m' + toView $ CEvtGroupRelayUpdated user gInfo m' relay + | otherwise -> messageError "x.grp.relay.reject: only owner can receive relay rejection" +``` + +`getGroupRelayByGMId` (Store/Groups.hs:1307) and `updateRelayStatusFromTo` (1438-1442) are already exported. `updateRelayStatusFromTo` is conditional on the current status equalling `RSInvited` — racing CONFs cannot regress an already-rejected or already-active row. `deleteMemberConnection` (Internal.hs:1807-1808) safely no-ops when `activeConn` is `Nothing`. `CEvtGroupRelayUpdated` (Controller.hs:900) carries exactly the iOS payload. + +`addRelays` (Commands.hs:3942-3976) persists `GroupRelay` with `RSNew → RSInvited` before sending `XGrpRelayInv`, so the row exists when the CONF arrives. A second user-initiated `addRelays` after rejection creates a fresh row, independent of the rejected one — no automatic retry. + +## 6. Refusal write — `APILeaveGroup` (Commands.hs:2919-2935) + +Currently `leaveChannelRelay` does NOT touch `relay_own_status` — verified at Commands.hs:2938-2947. The new write is added to the existing leave path, unconditionally on the relay-leave branch: + +```haskell +APILeaveGroup groupId -> withUser $ \user@User {userId} -> do + gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db vr user groupId + filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo + withGroupLock "leaveGroup" groupId $ do + cancelFilesInProgress user filesInfo + msg <- if useRelays' gInfo && isRelay membership + then leaveChannelRelay gInfo + else leaveGroupSendMsg user gInfo + (gInfo', scopeInfo) <- mkLocalGroupChatScope gInfo + ci <- saveSndChatItem user (CDGroupSnd gInfo' scopeInfo) msg (CISndGroupEvent SGEUserLeft) + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo' scopeInfo) ci] + deleteGroupLinkIfExists user gInfo' + withFastStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft + -- NEW: mark the relay's local groups row as refused + when (useRelays' gInfo && isRelay membership) $ + withFastStore' $ \db -> updateRelayOwnStatus_ db gInfo RSRejected + pure $ CRLeftMemberUser user gInfo' {membership = membership {memberStatus = GSMemLeft}} +``` + +`updateRelayOwnStatus_` (Store/Groups.hs:1593) writes unconditionally. The prior status can legitimately be any of `RSInvited` (operator leaves mid-request, placeholder profile still in place — verified at Store/Groups.hs:1531-1541), `RSAccepted` (waiting for health-check), `RSActive` (steady state), or `RSInactive` (already inactive — re-leaving). The earlier rev's `publicGroup == Nothing` throw was wrong: `RSInvited` is a real lifecycle state with `publicGroup = Nothing` (`createRelayRequestGroup` at Store/Groups.hs:1531 uses a placeholder profile until `updateGroupProfile` at Subscriber.hs:3847 runs inside the relay-request worker). Writing `RSRejected` unconditionally on the relay-leave path correctly cancels an in-progress invitation: `getNextPendingRelayRequest` (Store/RelayRequests.hs:60-72) selects only rows where `relay_own_status = 'invited'`, so the flip to `RSRejected` removes the row from the worker queue. + +## 7. Operator command — relay side + +One API command. Operator discovers rejected channels through `/gs` (see §7.2). + +`src/Simplex/Chat/Controller.hs` (after `APITestChatRelay` at ~408): + +```haskell +| APIAllowRelayGroup {groupId :: GroupId} +-- response (after CRGroupRelays at ~737): +| CRRelayGroupAllowed {user :: User, groupInfo :: GroupInfo} +``` + +Parser entries (`src/Simplex/Chat/Library/Commands.hs:5033+`). `GroupId = Int64` is a type alias (Types.hs:449), so `A.decimal` decodes directly — matches `APILeaveGroup <$> A.decimal` at 5021: + +```haskell +"/_relay allow " *> (APIAllowRelayGroup <$> A.decimal), +"/group allow " *> (APIAllowRelayGroup <$> A.decimal), +``` + +Handler: + +```haskell +APIAllowRelayGroup groupId -> withUser $ \user -> do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo' <- withStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSRejected RSInactive + pure $ CRRelayGroupAllowed user gInfo' +``` + +`updateRelayOwnStatusFromTo` (Store/Groups.hs:1587-1591) atomically transitions only if the current status equals the from-state — a non-rejected row stays unchanged and the response reports the unchanged `gInfo`. The transition to `RSInactive` writes `relay_inactive_at = currentTs` via `updateRelayOwnStatus_` (1593-1597), so the row becomes eligible for `checkRelayInactiveGroups` connection cleanup on TTL — the correct hygiene state for a previously-rejected, now-cleared row. + +No event to the owner. The owner's next user-initiated `addRelays` succeeds normally (the relay's `xGrpRelayInv` finds no `'rejected'` row for the link). Operator authorization is the chat-relay binary's process-level access. + +### 7.1 Guard against deleting a rejected group + +`APIDeleteChat CTGroup` at Commands.hs:1242-1246 lets the operator delete the group once `memberCurrent membership` is false (post-leave). That path would silently clear the refusal — an accidental `/d` should not undo a moderation decision. Add a guard immediately after the existing `unless canDelete` check: + +```haskell +when (relayOwnStatus gInfo == Just RSRejected) $ + throwChatError $ CECommandError "cannot delete a rejected channel; run /_relay allow first" +``` + +`checkRelayInactiveGroups` (Commands.hs:4812-4817) only deletes connections via `deleteGroupConnections`, not group rows, so no guard is needed there. + +### 7.2 Surface `[rejected]` in `/gs` + +`viewGroupsList` in `src/Simplex/Chat/View.hs:1432-1459`. Extend `groupSS`'s destructure to pull `relayOwnStatus` while keeping the existing `GroupSummary {currentMembers}` pattern (used at line 1456 by `memberCount`), and append `[rejected]` between status and alias: + +```haskell +groupSS g@GroupInfo { membership + , chatSettings = ChatSettings {enableNtfs} + , groupSummary = GroupSummary {currentMembers} + , relayOwnStatus + } = + case memberStatus membership of + GSMemInvited -> groupInvitation' g + s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s <> rejectionSuffix <> alias g + where + rejectionSuffix = case relayOwnStatus of + Just RSRejected -> " [rejected]" + _ -> "" + … +``` + +## 8. iOS + +No iOS storage-side change. The owner-side `RSRejected` rendering is the same as the rev-4 plan. + +`apps/ios/SimpleXChat/ChatTypes.swift:2637-2643 + 2708-2718`: + +```swift +public enum RelayStatus: String, Decodable, Equatable, Hashable { + … + case rsRejected = "rejected" +} +extension RelayStatus { public var text: LocalizedStringKey { + switch self { … case .rsRejected: "rejected" } +}} +``` + +`apps/ios/Shared/Views/NewChat/AddChannelView.swift:487-504` (`relayStatusIndicator`): + +```swift +let isRejected = status == .rsRejected +let color: Color = + connFailed || removed || isRejected ? .red + : (status == .rsActive ? .green : .yellow) +let text: LocalizedStringKey = + connFailed ? "failed" + : memberStatus == .memLeft ? "removed by operator" + : isRejected ? "rejected" + : removed ? "removed" + : status.text +``` + +`apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift`, inside the existing `Section` after the `Relay address` block at line 195: + +```swift +if groupRelay?.relayStatus == .rsRejected { + infoRow("Status", "rejected by relay operator") +} +``` + +`ChannelRelaysView.swift` requires no change — the existing fall-through in `ownerRelayStatusText` (line 114-127) to `groupRelays.first(…)?.relayStatus.text` already renders `"rejected"`. + +`GroupMemberStatus.memRejected` already exists at ChatTypes.swift:3002. No iOS enum change; cited here so an iOS-only reviewer doesn't drop the case. + +Per `apps/ios/CODE.md` Change Protocol, the implementer updates `apps/ios/spec/state.md`, `apps/ios/spec/api.md`, `apps/ios/spec/client/chat-view.md`, `apps/ios/product/views/group-info.md`, `apps/ios/spec/impact.md`, and `apps/ios/product/concepts.md`. + +Kotlin/Android/desktop port is a separate PR. + +## 9. Tests — `tests/ChatTests/RelayRefused.hs` + +All tests use the existing channel harness and block on chat events, not `threadDelay`. + +- **`testRelayRefuseAfterLeave`** — relay1 leaves; owner re-adds; owner blocks on `CEvtGroupRelayUpdated`; assert owner `relayStatus == RSRejected`, member `GSMemRejected`, channel link data excludes relay1. Also assert relay's `groups.relay_own_status = 'rejected'`. Deterministic delivery check for the sync-accept-then-delete path. +- **`testRelayAllowAcceptsAgain`** — operator runs `/group allow `; relay's `groups.relay_own_status` becomes `'inactive'`; owner re-adds; relay reaches `RSActive` on a fresh `GroupRelay` row. +- **`testRelayDoesNotRefuseUnrelatedChannel`** — relay1 leaves channel A; owner of unrelated channel B issues `XGrpRelayInv`; relay1 accepts B; only A's `groups` row has `relay_own_status = 'rejected'`. +- **`testRelayRefuseRaceConcurrentInvitations`** — owner sends two `XGrpRelayInv` for the same channel concurrently after the relay has left; both refuse; relay's `groups` table acquires no placeholder row for the second invitation (both lookups match the same rejected row). +- **`testRelayForwardCompatOldOwner`** — owner's `chatVersionRange` excludes `x.grp.relay.reject`; relay refuses; owner emits `messageError` and the GroupRelay row stays at `RSInvited`; no crash. +- **`testRelayDeleteRejectedBlocked`** — relay1 leaves channel A; operator runs `/d #A`; deletion fails with the guard error from §7.1; channel still exists; operator runs `/group allow ` then `/d #A`; deletion succeeds. +- **`testRelayRejectSurvivesOwnerRemoveRelayMember`** — relay1 leaves channel A (sets `RSRejected`); owner sends `XGrpMemDel` removing relay1; assert relay's `groups.relay_own_status` is still `'rejected'`, not flipped to `'inactive'`. Covers the §2 tightening of `xGrpMemDel` at Subscriber.hs:3132. +- **`testNonOwnerXGrpRelayRejectIgnored`** — owner-side negative case: deliver an `XGrpRelayReject` CONF on a connection where either `memberRole' membership /= GROwner` or the sender member is not `isRelay`; assert the owner emits `messageError` and neither the GroupRelay row nor the member status changes. + +## 10. Adversarial review + +- **Existing `RSInactive` consumers.** Three call sites filter on `Just RSInactive` to mean "relay not serving — drop normal delivery": + - Subscriber.hs:936 (`MSG` handler filters delivery tasks). + - Subscriber.hs:3571 (delivery-task worker rejects `DJDeliveryJob`). + - Subscriber.hs:3641 (delivery-job worker errors `DJDeliveryJob`). + All three must broaden to also match `Just RSRejected` — both states share the "not serving" semantic. `DJRelayRemoved` is handled in a separate branch and remains status-independent. Add a small predicate (e.g., `relayNotServing :: Maybe RelayStatus -> Bool`) near the existing `relayOwnStatus` accessors. +- **`xGrpMemDel` writer at Subscriber.hs:3132** — this is also a writer of `relay_own_status`, not a filter. It flips any non-NULL status to `RSInactive` when the owner removes the relay member. Tightened in §2 to skip when the row is already `RSRejected`; otherwise the refusal would be silently undone by a normal protocol event. +- **Health-check loop never touches RSRejected.** `getRelayServedGroups` filters `relay_own_status IN (RSAccepted, RSActive)` (Store/Groups.hs:1607); RSRejected rows are not iterated. `updateRelayOwnStatusFromTo` calls in Commands.hs:4808-4809 only transition RSAccepted↔RSActive↔RSInactive. With the §2 tightening of `xGrpMemDel` line 3132, no path can silently undo a refusal. +- **Operator deletes a rejected group** — blocked at `APIDeleteChat CTGroup` per §7.1. +- **Timing side channel** — refusal path is one synchronous SMP round-trip; accepted path is much longer. Passive SMP-server observation can distinguish, though relay load adds variance to both paths. SMP server already infers relay-channel relationships from connection patterns; marginal additional leak. +- **Information leakage in `XGrpRelayReject`** — empty payload. +- **Concurrent leave-then-rejoin** — operator-facing contract: invitations arriving before the leave commits locally are processed normally; invitations after are refused. Note that `xGrpRelayInv` does NOT take `withGroupLock "leaveGroup" groupId` (no group ID is known at REQ time); the bound is the SQL commit of the `relay_own_status = 'rejected'` write, not a lock. Sibling rows already at `RSInvited` from before the leave are not retroactively rejected — they are processed normally by the worker. See §12 for follow-up scope. +- **Two concurrent `XGrpRelayInv` for the same rejected channel** — both lookups hit the same indexed row, both refuse. No race. +- **Duplicate `groups` rows for the same `relay_request_group_link`** — pre-existing (`createRelayRequestGroup` INSERTs unconditionally; no uniqueness on `relay_request_inv_id` or `relay_request_group_link`). Any `RSRejected` row blocks *future invitations from creating new rows that progress to acceptance* (the lookup uses `EXISTS … LIMIT 1`). Sibling rows already in `RSInvited` continue to be processed by the worker — see §12. +- **Operator-allow vs. concurrent invitation** — UPDATE-SELECT race resolves to either "still refused" or "slipped through with accept"; both match operator intent. +- **`getGroupRelayByGMId` failure on owner side** — propagates as `ChatErrorStore`; cannot happen in normal operation. +- **Multi-user relay binary** — `groups.user_id` scopes both lookup and write. `withUser` for the CLI. No cross-user pollution. +- **`sendRelayRejection` SMP failure** — wrapped in `catchAllErrors eToView` per §3 so a single SMP failure during refusal does not propagate to the agent receive loop. The owner falls back to silent-degradation (GroupRelay stuck at RSInvited), matching today's "relay unresponsive" mode. +- **Forward compat — mixed-version relays.** An old relay binary leaves a channel by writing `RSInactive`, not `RSRejected`, and does not enforce refusal at `xGrpRelayInv`. Mixed-version deployments (some relays new, some old) have asymmetric behavior: new relays refuse, old relays accept. Acceptable v1 limitation; document in `docs/protocol/channels-protocol.md`. Operator on an upgraded relay can `/leave` again under the new binary to re-establish refusal. +- **Forward compat (old owner)** — old owner's CONF handler lands in the `_ -> messageError "CONF from invited member must have x.grp.acpt"` catch-all (Subscriber.hs:773). GroupRelay stays at `RSInvited`; same end state as today's "relay never responds" mode. Documented in the protocol doc. + +## 11. Files changed + +| File | Change | +|---|---| +| `src/Simplex/Chat/Types/Shared.hs` | `RSRejected` variant + text encodings | +| `src/Simplex/Chat/Protocol.hs` | `XGrpRelayReject` constructor, tag, str enc/dec, JSON enc/dec | +| `src/Simplex/Chat/Store/Groups.hs` | `isRelayGroupRefused` helper | +| `src/Simplex/Chat/Store/SQLite/Migrations/M20260514_relay_request_group_link_index.hs` | NEW. Partial index | +| `src/Simplex/Chat/Store/SQLite/Migrations.hs` | Register migration | +| `src/Simplex/Chat/Store/Postgres/Migrations/M20260514_relay_request_group_link_index.hs` | NEW | +| `src/Simplex/Chat/Store/Postgres/Migrations.hs` | Register migration | +| `src/Simplex/Chat/Controller.hs` | `APIAllowRelayGroup` command; `CRRelayGroupAllowed` response | +| `src/Simplex/Chat/Library/Commands.hs` | Parser; handler; refusal write in `APILeaveGroup`; delete guard in `APIDeleteChat CTGroup` | +| `src/Simplex/Chat/Library/Subscriber.hs` | Gate in `xGrpRelayInv`; `XGrpRelayReject` arm in CONF handler; broaden three RSInactive filters to also match RSRejected (lines 936, 3571, 3641); tighten `xGrpMemDel` writer at 3132 to skip when row is `RSRejected` | +| `src/Simplex/Chat/View.hs` | `[rejected]` suffix in `viewGroupsList` | +| `simplex-chat.cabal` | Register new migration modules | +| `docs/protocol/channels-protocol.md` | Insert "Relay refusal" subsection | +| `apps/ios/SimpleXChat/ChatTypes.swift` | `rsRejected` case + text | +| `apps/ios/Shared/Views/NewChat/AddChannelView.swift` | Red dot + "rejected" in `relayStatusIndicator` | +| `apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift` | "Status: rejected by relay operator" row | +| `tests/ChatTests/RelayRefused.hs` | NEW. Eight tests | +| Test list registration | Add the new module | + +`chat_schema.sql` is auto-regenerated by tests. + +## 12. Out of scope + +- Kotlin/Android/desktop UI port. +- New alerts, modals, banners, compose-bar changes. +- Refusal triggered by `xGrpMemDel` (owner removing relay). +- Pre-emptive blocking of unseen channels. +- Owner-side independent clear of `RSRejected`. +- `publicGroupId`-keyed refusal. +- Timing-uniform refusal. +- **Sibling-row worker race.** When a relay leaves a channel for which it has a sibling `groups` row in `RSInvited` (e.g., the owner re-sent `XGrpRelayInv` and `createRelayRequestGroup` created a second row), only the row whose ID `APILeaveGroup` targets is flipped to `RSRejected`; sibling `RSInvited` rows continue through the worker. Pre-existing behavior — `leaveChannelRelay` doesn't touch sibling rows today either. Cheapest future closure: in §6, also `UPDATE groups SET relay_request_failed = 1 WHERE user_id = ? AND relay_request_group_link = ? AND relay_own_status = 'invited'` in the same transaction (the worker filters on `relay_request_failed = 0` at Store/RelayRequests.hs:67). Deferred to a follow-up. +- **`XGrpRelayInv` re-delivery duplicates.** `createRelayRequestGroup` has no uniqueness on `relay_request_inv_id` or `relay_request_group_link`; an owner retry of `XGrpRelayInv` creates duplicate rows. Pre-existing; closure ties to the sibling-row item above. + +The mixed-version-relay asymmetry and the old-owner stuck-RSInvited UI degradation are documented in `docs/protocol/channels-protocol.md` alongside the new `### Relay refusal` subsection. diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index a2bafe8a70..b55a08df26 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,32 @@ + + https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html + +

New in v6.5.2:

+
    +
  • allow deleting messages from channel history without time limit.
  • +
+

New in v6.5:

+

Public channels - speak freely!

+
    +
  • Reliability: many relays per channel.
  • +
  • Ownership: you can run your own relays.
  • +
  • Security: owners hold channel keys.
  • +
  • Privacy: for owners and subscribers.
  • +
+

Easier to invite your friends: we made connecting simpler for new users.

+

Safe web links:

+
    +
  • opt-in to send link previews.
  • +
  • use SOCKS proxy for previews (if enabled).
  • +
  • prevent hyperlink phishing.
  • +
  • remove link tracking.
  • +
+

Non-profit governance: to make SimpleX Network last.

+
+
https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 13aa440e7b..8a91d35f05 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."1f173abf6d6fccb617be1e7994629c405983c431" = "1myfs7yi8bmbrzapbhz6rmvxknpdzv6rxyypg811mhsw7rfphn65"; + "https://github.com/simplex-chat/simplexmq.git"."f03cec7a58ed13a39a52886888c74bcefdb64479" = "0bkd8kqgmwgfh5rwnw7s4p6mx9kwigi4jq9ljlfvzj23pslk1aq7"; "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 3c260825b7..6e459d6484 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.5.2.0 +version: 6.5.3.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -132,6 +132,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260403_item_viewed Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries Simplex.Chat.Store.Postgres.Migrations.M20260507_relay_inactive_at + Simplex.Chat.Store.Postgres.Migrations.M20260514_relay_request_group_link_index else exposed-modules: Simplex.Chat.Archive @@ -286,6 +287,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries Simplex.Chat.Store.SQLite.Migrations.M20260507_relay_inactive_at + Simplex.Chat.Store.SQLite.Migrations.M20260514_relay_request_group_link_index other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/AppSettings.hs b/src/Simplex/Chat/AppSettings.hs index 1efa69fad4..22938dd48c 100644 --- a/src/Simplex/Chat/AppSettings.hs +++ b/src/Simplex/Chat/AppSettings.hs @@ -33,6 +33,7 @@ data AppSettings = AppSettings privacyAskToApproveRelays :: Maybe Bool, privacyAcceptImages :: Maybe Bool, privacyLinkPreviews :: Maybe Bool, + privacySanitizeLinks :: Maybe Bool, privacyShowChatPreviews :: Maybe Bool, privacySaveLastDraft :: Maybe Bool, privacyProtectScreen :: Maybe Bool, @@ -83,6 +84,7 @@ defaultAppSettings = privacyAskToApproveRelays = Just True, privacyAcceptImages = Just True, privacyLinkPreviews = Just True, + privacySanitizeLinks = Just False, privacyShowChatPreviews = Just True, privacySaveLastDraft = Just True, privacyProtectScreen = Just False, @@ -120,6 +122,7 @@ defaultParseAppSettings = privacyAskToApproveRelays = Nothing, privacyAcceptImages = Nothing, privacyLinkPreviews = Nothing, + privacySanitizeLinks = Nothing, privacyShowChatPreviews = Nothing, privacySaveLastDraft = Nothing, privacyProtectScreen = Nothing, @@ -157,6 +160,7 @@ combineAppSettings platformDefaults storedSettings = privacyAskToApproveRelays = p privacyAskToApproveRelays, privacyAcceptImages = p privacyAcceptImages, privacyLinkPreviews = p privacyLinkPreviews, + privacySanitizeLinks = p privacySanitizeLinks, privacyShowChatPreviews = p privacyShowChatPreviews, privacySaveLastDraft = p privacySaveLastDraft, privacyProtectScreen = p privacyProtectScreen, @@ -210,6 +214,7 @@ instance FromJSON AppSettings where privacyAskToApproveRelays <- p "privacyAskToApproveRelays" privacyAcceptImages <- p "privacyAcceptImages" privacyLinkPreviews <- p "privacyLinkPreviews" + privacySanitizeLinks <- p "privacySanitizeLinks" privacyShowChatPreviews <- p "privacyShowChatPreviews" privacySaveLastDraft <- p "privacySaveLastDraft" privacyProtectScreen <- p "privacyProtectScreen" @@ -244,6 +249,7 @@ instance FromJSON AppSettings where privacyAskToApproveRelays, privacyAcceptImages, privacyLinkPreviews, + privacySanitizeLinks, privacyShowChatPreviews, privacySaveLastDraft, privacyProtectScreen, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 84bebb3de6..fa2d0af009 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -407,6 +407,7 @@ data ChatCommand | SetUserChatRelays [CLINewRelay] | APITestChatRelay UserId ShortLinkContact | TestChatRelay ShortLinkContact + | APIAllowRelayGroup {groupId :: GroupId} | APIGetServerOperators | APISetServerOperators (NonEmpty ServerOperator) | SetServerOperators (NonEmpty ServerOperatorRoles) @@ -532,6 +533,7 @@ data ChatCommand | BlockForAll GroupName ContactName Bool | RemoveMembers {groupName :: GroupName, members :: NonEmpty ContactName, withMessages :: Bool} | LeaveGroup GroupName + | AllowRelayGroup GroupName | DeleteGroup GroupName | ClearGroup GroupName | ListMembers GroupName @@ -735,6 +737,7 @@ data ChatResponse | CRPublicGroupCreated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]} | CRPublicGroupCreationFailed {user :: User, addRelayResults :: [AddRelayResult]} | CRGroupRelays {user :: User, groupInfo :: GroupInfo, groupRelays :: [GroupRelay]} + | CRRelayGroupAllowed {user :: User, groupInfo :: GroupInfo} | CRGroupRelaysAdded {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]} | CRGroupRelaysAddFailed {user :: User, addRelayResults :: [AddRelayResult]} | CRGroupMembers {user :: User, group :: Group} @@ -945,6 +948,7 @@ data ChatEvent data TerminalEvent = TEGroupLinkRejected {user :: User, groupInfo :: GroupInfo, groupRejectionReason :: GroupRejectionReason} + | TERelayRejected {user :: User, groupInfo :: GroupInfo, relayRejectionReason :: RelayRejectionReason} | TERejectingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, groupRejectionReason :: GroupRejectionReason} | TENewMemberContact {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} | TEContactVerificationReset {user :: User, contact :: Contact} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 3f66579969..bb31ee26a5 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1587,6 +1587,9 @@ processChatCommand vr nm = \case Right (Just (Just failure)) -> pure $ CRChatRelayTestResult user (Just relayProfile) (Just failure) TestChatRelay address -> withUser $ \User {userId} -> processChatCommand vr nm $ APITestChatRelay userId address + APIAllowRelayGroup groupId -> withUser $ \user -> do + gInfo' <- withStore $ \db -> allowRelayGroup db vr user groupId + pure $ CRRelayGroupAllowed user gInfo' GetUserChatRelays -> withUser $ \user -> do srvs <- withFastStore (`getUserServers` user) liftIO $ CRUserServers user <$> groupByOperator (onlyRelays srvs) @@ -2939,9 +2942,13 @@ processChatCommand vr nm = \case toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo' scopeInfo) ci] -- TODO delete direct connections that were unused deleteGroupLinkIfExists user gInfo' + let relayRejected = useRelays' gInfo && isRelay membership -- member records are not deleted to keep history - withFastStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft - pure $ CRLeftMemberUser user gInfo' {membership = membership {memberStatus = GSMemLeft}} + withFastStore' $ \db -> do + updateGroupMemberStatus db userId membership GSMemLeft + when relayRejected $ updateRelayOwnStatus_ db gInfo RSRejected + let relayOwnStatus' = if relayRejected then Just RSRejected else relayOwnStatus gInfo + pure $ CRLeftMemberUser user gInfo' {membership = membership {memberStatus = GSMemLeft}, relayOwnStatus = relayOwnStatus'} where -- Relay leaving channel: create delivery job for cursor-based sending and async connection cleanup. leaveChannelRelay gInfo = do @@ -2993,6 +3000,9 @@ processChatCommand vr nm = \case LeaveGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand vr nm $ APILeaveGroup groupId + AllowRelayGroup gName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand vr nm $ APIAllowRelayGroup groupId DeleteGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand vr nm $ APIDeleteChat (ChatRef CTGroup groupId Nothing) (CDMFull True) @@ -5041,6 +5051,8 @@ chatCommandP = "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), "/_relay test " *> (APITestChatRelay <$> A.decimal <* A.space <*> strP), "/relay test " *> (TestChatRelay <$> strP), + "/_relay allow #" *> (APIAllowRelayGroup <$> A.decimal), + "/group allow #" *> (AllowRelayGroup <$> displayNameP), "/relays " *> (SetUserChatRelays <$> chatRelaysP), "/relays" $> GetUserChatRelays, "/_operators" $> APIGetServerOperators, diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 8324107a11..c6c3f92752 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1059,6 +1059,28 @@ acceptRelayJoinRequestAsync ownerMember' <- getGroupMemberById db vr user groupMemberId pure (gInfo', ownerMember') +rejectRelayInvitationAsync + :: User + -> Int64 + -> VersionRangeChat + -> GroupRelayInvitation + -> InvitationId + -> VersionRangeChat + -> Int64 + -> RelayRejectionReason + -> CM () +rejectRelayInvitationAsync user uclId vr groupRelayInv invId reqChatVRange initialDelay reason = do + (_gInfo, ownerMember) <- withStore $ \db -> + createRelayRequestGroup db vr user groupRelayInv invId reqChatVRange initialDelay GSMemInvited RSRejected + let GroupMember {groupMemberId} = ownerMember + msg = XGrpRelayReject reason + subMode <- chatReadVar subscriptionMode + chatVR <- chatVersionRange + let chatV = chatVR `peerConnChatVersion` reqChatVRange + connIds <- agentAcceptContactAsync user False invId msg subMode PQSupportOff chatV + withStore' $ \db -> + createJoiningMemberConnection db user uclId connIds chatV reqChatVRange groupMemberId subMode + businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences = GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, publicGroup = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing} diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 6c960c3ce8..08ca90f2a6 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -770,6 +770,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> setRelayLinkConfId db m confId relayLink void $ getAgentConnShortLinkAsync user CFGetRelayDataAccept (Just conn') relayLink | otherwise -> messageError "x.grp.relay.acpt: only owner can add relay" + XGrpRelayReject reason + | memberRole' membership == GROwner && isRelay m -> do + -- GSMemLeft (not GSMemRejected): owner UI treats this identically to an explicit /leave from the relay; GSMemRejected has knocking-admission semantics. + (relay', m') <- withStore $ \db -> do + relay <- getGroupRelayByGMId db (groupMemberId' m) + relay' <- if relayStatus relay == RSInvited + then liftIO $ updateRelayStatusFromTo db relay RSInvited RSRejected + else pure relay + liftIO $ updateGroupMemberStatus db userId m GSMemLeft + pure (relay', m {memberStatus = GSMemLeft}) + -- complete the contact handshake so the relay receives INFO and cleans up its transient bookkeeping + allowAgentConnectionAsync user conn' confId XOk + toView $ CEvtGroupRelayUpdated user gInfo m' relay' + toViewTE $ TERelayRejected user gInfo reason + | otherwise -> messageError "x.grp.relay.reject: only owner should receive relay rejection" _ -> messageError "CONF from invited member must have x.grp.acpt" GCHostMember -> case chatMsgEvent of @@ -817,10 +832,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when (memberStatus m == GSMemRejected) $ do deleteMemberConnection' m True withStore' $ \db -> deleteGroupMember db user m - XOk -> pure () + XOk -> + -- transient relay-reject row cleanup after the rejection handshake completes + when (memberCategory m == GCHostMember && not (relayServesGroup gInfo)) $ do + deleteMemberConnection' m True + withStore' $ \db -> do + deleteGroupMember db user m + deleteGroup db user gInfo _ -> messageError "INFO from member must have x.grp.mem.info, x.info or x.ok" pure () - CON _pqEnc -> unless (memberStatus m == GSMemRejected || memberStatus membership == GSMemRejected) $ do + CON _pqEnc -> unless rejected $ do -- TODO [knocking] send pending messages after accepting? -- possible improvement: check for each pending message, requires keeping track of connection state unless (connDisabled conn) $ sendPendingGroupMessages user gInfo m conn @@ -922,6 +943,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = forM_ (memberConn im) $ \imConn -> void $ sendDirectMemberMessage imConn (XGrpMemCon memberId) groupId _ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected" + where + rejected = + memberStatus m `elem` ([GSMemRejected, GSMemLeft, GSMemRemoved, GSMemGroupDeleted] :: [GroupMemberStatus]) + || memberStatus membership == GSMemRejected + || not (relayServesGroup gInfo) MSG msgMeta _msgFlags msgBody -> do tags <- newTVarIO [] withAckMessage "group msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do @@ -933,7 +959,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = if isUserGrpFwdRelay gInfo' && not (blockedByAdmin m) then let tasks - | relayOwnStatus gInfo' == Just RSInactive = filter relayRemovedNewTask newDeliveryTasks + | not (relayServesGroup gInfo') = filter relayRemovedNewTask newDeliveryTasks | otherwise = newDeliveryTasks in createDeliveryTasks gInfo' m' tasks else pure False @@ -1523,10 +1549,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = mem <- acceptGroupJoinSendRejectAsync user uclId gInfo invId chatVRange p xContactId_ rjctReason toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason xGrpRelayInv :: InvitationId -> VersionRangeChat -> GroupRelayInvitation -> CM () - xGrpRelayInv invId chatVRange groupRelayInv = do + xGrpRelayInv invId chatVRange groupRelayInv@GroupRelayInvitation {groupLink} = do + rejected <- withStore' $ \db -> isRelayGroupRejected db user groupLink initialDelay <- asks $ initialInterval . relayRequestRetryInterval . config - (_gInfo, _ownerMember) <- withStore $ \db -> createRelayRequestGroup db vr user groupRelayInv invId chatVRange initialDelay - lift $ void $ getRelayRequestWorker True + if rejected + then rejectRelayInvitationAsync user uclId vr groupRelayInv invId chatVRange initialDelay RRRRejoinRejected + else do + (_gInfo, _ownerMember) <- withStore $ \db -> + createRelayRequestGroup db vr user groupRelayInv invId chatVRange initialDelay GSMemAccepted RSInvited + lift $ void $ getRelayRequestWorker True xGrpRelayTest :: InvitationId -> VersionRangeChat -> ByteString -> CM () xGrpRelayTest invId chatVRange challenge = do privKey_ <- withAgent $ \a -> getConnLinkPrivKey a (aConnId conn) @@ -3133,7 +3164,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False withStore' $ \db -> do updateGroupMemberStatus db userId membership GSMemRemoved - when (isJust $ relayOwnStatus gInfo) $ updateRelayOwnStatus_ db gInfo RSInactive + when (maybe False (/= RSRejected) (relayOwnStatus gInfo)) $ updateRelayOwnStatus_ db gInfo RSInactive let membership' = membership {memberStatus = GSMemRemoved} when withMessages $ deleteMessages gInfo membership' SMDSnd deleteMemberItem msg gInfo RGEUserDeleted @@ -3572,7 +3603,7 @@ runDeliveryTaskWorker a deliveryKey Worker {doWork} = do processDeliveryTask task@MessageDeliveryTask {jobScope} = case jobScopeImpliedSpec jobScope of DJDeliveryJob _includePending - | relayOwnStatus gInfo == Just RSInactive -> do + | not (relayServesGroup gInfo) -> do logWarn "delivery task worker: relay inactive" withStore' $ \db -> setDeliveryTaskErrStatus db (deliveryTaskId task) "relay inactive" | otherwise -> @@ -3642,7 +3673,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do processDeliveryJob job = case jobScopeImpliedSpec jobScope of DJDeliveryJob _includePending - | relayOwnStatus gInfo == Just RSInactive -> do + | not (relayServesGroup gInfo) -> do logWarn "delivery job worker: relay inactive" withStore' $ \db -> setDeliveryJobErrStatus db (deliveryJobId job) "relay inactive" | otherwise -> do diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 3436a64132..f9c29e3552 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -444,6 +444,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpRelayAcpt :: ShortLinkContact -> ChatMsgEvent 'Json XGrpRelayTest :: ByteString -> Maybe ByteString -> ChatMsgEvent 'Json XGrpRelayNew :: ShortLinkContact -> ChatMsgEvent 'Json + XGrpRelayReject :: RelayRejectionReason -> ChatMsgEvent 'Json XGrpMemNew :: MemberInfo -> Maybe MsgScope -> ChatMsgEvent 'Json XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json @@ -989,6 +990,7 @@ data CMEventTag (e :: MsgEncoding) where XGrpRelayAcpt_ :: CMEventTag 'Json XGrpRelayTest_ :: CMEventTag 'Json XGrpRelayNew_ :: CMEventTag 'Json + XGrpRelayReject_ :: CMEventTag 'Json XGrpMemNew_ :: CMEventTag 'Json XGrpMemIntro_ :: CMEventTag 'Json XGrpMemInv_ :: CMEventTag 'Json @@ -1047,6 +1049,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpRelayAcpt_ -> "x.grp.relay.acpt" XGrpRelayTest_ -> "x.grp.relay.test" XGrpRelayNew_ -> "x.grp.relay.new" + XGrpRelayReject_ -> "x.grp.relay.reject" XGrpMemNew_ -> "x.grp.mem.new" XGrpMemIntro_ -> "x.grp.mem.intro" XGrpMemInv_ -> "x.grp.mem.inv" @@ -1106,6 +1109,7 @@ instance StrEncoding ACMEventTag where "x.grp.relay.acpt" -> XGrpRelayAcpt_ "x.grp.relay.test" -> XGrpRelayTest_ "x.grp.relay.new" -> XGrpRelayNew_ + "x.grp.relay.reject" -> XGrpRelayReject_ "x.grp.mem.new" -> XGrpMemNew_ "x.grp.mem.intro" -> XGrpMemIntro_ "x.grp.mem.inv" -> XGrpMemInv_ @@ -1161,6 +1165,7 @@ toCMEventTag msg = case msg of XGrpRelayAcpt _ -> XGrpRelayAcpt_ XGrpRelayTest {} -> XGrpRelayTest_ XGrpRelayNew _ -> XGrpRelayNew_ + XGrpRelayReject _ -> XGrpRelayReject_ XGrpMemNew {} -> XGrpMemNew_ XGrpMemIntro _ _ -> XGrpMemIntro_ XGrpMemInv _ _ -> XGrpMemInv_ @@ -1319,6 +1324,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do sig_ <- fmap (\(B64UrlByteString s) -> s) <$> opt "signature" pure $ XGrpRelayTest challenge sig_ XGrpRelayNew_ -> XGrpRelayNew <$> p "relayLink" + XGrpRelayReject_ -> XGrpRelayReject <$> p "reason" XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" <*> opt "scope" XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" <*> opt "memberRestrictions" XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro" @@ -1389,6 +1395,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en ("signature" .=? (B64UrlByteString <$> sig_)) ["challenge" .= B64UrlByteString challenge] XGrpRelayNew relayLink -> o ["relayLink" .= relayLink] + XGrpRelayReject reason -> o ["reason" .= reason] XGrpMemNew memInfo scope -> o $ ("scope" .=? scope) ["memberInfo" .= memInfo] XGrpMemIntro memInfo memRestrictions -> o $ ("memberRestrictions" .=? memRestrictions) ["memberInfo" .= memInfo] XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro] diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 4bb94ba2a8..9b21f0697b 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -95,6 +95,8 @@ module Simplex.Chat.Store.Groups createRelayRequestGroup, updateRelayOwnStatusFromTo, updateRelayOwnStatus_, + isRelayGroupRejected, + allowRelayGroup, getRelayServedGroups, getRelayInactiveGroups, createNewContactMemberAsync, @@ -1523,8 +1525,8 @@ setGroupInProgressDone db GroupInfo {groupId} = do "UPDATE groups SET creating_in_progress = 0, updated_at = ? WHERE group_id = ?" (currentTs, groupId) -createRelayRequestGroup :: DB.Connection -> VersionRangeChat -> User -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> Int64 -> ExceptT StoreError IO (GroupInfo, GroupMember) -createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink} invId reqChatVRange initialDelay = do +createRelayRequestGroup :: DB.Connection -> VersionRangeChat -> User -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> Int64 -> GroupMemberStatus -> RelayStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) +createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink} invId reqChatVRange initialDelay memberStatus relayStatus = do currentTs <- liftIO getCurrentTime -- Create group with placeholder profile let Profile {displayName = fromMemberLDN} = fromMemberProfile @@ -1538,13 +1540,13 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe groupPreferences = Nothing, memberAdmission = Nothing } - (groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just RSInvited) Nothing currentTs + (groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just relayStatus) Nothing currentTs -- Store relay request data for recovery liftIO $ setRelayRequestData_ groupId currentTs ownerMemberId <- insertOwner_ currentTs groupId let relayMember = MemberIdRole relayMemberId GRRelay -- TODO [member keys] should relays use member keys? - _membership <- createContactMemberInv_ db user groupId (Just ownerMemberId) user relayMember GCUserMember GSMemAccepted IBUnknown Nothing Nothing currentTs vr + _membership <- createContactMemberInv_ db user groupId (Just ownerMemberId) user relayMember GCUserMember memberStatus IBUnknown Nothing Nothing currentTs vr ownerMember <- getGroupMember db vr user groupId ownerMemberId g <- getGroupInfo db vr user groupId pure (g, ownerMember) @@ -1578,7 +1580,7 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe peer_chat_min_version, peer_chat_max_version) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, indexInGroup, memberId, memberRole, GCHostMember, GSMemAccepted) + ( (groupId, indexInGroup, memberId, memberRole, GCHostMember, memberStatus) :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) :. (minV, maxV) ) @@ -1596,6 +1598,41 @@ updateRelayOwnStatus_ db GroupInfo {groupId} relayStatus = do let inactiveAt_ = if relayStatus == RSInactive then Just currentTs else Nothing DB.execute db "UPDATE groups SET relay_own_status = ?, relay_inactive_at = ?, updated_at = ? WHERE group_id = ?" (relayStatus, inactiveAt_, currentTs, groupId) +-- Flip every RSRejected row sharing the targeted group's relay_request_group_link +-- to RSInactive in one statement; returns the refreshed GroupInfo for the targeted groupId. +allowRelayGroup :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO GroupInfo +allowRelayGroup db vr user@User {userId} groupId = do + currentTs <- liftIO getCurrentTime + liftIO $ + DB.execute + db + [sql| + UPDATE groups + SET relay_own_status = ?, relay_inactive_at = ?, updated_at = ? + WHERE user_id = ? + AND relay_request_group_link = (SELECT relay_request_group_link FROM groups WHERE group_id = ?) + AND relay_own_status = ? + |] + (RSInactive, currentTs, currentTs, userId, groupId, RSRejected) + getGroupInfo db vr user groupId + +isRelayGroupRejected :: DB.Connection -> User -> ShortLinkContact -> IO Bool +isRelayGroupRejected db User {userId} groupLink = + fromMaybe False <$> maybeFirstRow fromOnly ( + DB.query + db + [sql| + SELECT EXISTS ( + SELECT 1 FROM groups + WHERE user_id = ? + AND relay_request_group_link = ? + AND relay_own_status = ? + LIMIT 1 + ) + |] + (userId, groupLink, RSRejected) + ) + getRelayServedGroups :: DB.Connection -> VersionRangeChat -> User -> IO [GroupInfo] getRelayServedGroups db vr User {userId, userContactId} = do map (toGroupInfo vr userContactId []) diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 822068a771..437f16a43c 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -30,6 +30,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays import Simplex.Chat.Store.Postgres.Migrations.M20260403_item_viewed import Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries import Simplex.Chat.Store.Postgres.Migrations.M20260507_relay_inactive_at +import Simplex.Chat.Store.Postgres.Migrations.M20260514_relay_request_group_link_index import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -59,7 +60,8 @@ schemaMigrations = ("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays), ("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed), ("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries), - ("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at) + ("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at), + ("20260514_relay_request_group_link_index", m20260514_relay_request_group_link_index, Just down_m20260514_relay_request_group_link_index) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260514_relay_request_group_link_index.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260514_relay_request_group_link_index.hs new file mode 100644 index 0000000000..217b56d2fa --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260514_relay_request_group_link_index.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260514_relay_request_group_link_index where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260514_relay_request_group_link_index :: Text +m20260514_relay_request_group_link_index = + [r| +CREATE INDEX idx_groups_relay_request_group_link + ON groups(user_id, relay_request_group_link) + WHERE relay_request_group_link IS NOT NULL; +|] + +down_m20260514_relay_request_group_link_index :: Text +down_m20260514_relay_request_group_link_index = + [r| +DROP INDEX idx_groups_relay_request_group_link; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 495a6bb752..6026049313 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -2359,6 +2359,10 @@ CREATE INDEX idx_groups_inv_queue_info ON test_chat_schema.groups USING btree (i +CREATE INDEX idx_groups_relay_request_group_link ON test_chat_schema.groups USING btree (user_id, relay_request_group_link) WHERE (relay_request_group_link IS NOT NULL); + + + CREATE INDEX idx_groups_summary_current_members_count ON test_chat_schema.groups USING btree (summary_current_members_count); diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 4ee3f44b07..9990ed74fd 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -153,6 +153,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays import Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed import Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries import Simplex.Chat.Store.SQLite.Migrations.M20260507_relay_inactive_at +import Simplex.Chat.Store.SQLite.Migrations.M20260514_relay_request_group_link_index import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -305,7 +306,8 @@ schemaMigrations = ("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays), ("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed), ("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries), - ("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at) + ("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at), + ("20260514_relay_request_group_link_index", m20260514_relay_request_group_link_index, Just down_m20260514_relay_request_group_link_index) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260514_relay_request_group_link_index.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260514_relay_request_group_link_index.hs new file mode 100644 index 0000000000..ef2bc8ccd0 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260514_relay_request_group_link_index.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260514_relay_request_group_link_index where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260514_relay_request_group_link_index :: Query +m20260514_relay_request_group_link_index = + [sql| +CREATE INDEX idx_groups_relay_request_group_link + ON groups(user_id, relay_request_group_link) + WHERE relay_request_group_link IS NOT NULL; +|] + +down_m20260514_relay_request_group_link_index :: Query +down_m20260514_relay_request_group_link_index = + [sql| +DROP INDEX idx_groups_relay_request_group_link; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index f4590f48c9..127fce8e45 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -3338,6 +3338,20 @@ SCAN CONSTANT ROW SCALAR SUBQUERY 1 SCAN groups +Query: + SELECT EXISTS ( + SELECT 1 FROM groups + WHERE user_id = ? + AND relay_request_group_link = ? + AND relay_own_status = ? + LIMIT 1 + ) + +Plan: +SCAN CONSTANT ROW +SCALAR SUBQUERY 1 +SEARCH groups USING INDEX idx_groups_relay_request_group_link (user_id=? AND relay_request_group_link=?) + Query: SELECT agent_conn_id FROM connections @@ -3955,15 +3969,6 @@ Query: Plan: SEARCH chat_items USING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) -Query: - UPDATE chat_items - SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 - RETURNING chat_item_id - -Plan: -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id=? AND msg_content_tag=? AND item_deleted=? AND item_sent=?) - Query: UPDATE chat_items SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? @@ -4044,6 +4049,18 @@ Query: Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE groups + SET relay_own_status = ?, relay_inactive_at = ?, updated_at = ? + WHERE user_id = ? + AND relay_request_group_link = (SELECT relay_request_group_link FROM groups WHERE group_id = ?) + AND relay_own_status = ? + +Plan: +SEARCH groups USING INDEX idx_groups_relay_request_group_link (user_id=? AND relay_request_group_link=?) +SCALAR SUBQUERY 1 +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET via_group_link_uri = ?, via_group_link_uri_hash = ? @@ -6586,6 +6603,10 @@ Query: SELECT 1 FROM settings WHERE user_id = ? LIMIT 1 Plan: SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?) +Query: SELECT COUNT(*) FROM groups WHERE relay_own_status IS NOT NULL +Plan: +SCAN groups + Query: SELECT COUNT(1) FROM chat_item_versions WHERE chat_item_id = ? Plan: SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) @@ -6801,6 +6822,10 @@ Query: SELECT group_id, conn_full_link_to_connect FROM groups WHERE user_id = ? Plan: SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?) +Query: SELECT group_id, relay_own_status FROM groups WHERE relay_own_status IS NOT NULL ORDER BY group_id +Plan: +SCAN groups + Query: SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1 Plan: SEARCH group_members USING INDEX idx_group_members_user_id (user_id=?) @@ -6865,6 +6890,10 @@ Query: SELECT relay_own_status FROM groups WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT relay_status FROM group_relays +Plan: +SCAN group_relays + Query: SELECT relay_status FROM group_relays WHERE group_relay_id = ? Plan: SEARCH group_relays USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index b7a6db437b..86c198670c 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -1295,6 +1295,12 @@ CREATE INDEX idx_chat_items_groups_item_viewed ON chat_items( item_viewed, item_ts ); +CREATE INDEX idx_groups_relay_request_group_link +ON groups( + user_id, + relay_request_group_link +) +WHERE relay_request_group_link IS NOT NULL; CREATE TRIGGER on_group_members_insert_update_summary AFTER INSERT ON group_members FOR EACH ROW diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index e2efcdf6d6..f2892898c4 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -494,6 +494,12 @@ data GroupInfo = GroupInfo useRelays' :: GroupInfo -> Bool useRelays' GroupInfo {useRelays} = isTrue useRelays +relayServesGroup :: GroupInfo -> Bool +relayServesGroup GroupInfo {relayOwnStatus} = case relayOwnStatus of + Just RSInactive -> False + Just RSRejected -> False + _ -> True + publicGroupEditor :: GroupInfo -> GroupMember -> Bool publicGroupEditor gInfo mem = useRelays' gInfo && memberRole' mem >= GRModerator @@ -919,6 +925,26 @@ instance ToJSON GroupRejectionReason where toJSON = strToJSON toEncoding = strToJEncoding +data RelayRejectionReason + = RRRRejoinRejected + | RRRUnknown {text :: Text} + deriving (Eq, Show) + +instance StrEncoding RelayRejectionReason where + strEncode = \case + RRRRejoinRejected -> "rejoin_rejected" + RRRUnknown text -> encodeUtf8 text + strP = + "rejoin_rejected" $> RRRRejoinRejected + <|> RRRUnknown . safeDecodeUtf8 <$> A.takeByteString + +instance FromJSON RelayRejectionReason where + parseJSON = strParseJSON "RelayRejectionReason" + +instance ToJSON RelayRejectionReason where + toJSON = strToJSON + toEncoding = strToJEncoding + data MemberIdRole = MemberIdRole { memberId :: MemberId, memberRole :: GroupMemberRole diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index e0630e2e42..c71f7ce37a 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -84,6 +84,7 @@ data RelayStatus | RSAccepted | RSActive | RSInactive + | RSRejected deriving (Eq, Show) relayStatusText :: RelayStatus -> Text @@ -93,6 +94,7 @@ relayStatusText = \case RSAccepted -> "accepted" RSActive -> "active" RSInactive -> "inactive" + RSRejected -> "rejected" instance TextEncoding RelayStatus where textEncode = \case @@ -101,12 +103,14 @@ instance TextEncoding RelayStatus where RSAccepted -> "accepted" RSActive -> "active" RSInactive -> "inactive" + RSRejected -> "rejected" textDecode = \case "new" -> Just RSNew "invited" -> Just RSInvited "accepted" -> Just RSAccepted "active" -> Just RSActive "inactive" -> Just RSInactive + "rejected" -> Just RSRejected _ -> Nothing instance FromField RelayStatus where fromField = fromTextField_ textDecode @@ -115,6 +119,7 @@ instance ToField RelayStatus where toField = toField . textEncode $(JQ.deriveJSON (enumJSON $ dropPrefix "RS") ''RelayStatus) + data MsgSigStatus = MSSVerified | MSSSignedNoKey deriving (Eq, Show) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 1211dc55a9..477850d4b0 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -184,6 +184,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRGroupRelays u g relays -> ttyUser u $ viewGroupRelays g relays CRGroupRelaysAdded u g _groupLink relays -> ttyUser u $ viewGroupRelays g relays CRGroupRelaysAddFailed u results -> ttyUser u $ viewGroupRelaysAddFailed results + CRRelayGroupAllowed u g -> ttyUser u [ttyFullGroup g <> ": relay rejection cleared"] CRGroupMembers u g -> ttyUser u $ viewGroupMembers g CRMemberSupportChats u g ms -> ttyUser u $ viewMemberSupportChats g ms -- CRGroupConversationsArchived u _g _conversations -> ttyUser u [] @@ -222,7 +223,14 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRUserDeletedMembers u g members wm signed -> case members of [m] -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group" <> withMessages wm <> signedStr signed] mems' -> ttyUser u [ttyGroup' g <> ": you removed " <> sShow (length mems') <> " members from the group" <> withMessages wm <> signedStr signed] - CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g + CRLeftMemberUser u g + | relayOwnStatus g == Just RSRejected -> + ttyUser u + [ ttyGroup' g <> ": you left the group (future invitations will be rejected)", + "use " <> highlight ("/group allow #" <> viewGroupName g) <> " to allow future invitations", + "use " <> highlight ("/d #" <> viewGroupName g) <> " to delete the group (also clears the rejection)" + ] + | otherwise -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g CRGroupDeletedUser u g signed -> ttyUser u [ttyGroup' g <> ": you deleted the group" <> signedStr signed] CRForwardPlan u count itemIds fc -> ttyUser u $ viewForwardPlan count itemIds fc CRChatMsgContent u mc -> ttyUser u $ ttyMsgContent mc <> viewMsgTestInfo testView mc @@ -541,6 +549,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtTerminalEvent te -> case te of TERejectingGroupJoinRequestMember _ g m reason -> [ttyFullMember m <> ": rejecting request to join group " <> ttyGroup' g <> ", reason: " <> sShow reason] TEGroupLinkRejected u g reason -> ttyUser u [ttyGroup' g <> ": join rejected, reason: " <> sShow reason] + TERelayRejected u g reason -> ttyUser u [ttyGroup' g <> ": relay rejected, reason: " <> sShow reason] TENewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"] TEContactVerificationReset u ct -> ttyUser u $ viewContactVerificationReset ct TEGroupMemberVerificationReset u g m -> ttyUser u $ viewGroupMemberVerificationReset g m @@ -1435,11 +1444,14 @@ viewGroupsList gs = map groupSS $ sortOn ldn_ gs where ldn_ :: GroupInfo -> Text ldn_ GroupInfo {localDisplayName} = T.toLower localDisplayName - groupSS g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}, groupSummary = GroupSummary {currentMembers}} = + groupSS g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}, groupSummary = GroupSummary {currentMembers}, relayOwnStatus} = case memberStatus membership of GSMemInvited -> groupInvitation' g - s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s <> alias g + s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s <> rejectionSuffix <> alias g where + rejectionSuffix = case relayOwnStatus of + Just RSRejected -> " [rejected]" + _ -> "" viewMemberStatus = \case GSMemRejected -> delete "you are rejected" GSMemRemoved -> delete "you are removed" diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 6ceb3c2cbe..e0ff178db4 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -272,6 +272,11 @@ chatGroupTests = do it "should add relay to existing channel" testChannelAddRelay it "should remove relay from channel" testChannelRemoveRelay it "should remove left relay from channel" testChannelRemoveLeftRelay + describe "relay rejection" $ do + it "relay rejects fresh invitation after leaving the same channel" testRelayRejectAfterLeave + it "operator allow clears rejection and relay accepts again" testRelayAllowAcceptsAgain + it "rejection on channel A does not affect unrelated channel B" testRelayDoesNotRejectUnrelatedChannel + it "concurrent fresh invitations both rejected" testRelayRejectRaceConcurrentInvitations describe "channel message operations" $ do it "should update channel message" testChannelMessageUpdate it "should delete channel message" testChannelMessageDelete @@ -9474,8 +9479,9 @@ testChannelRelayLeave ps = -- relay1 (bob) leaves threadDelay 100000 bob ##> "/leave #team" - bob <## "#team: you left the group" - bob <## "use /d #team to delete the group" + bob <## "#team: you left the group (future invitations will be rejected)" + bob <## "use /group allow #team to allow future invitations" + bob <## "use /d #team to delete the group (also clears the rejection)" concurrentlyN_ [ alice <## "#team: bob left the group (signed)", -- cath: not notified (relays not connected, owner doesn't forward) @@ -9497,8 +9503,9 @@ testChannelRelayLeave ps = -- relay2 (cath) leaves threadDelay 100000 cath ##> "/leave #team" - cath <## "#team: you left the group" - cath <## "use /d #team to delete the group" + cath <## "#team: you left the group (future invitations will be rejected)" + cath <## "use /group allow #team to allow future invitations" + cath <## "use /d #team to delete the group (also clears the rejection)" concurrentlyN_ [ alice <## "#team: cath left the group (signed)", dan <## "#team: cath left the group (signed)", @@ -9869,8 +9876,9 @@ testChannelRemoveLeftRelay ps = bob ##> "/l team" concurrentlyN_ [ do - bob <## "#team: you left the group" - bob <## "use /d #team to delete the group", + bob <## "#team: you left the group (future invitations will be rejected)" + bob <## "use /group allow #team to allow future invitations" + bob <## "use /d #team to delete the group (also clears the rejection)", alice <## "#team: bob left the group (signed)", dan <## "#team: bob left the group (signed)" ] @@ -9898,8 +9906,9 @@ testChannelRemoveLeftRelay ps = cath ##> "/l team" concurrentlyN_ [ do - cath <## "#team: you left the group" - cath <## "use /d #team to delete the group", + cath <## "#team: you left the group (future invitations will be rejected)" + cath <## "use /group allow #team to allow future invitations" + cath <## "use /d #team to delete the group (also clears the rejection)", alice <## "#team: cath left the group (signed)", dan <## "#team: cath left the group (signed)" ] @@ -9921,6 +9930,271 @@ testChannelRemoveLeftRelay ps = DB.query_ db "SELECT local_display_name FROM group_members" :: IO [Only T.Text] danMembers2 `shouldMatchList` [Only "dan", Only "alice"] +queryRelayOwnStatus :: TestCC -> Int64 -> IO (Maybe T.Text) +queryRelayOwnStatus cc gId = do + rows <- withCCTransaction cc $ \db -> + DB.query db "SELECT relay_own_status FROM groups WHERE group_id = ?" (Only gId) + :: IO [Only (Maybe T.Text)] + pure $ case rows of + [Only s] -> s + _ -> Nothing + +listRelayOwnStatuses :: TestCC -> IO [(Int64, T.Text)] +listRelayOwnStatuses cc = + withCCTransaction cc $ \db -> + DB.query_ + db + "SELECT group_id, relay_own_status FROM groups WHERE relay_own_status IS NOT NULL ORDER BY group_id" + :: IO [(Int64, T.Text)] + +checkRelayGroupCount :: TestCC -> Int -> IO () +checkRelayGroupCount cc expected = do + rows <- withCCTransaction cc $ \db -> + DB.query_ db "SELECT COUNT(*) FROM groups WHERE relay_own_status IS NOT NULL" :: IO [Only Int] + let n = case rows of + [Only c] -> c + _ -> 0 + n `shouldBe` expected + +testRelayRejectAfterLeave :: HasCallStack => TestParams -> IO () +testRelayRejectAfterLeave ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + threadDelay 100000 + + -- baseline: subscriber receives forwarded messages via the active relay + alice #> "#team hello" + bob <# "#team> hello" + cath <# "#team> hello [>>]" + + -- relay leaves the channel: subscriber gets the signed leave notice via bob's + -- DJRelayRemoved job, then has no relay to forward subsequent messages. + bob ##> "/leave #team" + bob <## "#team: you left the group (future invitations will be rejected)" + bob <## "use /group allow #team to allow future invitations" + bob <## "use /d #team to delete the group (also clears the rejection)" + concurrentlyN_ + [ alice <## "#team: bob left the group (signed)", + cath <## "#team: bob left the group (signed)" + ] + threadDelay 100000 + + bobLeaveStatus <- queryRelayOwnStatus bob 1 + bobLeaveStatus `shouldBe` Just "rejected" + + -- with no active relay, owner's messages don't reach the subscriber + alice #> "#team after leave" + (cath "/rm #team bob" + alice <## "#team: you removed bob from the group (signed)" + threadDelay 100000 + + -- owner re-adds bob as relay + alice ##> "/_add relays #1 1" + alice <## "#team: group relays:" + alice .<##. (" - relay id", ": invited") + + -- bob's xGrpRelayInv finds the 'rejected' row for this link and sends XGrpRelayReject. + -- alice's CONF handler emits TERelayRejected; the relay row flips to 'rejected'. + alice <## "#team: relay rejected, reason: RRRRejoinRejected" + + -- assert alice's fresh GroupRelay row is marked 'rejected' and the relay + -- GroupMember is GSMemLeft so the owner UI treats it as gone + aliceRelayStatuses <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT relay_status FROM group_relays" :: IO [Only T.Text] + map (\(Only s) -> s) aliceRelayStatuses `shouldBe` ["rejected"] + aliceRelayMemStatuses <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT member_status FROM group_members WHERE member_role = 'relay'" + :: IO [Only T.Text] + map (\(Only s) -> s) aliceRelayMemStatuses `shouldBe` ["left"] + + -- subscriber still doesn't receive after the failed re-invitation + alice #> "#team after rejection" + (cath TestParams -> IO () +testRelayAllowAcceptsAgain ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + threadDelay 100000 + + -- baseline: subscriber receives forwarded messages + alice #> "#team hello" + bob <# "#team> hello" + cath <# "#team> hello [>>]" + + bob ##> "/leave #team" + bob <## "#team: you left the group (future invitations will be rejected)" + bob <## "use /group allow #team to allow future invitations" + bob <## "use /d #team to delete the group (also clears the rejection)" + concurrentlyN_ + [ alice <## "#team: bob left the group (signed)", + cath <## "#team: bob left the group (signed)" + ] + threadDelay 100000 + + -- with no relay, subscriber doesn't receive + alice #> "#team during downtime" + (cath "/group allow #team" + bob <## "#team: relay rejection cleared" + bobClearStatus <- queryRelayOwnStatus bob 1 + bobClearStatus `shouldBe` Just "inactive" + + -- owner can now re-add and bob accepts as relay (the rejection has been cleared) + alice ##> "/rm #team bob" + alice <## "#team: you removed bob from the group (signed)" + threadDelay 100000 + + alice ##> "/_add relays #1 1" + concurrentlyN_ + [ do + alice <## "#team: group relays:" + alice .<##. (" - relay id", ": invited") + alice <## "#team: group link relays updated, current relays:" + alice .<##. (" - relay id", ": active") + alice <## "group link:" + void $ getTermLine alice, + bob <## "#team_1: you joined the group as relay" + ] + threadDelay 100000 + + -- subscriber syncs against link data and reconnects to the new relay + cath ##> "/_get group link data #1" + cath <## "group ID: 1" + void $ getTermLine cath + concurrentlyN_ + [ do + cath <## "#team: joining the group (connecting to relay bob)..." + cath <## "#team: you joined the group (connected to relay bob)", + do + bob <## "cath_1 (Catherine): accepting request to join group #team_1..." + bob <## "#team_1: cath_1 joined the group" + ] + threadDelay 100000 + + -- delivery resumes through the freshly accepted relay + alice #> "#team after allow" + bob <# "#team_1> after allow" + cath <# "#team> after allow [>>]" + + -- after re-acceptance, the relay GroupMember is not in the rejected/left state + aliceRelayMemStatuses <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT member_status FROM group_members WHERE member_role = 'relay'" + :: IO [Only T.Text] + map (\(Only s) -> s) aliceRelayMemStatuses `shouldBe` ["connected"] + +testRelayDoesNotRejectUnrelatedChannel :: HasCallStack => TestParams -> IO () +testRelayDoesNotRejectUnrelatedChannel ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + _ <- prepareChannel1Relay "teama" alice bob + threadDelay 100000 + + bob ##> "/leave #teama" + bob <## "#teama: you left the group (future invitations will be rejected)" + bob <## "use /group allow #teama to allow future invitations" + bob <## "use /d #teama to delete the group (also clears the rejection)" + alice <## "#teama: bob left the group (signed)" + threadDelay 100000 + + bobAStatus <- queryRelayOwnStatus bob 1 + bobAStatus `shouldBe` Just "rejected" + + -- alice creates a second channel reusing the same bob relay config. + -- bob's xGrpRelayInv for teamb's link finds no rejection and accepts normally. + (shortLinkB, fullLinkB) <- prepareChannel' 2 "teamb" alice bob + memberJoinChannel "teamb" [bob] [alice] shortLinkB fullLinkB cath + threadDelay 100000 + + -- subscriber on teamb receives forwarded messages, proving bob accepts teamb + -- even though teama remains rejected on bob's side. + alice #> "#teamb hello" + bob <# "#teamb> hello" + cath <# "#teamb> hello [>>]" + + bobBStatus <- queryRelayOwnStatus bob 2 + bobBStatus `shouldNotBe` Just "rejected" + bobBStatus `shouldNotBe` Nothing + +testRelayRejectRaceConcurrentInvitations :: HasCallStack => TestParams -> IO () +testRelayRejectRaceConcurrentInvitations ps = + -- After rejection, multiple sequential re-invitations must all reject with + -- consistent state (each transient row created with RSRejected and cleaned + -- up by its own INFO). + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + threadDelay 100000 + + -- baseline: subscriber receives forwarded messages + alice #> "#team hello" + bob <# "#team> hello" + cath <# "#team> hello [>>]" + + bob ##> "/leave #team" + bob <## "#team: you left the group (future invitations will be rejected)" + bob <## "use /group allow #team to allow future invitations" + bob <## "use /d #team to delete the group (also clears the rejection)" + concurrentlyN_ + [ alice <## "#team: bob left the group (signed)", + cath <## "#team: bob left the group (signed)" + ] + threadDelay 100000 + + -- first rejection + alice ##> "/rm #team bob" + alice .<##. ("#team: you removed bob from the group", "") + threadDelay 100000 + alice ##> "/_add relays #1 1" + alice <## "#team: group relays:" + alice .<##. (" - relay id", ": invited") + alice <## "#team: relay rejected, reason: RRRRejoinRejected" + threadDelay 1000000 + checkRelayGroupCount bob 1 + + -- subscriber doesn't receive between rejections (no active relay) + alice #> "#team between rejections" + (cath "/rm #team bob" + alice .<##. ("#team: you removed bob from the group", "") + threadDelay 100000 + alice ##> "/_add relays #1 1" + alice <## "#team: group relays:" + alice .<##. (" - relay id", ": invited") + alice <## "#team: relay rejected, reason: RRRRejoinRejected" + + -- subscriber still doesn't receive after the second rejection + alice #> "#team after second rejection" + (cath TestParams -> IO () testChannelCreateDeletedRelay ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do diff --git a/website/.eleventy.js b/website/.eleventy.js index b02cc49e78..f0310c5665 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -12,6 +12,15 @@ const pluginRss = require('@11ty/eleventy-plugin-rss') const { JSDOM } = require('jsdom') +// Links page data +const parseLinks = require('./parse_links') +const linksFilePath = path.resolve(__dirname, '../docs/LINKS.md') +const linksData = fs.existsSync(linksFilePath) ? parseLinks(linksFilePath) : [] +const linkImagesDir = path.resolve(__dirname, 'src/link-images') +linksData.forEach(entry => { + entry.imageExists = entry.image && fs.existsSync(path.join(linkImagesDir, entry.image)) +}) + // The implementation of Glossary feature const md = new markdownIt() const glossaryMarkdownContent = fs.readFileSync(path.resolve(__dirname, '../docs/GLOSSARY.md'), 'utf8') @@ -77,6 +86,15 @@ module.exports = function (ty) { return markdownLib.render(content); }); + ty.addGlobalData("links", linksData) + ty.addGlobalData("linkLanguages", [...new Set(linksData.map(e => e.language).filter(Boolean))].sort()) + + const catCounts = {} + linksData.forEach(e => { if (e.category) { const c = e.category.toLowerCase(); catCounts[c] = (catCounts[c] || 0) + 1 } }) + const mediaPills = ["Video", "Audio"].filter(p => linksData.some(e => e.mediaType === p.toLowerCase())) + const catPills = Object.keys(catCounts).sort() + ty.addGlobalData("linkPills", mediaPills.concat(catPills)) + ty.addShortcode("cfg", (name) => globalConfig[name]) ty.addFilter("getlang", (path) => { @@ -298,6 +316,7 @@ module.exports = function (ty) { ty.addPassthroughCopy("src/call") ty.addPassthroughCopy("src/hero-phone") ty.addPassthroughCopy("src/hero-phone-dark") + ty.addPassthroughCopy({ "src/link-images": "links/images" }) ty.addPassthroughCopy("src/blog/images") ty.addPassthroughCopy("src/docs/*.png") ty.addPassthroughCopy("src/docs/images") diff --git a/website/langs/en.json b/website/langs/en.json index 490e693f18..2e860d76d4 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -368,5 +368,8 @@ "file-proto-p-2": "File encryption key is present only in the URL hash fragment - your browser never sends it to a server. There are 3 encryption layers: TLS transport, per-recipient encryption (unique ephemeral key per transfer), and file end-to-end encryption.", "file-proto-h-4": "Independent data routers", "file-proto-p-4": "When file is split to fragments, it is sent via network routers operated by independent parties. No operator can see the actual file size or name. Even if a router is compromised, it can only see encrypted fragments of fixed size. File fragments are cached by network routers for approximately 48 hours.", - "file-proto-spec": "Read the XFTP protocol specification →" + "file-proto-spec": "Read the XFTP protocol specification →", + "links": "Links", + "links-title": "Community Links", + "links-all-languages": "All languages" } diff --git a/website/parse_links.js b/website/parse_links.js new file mode 100644 index 0000000000..487330b861 --- /dev/null +++ b/website/parse_links.js @@ -0,0 +1,184 @@ +const fs = require("fs") +const slugify = require("slugify") + +function parseLinks(linksFilePath) { + const content = fs.readFileSync(linksFilePath, "utf8") + const lines = content.split("\n") + const entries = [] + + // First pass: split into raw entry blocks at ## boundaries + const blocks = [] + let current = null + for (const line of lines) { + if (line.startsWith("## ")) { + if (current) blocks.push(current) + current = { title: line.slice(3).trim(), lines: [] } + } else if (current) { + current.lines.push(line) + } + } + if (current) blocks.push(current) + + // Second pass: parse each block + for (const block of blocks) { + // Collect non-empty lines in order + const parts = block.lines.map(l => l.trim()).filter(l => l) + + let originalTitle = "" + let publisher = "" + let category = "" + let featured = false + let preview = "" + let image = "" + let language = "" + let date = "" + let estimated = false + let url = "" + + let idx = 0 + + // Optional: original title in parentheses + if (idx < parts.length && parts[idx].startsWith("(") && parts[idx].endsWith(")")) { + originalTitle = parts[idx].slice(1, -1) + idx++ + } + + // Publisher: first line that's not a metadata prefix and not "Featured" + if (idx < parts.length && !isMetadata(parts[idx]) && parts[idx] !== "Featured") { + publisher = parts[idx] + idx++ + } + + // Category: next non-metadata, non-Featured line + if (idx < parts.length && !isMetadata(parts[idx]) && parts[idx] !== "Featured") { + category = parts[idx] + idx++ + } + + // Optional: Featured + if (idx < parts.length && parts[idx] === "Featured") { + featured = true + idx++ + } + + // Preview: collect lines until we hit a metadata line + const previewLines = [] + while (idx < parts.length && !isMetadata(parts[idx])) { + previewLines.push(parts[idx]) + idx++ + } + preview = previewLines.join(" ") + + // Metadata lines: Image, Language, Date, URL + while (idx < parts.length) { + const line = parts[idx] + if (line.startsWith("Image: ")) { + image = line.slice(7) + } else if (line.startsWith("Language: ")) { + language = line.slice(10) + } else if (line.startsWith("Date: ")) { + const rawDate = line.slice(6) + if (rawDate.includes("(estimated)")) { + estimated = true + date = rawDate.replace("(estimated)", "").trim() + } else { + date = rawDate + } + } else if (line.startsWith("http")) { + url = line + } + idx++ + } + + if (!block.title || !url) continue + + let contentCategory = category + let explicitMedia = "" + if (category.includes(", ")) { + const parts = category.split(", ") + contentCategory = parts[0].trim() + explicitMedia = parts[1].trim().toLowerCase() + } + + entries.push({ + id: slugify(block.title, { lower: true, strict: true }).slice(0, 80), + title: block.title, + originalTitle, + publisher, + category: contentCategory, + featured, + preview, + image, + language, + date, + dateSort: normalizeDateForSort(date), + estimated, + url, + mediaType: explicitMedia || deriveMediaType(category), + }) + } + + // Deduplicate IDs by appending language suffix where needed + const idCounts = {} + for (const entry of entries) { + idCounts[entry.id] = (idCounts[entry.id] || 0) + 1 + } + for (const entry of entries) { + if (idCounts[entry.id] > 1 && entry.language) { + entry.id = entry.id.slice(0, 70) + "-" + slugify(entry.language, { lower: true, strict: true }) + } + } + // Final pass: if still duplicates, append index + const seen = {} + for (const entry of entries) { + if (seen[entry.id]) { + entry.id = entry.id + "-" + (seen[entry.id]++) + } else { + seen[entry.id] = 1 + } + } + + entries.sort((a, b) => b.dateSort.localeCompare(a.dateSort)) + return entries +} + +function isMetadata(line) { + return line.startsWith("Image: ") || + line.startsWith("Language: ") || + line.startsWith("Date: ") || + line.startsWith("http") +} + +function deriveMediaType(category) { + const lower = category.toLowerCase() + if (lower.includes("video") || lower.includes("livestream") || lower.includes("conference talk")) return "video" + if (lower.includes("podcast") || lower.includes("audio")) return "audio" + return "text" +} + + +function normalizeDateForSort(dateStr) { + if (!dateStr) return "1970-01-01" + + // Full date: "Apr 29, 2026" or "Dec 2, 2022" + const fullDate = new Date(dateStr) + if (!isNaN(fullDate.getTime())) { + return fullDate.toISOString().slice(0, 10) + } + + // Month + year: "May 2026" + const monthYear = new Date(dateStr + " 1") + if (!isNaN(monthYear.getTime())) { + return monthYear.toISOString().slice(0, 10) + } + + // Year only: "2024" + const yearMatch = dateStr.match(/(\d{4})/) + if (yearMatch) { + return yearMatch[1] + "-01-01" + } + + return "1970-01-01" +} + +module.exports = parseLinks diff --git a/website/plans/2026-05-20-links-page.md b/website/plans/2026-05-20-links-page.md new file mode 100644 index 0000000000..eb4760620f --- /dev/null +++ b/website/plans/2026-05-20-links-page.md @@ -0,0 +1,189 @@ +# Links Page Implementation Plan + +## Overview + +Single page at `/links` showing 300+ external publications, reviews, bots, services, and community content about SimpleX Chat. All items rendered as HTML in the DOM for SEO. Client-side JS handles pagination via `display:none` and hash-based navigation. + +Content source: `docs/LINKS.md` (parsed at build time). +Images: `docs/links/images/` (copied at build time, missing images handled gracefully). + +## Architecture + +The parser is a Node.js module imported directly by `.eleventy.js` (like the glossary parser), not a separate build step writing to `_data/`. It reads `docs/LINKS.md` and returns a structured array that Eleventy uses as template data. + +## Files to Create + +### 1. `website/parse_links.js` - Markdown parser module + +Exports a function that reads `src/docs/LINKS.md` (after `web.sh` copies docs into src/) and returns an array of entry objects. + +Parser logic - reads line by line, entry starts at `## `: +``` +## Title -> title +(Original Title) -> originalTitle (optional, detected by leading paren) +Publisher -> publisher +Category -> category +Featured -> featured: true (optional, detected by exact match) +Preview paragraph text. -> preview (first non-metadata, non-empty line after above) +Image: filename.jpg -> image (strip "Image: " prefix) +Language: German -> language (strip "Language: " prefix) +Date: Dec 2, 2022 -> date (raw string), dateSort (normalized YYYY-MM-DD) + -> estimated: true if "(estimated)" in date string +https://example.com/... -> url (bare URL line) +``` + +Derives from category: +- `mediaType`: "video" if category contains "video"/"livestream", "audio" if contains "podcast"/"audio", "text" otherwise + +Generates: +- `id`: semantic slug from title (slugify, lowercase, truncated to reasonable length) +- `imageExists`: checks if file exists in `src/link-images/` + +Returns array sorted reverse-chronologically by `dateSort`. + +### 2. `website/src/links.html` - Page template + +Frontmatter: +```yaml +layout: layouts/main.html +title: "SimpleX Chat Links" +description: "Reviews, articles, videos, podcasts, and community content about SimpleX Chat" +permalink: /links/ +templateEngineOverride: njk +active_links: true +``` + +Structure: +- Page heading (i18n) +- Filter bar: language dropdown, category chips, media type chips +- Items list: reuses blog card layout pattern (same Tailwind classes from blog.html - `shadow-[0px_20px_30px_rgba(0,0,0,0.12)]`, `dark:bg-[#0B2A59]`, etc.) +- Each item is an `
` with data attributes: `data-lang`, `data-category`, `data-media`, `data-date`, `data-featured`, and `id` attribute (semantic slug) +- Image with `loading="lazy"`, fallback to SimpleX logo if no image +- Title as link to external URL (opens in new tab) +- Anchor icon (link/chain icon) appears on hover, links to `#link=item-id` for sharing +- Publisher, category, language badge, date shown as metadata +- Preview paragraph +- Featured items get a subtle highlight (border or background tint) +- Pagination controls at bottom + +### 3. `website/src/js/links.js` - Client-side pagination/filtering + +On page load: +1. Collect all `
` elements +2. Read hash: `#page=N` or `#link=slug` +3. Apply any active filters +4. Paginate: show N items per page, hide rest with `display:none` +5. If `#link=slug`: find the item, calculate its page, show that page, scroll to item, briefly highlight it +6. If `#page=N`: show page N + +Filter logic: +- Filtering by language/category/media: iterate all articles, set `display:none` on non-matching, re-paginate the visible set +- Filters update the hash + +Pagination: +- Items per page: ~20 +- Page controls: prev/next + page numbers +- Clicking a page link sets `#page=N` in hash +- Hash change listener re-renders + +Share anchors: +- Each item has a hover-visible link icon +- Clicking it copies `#link=item-id` to clipboard / updates URL hash +- When page loads with `#link=item-id`, JS finds the item, determines which page it falls on (accounting for active filters), shows that page, scrolls to item + +## Files to Modify + +### 4. `website/.eleventy.js` + +At the top, after glossary parsing: +```js +const parseLinks = require('./parse_links') +``` + +Inside `module.exports`: +- Add `links` as global data: parsed from LINKS.md +- Add passthrough copy: `ty.addPassthroughCopy("src/link-images")` +- The docs collection already globs `src/docs/**/*.md` - LINKS.md must be excluded. Options: + - `web.sh` deletes `src/docs/LINKS.md` after parse_links reads it + - Or add frontmatter to LINKS.md with `eleventyExcludeFromCollections: true` and `permalink: false` + - Simplest: delete in web.sh after copy + +### 5. `website/web.sh` + +After `cp -R docs website/src`: +```bash +cp -R docs/links/images website/src/link-images +``` + +After `node customize_docs_frontmatter.js`: +```bash +rm website/src/docs/LINKS.md # prevent Eleventy from processing as doc page +``` + +### 6. `website/src/_includes/navbar.html` + +Add "Links" nav item after Blog (line ~115): +```html + +``` + +Language dropdown stays enabled on the links page - content spans 30 languages, so visitors from any language should be able to navigate and use the filters naturally. + +### 7. `website/langs/en.json` (and other lang files) + +Add i18n keys for page chrome: +- `"links"` - nav label +- `"links-title"` - page heading (e.g. "Community Links" or "Links to Community Publications") +- `"links-filter-language"` - "Language" dropdown label +- `"links-filter-all"` - "All" filter chip +- `"links-filter-category"` - "Category" label +- `"links-filter-media"` - "Media" label +- `"links-featured"` - "Featured" badge text + +Translate these keys across all language files (same approach as the 14-key translation done earlier in this branch). + +### 8. `website/src/index.html` - Homepage hero (follow-up) + +Change the 5 publication logo links (`publications-btns` section, lines 135-151) from external URLs to `/links#link=semantic-slug`. The JS on the links page handles showing the correct page and scrolling to the item. + +## Not in scope + +- RSS feed for links +- Per-language page copies in web.sh (one page, i18n handles chrome translation via language dropdown) + +## Build flow + +``` +web.sh: + cp -R docs website/src # copies LINKS.md into src/docs/ + cp -R docs/links/images website/src/link-images + cd website + npm install + node merge_translations.js + node customize_docs_frontmatter.js + rm src/docs/LINKS.md # prevent doc collection processing + npm run build # .eleventy.js imports parse_links.js, + # reads src/docs/LINKS.md -> data, + # links.html renders from that data +``` + +Wait - there's a sequencing issue. If web.sh deletes LINKS.md before Eleventy runs, parse_links.js can't read it. Two options: +1. parse_links.js reads from `../docs/LINKS.md` (the original, not the copy) +2. web.sh deletes LINKS.md AFTER parse_links reads it but BEFORE Eleventy processes docs + +Option 1 is simpler - parse_links.js always reads from repo root `docs/LINKS.md`, not from `src/docs/`. Then web.sh just never copies it (or deletes it after `cp -R docs website/src`). + +Revised flow: +``` +web.sh: + cp -R docs website/src + rm website/src/docs/LINKS.md # immediately remove from src/docs/ + cp -R docs/links/images website/src/link-images + cd website + ...existing steps... + npm run build # parse_links.js reads ../docs/LINKS.md +``` diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index 37daa78d3c..34ee893dd3 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -110,6 +110,12 @@
+ +