Compare commits
2 Commits
v5.5.2
...
av/multipl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1e2fe22ce | ||
|
|
be9d6de767 |
@@ -234,8 +234,6 @@ You can use SimpleX with your own servers and still communicate with people usin
|
|||||||
|
|
||||||
Recent and important updates:
|
Recent and important updates:
|
||||||
|
|
||||||
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
|
||||||
|
|
||||||
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
|
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
|
||||||
|
|
||||||
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
|
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
|
||||||
@@ -301,7 +299,7 @@ What is already implemented:
|
|||||||
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
|
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
|
||||||
12. Manual messaging queue rotations to move conversation to another SMP relay.
|
12. Manual messaging queue rotations to move conversation to another SMP relay.
|
||||||
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
|
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
|
||||||
14. Local files encryption.
|
14. Local files encryption, except videos (to be added later).
|
||||||
|
|
||||||
We plan to add:
|
We plan to add:
|
||||||
|
|
||||||
@@ -373,13 +371,12 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
|
|||||||
- ✅ Desktop client.
|
- ✅ Desktop client.
|
||||||
- ✅ Encryption of local files stored in the app.
|
- ✅ Encryption of local files stored in the app.
|
||||||
- ✅ Using mobile profiles from the desktop app.
|
- ✅ Using mobile profiles from the desktop app.
|
||||||
- ✅ Private notes.
|
|
||||||
- ✅ Improve sending videos (including encryption of locally stored videos).
|
|
||||||
- 🏗 Improve experience for the new users.
|
- 🏗 Improve experience for the new users.
|
||||||
- 🏗 Post-quantum resistant key exchange in double ratchet protocol.
|
- 🏗 Post-quantum resistant key exchange in double ratchet protocol.
|
||||||
- 🏗 Large groups, communities and public channels.
|
- 🏗 Large groups, communities and public channels.
|
||||||
- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||||
- Privacy & security slider - a simple way to set all settings at once.
|
- Privacy & security slider - a simple way to set all settings at once.
|
||||||
|
- Improve sending videos (including encryption of locally stored videos).
|
||||||
- SMP queue redundancy and rotation (manual is supported).
|
- SMP queue redundancy and rotation (manual is supported).
|
||||||
- Include optional message into connection request sent via contact address.
|
- Include optional message into connection request sent via contact address.
|
||||||
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
|
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
|
||||||
|
|||||||
@@ -556,9 +556,10 @@ final class ChatModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this function analyses "connected" events and assumes that each member will be there only once
|
// this function analyses "connected" events and assumes that each member will be there only once
|
||||||
func getConnectedMemberNames(_ chatItem: ChatItem) -> (Int, [String]) {
|
func getConnectedMemberNames(_ chatItem: ChatItem) -> (Int, [String], String?) {
|
||||||
var count = 0
|
var count = 0
|
||||||
var ns: [String] = []
|
var ns: [String] = []
|
||||||
|
var lastNonConnectedEvent: String? = nil
|
||||||
if let ciCategory = chatItem.mergeCategory,
|
if let ciCategory = chatItem.mergeCategory,
|
||||||
var i = getChatItemIndex(chatItem) {
|
var i = getChatItemIndex(chatItem) {
|
||||||
while i < reversedChatItems.count {
|
while i < reversedChatItems.count {
|
||||||
@@ -566,12 +567,14 @@ final class ChatModel: ObservableObject {
|
|||||||
if ci.mergeCategory != ciCategory { break }
|
if ci.mergeCategory != ciCategory { break }
|
||||||
if let m = ci.memberConnected {
|
if let m = ci.memberConnected {
|
||||||
ns.append(m.displayName)
|
ns.append(m.displayName)
|
||||||
|
} else if count == 0 {
|
||||||
|
lastNonConnectedEvent = if let name = ci.memberDisplayName { name + " " + ci.text } else { ci.text }
|
||||||
}
|
}
|
||||||
count += 1
|
count += 1
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (count, ns)
|
return (count, ns, lastNonConnectedEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the index of the passed item and the next item (it has smaller index)
|
// returns the index of the passed item and the next item (it has smaller index)
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ struct ChatItemContentView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var mergedGroupEventText: Text? {
|
private var mergedGroupEventText: Text? {
|
||||||
let (count, ns) = chatModel.getConnectedMemberNames(chatItem)
|
let (count, ns, lastNonConnectedEvent) = chatModel.getConnectedMemberNames(chatItem)
|
||||||
let members: LocalizedStringKey =
|
let members: LocalizedStringKey =
|
||||||
switch ns.count {
|
switch ns.count {
|
||||||
case 1: "\(ns[0]) connected"
|
case 1: "\(ns[0]) connected"
|
||||||
@@ -162,6 +162,8 @@ struct ChatItemContentView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
return if count <= 1 {
|
return if count <= 1 {
|
||||||
nil
|
nil
|
||||||
|
} else if let last = lastNonConnectedEvent {
|
||||||
|
Text(last) + Text(" ") + Text("and \(count - ns.count) other events")
|
||||||
} else if ns.count == 0 {
|
} else if ns.count == 0 {
|
||||||
Text("\(count) group events")
|
Text("\(count) group events")
|
||||||
} else if count > ns.count {
|
} else if count > ns.count {
|
||||||
|
|||||||
@@ -749,9 +749,7 @@ struct ChatView: View {
|
|||||||
if ci.meta.editable && !mc.isVoice && !live {
|
if ci.meta.editable && !mc.isVoice && !live {
|
||||||
menu.append(editAction(ci))
|
menu.append(editAction(ci))
|
||||||
}
|
}
|
||||||
if !ci.isLiveDummy {
|
menu.append(viewInfoUIAction(ci))
|
||||||
menu.append(viewInfoUIAction(ci))
|
|
||||||
}
|
|
||||||
if revealed {
|
if revealed {
|
||||||
menu.append(hideUIAction())
|
menu.append(hideUIAction())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -978,9 +978,6 @@ struct ComposeView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func cancelLinkPreview() {
|
private func cancelLinkPreview() {
|
||||||
if let pendingLink = pendingLinkUrl?.absoluteString {
|
|
||||||
cancelledLinks.insert(pendingLink)
|
|
||||||
}
|
|
||||||
if let uri = composeState.linkPreview?.uri.absoluteString {
|
if let uri = composeState.linkPreview?.uri.absoluteString {
|
||||||
cancelledLinks.insert(uri)
|
cancelledLinks.insert(uri)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -370,11 +370,7 @@ struct GroupChatInfoView: View {
|
|||||||
|
|
||||||
private func addOrEditWelcomeMessage() -> some View {
|
private func addOrEditWelcomeMessage() -> some View {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
GroupWelcomeView(
|
GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo)
|
||||||
groupInfo: $groupInfo,
|
|
||||||
groupProfile: groupInfo.groupProfile,
|
|
||||||
welcomeText: groupInfo.groupProfile.description ?? ""
|
|
||||||
)
|
|
||||||
.navigationTitle("Welcome message")
|
.navigationTitle("Welcome message")
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
} label: {
|
} label: {
|
||||||
|
|||||||
@@ -11,32 +11,29 @@ import SimpleXChat
|
|||||||
|
|
||||||
struct GroupWelcomeView: View {
|
struct GroupWelcomeView: View {
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@EnvironmentObject private var m: ChatModel
|
||||||
|
var groupId: Int64
|
||||||
@Binding var groupInfo: GroupInfo
|
@Binding var groupInfo: GroupInfo
|
||||||
@State var groupProfile: GroupProfile
|
@State private var welcomeText: String = ""
|
||||||
@State var welcomeText: String
|
|
||||||
@State private var editMode = true
|
@State private var editMode = true
|
||||||
@FocusState private var keyboardVisible: Bool
|
@FocusState private var keyboardVisible: Bool
|
||||||
@State private var showSaveDialog = false
|
@State private var showSaveDialog = false
|
||||||
|
|
||||||
let maxByteCount = 1200
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if groupInfo.canEdit {
|
if groupInfo.canEdit {
|
||||||
editorView()
|
editorView()
|
||||||
.modifier(BackButton {
|
.modifier(BackButton {
|
||||||
if welcomeTextUnchanged() {
|
if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) {
|
||||||
dismiss()
|
dismiss()
|
||||||
} else {
|
} else {
|
||||||
showSaveDialog = true
|
showSaveDialog = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.confirmationDialog(
|
.confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) {
|
||||||
welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long",
|
Button("Save and update group profile") {
|
||||||
isPresented: $showSaveDialog
|
save()
|
||||||
) {
|
dismiss()
|
||||||
if welcomeTextFitsLimit() {
|
|
||||||
Button("Save and update group profile") { save() }
|
|
||||||
}
|
}
|
||||||
Button("Exit without saving") { dismiss() }
|
Button("Exit without saving") { dismiss() }
|
||||||
}
|
}
|
||||||
@@ -50,15 +47,14 @@ struct GroupWelcomeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
welcomeText = groupInfo.groupProfile.description ?? ""
|
||||||
keyboardVisible = true
|
keyboardVisible = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func textPreview() -> some View {
|
private func textPreview() -> some View {
|
||||||
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
|
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
|
||||||
.frame(minHeight: 130, alignment: .topLeading)
|
.frame(minHeight: 140, alignment: .topLeading)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +74,7 @@ struct GroupWelcomeView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, -5)
|
.padding(.horizontal, -5)
|
||||||
.padding(.top, -8)
|
.padding(.top, -8)
|
||||||
.frame(height: 130, alignment: .topLeading)
|
.frame(height: 140, alignment: .topLeading)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -97,9 +93,6 @@ struct GroupWelcomeView: View {
|
|||||||
}
|
}
|
||||||
.disabled(welcomeText.isEmpty)
|
.disabled(welcomeText.isEmpty)
|
||||||
copyButton()
|
copyButton()
|
||||||
} footer: {
|
|
||||||
Text(!welcomeTextFitsLimit() ? "Message too large" : "")
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
@@ -120,15 +113,7 @@ struct GroupWelcomeView: View {
|
|||||||
Button("Save and update group profile") {
|
Button("Save and update group profile") {
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
.disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit())
|
.disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil))
|
||||||
}
|
|
||||||
|
|
||||||
private func welcomeTextUnchanged() -> Bool {
|
|
||||||
welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func welcomeTextFitsLimit() -> Bool {
|
|
||||||
chatJsonLength(welcomeText) <= maxByteCount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
@@ -138,13 +123,11 @@ struct GroupWelcomeView: View {
|
|||||||
if welcome?.count == 0 {
|
if welcome?.count == 0 {
|
||||||
welcome = nil
|
welcome = nil
|
||||||
}
|
}
|
||||||
groupProfile.description = welcome
|
var groupProfileUpdated = groupInfo.groupProfile
|
||||||
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
|
groupProfileUpdated.description = welcome
|
||||||
await MainActor.run {
|
groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated)
|
||||||
groupInfo = gInfo
|
m.updateGroup(groupInfo)
|
||||||
ChatModel.shared.updateGroup(gInfo)
|
welcomeText = welcome ?? ""
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("apiUpdateGroup error: \(responseError(error))")
|
logger.error("apiUpdateGroup error: \(responseError(error))")
|
||||||
}
|
}
|
||||||
@@ -154,6 +137,6 @@ struct GroupWelcomeView: View {
|
|||||||
|
|
||||||
struct GroupWelcomeView_Previews: PreviewProvider {
|
struct GroupWelcomeView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData)
|
GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,6 @@
|
|||||||
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
|
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
|
||||||
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
|
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
|
||||||
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
|
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
|
||||||
5C29C3A52B6D09B2003DF84C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3A02B6D09B2003DF84C /* libgmpxx.a */; };
|
|
||||||
5C29C3A62B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3A12B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a */; };
|
|
||||||
5C29C3A72B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3A22B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a */; };
|
|
||||||
5C29C3A82B6D09B2003DF84C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3A32B6D09B2003DF84C /* libgmp.a */; };
|
|
||||||
5C29C3A92B6D09B2003DF84C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3A42B6D09B2003DF84C /* libffi.a */; };
|
|
||||||
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
|
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
|
||||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
|
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
|
||||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; };
|
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; };
|
||||||
@@ -66,6 +61,11 @@
|
|||||||
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
||||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
|
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
|
||||||
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
|
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
|
||||||
|
5C83A1AD2B5EF67D00AE0A4A /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C83A1A82B5EF67D00AE0A4A /* libgmp.a */; };
|
||||||
|
5C83A1AE2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C83A1A92B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a */; };
|
||||||
|
5C83A1AF2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C83A1AA2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a */; };
|
||||||
|
5C83A1B02B5EF67D00AE0A4A /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C83A1AB2B5EF67D00AE0A4A /* libffi.a */; };
|
||||||
|
5C83A1B12B5EF67D00AE0A4A /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C83A1AC2B5EF67D00AE0A4A /* libgmpxx.a */; };
|
||||||
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
|
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
|
||||||
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; };
|
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; };
|
||||||
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; };
|
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; };
|
||||||
@@ -278,11 +278,6 @@
|
|||||||
5C245F3C2B501E98001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
5C245F3C2B501E98001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
5C245F3D2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = "tr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
5C245F3D2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = "tr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
5C245F3E2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
5C245F3E2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
5C29C3A02B6D09B2003DF84C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
|
||||||
5C29C3A12B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a"; sourceTree = "<group>"; };
|
|
||||||
5C29C3A22B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a"; sourceTree = "<group>"; };
|
|
||||||
5C29C3A32B6D09B2003DF84C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
|
||||||
5C29C3A42B6D09B2003DF84C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
|
||||||
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
|
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
|
||||||
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
|
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
|
||||||
5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
||||||
@@ -330,6 +325,11 @@
|
|||||||
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
||||||
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
||||||
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
|
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
|
||||||
|
5C83A1A82B5EF67D00AE0A4A /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
|
5C83A1A92B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||||
|
5C83A1AA2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a"; sourceTree = "<group>"; };
|
||||||
|
5C83A1AB2B5EF67D00AE0A4A /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
|
5C83A1AC2B5EF67D00AE0A4A /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
5C84FE9429A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
5C84FE9429A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
@@ -514,13 +514,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5C29C3A62B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a in Frameworks */,
|
|
||||||
5C29C3A52B6D09B2003DF84C /* libgmpxx.a in Frameworks */,
|
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
|
5C83A1B02B5EF67D00AE0A4A /* libffi.a in Frameworks */,
|
||||||
|
5C83A1AD2B5EF67D00AE0A4A /* libgmp.a in Frameworks */,
|
||||||
|
5C83A1AE2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a in Frameworks */,
|
||||||
|
5C83A1AF2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a in Frameworks */,
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
5C29C3A92B6D09B2003DF84C /* libffi.a in Frameworks */,
|
5C83A1B12B5EF67D00AE0A4A /* libgmpxx.a in Frameworks */,
|
||||||
5C29C3A82B6D09B2003DF84C /* libgmp.a in Frameworks */,
|
|
||||||
5C29C3A72B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a in Frameworks */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -582,11 +582,11 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5C29C3A42B6D09B2003DF84C /* libffi.a */,
|
5C83A1AB2B5EF67D00AE0A4A /* libffi.a */,
|
||||||
5C29C3A32B6D09B2003DF84C /* libgmp.a */,
|
5C83A1A82B5EF67D00AE0A4A /* libgmp.a */,
|
||||||
5C29C3A02B6D09B2003DF84C /* libgmpxx.a */,
|
5C83A1AC2B5EF67D00AE0A4A /* libgmpxx.a */,
|
||||||
5C29C3A22B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a */,
|
5C83A1A92B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a */,
|
||||||
5C29C3A12B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a */,
|
5C83A1AA2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a */,
|
||||||
);
|
);
|
||||||
path = Libraries;
|
path = Libraries;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1509,7 +1509,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 196;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1531,7 +1531,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.2;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1552,7 +1552,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 196;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1574,7 +1574,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.2;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1633,7 +1633,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 196;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1646,7 +1646,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.2;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -1665,7 +1665,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 196;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1678,7 +1678,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.2;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -1697,7 +1697,7 @@
|
|||||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 196;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
@@ -1721,7 +1721,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Libraries/sim",
|
"$(PROJECT_DIR)/Libraries/sim",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.2;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1743,7 +1743,7 @@
|
|||||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 196;
|
CURRENT_PROJECT_VERSION = 194;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
@@ -1767,7 +1767,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Libraries/sim",
|
"$(PROJECT_DIR)/Libraries/sim",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.2;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|||||||
@@ -105,11 +105,6 @@ public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func chatJsonLength(_ s: String) -> Int {
|
|
||||||
var c = s.cString(using: .utf8)!
|
|
||||||
return Int(chat_json_length(&c))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ParsedMarkdown: Decodable {
|
struct ParsedMarkdown: Decodable {
|
||||||
var formattedText: [FormattedText]?
|
var formattedText: [FormattedText]?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ extern char *chat_parse_markdown(char *str);
|
|||||||
extern char *chat_parse_server(char *str);
|
extern char *chat_parse_server(char *str);
|
||||||
extern char *chat_password_hash(char *pwd, char *salt);
|
extern char *chat_password_hash(char *pwd, char *salt);
|
||||||
extern char *chat_valid_name(char *name);
|
extern char *chat_valid_name(char *name);
|
||||||
extern int chat_json_length(char *str);
|
|
||||||
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
|
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
|
||||||
extern char *chat_decrypt_media(char *key, char *frame, int len);
|
extern char *chat_decrypt_media(char *key, char *frame, int len);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package chat.simplex.app
|
package chat.simplex.app
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import chat.simplex.app.model.NtfManager
|
import chat.simplex.app.model.NtfManager
|
||||||
import chat.simplex.app.model.NtfManager.getUserIdFromIntent
|
import chat.simplex.app.model.NtfManager.getUserIdFromIntent
|
||||||
@@ -59,17 +58,6 @@ class MainActivity: FragmentActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
AppLock.recheckAuthState()
|
AppLock.recheckAuthState()
|
||||||
withApi {
|
|
||||||
delay(1000)
|
|
||||||
if (!isAppOnForeground) return@withApi
|
|
||||||
/**
|
|
||||||
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
|
|
||||||
* access to clipboard because the app considered in background.
|
|
||||||
* This will ensure that the app will get the event on resume
|
|
||||||
* */
|
|
||||||
val service = getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
|
||||||
chatModel.clipboardHasText.value = service.hasPrimaryClip()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
}
|
}
|
||||||
Lifecycle.Event.ON_RESUME -> {
|
Lifecycle.Event.ON_RESUME -> {
|
||||||
isAppOnForeground = true
|
isAppOnForeground = true
|
||||||
|
/**
|
||||||
|
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
|
||||||
|
* access to clipboard because the app considered in background.
|
||||||
|
* This will ensure that the app will get the event on resume
|
||||||
|
* */
|
||||||
|
val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||||
|
chatModel.clipboardHasText.value = service.hasPrimaryClip()
|
||||||
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) {
|
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) {
|
||||||
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
||||||
}
|
}
|
||||||
@@ -190,18 +197,10 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
}
|
}
|
||||||
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
|
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
if (mode == NotificationsMode.SERVICE) {
|
if (mode == NotificationsMode.SERVICE)
|
||||||
SimplexService.start()
|
SimplexService.start()
|
||||||
// Sometimes, when we change modes fast from one to another, system destroys the service after start.
|
else
|
||||||
// We can wait a little and restart the service, and it will work in 100% of cases
|
|
||||||
delay(2000)
|
|
||||||
if (!SimplexService.isServiceStarted && appPrefs.notificationsMode.get() == NotificationsMode.SERVICE) {
|
|
||||||
Log.i(TAG, "Service tried to start but destroyed by system, repeating once more")
|
|
||||||
SimplexService.start()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SimplexService.safeStopService()
|
SimplexService.safeStopService()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode != NotificationsMode.PERIODIC) {
|
if (mode != NotificationsMode.PERIODIC) {
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class SimplexService: Service() {
|
|||||||
if (wakeLock != null || isStartingService) return
|
if (wakeLock != null || isStartingService) return
|
||||||
val self = this
|
val self = this
|
||||||
isStartingService = true
|
isStartingService = true
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
val chatController = ChatController
|
val chatController = ChatController
|
||||||
waitDbMigrationEnds(chatController)
|
waitDbMigrationEnds(chatController)
|
||||||
try {
|
try {
|
||||||
@@ -262,7 +262,7 @@ class SimplexService: Service() {
|
|||||||
private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE"
|
private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE"
|
||||||
private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
|
private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
|
||||||
|
|
||||||
var isServiceStarted = false
|
private var isServiceStarted = false
|
||||||
private var stopAfterStart = false
|
private var stopAfterStart = false
|
||||||
|
|
||||||
fun scheduleStart(context: Context) {
|
fun scheduleStart(context: Context) {
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import chat.simplex.common.AppScreen
|
import chat.simplex.common.AppScreen
|
||||||
import chat.simplex.common.model.clear
|
|
||||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import androidx.compose.ui.platform.LocalContext as LocalContext1
|
import androidx.compose.ui.platform.LocalContext as LocalContext1
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
package chat.simplex.common.views.helpers
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import chat.simplex.common.model.CustomTimeUnit
|
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
|
||||||
import com.sd.lib.compose.wheel_picker.*
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
actual fun CustomTimePicker(
|
|
||||||
selection: MutableState<Int>,
|
|
||||||
timeUnitsLimits: List<TimeUnitLimits>
|
|
||||||
) {
|
|
||||||
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
|
|
||||||
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
|
|
||||||
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
|
|
||||||
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
|
|
||||||
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
|
|
||||||
val selectedDuration = remember { mutableStateOf(duration) }
|
|
||||||
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
|
|
||||||
val isTriggered = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(selectedUnit.value) {
|
|
||||||
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
|
|
||||||
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
|
|
||||||
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
|
|
||||||
if (isTriggered.value) {
|
|
||||||
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
|
|
||||||
if (maxValue != null && selectedDuration.value > maxValue) {
|
|
||||||
selectedDuration.value = maxValue
|
|
||||||
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
|
|
||||||
} else {
|
|
||||||
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
|
|
||||||
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isTriggered.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(selectedDuration.value) {
|
|
||||||
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = DEFAULT_PADDING),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(0.dp)
|
|
||||||
) {
|
|
||||||
Column(Modifier.weight(1f)) {
|
|
||||||
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
|
|
||||||
FVerticalWheelPicker(
|
|
||||||
count = selectedUnitValues.value.count(),
|
|
||||||
state = durationPickerState,
|
|
||||||
unfocusedCount = 2,
|
|
||||||
focus = {
|
|
||||||
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
|
|
||||||
}
|
|
||||||
) { index ->
|
|
||||||
Text(
|
|
||||||
selectedUnitValues.value[index].toString(),
|
|
||||||
fontSize = 18.sp,
|
|
||||||
color = MaterialTheme.colors.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LaunchedEffect(durationPickerState) {
|
|
||||||
snapshotFlow { durationPickerState.currentIndex }
|
|
||||||
.collect {
|
|
||||||
selectedDuration.value = selectedUnitValues.value[it]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column(Modifier.weight(1f)) {
|
|
||||||
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
|
|
||||||
FVerticalWheelPicker(
|
|
||||||
count = timeUnitsLimits.count(),
|
|
||||||
state = unitPickerState,
|
|
||||||
unfocusedCount = 2,
|
|
||||||
focus = {
|
|
||||||
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
|
|
||||||
}
|
|
||||||
) { index ->
|
|
||||||
Text(
|
|
||||||
timeUnitsLimits[index].timeUnit.text,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
color = MaterialTheme.colors.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LaunchedEffect(unitPickerState) {
|
|
||||||
snapshotFlow { unitPickerState.currentIndex }
|
|
||||||
.collect {
|
|
||||||
selectedUnit.value = timeUnitsLimits[it].timeUnit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -66,7 +66,6 @@ extern char *chat_parse_markdown(const char *str);
|
|||||||
extern char *chat_parse_server(const char *str);
|
extern char *chat_parse_server(const char *str);
|
||||||
extern char *chat_password_hash(const char *pwd, const char *salt);
|
extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||||
extern char *chat_valid_name(const char *name);
|
extern char *chat_valid_name(const char *name);
|
||||||
extern int chat_json_length(const char *str);
|
|
||||||
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
||||||
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
||||||
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
|
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
|
||||||
@@ -164,14 +163,6 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT int JNICALL
|
|
||||||
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
|
|
||||||
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
|
|
||||||
int res = chat_json_length(_str);
|
|
||||||
(*env)->ReleaseStringUTFChars(env, str, _str);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL
|
JNIEXPORT jstring JNICALL
|
||||||
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
|
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
|
||||||
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
|
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ extern char *chat_parse_markdown(const char *str);
|
|||||||
extern char *chat_parse_server(const char *str);
|
extern char *chat_parse_server(const char *str);
|
||||||
extern char *chat_password_hash(const char *pwd, const char *salt);
|
extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||||
extern char *chat_valid_name(const char *name);
|
extern char *chat_valid_name(const char *name);
|
||||||
extern int chat_json_length(const char *str);
|
|
||||||
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
||||||
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
||||||
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
|
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
|
||||||
@@ -174,14 +173,6 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT int JNICALL
|
|
||||||
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
|
|
||||||
const char *_str = encode_to_utf8_chars(env, str);
|
|
||||||
int res = chat_json_length(_str);
|
|
||||||
(*env)->ReleaseStringUTFChars(env, str, _str);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL
|
JNIEXPORT jstring JNICALL
|
||||||
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
|
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
|
||||||
const char *_path = encode_to_utf8_chars(env, path);
|
const char *_path = encode_to_utf8_chars(env, path);
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ fun MainScreen() {
|
|||||||
val localUserCreated = chatModel.localUserCreated.value
|
val localUserCreated = chatModel.localUserCreated.value
|
||||||
var showInitializationView by remember { mutableStateOf(false) }
|
var showInitializationView by remember { mutableStateOf(false) }
|
||||||
when {
|
when {
|
||||||
chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress))
|
|
||||||
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
|
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
|
||||||
showChatDatabaseError -> {
|
showChatDatabaseError -> {
|
||||||
// Prevent showing keyboard on Android when: passcode enabled and database password not saved
|
// Prevent showing keyboard on Android when: passcode enabled and database password not saved
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package chat.simplex.common.model
|
|||||||
|
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
@@ -49,7 +48,6 @@ object ChatModel {
|
|||||||
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
|
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
|
||||||
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
|
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
|
||||||
val ctrlInitInProgress = mutableStateOf(false)
|
val ctrlInitInProgress = mutableStateOf(false)
|
||||||
val dbMigrationInProgress = mutableStateOf(false)
|
|
||||||
val chats = mutableStateListOf<Chat>()
|
val chats = mutableStateListOf<Chat>()
|
||||||
// map of connections network statuses, key is agent connection id
|
// map of connections network statuses, key is agent connection id
|
||||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||||
@@ -57,7 +55,7 @@ object ChatModel {
|
|||||||
|
|
||||||
// current chat
|
// current chat
|
||||||
val chatId = mutableStateOf<String?>(null)
|
val chatId = mutableStateOf<String?>(null)
|
||||||
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
|
val chatItems = mutableStateListOf<ChatItem>()
|
||||||
// rhId, chatId
|
// rhId, chatId
|
||||||
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
|
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
|
||||||
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
|
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
|
||||||
@@ -65,6 +63,8 @@ object ChatModel {
|
|||||||
|
|
||||||
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
|
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
|
||||||
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
|
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
|
||||||
|
// Allows to temporary save servers that are being edited on multiple screens
|
||||||
|
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
|
||||||
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
|
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
|
||||||
|
|
||||||
// set when app opened from external intent
|
// set when app opened from external intent
|
||||||
@@ -269,15 +269,18 @@ object ChatModel {
|
|||||||
} else {
|
} else {
|
||||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
// add to current chat
|
// add to current chat
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
|
Log.d(TAG, "TODOCHAT: addChatItem: chatIds are equal, size ${chatItems.size}")
|
||||||
// Prevent situation when chat item already in the list received from backend
|
// Prevent situation when chat item already in the list received from backend
|
||||||
if (chatItems.value.none { it.id == cItem.id }) {
|
if (chatItems.none { it.id == cItem.id }) {
|
||||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
|
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
|
||||||
} else {
|
} else {
|
||||||
chatItems.add(cItem)
|
chatItems.add(cItem)
|
||||||
|
Log.d(TAG, "TODOCHAT: addChatItem: added to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,13 +307,14 @@ object ChatModel {
|
|||||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||||
res = true
|
res = true
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
return withContext(Dispatchers.Main) {
|
return withContext(Dispatchers.Main) {
|
||||||
// update current chat
|
// update current chat
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
val items = chatItems.value
|
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
|
||||||
if (itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
items[itemIndex] = cItem
|
chatItems[itemIndex] = cItem
|
||||||
|
Log.d(TAG, "TODOCHAT: upsertChatItem: updated in chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
val status = chatItemStatuses.remove(cItem.id)
|
val status = chatItemStatuses.remove(cItem.id)
|
||||||
@@ -320,6 +324,7 @@ object ChatModel {
|
|||||||
cItem
|
cItem
|
||||||
}
|
}
|
||||||
chatItems.add(ci)
|
chatItems.add(ci)
|
||||||
|
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -331,10 +336,9 @@ object ChatModel {
|
|||||||
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
|
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
val items = chatItems.value
|
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
|
||||||
if (itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
items[itemIndex] = cItem
|
chatItems[itemIndex] = cItem
|
||||||
}
|
}
|
||||||
} else if (status != null) {
|
} else if (status != null) {
|
||||||
chatItemStatuses[cItem.id] = status
|
chatItemStatuses[cItem.id] = status
|
||||||
@@ -358,10 +362,10 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
// remove from current chat
|
// remove from current chat
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
chatItems.removeAll {
|
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||||
val remove = it.id == cItem.id
|
if (itemIndex >= 0) {
|
||||||
if (remove) { AudioPlayer.stop(it) }
|
AudioPlayer.stop(chatItems[itemIndex])
|
||||||
remove
|
chatItems.removeAt(itemIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,7 +406,7 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeLiveDummy() {
|
fun removeLiveDummy() {
|
||||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
chatItems.removeLast()
|
chatItems.removeLast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,14 +438,14 @@ object ChatModel {
|
|||||||
var markedRead = 0
|
var markedRead = 0
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
var i = 0
|
var i = 0
|
||||||
val items = chatItems.value
|
Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marking read ${cInfo.id}, current chatId ${chatId.value}, size was ${chatItems.size}")
|
||||||
while (i < items.size) {
|
while (i < chatItems.count()) {
|
||||||
val item = items[i]
|
val item = chatItems[i]
|
||||||
if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) {
|
if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) {
|
||||||
val newItem = item.withStatus(CIStatus.RcvRead())
|
val newItem = item.withStatus(CIStatus.RcvRead())
|
||||||
items[i] = newItem
|
chatItems[i] = newItem
|
||||||
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
|
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
|
||||||
items[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
|
chatItems[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
|
||||||
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
|
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -449,6 +453,7 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marked read ${cInfo.id}, current chatId ${chatId.value}, size now ${chatItems.size}")
|
||||||
}
|
}
|
||||||
return markedRead
|
return markedRead
|
||||||
}
|
}
|
||||||
@@ -489,10 +494,11 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this function analyses "connected" events and assumes that each member will be there only once
|
// this function analyses "connected" events and assumes that each member will be there only once
|
||||||
fun getConnectedMemberNames(cItem: ChatItem): Pair<Int, List<String>> {
|
fun getConnectedMemberNames(cItem: ChatItem): Triple<Int, List<String>, String?> {
|
||||||
var count = 0
|
var count = 0
|
||||||
val ns = mutableListOf<String>()
|
val ns = mutableListOf<String>()
|
||||||
var idx = getChatItemIndexOrNull(cItem)
|
var idx = getChatItemIndexOrNull(cItem)
|
||||||
|
var lastNonConnectedEvent: String? = null
|
||||||
if (cItem.mergeCategory != null && idx != null) {
|
if (cItem.mergeCategory != null && idx != null) {
|
||||||
val reversedChatItems = chatItems.asReversed()
|
val reversedChatItems = chatItems.asReversed()
|
||||||
while (idx < reversedChatItems.size) {
|
while (idx < reversedChatItems.size) {
|
||||||
@@ -501,12 +507,14 @@ object ChatModel {
|
|||||||
val m = ci.memberConnected
|
val m = ci.memberConnected
|
||||||
if (m != null) {
|
if (m != null) {
|
||||||
ns.add(m.displayName)
|
ns.add(m.displayName)
|
||||||
|
} else if (count == 0) {
|
||||||
|
lastNonConnectedEvent = if (ci.memberDisplayName != null) ci.memberDisplayName + " " + ci.text else ci.text
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count to ns
|
return Triple(count, ns, lastNonConnectedEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the index of the passed item and the next item (it has smaller index)
|
// returns the index of the passed item and the next item (it has smaller index)
|
||||||
@@ -639,8 +647,7 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addTerminalItem(item: TerminalItem) {
|
fun addTerminalItem(item: TerminalItem) {
|
||||||
val maxItems = if (appPreferences.developerTools.get()) 500 else 200
|
if (terminalItems.value.size >= 500) {
|
||||||
if (terminalItems.value.size >= maxItems) {
|
|
||||||
terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size)
|
terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size)
|
||||||
}
|
}
|
||||||
terminalItems.value += item
|
terminalItems.value += item
|
||||||
@@ -2002,46 +2009,6 @@ data class ChatItem (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.add(index: Int, chatItem: ChatItem) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, chatItem) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.add(chatItem: ChatItem) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(chatItem) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.addAll(index: Int, chatItems: List<ChatItem>) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(index, chatItems) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.addAll(chatItems: List<ChatItem>) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(chatItems) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.removeAll(block: (ChatItem) -> Boolean) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAll(block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.removeAt(index: Int) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAt(index) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.removeLast() {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeLast() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.replaceAll(chatItems: List<ChatItem>) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(chatItems) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.clear() {
|
|
||||||
value = SnapshotStateList<ChatItem>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun State<SnapshotStateList<ChatItem>>.asReversed(): MutableList<ChatItem> = value.asReversed()
|
|
||||||
|
|
||||||
val State<List<ChatItem>>.size: Int get() = value.size
|
|
||||||
|
|
||||||
enum class CIMergeCategory {
|
enum class CIMergeCategory {
|
||||||
MemberConnected,
|
MemberConnected,
|
||||||
RcvGroupEvent,
|
RcvGroupEvent,
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ external fun chatParseMarkdown(str: String): String
|
|||||||
external fun chatParseServer(str: String): String
|
external fun chatParseServer(str: String): String
|
||||||
external fun chatPasswordHash(pwd: String, salt: String): String
|
external fun chatPasswordHash(pwd: String, salt: String): String
|
||||||
external fun chatValidName(name: String): String
|
external fun chatValidName(name: String): String
|
||||||
external fun chatJsonLength(str: String): Int
|
|
||||||
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
|
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
|
||||||
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
|
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
|
||||||
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
|
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
|
||||||
@@ -43,7 +42,7 @@ val appPreferences: AppPreferences
|
|||||||
val chatController: ChatController = ChatController
|
val chatController: ChatController = ChatController
|
||||||
|
|
||||||
fun initChatControllerAndRunMigrations() {
|
fun initChatControllerAndRunMigrations() {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
||||||
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
||||||
} else {
|
} else {
|
||||||
@@ -59,23 +58,10 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
|||||||
chatModel.ctrlInitInProgress.value = true
|
chatModel.ctrlInitInProgress.value = true
|
||||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||||
val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||||
var migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value)
|
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
|
||||||
var res: DBMigrationResult = runCatching {
|
val res: DBMigrationResult = kotlin.runCatching {
|
||||||
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
||||||
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
||||||
val rerunMigration = res is DBMigrationResult.ErrorMigration && when (res.migrationError) {
|
|
||||||
// we don't allow to run down migrations without confirmation in UI, so currently it won't be YesUpDown
|
|
||||||
is MigrationError.Upgrade -> confirm == MigrationConfirmation.YesUp || confirm == MigrationConfirmation.YesUpDown
|
|
||||||
is MigrationError.Downgrade -> confirm == MigrationConfirmation.YesUpDown
|
|
||||||
is MigrationError.Error -> false
|
|
||||||
}
|
|
||||||
if (rerunMigration) {
|
|
||||||
chatModel.dbMigrationInProgress.value = true
|
|
||||||
migrated = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
|
|
||||||
res = runCatching {
|
|
||||||
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
|
||||||
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
|
||||||
}
|
|
||||||
val ctrl = if (res is DBMigrationResult.OK) {
|
val ctrl = if (res is DBMigrationResult.OK) {
|
||||||
migrated[1] as Long
|
migrated[1] as Long
|
||||||
} else null
|
} else null
|
||||||
@@ -133,7 +119,6 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
chatModel.ctrlInitInProgress.value = false
|
chatModel.ctrlInitInProgress.value = false
|
||||||
chatModel.dbMigrationInProgress.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ abstract class NtfManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun openChatAction(userId: Long?, chatId: ChatId) {
|
fun openChatAction(userId: Long?, chatId: ChatId) {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
awaitChatStartedIfNeeded(chatModel)
|
awaitChatStartedIfNeeded(chatModel)
|
||||||
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
||||||
// TODO include remote host ID in desktop notifications?
|
// TODO include remote host ID in desktop notifications?
|
||||||
@@ -70,7 +70,7 @@ abstract class NtfManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showChatsAction(userId: Long?) {
|
fun showChatsAction(userId: Long?) {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
awaitChatStartedIfNeeded(chatModel)
|
awaitChatStartedIfNeeded(chatModel)
|
||||||
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
||||||
// TODO include remote host ID in desktop notifications?
|
// TODO include remote host ID in desktop notifications?
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
|
|||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(ciInfo) {
|
LaunchedEffect(Unit) {
|
||||||
if (ciInfo.memberDeliveryStatuses != null) {
|
if (ciInfo.memberDeliveryStatuses != null) {
|
||||||
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
|
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,13 +67,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
launch {
|
launch {
|
||||||
snapshotFlow { chatModel.chatId.value }
|
snapshotFlow { chatModel.chatId.value }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.filterNotNull()
|
.onEach { Log.d(TAG, "TODOCHAT: chatId: activeChatId ${activeChat.value?.id} == new chatId $it ${activeChat.value?.id == it} ") }
|
||||||
|
.filter { it != null && activeChat.value?.id != it }
|
||||||
.collect { chatId ->
|
.collect { chatId ->
|
||||||
if (activeChat.value?.id != chatId) {
|
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
||||||
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
// Also for situation when chatId changes after clicking in notification, etc
|
||||||
// Also for situation when chatId changes after clicking in notification, etc
|
activeChat.value = chatModel.getChat(chatId!!)
|
||||||
activeChat.value = chatModel.getChat(chatId)
|
Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}")
|
||||||
}
|
|
||||||
markUnreadChatAsRead(activeChat, chatModel)
|
markUnreadChatAsRead(activeChat, chatModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,10 +92,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
.onEach { Log.d(TAG, "TODOCHAT: chats: activeChatId ${activeChat.value?.id} == new chatId ${it?.id} ${activeChat.value?.id == it?.id} ") }
|
||||||
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
|
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
|
||||||
.filter { it != null && it.chatInfo != activeChat.value?.chatInfo }
|
.filter { it != null && it?.chatInfo != activeChat.value?.chatInfo }
|
||||||
.collect {
|
.collect {
|
||||||
activeChat.value = it
|
activeChat.value = it
|
||||||
|
Log.d(TAG, "TODOCHAT: chats: activeChatId became ${activeChat.value?.id}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,6 +148,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
},
|
},
|
||||||
attachmentOption,
|
attachmentOption,
|
||||||
attachmentBottomSheetState,
|
attachmentBottomSheetState,
|
||||||
|
chatModel.chatItems,
|
||||||
searchText,
|
searchText,
|
||||||
useLinkPreviews = useLinkPreviews,
|
useLinkPreviews = useLinkPreviews,
|
||||||
linkMode = chatModel.simplexLinkMode.value,
|
linkMode = chatModel.simplexLinkMode.value,
|
||||||
@@ -223,17 +226,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
loadPrevMessages = {
|
loadPrevMessages = {
|
||||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
||||||
val firstId = chatModel.chatItems.value.firstOrNull()?.id
|
val firstId = chatModel.chatItems.firstOrNull()?.id
|
||||||
if (c != null && firstId != null) {
|
if (c != null && firstId != null) {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
|
Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}")
|
||||||
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
||||||
|
Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteMessage = { itemId, mode ->
|
deleteMessage = { itemId, mode ->
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val cInfo = chat.chatInfo
|
val cInfo = chat.chatInfo
|
||||||
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
|
val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId }
|
||||||
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
||||||
val groupInfo = toModerate?.first
|
val groupInfo = toModerate?.first
|
||||||
val groupMember = toModerate?.second
|
val groupMember = toModerate?.second
|
||||||
@@ -399,15 +404,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||||
}
|
}
|
||||||
ModalManager.end.closeModals()
|
ModalManager.end.closeModals()
|
||||||
ModalManager.end.showModalCloseable(endButtons = {
|
ModalManager.end.showModal(endButtons = {
|
||||||
ShareButton {
|
ShareButton {
|
||||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||||
}
|
}
|
||||||
}) { close ->
|
}) {
|
||||||
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||||
KeyChangeEffect(chatModel.chatId.value) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,6 +495,7 @@ fun ChatLayout(
|
|||||||
composeView: (@Composable () -> Unit),
|
composeView: (@Composable () -> Unit),
|
||||||
attachmentOption: MutableState<AttachmentOption?>,
|
attachmentOption: MutableState<AttachmentOption?>,
|
||||||
attachmentBottomSheetState: ModalBottomSheetState,
|
attachmentBottomSheetState: ModalBottomSheetState,
|
||||||
|
chatItems: List<ChatItem>,
|
||||||
searchValue: State<String>,
|
searchValue: State<String>,
|
||||||
useLinkPreviews: Boolean,
|
useLinkPreviews: Boolean,
|
||||||
linkMode: SimplexLinkMode,
|
linkMode: SimplexLinkMode,
|
||||||
@@ -579,7 +582,7 @@ fun ChatLayout(
|
|||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
) {
|
) {
|
||||||
ChatItemsList(
|
ChatItemsList(
|
||||||
chat, unreadCount, composeState, searchValue,
|
chat, unreadCount, composeState, chatItems, searchValue,
|
||||||
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
|
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
|
||||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||||
@@ -837,6 +840,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
chat: Chat,
|
chat: Chat,
|
||||||
unreadCount: State<Int>,
|
unreadCount: State<Int>,
|
||||||
composeState: MutableState<ComposeState>,
|
composeState: MutableState<ComposeState>,
|
||||||
|
chatItems: List<ChatItem>,
|
||||||
searchValue: State<String>,
|
searchValue: State<String>,
|
||||||
useLinkPreviews: Boolean,
|
useLinkPreviews: Boolean,
|
||||||
linkMode: SimplexLinkMode,
|
linkMode: SimplexLinkMode,
|
||||||
@@ -865,7 +869,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
ScrollToBottom(chat.id, listState, chatModel.chatItems)
|
ScrollToBottom(chat.id, listState, chatItems)
|
||||||
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
|
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
|
||||||
// Scroll to bottom when search value changes from something to nothing and back
|
// Scroll to bottom when search value changes from something to nothing and back
|
||||||
LaunchedEffect(searchValue.value.isEmpty()) {
|
LaunchedEffect(searchValue.value.isEmpty()) {
|
||||||
@@ -882,7 +886,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
|
PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
|
||||||
|
|
||||||
Spacer(Modifier.size(8.dp))
|
Spacer(Modifier.size(8.dp))
|
||||||
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
val reversedChatItems by remember { derivedStateOf { chatItems.reversed().toList() } }
|
||||||
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
|
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
|
||||||
val scrollToItem: (Long) -> Unit = { itemId: Long ->
|
val scrollToItem: (Long) -> Unit = { itemId: Long ->
|
||||||
val index = reversedChatItems.indexOfFirst { it.id == itemId }
|
val index = reversedChatItems.indexOfFirst { it.id == itemId }
|
||||||
@@ -935,7 +939,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val provider = {
|
val provider = {
|
||||||
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
providerForGallery(i, chatItems, cItem.id) { indexInReversed ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
listState.scrollToItem(
|
listState.scrollToItem(
|
||||||
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1),
|
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1),
|
||||||
@@ -1058,11 +1062,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
|
FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: State<List<ChatItem>>) {
|
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: List<ChatItem>) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
// Helps to scroll to bottom after moving from Group to Direct chat
|
// Helps to scroll to bottom after moving from Group to Direct chat
|
||||||
// and prevents scrolling to bottom on orientation change
|
// and prevents scrolling to bottom on orientation change
|
||||||
@@ -1080,7 +1084,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
|||||||
* When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise
|
* When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise
|
||||||
* */
|
* */
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
snapshotFlow { chatItems.value.lastOrNull()?.id }
|
snapshotFlow { chatItems.lastOrNull()?.id }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
||||||
.collect {
|
.collect {
|
||||||
@@ -1103,7 +1107,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BoxWithConstraintsScope.FloatingButtons(
|
fun BoxWithConstraintsScope.FloatingButtons(
|
||||||
chatItems: State<List<ChatItem>>,
|
chatItems: List<ChatItem>,
|
||||||
unreadCount: State<Int>,
|
unreadCount: State<Int>,
|
||||||
minUnreadItemId: Long,
|
minUnreadItemId: Long,
|
||||||
searchValue: State<String>,
|
searchValue: State<String>,
|
||||||
@@ -1137,11 +1141,10 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
|||||||
val bottomUnreadCount by remember {
|
val bottomUnreadCount by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (unreadCount.value == 0) return@derivedStateOf 0
|
if (unreadCount.value == 0) return@derivedStateOf 0
|
||||||
val items = chatItems.value
|
val from = chatItems.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
|
||||||
val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
|
if (chatItems.size <= from || from < 0) return@derivedStateOf 0
|
||||||
if (items.size <= from || from < 0) return@derivedStateOf 0
|
|
||||||
|
|
||||||
items.subList(from, items.size).count { it.isRcvNew }
|
chatItems.subList(from, chatItems.size).count { it.isRcvNew }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
|
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
|
||||||
@@ -1187,7 +1190,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
|||||||
painterResource(MR.images.ic_check),
|
painterResource(MR.images.ic_check),
|
||||||
onClick = {
|
onClick = {
|
||||||
markRead(
|
markRead(
|
||||||
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||||
bottomUnreadCount
|
bottomUnreadCount
|
||||||
)
|
)
|
||||||
showDropDown.value = false
|
showDropDown.value = false
|
||||||
@@ -1492,6 +1495,7 @@ fun PreviewChatLayout() {
|
|||||||
composeView = {},
|
composeView = {},
|
||||||
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
||||||
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
||||||
|
chatItems = chatItems,
|
||||||
searchValue,
|
searchValue,
|
||||||
useLinkPreviews = true,
|
useLinkPreviews = true,
|
||||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||||
@@ -1564,6 +1568,7 @@ fun PreviewGroupChatLayout() {
|
|||||||
composeView = {},
|
composeView = {},
|
||||||
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
||||||
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
||||||
|
chatItems = chatItems,
|
||||||
searchValue,
|
searchValue,
|
||||||
useLinkPreviews = true,
|
useLinkPreviews = true,
|
||||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||||
|
|||||||
@@ -583,10 +583,6 @@ fun ComposeView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cancelLinkPreview() {
|
fun cancelLinkPreview() {
|
||||||
val pendingLink = pendingLinkUrl.value
|
|
||||||
if (pendingLink != null) {
|
|
||||||
cancelledLinks.add(pendingLink)
|
|
||||||
}
|
|
||||||
val uri = composeState.value.linkPreview?.uri
|
val uri = composeState.value.linkPreview?.uri
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
cancelledLinks.add(uri)
|
cancelledLinks.add(uri)
|
||||||
@@ -665,7 +661,7 @@ fun ComposeView(
|
|||||||
|
|
||||||
fun editPrevMessage() {
|
fun editPrevMessage() {
|
||||||
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
|
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
|
||||||
val lastEditable = chatModel.chatItems.value.findLast { it.meta.editable }
|
val lastEditable = chatModel.chatItems.findLast { it.meta.editable }
|
||||||
if (lastEditable != null) {
|
if (lastEditable != null) {
|
||||||
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
|
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ fun SendMsgView(
|
|||||||
) {
|
) {
|
||||||
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
|
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showCustomDisappearingMessageDialog.value) {
|
||||||
|
CustomDisappearingMessageDialog(
|
||||||
|
sendMessage = sendMessage,
|
||||||
|
setShowDialog = { showCustomDisappearingMessageDialog.value = it },
|
||||||
|
customDisappearingMessageTimePref = customDisappearingMessageTimePref
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Box(Modifier.padding(vertical = 8.dp)) {
|
Box(Modifier.padding(vertical = 8.dp)) {
|
||||||
val cs = composeState.value
|
val cs = composeState.value
|
||||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -195,11 +203,6 @@ fun SendMsgView(
|
|||||||
DefaultDropdownMenu(showDropdown) {
|
DefaultDropdownMenu(showDropdown) {
|
||||||
menuItems.forEach { composable -> composable() }
|
menuItems.forEach { composable -> composable() }
|
||||||
}
|
}
|
||||||
CustomDisappearingMessageDialog(
|
|
||||||
showCustomDisappearingMessageDialog,
|
|
||||||
sendMessage = sendMessage,
|
|
||||||
customDisappearingMessageTimePref = customDisappearingMessageTimePref
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage)
|
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage)
|
||||||
}
|
}
|
||||||
@@ -217,43 +220,93 @@ expect fun VoiceButtonWithoutPermissionByPlatform()
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CustomDisappearingMessageDialog(
|
private fun CustomDisappearingMessageDialog(
|
||||||
showMenu: MutableState<Boolean>,
|
|
||||||
sendMessage: (Int?) -> Unit,
|
sendMessage: (Int?) -> Unit,
|
||||||
|
setShowDialog: (Boolean) -> Unit,
|
||||||
customDisappearingMessageTimePref: SharedPreference<Int>?
|
customDisappearingMessageTimePref: SharedPreference<Int>?
|
||||||
) {
|
) {
|
||||||
DefaultDropdownMenu(showMenu) {
|
val showCustomTimePicker = remember { mutableStateOf(false) }
|
||||||
Text(
|
|
||||||
generalGetString(MR.strings.send_disappearing_message),
|
|
||||||
Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING * 1.5f),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = MaterialTheme.colors.secondary
|
|
||||||
)
|
|
||||||
|
|
||||||
ItemAction(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
|
if (showCustomTimePicker.value) {
|
||||||
sendMessage(30)
|
val selectedDisappearingMessageTime = remember {
|
||||||
showMenu.value = false
|
mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
|
||||||
}
|
}
|
||||||
ItemAction(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
|
CustomTimePickerDialog(
|
||||||
sendMessage(60)
|
selectedDisappearingMessageTime,
|
||||||
showMenu.value = false
|
title = generalGetString(MR.strings.delete_after),
|
||||||
|
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
|
||||||
|
confirmButtonAction = { ttl ->
|
||||||
|
sendMessage(ttl)
|
||||||
|
customDisappearingMessageTimePref?.set?.invoke(ttl)
|
||||||
|
setShowDialog(false)
|
||||||
|
},
|
||||||
|
cancel = { setShowDialog(false) }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Composable
|
||||||
|
fun ChoiceButton(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
TextButton(onClick) {
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ItemAction(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
|
|
||||||
sendMessage(300)
|
DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
|
||||||
showMenu.value = false
|
Surface(
|
||||||
}
|
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
|
||||||
ItemAction(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
|
contentColor = LocalContentColor.current
|
||||||
showMenu.value = false
|
) {
|
||||||
val selectedDisappearingMessageTime = mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
|
Box(
|
||||||
showCustomTimePickerDialog(
|
contentAlignment = Alignment.Center
|
||||||
selectedDisappearingMessageTime,
|
) {
|
||||||
title = generalGetString(MR.strings.delete_after),
|
Column(
|
||||||
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
|
modifier = Modifier.padding(DEFAULT_PADDING),
|
||||||
confirmButtonAction = { ttl ->
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
sendMessage(ttl)
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
customDisappearingMessageTimePref?.set?.invoke(ttl)
|
) {
|
||||||
},
|
Row(
|
||||||
cancel = { showMenu.value = false }
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(" ") // centers title
|
||||||
|
Text(
|
||||||
|
generalGetString(MR.strings.send_disappearing_message),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_close),
|
||||||
|
generalGetString(MR.strings.icon_descr_close_button),
|
||||||
|
tint = MaterialTheme.colors.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(25.dp)
|
||||||
|
.clickable { setShowDialog(false) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
|
||||||
|
sendMessage(30)
|
||||||
|
setShowDialog(false)
|
||||||
|
}
|
||||||
|
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
|
||||||
|
sendMessage(60)
|
||||||
|
setShowDialog(false)
|
||||||
|
}
|
||||||
|
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
|
||||||
|
sendMessage(300)
|
||||||
|
setShowDialog(false)
|
||||||
|
}
|
||||||
|
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
|
||||||
|
showCustomTimePicker.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
|
|||||||
},
|
},
|
||||||
inviteMembers = {
|
inviteMembers = {
|
||||||
allowModifyMembers = false
|
allowModifyMembers = false
|
||||||
withLongRunningApi(slow = 30_000, deadlock = 120_000) {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
for (contactId in selectedContacts) {
|
for (contactId in selectedContacts) {
|
||||||
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
|
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
|
||||||
if (member != null) {
|
if (member != null) {
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package chat.simplex.common.views.chat.group
|
|||||||
import InfoRow
|
import InfoRow
|
||||||
import SectionBottomSpacer
|
import SectionBottomSpacer
|
||||||
import SectionDividerSpaced
|
import SectionDividerSpaced
|
||||||
|
import SectionItemView
|
||||||
import SectionSpacer
|
import SectionSpacer
|
||||||
import SectionTextFooter
|
import SectionTextFooter
|
||||||
import SectionView
|
import SectionView
|
||||||
|
import TextIconSpaced
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
@@ -72,8 +74,9 @@ fun GroupMemberInfoView(
|
|||||||
if (chatModel.getContactChat(it) == null) {
|
if (chatModel.getContactChat(it) == null) {
|
||||||
chatModel.addChat(c)
|
chatModel.addChat(c)
|
||||||
}
|
}
|
||||||
|
chatModel.chatItems.clear()
|
||||||
chatModel.chatItemStatuses.clear()
|
chatModel.chatItemStatuses.clear()
|
||||||
chatModel.chatItems.replaceAll(c.chatItems)
|
chatModel.chatItems.addAll(c.chatItems)
|
||||||
chatModel.chatId.value = c.id
|
chatModel.chatId.value = c.id
|
||||||
closeAll()
|
closeAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package chat.simplex.common.views.chat.group
|
|||||||
import SectionBottomSpacer
|
import SectionBottomSpacer
|
||||||
import SectionDividerSpaced
|
import SectionDividerSpaced
|
||||||
import SectionItemView
|
import SectionItemView
|
||||||
import SectionTextFooter
|
|
||||||
import SectionView
|
import SectionView
|
||||||
import TextIconSpaced
|
import TextIconSpaced
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -15,7 +14,6 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
@@ -29,13 +27,9 @@ import chat.simplex.common.views.chat.item.MarkdownText
|
|||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.model.ChatModel
|
import chat.simplex.common.model.ChatModel
|
||||||
import chat.simplex.common.model.GroupInfo
|
import chat.simplex.common.model.GroupInfo
|
||||||
import chat.simplex.common.platform.chatJsonLength
|
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
private const val maxByteCount = 1200
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) {
|
fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) {
|
||||||
var gInfo by remember { mutableStateOf(groupInfo) }
|
var gInfo by remember { mutableStateOf(groupInfo) }
|
||||||
@@ -60,11 +54,8 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
|
|||||||
|
|
||||||
ModalView(
|
ModalView(
|
||||||
close = {
|
close = {
|
||||||
when {
|
if (welcomeText.value == gInfo.groupProfile.description || (welcomeText.value == "" && gInfo.groupProfile.description == null)) close()
|
||||||
welcomeTextUnchanged(welcomeText, gInfo) -> close()
|
else showUnsavedChangesAlert({ save(close) }, close)
|
||||||
!welcomeTextFitsLimit(welcomeText) -> showUnsavedChangesTooLongAlert(close)
|
|
||||||
else -> showUnsavedChangesAlert({ save(close) }, close)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
GroupWelcomeLayout(
|
GroupWelcomeLayout(
|
||||||
@@ -76,14 +67,6 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun welcomeTextUnchanged(welcomeText: MutableState<String>, groupInfo: GroupInfo): Boolean {
|
|
||||||
return welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun welcomeTextFitsLimit(welcomeText: MutableState<String>): Boolean {
|
|
||||||
return chatJsonLength(welcomeText.value) <= maxByteCount
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GroupWelcomeLayout(
|
private fun GroupWelcomeLayout(
|
||||||
welcomeText: MutableState<String>,
|
welcomeText: MutableState<String>,
|
||||||
@@ -112,13 +95,6 @@ private fun GroupWelcomeLayout(
|
|||||||
} else {
|
} else {
|
||||||
TextPreview(wt.value, linkMode)
|
TextPreview(wt.value, linkMode)
|
||||||
}
|
}
|
||||||
SectionTextFooter(
|
|
||||||
if (!welcomeTextFitsLimit(wt)) { generalGetString(MR.strings.message_too_large) } else "",
|
|
||||||
color = if (welcomeTextFitsLimit(wt)) MaterialTheme.colors.secondary else Color.Red
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(Modifier.size(8.dp))
|
|
||||||
|
|
||||||
ChangeModeButton(
|
ChangeModeButton(
|
||||||
editMode.value,
|
editMode.value,
|
||||||
click = {
|
click = {
|
||||||
@@ -128,18 +104,10 @@ private fun GroupWelcomeLayout(
|
|||||||
)
|
)
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) }
|
CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) }
|
||||||
|
SectionDividerSpaced(maxBottomPadding = false)
|
||||||
Divider(
|
|
||||||
Modifier.padding(
|
|
||||||
start = DEFAULT_PADDING_HALF,
|
|
||||||
top = 8.dp,
|
|
||||||
end = DEFAULT_PADDING_HALF,
|
|
||||||
bottom = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
SaveButton(
|
SaveButton(
|
||||||
save = save,
|
save = save,
|
||||||
disabled = welcomeTextUnchanged(wt, groupInfo) || !welcomeTextFitsLimit(wt)
|
disabled = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
@@ -214,11 +182,3 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
|||||||
onDismiss = revert,
|
onDismiss = revert,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showUnsavedChangesTooLongAlert(revert: () -> Unit) {
|
|
||||||
AlertManager.shared.showAlertDialogStacked(
|
|
||||||
title = generalGetString(MR.strings.welcome_message_is_too_long),
|
|
||||||
confirmText = generalGetString(MR.strings.exit_without_saving),
|
|
||||||
onConfirm = revert,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ fun ChatItemView(
|
|||||||
setReaction(cInfo, cItem, !r.userReacted, r.reaction)
|
setReaction(cInfo, cItem, !r.userReacted, r.reaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier.padding(2.dp)) {
|
||||||
ReactionIcon(r.reaction.text, fontSize = 12.sp)
|
ReactionIcon(r.reaction.text, fontSize = 12.sp)
|
||||||
if (r.totalReacted > 1) {
|
if (r.totalReacted > 1) {
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
@@ -112,6 +112,7 @@ fun ChatItemView(
|
|||||||
fontSize = 11.5.sp,
|
fontSize = 11.5.sp,
|
||||||
fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal,
|
fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal,
|
||||||
color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||||
|
modifier = if (appPlatform.isAndroid) Modifier else Modifier.padding(top = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,8 +178,7 @@ fun ChatItemView(
|
|||||||
fun MsgContentItemDropdownMenu() {
|
fun MsgContentItemDropdownMenu() {
|
||||||
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
|
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
|
||||||
when {
|
when {
|
||||||
// cItem.id check is a special case for live message chat item which has negative ID while not sent yet
|
cItem.content.msgContent != null -> {
|
||||||
cItem.content.msgContent != null && cItem.id >= 0 -> {
|
|
||||||
DefaultDropdownMenu(showMenu) {
|
DefaultDropdownMenu(showMenu) {
|
||||||
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
|
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
|
||||||
MsgReactionsMenu()
|
MsgReactionsMenu()
|
||||||
@@ -332,7 +332,7 @@ fun ChatItemView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mergedGroupEventText(chatItem: ChatItem): String? {
|
fun mergedGroupEventText(chatItem: ChatItem): String? {
|
||||||
val (count, ns) = chatModel.getConnectedMemberNames(chatItem)
|
val (count, ns, lastNonConnectedEvent) = chatModel.getConnectedMemberNames(chatItem)
|
||||||
val members = when {
|
val members = when {
|
||||||
ns.size == 1 -> String.format(generalGetString(MR.strings.rcv_group_event_1_member_connected), ns[0])
|
ns.size == 1 -> String.format(generalGetString(MR.strings.rcv_group_event_1_member_connected), ns[0])
|
||||||
ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1])
|
ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1])
|
||||||
@@ -342,6 +342,8 @@ fun ChatItemView(
|
|||||||
}
|
}
|
||||||
return if (count <= 1) {
|
return if (count <= 1) {
|
||||||
null
|
null
|
||||||
|
} else if (lastNonConnectedEvent != null) {
|
||||||
|
lastNonConnectedEvent + " " + generalGetString(MR.strings.rcv_group_and_other_events).format(count - ns.size)
|
||||||
} else if (ns.isEmpty()) {
|
} else if (ns.isEmpty()) {
|
||||||
generalGetString(MR.strings.rcv_group_events_count).format(count)
|
generalGetString(MR.strings.rcv_group_events_count).format(count)
|
||||||
} else if (count > ns.size) {
|
} else if (count > ns.size) {
|
||||||
@@ -527,9 +529,8 @@ fun DeleteItemAction(
|
|||||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||||
if (range != null) {
|
if (range != null) {
|
||||||
val itemIds: ArrayList<Long> = arrayListOf()
|
val itemIds: ArrayList<Long> = arrayListOf()
|
||||||
val reversedChatItems = chatModel.chatItems.asReversed()
|
|
||||||
for (i in range) {
|
for (i in range) {
|
||||||
itemIds.add(reversedChatItems[i].id)
|
itemIds.add(chatModel.chatItems.asReversed()[i].id)
|
||||||
}
|
}
|
||||||
deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages)
|
deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages)
|
||||||
} else {
|
} else {
|
||||||
@@ -652,23 +653,6 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Unit) {
|
|
||||||
val finalColor = if (color == Color.Unspecified) {
|
|
||||||
MenuTextColor
|
|
||||||
} else color
|
|
||||||
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1F)
|
|
||||||
.padding(end = 15.dp),
|
|
||||||
color = finalColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
|
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
|
||||||
AlertManager.shared.showAlertDialog(
|
AlertManager.shared.showAlertDialog(
|
||||||
title = generalGetString(cancelAction.alert.titleId),
|
title = generalGetString(cancelAction.alert.titleId),
|
||||||
|
|||||||
@@ -212,15 +212,18 @@ suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
|
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||||
|
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
|
||||||
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
||||||
if (chat != null) {
|
if (chat != null) {
|
||||||
openLoadedChat(chat, chatModel)
|
openLoadedChat(chat, chatModel)
|
||||||
|
Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
|
fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
|
||||||
|
chatModel.chatItems.clear()
|
||||||
chatModel.chatItemStatuses.clear()
|
chatModel.chatItemStatuses.clear()
|
||||||
chatModel.chatItems.replaceAll(chat.chatItems)
|
chatModel.chatItems.addAll(chat.chatItems)
|
||||||
chatModel.chatId.value = chat.chatInfo.id
|
chatModel.chatId.value = chat.chatInfo.id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +239,8 @@ suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) {
|
|||||||
val chatInfo = ch.chatInfo
|
val chatInfo = ch.chatInfo
|
||||||
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
|
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
|
||||||
if (chatModel.chatId.value != chat.id) return
|
if (chatModel.chatId.value != chat.id) return
|
||||||
chatModel.chatItems.replaceAll(chat.chatItems)
|
chatModel.chatItems.clear()
|
||||||
|
chatModel.chatItems.addAll(0, chat.chatItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
|
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
|||||||
initialRandomDBPassphrase,
|
initialRandomDBPassphrase,
|
||||||
progressIndicator,
|
progressIndicator,
|
||||||
onConfirmEncrypt = {
|
onConfirmEncrypt = {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
|
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>, progressIndicator: MutableState<Boolean>? = null) {
|
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>, progressIndicator: MutableState<Boolean>? = null) {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
try {
|
try {
|
||||||
progressIndicator?.value = true
|
progressIndicator?.value = true
|
||||||
if (chatDbChanged.value) {
|
if (chatDbChanged.value) {
|
||||||
@@ -581,7 +581,7 @@ private fun importArchive(
|
|||||||
progressIndicator.value = true
|
progressIndicator.value = true
|
||||||
val archivePath = saveArchiveFromURI(importedArchiveURI)
|
val archivePath = saveArchiveFromURI(importedArchiveURI)
|
||||||
if (archivePath != null) {
|
if (archivePath != null) {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 60_000, deadlock = 180_000) {
|
||||||
try {
|
try {
|
||||||
m.controller.apiDeleteStorage()
|
m.controller.apiDeleteStorage()
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import chat.simplex.common.ui.theme.*
|
|||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
class AlertManager {
|
class AlertManager {
|
||||||
@@ -129,8 +128,6 @@ class AlertManager {
|
|||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
|
|
||||||
delay(200)
|
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
@@ -198,8 +195,6 @@ class AlertManager {
|
|||||||
AlertContent(text, hostDevice, extraPadding = true) {
|
AlertContent(text, hostDevice, extraPadding = true) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
|
|
||||||
delay(200)
|
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -1,21 +1,116 @@
|
|||||||
package chat.simplex.common.views.helpers
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
import SectionItemView
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||||
import chat.simplex.common.model.CustomTimeUnit
|
import chat.simplex.common.model.CustomTimeUnit
|
||||||
import chat.simplex.common.model.timeText
|
import chat.simplex.common.model.timeText
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
import com.sd.lib.compose.wheel_picker.*
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun CustomTimePicker(
|
fun CustomTimePicker(
|
||||||
selection: MutableState<Int>,
|
selection: MutableState<Int>,
|
||||||
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits
|
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits
|
||||||
)
|
) {
|
||||||
|
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
|
||||||
|
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
|
||||||
|
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
|
||||||
|
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
|
||||||
|
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
|
||||||
|
val selectedDuration = remember { mutableStateOf(duration) }
|
||||||
|
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
|
||||||
|
val isTriggered = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(selectedUnit.value) {
|
||||||
|
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
|
||||||
|
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
|
||||||
|
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
|
||||||
|
if (isTriggered.value) {
|
||||||
|
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
|
||||||
|
if (maxValue != null && selectedDuration.value > maxValue) {
|
||||||
|
selectedDuration.value = maxValue
|
||||||
|
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
|
||||||
|
} else {
|
||||||
|
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
|
||||||
|
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isTriggered.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(selectedDuration.value) {
|
||||||
|
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = DEFAULT_PADDING),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(0.dp)
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
|
||||||
|
FVerticalWheelPicker(
|
||||||
|
count = selectedUnitValues.value.count(),
|
||||||
|
state = durationPickerState,
|
||||||
|
unfocusedCount = 2,
|
||||||
|
focus = {
|
||||||
|
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
) { index ->
|
||||||
|
Text(
|
||||||
|
selectedUnitValues.value[index].toString(),
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LaunchedEffect(durationPickerState) {
|
||||||
|
snapshotFlow { durationPickerState.currentIndex }
|
||||||
|
.collect {
|
||||||
|
selectedDuration.value = selectedUnitValues.value[it]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
|
||||||
|
FVerticalWheelPicker(
|
||||||
|
count = timeUnitsLimits.count(),
|
||||||
|
state = unitPickerState,
|
||||||
|
unfocusedCount = 2,
|
||||||
|
focus = {
|
||||||
|
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
) { index ->
|
||||||
|
Text(
|
||||||
|
timeUnitsLimits[index].timeUnit.text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LaunchedEffect(unitPickerState) {
|
||||||
|
snapshotFlow { unitPickerState.currentIndex }
|
||||||
|
.collect {
|
||||||
|
selectedUnit.value = timeUnitsLimits[it].timeUnit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class TimeUnitLimits(
|
data class TimeUnitLimits(
|
||||||
val timeUnit: CustomTimeUnit,
|
val timeUnit: CustomTimeUnit,
|
||||||
@@ -46,7 +141,8 @@ data class TimeUnitLimits(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showCustomTimePickerDialog(
|
@Composable
|
||||||
|
fun CustomTimePickerDialog(
|
||||||
selection: MutableState<Int>,
|
selection: MutableState<Int>,
|
||||||
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
|
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
|
||||||
title: String,
|
title: String,
|
||||||
@@ -54,26 +150,53 @@ fun showCustomTimePickerDialog(
|
|||||||
confirmButtonAction: (Int) -> Unit,
|
confirmButtonAction: (Int) -> Unit,
|
||||||
cancel: () -> Unit
|
cancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
DefaultDialog(onDismissRequest = cancel) {
|
||||||
title = title,
|
Surface(
|
||||||
onDismissRequest = cancel
|
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
|
||||||
) {
|
contentColor = LocalContentColor.current
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
) {
|
||||||
CustomTimePicker(
|
Box(
|
||||||
selection,
|
contentAlignment = Alignment.Center
|
||||||
timeUnitsLimits
|
|
||||||
)
|
|
||||||
SectionItemView({
|
|
||||||
AlertManager.shared.hideAlert()
|
|
||||||
confirmButtonAction(selection.value)
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
confirmButtonText,
|
modifier = Modifier.padding(DEFAULT_PADDING),
|
||||||
Modifier.fillMaxWidth(),
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
textAlign = TextAlign.Center,
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
color = MaterialTheme.colors.primary
|
) {
|
||||||
)
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(" ") // centers title
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_close),
|
||||||
|
generalGetString(MR.strings.icon_descr_close_button),
|
||||||
|
tint = MaterialTheme.colors.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(25.dp)
|
||||||
|
.clickable { cancel() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomTimePicker(
|
||||||
|
selection,
|
||||||
|
timeUnitsLimits
|
||||||
|
)
|
||||||
|
|
||||||
|
TextButton(onClick = { confirmButtonAction(selection.value) }) {
|
||||||
|
Text(
|
||||||
|
confirmButtonText,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,6 +220,7 @@ fun DropdownCustomTimePickerSettingRow(
|
|||||||
|
|
||||||
val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) }
|
val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) }
|
||||||
val values: MutableState<List<DropdownSelection>> = remember { mutableStateOf(getValues(selection.value)) }
|
val values: MutableState<List<DropdownSelection>> = remember { mutableStateOf(getValues(selection.value)) }
|
||||||
|
val showCustomTimePicker = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
fun updateValue(selectedValue: Int?) {
|
fun updateValue(selectedValue: Int?) {
|
||||||
values.value = getValues(selectedValue)
|
values.value = getValues(selectedValue)
|
||||||
@@ -123,22 +247,28 @@ fun DropdownCustomTimePickerSettingRow(
|
|||||||
onSelected = { sel: DropdownSelection ->
|
onSelected = { sel: DropdownSelection ->
|
||||||
when (sel) {
|
when (sel) {
|
||||||
is DropdownSelection.DropdownValue -> updateValue(sel.value)
|
is DropdownSelection.DropdownValue -> updateValue(sel.value)
|
||||||
DropdownSelection.Custom -> {
|
DropdownSelection.Custom -> showCustomTimePicker.value = true
|
||||||
val selectedCustomTime = mutableStateOf(selection.value ?: 86400)
|
|
||||||
showCustomTimePickerDialog(
|
|
||||||
selectedCustomTime,
|
|
||||||
timeUnitsLimits = customPickerTimeUnitsLimits,
|
|
||||||
title = customPickerTitle,
|
|
||||||
confirmButtonText = customPickerConfirmButtonText,
|
|
||||||
confirmButtonAction = ::updateValue,
|
|
||||||
cancel = {
|
|
||||||
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (showCustomTimePicker.value) {
|
||||||
|
val selectedCustomTime = remember { mutableStateOf(selection.value ?: 86400) }
|
||||||
|
CustomTimePickerDialog(
|
||||||
|
selectedCustomTime,
|
||||||
|
timeUnitsLimits = customPickerTimeUnitsLimits,
|
||||||
|
title = customPickerTitle,
|
||||||
|
confirmButtonText = customPickerConfirmButtonText,
|
||||||
|
confirmButtonAction = { time ->
|
||||||
|
updateValue(time)
|
||||||
|
showCustomTimePicker.value = false
|
||||||
|
},
|
||||||
|
cancel = {
|
||||||
|
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
|
||||||
|
showCustomTimePicker.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class DropdownSelection {
|
private sealed class DropdownSelection {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ fun DefaultProgressView(description: String?) {
|
|||||||
strokeWidth = 2.5.dp
|
strokeWidth = 2.5.dp
|
||||||
)
|
)
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
Text(description, textAlign = TextAlign.Center)
|
Text(description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,10 +61,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||||
val data = ModalData()
|
val data = ModalData()
|
||||||
showCustomModal { close ->
|
showCustomModal { close ->
|
||||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) })
|
ModalView(close, showClose = showClose, content = { data.content(close) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -198,16 +198,16 @@ fun <T> SectionItemWithValue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionTextFooter(text: String, color: Color = MaterialTheme.colors.secondary) {
|
fun SectionTextFooter(text: String) {
|
||||||
SectionTextFooter(AnnotatedString(text), color = color)
|
SectionTextFooter(AnnotatedString(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start, color: Color = MaterialTheme.colors.secondary) {
|
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) {
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
|
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
|
||||||
color = color,
|
color = MaterialTheme.colors.secondary,
|
||||||
lineHeight = 18.sp,
|
lineHeight = 18.sp,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
textAlign = textAlign
|
textAlign = textAlign
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) {
|
private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
try {
|
try {
|
||||||
/** Waiting until [initChatController] finishes */
|
/** Waiting until [initChatController] finishes */
|
||||||
while (m.ctrlInitInProgress.value) {
|
while (m.ctrlInitInProgress.value) {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
|
|||||||
confirmNewKey,
|
confirmNewKey,
|
||||||
progressIndicator,
|
progressIndicator,
|
||||||
onConfirmEncrypt = {
|
onConfirmEncrypt = {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
if (m.chatRunning.value == true) {
|
if (m.chatRunning.value == true) {
|
||||||
// Stop chat if it's started before doing anything
|
// Stop chat if it's started before doing anything
|
||||||
stopChatAsync(m)
|
stopChatAsync(m)
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ fun NetworkAndServersView(
|
|||||||
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
|
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
|
||||||
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
|
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
chatModel.userSMPServersUnsaved.value = null
|
||||||
|
}
|
||||||
|
|
||||||
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
|
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
|
||||||
NetworkAndServersLayout(
|
NetworkAndServersLayout(
|
||||||
currentRemoteHost = currentRemoteHost,
|
currentRemoteHost = currentRemoteHost,
|
||||||
|
|||||||
@@ -28,18 +28,19 @@ import chat.simplex.res.MR
|
|||||||
@Composable
|
@Composable
|
||||||
fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
|
fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
|
||||||
var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) }
|
var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) }
|
||||||
var servers by remember { stateGetOrPut("servers") { emptyList<ServerCfg>() } }
|
var servers by remember(rhId) {
|
||||||
var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } }
|
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
|
||||||
|
}
|
||||||
val currServers = remember(rhId) { mutableStateOf(servers) }
|
val currServers = remember(rhId) { mutableStateOf(servers) }
|
||||||
val testing = rememberSaveable(rhId) { mutableStateOf(false) }
|
val testing = rememberSaveable(rhId) { mutableStateOf(false) }
|
||||||
val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } }
|
val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } }
|
||||||
val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } }
|
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
|
||||||
val saveDisabled = remember(servers) {
|
val saveDisabled = remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
servers.isEmpty() ||
|
servers.isEmpty() ||
|
||||||
servers == currServers.value ||
|
servers == currServers.value ||
|
||||||
testing.value ||
|
testing.value ||
|
||||||
servers.none { srv ->
|
!servers.all { srv ->
|
||||||
val address = parseServerAddress(srv.server)
|
val address = parseServerAddress(srv.server)
|
||||||
address != null && uniqueAddress(srv, address, servers)
|
address != null && uniqueAddress(srv, address, servers)
|
||||||
} ||
|
} ||
|
||||||
@@ -48,8 +49,8 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeyChangeEffect(rhId) {
|
KeyChangeEffect(rhId) {
|
||||||
|
m.userSMPServersUnsaved.value = null
|
||||||
servers = emptyList()
|
servers = emptyList()
|
||||||
serversAlreadyLoaded = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(rhId) {
|
LaunchedEffect(rhId) {
|
||||||
@@ -58,9 +59,8 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
|
|||||||
if (res != null) {
|
if (res != null) {
|
||||||
currServers.value = res.protoServers
|
currServers.value = res.protoServers
|
||||||
presetServers = res.presetServers
|
presetServers = res.presetServers
|
||||||
if (servers.isEmpty() && !serversAlreadyLoaded) {
|
if (servers.isEmpty()) {
|
||||||
servers = currServers.value
|
servers = currServers.value
|
||||||
serversAlreadyLoaded = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,11 +80,13 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
|
|||||||
newServers.add(index, updated)
|
newServers.add(index, updated)
|
||||||
old = updated
|
old = updated
|
||||||
servers = newServers
|
servers = newServers
|
||||||
|
m.userSMPServersUnsaved.value = servers
|
||||||
},
|
},
|
||||||
onDelete = {
|
onDelete = {
|
||||||
val newServers = ArrayList(servers)
|
val newServers = ArrayList(servers)
|
||||||
newServers.removeAt(index)
|
newServers.removeAt(index)
|
||||||
servers = newServers
|
servers = newServers
|
||||||
|
m.userSMPServersUnsaved.value = servers
|
||||||
close()
|
close()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -123,6 +125,7 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
|
|||||||
ScanProtocolServer(rhId) {
|
ScanProtocolServer(rhId) {
|
||||||
close()
|
close()
|
||||||
servers = servers + it
|
servers = servers + it
|
||||||
|
m.userSMPServersUnsaved.value = servers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,11 +150,13 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
|
|||||||
testServersJob.value = withLongRunningApi {
|
testServersJob.value = withLongRunningApi {
|
||||||
testServers(testing, servers, m) {
|
testServers(testing, servers, m) {
|
||||||
servers = it
|
servers = it
|
||||||
|
m.userSMPServersUnsaved.value = servers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetServers = {
|
resetServers = {
|
||||||
servers = currServers.value
|
servers = currServers.value ?: emptyList()
|
||||||
|
m.userSMPServersUnsaved.value = null
|
||||||
},
|
},
|
||||||
saveSMPServers = {
|
saveSMPServers = {
|
||||||
saveServers(rhId, serverProtocol, currServers, servers, m)
|
saveServers(rhId, serverProtocol, currServers, servers, m)
|
||||||
@@ -350,6 +355,7 @@ private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: Muta
|
|||||||
withBGApi {
|
withBGApi {
|
||||||
if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
|
if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
|
||||||
currServers.value = servers
|
currServers.value = servers
|
||||||
|
m.userSMPServersUnsaved.value = null
|
||||||
}
|
}
|
||||||
afterSave()
|
afterSave()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
<!-- MainActivity.kt -->
|
<!-- MainActivity.kt -->
|
||||||
<string name="opening_database">Opening database…</string>
|
<string name="opening_database">Opening database…</string>
|
||||||
<string name="database_migration_in_progress">Database migration is in progress.\nIt may take a few minutes.</string>
|
|
||||||
<string name="non_content_uri_alert_title">Invalid file path</string>
|
<string name="non_content_uri_alert_title">Invalid file path</string>
|
||||||
<string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string>
|
<string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string>
|
||||||
<string name="app_was_crashed">View crashed</string>
|
<string name="app_was_crashed">View crashed</string>
|
||||||
@@ -1378,11 +1377,9 @@
|
|||||||
<!-- GroupWelcomeView.kt -->
|
<!-- GroupWelcomeView.kt -->
|
||||||
<string name="group_welcome_title">Welcome message</string>
|
<string name="group_welcome_title">Welcome message</string>
|
||||||
<string name="save_welcome_message_question">Save welcome message?</string>
|
<string name="save_welcome_message_question">Save welcome message?</string>
|
||||||
<string name="welcome_message_is_too_long">Welcome message is too long</string>
|
|
||||||
<string name="save_and_update_group_profile">Save and update group profile</string>
|
<string name="save_and_update_group_profile">Save and update group profile</string>
|
||||||
<string name="group_welcome_preview">Preview</string>
|
<string name="group_welcome_preview">Preview</string>
|
||||||
<string name="enter_welcome_message">Enter welcome message…</string>
|
<string name="enter_welcome_message">Enter welcome message…</string>
|
||||||
<string name="message_too_large">Message too large</string>
|
|
||||||
|
|
||||||
<!-- ConnectionStats -->
|
<!-- ConnectionStats -->
|
||||||
<string name="conn_stats_section_title_servers">SERVERS</string>
|
<string name="conn_stats_section_title_servers">SERVERS</string>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import androidx.compose.ui.input.key.*
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.*
|
import androidx.compose.ui.window.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.ChatController
|
||||||
|
import chat.simplex.common.model.ChatModel
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
||||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
package chat.simplex.common.views.helpers
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import chat.simplex.common.model.CustomTimeUnit
|
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
actual fun CustomTimePicker(
|
|
||||||
selection: MutableState<Int>,
|
|
||||||
timeUnitsLimits: List<TimeUnitLimits>
|
|
||||||
) {
|
|
||||||
val unit = remember {
|
|
||||||
var res: CustomTimeUnit = CustomTimeUnit.Second
|
|
||||||
val found = timeUnitsLimits.asReversed().any {
|
|
||||||
if (selection.value >= it.minValue * it.timeUnit.toSeconds && selection.value <= it.maxValue * it.timeUnit.toSeconds) {
|
|
||||||
res = it.timeUnit
|
|
||||||
selection.value = (selection.value / it.timeUnit.toSeconds).coerceIn(it.minValue, it.maxValue) * it.timeUnit.toSeconds
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
// If custom interval doesn't fit in any category, set it to 1 second interval
|
|
||||||
selection.value = 1
|
|
||||||
}
|
|
||||||
mutableStateOf(res)
|
|
||||||
}
|
|
||||||
val values = remember(unit.value) {
|
|
||||||
val limit = timeUnitsLimits.first { it.timeUnit == unit.value }
|
|
||||||
val res = ArrayList<Pair<Int, String>>()
|
|
||||||
for (i in limit.minValue..limit.maxValue) {
|
|
||||||
val seconds = i * limit.timeUnit.toSeconds
|
|
||||||
val desc = i.toString()
|
|
||||||
res.add(seconds to desc)
|
|
||||||
}
|
|
||||||
if (res.none { it.first == selection.value }) {
|
|
||||||
// Doesn't fit into min..max, put it equal to the closest value
|
|
||||||
selection.value = selection.value.coerceIn(res.first().first, res.last().first)
|
|
||||||
//selection.value = res.last { it.first <= selection.value }.first
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
val units = remember {
|
|
||||||
val res = ArrayList<Pair<CustomTimeUnit, String>>()
|
|
||||||
for (unit in timeUnitsLimits) {
|
|
||||||
res.add(unit.timeUnit to unit.timeUnit.text)
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
Modifier.padding(bottom = DEFAULT_PADDING),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
ExposedDropDownSetting(
|
|
||||||
values,
|
|
||||||
selection,
|
|
||||||
textColor = MaterialTheme.colors.onBackground,
|
|
||||||
enabled = remember { mutableStateOf(true) },
|
|
||||||
onSelected = { selection.value = it }
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
|
||||||
ExposedDropDownSetting(
|
|
||||||
units,
|
|
||||||
unit,
|
|
||||||
textColor = MaterialTheme.colors.onBackground,
|
|
||||||
enabled = remember { mutableStateOf(true) },
|
|
||||||
onSelected = {
|
|
||||||
selection.value = selection.value / unit.value.toSeconds * it.toSeconds
|
|
||||||
unit.value = it
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||||
|
|
||||||
android.version_name=5.5.2
|
android.version_name=5.5
|
||||||
android.version_code=179
|
android.version_code=175
|
||||||
|
|
||||||
desktop.version_name=5.5.2
|
desktop.version_name=5.5
|
||||||
desktop.version_code=28
|
desktop.version_code=26
|
||||||
|
|
||||||
kotlin.version=1.8.20
|
kotlin.version=1.8.20
|
||||||
gradle.plugin.version=7.4.2
|
gradle.plugin.version=7.4.2
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
---
|
|
||||||
layout: layouts/article.html
|
|
||||||
title: "SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect."
|
|
||||||
date: 2024-01-24
|
|
||||||
previewBody: blog_previews/20240124.html
|
|
||||||
image: images/20240124-connect1.png
|
|
||||||
permalink: "/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html"
|
|
||||||
---
|
|
||||||
|
|
||||||
# SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.
|
|
||||||
|
|
||||||
**Published:** Jan 24, 2024
|
|
||||||
|
|
||||||
[SimpleX Chat infrastructure on Linode](#simplex-chat-infrastructure-on-linode):
|
|
||||||
- Free infrastructure.
|
|
||||||
- SimpleX servers in Linode Marketplace.
|
|
||||||
- High capacity messaging servers.
|
|
||||||
|
|
||||||
What's new in v5.5:
|
|
||||||
- [private notes](#private-notes)
|
|
||||||
- [group history](#group-history)
|
|
||||||
- [simpler UX to connect to other users](#simpler-ux-to-connect-to-other-users)
|
|
||||||
- [message delivery stability and other improvements](#message-delivery-stability-and-other-improvements)
|
|
||||||
|
|
||||||
Also, we added Hungarian (only Android) and Turkish interface languages, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat).
|
|
||||||
|
|
||||||
SimpleX Chat Android app is now available in 20 languages!
|
|
||||||
|
|
||||||
## SimpleX Chat infrastructure on Linode
|
|
||||||
|
|
||||||
We chose Linode as our hosting provider as and they have been consistently reliable, cheaper than alternatives, with excellent support and great documentation.
|
|
||||||
|
|
||||||
When Linode was acquired by Akamai, we were a bit nervous about how it may affect service quality. So far it's been working out quite well.
|
|
||||||
|
|
||||||
As the usage of SimpleX network was growing, so did our hosting costs, and from being really small they started to become significant, particularly as we didn't yet manage to optimize the servers last year.
|
|
||||||
|
|
||||||
Linode helped - we're really excited to announce that Akamai decided to support SimpleX Chat growth by accepting it into their [Linode Rise startup program](https://www.linode.com/linode-for-startups/).
|
|
||||||
|
|
||||||
Thanks to this program:
|
|
||||||
|
|
||||||
- we received free infrastructure for the first year up to $10,000 per month, no strings attached. It already saved us some money, and gave us enough time to optimize the servers - the latest version of the servers are much less costly to operate with the current traffic, and can support a much larger traffic within this limit. In the year 2 of the program we will receive 50% discount with unlimited traffic, and in year 3 - 25% discount.
|
|
||||||
|
|
||||||
- Linode Marketplace now includes [SimpleX Chat messages and file servers](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) - you can get free $100 credits for the first 2 months and run your own servers in just a few clicks, and use them in SimpleX Chat apps. Anybody can submit their application to Linode marketplace, but dedicated support we have from Linode team via this program made it simpler.
|
|
||||||
|
|
||||||
- Akamai solution engineers are helping us to design high capacity server solution, free of charge, so that a single host can provide horizontally scalable capacity for messaging, allowing for a much larger number of concurrent users on a single server address. Initially we considered using HAProxy, and the latest proof of concept uses OpenResty - a fork of Nginx with Lua script engine - to route requests from a single host to multiple SMP relays, reducing an overhead for the clients that would be configured with a smaller number of higher capacity servers. This project is still in progress, there will be more details as we roll it out.
|
|
||||||
|
|
||||||
## What's new in v5.5
|
|
||||||
|
|
||||||
### Private notes
|
|
||||||
|
|
||||||
<img src="./images/20240124-notes1.png" width="220" class="float-to-left"> <img src="./images/20240124-notes2.png" width="220" class="float-to-left">
|
|
||||||
|
|
||||||
*"Where do I put notes for myself?"* was a very common support question. There was a workaround - you could create an empty group, just with yourself, and use it to save notes, but it was not very convenient, and you could accidentally add members there.
|
|
||||||
|
|
||||||
This version has a more convenient and private alternative - the Private notes. It looks like an ordinary conversation where you can put text messages, links with previews, and any media and files, but they are not sent anywhere - they are stored locally, only on your device, with encrypted files.
|
|
||||||
|
|
||||||
You can access the Private notes created in mobile app from desktop app too, by linking a mobile and desktop apps - the feature [added in the previous version](./20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md). It allows to conveniently share files between the devices without sending them over the Internet.
|
|
||||||
|
|
||||||
### Group history
|
|
||||||
|
|
||||||
<img src="./images/20240124-history1.png" width="220" class="float-to-left"> <img src="./images/20240124-history2.png" width="220" class="float-to-left">
|
|
||||||
|
|
||||||
In the previous version, when users joined groups, they only saw an empty conversation, and the notifications of being connected to other members. This version allows group admins sending recent group history to the new members - this option is enabled by default for new groups, and can be enabled for the existing groups in the preferences. So now new members can join the conversation as soon as they join.
|
|
||||||
|
|
||||||
This does not mean that these messages are stored on any servers - the admin member that adds a new member to the group sends these messages directly when a new member joins. Groups are still fully decentralized, do not have any identity on the network, and fully private - only their members know they exist.
|
|
||||||
|
|
||||||
That is, unless a group owner decides to make it public. Groups can be registered in [SimpleX groups directory](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) to be discovered by the new members - group directory is also improved.
|
|
||||||
|
|
||||||
### Simpler UX to connect to other users
|
|
||||||
|
|
||||||
<img src="./images/20240124-connect1.png" width="220" class="float-to-left"> <img src="./images/20240124-connect2.png" width="220" class="float-to-left">
|
|
||||||
|
|
||||||
SimpleX platform has no user accounts or identities, and while it improves metadata privacy, it also makes it harder to understand how to connect to other people, particularly for the new users who are not invited by the existing users.
|
|
||||||
|
|
||||||
This version simplifies this interface by allowing to connect via the received link just by pasting the address into the search bar, as is common in many wallet apps and some other decentralized messengers. We also improved the interface of creating invitation links.
|
|
||||||
|
|
||||||
We will continue working on improving and simplifying user interface throughout the year. Please send us any feedback and suggestions to the team's address available in the app.
|
|
||||||
|
|
||||||
### Message delivery stability and other improvements
|
|
||||||
|
|
||||||
One of the long standing issues was that message reception could get stuck in some rare occasions, and only get resumed once the app is fully restarted. As Android app includes an always-on notification service that runs in background, full restart should be done via the app settings.
|
|
||||||
|
|
||||||
This version fixed many issues with message delivery stability and also added some diagnostics to identify any other cases when message delivery may stop. These fixes should also reduce battery usage, particularly on slow internet connections.
|
|
||||||
|
|
||||||
Other improvements in this version:
|
|
||||||
- you can now reveal secret messages by tapping. To send a secret message wrap in "#" characters, e.g. "\#password\#".
|
|
||||||
- you can delete the last user profile, simplifying account deletion. If you have [hidden user profiles](./20230328-simplex-chat-v4-6-hidden-profiles.md), they won't be deleted in this case, and will be accessible again once you create a new profile.
|
|
||||||
|
|
||||||
## SimpleX platform
|
|
||||||
|
|
||||||
Some links to answer the most common questions:
|
|
||||||
|
|
||||||
[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers).
|
|
||||||
|
|
||||||
[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users).
|
|
||||||
|
|
||||||
[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations).
|
|
||||||
|
|
||||||
[How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions).
|
|
||||||
|
|
||||||
Please also see our [website](https://simplex.chat).
|
|
||||||
|
|
||||||
## Help us with donations
|
|
||||||
|
|
||||||
Huge thank you to everybody who donated to SimpleX Chat!
|
|
||||||
|
|
||||||
We are prioritizing users privacy and security - it would be impossible without your support.
|
|
||||||
|
|
||||||
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
|
|
||||||
|
|
||||||
Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us.
|
|
||||||
|
|
||||||
See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate.
|
|
||||||
|
|
||||||
Thank you,
|
|
||||||
|
|
||||||
Evgeny
|
|
||||||
|
|
||||||
SimpleX Chat founder
|
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
# Blog
|
# Blog
|
||||||
|
|
||||||
Jan 24, 2024 [SimpleX Chat: free infrastructure from Linode, v5.5 released](./20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
|
||||||
|
|
||||||
SimpleX Chat infrastructure on Linode:
|
|
||||||
- Free infrastructure.
|
|
||||||
- SimpleX servers in Linode Marketplace.
|
|
||||||
- High capacity messaging servers.
|
|
||||||
|
|
||||||
What's new in v5.5:
|
|
||||||
- private notes.
|
|
||||||
- group history.
|
|
||||||
- simpler UX to connect to other users.
|
|
||||||
- message delivery stability and other improvements.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Nov 25, 2023 [SimpleX Chat v5.4 released](./20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md)
|
Nov 25, 2023 [SimpleX Chat v5.4 released](./20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md)
|
||||||
|
|
||||||
- Link mobile and desktop apps via secure quantum-resistant protocol. 🔗
|
- Link mobile and desktop apps via secure quantum-resistant protocol. 🔗
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 358 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 323 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 303 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 363 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 259 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 256 KiB |
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/simplexmq.git
|
location: https://github.com/simplex-chat/simplexmq.git
|
||||||
tag: a516c2f72c81bb4a433c4065b1b5aa484b8292b1
|
tag: 7a0cd8041bbb7d7ab2f089395a244dc4af0f9e3b
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
|||||||
@@ -385,7 +385,6 @@
|
|||||||
"chat_send_cmd"
|
"chat_send_cmd"
|
||||||
"chat_send_remote_cmd"
|
"chat_send_remote_cmd"
|
||||||
"chat_valid_name"
|
"chat_valid_name"
|
||||||
"chat_json_length"
|
|
||||||
"chat_write_file"
|
"chat_write_file"
|
||||||
];
|
];
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
@@ -488,7 +487,6 @@
|
|||||||
"chat_send_cmd"
|
"chat_send_cmd"
|
||||||
"chat_send_remote_cmd"
|
"chat_send_remote_cmd"
|
||||||
"chat_valid_name"
|
"chat_valid_name"
|
||||||
"chat_json_length"
|
|
||||||
"chat_write_file"
|
"chat_write_file"
|
||||||
];
|
];
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ EXPORTS
|
|||||||
chat_parse_server
|
chat_parse_server
|
||||||
chat_password_hash
|
chat_password_hash
|
||||||
chat_valid_name
|
chat_valid_name
|
||||||
chat_json_length
|
|
||||||
chat_encrypt_media
|
chat_encrypt_media
|
||||||
chat_decrypt_media
|
chat_decrypt_media
|
||||||
chat_write_file
|
chat_write_file
|
||||||
|
|||||||
13
package.yaml
13
package.yaml
@@ -1,5 +1,5 @@
|
|||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 5.5.2.0
|
version: 5.5.0.4
|
||||||
#synopsis:
|
#synopsis:
|
||||||
#description:
|
#description:
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
@@ -36,6 +36,7 @@ dependencies:
|
|||||||
- network >= 3.1.2.7 && < 3.2
|
- network >= 3.1.2.7 && < 3.2
|
||||||
- network-transport == 0.5.6
|
- network-transport == 0.5.6
|
||||||
- optparse-applicative >= 0.15 && < 0.17
|
- optparse-applicative >= 0.15 && < 0.17
|
||||||
|
- process == 1.6.*
|
||||||
- random >= 1.1 && < 1.3
|
- random >= 1.1 && < 1.3
|
||||||
- record-hasfield == 1.0.*
|
- record-hasfield == 1.0.*
|
||||||
- simple-logger == 0.1.*
|
- simple-logger == 0.1.*
|
||||||
@@ -63,13 +64,11 @@ when:
|
|||||||
- condition: impl(ghc >= 9.6.2)
|
- condition: impl(ghc >= 9.6.2)
|
||||||
dependencies:
|
dependencies:
|
||||||
- bytestring == 0.11.*
|
- bytestring == 0.11.*
|
||||||
- process == 1.6.*
|
|
||||||
- template-haskell == 2.20.*
|
- template-haskell == 2.20.*
|
||||||
- text >= 2.0.1 && < 2.2
|
- text >= 2.0.1 && < 2.2
|
||||||
- condition: impl(ghc < 9.6.2)
|
- condition: impl(ghc < 9.6.2)
|
||||||
dependencies:
|
dependencies:
|
||||||
- bytestring == 0.10.*
|
- bytestring == 0.10.*
|
||||||
- process >= 1.6 && < 1.6.18
|
|
||||||
- template-haskell == 2.16.*
|
- template-haskell == 2.16.*
|
||||||
- text >= 1.2.3.0 && < 1.3
|
- text >= 1.2.3.0 && < 1.3
|
||||||
|
|
||||||
@@ -126,19 +125,13 @@ tests:
|
|||||||
- apps/simplex-broadcast-bot/src
|
- apps/simplex-broadcast-bot/src
|
||||||
- apps/simplex-directory-service/src
|
- apps/simplex-directory-service/src
|
||||||
main: Test.hs
|
main: Test.hs
|
||||||
when:
|
|
||||||
- condition: impl(ghc >= 9.6.2)
|
|
||||||
dependencies:
|
|
||||||
- hspec == 2.11.*
|
|
||||||
- condition: impl(ghc < 9.6.2)
|
|
||||||
dependencies:
|
|
||||||
- hspec == 2.7.*
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- QuickCheck == 2.14.*
|
- QuickCheck == 2.14.*
|
||||||
- simplex-chat
|
- simplex-chat
|
||||||
- async == 2.2.*
|
- async == 2.2.*
|
||||||
- deepseq == 1.4.*
|
- deepseq == 1.4.*
|
||||||
- generic-random == 1.5.*
|
- generic-random == 1.5.*
|
||||||
|
- hspec == 2.11.*
|
||||||
- network == 3.1.*
|
- network == 3.1.*
|
||||||
- silently == 1.2.*
|
- silently == 1.2.*
|
||||||
- stm == 2.5.*
|
- stm == 2.5.*
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
|
|||||||
cd $root_dir
|
cd $root_dir
|
||||||
BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-*
|
BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-*
|
||||||
|
|
||||||
exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/Chat/Mobile.hs | sed 's/foreign export ccall "chat_reopen_store"//' |grep "foreign export ccall" | cut -d '"' -f2) )
|
|
||||||
for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done
|
|
||||||
for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done
|
|
||||||
|
|
||||||
rm -rf $BUILD_DIR
|
rm -rf $BUILD_DIR
|
||||||
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded'
|
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded'
|
||||||
cd $BUILD_DIR/build
|
cd $BUILD_DIR/build
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ GHC_LIBS_DIR=$(ghc --print-libdir)
|
|||||||
|
|
||||||
BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-*
|
BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-*
|
||||||
|
|
||||||
exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/Chat/Mobile.hs | sed 's/foreign export ccall "chat_reopen_store"//' |grep "foreign export ccall" | cut -d '"' -f2) )
|
|
||||||
for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done
|
|
||||||
for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done
|
|
||||||
|
|
||||||
rm -rf $BUILD_DIR
|
rm -rf $BUILD_DIR
|
||||||
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi"
|
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi"
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ fi
|
|||||||
|
|
||||||
BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-*/simplex-chat-*
|
BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-*/simplex-chat-*
|
||||||
|
|
||||||
exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/Chat/Mobile.hs | sed 's/foreign export ccall "chat_reopen_store"//' |grep "foreign export ccall" | cut -d '"' -f2) )
|
|
||||||
for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done
|
|
||||||
for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done
|
|
||||||
|
|
||||||
# IMPORTANT: in order to get a working build you should use x86_64 MinGW with make, cmake, gcc.
|
# IMPORTANT: in order to get a working build you should use x86_64 MinGW with make, cmake, gcc.
|
||||||
# 100% working MinGW is https://github.com/brechtsanders/winlibs_mingw/releases/download/13.1.0-16.0.5-11.0.0-ucrt-r5/winlibs-x86_64-posix-seh-gcc-13.1.0-mingw-w64ucrt-11.0.0-r5.zip
|
# 100% working MinGW is https://github.com/brechtsanders/winlibs_mingw/releases/download/13.1.0-16.0.5-11.0.0-ucrt-r5/winlibs-x86_64-posix-seh-gcc-13.1.0-mingw-w64ucrt-11.0.0-r5.zip
|
||||||
# Many other distributions I tested don't work in some cases or don't have required tools.
|
# Many other distributions I tested don't work in some cases or don't have required tools.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."a516c2f72c81bb4a433c4065b1b5aa484b8292b1" = "05ny2i262c236li5w040i1nd3l037cpzgbzjknlla9dd139f3al3";
|
"https://github.com/simplex-chat/simplexmq.git"."7a0cd8041bbb7d7ab2f089395a244dc4af0f9e3b" = "0jxf9dnsg14ffd1y3i7md2ninrds4daq1fmpnd6j5z99im07ns52";
|
||||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
|||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
|
|
||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 5.5.2.0
|
version: 5.5.0.4
|
||||||
category: Web, System, Services, Cryptography
|
category: Web, System, Services, Cryptography
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
author: simplex.chat
|
author: simplex.chat
|
||||||
@@ -197,6 +197,7 @@ library
|
|||||||
, network >=3.1.2.7 && <3.2
|
, network >=3.1.2.7 && <3.2
|
||||||
, network-transport ==0.5.6
|
, network-transport ==0.5.6
|
||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
|
, process ==1.6.*
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
@@ -216,13 +217,11 @@ library
|
|||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
bytestring ==0.11.*
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
, template-haskell ==2.20.*
|
||||||
, text >=2.0.1 && <2.2
|
, text >=2.0.1 && <2.2
|
||||||
if impl(ghc < 9.6.2)
|
if impl(ghc < 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.10.*
|
bytestring ==0.10.*
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
, template-haskell ==2.16.*
|
||||||
, text >=1.2.3.0 && <1.3
|
, text >=1.2.3.0 && <1.3
|
||||||
|
|
||||||
@@ -257,6 +256,7 @@ executable simplex-bot
|
|||||||
, network >=3.1.2.7 && <3.2
|
, network >=3.1.2.7 && <3.2
|
||||||
, network-transport ==0.5.6
|
, network-transport ==0.5.6
|
||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
|
, process ==1.6.*
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
@@ -277,13 +277,11 @@ executable simplex-bot
|
|||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
bytestring ==0.11.*
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
, template-haskell ==2.20.*
|
||||||
, text >=2.0.1 && <2.2
|
, text >=2.0.1 && <2.2
|
||||||
if impl(ghc < 9.6.2)
|
if impl(ghc < 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.10.*
|
bytestring ==0.10.*
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
, template-haskell ==2.16.*
|
||||||
, text >=1.2.3.0 && <1.3
|
, text >=1.2.3.0 && <1.3
|
||||||
|
|
||||||
@@ -318,6 +316,7 @@ executable simplex-bot-advanced
|
|||||||
, network >=3.1.2.7 && <3.2
|
, network >=3.1.2.7 && <3.2
|
||||||
, network-transport ==0.5.6
|
, network-transport ==0.5.6
|
||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
|
, process ==1.6.*
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
@@ -338,13 +337,11 @@ executable simplex-bot-advanced
|
|||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
bytestring ==0.11.*
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
, template-haskell ==2.20.*
|
||||||
, text >=2.0.1 && <2.2
|
, text >=2.0.1 && <2.2
|
||||||
if impl(ghc < 9.6.2)
|
if impl(ghc < 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.10.*
|
bytestring ==0.10.*
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
, template-haskell ==2.16.*
|
||||||
, text >=1.2.3.0 && <1.3
|
, text >=1.2.3.0 && <1.3
|
||||||
|
|
||||||
@@ -381,6 +378,7 @@ executable simplex-broadcast-bot
|
|||||||
, network >=3.1.2.7 && <3.2
|
, network >=3.1.2.7 && <3.2
|
||||||
, network-transport ==0.5.6
|
, network-transport ==0.5.6
|
||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
|
, process ==1.6.*
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
@@ -401,13 +399,11 @@ executable simplex-broadcast-bot
|
|||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
bytestring ==0.11.*
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
, template-haskell ==2.20.*
|
||||||
, text >=2.0.1 && <2.2
|
, text >=2.0.1 && <2.2
|
||||||
if impl(ghc < 9.6.2)
|
if impl(ghc < 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.10.*
|
bytestring ==0.10.*
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
, template-haskell ==2.16.*
|
||||||
, text >=1.2.3.0 && <1.3
|
, text >=1.2.3.0 && <1.3
|
||||||
|
|
||||||
@@ -443,6 +439,7 @@ executable simplex-chat
|
|||||||
, network ==3.1.*
|
, network ==3.1.*
|
||||||
, network-transport ==0.5.6
|
, network-transport ==0.5.6
|
||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
|
, process ==1.6.*
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
@@ -464,13 +461,11 @@ executable simplex-chat
|
|||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
bytestring ==0.11.*
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
, template-haskell ==2.20.*
|
||||||
, text >=2.0.1 && <2.2
|
, text >=2.0.1 && <2.2
|
||||||
if impl(ghc < 9.6.2)
|
if impl(ghc < 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.10.*
|
bytestring ==0.10.*
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
, template-haskell ==2.16.*
|
||||||
, text >=1.2.3.0 && <1.3
|
, text >=1.2.3.0 && <1.3
|
||||||
|
|
||||||
@@ -510,6 +505,7 @@ executable simplex-directory-service
|
|||||||
, network >=3.1.2.7 && <3.2
|
, network >=3.1.2.7 && <3.2
|
||||||
, network-transport ==0.5.6
|
, network-transport ==0.5.6
|
||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
|
, process ==1.6.*
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
@@ -530,13 +526,11 @@ executable simplex-directory-service
|
|||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
bytestring ==0.11.*
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
, template-haskell ==2.20.*
|
||||||
, text >=2.0.1 && <2.2
|
, text >=2.0.1 && <2.2
|
||||||
if impl(ghc < 9.6.2)
|
if impl(ghc < 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.10.*
|
bytestring ==0.10.*
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
, template-haskell ==2.16.*
|
||||||
, text >=1.2.3.0 && <1.3
|
, text >=1.2.3.0 && <1.3
|
||||||
|
|
||||||
@@ -598,6 +592,7 @@ test-suite simplex-chat-test
|
|||||||
, exceptions ==0.10.*
|
, exceptions ==0.10.*
|
||||||
, filepath ==1.4.*
|
, filepath ==1.4.*
|
||||||
, generic-random ==1.5.*
|
, generic-random ==1.5.*
|
||||||
|
, hspec ==2.11.*
|
||||||
, http-types ==0.12.*
|
, http-types ==0.12.*
|
||||||
, http2 >=4.2.2 && <4.3
|
, http2 >=4.2.2 && <4.3
|
||||||
, memory ==0.18.*
|
, memory ==0.18.*
|
||||||
@@ -605,6 +600,7 @@ test-suite simplex-chat-test
|
|||||||
, network ==3.1.*
|
, network ==3.1.*
|
||||||
, network-transport ==0.5.6
|
, network-transport ==0.5.6
|
||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
|
, process ==1.6.*
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, silently ==1.2.*
|
, silently ==1.2.*
|
||||||
@@ -626,18 +622,10 @@ test-suite simplex-chat-test
|
|||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
bytestring ==0.11.*
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
, template-haskell ==2.20.*
|
||||||
, text >=2.0.1 && <2.2
|
, text >=2.0.1 && <2.2
|
||||||
if impl(ghc < 9.6.2)
|
if impl(ghc < 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.10.*
|
bytestring ==0.10.*
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
, template-haskell ==2.16.*
|
||||||
, text >=1.2.3.0 && <1.3
|
, text >=1.2.3.0 && <1.3
|
||||||
if impl(ghc >= 9.6.2)
|
|
||||||
build-depends:
|
|
||||||
hspec ==2.11.*
|
|
||||||
if impl(ghc < 9.6.2)
|
|
||||||
build-depends:
|
|
||||||
hspec ==2.7.*
|
|
||||||
|
|||||||
@@ -1028,7 +1028,6 @@ processChatCommand' vr = \case
|
|||||||
when (memberActive membership && isOwner) . void $ sendGroupMessage' user gInfo members XGrpDel
|
when (memberActive membership && isOwner) . void $ sendGroupMessage' user gInfo members XGrpDel
|
||||||
deleteGroupLinkIfExists user gInfo
|
deleteGroupLinkIfExists user gInfo
|
||||||
deleteMembersConnections user members
|
deleteMembersConnections user members
|
||||||
updateCIGroupInvitationStatus user gInfo CIGISRejected `catchChatError` \_ -> pure ()
|
|
||||||
-- functions below are called in separate transactions to prevent crashes on android
|
-- functions below are called in separate transactions to prevent crashes on android
|
||||||
-- (possibly, race condition on integrity check?)
|
-- (possibly, race condition on integrity check?)
|
||||||
withStore' $ \db -> deleteGroupConnectionsAndFiles db user gInfo members
|
withStore' $ \db -> deleteGroupConnectionsAndFiles db user gInfo members
|
||||||
@@ -1687,9 +1686,17 @@ processChatCommand' vr = \case
|
|||||||
createMemberConnection db userId fromMember agentConnId (fromJVersionRange peerChatVRange) subMode
|
createMemberConnection db userId fromMember agentConnId (fromJVersionRange peerChatVRange) subMode
|
||||||
updateGroupMemberStatus db userId fromMember GSMemAccepted
|
updateGroupMemberStatus db userId fromMember GSMemAccepted
|
||||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||||
updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` \_ -> pure ()
|
updateCIGroupInvitationStatus user
|
||||||
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
|
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
|
||||||
Nothing -> throwChatError $ CEContactNotActive ct
|
Nothing -> throwChatError $ CEContactNotActive ct
|
||||||
|
where
|
||||||
|
updateCIGroupInvitationStatus user = do
|
||||||
|
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId
|
||||||
|
case (cInfo, content) of
|
||||||
|
(DirectChat ct, CIRcvGroupInvitation ciGroupInv memRole) -> do
|
||||||
|
let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = CIGISAccepted} memRole
|
||||||
|
updateDirectChatItemView user ct itemId aciContent False Nothing
|
||||||
|
_ -> pure () -- prohibited
|
||||||
APIMemberRole groupId memberId memRole -> withUser $ \user -> do
|
APIMemberRole groupId memberId memRole -> withUser $ \user -> do
|
||||||
Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId
|
Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId
|
||||||
if memberId == groupMemberId' membership
|
if memberId == groupMemberId' membership
|
||||||
@@ -2505,14 +2512,6 @@ processChatCommand' vr = \case
|
|||||||
cReqHashes :: (ConnReqUriHash, ConnReqUriHash)
|
cReqHashes :: (ConnReqUriHash, ConnReqUriHash)
|
||||||
cReqHashes = bimap hash hash cReqSchemas
|
cReqHashes = bimap hash hash cReqSchemas
|
||||||
hash = ConnReqUriHash . C.sha256Hash . strEncode
|
hash = ConnReqUriHash . C.sha256Hash . strEncode
|
||||||
updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do
|
|
||||||
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId
|
|
||||||
case (cInfo, content) of
|
|
||||||
(DirectChat ct, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole)
|
|
||||||
| status == CIGISPending -> do
|
|
||||||
let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = newStatus} memRole
|
|
||||||
updateDirectChatItemView user ct itemId aciContent False Nothing
|
|
||||||
_ -> pure () -- prohibited
|
|
||||||
|
|
||||||
toggleNtf :: ChatMonad m => User -> GroupMember -> Bool -> m ()
|
toggleNtf :: ChatMonad m => User -> GroupMember -> Bool -> m ()
|
||||||
toggleNtf user m ntfOn =
|
toggleNtf user m ntfOn =
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import qualified Data.ByteArray as BA
|
|||||||
import qualified Data.ByteString.Base64.URL as U
|
import qualified Data.ByteString.Base64.URL as U
|
||||||
import Data.ByteString.Char8 (ByteString)
|
import Data.ByteString.Char8 (ByteString)
|
||||||
import qualified Data.ByteString.Char8 as B
|
import qualified Data.ByteString.Char8 as B
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
|
||||||
import Data.Functor (($>))
|
import Data.Functor (($>))
|
||||||
import Data.List (find)
|
import Data.List (find)
|
||||||
import qualified Data.List.NonEmpty as L
|
import qualified Data.List.NonEmpty as L
|
||||||
@@ -95,8 +94,6 @@ foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CStrin
|
|||||||
|
|
||||||
foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString
|
foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString
|
||||||
|
|
||||||
foreign export ccall "chat_json_length" cChatJsonLength :: CString -> IO CInt
|
|
||||||
|
|
||||||
foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString
|
foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString
|
||||||
|
|
||||||
foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
|
foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
|
||||||
@@ -179,10 +176,6 @@ cChatPasswordHash cPwd cSalt = do
|
|||||||
cChatValidName :: CString -> IO CString
|
cChatValidName :: CString -> IO CString
|
||||||
cChatValidName cName = newCString . mkValidName =<< peekCString cName
|
cChatValidName cName = newCString . mkValidName =<< peekCString cName
|
||||||
|
|
||||||
-- | returns length of JSON encoded string
|
|
||||||
cChatJsonLength :: CString -> IO CInt
|
|
||||||
cChatJsonLength s = fromIntegral . subtract 2 . LB.length . J.encode . safeDecodeUtf8 <$> B.packCString s
|
|
||||||
|
|
||||||
mobileChatOpts :: String -> ChatOpts
|
mobileChatOpts :: String -> ChatOpts
|
||||||
mobileChatOpts dbFilePrefix =
|
mobileChatOpts dbFilePrefix =
|
||||||
ChatOpts
|
ChatOpts
|
||||||
@@ -271,18 +264,9 @@ chatSendRemoteCmd :: ChatController -> Maybe RemoteHostId -> B.ByteString -> IO
|
|||||||
chatSendRemoteCmd cc rh s = J.encode . APIResponse Nothing rh <$> runReaderT (execChatCommand rh s) cc
|
chatSendRemoteCmd cc rh s = J.encode . APIResponse Nothing rh <$> runReaderT (execChatCommand rh s) cc
|
||||||
|
|
||||||
chatRecvMsg :: ChatController -> IO JSONByteString
|
chatRecvMsg :: ChatController -> IO JSONByteString
|
||||||
chatRecvMsg ChatController {outputQ} = json <$> readChatResponse
|
chatRecvMsg ChatController {outputQ} = json <$> atomically (readTBQueue outputQ)
|
||||||
where
|
where
|
||||||
json (corr, remoteHostId, resp) = J.encode APIResponse {corr, remoteHostId, resp}
|
json (corr, remoteHostId, resp) = J.encode APIResponse {corr, remoteHostId, resp}
|
||||||
readChatResponse = do
|
|
||||||
out@(_, _, cr) <- atomically $ readTBQueue outputQ
|
|
||||||
if filterEvent cr then pure out else readChatResponse
|
|
||||||
filterEvent = \case
|
|
||||||
CRGroupSubscribed {} -> False
|
|
||||||
CRGroupEmpty {} -> False
|
|
||||||
CRMemberSubSummary {} -> False
|
|
||||||
CRPendingSubSummary {} -> False
|
|
||||||
_ -> True
|
|
||||||
|
|
||||||
chatRecvMsgWait :: ChatController -> Int -> IO JSONByteString
|
chatRecvMsgWait :: ChatController -> Int -> IO JSONByteString
|
||||||
chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc)
|
chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc)
|
||||||
|
|||||||
@@ -68,8 +68,6 @@ mobileTests = do
|
|||||||
it "no exception on missing file" testMissingFileEncryptionCApi
|
it "no exception on missing file" testMissingFileEncryptionCApi
|
||||||
describe "validate name" $ do
|
describe "validate name" $ do
|
||||||
it "should convert invalid name to a valid name" testValidNameCApi
|
it "should convert invalid name to a valid name" testValidNameCApi
|
||||||
describe "JSON length" $ do
|
|
||||||
it "should compute length of JSON encoded string" testChatJsonLengthCApi
|
|
||||||
|
|
||||||
noActiveUser :: LB.ByteString
|
noActiveUser :: LB.ByteString
|
||||||
noActiveUser =
|
noActiveUser =
|
||||||
@@ -224,6 +222,8 @@ testChatApi tmp = do
|
|||||||
chatSendCmd cc "/_start" `shouldReturn` chatStarted
|
chatSendCmd cc "/_start" `shouldReturn` chatStarted
|
||||||
chatRecvMsg cc `shouldReturn` networkStatuses
|
chatRecvMsg cc `shouldReturn` networkStatuses
|
||||||
chatRecvMsg cc `shouldReturn` userContactSubSummary
|
chatRecvMsg cc `shouldReturn` userContactSubSummary
|
||||||
|
chatRecvMsg cc `shouldReturn` memberSubSummary
|
||||||
|
chatRecvMsgWait cc 10000 `shouldReturn` pendingSubSummary
|
||||||
chatRecvMsgWait cc 10000 `shouldReturn` ""
|
chatRecvMsgWait cc 10000 `shouldReturn` ""
|
||||||
chatParseMarkdown "hello" `shouldBe` "{}"
|
chatParseMarkdown "hello" `shouldBe` "{}"
|
||||||
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown
|
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown
|
||||||
@@ -356,13 +356,6 @@ testValidNameCApi _ = do
|
|||||||
cName2 <- cChatValidName =<< newCString " @'Джон' Доу 👍 "
|
cName2 <- cChatValidName =<< newCString " @'Джон' Доу 👍 "
|
||||||
peekCString cName2 `shouldReturn` goodName
|
peekCString cName2 `shouldReturn` goodName
|
||||||
|
|
||||||
testChatJsonLengthCApi :: FilePath -> IO ()
|
|
||||||
testChatJsonLengthCApi _ = do
|
|
||||||
cInt1 <- cChatJsonLength =<< newCString "Hello!"
|
|
||||||
cInt1 `shouldBe` 6
|
|
||||||
cInt2 <- cChatJsonLength =<< newCString "こんにちは!"
|
|
||||||
cInt2 `shouldBe` 18
|
|
||||||
|
|
||||||
jDecode :: FromJSON a => String -> IO (Maybe a)
|
jDecode :: FromJSON a => String -> IO (Maybe a)
|
||||||
jDecode = pure . J.decode . LB.pack
|
jDecode = pure . J.decode . LB.pack
|
||||||
|
|
||||||
|
|||||||
@@ -250,7 +250,5 @@
|
|||||||
"stable-versions-built-by-f-droid-org": "Stable versions built by F-Droid.org",
|
"stable-versions-built-by-f-droid-org": "Stable versions built by F-Droid.org",
|
||||||
"releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done 1-2 days later",
|
"releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done 1-2 days later",
|
||||||
"f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>export</a> the chat database and re-install the app.",
|
"f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>export</a> the chat database and re-install the app.",
|
||||||
"jobs": "Join team",
|
"jobs": "Join team"
|
||||||
"please-enable-javascript": "Please enable JavaScript to see the QR code.",
|
|
||||||
"please-use-link-in-mobile-app": "Please use the link in the mobile app"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<p><strong>SimpleX Chat infrastructure on Linode:</strong></p>
|
|
||||||
|
|
||||||
<ul class="mb-[12px]">
|
|
||||||
<li>free infrastructure.</li>
|
|
||||||
<li>SimpleX servers in Linode Marketplace.</li>
|
|
||||||
<li>high capacity messaging servers.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>v5.5 is released:</strong></p>
|
|
||||||
|
|
||||||
<ul class="mb-[12px]">
|
|
||||||
<li>private notes.</li>
|
|
||||||
<li>group history</li>
|
|
||||||
<li>simpler UX to connect to other people</li>
|
|
||||||
<li>message delivery, battery usage and other improvements</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Also, the app interface is now available in Hungarian and Turkish - thanks to our users.</p>
|
|
||||||
|
|
||||||
<p>SimpleX Chat Android and desktop apps are now available in 20 languages!</p>
|
|
||||||
@@ -30,12 +30,8 @@
|
|||||||
<div class="absolute mt-[-100px]">
|
<div class="absolute mt-[-100px]">
|
||||||
<img class="" src="/img/new/contact_page_mobile.png" alt="">
|
<img class="" src="/img/new/contact_page_mobile.png" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<noscript class="z-10 flex flex-col items-center pt-[40px] ml-[-15px]">
|
|
||||||
<p class="text-2xl font-medium text-center max-w-[234px] mb-32">{{ "please-enable-javascript" | i18n({}, lang ) | safe }}</p>
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div class="z-10 flex flex-col items-center pt-[40px] ml-[-15px] d-none-if-js-disabled">
|
<div class="z-10 flex flex-col items-center pt-[40px] ml-[-15px]">
|
||||||
<p class="text-base font-medium text-center max-w-[234px]">{{ "scan-qr-code-from-mobile-app" | i18n({}, lang ) | safe }}</p>
|
<p class="text-base font-medium text-center max-w-[234px]">{{ "scan-qr-code-from-mobile-app" | i18n({}, lang ) | safe }}</p>
|
||||||
<canvas class="conn_req_uri_qrcode"></canvas>
|
<canvas class="conn_req_uri_qrcode"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,11 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 mb-6 relative">
|
<div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 mb-6 relative">
|
||||||
<p class="text-xl font-medium text-grey-black dark:text-white mb-4 d-none-if-js-disabled">{{ "connect-in-app" | i18n({}, lang ) | safe }}</p>
|
<p class="text-xl font-medium text-grey-black dark:text-white mb-4">{{ "connect-in-app" | i18n({}, lang ) | safe }}</p>
|
||||||
<noscript>
|
|
||||||
<p class="text-xl font-medium text-grey-black dark:text-white mb-4">{{ "please-use-link-in-mobile-app" | i18n({}, lang ) | safe }}</p>
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<a id="mobile_conn_req_uri" class="bg-[#0053D0] text-white py-3 px-8 rounded-[34px] h-[44px] text-[16px] leading-[19px] tracking-[0.02em]">{{ "open-simplex-app" | i18n({}, lang ) | safe }}</a>
|
<a id="mobile_conn_req_uri" class="bg-[#0053D0] text-white py-3 px-8 rounded-[34px] h-[44px] text-[16px] leading-[19px] tracking-[0.02em]">{{ "open-simplex-app" | i18n({}, lang ) | safe }}</a>
|
||||||
|
|
||||||
<div class="absolute bg-[#0197FF] h-[44px] w-[44px] rounded-full flex items-center justify-center top-0 left-0 translate-x-[-30%] translate-y-[-30%]">
|
<div class="absolute bg-[#0197FF] h-[44px] w-[44px] rounded-full flex items-center justify-center top-0 left-0 translate-x-[-30%] translate-y-[-30%]">
|
||||||
@@ -77,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 relative d-none-if-js-disabled">
|
<div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 relative">
|
||||||
<p class="text-xl font-medium text-grey-black dark:text-white max-w-[230px] text-center">{{ "tap-the-connect-button-in-the-app" | i18n({}, lang ) | safe }}</p>
|
<p class="text-xl font-medium text-grey-black dark:text-white max-w-[230px] text-center">{{ "tap-the-connect-button-in-the-app" | i18n({}, lang ) | safe }}</p>
|
||||||
|
|
||||||
<div class="absolute bg-[#0197FF] h-[44px] w-[44px] rounded-full flex items-center justify-center top-0 left-0 translate-x-[-30%] translate-y-[-30%]">
|
<div class="absolute bg-[#0197FF] h-[44px] w-[44px] rounded-full flex items-center justify-center top-0 left-0 translate-x-[-30%] translate-y-[-30%]">
|
||||||
@@ -89,7 +81,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
<section class="hidden md:block bg-secondary-bg-light dark:bg-secondary-bg-dark py-[20px] d-none-if-js-disabled">
|
<section class="hidden md:block bg-secondary-bg-light dark:bg-secondary-bg-dark py-[20px]">
|
||||||
<div class="container px-5">
|
<div class="container px-5">
|
||||||
<div class="text-grey-black dark:text-white">
|
<div class="text-grey-black dark:text-white">
|
||||||
|
|
||||||
@@ -172,7 +164,3 @@
|
|||||||
|
|
||||||
{# join simplex #}
|
{# join simplex #}
|
||||||
{% include "sections/join_simplex.html" %}
|
{% include "sections/join_simplex.html" %}
|
||||||
|
|
||||||
<script>
|
|
||||||
document.querySelectorAll('.d-none-if-js-disabled').forEach(el => el.classList.remove('d-none-if-js-disabled'));
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -957,7 +957,3 @@ p a{
|
|||||||
top: calc(66px + 2rem);
|
top: calc(66px + 2rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-none-if-js-disabled{
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user