ios: groups miscellaneous (#843)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
@@ -306,7 +306,7 @@ func apiContactInfo(contactId: Int64) async throws -> ConnectionStats? {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) async throws -> ConnectionStats? {
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> ConnectionStats? {
|
||||
let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberInfo(_, _, connStats_) = r { return connStats_ }
|
||||
throw r
|
||||
@@ -565,9 +565,9 @@ func apiNewGroup(_ gp: GroupProfile) throws -> GroupInfo {
|
||||
throw r
|
||||
}
|
||||
|
||||
func addMember(groupId: Int64, contactId: Int64) async {
|
||||
func addMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) async {
|
||||
do {
|
||||
try await apiAddMember(groupId: groupId, contactId: contactId, memberRole: .admin)
|
||||
try await apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)
|
||||
} catch let error {
|
||||
logger.error("addMember error: \(responseError(error))")
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9)
|
||||
let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2 )
|
||||
let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2)
|
||||
struct ChatInfoToolbar: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
@@ -11,19 +11,12 @@ import SimpleXChat
|
||||
|
||||
private let memberImageSize: CGFloat = 34
|
||||
|
||||
enum ChatViewSheet: Identifiable {
|
||||
case chatInfo
|
||||
case addMember
|
||||
|
||||
var id: ChatViewSheet { get { self } }
|
||||
}
|
||||
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
@State private var showChatViewSheet: Bool = false
|
||||
@State private var chatViewSheet: ChatViewSheet?
|
||||
@State private var showChatInfoSheet: Bool = false
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var composeState = ComposeState()
|
||||
@State private var deletingItem: ChatItem? = nil
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@@ -107,22 +100,15 @@ struct ChatView: View {
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Button {
|
||||
chatViewSheet = .chatInfo
|
||||
showChatViewSheet = true
|
||||
showChatInfoSheet = true
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.sheet(isPresented: $showChatViewSheet) {
|
||||
switch chatViewSheet {
|
||||
case .chatInfo:
|
||||
if case .direct = chat.chatInfo {
|
||||
ChatInfoView(chat: chat, showSheet: $showChatViewSheet)
|
||||
} else if case let .group(groupInfo) = chat.chatInfo {
|
||||
GroupChatInfoView(chat: chat, groupInfo: groupInfo, showSheet: $showChatViewSheet)
|
||||
}
|
||||
case .addMember:
|
||||
AddGroupMembersView(chat: chat, showSheet: $showChatViewSheet)
|
||||
default: EmptyView()
|
||||
.sheet(isPresented: $showChatInfoSheet) {
|
||||
if case .direct = chat.chatInfo {
|
||||
ChatInfoView(chat: chat, showSheet: $showChatInfoSheet)
|
||||
} else if case let .group(groupInfo) = chat.chatInfo {
|
||||
GroupChatInfoView(chat: chat, groupInfo: groupInfo, showSheet: $showChatInfoSheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,8 +118,12 @@ struct ChatView: View {
|
||||
callButton(contact, .audio, imageName: "phone")
|
||||
callButton(contact, .video, imageName: "video")
|
||||
}
|
||||
} else if case .group = chat.chatInfo {
|
||||
} else if case let .group(groupInfo) = chat.chatInfo,
|
||||
groupInfo.canAddMembers {
|
||||
addMembersButton()
|
||||
.sheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo, showSheet: $showAddMembersSheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,8 +140,7 @@ struct ChatView: View {
|
||||
|
||||
private func addMembersButton() -> some View {
|
||||
Button {
|
||||
chatViewSheet = .addMember
|
||||
showChatViewSheet = true
|
||||
showAddMembersSheet = true
|
||||
} label: {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
}
|
||||
|
||||
@@ -12,56 +12,53 @@ import SimpleXChat
|
||||
struct AddGroupMembersView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
var chat: Chat
|
||||
var groupInfo: GroupInfo
|
||||
@Binding var showSheet: Bool
|
||||
@State private var contactsToAdd: [Contact] = []
|
||||
@State private var selectedContacts = Set<Int64>()
|
||||
@State private var selectedRole: GroupMemberRole = .admin
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ChatInfoToolbar(chat: chat, imageSize: 48)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(Color(uiColor: .quaternarySystemFill))
|
||||
if (contactsToAdd.isEmpty) {
|
||||
Text("No contacts to add")
|
||||
.foregroundColor(.secondary)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
} else {
|
||||
HStack {
|
||||
let count = selectedContacts.count
|
||||
Button {
|
||||
Task {
|
||||
for contactId in selectedContacts {
|
||||
await addMember(groupId: chat.chatInfo.apiId, contactId: contactId)
|
||||
}
|
||||
showSheet = false
|
||||
}
|
||||
} label: {
|
||||
Label(
|
||||
count > 0 ? "Invite \(count) member(s)" : "Invite new members",
|
||||
systemImage: "plus")
|
||||
}
|
||||
.disabled(count < 1)
|
||||
Spacer()
|
||||
if count > 0 {
|
||||
Button {
|
||||
selectedContacts.removeAll()
|
||||
} label: {
|
||||
Label("Clear", systemImage: "multiply")
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 12)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(uiColor: .quaternarySystemFill))
|
||||
List(contactsToAdd) { contact in
|
||||
contactCheckView(contact)
|
||||
NavigationView {
|
||||
List {
|
||||
ChatInfoToolbar(chat: chat, imageSize: 48)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
if (contactsToAdd.isEmpty) {
|
||||
Text("No contacts to add")
|
||||
.foregroundColor(.secondary)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(Color.clear)
|
||||
} else {
|
||||
let count = selectedContacts.count
|
||||
Section {
|
||||
rolePicker()
|
||||
inviteMembersButton()
|
||||
.disabled(count < 1)
|
||||
} footer: {
|
||||
if (count >= 1) {
|
||||
HStack {
|
||||
Button { selectedContacts.removeAll() } label: { Text("Clear") }
|
||||
Spacer()
|
||||
Text("\(count) contact(s) selected")
|
||||
}
|
||||
} else {
|
||||
Text("No contacts selected")
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(contactsToAdd) { contact in
|
||||
contactCheckView(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.task {
|
||||
@@ -78,6 +75,33 @@ struct AddGroupMembersView: View {
|
||||
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
|
||||
}
|
||||
|
||||
func inviteMembersButton() -> some View {
|
||||
Button {
|
||||
Task {
|
||||
for contactId in selectedContacts {
|
||||
await addMember(groupId: chat.chatInfo.apiId, contactId: contactId, memberRole: selectedRole)
|
||||
}
|
||||
showSheet = false
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Invite to group")
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
|
||||
func rolePicker() -> some View {
|
||||
Picker("New member role", selection: $selectedRole) {
|
||||
ForEach(GroupMemberRole.allCases) { role in
|
||||
if role <= groupInfo.membership.memberRole {
|
||||
Text(role.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func contactCheckView(_ contact: Contact) -> some View {
|
||||
let checked = selectedContacts.contains(contact.apiId)
|
||||
return Button {
|
||||
@@ -92,10 +116,11 @@ struct AddGroupMembersView: View {
|
||||
.frame(width: 30, height: 30)
|
||||
.padding(.trailing, 2)
|
||||
Text(ChatInfo.direct(contact: contact).chatViewName)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
Image(systemName: checked ? "checkmark.circle.fill": "circle")
|
||||
.foregroundColor(checked ? .accentColor : .secondary)
|
||||
.foregroundColor(checked ? .accentColor : Color(uiColor: .tertiaryLabel))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +129,6 @@ struct AddGroupMembersView: View {
|
||||
struct AddGroupMembersView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var showSheet = true
|
||||
return AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), showSheet: $showSheet)
|
||||
return AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), groupInfo: GroupInfo.sampleData, showSheet: $showSheet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ import SimpleXChat
|
||||
|
||||
struct GroupMemberInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
var groupInfo: GroupInfo
|
||||
var member: GroupMember
|
||||
@State private var alert: GroupMemberInfoViewAlert? = nil
|
||||
@State private var alert: GroupMemberInfoViewAlert?
|
||||
@State private var connectionStats: ConnectionStats?
|
||||
|
||||
enum GroupMemberInfoViewAlert: Identifiable {
|
||||
case removeMemberAlert
|
||||
@@ -26,20 +29,34 @@ struct GroupMemberInfoView: View {
|
||||
groupMemberInfoHeader()
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
// TODO server status
|
||||
|
||||
Section(header: Text("Info")) {
|
||||
Text("Role: ") + Text(member.memberRole.text)
|
||||
Section("Member") {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
// TODO change role
|
||||
// localizedInfoRow("Role", member.memberRole.text)
|
||||
// TODO invited by - need to get contact by contact id
|
||||
Text("Status: ") + Text(member.memberStatus.text)
|
||||
if let conn = member.activeConn {
|
||||
let connLevelDesc = conn.connLevel == 0 ? "Direct" : "Indirect (\(conn.connLevel))"
|
||||
Text("Connection level: \(connLevelDesc)")
|
||||
let connLevelDesc = conn.connLevel == 0 ? "direct" : "indirect (\(conn.connLevel))"
|
||||
infoRow("Connection", connLevelDesc)
|
||||
}
|
||||
}
|
||||
|
||||
if let connStats = connectionStats {
|
||||
Section("Servers") {
|
||||
// TODO network connection status
|
||||
smpServers("receiving via", connStats.rcvServers)
|
||||
smpServers("sending via", connStats.sndServers)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
removeMemberButton()
|
||||
if member.canRemove(userRole: groupInfo.membership.memberRole) && member.memberStatus != .memRemoved {
|
||||
removeMemberButton()
|
||||
}
|
||||
}
|
||||
|
||||
Section("For console") {
|
||||
infoRow("Local name", member.localDisplayName)
|
||||
infoRow("Database ID", "\(member.groupMemberId)")
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
@@ -50,25 +67,50 @@ struct GroupMemberInfoView: View {
|
||||
case .removeMemberAlert: return removeMemberAlert()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
do {
|
||||
let stats = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
|
||||
await MainActor.run { connectionStats = stats }
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func groupMemberInfoHeader() -> some View {
|
||||
private func groupMemberInfoHeader() -> some View {
|
||||
VStack {
|
||||
ProfileImage(imageStr: member.image, color: Color(uiColor: .tertiarySystemFill))
|
||||
.frame(width: 192, height: 192)
|
||||
.padding(.top, 12)
|
||||
.padding()
|
||||
Text(member.localDisplayName)
|
||||
Text(member.displayName)
|
||||
.font(.largeTitle)
|
||||
.lineLimit(1)
|
||||
.padding(.bottom, 2)
|
||||
Text(member.fullName)
|
||||
.font(.title2)
|
||||
.lineLimit(2)
|
||||
if member.fullName != "" && member.fullName != member.displayName {
|
||||
Text(member.fullName)
|
||||
.font(.title2)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
@ViewBuilder private func smpServers(_ title: LocalizedStringKey, _ servers: [String]?) -> some View {
|
||||
if let servers = servers,
|
||||
servers.count > 0 {
|
||||
infoRow(title, serverHost(servers[0]))
|
||||
}
|
||||
}
|
||||
|
||||
private func serverHost(_ s: String) -> String {
|
||||
if let i = s.range(of: "@")?.lowerBound {
|
||||
return String(s[i...].dropFirst())
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func removeMemberButton() -> some View {
|
||||
Button(role: .destructive) {
|
||||
alert = .removeMemberAlert
|
||||
@@ -86,7 +128,7 @@ struct GroupMemberInfoView: View {
|
||||
Task {
|
||||
do {
|
||||
_ = try await apiRemoveMember(groupId: member.groupId, memberId: member.groupMemberId)
|
||||
// TODO navigate back
|
||||
dismiss()
|
||||
} catch let error {
|
||||
logger.error("removeMemberAlert apiRemoveMember error: \(error.localizedDescription)")
|
||||
}
|
||||
@@ -99,6 +141,6 @@ struct GroupMemberInfoView: View {
|
||||
|
||||
struct GroupMemberInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupMemberInfoView(member: GroupMember.sampleData)
|
||||
return GroupMemberInfoView(groupInfo: GroupInfo.sampleData, member: GroupMember.sampleData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,24 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
func infoRow(_ title: LocalizedStringKey, _ value: String) -> some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey) -> some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@@ -18,6 +36,7 @@ struct GroupChatInfoView: View {
|
||||
@State private var members: [GroupMember] = []
|
||||
@State private var alert: GroupChatInfoViewAlert? = nil
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var selectedMember: GroupMember? = nil
|
||||
|
||||
enum GroupChatInfoViewAlert: Identifiable {
|
||||
case deleteGroupAlert
|
||||
@@ -33,11 +52,20 @@ struct GroupChatInfoView: View {
|
||||
groupInfoHeader()
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
Section(header: Text("\(members.count) Members")) {
|
||||
addMembersButton()
|
||||
ForEach(members) { member in
|
||||
memberView(member)
|
||||
Section(header: Text("\(members.count + 1) members")) {
|
||||
if (groupInfo.canAddMembers) {
|
||||
addMembersButton()
|
||||
}
|
||||
memberView(groupInfo.membership, user: true)
|
||||
ForEach(members) { member in
|
||||
Button { selectedMember = member } label: { memberView(member) }
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo, showSheet: $showAddMembersSheet)
|
||||
}
|
||||
.sheet(item: $selectedMember) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member)
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -45,15 +73,19 @@ struct GroupChatInfoView: View {
|
||||
if groupInfo.canDelete {
|
||||
deleteGroupButton()
|
||||
}
|
||||
leaveGroupButton()
|
||||
if (groupInfo.membership.memberStatus != .memLeft) {
|
||||
leaveGroupButton()
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("For console")) {
|
||||
infoRow("Local name", chat.chatInfo.localDisplayName)
|
||||
infoRow("Database ID", "\(chat.chatInfo.apiId)")
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.sheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, showSheet: $showAddMembersSheet)
|
||||
}
|
||||
.alert(item: $alert) { alertItem in
|
||||
switch(alertItem) {
|
||||
case .deleteGroupAlert: return deleteGroupAlert()
|
||||
@@ -69,17 +101,20 @@ struct GroupChatInfoView: View {
|
||||
|
||||
func groupInfoHeader() -> some View {
|
||||
VStack {
|
||||
let cInfo = chat.chatInfo
|
||||
ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill))
|
||||
.frame(width: 192, height: 192)
|
||||
.padding(.top, 12)
|
||||
.padding()
|
||||
Text(chat.chatInfo.localDisplayName)
|
||||
Text(cInfo.displayName)
|
||||
.font(.largeTitle)
|
||||
.lineLimit(1)
|
||||
.padding(.bottom, 2)
|
||||
Text(chat.chatInfo.fullName)
|
||||
.font(.title2)
|
||||
.lineLimit(2)
|
||||
if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName {
|
||||
Text(cInfo.fullName)
|
||||
.font(.title2)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
@@ -98,28 +133,27 @@ struct GroupChatInfoView: View {
|
||||
.foregroundColor(status == .connected ? .green : .secondary)
|
||||
}
|
||||
|
||||
func memberView(_ member: GroupMember) -> some View {
|
||||
NavigationLink {
|
||||
GroupMemberInfoView(member: member)
|
||||
} label: {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image)
|
||||
.frame(width: 38, height: 38)
|
||||
.padding(.trailing, 2)
|
||||
// TODO server connection status
|
||||
VStack(alignment: .leading) {
|
||||
Text(member.chatViewName)
|
||||
.lineLimit(1)
|
||||
Text(member.memberStatus.shortText)
|
||||
.lineLimit(1)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
let role = member.memberRole
|
||||
if role == .owner || role == .admin {
|
||||
Text(member.memberRole.text)
|
||||
}
|
||||
func memberView(_ member: GroupMember, user: Bool = false) -> some View {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image)
|
||||
.frame(width: 38, height: 38)
|
||||
.padding(.trailing, 2)
|
||||
// TODO server connection status
|
||||
VStack(alignment: .leading) {
|
||||
Text(member.chatViewName)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.primary)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,13 @@ struct AddGroupView: View {
|
||||
m.chatId = groupInfo.id
|
||||
}
|
||||
} catch {
|
||||
fatalError("Failed to create group: \(responseError(error))")
|
||||
openedSheet = nil
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text("Failed to create group"),
|
||||
message: Text(responseError(error))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ struct NewChatButton: View {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 24, height: 24 )
|
||||
.frame(width: 24, height: 24)
|
||||
}
|
||||
.confirmationDialog("Add contact to start a new chat", isPresented: $showAddChat, titleVisibility: .visible) {
|
||||
Button("Create link / QR code") { addContactAction() }
|
||||
|
||||
@@ -500,8 +500,8 @@ public struct NetCfg: Codable {
|
||||
}
|
||||
|
||||
public struct ConnectionStats: Codable {
|
||||
var rcvServers: [String]?
|
||||
var sndServers: [String]?
|
||||
public var rcvServers: [String]?
|
||||
public var sndServers: [String]?
|
||||
}
|
||||
|
||||
public protocol SelectableItem: Hashable, Identifiable {
|
||||
|
||||
@@ -444,6 +444,10 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
return membership.memberRole == .owner || (s == .memRemoved || s == .memLeft || s == .memGroupDeleted || s == .memInvited)
|
||||
}
|
||||
|
||||
public var canAddMembers: Bool {
|
||||
return membership.memberRole >= .admin && membership.memberActive
|
||||
}
|
||||
|
||||
public static let sampleData = GroupInfo(
|
||||
groupId: 1,
|
||||
localDisplayName: "team",
|
||||
@@ -524,6 +528,10 @@ public struct GroupMember: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public func canRemove(userRole: GroupMemberRole) -> Bool {
|
||||
return userRole >= .admin && userRole >= memberRole
|
||||
}
|
||||
|
||||
public static let sampleData = GroupMember(
|
||||
groupMemberId: 1,
|
||||
groupId: 1,
|
||||
@@ -539,18 +547,32 @@ public struct GroupMember: Identifiable, Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
public enum GroupMemberRole: String, Decodable {
|
||||
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
|
||||
case member = "member"
|
||||
case admin = "admin"
|
||||
case owner = "owner"
|
||||
|
||||
public var id: Self { self }
|
||||
|
||||
public var text: LocalizedStringKey {
|
||||
switch self {
|
||||
case .member: return "Member"
|
||||
case .admin: return "Admin"
|
||||
case .owner: return "Owner"
|
||||
case .member: return "member"
|
||||
case .admin: return "admin"
|
||||
case .owner: return "owner"
|
||||
}
|
||||
}
|
||||
|
||||
private var comparisonValue: Int {
|
||||
switch self {
|
||||
case .member: return 0
|
||||
case .admin: return 1
|
||||
case .owner: return 2
|
||||
}
|
||||
}
|
||||
|
||||
public static func < (lhs: Self, rhs: Self) -> Bool {
|
||||
return lhs.comparisonValue < rhs.comparisonValue
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupMemberCategory: String, Decodable {
|
||||
@@ -576,17 +598,17 @@ public enum GroupMemberStatus: String, Decodable {
|
||||
|
||||
public var text: LocalizedStringKey {
|
||||
switch self {
|
||||
case .memRemoved: return "Removed"
|
||||
case .memLeft: return "Left"
|
||||
case .memGroupDeleted: return "Group deleted"
|
||||
case .memInvited: return "Invited"
|
||||
case .memIntroduced: return "Connecting (introduced)"
|
||||
case .memIntroInvited: return "Connecting (introduction invitation)"
|
||||
case .memAccepted: return "Connecting (accepted)"
|
||||
case .memAnnounced: return "Connecting (announced)"
|
||||
case .memConnected: return "Connected"
|
||||
case .memComplete: return "Complete"
|
||||
case .memCreator: return "Creator"
|
||||
case .memRemoved: return "removed"
|
||||
case .memLeft: return "left"
|
||||
case .memGroupDeleted: return "group deleted"
|
||||
case .memInvited: return "invited"
|
||||
case .memIntroduced: return "connecting (introduced)"
|
||||
case .memIntroInvited: return "connecting (introduction invitation)"
|
||||
case .memAccepted: return "connecting (accepted)"
|
||||
case .memAnnounced: return "connecting (announced)"
|
||||
case .memConnected: return "connected"
|
||||
case .memComplete: return "complete"
|
||||
case .memCreator: return "creator"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user