ios: contacts UI improvements
@@ -1,5 +1,6 @@
|
||||
---
|
||||
layout: layouts/privacy.html
|
||||
permalink: /privacy/index.html
|
||||
---
|
||||
|
||||
# SimpleX Chat Privacy Policy and Conditions of Use
|
||||
@@ -10,7 +11,7 @@ SimpleX Chat communication protocol is the first protocol that has no user profi
|
||||
|
||||
Double ratchet algorithm has such important properties as [forward secrecy](/docs/GLOSSARY.md#forward-secrecy), sender [repudiation](/docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](/docs/GLOSSARY.md#post-compromise-security)).
|
||||
|
||||
If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
@@ -78,7 +79,7 @@ When you choose to use instant push notifications in SimpleX iOS app, because th
|
||||
|
||||
Preset notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who sends messages to you. Apple push notifications servers can only observe how many notifications are sent to you, but not from how many contacts, or from which messaging relays, as notifications are delivered to your device end-to-end encrypted by one of the preset notification servers - these notifications only contain end-to-end encrypted metadata, not even encrypted message content, and they look completely random to Apple push notification servers.
|
||||
|
||||
You can read more about the design of iOS push notifications [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html#our-ios-approach-has-one-trade-off).
|
||||
You can read more about the design of iOS push notifications [here](./blog/20220404-simplex-chat-instant-notifications.md#our-ios-approach-has-one-trade-off).
|
||||
|
||||
#### Another information stored on the servers
|
||||
|
||||
@@ -115,7 +116,7 @@ We will update this Privacy Policy as needed so that it is current, accurate, an
|
||||
|
||||
Please also read our Conditions of Use of Software and Infrastructure below.
|
||||
|
||||
If you have questions about our Privacy Policy please contact us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
If you have questions about our Privacy Policy please contact us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
|
||||
## Conditions of Use of Software and Infrastructure
|
||||
|
||||
|
||||
@@ -341,8 +341,8 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) async -> ChatItem? {
|
||||
let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId)
|
||||
func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) async -> ChatItem? {
|
||||
let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId, ttl: ttl)
|
||||
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
|
||||
}
|
||||
|
||||
@@ -1903,21 +1903,35 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
case .chatSuspended:
|
||||
chatSuspended()
|
||||
case let .contactSwitch(_, contact, switchProgress):
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
|
||||
case let .contactSwitch(user, contact, switchProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .groupMemberSwitch(_, groupInfo, member, switchProgress):
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
|
||||
case let .groupMemberSwitch(user, groupInfo, member, switchProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .contactRatchetSync(_, contact, ratchetSyncProgress):
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
|
||||
case let .contactRatchetSync(user, contact, ratchetSyncProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress):
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
||||
case let .groupMemberRatchetSync(user, groupInfo, member, ratchetSyncProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .contactDisabled(user, contact):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContact(contact)
|
||||
}
|
||||
}
|
||||
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible):
|
||||
await MainActor.run {
|
||||
|
||||
@@ -21,6 +21,8 @@ struct CIGroupInvitationView: View {
|
||||
@State private var inProgress = false
|
||||
@State private var progressByTimeout = false
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
var body: some View {
|
||||
let action = !chatItem.chatDir.sent && groupInvitation.status == .pending
|
||||
let v = ZStack(alignment: .bottomTrailing) {
|
||||
@@ -43,7 +45,7 @@ struct CIGroupInvitationView: View {
|
||||
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
@@ -51,7 +53,7 @@ struct CIGroupInvitationView: View {
|
||||
(
|
||||
groupInvitationText()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ struct CIMetaView: View {
|
||||
var showStatus = true
|
||||
var showEdited = true
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
var body: some View {
|
||||
if chatItem.isDeletedContent {
|
||||
chatItem.timestampText.font(.caption).foregroundColor(metaColor)
|
||||
@@ -27,24 +29,24 @@ struct CIMetaView: View {
|
||||
switch meta.itemStatus {
|
||||
case let .sndSent(sndProgress):
|
||||
switch sndProgress {
|
||||
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited)
|
||||
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited)
|
||||
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
case let .sndRcvd(_, sndProgress):
|
||||
switch sndProgress {
|
||||
case .complete:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
case .partial:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
}
|
||||
default:
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +66,8 @@ func ciMetaText(
|
||||
transparent: Bool = false,
|
||||
sent: SentCheckmark? = nil,
|
||||
showStatus: Bool = true,
|
||||
showEdited: Bool = true
|
||||
showEdited: Bool = true,
|
||||
showViaProxy: Bool
|
||||
) -> Text {
|
||||
var r = Text("")
|
||||
if showEdited, meta.itemEdited {
|
||||
@@ -78,6 +81,9 @@ func ciMetaText(
|
||||
}
|
||||
r = r + Text(" ")
|
||||
}
|
||||
if showViaProxy, meta.sentViaProxy == true {
|
||||
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
|
||||
}
|
||||
if showStatus {
|
||||
if let (icon, statusColor) = meta.statusIcon(color) {
|
||||
let t = Text(Image(systemName: icon)).font(.caption2)
|
||||
|
||||
@@ -19,6 +19,8 @@ struct CIRcvDecryptionError: View {
|
||||
var chatItem: ChatItem
|
||||
@State private var alert: CIRcvDecryptionErrorAlert?
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
enum CIRcvDecryptionErrorAlert: Identifiable {
|
||||
case syncAllowedAlert(_ syncConnection: () -> Void)
|
||||
case syncNotSupportedContactAlert
|
||||
@@ -119,7 +121,7 @@ struct CIRcvDecryptionError: View {
|
||||
.foregroundColor(syncSupported ? .accentColor : .secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
@@ -140,7 +142,7 @@ struct CIRcvDecryptionError: View {
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
|
||||
@@ -87,12 +87,17 @@ struct FramedItemView: View {
|
||||
.cornerRadius(18)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
switch chatItem.meta.itemStatus {
|
||||
case .sndErrorAuth:
|
||||
v.onTapGesture { msgDeliveryError("Most likely this contact has deleted the connection with you.") }
|
||||
case let .sndError(agentError):
|
||||
v.onTapGesture { msgDeliveryError("Unexpected error: \(agentError)") }
|
||||
default: v
|
||||
if let (title, text) = chatItem.meta.itemStatus.statusInfo {
|
||||
v.onTapGesture {
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text(title),
|
||||
message: Text(text)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,13 +162,6 @@ struct FramedItemView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func msgDeliveryError(_ err: LocalizedStringKey) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Message delivery error",
|
||||
message: err
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = false) -> some View {
|
||||
let v = HStack(spacing: 6) {
|
||||
|
||||
@@ -35,6 +35,8 @@ struct MsgContentView: View {
|
||||
@State private var typingIdx = 0
|
||||
@State private var timer: Timer?
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
var body: some View {
|
||||
if meta?.isLive == true {
|
||||
msgContentView()
|
||||
@@ -81,7 +83,7 @@ struct MsgContentView: View {
|
||||
}
|
||||
|
||||
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true)
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@ struct ChatItemInfoView: View {
|
||||
let mss = membersStatuses(memberDeliveryStatuses)
|
||||
if !mss.isEmpty {
|
||||
ForEach(mss, id: \.0.groupMemberId) { memberStatus in
|
||||
memberDeliveryStatusView(memberStatus.0, memberStatus.1)
|
||||
memberDeliveryStatusView(memberStatus.0, memberStatus.1, memberStatus.2)
|
||||
}
|
||||
} else {
|
||||
Text("No delivery information")
|
||||
@@ -392,23 +392,27 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] {
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus, Bool?)] {
|
||||
memberDeliveryStatuses.compactMap({ mds in
|
||||
if let mem = chatModel.getGroupMember(mds.groupMemberId) {
|
||||
return (mem.wrapped, mds.memberDeliveryStatus)
|
||||
return (mem.wrapped, mds.memberDeliveryStatus, mds.sentViaProxy)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View {
|
||||
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus, _ sentViaProxy: Bool?) -> some View {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image, size: 30)
|
||||
.padding(.trailing, 2)
|
||||
Text(member.chatViewName)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
if sentViaProxy == true {
|
||||
Image(systemName: "arrow.forward")
|
||||
.foregroundColor(.secondary).opacity(0.67)
|
||||
}
|
||||
let v = Group {
|
||||
if let (icon, statusColor) = status.statusIcon(Color.secondary) {
|
||||
switch status {
|
||||
|
||||
@@ -42,6 +42,7 @@ struct ChatView: View {
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
viewBody
|
||||
.toolbarBackground(.visible, for: .navigationBar)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.keyboardPadding()
|
||||
} else {
|
||||
|
||||
@@ -712,9 +712,9 @@ struct ComposeView: View {
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
await sendMemberContactInvitation()
|
||||
} else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem {
|
||||
sent = await forwardItem(ci, fromChatInfo)
|
||||
sent = await forwardItem(ci, fromChatInfo, ttl)
|
||||
if !composeState.message.isEmpty {
|
||||
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: nil)
|
||||
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
|
||||
}
|
||||
} else if case let .editingItem(ci) = composeState.contextItem {
|
||||
sent = await updateMessage(ci, live: live)
|
||||
@@ -890,13 +890,14 @@ struct ComposeView: View {
|
||||
return nil
|
||||
}
|
||||
|
||||
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo) async -> ChatItem? {
|
||||
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? {
|
||||
if let chatItem = await apiForwardChatItem(
|
||||
toChatType: chat.chatInfo.chatType,
|
||||
toChatId: chat.chatInfo.apiId,
|
||||
fromChatType: fromChatInfo.chatType,
|
||||
fromChatId: fromChatInfo.apiId,
|
||||
itemId: forwardedItem.id
|
||||
itemId: forwardedItem.id,
|
||||
ttl: ttl
|
||||
) {
|
||||
await MainActor.run {
|
||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||
|
||||
@@ -224,8 +224,7 @@ struct SendMessageView: View {
|
||||
|
||||
@ViewBuilder private func sendButtonContextMenuItems() -> some View {
|
||||
if composeState.liveMessage == nil,
|
||||
!composeState.editing,
|
||||
!composeState.forwarding {
|
||||
!composeState.editing {
|
||||
if case .noContextItem = composeState.contextItem,
|
||||
!composeState.voicePreview,
|
||||
let send = sendLiveMessage,
|
||||
|
||||
@@ -17,7 +17,7 @@ struct ContactsView: View {
|
||||
@State private var searchText = ""
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = true
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
|
||||
@@ -19,7 +19,7 @@ struct ChatListView: View {
|
||||
@State private var searchChatFilteredBySimplexLink: String? = nil
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = true
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
|
||||
@@ -240,14 +240,14 @@ struct ChatPreviewView: View {
|
||||
|
||||
private func itemStatusMark(_ cItem: ChatItem) -> Text {
|
||||
switch cItem.meta.itemStatus {
|
||||
case .sndErrorAuth:
|
||||
case .sndErrorAuth, .sndError:
|
||||
return Text(Image(systemName: "multiply"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.red) + Text(" ")
|
||||
case .sndError:
|
||||
case .sndWarning:
|
||||
return Text(Image(systemName: "exclamationmark.triangle.fill"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.yellow) + Text(" ")
|
||||
.foregroundColor(.orange) + Text(" ")
|
||||
default: return Text("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ struct HomeView: View {
|
||||
@State private var userPickerVisible = false
|
||||
@State private var showConnectDesktop = false
|
||||
@State private var newChatMenuOption: NewChatMenuOption? = nil
|
||||
@State private var toolbarHeight: CGFloat = 0
|
||||
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = true
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
@@ -31,32 +32,9 @@ struct HomeView: View {
|
||||
get: { chatModel.chatId != nil },
|
||||
set: { _ in }
|
||||
),
|
||||
destination: chatView
|
||||
) {
|
||||
VStack {
|
||||
switch homeTab {
|
||||
case .contacts:
|
||||
contactsView()
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Contacts")
|
||||
case .chats:
|
||||
chatListView()
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Chats")
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
settingsButton()
|
||||
Spacer()
|
||||
contactsButton()
|
||||
Spacer()
|
||||
chatsButton()
|
||||
Spacer()
|
||||
newChatButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
destination: chatView,
|
||||
content: homeView
|
||||
)
|
||||
|
||||
if userPickerVisible {
|
||||
Rectangle().fill(.white.opacity(0.001)).onTapGesture {
|
||||
@@ -76,6 +54,40 @@ struct HomeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func homeView() -> some View {
|
||||
let v = VStack {
|
||||
switch homeTab {
|
||||
case .contacts: withToolbar("Contacts", contactsView)
|
||||
case .chats: withToolbar("Chats", chatListView)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
settingsButton()
|
||||
Spacer()
|
||||
contactsButton()
|
||||
Spacer()
|
||||
chatsButton()
|
||||
Spacer()
|
||||
newChatButton()
|
||||
}
|
||||
}
|
||||
|
||||
if #unavailable(iOS 16) {
|
||||
v
|
||||
} else if oneHandUI {
|
||||
v.toolbarBackground(.visible, for: .navigationBar, .bottomBar)
|
||||
} else {
|
||||
v.toolbarBackground(.visible, for: .bottomBar)
|
||||
}
|
||||
}
|
||||
|
||||
func withToolbar<V: View>(_ title: LocalizedStringKey, _ content: () -> V) -> some View {
|
||||
content()
|
||||
.navigationBarTitleDisplayMode(oneHandUI ? .inline : .large)
|
||||
.navigationTitle(title)
|
||||
}
|
||||
|
||||
@ViewBuilder private func settingsButton() -> some View {
|
||||
let user = chatModel.currentUser ?? User.sampleData
|
||||
let multiUser = chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1
|
||||
@@ -90,8 +102,8 @@ struct HomeView: View {
|
||||
} label: {
|
||||
VStack(spacing: 2) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
ProfileImage(imageStr: user.image, size: 38, color: Color(uiColor: .quaternaryLabel))
|
||||
.padding(.top, 2)
|
||||
ProfileImage(imageStr: user.image, size: 42, color: Color(uiColor: .quaternaryLabel))
|
||||
.padding(.top, 3)
|
||||
.padding(.trailing, 3)
|
||||
let allRead = chatModel.users
|
||||
.filter { u in !u.user.activeUser && !u.user.hidden }
|
||||
@@ -115,7 +127,7 @@ struct HomeView: View {
|
||||
Button {
|
||||
homeTab = .contacts
|
||||
} label: {
|
||||
iconLabel(homeTab == .contacts ? "book.fill" : "book", "Contacts")
|
||||
iconLabel(homeTab == .contacts ? "person.crop.circle.fill" : "person.crop.circle", "Contacts")
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@@ -196,13 +208,11 @@ struct HomeView: View {
|
||||
|
||||
@ViewBuilder private func contactsView() -> some View {
|
||||
ContactsView()
|
||||
.padding(.vertical, oneHandUI ? 1 : 0)
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatListView() -> some View {
|
||||
// TODO reverse scale effect for swipe actions
|
||||
ChatListView()
|
||||
.padding(.vertical, oneHandUI ? 1 : 0)
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatView() -> some View {
|
||||
|
||||
@@ -25,7 +25,7 @@ struct AppearanceSettings: View {
|
||||
@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
@State private var uiTintColor = getUIAccentColorDefault()
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = true
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||
|
||||
var body: some View {
|
||||
VStack{
|
||||
@@ -40,11 +40,11 @@ struct AppearanceSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section("Interface") {
|
||||
settingsRow("hand.wave") {
|
||||
Toggle("One-hand UI", isOn: $oneHandUI)
|
||||
}
|
||||
}
|
||||
// Section("Interface") {
|
||||
// settingsRow("hand.wave") {
|
||||
// Toggle("One-hand UI", isOn: $oneHandUI)
|
||||
// }
|
||||
// }
|
||||
|
||||
Section("App icon") {
|
||||
HStack {
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct DeveloperView: View {
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
@@ -31,9 +32,6 @@ struct DeveloperView: View {
|
||||
} label: {
|
||||
settingsRow("terminal") { Text("Chat console") }
|
||||
}
|
||||
settingsRow("internaldrive") {
|
||||
Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades)
|
||||
}
|
||||
settingsRow("chevron.left.forwardslash.chevron.right") {
|
||||
Toggle("Show developer options", isOn: $developerTools)
|
||||
}
|
||||
@@ -42,6 +40,19 @@ struct DeveloperView: View {
|
||||
} footer: {
|
||||
(developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
|
||||
}
|
||||
|
||||
if developerTools {
|
||||
Section {
|
||||
settingsRow("internaldrive") {
|
||||
Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades)
|
||||
}
|
||||
settingsRow("hand.wave") {
|
||||
Toggle("One-hand UI", isOn: $oneHandUI)
|
||||
}
|
||||
} header: {
|
||||
Text("Developer options")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,16 @@ import SimpleXChat
|
||||
private enum NetworkAlert: Identifiable {
|
||||
case updateOnionHosts(hosts: OnionHosts)
|
||||
case updateSessionMode(mode: TransportSessionMode)
|
||||
case updateSMPProxyMode(proxyMode: SMPProxyMode)
|
||||
case updateSMPProxyFallback(proxyFallback: SMPProxyFallback)
|
||||
case error(err: String)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .updateOnionHosts(hosts): return "updateOnionHosts \(hosts)"
|
||||
case let .updateSessionMode(mode): return "updateSessionMode \(mode)"
|
||||
case let .updateSMPProxyMode(proxyMode): return "updateSMPProxyMode \(proxyMode)"
|
||||
case let .updateSMPProxyFallback(proxyFallback): return "updateSMPProxyFallback \(proxyFallback)"
|
||||
case let .error(err): return "error \(err)"
|
||||
}
|
||||
}
|
||||
@@ -26,11 +30,14 @@ private enum NetworkAlert: Identifiable {
|
||||
struct NetworkAndServers: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = true
|
||||
@State private var cfgLoaded = false
|
||||
@State private var currentNetCfg = NetCfg.defaults
|
||||
@State private var netCfg = NetCfg.defaults
|
||||
@State private var onionHosts: OnionHosts = .no
|
||||
@State private var sessionMode: TransportSessionMode = .user
|
||||
@State private var proxyMode: SMPProxyMode = .never
|
||||
@State private var proxyFallback: SMPProxyFallback = .allow
|
||||
@State private var alert: NetworkAlert?
|
||||
|
||||
var body: some View {
|
||||
@@ -75,6 +82,30 @@ struct NetworkAndServers: View {
|
||||
Text("Using .onion hosts requires compatible VPN provider.")
|
||||
}
|
||||
|
||||
// Section {
|
||||
// Picker("Private routing", selection: $proxyMode) {
|
||||
// ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) }
|
||||
// }
|
||||
// .frame(height: 36)
|
||||
//
|
||||
// Picker("Allow downgrade", selection: $proxyFallback) {
|
||||
// ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) }
|
||||
// }
|
||||
// .disabled(proxyMode == .never)
|
||||
// .frame(height: 36)
|
||||
//
|
||||
// Toggle("Show message status", isOn: $showSentViaProxy)
|
||||
// } header: {
|
||||
// Text("Private message routing")
|
||||
// } footer: {
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("To protect your IP address, private routing uses your SMP servers to deliver messages.")
|
||||
// if showSentViaProxy {
|
||||
// Text("Show → on messages sent via private routing.")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
Section("Calls") {
|
||||
NavigationLink {
|
||||
RTCServers()
|
||||
@@ -99,14 +130,24 @@ struct NetworkAndServers: View {
|
||||
currentNetCfg = getNetCfg()
|
||||
resetNetCfgView()
|
||||
}
|
||||
.onChange(of: onionHosts) { _ in
|
||||
if onionHosts != OnionHosts(netCfg: currentNetCfg) {
|
||||
alert = .updateOnionHosts(hosts: onionHosts)
|
||||
.onChange(of: onionHosts) { hosts in
|
||||
if hosts != OnionHosts(netCfg: currentNetCfg) {
|
||||
alert = .updateOnionHosts(hosts: hosts)
|
||||
}
|
||||
}
|
||||
.onChange(of: sessionMode) { _ in
|
||||
if sessionMode != netCfg.sessionMode {
|
||||
alert = .updateSessionMode(mode: sessionMode)
|
||||
.onChange(of: sessionMode) { mode in
|
||||
if mode != netCfg.sessionMode {
|
||||
alert = .updateSessionMode(mode: mode)
|
||||
}
|
||||
}
|
||||
.onChange(of: proxyMode) { mode in
|
||||
if mode != netCfg.smpProxyMode {
|
||||
alert = .updateSMPProxyMode(proxyMode: mode)
|
||||
}
|
||||
}
|
||||
.onChange(of: proxyFallback) { fallbackMode in
|
||||
if fallbackMode != netCfg.smpProxyFallback {
|
||||
alert = .updateSMPProxyFallback(proxyFallback: fallbackMode)
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { a in
|
||||
@@ -137,6 +178,30 @@ struct NetworkAndServers: View {
|
||||
resetNetCfgView()
|
||||
}
|
||||
)
|
||||
case let .updateSMPProxyMode(proxyMode):
|
||||
return Alert(
|
||||
title: Text("Message routing mode"),
|
||||
message: Text(proxyModeInfo(proxyMode)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
netCfg.smpProxyMode = proxyMode
|
||||
saveNetCfg()
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
resetNetCfgView()
|
||||
}
|
||||
)
|
||||
case let .updateSMPProxyFallback(proxyFallback):
|
||||
return Alert(
|
||||
title: Text("Message routing fallback"),
|
||||
message: Text(proxyFallbackInfo(proxyFallback)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
netCfg.smpProxyFallback = proxyFallback
|
||||
saveNetCfg()
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
resetNetCfgView()
|
||||
}
|
||||
)
|
||||
case let .error(err):
|
||||
return Alert(
|
||||
title: Text("Error updating settings"),
|
||||
@@ -166,6 +231,8 @@ struct NetworkAndServers: View {
|
||||
netCfg = currentNetCfg
|
||||
onionHosts = OnionHosts(netCfg: netCfg)
|
||||
sessionMode = netCfg.sessionMode
|
||||
proxyMode = netCfg.smpProxyMode
|
||||
proxyFallback = netCfg.smpProxyFallback
|
||||
}
|
||||
|
||||
private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey {
|
||||
@@ -182,6 +249,23 @@ struct NetworkAndServers: View {
|
||||
case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail."
|
||||
}
|
||||
}
|
||||
|
||||
private func proxyModeInfo(_ mode: SMPProxyMode) -> LocalizedStringKey {
|
||||
switch mode {
|
||||
case .always: return "Always use private routing."
|
||||
case .unknown: return "Use private routing with unknown servers."
|
||||
case .unprotected: return "Use private routing with unknown servers when IP address is not protected."
|
||||
case .never: return "Do NOT use private routing."
|
||||
}
|
||||
}
|
||||
|
||||
private func proxyFallbackInfo(_ proxyFallback: SMPProxyFallback) -> LocalizedStringKey {
|
||||
switch proxyFallback {
|
||||
case .allow: return "Send messages directly when your or destination server does not support private routing."
|
||||
case .allowProtected: return "Send messages directly when IP address is protected and your or destination server does not support private routing."
|
||||
case .prohibit: return "Do NOT send messages directly, even if your or destination server does not support private routing."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkServersView_Previews: PreviewProvider {
|
||||
|
||||
@@ -63,6 +63,7 @@ let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
||||
let DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice"
|
||||
let DEFAULT_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice"
|
||||
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||
|
||||
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||
|
||||
@@ -92,7 +93,7 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
|
||||
DEFAULT_USER_INTERFACE_STYLE: 0,
|
||||
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
|
||||
DEFAULT_ONE_HAND_UI: true,
|
||||
DEFAULT_ONE_HAND_UI: false,
|
||||
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
||||
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true,
|
||||
@@ -105,7 +106,8 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
||||
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue,
|
||||
DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE: true,
|
||||
DEFAULT_SHOW_DELETE_CONTACT_NOTICE: true
|
||||
DEFAULT_SHOW_DELETE_CONTACT_NOTICE: true,
|
||||
DEFAULT_SHOW_SENT_VIA_RPOXY: false
|
||||
]
|
||||
|
||||
// not used anymore
|
||||
|
||||
@@ -76,6 +76,11 @@
|
||||
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
|
||||
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
|
||||
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; };
|
||||
5C9F3DCC2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */; };
|
||||
5C9F3DCD2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */; };
|
||||
5C9F3DCE2BF7A6900003B86B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC92BF7A6900003B86B /* libgmp.a */; };
|
||||
5C9F3DCF2BF7A6900003B86B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */; };
|
||||
5C9F3DD02BF7A6900003B86B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DCB2BF7A6900003B86B /* libffi.a */; };
|
||||
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
|
||||
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
|
||||
5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; };
|
||||
@@ -115,11 +120,6 @@
|
||||
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
|
||||
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
|
||||
5CE0E8AB2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */; };
|
||||
5CE0E8AC2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */; };
|
||||
5CE0E8AD2BF0C1B5008D6E06 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */; };
|
||||
5CE0E8AE2BF0C1B5008D6E06 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A92BF0C1B5008D6E06 /* libffi.a */; };
|
||||
5CE0E8AF2BF0C1B5008D6E06 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */; };
|
||||
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; };
|
||||
@@ -357,6 +357,11 @@
|
||||
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
|
||||
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
|
||||
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; };
|
||||
5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a"; sourceTree = "<group>"; };
|
||||
5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5C9F3DC92BF7A6900003B86B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C9F3DCB2BF7A6900003B86B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
||||
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
|
||||
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; };
|
||||
@@ -423,11 +428,6 @@
|
||||
5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = "<group>"; };
|
||||
5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
|
||||
5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
|
||||
5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a"; sourceTree = "<group>"; };
|
||||
5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CE0E8A92BF0C1B5008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -535,13 +535,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C9F3DCF2BF7A6900003B86B /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CE0E8AE2BF0C1B5008D6E06 /* libffi.a in Frameworks */,
|
||||
5CE0E8AD2BF0C1B5008D6E06 /* libgmp.a in Frameworks */,
|
||||
5C9F3DCE2BF7A6900003B86B /* libgmp.a in Frameworks */,
|
||||
5C9F3DCC2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a in Frameworks */,
|
||||
5C9F3DD02BF7A6900003B86B /* libffi.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5CE0E8AC2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a in Frameworks */,
|
||||
5CE0E8AB2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a in Frameworks */,
|
||||
5CE0E8AF2BF0C1B5008D6E06 /* libgmpxx.a in Frameworks */,
|
||||
5C9F3DCD2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -609,11 +609,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CE0E8A92BF0C1B5008D6E06 /* libffi.a */,
|
||||
5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */,
|
||||
5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */,
|
||||
5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */,
|
||||
5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */,
|
||||
5C9F3DCB2BF7A6900003B86B /* libffi.a */,
|
||||
5C9F3DC92BF7A6900003B86B /* libgmp.a */,
|
||||
5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */,
|
||||
5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */,
|
||||
5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -1580,7 +1580,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1605,7 +1605,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1629,7 +1629,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1654,7 +1654,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1715,7 +1715,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -1730,7 +1730,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -1752,7 +1752,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -1789,7 +1789,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1815,7 +1815,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1840,7 +1840,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1866,7 +1866,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -48,7 +48,7 @@ public enum ChatCommand {
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
|
||||
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
|
||||
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
|
||||
case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64)
|
||||
case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?)
|
||||
case apiGetNtfToken
|
||||
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
|
||||
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
|
||||
@@ -192,7 +192,9 @@ public enum ChatCommand {
|
||||
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
|
||||
case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)"
|
||||
case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
|
||||
case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId): return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId)"
|
||||
case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl):
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId) ttl=\(ttlStr)"
|
||||
case .apiGetNtfToken: return "/_ntf get "
|
||||
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
|
||||
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
|
||||
@@ -251,7 +253,7 @@ public enum ChatCommand {
|
||||
case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)"
|
||||
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)"
|
||||
case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)"
|
||||
case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(encodeJSON(chatDeleteMode))"
|
||||
case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)"
|
||||
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
|
||||
case let .apiListContacts(userId): return "/_contacts \(userId)"
|
||||
case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))"
|
||||
@@ -473,10 +475,6 @@ public enum ChatCommand {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func onOff(_ b: Bool) -> String {
|
||||
b ? "on" : "off"
|
||||
}
|
||||
|
||||
private func onOffParam(_ param: String, _ b: Bool?) -> String {
|
||||
if let b = b {
|
||||
return " \(param)=\(onOff(b))"
|
||||
@@ -489,6 +487,10 @@ public enum ChatCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private func onOff(_ b: Bool) -> String {
|
||||
b ? "on" : "off"
|
||||
}
|
||||
|
||||
public struct APIResponse: Decodable {
|
||||
var resp: ChatResponse
|
||||
}
|
||||
@@ -635,6 +637,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo)
|
||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||
case contactDisabled(user: UserRef, contact: Contact)
|
||||
// remote desktop responses/events
|
||||
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
|
||||
case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo, ctrlAppInfo_: CtrlAppInfo?, appVersion: String, compatible: Bool)
|
||||
@@ -792,6 +795,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .ntfMessages: return "ntfMessages"
|
||||
case .ntfMessage: return "ntfMessage"
|
||||
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
||||
case .contactDisabled: return "contactDisabled"
|
||||
case .remoteCtrlList: return "remoteCtrlList"
|
||||
case .remoteCtrlFound: return "remoteCtrlFound"
|
||||
case .remoteCtrlConnecting: return "remoteCtrlConnecting"
|
||||
@@ -949,6 +953,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
|
||||
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
|
||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .contactDisabled(u, contact): return withUser(u, String(describing: contact))
|
||||
case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
|
||||
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)"
|
||||
case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)"
|
||||
@@ -988,6 +993,14 @@ public enum ChatDeleteMode: Codable {
|
||||
case full(notify: Bool)
|
||||
case entity(notify: Bool)
|
||||
case messages
|
||||
|
||||
var cmdString: String {
|
||||
switch self {
|
||||
case let .full(notify): "full notify=\(onOff(notify))"
|
||||
case let .entity(notify): "entity notify=\(onOff(notify))"
|
||||
case .messages: "messages"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConnectionPlan: Decodable {
|
||||
@@ -1244,9 +1257,12 @@ public struct ServerAddress: Decodable {
|
||||
|
||||
public struct NetCfg: Codable, Equatable {
|
||||
public var socksProxy: String? = nil
|
||||
var socksMode: SocksMode = .always
|
||||
public var hostMode: HostMode = .publicHost
|
||||
public var requiredHostMode = true
|
||||
public var sessionMode: TransportSessionMode
|
||||
public var smpProxyMode: SMPProxyMode = .never
|
||||
public var smpProxyFallback: SMPProxyFallback = .allow
|
||||
public var tcpConnectTimeout: Int // microseconds
|
||||
public var tcpTimeout: Int // microseconds
|
||||
public var tcpTimeoutPerKb: Int // microseconds
|
||||
@@ -1291,6 +1307,49 @@ public enum HostMode: String, Codable {
|
||||
case publicHost = "public"
|
||||
}
|
||||
|
||||
public enum SocksMode: String, Codable {
|
||||
case always = "always"
|
||||
case onion = "onion"
|
||||
}
|
||||
|
||||
public enum SMPProxyMode: String, Codable {
|
||||
case always = "always"
|
||||
case unknown = "unknown"
|
||||
case unprotected = "unprotected"
|
||||
case never = "never"
|
||||
|
||||
public var text: LocalizedStringKey {
|
||||
switch self {
|
||||
case .always: return "always"
|
||||
case .unknown: return "unknown relays"
|
||||
case .unprotected: return "unprotected"
|
||||
case .never: return "never"
|
||||
}
|
||||
}
|
||||
|
||||
public var id: SMPProxyMode { self }
|
||||
|
||||
public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never]
|
||||
}
|
||||
|
||||
public enum SMPProxyFallback: String, Codable {
|
||||
case allow = "allow"
|
||||
case allowProtected = "allowProtected"
|
||||
case prohibit = "prohibit"
|
||||
|
||||
public var text: LocalizedStringKey {
|
||||
switch self {
|
||||
case .allow: return "yes"
|
||||
case .allowProtected: return "when IP hidden"
|
||||
case .prohibit: return "no"
|
||||
}
|
||||
}
|
||||
|
||||
public var id: SMPProxyFallback { self }
|
||||
|
||||
public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit]
|
||||
}
|
||||
|
||||
public enum OnionHosts: String, Identifiable {
|
||||
case no
|
||||
case prefer
|
||||
@@ -2055,7 +2114,7 @@ public struct AppSettings: Codable, Equatable {
|
||||
androidCallOnLockScreen: AppSettingsLockScreenCalls.show,
|
||||
iosCallKitEnabled: true,
|
||||
iosCallKitCallsInRecents: false,
|
||||
oneHandUI: true
|
||||
oneHandUI: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles
|
||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
||||
let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode"
|
||||
let GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK = "networkSMPProxyFallback"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb"
|
||||
@@ -53,6 +55,8 @@ public func registerGroupDefaults() {
|
||||
GROUP_DEFAULT_NTF_ENABLE_PERIODIC: false,
|
||||
GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue,
|
||||
GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.user.rawValue,
|
||||
GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE: SMPProxyMode.never.rawValue,
|
||||
GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK: SMPProxyFallback.allow.rawValue,
|
||||
GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout,
|
||||
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout,
|
||||
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb,
|
||||
@@ -191,6 +195,18 @@ public let networkSessionModeGroupDefault = EnumDefault<TransportSessionMode>(
|
||||
withDefault: .user
|
||||
)
|
||||
|
||||
public let networkSMPProxyModeGroupDefault = EnumDefault<SMPProxyMode>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE,
|
||||
withDefault: .never
|
||||
)
|
||||
|
||||
public let networkSMPProxyFallbackGroupDefault = EnumDefault<SMPProxyFallback>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK,
|
||||
withDefault: .allow
|
||||
)
|
||||
|
||||
public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE)
|
||||
|
||||
public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE)
|
||||
@@ -275,6 +291,8 @@ public func getNetCfg() -> NetCfg {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (hostMode, requiredHostMode) = onionHosts.hostMode
|
||||
let sessionMode = networkSessionModeGroupDefault.get()
|
||||
let smpProxyMode = networkSMPProxyModeGroupDefault.get()
|
||||
let smpProxyFallback = networkSMPProxyFallbackGroupDefault.get()
|
||||
let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
|
||||
let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
|
||||
let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
|
||||
@@ -295,6 +313,8 @@ public func getNetCfg() -> NetCfg {
|
||||
hostMode: hostMode,
|
||||
requiredHostMode: requiredHostMode,
|
||||
sessionMode: sessionMode,
|
||||
smpProxyMode: smpProxyMode,
|
||||
smpProxyFallback: smpProxyFallback,
|
||||
tcpConnectTimeout: tcpConnectTimeout,
|
||||
tcpTimeout: tcpTimeout,
|
||||
tcpTimeoutPerKb: tcpTimeoutPerKb,
|
||||
@@ -309,6 +329,8 @@ public func getNetCfg() -> NetCfg {
|
||||
public func setNetCfg(_ cfg: NetCfg) {
|
||||
networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg))
|
||||
networkSessionModeGroupDefault.set(cfg.sessionMode)
|
||||
networkSMPProxyModeGroupDefault.set(cfg.smpProxyMode)
|
||||
networkSMPProxyFallbackGroupDefault.set(cfg.smpProxyFallback)
|
||||
groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
|
||||
|
||||
@@ -1520,7 +1520,12 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
public var ready: Bool { get { activeConn?.connStatus == .ready } }
|
||||
public var active: Bool { get { contactStatus == .active } }
|
||||
public var sendMsgEnabled: Bool { get {
|
||||
(ready && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false))
|
||||
(
|
||||
ready
|
||||
&& active
|
||||
&& !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false)
|
||||
&& !(activeConn?.connDisabled ?? true)
|
||||
)
|
||||
|| nextSendGrpInv
|
||||
} }
|
||||
public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } }
|
||||
@@ -1613,15 +1618,20 @@ public struct Connection: Decodable {
|
||||
public var pqEncryption: Bool
|
||||
public var pqSndEnabled: Bool?
|
||||
public var pqRcvEnabled: Bool?
|
||||
public var authErrCounter: Int
|
||||
|
||||
public var connectionStats: ConnectionStats? = nil
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled
|
||||
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter
|
||||
}
|
||||
|
||||
public var id: ChatId { get { ":\(connId)" } }
|
||||
|
||||
public var connDisabled: Bool {
|
||||
authErrCounter >= 10 // authErrDisableCount in core
|
||||
}
|
||||
|
||||
public var connPQEnabled: Bool {
|
||||
pqSndEnabled == true && pqRcvEnabled == true
|
||||
}
|
||||
@@ -1634,7 +1644,8 @@ public struct Connection: Decodable {
|
||||
connLevel: 0,
|
||||
viaGroupLink: false,
|
||||
pqSupport: false,
|
||||
pqEncryption: false
|
||||
pqEncryption: false,
|
||||
authErrCounter: 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2633,6 +2644,7 @@ public struct CIMeta: Decodable {
|
||||
public var itemTs: Date
|
||||
var itemText: String
|
||||
public var itemStatus: CIStatus
|
||||
public var sentViaProxy: Bool?
|
||||
public var createdAt: Date
|
||||
public var updatedAt: Date
|
||||
public var itemForwarded: CIForwardedFrom?
|
||||
@@ -2722,7 +2734,8 @@ public enum CIStatus: Decodable {
|
||||
case sndSent(sndProgress: SndCIStatusProgress)
|
||||
case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress)
|
||||
case sndErrorAuth
|
||||
case sndError(agentError: String)
|
||||
case sndError(agentError: SndError)
|
||||
case sndWarning(agentError: SndError)
|
||||
case rcvNew
|
||||
case rcvRead
|
||||
case invalid(text: String)
|
||||
@@ -2734,6 +2747,7 @@ public enum CIStatus: Decodable {
|
||||
case .sndRcvd: return "sndRcvd"
|
||||
case .sndErrorAuth: return "sndErrorAuth"
|
||||
case .sndError: return "sndError"
|
||||
case .sndWarning: return "sndWarning"
|
||||
case .rcvNew: return "rcvNew"
|
||||
case .rcvRead: return "rcvRead"
|
||||
case .invalid: return "invalid"
|
||||
@@ -2750,7 +2764,8 @@ public enum CIStatus: Decodable {
|
||||
case .badMsgHash: return ("checkmark", .red)
|
||||
}
|
||||
case .sndErrorAuth: return ("multiply", .red)
|
||||
case .sndError: return ("exclamationmark.triangle.fill", .yellow)
|
||||
case .sndError: return ("multiply", .red)
|
||||
case .sndWarning: return ("exclamationmark.triangle.fill", .orange)
|
||||
case .rcvNew: return ("circlebadge.fill", Color.accentColor)
|
||||
case .rcvRead: return nil
|
||||
case .invalid: return ("questionmark", metaColor)
|
||||
@@ -2768,7 +2783,11 @@ public enum CIStatus: Decodable {
|
||||
)
|
||||
case let .sndError(agentError): return (
|
||||
NSLocalizedString("Message delivery error", comment: "item status text"),
|
||||
String.localizedStringWithFormat(NSLocalizedString("Unexpected error: %@", comment: "item status description"), agentError)
|
||||
agentError.errorInfo
|
||||
)
|
||||
case let .sndWarning(agentError): return (
|
||||
NSLocalizedString("Message delivery warning", comment: "item status text"),
|
||||
agentError.errorInfo
|
||||
)
|
||||
case .rcvNew: return nil
|
||||
case .rcvRead: return nil
|
||||
@@ -2780,6 +2799,42 @@ public enum CIStatus: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SndError: Decodable {
|
||||
case auth
|
||||
case quota
|
||||
case expired
|
||||
case relay(srvError: SrvError)
|
||||
case proxy(proxyServer: String, srvError: SrvError)
|
||||
case proxyRelay(proxyServer: String, srvError: SrvError)
|
||||
case other(sndError: String)
|
||||
|
||||
public var errorInfo: String {
|
||||
switch self {
|
||||
case .auth: NSLocalizedString("Wrong key or unknown connection - most likely this connection is deleted.", comment: "snd error text")
|
||||
case .quota: NSLocalizedString("Capacity exceeded - recipient did not receive previously sent messages.", comment: "snd error text")
|
||||
case .expired: NSLocalizedString("Network issues - message expired after many attempts to send it.", comment: "snd error text")
|
||||
case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("Destination server error: %@", comment: "snd error text"), srvError.errorInfo)
|
||||
case let .proxy(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nError: %@", comment: "snd error text"), proxyServer, srvError.errorInfo)
|
||||
case let .proxyRelay(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nDestination server error: %@", comment: "snd error text"), proxyServer, srvError.errorInfo)
|
||||
case let .other(sndError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "snd error text"), sndError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum SrvError: Decodable {
|
||||
case host
|
||||
case version
|
||||
case other(srvError: String)
|
||||
|
||||
public var errorInfo: String {
|
||||
switch self {
|
||||
case .host: NSLocalizedString("Server address is incompatible with network settings.", comment: "srv error text.")
|
||||
case .version: NSLocalizedString("Server version is incompatible with network settings.", comment: "srv error text")
|
||||
case let .other(srvError): srvError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MsgReceiptStatus: String, Decodable {
|
||||
case ok
|
||||
case badMsgHash
|
||||
@@ -3898,4 +3953,5 @@ public struct ChatItemVersion: Decodable {
|
||||
public struct MemberDeliveryStatus: Decodable {
|
||||
public var groupMemberId: Int64
|
||||
public var memberDeliveryStatus: CIStatus
|
||||
public var sentViaProxy: Bool?
|
||||
}
|
||||
|
||||
@@ -1048,9 +1048,13 @@ data class Contact(
|
||||
override val apiId get() = contactId
|
||||
override val ready get() = activeConn?.connStatus == ConnStatus.Ready
|
||||
val active get() = contactStatus == ContactStatus.Active
|
||||
override val sendMsgEnabled get() =
|
||||
(ready && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false))
|
||||
|| nextSendGrpInv
|
||||
override val sendMsgEnabled get() = (
|
||||
ready
|
||||
&& active
|
||||
&& !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false)
|
||||
&& !(activeConn?.connDisabled ?: true)
|
||||
)
|
||||
|| nextSendGrpInv
|
||||
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
|
||||
override val incognito get() = contactConnIncognito
|
||||
@@ -1150,15 +1154,19 @@ data class Connection(
|
||||
val pqEncryption: Boolean,
|
||||
val pqSndEnabled: Boolean? = null,
|
||||
val pqRcvEnabled: Boolean? = null,
|
||||
val connectionStats: ConnectionStats? = null
|
||||
val connectionStats: ConnectionStats? = null,
|
||||
val authErrCounter: Int
|
||||
) {
|
||||
val id: ChatId get() = ":$connId"
|
||||
|
||||
val connDisabled: Boolean
|
||||
get() = authErrCounter >= 10 // authErrDisableCount in core
|
||||
|
||||
val connPQEnabled: Boolean
|
||||
get() = pqSndEnabled == true && pqRcvEnabled == true
|
||||
|
||||
companion object {
|
||||
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false)
|
||||
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false, authErrCounter = 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1900,6 +1908,7 @@ data class ChatItem (
|
||||
ts: Instant = Clock.System.now(),
|
||||
text: String = "hello\nthere",
|
||||
status: CIStatus = CIStatus.SndNew(),
|
||||
sentViaProxy: Boolean? = null,
|
||||
quotedItem: CIQuote? = null,
|
||||
file: CIFile? = null,
|
||||
itemForwarded: CIForwardedFrom? = null,
|
||||
@@ -1911,7 +1920,7 @@ data class ChatItem (
|
||||
) =
|
||||
ChatItem(
|
||||
chatDir = dir,
|
||||
meta = CIMeta.getSample(id, ts, text, status, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable),
|
||||
meta = CIMeta.getSample(id, ts, text, status, sentViaProxy, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable),
|
||||
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
|
||||
quotedItem = quotedItem,
|
||||
reactions = listOf(),
|
||||
@@ -1993,6 +2002,7 @@ data class ChatItem (
|
||||
itemTs = Clock.System.now(),
|
||||
itemText = generalGetString(MR.strings.deleted_description),
|
||||
itemStatus = CIStatus.RcvRead(),
|
||||
sentViaProxy = null,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
itemForwarded = null,
|
||||
@@ -2016,6 +2026,7 @@ data class ChatItem (
|
||||
itemTs = Clock.System.now(),
|
||||
itemText = "",
|
||||
itemStatus = CIStatus.RcvRead(),
|
||||
sentViaProxy = null,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
itemForwarded = null,
|
||||
@@ -2118,6 +2129,7 @@ data class CIMeta (
|
||||
val itemTs: Instant,
|
||||
val itemText: String,
|
||||
val itemStatus: CIStatus,
|
||||
val sentViaProxy: Boolean?,
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant,
|
||||
val itemForwarded: CIForwardedFrom?,
|
||||
@@ -2144,7 +2156,7 @@ data class CIMeta (
|
||||
|
||||
companion object {
|
||||
fun getSample(
|
||||
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(),
|
||||
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), sentViaProxy: Boolean? = null,
|
||||
itemForwarded: CIForwardedFrom? = null, itemDeleted: CIDeleted? = null, itemEdited: Boolean = false,
|
||||
itemTimed: CITimed? = null, itemLive: Boolean = false, deletable: Boolean = true, editable: Boolean = true
|
||||
): CIMeta =
|
||||
@@ -2153,6 +2165,7 @@ data class CIMeta (
|
||||
itemTs = ts,
|
||||
itemText = text,
|
||||
itemStatus = status,
|
||||
sentViaProxy = sentViaProxy,
|
||||
createdAt = ts,
|
||||
updatedAt = ts,
|
||||
itemForwarded = itemForwarded,
|
||||
@@ -2171,6 +2184,7 @@ data class CIMeta (
|
||||
itemTs = Clock.System.now(),
|
||||
itemText = "invalid JSON",
|
||||
itemStatus = CIStatus.SndNew(),
|
||||
sentViaProxy = null,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
itemForwarded = null,
|
||||
@@ -2227,7 +2241,8 @@ sealed class CIStatus {
|
||||
@Serializable @SerialName("sndSent") class SndSent(val sndProgress: SndCIStatusProgress): CIStatus()
|
||||
@Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus, val sndProgress: SndCIStatusProgress): CIStatus()
|
||||
@Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus()
|
||||
@Serializable @SerialName("sndError") class SndError(val agentError: String): CIStatus()
|
||||
@Serializable @SerialName("sndError") class CISSndError(val agentError: SndError): CIStatus()
|
||||
@Serializable @SerialName("sndWarning") class SndWarning(val agentError: SndError): CIStatus()
|
||||
@Serializable @SerialName("rcvNew") class RcvNew: CIStatus()
|
||||
@Serializable @SerialName("rcvRead") class RcvRead: CIStatus()
|
||||
@Serializable @SerialName("invalid") class Invalid(val text: String): CIStatus()
|
||||
@@ -2251,7 +2266,8 @@ sealed class CIStatus {
|
||||
MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red
|
||||
}
|
||||
is SndErrorAuth -> MR.images.ic_close to Color.Red
|
||||
is SndError -> MR.images.ic_warning_filled to WarningYellow
|
||||
is CISSndError -> MR.images.ic_close to Color.Red
|
||||
is SndWarning -> MR.images.ic_warning_filled to WarningOrange
|
||||
is RcvNew -> MR.images.ic_circle_filled to primaryColor
|
||||
is RcvRead -> null
|
||||
is CIStatus.Invalid -> MR.images.ic_question_mark to metaColor
|
||||
@@ -2262,13 +2278,48 @@ sealed class CIStatus {
|
||||
is SndSent -> null
|
||||
is SndRcvd -> null
|
||||
is SndErrorAuth -> generalGetString(MR.strings.message_delivery_error_title) to generalGetString(MR.strings.message_delivery_error_desc)
|
||||
is SndError -> generalGetString(MR.strings.message_delivery_error_title) to (generalGetString(MR.strings.unknown_error) + ": $agentError")
|
||||
is CISSndError -> generalGetString(MR.strings.message_delivery_error_title) to agentError.errorInfo
|
||||
is SndWarning -> generalGetString(MR.strings.message_delivery_warning_title) to agentError.errorInfo
|
||||
is RcvNew -> null
|
||||
is RcvRead -> null
|
||||
is Invalid -> "Invalid status" to this.text
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class SndError {
|
||||
@Serializable @SerialName("auth") class Auth: SndError()
|
||||
@Serializable @SerialName("quota") class Quota: SndError()
|
||||
@Serializable @SerialName("expired") class Expired: SndError()
|
||||
@Serializable @SerialName("relay") class Relay(val srvError: SrvError): SndError()
|
||||
@Serializable @SerialName("proxy") class Proxy(val proxyServer: String, val srvError: SrvError): SndError()
|
||||
@Serializable @SerialName("proxyRelay") class ProxyRelay(val proxyServer: String, val srvError: SrvError): SndError()
|
||||
@Serializable @SerialName("other") class Other(val sndError: String): SndError()
|
||||
|
||||
val errorInfo: String get() = when (this) {
|
||||
is SndError.Auth -> generalGetString(MR.strings.snd_error_auth)
|
||||
is SndError.Quota -> generalGetString(MR.strings.snd_error_quota)
|
||||
is SndError.Expired -> generalGetString(MR.strings.snd_error_expired)
|
||||
is SndError.Relay -> generalGetString(MR.strings.snd_error_relay).format(srvError.errorInfo)
|
||||
is SndError.Proxy -> generalGetString(MR.strings.snd_error_proxy).format(proxyServer, srvError.errorInfo)
|
||||
is SndError.ProxyRelay -> generalGetString(MR.strings.snd_error_proxy_relay).format(proxyServer, srvError.errorInfo)
|
||||
is SndError.Other -> generalGetString(MR.strings.ci_status_other_error).format(sndError)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class SrvError {
|
||||
@Serializable @SerialName("host") class Host: SrvError()
|
||||
@Serializable @SerialName("version") class Version: SrvError()
|
||||
@Serializable @SerialName("other") class Other(val srvError: String): SrvError()
|
||||
|
||||
val errorInfo: String get() = when (this) {
|
||||
is SrvError.Host -> generalGetString(MR.strings.srv_error_host)
|
||||
is SrvError.Version -> generalGetString(MR.strings.srv_error_version)
|
||||
is SrvError.Other -> srvError
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class MsgReceiptStatus {
|
||||
@SerialName("ok") Ok,
|
||||
@@ -2653,6 +2704,12 @@ data class CIFile(
|
||||
return res
|
||||
}
|
||||
|
||||
fun forwardingAllowed(): Boolean = when {
|
||||
chatModel.connectedToRemote() && cachedRemoteFileRequests[fileSource] != false && loaded -> true
|
||||
getLoadedFilePath(this) != null -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getSample(
|
||||
fileId: Long = 1,
|
||||
@@ -3348,7 +3405,8 @@ data class ChatItemVersion(
|
||||
@Serializable
|
||||
data class MemberDeliveryStatus(
|
||||
val groupMemberId: Long,
|
||||
val memberDeliveryStatus: CIStatus
|
||||
val memberDeliveryStatus: CIStatus,
|
||||
val sentViaProxy: Boolean?
|
||||
)
|
||||
|
||||
enum class NotificationPreviewMode {
|
||||
|
||||
@@ -130,6 +130,8 @@ class AppPreferences {
|
||||
},
|
||||
set = fun(mode: TransportSessionMode) { _networkSessionMode.set(mode.name) }
|
||||
)
|
||||
val networkSMPProxyMode = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_MODE, SMPProxyMode.Never.name)
|
||||
val networkSMPProxyFallback = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK, SMPProxyFallback.Allow.name)
|
||||
val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name)
|
||||
val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false)
|
||||
val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout)
|
||||
@@ -185,6 +187,8 @@ class AppPreferences {
|
||||
|
||||
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
||||
|
||||
val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false)
|
||||
|
||||
|
||||
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
||||
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
||||
@@ -306,6 +310,8 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
|
||||
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
|
||||
private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode"
|
||||
private const val SHARED_PREFS_NETWORK_SMP_PROXY_MODE = "NetworkSMPProxyMode"
|
||||
private const val SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK = "NetworkSMPProxyFallback"
|
||||
private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode"
|
||||
private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode"
|
||||
private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout"
|
||||
@@ -348,6 +354,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
||||
private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||
|
||||
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
||||
private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents"
|
||||
@@ -775,8 +782,8 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long): ChatItem? {
|
||||
val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId)
|
||||
suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long, ttl: Int?): ChatItem? {
|
||||
val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl)
|
||||
return processSendMessageCmd(rh, cmd)?.chatItem
|
||||
}
|
||||
|
||||
@@ -2031,13 +2038,21 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
is CR.ContactSwitch ->
|
||||
chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats)
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats)
|
||||
}
|
||||
is CR.GroupMemberSwitch ->
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats)
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats)
|
||||
}
|
||||
is CR.ContactRatchetSync ->
|
||||
chatModel.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats)
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
is CR.GroupMemberRatchetSync ->
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats)
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
is CR.RemoteHostSessionCode -> {
|
||||
chatModel.remoteHostPairing.value = r.remoteHost_ to RemoteHostSessionState.PendingConfirmation(r.sessionCode)
|
||||
}
|
||||
@@ -2046,6 +2061,11 @@ object ChatController {
|
||||
chatModel.currentRemoteHost.value = r.remoteHost
|
||||
switchUIRemoteHost(r.remoteHost.remoteHostId)
|
||||
}
|
||||
is CR.ContactDisabled -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
is CR.RemoteHostStopped -> {
|
||||
val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ }
|
||||
chatModel.remoteHostPairing.value = null
|
||||
@@ -2302,6 +2322,8 @@ object ChatController {
|
||||
val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!)
|
||||
val requiredHostMode = appPrefs.networkRequiredHostMode.get()
|
||||
val sessionMode = appPrefs.networkSessionMode.get()
|
||||
val smpProxyMode = SMPProxyMode.valueOf(appPrefs.networkSMPProxyMode.get()!!)
|
||||
val smpProxyFallback = SMPProxyFallback.valueOf(appPrefs.networkSMPProxyFallback.get()!!)
|
||||
val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get()
|
||||
val tcpTimeout = appPrefs.networkTCPTimeout.get()
|
||||
val tcpTimeoutPerKb = appPrefs.networkTCPTimeoutPerKb.get()
|
||||
@@ -2322,6 +2344,8 @@ object ChatController {
|
||||
hostMode = hostMode,
|
||||
requiredHostMode = requiredHostMode,
|
||||
sessionMode = sessionMode,
|
||||
smpProxyMode = smpProxyMode,
|
||||
smpProxyFallback = smpProxyFallback,
|
||||
tcpConnectTimeout = tcpConnectTimeout,
|
||||
tcpTimeout = tcpTimeout,
|
||||
tcpTimeoutPerKb = tcpTimeoutPerKb,
|
||||
@@ -2340,6 +2364,8 @@ object ChatController {
|
||||
appPrefs.networkHostMode.set(cfg.hostMode.name)
|
||||
appPrefs.networkRequiredHostMode.set(cfg.requiredHostMode)
|
||||
appPrefs.networkSessionMode.set(cfg.sessionMode)
|
||||
appPrefs.networkSMPProxyMode.set(cfg.smpProxyMode.name)
|
||||
appPrefs.networkSMPProxyFallback.set(cfg.smpProxyFallback.name)
|
||||
appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout)
|
||||
appPrefs.networkTCPTimeout.set(cfg.tcpTimeout)
|
||||
appPrefs.networkTCPTimeoutPerKb.set(cfg.tcpTimeoutPerKb)
|
||||
@@ -2407,7 +2433,7 @@ sealed class CC {
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
|
||||
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
|
||||
class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long): CC()
|
||||
class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long, val ttl: Int?): CC()
|
||||
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
|
||||
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
|
||||
class ApiJoinGroup(val groupId: Long): CC()
|
||||
@@ -2549,7 +2575,10 @@ sealed class CC {
|
||||
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
|
||||
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId"
|
||||
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
|
||||
is ApiForwardChatItem -> "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId"
|
||||
is ApiForwardChatItem -> {
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId ttl=${ttlStr}"
|
||||
}
|
||||
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
|
||||
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
|
||||
is ApiJoinGroup -> "/_join #$groupId"
|
||||
@@ -3025,9 +3054,12 @@ data class ParsedServerAddress (
|
||||
@Serializable
|
||||
data class NetCfg(
|
||||
val socksProxy: String?,
|
||||
val socksMode: SocksMode = SocksMode.Always,
|
||||
val hostMode: HostMode,
|
||||
val requiredHostMode: Boolean,
|
||||
val sessionMode: TransportSessionMode,
|
||||
val smpProxyMode: SMPProxyMode,
|
||||
val smpProxyFallback: SMPProxyFallback,
|
||||
val tcpConnectTimeout: Long, // microseconds
|
||||
val tcpTimeout: Long, // microseconds
|
||||
val tcpTimeoutPerKb: Long, // microseconds
|
||||
@@ -3056,6 +3088,8 @@ data class NetCfg(
|
||||
hostMode = HostMode.OnionViaSocks,
|
||||
requiredHostMode = false,
|
||||
sessionMode = TransportSessionMode.User,
|
||||
smpProxyMode = SMPProxyMode.Never,
|
||||
smpProxyFallback = SMPProxyFallback.Allow,
|
||||
tcpConnectTimeout = 25_000_000,
|
||||
tcpTimeout = 15_000_000,
|
||||
tcpTimeoutPerKb = 10_000,
|
||||
@@ -3071,6 +3105,8 @@ data class NetCfg(
|
||||
hostMode = HostMode.OnionViaSocks,
|
||||
requiredHostMode = false,
|
||||
sessionMode = TransportSessionMode.User,
|
||||
smpProxyMode = SMPProxyMode.Never,
|
||||
smpProxyFallback = SMPProxyFallback.Allow,
|
||||
tcpConnectTimeout = 35_000_000,
|
||||
tcpTimeout = 20_000_000,
|
||||
tcpTimeoutPerKb = 15_000,
|
||||
@@ -3109,6 +3145,35 @@ enum class HostMode {
|
||||
@SerialName("public") Public;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class SocksMode {
|
||||
@SerialName("always") Always,
|
||||
@SerialName("onion") Onion;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class SMPProxyMode {
|
||||
@SerialName("always") Always,
|
||||
@SerialName("unknown") Unknown,
|
||||
@SerialName("unprotected") Unprotected,
|
||||
@SerialName("never") Never;
|
||||
|
||||
companion object {
|
||||
val default = Never
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class SMPProxyFallback {
|
||||
@SerialName("allow") Allow,
|
||||
@SerialName("allowProtected") AllowProtected,
|
||||
@SerialName("prohibit") Prohibit;
|
||||
|
||||
companion object {
|
||||
val default = Allow
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class TransportSessionMode {
|
||||
@SerialName("user") User,
|
||||
@@ -4197,6 +4262,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
|
||||
@Serializable @SerialName("callEnded") class CallEnded(val user: UserRef, val contact: Contact): CR()
|
||||
@Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("contactDisabled") class ContactDisabled(val user: UserRef, val contact: Contact): CR()
|
||||
// remote events (desktop)
|
||||
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
|
||||
@Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR()
|
||||
@@ -4361,6 +4427,7 @@ sealed class CR {
|
||||
is CallExtraInfo -> "callExtraInfo"
|
||||
is CallEnded -> "callEnded"
|
||||
is ContactConnectionDeleted -> "contactConnectionDeleted"
|
||||
is ContactDisabled -> "contactDisabled"
|
||||
is RemoteHostList -> "remoteHostList"
|
||||
is CurrentRemoteHost -> "currentRemoteHost"
|
||||
is RemoteHostStarted -> "remoteHostStarted"
|
||||
@@ -4521,6 +4588,7 @@ sealed class CR {
|
||||
is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}")
|
||||
is CallEnded -> withUser(user, "contact: ${contact.id}")
|
||||
is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection))
|
||||
is ContactDisabled -> withUser(user, json.encodeToString(contact))
|
||||
// remote events (mobile)
|
||||
is RemoteHostList -> json.encodeToString(remoteHosts)
|
||||
is CurrentRemoteHost -> if (remoteHost_ == null) "local" else json.encodeToString(remoteHost_)
|
||||
|
||||
@@ -304,7 +304,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus) {
|
||||
fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus, sentViaProxy: Boolean?) {
|
||||
SectionItemView(
|
||||
padding = PaddingValues(horizontal = 0.dp)
|
||||
) {
|
||||
@@ -317,6 +317,18 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
if (sentViaProxy == true) {
|
||||
Box(
|
||||
Modifier.size(36.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_arrow_forward),
|
||||
contentDescription = null,
|
||||
tint = CurrentColors.value.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary)
|
||||
var modifier = Modifier.size(36.dp).clip(RoundedCornerShape(20.dp))
|
||||
val info = status.statusInto
|
||||
@@ -357,8 +369,8 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
if (mss.isNotEmpty()) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
mss.forEach { (member, status) ->
|
||||
MemberDeliveryStatusView(member, status)
|
||||
mss.forEach { (member, status, sentViaProxy) ->
|
||||
MemberDeliveryStatusView(member, status, sentViaProxy)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -482,10 +494,10 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
}
|
||||
}
|
||||
|
||||
private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Pair<GroupMember, CIStatus>> {
|
||||
private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Triple<GroupMember, CIStatus, Boolean?>> {
|
||||
return memberDeliveryStatuses.mapNotNull { mds ->
|
||||
chatModel.getGroupMember(mds.groupMemberId)?.let { mem ->
|
||||
mem to mds.memberDeliveryStatus
|
||||
Triple(mem, mds.memberDeliveryStatus, mds.sentViaProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,6 +479,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
},
|
||||
onComposed,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactConnection -> {
|
||||
@@ -548,6 +549,7 @@ fun ChatLayout(
|
||||
onSearchValueChanged: (String) -> Unit,
|
||||
onComposed: suspend (chatId: String) -> Unit,
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } }
|
||||
@@ -606,7 +608,7 @@ fun ChatLayout(
|
||||
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -885,6 +887,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
||||
onComposed: suspend (chatId: String) -> Unit,
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -975,7 +978,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
tryOrShowError("${cItem.id}ChatItem", error = {
|
||||
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
||||
}) {
|
||||
ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools)
|
||||
ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1543,6 +1546,7 @@ fun PreviewChatLayout() {
|
||||
onSearchValueChanged = {},
|
||||
onComposed = {},
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1615,6 +1619,7 @@ fun PreviewGroupChatLayout() {
|
||||
onSearchValueChanged = {},
|
||||
onComposed = {},
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,14 +408,15 @@ fun ComposeView(
|
||||
composeState.value = composeState.value.copy(inProgress = true)
|
||||
}
|
||||
|
||||
suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo): ChatItem? {
|
||||
suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo, ttl: Int?): ChatItem? {
|
||||
val chatItem = controller.apiForwardChatItem(
|
||||
rh = rhId,
|
||||
toChatType = chat.chatInfo.chatType,
|
||||
toChatId = chat.chatInfo.apiId,
|
||||
fromChatType = fromChatInfo.chatType,
|
||||
fromChatId = fromChatInfo.apiId,
|
||||
itemId = forwardedItem.id
|
||||
itemId = forwardedItem.id,
|
||||
ttl = ttl
|
||||
)
|
||||
if (chatItem != null) {
|
||||
chatModel.addChatItem(rhId, chat.chatInfo, chatItem)
|
||||
@@ -490,9 +491,9 @@ fun ComposeView(
|
||||
sendMemberContactInvitation()
|
||||
sent = null
|
||||
} else if (cs.contextItem is ComposeContextItem.ForwardingItem) {
|
||||
sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItem, cs.contextItem.fromChatInfo)
|
||||
sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItem, cs.contextItem.fromChatInfo, ttl = ttl)
|
||||
if (cs.message.isNotEmpty()) {
|
||||
sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = null)
|
||||
sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = ttl)
|
||||
}
|
||||
} else if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||
val ei = cs.contextItem.chatItem
|
||||
|
||||
@@ -157,7 +157,7 @@ fun SendMsgView(
|
||||
fun MenuItems(): List<@Composable () -> Unit> {
|
||||
val menuItems = mutableListOf<@Composable () -> Unit>()
|
||||
|
||||
if (cs.liveMessage == null && !cs.editing && !cs.forwarding && !nextSendGrpInv || sendMsgEnabled) {
|
||||
if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) {
|
||||
if (
|
||||
cs.preview !is ComposePreview.VoicePreview &&
|
||||
cs.contextItem is ComposeContextItem.NoContextItem &&
|
||||
|
||||
@@ -47,7 +47,7 @@ fun CICallItemView(
|
||||
CICallStatus.Error -> {}
|
||||
}
|
||||
|
||||
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false)
|
||||
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -28,6 +29,7 @@ import java.net.URI
|
||||
fun CIFileView(
|
||||
file: CIFile?,
|
||||
edited: Boolean,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val saveFileLauncher = rememberSaveFileLauncher(ciFile = file)
|
||||
@@ -86,7 +88,7 @@ fun CIFileView(
|
||||
)
|
||||
FileProtocol.LOCAL -> {}
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> {
|
||||
file.forwardingAllowed() -> {
|
||||
withLongRunningApi(slow = 600_000) {
|
||||
var filePath = getLoadedFilePath(file)
|
||||
if (chatModel.connectedToRemote() && filePath == null) {
|
||||
@@ -136,8 +138,7 @@ fun CIFileView(
|
||||
Box(
|
||||
Modifier
|
||||
.size(42.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.clickable(onClick = { fileAction() }),
|
||||
.clip(RoundedCornerShape(4.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (file != null) {
|
||||
@@ -154,7 +155,13 @@ fun CIFileView(
|
||||
FileProtocol.SMP -> progressIndicator()
|
||||
FileProtocol.LOCAL -> {}
|
||||
}
|
||||
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
|
||||
is CIFileStatus.SndComplete -> {
|
||||
if ((file.forwardingAllowed() || (chatModel.connectedToRemote() && CIFile.cachedRemoteFileRequests[file.fileSource] == true))) {
|
||||
fileIcon()
|
||||
} else {
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
|
||||
}
|
||||
}
|
||||
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
@@ -181,7 +188,12 @@ fun CIFileView(
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.clickable(onClick = { fileAction() }).padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
|
||||
Modifier
|
||||
.combinedClickable(
|
||||
onClick = { fileAction() },
|
||||
onLongClick = { showMenu.value = true }
|
||||
)
|
||||
.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
|
||||
//Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||
|
||||
@@ -144,7 +144,7 @@ fun CIGroupInvitationView(
|
||||
}
|
||||
}
|
||||
|
||||
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false)
|
||||
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ fun CIMetaView(
|
||||
blue = minOf(metaColor.red * 1.33F, 1F))
|
||||
},
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean
|
||||
) {
|
||||
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (chatItem.isDeletedContent) {
|
||||
@@ -53,7 +54,8 @@ fun CIMetaView(
|
||||
metaColor,
|
||||
paleMetaColor,
|
||||
showStatus = showStatus,
|
||||
showEdited = showEdited
|
||||
showEdited = showEdited,
|
||||
showViaProxy = showViaProxy
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -68,7 +70,8 @@ private fun CIMetaText(
|
||||
color: Color,
|
||||
paleColor: Color,
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean
|
||||
) {
|
||||
if (showEdited && meta.itemEdited) {
|
||||
StatusIconText(painterResource(MR.images.ic_edit), color)
|
||||
@@ -82,6 +85,9 @@ private fun CIMetaText(
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
if (showViaProxy && meta.sentViaProxy == true) {
|
||||
Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = CurrentColors.value.colors.secondary)
|
||||
}
|
||||
if (showStatus) {
|
||||
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor)
|
||||
if (statusIcon != null) {
|
||||
@@ -105,7 +111,14 @@ private fun CIMetaText(
|
||||
}
|
||||
|
||||
// the conditions in this function should match CIMetaText
|
||||
fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, showStatus: Boolean = true, showEdited: Boolean = true): String {
|
||||
fun reserveSpaceForMeta(
|
||||
meta: CIMeta,
|
||||
chatTTL: Int?,
|
||||
encrypted: Boolean?,
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean = false
|
||||
): String {
|
||||
val iconSpace = " "
|
||||
var res = ""
|
||||
if (showEdited && meta.itemEdited) res += iconSpace
|
||||
@@ -116,6 +129,9 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, showSt
|
||||
res += shortTimeText(ttl)
|
||||
}
|
||||
}
|
||||
if (showViaProxy && meta.sentViaProxy == true) {
|
||||
res += iconSpace
|
||||
}
|
||||
if (showStatus && (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing)) {
|
||||
res += iconSpace
|
||||
}
|
||||
@@ -137,7 +153,8 @@ fun PreviewCIMetaView() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
||||
),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,7 +166,8 @@ fun PreviewCIMetaViewUnread() {
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
status = CIStatus.RcvNew()
|
||||
),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -159,9 +177,10 @@ fun PreviewCIMetaViewSendFailed() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
status = CIStatus.SndError("CMD SYNTAX")
|
||||
status = CIStatus.CISSndError(SndError.Other("CMD SYNTAX"))
|
||||
),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -172,7 +191,8 @@ fun PreviewCIMetaViewSendNoAuth() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth()
|
||||
),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -183,7 +203,8 @@ fun PreviewCIMetaViewSendSent() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete)
|
||||
),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -195,7 +216,8 @@ fun PreviewCIMetaViewEdited() {
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
itemEdited = true
|
||||
),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -208,7 +230,8 @@ fun PreviewCIMetaViewEditedUnread() {
|
||||
itemEdited = true,
|
||||
status= CIStatus.RcvNew()
|
||||
),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -221,7 +244,8 @@ fun PreviewCIMetaViewEditedSent() {
|
||||
itemEdited = true,
|
||||
status= CIStatus.SndSent(SndCIStatusProgress.Complete)
|
||||
),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -230,6 +254,7 @@ fun PreviewCIMetaViewEditedSent() {
|
||||
fun PreviewCIMetaViewDeletedContent() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ fun DecryptionErrorItemFixButton(
|
||||
)
|
||||
}
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL = null)
|
||||
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,7 @@ fun DecryptionErrorItem(
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL = null)
|
||||
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ fun CIVoiceView(
|
||||
hasText: Boolean,
|
||||
ci: ChatItem,
|
||||
timedMessagesTTL: Int?,
|
||||
showViaProxy: Boolean,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
) {
|
||||
@@ -76,7 +77,7 @@ fun CIVoiceView(
|
||||
durationText(time / 1000)
|
||||
}
|
||||
}
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) {
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, play, pause, longClick, receiveFile) {
|
||||
AudioPlayer.seekTo(it, progress, fileSource.value?.filePath)
|
||||
}
|
||||
} else {
|
||||
@@ -102,6 +103,7 @@ private fun VoiceLayout(
|
||||
sent: Boolean,
|
||||
hasText: Boolean,
|
||||
timedMessagesTTL: Int?,
|
||||
showViaProxy: Boolean,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit,
|
||||
@@ -171,7 +173,7 @@ private fun VoiceLayout(
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
|
||||
}
|
||||
Box(Modifier.padding(top = 6.dp, end = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,7 +188,7 @@ private fun VoiceLayout(
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(top = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ fun ChatItemView(
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val sent = cItem.chatDir.sent
|
||||
@@ -83,17 +84,15 @@ fun ChatItemView(
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = alignment,
|
||||
) {
|
||||
val onClick = {
|
||||
when (cItem.meta.itemStatus) {
|
||||
is CIStatus.SndErrorAuth -> {
|
||||
showMsgDeliveryErrorAlert(generalGetString(MR.strings.message_delivery_error_desc))
|
||||
}
|
||||
is CIStatus.SndError -> {
|
||||
showMsgDeliveryErrorAlert(generalGetString(MR.strings.unknown_error) + ": ${cItem.meta.itemStatus.agentError}")
|
||||
}
|
||||
else -> {}
|
||||
val info = cItem.meta.itemStatus.statusInto
|
||||
val onClick = if (info != null) {
|
||||
{
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = info.first,
|
||||
text = info.second,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else { {} }
|
||||
|
||||
@Composable
|
||||
fun ChatItemReactions() {
|
||||
@@ -130,7 +129,7 @@ fun ChatItemView(
|
||||
) {
|
||||
@Composable
|
||||
fun framedItemView() {
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, receiveFile, onLinkLongClick, scrollToItem)
|
||||
}
|
||||
|
||||
fun deleteMessageQuestionText(): String {
|
||||
@@ -204,14 +203,10 @@ fun ChatItemView(
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests }
|
||||
fun fileForwardingAllowed() = when {
|
||||
cItem.file != null && chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded -> true
|
||||
getLoadedFilePath(cItem.file) != null -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val copyAndShareAllowed = when {
|
||||
cItem.content.text.isNotEmpty() -> true
|
||||
fileForwardingAllowed() -> true
|
||||
cItem.file?.forwardingAllowed() == true -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
@@ -261,7 +256,7 @@ fun ChatItemView(
|
||||
})
|
||||
}
|
||||
if (cItem.meta.itemDeleted == null &&
|
||||
(cItem.file == null || fileForwardingAllowed()) &&
|
||||
(cItem.file == null || cItem.file.forwardingAllowed()) &&
|
||||
!cItem.isLiveDummy && !live
|
||||
) {
|
||||
ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = {
|
||||
@@ -338,14 +333,14 @@ fun ChatItemView(
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else {
|
||||
if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL)
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") }, receiveFile)
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile)
|
||||
} else {
|
||||
framedItemView()
|
||||
}
|
||||
@@ -357,7 +352,7 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
@Composable fun LegacyDeletedItem() {
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL)
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
@@ -410,7 +405,7 @@ fun ChatItemView(
|
||||
|
||||
@Composable
|
||||
fun DeletedItem() {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
|
||||
@@ -820,13 +815,6 @@ fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteM
|
||||
)
|
||||
}
|
||||
|
||||
private fun showMsgDeliveryErrorAlert(description: String) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.message_delivery_error_title),
|
||||
text = description,
|
||||
)
|
||||
}
|
||||
|
||||
expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager)
|
||||
|
||||
@Preview
|
||||
@@ -862,6 +850,7 @@ fun PreviewChatItemView() {
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
developerTools = false,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -897,6 +886,7 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
developerTools = false,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
|
||||
val sent = ci.chatDir.sent
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
@@ -36,7 +36,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,8 @@ fun PreviewDeletedItemView() {
|
||||
SimpleXTheme {
|
||||
DeletedItemView(
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFo
|
||||
val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont)
|
||||
|
||||
@Composable
|
||||
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) {
|
||||
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
|
||||
Column(
|
||||
Modifier.padding(vertical = 8.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EmojiText(chatItem.content.text)
|
||||
CIMetaView(chatItem, timedMessagesTTL)
|
||||
CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.chat.MEMBER_IMAGE_SIZE
|
||||
import chat.simplex.res.MR
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -34,6 +33,7 @@ fun FramedItemView(
|
||||
uriHandler: UriHandler? = null,
|
||||
imageProvider: (() -> ImageGalleryProvider)? = null,
|
||||
linkMode: SimplexLinkMode,
|
||||
showViaProxy: Boolean,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit,
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
@@ -179,9 +179,9 @@ fun FramedItemView(
|
||||
|
||||
@Composable
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
|
||||
CIFileView(ci.file, ci.meta.itemEdited, showMenu, receiveFile)
|
||||
if (text != "" || ci.meta.isLive) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ fun FramedItemView(
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
@@ -253,35 +253,35 @@ fun FramedItemView(
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVoice -> {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile)
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile)
|
||||
if (mc.text != "") {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> ciFileView(ci, mc.text)
|
||||
is MsgContent.MCUnknown ->
|
||||
if (ci.file == null) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
|
||||
} else {
|
||||
ciFileView(ci, mc.text)
|
||||
}
|
||||
is MsgContent.MCLink -> {
|
||||
ChatItemLinkView(mc.preview)
|
||||
Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick)
|
||||
else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
|
||||
CIMetaView(ci, chatTTL, metaColor)
|
||||
CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,14 +293,15 @@ fun CIMarkdownText(
|
||||
chatTTL: Int?,
|
||||
linkMode: SimplexLinkMode,
|
||||
uriHandler: UriHandler?,
|
||||
onLinkLongClick: (link: String) -> Unit = {}
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
showViaProxy: Boolean
|
||||
) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
|
||||
MarkdownText(
|
||||
text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true,
|
||||
meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode,
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>) {
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean) {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
Surface(
|
||||
@@ -35,7 +35,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl
|
||||
Box(Modifier.weight(1f, false)) {
|
||||
MergedMarkedDeletedText(ci, revealed)
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,8 @@ fun PreviewMarkedDeletedItemView() {
|
||||
SimpleXTheme {
|
||||
DeletedItemView(
|
||||
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())),
|
||||
null
|
||||
null,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@ fun MarkdownText (
|
||||
modifier: Modifier = Modifier,
|
||||
linkMode: SimplexLinkMode,
|
||||
inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = null,
|
||||
onLinkLongClick: (link: String) -> Unit = {}
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
showViaProxy: Boolean = false
|
||||
) {
|
||||
val textLayoutDirection = remember (text) {
|
||||
if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
@@ -77,7 +78,7 @@ fun MarkdownText (
|
||||
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
|
||||
"\n"
|
||||
} else if (meta != null) {
|
||||
reserveSpaceForMeta(meta, chatTTL, null) // LALAL
|
||||
reserveSpaceForMeta(meta, chatTTL, null, showViaProxy = showViaProxy)
|
||||
} else {
|
||||
" "
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
hostMode = currentCfg.value.hostMode,
|
||||
requiredHostMode = currentCfg.value.requiredHostMode,
|
||||
sessionMode = currentCfg.value.sessionMode,
|
||||
smpProxyMode = currentCfg.value.smpProxyMode,
|
||||
smpProxyFallback = currentCfg.value.smpProxyFallback,
|
||||
tcpConnectTimeout = networkTCPConnectTimeout.value,
|
||||
tcpTimeout = networkTCPTimeout.value,
|
||||
tcpTimeoutPerKb = networkTCPTimeoutPerKb.value,
|
||||
|
||||
@@ -43,6 +43,8 @@ fun NetworkAndServersView() {
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
|
||||
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
|
||||
val smpProxyMode = remember { mutableStateOf(netCfg.smpProxyMode) }
|
||||
val smpProxyFallback = remember { mutableStateOf(netCfg.smpProxyFallback) }
|
||||
|
||||
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
|
||||
NetworkAndServersLayout(
|
||||
@@ -51,6 +53,8 @@ fun NetworkAndServersView() {
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = onionHosts,
|
||||
sessionMode = sessionMode,
|
||||
smpProxyMode = smpProxyMode,
|
||||
smpProxyFallback = smpProxyFallback,
|
||||
proxyPort = proxyPort,
|
||||
toggleSocksProxy = { enable ->
|
||||
if (enable) {
|
||||
@@ -137,6 +141,59 @@ fun NetworkAndServersView() {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateSMPProxyMode = {
|
||||
if (smpProxyMode.value == it) return@NetworkAndServersLayout
|
||||
val prevValue = smpProxyMode.value
|
||||
smpProxyMode.value = it
|
||||
val startsWith = when (it) {
|
||||
SMPProxyMode.Always -> generalGetString(MR.strings.network_smp_proxy_mode_always_description)
|
||||
SMPProxyMode.Unknown -> generalGetString(MR.strings.network_smp_proxy_mode_unknown_description)
|
||||
SMPProxyMode.Unprotected -> generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description)
|
||||
SMPProxyMode.Never -> generalGetString(MR.strings.network_smp_proxy_mode_never_description)
|
||||
}
|
||||
showUpdateNetworkSettingsDialog(
|
||||
title = generalGetString(MR.strings.update_network_smp_proxy_mode_question),
|
||||
startsWith,
|
||||
onDismiss = { smpProxyMode.value = prevValue }
|
||||
) {
|
||||
withBGApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().copy(smpProxyMode = it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
if (res) {
|
||||
chatModel.controller.setNetCfg(newCfg)
|
||||
smpProxyMode.value = it
|
||||
} else {
|
||||
smpProxyMode.value = prevValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateSMPProxyFallback = {
|
||||
if (smpProxyFallback.value == it) return@NetworkAndServersLayout
|
||||
val prevValue = smpProxyFallback.value
|
||||
smpProxyFallback.value = it
|
||||
val startsWith = when (it) {
|
||||
SMPProxyFallback.Allow -> generalGetString(MR.strings.network_smp_proxy_fallback_allow_description)
|
||||
SMPProxyFallback.AllowProtected -> generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description)
|
||||
SMPProxyFallback.Prohibit -> generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description)
|
||||
}
|
||||
showUpdateNetworkSettingsDialog(
|
||||
title = generalGetString(MR.strings.update_network_smp_proxy_fallback_question),
|
||||
startsWith,
|
||||
onDismiss = { smpProxyFallback.value = prevValue }
|
||||
) {
|
||||
withBGApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().copy(smpProxyFallback = it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
if (res) {
|
||||
chatModel.controller.setNetCfg(newCfg)
|
||||
smpProxyFallback.value = it
|
||||
} else {
|
||||
smpProxyFallback.value = prevValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -147,16 +204,22 @@ fun NetworkAndServersView() {
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
smpProxyMode: MutableState<SMPProxyMode>,
|
||||
smpProxyFallback: MutableState<SMPProxyFallback>,
|
||||
proxyPort: State<Int>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
updateSMPProxyMode: (SMPProxyMode) -> Unit,
|
||||
updateSMPProxyFallback: (SMPProxyFallback) -> Unit,
|
||||
) {
|
||||
val m = chatModel
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
|
||||
|
||||
AppBarTitle(stringResource(MR.strings.network_and_servers))
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
|
||||
@@ -165,7 +228,6 @@ fun NetworkAndServersView() {
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } })
|
||||
|
||||
if (currentRemoteHost == null) {
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
|
||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false)
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||
if (developerTools) {
|
||||
@@ -188,6 +250,18 @@ fun NetworkAndServersView() {
|
||||
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||
}
|
||||
|
||||
// if (currentRemoteHost == null) {
|
||||
// SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) {
|
||||
// SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode)
|
||||
// SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) })
|
||||
// SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
|
||||
// }
|
||||
// SectionCustomFooter {
|
||||
// Text(stringResource(MR.strings.private_routing_explanation))
|
||||
// }
|
||||
// Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||
// }
|
||||
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
|
||||
}
|
||||
@@ -452,6 +526,79 @@ private fun SessionModePicker(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPProxyModePicker(
|
||||
smpProxyMode: MutableState<SMPProxyMode>,
|
||||
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||
updateSMPProxyMode: (SMPProxyMode) -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val values = remember {
|
||||
SMPProxyMode.values().map {
|
||||
when (it) {
|
||||
SMPProxyMode.Always -> ValueTitleDesc(SMPProxyMode.Always, generalGetString(MR.strings.network_smp_proxy_mode_always), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_always_description), density))
|
||||
SMPProxyMode.Unknown -> ValueTitleDesc(SMPProxyMode.Unknown, generalGetString(MR.strings.network_smp_proxy_mode_unknown), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unknown_description), density))
|
||||
SMPProxyMode.Unprotected -> ValueTitleDesc(SMPProxyMode.Unprotected, generalGetString(MR.strings.network_smp_proxy_mode_unprotected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description), density))
|
||||
SMPProxyMode.Never -> ValueTitleDesc(SMPProxyMode.Never, generalGetString(MR.strings.network_smp_proxy_mode_never), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_never_description), density))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_smp_proxy_mode_private_routing),
|
||||
smpProxyMode,
|
||||
values,
|
||||
icon = painterResource(MR.images.ic_settings_ethernet),
|
||||
onSelected = {
|
||||
showModal {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing))
|
||||
SectionViewSelectable(null, smpProxyMode, values, updateSMPProxyMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPProxyFallbackPicker(
|
||||
smpProxyFallback: MutableState<SMPProxyFallback>,
|
||||
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||
updateSMPProxyFallback: (SMPProxyFallback) -> Unit,
|
||||
enabled: State<Boolean>,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val values = remember {
|
||||
SMPProxyFallback.values().map {
|
||||
when (it) {
|
||||
SMPProxyFallback.Allow -> ValueTitleDesc(SMPProxyFallback.Allow, generalGetString(MR.strings.network_smp_proxy_fallback_allow), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_description), density))
|
||||
SMPProxyFallback.AllowProtected -> ValueTitleDesc(SMPProxyFallback.AllowProtected, generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description), density))
|
||||
SMPProxyFallback.Prohibit -> ValueTitleDesc(SMPProxyFallback.Prohibit, generalGetString(MR.strings.network_smp_proxy_fallback_prohibit), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description), density))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_smp_proxy_fallback_allow_downgrade),
|
||||
smpProxyFallback,
|
||||
values,
|
||||
icon = painterResource(MR.images.ic_arrows_left_right),
|
||||
enabled = enabled,
|
||||
onSelected = {
|
||||
showModal {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade))
|
||||
SectionViewSelectable(null, smpProxyFallback, values, updateSMPProxyFallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) {
|
||||
Row(
|
||||
@@ -506,8 +653,12 @@ fun PreviewNetworkAndServersLayout() {
|
||||
toggleSocksProxy = {},
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
||||
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
|
||||
smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) },
|
||||
useOnion = {},
|
||||
updateSessionMode = {},
|
||||
updateSMPProxyMode = {},
|
||||
updateSMPProxyFallback = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,8 +255,20 @@
|
||||
|
||||
<!-- Chat Alerts - ChatItemView.kt -->
|
||||
<string name="message_delivery_error_title">Message delivery error</string>
|
||||
<string name="message_delivery_warning_title">Message delivery warning</string>
|
||||
<string name="message_delivery_error_desc">Most likely this contact has deleted the connection with you.</string>
|
||||
|
||||
<!-- CIStatus errors -->
|
||||
<string name="ci_status_other_error">Error: %1$s</string>
|
||||
<string name="snd_error_auth">Wrong key or unknown connection - most likely this connection is deleted.</string>
|
||||
<string name="snd_error_quota">Capacity exceeded - recipient did not receive previously sent messages.</string>
|
||||
<string name="snd_error_expired">Network issues - message expired after many attempts to send it.</string>
|
||||
<string name="snd_error_relay">Destination server error: %1$s</string>
|
||||
<string name="snd_error_proxy">Forwarding server: %1$s\nError: %2$s</string>
|
||||
<string name="snd_error_proxy_relay">Forwarding server: %1$s\nDestination server error: %2$s</string>
|
||||
<string name="srv_error_host">Server address is incompatible with network settings.</string>
|
||||
<string name="srv_error_version">Server version is incompatible with network settings.</string>
|
||||
|
||||
<!-- Chat Actions - ChatItemView.kt (and general) -->
|
||||
<string name="reply_verb">Reply</string>
|
||||
<string name="share_verb">Share</string>
|
||||
@@ -710,6 +722,26 @@
|
||||
<string name="update_network_session_mode_question">Update transport isolation mode?</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string>
|
||||
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]></string>
|
||||
<string name="network_smp_proxy_mode_private_routing">Private routing</string>
|
||||
<string name="network_smp_proxy_mode_always">Always</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Unknown relays</string>
|
||||
<string name="network_smp_proxy_mode_unprotected">Unprotected</string>
|
||||
<string name="network_smp_proxy_mode_never">Never</string>
|
||||
<string name="network_smp_proxy_mode_always_description">Always use private routing.</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">Use private routing with unknown servers.</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Use private routing with unknown servers when IP address is not protected.</string>
|
||||
<string name="network_smp_proxy_mode_never_description">Do NOT use private routing.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Message routing mode</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Allow downgrade</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Yes</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">When IP hidden</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">No</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">Send messages directly when your or destination server does not support private routing.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">Send messages directly when IP address is protected and your or destination server does not support private routing.</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">Do NOT send messages directly, even if your or destination server does not support private routing.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Message routing fallback</string>
|
||||
<string name="private_routing_show_message_status">Show message status</string>
|
||||
<string name="private_routing_explanation">To protect your IP address, private routing uses your SMP servers to deliver messages.</string>
|
||||
<string name="appearance_settings">Appearance</string>
|
||||
<string name="customize_theme_title">Customize theme</string>
|
||||
<string name="theme_colors_section_title">THEME COLORS</string>
|
||||
@@ -1035,6 +1067,7 @@
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
<string name="settings_section_title_profile_images">Profile images</string>
|
||||
<string name="settings_section_title_messages">MESSAGES AND FILES</string>
|
||||
<string name="settings_section_title_private_message_routing">PRIVATE MESSAGE ROUTING</string>
|
||||
<string name="settings_section_title_calls">CALLS</string>
|
||||
<string name="settings_section_title_network_connection">Network connection</string>
|
||||
<string name="settings_section_title_incognito">Incognito mode</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#5f6368"><path d="M629-446.5H235.48q-13.79 0-23.64-9.79-9.84-9.79-9.84-23.5t9.84-23.71q9.85-10 23.64-10H629L455.79-686.71Q445.5-697 445.25-710.5t10.25-24.48q10.5-10.52 24-10.27t23.81 10.57L734.1-503.59q4.9 4.91 7.65 10.97 2.75 6.06 2.75 12.78 0 6.71-2.75 12.78Q739-461 734.5-456.5l-231 231q-11 11-23.75 10.5t-23.25-11.02Q446-237 446-250.42q0-13.41 10.5-23.58L629-446.5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 472 B |
@@ -0,0 +1,4 @@
|
||||
<svg height="24" viewBox="0 -960 960 960" width="24" fill="#000000" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 831.5 -480 L 657 -656.5 C 651.667 -662.167 648.917 -668.833 648.75 -676.5 C 648.583 -684.167 651.333 -690.833 657 -696.5 C 662.667 -702.5 669.333 -705.5 677 -705.5 C 684.667 -705.5 691.5 -702.667 697.5 -697 L 894 -500.5 C 897 -497.167 899.25 -493.917 900.75 -490.75 C 902.25 -487.583 903 -484 903 -480 C 903 -476 902.25 -472.417 900.75 -469.25 C 899.25 -466.083 897 -463 894 -460 L 697.5 -263.5 C 691.5 -257.5 684.667 -254.583 677 -254.75 C 669.333 -254.917 662.667 -257.833 657 -263.5 C 651 -269.5 648.167 -276.25 648.5 -283.75 C 648.833 -291.25 651.667 -297.833 657 -303.5 L 831.5 -480 Z M 128.5 -480 L 303 -303.5 C 308.333 -297.833 311.083 -291.167 311.25 -283.5 C 311.417 -275.833 308.667 -269.167 303 -263.5 C 297.333 -257.5 290.667 -254.5 283 -254.5 C 275.333 -254.5 268.667 -257.5 263 -263.5 L 66.5 -460 C 63.167 -463 60.833 -466.083 59.5 -469.25 C 58.167 -472.417 57.5 -476 57.5 -480 C 57.5 -484 58.167 -487.583 59.5 -490.75 C 60.833 -493.917 63.167 -497.167 66.5 -500.5 L 263 -697 C 268.667 -702.667 275.333 -705.417 283 -705.25 C 290.667 -705.083 297.5 -702.167 303.5 -696.5 C 309.167 -690.5 311.833 -683.75 311.5 -676.25 C 311.167 -668.75 308.333 -662.167 303 -656.5 L 128.5 -480 Z" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/>
|
||||
<rect x="123" y="-514" width="711.266" height="68" style="stroke: rgb(0, 0, 0);" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=5.7.3
|
||||
android.version_code=206
|
||||
android.version_name=5.8-beta.0
|
||||
android.version_code=208
|
||||
|
||||
desktop.version_name=5.7.3
|
||||
desktop.version_code=44
|
||||
desktop.version_name=5.8-beta.0
|
||||
desktop.version_code=45
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||
@@ -14,6 +14,7 @@ module Directory.Events
|
||||
DirectoryRole (..),
|
||||
SDirectoryRole (..),
|
||||
crDirectoryEvent,
|
||||
directoryCmdTag,
|
||||
viewName,
|
||||
)
|
||||
where
|
||||
@@ -21,6 +22,8 @@ where
|
||||
import Control.Applicative ((<|>))
|
||||
import Data.Attoparsec.Text (Parser)
|
||||
import qualified Data.Attoparsec.Text as A
|
||||
import Data.Char (isSpace)
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -34,13 +37,11 @@ import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Data.Char (isSpace)
|
||||
import Data.Either (fromRight)
|
||||
|
||||
data DirectoryEvent
|
||||
= DEContactConnected Contact
|
||||
| DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole}
|
||||
| DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember}
|
||||
| DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember}
|
||||
| DEGroupUpdated {contactId :: ContactId, fromGroup :: GroupInfo, toGroup :: GroupInfo}
|
||||
| DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed
|
||||
| DEServiceRoleChanged GroupInfo GroupMemberRole
|
||||
@@ -140,25 +141,26 @@ directoryCmdP =
|
||||
cmdStrP =
|
||||
(tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t)))
|
||||
<|> pure (ADC SDRUser DCUnknownCommand)
|
||||
tagP = A.takeTill (== ' ') >>= \case
|
||||
"help" -> u DCHelp_
|
||||
"h" -> u DCHelp_
|
||||
"next" -> u DCSearchNext_
|
||||
"all" -> u DCAllGroups_
|
||||
"new" -> u DCRecentGroups_
|
||||
"submit" -> u DCSubmitGroup_
|
||||
"confirm" -> u DCConfirmDuplicateGroup_
|
||||
"list" -> u DCListUserGroups_
|
||||
"ls" -> u DCListUserGroups_
|
||||
"delete" -> u DCDeleteGroup_
|
||||
"approve" -> su DCApproveGroup_
|
||||
"reject" -> su DCRejectGroup_
|
||||
"suspend" -> su DCSuspendGroup_
|
||||
"resume" -> su DCResumeGroup_
|
||||
"last" -> su DCListLastGroups_
|
||||
"exec" -> su DCExecuteCommand_
|
||||
"x" -> su DCExecuteCommand_
|
||||
_ -> fail "bad command tag"
|
||||
tagP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"help" -> u DCHelp_
|
||||
"h" -> u DCHelp_
|
||||
"next" -> u DCSearchNext_
|
||||
"all" -> u DCAllGroups_
|
||||
"new" -> u DCRecentGroups_
|
||||
"submit" -> u DCSubmitGroup_
|
||||
"confirm" -> u DCConfirmDuplicateGroup_
|
||||
"list" -> u DCListUserGroups_
|
||||
"ls" -> u DCListUserGroups_
|
||||
"delete" -> u DCDeleteGroup_
|
||||
"approve" -> su DCApproveGroup_
|
||||
"reject" -> su DCRejectGroup_
|
||||
"suspend" -> su DCSuspendGroup_
|
||||
"resume" -> su DCResumeGroup_
|
||||
"last" -> su DCListLastGroups_
|
||||
"exec" -> su DCExecuteCommand_
|
||||
"x" -> su DCExecuteCommand_
|
||||
_ -> fail "bad command tag"
|
||||
where
|
||||
u = pure . ADCT SDRUser
|
||||
su = pure . ADCT SDRSuperUser
|
||||
@@ -192,3 +194,23 @@ directoryCmdP =
|
||||
|
||||
viewName :: String -> String
|
||||
viewName n = if ' ' `elem` n then "'" <> n <> "'" else n
|
||||
|
||||
directoryCmdTag :: DirectoryCmd r -> Text
|
||||
directoryCmdTag = \case
|
||||
DCHelp -> "help"
|
||||
DCSearchGroup _ -> "search"
|
||||
DCSearchNext -> "next"
|
||||
DCAllGroups -> "all"
|
||||
DCRecentGroups -> "new"
|
||||
DCSubmitGroup _ -> "submit"
|
||||
DCConfirmDuplicateGroup {} -> "confirm"
|
||||
DCListUserGroups -> "list"
|
||||
DCDeleteGroup {} -> "delete"
|
||||
DCApproveGroup {} -> "approve"
|
||||
DCRejectGroup {} -> "reject"
|
||||
DCSuspendGroup {} -> "suspend"
|
||||
DCResumeGroup {} -> "resume"
|
||||
DCListLastGroups _ -> "last"
|
||||
DCExecuteCommand _ -> "exec"
|
||||
DCUnknownCommand -> "unknown"
|
||||
DCCommandError _ -> "error"
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
|
||||
module Directory.Service
|
||||
( welcomeGetOpts,
|
||||
@@ -15,6 +15,7 @@ where
|
||||
import Control.Concurrent (forkIO)
|
||||
import Control.Concurrent.Async
|
||||
import Control.Concurrent.STM
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Maybe (fromMaybe, maybeToList)
|
||||
@@ -37,7 +38,7 @@ import Simplex.Chat.Options
|
||||
import Simplex.Chat.Protocol (MsgContent (..))
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.View (serializeChatResponse, simplexChatContact)
|
||||
import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
@@ -96,9 +97,11 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
DEUnsupportedMessage _ct _ciId -> pure ()
|
||||
DEItemEditIgnored _ct -> pure ()
|
||||
DEItemDeleteIgnored _ct -> pure ()
|
||||
DEContactCommand ct ciId aCmd -> case aCmd of
|
||||
ADC SDRUser cmd -> deUserCommand env ct ciId cmd
|
||||
ADC SDRSuperUser cmd -> deSuperUserCommand ct ciId cmd
|
||||
DEContactCommand ct ciId (ADC sUser cmd) -> do
|
||||
logInfo $ "command received " <> directoryCmdTag cmd
|
||||
case sUser of
|
||||
SDRUser -> deUserCommand env ct ciId cmd
|
||||
SDRSuperUser -> deSuperUserCommand ct ciId cmd
|
||||
where
|
||||
withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId
|
||||
notifySuperUsers s = withSuperUsers $ \contactId -> sendMessage' cc contactId s
|
||||
@@ -107,7 +110,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
withGroupReg GroupInfo {groupId, localDisplayName} err action = do
|
||||
atomically (getGroupReg st groupId) >>= \case
|
||||
Just gr -> action gr
|
||||
Nothing -> putStrLn $ T.unpack $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId
|
||||
Nothing -> logError $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId
|
||||
groupInfoText GroupProfile {displayName = n, fullName = fn, description = d} =
|
||||
n <> (if n == fn || T.null fn then "" else " (" <> fn <> ")") <> maybe "" ("\nWelcome message:\n" <>) d
|
||||
userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName
|
||||
@@ -152,23 +155,25 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
|
||||
deContactConnected :: Contact -> IO ()
|
||||
deContactConnected ct = when (contactDirect ct) $ do
|
||||
unless testing $ putStrLn $ T.unpack (localDisplayName' ct) <> " connected"
|
||||
logInfo $ (viewContactName ct) <> " connected"
|
||||
sendMessage cc ct $
|
||||
"Welcome to " <> serviceName <> " service!\n\
|
||||
\Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\
|
||||
\For example, send _privacy_ to find groups about privacy.\n\
|
||||
\Or send */all* or */new* to list groups.\n\n\
|
||||
\Content and privacy policy: https://simplex.chat/docs/directory.html"
|
||||
("Welcome to " <> serviceName <> " service!\n")
|
||||
<> "Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\
|
||||
\For example, send _privacy_ to find groups about privacy.\n\
|
||||
\Or send */all* or */new* to list groups.\n\n\
|
||||
\Content and privacy policy: https://simplex.chat/docs/directory.html"
|
||||
|
||||
deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO ()
|
||||
deGroupInvitation ct g@GroupInfo {groupProfile = GroupProfile {displayName, fullName}} fromMemberRole memberRole = do
|
||||
logInfo $ "invited to group " <> viewGroupName g <> " by " <> viewContactName ct
|
||||
case badRolesMsg $ groupRolesStatus fromMemberRole memberRole of
|
||||
Just msg -> sendMessage cc ct msg
|
||||
Nothing -> getDuplicateGroup g >>= \case
|
||||
Just DGUnique -> processInvitation ct g
|
||||
Just DGRegistered -> askConfirmation
|
||||
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
|
||||
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
|
||||
Nothing ->
|
||||
getDuplicateGroup g >>= \case
|
||||
Just DGUnique -> processInvitation ct g
|
||||
Just DGRegistered -> askConfirmation
|
||||
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
|
||||
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
|
||||
where
|
||||
askConfirmation = do
|
||||
ugrId <- addGroupReg st ct g GRSPendingConfirmation
|
||||
@@ -205,7 +210,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
_ -> Nothing
|
||||
|
||||
deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO ()
|
||||
deServiceJoinedGroup ctId g owner =
|
||||
deServiceJoinedGroup ctId g owner = do
|
||||
logInfo $ "service joined group " <> viewGroupName g
|
||||
withGroupReg g "joined group" $ \gr ->
|
||||
when (ctId `isOwner` gr) $ do
|
||||
setGroupRegOwner st gr owner
|
||||
@@ -214,7 +220,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case
|
||||
CRGroupLinkCreated {connReqContact} -> do
|
||||
setGroupStatus st gr GRSPendingUpdate
|
||||
notifyOwner gr
|
||||
notifyOwner
|
||||
gr
|
||||
"Created the public link to join the group via this directory service that is always online.\n\n\
|
||||
\Please add it to the group welcome message.\n\
|
||||
\For example, add:"
|
||||
@@ -228,24 +235,26 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
_ -> notifyOwner gr $ unexpectedError "can't create group link"
|
||||
|
||||
deGroupUpdated :: ContactId -> GroupInfo -> GroupInfo -> IO ()
|
||||
deGroupUpdated ctId fromGroup toGroup =
|
||||
deGroupUpdated ctId fromGroup toGroup = do
|
||||
logInfo $ "group updated " <> viewGroupName toGroup
|
||||
unless (sameProfile p p') $ do
|
||||
withGroupReg toGroup "group updated" $ \gr -> do
|
||||
let userGroupRef = userGroupReference gr toGroup
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSPendingConfirmation -> pure ()
|
||||
GRSProposed -> pure ()
|
||||
GRSPendingUpdate -> groupProfileUpdate >>= \case
|
||||
GPNoServiceLink ->
|
||||
when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message."
|
||||
GPServiceLinkAdded
|
||||
| ctId `isOwner` gr -> groupLinkAdded gr
|
||||
| otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself."
|
||||
GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it."
|
||||
GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr
|
||||
GPServiceLinkError -> do
|
||||
when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers."
|
||||
putStrLn $ "Error: no group link for " <> userGroupRef
|
||||
GRSPendingUpdate ->
|
||||
groupProfileUpdate >>= \case
|
||||
GPNoServiceLink ->
|
||||
when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message."
|
||||
GPServiceLinkAdded
|
||||
| ctId `isOwner` gr -> groupLinkAdded gr
|
||||
| otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself."
|
||||
GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it."
|
||||
GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr
|
||||
GPServiceLinkError -> do
|
||||
when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers."
|
||||
logError $ "Error: no group link for " <> T.pack userGroupRef
|
||||
GRSPendingApproval n -> processProfileChange gr $ n + 1
|
||||
GRSActive -> processProfileChange gr 1
|
||||
GRSSuspended -> processProfileChange gr 1
|
||||
@@ -288,7 +297,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved."
|
||||
notifySuperUsers $ "The group " <> groupRef <> " is updated."
|
||||
checkRolesSendToApprove gr n'
|
||||
GPServiceLinkError -> putStrLn $ "Error: no group link for " <> groupRef <> " pending approval."
|
||||
GPServiceLinkError -> logError $ "Error: no group link for " <> T.pack groupRef <> " pending approval."
|
||||
groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId)
|
||||
where
|
||||
profileUpdate = \case
|
||||
@@ -297,7 +306,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact
|
||||
hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p
|
||||
hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p'
|
||||
in if
|
||||
in if
|
||||
| hadLinkBefore && hasLinkNow -> GPHasServiceLink
|
||||
| hadLinkBefore -> GPServiceLinkRemoved
|
||||
| hasLinkNow -> GPServiceLinkAdded
|
||||
@@ -311,18 +320,20 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
|
||||
sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO ()
|
||||
sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId} gaId = do
|
||||
ct_ <- getContact cc dbContactId
|
||||
ct_ <- getContact cc dbContactId
|
||||
gr_ <- getGroupAndSummary cc dbGroupId
|
||||
let membersStr = maybe "" (\(_, s) -> "_" <> tshow (currentMembers s) <> " members_\n") gr_
|
||||
text = maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_
|
||||
<> "\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:"
|
||||
text =
|
||||
maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_
|
||||
<> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:")
|
||||
msg = maybe (MCText text) (\image -> MCImage {text, image}) image'
|
||||
withSuperUsers $ \cId -> do
|
||||
sendComposedMessage' cc cId Nothing msg
|
||||
sendMessage' cc cId $ "/approve " <> show dbGroupId <> ":" <> viewName (T.unpack displayName) <> " " <> show gaId
|
||||
|
||||
deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO ()
|
||||
deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole =
|
||||
deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do
|
||||
logInfo $ "contact ID " <> tshow ctId <> " role changed in group " <> viewGroupName g <> " to " <> tshow contactRole
|
||||
withGroupReg g "contact role changed" $ \gr -> do
|
||||
let userGroupRef = userGroupReference gr g
|
||||
uCtRole = "Your role in the group " <> userGroupRef <> " is changed to " <> ctRole
|
||||
@@ -348,6 +359,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
|
||||
deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO ()
|
||||
deServiceRoleChanged g serviceRole = do
|
||||
logInfo $ "service role changed in group " <> viewGroupName g <> " to " <> tshow serviceRole
|
||||
withGroupReg g "service role changed" $ \gr -> do
|
||||
let userGroupRef = userGroupReference gr g
|
||||
uSrvRole = serviceName <> " role in the group " <> userGroupRef <> " is changed to " <> srvRole
|
||||
@@ -371,11 +383,12 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
srvRole = "*" <> B.unpack (strEncode serviceRole) <> "*"
|
||||
suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")."
|
||||
whenContactIsOwner gr action =
|
||||
getGroupMember gr >>=
|
||||
mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action)
|
||||
getGroupMember gr
|
||||
>>= mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action)
|
||||
|
||||
deContactRemovedFromGroup :: ContactId -> GroupInfo -> IO ()
|
||||
deContactRemovedFromGroup ctId g =
|
||||
deContactRemovedFromGroup ctId g = do
|
||||
logInfo $ "contact ID " <> tshow ctId <> " removed from group " <> viewGroupName g
|
||||
withGroupReg g "contact removed" $ \gr -> do
|
||||
when (ctId `isOwner` gr) $ do
|
||||
setGroupStatus st gr GRSRemoved
|
||||
@@ -383,7 +396,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)."
|
||||
|
||||
deContactLeftGroup :: ContactId -> GroupInfo -> IO ()
|
||||
deContactLeftGroup ctId g =
|
||||
deContactLeftGroup ctId g = do
|
||||
logInfo $ "contact ID " <> tshow ctId <> " left group " <> viewGroupName g
|
||||
withGroupReg g "contact left" $ \gr -> do
|
||||
when (ctId `isOwner` gr) $ do
|
||||
setGroupStatus st gr GRSRemoved
|
||||
@@ -391,7 +405,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)."
|
||||
|
||||
deServiceRemovedFromGroup :: GroupInfo -> IO ()
|
||||
deServiceRemovedFromGroup g =
|
||||
deServiceRemovedFromGroup g = do
|
||||
logInfo $ "service removed from group " <> viewGroupName g
|
||||
withGroupReg g "service removed" $ \gr -> do
|
||||
setGroupStatus st gr GRSRemoved
|
||||
notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory."
|
||||
@@ -402,11 +417,15 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
DCHelp ->
|
||||
sendMessage cc ct $
|
||||
"You must be the owner to add the group to the directory:\n\
|
||||
\1. Invite " <> serviceName <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\
|
||||
\2. " <> serviceName <> " bot will create a public group link for the new members to join even when you are offline.\n\
|
||||
\3. You will then need to add this link to the group welcome message.\n\
|
||||
\4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\
|
||||
\Start from inviting the bot to your group as admin - it will guide you through the process"
|
||||
\1. Invite "
|
||||
<> serviceName
|
||||
<> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\
|
||||
\2. "
|
||||
<> serviceName
|
||||
<> " bot will create a public group link for the new members to join even when you are offline.\n\
|
||||
\3. You will then need to add this link to the group welcome message.\n\
|
||||
\4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\
|
||||
\Start from inviting the bot to your group as admin - it will guide you through the process"
|
||||
DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s
|
||||
DCSearchNext ->
|
||||
atomically (TM.lookup (contactId' ct) searchRequests) >>= \case
|
||||
@@ -434,13 +453,13 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found"
|
||||
Just g@GroupInfo {groupProfile = GroupProfile {displayName}}
|
||||
| displayName == gName ->
|
||||
readTVarIO groupRegStatus >>= \case
|
||||
GRSPendingConfirmation -> do
|
||||
getDuplicateGroup g >>= \case
|
||||
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
|
||||
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
|
||||
_ -> processInvitation ct g
|
||||
_ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation."
|
||||
readTVarIO groupRegStatus >>= \case
|
||||
GRSPendingConfirmation -> do
|
||||
getDuplicateGroup g >>= \case
|
||||
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
|
||||
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
|
||||
_ -> processInvitation ct g
|
||||
_ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation."
|
||||
| otherwise -> sendReply $ "Group ID " <> show ugrId <> " has the display name " <> T.unpack displayName
|
||||
DCListUserGroups ->
|
||||
atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do
|
||||
@@ -462,7 +481,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
let gs' = takeTop searchResults gs
|
||||
moreGroups = length gs - length gs'
|
||||
more = if moreGroups > 0 then ", sending top " <> show (length gs') else ""
|
||||
sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "."
|
||||
sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "."
|
||||
updateSearchRequest (STSearch s) $ groupIds gs'
|
||||
sendFoundGroups gs' moreGroups
|
||||
sendAllGroups takeFirst sortName searchType = \case
|
||||
@@ -499,74 +518,76 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
msg = maybe (MCText text) (\image -> MCImage {text, image}) image_
|
||||
sendComposedMessage cc ct Nothing msg
|
||||
when (moreGroups > 0) $
|
||||
sendComposedMessage cc ct Nothing $ MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)."
|
||||
sendComposedMessage cc ct Nothing $
|
||||
MCText $
|
||||
"Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)."
|
||||
|
||||
deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO ()
|
||||
deSuperUserCommand ct ciId cmd
|
||||
| superUser `elem` superUsers = case cmd of
|
||||
DCApproveGroup {groupId, displayName = n, groupApprovalId} -> do
|
||||
getGroupAndReg groupId n >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (g, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSPendingApproval gaId
|
||||
| gaId == groupApprovalId -> do
|
||||
getDuplicateGroup g >>= \case
|
||||
Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers."
|
||||
Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory."
|
||||
_ -> do
|
||||
getGroupRolesStatus g gr >>= \case
|
||||
Just GRSOk -> do
|
||||
setGroupStatus st gr GRSActive
|
||||
sendReply "Group approved!"
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved."
|
||||
Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin
|
||||
Just GRSContactNotOwner -> replyNotApproved "user is not an owner."
|
||||
Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin
|
||||
Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers."
|
||||
where
|
||||
replyNotApproved reason = sendReply $ "Group is not approved: " <> reason
|
||||
serviceNotAdmin = serviceName <> " is not an admin."
|
||||
| otherwise -> sendReply "Incorrect approval code"
|
||||
_ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval."
|
||||
where
|
||||
groupRef = groupReference' groupId n
|
||||
DCRejectGroup _gaId _gName -> pure ()
|
||||
DCSuspendGroup groupId gName -> do
|
||||
let groupRef = groupReference' groupId gName
|
||||
getGroupAndReg groupId gName >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (_, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSActive -> do
|
||||
setGroupStatus st gr GRSSuspended
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators."
|
||||
sendReply "Group suspended!"
|
||||
_ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended."
|
||||
DCResumeGroup groupId gName -> do
|
||||
let groupRef = groupReference' groupId gName
|
||||
getGroupAndReg groupId gName >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (_, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSSuspended -> do
|
||||
setGroupStatus st gr GRSActive
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!"
|
||||
sendReply "Group listing resumed!"
|
||||
_ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed."
|
||||
DCListLastGroups count ->
|
||||
readTVarIO (groupRegs st) >>= \grs -> do
|
||||
sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "")
|
||||
void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do
|
||||
ct_ <- getContact cc dbContactId
|
||||
let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_
|
||||
sendGroupInfo ct gr dbGroupId $ Just ownerStr
|
||||
DCExecuteCommand cmdStr ->
|
||||
sendChatCmdStr cc cmdStr >>= \r -> do
|
||||
ts <- getCurrentTime
|
||||
tz <- getCurrentTimeZone
|
||||
sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
|
||||
DCCommandError tag -> sendReply $ "Command error: " <> show tag
|
||||
DCApproveGroup {groupId, displayName = n, groupApprovalId} ->
|
||||
getGroupAndReg groupId n >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (g, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSPendingApproval gaId
|
||||
| gaId == groupApprovalId -> do
|
||||
getDuplicateGroup g >>= \case
|
||||
Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers."
|
||||
Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory."
|
||||
_ -> do
|
||||
getGroupRolesStatus g gr >>= \case
|
||||
Just GRSOk -> do
|
||||
setGroupStatus st gr GRSActive
|
||||
sendReply "Group approved!"
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved."
|
||||
Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin
|
||||
Just GRSContactNotOwner -> replyNotApproved "user is not an owner."
|
||||
Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin
|
||||
Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers."
|
||||
where
|
||||
replyNotApproved reason = sendReply $ "Group is not approved: " <> reason
|
||||
serviceNotAdmin = serviceName <> " is not an admin."
|
||||
| otherwise -> sendReply "Incorrect approval code"
|
||||
_ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval."
|
||||
where
|
||||
groupRef = groupReference' groupId n
|
||||
DCRejectGroup _gaId _gName -> pure ()
|
||||
DCSuspendGroup groupId gName -> do
|
||||
let groupRef = groupReference' groupId gName
|
||||
getGroupAndReg groupId gName >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (_, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSActive -> do
|
||||
setGroupStatus st gr GRSSuspended
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators."
|
||||
sendReply "Group suspended!"
|
||||
_ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended."
|
||||
DCResumeGroup groupId gName -> do
|
||||
let groupRef = groupReference' groupId gName
|
||||
getGroupAndReg groupId gName >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (_, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSSuspended -> do
|
||||
setGroupStatus st gr GRSActive
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!"
|
||||
sendReply "Group listing resumed!"
|
||||
_ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed."
|
||||
DCListLastGroups count ->
|
||||
readTVarIO (groupRegs st) >>= \grs -> do
|
||||
sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "")
|
||||
void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do
|
||||
ct_ <- getContact cc dbContactId
|
||||
let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_
|
||||
sendGroupInfo ct gr dbGroupId $ Just ownerStr
|
||||
DCExecuteCommand cmdStr ->
|
||||
sendChatCmdStr cc cmdStr >>= \r -> do
|
||||
ts <- getCurrentTime
|
||||
tz <- getCurrentTimeZone
|
||||
sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
|
||||
DCCommandError tag -> sendReply $ "Command error: " <> show tag
|
||||
| otherwise = sendReply "You are not allowed to use this command"
|
||||
where
|
||||
superUser = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct}
|
||||
@@ -577,8 +598,9 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
getGroup cc gId
|
||||
$>>= \g@GroupInfo {groupProfile = GroupProfile {displayName}} ->
|
||||
if displayName == gName
|
||||
then atomically (getGroupReg st gId)
|
||||
$>>= \gr -> pure $ Just (g, gr)
|
||||
then
|
||||
atomically (getGroupReg st gId)
|
||||
$>>= \gr -> pure $ Just (g, gr)
|
||||
else pure Nothing
|
||||
|
||||
sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO ()
|
||||
|
||||
@@ -14,7 +14,7 @@ permalink: "/blog/20221206-simplex-chat-v4.3-voice-messages.html"
|
||||
|
||||
## SimpleX Chat reviews
|
||||
|
||||
Since we published [the security assessment of SimpleX Chat](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) completed by Trail of Bits in November, several sites published the reviews and included it in their recommendations:
|
||||
Since we published [the security assessment of SimpleX Chat](./20221108-simplex-chat-v4.2-security-audit-new-website.md) completed by Trail of Bits in November, several sites published the reviews and included it in their recommendations:
|
||||
|
||||
- Privacy Guides added SimpleX Chat to [the recommended private and secure messengers](https://www.privacyguides.org/real-time-communication/#simplex-chat).
|
||||
- Mike Kuketz – a well-known security expert – published [the review of SimpleX Chat](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) and added it to [the messenger matrix](https://www.messenger-matrix.de).
|
||||
|
||||
@@ -42,7 +42,7 @@ Many large tech companies prioritizing value extraction over value creation earn
|
||||
|
||||
We started working full-time on the project in 2021 when [Portman Wills](https://www.linkedin.com/in/portmanwills/) and [Peter Briffett](https://www.linkedin.com/in/peterbriffett/) (the founders of [Wagestream](https://wagestream.com/en/) where I led the engineering team) supported the company very early on, and several other angel investors joined later. In July 2022 SimpleX Chat raised a pre-seed funding from the VC fund [Village Global](https://www.villageglobal.vc) - its co-founder [Ben Casnocha](https://casnocha.com) was very excited about our vision of privacy-first fully decentralized messaging and community platform, both for the individual users and for the companies, independent of any crypto-currencies, that might grow to replace large centralized platforms, such as WhatsApp, Telegram and Signal.
|
||||
|
||||
Overall we raised from our investors approximately $370,000 for a small share of the company to allow the project team working full time for almost two years, funding product design and development, infrastructure, and also [the security assessment by Trail of Bits](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html). A large part of this money is not spent yet.
|
||||
Overall we raised from our investors approximately $370,000 for a small share of the company to allow the project team working full time for almost two years, funding product design and development, infrastructure, and also [the security assessment by Trail of Bits](./20221108-simplex-chat-v4.2-security-audit-new-website.md). A large part of this money is not spent yet.
|
||||
|
||||
The project was hugely supported by the users as well - collectively, [you donated](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) over $25,000. Without these donations the investment we raised would not be possible, because we believe that voluntary user donations can sustain the project in the long term – it already covers all infrastructure costs. There are only two ways an Internet service can exist - either users are paying for it, or the users data becomes the product for the real customers, as happened with many large Internet companies. In the latter case the users are losing much more money than they are saving by giving away their privacy and the rights to the content they create on the centralized platforms.
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Other limitations of the desktop app:
|
||||
- you cannot send voice messages.
|
||||
- there is no support for calls yet.
|
||||
|
||||
You can download the desktop app for Linux and Mac via [downloads page](https://simplex.chat/downloads). Windows version will be available soon.
|
||||
You can download the desktop app for Linux and Mac via [downloads page](../docs/DOWNLOADS.md). Windows version will be available soon.
|
||||
|
||||
## Group directory service and other group improvements
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ This is only possible when both devices are connected to the same local network.
|
||||
|
||||
**On desktop**
|
||||
|
||||
If you don't have desktop app installed yet, [download it](https://simplex.chat/downloads/) and create any chat profile - you don't need to use it, and when you create it there are no server requests sent and no accounts are created. Think about it as about user profile on your computer.
|
||||
If you don't have desktop app installed yet, [download it](../docs/DOWNLOADS.md) and create any chat profile - you don't need to use it, and when you create it there are no server requests sent and no accounts are created. Think about it as about user profile on your computer.
|
||||
|
||||
Then in desktop app settings choose *Link a mobile* - it will show a QR code.
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ permalink: "/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ra
|
||||
|
||||
# SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm
|
||||
|
||||
**Published:** Mar 14, 2024
|
||||
|
||||
This is a major upgrade for SimpleX messaging protocols, we are really proud to present the results of the hard work of our whole team on the [Pi day](https://en.wikipedia.org/wiki/Pi_Day).
|
||||
|
||||
This post also covers various aspects of end-to-end encryption, compares different messengers, and explains why and how quantum-resistant encryption is added to SimpleX Chat:
|
||||
@@ -101,7 +103,7 @@ This attack is much less understood by the users, and forward secrecy does not p
|
||||
|
||||
Out of all encryption algorithms known to us only _Signal double ratchet algorithm_ (also referred to as _Signal algorithm_ or _double ratchet algorithm_, which is not the same as Signal messaging platform and protocols) provides the ability for the encryption security to recover after break-ins attacks. This recovery happens automatically and transparently to the users, without them doing anything special or even knowing about break-in, by simply sending messages. Every time one of the communication parties replies to another party message, new random keys are generated and previously stolen keys become useless.
|
||||
|
||||
Double ratchet algorithm is used in Signal, Cwtch and SimpleX Chat. This is why you cannot use SimpleX Chat profile on more than one device at the same time - the encryption scheme rotates the long term keys, randomly, and keys on another device become useless, as they would become useless for the attacker who stole them. Security always has some costs to the convenience.
|
||||
Double ratchet algorithm is used in Signal, Cwtch and SimpleX Chat. But Signal app by allowing to use the same profile on multiple devices compromises the break-in recovery function of Signal algorithm, as explained in [this paper](https://eprint.iacr.org/2021/626.pdf). Because of break-in recovery you cannot use SimpleX Chat profile on more than one device at the same time - the encryption scheme rotates the long term keys, randomly, and keys on another device become useless, as they would become useless for the attacker who stole them. Security always has some costs to the convenience.
|
||||
|
||||
### 5. Man-in-the-middle attack - mitigated by two-factor key exchange
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ For example, while WhatsApp messages are [end-to-end encrypted](https://faq.what
|
||||
|
||||
This is called [metadata](https://en.wikipedia.org/wiki/Metadata). It reveals a wealth of information about you and your connections, and in the hands of a centralized monopoly, this can and does get misused in incredibly dangerous ways. Once such metadata is logged, it can create very detailed profiles about who you are, everywhere you’ve been, and everyone you’ve ever spoken to. In settling for apps that normalize this while giving you the illusion of privacy in their marketing, we are doing ourselves a disservice by accepting this as the default. Collectively, we aren’t doing enough to protect ourselves and our social graph from this invasive overreach.
|
||||
|
||||
When stored, aggregated and analyzed, this metadata provides ample information that could potentially incriminate someone or be submitted to authorities. When WhatsApp and Facebook Messenger enabled end-to-end encryption for messages, of course it was a welcome and widely celebrated change. But it’s important to remember that not all end-to-end encryption utilizes the same standards, [some implementations are more secure](https://simplex.chat/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.html#how-secure-is-end-to-end-encryption-in-different-messengers) than others, so it’s something that shouldn’t necessarily be accepted at face value. More importantly: collecting and storing an obscene amount of metadata should invite global scrutiny, considering this data is often combined with whatever other information companies like Meta harvest about your identity (which is [a lot](https://www.vox.com/recode/23172691/meta-tracking-privacy-hospitals).)
|
||||
When stored, aggregated and analyzed, this metadata provides ample information that could potentially incriminate someone or be submitted to authorities. When WhatsApp and Facebook Messenger enabled end-to-end encryption for messages, of course it was a welcome and widely celebrated change. But it’s important to remember that not all end-to-end encryption utilizes the same standards, [some implementations are more secure](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#how-secure-is-end-to-end-encryption-in-different-messengers) than others, so it’s something that shouldn’t necessarily be accepted at face value. More importantly: collecting and storing an obscene amount of metadata should invite global scrutiny, considering this data is often combined with whatever other information companies like Meta harvest about your identity (which is [a lot](https://www.vox.com/recode/23172691/meta-tracking-privacy-hospitals).)
|
||||
|
||||
<img src="./images/20240416-metadata.png" class="float-to-right" width="38%">
|
||||
|
||||
@@ -45,8 +45,8 @@ But we also need to acknowledge that the world is becoming increasingly dangerou
|
||||
|
||||
End-to-end encryption is a solid start, but it's just the beginning of our pursuit for true privacy and security. True privacy means that even when legal demands come knocking, there's no useful metadata to hand over. It's not enough to just protect the content of messages; we need consistent innovation in protecting metadata too.
|
||||
|
||||
Changing ingrained habits is tough, but your privacy is always worth the fight. Although giants like WhatsApp and Telegram may dominate global messaging for now, increasing concerns about data harvesting and AI-driven surveillance are fueling demand for alternatives. SimpleX Chat aims to be one of those strong alternatives, hence its radical focus on a decentralized framework with no user identifiers (in other words, nothing that uniquely identifies users on the protocol level to their contacts or to the relays) and extra optionality (self-hosting an [SMP server](https://simplex.chat/docs/server.html) or [XFTP server](https://simplex.chat/docs/xftp-server.html), access via Tor, [chat profiles](https://simplex.chat/docs/guide/chat-profiles.html) with incognito mode, etc.)
|
||||
Changing ingrained habits is tough, but your privacy is always worth the fight. Although giants like WhatsApp and Telegram may dominate global messaging for now, increasing concerns about data harvesting and AI-driven surveillance are fueling demand for alternatives. SimpleX Chat aims to be one of those strong alternatives, hence its radical focus on a decentralized framework with no user identifiers (in other words, nothing that uniquely identifies users on the protocol level to their contacts or to the relays) and extra optionality (self-hosting an [SMP server](../docs/SERVER.md) or [XFTP server](../docs/XFTP-SERVER.md), access via Tor, [chat profiles](../docs/guide/chat-profiles.md) with incognito mode, etc.)
|
||||
|
||||
As of today, most messaging alternatives, including SimpleX, will have some limitations. But with the limited resources we have, we are committed to daily progress towards creating a truly private messenger that anyone can use while maintaining the features that users have come to know and love in messaging interfaces. We want to be the prime example of a messenger that achieves genuine privacy without compromising it for convenience. We need to be able to reliably move away from small and niche use cases to endorsing and enforcing global standards for privacy and making it accessible for all users regardless of their technical expertise.
|
||||
|
||||
We’re grateful for the users and [donors](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) who have been following along on this journey thus far and helping with feedback, anything from bug reports to identifying potential risks. Building in the open has always been a necessity for transparency and ongoing [auditability](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html), because we don’t want anyone to just take our word for it. [See for yourself](https://github.com/simplex-chat) and engage in the discussions. We fully expect you to hold us accountable to our word.
|
||||
We’re grateful for the users and [donors](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) who have been following along on this journey thus far and helping with feedback, anything from bug reports to identifying potential risks. Building in the open has always been a necessity for transparency and ongoing [auditability](./20221108-simplex-chat-v4.2-security-audit-new-website.md), because we don’t want anyone to just take our word for it. [See for yourself](https://github.com/simplex-chat) and engage in the discussions. We fully expect you to hold us accountable to our word.
|
||||
|
||||
@@ -10,6 +10,8 @@ permalink: "/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user
|
||||
|
||||
# SimpleX network: legally binding transparency, v5.7 released with better calls and messages
|
||||
|
||||
**Published:** Apr 26, 2024
|
||||
|
||||
What's new in v5.7:
|
||||
- [quantum resistant end-to-end encryption](#quantum-resistant-end-to-end-encryption) with all contacts.
|
||||
- [forward and save messages](#forward-and-save-messages) without revealing the source.
|
||||
@@ -23,10 +25,10 @@ Also, we added Lithuanian interface language to the Android and desktop apps, th
|
||||
|
||||
We are committed to open-source, privacy and security. Here are the recent changes we made:
|
||||
|
||||
- We now have a [Transparency Reports](https://simplex.chat/transparency/) page.
|
||||
- We updated our [Privacy Policy](https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md) to remove undefined terms "impermissible" and "acceptable", which would allow us to remove anything we don't like, without any clarity on what that is. You can see the edits [here](https://github.com/simplex-chat/simplex-chat/pull/4076/files).
|
||||
- We published a new page with [Frequently Asked Questions](https://simplex.chat/faq/), thanks to the guidance from users.
|
||||
- We also have a new [Security Policy](https://simplex.chat/security/) – we welcome your feedback on it.
|
||||
- We now have a [Transparency Reports](../docs/TRANSPARENCY.md) page.
|
||||
- We updated our [Privacy Policy](../PRIVACY.md) to remove undefined terms "impermissible" and "acceptable", which would allow us to remove anything we don't like, without any clarity on what that is. You can see the edits [here](https://github.com/simplex-chat/simplex-chat/pull/4076/files).
|
||||
- We published a new page with [Frequently Asked Questions](../docs/FAQ.md), thanks to the guidance from users.
|
||||
- We also have a new [Security Policy](../docs/SECURITY.md) – we welcome your feedback on it.
|
||||
|
||||
What do we mean by “legally binding transparency?”. It includes these principles:
|
||||
- Accountability: an empty promise or commitment to transparency that is not legally binding is just marketing, and can provide opportunities for the organizations to be misleading or not disclose important information that can affect their users privacy and security.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
layout: layouts/article.html
|
||||
title: "SimpleX: Redefining Privacy by Making Hard Choices"
|
||||
date: 2024-05-16
|
||||
previewBody: blog_previews/20240516.html
|
||||
image: images/simplex-explained.png
|
||||
imageWide: true
|
||||
permalink: "/blog/20240516-simplex-redefining-privacy-hard-choices.html"
|
||||
---
|
||||
|
||||
# SimpleX: Redefining Privacy by Making Hard Choices
|
||||
|
||||
**Published:** May 16, 2024
|
||||
|
||||
When it comes to open source privacy tools, the status quo often dictates the limitations of existing protocols and structures. However, these norms need to be challenged to radically shift how we approach genuinely private communication. This requires doing some uncomfortable things, like making hard choices as it relates to funding, alternative decentralization models, doubling down on privacy over convenience, and more.
|
||||
|
||||
There will always be questions on why the SimpleX Chat and network makes the choices it makes, and that’s good! It’s important to question us and to understand the reasoning behind each decision, whether it’s technical, structural, financial or any other.
|
||||
|
||||
In this post we explain a bit more about why SimpleX operates and makes decisions the way it does.
|
||||
|
||||
## No user accounts
|
||||
|
||||
Within SimpleX network there are no user accounts, and more importantly, no user profile identifiers whatsoever at the protocol level, not even random numbers or cryptographic keys used to identify the users. This means there is absolutely nothing that uniquely links users to their contacts or to the network relays. While it's accurate to say, "You need an address to send something," it's crucial to understand that this "address" serves merely as a transient delivery destination, and not as a user profile identifier in any sense.
|
||||
|
||||
You can read more about how SimpleX works [here](https://simplex.chat/#how-simplex-works).
|
||||
|
||||
## Privacy over convenience
|
||||
|
||||
One of the main considerations often ignored in security and privacy comparisons between messaging applications is multi-device access. For example, in Signal’s case, the Sesame protocol used to support multi-device access has the vulnerability that is [explained in detail here](https://eprint.iacr.org/2021/626.pdf):
|
||||
|
||||
_"We present an attack on the post-compromise security of the Signal messenger that allows to stealthily register a new device via the Sesame protocol. [...] This new device can send and receive messages without raising any ‘Bad encrypted message’ errors. Our attack thus shows that the Signal messenger does not guarantee post-compromise security at all in the multi-device setting"_.
|
||||
|
||||
<img src="./images/20231125-mobile2.png" width="170" class="float-right">
|
||||
|
||||
Solutions are possible, and even the quoted paper proposes improvements, but they are not implemented in any existing communication solutions. Unfortunately this results in most communication systems, even those in the privacy space, having compromised security in multi-device settings due to these limitations. That's the reason we are not rushing a full multi-device support, and currently only provide [the ability to use mobile app profiles via the desktop app](./20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md#link-mobile-and-desktop-apps-via-secure-quantum-resistant-protocol), while they are on the same network.
|
||||
|
||||
Another choice that compromises privacy for convenience and usability is 3rd party push notifications. At SimpleX, we take a slow path of optimizing the network and battery consumption in the app, rather than simply hiding inefficiencies behind the quick fix solution of 3rd party push notifications that [increases vulnerability](https://www.wired.com/story/apple-google-push-notification-surveillance/), a path Signal and others chose. Like other choices, it has usability and optimization trade offs, but ultimately it’s the right thing to continue progressing towards a better solution as we explain [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html).
|
||||
|
||||
Whenever possible, we strive to achieve significantly higher levels of privacy and security. For example, unlike most, if not all, applications (including Signal), [we encrypt application files with per-file unique key](https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html#encrypted-local-files-and-media-with-forward-secrecy). Consequently, once a message is deleted, there's no means to open a file that someone may have stolen in hopes of acquiring the key later. Similarly, apps like Session have done away with forward secrecy, a decision which caused them [not to be recommended](https://www.privacyguides.org/en/real-time-communication/#additional-options) for "long-term or sensitive communications". And [misinformation](https://simplifiedprivacy.com/spain-has-banned-telegram-defending-session/) around this makes it dangerous and irresponsible to recommend without such necessary disclosures for people’s awareness.
|
||||
|
||||
Session’s decision was based on [the incorrect statements](https://getsession.org/blog/session-protocol-explained) about double ratchet being impossible in decentralized networks, and underplayed importance of forward secrecy, break-in recovery and deniability - the absence of these crucial qualities makes Session a much weaker choice for private messaging. For transparency, this was something that was debated with their team [here](https://twitter.com/SimpleXChat/status/1755216356159414602). We also made [a separate post](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#end-to-end-encryption-security-attacks-and-defense) about these qualities of end-to-end encryption and their presence in different messengers, to show that not all end-to-end encrypted apps offer the same level of protections.
|
||||
|
||||
## Network decentralization
|
||||
|
||||
<img src="./images/simplex-explained.svg" width="40%" class="float-right">
|
||||
|
||||
It's important to recognize that a model of decentralization where all servers are openly known and accessible to all clients, that some users ask for, actually results in a less decentralized network, and as the network grows it often requires an introduction of a central authority to protect from bad actors with malicious intent. Therefore, we've deliberately opted for a slower path towards achieving a higher degree of decentralization where there is no central server registry or network authority. For example, p2p designs may offer higher initial decentralization but often compromise on privacy and eventual decentralization. In essence, our approach prioritizes a balance between initial decentralization, privacy, and higher degree of decentralization down the line.
|
||||
|
||||
Additionally, while it's true that we haven't yet established a model to incentivize other network operators, it's certainly on the roadmap. We see the decentralization of network operators offered within the app as a top priority.
|
||||
|
||||
Where it stands today, users have the freedom to select their preferred servers within the SimpleX network by configuring the app, with thousands of self-hosted servers in operation. Moreover, numerous third-party applications rely on our code for their in-app communications, operating independently of our servers, many of which we may not even be aware of.
|
||||
|
||||
Decentralization is an ongoing journey, and we strive to proceed at a measured pace to ensure its proper implementation. While the immediate results may not always appear ideal, prioritizing a careful approach ensures that in the long run, the decisions made in this area align with our ultimate objectives of a private, efficient, reliable and fully decentralized network.
|
||||
|
||||
## Funding and profitability
|
||||
|
||||
We explain our rationale for funding [here](../docs/FAQ.md#funding-and-business-model). Funding sources is always one of the most difficult choices to make, and it’s important to underline that VC models don’t necessarily translate to a quest for control, interference of any kind, or overall influence on product roadmap and strategy. The vast majority of investors seek profitability. Irrespective of the organization type profitability is essential for a sustainable operation, and it can and should be done while adhering to the highest possible standards for privacy. For-profit vs. nonprofit is also not an accurate metric to measure a commitment towards privacy and open standards, which is further explained [here](./20240404-why-i-joined-simplex-chat-esraa-al-shafei.md).
|
||||
|
||||
To make a profit, satisfying customers is the key. Unlike the many companies that profit from selling customer data, we put user privacy first. Doing this at scale requires investments. If the investors don’t own or control a company, their participation becomes merely about profit for them, and not about how this profit is obtained. With the investors we have, we are completely aligned on this - they are betting on the future where privacy is the norm. They do not dictate on anything related to our model. We build SimpleX chat, protocols and network the way Internet should have been built if we as developers always put the privacy and empowerment of users first.
|
||||
|
||||
## Company jurisdiction
|
||||
|
||||
<img src="./images/20240516-parliament.jpg" width="40%" class="float-right">
|
||||
|
||||
With regards to jurisdictions, nowhere is perfect. For that reason we plan to establish the foundations for protocol governance in [various jurisdictions](https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html#the-journey-to-the-decentralized-non-profit-protocol-governance).
|
||||
|
||||
But we’d like to clarify some misconceptions about the UK, where SimpleX Chat Ltd. is registered, and the UK legislation.
|
||||
|
||||
For example, the Online Safety Act (OSA). Some people believe that it applies only to UK companies. But the OSA applicability isn’t determined by the company’s jurisdiction - it applies based on the nature and characteristics of the business and its services, as well as the number of its users in the UK. In case of SimpleX network, the OSA doesn’t apply for both of these reasons.
|
||||
|
||||
The UK’s position on communication encryption, and more specifically, on end-to-end encrypted messaging, remains the subject of political debates. But with the OSA, the legislative intent was to propose technical measures to block CSAM, and it was trying to explore ways to do this via client-side scanning, which of course would undermine the encryption. However, and thanks to the hard work of privacy experts, researchers, academics and rights organizations throughout the UK and the rest of the world, the Online Safety Bill did not prohibit end-to-end encrypted apps without such scanners. It is an open question whether such technology will ever be possible, and the UK government made a public commitment that client-side scanning won't be required until it is.
|
||||
|
||||
For now, strong end-to-end encryption remains permissible and protected, and we hope to also add to the privacy advocacy and debates as a UK-based company to keep it legally protected.
|
||||
|
||||
Overall, we view the UK as being better jurisdiction for privacy than many alternatives - there are some trade-offs everywhere.
|
||||
|
||||
## Looking ahead
|
||||
|
||||
The future of the Internet should be based on decentralized infrastructure operated by commercially viable organizations. These operators need to possess minimal user data, so that users have genuine control over their identities, and free from lock-in by the operators, to support fair competition. This requires a drastic re-imagining of the current norms and newer, more privacy-minded protocols. All in all, private messaging is surrounded by very difficult challenges but it’s worth it to keep pushing the industry forward and not settle for the status quo and current trade offs, protocol limitations and vulnerabilities. The Internet deserves better standards, and so do users.
|
||||
@@ -1,5 +1,19 @@
|
||||
# Blog
|
||||
|
||||
May 16, 2024 [SimpleX: Redefining Privacy by Making Hard Choices](./20240516-simplex-redefining-privacy-hard-choices.md)
|
||||
|
||||
When it comes to open source privacy tools, the status quo often dictates the limitations of existing protocols and structures. However, these norms need to be challenged to radically shift how we approach genuinely private communication. This requires doing some uncomfortable things, like making hard choices as it relates to funding, alternative decentralization models, doubling down on privacy over convenience, and more.
|
||||
|
||||
In this post we explain a bit more about why SimpleX operates and makes decisions the way it does:
|
||||
|
||||
- No user accounts.
|
||||
- Privacy over convenience.
|
||||
- Network decentralization.
|
||||
- Funding and profitability.
|
||||
- Company jurisdiction.
|
||||
|
||||
---
|
||||
|
||||
Apr 26, 2024 [SimpleX network: legally binding transparency, v5.7 released with better calls and messages](./20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
|
||||
|
||||
We published Transparency Reports, Security Policy, and Frequently Asked Questions, and updated Privacy Policy.
|
||||
|
||||
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,82 @@
|
||||
<svg width="539" height="399" viewBox="0 0 539 399" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="268.083" cy="80.9476" r="32.1985" fill="url(#paint0_linear_651_6627)"/>
|
||||
<circle cx="268.082" cy="80.9513" r="39.3169" stroke="url(#paint1_linear_651_6627)" stroke-width="0.956242"/>
|
||||
<circle cx="267.428" cy="73.3473" r="5.65297" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M256.025 90.0869H278.595C277.039 85.3429 272.575 81.917 267.31 81.917C262.046 81.917 257.582 85.3429 256.025 90.0869Z" fill="white"/>
|
||||
<circle cx="40.5779" cy="218.884" r="32.1985" fill="url(#paint2_linear_651_6627)"/>
|
||||
<circle cx="40.5777" cy="218.888" r="39.3169" stroke="url(#paint3_linear_651_6627)" stroke-width="0.956242"/>
|
||||
<circle cx="39.9227" cy="211.284" r="5.65297" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.0904 228.023H28.5205C30.0767 223.279 34.541 219.854 39.8054 219.854C45.0699 219.854 49.5341 223.279 51.0904 228.023Z" fill="white"/>
|
||||
<circle cx="268.083" cy="358.611" r="32.1985" fill="url(#paint4_linear_651_6627)"/>
|
||||
<circle cx="268.082" cy="358.614" r="39.3169" stroke="url(#paint5_linear_651_6627)" stroke-width="0.956242"/>
|
||||
<circle cx="267.428" cy="351.01" r="5.65297" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M256.025 367.75H278.595C277.039 363.006 272.575 359.58 267.31 359.58C262.046 359.58 257.582 363.006 256.025 367.75Z" fill="white"/>
|
||||
<circle cx="499.17" cy="217.092" r="32.1985" fill="url(#paint6_linear_651_6627)"/>
|
||||
<circle cx="499.17" cy="217.096" r="39.3169" stroke="url(#paint7_linear_651_6627)" stroke-width="0.956242"/>
|
||||
<circle cx="498.515" cy="209.492" r="5.65297" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M509.683 226.231H487.113C488.669 221.487 493.133 218.062 498.398 218.062C503.662 218.062 508.126 221.487 509.683 226.231Z" fill="white"/>
|
||||
<path d="M79.8735 192.248L224.526 103.265" stroke="#0197FF" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/>
|
||||
<path d="M455.16 192.248L310.507 103.265" stroke="#CD5CAE" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/>
|
||||
<path d="M222.747 338.695L78.0943 249.712" stroke="#9B90FB" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/>
|
||||
<path d="M268.016 301.529L268.016 137.652" stroke="#1BC3BF" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/>
|
||||
<rect x="87.3558" y="63.0347" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/>
|
||||
<path d="M108.408 76.2031H111.281V92.9629H107.977V79.6987L104.817 80.5846L104.002 77.7594L108.408 76.2031Z" fill="#0197FF"/>
|
||||
<rect x="167.228" y="198.131" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/>
|
||||
<path d="M190.169 218.101C191.303 218.437 192.22 219.027 192.923 219.873C193.641 220.703 194 221.709 194 222.89C194 224.614 193.417 225.963 192.252 226.936C191.103 227.91 189.69 228.397 188.014 228.397C186.706 228.397 185.532 228.101 184.495 227.511C183.473 226.904 182.731 226.018 182.268 224.853L185.093 223.225C185.508 224.518 186.482 225.164 188.014 225.164C188.86 225.164 189.515 224.965 189.978 224.566C190.457 224.151 190.696 223.592 190.696 222.89C190.696 222.204 190.457 221.653 189.978 221.238C189.515 220.823 188.86 220.615 188.014 220.615H187.296L186.027 218.7L189.331 214.39H182.771V211.302H193.306V214.031L190.169 218.101Z" fill="#0197FF"/>
|
||||
<rect x="86.9073" y="326.685" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/>
|
||||
<path d="M108.065 345.96C109.677 345.96 111.034 346.447 112.136 347.42C113.237 348.378 113.788 349.719 113.788 351.443C113.788 353.167 113.205 354.515 112.04 355.489C110.891 356.463 109.478 356.949 107.802 356.949C106.445 356.949 105.248 356.646 104.211 356.04C103.173 355.433 102.423 354.539 101.96 353.358L104.785 351.706C105.2 353.047 106.206 353.717 107.802 353.717C108.648 353.717 109.302 353.518 109.765 353.119C110.244 352.704 110.484 352.145 110.484 351.443C110.484 350.756 110.252 350.206 109.789 349.791C109.326 349.376 108.696 349.168 107.898 349.168H102.822L103.492 339.854H112.997V342.943H106.557L106.341 345.96H108.065Z" fill="#0197FF"/>
|
||||
<path d="M72.9777 331.097C72.8728 331.917 72.1232 332.497 71.3035 332.392L57.9447 330.682C57.125 330.577 56.5455 329.828 56.6504 329.008C56.7553 328.188 57.5049 327.609 58.3246 327.714L70.199 329.233L71.7186 317.359C71.8235 316.539 72.5731 315.959 73.3928 316.064C74.2126 316.169 74.7921 316.919 74.6872 317.739L72.9777 331.097ZM38.6697 271.837C44.3979 288.6 58.4045 311.53 72.6773 329.992L70.3095 331.823C55.9494 313.248 41.7191 290.016 35.8377 272.805L38.6697 271.837Z" fill="#9B90FB"/>
|
||||
<path d="M215.639 371.664C216.25 372.22 216.295 373.167 215.738 373.778L206.672 383.737C206.115 384.348 205.169 384.392 204.558 383.836C203.947 383.279 203.902 382.333 204.459 381.722L212.518 372.87L203.666 364.811C203.054 364.254 203.01 363.308 203.566 362.697C204.123 362.085 205.069 362.041 205.68 362.597L215.639 371.664ZM147.42 365.75C164.435 370.679 191.252 372.369 214.562 371.276L214.702 374.265C191.249 375.365 164.057 373.686 146.587 368.625L147.42 365.75Z" fill="#9B90FB"/>
|
||||
<path d="M186.13 264.557C186.388 263.772 187.233 263.344 188.018 263.601L200.815 267.8C201.6 268.057 202.028 268.903 201.77 269.688C201.513 270.473 200.667 270.901 199.882 270.644L188.507 266.912L184.775 278.286C184.518 279.072 183.672 279.499 182.887 279.242C182.102 278.984 181.674 278.139 181.932 277.353L186.13 264.557ZM228.079 319.816C211.627 305.232 196.047 285.13 186.217 265.699L188.887 264.348C198.544 283.436 213.89 303.238 230.065 317.576L228.079 319.816Z" fill="#9B90FB"/>
|
||||
<path d="M101.379 249.32C100.828 248.704 100.881 247.758 101.497 247.208L111.539 238.233C112.155 237.682 113.101 237.736 113.652 238.352C114.203 238.968 114.149 239.914 113.533 240.465L104.607 248.442L112.584 257.368C113.135 257.984 113.082 258.93 112.466 259.481C111.85 260.032 110.904 259.978 110.353 259.362L101.379 249.32ZM162.973 242.498C143.252 249.825 124.069 251.033 102.41 249.817L102.578 246.829C124.02 248.033 142.745 246.821 161.931 239.692L162.973 242.498Z" fill="#9B90FB"/>
|
||||
<rect x="325.884" y="198.131" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/>
|
||||
<path d="M353.504 222.148V225.212H351.708V228.062H348.38V225.212H340.455V222.148L346.034 211.302H349.577L344.022 222.148H348.38V218.173H351.708V222.148H353.504Z" fill="#0197FF"/>
|
||||
<path d="M317.746 191.65C318.511 191.964 319.385 191.599 319.699 190.835L324.815 178.376C325.129 177.612 324.764 176.738 323.999 176.424C323.235 176.11 322.36 176.475 322.046 177.24L317.499 188.313L306.425 183.766C305.661 183.452 304.786 183.817 304.472 184.582C304.158 185.346 304.524 186.221 305.288 186.535L317.746 191.65ZM283.185 132.538C294.926 145.802 307.938 169.311 316.934 190.843L319.696 189.689C310.645 168.026 297.481 144.173 285.425 130.554L283.185 132.538Z" fill="#1BC3BF"/>
|
||||
<path d="M302.485 317.64C302.635 318.453 303.416 318.99 304.228 318.839L317.472 316.393C318.284 316.243 318.822 315.462 318.671 314.649C318.521 313.837 317.741 313.3 316.928 313.45L305.156 315.625L302.981 303.853C302.831 303.04 302.05 302.503 301.238 302.653C300.425 302.803 299.888 303.584 300.038 304.396L302.485 317.64ZM333.458 256.571C328.667 273.625 315.952 297.296 302.724 316.52L305.189 318.216C318.498 298.875 331.42 274.89 336.339 257.38L333.458 256.571Z" fill="#1BC3BF"/>
|
||||
<path d="M214.41 250.543C213.646 250.228 212.771 250.592 212.456 251.356L207.324 263.807C207.009 264.571 207.373 265.446 208.137 265.761C208.902 266.076 209.776 265.712 210.091 264.948L214.653 253.88L225.721 258.442C226.485 258.757 227.36 258.393 227.675 257.629C227.99 256.864 227.626 255.99 226.862 255.675L214.41 250.543ZM248.894 309.7C237.17 296.421 224.189 272.895 215.221 251.351L212.458 252.501C221.481 274.177 234.613 298.046 246.651 311.681L248.894 309.7Z" fill="#1BC3BF"/>
|
||||
<path d="M252.992 138.384C252.581 137.667 251.666 137.42 250.949 137.832L239.272 144.542C238.556 144.954 238.309 145.868 238.72 146.585C239.132 147.302 240.047 147.549 240.763 147.137L251.143 141.172L257.107 151.552C257.519 152.269 258.434 152.516 259.15 152.104C259.867 151.692 260.114 150.778 259.702 150.061L252.992 138.384ZM223.657 201.324C235.535 186.647 247.194 161.53 253.14 139.52L250.25 138.739C244.379 160.474 232.874 185.176 221.331 199.442L223.657 201.324Z" fill="#1BC3BF"/>
|
||||
<rect x="398.022" y="63.0347" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/>
|
||||
<path d="M413.321 92.9629V90.4968L419.044 84.607C420.321 83.2662 420.959 82.1489 420.959 81.255C420.959 80.6006 420.752 80.0738 420.337 79.6748C419.938 79.2757 419.419 79.0762 418.78 79.0762C417.519 79.0762 416.586 79.7307 415.979 81.0395L413.202 79.4114C413.728 78.2622 414.487 77.3843 415.476 76.7777C416.466 76.1712 417.551 75.8679 418.732 75.8679C420.249 75.8679 421.55 76.3468 422.635 77.3045C423.721 78.2462 424.263 79.5232 424.263 81.1353C424.263 82.8751 423.345 84.6628 421.51 86.4984L418.23 89.7785H424.527V92.9629H413.321Z" fill="#0197FF"/>
|
||||
<path d="M77.1479 111.221C77.253 110.401 76.6737 109.651 75.854 109.546L62.4958 107.833C61.676 107.727 60.9263 108.307 60.8211 109.126C60.716 109.946 61.2953 110.696 62.115 110.801L73.989 112.324L72.4659 124.198C72.3607 125.018 72.94 125.768 73.7598 125.873C74.5795 125.978 75.3292 125.399 75.4344 124.579L77.1479 111.221ZM42.3631 168.218C46.7014 146.722 60.4086 124.708 76.5785 112.214L74.7487 109.846C57.9918 122.793 43.9057 145.446 39.4294 167.626L42.3631 168.218Z" fill="#0197FF"/>
|
||||
<path d="M215.45 70.1934C216.205 69.8575 216.545 68.973 216.209 68.2179L210.734 55.9134C210.398 55.1584 209.513 54.8186 208.758 55.1546C208.003 55.4906 207.663 56.3751 207.999 57.1301L212.866 68.0675L201.929 72.9343C201.174 73.2703 200.834 74.1548 201.17 74.9098C201.506 75.6649 202.391 76.0046 203.146 75.6686L215.45 70.1934ZM148.677 70.0794C169.298 62.6197 195.23 62.8963 214.305 70.2232L215.378 67.4294C195.611 59.8365 168.936 59.5682 147.659 67.2651L148.677 70.0794Z" fill="#0197FF"/>
|
||||
<path d="M203.562 181.745C203.705 182.559 204.482 183.102 205.295 182.959L218.558 180.62C219.372 180.476 219.916 179.7 219.772 178.886C219.629 178.072 218.852 177.529 218.039 177.672L206.249 179.752L204.17 167.962C204.027 167.148 203.25 166.605 202.436 166.749C201.623 166.892 201.079 167.668 201.223 168.482L203.562 181.745ZM235.03 120.93C230.101 137.944 217.194 161.511 203.81 180.627L206.261 182.343C219.727 163.111 232.843 139.232 237.905 121.762L235.03 120.93Z" fill="#0197FF"/>
|
||||
<path d="M86.7735 223.138C86.1511 223.682 86.0872 224.627 86.6309 225.249L95.4903 235.393C96.034 236.015 96.9793 236.079 97.6018 235.535C98.2242 234.992 98.2881 234.046 97.7444 233.424L89.8693 224.408L98.8857 216.533C99.5081 215.989 99.572 215.044 99.0283 214.421C98.4846 213.799 97.5393 213.735 96.9169 214.279L86.7735 223.138ZM155.1 218.631C137.987 223.208 111.141 224.345 87.8588 222.772L87.657 225.758C111.082 227.341 138.303 226.222 155.873 221.522L155.1 218.631Z" fill="#0197FF"/>
|
||||
<path d="M383.345 66.4144C384.119 66.1236 384.51 65.2608 384.219 64.4872L379.481 51.8806C379.19 51.107 378.327 50.7156 377.554 51.0064C376.78 51.2971 376.389 52.16 376.679 52.9336L380.891 64.1394L369.686 68.3513C368.912 68.6421 368.521 69.505 368.811 70.2786C369.102 71.0522 369.965 71.4436 370.739 71.1528L383.345 66.4144ZM316.695 62.3541C337.722 56.1262 363.592 57.9349 382.2 66.3764L383.437 63.6509C364.152 54.903 337.54 53.0586 315.845 59.4845L316.695 62.3541Z" fill="#CD5CAE"/>
|
||||
<path d="M484.334 169.427C484.984 169.937 485.925 169.823 486.435 169.173L494.746 158.575C495.256 157.925 495.142 156.984 494.491 156.474C493.841 155.964 492.901 156.078 492.391 156.728L485.003 166.149L475.583 158.761C474.933 158.251 473.992 158.365 473.482 159.016C472.972 159.666 473.086 160.607 473.736 161.117L484.334 169.427ZM453.18 110.367C469.425 125.098 481.318 148.143 483.772 168.429L486.743 168.07C484.2 147.047 471.952 123.35 455.191 108.15L453.18 110.367Z" fill="#CD5CAE"/>
|
||||
<path d="M381.01 219.006C380.358 219.514 380.241 220.455 380.749 221.107L389.024 231.732C389.532 232.384 390.472 232.501 391.124 231.993C391.776 231.485 391.893 230.545 391.386 229.893L384.03 220.448L393.475 213.093C394.127 212.585 394.244 211.645 393.736 210.993C393.228 210.341 392.288 210.224 391.636 210.731L381.01 219.006ZM449.482 218.349C432.139 221.957 405.272 221.582 382.115 218.702L381.745 221.672C405.044 224.57 432.285 224.983 450.091 221.279L449.482 218.349Z" fill="#CD5CAE"/>
|
||||
<path d="M305.2 125.099C304.379 125.194 303.791 125.937 303.886 126.758L305.439 140.136C305.534 140.957 306.277 141.545 307.098 141.45C307.919 141.355 308.507 140.612 308.412 139.791L307.032 127.9L318.923 126.52C319.744 126.424 320.332 125.682 320.237 124.861C320.142 124.04 319.399 123.451 318.578 123.547L305.2 125.099ZM342.253 184.234C340.61 174.231 335.593 162.688 329.05 152.113C322.503 141.531 314.345 131.783 306.302 125.413L304.444 127.759C312.143 133.857 320.086 143.312 326.505 153.687C332.928 164.069 337.742 175.237 339.299 184.719L342.253 184.234Z" fill="#CD5CAE"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_651_6627" x1="299.334" y1="48.749" x2="222.184" y2="75.0796" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#53C1FF"/>
|
||||
<stop offset="1" stop-color="#0053D0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_651_6627" x1="306.707" y1="41.1562" x2="211.356" y2="73.6989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#53C1FF"/>
|
||||
<stop offset="1" stop-color="#0053D0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_651_6627" x1="71.8294" y1="186.686" x2="-5.32023" y2="213.016" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#53C1FF"/>
|
||||
<stop offset="1" stop-color="#0053D0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_651_6627" x1="79.2023" y1="179.093" x2="-16.149" y2="211.635" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#53C1FF"/>
|
||||
<stop offset="1" stop-color="#0053D0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_651_6627" x1="299.334" y1="326.412" x2="222.184" y2="352.743" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#53C1FF"/>
|
||||
<stop offset="1" stop-color="#0053D0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_651_6627" x1="306.707" y1="318.819" x2="211.356" y2="351.362" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#53C1FF"/>
|
||||
<stop offset="1" stop-color="#0053D0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_651_6627" x1="530.421" y1="184.894" x2="453.272" y2="211.224" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#53C1FF"/>
|
||||
<stop offset="1" stop-color="#0053D0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_651_6627" x1="537.794" y1="177.301" x2="442.443" y2="209.843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#53C1FF"/>
|
||||
<stop offset="1" stop-color="#0053D0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -1,6 +1,6 @@
|
||||
# Blog
|
||||
|
||||
4 févr. 2023 [SimpleX Chat v4.5 publié](./20230103-simplex-chat-v4.4-disappearing-messages.md)
|
||||
4 févr. 2023 [SimpleX Chat v4.5 publié](../../20230103-simplex-chat-v4.4-disappearing-messages.md)
|
||||
|
||||
- profils de chat multiples.
|
||||
- brouillon de message.
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
Nous avons également ajouté [l'interface en italien](#french-language-interface), grâce à nos utilisateurs et à Weblate !
|
||||
|
||||
3 janv. 2023 [SimpleX Chat v4.4 publié](./20230103-simplex-chat-v4.4-disappearing-messages.md)
|
||||
3 janv. 2023 [SimpleX Chat v4.4 publié](../../20230103-simplex-chat-v4.4-disappearing-messages.md)
|
||||
|
||||
- messages éphèméres.
|
||||
- messages "en direct" (dynamique).
|
||||
@@ -19,7 +19,7 @@ Nous avons également ajouté [l'interface en italien](#french-language-interfac
|
||||
|
||||
Nous avons également ajouté [l'interface en français](#french-language-interface), grâce à nos utilisateurs et à Weblate !
|
||||
|
||||
6 déc. 2022 [SimpleX Chat : révision et sortie de la v4.3](./20221206-simplex-chat-v4.3-voice-messages.md)
|
||||
6 déc. 2022 [SimpleX Chat : révision et sortie de la v4.3](../../20221206-simplex-chat-v4.3-voice-messages.md)
|
||||
|
||||
Critiques de novembre :
|
||||
|
||||
@@ -35,7 +35,7 @@ Sortie de la v4.3 :
|
||||
- amélioration de la configuration du serveur SMP et du support des mots de passe du serveur
|
||||
- améliorations de la confidentialité et de la sécurité : protection de l'écran de l'application, sécurité des liens SimpleX, etc.
|
||||
|
||||
8 nov. 2022 [Audit de sécurité par Trail of Bits, nouveau site web et sortie de la v4.2](./20221108-simplex-chat-v4.2-security-audit-new-website.md)
|
||||
8 nov. 2022 [Audit de sécurité par Trail of Bits, nouveau site web et sortie de la v4.2](../../20221108-simplex-chat-v4.2-security-audit-new-website.md)
|
||||
|
||||
_"Avez-vous été audité ou devons-nous simplement vous ignorer ?"_
|
||||
|
||||
@@ -51,7 +51,7 @@ Sortie de la v4.2 :
|
||||
- changer manuellement de contact ou de membre vers une autre adresse / serveur (BETA)
|
||||
- recevoir des fichiers plus rapidement (BETA)
|
||||
|
||||
28 sept. 2022 [v4 : chiffrement de la base de données locale](./20220928-simplex-chat-v4-encrypted-database.md)
|
||||
28 sept. 2022 [v4 : chiffrement de la base de données locale](../../20220928-simplex-chat-v4-encrypted-database.md)
|
||||
|
||||
- base de données locale de chat chiffrée - si vous utilisez déjà l'application, vous pouvez chiffrer la base de données dans les paramètres de l'application.
|
||||
- support pour les serveurs WebRTC ICE auto-hébergés
|
||||
@@ -61,7 +61,7 @@ Sortie de la v4.2 :
|
||||
- support des images animées dans l'application Android
|
||||
- Interface utilisateur en allemand pour les applications mobiles
|
||||
|
||||
1 sept. 2022 [v3.2 : Mode Incognito](./20220901-simplex-chat-v3.2-incognito-mode.md)
|
||||
1 sept. 2022 [v3.2 : Mode Incognito](../../20220901-simplex-chat-v3.2-incognito-mode.md)
|
||||
|
||||
- Mode Incognito - utiliser un nouveau nom de profil aléatoire pour chaque contact
|
||||
- utiliser des adresses de serveur .onion avec Tor
|
||||
@@ -71,7 +71,7 @@ Sortie de la v4.2 :
|
||||
|
||||
L'audit d'implémentation est prévu pour Octobre !
|
||||
|
||||
8 août 2022 [v3.1 : groupes de discussion](./20220808-simplex-chat-v3.1-chat-groups.md)
|
||||
8 août 2022 [v3.1 : groupes de discussion](../../20220808-simplex-chat-v3.1-chat-groups.md)
|
||||
|
||||
- enfin, des groupes de chat secrets - personne d'autre que les membres ne sait qu'ils existent !
|
||||
- accès aux serveurs de messagerie via Tor sur toutes les plateformes
|
||||
@@ -79,37 +79,37 @@ L'audit d'implémentation est prévu pour Octobre !
|
||||
- protocole de chat publié
|
||||
- nouvelles icônes d'application
|
||||
|
||||
23 juil. 2022 [v3.1-beta : accès aux serveurs via Tor](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md)
|
||||
23 juil. 2022 [v3.1-beta : accès aux serveurs via Tor](../../20220723-simplex-chat-v3.1-tor-groups-efficiency.md)
|
||||
|
||||
- application terminale : accès aux serveurs de messagerie via un proxy SOCKS5 (par exemple, Tor).
|
||||
- applications mobiles : rejoindre et quitter des groupes de discussion.
|
||||
- utilisation optimisée de la batterie et du trafic - réduction jusqu'à 90x !
|
||||
- deux configurations docker pour les serveurs SMP auto-hébergés.
|
||||
|
||||
11 juil. 2022 [v3 : notifications push instantanées pour iOS et appels audio/vidéo](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md) :
|
||||
11 juil. 2022 [v3 : notifications push instantanées pour iOS et appels audio/vidéo](../../20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md) :
|
||||
|
||||
- exportation et importation de la base de données de chat
|
||||
- appels audio/vidéo chiffrés de bout en bout
|
||||
- amélioration de la confidentialité du protocole et des performances
|
||||
|
||||
4 juin 2022 [v2.2 : nouveaux paramètres de confidentialité et de sécurité](./20220604-simplex-chat-new-privacy-security-settings.md)
|
||||
4 juin 2022 [v2.2 : nouveaux paramètres de confidentialité et de sécurité](../../20220604-simplex-chat-new-privacy-security-settings.md)
|
||||
|
||||
24 mai 2022 [v2.1 : effacement des messages pour une meilleure confidentialité des conversations](./20220524-simplex-chat-better-privacy.md)
|
||||
24 mai 2022 [v2.1 : effacement des messages pour une meilleure confidentialité des conversations](../../20220524-simplex-chat-better-privacy.md)
|
||||
|
||||
11 mai 2022 [Publication de la v2.0 - envoi d'images et de fichiers dans les applications mobiles](./20220511-simplex-chat-v2-images-files.md)
|
||||
11 mai 2022 [Publication de la v2.0 - envoi d'images et de fichiers dans les applications mobiles](../../20220511-simplex-chat-v2-images-files.md)
|
||||
|
||||
04 avr. 2022 [Notifications instantanées pour les applications mobiles SimpleX Chat](./20220404-simplex-chat-instant-notifications.md)
|
||||
04 avr. 2022 [Notifications instantanées pour les applications mobiles SimpleX Chat](../../20220404-simplex-chat-instant-notifications.md)
|
||||
|
||||
08 mars 2022 [Applications mobiles pour iOS et Android](./20220308-simplex-chat-mobile-apps.md)
|
||||
08 mars 2022 [Applications mobiles pour iOS et Android](../../20220308-simplex-chat-mobile-apps.md)
|
||||
|
||||
14 févr. 2022. [SimpleX Chat : rejoignez notre version bêta publique pour iOS](./20220214-simplex-chat-ios-public-beta.md)
|
||||
14 févr. 2022. [SimpleX Chat : rejoignez notre version bêta publique pour iOS](../../20220214-simplex-chat-ios-public-beta.md)
|
||||
|
||||
12 janv. 2022. [SimpleX Chat v1 : la plateforme de chat et d'application la plus privée et la plus sécurisée](./20220112-simplex-chat-v1-released.md)
|
||||
12 janv. 2022. [SimpleX Chat v1 : la plateforme de chat et d'application la plus privée et la plus sécurisée](../../20220112-simplex-chat-v1-released.md)
|
||||
|
||||
08 déc. 2021. [Sortie de SimpleX Chat v0.5 : la première plateforme de chat 100% privée par définition - aucun accès à votre graphe de connexions](./20211208-simplex-chat-v0.5-released.md)
|
||||
08 déc. 2021. [Sortie de SimpleX Chat v0.5 : la première plateforme de chat 100% privée par définition - aucun accès à votre graphe de connexions](../../20211208-simplex-chat-v0.5-released.md)
|
||||
|
||||
14 septembre 2021. [SimpleX Chat v0.4 publié : chat open-source qui utilise un protocole de routage de messages préservant la confidentialité](./20210914-simplex-chat-v0.4-released.md)
|
||||
14 septembre 2021. [SimpleX Chat v0.4 publié : chat open-source qui utilise un protocole de routage de messages préservant la confidentialité](../../20210914-simplex-chat-v0.4-released.md)
|
||||
|
||||
12 mai 2021. [Prototype de chat SimpleX](./20210512-simplex-chat-terminal-ui.md)
|
||||
12 mai 2021. [Prototype de chat SimpleX](../../20210512-simplex-chat-terminal-ui.md)
|
||||
|
||||
22 oct. 2020. [SimpleX Chat](./20201022-simplex-chat.md)
|
||||
22 oct. 2020. [SimpleX Chat](../../20201022-simplex-chat.md)
|
||||
|
||||
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 4455b8bd0e243aa3bb4dc854037b2e64677963b0
|
||||
tag: 1116aeeea1869e0de38e9faccea76b329b549804
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -98,7 +98,7 @@ git checkout stable
|
||||
DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
|
||||
```
|
||||
|
||||
> **Please note:** If you encounter `` version `GLIBC_2.28' not found `` error, rebuild it with `haskell:8.10.7-stretch` base image (change it in your local [Dockerfile](Dockerfile)).
|
||||
> **Please note:** If you encounter `` version `GLIBC_2.28' not found `` error, rebuild it with `haskell:8.10.7-stretch` base image (change it in your local [Dockerfile](/Dockerfile)).
|
||||
|
||||
#### In any OS
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ revision: 23.04.2024
|
||||
|
||||
While great care is taken to ensure the highest level of security and privacy in SimpleX network servers and clients, all software can have flaws, and we believe it is a critical part of an organization's social responsibility to minimize the impact of these flaws through continual vulnerability discovery efforts, defense in depth design, and prompt remediation and notification.
|
||||
|
||||
The security assessment of SimpleX cryptography and networking was done by Trail of Bits in [November 2022](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html).
|
||||
The security assessment of SimpleX cryptography and networking was done by Trail of Bits in [November 2022](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
We are planning design review of SimpleX protocols in July 2024 and implementation review in December 2024/January 2025.
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ git checkout stable
|
||||
DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
|
||||
```
|
||||
|
||||
> **Upozornění:** Pokud narazíte na chybu `` verze `GLIBC_2.28' nenalezena ``, obnovte jej pomocí základního obrazu `haskell:8.10.7-stretch` (změňte jej ve svém lokálním [Dockerfile](Dockerfile)).
|
||||
> **Upozornění:** Pokud narazíte na chybu `` verze `GLIBC_2.28' nenalezena ``, obnovte jej pomocí základního obrazu `haskell:8.10.7-stretch` (změňte jej ve svém lokálním [Dockerfile](/Dockerfile)).
|
||||
|
||||
#### V libovolném operačním systému
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) s novými funkcemi o 1-2 týdny dříve - **omezeno na 10 000 uživatelů**!
|
||||
- 🖥 K dispozici jako terminálová (konzolová) [aplikace / CLI](#zap-quick-installation-of-a-terminal-app) v systémech Linux, MacOS, Windows.
|
||||
|
||||
**NOVINKA**: Bezpečnostní audit od [Trail of Bits](https://www.trailofbits.com/about), [nové webové stránky](https://simplex.chat) a vydána verze 4.2! [Viz oznámení](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
**NOVINKA**: Bezpečnostní audit od [Trail of Bits](https://www.trailofbits.com/about), [nové webové stránky](https://simplex.chat) a vydána verze 4.2! [Viz oznámení](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
## Obsah
|
||||
|
||||
@@ -62,23 +62,23 @@ Nestačí používat end-to-end šifrovaný messenger, všichni bychom měli pou
|
||||
|
||||
### Úplné soukromí vaší identity, profilu, kontaktů a metadat.
|
||||
|
||||
**Na rozdíl od všech ostatních existujících platforem pro zasílání zpráv nemá SimpleX přiřazeny žádné identifikátory uživatelů** - dokonce ani náhodná čísla. To chrání soukromí toho, s kým komunikujete, a skrývá to před servery platformy SimpleX i před jakýmikoli pozorovateli. [Více informací](./docs/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata).
|
||||
**Na rozdíl od všech ostatních existujících platforem pro zasílání zpráv nemá SimpleX přiřazeny žádné identifikátory uživatelů** - dokonce ani náhodná čísla. To chrání soukromí toho, s kým komunikujete, a skrývá to před servery platformy SimpleX i před jakýmikoli pozorovateli. [Více informací](./SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata).
|
||||
|
||||
### Nejlepší ochrana proti spamu a zneužití
|
||||
|
||||
Protože na platformě SimpleX nemáte žádný identifikátor, nelze vás kontaktovat, pokud nesdílíte odkaz na jednorázovou pozvánku nebo volitelnou dočasnou uživatelskou adresu. [Více informací](./docs/SIMPLEX.md#nejlepší-ochrana-před-spamem-a-zneužitím).
|
||||
Protože na platformě SimpleX nemáte žádný identifikátor, nelze vás kontaktovat, pokud nesdílíte odkaz na jednorázovou pozvánku nebo volitelnou dočasnou uživatelskou adresu. [Více informací](./SIMPLEX.md#nejlepší-ochrana-proti-spamu-a-zneužití).
|
||||
|
||||
### Úplné vlastnictví, kontrola a zabezpečení vašich dat
|
||||
|
||||
SimpleX ukládá všechna uživatelská data na klientských zařízeních, zprávy jsou pouze dočasně uchovávány na relay serverech SimpleX, dokud nejsou přijaty. [Více informací](./docs/SIMPLEX.md#complete-ownership-control-and-security-of-your-data).
|
||||
SimpleX ukládá všechna uživatelská data na klientských zařízeních, zprávy jsou pouze dočasně uchovávány na relay serverech SimpleX, dokud nejsou přijaty. [Více informací](./SIMPLEX.md#complete-ownership-control-and-security-of-your-data).
|
||||
|
||||
### Uživatelé vlastní síť SimpleX
|
||||
|
||||
Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi, kteří používají servery předkonfigurované v aplikacích nebo jakékoli jiné servery SimpleX. [Více informací](./docs/SIMPLEX.md#users-own-simplex-network).
|
||||
Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi, kteří používají servery předkonfigurované v aplikacích nebo jakékoli jiné servery SimpleX. [Více informací](./SIMPLEX.md#users-own-simplex-network).
|
||||
|
||||
## Často kladené otázky
|
||||
|
||||
1. _Jak může SimpleX doručovat zprávy bez identifikátorů uživatelů?_ Viz [oznámení o vydání v2](./blog/20220511-simplex-chat-v2-images-files.md#prvni-platforma-zasilani-zpráv-bez-identifikátoru-uživatele), kde je vysvětleno, jak SimpleX funguje.
|
||||
1. _Jak může SimpleX doručovat zprávy bez identifikátorů uživatelů?_ Viz [oznámení o vydání v2](../../../blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers), kde je vysvětleno, jak SimpleX funguje.
|
||||
|
||||
2. _Proč bych neměl používat jen Signal?_ Signal je centralizovaná platforma, která k identifikaci svých uživatelů a jejich kontaktů používá telefonní čísla. To znamená, že zatímco obsah vašich zpráv na službě Signal je chráněn robustním šifrováním end-to-end, pro službu Signal je viditelné velké množství metadat - s kým a kdy hovoříte.
|
||||
|
||||
@@ -88,17 +88,17 @@ Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi,
|
||||
|
||||
Poslední aktualizace: V současné době je k dispozici několik nových aplikací, např:
|
||||
|
||||
[Vydání verze 4.5 - s více uživatelskými profily, návrhem zpráv, izolací transportu a italským rozhraním](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
|
||||
[Vydání verze 4.5 - s více uživatelskými profily, návrhem zpráv, izolací transportu a italským rozhraním](../../../blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
|
||||
|
||||
[03. 01. 2023. v4.4 vydána - s mizejícími zprávami, "živými" zprávami, bezpečnostním ověřováním spojení, GIFy a nálepkami a s francouzským jazykem rozhraní](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
|
||||
[03. 01. 2023. v4.4 vydána - s mizejícími zprávami, "živými" zprávami, bezpečnostním ověřováním spojení, GIFy a nálepkami a s francouzským jazykem rozhraní](../../../blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
|
||||
|
||||
[prosinec 06, 2022. Listopadové recenze a vydána verze 4.3 - s okamžitými hlasovými zprávami, nevratným mazáním odeslaných zpráv a vylepšenou konfigurací serveru](./blog/20221206-simplex-chat-v4.3-hlasove-zpravy.md).
|
||||
[prosinec 06, 2022. Listopadové recenze a vydána verze 4.3 - s okamžitými hlasovými zprávami, nevratným mazáním odeslaných zpráv a vylepšenou konfigurací serveru](../../../blog/20221206-simplex-chat-v4.3-voice-messages.md).
|
||||
|
||||
[Nov 08, 2022. Bezpečnostní audit Trail of Bits, vydány nové webové stránky a verze 4.2](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
[Nov 08, 2022. Bezpečnostní audit Trail of Bits, vydány nové webové stránky a verze 4.2](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
[28. 9. 2022. v4.0: šifrovaná lokální databáze chatu a mnoho dalších změn](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
[28. 9. 2022. v4.0: šifrovaná lokální databáze chatu a mnoho dalších změn](../../../blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
|
||||
[Všechny aktualizace](./blog)
|
||||
[Všechny aktualizace](../../../blog)
|
||||
|
||||
## Vytvoření soukromého připojení
|
||||
|
||||
@@ -118,13 +118,13 @@ Po instalaci chatovacího klienta jednoduše spusťte `simplex-chat` z terminál
|
||||
|
||||

|
||||
|
||||
Více informací o [instalaci a používání terminálové aplikace](./docs/CLI.md).
|
||||
Více informací o [instalaci a používání terminálové aplikace](./CLI.md).
|
||||
|
||||
## Návrh platformy SimpleX
|
||||
|
||||
SimpleX je síť klient-server s unikátní topologií sítě, která využívá redundantní, jednorázové uzly pro předávání zpráv (relay nodes) k asynchronnímu předávání zpráv prostřednictvím jednosměrných (simplexních) front zpráv, což zajišťuje anonymitu příjemce i odesílatele.
|
||||
|
||||
Na rozdíl od sítí P2P jsou všechny zprávy předávány přes jeden nebo několik serverových uzlů, které ani nemusí mít perzistenci. Současná implementace [SMP serveru](https://github.com/simplex-chat/simplexmq#smp-server) ve skutečnosti používá ukládání zpráv v paměti a uchovává pouze záznamy o frontách. SimpleX poskytuje lepší ochranu metadat než návrhy P2P, protože k doručování zpráv se nepoužívají globální identifikátory účastníků, a vyhýbá se [problémům sítí P2P](./docs/SIMPLEX.md#comparison-with-p2p-messaging-protocols).
|
||||
Na rozdíl od sítí P2P jsou všechny zprávy předávány přes jeden nebo několik serverových uzlů, které ani nemusí mít perzistenci. Současná implementace [SMP serveru](https://github.com/simplex-chat/simplexmq#smp-server) ve skutečnosti používá ukládání zpráv v paměti a uchovává pouze záznamy o frontách. SimpleX poskytuje lepší ochranu metadat než návrhy P2P, protože k doručování zpráv se nepoužívají globální identifikátory účastníků, a vyhýbá se [problémům sítí P2P](./SIMPLEX.md#comparison-with-p2p-messaging-protocols).
|
||||
|
||||
Na rozdíl od federativních sítí nemají uzly serveru **záznamy o uživatelích**, **nekomunikují mezi sebou** a **neukládají zprávy** po jejich doručení příjemcům. Neexistuje způsob, jak zjistit úplný seznam serverů účastnících se sítě SimpleX. Tato konstrukce se vyhýbá problému viditelnosti metadat, který mají všechny federované sítě, a lépe chrání před útoky na celou síť.
|
||||
|
||||
@@ -132,7 +132,7 @@ Informace o uživatelích, jejich kontaktech a skupinách mají pouze klientská
|
||||
|
||||
Další informace o cílech a technickém návrhu platformy naleznete v dokumentu [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md).
|
||||
|
||||
Formát zpráv zasílaných mezi klienty chatu prostřednictvím [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md) viz [SimpleX Chat Protocol](./docs/protocol/simplex-chat.md).
|
||||
Formát zpráv zasílaných mezi klienty chatu prostřednictvím [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md) viz [SimpleX Chat Protocol](../../protocol/simplex-chat.md).
|
||||
|
||||
## Soukromí: technické podrobnosti a omezení
|
||||
|
||||
@@ -149,7 +149,7 @@ Co je již implementováno:
|
||||
6. Počínaje verzí v2 protokolu SMP (současná verze je v4) jsou všechna metadata zprávy včetně času, kdy byla zpráva přijata serverem (zaokrouhleno na sekundy), odesílána příjemcům uvnitř šifrované obálky, takže ani v případě kompromitace TLS je nelze pozorovat.
|
||||
7. Pro spojení klient-server je povoleno pouze TLS 1.2/1.3, omezené na kryptografické algoritmy: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
|
||||
8. Na ochranu proti útokům typu replay vyžadují servery SimpleX [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako ID relace v každém klientském příkazu podepsaném efemérním klíčem per-queue.
|
||||
9. Pro ochranu vaší IP adresy podporují všichni klienti SimpleX Chat přístup k serverům pro zasílání zpráv přes Tor - více informací najdete v [oznámení o vydání v3.1](./blog/20220808-simplex-chat-v3.1-chat-groups.md).
|
||||
9. Pro ochranu vaší IP adresy podporují všichni klienti SimpleX Chat přístup k serverům pro zasílání zpráv přes Tor - více informací najdete v [oznámení o vydání v3.1](../../../blog/20220808-simplex-chat-v3.1-chat-groups.md).
|
||||
10. Šifrování místní databáze s přístupovou frází - kontakty, skupiny a všechny odeslané a přijaté zprávy jsou uloženy šifrovaně. Pokud jste používali SimpleX Chat před verzí 4.0, musíte šifrování povolit prostřednictvím nastavení aplikace.
|
||||
11. Izolace transportu - pro provoz různých uživatelských profilů se používají různá spojení TCP a okruhy Tor, volitelně - pro různá spojení kontaktů a členů skupin.
|
||||
|
||||
@@ -166,7 +166,7 @@ Můžete:
|
||||
- použít knihovnu SimpleX Chat k integraci funkcí chatu do svých mobilních aplikací.
|
||||
- vytvářet chatovací boty a služby v jazyce Haskell - viz [simple](./apps/simplex-bot/) a více [advanced chat bot example](./apps/simplex-bot-advanced/).
|
||||
- vytvářet chatovací boty a služby v libovolném jazyce se spuštěným terminálem SimpleX Chat CLI jako lokálním serverem WebSocket. Viz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) a [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
|
||||
- spustit [simplex-chat terminal CLI](./docs/CLI.md) pro provádění jednotlivých příkazů chatu, např. pro odesílání zpráv v rámci provádění shellových skriptů.
|
||||
- spustit [simplex-chat terminal CLI](./CLI.md) pro provádění jednotlivých příkazů chatu, např. pro odesílání zpráv v rámci provádění shellových skriptů.
|
||||
|
||||
Pokud uvažujete o vývoji s platformou SimpleX, obraťte se na nás pro případné rady a podporu.
|
||||
|
||||
@@ -253,7 +253,7 @@ Aktuální jazyky rozhraní:
|
||||
- Italština: [@unbranched](https://github.com/unbranched)
|
||||
- Ruština: projektový tým
|
||||
|
||||
Jazyky ve vývoji: Čínština, hindština, čeština, japonština, holandština a [mnoho dalších](https://hosted.weblate.org/projects/simplex-chat/#languages). Další jazyky budeme přidávat, jakmile budou některé z již přidaných jazyků dokončeny - navrhněte prosím nové jazyky, projděte si [průvodce překladem](./docs/TRANSLATIONS.md) a kontaktujte nás!
|
||||
Jazyky ve vývoji: Čínština, hindština, čeština, japonština, holandština a [mnoho dalších](https://hosted.weblate.org/projects/simplex-chat/#languages). Další jazyky budeme přidávat, jakmile budou některé z již přidaných jazyků dokončeny - navrhněte prosím nové jazyky, projděte si [průvodce překladem](./TRANSLATIONS.md) a kontaktujte nás!
|
||||
|
||||
## Přispívejte
|
||||
|
||||
@@ -294,7 +294,7 @@ Zakladatel SimpleX Chat
|
||||
|
||||
Protokoly a bezpečnostní model [SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) byly revidovány a ve verzi 1.0.0 došlo k mnoha zlomovým změnám a vylepšením.
|
||||
|
||||
Bezpečnostní audit provedla v říjnu 2022 společnost [Trail of Bits](https://www.trailofbits.com/about) a většina oprav byla vydána ve verzi 4.2.0 - viz [oznámení](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
Bezpečnostní audit provedla v říjnu 2022 společnost [Trail of Bits](https://www.trailofbits.com/about) a většina oprav byla vydána ve verzi 4.2.0 - viz [oznámení](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
SimpleX Chat je stále relativně ranou fází platformy (mobilní aplikace byly vydány v březnu 2022), takže můžete objevit některé chyby a chybějící funkce. Velmi oceníme, pokud nám dáte vědět o všem, co je třeba opravit nebo vylepšit.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ SMP server je relay server používaný k předávání zpráv v síti SimpleX.
|
||||
|
||||
Klienti SimpleX pouze určují, který server bude použit pro příjem zpráv, a to pro každý kontakt (nebo spojení skupiny s členem skupiny) zvlášť, přičemž tyto servery jsou pouze dočasné, protože adresa pro doručování se může změnit.
|
||||
|
||||
_Upozornění_: když změníte servery v konfiguraci aplikace, ovlivní to pouze to, který server bude použit pro nové kontakty, stávající kontakty se na nové servery automaticky nepřesunou, ale můžete je přesunout ručně pomocí tlačítka ["Změnit adresu příjmu"](../blog/20221108-simplex-chat-v4.2-bezpecnostni-audit-novy-website.md#zmeny-dorucovani-adresy-beta) na stránkách s informacemi o kontaktech/členech - brzy bude automatizováno.
|
||||
_Upozornění_: když změníte servery v konfiguraci aplikace, ovlivní to pouze to, který server bude použit pro nové kontakty, stávající kontakty se na nové servery automaticky nepřesunou, ale můžete je přesunout ručně pomocí tlačítka ["Změnit adresu příjmu"](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stránkách s informacemi o kontaktech/členech - brzy bude automatizováno.
|
||||
|
||||
## Instalace
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ git checkout stable
|
||||
DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
|
||||
```
|
||||
|
||||
> **Veuillez noter** : Si vous rencontrez l'erreur ``version `GLIBC_2.28' non trouvée``, reconstruisez-le avec l'image de base `haskell:8.10.7-stretch`(changez-la dans votre [Dockerfile](Dockerfile) local).
|
||||
> **Veuillez noter** : Si vous rencontrez l'erreur ``version `GLIBC_2.28' non trouvée``, reconstruisez-le avec l'image de base `haskell:8.10.7-stretch`(changez-la dans votre [Dockerfile](/Dockerfile) local).
|
||||
|
||||
#### Utiliser Haskell stack
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ git checkout stable
|
||||
DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
|
||||
```
|
||||
|
||||
> **Uwaga:** Jeśli napotkasz błąd `` version `GLIBC_2.28' not found ``, przebuduj go z obrazem bazowym `haskell:8.10.7-stretch` (zmień go w Twoim lokalnym pliku [Dockerfile](Dockerfile)).
|
||||
> **Uwaga:** Jeśli napotkasz błąd `` version `GLIBC_2.28' not found ``, przebuduj go z obrazem bazowym `haskell:8.10.7-stretch` (zmień go w Twoim lokalnym pliku [Dockerfile](/Dockerfile)).
|
||||
|
||||
#### Używając Haskella na dowolnym systemie operacyjnym
|
||||
|
||||
@@ -200,7 +200,7 @@ Po uruchomieniu czatu zostaniesz poproszony o podanie swojej "nazwy wyświetlane
|
||||
Poniższy schemat przedstawia sposób łączenia się z kontaktem i wysyłania do niego wiadomości:
|
||||
|
||||
<div align="center">
|
||||
<img align="center" src="../images/how-to-use-simplex.svg">
|
||||
<img align="center" src="/images/how-to-use-simplex.svg">
|
||||
</div>
|
||||
|
||||
Gdy już skonfigurujesz swój profil lokalny, wpisz `/c` (oznaczające `/connect`), aby utworzyć nowe połączenie i wygenerować zaproszenie. Wyślij to zaproszenie do swojego kontaktu za pośrednictwem dowolnego innego kanału komunikacji.
|
||||
@@ -219,7 +219,7 @@ Użyj `/help` na czacie, by uzyskać listę pozostałych dostępnych komend.
|
||||
|
||||
Aby utworzyć grupę, użyj `/g <nazwa_grupy>`, a następnie dodaj do niej kontakty za pomocą `/a <nazwa_grupy> <nazwa_kontaktu>`. Możesz wysyłać wiadomości do grupy wpisując `#<nazwa_grupy> <wiadomość>`. Użyj `/help groups`, by uzyskać listę pozostałych dostępnych komend.
|
||||
|
||||

|
||||

|
||||
|
||||
> **Uwaga**: informacje o grupach nie są przechowywane na żadnym serwerze, są one zapisywane jako lista członków w bazie danych aplikacji klientów, do których będą wysyłane wiadomości.
|
||||
|
||||
@@ -227,7 +227,7 @@ Aby utworzyć grupę, użyj `/g <nazwa_grupy>`, a następnie dodaj do niej konta
|
||||
|
||||
Możesz wysłać plik do kontaktu za pomocą `/f @<nazwa_kontaktu> <ścieżka_do_pliku>` - odbiorca będzie musiał go zaakceptować przed rozpoczęciem wysyłania. Użyj `/help files`, by uzyskać listę pozostałych dostępnych komend.
|
||||
|
||||

|
||||

|
||||
|
||||
Możesz wysyłać pliki do grupy za pomocą `/f #<nazwa_grupy> <ścieżka_do_pliku>`.
|
||||
|
||||
@@ -241,4 +241,4 @@ Prośby o kontakt możesz przyjąć za pomocą komendy `/ac <nazwa>` oraz odrzuc
|
||||
|
||||
Użyj `/help address`, by uzyskać listę pozostałych dostępnych komend.
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -50,7 +50,7 @@ Możesz połączyć się z naszym zespołem za pośrednictwem aplikacji, korzyst
|
||||
|
||||
Odpowiadamy na pytania manualnie, więc nie jest to natychmiastowe - może to potrwać do 24 godzin.
|
||||
|
||||
Jeśli jesteś zainteresowany pomocą w integracji otwartoźródłowych modeli językowych i [dołączeniem do naszego zespołu](./docs/lang/pl/JOIN_TEAM.md), skontaktuj się z nami.
|
||||
Jeśli jesteś zainteresowany pomocą w integracji otwartoźródłowych modeli językowych i [dołączeniem do naszego zespołu](../../JOIN_TEAM.md), skontaktuj się z nami.
|
||||
|
||||
## Dołącz do grup użytkowników
|
||||
|
||||
@@ -62,7 +62,7 @@ Możesz również:
|
||||
- krytykować aplikację i dokonywać porównań z innymi komunikatorami.
|
||||
- udostępniać nowe komunikatory, które Twoim zdaniem mogą być interesujące z punktu widzenia prywatności, o ile nie spamujesz.
|
||||
- udostępniać niektóre publikacje związane z prywatnością, raczej dość rzadko.
|
||||
- po wstępnym zatwierdzeniu przez administratora w prywatnej wiadomości, udostępnić link do utworzonej grupy, ale tylko raz. Gdy grupa ma więcej niż 10 członków, może zostać przesłana do [SimpleX Directory Service](./docs/DIRECTORY.md), gdzie nowi użytkownicy będą mogli ją odkryć.
|
||||
- po wstępnym zatwierdzeniu przez administratora w prywatnej wiadomości, udostępnić link do utworzonej grupy, ale tylko raz. Gdy grupa ma więcej niż 10 członków, może zostać przesłana do [SimpleX Directory Service](../../DIRECTORY.md), gdzie nowi użytkownicy będą mogli ją odkryć.
|
||||
|
||||
Musisz:
|
||||
- być uprzejmym wobec innych użytkowników.
|
||||
@@ -104,11 +104,11 @@ Kanał, za pośrednictwem którego udostępniasz link, nie musi być bezpieczny
|
||||
|
||||
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Wykonaj prywatne połączenie" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Połączenie wideo" height="360">
|
||||
|
||||
Po wykonaniu połączenia możesz [zweryfikować kod bezpieczeństwa połączenia](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification).
|
||||
Po wykonaniu połączenia możesz [zweryfikować kod bezpieczeństwa połączenia](../../../blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification).
|
||||
|
||||
## Poradnik dla użytkownika (NOWE)
|
||||
|
||||
Przeczytaj o funkcjach i ustawieniach aplikacji w nowym [Przewodniku użytkownika](./docs/guide/README.md).
|
||||
Przeczytaj o funkcjach i ustawieniach aplikacji w nowym [Przewodniku użytkownika](../../guide/README.md).
|
||||
|
||||
## Pomóż nam przetłumaczyć SimpleX Chat
|
||||
|
||||
@@ -139,13 +139,13 @@ Dołącz do naszych tłumaczy, aby pomóc SimpleX w rozwoju!
|
||||
|🇺🇦 uk|Українська| |[](https://hosted.weblate.org/projects/simplex-chat/android/uk/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[](https://hosted.weblate.org/projects/simplex-chat/website/uk/)||
|
||||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br> |<br><br>[](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
|
||||
|
||||
Trwają prace nad wersjami językowymi: Arabski, japoński, koreański, portugalski i [inne](https://hosted.weblate.org/projects/simplex-chat/#languages). Będziemy dodawać kolejne języki, gdy niektóre z już dodanych zostaną ukończone - zasugeruj nowe języki, przejrzyj [przewodnik po tłumaczeniach](./docs/lang/pl/TRANSLATIONS.md) i skontaktuj się z nami!
|
||||
Trwają prace nad wersjami językowymi: Arabski, japoński, koreański, portugalski i [inne](https://hosted.weblate.org/projects/simplex-chat/#languages). Będziemy dodawać kolejne języki, gdy niektóre z już dodanych zostaną ukończone - zasugeruj nowe języki, przejrzyj [przewodnik po tłumaczeniach](./TRANSLATIONS.md) i skontaktuj się z nami!
|
||||
|
||||
## Kontrybuuj
|
||||
|
||||
Chcielibyśmy, abyś przyczynił się do naszego rozwoju! Możesz nam pomóc:
|
||||
|
||||
- [dzieląc się motywem kolorystycznym](./docs/THEMES.md), którego używasz w aplikacji na Androida!
|
||||
- [dzieląc się motywem kolorystycznym](../../THEMES.md), którego używasz w aplikacji na Androida!
|
||||
- pisząc samouczki lub poradniki, które dotyczą hostowania serwerów, automatyzacji czatbotów itp.
|
||||
- współtworząc bazy wiedzy SimpleX Chat.
|
||||
- rozwijając funkcje - skontaktuj się z nami za pośrednictwem czatu, abyśmy mogli pomóc Ci zacząć.
|
||||
@@ -208,23 +208,23 @@ Używanie szyfrowanego komunikatora end-to-end nie jest wystarczające. Powinni
|
||||
|
||||
### Kompletna prywatność Twojej tożsamości, profilu, kontaktów i metadanych.
|
||||
|
||||
**W przeciwieństwie do innych komunikatorów, SimpleX nie posiada żadnych identyfikatorów przypisanych do użytkowników**. Nie posiada nawet numerów generowanych losowo. Zapewnia to prywatność tego, z kim się komunikujesz, ukrywając jego tożsamość oraz fakt komunikacji przed serwerami platformy SimpleX i wszelkimi obserwatorami [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata).
|
||||
**W przeciwieństwie do innych komunikatorów, SimpleX nie posiada żadnych identyfikatorów przypisanych do użytkowników**. Nie posiada nawet numerów generowanych losowo. Zapewnia to prywatność tego, z kim się komunikujesz, ukrywając jego tożsamość oraz fakt komunikacji przed serwerami platformy SimpleX i wszelkimi obserwatorami [Czytaj więcej](./SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata).
|
||||
|
||||
### Najlepsza ochrona przed spamem i nadużyciami
|
||||
|
||||
Ponieważ na platformie SimpleX nie masz identyfikatora ani stałego adresu, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#the-best-protection-against-spam-and-abuse).
|
||||
Ponieważ na platformie SimpleX nie masz identyfikatora ani stałego adresu, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku. [Czytaj więcej](./SIMPLEX.md#the-best-protection-against-spam-and-abuse).
|
||||
|
||||
### Pełna kontrola i bezpieczeństwo Twoich danych
|
||||
|
||||
SimpleX przechowuje wszystkie dane użytkownika na urządzeniach klienckich, wiadomości są przechowywane tymczasowo na serwerach przekaźnikowych SimpleX do momentu ich odebrania, po czym są trwale usuwane. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#complete-ownership-control-and-security-of-your-data).
|
||||
SimpleX przechowuje wszystkie dane użytkownika na urządzeniach klienckich, wiadomości są przechowywane tymczasowo na serwerach przekaźnikowych SimpleX do momentu ich odebrania, po czym są trwale usuwane. [Czytaj więcej](./SIMPLEX.md#complete-ownership-control-and-security-of-your-data).
|
||||
|
||||
### Użytkownicy są właścicielami sieci SimpleX
|
||||
|
||||
Możesz używać SimpleX na własnych serwerach i nadal komunikować się z ludźmi za pomocą serwerów, które są wstępnie skonfigurowane w aplikacjach lub z dowolnymi innymi serwerami SimpleX. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#users-own-simplex-network).
|
||||
Możesz używać SimpleX na własnych serwerach i nadal komunikować się z ludźmi za pomocą serwerów, które są wstępnie skonfigurowane w aplikacjach lub z dowolnymi innymi serwerami SimpleX. [Czytaj więcej](./SIMPLEX.md#users-own-simplex-network).
|
||||
|
||||
## Często zadawane pytania
|
||||
|
||||
1. _W jaki sposób SimpleX może dostarczać wiadomości bez jakichkolwiek identyfikatorów użytkownika?_ Zobacz [ogłoszenie wydania v2](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) wyjaśniające jak SimpleX działa.
|
||||
1. _W jaki sposób SimpleX może dostarczać wiadomości bez jakichkolwiek identyfikatorów użytkownika?_ Zobacz [ogłoszenie wydania v2](../../../blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) wyjaśniające jak SimpleX działa.
|
||||
|
||||
2. _Dlaczego po prostu nie mogę używać Signal?_ Signal to scentralizowana platforma, która wykorzystuje numery telefonów do identyfikacji użytkowników i ich kontaktów. Oznacza to, że podczas gdy treść wiadomości w Signal jest chroniona solidnym szyfrowaniem end-to-end, istnieje duża ilość metadanych widocznych dla Signal - to, z kim rozmawiasz i kiedy.
|
||||
|
||||
@@ -234,29 +234,29 @@ Możesz używać SimpleX na własnych serwerach i nadal komunikować się z lud
|
||||
|
||||
Najnowsze i ważne wiadomości:
|
||||
|
||||
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
|
||||
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](../../../blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
|
||||
|
||||
[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
|
||||
[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](../../../blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
|
||||
|
||||
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
||||
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](../../../blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
||||
|
||||
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
|
||||
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](../../../blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
|
||||
|
||||
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
|
||||
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](../../../blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
|
||||
|
||||
[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
|
||||
[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](../../../blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
|
||||
|
||||
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
|
||||
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](../../../blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
|
||||
|
||||
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
|
||||
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](../../../blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
|
||||
|
||||
[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md).
|
||||
[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](../../../blog/20230301-simplex-file-transfer-protocol.md).
|
||||
|
||||
[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](../../../blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
|
||||
[All updates](./blog)
|
||||
[All updates](../../../blog)
|
||||
|
||||
## :zap: Szybka instalacja terminalowej wersji aplikacji
|
||||
|
||||
@@ -268,13 +268,13 @@ Po pobraniu klienta czatu można go uruchomić za pomocą polecenia `simplex-cha
|
||||
|
||||

|
||||
|
||||
Przeczytaj więcej o [instalowaniu i używaniu terminalowej wersji czatu](./docs/lang/pl/CLI.md).
|
||||
Przeczytaj więcej o [instalowaniu i używaniu terminalowej wersji czatu](./CLI.md).
|
||||
|
||||
## Budowa Platformy SimpleX
|
||||
|
||||
SimpleX to sieć typu klient-serwer z unikatową topologią sieciową, która wykorzystuje redundantne, jednorazowe węzły przekazywania wiadomości do asynchronicznego przekazywania wiadomości za pośrednictwem jednokierunkowych (simpleksowych) kolejek wiadomości, zapewniając anonimowość odbiorcy i nadawcy.
|
||||
|
||||
W przeciwieństwie do sieci P2P, wszystkie wiadomości są przekazywane przez jeden lub kilka węzłów serwera, które nawet nie muszą być trwałe. Obecna implementacja [serwera SMP](https://github.com/simplex-chat/simplexmq#smp-server) wykorzystuje przechowywanie wiadomości w pamięci, utrzymując jedynie rejestr kolejki. SimpleX zapewnia lepszą ochronę metadanych niż projekty P2P, ponieważ żadne globalne identyfikatory uczestników nie są używane do dostarczania wiadomości i pozwala to uniknąć [różnych problemów związanych z sieciami P2P](./docs/lang/pl/SIMPLEX.md#comparison-with-p2p-messaging-protocols).
|
||||
W przeciwieństwie do sieci P2P, wszystkie wiadomości są przekazywane przez jeden lub kilka węzłów serwera, które nawet nie muszą być trwałe. Obecna implementacja [serwera SMP](https://github.com/simplex-chat/simplexmq#smp-server) wykorzystuje przechowywanie wiadomości w pamięci, utrzymując jedynie rejestr kolejki. SimpleX zapewnia lepszą ochronę metadanych niż projekty P2P, ponieważ żadne globalne identyfikatory uczestników nie są używane do dostarczania wiadomości i pozwala to uniknąć [różnych problemów związanych z sieciami P2P](./SIMPLEX.md#comparison-with-p2p-messaging-protocols).
|
||||
|
||||
W przeciwieństwie do sieci sfederowanych, węzły serwera **nie posiadają danych użytkowników**, **nie komunikują się ze sobą** i **nie przechowują wiadomości** po ich dostarczeniu do odbiorców. Nie ma możliwości na odkrycie pełnej listy serwerów działających w sieci SimpleX. Taka konstrukcja pozwala uniknąć problemu związanego z widocznością metadanych, z którym borykają się wszystkie sieci sfederowane i pozwala ona na lepszą ochronę przed atakami obejmującymi całą sieć.
|
||||
|
||||
@@ -282,29 +282,29 @@ Informacje o użytkownikach, ich kontaktach i grupach znajdują się wyłącznie
|
||||
|
||||
Przeczytaj [whitepaper SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md) po więcej informacji o zadaniach platformy oraz by dowiedzieć się jak wygląda koncepcja techniczna modelu.
|
||||
|
||||
Zobacz [Protokół Czatu SimpleX](./docs/protocol/simplex-chat.md) by dowiedzieć się o formacie wiadomości wysyłanych między klientem czatu za pośrednictwem [Protokołu Wiadomości SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md).
|
||||
Zobacz [Protokół Czatu SimpleX](../../protocol/simplex-chat.md) by dowiedzieć się o formacie wiadomości wysyłanych między klientem czatu za pośrednictwem [Protokołu Wiadomości SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md).
|
||||
|
||||
## Prywatność i bezpieczeństwo: szczegóły techniczne i ograniczenia
|
||||
|
||||
Prace nad SimpleX Chat wciąż trwają - udostępniamy nowe ulepszenia, gdy tylko będą gotowe. To Ty musisz zdecydować, czy obecny stan jest wystarczająco dobry dla Twojego przypadku zastosowania.
|
||||
|
||||
Stworzyliśmy [słownik pojęć](./docs/GLOSSARY.md) używany do opisu systemów komunikacyjnych, aby pomóc zrozumieć niektóre z poniższych pojęć oraz aby pomóc Ci w porównaniu zalet i wad różnych systemów komunikacyjnych.
|
||||
Stworzyliśmy [słownik pojęć](../../GLOSSARY.md) używany do opisu systemów komunikacyjnych, aby pomóc zrozumieć niektóre z poniższych pojęć oraz aby pomóc Ci w porównaniu zalet i wad różnych systemów komunikacyjnych.
|
||||
|
||||
Co zostało już wprowadzone:
|
||||
|
||||
1. Zamiast identyfikatorów użytkownika używanych przez wszystkie inne platformy, nawet te najbardziej prywatne, SimpleX używa [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 adresy dla każdej jednokierunkowej kolejki wiadomości, z opcjonalnym trzecim adresem dla powiadomień push na iOS, 2 kolejki w każdym połączeniu między użytkownikami). Sprawia to, że trudniej jest w ten sposób obserwować przebieg połączeń sieciowych na poziomie aplikacji, ponieważ dla `n` użytkowników może istnieć do `n * (n-1)` kolejek wiadomości.
|
||||
2. [Szyfrowanie end-to-end](./docs/GLOSSARY.md#end-to-end-encryption) w każdej kolejce wiadomości używając [cryptoboxa NaCl](https://nacl.cr.yp.to/box.html). Zostało to dodane, aby umożliwić redundancję w przyszłości (przekazywanie każdej wiadomości przez kilka serwerów), aby uniknąć posiadania tego samego ciphertext w różnych kolejkach (które byłyby widoczne tylko dla atakującego, w przypadku przejęcia TLS). Klucze szyfrujące używane do tego szyfrowania nie są rotowane, zamiast tego planujemy rotować kolejki. Do negocjacji kluczy używane są klucze Curve25519.
|
||||
3. Szyfrowanie end-to-end [double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) w każdej rozmowie między dwoma użytkownikami (lub członkami grupy). Jest to ten sam algorytm, który jest używany w Signal i wielu innych komunikatorach; zapewnia on komunikację OTR z [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (każda wiadomość jest szyfrowana własnym kluczem efemerycznym) i [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (klucze są często renegocjowane w ramach wymiany wiadomości). Dwie pary kluczy Curve448 są używane do początkowego [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), strona inicjująca przekazuje te klucze przez link połączenia, a strona akceptująca - w nagłówku wiadomości potwierdzającej.
|
||||
1. Zamiast identyfikatorów użytkownika używanych przez wszystkie inne platformy, nawet te najbardziej prywatne, SimpleX używa [pairwise per-queue identifiers](../../GLOSSARY.md#pairwise-pseudonymous-identifier) (2 adresy dla każdej jednokierunkowej kolejki wiadomości, z opcjonalnym trzecim adresem dla powiadomień push na iOS, 2 kolejki w każdym połączeniu między użytkownikami). Sprawia to, że trudniej jest w ten sposób obserwować przebieg połączeń sieciowych na poziomie aplikacji, ponieważ dla `n` użytkowników może istnieć do `n * (n-1)` kolejek wiadomości.
|
||||
2. [Szyfrowanie end-to-end](../../GLOSSARY.md#end-to-end-encryption) w każdej kolejce wiadomości używając [cryptoboxa NaCl](https://nacl.cr.yp.to/box.html). Zostało to dodane, aby umożliwić redundancję w przyszłości (przekazywanie każdej wiadomości przez kilka serwerów), aby uniknąć posiadania tego samego ciphertext w różnych kolejkach (które byłyby widoczne tylko dla atakującego, w przypadku przejęcia TLS). Klucze szyfrujące używane do tego szyfrowania nie są rotowane, zamiast tego planujemy rotować kolejki. Do negocjacji kluczy używane są klucze Curve25519.
|
||||
3. Szyfrowanie end-to-end [double ratchet](../../GLOSSARY.md#double-ratchet-algorithm) w każdej rozmowie między dwoma użytkownikami (lub członkami grupy). Jest to ten sam algorytm, który jest używany w Signal i wielu innych komunikatorach; zapewnia on komunikację OTR z [forward secrecy](../../GLOSSARY.md#forward-secrecy) (każda wiadomość jest szyfrowana własnym kluczem efemerycznym) i [break-in recovery](../../GLOSSARY.md#post-compromise-security) (klucze są często renegocjowane w ramach wymiany wiadomości). Dwie pary kluczy Curve448 są używane do początkowego [key agreement](../../GLOSSARY.md#key-agreement-protocol), strona inicjująca przekazuje te klucze przez link połączenia, a strona akceptująca - w nagłówku wiadomości potwierdzającej.
|
||||
4. Dodatkowa warstwa szyfrowania przy użyciu NaCL cryptobox dla wiadomości dostarczanych z serwera do odbiorcy. Warstwa ta pozwala uniknąć wspólnego szyfrogramu między wysyłanym i odbieranym ruchem serwera wewnątrz TLS (i nie ma też wspólnych identyfikatorów).
|
||||
5. Kilka poziomów [content padding](./docs/GLOSSARY.md#message-padding) w celu utrudnienia ataków na rozmiar wiadomości.
|
||||
5. Kilka poziomów [content padding](../../GLOSSARY.md#message-padding) w celu utrudnienia ataków na rozmiar wiadomości.
|
||||
6. Wszystkie metadane wiadomości, w tym czas odebrania wiadomości przez serwer (zaokrąglony do sekundy), są wysyłane do odbiorców w zaszyfrowanej postaci, więc nawet jeśli TLS zostanie przejęty, nie można ich zobaczyć.
|
||||
7. Dozwolone są tylko TLS 1.2/1.3 dla połączeń klient-serwer, z ograniczeniem do algorytmów kryptograficznych: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
|
||||
8. Aby zapobiec atakom typu replay, serwery SimpleX wymagają [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako identyfikatora sesji w każdym poleceniu klienta podpisanym kluczem efemerycznym dla każdej kolejki.
|
||||
9. Aby ochronić swój adres IP, wszystkie klienty SimpleX Chat obsługują dostęp do serwerów komunikacyjnych za pośrednictwem Tora - zobacz [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) po więcej szczegółów.
|
||||
9. Aby ochronić swój adres IP, wszystkie klienty SimpleX Chat obsługują dostęp do serwerów komunikacyjnych za pośrednictwem Tora - zobacz [v3.1 release announcement](../../../blog/20220808-simplex-chat-v3.1-chat-groups.md) po więcej szczegółów.
|
||||
10. Lokalne szyfrowanie bazy danych z hasłem - kontakty, grupy oraz wszystkie wysłane i odebrane wiadomości są przechowywane w postaci zaszyfrowanej. Jeśli korzystałeś z SimpleX Chat przed wersją v4.0, musisz włączyć szyfrowanie w ustawieniach aplikacji.
|
||||
11. Izolacja transportu - różne połączenia TCP i obwody Tor używane są dla ruchu różnych profili użytkowników, opcjonalnie - dla różnych kontaktów i połączeń członków grupy.
|
||||
12. Ręczne obracanie kolejki wiadomości w celu przeniesienia konwersacji do innego przekaźnika SMP.
|
||||
13. Wysyłanie zaszyfrowanych plików end-to-end przy użyciu [protokołu XFTP](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
|
||||
13. Wysyłanie zaszyfrowanych plików end-to-end przy użyciu [protokołu XFTP](../../../blog/20230301-simplex-file-transfer-protocol.md).
|
||||
14. Szyfrowanie plików lokalnych.
|
||||
|
||||
Planujemy dodać:
|
||||
@@ -322,7 +322,7 @@ Możesz:
|
||||
- korzystać z biblioteki SimpleX Chat w celu zintegrowania funkcji czatu z aplikacjami mobilnymi.
|
||||
- tworzyć boty i usługi czatu w języku Haskell - zobacz [prosty](./apps/simplex-bot/) i bardziej [zaawansowany przykład bota czatu](./apps/simplex-bot-advanced/).
|
||||
- tworzenie chat botów i usług w dowolnym języku z wykorzystaniem terminala CLI SimpleX Chat jako lokalnego serwera WebSocket. Zobacz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) i [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
|
||||
- uruchomić [simplex-chat w terminal ](./docs/lang/pl/CLI.md), aby wykonywać poszczególne polecenia czatu, np. wysyłać wiadomości w ramach wykonywania skryptu powłoki.
|
||||
- uruchomić [simplex-chat w terminal ](./CLI.md), aby wykonywać poszczególne polecenia czatu, np. wysyłać wiadomości w ramach wykonywania skryptu powłoki.
|
||||
|
||||
Jeśli chcesz rozwijać platformę SimpleX, skontaktuj się z nami, aby uzyskać porady i wsparcie.
|
||||
|
||||
@@ -365,7 +365,7 @@ Dołącz również do grupy [#simplex-devs](https://simplex.chat/contact#/?v=1-2
|
||||
- ✅ Ulepszone połączenia audio i wideo.
|
||||
- ✅ Obsługa starszego systemu operacyjnego Android i 32-bitowych procesorów.
|
||||
- ✅ Ukryte profile czatu.
|
||||
- ✅ Wysyłanie i odbieranie dużych plików przez [protokół XFTP](./blog/20230301-simplex-file-transfer-protocol.md).
|
||||
- ✅ Wysyłanie i odbieranie dużych plików przez [protokół XFTP](../../../blog/20230301-simplex-file-transfer-protocol.md).
|
||||
- ✅ Wiadomości wideo.
|
||||
- ✅ Kod dostępu do aplikacji.
|
||||
- ✅ Ulepszenie interfejsu Androidowej aplikacji.
|
||||
@@ -402,7 +402,7 @@ Dołącz również do grupy [#simplex-devs](https://simplex.chat/contact#/?v=1-2
|
||||
|
||||
[Protokoły i model bezpieczeństwa SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) zostały poddane przeglądowi i zawierały wiele istotnych zmian i ulepszeń w wersji v1.0.0.
|
||||
|
||||
Audyt bezpieczeństwa został przeprowadzony w październiku 2022 r. przez [Trail of Bits](https://www.trailofbits.com/about), a większość poprawek została wydana w wersji 4.2.0 - zobacz [ogłoszenie](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
Audyt bezpieczeństwa został przeprowadzony w październiku 2022 r. przez [Trail of Bits](https://www.trailofbits.com/about), a większość poprawek została wydana w wersji 4.2.0 - zobacz [ogłoszenie](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
SimpleX Chat jest nadal na stosunkowo wczesnym etapie rozwoju (aplikacje mobilne zostały wydane w marcu 2022 r.), więc możesz odkryć pewne błędy i brakujące funkcje. Będziemy bardzo wdzięczni za poinformowanie nas o wszystkim, co wymaga naprawy lub ulepszenia.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Serwer SMP to serwer przekaźnikowy używany do przekazywania wiadomości w siec
|
||||
|
||||
Klienty SimpleX określają tylko, który serwer jest używany do odbierania wiadomości, oddzielnie dla każdego kontaktu (lub połączenia grupowego z członkiem grupy), a serwery te są tylko tymczasowe, ponieważ adres dostawy może ulec zmianie.
|
||||
|
||||
_Uwaga_: gdy zmienisz serwery w ustawieniach aplikacji, wpłynie to tylko na to, który serwer będzie używany dla nowych kontaktów, istniejące kontakty nie zostaną automatycznie przeniesione na nowe serwery, ale możesz przenieść je ręcznie za pomocą przycisku ["Zmień adres odbiorczy"](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stronie z informacjami kontaktu/członka - wkrótce zostanie to zautomatyzowane.
|
||||
_Uwaga_: gdy zmienisz serwery w ustawieniach aplikacji, wpłynie to tylko na to, który serwer będzie używany dla nowych kontaktów, istniejące kontakty nie zostaną automatycznie przeniesione na nowe serwery, ale możesz przenieść je ręcznie za pomocą przycisku ["Zmień adres odbiorczy"](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stronie z informacjami kontaktu/członka - wkrótce zostanie to zautomatyzowane.
|
||||
|
||||
## Instalacja
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ revision: 07.02.2023
|
||||
Istniejące komunikatory oraz protokoły borykają się ze wszystkimi lub kilkoma podanymi problemami:
|
||||
|
||||
- Brak zachowania prywatności profilu i kontaktów użytkownika (zachowanie poufności metadanych).
|
||||
- Brak ochrony (lub jedynie opcjonalna ochrona) przed atakami MITM przez dostawcę usług przy użyciu szyfrowania [end to end](1)
|
||||
- Brak ochrony (lub jedynie opcjonalna ochrona) przed atakami MITM przez dostawcę usług przy użyciu szyfrowania [end to end][1]
|
||||
- Niechciane wiadomości (spam i nadużycia).
|
||||
- Brak własności danych i ich ochrony.
|
||||
- Dla nietechnicznych użytkowników używanie niescentralizowanych protokołów jest skomplikowane.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 5.7.3.0
|
||||
version: 5.8.0.1
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."4455b8bd0e243aa3bb4dc854037b2e64677963b0" = "11vmr7r9r611lcamf9ay34axw0yz402gif59bhpipjkn95bgjx0p";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."1116aeeea1869e0de38e9faccea76b329b549804" = "07ynn7f70hfsdrirmhb9zd257bx90d29l5gjyhh50wd12gaqdm0w";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
||||
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 5.7.3.0
|
||||
version: 5.8.0.1
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
@@ -143,6 +143,7 @@ library
|
||||
Simplex.Chat.Migrations.M20240402_item_forwarded
|
||||
Simplex.Chat.Migrations.M20240430_ui_theme
|
||||
Simplex.Chat.Migrations.M20240501_chat_deleted
|
||||
Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Mobile.File
|
||||
Simplex.Chat.Mobile.Shared
|
||||
|
||||
@@ -46,6 +46,7 @@ import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
|
||||
import Data.Ord (Down (..))
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
@@ -99,7 +100,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client (ProxyClientError (..), defaultNetworkConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
@@ -112,6 +113,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (TransportError (..))
|
||||
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
@@ -698,10 +700,7 @@ processChatCommand' vr = \case
|
||||
(,) <$> getAChatItem db vr user chatRef itemId <*> liftIO (getChatItemVersions db itemId)
|
||||
let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions
|
||||
memberDeliveryStatuses <- case (cType, dir) of
|
||||
(SCTGroup, SMDSnd) -> do
|
||||
withStore' (`getGroupSndStatuses` itemId) >>= \case
|
||||
[] -> pure Nothing
|
||||
memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses
|
||||
(SCTGroup, SMDSnd) -> L.nonEmpty <$> withStore' (`getGroupSndStatuses` itemId)
|
||||
_ -> pure Nothing
|
||||
forwardedFromChatItem <- getForwardedFromItem user ci
|
||||
pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem}
|
||||
@@ -871,15 +870,15 @@ processChatCommand' vr = \case
|
||||
throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed")
|
||||
when (add && length rs >= maxMsgReactions) $
|
||||
throwChatError (CECommandError "too many reactions")
|
||||
APIForwardChatItem (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemId -> withUser $ \user -> case toCType of
|
||||
APIForwardChatItem (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemId itemTTL -> withUser $ \user -> case toCType of
|
||||
CTDirect -> do
|
||||
(cm, ciff) <- prepareForward user
|
||||
withContactLock "forwardChatItem, to contact" toChatId $
|
||||
sendContactContentMessage user toChatId False Nothing cm ciff
|
||||
sendContactContentMessage user toChatId False itemTTL cm ciff
|
||||
CTGroup -> do
|
||||
(cm, ciff) <- prepareForward user
|
||||
withGroupLock "forwardChatItem, to group" toChatId $
|
||||
sendGroupContentMessage user toChatId False Nothing cm ciff
|
||||
sendGroupContentMessage user toChatId False itemTTL cm ciff
|
||||
CTLocal -> do
|
||||
(cm, ciff) <- prepareForward user
|
||||
createNoteFolderContentItem user toChatId cm ciff
|
||||
@@ -1022,12 +1021,12 @@ processChatCommand' vr = \case
|
||||
liftIO $ updateNoteFolderUnreadChat db user nf unreadChat
|
||||
ok user
|
||||
_ -> pure $ chatCmdError (Just user) "not supported"
|
||||
APIDeleteChat cRef@(ChatRef cType chatId) chatDeleteMode -> withUser $ \user@User {userId} -> case cType of
|
||||
APIDeleteChat cRef@(ChatRef cType chatId) cdm -> withUser $ \user@User {userId} -> case cType of
|
||||
CTDirect -> do
|
||||
ct <- withStore $ \db -> getContact db vr user chatId
|
||||
filesInfo <- withStore' $ \db -> getContactFileInfo db user ct
|
||||
withContactLock "deleteChat direct" chatId . procCmd $
|
||||
case chatDeleteMode of
|
||||
case cdm of
|
||||
CDMFull notify -> do
|
||||
cancelFilesInProgress user filesInfo
|
||||
deleteFilesLocally filesInfo
|
||||
@@ -1573,7 +1572,7 @@ processChatCommand' vr = \case
|
||||
CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
|
||||
processChatCommand $ APIConnectContactViaAddress userId incognito contactId
|
||||
_ -> processChatCommand $ APIConnect userId incognito (Just cReqUri)
|
||||
DeleteContact cName -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) (CDMFull True)
|
||||
DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) cdm
|
||||
ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect
|
||||
APIListContacts userId -> withUserId userId $ \user ->
|
||||
CRContactsList user <$> withStore' (\db -> getUserContacts db vr user)
|
||||
@@ -1627,17 +1626,17 @@ processChatCommand' vr = \case
|
||||
contactId <- withStore $ \db -> getContactIdByName db user fromContactName
|
||||
forwardedItemId <- withStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg
|
||||
toChatRef <- getChatRef user toChatName
|
||||
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTDirect contactId) forwardedItemId
|
||||
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTDirect contactId) forwardedItemId Nothing
|
||||
ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do
|
||||
groupId <- withStore $ \db -> getGroupIdByName db user fromGroupName
|
||||
forwardedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg
|
||||
toChatRef <- getChatRef user toChatName
|
||||
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTGroup groupId) forwardedItemId
|
||||
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTGroup groupId) forwardedItemId Nothing
|
||||
ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do
|
||||
folderId <- withStore (`getUserNoteFolderId` user)
|
||||
forwardedItemId <- withStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg
|
||||
toChatRef <- getChatRef user toChatName
|
||||
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTLocal folderId) forwardedItemId
|
||||
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTLocal folderId) forwardedItemId Nothing
|
||||
SendMessage (ChatName cType name) msg -> withUser $ \user -> do
|
||||
let mc = MCText msg
|
||||
case cType of
|
||||
@@ -2222,6 +2221,10 @@ processChatCommand' vr = \case
|
||||
stat (AgentStatsKey {host, clientTs, cmd, res}, count) =
|
||||
map B.unpack [host, clientTs, cmd, res, bshow count]
|
||||
ResetAgentStats -> lift (withAgent' resetAgentStats) >> ok_
|
||||
GetAgentMsgCounts -> lift $ do
|
||||
counts <- map (first decodeLatin1) <$> withAgent' getMsgCounts
|
||||
let allMsgs = foldl' (\(ts, ds) (_, (t, d)) -> (ts + t, ds + d)) (0, 0) counts
|
||||
pure CRAgentMsgCounts {msgCounts = ("all", allMsgs) : sortOn (Down . snd) (filter (\(_, (_, d)) -> d /= 0) counts)}
|
||||
GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions
|
||||
where
|
||||
summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} =
|
||||
@@ -3891,7 +3894,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
withAckMessage' agentConnId meta $
|
||||
void $
|
||||
saveDirectRcvMSG conn meta msgBody
|
||||
SENT msgId ->
|
||||
SENT msgId _proxy ->
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
OK ->
|
||||
-- [async agent commands] continuation on receiving OK
|
||||
@@ -4024,10 +4027,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
notifyMemberConnected gInfo m $ Just ct
|
||||
let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo
|
||||
when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True
|
||||
SENT msgId -> do
|
||||
SENT msgId proxy -> do
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
checkSndInlineFTComplete conn msgId
|
||||
updateDirectItemStatus ct conn msgId $ CISSndSent SSPComplete
|
||||
ci_ <- withStore $ \db -> do
|
||||
ci_ <- updateDirectItemStatus' db ct conn msgId (CISSndSent SSPComplete)
|
||||
forM ci_ $ \ci -> liftIO $ setDirectSndChatItemViaProxy db user ct ci (isJust proxy)
|
||||
forM_ ci_ $ \ci -> toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
|
||||
SWITCH qd phase cStats -> do
|
||||
toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats)
|
||||
when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
|
||||
@@ -4062,13 +4068,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
OK ->
|
||||
-- [async agent commands] continuation on receiving OK
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
MWARN msgId err ->
|
||||
updateDirectItemStatus ct conn msgId (CISSndWarning $ agentSndError err)
|
||||
MERR msgId err -> do
|
||||
updateDirectItemStatus ct conn msgId $ agentErrToItemStatus err
|
||||
updateDirectItemStatus ct conn msgId (CISSndError $ agentSndError err)
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
incAuthErrCounter connEntity conn err
|
||||
MERRS msgIds err -> do
|
||||
-- error cannot be AUTH error here
|
||||
updateDirectItemsStatus ct conn (L.toList msgIds) $ agentErrToItemStatus err
|
||||
updateDirectItemsStatus ct conn (L.toList msgIds) (CISSndError $ agentSndError err)
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
ERR err -> do
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
@@ -4406,10 +4414,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
RCVD msgMeta msgRcpt ->
|
||||
withAckMessage' agentConnId msgMeta $
|
||||
groupMsgReceived gInfo m conn msgMeta msgRcpt
|
||||
SENT msgId -> do
|
||||
SENT msgId proxy -> do
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
checkSndInlineFTComplete conn msgId
|
||||
updateGroupItemStatus gInfo m conn msgId $ CISSndSent SSPComplete
|
||||
updateGroupItemStatus gInfo m conn msgId (CISSndSent SSPComplete) (Just $ isJust proxy)
|
||||
SWITCH qd phase cStats -> do
|
||||
toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats)
|
||||
when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
|
||||
@@ -4445,13 +4453,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
OK ->
|
||||
-- [async agent commands] continuation on receiving OK
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
MWARN msgId err ->
|
||||
withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (CISSndWarning $ agentSndError err)
|
||||
MERR msgId err -> do
|
||||
withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) $ agentErrToItemStatus err
|
||||
withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (CISSndError $ agentSndError err)
|
||||
-- group errors are silenced to reduce load on UI event log
|
||||
-- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
incAuthErrCounter connEntity conn err
|
||||
MERRS msgIds err -> do
|
||||
let newStatus = agentErrToItemStatus err
|
||||
let newStatus = CISSndError $ agentSndError err
|
||||
-- error cannot be AUTH error here
|
||||
withStore' $ \db -> forM_ msgIds $ \msgId ->
|
||||
updateGroupItemErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure ()
|
||||
@@ -4512,7 +4522,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1
|
||||
toView $ CRSndFileStart user ci ft
|
||||
sendFileChunk user ft
|
||||
SENT msgId -> do
|
||||
SENT msgId _proxy -> do
|
||||
withStore' $ \db -> updateSndFileChunkSent db ft msgId
|
||||
unless (fileStatus == FSCancelled) $ sendFileChunk user ft
|
||||
MERR _ err -> do
|
||||
@@ -4675,8 +4685,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
case err of
|
||||
SMP _ SMP.AUTH -> do
|
||||
authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn
|
||||
when (authErrCounter' >= authErrDisableCount) $ do
|
||||
toView $ CRConnectionDisabled connEntity
|
||||
when (authErrCounter' >= authErrDisableCount) $ case connEntity of
|
||||
RcvDirectMsgConnection ctConn (Just ct) -> do
|
||||
toView $ CRContactDisabled user ct {activeConn = Just ctConn {authErrCounter = authErrCounter'}}
|
||||
_ -> toView $ CRConnectionDisabled connEntity
|
||||
_ -> pure ()
|
||||
|
||||
-- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections?
|
||||
@@ -4724,9 +4736,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
sentMsgDeliveryEvent Connection {connId} msgId =
|
||||
withStore' $ \db -> updateSndMsgDeliveryStatus db connId msgId MDSSndSent
|
||||
|
||||
agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd
|
||||
agentErrToItemStatus (SMP _ AUTH) = CISSndErrorAuth
|
||||
agentErrToItemStatus err = CISSndError . T.unpack . safeDecodeUtf8 $ strEncode err
|
||||
agentSndError :: AgentErrorType -> SndError
|
||||
agentSndError = \case
|
||||
SMP _ AUTH -> SndErrAuth
|
||||
SMP _ QUOTA -> SndErrQuota
|
||||
BROKER _ e -> brokerError SndErrRelay e
|
||||
SMP proxySrv (SMP.PROXY (SMP.BROKER e)) -> brokerError (SndErrProxy proxySrv) e
|
||||
AP.PROXY proxySrv _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerError (SndErrProxyRelay proxySrv) e
|
||||
e -> SndErrOther . safeDecodeUtf8 $ strEncode e
|
||||
where
|
||||
brokerError srvErr = \case
|
||||
NETWORK -> SndErrExpired
|
||||
TIMEOUT -> SndErrExpired
|
||||
HOST -> srvErr SrvErrHost
|
||||
SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion
|
||||
e -> srvErr . SrvErrOther . safeDecodeUtf8 $ strEncode e
|
||||
|
||||
badRcvFileChunk :: RcvFileTransfer -> String -> CM ()
|
||||
badRcvFileChunk ft err =
|
||||
@@ -6055,7 +6079,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
|
||||
forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do
|
||||
withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus
|
||||
updateGroupItemStatus gInfo m conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete
|
||||
updateGroupItemStatus gInfo m conn agentMsgId (CISSndRcvd msgRcptStatus SSPComplete) Nothing
|
||||
|
||||
updateDirectItemsStatus :: Contact -> Connection -> [AgentMsgId] -> CIStatus 'MDSnd -> CM ()
|
||||
updateDirectItemsStatus ct conn msgIds newStatus = do
|
||||
@@ -6092,11 +6116,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
| otherwise -> updateGroupSndStatus db itemId groupMemberId newStatus $> True
|
||||
_ -> pure False
|
||||
|
||||
updateGroupItemStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> CM ()
|
||||
updateGroupItemStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus =
|
||||
updateGroupItemStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> Maybe Bool -> CM ()
|
||||
updateGroupItemStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus viaProxy_ =
|
||||
withStore' (\db -> getGroupChatItemByAgentMsgId db user groupId connId msgId) >>= \case
|
||||
Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ SSPComplete}}) -> pure ()
|
||||
Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) -> do
|
||||
forM_ viaProxy_ $ \viaProxy -> withStore' $ \db -> setGroupSndViaProxy db itemId groupMemberId viaProxy
|
||||
memStatusChanged <- updateGroupMemSndStatus itemId groupMemberId newMemStatus
|
||||
when memStatusChanged $ do
|
||||
memStatusCounts <- withStore' (`getGroupSndStatusCounts` itemId)
|
||||
@@ -6719,7 +6744,7 @@ mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId
|
||||
mkChatItem cd ciId content file quotedItem sharedMsgId itemForwarded itemTimed live itemTs forwardedByMember currentTs =
|
||||
let itemText = ciContentToText content
|
||||
itemStatus = ciCreateStatus content
|
||||
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs
|
||||
meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs
|
||||
in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file}
|
||||
|
||||
deleteDirectCI :: MsgDirectionI d => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> CM ChatResponse
|
||||
@@ -7118,13 +7143,12 @@ chatCommandP =
|
||||
"/_delete item " *> (APIDeleteChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> ciDeleteMode),
|
||||
"/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal),
|
||||
"/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP),
|
||||
"/_forward " *> (APIForwardChatItem <$> chatRefP <* A.space <*> chatRefP <* A.space <*> A.decimal),
|
||||
"/_forward " *> (APIForwardChatItem <$> chatRefP <* A.space <*> chatRefP <* A.space <*> A.decimal <*> sendMessageTTLP),
|
||||
"/_read user " *> (APIUserRead <$> A.decimal),
|
||||
"/read user" $> UserRead,
|
||||
"/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))),
|
||||
"/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP),
|
||||
"/_delete " *> (APIDeleteChat <$> chatRefP <* A.space <*> jsonP),
|
||||
"/_delete " *> (APIDeleteChat <$> chatRefP <*> (CDMFull <$> (A.space *> "notify=" *> onOffP <|> pure True))),
|
||||
"/_delete " *> (APIDeleteChat <$> chatRefP <*> chatDeleteMode),
|
||||
"/_clear chat " *> (APIClearChat <$> chatRefP),
|
||||
"/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal),
|
||||
"/_reject " *> (APIRejectContact <$> A.decimal),
|
||||
@@ -7230,7 +7254,7 @@ chatCommandP =
|
||||
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||
("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayName),
|
||||
("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName),
|
||||
("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayName),
|
||||
("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayName <*> chatDeleteMode),
|
||||
"/clear *" $> ClearNoteFolder,
|
||||
"/clear #" *> (ClearGroup <$> displayName),
|
||||
"/clear " *> char_ '@' *> (ClearContact <$> displayName),
|
||||
@@ -7361,6 +7385,7 @@ chatCommandP =
|
||||
"/get subs details" $> GetAgentSubsDetails,
|
||||
"/get workers" $> GetAgentWorkers,
|
||||
"/get workers details" $> GetAgentWorkersDetails,
|
||||
"/get msgs" $> GetAgentMsgCounts,
|
||||
"//" *> (CustomChatCommand <$> A.takeByteString)
|
||||
]
|
||||
where
|
||||
@@ -7381,6 +7406,15 @@ chatCommandP =
|
||||
mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString
|
||||
msgContentP = "text " *> mcTextP <|> "json " *> jsonP
|
||||
ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal
|
||||
chatDeleteMode =
|
||||
A.choice
|
||||
[ " full" *> (CDMFull <$> notifyP),
|
||||
" entity" *> (CDMEntity <$> notifyP),
|
||||
" messages" $> CDMMessages,
|
||||
CDMFull <$> notifyP -- backwards compatible
|
||||
]
|
||||
where
|
||||
notifyP = " notify=" *> onOffP <|> pure True
|
||||
displayName = safeDecodeUtf8 <$> (quoted "'" <|> takeNameTill isSpace)
|
||||
where
|
||||
takeNameTill p =
|
||||
|
||||
@@ -83,7 +83,7 @@ defaultAppSettings =
|
||||
uiDarkColorScheme = Just DCSSimplex,
|
||||
uiCurrentThemeIds = Nothing,
|
||||
uiThemes = Nothing,
|
||||
oneHandUI = Just True
|
||||
oneHandUI = Just False
|
||||
}
|
||||
|
||||
defaultParseAppSettings :: AppSettings
|
||||
|
||||
@@ -291,7 +291,7 @@ data ChatCommand
|
||||
| APIDeleteChatItem ChatRef ChatItemId CIDeleteMode
|
||||
| APIDeleteMemberChatItem GroupId GroupMemberId ChatItemId
|
||||
| APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction}
|
||||
| APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId}
|
||||
| APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int}
|
||||
| APIUserRead UserId
|
||||
| UserRead
|
||||
| APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId))
|
||||
@@ -395,7 +395,7 @@ data ChatCommand
|
||||
| Connect IncognitoEnabled (Maybe AConnectionRequestUri)
|
||||
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId
|
||||
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
|
||||
| DeleteContact ContactName
|
||||
| DeleteContact ContactName ChatDeleteMode
|
||||
| ClearContact ContactName
|
||||
| APIListContacts UserId
|
||||
| ListContacts
|
||||
@@ -501,6 +501,7 @@ data ChatCommand
|
||||
| GetAgentSubsDetails
|
||||
| GetAgentWorkers
|
||||
| GetAgentWorkersDetails
|
||||
| GetAgentMsgCounts
|
||||
| -- The parser will return this command for strings that start from "//".
|
||||
-- This command should be processed in preCmdHook
|
||||
CustomChatCommand ByteString
|
||||
@@ -746,6 +747,8 @@ data ChatResponse
|
||||
| CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary}
|
||||
| CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]}
|
||||
| CRAgentSubsDetails {agentSubs :: SubscriptionsInfo}
|
||||
| CRAgentMsgCounts {msgCounts :: [(Text, (Int, Int))]}
|
||||
| CRContactDisabled {user :: User, contact :: Contact}
|
||||
| CRConnectionDisabled {connectionEntity :: ConnectionEntity}
|
||||
| CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType}
|
||||
| CRAgentConnDeleted {agentConnId :: AgentConnId}
|
||||
@@ -825,9 +828,9 @@ clqNoFilters :: ChatListQuery
|
||||
clqNoFilters = CLQFilters {favorite = False, unread = False}
|
||||
|
||||
data ChatDeleteMode
|
||||
= CDMFull {notify :: Bool} -- delete both contact and conversation
|
||||
= CDMFull {notify :: Bool} -- delete both contact and conversation
|
||||
| CDMEntity {notify :: Bool} -- delete contact (connection), keep conversation
|
||||
| CDMMessages -- delete conversation, keep contact - can be re-opened from Contacts view
|
||||
| CDMMessages -- delete conversation, keep contact - can be re-opened from Contacts view
|
||||
deriving (Show)
|
||||
|
||||
data ConnectionPlan
|
||||
@@ -1398,8 +1401,6 @@ $(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CLQ") ''ChatListQuery)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CDM") ''ChatDeleteMode)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "ILP") ''InvitationLinkPlan)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CAP") ''ContactAddressPlan)
|
||||
|
||||
@@ -29,6 +29,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Char (isSpace)
|
||||
import Data.Int (Int64)
|
||||
import Data.Kind (Constraint)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Maybe (fromMaybe, isJust, isNothing)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -345,6 +346,7 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta
|
||||
itemTs :: ChatItemTs,
|
||||
itemText :: Text,
|
||||
itemStatus :: CIStatus d,
|
||||
sentViaProxy :: Maybe Bool,
|
||||
itemSharedMsgId :: Maybe SharedMsgId,
|
||||
itemForwarded :: Maybe CIForwardedFrom,
|
||||
itemDeleted :: Maybe (CIDeleted c),
|
||||
@@ -359,8 +361,8 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d
|
||||
mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt =
|
||||
mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d
|
||||
mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt =
|
||||
let deletable = case itemContent of
|
||||
CISndMsgContent _ ->
|
||||
case chatTypeI @c of
|
||||
@@ -368,7 +370,7 @@ mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemForwarded it
|
||||
_ -> diffUTCTime currentTs itemTs < nominalDay && isNothing itemDeleted
|
||||
_ -> False
|
||||
editable = deletable && isNothing itemForwarded
|
||||
in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable, forwardedByMember, createdAt, updatedAt}
|
||||
in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable, forwardedByMember, createdAt, updatedAt}
|
||||
|
||||
dummyMeta :: ChatItemId -> UTCTime -> Text -> CIMeta c 'MDSnd
|
||||
dummyMeta itemId ts itemText =
|
||||
@@ -377,6 +379,7 @@ dummyMeta itemId ts itemText =
|
||||
itemTs = ts,
|
||||
itemText,
|
||||
itemStatus = CISSndNew,
|
||||
sentViaProxy = Nothing,
|
||||
itemSharedMsgId = Nothing,
|
||||
itemForwarded = Nothing,
|
||||
itemDeleted = Nothing,
|
||||
@@ -683,8 +686,9 @@ data CIStatus (d :: MsgDirection) where
|
||||
CISSndNew :: CIStatus 'MDSnd
|
||||
CISSndSent :: SndCIStatusProgress -> CIStatus 'MDSnd
|
||||
CISSndRcvd :: MsgReceiptStatus -> SndCIStatusProgress -> CIStatus 'MDSnd
|
||||
CISSndErrorAuth :: CIStatus 'MDSnd
|
||||
CISSndError :: String -> CIStatus 'MDSnd
|
||||
CISSndErrorAuth :: CIStatus 'MDSnd -- deprecated
|
||||
CISSndError :: SndError -> CIStatus 'MDSnd
|
||||
CISSndWarning :: SndError -> CIStatus 'MDSnd
|
||||
CISRcvNew :: CIStatus 'MDRcv
|
||||
CISRcvRead :: CIStatus 'MDRcv
|
||||
CISInvalid :: Text -> CIStatus 'MDSnd
|
||||
@@ -703,7 +707,8 @@ instance MsgDirectionI d => StrEncoding (CIStatus d) where
|
||||
CISSndSent sndProgress -> "snd_sent " <> strEncode sndProgress
|
||||
CISSndRcvd msgRcptStatus sndProgress -> "snd_rcvd " <> strEncode msgRcptStatus <> " " <> strEncode sndProgress
|
||||
CISSndErrorAuth -> "snd_error_auth"
|
||||
CISSndError e -> "snd_error " <> encodeUtf8 (T.pack e)
|
||||
CISSndError sndErr -> "snd_error " <> strEncode sndErr
|
||||
CISSndWarning sndErr -> "snd_warning " <> strEncode sndErr
|
||||
CISRcvNew -> "rcv_new"
|
||||
CISRcvRead -> "rcv_read"
|
||||
CISInvalid {} -> "invalid"
|
||||
@@ -721,17 +726,68 @@ instance StrEncoding ACIStatus where
|
||||
"snd_sent" -> ACIStatus SMDSnd . CISSndSent <$> ((A.space *> strP) <|> pure SSPComplete)
|
||||
"snd_rcvd" -> ACIStatus SMDSnd <$> (CISSndRcvd <$> (A.space *> strP) <*> ((A.space *> strP) <|> pure SSPComplete))
|
||||
"snd_error_auth" -> pure $ ACIStatus SMDSnd CISSndErrorAuth
|
||||
"snd_error" -> ACIStatus SMDSnd . CISSndError . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
|
||||
"snd_error" -> ACIStatus SMDSnd . CISSndError <$> (A.space *> strP)
|
||||
"snd_warning" -> ACIStatus SMDSnd . CISSndWarning <$> (A.space *> strP)
|
||||
"rcv_new" -> pure $ ACIStatus SMDRcv CISRcvNew
|
||||
"rcv_read" -> pure $ ACIStatus SMDRcv CISRcvRead
|
||||
_ -> fail "bad status"
|
||||
|
||||
-- see serverHostError in agent
|
||||
data SndError
|
||||
= SndErrAuth
|
||||
| SndErrQuota
|
||||
| SndErrExpired -- TIMEOUT/NETWORK errors
|
||||
| SndErrRelay {srvError :: SrvError} -- BROKER errors (other than TIMEOUT/NETWORK)
|
||||
| SndErrProxy {proxyServer :: String, srvError :: SrvError} -- SMP PROXY errors
|
||||
| SndErrProxyRelay {proxyServer :: String, srvError :: SrvError} -- PROXY BROKER errors
|
||||
| SndErrOther {sndError :: Text} -- other errors
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SrvError
|
||||
= SrvErrHost
|
||||
| SrvErrVersion
|
||||
| SrvErrOther {srvError :: Text}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding SndError where
|
||||
strEncode = \case
|
||||
SndErrAuth -> "auth"
|
||||
SndErrQuota -> "quota"
|
||||
SndErrExpired -> "expired"
|
||||
SndErrRelay srvErr -> "relay " <> strEncode srvErr
|
||||
SndErrProxy proxy srvErr -> "proxy " <> encodeUtf8 (T.pack proxy) <> " " <> strEncode srvErr
|
||||
SndErrProxyRelay proxy srvErr -> "proxy_relay " <> encodeUtf8 (T.pack proxy) <> " " <> strEncode srvErr
|
||||
SndErrOther e -> "other " <> encodeUtf8 e
|
||||
strP =
|
||||
A.takeWhile1 (/= ' ') >>= \case
|
||||
"auth" -> pure SndErrAuth
|
||||
"quota" -> pure SndErrQuota
|
||||
"expired" -> pure SndErrExpired
|
||||
"relay" -> SndErrRelay <$> (A.space *> strP)
|
||||
"proxy" -> SndErrProxy . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeWhile1 (/= ' ') <* A.space) <*> strP
|
||||
"proxy_relay" -> SndErrProxyRelay . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeWhile1 (/= ' ') <* A.space) <*> strP
|
||||
"other" -> SndErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
|
||||
s -> SndErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString -- for backward compatibility with `CISSndError String`
|
||||
|
||||
instance StrEncoding SrvError where
|
||||
strEncode = \case
|
||||
SrvErrHost -> "host"
|
||||
SrvErrVersion -> "version"
|
||||
SrvErrOther e -> "other " <> encodeUtf8 e
|
||||
strP =
|
||||
A.takeWhile1 (/= ' ') >>= \case
|
||||
"host" -> pure SrvErrHost
|
||||
"version" -> pure SrvErrVersion
|
||||
"other" -> SrvErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
|
||||
_ -> fail "bad SrvError"
|
||||
|
||||
data JSONCIStatus
|
||||
= JCISSndNew
|
||||
| JCISSndSent {sndProgress :: SndCIStatusProgress}
|
||||
| JCISSndRcvd {msgRcptStatus :: MsgReceiptStatus, sndProgress :: SndCIStatusProgress}
|
||||
| JCISSndErrorAuth
|
||||
| JCISSndError {agentError :: String}
|
||||
| JCISSndErrorAuth -- deprecated
|
||||
| JCISSndError {agentError :: SndError}
|
||||
| JCISSndWarning {agentError :: SndError}
|
||||
| JCISRcvNew
|
||||
| JCISRcvRead
|
||||
| JCISInvalid {text :: Text}
|
||||
@@ -743,7 +799,8 @@ jsonCIStatus = \case
|
||||
CISSndSent sndProgress -> JCISSndSent sndProgress
|
||||
CISSndRcvd msgRcptStatus sndProgress -> JCISSndRcvd msgRcptStatus sndProgress
|
||||
CISSndErrorAuth -> JCISSndErrorAuth
|
||||
CISSndError e -> JCISSndError e
|
||||
CISSndError sndErr -> JCISSndError sndErr
|
||||
CISSndWarning sndErr -> JCISSndWarning sndErr
|
||||
CISRcvNew -> JCISRcvNew
|
||||
CISRcvRead -> JCISRcvRead
|
||||
CISInvalid text -> JCISInvalid text
|
||||
@@ -754,7 +811,8 @@ jsonACIStatus = \case
|
||||
JCISSndSent sndProgress -> ACIStatus SMDSnd $ CISSndSent sndProgress
|
||||
JCISSndRcvd msgRcptStatus sndProgress -> ACIStatus SMDSnd $ CISSndRcvd msgRcptStatus sndProgress
|
||||
JCISSndErrorAuth -> ACIStatus SMDSnd CISSndErrorAuth
|
||||
JCISSndError e -> ACIStatus SMDSnd $ CISSndError e
|
||||
JCISSndError sndErr -> ACIStatus SMDSnd $ CISSndError sndErr
|
||||
JCISSndWarning sndErr -> ACIStatus SMDSnd $ CISSndWarning sndErr
|
||||
JCISRcvNew -> ACIStatus SMDRcv CISRcvNew
|
||||
JCISRcvRead -> ACIStatus SMDRcv CISRcvRead
|
||||
JCISInvalid text -> ACIStatus SMDSnd $ CISInvalid text
|
||||
@@ -1041,7 +1099,7 @@ instance TextEncoding CIForwardedFromTag where
|
||||
|
||||
data ChatItemInfo = ChatItemInfo
|
||||
{ itemVersions :: [ChatItemVersion],
|
||||
memberDeliveryStatuses :: Maybe [MemberDeliveryStatus],
|
||||
memberDeliveryStatuses :: Maybe (NonEmpty MemberDeliveryStatus),
|
||||
forwardedFromChatItem :: Maybe AChatItem
|
||||
}
|
||||
deriving (Show)
|
||||
@@ -1070,7 +1128,8 @@ mkItemVersion ChatItem {content, meta} = version <$> ciMsgContent content
|
||||
|
||||
data MemberDeliveryStatus = MemberDeliveryStatus
|
||||
{ groupMemberId :: GroupMemberId,
|
||||
memberDeliveryStatus :: CIStatus 'MDSnd
|
||||
memberDeliveryStatus :: CIStatus 'MDSnd,
|
||||
sentViaProxy :: Maybe Bool
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -1108,6 +1167,10 @@ $(JQ.deriveJSON defaultJSON ''CITimed)
|
||||
|
||||
$(JQ.deriveJSON (enumJSON $ dropPrefix "SSP") ''SndCIStatusProgress)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SrvErr") ''SrvError)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SndErr") ''SndError)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIS") ''JSONCIStatus)
|
||||
|
||||
instance MsgDirectionI d => FromJSON (CIStatus d) where
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20240510_chat_items_via_proxy where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20240510_chat_items_via_proxy :: Query
|
||||
m20240510_chat_items_via_proxy =
|
||||
[sql|
|
||||
ALTER TABLE chat_items ADD COLUMN via_proxy INTEGER;
|
||||
ALTER TABLE group_snd_item_statuses ADD COLUMN via_proxy INTEGER;
|
||||
|]
|
||||
|
||||
down_m20240510_chat_items_via_proxy :: Query
|
||||
down_m20240510_chat_items_via_proxy =
|
||||
[sql|
|
||||
ALTER TABLE chat_items DROP COLUMN via_proxy;
|
||||
ALTER TABLE group_snd_item_statuses DROP COLUMN via_proxy;
|
||||
|]
|
||||
@@ -392,7 +392,8 @@ CREATE TABLE chat_items(
|
||||
fwd_from_msg_dir INTEGER,
|
||||
fwd_from_contact_id INTEGER REFERENCES contacts ON DELETE SET NULL,
|
||||
fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL,
|
||||
fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL
|
||||
fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL,
|
||||
via_proxy INTEGER
|
||||
);
|
||||
CREATE TABLE chat_item_messages(
|
||||
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
|
||||
@@ -503,6 +504,8 @@ CREATE TABLE group_snd_item_statuses(
|
||||
group_snd_item_status TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
,
|
||||
via_proxy INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "sent_probes"(
|
||||
sent_probe_id INTEGER PRIMARY KEY,
|
||||
|
||||
@@ -95,6 +95,7 @@ module Simplex.Chat.Store.Messages
|
||||
lookupChatItemByFileId,
|
||||
getChatItemByGroupId,
|
||||
updateDirectChatItemStatus,
|
||||
setDirectSndChatItemViaProxy,
|
||||
getTimedItems,
|
||||
getChatItemTTL,
|
||||
setChatItemTTL,
|
||||
@@ -108,6 +109,7 @@ module Simplex.Chat.Store.Messages
|
||||
createGroupSndStatus,
|
||||
getGroupSndStatus,
|
||||
updateGroupSndStatus,
|
||||
setGroupSndViaProxy,
|
||||
getGroupSndStatuses,
|
||||
getGroupSndStatusCounts,
|
||||
getGroupHistoryItems,
|
||||
@@ -806,7 +808,7 @@ getLocalChatPreview_ db user (LocalChatPD _ noteFolderId lastItemId_ stats) = do
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal)
|
||||
toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) =
|
||||
toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) =
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
|
||||
@@ -839,7 +841,7 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex
|
||||
_ -> Just (CIDeleted @CTLocal deletedTs)
|
||||
itemEdited' = fromMaybe False itemEdited
|
||||
itemForwarded = toCIForwardedFrom forwardedFromRow
|
||||
in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt
|
||||
in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt
|
||||
ciTimed :: Maybe CITimed
|
||||
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
||||
|
||||
@@ -1407,7 +1409,7 @@ type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool)
|
||||
type ChatItemForwardedFromRow = (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64)
|
||||
|
||||
type ChatItemRow =
|
||||
(Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId)
|
||||
(Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe Bool, Maybe SharedMsgId)
|
||||
:. (Int, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime)
|
||||
:. ChatItemForwardedFromRow
|
||||
:. ChatItemModeRow
|
||||
@@ -1426,7 +1428,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
|
||||
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) =
|
||||
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) =
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
|
||||
@@ -1459,7 +1461,7 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
|
||||
_ -> Just (CIDeleted @CTDirect deletedTs)
|
||||
itemEdited' = fromMaybe False itemEdited
|
||||
itemForwarded = toCIForwardedFrom forwardedFromRow
|
||||
in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt
|
||||
in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt
|
||||
ciTimed :: Maybe CITimed
|
||||
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
||||
|
||||
@@ -1483,7 +1485,7 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. Only (Maybe GroupMemberId) :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup)
|
||||
toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
|
||||
toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
member_ = toMaybeGroupMember userContactId memberRow_
|
||||
@@ -1521,7 +1523,7 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir,
|
||||
_ -> Just (maybe (CIDeleted @CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_)
|
||||
itemEdited' = fromMaybe False itemEdited
|
||||
itemForwarded = toCIForwardedFrom forwardedFromRow
|
||||
in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt
|
||||
in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt
|
||||
ciTimed :: Maybe CITimed
|
||||
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
||||
|
||||
@@ -1600,6 +1602,11 @@ updateDirectChatItemStatus db user@User {userId} ct@Contact {contactId} itemId i
|
||||
liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, contactId, itemId)
|
||||
pure ci {meta = (meta ci) {itemStatus}}
|
||||
|
||||
setDirectSndChatItemViaProxy :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect 'MDSnd -> Bool -> IO (ChatItem 'CTDirect 'MDSnd)
|
||||
setDirectSndChatItemViaProxy db User {userId} Contact {contactId} ci viaProxy = do
|
||||
DB.execute db "UPDATE chat_items SET via_proxy = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (viaProxy, userId, contactId, chatItemId' ci)
|
||||
pure ci {meta = (meta ci) {sentViaProxy = Just viaProxy}}
|
||||
|
||||
updateDirectChatItem :: MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIContent d -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d)
|
||||
updateDirectChatItem db user ct@Contact {contactId} itemId newContent edited live timed_ msgId_ = do
|
||||
ci <- liftEither . correctDir =<< getDirectCIWithReactions db user ct itemId
|
||||
@@ -1758,7 +1765,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id,
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
|
||||
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
|
||||
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live,
|
||||
@@ -2001,7 +2008,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id,
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
|
||||
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
|
||||
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live,
|
||||
@@ -2105,7 +2112,7 @@ getLocalChatItem db User {userId} folderId itemId = ExceptT $ do
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id,
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
|
||||
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
|
||||
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live,
|
||||
@@ -2538,16 +2545,31 @@ updateGroupSndStatus db itemId memberId status = do
|
||||
|]
|
||||
(status, currentTs, itemId, memberId)
|
||||
|
||||
getGroupSndStatuses :: DB.Connection -> ChatItemId -> IO [(GroupMemberId, CIStatus 'MDSnd)]
|
||||
getGroupSndStatuses db itemId =
|
||||
DB.query
|
||||
setGroupSndViaProxy :: DB.Connection -> ChatItemId -> GroupMemberId -> Bool -> IO ()
|
||||
setGroupSndViaProxy db itemId memberId viaProxy =
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
SELECT group_member_id, group_snd_item_status
|
||||
FROM group_snd_item_statuses
|
||||
WHERE chat_item_id = ?
|
||||
UPDATE group_snd_item_statuses
|
||||
SET via_proxy = ?
|
||||
WHERE chat_item_id = ? AND group_member_id = ?
|
||||
|]
|
||||
(Only itemId)
|
||||
(viaProxy, itemId, memberId)
|
||||
|
||||
getGroupSndStatuses :: DB.Connection -> ChatItemId -> IO [MemberDeliveryStatus]
|
||||
getGroupSndStatuses db itemId =
|
||||
map memStatus
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT group_member_id, group_snd_item_status, via_proxy
|
||||
FROM group_snd_item_statuses
|
||||
WHERE chat_item_id = ?
|
||||
|]
|
||||
(Only itemId)
|
||||
where
|
||||
memStatus (groupMemberId, memberDeliveryStatus, sentViaProxy) =
|
||||
MemberDeliveryStatus {groupMemberId, memberDeliveryStatus, sentViaProxy}
|
||||
|
||||
getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(CIStatus 'MDSnd, Int)]
|
||||
getGroupSndStatusCounts db itemId =
|
||||
|
||||
@@ -107,6 +107,7 @@ import Simplex.Chat.Migrations.M20240324_custom_data
|
||||
import Simplex.Chat.Migrations.M20240402_item_forwarded
|
||||
import Simplex.Chat.Migrations.M20240430_ui_theme
|
||||
import Simplex.Chat.Migrations.M20240501_chat_deleted
|
||||
import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -213,7 +214,8 @@ schemaMigrations =
|
||||
("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data),
|
||||
("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded),
|
||||
("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme),
|
||||
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted)
|
||||
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted),
|
||||
("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -375,6 +375,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
||||
[ "agent workers details:",
|
||||
plain . LB.unpack $ J.encode agentWorkersDetails -- this would be huge, but copypastable when has its own line
|
||||
]
|
||||
CRAgentMsgCounts {msgCounts} -> ["received messages (total, duplicates):", plain . LB.unpack $ J.encode msgCounts]
|
||||
CRContactDisabled u c -> ttyUser u ["[" <> ttyContact' c <> "] connection is disabled, to enable: " <> highlight ("/enable " <> viewContactName c) <> ", to delete: " <> highlight ("/d " <> viewContactName c)]
|
||||
CRConnectionDisabled entity -> viewConnectionEntityDisabled entity
|
||||
CRAgentRcvQueueDeleted acId srv aqId err_ ->
|
||||
[ ("completed deleting rcv queue, agent connection id: " <> sShow acId)
|
||||
|
||||
@@ -37,6 +37,7 @@ import Simplex.Chat.Types
|
||||
import Simplex.FileTransfer.Description (kb, mb)
|
||||
import Simplex.FileTransfer.Server (runXFTPServerBlocking)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration)
|
||||
import Simplex.FileTransfer.Transport (supportedFileServerVRange)
|
||||
import Simplex.Messaging.Agent (disposeAgentClient)
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Protocol (currentSMPAgentVersion, duplexHandshakeSMPAgentVersion, pqdrSMPAgentVersion, supportedSMPAgentVRange)
|
||||
@@ -44,6 +45,7 @@ import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), closeSQLiteStore)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig)
|
||||
import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange)
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
import Simplex.Messaging.Server (runSMPServerBlocking)
|
||||
@@ -427,7 +429,9 @@ serverCfg =
|
||||
smpServerVRange = supportedServerSMPRelayVRange,
|
||||
transportConfig = defaultTransportServerConfig,
|
||||
smpHandshakeTimeout = 1000000,
|
||||
controlPort = Nothing
|
||||
controlPort = Nothing,
|
||||
smpAgentCfg = defaultSMPClientAgentConfig,
|
||||
allowSMPProxy = False
|
||||
}
|
||||
|
||||
withSmpServer :: IO () -> IO ()
|
||||
@@ -458,6 +462,7 @@ xftpServerConfig =
|
||||
caCertificateFile = "tests/fixtures/tls/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/tls/server.key",
|
||||
certificateFile = "tests/fixtures/tls/server.crt",
|
||||
xftpServerVRange = supportedFileServerVRange,
|
||||
logStatsInterval = Nothing,
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
|
||||
|
||||
@@ -298,6 +298,7 @@ testPlanInvitationLinkOwn tmp =
|
||||
|
||||
alice ##> ("/_connect plan 1 " <> inv)
|
||||
alice <## "invitation link: ok to connect" -- conn_req_inv is forgotten after connection
|
||||
threadDelay 100000
|
||||
alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)]
|
||||
alice `send` "@alice_2 hi"
|
||||
alice
|
||||
@@ -346,7 +347,7 @@ testDeleteContactDeletesProfile =
|
||||
connectUsers alice bob
|
||||
alice <##> bob
|
||||
-- alice deletes contact, profile is deleted
|
||||
alice ##> "/_delete @2 {\"type\": \"full\", \"notify\": true}"
|
||||
alice ##> "/_delete @2 full notify=on"
|
||||
alice <## "bob: contact is deleted"
|
||||
bob <## "alice (Alice) deleted contact with you"
|
||||
alice ##> "/_contacts 1"
|
||||
@@ -366,7 +367,7 @@ testDeleteContactKeepConversation =
|
||||
connectUsers alice bob
|
||||
alice <##> bob
|
||||
|
||||
alice ##> "/_delete @2 {\"type\": \"entity\", \"notify\": true}"
|
||||
alice ##> "/_delete @2 entity notify=on"
|
||||
alice <## "bob: contact is deleted"
|
||||
bob <## "alice (Alice) deleted contact with you"
|
||||
|
||||
@@ -386,7 +387,7 @@ testDeleteConversationKeepContact =
|
||||
|
||||
alice @@@ [("@bob", "hey")]
|
||||
|
||||
alice ##> "/_delete @2 {\"type\": \"messages\", \"notify\": true}"
|
||||
alice ##> "/_delete @2 messages"
|
||||
alice <## "bob: contact is deleted"
|
||||
|
||||
alice @@@ [("@bob", "")] -- UI would filter
|
||||
@@ -1068,20 +1069,25 @@ testNegotiateCall =
|
||||
-- alice confirms call by sending WebRTC answer
|
||||
alice ##> ("/_call answer @2 " <> serialize testWebRTCSession)
|
||||
alice <## "ok"
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: connecting...")])
|
||||
bob <## "alice continued the WebRTC call"
|
||||
repeatM_ 3 $ getTermLine bob
|
||||
threadDelay 100000
|
||||
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: connecting...")])
|
||||
-- participants can update calls as connected
|
||||
alice ##> "/_call status @2 connected"
|
||||
alice <## "ok"
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: in progress (00:00)")])
|
||||
bob ##> "/_call status @2 connected"
|
||||
bob <## "ok"
|
||||
threadDelay 100000
|
||||
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: in progress (00:00)")])
|
||||
-- either party can end the call
|
||||
bob ##> "/_call end @2"
|
||||
bob <## "ok"
|
||||
threadDelay 100000
|
||||
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: ended (00:00)")])
|
||||
alice <## "call with bob ended"
|
||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: ended (00:00)")])
|
||||
@@ -2290,6 +2296,7 @@ testSwitchContact =
|
||||
alice <## "bob: you started changing address"
|
||||
bob <## "alice changed address for you"
|
||||
alice <## "bob: you changed address"
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "started changing address..."), (1, "you changed address")])
|
||||
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "started changing address for you..."), (0, "changed address for you")])
|
||||
alice <##> bob
|
||||
@@ -2317,6 +2324,7 @@ testAbortSwitchContact tmp = do
|
||||
bob <## "alice started changing address for you"
|
||||
bob <## "alice changed address for you"
|
||||
alice <## "bob: you changed address"
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "started changing address..."), (1, "started changing address..."), (1, "you changed address")])
|
||||
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "started changing address for you..."), (0, "started changing address for you..."), (0, "changed address for you")])
|
||||
alice <##> bob
|
||||
@@ -2331,6 +2339,7 @@ testSwitchGroupMember =
|
||||
alice <## "#team: you started changing address for bob"
|
||||
bob <## "#team: alice changed address for you"
|
||||
alice <## "#team: you changed address for bob"
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "started changing address for bob..."), (1, "you changed address for bob")])
|
||||
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "started changing address for you..."), (0, "changed address for you")])
|
||||
alice #> "#team hey"
|
||||
@@ -2362,6 +2371,7 @@ testAbortSwitchGroupMember tmp = do
|
||||
bob <## "#team: alice started changing address for you"
|
||||
bob <## "#team: alice changed address for you"
|
||||
alice <## "#team: you changed address for bob"
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "started changing address for bob..."), (1, "started changing address for bob..."), (1, "you changed address for bob")])
|
||||
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "started changing address for you..."), (0, "started changing address for you..."), (0, "changed address for you")])
|
||||
alice #> "#team hey"
|
||||
@@ -2511,6 +2521,7 @@ testSyncRatchet tmp =
|
||||
alice <## "bob: connection synchronized"
|
||||
bob <## "alice: connection synchronized"
|
||||
|
||||
threadDelay 100000
|
||||
bob #$> ("/_get chat @2 count=3", chat, [(1, "connection synchronization started"), (0, "connection synchronization agreed"), (0, "connection synchronized")])
|
||||
alice #$> ("/_get chat @2 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")])
|
||||
|
||||
@@ -2550,6 +2561,7 @@ testSyncRatchetCodeReset tmp =
|
||||
alice <## "bob: connection synchronized"
|
||||
bob <## "alice: connection synchronized"
|
||||
|
||||
threadDelay 100000
|
||||
bob #$> ("/_get chat @2 count=4", chat, [(1, "connection synchronization started"), (0, "connection synchronization agreed"), (0, "security code changed"), (0, "connection synchronized")])
|
||||
alice #$> ("/_get chat @2 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")])
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ chatGroupTests = do
|
||||
it "group is known if host contact was deleted" testPlanHostContactDeletedGroupLinkKnown
|
||||
it "own group link" testPlanGroupLinkOwn
|
||||
it "connecting via group link" testPlanGroupLinkConnecting
|
||||
xit "re-join existing group after leaving" testPlanGroupLinkLeaveRejoin
|
||||
it "re-join existing group after leaving" testPlanGroupLinkLeaveRejoin
|
||||
describe "group links without contact" $ do
|
||||
it "join via group link without creating contact" testGroupLinkNoContact
|
||||
it "invitees were previously connected as contacts" testGroupLinkNoContactInviteesWereConnected
|
||||
@@ -2578,13 +2578,15 @@ testPlanGroupLinkOwn tmp =
|
||||
|
||||
testPlanGroupLinkConnecting :: HasCallStack => FilePath -> IO ()
|
||||
testPlanGroupLinkConnecting tmp = do
|
||||
gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
|
||||
-- gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
|
||||
gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice ##> "/create link #team"
|
||||
getGroupLink alice "team" GRMember True
|
||||
withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
|
||||
-- withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg tmp cfg "bob" bobProfile $ \b -> withTestOutput b $ \bob -> do
|
||||
threadDelay 100000
|
||||
|
||||
bob ##> ("/c " <> gLink)
|
||||
@@ -2598,13 +2600,15 @@ testPlanGroupLinkConnecting tmp = do
|
||||
bob <## "group link: connecting, allowed to reconnect"
|
||||
|
||||
threadDelay 100000
|
||||
withTestChatCfg tmp cfg "alice" $ \alice -> do
|
||||
-- withTestChatCfg tmp cfg "alice" $ \alice -> do
|
||||
withTestChatCfg tmp cfg "alice" $ \a -> withTestOutput a $ \alice -> do
|
||||
alice
|
||||
<### [ "1 group links active",
|
||||
"#team: group is empty",
|
||||
"bob (Bob): accepting request to join group #team..."
|
||||
]
|
||||
withTestChatCfg tmp cfg "bob" $ \bob -> do
|
||||
-- withTestChatCfg tmp cfg "bob" $ \bob -> do
|
||||
withTestChatCfg tmp cfg "bob" $ \b -> withTestOutput b $ \bob -> do
|
||||
threadDelay 500000
|
||||
bob ##> ("/_connect plan 1 " <> gLink)
|
||||
bob <## "group link: connecting"
|
||||
@@ -2621,9 +2625,8 @@ testPlanGroupLinkConnecting tmp = do
|
||||
testPlanGroupLinkLeaveRejoin :: HasCallStack => FilePath -> IO ()
|
||||
testPlanGroupLinkLeaveRejoin =
|
||||
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
|
||||
\a b -> do
|
||||
let alice = a {printOutput = True}
|
||||
bob = b {printOutput = True}
|
||||
-- \alice bob -> do
|
||||
\a b -> withTestOutput a $ \alice -> withTestOutput b $ \bob -> do
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -2643,6 +2646,8 @@ testPlanGroupLinkLeaveRejoin =
|
||||
bob <## "#team: you joined the group"
|
||||
]
|
||||
|
||||
threadDelay 100000
|
||||
|
||||
bob ##> ("/_connect plan 1 " <> gLink)
|
||||
bob <## "group link: known group #team"
|
||||
bob <## "use #team <message> to send messages"
|
||||
@@ -2651,6 +2656,8 @@ testPlanGroupLinkLeaveRejoin =
|
||||
bob <## "group link: known group #team"
|
||||
bob <## "use #team <message> to send messages"
|
||||
|
||||
threadDelay 100000
|
||||
|
||||
bob ##> "/leave #team"
|
||||
concurrentlyN_
|
||||
[ do
|
||||
@@ -2659,6 +2666,8 @@ testPlanGroupLinkLeaveRejoin =
|
||||
alice <## "#team: bob left the group"
|
||||
]
|
||||
|
||||
threadDelay 100000
|
||||
|
||||
bob ##> ("/_connect plan 1 " <> gLink)
|
||||
bob <## "group link: ok to connect"
|
||||
|
||||
@@ -3296,6 +3305,7 @@ testGroupSyncRatchet tmp =
|
||||
alice <## "#team bob: connection synchronized"
|
||||
bob <## "#team alice: connection synchronized"
|
||||
|
||||
threadDelay 100000
|
||||
bob #$> ("/_get chat #1 count=3", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "connection synchronized")])
|
||||
alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")])
|
||||
|
||||
@@ -3336,6 +3346,7 @@ testGroupSyncRatchetCodeReset tmp =
|
||||
alice <## "#team bob: connection synchronized"
|
||||
bob <## "#team alice: connection synchronized"
|
||||
|
||||
threadDelay 100000
|
||||
bob #$> ("/_get chat #1 count=4", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "security code changed"), (0, "connection synchronized")])
|
||||
alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")])
|
||||
|
||||
|
||||
@@ -397,6 +397,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $
|
||||
bob ##> ("/c " <> cLink)
|
||||
bob <## "contact address: known contact alice"
|
||||
bob <## "use @alice <message> to send messages"
|
||||
threadDelay 100000
|
||||
alice @@@ [("@bob", lastChatFeature)]
|
||||
bob @@@ [("@alice", lastChatFeature), (":2", ""), (":1", "")]
|
||||
bob ##> "/_delete :1"
|
||||
@@ -470,6 +471,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile
|
||||
bob ##> ("/c " <> cLink)
|
||||
bob <## "contact address: known contact alice"
|
||||
bob <## "use @alice <message> to send messages"
|
||||
threadDelay 100000
|
||||
alice @@@ [("@robert", lastChatFeature)]
|
||||
bob @@@ [("@alice", lastChatFeature), (":3", ""), (":2", ""), (":1", "")]
|
||||
bob ##> "/_delete :1"
|
||||
@@ -488,6 +490,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile
|
||||
bob <## "use @alice <message> to send messages"
|
||||
|
||||
alice <##> bob
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")])
|
||||
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hi"), (1, "hey"), (0, "hi"), (1, "hey")])
|
||||
|
||||
@@ -655,6 +658,7 @@ testPlanAddressOwn tmp =
|
||||
"alice_2 (Alice): contact is connected"
|
||||
]
|
||||
|
||||
threadDelay 100000
|
||||
alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)]
|
||||
alice `send` "@alice_2 hi"
|
||||
alice
|
||||
@@ -1948,21 +1952,29 @@ testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danP
|
||||
bob <## "#team: cath added dan (Daniel) to the group (connecting...)"
|
||||
bob <## "#team: new member dan is connected"
|
||||
]
|
||||
-- dan cannot send direct messages to alice (owner)
|
||||
|
||||
-- dan cannot send direct messages to alice
|
||||
dan ##> "@alice hello alice"
|
||||
dan <## "bad chat command: direct messages not allowed"
|
||||
(alice </)
|
||||
-- but alice can
|
||||
|
||||
-- alice (owner) can send direct messages to dan
|
||||
alice `send` "@dan hello dan"
|
||||
alice <## "member #team dan does not have direct connection, creating"
|
||||
alice <## "contact for member #team dan is created"
|
||||
alice <## "sent invitation to connect directly to member #team dan"
|
||||
alice <# "@dan hello dan"
|
||||
alice <## "dan (Daniel): contact is connected"
|
||||
dan <## "#team alice is creating direct contact alice with you"
|
||||
dan <# "alice> hello dan"
|
||||
dan <## "alice (Alice): contact is connected"
|
||||
-- and now dan can too
|
||||
alice
|
||||
<### [ "member #team dan does not have direct connection, creating",
|
||||
"contact for member #team dan is created",
|
||||
"sent invitation to connect directly to member #team dan",
|
||||
WithTime "@dan hello dan"
|
||||
]
|
||||
dan
|
||||
<### [ "#team alice is creating direct contact alice with you",
|
||||
WithTime "alice> hello dan"
|
||||
]
|
||||
concurrently_
|
||||
(alice <## "dan (Daniel): contact is connected")
|
||||
(dan <## "alice (Alice): contact is connected")
|
||||
|
||||
-- now dan can send messages to alice
|
||||
dan #> "@alice hi alice"
|
||||
alice <# "dan> hi alice"
|
||||
where
|
||||
|
||||
@@ -298,7 +298,7 @@ itemId i = show $ length chatFeatures + i
|
||||
|
||||
(@@@) :: HasCallStack => TestCC -> [(String, String)] -> Expectation
|
||||
(@@@) cc res = do
|
||||
threadDelay 10000
|
||||
threadDelay 100000
|
||||
getChats mapChats cc res
|
||||
|
||||
mapChats :: [(String, String, Maybe ConnStatus)] -> [(String, String)]
|
||||
|
||||
@@ -6,6 +6,7 @@ const uri = require('fast-uri')
|
||||
const i18n = require('eleventy-plugin-i18n')
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const matter = require('gray-matter')
|
||||
const pluginRss = require('@11ty/eleventy-plugin-rss')
|
||||
const { JSDOM } = require('jsdom')
|
||||
|
||||
@@ -388,16 +389,36 @@ module.exports = function (ty) {
|
||||
linkify: true,
|
||||
replaceLink: function (link, _env) {
|
||||
let parsed = uri.parse(link)
|
||||
if (parsed.scheme || parsed.host || !parsed.path.endsWith(".md")) {
|
||||
return link
|
||||
if (parsed.scheme || parsed.host) return link
|
||||
|
||||
let hostFile = path.resolve(_env.page.inputPath)
|
||||
let linkFile = path.resolve(hostFile, '..', parsed.path)
|
||||
if (parsed.path.startsWith('/')) {
|
||||
let srcIndex = hostFile.indexOf("/src")
|
||||
if (srcIndex !== -1) {
|
||||
linkFile = path.join(hostFile.slice(0, srcIndex + 4), parsed.path)
|
||||
}
|
||||
}
|
||||
if (parsed.path.startsWith("../../blog")) {
|
||||
parsed.path = parsed.path.replace("../../blog", "/blog")
|
||||
|
||||
if (fs.existsSync(linkFile) && fs.statSync(linkFile).isFile()) {
|
||||
// this condition works if the link is a valid website file
|
||||
const fileContent = fs.readFileSync(linkFile, 'utf8')
|
||||
parsed.path = (matter(fileContent).data?.permalink || parsed.path).replace(/\.md$/, ".html").toLowerCase()
|
||||
} else if (!fs.existsSync(linkFile)) {
|
||||
linkFile = linkFile.replace('/website/src', '')
|
||||
if (fs.existsSync(linkFile)) {
|
||||
// this condition works if the link is a valid project file
|
||||
const githubUrl = "https://github.com/simplex-chat/simplex-chat/blob/stable"
|
||||
const keyword = "/simplex-chat"
|
||||
index = linkFile.indexOf(keyword)
|
||||
linkFile = linkFile.substring(index + keyword.length)
|
||||
parsed.path = `${githubUrl}${linkFile}`
|
||||
} else {
|
||||
// if the link is not a valid website file or project file
|
||||
throw new Error(`Broken link: ${parsed.path} in ${hostFile}`)
|
||||
}
|
||||
}
|
||||
if (parsed.path.startsWith("../PRIVACY.md")) {
|
||||
parsed.path = parsed.path.replace("../PRIVACY.md", "/privacy")
|
||||
}
|
||||
parsed.path = parsed.path.replace(/\.md$/, ".html").toLowerCase()
|
||||
|
||||
return uri.serialize(parsed)
|
||||
}
|
||||
}).use(markdownItAnchor, {
|
||||
@@ -422,4 +443,4 @@ module.exports = function (ty) {
|
||||
htmlTemplateEngine: 'njk',
|
||||
dataTemplateEngine: 'njk',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<p class="mb-[12px]">When it comes to open source privacy tools, the status quo often dictates the limitations of
|
||||
existing protocols and
|
||||
structures. However, these norms need to be challenged to radically shift how we approach genuinely
|
||||
private communication. This requires doing some uncomfortable things, like making hard choices as it relates to
|
||||
funding, alternative decentralization models, doubling down on privacy over convenience, and more.
|
||||
</p>
|
||||
|
||||
<p class="mb-[12px]">In this post we explain a bit more about why SimpleX operates and makes decisions the way it does:
|
||||
</p>
|
||||
|
||||
<ul class="mb-[12px]">
|
||||
<li>No user accounts.</li>
|
||||
<li>Privacy over convenience.</li>
|
||||
<li>Network decentralization.</li>
|
||||
<li>Funding and profitability.</li>
|
||||
<li>Company jurisdiction.</li>
|
||||
</ul>
|
||||
@@ -24,9 +24,8 @@ var TransformOperation;
|
||||
let activeCall;
|
||||
const processCommand = (function () {
|
||||
const defaultIceServers = [
|
||||
{ urls: ["stuns:stun.simplex.im:443"] },
|
||||
{ urls: ["stun:stun.simplex.im:443"] },
|
||||
{ urls: ["turns:turn.simplex.im:443"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj" },
|
||||
{ urls: ["turn:turn.simplex.im:443"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
|
||||
];
|
||||
function getCallConfig(encodedInsertableStreams, iceServers, relay) {
|
||||
return {
|
||||
|
||||