Merge branch 'master' into master-ios

This commit is contained in:
spaced4ndy
2023-08-16 10:44:15 +04:00
38 changed files with 559 additions and 337 deletions
+19 -6
View File
@@ -483,14 +483,27 @@ final class ChatModel: ObservableObject {
users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount })
}
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {
if let i = getChatItemIndex(ci), i < reversedChatItems.count - 1 {
return reversedChatItems[i + 1]
func getConnectedMemberNames(_ ci: ChatItem) -> [String] {
guard var i = getChatItemIndex(ci) else { return [] }
var ns: [String] = []
while i < reversedChatItems.count, let m = reversedChatItems[i].memberConnected {
ns.append(m.chatViewName)
i += 1
}
return ns
}
func getChatItemNeighbors(_ ci: ChatItem) -> (ChatItem?, ChatItem?) {
if let i = getChatItemIndex(ci) {
return (
i + 1 < reversedChatItems.count ? reversedChatItems[i + 1] : nil,
i - 1 >= 0 ? reversedChatItems[i - 1] : nil
)
} else {
return nil
return (nil, nil)
}
}
func popChat(_ id: String) {
if let i = getChatIndex(id) {
popChat_(i)
@@ -582,7 +595,7 @@ final class ChatModel: ObservableObject {
func addTerminalItem(_ item: TerminalItem) {
if terminalItems.count >= 500 {
terminalItems.remove(at: 0)
terminalItems.removeFirst()
}
terminalItems.append(item)
}
@@ -10,20 +10,11 @@ import SwiftUI
import SimpleXChat
struct CIEventView: View {
var chatItem: ChatItem
var eventText: Text
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
if let member = chatItem.memberDisplayName {
Text(member)
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
+ Text(" ")
+ chatEventText(chatItem)
} else {
chatEventText(chatItem)
}
eventText
}
.padding(.leading, 6)
.padding(.bottom, 6)
@@ -31,20 +22,8 @@ struct CIEventView: View {
}
}
func chatEventText(_ ci: ChatItem) -> Text {
Text(ci.content.text)
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
+ Text(" ")
+ ci.timestampText
.font(.caption)
.foregroundColor(Color.secondary)
.fontWeight(.light)
}
struct CIEventView_Previews: PreviewProvider {
static var previews: some View {
CIEventView(chatItem: ChatItem.getGroupEventSample())
CIEventView(eventText: Text("event happened"))
}
}
@@ -12,13 +12,9 @@ import SimpleXChat
struct DeletedItemView: View {
@Environment(\.colorScheme) var colorScheme
var chatItem: ChatItem
var showMember = false
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
if showMember, let member = chatItem.memberDisplayName {
Text(member).fontWeight(.medium) + Text(": ")
}
Text(chatItem.content.text)
.foregroundColor(.secondary)
.italic()
@@ -37,10 +33,7 @@ struct DeletedItemView_Previews: PreviewProvider {
static var previews: some View {
Group {
DeletedItemView(chatItem: ChatItem.getDeletedContentSample())
DeletedItemView(
chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)),
showMember: true
)
DeletedItemView(chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)))
}
.previewLayout(.fixed(width: 360, height: 200))
}
@@ -18,7 +18,6 @@ struct FramedItemView: View {
@Environment(\.colorScheme) var colorScheme
var chatInfo: ChatInfo
var chatItem: ChatItem
var showMember = false
var maxWidth: CGFloat = .infinity
@State var scrollProxy: ScrollViewProxy? = nil
@State var msgWidth: CGFloat = 0
@@ -57,7 +56,7 @@ struct FramedItemView: View {
}
}
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, msgContentView: framedMsgContentView)
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, msgContentView: framedMsgContentView)
.padding(chatItem.content.msgContent != nil ? 0 : 4)
.overlay(DetermineWidth())
}
@@ -107,7 +106,7 @@ struct FramedItemView: View {
value: .white
)
} else {
ciMsgContentView (chatItem, showMember)
ciMsgContentView(chatItem)
}
case let .video(text, image, duration):
CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy)
@@ -120,27 +119,27 @@ struct FramedItemView: View {
value: .white
)
} else {
ciMsgContentView (chatItem, showMember)
ciMsgContentView(chatItem)
}
case let .voice(text, duration):
FramedCIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
.overlay(DetermineWidth())
if text != "" {
ciMsgContentView (chatItem, showMember)
ciMsgContentView(chatItem)
}
case let .file(text):
ciFileView(chatItem, text)
case let .link(_, preview):
CILinkView(linkPreview: preview)
ciMsgContentView (chatItem, showMember)
ciMsgContentView(chatItem)
case let .unknown(_, text: text):
if chatItem.file == nil {
ciMsgContentView (chatItem, showMember)
ciMsgContentView(chatItem)
} else {
ciFileView(chatItem, text)
}
default:
ciMsgContentView (chatItem, showMember)
ciMsgContentView(chatItem)
}
}
}
@@ -232,17 +231,27 @@ struct FramedItemView: View {
}
private func ciQuotedMsgView(_ qi: CIQuote) -> some View {
MsgContentView(
text: qi.text,
formattedText: qi.formattedText,
sender: qi.getSender(membership())
)
.lineLimit(3)
.font(.subheadline)
.padding(.vertical, 6)
Group {
if let sender = qi.getSender(membership()) {
VStack(alignment: .leading, spacing: 2) {
Text(sender).font(.caption).foregroundColor(.secondary)
ciQuotedMsgTextView(qi, lines: 2)
}
} else {
ciQuotedMsgTextView(qi, lines: 3)
}
}
.padding(.top, 6)
.padding(.horizontal, 12)
}
private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View {
MsgContentView(text: qi.text, formattedText: qi.formattedText)
.lineLimit(lines)
.font(.subheadline)
.padding(.bottom, 6)
}
private func ciQuoteIconView(_ image: String) -> some View {
Image(systemName: image)
.resizable()
@@ -260,13 +269,12 @@ struct FramedItemView: View {
}
}
@ViewBuilder private func ciMsgContentView(_ ci: ChatItem, _ showMember: Bool = false) -> some View {
@ViewBuilder private func ciMsgContentView(_ ci: ChatItem) -> some View {
let text = ci.meta.isLive ? ci.content.msgContent?.text ?? ci.text : ci.text
let rtl = isRightToLeft(text)
let v = MsgContentView(
text: text,
formattedText: text == "" ? [] : ci.formattedText,
sender: showMember ? ci.memberDisplayName : nil,
meta: ci.meta,
rightToLeft: rtl
)
@@ -288,7 +296,7 @@ struct FramedItemView: View {
CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited)
.overlay(DetermineWidth())
if text != "" || ci.meta.isLive {
ciMsgContentView (chatItem, showMember)
ciMsgContentView (chatItem)
}
}
@@ -12,10 +12,9 @@ import SimpleXChat
struct IntegrityErrorItemView: View {
var msgError: MsgErrorType
var chatItem: ChatItem
var showMember = false
var body: some View {
CIMsgError(chatItem: chatItem, showMember: showMember) {
CIMsgError(chatItem: chatItem) {
switch msgError {
case .msgSkipped:
AlertManager.shared.showAlertMsg(
@@ -54,14 +53,10 @@ struct IntegrityErrorItemView: View {
struct CIMsgError: View {
var chatItem: ChatItem
var showMember = false
var onTap: () -> Void
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
if showMember, let member = chatItem.memberDisplayName {
Text(member).fontWeight(.medium) + Text(": ")
}
Text(chatItem.content.text)
.foregroundColor(.red)
.italic()
@@ -12,13 +12,9 @@ import SimpleXChat
struct MarkedDeletedItemView: View {
@Environment(\.colorScheme) var colorScheme
var chatItem: ChatItem
var showMember = false
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
if showMember, let member = chatItem.memberDisplayName {
Text(member).font(.caption).fontWeight(.medium) + Text(": ").font(.caption)
}
if case let .moderated(_, byGroupMember) = chatItem.meta.itemDeleted {
markedDeletedText("moderated by \(byGroupMember.chatViewName)")
} else {
+51 -10
View File
@@ -12,7 +12,6 @@ import SimpleXChat
struct ChatItemView: View {
var chatInfo: ChatInfo
var chatItem: ChatItem
var showMember = false
var maxWidth: CGFloat = .infinity
@State var scrollProxy: ScrollViewProxy? = nil
@Binding var revealed: Bool
@@ -23,7 +22,6 @@ struct ChatItemView: View {
init(chatInfo: ChatInfo, chatItem: ChatItem, showMember: Bool = false, maxWidth: CGFloat = .infinity, scrollProxy: ScrollViewProxy? = nil, revealed: Binding<Bool>, allowMenu: Binding<Bool> = .constant(false), audioPlayer: Binding<AudioPlayer?> = .constant(nil), playbackState: Binding<VoiceMessagePlaybackState> = .constant(.noPlayback), playbackTime: Binding<TimeInterval?> = .constant(nil)) {
self.chatInfo = chatInfo
self.chatItem = chatItem
self.showMember = showMember
self.maxWidth = maxWidth
_scrollProxy = .init(initialValue: scrollProxy)
_revealed = revealed
@@ -36,14 +34,14 @@ struct ChatItemView: View {
var body: some View {
let ci = chatItem
if chatItem.meta.itemDeleted != nil && !revealed {
MarkedDeletedItemView(chatItem: chatItem, showMember: showMember)
MarkedDeletedItemView(chatItem: chatItem)
} else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
EmojiItemView(chatItem: ci)
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
CIVoiceView(chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu)
} else if ci.content.msgContent == nil {
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
} else {
framedItemView()
}
@@ -53,14 +51,14 @@ struct ChatItemView: View {
}
private func framedItemView() -> some View {
FramedItemView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
FramedItemView(chatInfo: chatInfo, chatItem: chatItem, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
}
}
struct ChatItemContentView<Content: View>: View {
@EnvironmentObject var chatModel: ChatModel
var chatInfo: ChatInfo
var chatItem: ChatItem
var showMember: Bool
var msgContentView: () -> Content
var body: some View {
@@ -71,10 +69,11 @@ struct ChatItemContentView<Content: View>: View {
case .rcvDeleted: deletedItemView()
case let .sndCall(status, duration): callItemView(status, duration)
case let .rcvCall(status, duration): callItemView(status, duration)
case let .rcvIntegrityError(msgError): IntegrityErrorItemView(msgError: msgError, chatItem: chatItem, showMember: showMember)
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem, showMember: showMember)
case let .rcvIntegrityError(msgError): IntegrityErrorItemView(msgError: msgError, chatItem: chatItem)
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem)
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case .rcvGroupEvent(.memberConnected): CIEventView(eventText: membersConnectedItemText)
case .rcvGroupEvent: eventItemView()
case .sndGroupEvent: eventItemView()
case .rcvConnEvent: eventItemView()
@@ -96,7 +95,7 @@ struct ChatItemContentView<Content: View>: View {
}
private func deletedItemView() -> some View {
DeletedItemView(chatItem: chatItem, showMember: showMember)
DeletedItemView(chatItem: chatItem)
}
private func callItemView(_ status: CICallStatus, _ duration: Int) -> some View {
@@ -108,12 +107,54 @@ struct ChatItemContentView<Content: View>: View {
}
private func eventItemView() -> some View {
CIEventView(chatItem: chatItem)
return CIEventView(eventText: eventItemViewText())
}
private func eventItemViewText() -> Text {
if let member = chatItem.memberDisplayName {
return Text(member + " ")
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
+ chatEventText(chatItem)
} else {
return chatEventText(chatItem)
}
}
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
CIChatFeatureView(chatItem: chatItem, feature: feature, iconColor: iconColor)
}
private var membersConnectedItemText: Text {
if let t = membersConnectedText {
return chatEventText(t, chatItem.timestampText)
} else {
return eventItemViewText()
}
}
private var membersConnectedText: LocalizedStringKey? {
let ns = chatModel.getConnectedMemberNames(chatItem)
return ns.count > 3
? "\(ns[0]), \(ns[1]) and \(ns.count - 2) other members connected"
: ns.count == 3
? "\(ns[0] + ", " + ns[1]) and \(ns[2]) connected"
: ns.count == 2
? "\(ns[0]) and \(ns[1]) connected"
: nil
}
}
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text {
(Text(eventText) + Text(" ") + ts)
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
}
func chatEventText(_ ci: ChatItem) -> Text {
chatEventText("\(ci.content.text)", ci.timestampText)
}
struct ChatItemView_Previews: PreviewProvider {
+52 -43
View File
@@ -261,7 +261,7 @@ struct ChatView: View {
return GeometryReader { g in
ScrollViewReader { proxy in
ScrollView {
LazyVStack(spacing: 5) {
LazyVStack(spacing: 0) {
ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in
let voiceNoFrame = voiceWithoutFrame(ci)
let maxWidth = cInfo.chatType == .group
@@ -430,68 +430,77 @@ struct ChatView: View {
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
if case let .groupRcv(member) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
let prevItem = chatModel.getPrevChatItem(ci)
HStack(alignment: .top, spacing: 0) {
let showMember = prevItem == nil || showMemberImage(member, prevItem)
if showMember {
ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
.onTapGesture { selectedMember = member }
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
let (prevItem, nextItem) = chatModel.getChatItemNeighbors(ci)
if ci.memberConnected != nil && nextItem?.memberConnected != nil {
// memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView
ZStack {} // scroll doesn't work if it's EmptyView()
} else {
if prevItem == nil || showMemberImage(member, prevItem) {
VStack(alignment: .leading, spacing: 4) {
if ci.content.showMemberName {
Text(member.displayName)
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, memberImageSize + 14)
.padding(.top, 7)
}
HStack(alignment: .top, spacing: 8) {
ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
.onTapGesture { selectedMember = member }
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
}
chatItemWithMenu(ci, maxWidth)
}
}
.padding(.top, 5)
.padding(.trailing)
.padding(.leading, 12)
} else {
Rectangle().fill(.clear)
.frame(width: memberImageSize, height: memberImageSize)
chatItemWithMenu(ci, maxWidth)
.padding(.top, 5)
.padding(.trailing)
.padding(.leading, memberImageSize + 8 + 12)
}
ChatItemWithMenu(
ci: ci,
showMember: showMember,
maxWidth: maxWidth,
scrollProxy: scrollProxy,
deleteMessage: deleteMessage,
deletingItem: $deletingItem,
composeState: $composeState,
showDeleteMessage: $showDeleteMessage
)
.padding(.leading, 8)
.environmentObject(chat)
}
.padding(.trailing)
.padding(.leading, 12)
} else {
ChatItemWithMenu(
ci: ci,
maxWidth: maxWidth,
scrollProxy: scrollProxy,
deleteMessage: deleteMessage,
deletingItem: $deletingItem,
composeState: $composeState,
showDeleteMessage: $showDeleteMessage
)
.padding(.horizontal)
.environmentObject(chat)
chatItemWithMenu(ci, maxWidth)
.padding(.horizontal)
.padding(.top, 5)
}
}
private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
ChatItemWithMenu(
ci: ci,
maxWidth: maxWidth,
scrollProxy: scrollProxy,
deleteMessage: deleteMessage,
deletingItem: $deletingItem,
composeState: $composeState,
showDeleteMessage: $showDeleteMessage
)
.environmentObject(chat)
}
private struct ChatItemWithMenu: View {
@EnvironmentObject var chat: Chat
@Environment(\.colorScheme) var colorScheme
var ci: ChatItem
var showMember: Bool = false
var maxWidth: CGFloat
var scrollProxy: ScrollViewProxy?
var deleteMessage: (CIDeleteMode) -> Void
@Binding var deletingItem: ChatItem?
@Binding var composeState: ComposeState
@Binding var showDeleteMessage: Bool
@State private var revealed = false
@State private var showChatItemInfoSheet: Bool = false
@State private var chatItemInfo: ChatItemInfo?
@State private var allowMenu: Bool = true
@State private var audioPlayer: AudioPlayer?
@State private var playbackState: VoiceMessagePlaybackState = .noPlayback
@State private var playbackTime: TimeInterval?
@@ -504,7 +513,7 @@ struct ChatView: View {
)
VStack(alignment: alignment.horizontal, spacing: 3) {
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
chatItemReactions()
@@ -240,6 +240,7 @@ struct ComposeView: View {
@State var pendingLinkUrl: URL? = nil
@State var cancelledLinks: Set<String> = []
@Environment(\.colorScheme) private var colorScheme
@State private var showChooseSource = false
@State private var showMediaPicker = false
@State private var showTakePhoto = false
@@ -308,7 +309,10 @@ struct ComposeView: View {
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages),
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
keyboardVisible: $keyboardVisible
keyboardVisible: $keyboardVisible,
sendButtonColor: chat.chatInfo.incognito
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
: .accentColor
)
.padding(.trailing, 12)
.background(.background)
@@ -22,13 +22,14 @@ struct ContextItemView: View {
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(.secondary)
MsgContentView(
text: contextItem.text,
formattedText: contextItem.formattedText,
sender: contextItem.memberDisplayName
)
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
.lineLimit(3)
if let sender = contextItem.memberDisplayName {
VStack(alignment: .leading, spacing: 4) {
Text(sender).font(.caption).foregroundColor(.secondary)
msgContentView(lines: 2)
}
} else {
msgContentView(lines: 3)
}
Spacer()
Button {
withAnimation {
@@ -44,6 +45,15 @@ struct ContextItemView: View {
.background(chatItemFrameColor(contextItem, colorScheme))
.padding(.top, 8)
}
private func msgContentView(lines: Int) -> some View {
MsgContentView(
text: contextItem.text,
formattedText: contextItem.formattedText
)
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
.lineLimit(lines)
}
}
struct ContextItemView_Previews: PreviewProvider {
@@ -28,6 +28,7 @@ struct SendMessageView: View {
@State private var holdingVMR = false
@Namespace var namespace
@Binding var keyboardVisible: Bool
var sendButtonColor = Color.accentColor
@State private var teHeight: CGFloat = 42
@State private var teFont: Font = .body
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
@@ -169,7 +170,7 @@ struct SendMessageView: View {
? "checkmark.circle.fill"
: "arrow.up.circle.fill")
.resizable()
.foregroundColor(.accentColor)
.foregroundColor(sendButtonColor)
.frame(width: sendButtonSize, height: sendButtonSize)
.opacity(sendButtonOpacity)
}
+26 -26
View File
@@ -24,11 +24,6 @@
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00168028C4FE760094D739 /* KeyChain.swift */; };
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; };
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
5C0403922A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C04038D2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a */; };
5C0403932A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C04038E2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a */; };
5C0403942A7EAA41006ACFE8 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C04038F2A7EAA41006ACFE8 /* libffi.a */; };
5C0403952A7EAA41006ACFE8 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0403902A7EAA41006ACFE8 /* libgmp.a */; };
5C0403962A7EAA41006ACFE8 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0403912A7EAA41006ACFE8 /* libgmpxx.a */; };
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; };
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */; };
@@ -48,6 +43,11 @@
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
5C4CC1B02A88383C006BF552 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4CC1AB2A88383C006BF552 /* libffi.a */; };
5C4CC1B12A88383C006BF552 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4CC1AC2A88383C006BF552 /* libgmpxx.a */; };
5C4CC1B22A88383C006BF552 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4CC1AD2A88383C006BF552 /* libgmp.a */; };
5C4CC1B32A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4CC1AE2A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx.a */; };
5C4CC1B42A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4CC1AF2A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx-ghc8.10.7.a */; };
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
@@ -263,11 +263,6 @@
5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = "<group>"; };
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
5C04038D2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a"; sourceTree = "<group>"; };
5C04038E2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a"; sourceTree = "<group>"; };
5C04038F2A7EAA41006ACFE8 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C0403902A7EAA41006ACFE8 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C0403912A7EAA41006ACFE8 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; };
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = "<group>"; };
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = "<group>"; };
@@ -289,6 +284,11 @@
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
5C4CC1AB2A88383C006BF552 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C4CC1AC2A88383C006BF552 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C4CC1AD2A88383C006BF552 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C4CC1AE2A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx.a"; sourceTree = "<group>"; };
5C4CC1AF2A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx-ghc8.10.7.a"; sourceTree = "<group>"; };
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
@@ -501,13 +501,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5C0403932A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a in Frameworks */,
5C0403922A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a in Frameworks */,
5C4CC1B12A88383C006BF552 /* libgmpxx.a in Frameworks */,
5C4CC1B32A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx.a in Frameworks */,
5C4CC1B22A88383C006BF552 /* libgmp.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C0403942A7EAA41006ACFE8 /* libffi.a in Frameworks */,
5C0403952A7EAA41006ACFE8 /* libgmp.a in Frameworks */,
5C4CC1B02A88383C006BF552 /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C0403962A7EAA41006ACFE8 /* libgmpxx.a in Frameworks */,
5C4CC1B42A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx-ghc8.10.7.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -568,11 +568,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
5C04038F2A7EAA41006ACFE8 /* libffi.a */,
5C0403902A7EAA41006ACFE8 /* libgmp.a */,
5C0403912A7EAA41006ACFE8 /* libgmpxx.a */,
5C04038D2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a */,
5C04038E2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a */,
5C4CC1AB2A88383C006BF552 /* libffi.a */,
5C4CC1AD2A88383C006BF552 /* libgmp.a */,
5C4CC1AC2A88383C006BF552 /* libgmpxx.a */,
5C4CC1AF2A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx-ghc8.10.7.a */,
5C4CC1AE2A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1478,7 +1478,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 164;
CURRENT_PROJECT_VERSION = 167;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1520,7 +1520,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 164;
CURRENT_PROJECT_VERSION = 167;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1600,7 +1600,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 164;
CURRENT_PROJECT_VERSION = 167;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1632,7 +1632,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 164;
CURRENT_PROJECT_VERSION = 167;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1664,7 +1664,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 164;
CURRENT_PROJECT_VERSION = 167;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1710,7 +1710,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 164;
CURRENT_PROJECT_VERSION = 167;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
+30
View File
@@ -2021,6 +2021,17 @@ public struct ChatItem: Identifiable, Decodable {
}
}
public var memberConnected: GroupMember? {
switch chatDir {
case .groupRcv(let groupMember):
switch content {
case .rcvGroupEvent(rcvGroupEvent: .memberConnected): return groupMember
default: return nil
}
default: return nil
}
}
private var showNtfDir: Bool {
return !chatDir.sent
}
@@ -2519,6 +2530,25 @@ public enum CIContent: Decodable, ItemContent {
}
}
}
public var showMemberName: Bool {
switch self {
case .sndMsgContent: return true
case .rcvMsgContent: return true
case .sndDeleted: return true
case .rcvDeleted: return true
case .sndCall: return true
case .rcvCall: return true
case .rcvIntegrityError: return true
case .rcvDecryptionError: return true
case .rcvGroupInvitation: return true
case .sndChatPreference: return true
case .sndModerated: return true
case .rcvModerated: return true
case .invalidJSON: return true
default: return false
}
}
}
public enum MsgDecryptError: String, Decodable {
@@ -38,6 +38,7 @@ import chat.simplex.common.views.chatlist.updateChatSettings
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
@Composable
@@ -51,15 +52,16 @@ fun ChatInfoView(
close: () -> Unit,
) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val currentUser = chatModel.currentUser.value
val connStats = remember { mutableStateOf(connectionStats) }
val contact = rememberUpdatedState(contact).value
val chat = remember(contact.id) { chatModel.chats.firstOrNull { it.id == contact.id } }
val currentUser = remember { chatModel.currentUser }.value
val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null && currentUser != null) {
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) {
mutableStateOf(chatModel.contactNetworkStatus(contact))
}
val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
ChatInfoLayout(
chat,
contact,
@@ -203,7 +205,10 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
if (r) {
chatModel.removeChat(chatInfo.id)
chatModel.chatId.value = null
if (chatModel.chatId.value == chatInfo.id) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
}
ntfManager.cancelNotificationsForChat(chatInfo.id)
close?.invoke()
}
@@ -239,7 +244,7 @@ fun ChatInfoLayout(
currentUser: User,
sendReceipts: State<SendReceipts>,
setSendReceipts: (SendReceipts) -> Unit,
connStats: MutableState<ConnectionStats?>,
connStats: State<ConnectionStats?>,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
localAlias: String,
@@ -256,10 +261,15 @@ fun ChatInfoLayout(
verifyClicked: () -> Unit,
) {
val cStats = connStats.value
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
KeyChangeEffect(chat.id) {
scope.launch { scrollState.scrollTo(0) }
}
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.verticalScroll(scrollState)
) {
Row(
Modifier.fillMaxWidth(),
@@ -98,6 +98,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
val view = LocalMultiplatformView()
if (activeChat.value == null || user == null) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
} else {
val chat = activeChat.value!!
// We need to have real unreadCount value for displaying it inside top right button
@@ -133,27 +134,45 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
chatModel.chatId.value = null
},
info = {
if (ModalManager.end.hasModalsOpen()) {
ModalManager.end.closeModals()
return@ChatLayout
}
hideKeyboard(view)
withApi {
// The idea is to preload information before showing a modal because large groups can take time to load all members
var preloadedContactInfo: Pair<ConnectionStats, Profile?>? = null
var preloadedCode: String? = null
var preloadedLink: Pair<String, GroupMemberRole>? = null
if (chat.chatInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
val (_, code) = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.apiId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
}
}
preloadedContactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
preloadedCode = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second
} else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
val link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
var groupLink = link?.first
var groupLinkMemberRole = link?.second
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, {
groupLink = it.first;
groupLinkMemberRole = it.second
preloadedLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
}
ModalManager.end.showModalCloseable(true) { close ->
val chat = remember { activeChat }.value
if (chat?.chatInfo is ChatInfo.Direct) {
var contactInfo: Pair<ConnectionStats, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
var code: String? by remember { mutableStateOf(preloadedCode) }
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
preloadedContactInfo = contactInfo
code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second
preloadedCode = code
}
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
} else if (chat?.chatInfo is ChatInfo.Group) {
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
KeyChangeEffect(chat.id) {
setGroupMembers((chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
preloadedLink = link
}
GroupChatInfoView(chatModel, link?.first, link?.second, {
link = it
preloadedLink = it
}, close)
}
}
@@ -112,7 +112,7 @@ data class ComposeState(
}
val empty: Boolean
get() = message.isEmpty() && preview is ComposePreview.NoPreview
get() = message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
companion object {
fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
@@ -283,6 +283,20 @@ fun ComposeView(
cancelledLinks.clear()
}
fun clearPrevDraft(prevChatId: String?) {
if (chatModel.draftChatId.value == prevChatId) {
chatModel.draft.value = null
chatModel.draftChatId.value = null
}
}
fun clearCurrentDraft() {
if (chatModel.draftChatId.value == chat.id) {
chatModel.draft.value = null
chatModel.draftChatId.value = null
}
}
fun clearState(live: Boolean = false) {
if (live) {
composeState.value = composeState.value.copy(inProgress = false)
@@ -378,6 +392,7 @@ fun ComposeView(
if (liveMessage != null) composeState.value = cs.copy(liveMessage = null)
sending()
}
clearCurrentDraft()
if (cs.contextItem is ComposeContextItem.EditingItem) {
val ei = cs.contextItem.chatItem
@@ -705,13 +720,6 @@ fun ComposeView(
}
}
fun clearCurrentDraft() {
if (chatModel.draftChatId.value == chat.id) {
chatModel.draft.value = null
chatModel.draftChatId.value = null
}
}
LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) {
if (!chat.userCanSend) {
clearCurrentDraft()
@@ -719,23 +727,26 @@ fun ComposeView(
}
}
DisposableEffectOnGone {
KeyChangeEffect(chatModel.chatId.value) { prevChatId ->
val cs = composeState.value
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
sendMessage(null)
resetLinkPreview()
clearCurrentDraft()
clearPrevDraft(prevChatId)
deleteUnusedFiles()
} else if (composeState.value.inProgress) {
clearCurrentDraft()
} else if (!composeState.value.empty) {
} else if (cs.inProgress) {
clearPrevDraft(prevChatId)
} else if (!cs.empty) {
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
}
chatModel.draft.value = composeState.value
chatModel.draftChatId.value = chat.id
chatModel.draftChatId.value = prevChatId
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
} else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) {
composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
} else {
clearCurrentDraft()
clearPrevDraft(prevChatId)
deleteUnusedFiles()
}
chatModel.removeLiveDummy()
@@ -71,6 +71,9 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
removeContact = { contactId -> selectedContacts.removeIf { it == contactId } },
close = close,
)
KeyChangeEffect(chatModel.chatId.value) {
close()
}
}
fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
@@ -173,7 +176,7 @@ fun AddGroupMembersLayout(
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.select_contacts)) {
SectionItemView(padding = PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)) {
SearchRowView(searchText, selectedContacts.size)
SearchRowView(searchText)
}
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact)
}
@@ -184,8 +187,7 @@ fun AddGroupMembersLayout(
@Composable
private fun SearchRowView(
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) },
selectedContactsSize: Int
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
) {
Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) {
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.secondary)
@@ -194,11 +196,6 @@ private fun SearchRowView(
SearchTextField(Modifier.fillMaxWidth(), searchText = searchText, alwaysVisible = true) {
searchText.value = searchText.value.copy(it)
}
val view = LocalMultiplatformView()
LaunchedEffect(selectedContactsSize) {
searchText.value = searchText.value.copy("")
hideKeyboard(view)
}
}
@Composable
@@ -8,8 +8,8 @@ import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -33,11 +33,12 @@ import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import kotlinx.coroutines.launch
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
@Composable
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) {
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val currentUser = chatModel.currentUser.value
@@ -132,7 +133,10 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
if (r) {
chatModel.removeChat(chatInfo.id)
chatModel.chatId.value = null
if (chatModel.chatId.value == chatInfo.id) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
}
ntfManager.cancelNotificationsForChat(chatInfo.id)
close?.invoke()
}
@@ -177,79 +181,92 @@ fun GroupChatInfoLayout(
leaveGroup: () -> Unit,
manageGroupLink: () -> Unit,
) {
Column(
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
KeyChangeEffect(chat.id) {
scope.launch { listState.scrollToItem(0) }
}
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } }
LazyColumn(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.fillMaxWidth(),
state = listState
) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
GroupChatInfoHeader(chat.chatInfo)
}
SectionSpacer()
item {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
GroupChatInfoHeader(chat.chatInfo)
}
SectionSpacer()
SectionView {
if (groupInfo.canEdit) {
EditGroupProfileButton(editGroupProfile)
}
if (groupInfo.groupProfile.description != null || groupInfo.canEdit) {
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
}
GroupPreferencesButton(openPreferences)
if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
} else {
SendReceiptsOptionDisabled()
}
}
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
SectionDividerSpaced(maxTopPadding = true)
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) {
if (groupInfo.canAddMembers) {
if (groupLink == null) {
CreateGroupLinkButton(manageGroupLink)
SectionView {
if (groupInfo.canEdit) {
EditGroupProfileButton(editGroupProfile)
}
if (groupInfo.groupProfile.description != null || groupInfo.canEdit) {
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
}
GroupPreferencesButton(openPreferences)
if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
} else {
GroupLinkButton(manageGroupLink)
}
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
AddMembersButton(tint, onAddMembersClick)
}
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
val filteredMembers = remember { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } }
if (members.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
SearchRowView(searchText)
SendReceiptsOptionDisabled()
}
}
SectionItemView(minHeight = 54.dp) {
MemberRow(groupInfo.membership, user = true)
}
MembersList(filteredMembers.value, showMemberInfo)
}
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView {
ClearChatButton(clearChat)
if (groupInfo.canDelete) {
DeleteGroupButton(deleteGroup)
}
if (groupInfo.membership.memberCurrent) {
LeaveGroupButton(leaveGroup)
}
}
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
SectionDividerSpaced(maxTopPadding = true)
if (developerTools) {
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName)
InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString())
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) {
if (groupInfo.canAddMembers) {
if (groupLink == null) {
CreateGroupLinkButton(manageGroupLink)
} else {
GroupLinkButton(manageGroupLink)
}
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
AddMembersButton(tint, onAddMembersClick)
}
if (members.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
SearchRowView(searchText)
}
}
SectionItemView(minHeight = 54.dp) {
MemberRow(groupInfo.membership, user = true)
}
}
}
SectionBottomSpacer()
items(filteredMembers.value) { member ->
Divider()
SectionItemView({ showMemberInfo(member) }, minHeight = 54.dp) {
MemberRow(member)
}
}
item {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView {
ClearChatButton(clearChat)
if (groupInfo.canDelete) {
DeleteGroupButton(deleteGroup)
}
if (groupInfo.membership.memberCurrent) {
LeaveGroupButton(leaveGroup)
}
}
if (developerTools) {
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName)
InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString())
}
}
SectionBottomSpacer()
}
}
}
@@ -330,18 +347,6 @@ private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick
)
}
@Composable
private fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) {
Column {
members.forEachIndexed { index, member ->
Divider()
SectionItemView({ showMemberInfo(member) }, minHeight = 54.dp) {
MemberRow(member)
}
}
}
}
@Composable
private fun MemberRow(member: GroupMember, user: Boolean = false) {
Row(
@@ -23,7 +23,7 @@ import chat.simplex.common.views.newchat.QRCode
import chat.simplex.res.MR
@Composable
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit) {
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit) {
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
var creatingLink by rememberSaveable { mutableStateOf(false) }
@@ -34,7 +34,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
onGroupLinkUpdated(link)
}
creatingLink = false
}
@@ -60,7 +60,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
onGroupLinkUpdated(link)
}
}
}
@@ -75,7 +75,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
if (r) {
groupLink = null
onGroupLinkUpdated(null to null)
onGroupLinkUpdated(null)
}
}
},
@@ -368,7 +368,12 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
stringResource(MR.strings.delete_verb),
painterResource(MR.images.ic_delete),
onClick = {
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {}
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {
if (chatModel.chatId.value == null) {
ModalManager.center.closeModals()
ModalManager.end.closeModals()
}
}
showMenu.value = false
},
color = Color.Red
@@ -517,7 +522,10 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
if (r) {
chatModel.removeChat(chatInfo.id)
chatModel.chatId.value = null
if (chatModel.chatId.value == chatInfo.id) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
}
}
}
},
@@ -550,7 +558,10 @@ fun deleteGroup(groupInfo: GroupInfo, chatModel: ChatModel) {
val r = chatModel.controller.apiDeleteChat(ChatType.Group, groupInfo.apiId)
if (r) {
chatModel.removeChat(groupInfo.id)
chatModel.chatId.value = null
if (chatModel.chatId.value == groupInfo.id) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
}
ntfManager.cancelNotificationsForChat(groupInfo.id)
}
}
@@ -61,6 +61,13 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
connectIfOpenedViaUri(url, chatModel)
}
}
if (appPlatform.isDesktop) {
KeyChangeEffect(chatModel.chatId.value) {
if (chatModel.chatId.value != null) {
ModalManager.end.closeModalsExceptFirst()
}
}
}
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
var searchInList by rememberSaveable { mutableStateOf("") }
val scope = rememberCoroutineScope()
@@ -51,26 +51,31 @@ class AlertManager {
buttons: @Composable () -> Unit,
) {
showAlert {
DefaultDialog(onDismissRequest = ::hideAlert) {
Column(
Modifier
.background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp)))
.padding(bottom = DEFAULT_PADDING)
) {
AlertDialog(
onDismissRequest = ::hideAlert,
title = {
Text(
title,
Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
if (text != null) {
Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary)
},
buttons = {
Column(
Modifier
.padding(bottom = DEFAULT_PADDING)
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
if (text != null) {
Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary)
}
buttons()
}
buttons()
}
}
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
}
@@ -97,6 +97,12 @@ class ModalManager(private val placement: ModalPlacement? = null) {
modalCount.value = 0
}
fun closeModalsExceptFirst() {
while (modalCount.value > 1) {
closeModal()
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun showInView() {
@@ -329,3 +329,44 @@ fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit =
}
}
}
/**
* Runs the [block] only after initial value of the [key1] changes, not after initial launch
* */
@Composable
@NonRestartableComposable
fun <T> KeyChangeEffect(
key1: T?,
block: suspend CoroutineScope.(prevKey: T?) -> Unit
) {
var prevKey by remember { mutableStateOf(key1) }
var anyChange by remember { mutableStateOf(false) }
LaunchedEffect(key1) {
if (anyChange || key1 != prevKey) {
block(prevKey)
prevKey = key1
anyChange = true
}
}
}
/**
* Runs the [block] only after initial value of the [key1] or [key2] changes, not after initial launch
* */
@Composable
@NonRestartableComposable
fun KeyChangeEffect(
key1: Any?,
key2: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val initialKey = remember { key1 }
val initialKey2 = remember { key2 }
var anyChange by remember { mutableStateOf(false) }
LaunchedEffect(key1) {
if (anyChange || key1 != initialKey || key2 != initialKey2) {
block()
anyChange = true
}
}
}
@@ -23,6 +23,7 @@ import chat.simplex.common.views.usersettings.IncognitoView
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
import java.net.URI
import java.net.URISyntaxException
@Composable
fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
@@ -73,6 +74,11 @@ fun PasteToConnectLayout(
title = generalGetString(MR.strings.invalid_connection_link),
text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
)
} catch (e: URISyntaxException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_connection_link),
text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
)
}
}
@@ -40,7 +40,7 @@ actual fun screenOrientation(): ScreenOrientation = ScreenOrientation.UNDEFINED
@Composable // LALAL
actual fun screenWidth(): Dp {
return java.awt.Toolkit.getDefaultToolkit().screenSize.width.dp.also { println("LALAL $it") }
return java.awt.Toolkit.getDefaultToolkit().screenSize.width.dp
/*var width by remember { mutableStateOf(java.awt.Toolkit.getDefaultToolkit().screenSize.width.also { println("LALAL $it") }) }
SideEffect {
if (width != java.awt.Toolkit.getDefaultToolkit().screenSize.width)
+4 -4
View File
@@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
android.version_name=5.3-beta.3
android.version_code=141
android.version_name=5.3-beta.4
android.version_code=146
desktop.version_name=1.1.0
desktop.version_code=3
desktop.version_name=1.2.0
desktop.version_code=4
kotlin.version=1.8.20
gradle.plugin.version=7.4.2
+1 -1
View File
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: e2065ab3525ca67e39700ee8040839d667f75ea2
tag: e586bef57a1391d8bdedc2afa645926931549e16
source-repository-package
type: git
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."e2065ab3525ca67e39700ee8040839d667f75ea2" = "13rz58bdpkhfrp1d58ylpbazhzs26q5fjg0sclxgvdqnnmac5cgz";
"https://github.com/simplex-chat/simplexmq.git"."e586bef57a1391d8bdedc2afa645926931549e16" = "00804ck1xka37j5gwaiyd3a8vflv8z1hmip1wyynkvr7naxblvzh";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
+1
View File
@@ -107,6 +107,7 @@ library
Simplex.Chat.Migrations.M20230621_chat_item_moderations
Simplex.Chat.Migrations.M20230705_delivery_receipts
Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
Simplex.Chat.Migrations.M20230814_indexes
Simplex.Chat.Mobile
Simplex.Chat.Mobile.WebRTC
Simplex.Chat.Options
+1 -1
View File
@@ -5150,7 +5150,7 @@ chatCommandP =
("/help" <|> "/h") $> ChatHelp HSMain,
("/group " <|> "/g ") *> char_ '#' *> (NewGroup <$> groupProfile),
"/_group " *> (APINewGroup <$> A.decimal <* A.space <*> jsonP),
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRAdmin)),
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRMember)),
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName),
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName),
@@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20230814_indexes where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20230814_indexes :: Query
m20230814_indexes =
[sql|
CREATE INDEX idx_chat_items_user_id_item_status ON chat_items(user_id, item_status);
|]
down_m20230814_indexes :: Query
down_m20230814_indexes =
[sql|
DROP INDEX idx_chat_items_user_id_item_status;
|]
@@ -701,3 +701,7 @@ CREATE INDEX idx_group_snd_item_statuses_chat_item_id ON group_snd_item_statuses
CREATE INDEX idx_group_snd_item_statuses_group_member_id ON group_snd_item_statuses(
group_member_id
);
CREATE INDEX idx_chat_items_user_id_item_status ON chat_items(
user_id,
item_status
);
+3 -1
View File
@@ -75,6 +75,7 @@ import Simplex.Chat.Migrations.M20230618_favorite_chats
import Simplex.Chat.Migrations.M20230621_chat_item_moderations
import Simplex.Chat.Migrations.M20230705_delivery_receipts
import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
import Simplex.Chat.Migrations.M20230814_indexes
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -149,7 +150,8 @@ schemaMigrations =
("20230618_favorite_chats", m20230618_favorite_chats, Just down_m20230618_favorite_chats),
("20230621_chat_item_moderations", m20230621_chat_item_moderations, Just down_m20230621_chat_item_moderations),
("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts),
("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses)
("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses),
("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes)
]
-- | The list of migrations in ascending order by date
+1 -1
View File
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
commit: e2065ab3525ca67e39700ee8040839d667f75ea2
commit: e586bef57a1391d8bdedc2afa645926931549e16
- github: kazu-yamamoto/http2
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
# - ../direct-sqlcipher
+21 -21
View File
@@ -82,7 +82,7 @@ testGroupShared alice bob cath checkMessages = do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
alice ##> "/a team bob admin"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
@@ -94,7 +94,7 @@ testGroupShared alice bob cath checkMessages = do
(alice <## "#team: bob joined the group")
(bob <## "#team: you joined the group")
when checkMessages $ threadDelay 1000000 -- for deterministic order of messages and "connected" events
alice ##> "/a team cath"
alice ##> "/a team cath admin"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to cath",
do
@@ -242,14 +242,14 @@ testGroup2 =
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/a club bob"
alice ##> "/a club bob admin"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
do
bob <## "#club: alice invites you to join the group as admin"
bob <## "use /j club to accept"
]
alice ##> "/a club cath"
alice ##> "/a club cath admin"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to cath",
do
@@ -274,7 +274,7 @@ testGroup2 =
concurrentlyN_
[ bob <## "invitation to join the group #club sent to dan",
do
dan <## "#club: bob invites you to join the group as admin"
dan <## "#club: bob invites you to join the group as member"
dan <## "use /j club to accept"
]
dan ##> "/j club"
@@ -486,7 +486,7 @@ testGroupDeleteWhenInvited =
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
bob ##> "/d #team"
@@ -497,7 +497,7 @@ testGroupDeleteWhenInvited =
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
@@ -513,7 +513,7 @@ testGroupReAddInvited =
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
-- alice re-adds bob, he sees it as the same group
@@ -521,7 +521,7 @@ testGroupReAddInvited =
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
-- if alice removes bob and then re-adds him, she uses a new connection request
@@ -532,7 +532,7 @@ testGroupReAddInvited =
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team_1: alice invites you to join the group as admin"
bob <## "#team_1: alice invites you to join the group as member"
bob <## "use /j team_1 to accept"
]
@@ -548,7 +548,7 @@ testGroupReAddInvitedChangeRole =
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
-- alice re-adds bob, he sees it as the same group
@@ -588,7 +588,7 @@ testGroupDeleteInvitedContact =
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
threadDelay 500000
@@ -621,7 +621,7 @@ testDeleteGroupMemberProfileKept =
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
bob ##> "/j team"
@@ -640,7 +640,7 @@ testDeleteGroupMemberProfileKept =
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
do
bob <## "#club: alice invites you to join the group as admin"
bob <## "#club: alice invites you to join the group as member"
bob <## "use /j club to accept"
]
bob ##> "/j club"
@@ -693,7 +693,7 @@ testGroupRemoveAdd =
]
alice ##> "/a team bob"
alice <## "invitation to join the group #team sent to bob"
bob <## "#team_1: alice invites you to join the group as admin"
bob <## "#team_1: alice invites you to join the group as member"
bob <## "use /j team_1 to accept"
bob ##> "/j team_1"
concurrentlyN_
@@ -734,7 +734,7 @@ testGroupList =
concurrentlyN_
[ alice <## "invitation to join the group #tennis sent to bob",
do
bob <## "#tennis: alice invites you to join the group as admin"
bob <## "#tennis: alice invites you to join the group as member"
bob <## "use /j tennis to accept"
]
-- alice sees both groups
@@ -1177,7 +1177,7 @@ testGroupDeleteUnusedContacts =
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
do
bob <## "#club: alice invites you to join the group as admin"
bob <## "#club: alice invites you to join the group as member"
bob <## "use /j club to accept"
]
bob ##> "/j club"
@@ -1188,7 +1188,7 @@ testGroupDeleteUnusedContacts =
concurrentlyN_
[ alice <## "invitation to join the group #club sent to cath",
do
cath <## "#club: alice invites you to join the group as admin"
cath <## "#club: alice invites you to join the group as member"
cath <## "use /j club to accept"
]
cath ##> "/j club"
@@ -1831,7 +1831,7 @@ testGroupLinkIncognitoMembership =
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
-- alice invites bob
alice ##> ("/a team " <> bobIncognito)
alice ##> ("/a team " <> bobIncognito <> " admin")
concurrentlyN_
[ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
do
@@ -2456,7 +2456,7 @@ testConfigureGroupDeliveryReceipts tmp =
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
do
bob <## "#club: alice invites you to join the group as admin"
bob <## "#club: alice invites you to join the group as member"
bob <## "use /j club to accept"
]
bob ##> "/j club"
@@ -2467,7 +2467,7 @@ testConfigureGroupDeliveryReceipts tmp =
concurrentlyN_
[ alice <## "invitation to join the group #club sent to cath",
do
cath <## "#club: alice invites you to join the group as admin"
cath <## "#club: alice invites you to join the group as member"
cath <## "use /j club to accept"
]
cath ##> "/j club"
+5 -5
View File
@@ -785,7 +785,7 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil
alice <## "group #secret_club is created"
alice <## "to add members use /a secret_club <name> or /create link #secret_club"
-- alice invites bob
alice ##> "/a secret_club bob"
alice ##> "/a secret_club bob admin"
concurrentlyN_
[ alice <## "invitation to join the group #secret_club sent to bob",
do
@@ -797,7 +797,7 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil
(alice <## "#secret_club: bob joined the group")
(bob <## "#secret_club: you joined the group")
-- alice invites cath
alice ##> ("/a secret_club " <> cathIncognito)
alice ##> ("/a secret_club " <> cathIncognito <> " admin")
concurrentlyN_
[ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito),
do
@@ -819,7 +819,7 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil
cath ##> "/a secret_club dan"
cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts"
-- alice invites dan
alice ##> "/a secret_club dan"
alice ##> "/a secret_club dan admin"
concurrentlyN_
[ alice <## "invitation to join the group #secret_club sent to dan",
do
@@ -1045,7 +1045,7 @@ testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobPr
concurrentlyN_
[ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "#team: alice invites you to join the group as member"
bob <## ("use /j team to join incognito as " <> bobIncognito)
]
bob ##> "/j team"
@@ -1096,7 +1096,7 @@ testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobPr
concurrentlyN_
[ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "#team: alice invites you to join the group as member"
bob <## ("use /j team to join incognito as " <> bobIncognito)
]
bob ##> "/j team"
+1 -1
View File
@@ -18,11 +18,11 @@ main :: IO ()
main = do
setLogLevel LogError -- LogDebug
withGlobalLogging logCfg . hspec $ do
describe "Schema dump" schemaDumpTest
describe "SimpleX chat markdown" markdownTests
describe "SimpleX chat view" viewTests
describe "SimpleX chat protocol" protocolTests
describe "WebRTC encryption" webRTCTests
describe "Schema dump" schemaDumpTest
around testBracket $ do
describe "Mobile API Tests" mobileTests
describe "SimpleX chat client" chatTests