ios: members connected aggregated item (#2900)

* ios: members connected aggregated item

* wrapping hstack wip

* Revert "wrapping hstack wip"

This reverts commit 75af7473fc.

* 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>
This commit is contained in:
spaced4ndy 2023-08-14 17:34:22 +04:00 committed by GitHub
parent e326227d06
commit 8dcb70c019
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 138 additions and 58 deletions

View File

@ -484,13 +484,21 @@ final class ChatModel: ObservableObject {
} }
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? { 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] return reversedChatItems[i + 1]
} else { } else {
return nil 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) { func popChat(_ id: String) {
if let i = getChatIndex(id) { if let i = getChatIndex(id) {
popChat_(i) popChat_(i)

View File

@ -10,20 +10,11 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct CIEventView: View { struct CIEventView: View {
var chatItem: ChatItem var eventText: Text
var body: some View { var body: some View {
HStack(alignment: .bottom, spacing: 0) { HStack(alignment: .bottom, spacing: 0) {
if let member = chatItem.memberDisplayName { eventText
Text(member)
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
+ Text(" ")
+ chatEventText(chatItem)
} else {
chatEventText(chatItem)
}
} }
.padding(.leading, 6) .padding(.leading, 6)
.padding(.bottom, 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 { struct CIEventView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
CIEventView(chatItem: ChatItem.getGroupEventSample()) CIEventView(eventText: Text("event happened"))
} }
} }

View File

@ -58,6 +58,7 @@ struct ChatItemView: View {
} }
struct ChatItemContentView<Content: View>: View { struct ChatItemContentView<Content: View>: View {
@EnvironmentObject var chatModel: ChatModel
var chatInfo: ChatInfo var chatInfo: ChatInfo
var chatItem: ChatItem var chatItem: ChatItem
var showMember: Bool var showMember: Bool
@ -75,6 +76,7 @@ struct ChatItemContentView<Content: View>: View {
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem, showMember: showMember) case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem, showMember: showMember)
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case .rcvGroupEvent(.memberConnected): CIEventView(eventText: membersConnectedItemText())
case .rcvGroupEvent: eventItemView() case .rcvGroupEvent: eventItemView()
case .sndGroupEvent: eventItemView() case .sndGroupEvent: eventItemView()
case .rcvConnEvent: eventItemView() case .rcvConnEvent: eventItemView()
@ -108,12 +110,84 @@ struct ChatItemContentView<Content: View>: View {
} }
private func eventItemView() -> some 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 { private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
CIChatFeatureView(chatItem: chatItem, feature: feature, iconColor: iconColor) 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: "<member_names> and <n >= 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: "<member_name(s)> and <member_name> 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 { struct ChatItemView_Previews: PreviewProvider {

View File

@ -261,7 +261,7 @@ struct ChatView: View {
return GeometryReader { g in return GeometryReader { g in
ScrollViewReader { proxy in ScrollViewReader { proxy in
ScrollView { ScrollView {
LazyVStack(spacing: 5) { LazyVStack(spacing: 0) {
ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in
let voiceNoFrame = voiceWithoutFrame(ci) let voiceNoFrame = voiceWithoutFrame(ci)
let maxWidth = cInfo.chatType == .group let maxWidth = cInfo.chatType == .group
@ -430,35 +430,42 @@ struct ChatView: View {
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
if case let .groupRcv(member) = ci.chatDir, if case let .groupRcv(member) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo { case let .group(groupInfo) = chat.chatInfo {
let prevItem = chatModel.getPrevChatItem(ci) let nextItem = chatModel.getNextChatItem(ci)
HStack(alignment: .top, spacing: 0) { if ci.memberConnected != nil && nextItem?.memberConnected != nil {
let showMember = prevItem == nil || showMemberImage(member, prevItem) // memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView
if showMember { ZStack {} // scroll doesn't work if it's EmptyView()
ProfileImage(imageStr: member.memberProfile.image) } else {
.frame(width: memberImageSize, height: memberImageSize) let prevItem = chatModel.getPrevChatItem(ci)
.onTapGesture { selectedMember = member } HStack(alignment: .top, spacing: 0) {
.appSheet(item: $selectedMember) { member in let showMember = prevItem == nil || showMemberImage(member, prevItem)
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true) if showMember {
} ProfileImage(imageStr: member.memberProfile.image)
} else { .frame(width: memberImageSize, height: memberImageSize)
Rectangle().fill(.clear) .onTapGesture { selectedMember = member }
.frame(width: memberImageSize, height: memberImageSize) .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( .padding(.trailing)
ci: ci, .padding(.leading, 12)
showMember: showMember, .padding(.bottom, 5)
maxWidth: maxWidth,
scrollProxy: scrollProxy,
deleteMessage: deleteMessage,
deletingItem: $deletingItem,
composeState: $composeState,
showDeleteMessage: $showDeleteMessage
)
.padding(.leading, 8)
.environmentObject(chat)
} }
.padding(.trailing)
.padding(.leading, 12)
} else { } else {
ChatItemWithMenu( ChatItemWithMenu(
ci: ci, ci: ci,
@ -470,6 +477,7 @@ struct ChatView: View {
showDeleteMessage: $showDeleteMessage showDeleteMessage: $showDeleteMessage
) )
.padding(.horizontal) .padding(.horizontal)
.padding(.bottom, 5)
.environmentObject(chat) .environmentObject(chat)
} }
} }

View File

@ -2021,6 +2021,17 @@ public struct ChatItem: Identifiable, Decodable {
} }
} }
public var memberConnected: GroupMember? {
switch chatDir {
case .groupRcv(let groupMember):
switch content {
case .rcvGroupEvent(rcvGroupEvent: .memberConnected): return groupMember
default: return nil
}
default: return nil
}
}
private var showNtfDir: Bool { private var showNtfDir: Bool {
return !chatDir.sent return !chatDir.sent
} }