mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
ios: update group members in model (#915)
This commit is contained in:
@@ -37,7 +37,7 @@ import chat.simplex.app.views.chat.group.AddGroupMembersView
|
||||
import chat.simplex.app.views.chat.group.GroupChatInfoView
|
||||
import chat.simplex.app.views.chat.item.ChatItemView
|
||||
import chat.simplex.app.views.chatlist.openChat
|
||||
import chat.simplex.app.views.chatlist.populateGroupMembers
|
||||
import chat.simplex.app.views.chatlist.setGroupMembers
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
@@ -106,7 +106,7 @@ fun ChatView(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
} else if (cInfo is ChatInfo.Group) {
|
||||
populateGroupMembers(cInfo.groupInfo, chatModel)
|
||||
setGroupMembers(cInfo.groupInfo, chatModel)
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
@@ -160,7 +160,7 @@ fun ChatView(chatModel: ChatModel) {
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
withApi {
|
||||
populateGroupMembers(groupInfo, chatModel)
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.*
|
||||
import chat.simplex.app.views.chatlist.populateGroupMembers
|
||||
import chat.simplex.app.views.chatlist.setGroupMembers
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
@@ -43,7 +43,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||
developerTools,
|
||||
addMembers = {
|
||||
withApi {
|
||||
populateGroupMembers(groupInfo, chatModel)
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun populateGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
val groupMembers = chatModel.controller.apiListMembers(groupInfo.groupId)
|
||||
chatModel.groupMembers.clear()
|
||||
chatModel.groupMembers.addAll(groupMembers)
|
||||
|
||||
@@ -21,7 +21,7 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.ProfileNameField
|
||||
import chat.simplex.app.views.chat.group.AddGroupMembersView
|
||||
import chat.simplex.app.views.chatlist.populateGroupMembers
|
||||
import chat.simplex.app.views.chatlist.setGroupMembers
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.isValidDisplayName
|
||||
import chat.simplex.app.views.onboarding.ReadableText
|
||||
@@ -41,7 +41,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
|
||||
chatModel.addChat(Chat(chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatId.value = groupInfo.id
|
||||
populateGroupMembers(groupInfo, chatModel)
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
close.invoke()
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
|
||||
@@ -24,6 +24,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var chatId: String?
|
||||
@Published var chatItems: [ChatItem] = []
|
||||
@Published var chatToTop: String?
|
||||
@Published var groupMembers: [GroupMember] = []
|
||||
// items in the terminal view
|
||||
@Published var terminalItems: [TerminalItem] = []
|
||||
@Published var userAddress: String?
|
||||
@@ -291,6 +292,34 @@ final class ChatModel: ObservableObject {
|
||||
chats.removeAll(where: { $0.id == id })
|
||||
}
|
||||
}
|
||||
|
||||
func upsertGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) -> Bool {
|
||||
// update current chat
|
||||
if chatId == groupInfo.id {
|
||||
if let i = groupMembers.firstIndex(where: { $0.id == member.id }) {
|
||||
withAnimation(.default) {
|
||||
self.groupMembers[i] = member
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
withAnimation { groupMembers.append(member) }
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func removeGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) {
|
||||
// remove from current chat
|
||||
if chatId == groupInfo.id {
|
||||
if let i = groupMembers.firstIndex(where: { $0.id == member.id }) {
|
||||
_ = withAnimation {
|
||||
self.groupMembers.remove(at: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class Chat: ObservableObject, Identifiable {
|
||||
|
||||
@@ -567,20 +567,18 @@ func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo {
|
||||
throw r
|
||||
}
|
||||
|
||||
func addMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) async {
|
||||
do {
|
||||
try await apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)
|
||||
} catch let error {
|
||||
logger.error("addMember error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) async throws {
|
||||
func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws {
|
||||
let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole))
|
||||
if case .sentGroupInvitation = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
//func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember {
|
||||
// let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole))
|
||||
// if case let .sentGroupInvitation(_, _, member) = r { return member }
|
||||
// throw r
|
||||
//}
|
||||
|
||||
enum JoinGroupResult {
|
||||
case joined(groupInfo: GroupInfo)
|
||||
case invitationRemoved
|
||||
@@ -597,7 +595,7 @@ func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult {
|
||||
}
|
||||
}
|
||||
|
||||
func apiRemoveMember(groupId: Int64, memberId: Int64) async throws -> GroupMember {
|
||||
func apiRemoveMember(_ groupId: Int64, _ memberId: Int64) async throws -> GroupMember {
|
||||
let r = await chatSendCmd(.apiRemoveMember(groupId: groupId, memberId: memberId), bgTask: false)
|
||||
if case let .userDeletedMember(_, member) = r { return member }
|
||||
throw r
|
||||
@@ -820,12 +818,22 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
chatItems: []
|
||||
))
|
||||
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
|
||||
case let .joinedGroupMemberConnecting(groupInfo, _, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .deletedMemberUser(groupInfo, _): // TODO update user member
|
||||
m.updateGroup(groupInfo)
|
||||
case let .deletedMember(groupInfo, _, deletedMember):
|
||||
m.removeGroupMember(groupInfo, deletedMember)
|
||||
case let .leftMember(groupInfo, member):
|
||||
m.removeGroupMember(groupInfo, member)
|
||||
case let .groupDeleted(groupInfo, _): // TODO update user member
|
||||
m.updateGroup(groupInfo)
|
||||
case let .userJoinedGroup(groupInfo):
|
||||
m.updateGroup(groupInfo)
|
||||
case let .groupDeleted(groupInfo, _):
|
||||
m.updateGroup(groupInfo)
|
||||
case let .deletedMemberUser(groupInfo, _):
|
||||
m.updateGroup(groupInfo)
|
||||
case let .joinedGroupMember(groupInfo, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .connectedToGroupMember(groupInfo, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .groupUpdated(toGroup):
|
||||
m.updateGroup(toGroup)
|
||||
case let .rcvFileStart(aChatItem):
|
||||
|
||||
@@ -17,7 +17,6 @@ struct ChatView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@State private var showChatInfoSheet: Bool = false
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var membersToAdd: [Contact] = []
|
||||
@State private var composeState = ComposeState()
|
||||
@State private var deletingItem: ChatItem? = nil
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@@ -110,8 +109,14 @@ struct ChatView: View {
|
||||
}
|
||||
await MainActor.run { showChatInfoSheet = true }
|
||||
}
|
||||
} else {
|
||||
showChatInfoSheet = true
|
||||
} else if case let .group(groupInfo) = cInfo {
|
||||
Task {
|
||||
let groupMembers = await apiListMembers(groupInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
showChatInfoSheet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
@@ -138,7 +143,7 @@ struct ChatView: View {
|
||||
if groupInfo.canAddMembers {
|
||||
addMembersButton()
|
||||
.sheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo, membersToAdd: membersToAdd)
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
}
|
||||
default:
|
||||
@@ -161,9 +166,9 @@ struct ChatView: View {
|
||||
Button {
|
||||
if case let .group(gInfo) = chat.chatInfo {
|
||||
Task {
|
||||
let ms = await apiListMembers(gInfo.apiId)
|
||||
let groupMembers = await apiListMembers(gInfo.groupId)
|
||||
await MainActor.run {
|
||||
membersToAdd = filterMembersToAdd(ms)
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
showAddMembersSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ struct AddGroupMembersView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
var chat: Chat
|
||||
var groupInfo: GroupInfo
|
||||
var membersToAdd: [Contact]
|
||||
var showSkip: Bool = false
|
||||
var addedMembersCb: ((Set<Int64>) -> Void)? = nil
|
||||
@State private var selectedContacts = Set<Int64>()
|
||||
@@ -22,6 +21,8 @@ struct AddGroupMembersView: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
let membersToAdd = filterMembersToAdd(chatModel.groupMembers)
|
||||
|
||||
let v = List {
|
||||
ChatInfoToolbar(chat: chat, imageSize: 48)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
@@ -81,11 +82,21 @@ struct AddGroupMembersView: View {
|
||||
func inviteMembersButton() -> some View {
|
||||
Button {
|
||||
Task {
|
||||
for contactId in selectedContacts {
|
||||
await addMember(groupId: chat.chatInfo.apiId, contactId: contactId, memberRole: selectedRole)
|
||||
do {
|
||||
for contactId in selectedContacts {
|
||||
try await apiAddMember(groupInfo.groupId, contactId, selectedRole)
|
||||
// await MainActor.run { ChatModel.shared.upsertGroupMember(groupInfo, member) }
|
||||
}
|
||||
await MainActor.run { dismiss() }
|
||||
if let cb = addedMembersCb { cb(selectedContacts) }
|
||||
} catch {
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text("Error adding member(s)"),
|
||||
message: Text(responseError(error))
|
||||
)
|
||||
)
|
||||
}
|
||||
await MainActor.run { dismiss() }
|
||||
if let cb = addedMembersCb { cb(selectedContacts) }
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
@@ -132,6 +143,6 @@ struct AddGroupMembersView: View {
|
||||
|
||||
struct AddGroupMembersView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), groupInfo: GroupInfo.sampleData, membersToAdd: [])
|
||||
AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), groupInfo: GroupInfo.sampleData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ struct GroupChatInfoView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
var groupInfo: GroupInfo
|
||||
@ObservedObject private var alertManager = AlertManager.shared
|
||||
@State private var members: [GroupMember] = []
|
||||
@State private var alert: GroupChatInfoViewAlert? = nil
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var selectedMember: GroupMember? = nil
|
||||
@@ -33,6 +32,10 @@ struct GroupChatInfoView: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
let members = chatModel.groupMembers
|
||||
.filter { $0.memberStatus != .memLeft && $0.memberStatus != .memRemoved }
|
||||
.sorted { $0.displayName.lowercased() < $1.displayName.lowercased() }
|
||||
|
||||
List {
|
||||
groupInfoHeader()
|
||||
.listRowBackground(Color.clear)
|
||||
@@ -57,7 +60,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo, membersToAdd: filterMembersToAdd(members))
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
.sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats)
|
||||
@@ -96,14 +99,6 @@ struct GroupChatInfoView: View {
|
||||
case .leaveGroupAlert: return leaveGroupAlert()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
let ms = await apiListMembers(chat.chatInfo.apiId)
|
||||
.filter { $0.memberStatus != .memLeft && $0.memberStatus != .memRemoved }
|
||||
.sorted { $0.displayName.lowercased() < $1.displayName.lowercased() }
|
||||
await MainActor.run {
|
||||
members = ms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func groupInfoHeader() -> some View {
|
||||
@@ -128,7 +123,13 @@ struct GroupChatInfoView: View {
|
||||
|
||||
private func addMembersButton() -> some View {
|
||||
Button {
|
||||
showAddMembersSheet = true
|
||||
Task {
|
||||
let groupMembers = await apiListMembers(groupInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
showAddMembersSheet = true
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Invite members", systemImage: "plus")
|
||||
}
|
||||
|
||||
@@ -107,10 +107,13 @@ struct GroupMemberInfoView: View {
|
||||
primaryButton: .destructive(Text("Remove")) {
|
||||
Task {
|
||||
do {
|
||||
_ = try await apiRemoveMember(groupId: member.groupId, memberId: member.groupMemberId)
|
||||
dismiss()
|
||||
let member = try await apiRemoveMember(groupInfo.groupId, member.groupMemberId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.removeGroupMember(groupInfo, member)
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("removeMemberAlert apiRemoveMember error: \(error.localizedDescription)")
|
||||
logger.error("apiRemoveMember error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,7 +26,6 @@ struct AddGroupView: View {
|
||||
if let chat = chat, let groupInfo = groupInfo {
|
||||
AddGroupMembersView(chat: chat,
|
||||
groupInfo: groupInfo,
|
||||
membersToAdd: filterMembersToAdd([]),
|
||||
showSkip: true) { _ in
|
||||
dismiss()
|
||||
DispatchQueue.main.async {
|
||||
@@ -148,6 +147,12 @@ struct AddGroupView: View {
|
||||
hideKeyboard()
|
||||
do {
|
||||
let gInfo = try apiNewGroup(profile)
|
||||
Task {
|
||||
let groupMembers = await apiListMembers(gInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
}
|
||||
}
|
||||
let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: [])
|
||||
m.addChat(c)
|
||||
withAnimation {
|
||||
|
||||
@@ -256,6 +256,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
// group events
|
||||
case groupCreated(groupInfo: GroupInfo)
|
||||
case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact)
|
||||
// case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact, member: GroupMember)
|
||||
case userAcceptedGroupSent(groupInfo: GroupInfo)
|
||||
case userDeletedMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case leftMemberUser(groupInfo: GroupInfo)
|
||||
@@ -268,11 +269,11 @@ public enum ChatResponse: Decodable, Error {
|
||||
case leftMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case groupDeleted(groupInfo: GroupInfo, member: GroupMember)
|
||||
case contactsMerged(intoContact: Contact, mergedContact: Contact)
|
||||
case groupInvitation(groupInfo: GroupInfo)
|
||||
case groupInvitation(groupInfo: GroupInfo) // unused
|
||||
case userJoinedGroup(groupInfo: GroupInfo)
|
||||
case joinedGroupMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case connectedToGroupMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case groupRemoved(groupInfo: GroupInfo)
|
||||
case groupRemoved(groupInfo: GroupInfo) // unused
|
||||
case groupUpdated(toGroup: GroupInfo)
|
||||
// receiving file events
|
||||
case rcvFileAccepted(chatItem: AChatItem)
|
||||
|
||||
+6
-6
@@ -695,24 +695,24 @@ processChatCommand = \case
|
||||
when (userRole < GRAdmin || userRole < memRole) $ throwChatError CEGroupUserRole
|
||||
when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined gInfo)
|
||||
unless (memberActive membership) $ throwChatError CEGroupMemberNotActive
|
||||
let sendInvitation groupMemberId memberId cReq = do
|
||||
let sendInvitation member@GroupMember {groupMemberId, memberId} cReq = do
|
||||
let groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile
|
||||
msg <- sendDirectContactMessage contact $ XGrpInv groupInv
|
||||
let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
|
||||
ci <- saveSndChatItem user (CDDirectSnd contact) msg content Nothing Nothing
|
||||
toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat contact) ci
|
||||
setActive $ ActiveG localDisplayName
|
||||
pure $ CRSentGroupInvitation gInfo contact
|
||||
pure $ CRSentGroupInvitation gInfo contact member
|
||||
case contactMember contact members of
|
||||
Nothing -> do
|
||||
gVar <- asks idsDrg
|
||||
(agentConnId, cReq) <- withAgent (`createConnection` SCMInvitation)
|
||||
GroupMember {memberId, groupMemberId} <- withStore $ \db -> createContactMember db gVar user groupId contact memRole agentConnId cReq
|
||||
sendInvitation groupMemberId memberId cReq
|
||||
Just GroupMember {groupMemberId, memberId, memberStatus}
|
||||
member <- withStore $ \db -> createContactMember db gVar user groupId contact memRole agentConnId cReq
|
||||
sendInvitation member cReq
|
||||
Just member@GroupMember {groupMemberId, memberStatus}
|
||||
| memberStatus == GSMemInvited ->
|
||||
withStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case
|
||||
Just cReq -> sendInvitation groupMemberId memberId cReq
|
||||
Just cReq -> sendInvitation member cReq
|
||||
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
|
||||
| otherwise -> throwChatError $ CEGroupDuplicateMember cName
|
||||
APIJoinGroup groupId -> withUser $ \user@User {userId} -> do
|
||||
|
||||
@@ -230,7 +230,7 @@ data ChatResponse
|
||||
| CRUserAcceptedGroupSent {groupInfo :: GroupInfo}
|
||||
| CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRGroupsList {groups :: [GroupInfo]}
|
||||
| CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact}
|
||||
| CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
|
||||
| CRFileTransferStatus (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
|
||||
| CRUserProfile {profile :: Profile}
|
||||
| CRUserProfileNoChange
|
||||
|
||||
@@ -90,7 +90,7 @@ responseToView testView = \case
|
||||
CRGroupCreated g -> viewGroupCreated g
|
||||
CRGroupMembers g -> viewGroupMembers g
|
||||
CRGroupsList gs -> viewGroupsList gs
|
||||
CRSentGroupInvitation g c -> ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c]
|
||||
CRSentGroupInvitation g c _ -> ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c]
|
||||
CRFileTransferStatus ftStatus -> viewFileTransferStatus ftStatus
|
||||
CRUserProfile p -> viewUserProfile p
|
||||
CRUserProfileNoChange -> ["user profile did not change"]
|
||||
|
||||
Reference in New Issue
Block a user