Merge branch 'stable' into stable-android
This commit is contained in:
commit
e5011ad150
@ -1172,7 +1172,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
|
|||||||
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
|
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
|
||||||
return ChatModel.shared.chats
|
return ChatModel.shared.chats
|
||||||
.compactMap{ $0.chatInfo.contact }
|
.compactMap{ $0.chatInfo.contact }
|
||||||
.filter{ !memberContactIds.contains($0.apiId) }
|
.filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) }
|
||||||
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
|
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +65,8 @@ struct MarkedDeletedItemView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// same texts are in markedDeletedText in ChatPreviewView, but it returns String;
|
||||||
|
// can be refactored into a single function if functions calling these are changed to return same type
|
||||||
var markedDeletedText: LocalizedStringKey {
|
var markedDeletedText: LocalizedStringKey {
|
||||||
switch chatItem.meta.itemDeleted {
|
switch chatItem.meta.itemDeleted {
|
||||||
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"
|
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"
|
||||||
|
@ -159,12 +159,13 @@ struct ChatView: View {
|
|||||||
switch cInfo {
|
switch cInfo {
|
||||||
case let .direct(contact):
|
case let .direct(contact):
|
||||||
HStack {
|
HStack {
|
||||||
if contact.allowsFeature(.calls) {
|
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
||||||
|
if callsPrefEnabled {
|
||||||
callButton(contact, .audio, imageName: "phone")
|
callButton(contact, .audio, imageName: "phone")
|
||||||
.disabled(!contact.ready || !contact.active)
|
.disabled(!contact.ready || !contact.active)
|
||||||
}
|
}
|
||||||
Menu {
|
Menu {
|
||||||
if contact.allowsFeature(.calls) {
|
if callsPrefEnabled {
|
||||||
Button {
|
Button {
|
||||||
CallController.shared.startCall(contact, .video)
|
CallController.shared.startCall(contact, .video)
|
||||||
} label: {
|
} label: {
|
||||||
@ -748,7 +749,9 @@ 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))
|
||||||
}
|
}
|
||||||
menu.append(viewInfoUIAction(ci))
|
if !ci.isLiveDummy {
|
||||||
|
menu.append(viewInfoUIAction(ci))
|
||||||
|
}
|
||||||
if revealed {
|
if revealed {
|
||||||
menu.append(hideUIAction())
|
menu.append(hideUIAction())
|
||||||
}
|
}
|
||||||
|
@ -978,6 +978,9 @@ 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,7 +370,11 @@ struct GroupChatInfoView: View {
|
|||||||
|
|
||||||
private func addOrEditWelcomeMessage() -> some View {
|
private func addOrEditWelcomeMessage() -> some View {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo)
|
GroupWelcomeView(
|
||||||
|
groupInfo: $groupInfo,
|
||||||
|
groupProfile: groupInfo.groupProfile,
|
||||||
|
welcomeText: groupInfo.groupProfile.description ?? ""
|
||||||
|
)
|
||||||
.navigationTitle("Welcome message")
|
.navigationTitle("Welcome message")
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -11,29 +11,32 @@ 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 private var welcomeText: String = ""
|
@State var groupProfile: GroupProfile
|
||||||
|
@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 welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) {
|
if welcomeTextUnchanged() {
|
||||||
dismiss()
|
dismiss()
|
||||||
} else {
|
} else {
|
||||||
showSaveDialog = true
|
showSaveDialog = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) {
|
.confirmationDialog(
|
||||||
Button("Save and update group profile") {
|
welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long",
|
||||||
save()
|
isPresented: $showSaveDialog
|
||||||
dismiss()
|
) {
|
||||||
|
if welcomeTextFitsLimit() {
|
||||||
|
Button("Save and update group profile") { save() }
|
||||||
}
|
}
|
||||||
Button("Exit without saving") { dismiss() }
|
Button("Exit without saving") { dismiss() }
|
||||||
}
|
}
|
||||||
@ -47,14 +50,15 @@ struct GroupWelcomeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
welcomeText = groupInfo.groupProfile.description ?? ""
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
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: 140, alignment: .topLeading)
|
.frame(minHeight: 130, alignment: .topLeading)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +78,7 @@ struct GroupWelcomeView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, -5)
|
.padding(.horizontal, -5)
|
||||||
.padding(.top, -8)
|
.padding(.top, -8)
|
||||||
.frame(height: 140, alignment: .topLeading)
|
.frame(height: 130, alignment: .topLeading)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -93,6 +97,9 @@ struct GroupWelcomeView: View {
|
|||||||
}
|
}
|
||||||
.disabled(welcomeText.isEmpty)
|
.disabled(welcomeText.isEmpty)
|
||||||
copyButton()
|
copyButton()
|
||||||
|
} footer: {
|
||||||
|
Text(!welcomeTextFitsLimit() ? "Message too large" : "")
|
||||||
|
.foregroundColor(.red)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
@ -113,7 +120,15 @@ struct GroupWelcomeView: View {
|
|||||||
Button("Save and update group profile") {
|
Button("Save and update group profile") {
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
.disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil))
|
.disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit())
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
@ -123,11 +138,13 @@ struct GroupWelcomeView: View {
|
|||||||
if welcome?.count == 0 {
|
if welcome?.count == 0 {
|
||||||
welcome = nil
|
welcome = nil
|
||||||
}
|
}
|
||||||
var groupProfileUpdated = groupInfo.groupProfile
|
groupProfile.description = welcome
|
||||||
groupProfileUpdated.description = welcome
|
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
|
||||||
groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated)
|
await MainActor.run {
|
||||||
m.updateGroup(groupInfo)
|
groupInfo = gInfo
|
||||||
welcomeText = welcome ?? ""
|
ChatModel.shared.updateGroup(gInfo)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("apiUpdateGroup error: \(responseError(error))")
|
logger.error("apiUpdateGroup error: \(responseError(error))")
|
||||||
}
|
}
|
||||||
@ -137,6 +154,6 @@ struct GroupWelcomeView: View {
|
|||||||
|
|
||||||
struct GroupWelcomeView_Previews: PreviewProvider {
|
struct GroupWelcomeView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData))
|
GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ struct ChatPreviewView: View {
|
|||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
chatPreviewTitle()
|
chatPreviewTitle()
|
||||||
Spacer()
|
Spacer()
|
||||||
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.updatedAt))
|
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.frame(minWidth: 60, alignment: .trailing)
|
.frame(minWidth: 60, alignment: .trailing)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
@ -171,10 +171,21 @@ struct ChatPreviewView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func chatItemPreview(_ cItem: ChatItem) -> Text {
|
func chatItemPreview(_ cItem: ChatItem) -> Text {
|
||||||
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
|
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
|
||||||
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
|
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
|
||||||
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
|
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
|
||||||
|
|
||||||
|
// same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey;
|
||||||
|
// can be refactored into a single function if functions calling these are changed to return same type
|
||||||
|
func markedDeletedText() -> String {
|
||||||
|
switch cItem.meta.itemDeleted {
|
||||||
|
case let .moderated(_, byGroupMember): String.localizedStringWithFormat(NSLocalizedString("moderated by %@", comment: "marked deleted chat item preview text"), byGroupMember.displayName)
|
||||||
|
case .blocked: NSLocalizedString("blocked", comment: "marked deleted chat item preview text")
|
||||||
|
case .blockedByAdmin: NSLocalizedString("blocked by admin", comment: "marked deleted chat item preview text")
|
||||||
|
case .deleted, nil: NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func attachment() -> String? {
|
func attachment() -> String? {
|
||||||
switch cItem.content.msgContent {
|
switch cItem.content.msgContent {
|
||||||
case .file: return "doc.fill"
|
case .file: return "doc.fill"
|
||||||
|
@ -61,11 +61,6 @@
|
|||||||
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 */; };
|
||||||
@ -142,6 +137,11 @@
|
|||||||
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; };
|
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; };
|
||||||
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; };
|
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; };
|
||||||
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
|
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
|
||||||
|
5CEB651B2B65B25500EF2982 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEB65162B65B25400EF2982 /* libgmpxx.a */; };
|
||||||
|
5CEB651C2B65B25500EF2982 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEB65172B65B25400EF2982 /* libffi.a */; };
|
||||||
|
5CEB651D2B65B25500EF2982 /* libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEB65182B65B25400EF2982 /* libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9.a */; };
|
||||||
|
5CEB651E2B65B25500EF2982 /* libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEB65192B65B25400EF2982 /* libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9-ghc9.6.3.a */; };
|
||||||
|
5CEB651F2B65B25500EF2982 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEB651A2B65B25500EF2982 /* libgmp.a */; };
|
||||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
|
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
|
||||||
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
|
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
|
||||||
5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */; };
|
5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */; };
|
||||||
@ -325,11 +325,6 @@
|
|||||||
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>"; };
|
||||||
@ -429,6 +424,11 @@
|
|||||||
5CE6C7B42AAB1527007F345C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
5CE6C7B42AAB1527007F345C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||||
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
||||||
|
5CEB65162B65B25400EF2982 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
|
5CEB65172B65B25400EF2982 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
|
5CEB65182B65B25400EF2982 /* libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9.a"; sourceTree = "<group>"; };
|
||||||
|
5CEB65192B65B25400EF2982 /* libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||||
|
5CEB651A2B65B25500EF2982 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
|
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
|
||||||
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
|
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
|
||||||
5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentQueue.swift; sourceTree = "<group>"; };
|
5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentQueue.swift; sourceTree = "<group>"; };
|
||||||
@ -514,13 +514,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
5CEB651B2B65B25500EF2982 /* libgmpxx.a in Frameworks */,
|
||||||
|
5CEB651F2B65B25500EF2982 /* libgmp.a in Frameworks */,
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
5C83A1B02B5EF67D00AE0A4A /* libffi.a in Frameworks */,
|
5CEB651D2B65B25500EF2982 /* libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9.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 */,
|
||||||
5C83A1B12B5EF67D00AE0A4A /* libgmpxx.a in Frameworks */,
|
5CEB651C2B65B25500EF2982 /* libffi.a in Frameworks */,
|
||||||
|
5CEB651E2B65B25500EF2982 /* libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9-ghc9.6.3.a in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -582,11 +582,11 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5C83A1AB2B5EF67D00AE0A4A /* libffi.a */,
|
5CEB65172B65B25400EF2982 /* libffi.a */,
|
||||||
5C83A1A82B5EF67D00AE0A4A /* libgmp.a */,
|
5CEB651A2B65B25500EF2982 /* libgmp.a */,
|
||||||
5C83A1AC2B5EF67D00AE0A4A /* libgmpxx.a */,
|
5CEB65162B65B25400EF2982 /* libgmpxx.a */,
|
||||||
5C83A1A92B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a */,
|
5CEB65192B65B25400EF2982 /* libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9-ghc9.6.3.a */,
|
||||||
5C83A1AA2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a */,
|
5CEB65182B65B25400EF2982 /* libHSsimplex-chat-5.5.1.0-3edL3RXPYL6hI3kOCWWU9.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 = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
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;
|
MARKETING_VERSION = 5.5.1;
|
||||||
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 = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
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;
|
MARKETING_VERSION = 5.5.1;
|
||||||
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 = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
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;
|
MARKETING_VERSION = 5.5.1;
|
||||||
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 = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
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;
|
MARKETING_VERSION = 5.5.1;
|
||||||
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 = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
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;
|
MARKETING_VERSION = 5.5.1;
|
||||||
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 = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
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;
|
MARKETING_VERSION = 5.5.1;
|
||||||
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,6 +105,11 @@ 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]?
|
||||||
}
|
}
|
||||||
|
@ -1367,6 +1367,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var chatTs: Date {
|
||||||
|
switch self {
|
||||||
|
case let .direct(contact): return contact.chatTs ?? contact.updatedAt
|
||||||
|
case let .group(groupInfo): return groupInfo.chatTs ?? groupInfo.updatedAt
|
||||||
|
case let .local(noteFolder): return noteFolder.chatTs
|
||||||
|
case let .contactRequest(contactRequest): return contactRequest.updatedAt
|
||||||
|
case let .contactConnection(contactConnection): return contactConnection.updatedAt
|
||||||
|
case .invalidJSON: return .now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct SampleData {
|
public struct SampleData {
|
||||||
public var direct: ChatInfo
|
public var direct: ChatInfo
|
||||||
public var group: ChatInfo
|
public var group: ChatInfo
|
||||||
@ -1425,6 +1436,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
|||||||
public var mergedPreferences: ContactUserPreferences
|
public var mergedPreferences: ContactUserPreferences
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
var updatedAt: Date
|
var updatedAt: Date
|
||||||
|
var chatTs: Date?
|
||||||
var contactGroupMemberId: Int64?
|
var contactGroupMemberId: Int64?
|
||||||
var contactGrpInvSent: Bool
|
var contactGrpInvSent: Bool
|
||||||
|
|
||||||
@ -1744,6 +1756,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
|
|||||||
public var chatSettings: ChatSettings
|
public var chatSettings: ChatSettings
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
var updatedAt: Date
|
var updatedAt: Date
|
||||||
|
var chatTs: Date?
|
||||||
|
|
||||||
public var id: ChatId { get { "#\(groupId)" } }
|
public var id: ChatId { get { "#\(groupId)" } }
|
||||||
public var apiId: Int64 { get { groupId } }
|
public var apiId: Int64 { get { groupId } }
|
||||||
@ -2049,6 +2062,7 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
|
|||||||
public var unread: Bool
|
public var unread: Bool
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
public var updatedAt: Date
|
public var updatedAt: Date
|
||||||
|
var chatTs: Date
|
||||||
|
|
||||||
public var id: ChatId { get { "*\(noteFolderId)" } }
|
public var id: ChatId { get { "*\(noteFolderId)" } }
|
||||||
public var apiId: Int64 { get { noteFolderId } }
|
public var apiId: Int64 { get { noteFolderId } }
|
||||||
@ -2070,7 +2084,8 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
|
|||||||
favorite: false,
|
favorite: false,
|
||||||
unread: false,
|
unread: false,
|
||||||
createdAt: .now,
|
createdAt: .now,
|
||||||
updatedAt: .now
|
updatedAt: .now,
|
||||||
|
chatTs: .now
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ 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,11 +1,12 @@
|
|||||||
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.appcompat.app.AppCompatDelegate
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
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
|
||||||
@ -58,6 +59,17 @@ 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,13 +97,6 @@ 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()
|
||||||
}
|
}
|
||||||
@ -197,10 +190,18 @@ 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()
|
||||||
else
|
// Sometimes, when we change modes fast from one to another, system destroys the service after start.
|
||||||
|
// 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(slow = 30_000, deadlock = 60_000) {
|
withLongRunningApi {
|
||||||
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"
|
||||||
|
|
||||||
private var isServiceStarted = false
|
var isServiceStarted = false
|
||||||
private var stopAfterStart = false
|
private var stopAfterStart = false
|
||||||
|
|
||||||
fun scheduleStart(context: Context) {
|
fun scheduleStart(context: Context) {
|
||||||
|
@ -12,6 +12,8 @@ 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
|
||||||
|
@ -0,0 +1,106 @@
|
|||||||
|
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,6 +66,7 @@ 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);
|
||||||
@ -163,6 +164,14 @@ 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,6 +39,7 @@ 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);
|
||||||
@ -173,6 +174,14 @@ 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,6 +108,7 @@ 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,6 +2,7 @@ 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
|
||||||
@ -48,6 +49,7 @@ 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>()
|
||||||
@ -55,7 +57,7 @@ object ChatModel {
|
|||||||
|
|
||||||
// current chat
|
// current chat
|
||||||
val chatId = mutableStateOf<String?>(null)
|
val chatId = mutableStateOf<String?>(null)
|
||||||
val chatItems = mutableStateListOf<ChatItem>()
|
val chatItems = mutableStateOf(SnapshotStateList<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>()
|
||||||
@ -63,8 +65,6 @@ 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,18 +269,15 @@ 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.none { it.id == cItem.id }) {
|
if (chatItems.value.none { it.id == cItem.id }) {
|
||||||
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
|
chatItems.add(kotlin.math.max(0, chatItems.value.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}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,14 +304,13 @@ 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 itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
val items = chatItems.value
|
||||||
|
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||||
if (itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
chatItems[itemIndex] = cItem
|
items[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)
|
||||||
@ -324,7 +320,6 @@ 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 {
|
||||||
@ -336,9 +331,10 @@ 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 itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
val items = chatItems.value
|
||||||
|
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||||
if (itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
chatItems[itemIndex] = cItem
|
items[itemIndex] = cItem
|
||||||
}
|
}
|
||||||
} else if (status != null) {
|
} else if (status != null) {
|
||||||
chatItemStatuses[cItem.id] = status
|
chatItemStatuses[cItem.id] = status
|
||||||
@ -362,10 +358,10 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
// remove from current chat
|
// remove from current chat
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
chatItems.removeAll {
|
||||||
if (itemIndex >= 0) {
|
val remove = it.id == cItem.id
|
||||||
AudioPlayer.stop(chatItems[itemIndex])
|
if (remove) { AudioPlayer.stop(it) }
|
||||||
chatItems.removeAt(itemIndex)
|
remove
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -406,7 +402,7 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeLiveDummy() {
|
fun removeLiveDummy() {
|
||||||
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
chatItems.removeLast()
|
chatItems.removeLast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -438,14 +434,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
|
||||||
Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marking read ${cInfo.id}, current chatId ${chatId.value}, size was ${chatItems.size}")
|
val items = chatItems.value
|
||||||
while (i < chatItems.count()) {
|
while (i < items.size) {
|
||||||
val item = chatItems[i]
|
val item = items[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())
|
||||||
chatItems[i] = newItem
|
items[i] = newItem
|
||||||
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
|
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
|
||||||
chatItems[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
|
items[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)))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -453,7 +449,6 @@ 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
|
||||||
}
|
}
|
||||||
@ -644,7 +639,8 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addTerminalItem(item: TerminalItem) {
|
fun addTerminalItem(item: TerminalItem) {
|
||||||
if (terminalItems.value.size >= 500) {
|
val maxItems = if (appPreferences.developerTools.get()) 500 else 200
|
||||||
|
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
|
||||||
@ -969,6 +965,16 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
is Group -> groupInfo.chatSettings
|
is Group -> groupInfo.chatSettings
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val chatTs: Instant
|
||||||
|
get() = when(this) {
|
||||||
|
is Direct -> contact.chatTs ?: contact.updatedAt
|
||||||
|
is Group -> groupInfo.chatTs ?: groupInfo.updatedAt
|
||||||
|
is Local -> noteFolder.chatTs
|
||||||
|
is ContactRequest -> contactRequest.updatedAt
|
||||||
|
is ContactConnection -> contactConnection.updatedAt
|
||||||
|
is InvalidJSON -> updatedAt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -1009,6 +1015,7 @@ data class Contact(
|
|||||||
val mergedPreferences: ContactUserPreferences,
|
val mergedPreferences: ContactUserPreferences,
|
||||||
override val createdAt: Instant,
|
override val createdAt: Instant,
|
||||||
override val updatedAt: Instant,
|
override val updatedAt: Instant,
|
||||||
|
val chatTs: Instant?,
|
||||||
val contactGroupMemberId: Long? = null,
|
val contactGroupMemberId: Long? = null,
|
||||||
val contactGrpInvSent: Boolean
|
val contactGrpInvSent: Boolean
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
@ -1077,6 +1084,7 @@ data class Contact(
|
|||||||
mergedPreferences = ContactUserPreferences.sampleData,
|
mergedPreferences = ContactUserPreferences.sampleData,
|
||||||
createdAt = Clock.System.now(),
|
createdAt = Clock.System.now(),
|
||||||
updatedAt = Clock.System.now(),
|
updatedAt = Clock.System.now(),
|
||||||
|
chatTs = Clock.System.now(),
|
||||||
contactGrpInvSent = false
|
contactGrpInvSent = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1204,7 +1212,8 @@ data class GroupInfo (
|
|||||||
val hostConnCustomUserProfileId: Long? = null,
|
val hostConnCustomUserProfileId: Long? = null,
|
||||||
val chatSettings: ChatSettings,
|
val chatSettings: ChatSettings,
|
||||||
override val createdAt: Instant,
|
override val createdAt: Instant,
|
||||||
override val updatedAt: Instant
|
override val updatedAt: Instant,
|
||||||
|
val chatTs: Instant?
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
override val chatType get() = ChatType.Group
|
override val chatType get() = ChatType.Group
|
||||||
override val id get() = "#$groupId"
|
override val id get() = "#$groupId"
|
||||||
@ -1245,7 +1254,8 @@ data class GroupInfo (
|
|||||||
hostConnCustomUserProfileId = null,
|
hostConnCustomUserProfileId = null,
|
||||||
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
|
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
|
||||||
createdAt = Clock.System.now(),
|
createdAt = Clock.System.now(),
|
||||||
updatedAt = Clock.System.now()
|
updatedAt = Clock.System.now(),
|
||||||
|
chatTs = Clock.System.now()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1507,7 +1517,8 @@ class NoteFolder(
|
|||||||
val favorite: Boolean,
|
val favorite: Boolean,
|
||||||
val unread: Boolean,
|
val unread: Boolean,
|
||||||
override val createdAt: Instant,
|
override val createdAt: Instant,
|
||||||
override val updatedAt: Instant
|
override val updatedAt: Instant,
|
||||||
|
val chatTs: Instant
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
override val chatType get() = ChatType.Local
|
override val chatType get() = ChatType.Local
|
||||||
override val id get() = "*$noteFolderId"
|
override val id get() = "*$noteFolderId"
|
||||||
@ -1530,7 +1541,8 @@ class NoteFolder(
|
|||||||
favorite = false,
|
favorite = false,
|
||||||
unread = false,
|
unread = false,
|
||||||
createdAt = Clock.System.now(),
|
createdAt = Clock.System.now(),
|
||||||
updatedAt = Clock.System.now()
|
updatedAt = Clock.System.now(),
|
||||||
|
chatTs = Clock.System.now()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1990,6 +2002,46 @@ 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,6 +28,7 @@ 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
|
||||||
@ -42,7 +43,7 @@ val appPreferences: AppPreferences
|
|||||||
val chatController: ChatController = ChatController
|
val chatController: ChatController = ChatController
|
||||||
|
|
||||||
fun initChatControllerAndRunMigrations() {
|
fun initChatControllerAndRunMigrations() {
|
||||||
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
withLongRunningApi {
|
||||||
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 {
|
||||||
@ -58,10 +59,23 @@ 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
|
||||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
|
var migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value)
|
||||||
val res: DBMigrationResult = kotlin.runCatching {
|
var res: DBMigrationResult = 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
|
||||||
@ -119,6 +133,7 @@ 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(slow = 30_000, deadlock = 60_000) {
|
withLongRunningApi {
|
||||||
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(slow = 30_000, deadlock = 60_000) {
|
withLongRunningApi {
|
||||||
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(Unit) {
|
LaunchedEffect(ciInfo) {
|
||||||
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()
|
||||||
.onEach { Log.d(TAG, "TODOCHAT: chatId: activeChatId ${activeChat.value?.id} == new chatId $it ${activeChat.value?.id == it} ") }
|
.filterNotNull()
|
||||||
.filter { it != null && activeChat.value?.id != it }
|
|
||||||
.collect { chatId ->
|
.collect { chatId ->
|
||||||
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
if (activeChat.value?.id != chatId) {
|
||||||
// Also for situation when chatId changes after clicking in notification, etc
|
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
||||||
activeChat.value = chatModel.getChat(chatId!!)
|
// Also for situation when chatId changes after clicking in notification, etc
|
||||||
Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}")
|
activeChat.value = chatModel.getChat(chatId)
|
||||||
|
}
|
||||||
markUnreadChatAsRead(activeChat, chatModel)
|
markUnreadChatAsRead(activeChat, chatModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,12 +92,10 @@ 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}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,7 +146,6 @@ 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,
|
||||||
@ -226,19 +223,17 @@ 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.firstOrNull()?.id
|
val firstId = chatModel.chatItems.value.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.firstOrNull { it.id == itemId }
|
val toDeleteItem = chatModel.chatItems.value.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
|
||||||
@ -404,12 +399,15 @@ 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.showModal(endButtons = {
|
ModalManager.end.showModalCloseable(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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -495,7 +493,6 @@ 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,
|
||||||
@ -582,7 +579,7 @@ fun ChatLayout(
|
|||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
) {
|
) {
|
||||||
ChatItemsList(
|
ChatItemsList(
|
||||||
chat, unreadCount, composeState, chatItems, searchValue,
|
chat, unreadCount, composeState, 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,
|
||||||
@ -647,7 +644,7 @@ fun ChatInfoToolbar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
|
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
|
||||||
if (activeCall == null) {
|
if (activeCall == null) {
|
||||||
barButtons.add {
|
barButtons.add {
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
@ -840,7 +837,6 @@ 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,
|
||||||
@ -869,7 +865,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
ScrollToBottom(chat.id, listState, chatItems)
|
ScrollToBottom(chat.id, listState, chatModel.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()) {
|
||||||
@ -886,7 +882,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 { chatItems.reversed().toList() } }
|
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
||||||
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 }
|
||||||
@ -939,7 +935,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val provider = {
|
val provider = {
|
||||||
providerForGallery(i, chatItems, cItem.id) { indexInReversed ->
|
providerForGallery(i, chatModel.chatItems.value, 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),
|
||||||
@ -1062,11 +1058,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
|
FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: List<ChatItem>) {
|
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: State<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
|
||||||
@ -1084,7 +1080,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.lastOrNull()?.id }
|
snapshotFlow { chatItems.value.lastOrNull()?.id }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
||||||
.collect {
|
.collect {
|
||||||
@ -1107,7 +1103,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BoxWithConstraintsScope.FloatingButtons(
|
fun BoxWithConstraintsScope.FloatingButtons(
|
||||||
chatItems: List<ChatItem>,
|
chatItems: State<List<ChatItem>>,
|
||||||
unreadCount: State<Int>,
|
unreadCount: State<Int>,
|
||||||
minUnreadItemId: Long,
|
minUnreadItemId: Long,
|
||||||
searchValue: State<String>,
|
searchValue: State<String>,
|
||||||
@ -1141,10 +1137,11 @@ 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 from = chatItems.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
|
val items = chatItems.value
|
||||||
if (chatItems.size <= from || from < 0) return@derivedStateOf 0
|
val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
|
||||||
|
if (items.size <= from || from < 0) return@derivedStateOf 0
|
||||||
|
|
||||||
chatItems.subList(from, chatItems.size).count { it.isRcvNew }
|
items.subList(from, items.size).count { it.isRcvNew }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
|
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
|
||||||
@ -1190,7 +1187,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
|||||||
painterResource(MR.images.ic_check),
|
painterResource(MR.images.ic_check),
|
||||||
onClick = {
|
onClick = {
|
||||||
markRead(
|
markRead(
|
||||||
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||||
bottomUnreadCount
|
bottomUnreadCount
|
||||||
)
|
)
|
||||||
showDropDown.value = false
|
showDropDown.value = false
|
||||||
@ -1495,7 +1492,6 @@ 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,
|
||||||
@ -1568,7 +1564,6 @@ 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,6 +583,10 @@ 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)
|
||||||
@ -661,7 +665,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.findLast { it.meta.editable }
|
val lastEditable = chatModel.chatItems.value.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,14 +59,6 @@ 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) }
|
||||||
@ -203,6 +195,11 @@ 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)
|
||||||
}
|
}
|
||||||
@ -220,93 +217,43 @@ 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>?
|
||||||
) {
|
) {
|
||||||
val showCustomTimePicker = remember { mutableStateOf(false) }
|
DefaultDropdownMenu(showMenu) {
|
||||||
|
Text(
|
||||||
if (showCustomTimePicker.value) {
|
generalGetString(MR.strings.send_disappearing_message),
|
||||||
val selectedDisappearingMessageTime = remember {
|
Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING * 1.5f),
|
||||||
mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
|
fontSize = 16.sp,
|
||||||
}
|
color = MaterialTheme.colors.secondary
|
||||||
CustomTimePickerDialog(
|
|
||||||
selectedDisappearingMessageTime,
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
|
ItemAction(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
|
||||||
Surface(
|
sendMessage(30)
|
||||||
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
|
showMenu.value = false
|
||||||
contentColor = LocalContentColor.current
|
}
|
||||||
) {
|
ItemAction(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
|
||||||
Box(
|
sendMessage(60)
|
||||||
contentAlignment = Alignment.Center
|
showMenu.value = false
|
||||||
) {
|
}
|
||||||
Column(
|
ItemAction(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
|
||||||
modifier = Modifier.padding(DEFAULT_PADDING),
|
sendMessage(300)
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
showMenu.value = false
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
}
|
||||||
) {
|
ItemAction(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
|
||||||
Row(
|
showMenu.value = false
|
||||||
modifier = Modifier.fillMaxWidth(),
|
val selectedDisappearingMessageTime = mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
showCustomTimePickerDialog(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
selectedDisappearingMessageTime,
|
||||||
) {
|
title = generalGetString(MR.strings.delete_after),
|
||||||
Text(" ") // centers title
|
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
|
||||||
Text(
|
confirmButtonAction = { ttl ->
|
||||||
generalGetString(MR.strings.send_disappearing_message),
|
sendMessage(ttl)
|
||||||
fontSize = 16.sp,
|
customDisappearingMessageTimePref?.set?.invoke(ttl)
|
||||||
color = MaterialTheme.colors.secondary
|
},
|
||||||
)
|
cancel = { showMenu.value = false }
|
||||||
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 = 60_000) {
|
withLongRunningApi(slow = 30_000, deadlock = 120_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) {
|
||||||
@ -86,7 +86,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
|
|||||||
.map { it.chatInfo }
|
.map { it.chatInfo }
|
||||||
.filterIsInstance<ChatInfo.Direct>()
|
.filterIsInstance<ChatInfo.Direct>()
|
||||||
.map { it.contact }
|
.map { it.contact }
|
||||||
.filter { it.contactId !in memberContactIds && it.chatViewName.lowercase().contains(s) }
|
.filter { c -> c.ready && c.active && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) }
|
||||||
.sortedBy { it.displayName.lowercase() }
|
.sortedBy { it.displayName.lowercase() }
|
||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,9 @@ 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.*
|
||||||
@ -74,9 +72,8 @@ 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.addAll(c.chatItems)
|
chatModel.chatItems.replaceAll(c.chatItems)
|
||||||
chatModel.chatId.value = c.id
|
chatModel.chatId.value = c.id
|
||||||
closeAll()
|
closeAll()
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ 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.*
|
||||||
@ -14,6 +15,7 @@ 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
|
||||||
@ -27,9 +29,13 @@ 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) }
|
||||||
@ -54,8 +60,11 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
|
|||||||
|
|
||||||
ModalView(
|
ModalView(
|
||||||
close = {
|
close = {
|
||||||
if (welcomeText.value == gInfo.groupProfile.description || (welcomeText.value == "" && gInfo.groupProfile.description == null)) close()
|
when {
|
||||||
else showUnsavedChangesAlert({ save(close) }, close)
|
welcomeTextUnchanged(welcomeText, gInfo) -> close()
|
||||||
|
!welcomeTextFitsLimit(welcomeText) -> showUnsavedChangesTooLongAlert(close)
|
||||||
|
else -> showUnsavedChangesAlert({ save(close) }, close)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
GroupWelcomeLayout(
|
GroupWelcomeLayout(
|
||||||
@ -67,6 +76,14 @@ 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>,
|
||||||
@ -95,6 +112,13 @@ 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 = {
|
||||||
@ -104,10 +128,18 @@ 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 = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null)
|
disabled = welcomeTextUnchanged(wt, groupInfo) || !welcomeTextFitsLimit(wt)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
@ -182,3 +214,11 @@ 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)) {
|
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
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,7 +112,6 @@ 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)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,7 +177,8 @@ fun ChatItemView(
|
|||||||
fun MsgContentItemDropdownMenu() {
|
fun MsgContentItemDropdownMenu() {
|
||||||
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
|
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
|
||||||
when {
|
when {
|
||||||
cItem.content.msgContent != null -> {
|
// 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.id >= 0 -> {
|
||||||
DefaultDropdownMenu(showMenu) {
|
DefaultDropdownMenu(showMenu) {
|
||||||
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
|
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
|
||||||
MsgReactionsMenu()
|
MsgReactionsMenu()
|
||||||
@ -527,8 +527,9 @@ 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(chatModel.chatItems.asReversed()[i].id)
|
itemIds.add(reversedChatItems[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 {
|
||||||
@ -651,6 +652,23 @@ 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),
|
||||||
|
@ -91,7 +91,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState<B
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun markedDeletedText(meta: CIMeta): String =
|
fun markedDeletedText(meta: CIMeta): String =
|
||||||
when (meta.itemDeleted) {
|
when (meta.itemDeleted) {
|
||||||
is CIDeleted.Moderated ->
|
is CIDeleted.Moderated ->
|
||||||
String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName)
|
String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName)
|
||||||
|
@ -212,18 +212,15 @@ 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.addAll(chat.chatItems)
|
chatModel.chatItems.replaceAll(chat.chatItems)
|
||||||
chatModel.chatId.value = chat.chatInfo.id
|
chatModel.chatId.value = chat.chatInfo.id
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,8 +236,7 @@ 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.clear()
|
chatModel.chatItems.replaceAll(chat.chatItems)
|
||||||
chatModel.chatItems.addAll(0, chat.chatItems)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
|
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||||
|
@ -26,6 +26,7 @@ import chat.simplex.common.views.helpers.*
|
|||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.model.GroupInfo
|
import chat.simplex.common.model.GroupInfo
|
||||||
import chat.simplex.common.platform.chatModel
|
import chat.simplex.common.platform.chatModel
|
||||||
|
import chat.simplex.common.views.chat.item.markedDeletedText
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.ImageResource
|
import dev.icerock.moko.resources.ImageResource
|
||||||
|
|
||||||
@ -170,7 +171,7 @@ fun ChatPreviewView(
|
|||||||
val (text: CharSequence, inlineTextContent) = when {
|
val (text: CharSequence, inlineTextContent) = when {
|
||||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
|
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
|
||||||
ci.meta.itemDeleted == null -> ci.text to null
|
ci.meta.itemDeleted == null -> ci.text to null
|
||||||
else -> generalGetString(MR.strings.marked_deleted_description) to null
|
else -> markedDeletedText(ci.meta) to null
|
||||||
}
|
}
|
||||||
val formattedText = when {
|
val formattedText = when {
|
||||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||||
@ -286,7 +287,7 @@ fun ChatPreviewView(
|
|||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.TopEnd
|
contentAlignment = Alignment.TopEnd
|
||||||
) {
|
) {
|
||||||
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt)
|
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs)
|
||||||
Text(
|
Text(
|
||||||
ts,
|
ts,
|
||||||
color = MaterialTheme.colors.secondary,
|
color = MaterialTheme.colors.secondary,
|
||||||
|
@ -62,7 +62,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
|||||||
initialRandomDBPassphrase,
|
initialRandomDBPassphrase,
|
||||||
progressIndicator,
|
progressIndicator,
|
||||||
onConfirmEncrypt = {
|
onConfirmEncrypt = {
|
||||||
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
withLongRunningApi {
|
||||||
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(slow = 30_000, deadlock = 60_000) {
|
withLongRunningApi {
|
||||||
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(slow = 60_000, deadlock = 180_000) {
|
withLongRunningApi {
|
||||||
try {
|
try {
|
||||||
m.controller.apiDeleteStorage()
|
m.controller.apiDeleteStorage()
|
||||||
try {
|
try {
|
||||||
|
@ -22,6 +22,7 @@ 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 {
|
||||||
@ -128,6 +129,8 @@ 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 = {
|
||||||
@ -195,6 +198,8 @@ 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,116 +1,21 @@
|
|||||||
package chat.simplex.common.views.helpers
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import SectionItemView
|
||||||
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 dev.icerock.moko.resources.compose.painterResource
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
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
|
||||||
fun CustomTimePicker(
|
expect 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,
|
||||||
@ -141,8 +46,7 @@ data class TimeUnitLimits(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
fun showCustomTimePickerDialog(
|
||||||
fun CustomTimePickerDialog(
|
|
||||||
selection: MutableState<Int>,
|
selection: MutableState<Int>,
|
||||||
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
|
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
|
||||||
title: String,
|
title: String,
|
||||||
@ -150,53 +54,26 @@ fun CustomTimePickerDialog(
|
|||||||
confirmButtonAction: (Int) -> Unit,
|
confirmButtonAction: (Int) -> Unit,
|
||||||
cancel: () -> Unit
|
cancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
DefaultDialog(onDismissRequest = cancel) {
|
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||||
Surface(
|
title = title,
|
||||||
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
|
onDismissRequest = cancel
|
||||||
contentColor = LocalContentColor.current
|
) {
|
||||||
) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Box(
|
CustomTimePicker(
|
||||||
contentAlignment = Alignment.Center
|
selection,
|
||||||
|
timeUnitsLimits
|
||||||
|
)
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
confirmButtonAction(selection.value)
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Column(
|
Text(
|
||||||
modifier = Modifier.padding(DEFAULT_PADDING),
|
confirmButtonText,
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
textAlign = TextAlign.Center,
|
||||||
) {
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -220,7 +97,6 @@ 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)
|
||||||
@ -247,28 +123,22 @@ 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 -> showCustomTimePicker.value = true
|
DropdownSelection.Custom -> {
|
||||||
|
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,6 +5,7 @@ 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
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ fun DefaultProgressView(description: String?) {
|
|||||||
strokeWidth = 2.5.dp
|
strokeWidth = 2.5.dp
|
||||||
)
|
)
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
Text(description)
|
Text(description, textAlign = TextAlign.Center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,10 +61,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||||
val data = ModalData()
|
val data = ModalData()
|
||||||
showCustomModal { close ->
|
showCustomModal { close ->
|
||||||
ModalView(close, showClose = showClose, content = { data.content(close) })
|
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,16 +198,16 @@ fun <T> SectionItemWithValue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionTextFooter(text: String) {
|
fun SectionTextFooter(text: String, color: Color = MaterialTheme.colors.secondary) {
|
||||||
SectionTextFooter(AnnotatedString(text))
|
SectionTextFooter(AnnotatedString(text), color = color)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) {
|
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start, color: Color = MaterialTheme.colors.secondary) {
|
||||||
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 = MaterialTheme.colors.secondary,
|
color = color,
|
||||||
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(slow = 30_000, deadlock = 60_000) {
|
withLongRunningApi {
|
||||||
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(slow = 30_000, deadlock = 60_000) {
|
withLongRunningApi {
|
||||||
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,10 +47,6 @@ 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,19 +28,18 @@ 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(rhId) {
|
var servers by remember { stateGetOrPut("servers") { emptyList<ServerCfg>() } }
|
||||||
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
|
var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } }
|
||||||
}
|
|
||||||
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 { derivedStateOf { servers == currServers.value || testing.value } }
|
val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } }
|
||||||
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
|
val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } }
|
||||||
val saveDisabled = remember {
|
val saveDisabled = remember(servers) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
servers.isEmpty() ||
|
servers.isEmpty() ||
|
||||||
servers == currServers.value ||
|
servers == currServers.value ||
|
||||||
testing.value ||
|
testing.value ||
|
||||||
!servers.all { srv ->
|
servers.none { srv ->
|
||||||
val address = parseServerAddress(srv.server)
|
val address = parseServerAddress(srv.server)
|
||||||
address != null && uniqueAddress(srv, address, servers)
|
address != null && uniqueAddress(srv, address, servers)
|
||||||
} ||
|
} ||
|
||||||
@ -49,8 +48,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) {
|
||||||
@ -59,8 +58,9 @@ 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()) {
|
if (servers.isEmpty() && !serversAlreadyLoaded) {
|
||||||
servers = currServers.value
|
servers = currServers.value
|
||||||
|
serversAlreadyLoaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,13 +80,11 @@ 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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -125,7 +123,6 @@ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,13 +147,11 @@ 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 ?: emptyList()
|
servers = currServers.value
|
||||||
m.userSMPServersUnsaved.value = null
|
|
||||||
},
|
},
|
||||||
saveSMPServers = {
|
saveSMPServers = {
|
||||||
saveServers(rhId, serverProtocol, currServers, servers, m)
|
saveServers(rhId, serverProtocol, currServers, servers, m)
|
||||||
@ -355,7 +350,6 @@ 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,6 +16,7 @@
|
|||||||
|
|
||||||
<!-- 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>
|
||||||
@ -1377,9 +1378,11 @@
|
|||||||
<!-- 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,8 +14,7 @@ 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.ChatController
|
import chat.simplex.common.model.*
|
||||||
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
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
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
|
android.version_name=5.5.1
|
||||||
android.version_code=175
|
android.version_code=177
|
||||||
|
|
||||||
desktop.version_name=5.5
|
desktop.version_name=5.5.1
|
||||||
desktop.version_code=26
|
desktop.version_code=27
|
||||||
|
|
||||||
kotlin.version=1.8.20
|
kotlin.version=1.8.20
|
||||||
gradle.plugin.version=7.4.2
|
gradle.plugin.version=7.4.2
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 5.5.0.4
|
version: 5.5.1.0
|
||||||
#synopsis:
|
#synopsis:
|
||||||
#description:
|
#description:
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
|
@ -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.0.4
|
version: 5.5.1.0
|
||||||
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
|
||||||
|
@ -1028,6 +1028,7 @@ 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
|
||||||
@ -1686,17 +1687,9 @@ 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
|
updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` \_ -> pure ()
|
||||||
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
|
||||||
@ -2512,6 +2505,14 @@ 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,6 +20,7 @@ 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
|
||||||
@ -94,6 +95,8 @@ 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
|
||||||
@ -176,6 +179,10 @@ 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
|
||||||
@ -264,9 +271,18 @@ 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 <$> atomically (readTBQueue outputQ)
|
chatRecvMsg ChatController {outputQ} = json <$> readChatResponse
|
||||||
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,6 +68,8 @@ 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 =
|
||||||
@ -222,8 +224,6 @@ 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,6 +356,13 @@ 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,5 +250,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,12 @@
|
|||||||
<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]">
|
<div class="z-10 flex flex-col items-center pt-[40px] ml-[-15px] d-none-if-js-disabled">
|
||||||
<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>
|
||||||
@ -61,7 +65,11 @@
|
|||||||
</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">{{ "connect-in-app" | i18n({}, lang ) | safe }}</p>
|
<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>
|
||||||
|
<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%]">
|
||||||
@ -69,7 +77,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">
|
<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">
|
||||||
<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%]">
|
||||||
@ -81,7 +89,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
<section class="hidden md:block bg-secondary-bg-light dark:bg-secondary-bg-dark py-[20px]">
|
<section class="hidden md:block bg-secondary-bg-light dark:bg-secondary-bg-dark py-[20px] d-none-if-js-disabled">
|
||||||
<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">
|
||||||
|
|
||||||
@ -164,3 +172,7 @@
|
|||||||
|
|
||||||
{# 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,3 +957,7 @@ p a{
|
|||||||
top: calc(66px + 2rem);
|
top: calc(66px + 2rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.d-none-if-js-disabled{
|
||||||
|
display: none !important;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user