From 8dcb70c01998cf027152cf7758ff63d5849fab0b Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 14 Aug 2023 17:34:22 +0400 Subject: [PATCH] ios: members connected aggregated item (#2900) * ios: members connected aggregated item * wrapping hstack wip * Revert "wrapping hstack wip" This reverts commit 75af7473fc42349b37110135d64f00288c32c323. * redesign * fix scroll * revert * comment * remove padding * brackets * texts, icon * optimize - collect only member names * refactor * check different index * refactor 2 --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Model/ChatModel.swift | 10 ++- .../Views/Chat/ChatItem/CIEventView.swift | 27 +------ apps/ios/Shared/Views/Chat/ChatItemView.swift | 76 ++++++++++++++++++- apps/ios/Shared/Views/Chat/ChatView.swift | 72 ++++++++++-------- apps/ios/SimpleXChat/ChatTypes.swift | 11 +++ 5 files changed, 138 insertions(+), 58 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 5c831ba32..af0dff94e 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -484,12 +484,20 @@ final class ChatModel: ObservableObject { } func getPrevChatItem(_ ci: ChatItem) -> ChatItem? { - if let i = getChatItemIndex(ci), i < reversedChatItems.count - 1 { + if let i = getChatItemIndex(ci), i + 1 < reversedChatItems.count { return reversedChatItems[i + 1] } else { return nil } } + + func getNextChatItem(_ ci: ChatItem) -> ChatItem? { + if let i = getChatItemIndex(ci), i > 0 { + return reversedChatItems[i - 1] + } else { + return nil + } + } func popChat(_ id: String) { if let i = getChatIndex(id) { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift index cd059b704..9b372154e 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/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 20a04250f..1a3704c1f 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -58,6 +58,7 @@ struct ChatItemView: View { } struct ChatItemContentView: View { + @EnvironmentObject var chatModel: ChatModel var chatInfo: ChatInfo var chatItem: ChatItem var showMember: Bool @@ -75,6 +76,7 @@ struct ChatItemContentView: View { case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem, showMember: showMember) 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() @@ -108,12 +110,84 @@ 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) + + Text(" ") + + chatEventText(chatItem) + } else { + return chatEventText(chatItem) + } } private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View { CIChatFeatureView(chatItem: chatItem, feature: feature, iconColor: iconColor) } + + private func membersConnectedItemText() -> Text { + if case let .groupRcv(member) = chatItem.chatDir, + let prevItem = chatModel.getPrevChatItem(chatItem), + let prevMember = prevItem.memberConnected, + let membersConnectedText = membersConnectedText(connectedMemberNames(member, prevMember, prevItem)) { + return chatEventText(membersConnectedText, chatItem.timestampText) + } else { + return eventItemViewText() + } + + func connectedMemberNames(_ member: GroupMember, _ prevMember: GroupMember, _ prevItem: ChatItem) -> [String] { + [member.chatViewName, prevMember.chatViewName] + collectPrevConnectedMemberNames(prevItem) + } + } + + private func collectPrevConnectedMemberNames(_ ci: ChatItem) -> [String] { + guard let prevItem = chatModel.getPrevChatItem(ci), + let memberConnected = prevItem.memberConnected else { + return [] + } + let prevMemberNames = collectPrevConnectedMemberNames(prevItem) + return [memberConnected.chatViewName] + prevMemberNames + } + + private func membersConnectedText(_ memberNames: [String]) -> String? { + if memberNames.count > 3 { + return String.localizedStringWithFormat( + NSLocalizedString("%@ and %d other members connected", comment: " and = 2> other members connected (plural)"), + Array(memberNames.prefix(2)).joined(separator: ", "), + memberNames.count - 2 + ) + } else if memberNames.count >= 2, + let lastMemberName = memberNames.last { + return String.localizedStringWithFormat( + NSLocalizedString("%@ and %@ connected", comment: " and connected (plural)"), + memberNames.dropLast().joined(separator: ", "), + lastMemberName + ) + } else { + return nil + } + } +} + +func chatEventText(_ eventText: String, _ ts: Text) -> Text { + Text(eventText) + .font(.caption) + .foregroundColor(.secondary) + .fontWeight(.light) + + Text(" ") + + ts + .font(.caption) + .foregroundColor(Color.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 ae6308e21..010fdc769 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,35 +430,42 @@ 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) - } - } else { - Rectangle().fill(.clear) - .frame(width: memberImageSize, height: memberImageSize) + let nextItem = chatModel.getNextChatItem(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 { + 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) + } + } else { + Rectangle().fill(.clear) + .frame(width: memberImageSize, height: memberImageSize) + } + ChatItemWithMenu( + ci: ci, + showMember: showMember, + maxWidth: maxWidth, + scrollProxy: scrollProxy, + deleteMessage: deleteMessage, + deletingItem: $deletingItem, + composeState: $composeState, + showDeleteMessage: $showDeleteMessage + ) + .padding(.leading, 8) + .environmentObject(chat) } - 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) + .padding(.bottom, 5) } - .padding(.trailing) - .padding(.leading, 12) } else { ChatItemWithMenu( ci: ci, @@ -470,10 +477,11 @@ struct ChatView: View { showDeleteMessage: $showDeleteMessage ) .padding(.horizontal) + .padding(.bottom, 5) .environmentObject(chat) } } - + private struct ChatItemWithMenu: View { @EnvironmentObject var chat: Chat @Environment(\.colorScheme) var colorScheme @@ -485,13 +493,13 @@ struct ChatView: View { @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? diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index be8619395..7bed96cb0 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 }