diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 266f739d9e..8c8229483c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -487,7 +487,7 @@ class Profile( override val fullName: String, override val image: String? = null ): NamedChat { - val displayNameWithOptionalFullName: String + val profileViewName: String get() { return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } @@ -525,11 +525,7 @@ class GroupInfo ( override val image get() = groupProfile.image val canDelete: Boolean - get() { - val s = membership.memberStatus - return membership.memberRole == GroupMemberRole.Owner - || (s == GroupMemberStatus.MemRemoved || s == GroupMemberStatus.MemLeft || s == GroupMemberStatus.MemGroupDeleted || s == GroupMemberStatus.MemInvited) - } + get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrent val canAddMembers: Boolean get() = membership.memberRole >= GroupMemberRole.Admin && membership.memberActive @@ -610,8 +606,11 @@ class GroupMember ( GroupMemberStatus.MemCreator -> true } - fun canRemove(userRole: GroupMemberRole): Boolean = - userRole >= GroupMemberRole.Admin && userRole >= memberRole + fun canBeRemoved(membership: GroupMember): Boolean { + val userRole = membership.memberRole + return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft + && userRole >= GroupMemberRole.Admin && userRole >= memberRole && membership.memberCurrent + } companion object { val sampleData = GroupMember( @@ -1367,10 +1366,10 @@ sealed class RcvGroupEvent() { @Serializable @SerialName("groupDeleted") class GroupDeleted(): RcvGroupEvent() val text: String get() = when (this) { - is MemberAdded -> String.format(generalGetString(R.string.rcv_group_event_member_added), profile.displayNameWithOptionalFullName) + is MemberAdded -> String.format(generalGetString(R.string.rcv_group_event_member_added), profile.profileViewName) is MemberConnected -> generalGetString(R.string.rcv_group_event_member_connected) is MemberLeft -> generalGetString(R.string.rcv_group_event_member_left) - is MemberDeleted -> String.format(generalGetString(R.string.rcv_group_event_member_deleted), profile.displayNameWithOptionalFullName) + is MemberDeleted -> String.format(generalGetString(R.string.rcv_group_event_member_deleted), profile.profileViewName) is UserDeleted -> generalGetString(R.string.rcv_group_event_user_deleted) is GroupDeleted -> generalGetString(R.string.rcv_group_event_group_deleted) } @@ -1382,7 +1381,7 @@ sealed class SndGroupEvent() { @Serializable @SerialName("userLeft") class UserLeft(): SndGroupEvent() val text: String get() = when (this) { - is MemberDeleted -> String.format(generalGetString(R.string.snd_group_event_member_deleted), profile.displayNameWithOptionalFullName) + is MemberDeleted -> String.format(generalGetString(R.string.snd_group_event_member_deleted), profile.profileViewName) is UserLeft -> generalGetString(R.string.snd_group_event_user_left) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index 7448650846..3106f08aaa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -152,7 +152,7 @@ fun GroupChatInfoLayout( DeleteGroupButton(deleteGroup) } } - if (groupInfo.membership.memberStatus != GroupMemberStatus.MemLeft) { + if (groupInfo.membership.memberCurrent) { SectionDivider() SectionItemView { LeaveGroupButton(leaveGroup) @@ -190,8 +190,6 @@ fun AddMembersButton(addMembers: () -> Unit) { @Composable fun MembersList(members: List, showMemberInfo: (GroupMember) -> Unit) { - // LazyColumn { - // itemsIndexed(members) { index, member -> Column { members.forEachIndexed { index, member -> SectionItemView(height = 50.dp) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt index ae447a98d5..1864c0a3aa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt @@ -107,7 +107,7 @@ fun GroupMemberInfoLayout( } } - if (member.canRemove(userRole = groupInfo.membership.memberRole) && member.memberStatus != GroupMemberStatus.MemRemoved) { + if (member.canBeRemoved(groupInfo.membership)) { SectionView { SectionItemView { RemoveMemberButton(removeMember) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index 9d1a7a0bbc..5d60090f48 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -125,7 +125,7 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM MarkReadChatAction(chat, chatModel, showMenu) } ClearChatAction(chat, chatModel, showMenu) - if (groupInfo.membership.memberStatus != GroupMemberStatus.MemLeft) { + if (groupInfo.membership.memberCurrent) { LeaveGroupAction(groupInfo, chatModel, showMenu) } if (groupInfo.canDelete) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 20e09cbdf2..cc0d81e366 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.RemoveCircle import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -29,6 +30,28 @@ import chat.simplex.app.views.helpers.badgeLayout fun ChatPreviewView(chat: Chat, stopped: Boolean) { val cInfo = chat.chatInfo + @Composable + fun groupInactiveIcon() { + Icon( + Icons.Filled.RemoveCircle, + stringResource(R.string.icon_descr_group_inactive), + Modifier.size(18.dp).background(MaterialTheme.colors.background, CircleShape), + tint = Color.Red + ) + } + + @Composable + fun chatPreviewImageOverlayIcon() { + if (cInfo is ChatInfo.Group) { + when (cInfo.groupInfo.membership.memberStatus) { + GroupMemberStatus.MemLeft -> groupInactiveIcon() + GroupMemberStatus.MemRemoved -> groupInactiveIcon() + GroupMemberStatus.MemGroupDeleted -> groupInactiveIcon() + else -> {} + } + } + } + @Composable fun chatPreviewTitleText(color: Color = Color.Unspecified) { Text( @@ -50,18 +73,6 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { when (cInfo.groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> chatPreviewTitleText(MaterialTheme.colors.primary) GroupMemberStatus.MemAccepted -> chatPreviewTitleText(HighOrLowlight) - GroupMemberStatus.MemLeft -> - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - stringResource(R.string.group_left_description), - style = MaterialTheme.typography.h3, - fontWeight = FontWeight.Bold, - color = HighOrLowlight - ) - chatPreviewTitleText() - } else -> chatPreviewTitleText() } else -> chatPreviewTitleText() @@ -87,7 +98,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { - GroupMemberStatus.MemInvited -> Text(stringResource(R.string.you_are_invited_to_group)) + GroupMemberStatus.MemInvited -> Text(stringResource(R.string.group_preview_you_are_invited)) GroupMemberStatus.MemAccepted -> Text(stringResource(R.string.group_connection_pending), color = HighOrLowlight) else -> {} } @@ -97,7 +108,12 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { } Row { - ChatInfoImage(cInfo, size = 72.dp) + Box(contentAlignment = Alignment.BottomEnd) { + ChatInfoImage(cInfo, size = 72.dp) + Box(Modifier.padding(end = 6.dp, bottom = 6.dp)) { + chatPreviewImageOverlayIcon() + } + } Column( modifier = Modifier .padding(horizontal = 8.dp) diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 71cfe72cc5..56e7062db7 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -112,6 +112,7 @@ Этот текст можно найти в Настройках Ваши чаты соединяется… + вы приглашены в группу соединяется… @@ -506,8 +507,8 @@ Выйти Выйти из группы Вы перестанете получать сообщения от этой группы. История чата будет сохранена. - [покинута] Пригласить участников + Группа неактивна Вы отправили приглашение в группу diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index ed7c8809d4..cd294488e5 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -112,6 +112,7 @@ This text is available in settings Your chats connecting… + you are invited to group connecting… @@ -508,8 +509,8 @@ Leave Leave group? You will stop receiving messages from this group. Chat history will be preserved. - [left] Invite members + Group inactive You sent group invitation diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 40b2838817..58d66a3658 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -48,8 +48,8 @@ struct GroupMemberInfoView: View { } } - Section { - if member.canRemove(userRole: groupInfo.membership.memberRole) && member.memberStatus != .memRemoved { + if member.canBeRemoved(membership: groupInfo.membership) { + Section { removeMemberButton() } } diff --git a/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift index 46d92a6718..acfc6567db 100644 --- a/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift @@ -35,19 +35,21 @@ struct GroupChatInfoView: View { groupInfoHeader() .listRowBackground(Color.clear) - Section { - Button { - showGroupProfile = true - } label: { - Label("Edit group profile", systemImage: "pencil") + if groupInfo.canEdit { + Section { + Button { + showGroupProfile = true + } label: { + Label("Edit group profile", systemImage: "pencil") + } + } + .sheet(isPresented: $showGroupProfile) { + GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile) } - } - .sheet(isPresented: $showGroupProfile) { - GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile) } Section("\(members.count + 1) members") { - if (groupInfo.canAddMembers) { + if groupInfo.canAddMembers { addMembersButton() } memberView(groupInfo.membership, user: true) @@ -67,7 +69,7 @@ struct GroupChatInfoView: View { if groupInfo.canDelete { deleteGroupButton() } - if (groupInfo.membership.memberStatus != .memLeft) { + if groupInfo.membership.memberCurrent { leaveGroupButton() } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 08f9c7b98d..71ed4453a6 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -113,7 +113,7 @@ struct ChatListNavLink: View { clearChatButton() } .swipeActions(edge: .trailing) { - if (groupInfo.membership.memberStatus != .memLeft) { + if (groupInfo.membership.memberCurrent) { Button { AlertManager.shared.showAlert(leaveGroupAlert(groupInfo)) } label: { diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 457721cf26..9ce4ef17a0 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -18,9 +18,14 @@ struct ChatPreviewView: View { let cItem = chat.chatItems.last let unread = chat.chatStats.unreadCount return HStack(spacing: 8) { - ChatInfoImage(chat: chat) - .frame(width: 63, height: 63) - .padding(.leading, 4) + ZStack(alignment: .bottomTrailing) { + ChatInfoImage(chat: chat) + .frame(width: 63, height: 63) + chatPreviewImageOverlayIcon() + .padding([.bottom, .trailing], 1) + + } + .padding(.leading, 4) VStack(spacing: 0) { HStack(alignment: .top) { @@ -51,6 +56,28 @@ struct ChatPreviewView: View { } } + @ViewBuilder private func chatPreviewImageOverlayIcon() -> some View { + if case let .group(groupInfo) = chat.chatInfo { + switch (groupInfo.membership.memberStatus) { + case .memLeft: + groupInactiveIcon() + case .memRemoved: + groupInactiveIcon() + case .memGroupDeleted: + groupInactiveIcon() + default: EmptyView() + } + } else { + EmptyView() + } + } + + @ViewBuilder private func groupInactiveIcon() -> some View { + Image(systemName: "minus.circle.fill") + .foregroundColor(.red) + .background(Circle().foregroundColor(Color(uiColor: .systemBackground))) + } + @ViewBuilder private func chatPreviewTitle() -> some View { let v = Text(chat.chatInfo.chatViewName) .font(.title3) @@ -66,18 +93,6 @@ struct ChatPreviewView: View { v.foregroundColor(.accentColor) case .memAccepted: v.foregroundColor(.secondary) - case .memLeft: - HStack { - Text(NSLocalizedString("[left]", comment: "group left description")) - .font(.title3) - .fontWeight(.bold) - .foregroundColor(.secondary) - Text(chat.chatInfo.chatViewName) - .font(.title3) - .fontWeight(.bold) - .lineLimit(1) - } - .frame(maxHeight: .infinity, alignment: .topLeading) default: v } default: v @@ -106,12 +121,12 @@ struct ChatPreviewView: View { switch (chat.chatInfo) { case let .direct(contact): if !contact.ready { - chatPreviewInfoText("Connecting...") + chatPreviewInfoText("connecting...") } case let .group(groupInfo): switch (groupInfo.membership.memberStatus) { - case .memInvited: chatPreviewInfoText("You are invited to group") - case .memAccepted: chatPreviewInfoText("Connecting...") + case .memInvited: chatPreviewInfoText("you are invited to group") + case .memAccepted: chatPreviewInfoText("connecting...") default: EmptyView() } default: EmptyView() diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index f2e25890b1..a88e81b254 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -44,7 +44,7 @@ public struct Profile: Codable, NamedChat { public var fullName: String public var image: String? - var displayNameWithOptionalFullName: String { + var profileViewName: String { (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))" } @@ -444,9 +444,12 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { public var fullName: String { get { groupProfile.fullName } } public var image: String? { get { groupProfile.image } } + public var canEdit: Bool { + return membership.memberRole == .owner && membership.memberCurrent + } + public var canDelete: Bool { - let s = membership.memberStatus - return membership.memberRole == .owner || (s == .memRemoved || s == .memLeft || s == .memGroupDeleted || s == .memInvited) + return membership.memberRole == .owner || !membership.memberCurrent } public var canAddMembers: Bool { @@ -547,8 +550,10 @@ public struct GroupMember: Identifiable, Decodable { } } - public func canRemove(userRole: GroupMemberRole) -> Bool { - return userRole >= .admin && userRole >= memberRole + public func canBeRemoved(membership: GroupMember) -> Bool { + let userRole = membership.memberRole + return memberStatus != .memRemoved && memberStatus != .memLeft + && userRole >= .admin && userRole >= memberRole && membership.memberCurrent } public static let sampleData = GroupMember( @@ -1271,11 +1276,11 @@ public enum RcvGroupEvent: Decodable { var text: String { switch self { case let .memberAdded(_, profile): - return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.displayNameWithOptionalFullName) + return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.profileViewName) case .memberConnected: return NSLocalizedString("member connected", comment: "rcv group event chat item") case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item") case let .memberDeleted(_, profile): - return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.displayNameWithOptionalFullName) + return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName) case .userDeleted: return NSLocalizedString("removed you", comment: "rcv group event chat item") case .groupDeleted: return NSLocalizedString("deleted group", comment: "rcv group event chat item") case .groupUpdated: return NSLocalizedString("updated group profile", comment: "rcv group event chat item") @@ -1291,7 +1296,7 @@ public enum SndGroupEvent: Decodable { var text: String { switch self { case let .memberDeleted(_, profile): - return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.displayNameWithOptionalFullName) + return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.profileViewName) case .userLeft: return NSLocalizedString("you left", comment: "snd group event chat item") case .groupUpdated: return NSLocalizedString("group profile updated", comment: "snd group event chat item") } diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 4912b795ec..4524760bbc 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -441,10 +441,7 @@ processChatCommand = \case pure $ CRContactConnectionDeleted conn CTGroup -> do g@(Group gInfo@GroupInfo {membership} members) <- withStore $ \db -> getGroup db user chatId - let s = memberStatus membership - canDelete = - memberRole (membership :: GroupMember) == GROwner - || (s == GSMemRemoved || s == GSMemLeft || s == GSMemGroupDeleted || s == GSMemInvited) + let canDelete = memberRole (membership :: GroupMember) == GROwner || not (memberCurrent membership) unless canDelete $ throwChatError CEGroupUserRole withChatLock . procCmd $ do when (memberActive membership) . void $ sendGroupMessage gInfo members XGrpDel @@ -745,7 +742,7 @@ processChatCommand = \case Nothing -> throwChatError CEGroupMemberNotFound Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberProfile} -> do let userRole = memberRole (membership :: GroupMember) - canRemove = userRole >= GRAdmin && userRole >= mRole + canRemove = userRole >= GRAdmin && userRole >= mRole && memberCurrent membership unless canRemove $ throwChatError CEGroupUserRole withChatLock . procCmd $ do when (mStatus /= GSMemInvited) $ do