ios: group link role, add observer role (#1978)

* ios: group link role, add observer role

* prevent observers from sending in UI, clear compose state on role change
This commit is contained in:
Evgeny Poberezkin 2023-03-06 13:54:43 +00:00
parent f915eb2a20
commit be19af62d9
7 changed files with 119 additions and 47 deletions

View File

@ -545,6 +545,16 @@ final class Chat: ObservableObject, Identifiable {
self.chatStats = chatStats
}
var userCanSend: Bool {
switch chatInfo {
case .direct: return true
case let .group(groupInfo):
let m = groupInfo.membership
return m.memberActive && m.memberRole >= .member
default: return false
}
}
var id: ChatId { get { chatInfo.id } }
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }

View File

@ -868,9 +868,15 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
throw r
}
func apiCreateGroupLink(_ groupId: Int64) async throws -> String {
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId))
if case let .groupLinkCreated(_, _, connReq) = r { return connReq }
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) {
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole))
if case let .groupLinkCreated(_, _, connReq, memberRole) = r { return (connReq, memberRole) }
throw r
}
func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) {
let r = await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole))
if case let .groupLink(_, _, connReq, memberRole) = r { return (connReq, memberRole) }
throw r
}
@ -880,11 +886,11 @@ func apiDeleteGroupLink(_ groupId: Int64) async throws {
throw r
}
func apiGetGroupLink(_ groupId: Int64) throws -> String? {
func apiGetGroupLink(_ groupId: Int64) throws -> (String, GroupMemberRole)? {
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
switch r {
case let .groupLink(_, _, connReq):
return connReq
case let .groupLink(_, _, connReq, memberRole):
return (connReq, memberRole)
case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)):
return nil
default: throw r
@ -1180,6 +1186,10 @@ func processReceivedMsg(_ res: ChatResponse) async {
if active(user) {
m.updateGroup(toGroup)
}
case let .memberRole(user, groupInfo, _, _, _, _):
if active(user) {
m.updateGroup(groupInfo)
}
case let .rcvFileStart(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileComplete(user, aChatItem):

View File

@ -258,36 +258,52 @@ struct ComposeView: View {
Image(systemName: "paperclip")
.resizable()
}
.disabled(composeState.attachmentDisabled)
.disabled(composeState.attachmentDisabled || !chat.userCanSend)
.frame(width: 25, height: 25)
.padding(.bottom, 12)
.padding(.leading, 12)
SendMessageView(
composeState: $composeState,
sendMessage: {
sendMessage()
resetLinkPreview()
},
sendLiveMessage: sendLiveMessage,
updateLiveMessage: updateLiveMessage,
cancelLiveMessage: {
composeState.liveMessage = nil
chatModel.removeLiveDummy()
},
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
startVoiceMessageRecording: {
Task {
await startVoiceMessageRecording()
}
},
finishVoiceMessageRecording: finishVoiceMessageRecording,
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
onImagesAdded: { images in if !images.isEmpty { chosenImages = images }},
keyboardVisible: $keyboardVisible
)
.padding(.trailing, 12)
.background(.background)
ZStack(alignment: .leading) {
SendMessageView(
composeState: $composeState,
sendMessage: {
sendMessage()
resetLinkPreview()
},
sendLiveMessage: sendLiveMessage,
updateLiveMessage: updateLiveMessage,
cancelLiveMessage: {
composeState.liveMessage = nil
chatModel.removeLiveDummy()
},
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
startVoiceMessageRecording: {
Task {
await startVoiceMessageRecording()
}
},
finishVoiceMessageRecording: finishVoiceMessageRecording,
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
onImagesAdded: { images in if !images.isEmpty { chosenImages = images }},
keyboardVisible: $keyboardVisible
)
.padding(.trailing, 12)
.background(.background)
.disabled(!chat.userCanSend)
if (!chat.userCanSend) {
Text("you are observer")
.italic()
.foregroundColor(.secondary)
.padding(.horizontal, 12)
.onTapGesture {
AlertManager.shared.showAlertMsg(
title: "You can't send messages!",
message: "Please contact group admin."
)
}
}
}
}
}
.onChange(of: composeState.message) { _ in
@ -299,6 +315,13 @@ struct ComposeView: View {
}
}
}
.onChange(of: chat.userCanSend) { canSend in
if !canSend {
cancelCurrentVoiceRecording()
clearCurrentDraft()
clearState()
}
}
.confirmationDialog("Attach", isPresented: $showChooseSource, titleVisibility: .visible) {
Button("Take picture") {
showTakePhoto = true

View File

@ -17,6 +17,7 @@ struct GroupChatInfoView: View {
@ObservedObject private var alertManager = AlertManager.shared
@State private var alert: GroupChatInfoViewAlert? = nil
@State private var groupLink: String?
@State private var groupLinkMemberRole: GroupMemberRole = .member
@State private var showAddMembersSheet: Bool = false
@State private var connectionStats: ConnectionStats?
@State private var connectionCode: String?
@ -107,7 +108,9 @@ struct GroupChatInfoView: View {
}
.onAppear {
do {
groupLink = try apiGetGroupLink(groupInfo.groupId)
if let link = try apiGetGroupLink(groupInfo.groupId) {
(groupLink, groupLinkMemberRole) = link
}
} catch let error {
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
}
@ -187,7 +190,7 @@ struct GroupChatInfoView: View {
private func groupLinkButton() -> some View {
NavigationLink {
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink)
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink, groupLinkMemberRole: $groupLinkMemberRole)
.navigationBarTitle("Group link")
.navigationBarTitleDisplayMode(.large)
} label: {

View File

@ -12,6 +12,7 @@ import SimpleXChat
struct GroupLinkView: View {
var groupId: Int64
@Binding var groupLink: String?
@Binding var groupLinkMemberRole: GroupMemberRole
@State private var creatingLink = false
@State private var alert: GroupLinkAlert?
@ -33,6 +34,15 @@ struct GroupLinkView: View {
Text("You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.")
.padding(.bottom)
if let groupLink = groupLink {
HStack {
Text("Initial role")
Picker("Initial role", selection: $groupLinkMemberRole) {
ForEach([GroupMemberRole.member, GroupMemberRole.observer]) { role in
Text(role.text)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
QRCode(uri: groupLink)
HStack {
Button {
@ -85,6 +95,16 @@ struct GroupLinkView: View {
return Alert(title: Text(title), message: Text(error))
}
}
.onChange(of: groupLinkMemberRole) { _ in
Task {
do {
_ = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole)
} catch let error {
let a = getErrorAlert(error, "Error updating group link")
alert = .error(title: a.title, error: a.message)
}
}
}
.onAppear {
if groupLink == nil && !creatingLink {
createGroupLink()
@ -100,7 +120,7 @@ struct GroupLinkView: View {
let link = try await apiCreateGroupLink(groupId)
await MainActor.run {
creatingLink = false
groupLink = link
(groupLink, groupLinkMemberRole) = link
}
} catch let error {
logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))")
@ -120,8 +140,8 @@ struct GroupLinkView_Previews: PreviewProvider {
@State var noGroupLink: String? = nil
return Group {
GroupLinkView(groupId: 1, groupLink: $groupLink)
GroupLinkView(groupId: 1, groupLink: $noGroupLink)
GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member))
GroupLinkView(groupId: 1, groupLink: $noGroupLink, groupLinkMemberRole: Binding.constant(.member))
}
}
}

View File

@ -46,7 +46,8 @@ public enum ChatCommand {
case apiLeaveGroup(groupId: Int64)
case apiListMembers(groupId: Int64)
case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile)
case apiCreateGroupLink(groupId: Int64)
case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole)
case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole)
case apiDeleteGroupLink(groupId: Int64)
case apiGetGroupLink(groupId: Int64)
case apiGetUserSMPServers(userId: Int64)
@ -134,7 +135,8 @@ public enum ChatCommand {
case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)"
case let .apiListMembers(groupId): return "/_members #\(groupId)"
case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))"
case let .apiCreateGroupLink(groupId): return "/_create link #\(groupId)"
case let .apiCreateGroupLink(groupId, memberRole): return "/_create link #\(groupId) \(memberRole)"
case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)"
case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
case let .apiGetUserSMPServers(userId): return "/_smp \(userId)"
@ -228,6 +230,7 @@ public enum ChatCommand {
case .apiListMembers: return "apiListMembers"
case .apiUpdateGroupProfile: return "apiUpdateGroupProfile"
case .apiCreateGroupLink: return "apiCreateGroupLink"
case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole"
case .apiDeleteGroupLink: return "apiDeleteGroupLink"
case .apiGetGroupLink: return "apiGetGroupLink"
case .apiGetUserSMPServers: return "apiGetUserSMPServers"
@ -391,8 +394,8 @@ public enum ChatResponse: Decodable, Error {
case connectedToGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
case groupRemoved(user: User, groupInfo: GroupInfo) // unused
case groupUpdated(user: User, toGroup: GroupInfo)
case groupLinkCreated(user: User, groupInfo: GroupInfo, connReqContact: String)
case groupLink(user: User, groupInfo: GroupInfo, connReqContact: String)
case groupLinkCreated(user: User, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole)
case groupLink(user: User, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole)
case groupLinkDeleted(user: User, groupInfo: GroupInfo)
// receiving file events
case rcvFileAccepted(user: User, chatItem: AChatItem)
@ -606,8 +609,8 @@ public enum ChatResponse: Decodable, Error {
case let .connectedToGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
case let .groupRemoved(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
case let .groupLinkCreated(u, groupInfo, connReqContact): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)")
case let .groupLink(u, groupInfo, connReqContact): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)")
case let .groupLinkCreated(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)")
case let .groupLink(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)")
case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
case .rcvFileAcceptedSndCancelled: return noDetails

View File

@ -1546,6 +1546,7 @@ public struct GroupMemberRef: Decodable {
}
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
case observer = "observer"
case member = "member"
case admin = "admin"
case owner = "owner"
@ -1554,6 +1555,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec
public var text: String {
switch self {
case .observer: return NSLocalizedString("observer", comment: "member role")
case .member: return NSLocalizedString("member", comment: "member role")
case .admin: return NSLocalizedString("admin", comment: "member role")
case .owner: return NSLocalizedString("owner", comment: "member role")
@ -1562,9 +1564,10 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec
private var comparisonValue: Int {
switch self {
case .member: return 0
case .admin: return 1
case .owner: return 2
case .observer: return 0
case .member: return 1
case .admin: return 2
case .owner: return 3
}
}