ios: contacts UI improvements

This commit is contained in:
spaced4ndy
2024-05-20 10:22:03 +04:00
parent 8f8601eaa4
commit 995863d78b
95 changed files with 1716 additions and 626 deletions
+28 -14
View File
@@ -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("")
}
}
+42 -32
View File
@@ -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
+32 -32
View File
@@ -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;
+67 -8
View File
@@ -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
)
}
}
+22
View File
@@ -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)
+62 -6
View File
@@ -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

+4 -4
View File
@@ -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 ()