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:
parent
e326227d06
commit
8dcb70c019
@ -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)
|
||||||
|
@ -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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user