ios: members connected aggregated item

This commit is contained in:
spaced4ndy 2023-08-11 19:42:04 +04:00
parent 1bc880877d
commit 08ad0ecf21
6 changed files with 182 additions and 27 deletions

View File

@ -491,6 +491,14 @@ final class ChatModel: ObservableObject {
} }
} }
func getNextChatItem(_ ci: ChatItem) -> ChatItem? {
if let i = getChatItemIndex(ci), i - 1 >= 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

@ -0,0 +1,51 @@
//
// CIMembersConnectedView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 11.08.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct CIMembersConnectedView: View {
@EnvironmentObject var chat: Chat
var chatItem: ChatItem
var members: [GroupMember]
@State private var selectedMember: GroupMember? = nil
var body: some View {
VStack(alignment: .leading) {
HStack {
ForEach(members, id: \.groupMemberId) { member in
memberPicture(member)
}
}
chatEventText(chatItem)
}
}
@ViewBuilder func memberPicture(_ member: GroupMember) -> some View {
let v = ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
if case let .group(groupInfo) = chat.chatInfo {
v
.onTapGesture { selectedMember = member }
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
}
} else {
v
}
}
}
struct CIMembersConnectedView_Previews: PreviewProvider {
static var previews: some View {
CIMembersConnectedView(
chatItem: ChatItem.getMembersConnectedSample(),
members: [GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData]
)
}
}

View File

@ -92,6 +92,7 @@ struct ChatItemContentView<Content: View>: View {
case .sndModerated: deletedItemView() case .sndModerated: deletedItemView()
case .rcvModerated: deletedItemView() case .rcvModerated: deletedItemView()
case let .invalidJSON(json): CIInvalidJSONView(json: json) case let .invalidJSON(json): CIInvalidJSONView(json: json)
case let .membersConnected(members): CIMembersConnectedView(chatItem: chatItem, members: members)
} }
} }

View File

@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat import SimpleXChat
import SwiftyGif import SwiftyGif
private let memberImageSize: CGFloat = 34 let memberImageSize: CGFloat = 34
struct ChatView: View { struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@ -430,35 +430,46 @@ 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.isMemberConnected != nil && (nextItem?.isMemberConnected != nil) {
let showMember = prevItem == nil || showMemberImage(member, prevItem) EmptyView()
if showMember { } else {
ProfileImage(imageStr: member.memberProfile.image) let prevItem = chatModel.getPrevChatItem(ci)
.frame(width: memberImageSize, height: memberImageSize) if ci.isMemberConnected != nil,
.onTapGesture { selectedMember = member } let prevItem = prevItem,
.appSheet(item: $selectedMember) { member in let prevMember = prevItem.isMemberConnected {
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true) membersConnectedItem(ci, maxWidth, member, prevMember, prevItem)
}
} else { } else {
Rectangle().fill(.clear) HStack(alignment: .top, spacing: 0) {
.frame(width: memberImageSize, height: memberImageSize) 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)
}
.padding(.trailing)
.padding(.leading, 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 { } else {
ChatItemWithMenu( ChatItemWithMenu(
ci: ci, ci: ci,
@ -474,6 +485,49 @@ struct ChatView: View {
} }
} }
@ViewBuilder private func membersConnectedItem(
_ ci: ChatItem,
_ maxWidth: CGFloat,
_ member: GroupMember,
_ prevMember: GroupMember,
_ prevItem: ChatItem
) -> some View {
let membersConnected: [GroupMember] = [member, prevMember] + collectPrevMembersConnected(prevItem)
let replacingItem = ChatItem(
chatDir: ci.chatDir,
meta: ci.meta,
content: .membersConnected(members: membersConnected)
)
let alignment: Alignment = .leading
VStack(alignment: alignment.horizontal, spacing: 3) {
ChatItemView(chatInfo: chat.chatInfo, chatItem: replacingItem, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: .constant(false))
}
.frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment)
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
.padding(.horizontal)
.environmentObject(chat)
// return ChatItemWithMenu(
// ci: replacingItem,
// maxWidth: maxWidth,
// scrollProxy: scrollProxy,
// deleteMessage: deleteMessage,
// deletingItem: $deletingItem,
// composeState: $composeState,
// showDeleteMessage: $showDeleteMessage
// )
// .padding(.horizontal)
// .environmentObject(chat)
}
private func collectPrevMembersConnected(_ ci: ChatItem) -> [GroupMember] {
guard let prevItem = chatModel.getPrevChatItem(ci),
let memberConnected = prevItem.isMemberConnected else {
return []
}
let prevMembers = collectPrevMembersConnected(prevItem)
return [memberConnected] + prevMembers
}
private struct ChatItemWithMenu: View { private struct ChatItemWithMenu: View {
@EnvironmentObject var chat: Chat @EnvironmentObject var chat: Chat
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme

View File

@ -180,6 +180,7 @@
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; }; 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; }; 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
64EC940B2A86805D0025EAA3 /* CIMembersConnectedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EC940A2A86805D0025EAA3 /* CIMembersConnectedView.swift */; };
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; }; 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; }; D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; }; D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
@ -459,6 +460,7 @@
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; }; 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; }; 64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; }; 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
64EC940A2A86805D0025EAA3 /* CIMembersConnectedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMembersConnectedView.swift; sourceTree = "<group>"; };
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; }; 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; }; D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
@ -823,6 +825,7 @@
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */, 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */,
18415FD2E36F13F596A45BB4 /* CIVideoView.swift */, 18415FD2E36F13F596A45BB4 /* CIVideoView.swift */,
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */, 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */,
64EC940A2A86805D0025EAA3 /* CIMembersConnectedView.swift */,
); );
path = ChatItem; path = ChatItem;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1145,6 +1148,7 @@
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */, 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */,
5C58BCD6292BEBE600AF9E4F /* CIChatFeatureView.swift in Sources */, 5C58BCD6292BEBE600AF9E4F /* CIChatFeatureView.swift in Sources */,
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */, 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */,
64EC940B2A86805D0025EAA3 /* CIMembersConnectedView.swift in Sources */,
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */, 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */,
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */, 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */,
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */, 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */,

View File

@ -2009,6 +2009,17 @@ public struct ChatItem: Identifiable, Decodable {
} }
} }
public var isMemberConnected: 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
} }
@ -2052,6 +2063,7 @@ public struct ChatItem: Identifiable, Decodable {
case .sndModerated: return true case .sndModerated: return true
case .rcvModerated: return true case .rcvModerated: return true
case .invalidJSON: return false case .invalidJSON: return false
case .membersConnected: return false
} }
} }
@ -2173,6 +2185,16 @@ public struct ChatItem: Identifiable, Decodable {
) )
} }
public static func getMembersConnectedSample () -> ChatItem {
ChatItem(
chatDir: .groupRcv(groupMember: GroupMember.sampleData),
meta: CIMeta.getSample(1, .now, "group event text", .rcvRead),
content: .membersConnected(members: [GroupMember.sampleData, GroupMember.sampleData]),
quotedItem: nil,
file: nil
)
}
public static func deletedItemDummy() -> ChatItem { public static func deletedItemDummy() -> ChatItem {
ChatItem( ChatItem(
chatDir: CIDirection.directRcv, chatDir: CIDirection.directRcv,
@ -2451,6 +2473,7 @@ public enum CIContent: Decodable, ItemContent {
case sndModerated case sndModerated
case rcvModerated case rcvModerated
case invalidJSON(json: String) case invalidJSON(json: String)
case membersConnected(members: [GroupMember])
public var text: String { public var text: String {
get { get {
@ -2480,10 +2503,24 @@ public enum CIContent: Decodable, ItemContent {
case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item") case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item") case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item") case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item")
case let .membersConnected(members):
if members.count > 4 {
return String.localizedStringWithFormat(
"%@ and %d other members connected",
CIContent.membersConnectedNames(Array(members.prefix(3))),
members.count - 3
)
} else {
return String.localizedStringWithFormat("%@ members connected", CIContent.membersConnectedNames(members))
}
} }
} }
} }
static func membersConnectedNames(_ members: [GroupMember]) -> String {
members.map { $0.chatViewName }.joined(separator: ", ")
}
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String { static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String {
feature.hasParam feature.hasParam
? "\(feature.text): \(timeText(param))" ? "\(feature.text): \(timeText(param))"