ios: update group members in model (#915)

This commit is contained in:
JRoberts
2022-08-09 13:43:19 +04:00
committed by GitHub
parent 7531791f1b
commit 26a019d9d2
15 changed files with 122 additions and 59 deletions
@@ -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,
@@ -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,
@@ -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(
+29
View File
@@ -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 {
+22 -14
View File
@@ -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):
+11 -6
View File
@@ -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 {
+3 -2
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"]