Files
2026-05-08 08:37:55 +00:00

176 lines
6.5 KiB
Swift

//
// ChannelRelaysView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 20.02.2026.
// Copyright © 2026 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ChannelRelaysView: View {
@ObservedObject var chat: Chat
var groupInfo: GroupInfo
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject private var channelRelaysModel = ChannelRelaysModel.shared
@State private var showAddRelay = false
private var groupRelays: [GroupRelay] {
channelRelaysModel.groupId == groupInfo.groupId ? channelRelaysModel.groupRelays : []
}
var body: some View {
List {
relaysList()
// TODO [relays] re-enable when relay management ships
// if groupInfo.isOwner {
// Section {
// Button {
// showAddRelay = true
// } label: {
// Label("Add relay", systemImage: "plus")
// }
// }
// }
}
// TODO [relays] re-enable when relay management ships
// .sheet(isPresented: $showAddRelay) {
// let existingRelayIds = Set(groupRelays.filter { $0.relayStatus != .rsInactive }.compactMap { $0.userChatRelay.chatRelayId })
// AddGroupRelayView(groupInfo: groupInfo, existingRelayIds: existingRelayIds) {
// Task { await chatModel.loadGroupMembers(groupInfo) }
// }
// }
.onAppear {
Task {
await chatModel.loadGroupMembers(groupInfo)
if groupInfo.isOwner {
let relays = await apiGetGroupRelays(groupInfo.groupId)
await MainActor.run {
ChannelRelaysModel.shared.set(groupId: groupInfo.groupId, groupRelays: relays)
}
}
}
}
}
@ViewBuilder private func relaysList() -> some View {
let relayMembers = chatModel.groupMembers.filter { $0.wrapped.memberRole == .relay && $0.wrapped.memberStatus != .memRemoved && $0.wrapped.memberStatus != .memGroupDeleted }
if relayMembers.isEmpty {
Section {
Text("No chat relays")
.foregroundColor(theme.colors.secondary)
}
} else {
Section {
ForEach(relayMembers) { member in
let link = NavigationLink {
GroupMemberInfoView(
groupInfo: groupInfo,
chat: chat,
groupMember: member,
scrollToItemId: Binding.constant(nil),
groupRelay: groupRelays.first(where: { $0.groupMemberId == member.wrapped.groupMemberId })
)
.navigationBarHidden(false)
} label: {
let statusText = groupInfo.isOwner
? ownerRelayStatusText(member.wrapped)
: subscriberRelayStatusText(member.wrapped)
relayMemberRow(member.wrapped, statusText: statusText)
}
// TODO [relays] re-enable when relay management ships
// if groupInfo.isOwner && member.wrapped.canBeRemoved(groupInfo: groupInfo) {
// link.swipeActions(edge: .trailing) {
// Button {
// showRemoveMemberAlert(groupInfo, member.wrapped)
// } label: {
// Label("Remove relay", systemImage: "trash")
// }
// .tint(.red)
// }
// } else {
// link
// }
link
}
} footer: {
Text("Chat relays forward messages to channel subscribers.")
}
}
}
private func subscriberRelayStatusText(_ member: GroupMember) -> LocalizedStringKey {
if member.activeConn?.connDisabled ?? false {
"disabled"
} else if member.activeConn?.connInactive ?? false {
"inactive"
} else {
relayConnStatus(member).text
}
}
private func ownerRelayStatusText(_ member: GroupMember) -> LocalizedStringKey {
if [.memLeft, .memRemoved, .memGroupDeleted].contains(member.memberStatus) {
relayConnStatus(member).text
} else if case .failed = member.activeConn?.connStatus {
"failed"
} else if member.activeConn?.connDisabled ?? false {
"disabled"
} else if member.activeConn?.connInactive ?? false {
"inactive"
} else {
groupRelays.first(where: { $0.groupMemberId == member.groupMemberId })?.relayStatus.text
?? relayConnStatus(member).text
}
}
private func relayMemberRow(_ member: GroupMember, statusText: LocalizedStringKey) -> some View {
HStack {
MemberProfileImage(member, size: 38)
.padding(.trailing, 2)
VStack(alignment: .leading) {
Text(member.chatViewName)
.foregroundColor(theme.colors.onBackground)
.lineLimit(1)
Text(statusText)
.lineLimit(1)
.font(.caption)
.foregroundColor(theme.colors.secondary)
}
Spacer()
}
}
}
func relayConnStatus(_ member: GroupMember) -> (text: LocalizedStringKey, color: Color) {
switch member.memberStatus {
case .memLeft: ("removed by operator", .red)
case .memRemoved, .memGroupDeleted: (member.memberStatus.text, .red)
default:
switch member.activeConn?.connStatus {
case .ready: ("connected", .green)
case .deleted: ("deleted", .red)
case .failed: ("failed", .red)
default: ("connecting", .yellow)
}
}
}
func hostFromRelayLink(_ link: String) -> String {
if let ft = parseSimpleXMarkdown(link) {
for f in ft {
if case let .simplexLink(_, _, _, smpHosts) = f.format,
let host = smpHosts.first {
return host
}
}
}
return link
}
#Preview {
ChannelRelaysView(chat: Chat.sampleData, groupInfo: GroupInfo.sampleData)
}