2022-07-14 16:40:32 +04:00
//
// G r o u p C h a t I n f o V i e w . s w i f t
// S i m p l e X ( i O S )
//
// C r e a t e d b y J R o b e r t s o n 1 4 . 0 7 . 2 0 2 2 .
// C o p y r i g h t © 2 0 2 2 S i m p l e X C h a t . A l l r i g h t s r e s e r v e d .
//
import SwiftUI
import SimpleXChat
2023-07-28 13:16:52 +04:00
let SMALL_GROUPS_RCPS_MEM_LIMIT : Int = 20
2022-07-14 16:40:32 +04:00
struct GroupChatInfoView : View {
@ EnvironmentObject var chatModel : ChatModel
2022-07-30 13:03:44 +01:00
@ Environment ( \ . dismiss ) var dismiss : DismissAction
2022-07-14 16:40:32 +04:00
@ ObservedObject var chat : Chat
2023-10-31 09:44:57 +00:00
@ Binding var groupInfo : GroupInfo
2022-07-30 13:03:44 +01:00
@ ObservedObject private var alertManager = AlertManager . shared
2022-07-26 12:33:10 +04:00
@ State private var alert : GroupChatInfoViewAlert ? = nil
2022-10-15 18:09:25 +04:00
@ State private var groupLink : String ?
2023-03-06 13:54:43 +00:00
@ State private var groupLinkMemberRole : GroupMemberRole = . member
2022-07-26 12:33:10 +04:00
@ State private var showAddMembersSheet : Bool = false
2022-08-02 14:48:31 +04:00
@ State private var connectionStats : ConnectionStats ?
2022-12-12 08:59:35 +00:00
@ State private var connectionCode : String ?
2023-07-28 13:16:52 +04:00
@ State private var sendReceipts = SendReceipts . userDefault ( true )
@ State private var sendReceiptsUserDefault = true
2022-08-02 17:00:12 +04:00
@ AppStorage ( DEFAULT_DEVELOPER_TOOLS ) private var developerTools = false
2023-06-19 11:49:45 +01:00
@ State private var searchText : String = " "
@ FocusState private var searchFocussed
2022-07-14 16:40:32 +04:00
2022-07-26 12:33:10 +04:00
enum GroupChatInfoViewAlert : Identifiable {
case deleteGroupAlert
2022-07-14 16:40:32 +04:00
case clearChatAlert
2022-07-26 12:33:10 +04:00
case leaveGroupAlert
2022-08-29 14:47:29 +04:00
case cantInviteIncognitoAlert
2023-07-28 13:16:52 +04:00
case largeGroupReceiptsDisabled
2023-10-31 09:44:57 +00:00
case blockMemberAlert ( mem : GroupMember )
case unblockMemberAlert ( mem : GroupMember )
case removeMemberAlert ( mem : GroupMember )
case error ( title : LocalizedStringKey , error : LocalizedStringKey )
var id : String {
switch self {
case . deleteGroupAlert : return " deleteGroupAlert "
case . clearChatAlert : return " clearChatAlert "
case . leaveGroupAlert : return " leaveGroupAlert "
case . cantInviteIncognitoAlert : return " cantInviteIncognitoAlert "
case . largeGroupReceiptsDisabled : return " largeGroupReceiptsDisabled "
case let . blockMemberAlert ( mem ) : return " blockMemberAlert \( mem . groupMemberId ) "
case let . unblockMemberAlert ( mem ) : return " unblockMemberAlert \( mem . groupMemberId ) "
case let . removeMemberAlert ( mem ) : return " removeMemberAlert \( mem . groupMemberId ) "
case let . error ( title , _ ) : return " error \( title ) "
}
}
2022-07-14 16:40:32 +04:00
}
var body : some View {
2022-07-26 12:33:10 +04:00
NavigationView {
2022-08-09 13:43:19 +04:00
let members = chatModel . groupMembers
2023-10-31 09:44:57 +00:00
. filter { m in let status = m . wrapped . memberStatus ; return status != . memLeft && status != . memRemoved }
2022-08-09 13:43:19 +04:00
. sorted { $0 . displayName . lowercased ( ) < $1 . displayName . lowercased ( ) }
2022-07-26 12:33:10 +04:00
List {
groupInfoHeader ( )
. listRowBackground ( Color . clear )
2022-11-16 20:26:43 +04:00
Section {
2022-11-17 12:59:13 +04:00
if groupInfo . canEdit {
editGroupButton ( )
2022-11-16 20:26:43 +04:00
}
2023-05-03 15:57:10 +03:00
if groupInfo . groupProfile . description != nil || groupInfo . canEdit {
addOrEditWelcomeMessage ( )
}
2022-12-04 11:30:51 +00:00
groupPreferencesButton ( $ groupInfo )
2023-10-31 09:44:57 +00:00
if members . filter ( { $0 . wrapped . memberCurrent } ) . count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
2023-07-28 13:16:52 +04:00
sendReceiptsOption ( )
} else {
sendReceiptsOptionDisabled ( )
}
2022-11-16 20:26:43 +04:00
} header : {
2022-11-17 12:59:13 +04:00
Text ( " " )
2022-11-16 20:26:43 +04:00
} footer : {
Text ( " Only group owners can change group preferences. " )
}
2022-07-30 13:03:44 +01:00
Section ( " \( members . count + 1 ) members " ) {
2022-07-30 16:49:34 +04:00
if groupInfo . canAddMembers {
2022-10-15 18:09:25 +04:00
groupLinkButton ( )
2022-08-29 14:47:29 +04:00
if ( chat . chatInfo . incognito ) {
Label ( " Invite members " , systemImage : " plus " )
. foregroundColor ( Color ( uiColor : . tertiaryLabel ) )
. onTapGesture { alert = . cantInviteIncognitoAlert }
} else {
addMembersButton ( )
}
2022-07-27 11:16:07 +04:00
}
2023-06-19 11:49:45 +01:00
if members . count > 8 {
searchFieldView ( text : $ searchText , focussed : $ searchFocussed )
. padding ( . leading , 8 )
}
let s = searchText . trimmingCharacters ( in : . whitespaces ) . localizedLowercase
2023-10-31 09:44:57 +00:00
let filteredMembers = s = = " " ? members : members . filter { $0 . wrapped . chatViewName . localizedLowercase . contains ( s ) }
MemberRowView ( groupInfo : groupInfo , groupMember : GMember ( groupInfo . membership ) , user : true , alert : $ alert )
2023-06-19 11:49:45 +01:00
ForEach ( filteredMembers ) { member in
2022-12-26 17:45:02 +04:00
ZStack {
NavigationLink {
2023-10-31 09:44:57 +00:00
memberInfoView ( member )
2022-12-26 17:45:02 +04:00
} label : {
EmptyView ( )
}
. opacity ( 0 )
2023-10-31 09:44:57 +00:00
MemberRowView ( groupInfo : groupInfo , groupMember : member , alert : $ alert )
2022-12-26 17:45:02 +04:00
}
2022-07-26 12:33:10 +04:00
}
2022-07-27 11:16:07 +04:00
}
2022-07-26 12:33:10 +04:00
Section {
clearChatButton ( )
if groupInfo . canDelete {
deleteGroupButton ( )
}
2022-07-30 16:49:34 +04:00
if groupInfo . membership . memberCurrent {
2022-07-27 11:16:07 +04:00
leaveGroupButton ( )
}
}
2022-08-02 17:00:12 +04:00
if developerTools {
Section ( header : Text ( " For console " ) ) {
infoRow ( " Local name " , chat . chatInfo . localDisplayName )
infoRow ( " Database ID " , " \( chat . chatInfo . apiId ) " )
}
2022-07-26 12:33:10 +04:00
}
2022-07-14 16:40:32 +04:00
}
2022-07-26 12:33:10 +04:00
. navigationBarHidden ( true )
}
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : . top )
2022-07-14 16:40:32 +04:00
. alert ( item : $ alert ) { alertItem in
switch ( alertItem ) {
2022-07-26 12:33:10 +04:00
case . deleteGroupAlert : return deleteGroupAlert ( )
2022-07-14 16:40:32 +04:00
case . clearChatAlert : return clearChatAlert ( )
2022-07-26 12:33:10 +04:00
case . leaveGroupAlert : return leaveGroupAlert ( )
2022-08-29 14:47:29 +04:00
case . cantInviteIncognitoAlert : return cantInviteIncognitoAlert ( )
2023-07-28 13:16:52 +04:00
case . largeGroupReceiptsDisabled : return largeGroupReceiptsDisabledAlert ( )
2023-10-31 09:44:57 +00:00
case let . blockMemberAlert ( mem ) : return blockMemberAlert ( groupInfo , mem )
case let . unblockMemberAlert ( mem ) : return unblockMemberAlert ( groupInfo , mem )
case let . removeMemberAlert ( mem ) : return removeMemberAlert ( mem )
case let . error ( title , error ) : return Alert ( title : Text ( title ) , message : Text ( error ) )
2022-10-15 18:09:25 +04:00
}
}
. onAppear {
2023-07-28 13:16:52 +04:00
if let currentUser = chatModel . currentUser {
sendReceiptsUserDefault = currentUser . sendRcptsSmallGroups
}
sendReceipts = SendReceipts . fromBool ( groupInfo . chatSettings . sendRcpts , userDefault : sendReceiptsUserDefault )
2022-10-15 18:09:25 +04:00
do {
2023-03-06 13:54:43 +00:00
if let link = try apiGetGroupLink ( groupInfo . groupId ) {
( groupLink , groupLinkMemberRole ) = link
}
2022-10-15 18:09:25 +04:00
} catch let error {
logger . error ( " GroupChatInfoView apiGetGroupLink: \( responseError ( error ) ) " )
2022-07-14 16:40:32 +04:00
}
}
2023-07-10 13:53:46 +01:00
. keyboardPadding ( )
2022-07-26 12:33:10 +04:00
}
2022-12-12 08:59:35 +00:00
private func groupInfoHeader ( ) -> some View {
2022-07-26 12:33:10 +04:00
VStack {
2022-07-27 11:16:07 +04:00
let cInfo = chat . chatInfo
2022-07-26 12:33:10 +04:00
ChatInfoImage ( chat : chat , color : Color ( uiColor : . tertiarySystemFill ) )
. frame ( width : 192 , height : 192 )
. padding ( . top , 12 )
. padding ( )
2022-07-27 11:16:07 +04:00
Text ( cInfo . displayName )
2022-07-26 12:33:10 +04:00
. font ( . largeTitle )
2023-07-19 15:16:50 +04:00
. multilineTextAlignment ( . center )
. lineLimit ( 4 )
2022-07-26 12:33:10 +04:00
. padding ( . bottom , 2 )
2022-07-27 11:16:07 +04:00
if cInfo . fullName != " " && cInfo . fullName != cInfo . displayName {
Text ( cInfo . fullName )
. font ( . title2 )
2023-07-19 15:16:50 +04:00
. multilineTextAlignment ( . center )
. lineLimit ( 8 )
2022-07-27 11:16:07 +04:00
}
2022-07-26 12:33:10 +04:00
}
. frame ( maxWidth : . infinity , alignment : . center )
}
private func addMembersButton ( ) -> some View {
2022-12-12 08:59:35 +00:00
NavigationLink {
AddGroupMembersView ( chat : chat , groupInfo : groupInfo )
. onAppear {
2023-06-19 11:49:45 +01:00
searchFocussed = false
2023-05-03 12:17:39 +04:00
Task {
let groupMembers = await apiListMembers ( groupInfo . groupId )
await MainActor . run {
2023-10-31 09:44:57 +00:00
chatModel . groupMembers = groupMembers . map { GMember . init ( $0 ) }
2023-05-03 12:17:39 +04:00
}
}
2022-08-09 13:43:19 +04:00
}
2022-07-26 12:33:10 +04:00
} label : {
Label ( " Invite members " , systemImage : " plus " )
}
2022-07-14 16:40:32 +04:00
}
2023-10-31 09:44:57 +00:00
private struct MemberRowView : View {
var groupInfo : GroupInfo
@ ObservedObject var groupMember : GMember
var user : Bool = false
@ Binding var alert : GroupChatInfoViewAlert ?
var body : some View {
let member = groupMember . wrapped
let v = HStack {
ProfileImage ( imageStr : member . image )
. frame ( width : 38 , height : 38 )
. padding ( . trailing , 2 )
// T O D O s e r v e r c o n n e c t i o n s t a t u s
VStack ( alignment : . leading ) {
let t = Text ( member . chatViewName ) . foregroundColor ( member . memberIncognito ? . indigo : . primary )
( member . verified ? memberVerifiedShield + t : t )
. lineLimit ( 1 )
let s = Text ( member . memberStatus . shortText )
( user ? Text ( " you: " ) + s : s )
. lineLimit ( 1 )
. font ( . caption )
. foregroundColor ( . secondary )
}
Spacer ( )
let role = member . memberRole
if role = = . owner || role = = . admin {
Text ( member . memberRole . text )
. foregroundColor ( . secondary )
}
2022-07-27 11:16:07 +04:00
}
2023-10-31 09:44:57 +00:00
if user {
v
} else if member . canBeRemoved ( groupInfo : groupInfo ) {
removeSwipe ( member , blockSwipe ( member , v ) )
} else {
blockSwipe ( member , v )
2022-07-26 12:33:10 +04:00
}
}
2023-10-31 09:44:57 +00:00
private func blockSwipe < V : View > ( _ member : GroupMember , _ v : V ) -> some View {
v . swipeActions ( edge : . leading ) {
if member . memberSettings . showMessages {
Button {
alert = . blockMemberAlert ( mem : member )
} label : {
Label ( " Block member " , systemImage : " hand.raised " ) . foregroundColor ( . secondary )
}
} else {
Button {
alert = . unblockMemberAlert ( mem : member )
} label : {
Label ( " Unblock member " , systemImage : " hand.raised.slash " ) . foregroundColor ( . accentColor )
}
}
}
}
2022-12-12 08:59:35 +00:00
2023-10-31 09:44:57 +00:00
private func removeSwipe < V : View > ( _ member : GroupMember , _ v : V ) -> some View {
v . swipeActions ( edge : . trailing ) {
Button ( role : . destructive ) {
alert = . removeMemberAlert ( mem : member )
} label : {
Label ( " Remove member " , systemImage : " trash " )
. foregroundColor ( Color . red )
}
}
2022-12-12 08:59:35 +00:00
}
}
2023-10-31 09:44:57 +00:00
private func memberInfoView ( _ groupMember : GMember ) -> some View {
GroupMemberInfoView ( groupInfo : groupInfo , groupMember : groupMember )
. navigationBarHidden ( false )
}
2022-10-15 18:09:25 +04:00
private func groupLinkButton ( ) -> some View {
NavigationLink {
2023-10-26 18:51:45 +04:00
GroupLinkView (
groupId : groupInfo . groupId ,
groupLink : $ groupLink ,
groupLinkMemberRole : $ groupLinkMemberRole ,
showTitle : false ,
creatingGroup : false
)
. navigationBarTitle ( " Group link " )
. navigationBarTitleDisplayMode ( . large )
2022-10-15 18:09:25 +04:00
} label : {
2022-12-13 17:15:45 +00:00
if groupLink = = nil {
Label ( " Create group link " , systemImage : " link.badge.plus " )
} else {
Label ( " Group link " , systemImage : " link " )
}
2022-10-15 18:09:25 +04:00
}
}
2022-12-12 08:59:35 +00:00
private func editGroupButton ( ) -> some View {
2022-11-17 12:59:13 +04:00
NavigationLink {
GroupProfileView (
groupInfo : $ groupInfo ,
groupProfile : groupInfo . groupProfile
)
. navigationBarTitle ( " Group profile " )
. navigationBarTitleDisplayMode ( . large )
2022-07-30 18:46:10 +01:00
} label : {
Label ( " Edit group profile " , systemImage : " pencil " )
}
}
2023-03-21 18:15:48 +03:00
private func addOrEditWelcomeMessage ( ) -> some View {
NavigationLink {
GroupWelcomeView ( groupId : groupInfo . groupId , groupInfo : $ groupInfo )
. navigationTitle ( " Welcome message " )
. navigationBarTitleDisplayMode ( . large )
} label : {
groupInfo . groupProfile . description = = nil
2023-03-22 17:45:55 +00:00
? Label ( " Add welcome message " , systemImage : " plus.message " )
: Label ( " Welcome message " , systemImage : " message " )
2023-03-21 18:15:48 +03:00
}
}
2022-12-12 08:59:35 +00:00
private func deleteGroupButton ( ) -> some View {
2022-07-26 12:33:10 +04:00
Button ( role : . destructive ) {
alert = . deleteGroupAlert
} label : {
Label ( " Delete group " , systemImage : " trash " )
. foregroundColor ( Color . red )
}
}
2022-12-12 08:59:35 +00:00
private func clearChatButton ( ) -> some View {
2022-07-26 12:33:10 +04:00
Button ( ) {
alert = . clearChatAlert
} label : {
Label ( " Clear conversation " , systemImage : " gobackward " )
. foregroundColor ( Color . orange )
}
}
2022-12-12 08:59:35 +00:00
private func leaveGroupButton ( ) -> some View {
2022-07-26 12:33:10 +04:00
Button ( role : . destructive ) {
alert = . leaveGroupAlert
} label : {
Label ( " Leave group " , systemImage : " rectangle.portrait.and.arrow.right " )
. foregroundColor ( Color . red )
}
}
// T O D O r e u s e t h i s a n d c l e a r C h a t A l e r t w i t h C h a t I n f o V i e w
private func deleteGroupAlert ( ) -> Alert {
2022-09-14 21:45:59 +04:00
return Alert (
2022-07-26 12:33:10 +04:00
title : Text ( " Delete group? " ) ,
2022-09-14 21:45:59 +04:00
message : deleteGroupAlertMessage ( ) ,
2022-07-14 16:40:32 +04:00
primaryButton : . destructive ( Text ( " Delete " ) ) {
Task {
do {
2022-07-26 12:33:10 +04:00
try await apiDeleteChat ( type : chat . chatInfo . chatType , id : chat . chatInfo . apiId )
await MainActor . run {
2022-07-30 13:03:44 +01:00
dismiss ( )
2023-09-22 12:23:53 +04:00
chatModel . chatId = nil
chatModel . removeChat ( chat . chatInfo . id )
2022-07-14 16:40:32 +04:00
}
} catch let error {
2022-07-26 12:33:10 +04:00
logger . error ( " deleteGroupAlert apiDeleteChat error: \( error . localizedDescription ) " )
2022-07-14 16:40:32 +04:00
}
}
} ,
secondaryButton : . cancel ( )
)
}
2022-09-14 21:45:59 +04:00
private func deleteGroupAlertMessage ( ) -> Text {
groupInfo . membership . memberCurrent ? Text ( " Group will be deleted for all members - this cannot be undone! " ) : Text ( " Group will be deleted for you - this cannot be undone! " )
}
2022-07-14 16:40:32 +04:00
private func clearChatAlert ( ) -> Alert {
Alert (
title : Text ( " Clear conversation? " ) ,
message : Text ( " All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. " ) ,
primaryButton : . destructive ( Text ( " Clear " ) ) {
Task {
await clearChat ( chat )
2022-07-30 13:03:44 +01:00
await MainActor . run { dismiss ( ) }
2022-07-26 12:33:10 +04:00
}
} ,
secondaryButton : . cancel ( )
)
}
private func leaveGroupAlert ( ) -> Alert {
Alert (
title : Text ( " Leave group? " ) ,
message : Text ( " You will stop receiving messages from this group. Chat history will be preserved. " ) ,
primaryButton : . destructive ( Text ( " Leave " ) ) {
Task {
await leaveGroup ( chat . chatInfo . apiId )
2022-07-30 13:03:44 +01:00
await MainActor . run { dismiss ( ) }
2022-07-14 16:40:32 +04:00
}
} ,
secondaryButton : . cancel ( )
)
}
2023-07-28 13:16:52 +04:00
private func sendReceiptsOption ( ) -> some View {
Picker ( selection : $ sendReceipts ) {
ForEach ( [ . yes , . no , . userDefault ( sendReceiptsUserDefault ) ] ) { ( opt : SendReceipts ) in
Text ( opt . text )
}
} label : {
Label ( " Send receipts " , systemImage : " checkmark.message " )
}
. frame ( height : 36 )
. onChange ( of : sendReceipts ) { _ in
setSendReceipts ( )
}
}
private func setSendReceipts ( ) {
var chatSettings = chat . chatInfo . chatSettings ? ? ChatSettings . defaults
chatSettings . sendRcpts = sendReceipts . bool ( )
updateChatSettings ( chat , chatSettings : chatSettings )
}
private func sendReceiptsOptionDisabled ( ) -> some View {
HStack {
Label ( " Send receipts " , systemImage : " checkmark.message " )
Spacer ( )
Text ( " disabled " )
. foregroundStyle ( . secondary )
}
. onTapGesture {
alert = . largeGroupReceiptsDisabled
}
}
2023-10-31 09:44:57 +00:00
private func removeMemberAlert ( _ mem : GroupMember ) -> Alert {
Alert (
title : Text ( " Remove member? " ) ,
message : Text ( " Member will be removed from group - this cannot be undone! " ) ,
primaryButton : . destructive ( Text ( " Remove " ) ) {
Task {
do {
let updatedMember = try await apiRemoveMember ( groupInfo . groupId , mem . groupMemberId )
await MainActor . run {
_ = chatModel . upsertGroupMember ( groupInfo , updatedMember )
}
} catch let error {
logger . error ( " apiRemoveMember error: \( responseError ( error ) ) " )
let a = getErrorAlert ( error , " Error removing member " )
alert = . error ( title : a . title , error : a . message )
}
}
} ,
secondaryButton : . cancel ( )
)
}
2022-07-14 16:40:32 +04:00
}
2022-12-04 11:30:51 +00:00
func groupPreferencesButton ( _ groupInfo : Binding < GroupInfo > , _ creatingGroup : Bool = false ) -> some View {
NavigationLink {
GroupPreferencesView (
groupInfo : groupInfo ,
preferences : groupInfo . wrappedValue . fullGroupPreferences ,
currentPreferences : groupInfo . wrappedValue . fullGroupPreferences ,
creatingGroup : creatingGroup
)
. navigationBarTitle ( " Group preferences " )
. navigationBarTitleDisplayMode ( . large )
} label : {
if creatingGroup {
Text ( " Set group preferences " )
} else {
Label ( " Group preferences " , systemImage : " switch.2 " )
}
}
}
2023-10-31 09:44:57 +00:00
private var memberVerifiedShield : Text {
( Text ( Image ( systemName : " checkmark.shield " ) ) + Text ( " " ) )
. font ( . caption )
. baselineOffset ( 2 )
. kerning ( - 2 )
. foregroundColor ( . secondary )
}
2022-08-29 14:47:29 +04:00
func cantInviteIncognitoAlert ( ) -> Alert {
Alert (
title : Text ( " Can't invite contacts! " ) ,
message : Text ( " You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed " )
)
}
2023-07-28 13:16:52 +04:00
func largeGroupReceiptsDisabledAlert ( ) -> Alert {
Alert (
title : Text ( " Receipts are disabled " ) ,
message : Text ( " This group has over \( SMALL_GROUPS_RCPS_MEM_LIMIT ) members, delivery receipts are not sent. " )
)
}
2022-07-14 16:40:32 +04:00
struct GroupChatInfoView_Previews : PreviewProvider {
static var previews : some View {
2023-10-31 09:44:57 +00:00
GroupChatInfoView (
chat : Chat ( chatInfo : ChatInfo . sampleData . group , chatItems : [ ] ) ,
groupInfo : Binding . constant ( GroupInfo . sampleData )
)
2022-07-14 16:40:32 +04:00
}
}