mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
ios: contacts UI improvements
This commit is contained in:
@@ -341,8 +341,8 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) async -> ChatItem? {
|
||||
let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId)
|
||||
func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) async -> ChatItem? {
|
||||
let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId, ttl: ttl)
|
||||
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
|
||||
}
|
||||
|
||||
@@ -1903,21 +1903,35 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
case .chatSuspended:
|
||||
chatSuspended()
|
||||
case let .contactSwitch(_, contact, switchProgress):
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
|
||||
case let .contactSwitch(user, contact, switchProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .groupMemberSwitch(_, groupInfo, member, switchProgress):
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
|
||||
case let .groupMemberSwitch(user, groupInfo, member, switchProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .contactRatchetSync(_, contact, ratchetSyncProgress):
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
|
||||
case let .contactRatchetSync(user, contact, ratchetSyncProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress):
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
||||
case let .groupMemberRatchetSync(user, groupInfo, member, ratchetSyncProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .contactDisabled(user, contact):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContact(contact)
|
||||
}
|
||||
}
|
||||
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible):
|
||||
await MainActor.run {
|
||||
|
||||
@@ -21,6 +21,8 @@ struct CIGroupInvitationView: View {
|
||||
@State private var inProgress = false
|
||||
@State private var progressByTimeout = false
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
var body: some View {
|
||||
let action = !chatItem.chatDir.sent && groupInvitation.status == .pending
|
||||
let v = ZStack(alignment: .bottomTrailing) {
|
||||
@@ -43,7 +45,7 @@ struct CIGroupInvitationView: View {
|
||||
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
@@ -51,7 +53,7 @@ struct CIGroupInvitationView: View {
|
||||
(
|
||||
groupInvitationText()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ struct CIMetaView: View {
|
||||
var showStatus = true
|
||||
var showEdited = true
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
var body: some View {
|
||||
if chatItem.isDeletedContent {
|
||||
chatItem.timestampText.font(.caption).foregroundColor(metaColor)
|
||||
@@ -27,24 +29,24 @@ struct CIMetaView: View {
|
||||
switch meta.itemStatus {
|
||||
case let .sndSent(sndProgress):
|
||||
switch sndProgress {
|
||||
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited)
|
||||
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited)
|
||||
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
case let .sndRcvd(_, sndProgress):
|
||||
switch sndProgress {
|
||||
case .complete:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
case .partial:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
}
|
||||
default:
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +66,8 @@ func ciMetaText(
|
||||
transparent: Bool = false,
|
||||
sent: SentCheckmark? = nil,
|
||||
showStatus: Bool = true,
|
||||
showEdited: Bool = true
|
||||
showEdited: Bool = true,
|
||||
showViaProxy: Bool
|
||||
) -> Text {
|
||||
var r = Text("")
|
||||
if showEdited, meta.itemEdited {
|
||||
@@ -78,6 +81,9 @@ func ciMetaText(
|
||||
}
|
||||
r = r + Text(" ")
|
||||
}
|
||||
if showViaProxy, meta.sentViaProxy == true {
|
||||
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
|
||||
}
|
||||
if showStatus {
|
||||
if let (icon, statusColor) = meta.statusIcon(color) {
|
||||
let t = Text(Image(systemName: icon)).font(.caption2)
|
||||
|
||||
@@ -19,6 +19,8 @@ struct CIRcvDecryptionError: View {
|
||||
var chatItem: ChatItem
|
||||
@State private var alert: CIRcvDecryptionErrorAlert?
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
enum CIRcvDecryptionErrorAlert: Identifiable {
|
||||
case syncAllowedAlert(_ syncConnection: () -> Void)
|
||||
case syncNotSupportedContactAlert
|
||||
@@ -119,7 +121,7 @@ struct CIRcvDecryptionError: View {
|
||||
.foregroundColor(syncSupported ? .accentColor : .secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
@@ -140,7 +142,7 @@ struct CIRcvDecryptionError: View {
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
|
||||
@@ -87,12 +87,17 @@ struct FramedItemView: View {
|
||||
.cornerRadius(18)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
switch chatItem.meta.itemStatus {
|
||||
case .sndErrorAuth:
|
||||
v.onTapGesture { msgDeliveryError("Most likely this contact has deleted the connection with you.") }
|
||||
case let .sndError(agentError):
|
||||
v.onTapGesture { msgDeliveryError("Unexpected error: \(agentError)") }
|
||||
default: v
|
||||
if let (title, text) = chatItem.meta.itemStatus.statusInfo {
|
||||
v.onTapGesture {
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text(title),
|
||||
message: Text(text)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,13 +162,6 @@ struct FramedItemView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func msgDeliveryError(_ err: LocalizedStringKey) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Message delivery error",
|
||||
message: err
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = false) -> some View {
|
||||
let v = HStack(spacing: 6) {
|
||||
|
||||
@@ -35,6 +35,8 @@ struct MsgContentView: View {
|
||||
@State private var typingIdx = 0
|
||||
@State private var timer: Timer?
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
var body: some View {
|
||||
if meta?.isLive == true {
|
||||
msgContentView()
|
||||
@@ -81,7 +83,7 @@ struct MsgContentView: View {
|
||||
}
|
||||
|
||||
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true)
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@ struct ChatItemInfoView: View {
|
||||
let mss = membersStatuses(memberDeliveryStatuses)
|
||||
if !mss.isEmpty {
|
||||
ForEach(mss, id: \.0.groupMemberId) { memberStatus in
|
||||
memberDeliveryStatusView(memberStatus.0, memberStatus.1)
|
||||
memberDeliveryStatusView(memberStatus.0, memberStatus.1, memberStatus.2)
|
||||
}
|
||||
} else {
|
||||
Text("No delivery information")
|
||||
@@ -392,23 +392,27 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] {
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus, Bool?)] {
|
||||
memberDeliveryStatuses.compactMap({ mds in
|
||||
if let mem = chatModel.getGroupMember(mds.groupMemberId) {
|
||||
return (mem.wrapped, mds.memberDeliveryStatus)
|
||||
return (mem.wrapped, mds.memberDeliveryStatus, mds.sentViaProxy)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View {
|
||||
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus, _ sentViaProxy: Bool?) -> some View {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image, size: 30)
|
||||
.padding(.trailing, 2)
|
||||
Text(member.chatViewName)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
if sentViaProxy == true {
|
||||
Image(systemName: "arrow.forward")
|
||||
.foregroundColor(.secondary).opacity(0.67)
|
||||
}
|
||||
let v = Group {
|
||||
if let (icon, statusColor) = status.statusIcon(Color.secondary) {
|
||||
switch status {
|
||||
|
||||
@@ -42,6 +42,7 @@ struct ChatView: View {
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
viewBody
|
||||
.toolbarBackground(.visible, for: .navigationBar)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.keyboardPadding()
|
||||
} else {
|
||||
|
||||
@@ -712,9 +712,9 @@ struct ComposeView: View {
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
await sendMemberContactInvitation()
|
||||
} else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem {
|
||||
sent = await forwardItem(ci, fromChatInfo)
|
||||
sent = await forwardItem(ci, fromChatInfo, ttl)
|
||||
if !composeState.message.isEmpty {
|
||||
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: nil)
|
||||
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
|
||||
}
|
||||
} else if case let .editingItem(ci) = composeState.contextItem {
|
||||
sent = await updateMessage(ci, live: live)
|
||||
@@ -890,13 +890,14 @@ struct ComposeView: View {
|
||||
return nil
|
||||
}
|
||||
|
||||
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo) async -> ChatItem? {
|
||||
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? {
|
||||
if let chatItem = await apiForwardChatItem(
|
||||
toChatType: chat.chatInfo.chatType,
|
||||
toChatId: chat.chatInfo.apiId,
|
||||
fromChatType: fromChatInfo.chatType,
|
||||
fromChatId: fromChatInfo.apiId,
|
||||
itemId: forwardedItem.id
|
||||
itemId: forwardedItem.id,
|
||||
ttl: ttl
|
||||
) {
|
||||
await MainActor.run {
|
||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||
|
||||
@@ -224,8 +224,7 @@ struct SendMessageView: View {
|
||||
|
||||
@ViewBuilder private func sendButtonContextMenuItems() -> some View {
|
||||
if composeState.liveMessage == nil,
|
||||
!composeState.editing,
|
||||
!composeState.forwarding {
|
||||
!composeState.editing {
|
||||
if case .noContextItem = composeState.contextItem,
|
||||
!composeState.voicePreview,
|
||||
let send = sendLiveMessage,
|
||||
|
||||
@@ -17,7 +17,7 @@ struct ContactsView: View {
|
||||
@State private var searchText = ""
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = true
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
|
||||
@@ -19,7 +19,7 @@ struct ChatListView: View {
|
||||
@State private var searchChatFilteredBySimplexLink: String? = nil
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = true
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
|
||||
@@ -240,14 +240,14 @@ struct ChatPreviewView: View {
|
||||
|
||||
private func itemStatusMark(_ cItem: ChatItem) -> Text {
|
||||
switch cItem.meta.itemStatus {
|
||||
case .sndErrorAuth:
|
||||
case .sndErrorAuth, .sndError:
|
||||
return Text(Image(systemName: "multiply"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.red) + Text(" ")
|
||||
case .sndError:
|
||||
case .sndWarning:
|
||||
return Text(Image(systemName: "exclamationmark.triangle.fill"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.yellow) + Text(" ")
|
||||
.foregroundColor(.orange) + Text(" ")
|
||||
default: return Text("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ struct HomeView: View {
|
||||
@State private var userPickerVisible = false
|
||||
@State private var showConnectDesktop = false
|
||||
@State private var newChatMenuOption: NewChatMenuOption? = nil
|
||||
@State private var toolbarHeight: CGFloat = 0
|
||||
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = true
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
@@ -31,32 +32,9 @@ struct HomeView: View {
|
||||
get: { chatModel.chatId != nil },
|
||||
set: { _ in }
|
||||
),
|
||||
destination: chatView
|
||||
) {
|
||||
VStack {
|
||||
switch homeTab {
|
||||
case .contacts:
|
||||
contactsView()
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Contacts")
|
||||
case .chats:
|
||||
chatListView()
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Chats")
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
settingsButton()
|
||||
Spacer()
|
||||
contactsButton()
|
||||
Spacer()
|
||||
chatsButton()
|
||||
Spacer()
|
||||
newChatButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
destination: chatView,
|
||||
content: homeView
|
||||
)
|
||||
|
||||
if userPickerVisible {
|
||||
Rectangle().fill(.white.opacity(0.001)).onTapGesture {
|
||||
@@ -76,6 +54,40 @@ struct HomeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func homeView() -> some View {
|
||||
let v = VStack {
|
||||
switch homeTab {
|
||||
case .contacts: withToolbar("Contacts", contactsView)
|
||||
case .chats: withToolbar("Chats", chatListView)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
settingsButton()
|
||||
Spacer()
|
||||
contactsButton()
|
||||
Spacer()
|
||||
chatsButton()
|
||||
Spacer()
|
||||
newChatButton()
|
||||
}
|
||||
}
|
||||
|
||||
if #unavailable(iOS 16) {
|
||||
v
|
||||
} else if oneHandUI {
|
||||
v.toolbarBackground(.visible, for: .navigationBar, .bottomBar)
|
||||
} else {
|
||||
v.toolbarBackground(.visible, for: .bottomBar)
|
||||
}
|
||||
}
|
||||
|
||||
func withToolbar<V: View>(_ title: LocalizedStringKey, _ content: () -> V) -> some View {
|
||||
content()
|
||||
.navigationBarTitleDisplayMode(oneHandUI ? .inline : .large)
|
||||
.navigationTitle(title)
|
||||
}
|
||||
|
||||
@ViewBuilder private func settingsButton() -> some View {
|
||||
let user = chatModel.currentUser ?? User.sampleData
|
||||
let multiUser = chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1
|
||||
@@ -90,8 +102,8 @@ struct HomeView: View {
|
||||
} label: {
|
||||
VStack(spacing: 2) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
ProfileImage(imageStr: user.image, size: 38, color: Color(uiColor: .quaternaryLabel))
|
||||
.padding(.top, 2)
|
||||
ProfileImage(imageStr: user.image, size: 42, color: Color(uiColor: .quaternaryLabel))
|
||||
.padding(.top, 3)
|
||||
.padding(.trailing, 3)
|
||||
let allRead = chatModel.users
|
||||
.filter { u in !u.user.activeUser && !u.user.hidden }
|
||||
@@ -115,7 +127,7 @@ struct HomeView: View {
|
||||
Button {
|
||||
homeTab = .contacts
|
||||
} label: {
|
||||
iconLabel(homeTab == .contacts ? "book.fill" : "book", "Contacts")
|
||||
iconLabel(homeTab == .contacts ? "person.crop.circle.fill" : "person.crop.circle", "Contacts")
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@@ -196,13 +208,11 @@ struct HomeView: View {
|
||||
|
||||
@ViewBuilder private func contactsView() -> some View {
|
||||
ContactsView()
|
||||
.padding(.vertical, oneHandUI ? 1 : 0)
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatListView() -> some View {
|
||||
// TODO reverse scale effect for swipe actions
|
||||
ChatListView()
|
||||
.padding(.vertical, oneHandUI ? 1 : 0)
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatView() -> some View {
|
||||
|
||||
@@ -25,7 +25,7 @@ struct AppearanceSettings: View {
|
||||
@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
@State private var uiTintColor = getUIAccentColorDefault()
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = true
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||
|
||||
var body: some View {
|
||||
VStack{
|
||||
@@ -40,11 +40,11 @@ struct AppearanceSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section("Interface") {
|
||||
settingsRow("hand.wave") {
|
||||
Toggle("One-hand UI", isOn: $oneHandUI)
|
||||
}
|
||||
}
|
||||
// Section("Interface") {
|
||||
// settingsRow("hand.wave") {
|
||||
// Toggle("One-hand UI", isOn: $oneHandUI)
|
||||
// }
|
||||
// }
|
||||
|
||||
Section("App icon") {
|
||||
HStack {
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct DeveloperView: View {
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI) private var oneHandUI = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
@@ -31,9 +32,6 @@ struct DeveloperView: View {
|
||||
} label: {
|
||||
settingsRow("terminal") { Text("Chat console") }
|
||||
}
|
||||
settingsRow("internaldrive") {
|
||||
Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades)
|
||||
}
|
||||
settingsRow("chevron.left.forwardslash.chevron.right") {
|
||||
Toggle("Show developer options", isOn: $developerTools)
|
||||
}
|
||||
@@ -42,6 +40,19 @@ struct DeveloperView: View {
|
||||
} footer: {
|
||||
(developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
|
||||
}
|
||||
|
||||
if developerTools {
|
||||
Section {
|
||||
settingsRow("internaldrive") {
|
||||
Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades)
|
||||
}
|
||||
settingsRow("hand.wave") {
|
||||
Toggle("One-hand UI", isOn: $oneHandUI)
|
||||
}
|
||||
} header: {
|
||||
Text("Developer options")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,16 @@ import SimpleXChat
|
||||
private enum NetworkAlert: Identifiable {
|
||||
case updateOnionHosts(hosts: OnionHosts)
|
||||
case updateSessionMode(mode: TransportSessionMode)
|
||||
case updateSMPProxyMode(proxyMode: SMPProxyMode)
|
||||
case updateSMPProxyFallback(proxyFallback: SMPProxyFallback)
|
||||
case error(err: String)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .updateOnionHosts(hosts): return "updateOnionHosts \(hosts)"
|
||||
case let .updateSessionMode(mode): return "updateSessionMode \(mode)"
|
||||
case let .updateSMPProxyMode(proxyMode): return "updateSMPProxyMode \(proxyMode)"
|
||||
case let .updateSMPProxyFallback(proxyFallback): return "updateSMPProxyFallback \(proxyFallback)"
|
||||
case let .error(err): return "error \(err)"
|
||||
}
|
||||
}
|
||||
@@ -26,11 +30,14 @@ private enum NetworkAlert: Identifiable {
|
||||
struct NetworkAndServers: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = true
|
||||
@State private var cfgLoaded = false
|
||||
@State private var currentNetCfg = NetCfg.defaults
|
||||
@State private var netCfg = NetCfg.defaults
|
||||
@State private var onionHosts: OnionHosts = .no
|
||||
@State private var sessionMode: TransportSessionMode = .user
|
||||
@State private var proxyMode: SMPProxyMode = .never
|
||||
@State private var proxyFallback: SMPProxyFallback = .allow
|
||||
@State private var alert: NetworkAlert?
|
||||
|
||||
var body: some View {
|
||||
@@ -75,6 +82,30 @@ struct NetworkAndServers: View {
|
||||
Text("Using .onion hosts requires compatible VPN provider.")
|
||||
}
|
||||
|
||||
// Section {
|
||||
// Picker("Private routing", selection: $proxyMode) {
|
||||
// ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) }
|
||||
// }
|
||||
// .frame(height: 36)
|
||||
//
|
||||
// Picker("Allow downgrade", selection: $proxyFallback) {
|
||||
// ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) }
|
||||
// }
|
||||
// .disabled(proxyMode == .never)
|
||||
// .frame(height: 36)
|
||||
//
|
||||
// Toggle("Show message status", isOn: $showSentViaProxy)
|
||||
// } header: {
|
||||
// Text("Private message routing")
|
||||
// } footer: {
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("To protect your IP address, private routing uses your SMP servers to deliver messages.")
|
||||
// if showSentViaProxy {
|
||||
// Text("Show → on messages sent via private routing.")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
Section("Calls") {
|
||||
NavigationLink {
|
||||
RTCServers()
|
||||
@@ -99,14 +130,24 @@ struct NetworkAndServers: View {
|
||||
currentNetCfg = getNetCfg()
|
||||
resetNetCfgView()
|
||||
}
|
||||
.onChange(of: onionHosts) { _ in
|
||||
if onionHosts != OnionHosts(netCfg: currentNetCfg) {
|
||||
alert = .updateOnionHosts(hosts: onionHosts)
|
||||
.onChange(of: onionHosts) { hosts in
|
||||
if hosts != OnionHosts(netCfg: currentNetCfg) {
|
||||
alert = .updateOnionHosts(hosts: hosts)
|
||||
}
|
||||
}
|
||||
.onChange(of: sessionMode) { _ in
|
||||
if sessionMode != netCfg.sessionMode {
|
||||
alert = .updateSessionMode(mode: sessionMode)
|
||||
.onChange(of: sessionMode) { mode in
|
||||
if mode != netCfg.sessionMode {
|
||||
alert = .updateSessionMode(mode: mode)
|
||||
}
|
||||
}
|
||||
.onChange(of: proxyMode) { mode in
|
||||
if mode != netCfg.smpProxyMode {
|
||||
alert = .updateSMPProxyMode(proxyMode: mode)
|
||||
}
|
||||
}
|
||||
.onChange(of: proxyFallback) { fallbackMode in
|
||||
if fallbackMode != netCfg.smpProxyFallback {
|
||||
alert = .updateSMPProxyFallback(proxyFallback: fallbackMode)
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { a in
|
||||
@@ -137,6 +178,30 @@ struct NetworkAndServers: View {
|
||||
resetNetCfgView()
|
||||
}
|
||||
)
|
||||
case let .updateSMPProxyMode(proxyMode):
|
||||
return Alert(
|
||||
title: Text("Message routing mode"),
|
||||
message: Text(proxyModeInfo(proxyMode)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
netCfg.smpProxyMode = proxyMode
|
||||
saveNetCfg()
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
resetNetCfgView()
|
||||
}
|
||||
)
|
||||
case let .updateSMPProxyFallback(proxyFallback):
|
||||
return Alert(
|
||||
title: Text("Message routing fallback"),
|
||||
message: Text(proxyFallbackInfo(proxyFallback)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
netCfg.smpProxyFallback = proxyFallback
|
||||
saveNetCfg()
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
resetNetCfgView()
|
||||
}
|
||||
)
|
||||
case let .error(err):
|
||||
return Alert(
|
||||
title: Text("Error updating settings"),
|
||||
@@ -166,6 +231,8 @@ struct NetworkAndServers: View {
|
||||
netCfg = currentNetCfg
|
||||
onionHosts = OnionHosts(netCfg: netCfg)
|
||||
sessionMode = netCfg.sessionMode
|
||||
proxyMode = netCfg.smpProxyMode
|
||||
proxyFallback = netCfg.smpProxyFallback
|
||||
}
|
||||
|
||||
private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey {
|
||||
@@ -182,6 +249,23 @@ struct NetworkAndServers: View {
|
||||
case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail."
|
||||
}
|
||||
}
|
||||
|
||||
private func proxyModeInfo(_ mode: SMPProxyMode) -> LocalizedStringKey {
|
||||
switch mode {
|
||||
case .always: return "Always use private routing."
|
||||
case .unknown: return "Use private routing with unknown servers."
|
||||
case .unprotected: return "Use private routing with unknown servers when IP address is not protected."
|
||||
case .never: return "Do NOT use private routing."
|
||||
}
|
||||
}
|
||||
|
||||
private func proxyFallbackInfo(_ proxyFallback: SMPProxyFallback) -> LocalizedStringKey {
|
||||
switch proxyFallback {
|
||||
case .allow: return "Send messages directly when your or destination server does not support private routing."
|
||||
case .allowProtected: return "Send messages directly when IP address is protected and your or destination server does not support private routing."
|
||||
case .prohibit: return "Do NOT send messages directly, even if your or destination server does not support private routing."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkServersView_Previews: PreviewProvider {
|
||||
|
||||
@@ -63,6 +63,7 @@ let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
||||
let DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice"
|
||||
let DEFAULT_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice"
|
||||
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||
|
||||
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||
|
||||
@@ -92,7 +93,7 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
|
||||
DEFAULT_USER_INTERFACE_STYLE: 0,
|
||||
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
|
||||
DEFAULT_ONE_HAND_UI: true,
|
||||
DEFAULT_ONE_HAND_UI: false,
|
||||
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
||||
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true,
|
||||
@@ -105,7 +106,8 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
||||
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue,
|
||||
DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE: true,
|
||||
DEFAULT_SHOW_DELETE_CONTACT_NOTICE: true
|
||||
DEFAULT_SHOW_DELETE_CONTACT_NOTICE: true,
|
||||
DEFAULT_SHOW_SENT_VIA_RPOXY: false
|
||||
]
|
||||
|
||||
// not used anymore
|
||||
|
||||
@@ -76,6 +76,11 @@
|
||||
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
|
||||
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
|
||||
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; };
|
||||
5C9F3DCC2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */; };
|
||||
5C9F3DCD2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */; };
|
||||
5C9F3DCE2BF7A6900003B86B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC92BF7A6900003B86B /* libgmp.a */; };
|
||||
5C9F3DCF2BF7A6900003B86B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */; };
|
||||
5C9F3DD02BF7A6900003B86B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DCB2BF7A6900003B86B /* libffi.a */; };
|
||||
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
|
||||
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
|
||||
5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; };
|
||||
@@ -115,11 +120,6 @@
|
||||
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
|
||||
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
|
||||
5CE0E8AB2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */; };
|
||||
5CE0E8AC2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */; };
|
||||
5CE0E8AD2BF0C1B5008D6E06 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */; };
|
||||
5CE0E8AE2BF0C1B5008D6E06 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A92BF0C1B5008D6E06 /* libffi.a */; };
|
||||
5CE0E8AF2BF0C1B5008D6E06 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */; };
|
||||
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; };
|
||||
@@ -357,6 +357,11 @@
|
||||
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
|
||||
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
|
||||
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; };
|
||||
5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a"; sourceTree = "<group>"; };
|
||||
5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5C9F3DC92BF7A6900003B86B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C9F3DCB2BF7A6900003B86B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
||||
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
|
||||
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; };
|
||||
@@ -423,11 +428,6 @@
|
||||
5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = "<group>"; };
|
||||
5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
|
||||
5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
|
||||
5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a"; sourceTree = "<group>"; };
|
||||
5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CE0E8A92BF0C1B5008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -535,13 +535,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C9F3DCF2BF7A6900003B86B /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CE0E8AE2BF0C1B5008D6E06 /* libffi.a in Frameworks */,
|
||||
5CE0E8AD2BF0C1B5008D6E06 /* libgmp.a in Frameworks */,
|
||||
5C9F3DCE2BF7A6900003B86B /* libgmp.a in Frameworks */,
|
||||
5C9F3DCC2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a in Frameworks */,
|
||||
5C9F3DD02BF7A6900003B86B /* libffi.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5CE0E8AC2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a in Frameworks */,
|
||||
5CE0E8AB2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a in Frameworks */,
|
||||
5CE0E8AF2BF0C1B5008D6E06 /* libgmpxx.a in Frameworks */,
|
||||
5C9F3DCD2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -609,11 +609,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CE0E8A92BF0C1B5008D6E06 /* libffi.a */,
|
||||
5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */,
|
||||
5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */,
|
||||
5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */,
|
||||
5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */,
|
||||
5C9F3DCB2BF7A6900003B86B /* libffi.a */,
|
||||
5C9F3DC92BF7A6900003B86B /* libgmp.a */,
|
||||
5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */,
|
||||
5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */,
|
||||
5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -1580,7 +1580,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1605,7 +1605,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1629,7 +1629,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1654,7 +1654,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1715,7 +1715,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -1730,7 +1730,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -1752,7 +1752,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -1789,7 +1789,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1815,7 +1815,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1840,7 +1840,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 216;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1866,7 +1866,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7.3;
|
||||
MARKETING_VERSION = 5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -48,7 +48,7 @@ public enum ChatCommand {
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
|
||||
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
|
||||
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
|
||||
case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64)
|
||||
case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?)
|
||||
case apiGetNtfToken
|
||||
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
|
||||
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
|
||||
@@ -192,7 +192,9 @@ public enum ChatCommand {
|
||||
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
|
||||
case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)"
|
||||
case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
|
||||
case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId): return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId)"
|
||||
case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl):
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId) ttl=\(ttlStr)"
|
||||
case .apiGetNtfToken: return "/_ntf get "
|
||||
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
|
||||
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
|
||||
@@ -251,7 +253,7 @@ public enum ChatCommand {
|
||||
case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)"
|
||||
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)"
|
||||
case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)"
|
||||
case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(encodeJSON(chatDeleteMode))"
|
||||
case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)"
|
||||
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
|
||||
case let .apiListContacts(userId): return "/_contacts \(userId)"
|
||||
case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))"
|
||||
@@ -473,10 +475,6 @@ public enum ChatCommand {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func onOff(_ b: Bool) -> String {
|
||||
b ? "on" : "off"
|
||||
}
|
||||
|
||||
private func onOffParam(_ param: String, _ b: Bool?) -> String {
|
||||
if let b = b {
|
||||
return " \(param)=\(onOff(b))"
|
||||
@@ -489,6 +487,10 @@ public enum ChatCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private func onOff(_ b: Bool) -> String {
|
||||
b ? "on" : "off"
|
||||
}
|
||||
|
||||
public struct APIResponse: Decodable {
|
||||
var resp: ChatResponse
|
||||
}
|
||||
@@ -635,6 +637,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo)
|
||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||
case contactDisabled(user: UserRef, contact: Contact)
|
||||
// remote desktop responses/events
|
||||
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
|
||||
case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo, ctrlAppInfo_: CtrlAppInfo?, appVersion: String, compatible: Bool)
|
||||
@@ -792,6 +795,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .ntfMessages: return "ntfMessages"
|
||||
case .ntfMessage: return "ntfMessage"
|
||||
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
||||
case .contactDisabled: return "contactDisabled"
|
||||
case .remoteCtrlList: return "remoteCtrlList"
|
||||
case .remoteCtrlFound: return "remoteCtrlFound"
|
||||
case .remoteCtrlConnecting: return "remoteCtrlConnecting"
|
||||
@@ -949,6 +953,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
|
||||
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
|
||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .contactDisabled(u, contact): return withUser(u, String(describing: contact))
|
||||
case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
|
||||
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)"
|
||||
case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)"
|
||||
@@ -988,6 +993,14 @@ public enum ChatDeleteMode: Codable {
|
||||
case full(notify: Bool)
|
||||
case entity(notify: Bool)
|
||||
case messages
|
||||
|
||||
var cmdString: String {
|
||||
switch self {
|
||||
case let .full(notify): "full notify=\(onOff(notify))"
|
||||
case let .entity(notify): "entity notify=\(onOff(notify))"
|
||||
case .messages: "messages"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConnectionPlan: Decodable {
|
||||
@@ -1244,9 +1257,12 @@ public struct ServerAddress: Decodable {
|
||||
|
||||
public struct NetCfg: Codable, Equatable {
|
||||
public var socksProxy: String? = nil
|
||||
var socksMode: SocksMode = .always
|
||||
public var hostMode: HostMode = .publicHost
|
||||
public var requiredHostMode = true
|
||||
public var sessionMode: TransportSessionMode
|
||||
public var smpProxyMode: SMPProxyMode = .never
|
||||
public var smpProxyFallback: SMPProxyFallback = .allow
|
||||
public var tcpConnectTimeout: Int // microseconds
|
||||
public var tcpTimeout: Int // microseconds
|
||||
public var tcpTimeoutPerKb: Int // microseconds
|
||||
@@ -1291,6 +1307,49 @@ public enum HostMode: String, Codable {
|
||||
case publicHost = "public"
|
||||
}
|
||||
|
||||
public enum SocksMode: String, Codable {
|
||||
case always = "always"
|
||||
case onion = "onion"
|
||||
}
|
||||
|
||||
public enum SMPProxyMode: String, Codable {
|
||||
case always = "always"
|
||||
case unknown = "unknown"
|
||||
case unprotected = "unprotected"
|
||||
case never = "never"
|
||||
|
||||
public var text: LocalizedStringKey {
|
||||
switch self {
|
||||
case .always: return "always"
|
||||
case .unknown: return "unknown relays"
|
||||
case .unprotected: return "unprotected"
|
||||
case .never: return "never"
|
||||
}
|
||||
}
|
||||
|
||||
public var id: SMPProxyMode { self }
|
||||
|
||||
public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never]
|
||||
}
|
||||
|
||||
public enum SMPProxyFallback: String, Codable {
|
||||
case allow = "allow"
|
||||
case allowProtected = "allowProtected"
|
||||
case prohibit = "prohibit"
|
||||
|
||||
public var text: LocalizedStringKey {
|
||||
switch self {
|
||||
case .allow: return "yes"
|
||||
case .allowProtected: return "when IP hidden"
|
||||
case .prohibit: return "no"
|
||||
}
|
||||
}
|
||||
|
||||
public var id: SMPProxyFallback { self }
|
||||
|
||||
public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit]
|
||||
}
|
||||
|
||||
public enum OnionHosts: String, Identifiable {
|
||||
case no
|
||||
case prefer
|
||||
@@ -2055,7 +2114,7 @@ public struct AppSettings: Codable, Equatable {
|
||||
androidCallOnLockScreen: AppSettingsLockScreenCalls.show,
|
||||
iosCallKitEnabled: true,
|
||||
iosCallKitCallsInRecents: false,
|
||||
oneHandUI: true
|
||||
oneHandUI: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles
|
||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
||||
let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode"
|
||||
let GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK = "networkSMPProxyFallback"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb"
|
||||
@@ -53,6 +55,8 @@ public func registerGroupDefaults() {
|
||||
GROUP_DEFAULT_NTF_ENABLE_PERIODIC: false,
|
||||
GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue,
|
||||
GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.user.rawValue,
|
||||
GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE: SMPProxyMode.never.rawValue,
|
||||
GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK: SMPProxyFallback.allow.rawValue,
|
||||
GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout,
|
||||
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout,
|
||||
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb,
|
||||
@@ -191,6 +195,18 @@ public let networkSessionModeGroupDefault = EnumDefault<TransportSessionMode>(
|
||||
withDefault: .user
|
||||
)
|
||||
|
||||
public let networkSMPProxyModeGroupDefault = EnumDefault<SMPProxyMode>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE,
|
||||
withDefault: .never
|
||||
)
|
||||
|
||||
public let networkSMPProxyFallbackGroupDefault = EnumDefault<SMPProxyFallback>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK,
|
||||
withDefault: .allow
|
||||
)
|
||||
|
||||
public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE)
|
||||
|
||||
public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE)
|
||||
@@ -275,6 +291,8 @@ public func getNetCfg() -> NetCfg {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (hostMode, requiredHostMode) = onionHosts.hostMode
|
||||
let sessionMode = networkSessionModeGroupDefault.get()
|
||||
let smpProxyMode = networkSMPProxyModeGroupDefault.get()
|
||||
let smpProxyFallback = networkSMPProxyFallbackGroupDefault.get()
|
||||
let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
|
||||
let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
|
||||
let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
|
||||
@@ -295,6 +313,8 @@ public func getNetCfg() -> NetCfg {
|
||||
hostMode: hostMode,
|
||||
requiredHostMode: requiredHostMode,
|
||||
sessionMode: sessionMode,
|
||||
smpProxyMode: smpProxyMode,
|
||||
smpProxyFallback: smpProxyFallback,
|
||||
tcpConnectTimeout: tcpConnectTimeout,
|
||||
tcpTimeout: tcpTimeout,
|
||||
tcpTimeoutPerKb: tcpTimeoutPerKb,
|
||||
@@ -309,6 +329,8 @@ public func getNetCfg() -> NetCfg {
|
||||
public func setNetCfg(_ cfg: NetCfg) {
|
||||
networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg))
|
||||
networkSessionModeGroupDefault.set(cfg.sessionMode)
|
||||
networkSMPProxyModeGroupDefault.set(cfg.smpProxyMode)
|
||||
networkSMPProxyFallbackGroupDefault.set(cfg.smpProxyFallback)
|
||||
groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
|
||||
|
||||
@@ -1520,7 +1520,12 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
public var ready: Bool { get { activeConn?.connStatus == .ready } }
|
||||
public var active: Bool { get { contactStatus == .active } }
|
||||
public var sendMsgEnabled: Bool { get {
|
||||
(ready && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false))
|
||||
(
|
||||
ready
|
||||
&& active
|
||||
&& !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false)
|
||||
&& !(activeConn?.connDisabled ?? true)
|
||||
)
|
||||
|| nextSendGrpInv
|
||||
} }
|
||||
public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } }
|
||||
@@ -1613,15 +1618,20 @@ public struct Connection: Decodable {
|
||||
public var pqEncryption: Bool
|
||||
public var pqSndEnabled: Bool?
|
||||
public var pqRcvEnabled: Bool?
|
||||
public var authErrCounter: Int
|
||||
|
||||
public var connectionStats: ConnectionStats? = nil
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled
|
||||
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter
|
||||
}
|
||||
|
||||
public var id: ChatId { get { ":\(connId)" } }
|
||||
|
||||
public var connDisabled: Bool {
|
||||
authErrCounter >= 10 // authErrDisableCount in core
|
||||
}
|
||||
|
||||
public var connPQEnabled: Bool {
|
||||
pqSndEnabled == true && pqRcvEnabled == true
|
||||
}
|
||||
@@ -1634,7 +1644,8 @@ public struct Connection: Decodable {
|
||||
connLevel: 0,
|
||||
viaGroupLink: false,
|
||||
pqSupport: false,
|
||||
pqEncryption: false
|
||||
pqEncryption: false,
|
||||
authErrCounter: 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2633,6 +2644,7 @@ public struct CIMeta: Decodable {
|
||||
public var itemTs: Date
|
||||
var itemText: String
|
||||
public var itemStatus: CIStatus
|
||||
public var sentViaProxy: Bool?
|
||||
public var createdAt: Date
|
||||
public var updatedAt: Date
|
||||
public var itemForwarded: CIForwardedFrom?
|
||||
@@ -2722,7 +2734,8 @@ public enum CIStatus: Decodable {
|
||||
case sndSent(sndProgress: SndCIStatusProgress)
|
||||
case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress)
|
||||
case sndErrorAuth
|
||||
case sndError(agentError: String)
|
||||
case sndError(agentError: SndError)
|
||||
case sndWarning(agentError: SndError)
|
||||
case rcvNew
|
||||
case rcvRead
|
||||
case invalid(text: String)
|
||||
@@ -2734,6 +2747,7 @@ public enum CIStatus: Decodable {
|
||||
case .sndRcvd: return "sndRcvd"
|
||||
case .sndErrorAuth: return "sndErrorAuth"
|
||||
case .sndError: return "sndError"
|
||||
case .sndWarning: return "sndWarning"
|
||||
case .rcvNew: return "rcvNew"
|
||||
case .rcvRead: return "rcvRead"
|
||||
case .invalid: return "invalid"
|
||||
@@ -2750,7 +2764,8 @@ public enum CIStatus: Decodable {
|
||||
case .badMsgHash: return ("checkmark", .red)
|
||||
}
|
||||
case .sndErrorAuth: return ("multiply", .red)
|
||||
case .sndError: return ("exclamationmark.triangle.fill", .yellow)
|
||||
case .sndError: return ("multiply", .red)
|
||||
case .sndWarning: return ("exclamationmark.triangle.fill", .orange)
|
||||
case .rcvNew: return ("circlebadge.fill", Color.accentColor)
|
||||
case .rcvRead: return nil
|
||||
case .invalid: return ("questionmark", metaColor)
|
||||
@@ -2768,7 +2783,11 @@ public enum CIStatus: Decodable {
|
||||
)
|
||||
case let .sndError(agentError): return (
|
||||
NSLocalizedString("Message delivery error", comment: "item status text"),
|
||||
String.localizedStringWithFormat(NSLocalizedString("Unexpected error: %@", comment: "item status description"), agentError)
|
||||
agentError.errorInfo
|
||||
)
|
||||
case let .sndWarning(agentError): return (
|
||||
NSLocalizedString("Message delivery warning", comment: "item status text"),
|
||||
agentError.errorInfo
|
||||
)
|
||||
case .rcvNew: return nil
|
||||
case .rcvRead: return nil
|
||||
@@ -2780,6 +2799,42 @@ public enum CIStatus: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SndError: Decodable {
|
||||
case auth
|
||||
case quota
|
||||
case expired
|
||||
case relay(srvError: SrvError)
|
||||
case proxy(proxyServer: String, srvError: SrvError)
|
||||
case proxyRelay(proxyServer: String, srvError: SrvError)
|
||||
case other(sndError: String)
|
||||
|
||||
public var errorInfo: String {
|
||||
switch self {
|
||||
case .auth: NSLocalizedString("Wrong key or unknown connection - most likely this connection is deleted.", comment: "snd error text")
|
||||
case .quota: NSLocalizedString("Capacity exceeded - recipient did not receive previously sent messages.", comment: "snd error text")
|
||||
case .expired: NSLocalizedString("Network issues - message expired after many attempts to send it.", comment: "snd error text")
|
||||
case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("Destination server error: %@", comment: "snd error text"), srvError.errorInfo)
|
||||
case let .proxy(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nError: %@", comment: "snd error text"), proxyServer, srvError.errorInfo)
|
||||
case let .proxyRelay(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nDestination server error: %@", comment: "snd error text"), proxyServer, srvError.errorInfo)
|
||||
case let .other(sndError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "snd error text"), sndError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum SrvError: Decodable {
|
||||
case host
|
||||
case version
|
||||
case other(srvError: String)
|
||||
|
||||
public var errorInfo: String {
|
||||
switch self {
|
||||
case .host: NSLocalizedString("Server address is incompatible with network settings.", comment: "srv error text.")
|
||||
case .version: NSLocalizedString("Server version is incompatible with network settings.", comment: "srv error text")
|
||||
case let .other(srvError): srvError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MsgReceiptStatus: String, Decodable {
|
||||
case ok
|
||||
case badMsgHash
|
||||
@@ -3898,4 +3953,5 @@ public struct ChatItemVersion: Decodable {
|
||||
public struct MemberDeliveryStatus: Decodable {
|
||||
public var groupMemberId: Int64
|
||||
public var memberDeliveryStatus: CIStatus
|
||||
public var sentViaProxy: Bool?
|
||||
}
|
||||
|
||||
+69
-11
@@ -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 {
|
||||
|
||||
+76
-8
@@ -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_)
|
||||
|
||||
+17
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -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
|
||||
|
||||
+1
-1
@@ -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 &&
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ fun CICallItemView(
|
||||
CICallStatus.Error -> {}
|
||||
}
|
||||
|
||||
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false)
|
||||
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-5
@@ -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)
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@ fun CIGroupInvitationView(
|
||||
}
|
||||
}
|
||||
|
||||
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false)
|
||||
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-14
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-30
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-13
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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 {
|
||||
" "
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+152
-1
@@ -43,6 +43,8 @@ fun NetworkAndServersView() {
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
|
||||
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
|
||||
val smpProxyMode = remember { mutableStateOf(netCfg.smpProxyMode) }
|
||||
val smpProxyFallback = remember { mutableStateOf(netCfg.smpProxyFallback) }
|
||||
|
||||
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
|
||||
NetworkAndServersLayout(
|
||||
@@ -51,6 +53,8 @@ fun NetworkAndServersView() {
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = onionHosts,
|
||||
sessionMode = sessionMode,
|
||||
smpProxyMode = smpProxyMode,
|
||||
smpProxyFallback = smpProxyFallback,
|
||||
proxyPort = proxyPort,
|
||||
toggleSocksProxy = { enable ->
|
||||
if (enable) {
|
||||
@@ -137,6 +141,59 @@ fun NetworkAndServersView() {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateSMPProxyMode = {
|
||||
if (smpProxyMode.value == it) return@NetworkAndServersLayout
|
||||
val prevValue = smpProxyMode.value
|
||||
smpProxyMode.value = it
|
||||
val startsWith = when (it) {
|
||||
SMPProxyMode.Always -> generalGetString(MR.strings.network_smp_proxy_mode_always_description)
|
||||
SMPProxyMode.Unknown -> generalGetString(MR.strings.network_smp_proxy_mode_unknown_description)
|
||||
SMPProxyMode.Unprotected -> generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description)
|
||||
SMPProxyMode.Never -> generalGetString(MR.strings.network_smp_proxy_mode_never_description)
|
||||
}
|
||||
showUpdateNetworkSettingsDialog(
|
||||
title = generalGetString(MR.strings.update_network_smp_proxy_mode_question),
|
||||
startsWith,
|
||||
onDismiss = { smpProxyMode.value = prevValue }
|
||||
) {
|
||||
withBGApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().copy(smpProxyMode = it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
if (res) {
|
||||
chatModel.controller.setNetCfg(newCfg)
|
||||
smpProxyMode.value = it
|
||||
} else {
|
||||
smpProxyMode.value = prevValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateSMPProxyFallback = {
|
||||
if (smpProxyFallback.value == it) return@NetworkAndServersLayout
|
||||
val prevValue = smpProxyFallback.value
|
||||
smpProxyFallback.value = it
|
||||
val startsWith = when (it) {
|
||||
SMPProxyFallback.Allow -> generalGetString(MR.strings.network_smp_proxy_fallback_allow_description)
|
||||
SMPProxyFallback.AllowProtected -> generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description)
|
||||
SMPProxyFallback.Prohibit -> generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description)
|
||||
}
|
||||
showUpdateNetworkSettingsDialog(
|
||||
title = generalGetString(MR.strings.update_network_smp_proxy_fallback_question),
|
||||
startsWith,
|
||||
onDismiss = { smpProxyFallback.value = prevValue }
|
||||
) {
|
||||
withBGApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().copy(smpProxyFallback = it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
if (res) {
|
||||
chatModel.controller.setNetCfg(newCfg)
|
||||
smpProxyFallback.value = it
|
||||
} else {
|
||||
smpProxyFallback.value = prevValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -147,16 +204,22 @@ fun NetworkAndServersView() {
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
smpProxyMode: MutableState<SMPProxyMode>,
|
||||
smpProxyFallback: MutableState<SMPProxyFallback>,
|
||||
proxyPort: State<Int>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
updateSMPProxyMode: (SMPProxyMode) -> Unit,
|
||||
updateSMPProxyFallback: (SMPProxyFallback) -> Unit,
|
||||
) {
|
||||
val m = chatModel
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
|
||||
|
||||
AppBarTitle(stringResource(MR.strings.network_and_servers))
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
|
||||
@@ -165,7 +228,6 @@ fun NetworkAndServersView() {
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } })
|
||||
|
||||
if (currentRemoteHost == null) {
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
|
||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false)
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||
if (developerTools) {
|
||||
@@ -188,6 +250,18 @@ fun NetworkAndServersView() {
|
||||
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||
}
|
||||
|
||||
// if (currentRemoteHost == null) {
|
||||
// SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) {
|
||||
// SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode)
|
||||
// SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) })
|
||||
// SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
|
||||
// }
|
||||
// SectionCustomFooter {
|
||||
// Text(stringResource(MR.strings.private_routing_explanation))
|
||||
// }
|
||||
// Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||
// }
|
||||
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
|
||||
}
|
||||
@@ -452,6 +526,79 @@ private fun SessionModePicker(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPProxyModePicker(
|
||||
smpProxyMode: MutableState<SMPProxyMode>,
|
||||
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||
updateSMPProxyMode: (SMPProxyMode) -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val values = remember {
|
||||
SMPProxyMode.values().map {
|
||||
when (it) {
|
||||
SMPProxyMode.Always -> ValueTitleDesc(SMPProxyMode.Always, generalGetString(MR.strings.network_smp_proxy_mode_always), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_always_description), density))
|
||||
SMPProxyMode.Unknown -> ValueTitleDesc(SMPProxyMode.Unknown, generalGetString(MR.strings.network_smp_proxy_mode_unknown), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unknown_description), density))
|
||||
SMPProxyMode.Unprotected -> ValueTitleDesc(SMPProxyMode.Unprotected, generalGetString(MR.strings.network_smp_proxy_mode_unprotected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description), density))
|
||||
SMPProxyMode.Never -> ValueTitleDesc(SMPProxyMode.Never, generalGetString(MR.strings.network_smp_proxy_mode_never), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_never_description), density))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_smp_proxy_mode_private_routing),
|
||||
smpProxyMode,
|
||||
values,
|
||||
icon = painterResource(MR.images.ic_settings_ethernet),
|
||||
onSelected = {
|
||||
showModal {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing))
|
||||
SectionViewSelectable(null, smpProxyMode, values, updateSMPProxyMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPProxyFallbackPicker(
|
||||
smpProxyFallback: MutableState<SMPProxyFallback>,
|
||||
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||
updateSMPProxyFallback: (SMPProxyFallback) -> Unit,
|
||||
enabled: State<Boolean>,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val values = remember {
|
||||
SMPProxyFallback.values().map {
|
||||
when (it) {
|
||||
SMPProxyFallback.Allow -> ValueTitleDesc(SMPProxyFallback.Allow, generalGetString(MR.strings.network_smp_proxy_fallback_allow), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_description), density))
|
||||
SMPProxyFallback.AllowProtected -> ValueTitleDesc(SMPProxyFallback.AllowProtected, generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description), density))
|
||||
SMPProxyFallback.Prohibit -> ValueTitleDesc(SMPProxyFallback.Prohibit, generalGetString(MR.strings.network_smp_proxy_fallback_prohibit), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description), density))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_smp_proxy_fallback_allow_downgrade),
|
||||
smpProxyFallback,
|
||||
values,
|
||||
icon = painterResource(MR.images.ic_arrows_left_right),
|
||||
enabled = enabled,
|
||||
onSelected = {
|
||||
showModal {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade))
|
||||
SectionViewSelectable(null, smpProxyFallback, values, updateSMPProxyFallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) {
|
||||
Row(
|
||||
@@ -506,8 +653,12 @@ fun PreviewNetworkAndServersLayout() {
|
||||
toggleSocksProxy = {},
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
||||
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
|
||||
smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) },
|
||||
useOnion = {},
|
||||
updateSessionMode = {},
|
||||
updateSMPProxyMode = {},
|
||||
updateSMPProxyFallback = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,8 +255,20 @@
|
||||
|
||||
<!-- Chat Alerts - ChatItemView.kt -->
|
||||
<string name="message_delivery_error_title">Message delivery error</string>
|
||||
<string name="message_delivery_warning_title">Message delivery warning</string>
|
||||
<string name="message_delivery_error_desc">Most likely this contact has deleted the connection with you.</string>
|
||||
|
||||
<!-- CIStatus errors -->
|
||||
<string name="ci_status_other_error">Error: %1$s</string>
|
||||
<string name="snd_error_auth">Wrong key or unknown connection - most likely this connection is deleted.</string>
|
||||
<string name="snd_error_quota">Capacity exceeded - recipient did not receive previously sent messages.</string>
|
||||
<string name="snd_error_expired">Network issues - message expired after many attempts to send it.</string>
|
||||
<string name="snd_error_relay">Destination server error: %1$s</string>
|
||||
<string name="snd_error_proxy">Forwarding server: %1$s\nError: %2$s</string>
|
||||
<string name="snd_error_proxy_relay">Forwarding server: %1$s\nDestination server error: %2$s</string>
|
||||
<string name="srv_error_host">Server address is incompatible with network settings.</string>
|
||||
<string name="srv_error_version">Server version is incompatible with network settings.</string>
|
||||
|
||||
<!-- Chat Actions - ChatItemView.kt (and general) -->
|
||||
<string name="reply_verb">Reply</string>
|
||||
<string name="share_verb">Share</string>
|
||||
@@ -710,6 +722,26 @@
|
||||
<string name="update_network_session_mode_question">Update transport isolation mode?</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string>
|
||||
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]></string>
|
||||
<string name="network_smp_proxy_mode_private_routing">Private routing</string>
|
||||
<string name="network_smp_proxy_mode_always">Always</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Unknown relays</string>
|
||||
<string name="network_smp_proxy_mode_unprotected">Unprotected</string>
|
||||
<string name="network_smp_proxy_mode_never">Never</string>
|
||||
<string name="network_smp_proxy_mode_always_description">Always use private routing.</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">Use private routing with unknown servers.</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Use private routing with unknown servers when IP address is not protected.</string>
|
||||
<string name="network_smp_proxy_mode_never_description">Do NOT use private routing.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Message routing mode</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Allow downgrade</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Yes</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">When IP hidden</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">No</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">Send messages directly when your or destination server does not support private routing.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">Send messages directly when IP address is protected and your or destination server does not support private routing.</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">Do NOT send messages directly, even if your or destination server does not support private routing.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Message routing fallback</string>
|
||||
<string name="private_routing_show_message_status">Show message status</string>
|
||||
<string name="private_routing_explanation">To protect your IP address, private routing uses your SMP servers to deliver messages.</string>
|
||||
<string name="appearance_settings">Appearance</string>
|
||||
<string name="customize_theme_title">Customize theme</string>
|
||||
<string name="theme_colors_section_title">THEME COLORS</string>
|
||||
@@ -1035,6 +1067,7 @@
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
<string name="settings_section_title_profile_images">Profile images</string>
|
||||
<string name="settings_section_title_messages">MESSAGES AND FILES</string>
|
||||
<string name="settings_section_title_private_message_routing">PRIVATE MESSAGE ROUTING</string>
|
||||
<string name="settings_section_title_calls">CALLS</string>
|
||||
<string name="settings_section_title_network_connection">Network connection</string>
|
||||
<string name="settings_section_title_incognito">Incognito mode</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#5f6368"><path d="M629-446.5H235.48q-13.79 0-23.64-9.79-9.84-9.79-9.84-23.5t9.84-23.71q9.85-10 23.64-10H629L455.79-686.71Q445.5-697 445.25-710.5t10.25-24.48q10.5-10.52 24-10.27t23.81 10.57L734.1-503.59q4.9 4.91 7.65 10.97 2.75 6.06 2.75 12.78 0 6.71-2.75 12.78Q739-461 734.5-456.5l-231 231q-11 11-23.75 10.5t-23.25-11.02Q446-237 446-250.42q0-13.41 10.5-23.58L629-446.5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 472 B |
@@ -0,0 +1,4 @@
|
||||
<svg height="24" viewBox="0 -960 960 960" width="24" fill="#000000" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 831.5 -480 L 657 -656.5 C 651.667 -662.167 648.917 -668.833 648.75 -676.5 C 648.583 -684.167 651.333 -690.833 657 -696.5 C 662.667 -702.5 669.333 -705.5 677 -705.5 C 684.667 -705.5 691.5 -702.667 697.5 -697 L 894 -500.5 C 897 -497.167 899.25 -493.917 900.75 -490.75 C 902.25 -487.583 903 -484 903 -480 C 903 -476 902.25 -472.417 900.75 -469.25 C 899.25 -466.083 897 -463 894 -460 L 697.5 -263.5 C 691.5 -257.5 684.667 -254.583 677 -254.75 C 669.333 -254.917 662.667 -257.833 657 -263.5 C 651 -269.5 648.167 -276.25 648.5 -283.75 C 648.833 -291.25 651.667 -297.833 657 -303.5 L 831.5 -480 Z M 128.5 -480 L 303 -303.5 C 308.333 -297.833 311.083 -291.167 311.25 -283.5 C 311.417 -275.833 308.667 -269.167 303 -263.5 C 297.333 -257.5 290.667 -254.5 283 -254.5 C 275.333 -254.5 268.667 -257.5 263 -263.5 L 66.5 -460 C 63.167 -463 60.833 -466.083 59.5 -469.25 C 58.167 -472.417 57.5 -476 57.5 -480 C 57.5 -484 58.167 -487.583 59.5 -490.75 C 60.833 -493.917 63.167 -497.167 66.5 -500.5 L 263 -697 C 268.667 -702.667 275.333 -705.417 283 -705.25 C 290.667 -705.083 297.5 -702.167 303.5 -696.5 C 309.167 -690.5 311.833 -683.75 311.5 -676.25 C 311.167 -668.75 308.333 -662.167 303 -656.5 L 128.5 -480 Z" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/>
|
||||
<rect x="123" y="-514" width="711.266" height="68" style="stroke: rgb(0, 0, 0);" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=5.7.3
|
||||
android.version_code=206
|
||||
android.version_name=5.8-beta.0
|
||||
android.version_code=208
|
||||
|
||||
desktop.version_name=5.7.3
|
||||
desktop.version_code=44
|
||||
desktop.version_name=5.8-beta.0
|
||||
desktop.version_code=45
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||
@@ -14,6 +14,7 @@ module Directory.Events
|
||||
DirectoryRole (..),
|
||||
SDirectoryRole (..),
|
||||
crDirectoryEvent,
|
||||
directoryCmdTag,
|
||||
viewName,
|
||||
)
|
||||
where
|
||||
@@ -21,6 +22,8 @@ where
|
||||
import Control.Applicative ((<|>))
|
||||
import Data.Attoparsec.Text (Parser)
|
||||
import qualified Data.Attoparsec.Text as A
|
||||
import Data.Char (isSpace)
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -34,13 +37,11 @@ import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Data.Char (isSpace)
|
||||
import Data.Either (fromRight)
|
||||
|
||||
data DirectoryEvent
|
||||
= DEContactConnected Contact
|
||||
| DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole}
|
||||
| DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember}
|
||||
| DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember}
|
||||
| DEGroupUpdated {contactId :: ContactId, fromGroup :: GroupInfo, toGroup :: GroupInfo}
|
||||
| DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed
|
||||
| DEServiceRoleChanged GroupInfo GroupMemberRole
|
||||
@@ -140,25 +141,26 @@ directoryCmdP =
|
||||
cmdStrP =
|
||||
(tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t)))
|
||||
<|> pure (ADC SDRUser DCUnknownCommand)
|
||||
tagP = A.takeTill (== ' ') >>= \case
|
||||
"help" -> u DCHelp_
|
||||
"h" -> u DCHelp_
|
||||
"next" -> u DCSearchNext_
|
||||
"all" -> u DCAllGroups_
|
||||
"new" -> u DCRecentGroups_
|
||||
"submit" -> u DCSubmitGroup_
|
||||
"confirm" -> u DCConfirmDuplicateGroup_
|
||||
"list" -> u DCListUserGroups_
|
||||
"ls" -> u DCListUserGroups_
|
||||
"delete" -> u DCDeleteGroup_
|
||||
"approve" -> su DCApproveGroup_
|
||||
"reject" -> su DCRejectGroup_
|
||||
"suspend" -> su DCSuspendGroup_
|
||||
"resume" -> su DCResumeGroup_
|
||||
"last" -> su DCListLastGroups_
|
||||
"exec" -> su DCExecuteCommand_
|
||||
"x" -> su DCExecuteCommand_
|
||||
_ -> fail "bad command tag"
|
||||
tagP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"help" -> u DCHelp_
|
||||
"h" -> u DCHelp_
|
||||
"next" -> u DCSearchNext_
|
||||
"all" -> u DCAllGroups_
|
||||
"new" -> u DCRecentGroups_
|
||||
"submit" -> u DCSubmitGroup_
|
||||
"confirm" -> u DCConfirmDuplicateGroup_
|
||||
"list" -> u DCListUserGroups_
|
||||
"ls" -> u DCListUserGroups_
|
||||
"delete" -> u DCDeleteGroup_
|
||||
"approve" -> su DCApproveGroup_
|
||||
"reject" -> su DCRejectGroup_
|
||||
"suspend" -> su DCSuspendGroup_
|
||||
"resume" -> su DCResumeGroup_
|
||||
"last" -> su DCListLastGroups_
|
||||
"exec" -> su DCExecuteCommand_
|
||||
"x" -> su DCExecuteCommand_
|
||||
_ -> fail "bad command tag"
|
||||
where
|
||||
u = pure . ADCT SDRUser
|
||||
su = pure . ADCT SDRSuperUser
|
||||
@@ -192,3 +194,23 @@ directoryCmdP =
|
||||
|
||||
viewName :: String -> String
|
||||
viewName n = if ' ' `elem` n then "'" <> n <> "'" else n
|
||||
|
||||
directoryCmdTag :: DirectoryCmd r -> Text
|
||||
directoryCmdTag = \case
|
||||
DCHelp -> "help"
|
||||
DCSearchGroup _ -> "search"
|
||||
DCSearchNext -> "next"
|
||||
DCAllGroups -> "all"
|
||||
DCRecentGroups -> "new"
|
||||
DCSubmitGroup _ -> "submit"
|
||||
DCConfirmDuplicateGroup {} -> "confirm"
|
||||
DCListUserGroups -> "list"
|
||||
DCDeleteGroup {} -> "delete"
|
||||
DCApproveGroup {} -> "approve"
|
||||
DCRejectGroup {} -> "reject"
|
||||
DCSuspendGroup {} -> "suspend"
|
||||
DCResumeGroup {} -> "resume"
|
||||
DCListLastGroups _ -> "last"
|
||||
DCExecuteCommand _ -> "exec"
|
||||
DCUnknownCommand -> "unknown"
|
||||
DCCommandError _ -> "error"
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
|
||||
module Directory.Service
|
||||
( welcomeGetOpts,
|
||||
@@ -15,6 +15,7 @@ where
|
||||
import Control.Concurrent (forkIO)
|
||||
import Control.Concurrent.Async
|
||||
import Control.Concurrent.STM
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Maybe (fromMaybe, maybeToList)
|
||||
@@ -37,7 +38,7 @@ import Simplex.Chat.Options
|
||||
import Simplex.Chat.Protocol (MsgContent (..))
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.View (serializeChatResponse, simplexChatContact)
|
||||
import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
@@ -96,9 +97,11 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
DEUnsupportedMessage _ct _ciId -> pure ()
|
||||
DEItemEditIgnored _ct -> pure ()
|
||||
DEItemDeleteIgnored _ct -> pure ()
|
||||
DEContactCommand ct ciId aCmd -> case aCmd of
|
||||
ADC SDRUser cmd -> deUserCommand env ct ciId cmd
|
||||
ADC SDRSuperUser cmd -> deSuperUserCommand ct ciId cmd
|
||||
DEContactCommand ct ciId (ADC sUser cmd) -> do
|
||||
logInfo $ "command received " <> directoryCmdTag cmd
|
||||
case sUser of
|
||||
SDRUser -> deUserCommand env ct ciId cmd
|
||||
SDRSuperUser -> deSuperUserCommand ct ciId cmd
|
||||
where
|
||||
withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId
|
||||
notifySuperUsers s = withSuperUsers $ \contactId -> sendMessage' cc contactId s
|
||||
@@ -107,7 +110,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
withGroupReg GroupInfo {groupId, localDisplayName} err action = do
|
||||
atomically (getGroupReg st groupId) >>= \case
|
||||
Just gr -> action gr
|
||||
Nothing -> putStrLn $ T.unpack $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId
|
||||
Nothing -> logError $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId
|
||||
groupInfoText GroupProfile {displayName = n, fullName = fn, description = d} =
|
||||
n <> (if n == fn || T.null fn then "" else " (" <> fn <> ")") <> maybe "" ("\nWelcome message:\n" <>) d
|
||||
userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName
|
||||
@@ -152,23 +155,25 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
|
||||
deContactConnected :: Contact -> IO ()
|
||||
deContactConnected ct = when (contactDirect ct) $ do
|
||||
unless testing $ putStrLn $ T.unpack (localDisplayName' ct) <> " connected"
|
||||
logInfo $ (viewContactName ct) <> " connected"
|
||||
sendMessage cc ct $
|
||||
"Welcome to " <> serviceName <> " service!\n\
|
||||
\Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\
|
||||
\For example, send _privacy_ to find groups about privacy.\n\
|
||||
\Or send */all* or */new* to list groups.\n\n\
|
||||
\Content and privacy policy: https://simplex.chat/docs/directory.html"
|
||||
("Welcome to " <> serviceName <> " service!\n")
|
||||
<> "Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\
|
||||
\For example, send _privacy_ to find groups about privacy.\n\
|
||||
\Or send */all* or */new* to list groups.\n\n\
|
||||
\Content and privacy policy: https://simplex.chat/docs/directory.html"
|
||||
|
||||
deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO ()
|
||||
deGroupInvitation ct g@GroupInfo {groupProfile = GroupProfile {displayName, fullName}} fromMemberRole memberRole = do
|
||||
logInfo $ "invited to group " <> viewGroupName g <> " by " <> viewContactName ct
|
||||
case badRolesMsg $ groupRolesStatus fromMemberRole memberRole of
|
||||
Just msg -> sendMessage cc ct msg
|
||||
Nothing -> getDuplicateGroup g >>= \case
|
||||
Just DGUnique -> processInvitation ct g
|
||||
Just DGRegistered -> askConfirmation
|
||||
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
|
||||
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
|
||||
Nothing ->
|
||||
getDuplicateGroup g >>= \case
|
||||
Just DGUnique -> processInvitation ct g
|
||||
Just DGRegistered -> askConfirmation
|
||||
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
|
||||
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
|
||||
where
|
||||
askConfirmation = do
|
||||
ugrId <- addGroupReg st ct g GRSPendingConfirmation
|
||||
@@ -205,7 +210,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
_ -> Nothing
|
||||
|
||||
deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO ()
|
||||
deServiceJoinedGroup ctId g owner =
|
||||
deServiceJoinedGroup ctId g owner = do
|
||||
logInfo $ "service joined group " <> viewGroupName g
|
||||
withGroupReg g "joined group" $ \gr ->
|
||||
when (ctId `isOwner` gr) $ do
|
||||
setGroupRegOwner st gr owner
|
||||
@@ -214,7 +220,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case
|
||||
CRGroupLinkCreated {connReqContact} -> do
|
||||
setGroupStatus st gr GRSPendingUpdate
|
||||
notifyOwner gr
|
||||
notifyOwner
|
||||
gr
|
||||
"Created the public link to join the group via this directory service that is always online.\n\n\
|
||||
\Please add it to the group welcome message.\n\
|
||||
\For example, add:"
|
||||
@@ -228,24 +235,26 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
_ -> notifyOwner gr $ unexpectedError "can't create group link"
|
||||
|
||||
deGroupUpdated :: ContactId -> GroupInfo -> GroupInfo -> IO ()
|
||||
deGroupUpdated ctId fromGroup toGroup =
|
||||
deGroupUpdated ctId fromGroup toGroup = do
|
||||
logInfo $ "group updated " <> viewGroupName toGroup
|
||||
unless (sameProfile p p') $ do
|
||||
withGroupReg toGroup "group updated" $ \gr -> do
|
||||
let userGroupRef = userGroupReference gr toGroup
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSPendingConfirmation -> pure ()
|
||||
GRSProposed -> pure ()
|
||||
GRSPendingUpdate -> groupProfileUpdate >>= \case
|
||||
GPNoServiceLink ->
|
||||
when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message."
|
||||
GPServiceLinkAdded
|
||||
| ctId `isOwner` gr -> groupLinkAdded gr
|
||||
| otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself."
|
||||
GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it."
|
||||
GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr
|
||||
GPServiceLinkError -> do
|
||||
when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers."
|
||||
putStrLn $ "Error: no group link for " <> userGroupRef
|
||||
GRSPendingUpdate ->
|
||||
groupProfileUpdate >>= \case
|
||||
GPNoServiceLink ->
|
||||
when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message."
|
||||
GPServiceLinkAdded
|
||||
| ctId `isOwner` gr -> groupLinkAdded gr
|
||||
| otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself."
|
||||
GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it."
|
||||
GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr
|
||||
GPServiceLinkError -> do
|
||||
when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers."
|
||||
logError $ "Error: no group link for " <> T.pack userGroupRef
|
||||
GRSPendingApproval n -> processProfileChange gr $ n + 1
|
||||
GRSActive -> processProfileChange gr 1
|
||||
GRSSuspended -> processProfileChange gr 1
|
||||
@@ -288,7 +297,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved."
|
||||
notifySuperUsers $ "The group " <> groupRef <> " is updated."
|
||||
checkRolesSendToApprove gr n'
|
||||
GPServiceLinkError -> putStrLn $ "Error: no group link for " <> groupRef <> " pending approval."
|
||||
GPServiceLinkError -> logError $ "Error: no group link for " <> T.pack groupRef <> " pending approval."
|
||||
groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId)
|
||||
where
|
||||
profileUpdate = \case
|
||||
@@ -297,7 +306,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact
|
||||
hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p
|
||||
hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p'
|
||||
in if
|
||||
in if
|
||||
| hadLinkBefore && hasLinkNow -> GPHasServiceLink
|
||||
| hadLinkBefore -> GPServiceLinkRemoved
|
||||
| hasLinkNow -> GPServiceLinkAdded
|
||||
@@ -311,18 +320,20 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
|
||||
sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO ()
|
||||
sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId} gaId = do
|
||||
ct_ <- getContact cc dbContactId
|
||||
ct_ <- getContact cc dbContactId
|
||||
gr_ <- getGroupAndSummary cc dbGroupId
|
||||
let membersStr = maybe "" (\(_, s) -> "_" <> tshow (currentMembers s) <> " members_\n") gr_
|
||||
text = maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_
|
||||
<> "\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:"
|
||||
text =
|
||||
maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_
|
||||
<> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:")
|
||||
msg = maybe (MCText text) (\image -> MCImage {text, image}) image'
|
||||
withSuperUsers $ \cId -> do
|
||||
sendComposedMessage' cc cId Nothing msg
|
||||
sendMessage' cc cId $ "/approve " <> show dbGroupId <> ":" <> viewName (T.unpack displayName) <> " " <> show gaId
|
||||
|
||||
deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO ()
|
||||
deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole =
|
||||
deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do
|
||||
logInfo $ "contact ID " <> tshow ctId <> " role changed in group " <> viewGroupName g <> " to " <> tshow contactRole
|
||||
withGroupReg g "contact role changed" $ \gr -> do
|
||||
let userGroupRef = userGroupReference gr g
|
||||
uCtRole = "Your role in the group " <> userGroupRef <> " is changed to " <> ctRole
|
||||
@@ -348,6 +359,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
|
||||
deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO ()
|
||||
deServiceRoleChanged g serviceRole = do
|
||||
logInfo $ "service role changed in group " <> viewGroupName g <> " to " <> tshow serviceRole
|
||||
withGroupReg g "service role changed" $ \gr -> do
|
||||
let userGroupRef = userGroupReference gr g
|
||||
uSrvRole = serviceName <> " role in the group " <> userGroupRef <> " is changed to " <> srvRole
|
||||
@@ -371,11 +383,12 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
srvRole = "*" <> B.unpack (strEncode serviceRole) <> "*"
|
||||
suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")."
|
||||
whenContactIsOwner gr action =
|
||||
getGroupMember gr >>=
|
||||
mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action)
|
||||
getGroupMember gr
|
||||
>>= mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action)
|
||||
|
||||
deContactRemovedFromGroup :: ContactId -> GroupInfo -> IO ()
|
||||
deContactRemovedFromGroup ctId g =
|
||||
deContactRemovedFromGroup ctId g = do
|
||||
logInfo $ "contact ID " <> tshow ctId <> " removed from group " <> viewGroupName g
|
||||
withGroupReg g "contact removed" $ \gr -> do
|
||||
when (ctId `isOwner` gr) $ do
|
||||
setGroupStatus st gr GRSRemoved
|
||||
@@ -383,7 +396,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)."
|
||||
|
||||
deContactLeftGroup :: ContactId -> GroupInfo -> IO ()
|
||||
deContactLeftGroup ctId g =
|
||||
deContactLeftGroup ctId g = do
|
||||
logInfo $ "contact ID " <> tshow ctId <> " left group " <> viewGroupName g
|
||||
withGroupReg g "contact left" $ \gr -> do
|
||||
when (ctId `isOwner` gr) $ do
|
||||
setGroupStatus st gr GRSRemoved
|
||||
@@ -391,7 +405,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)."
|
||||
|
||||
deServiceRemovedFromGroup :: GroupInfo -> IO ()
|
||||
deServiceRemovedFromGroup g =
|
||||
deServiceRemovedFromGroup g = do
|
||||
logInfo $ "service removed from group " <> viewGroupName g
|
||||
withGroupReg g "service removed" $ \gr -> do
|
||||
setGroupStatus st gr GRSRemoved
|
||||
notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory."
|
||||
@@ -402,11 +417,15 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
DCHelp ->
|
||||
sendMessage cc ct $
|
||||
"You must be the owner to add the group to the directory:\n\
|
||||
\1. Invite " <> serviceName <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\
|
||||
\2. " <> serviceName <> " bot will create a public group link for the new members to join even when you are offline.\n\
|
||||
\3. You will then need to add this link to the group welcome message.\n\
|
||||
\4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\
|
||||
\Start from inviting the bot to your group as admin - it will guide you through the process"
|
||||
\1. Invite "
|
||||
<> serviceName
|
||||
<> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\
|
||||
\2. "
|
||||
<> serviceName
|
||||
<> " bot will create a public group link for the new members to join even when you are offline.\n\
|
||||
\3. You will then need to add this link to the group welcome message.\n\
|
||||
\4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\
|
||||
\Start from inviting the bot to your group as admin - it will guide you through the process"
|
||||
DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s
|
||||
DCSearchNext ->
|
||||
atomically (TM.lookup (contactId' ct) searchRequests) >>= \case
|
||||
@@ -434,13 +453,13 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found"
|
||||
Just g@GroupInfo {groupProfile = GroupProfile {displayName}}
|
||||
| displayName == gName ->
|
||||
readTVarIO groupRegStatus >>= \case
|
||||
GRSPendingConfirmation -> do
|
||||
getDuplicateGroup g >>= \case
|
||||
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
|
||||
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
|
||||
_ -> processInvitation ct g
|
||||
_ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation."
|
||||
readTVarIO groupRegStatus >>= \case
|
||||
GRSPendingConfirmation -> do
|
||||
getDuplicateGroup g >>= \case
|
||||
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
|
||||
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
|
||||
_ -> processInvitation ct g
|
||||
_ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation."
|
||||
| otherwise -> sendReply $ "Group ID " <> show ugrId <> " has the display name " <> T.unpack displayName
|
||||
DCListUserGroups ->
|
||||
atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do
|
||||
@@ -462,7 +481,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
let gs' = takeTop searchResults gs
|
||||
moreGroups = length gs - length gs'
|
||||
more = if moreGroups > 0 then ", sending top " <> show (length gs') else ""
|
||||
sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "."
|
||||
sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "."
|
||||
updateSearchRequest (STSearch s) $ groupIds gs'
|
||||
sendFoundGroups gs' moreGroups
|
||||
sendAllGroups takeFirst sortName searchType = \case
|
||||
@@ -499,74 +518,76 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
msg = maybe (MCText text) (\image -> MCImage {text, image}) image_
|
||||
sendComposedMessage cc ct Nothing msg
|
||||
when (moreGroups > 0) $
|
||||
sendComposedMessage cc ct Nothing $ MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)."
|
||||
sendComposedMessage cc ct Nothing $
|
||||
MCText $
|
||||
"Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)."
|
||||
|
||||
deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO ()
|
||||
deSuperUserCommand ct ciId cmd
|
||||
| superUser `elem` superUsers = case cmd of
|
||||
DCApproveGroup {groupId, displayName = n, groupApprovalId} -> do
|
||||
getGroupAndReg groupId n >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (g, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSPendingApproval gaId
|
||||
| gaId == groupApprovalId -> do
|
||||
getDuplicateGroup g >>= \case
|
||||
Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers."
|
||||
Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory."
|
||||
_ -> do
|
||||
getGroupRolesStatus g gr >>= \case
|
||||
Just GRSOk -> do
|
||||
setGroupStatus st gr GRSActive
|
||||
sendReply "Group approved!"
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved."
|
||||
Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin
|
||||
Just GRSContactNotOwner -> replyNotApproved "user is not an owner."
|
||||
Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin
|
||||
Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers."
|
||||
where
|
||||
replyNotApproved reason = sendReply $ "Group is not approved: " <> reason
|
||||
serviceNotAdmin = serviceName <> " is not an admin."
|
||||
| otherwise -> sendReply "Incorrect approval code"
|
||||
_ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval."
|
||||
where
|
||||
groupRef = groupReference' groupId n
|
||||
DCRejectGroup _gaId _gName -> pure ()
|
||||
DCSuspendGroup groupId gName -> do
|
||||
let groupRef = groupReference' groupId gName
|
||||
getGroupAndReg groupId gName >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (_, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSActive -> do
|
||||
setGroupStatus st gr GRSSuspended
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators."
|
||||
sendReply "Group suspended!"
|
||||
_ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended."
|
||||
DCResumeGroup groupId gName -> do
|
||||
let groupRef = groupReference' groupId gName
|
||||
getGroupAndReg groupId gName >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (_, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSSuspended -> do
|
||||
setGroupStatus st gr GRSActive
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!"
|
||||
sendReply "Group listing resumed!"
|
||||
_ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed."
|
||||
DCListLastGroups count ->
|
||||
readTVarIO (groupRegs st) >>= \grs -> do
|
||||
sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "")
|
||||
void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do
|
||||
ct_ <- getContact cc dbContactId
|
||||
let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_
|
||||
sendGroupInfo ct gr dbGroupId $ Just ownerStr
|
||||
DCExecuteCommand cmdStr ->
|
||||
sendChatCmdStr cc cmdStr >>= \r -> do
|
||||
ts <- getCurrentTime
|
||||
tz <- getCurrentTimeZone
|
||||
sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
|
||||
DCCommandError tag -> sendReply $ "Command error: " <> show tag
|
||||
DCApproveGroup {groupId, displayName = n, groupApprovalId} ->
|
||||
getGroupAndReg groupId n >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (g, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSPendingApproval gaId
|
||||
| gaId == groupApprovalId -> do
|
||||
getDuplicateGroup g >>= \case
|
||||
Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers."
|
||||
Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory."
|
||||
_ -> do
|
||||
getGroupRolesStatus g gr >>= \case
|
||||
Just GRSOk -> do
|
||||
setGroupStatus st gr GRSActive
|
||||
sendReply "Group approved!"
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved."
|
||||
Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin
|
||||
Just GRSContactNotOwner -> replyNotApproved "user is not an owner."
|
||||
Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin
|
||||
Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers."
|
||||
where
|
||||
replyNotApproved reason = sendReply $ "Group is not approved: " <> reason
|
||||
serviceNotAdmin = serviceName <> " is not an admin."
|
||||
| otherwise -> sendReply "Incorrect approval code"
|
||||
_ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval."
|
||||
where
|
||||
groupRef = groupReference' groupId n
|
||||
DCRejectGroup _gaId _gName -> pure ()
|
||||
DCSuspendGroup groupId gName -> do
|
||||
let groupRef = groupReference' groupId gName
|
||||
getGroupAndReg groupId gName >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (_, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSActive -> do
|
||||
setGroupStatus st gr GRSSuspended
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators."
|
||||
sendReply "Group suspended!"
|
||||
_ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended."
|
||||
DCResumeGroup groupId gName -> do
|
||||
let groupRef = groupReference' groupId gName
|
||||
getGroupAndReg groupId gName >>= \case
|
||||
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||
Just (_, gr) ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSSuspended -> do
|
||||
setGroupStatus st gr GRSActive
|
||||
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!"
|
||||
sendReply "Group listing resumed!"
|
||||
_ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed."
|
||||
DCListLastGroups count ->
|
||||
readTVarIO (groupRegs st) >>= \grs -> do
|
||||
sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "")
|
||||
void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do
|
||||
ct_ <- getContact cc dbContactId
|
||||
let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_
|
||||
sendGroupInfo ct gr dbGroupId $ Just ownerStr
|
||||
DCExecuteCommand cmdStr ->
|
||||
sendChatCmdStr cc cmdStr >>= \r -> do
|
||||
ts <- getCurrentTime
|
||||
tz <- getCurrentTimeZone
|
||||
sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
|
||||
DCCommandError tag -> sendReply $ "Command error: " <> show tag
|
||||
| otherwise = sendReply "You are not allowed to use this command"
|
||||
where
|
||||
superUser = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct}
|
||||
@@ -577,8 +598,9 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
||||
getGroup cc gId
|
||||
$>>= \g@GroupInfo {groupProfile = GroupProfile {displayName}} ->
|
||||
if displayName == gName
|
||||
then atomically (getGroupReg st gId)
|
||||
$>>= \gr -> pure $ Just (g, gr)
|
||||
then
|
||||
atomically (getGroupReg st gId)
|
||||
$>>= \gr -> pure $ Just (g, gr)
|
||||
else pure Nothing
|
||||
|
||||
sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO ()
|
||||
|
||||
Reference in New Issue
Block a user