diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index c3331c822a..390e25bed1 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -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) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift index cd059b704c..9b372154ed 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift @@ -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")) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift index 80bb7ea1ef..af4df40978 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift @@ -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)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 3a31ee4508..2dfd0f15f6 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -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) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift index 1ab631232e..9908d4d104 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -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() diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index 96d3c8eca3..8e042a8c76 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -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 { diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 20a04250f5..5d816ac64a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -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, allowMenu: Binding = .constant(false), audioPlayer: Binding = .constant(nil), playbackState: Binding = .constant(.noPlayback), playbackTime: Binding = .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: 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: 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: 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: 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 { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index ae6308e213..a73bb766e9 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -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() diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index cfa4a63173..e2a6cd1d20 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -240,6 +240,7 @@ struct ComposeView: View { @State var pendingLinkUrl: URL? = nil @State var cancelledLinks: Set = [] + @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) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index a9847afe4d..153d89fc27 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -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 { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index 5da8a99f7b..73c7286925 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -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) } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 97281b4e35..53b5f6d31b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -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 = ""; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = ""; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; - 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 = ""; }; - 5C04038E2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a"; sourceTree = ""; }; - 5C04038F2A7EAA41006ACFE8 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C0403902A7EAA41006ACFE8 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C0403912A7EAA41006ACFE8 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = ""; }; 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = ""; }; @@ -289,6 +284,11 @@ 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = ""; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = ""; }; 5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = ""; }; + 5C4CC1AB2A88383C006BF552 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C4CC1AC2A88383C006BF552 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C4CC1AD2A88383C006BF552 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C4CC1AE2A88383C006BF552 /* libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.4-FE3hjvmcZIPFfEovdlxWAx.a"; sourceTree = ""; }; + 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 = ""; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = ""; }; 5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = ""; }; 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index be8619395b..5bdd03ace6 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -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 { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 273400854e..87f8a7e651 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -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, setSendReceipts: (SendReceipts) -> Unit, - connStats: MutableState, + connStats: State, 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(), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 70825e6fba..749afe8fa6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -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? = null + var preloadedCode: String? = null + var preloadedLink: Pair? = 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? 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? 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) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 6570db5861..9be98525a1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -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, *> = 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() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index 1b8310e18d..90ab1b45ff 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -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 { @@ -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 = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }, - selectedContactsSize: Int + searchText: MutableState = 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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 402a9f28ca..40291b8fe0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -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) -> Unit, close: () -> Unit) { +fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair?) -> 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, 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( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 22b9c8a93a..7c767f9b7e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -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) -> Unit) { +fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair?) -> 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) } } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index f924737490..18df5e8502 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -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) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index e2c316046b..f71ec865f7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -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() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index b5de31bfea..49a7ad748f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -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)) + ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index ec3ee8ecea..0f930b3126 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -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() { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 781adbd8e0..c07459a3da 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -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 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 + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt index a0d0da69ac..0c13bd4f68 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt @@ -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) + ) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index 3a76808b0c..5aeddd2996 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -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) diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 0c3aed6a88..76946786d2 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -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 diff --git a/cabal.project b/cabal.project index 4eb8bea101..0b27046572 100644 --- a/cabal.project +++ b/cabal.project @@ -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 diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index eb51dc5e12..c2530c26c1 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -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"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index d52e8014c0..ea1bc2e71c 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -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 diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 3bff04fb99..82861967c1 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -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), diff --git a/src/Simplex/Chat/Migrations/M20230814_indexes.hs b/src/Simplex/Chat/Migrations/M20230814_indexes.hs new file mode 100644 index 0000000000..a7419037ef --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230814_indexes.hs @@ -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; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index f925726436..76b7ba4a12 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -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 +); diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index 972f0718eb..6da0d1cdcd 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -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 diff --git a/stack.yaml b/stack.yaml index a39c671554..30dedb1b45 100644 --- a/stack.yaml +++ b/stack.yaml @@ -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 diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 55087e01ea..2f5b2689ba 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -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 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 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 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" diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 9af7a54623..98c840388e 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -785,7 +785,7 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil alice <## "group #secret_club is created" alice <## "to add members use /a secret_club 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" diff --git a/tests/Test.hs b/tests/Test.hs index d9d36d472b..455d5459c7 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -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