mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-27 12:56:03 +00:00
desktop, android: channels and chat relays ui (#6670)
This commit is contained in:
@@ -1807,10 +1807,6 @@ enum UserServersError: Decodable {
|
||||
case .smp: return globalSMPError
|
||||
case .xftp: return globalXFTPError
|
||||
}
|
||||
case let .duplicateChatRelayName(duplicateChatRelay):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("Duplicate chat relay name: %@", comment: "servers error"), duplicateChatRelay)
|
||||
case let .duplicateChatRelayAddress(_, duplicateAddress):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("Duplicate chat relay address: %@", comment: "servers error"), duplicateAddress)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,8 @@ struct AddChannelView: View {
|
||||
if !hasRelays {
|
||||
ServersWarningView(warnStr: NSLocalizedString("Enable at least one chat relay in Network & Servers.", comment: "channel creation warning"))
|
||||
} else {
|
||||
Text("Your profile will be shared with chat relays and subscribers.")
|
||||
let name = ChatModel.shared.currentUser?.displayName ?? ""
|
||||
Text("Your profile **\(name)** will be shared with channel relays and subscribers.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
@@ -219,8 +220,8 @@ struct AddChannelView: View {
|
||||
// MARK: - Step 2: Progress
|
||||
|
||||
private func progressStepView(_ gInfo: GroupInfo) -> some View {
|
||||
let failedCount = groupRelays.filter { relayConnFailed($0) != nil }.count
|
||||
let activeCount = groupRelays.filter { $0.relayStatus == .rsActive && relayConnFailed($0) == nil }.count
|
||||
let failedCount = groupRelays.filter { relayMemberConnFailed($0) != nil }.count
|
||||
let activeCount = groupRelays.filter { $0.relayStatus == .rsActive && relayMemberConnFailed($0) == nil }.count
|
||||
let total = groupRelays.count
|
||||
return List {
|
||||
Group {
|
||||
@@ -257,7 +258,7 @@ struct AddChannelView: View {
|
||||
|
||||
if relayListExpanded {
|
||||
ForEach(groupRelays) { relay in
|
||||
let failed = relayConnFailed(relay)
|
||||
let failed = relayMemberConnFailed(relay)
|
||||
if let err = failed {
|
||||
Button {
|
||||
showAlert(
|
||||
@@ -312,14 +313,14 @@ struct AddChannelView: View {
|
||||
.onChange(of: channelRelaysModel.groupRelays) { relays in
|
||||
guard channelRelaysModel.groupId == gInfo.groupId else { return }
|
||||
groupRelays = relays.sorted { relayDisplayName($0) < relayDisplayName($1) }
|
||||
if relays.allSatisfy({ $0.relayStatus == .rsActive && relayConnFailed($0) == nil }) {
|
||||
if relays.allSatisfy({ $0.relayStatus == .rsActive && relayMemberConnFailed($0) == nil }) {
|
||||
showLinkStep = true
|
||||
channelRelaysModel.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func relayConnFailed(_ relay: GroupRelay) -> String? {
|
||||
private func relayMemberConnFailed(_ relay: GroupRelay) -> String? {
|
||||
m.groupMembers.first(where: { $0.wrapped.groupMemberId == relay.groupMemberId })?
|
||||
.wrapped.activeConn?.connFailedErr
|
||||
}
|
||||
@@ -397,7 +398,7 @@ func relayDisplayName(_ relay: GroupRelay) -> String {
|
||||
}
|
||||
|
||||
func relayStatusIndicator(_ status: RelayStatus, connFailed: Bool = false) -> some View {
|
||||
let color: Color = connFailed ? .red : (status == .rsActive ? .green : .orange)
|
||||
let color: Color = connFailed ? .red : (status == .rsActive ? .green : .yellow)
|
||||
let text: LocalizedStringKey = connFailed ? "failed" : status.text
|
||||
return HStack(spacing: 4) {
|
||||
Circle()
|
||||
|
||||
@@ -214,6 +214,8 @@ struct ChatRelayViewLink: View {
|
||||
@Binding var serverErrors: [UserServersError]
|
||||
@Binding var serverWarnings: [UserServersWarning]
|
||||
@Binding var relay: UserChatRelay
|
||||
var duplicateRelayNames: Set<String>
|
||||
var duplicateRelayAddresses: Set<String>
|
||||
var backLabel: LocalizedStringKey
|
||||
@Binding var selectedServer: String?
|
||||
|
||||
@@ -233,7 +235,9 @@ struct ChatRelayViewLink: View {
|
||||
} label: {
|
||||
HStack {
|
||||
Group {
|
||||
if !relay.enabled {
|
||||
if duplicateRelayNames.contains(relay.name) || duplicateRelayAddresses.contains(relay.address) {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||
} else if !relay.enabled {
|
||||
Image(systemName: "slash.circle").foregroundColor(theme.colors.secondary)
|
||||
} else {
|
||||
showRelayTestStatus(relay: relay)
|
||||
|
||||
@@ -483,6 +483,21 @@ func findDuplicateHosts(_ serverErrors: [UserServersError]) -> Set<String> {
|
||||
return Set(duplicateHostsList)
|
||||
}
|
||||
|
||||
func findDuplicateRelayNames(_ serverErrors: [UserServersError]) -> Set<String> {
|
||||
Set(serverErrors.compactMap { err in
|
||||
if case let .duplicateChatRelayName(duplicateChatRelay) = err { return duplicateChatRelay }
|
||||
else { return nil }
|
||||
})
|
||||
}
|
||||
|
||||
func findDuplicateRelayAddresses(_ serverErrors: [UserServersError]) -> Set<String> {
|
||||
Set(serverErrors.compactMap { err in
|
||||
if case let .duplicateChatRelayAddress(_, duplicateAddress) = err { return duplicateAddress }
|
||||
else { return nil }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func saveServers(_ currUserServers: Binding<[UserOperatorServers]>, _ userServers: Binding<[UserOperatorServers]>) {
|
||||
let userServersToSave = userServers.wrappedValue
|
||||
Task {
|
||||
|
||||
@@ -42,6 +42,8 @@ struct OperatorView: View {
|
||||
|
||||
private func operatorView() -> some View {
|
||||
let duplicateHosts = findDuplicateHosts(serverErrors)
|
||||
let duplicateRelayNames = findDuplicateRelayNames(serverErrors)
|
||||
let duplicateRelayAddresses = findDuplicateRelayAddresses(serverErrors)
|
||||
return VStack {
|
||||
List {
|
||||
Section {
|
||||
@@ -81,6 +83,8 @@ struct OperatorView: View {
|
||||
serverErrors: $serverErrors,
|
||||
serverWarnings: $serverWarnings,
|
||||
relay: relay,
|
||||
duplicateRelayNames: duplicateRelayNames,
|
||||
duplicateRelayAddresses: duplicateRelayAddresses,
|
||||
backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers",
|
||||
selectedServer: $selectedServer
|
||||
)
|
||||
|
||||
@@ -43,6 +43,8 @@ struct YourServersView: View {
|
||||
|
||||
private func yourServersView() -> some View {
|
||||
let duplicateHosts = findDuplicateHosts(serverErrors)
|
||||
let duplicateRelayNames = findDuplicateRelayNames(serverErrors)
|
||||
let duplicateRelayAddresses = findDuplicateRelayAddresses(serverErrors)
|
||||
return List {
|
||||
if !userServers[operatorIndex].chatRelays.filter({ !$0.deleted }).isEmpty {
|
||||
Section {
|
||||
@@ -53,6 +55,8 @@ struct YourServersView: View {
|
||||
serverErrors: $serverErrors,
|
||||
serverWarnings: $serverWarnings,
|
||||
relay: relay,
|
||||
duplicateRelayNames: duplicateRelayNames,
|
||||
duplicateRelayAddresses: duplicateRelayAddresses,
|
||||
backLabel: "Your servers",
|
||||
selectedServer: $selectedServer
|
||||
)
|
||||
|
||||
+100
-8
@@ -78,6 +78,22 @@ object ConnectProgressManager {
|
||||
|
||||
val connectProgressManager = ConnectProgressManager
|
||||
|
||||
object ChannelRelaysModel {
|
||||
val groupId = mutableStateOf<Long?>(null)
|
||||
val groupRelays = mutableStateListOf<GroupRelay>()
|
||||
|
||||
fun set(groupId: Long, groupRelays: List<GroupRelay>) {
|
||||
this.groupId.value = groupId
|
||||
this.groupRelays.clear()
|
||||
this.groupRelays.addAll(groupRelays)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
groupId.value = null
|
||||
groupRelays.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it
|
||||
* */
|
||||
@@ -110,9 +126,13 @@ object ChatModel {
|
||||
val chats: State<List<Chat>> = chatsContext.chats
|
||||
// rhId, chatId
|
||||
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
|
||||
val creatingChannelId = mutableStateOf<String?>(null)
|
||||
val groupMembers = mutableStateOf<List<GroupMember>>(emptyList())
|
||||
val groupMembersIndexes = mutableStateOf<Map<Long, Int>>(emptyMap())
|
||||
val membersLoaded = mutableStateOf(false)
|
||||
// Runtime-only relay hostnames for pre-join channel display, not persisted — lost on app restart.
|
||||
// APIConnectPreparedGroup re-fetches fresh relays at connect time, so stale data doesn't affect join.
|
||||
val channelRelayHostnames = mutableStateMapOf<Long, List<String>>()
|
||||
|
||||
// Chat Tags
|
||||
val userTags = mutableStateOf(emptyList<ChatTag>())
|
||||
@@ -847,12 +867,22 @@ object ChatModel {
|
||||
}
|
||||
|
||||
fun removeChat(rhId: Long?, id: String) {
|
||||
var groupId: Long? = null
|
||||
val i = getChatIndex(rhId, id)
|
||||
if (i != -1) {
|
||||
val chat = chats.removeAt(i)
|
||||
groupId = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.groupId
|
||||
removePresetChatTags(chat.chatInfo, chat.chatStats)
|
||||
removeWallpaperFilesFromChat(chat)
|
||||
}
|
||||
if (chatId.value == id) {
|
||||
groupMembers.value = emptyList()
|
||||
groupMembersIndexes.value = emptyMap()
|
||||
if (groupId != null) {
|
||||
channelRelayHostnames.remove(groupId)
|
||||
}
|
||||
membersLoaded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
|
||||
@@ -861,8 +891,8 @@ object ChatModel {
|
||||
updateGroup(rhId, groupInfo)
|
||||
return false
|
||||
}
|
||||
// update current chat
|
||||
return if (chatId.value == groupInfo.id) {
|
||||
// update current chat or channel being created
|
||||
return if (chatId.value == groupInfo.id || creatingChannelId.value == groupInfo.id) {
|
||||
if (groupMembers.value.isNotEmpty() && groupMembers.value.firstOrNull()?.groupId != groupInfo.groupId) {
|
||||
// stale data, should be cleared at that point, otherwise, duplicated items will be here which will produce crashes in LazyColumn
|
||||
groupMembers.value = emptyList()
|
||||
@@ -1220,6 +1250,7 @@ data class User(
|
||||
val autoAcceptMemberContacts: Boolean,
|
||||
val viewPwdHash: UserPwdHash?,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
val userChatRelay: Boolean,
|
||||
): NamedChat, UserLike {
|
||||
override val displayName: String get() = profile.displayName
|
||||
override val fullName: String get() = profile.fullName
|
||||
@@ -1250,6 +1281,7 @@ data class User(
|
||||
autoAcceptMemberContacts = false,
|
||||
viewPwdHash = null,
|
||||
uiThemes = null,
|
||||
userChatRelay = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1583,7 +1615,11 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
return generalGetString(MR.strings.reviewed_by_admins) to generalGetString(MR.strings.observer_cant_send_message_desc)
|
||||
}
|
||||
if (groupInfo.membership.memberRole == GroupMemberRole.Observer) {
|
||||
return generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc)
|
||||
return if (groupInfo.useRelays) {
|
||||
generalGetString(MR.strings.you_are_subscriber) to null
|
||||
} else {
|
||||
generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -2009,6 +2045,8 @@ sealed class ForwardConfirmation {
|
||||
@Serializable
|
||||
data class GroupInfo (
|
||||
val groupId: Long,
|
||||
val useRelays: Boolean,
|
||||
val relayOwnStatus: RelayStatus? = null,
|
||||
override val localDisplayName: String,
|
||||
val groupProfile: GroupProfile,
|
||||
val businessChat: BusinessChatInfo? = null,
|
||||
@@ -2061,7 +2099,9 @@ data class GroupInfo (
|
||||
get() = membership.memberRole >= GroupMemberRole.Moderator && membership.memberActive
|
||||
|
||||
val chatIconName: ImageResource
|
||||
get() = when (businessChat?.chatType) {
|
||||
get() = if (useRelays) {
|
||||
MR.images.ic_bigtop_updates
|
||||
} else when (businessChat?.chatType) {
|
||||
null -> MR.images.ic_supervised_user_circle_filled
|
||||
BusinessChatType.Business -> MR.images.ic_work_filled_padded
|
||||
BusinessChatType.Customer -> MR.images.ic_account_circle_filled
|
||||
@@ -2085,6 +2125,7 @@ data class GroupInfo (
|
||||
companion object {
|
||||
val sampleData = GroupInfo(
|
||||
groupId = 1,
|
||||
useRelays = false,
|
||||
localDisplayName = "team",
|
||||
groupProfile = GroupProfile.sampleData,
|
||||
fullGroupPreferences = FullGroupPreferences.sampleData,
|
||||
@@ -2120,6 +2161,7 @@ data class GroupProfile (
|
||||
override val shortDescr: String?,
|
||||
val description: String? = null,
|
||||
override val image: String? = null,
|
||||
val groupLink: String? = null,
|
||||
override val localAlias: String = "",
|
||||
val groupPreferences: GroupPreferences? = null,
|
||||
val memberAdmission: GroupMemberAdmission? = null
|
||||
@@ -2166,6 +2208,48 @@ data class GroupShortLinkData (
|
||||
val groupProfile: GroupProfile
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class RelayStatus {
|
||||
@SerialName("new") RsNew,
|
||||
@SerialName("invited") RsInvited,
|
||||
@SerialName("accepted") RsAccepted,
|
||||
@SerialName("active") RsActive;
|
||||
|
||||
val text: String get() = when (this) {
|
||||
RsNew -> generalGetString(MR.strings.relay_status_new)
|
||||
RsInvited -> generalGetString(MR.strings.relay_status_invited)
|
||||
RsAccepted -> generalGetString(MR.strings.relay_status_accepted)
|
||||
RsActive -> generalGetString(MR.strings.relay_status_active)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserChatRelay(
|
||||
val chatRelayId: Long?,
|
||||
val address: String,
|
||||
val name: String,
|
||||
val domains: List<String>,
|
||||
val preset: Boolean,
|
||||
val tested: Boolean? = null,
|
||||
val enabled: Boolean,
|
||||
val deleted: Boolean,
|
||||
) {
|
||||
@Transient
|
||||
private val createdAt: Date = Date()
|
||||
val id: String get() = "$address $createdAt"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GroupRelay(
|
||||
val groupRelayId: Long,
|
||||
val groupMemberId: Long,
|
||||
val userChatRelay: UserChatRelay,
|
||||
val relayStatus: RelayStatus,
|
||||
val relayLink: String? = null
|
||||
) {
|
||||
val id: Long get() = groupRelayId
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class BusinessChatInfo (
|
||||
val chatType: BusinessChatType,
|
||||
@@ -2196,7 +2280,8 @@ data class GroupMember (
|
||||
val memberContactProfileId: Long,
|
||||
var activeConn: Connection? = null,
|
||||
val supportChat: GroupSupportChat? = null,
|
||||
val memberChatVRange: VersionRange
|
||||
val memberChatVRange: VersionRange,
|
||||
val relayLink: String? = null
|
||||
): NamedChat {
|
||||
val id: String get() = "#$groupId @$groupMemberId"
|
||||
val ready get() = activeConn?.connStatus == ConnStatus.Ready
|
||||
@@ -2305,14 +2390,14 @@ data class GroupMember (
|
||||
}
|
||||
|
||||
fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? =
|
||||
if (!canBeRemoved(groupInfo) || memberStatus == GroupMemberStatus.MemRemoved || memberStatus == GroupMemberStatus.MemLeft || memberPending) null
|
||||
if (memberRole == GroupMemberRole.Relay || !canBeRemoved(groupInfo) || memberStatus == GroupMemberStatus.MemRemoved || memberStatus == GroupMemberStatus.MemLeft || memberPending) null
|
||||
else groupInfo.membership.memberRole.let { userRole ->
|
||||
GroupMemberRole.selectableRoles.filter { it <= userRole }
|
||||
}
|
||||
|
||||
fun canBlockForAll(groupInfo: GroupInfo): Boolean {
|
||||
val userRole = groupInfo.membership.memberRole
|
||||
return memberRole < GroupMemberRole.Moderator
|
||||
return memberRole != GroupMemberRole.Relay && memberRole < GroupMemberRole.Moderator
|
||||
&& userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive
|
||||
&& !memberPending
|
||||
}
|
||||
@@ -2373,7 +2458,8 @@ data class GroupMemberIds(
|
||||
|
||||
@Serializable
|
||||
enum class GroupMemberRole(val memberRole: String) {
|
||||
@SerialName("observer") Observer("observer"), // order matters in comparisons
|
||||
@SerialName("relay") Relay("relay"), // order matters in comparisons
|
||||
@SerialName("observer") Observer("observer"),
|
||||
@SerialName("author") Author("author"),
|
||||
@SerialName("member") Member("member"),
|
||||
@SerialName("moderator") Moderator("moderator"),
|
||||
@@ -2385,6 +2471,7 @@ enum class GroupMemberRole(val memberRole: String) {
|
||||
}
|
||||
|
||||
val text: String get() = when (this) {
|
||||
Relay -> generalGetString(MR.strings.group_member_role_relay)
|
||||
Observer -> generalGetString(MR.strings.group_member_role_observer)
|
||||
Author -> generalGetString(MR.strings.group_member_role_author)
|
||||
Member -> generalGetString(MR.strings.group_member_role_member)
|
||||
@@ -2844,6 +2931,8 @@ data class ChatItem (
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else if (chatInfo is ChatInfo.Group && chatDir is CIDirection.ChannelRcv) {
|
||||
null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -3185,6 +3274,7 @@ sealed class CIDirection {
|
||||
@Serializable @SerialName("directRcv") class DirectRcv: CIDirection()
|
||||
@Serializable @SerialName("groupSnd") class GroupSnd: CIDirection()
|
||||
@Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember): CIDirection()
|
||||
@Serializable @SerialName("channelRcv") class ChannelRcv: CIDirection()
|
||||
@Serializable @SerialName("localSnd") class LocalSnd: CIDirection()
|
||||
@Serializable @SerialName("localRcv") class LocalRcv: CIDirection()
|
||||
|
||||
@@ -3193,6 +3283,7 @@ sealed class CIDirection {
|
||||
is DirectRcv -> false
|
||||
is GroupSnd -> true
|
||||
is GroupRcv -> false
|
||||
is ChannelRcv -> false
|
||||
is LocalSnd -> true
|
||||
is LocalRcv -> false
|
||||
}
|
||||
@@ -3733,6 +3824,7 @@ class CIQuote (
|
||||
is CIDirection.DirectRcv -> null
|
||||
is CIDirection.GroupSnd -> membership?.displayName ?: generalGetString(MR.strings.sender_you_pronoun)
|
||||
is CIDirection.GroupRcv -> chatDir.groupMember.displayName
|
||||
is CIDirection.ChannelRcv -> null
|
||||
is CIDirection.LocalSnd -> generalGetString(MR.strings.sender_you_pronoun)
|
||||
is CIDirection.LocalRcv -> null
|
||||
null -> null
|
||||
|
||||
+111
-23
@@ -1071,8 +1071,8 @@ object ChatController {
|
||||
|
||||
suspend fun apiReorderChatTags(rh: Long?, tagIds: List<Long>) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds))
|
||||
|
||||
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
|
||||
val cmd = CC.ApiSendMessages(type, id, scope, live, ttl, composedMessages)
|
||||
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, sendAsGroup: Boolean = false, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
|
||||
val cmd = CC.ApiSendMessages(type, id, scope, sendAsGroup, live, ttl, composedMessages)
|
||||
return processSendMessageCmd(rh, cmd)
|
||||
}
|
||||
|
||||
@@ -1130,8 +1130,8 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, itemIds: List<Long>, ttl: Int?): List<ChatItem>? {
|
||||
val cmd = CC.ApiForwardChatItems(toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl)
|
||||
suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, sendAsGroup: Boolean = false, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, itemIds: List<Long>, ttl: Int?): List<ChatItem>? {
|
||||
val cmd = CC.ApiForwardChatItems(toChatType, toChatId, toScope, sendAsGroup, fromChatType, fromChatId, fromScope, itemIds, ttl)
|
||||
return processSendMessageCmd(rh, cmd)?.map { it.chatItem }
|
||||
}
|
||||
|
||||
@@ -1250,10 +1250,10 @@ object ChatController {
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun validateServers(rh: Long?, userServers: List<UserOperatorServers>): List<UserServersError>? {
|
||||
suspend fun validateServers(rh: Long?, userServers: List<UserOperatorServers>): Pair<List<UserServersError>, List<UserServersWarning>>? {
|
||||
val userId = currentUserId("validateServers")
|
||||
val r = sendCmd(rh, CC.ApiValidateServers(userId, userServers))
|
||||
if (r is API.Result && r.res is CR.UserServersValidation) return r.res.serverErrors
|
||||
if (r is API.Result && r.res is CR.UserServersValidation) return Pair(r.res.serverErrors, r.res.serverWarnings)
|
||||
Log.e(TAG, "validateServers bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
@@ -1552,9 +1552,9 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData): Chat? {
|
||||
suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, directLink: Boolean, groupShortLinkData: GroupShortLinkData): Chat? {
|
||||
val userId = try { currentUserId("apiPrepareGroup") } catch (e: Exception) { return null }
|
||||
val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, groupShortLinkData))
|
||||
val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, directLink, groupShortLinkData))
|
||||
if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat
|
||||
Log.e(TAG, "apiPrepareGroup bad response: ${r.responseType} ${r.details}")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_group), "${r.responseType}: ${r.details}")
|
||||
@@ -1587,9 +1587,9 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiConnectPreparedGroup(rh: Long?, groupId: Long, incognito: Boolean, msg: MsgContent?): GroupInfo? {
|
||||
suspend fun apiConnectPreparedGroup(rh: Long?, groupId: Long, incognito: Boolean, msg: MsgContent?): Pair<GroupInfo, List<RelayConnectionResult>>? {
|
||||
val r = sendCmdWithRetry(rh, CC.APIConnectPreparedGroup(groupId, incognito, msg))
|
||||
if (r is API.Result && r.res is CR.StartedConnectionToGroup) return r.res.groupInfo
|
||||
if (r is API.Result && r.res is CR.StartedConnectionToGroup) return Pair(r.res.groupInfo, r.res.relayResults)
|
||||
if (r != null) {
|
||||
Log.e(TAG, "apiConnectPreparedGroup bad response: ${r.responseType} ${r.details}")
|
||||
apiConnectResponseAlert(r)
|
||||
@@ -2097,6 +2097,20 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiNewPublicGroup(rh: Long?, incognito: Boolean, relayIds: List<Long>, groupProfile: GroupProfile): Triple<GroupInfo, GroupLink, List<GroupRelay>>? {
|
||||
val userId = kotlin.runCatching { currentUserId("apiNewPublicGroup") }.getOrElse { return null }
|
||||
val r = sendCmdWithRetry(rh, CC.ApiNewPublicGroup(userId, incognito, relayIds, groupProfile))
|
||||
if (r is API.Result && r.res is CR.PublicGroupCreated) return Triple(r.res.groupInfo, r.res.groupLink, r.res.groupRelays)
|
||||
if (r != null) throw Exception("${r.responseType}: ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGetGroupRelays(groupId: Long): List<GroupRelay> {
|
||||
val r = sendCmd(null, CC.ApiGetGroupRelays(groupId))
|
||||
if (r is API.Result && r.res is CR.GroupRelays) return r.res.groupRelays
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
suspend fun apiAddMember(rh: Long?, groupId: Long, contactId: Long, memberRole: GroupMemberRole): GroupMember? {
|
||||
val r = sendCmd(rh, CC.ApiAddMember(groupId, contactId, memberRole))
|
||||
if (r is API.Result && r.res is CR.SentGroupInvitation) return r.res.member
|
||||
@@ -2812,6 +2826,7 @@ object ChatController {
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
|
||||
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.hostMember)
|
||||
val hostConn = r.hostMember.activeConn
|
||||
if (hostConn != null) {
|
||||
chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}")
|
||||
@@ -2926,6 +2941,7 @@ object ChatController {
|
||||
if (active(r.user)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
|
||||
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.hostMember)
|
||||
}
|
||||
if (
|
||||
chatModel.chatId.value == r.groupInfo.id
|
||||
@@ -2961,6 +2977,16 @@ object ChatController {
|
||||
chatModel.chatsContext.updateGroup(rhId, r.toGroup)
|
||||
}
|
||||
}
|
||||
is CR.GroupLinkRelaysUpdated ->
|
||||
if (active(r.user)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
|
||||
val relaysModel = ChannelRelaysModel
|
||||
if (relaysModel.groupId.value == r.groupInfo.groupId) {
|
||||
relaysModel.set(r.groupInfo.groupId, r.groupRelays)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.NewMemberContactReceivedInv ->
|
||||
if (active(r.user)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -3559,7 +3585,7 @@ sealed class CC {
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val scope: GroupChatScope?, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiGetChatContentTypes(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC()
|
||||
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long): CC()
|
||||
class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
|
||||
class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val sendAsGroup: Boolean, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
|
||||
class ApiCreateChatTag(val tag: ChatTagData): CC()
|
||||
class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List<Long>): CC()
|
||||
class ApiDeleteChatTag(val tagId: Long): CC()
|
||||
@@ -3575,8 +3601,10 @@ sealed class CC {
|
||||
class ApiChatItemReaction(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
|
||||
class ApiGetReactionMembers(val userId: Long, val groupId: Long, val itemId: Long, val reaction: MsgReaction): CC()
|
||||
class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List<Long>): CC()
|
||||
class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List<Long>, val ttl: Int?): CC()
|
||||
class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List<Long>, val ttl: Int?): CC()
|
||||
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
|
||||
class ApiNewPublicGroup(val userId: Long, val incognito: Boolean, val relayIds: List<Long>, val groupProfile: GroupProfile): CC()
|
||||
class ApiGetGroupRelays(val groupId: Long): CC()
|
||||
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
|
||||
class ApiJoinGroup(val groupId: Long): CC()
|
||||
class ApiAcceptMember(val groupId: Long, val groupMemberId: Long, val memberRole: GroupMemberRole): CC()
|
||||
@@ -3633,7 +3661,7 @@ sealed class CC {
|
||||
class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC()
|
||||
class APIConnectPlan(val userId: Long, val connLink: String): CC()
|
||||
class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData): CC()
|
||||
class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val groupShortLinkData: GroupShortLinkData): CC()
|
||||
class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData): CC()
|
||||
class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC()
|
||||
class APIChangePreparedGroupUser(val groupId: Long, val newUserId: Long): CC()
|
||||
class APIConnectPreparedContact(val contactId: Long, val incognito: Boolean, val msg: MsgContent?): CC()
|
||||
@@ -3747,7 +3775,7 @@ sealed class CC {
|
||||
is ApiSendMessages -> {
|
||||
val msgs = json.encodeToString(composedMessages)
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_send ${chatRef(type, id, scope)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
|
||||
"/_send ${chatRef(type, id, scope)}${if (sendAsGroup) "(as_group=on)" else ""} live=${onOff(live)} ttl=${ttlStr} json $msgs"
|
||||
}
|
||||
is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}"
|
||||
is ApiSetChatTags -> "/_tags ${chatRef(type, id, scope = null)} ${tagIds.joinToString(",")}"
|
||||
@@ -3768,12 +3796,14 @@ sealed class CC {
|
||||
is ApiGetReactionMembers -> "/_reaction members $userId #$groupId $itemId ${json.encodeToString(reaction)}"
|
||||
is ApiForwardChatItems -> {
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_forward ${chatRef(toChatType, toChatId, toScope)} ${chatRef(fromChatType, fromChatId, fromScope)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
|
||||
"/_forward ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) " as_group=on" else ""} ${chatRef(fromChatType, fromChatId, fromScope)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
|
||||
}
|
||||
is ApiPlanForwardChatItems -> {
|
||||
"/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${chatItemIds.joinToString(",")}"
|
||||
}
|
||||
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
|
||||
is ApiNewPublicGroup -> "/_public group $userId incognito=${onOff(incognito)} ${relayIds.joinToString(",")} ${json.encodeToString(groupProfile)}"
|
||||
is ApiGetGroupRelays -> "/_get relays #$groupId"
|
||||
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
|
||||
is ApiJoinGroup -> "/_join #$groupId"
|
||||
is ApiAcceptMember -> "/_accept member #$groupId $groupMemberId ${memberRole.memberRole}"
|
||||
@@ -3830,7 +3860,7 @@ sealed class CC {
|
||||
is ApiChangeConnectionUser -> "/_set conn user :$connId $userId"
|
||||
is APIConnectPlan -> "/_connect plan $userId $connLink"
|
||||
is APIPrepareContact -> "/_prepare contact $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(contactShortLinkData)}"
|
||||
is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(groupShortLinkData)}"
|
||||
is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} direct=${onOff(directLink)} ${json.encodeToString(groupShortLinkData)}"
|
||||
is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId"
|
||||
is APIChangePreparedGroupUser -> "/_set group user #$groupId $newUserId"
|
||||
is APIConnectPreparedContact -> "/_connect contact @$contactId incognito=${onOff(incognito)}${maybeContent(msg)}"
|
||||
@@ -3949,6 +3979,8 @@ sealed class CC {
|
||||
is ApiForwardChatItems -> "apiForwardChatItems"
|
||||
is ApiPlanForwardChatItems -> "apiPlanForwardChatItems"
|
||||
is ApiNewGroup -> "apiNewGroup"
|
||||
is ApiNewPublicGroup -> "apiNewPublicGroup"
|
||||
is ApiGetGroupRelays -> "apiGetGroupRelays"
|
||||
is ApiAddMember -> "apiAddMember"
|
||||
is ApiJoinGroup -> "apiJoinGroup"
|
||||
is ApiAcceptMember -> "apiAcceptMember"
|
||||
@@ -4120,7 +4152,8 @@ fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
@Serializable
|
||||
data class NewUser(
|
||||
val profile: Profile?,
|
||||
val pastTimestamp: Boolean
|
||||
val pastTimestamp: Boolean,
|
||||
val userChatRelay: Boolean = false
|
||||
)
|
||||
|
||||
sealed class ChatPagination {
|
||||
@@ -4373,7 +4406,8 @@ data class ServerRoles(
|
||||
data class UserOperatorServers(
|
||||
val operator: ServerOperator?,
|
||||
val smpServers: List<UserServer>,
|
||||
val xftpServers: List<UserServer>
|
||||
val xftpServers: List<UserServer>,
|
||||
val chatRelays: List<UserChatRelay> = emptyList()
|
||||
) {
|
||||
val id: String
|
||||
get() = operator?.operatorId?.toString() ?: "nil operator"
|
||||
@@ -4412,19 +4446,24 @@ sealed class UserServersError {
|
||||
@Serializable @SerialName("storageMissing") data class StorageMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
|
||||
@Serializable @SerialName("proxyMissing") data class ProxyMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
|
||||
@Serializable @SerialName("duplicateServer") data class DuplicateServer(val protocol: ServerProtocol, val duplicateServer: String, val duplicateHost: String): UserServersError()
|
||||
@Serializable @SerialName("duplicateChatRelayName") data class DuplicateChatRelayName(val duplicateChatRelay: String): UserServersError()
|
||||
@Serializable @SerialName("duplicateChatRelayAddress") data class DuplicateChatRelayAddress(val duplicateChatRelay: String, val duplicateAddress: String): UserServersError()
|
||||
|
||||
val globalError: String?
|
||||
get() = when (this.protocol_) {
|
||||
ServerProtocol.SMP -> globalSMPError
|
||||
ServerProtocol.XFTP -> globalXFTPError
|
||||
null -> null
|
||||
}
|
||||
|
||||
private val protocol_: ServerProtocol
|
||||
private val protocol_: ServerProtocol?
|
||||
get() = when (this) {
|
||||
is NoServers -> this.protocol
|
||||
is StorageMissing -> this.protocol
|
||||
is ProxyMissing -> this.protocol
|
||||
is DuplicateServer -> this.protocol
|
||||
is DuplicateChatRelayName -> null
|
||||
is DuplicateChatRelayAddress -> null
|
||||
}
|
||||
|
||||
val globalSMPError: String?
|
||||
@@ -4468,6 +4507,34 @@ sealed class UserServersError {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class UserServersWarning {
|
||||
@Serializable @SerialName("noChatRelays") data class NoChatRelays(val user: UserRef? = null): UserServersWarning()
|
||||
|
||||
val globalWarning: String?
|
||||
get() = when (this) {
|
||||
is NoChatRelays -> {
|
||||
val text = generalGetString(MR.strings.no_chat_relays_enabled)
|
||||
if (user != null) {
|
||||
String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName) + " " + text
|
||||
} else text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RelayConnectionResult(
|
||||
val relayMember: GroupMember,
|
||||
val relayError: ChatError? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GroupShortLinkInfo(
|
||||
val direct: Boolean,
|
||||
val groupRelays: List<String>,
|
||||
val sharedGroupId: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserServer(
|
||||
val remoteHostId: Long?,
|
||||
@@ -6125,7 +6192,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
|
||||
@Serializable @SerialName("serverOperatorConditions") class ServerOperatorConditions(val conditions: ServerOperatorConditionsDetail): CR()
|
||||
@Serializable @SerialName("userServers") class UserServers(val user: UserRef, val userServers: List<UserOperatorServers>): CR()
|
||||
@Serializable @SerialName("userServersValidation") class UserServersValidation(val user: UserRef, val serverErrors: List<UserServersError>): CR()
|
||||
@Serializable @SerialName("userServersValidation") class UserServersValidation(val user: UserRef, val serverErrors: List<UserServersError>, val serverWarnings: List<UserServersWarning> = emptyList()): CR()
|
||||
@Serializable @SerialName("usageConditions") class UsageConditions(val usageConditions: UsageConditionsDetail, val conditionsText: String?, val acceptedConditions: UsageConditionsDetail?): CR()
|
||||
@Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: UserRef, val chatItemTTL: Long? = null): CR()
|
||||
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
|
||||
@@ -6156,7 +6223,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("startedConnectionToContact") class StartedConnectionToContact(val user: UserRef, val contact: Contact): CR()
|
||||
@Serializable @SerialName("startedConnectionToGroup") class StartedConnectionToGroup(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("startedConnectionToGroup") class StartedConnectionToGroup(val user: UserRef, val groupInfo: GroupInfo, val relayResults: List<RelayConnectionResult> = emptyList()): CR()
|
||||
@Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR()
|
||||
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
|
||||
@Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR()
|
||||
@@ -6195,6 +6262,8 @@ sealed class CR {
|
||||
@Serializable @SerialName("forwardPlan") class ForwardPlan(val user: UserRef, val itemsCount: Int, val chatItemIds: List<Long>, val forwardConfirmation: ForwardConfirmation? = null): CR()
|
||||
// group events
|
||||
@Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("publicGroupCreated") class PublicGroupCreated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List<GroupRelay>): CR()
|
||||
@Serializable @SerialName("groupRelays") class GroupRelays(val user: UserRef, val groupInfo: GroupInfo, val groupRelays: List<GroupRelay>): CR()
|
||||
@Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val user: UserRef, val groupInfo: GroupInfo, val hostContact: Contact? = null): CR()
|
||||
@Serializable @SerialName("groupLinkConnecting") class GroupLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember): CR()
|
||||
@@ -6217,10 +6286,11 @@ sealed class CR {
|
||||
@Serializable @SerialName("deletedMember") class DeletedMember(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember, val withMessages: Boolean): CR()
|
||||
@Serializable @SerialName("leftMember") class LeftMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("groupDeleted") class GroupDeleted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember): CR()
|
||||
@Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR()
|
||||
@Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR()
|
||||
@Serializable @SerialName("groupLinkRelaysUpdated") class GroupLinkRelaysUpdated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List<GroupRelay>): CR()
|
||||
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink): CR()
|
||||
@Serializable @SerialName("groupLink") class CRGroupLink(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink): CR()
|
||||
@Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@@ -6376,6 +6446,8 @@ sealed class CR {
|
||||
is GroupChatItemsDeleted -> "groupChatItemsDeleted"
|
||||
is ForwardPlan -> "forwardPlan"
|
||||
is GroupCreated -> "groupCreated"
|
||||
is PublicGroupCreated -> "publicGroupCreated"
|
||||
is GroupRelays -> "groupRelays"
|
||||
is SentGroupInvitation -> "sentGroupInvitation"
|
||||
is UserAcceptedGroupSent -> "userAcceptedGroupSent"
|
||||
is GroupLinkConnecting -> "groupLinkConnecting"
|
||||
@@ -6402,6 +6474,7 @@ sealed class CR {
|
||||
is JoinedGroupMember -> "joinedGroupMember"
|
||||
is ConnectedToGroupMember -> "connectedToGroupMember"
|
||||
is GroupUpdated -> "groupUpdated"
|
||||
is GroupLinkRelaysUpdated -> "groupLinkRelaysUpdated"
|
||||
is GroupLinkCreated -> "groupLinkCreated"
|
||||
is CRGroupLink -> "groupLink"
|
||||
is GroupLinkDeleted -> "groupLinkDeleted"
|
||||
@@ -6550,6 +6623,8 @@ sealed class CR {
|
||||
is GroupChatItemsDeleted -> withUser(user, "chatItemIDs: $chatItemIDs\nbyUser: $byUser\nmember_: $member_")
|
||||
is ForwardPlan -> withUser(user, "itemsCount: $itemsCount\nchatItemIds: ${json.encodeToString(chatItemIds)}\nforwardConfirmation: ${json.encodeToString(forwardConfirmation)}")
|
||||
is GroupCreated -> withUser(user, json.encodeToString(groupInfo))
|
||||
is PublicGroupCreated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink\ngroupRelays: $groupRelays")
|
||||
is GroupRelays -> withUser(user, "groupInfo: $groupInfo\ngroupRelays: $groupRelays")
|
||||
is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member")
|
||||
is UserAcceptedGroupSent -> json.encodeToString(groupInfo)
|
||||
is GroupLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember")
|
||||
@@ -6576,6 +6651,7 @@ sealed class CR {
|
||||
is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
|
||||
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact")
|
||||
is GroupUpdated -> withUser(user, json.encodeToString(toGroup))
|
||||
is GroupLinkRelaysUpdated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink\ngroupRelays: $groupRelays")
|
||||
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink")
|
||||
is CRGroupLink -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink")
|
||||
is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo))
|
||||
@@ -6719,7 +6795,7 @@ sealed class ContactAddressPlan {
|
||||
|
||||
@Serializable
|
||||
sealed class GroupLinkPlan {
|
||||
@Serializable @SerialName("ok") class Ok(val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan()
|
||||
@Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan()
|
||||
@Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan()
|
||||
@Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan()
|
||||
@Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan()
|
||||
@@ -7011,6 +7087,7 @@ sealed class ChatErrorType {
|
||||
is UserUnknown -> "userUnknown"
|
||||
is ActiveUserExists -> "activeUserExists"
|
||||
is UserExists -> "userExists"
|
||||
is ChatRelayExists -> "chatRelayExists"
|
||||
is DifferentActiveUser -> "differentActiveUser"
|
||||
is CantDeleteActiveUser -> "cantDeleteActiveUser"
|
||||
is CantDeleteLastUser -> "cantDeleteLastUser"
|
||||
@@ -7091,6 +7168,7 @@ sealed class ChatErrorType {
|
||||
@Serializable @SerialName("userUnknown") object UserUnknown: ChatErrorType()
|
||||
@Serializable @SerialName("activeUserExists") object ActiveUserExists: ChatErrorType()
|
||||
@Serializable @SerialName("userExists") class UserExists(val contactName: String): ChatErrorType()
|
||||
@Serializable @SerialName("chatRelayExists") object ChatRelayExists: ChatErrorType()
|
||||
@Serializable @SerialName("differentActiveUser") class DifferentActiveUser(val commandUserId: Long, val activeUserId: Long): ChatErrorType()
|
||||
@Serializable @SerialName("cantDeleteActiveUser") class CantDeleteActiveUser(val userId: Long): ChatErrorType()
|
||||
@Serializable @SerialName("cantDeleteLastUser") class CantDeleteLastUser(val userId: Long): ChatErrorType()
|
||||
@@ -7170,6 +7248,7 @@ sealed class StoreError {
|
||||
get() = when (this) {
|
||||
is DuplicateName -> "duplicateName"
|
||||
is UserNotFound -> "userNotFound $userId"
|
||||
is RelayUserNotFound -> "relayUserNotFound"
|
||||
is UserNotFoundByName -> "userNotFoundByName $contactName"
|
||||
is UserNotFoundByContactId -> "userNotFoundByContactId $contactId"
|
||||
is UserNotFoundByGroupId -> "userNotFoundByGroupId $groupId"
|
||||
@@ -7193,6 +7272,7 @@ sealed class StoreError {
|
||||
is MemberContactGroupMemberNotFound -> "memberContactGroupMemberNotFound $contactId"
|
||||
is GroupWithoutUser -> "groupWithoutUser"
|
||||
is DuplicateGroupMember -> "duplicateGroupMember"
|
||||
is DuplicateMemberId -> "duplicateMemberId"
|
||||
is GroupAlreadyJoined -> "groupAlreadyJoined"
|
||||
is GroupInvitationNotFound -> "groupInvitationNotFound"
|
||||
is NoteFolderAlreadyExists -> "noteFolderAlreadyExists $noteFolderId"
|
||||
@@ -7233,6 +7313,9 @@ sealed class StoreError {
|
||||
is HostMemberIdNotFound -> "hostMemberIdNotFound $groupId"
|
||||
is ContactNotFoundByFileId -> "contactNotFoundByFileId $fileId"
|
||||
is NoGroupSndStatus -> "noGroupSndStatus $itemId $groupMemberId"
|
||||
is UserChatRelayNotFound -> "userChatRelayNotFound $chatRelayId"
|
||||
is GroupRelayNotFound -> "groupRelayNotFound $groupRelayId"
|
||||
is GroupRelayNotFoundByMemberId -> "groupRelayNotFoundByMemberId $groupMemberId"
|
||||
is DuplicateGroupMessage -> "duplicateGroupMessage $groupId $sharedMsgId $authorGroupMemberId $authorGroupMemberId"
|
||||
is RemoteHostNotFound -> "remoteHostNotFound $remoteHostId"
|
||||
is RemoteHostUnknown -> "remoteHostUnknown"
|
||||
@@ -7248,6 +7331,7 @@ sealed class StoreError {
|
||||
|
||||
@Serializable @SerialName("duplicateName") object DuplicateName: StoreError()
|
||||
@Serializable @SerialName("userNotFound") class UserNotFound(val userId: Long): StoreError()
|
||||
@Serializable @SerialName("relayUserNotFound") object RelayUserNotFound: StoreError()
|
||||
@Serializable @SerialName("userNotFoundByName") class UserNotFoundByName(val contactName: String): StoreError()
|
||||
@Serializable @SerialName("userNotFoundByContactId") class UserNotFoundByContactId(val contactId: Long): StoreError()
|
||||
@Serializable @SerialName("userNotFoundByGroupId") class UserNotFoundByGroupId(val groupId: Long): StoreError()
|
||||
@@ -7271,6 +7355,7 @@ sealed class StoreError {
|
||||
@Serializable @SerialName("memberContactGroupMemberNotFound") class MemberContactGroupMemberNotFound(val contactId: Long): StoreError()
|
||||
@Serializable @SerialName("groupWithoutUser") object GroupWithoutUser: StoreError()
|
||||
@Serializable @SerialName("duplicateGroupMember") object DuplicateGroupMember: StoreError()
|
||||
@Serializable @SerialName("duplicateMemberId") object DuplicateMemberId: StoreError()
|
||||
@Serializable @SerialName("groupAlreadyJoined") object GroupAlreadyJoined: StoreError()
|
||||
@Serializable @SerialName("groupInvitationNotFound") object GroupInvitationNotFound: StoreError()
|
||||
@Serializable @SerialName("noteFolderAlreadyExists") class NoteFolderAlreadyExists(val noteFolderId: Long): StoreError()
|
||||
@@ -7311,6 +7396,9 @@ sealed class StoreError {
|
||||
@Serializable @SerialName("hostMemberIdNotFound") class HostMemberIdNotFound(val groupId: Long): StoreError()
|
||||
@Serializable @SerialName("contactNotFoundByFileId") class ContactNotFoundByFileId(val fileId: Long): StoreError()
|
||||
@Serializable @SerialName("noGroupSndStatus") class NoGroupSndStatus(val itemId: Long, val groupMemberId: Long): StoreError()
|
||||
@Serializable @SerialName("userChatRelayNotFound") class UserChatRelayNotFound(val chatRelayId: Long): StoreError()
|
||||
@Serializable @SerialName("groupRelayNotFound") class GroupRelayNotFound(val groupRelayId: Long): StoreError()
|
||||
@Serializable @SerialName("groupRelayNotFoundByMemberId") class GroupRelayNotFoundByMemberId(val groupMemberId: Long): StoreError()
|
||||
@Serializable @SerialName("duplicateGroupMessage") class DuplicateGroupMessage(val groupId: Long, val sharedMsgId: String, val authorGroupMemberId: Long?, val forwardedByGroupMemberId: Long?): StoreError()
|
||||
@Serializable @SerialName("remoteHostNotFound") class RemoteHostNotFound(val remoteHostId: Long): StoreError()
|
||||
@Serializable @SerialName("remoteHostUnknown") object RemoteHostUnknown: StoreError()
|
||||
|
||||
+128
-12
@@ -201,6 +201,17 @@ fun ChatView(
|
||||
chatModel.chatSubStatus.value = null
|
||||
}
|
||||
}
|
||||
if (cInfo is ChatInfo.Group && cInfo.groupInfo.useRelays) {
|
||||
withBGApi {
|
||||
setGroupMembers(chatRh, cInfo.groupInfo, chatModel)
|
||||
if (cInfo.groupInfo.membership.memberRole == GroupMemberRole.Owner) {
|
||||
val relays = chatModel.controller.apiGetGroupRelays(cInfo.groupInfo.groupId)
|
||||
withContext(Dispatchers.Main) {
|
||||
ChannelRelaysModel.set(cInfo.groupInfo.groupId, relays)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,6 +369,7 @@ fun ChatView(
|
||||
chatModel.groupMembers.value = emptyList()
|
||||
chatModel.groupMembersIndexes.value = emptyMap()
|
||||
chatModel.membersLoaded.value = false
|
||||
ChannelRelaysModel.reset()
|
||||
},
|
||||
info = {
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
@@ -488,7 +500,7 @@ fun ChatView(
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, close, close)
|
||||
GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, close = close, closeAll = close)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -810,7 +822,7 @@ fun updateAvailableContent(chatRh: Long?, activeChat: State<Chat?>, availableCon
|
||||
withBGApi {
|
||||
Log.e(TAG, "updateAvailableContent")
|
||||
val chatInfo = activeChat.value?.chatInfo
|
||||
if (chatInfo == null) return@withBGApi
|
||||
if (chatInfo == null || chatInfo !is ChatInfo.Direct && chatInfo !is ChatInfo.Group && chatInfo !is ChatInfo.Local) return@withBGApi
|
||||
val types = chatModel.controller.apiGetChatContentTypes(chatRh, chatInfo.chatType, chatInfo.apiId, null)
|
||||
if (activeChat.value?.chatInfo?.id != chatInfo.id) return@withBGApi
|
||||
if (types == null) {
|
||||
@@ -842,10 +854,14 @@ private fun connectingText(chatInfo: ChatInfo): String? {
|
||||
}
|
||||
|
||||
is ChatInfo.Group ->
|
||||
when (chatInfo.groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemUnknown -> if (chatInfo.groupInfo.preparedGroup?.connLinkStartedConnection == true) generalGetString(MR.strings.group_connection_pending) else null
|
||||
GroupMemberStatus.MemAccepted -> generalGetString(MR.strings.group_connection_pending)
|
||||
else -> null
|
||||
if (chatInfo.groupInfo.useRelays) {
|
||||
null
|
||||
} else {
|
||||
when (chatInfo.groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemUnknown -> if (chatInfo.groupInfo.preparedGroup?.connLinkStartedConnection == true) generalGetString(MR.strings.group_connection_pending) else null
|
||||
GroupMemberStatus.MemAccepted -> generalGetString(MR.strings.group_connection_pending)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
@@ -1944,6 +1960,89 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cItem.chatDir is CIDirection.ChannelRcv) {
|
||||
if (showAvatar) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.fillMaxWidth()
|
||||
.then(swipeableModifier),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
@Composable
|
||||
fun ChannelNameAndRole() {
|
||||
Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
chatInfo.groupInfo.chatViewName,
|
||||
Modifier
|
||||
.padding(start = (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + DEFAULT_PADDING_HALF)
|
||||
.weight(1f, false),
|
||||
fontSize = 13.5.sp,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
||||
val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
|
||||
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
|
||||
Text(
|
||||
generalGetString(MR.strings.channel_role_label),
|
||||
Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp),
|
||||
fontSize = 13.5.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Item() {
|
||||
ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
|
||||
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedListItem(Modifier, cItem.id, selectedChatItems)
|
||||
}
|
||||
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
|
||||
Box(Modifier.clickable { showChatInfo() }) {
|
||||
ProfileImage(
|
||||
MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier,
|
||||
chatInfo.groupInfo.image,
|
||||
chatInfo.groupInfo.chatIconName,
|
||||
backgroundColor = MaterialTheme.colors.background
|
||||
)
|
||||
}
|
||||
Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) {
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cItem.content.showMemberName) {
|
||||
DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) {
|
||||
ChannelNameAndRole()
|
||||
Item()
|
||||
}
|
||||
} else {
|
||||
Item()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ChatItemBox {
|
||||
AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||
.then(swipeableOrSelectionModifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ChatItemBox {
|
||||
AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
@@ -2017,13 +2116,14 @@ fun BoxScope.ChatItemsList(
|
||||
val groupInfo = chatInfo.groupInfo
|
||||
when (groupInfo.businessChat?.chatType) {
|
||||
null -> {
|
||||
val isChannel = groupInfo.useRelays
|
||||
if (groupInfo.nextConnectPrepared) {
|
||||
generalGetString(MR.strings.chat_banner_join_group)
|
||||
generalGetString(if (isChannel) MR.strings.chat_banner_join_channel else MR.strings.chat_banner_join_group)
|
||||
} else {
|
||||
when (groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemInvited -> generalGetString(MR.strings.chat_banner_join_group)
|
||||
GroupMemberStatus.MemCreator -> generalGetString(MR.strings.chat_banner_your_group)
|
||||
else -> generalGetString(MR.strings.chat_banner_group)
|
||||
GroupMemberStatus.MemInvited -> generalGetString(if (isChannel) MR.strings.chat_banner_join_channel else MR.strings.chat_banner_join_group)
|
||||
GroupMemberStatus.MemCreator -> generalGetString(if (isChannel) MR.strings.chat_banner_your_channel else MR.strings.chat_banner_your_group)
|
||||
else -> generalGetString(if (isChannel) MR.strings.chat_banner_channel else MR.strings.chat_banner_group)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3477,6 +3577,8 @@ private fun getItemSeparation(chatItem: ChatItem, prevItem: ChatItem?): ItemSepa
|
||||
|
||||
val sameMemberAndDirection = if (prevItem.chatDir is GroupRcv && chatItem.chatDir is GroupRcv) {
|
||||
chatItem.chatDir.groupMember.groupMemberId == prevItem.chatDir.groupMember.groupMemberId
|
||||
} else if (chatItem.chatDir is CIDirection.ChannelRcv && prevItem.chatDir is CIDirection.ChannelRcv) {
|
||||
true
|
||||
} else chatItem.chatDir.sent == prevItem.chatDir.sent
|
||||
val largeGap = !sameMemberAndDirection || (abs(prevItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60)
|
||||
|
||||
@@ -3494,12 +3596,26 @@ private fun getItemSeparationLargeGap(chatItem: ChatItem, nextItem: ChatItem?):
|
||||
|
||||
val sameMemberAndDirection = if (nextItem.chatDir is GroupRcv && chatItem.chatDir is GroupRcv) {
|
||||
chatItem.chatDir.groupMember.groupMemberId == nextItem.chatDir.groupMember.groupMemberId
|
||||
} else if (chatItem.chatDir is CIDirection.ChannelRcv && nextItem.chatDir is CIDirection.ChannelRcv) {
|
||||
true
|
||||
} else chatItem.chatDir.sent == nextItem.chatDir.sent
|
||||
return !sameMemberAndDirection || (abs(nextItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60)
|
||||
}
|
||||
|
||||
private fun shouldShowAvatar(current: ChatItem, older: ChatItem?) =
|
||||
current.chatDir is CIDirection.GroupRcv && (older == null || (older.chatDir !is CIDirection.GroupRcv || older.chatDir.groupMember.memberId != current.chatDir.groupMember.memberId))
|
||||
private fun shouldShowAvatar(current: ChatItem, older: ChatItem?): Boolean {
|
||||
val oldIsGroupRcv = older?.chatDir is CIDirection.GroupRcv || older?.chatDir is CIDirection.ChannelRcv
|
||||
val sameMember = when {
|
||||
older?.chatDir is CIDirection.GroupRcv && current.chatDir is CIDirection.GroupRcv ->
|
||||
older.chatDir.groupMember.memberId == current.chatDir.groupMember.memberId
|
||||
older?.chatDir is CIDirection.ChannelRcv && current.chatDir is CIDirection.ChannelRcv -> true
|
||||
else -> false
|
||||
}
|
||||
return when {
|
||||
current.chatDir is CIDirection.GroupRcv -> older == null || !oldIsGroupRcv || !sameMember
|
||||
current.chatDir is CIDirection.ChannelRcv -> older == null || !oldIsGroupRcv || !sameMember
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview/*(
|
||||
|
||||
+227
-8
@@ -30,8 +30,13 @@ import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.filesToDelete
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.group.hostFromRelayLink
|
||||
import chat.simplex.common.views.chat.group.relayConnStatus
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.RelayProgressIndicator
|
||||
import chat.simplex.common.views.newchat.RelayStatusIndicator
|
||||
import chat.simplex.common.views.newchat.relayDisplayName
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import kotlinx.coroutines.*
|
||||
@@ -490,6 +495,7 @@ fun ComposeView(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
scope = cInfo.groupChatScope(),
|
||||
sendAsGroup = (cInfo as? ChatInfo.Group)?.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false,
|
||||
live = live,
|
||||
ttl = ttl,
|
||||
composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions))
|
||||
@@ -588,15 +594,19 @@ fun ComposeView(
|
||||
val mc = checkLinkPreview()
|
||||
sending()
|
||||
val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get()
|
||||
val groupInfo = chatModel.controller.apiConnectPreparedGroup(
|
||||
val result = chatModel.controller.apiConnectPreparedGroup(
|
||||
rh = chat.remoteHostId,
|
||||
groupId = chat.chatInfo.apiId,
|
||||
incognito = incognito,
|
||||
msg = mc
|
||||
)
|
||||
if (groupInfo != null) {
|
||||
if (result != null) {
|
||||
val (groupInfo, relayResults) = result
|
||||
withContext(Dispatchers.Main) {
|
||||
chatsCtx.updateGroup(chat.remoteHostId, groupInfo)
|
||||
chatModel.channelRelayHostnames.remove(groupInfo.groupId)
|
||||
chatModel.groupMembers.value = relayResults.map { it.relayMember }
|
||||
chatModel.populateGroupMembersIndexes()
|
||||
clearState()
|
||||
}
|
||||
} else {
|
||||
@@ -616,6 +626,7 @@ fun ComposeView(
|
||||
toChatType = chat.chatInfo.chatType,
|
||||
toChatId = chat.chatInfo.apiId,
|
||||
toScope = chat.chatInfo.groupChatScope(),
|
||||
sendAsGroup = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false,
|
||||
fromChatType = fromChatInfo.chatType,
|
||||
fromChatId = fromChatInfo.apiId,
|
||||
fromScope = fromChatInfo.groupChatScope(),
|
||||
@@ -1353,7 +1364,7 @@ fun ComposeView(
|
||||
icon: ImageResource,
|
||||
connect: () -> Unit
|
||||
) {
|
||||
var modifier = Modifier.height(60.dp).fillMaxWidth()
|
||||
var modifier = Modifier.height(57.dp).fillMaxWidth()
|
||||
modifier = if (composeState.value.inProgress) modifier else modifier.clickable(onClick = { connect() })
|
||||
Box(
|
||||
modifier,
|
||||
@@ -1374,7 +1385,7 @@ fun ComposeView(
|
||||
color = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
if (composeState.value.progressByTimeout) {
|
||||
if (composeState.value.progressByTimeout && chat.chatInfo.groupInfo_?.useRelays != true) {
|
||||
Box(
|
||||
Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING_HALF),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
@@ -1442,9 +1453,11 @@ fun ComposeView(
|
||||
composeState.value = composeState.value.copy(progressByTimeout = newProgressByTimeout)
|
||||
}
|
||||
|
||||
val relayListExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
Column {
|
||||
val currentUser = chatModel.currentUser.value
|
||||
if (chat.chatInfo.nextConnectPrepared && currentUser != null) {
|
||||
if (chat.chatInfo.nextConnectPrepared && !composeState.value.inProgress && currentUser != null) {
|
||||
ComposeContextProfilePickerView(
|
||||
rhId = rhId,
|
||||
chat = chat,
|
||||
@@ -1452,6 +1465,33 @@ fun ComposeView(
|
||||
)
|
||||
}
|
||||
|
||||
val gInfo = (chat.chatInfo as? ChatInfo.Group)?.groupInfo
|
||||
if (gInfo != null && gInfo.useRelays) {
|
||||
if (gInfo.membership.memberRole == GroupMemberRole.Owner) {
|
||||
val relays = if (ChannelRelaysModel.groupId.value == gInfo.groupId) ChannelRelaysModel.groupRelays.toList() else emptyList()
|
||||
val failedCount = relays.count { relayMemberConnFailed(chatModel, it) != null }
|
||||
val activeCount = relays.count { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null }
|
||||
if (relays.isNotEmpty() && activeCount < relays.size) {
|
||||
OwnerChannelRelayBar(chatModel, relays, activeCount, failedCount, relayListExpanded)
|
||||
}
|
||||
} else {
|
||||
val hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?: emptyList()).sorted()
|
||||
val relayMembers = chatModel.groupMembers.value
|
||||
.filter { it.memberRole == GroupMemberRole.Relay }
|
||||
.sortedBy { hostFromRelayLink(it.relayLink ?: "") }
|
||||
val showProgress = !gInfo.nextConnectPrepared || composeState.value.inProgress
|
||||
val connectedCount = relayMembers.count { it.activeConn?.connStatus == ConnStatus.Ready }
|
||||
val deletedCount = relayMembers.count { it.activeConn?.connStatus == ConnStatus.Deleted }
|
||||
val failedCount = relayMembers.count { it.activeConn?.connFailedErr != null }
|
||||
val errorCount = deletedCount + failedCount
|
||||
val resolvedCount = connectedCount + deletedCount
|
||||
val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size
|
||||
if (total > 0 && (!showProgress || resolvedCount < total)) {
|
||||
SubscriberChannelRelayBar(hostnames, relayMembers, connectedCount, errorCount, total, showProgress, relayListExpanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
chat.chatInfo is ChatInfo.Group
|
||||
&& chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext
|
||||
@@ -1506,9 +1546,10 @@ fun ComposeView(
|
||||
Divider()
|
||||
if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared) {
|
||||
if (chat.chatInfo.groupInfo.businessChat == null) {
|
||||
val isChannel = chat.chatInfo.groupInfo.useRelays
|
||||
ConnectButtonView(
|
||||
text = stringResource(MR.strings.compose_view_join_group),
|
||||
icon = MR.images.ic_group_filled,
|
||||
text = stringResource(if (isChannel) MR.strings.compose_view_join_channel else MR.strings.compose_view_join_group),
|
||||
icon = if (isChannel) MR.images.ic_bigtop_updates else MR.images.ic_group_filled,
|
||||
connect = { withApi { connectPreparedGroup() } }
|
||||
)
|
||||
} else {
|
||||
@@ -1579,9 +1620,187 @@ fun ComposeView(
|
||||
} else {
|
||||
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
|
||||
AttachmentAndCommandsButtons()
|
||||
SendMsgView_(disableSendButton = disableSendButton)
|
||||
val broadcastPlaceholder = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.let { gi ->
|
||||
if (gi.useRelays && gi.membership.memberRole >= GroupMemberRole.Owner) generalGetString(MR.strings.compose_view_broadcast)
|
||||
else null
|
||||
}
|
||||
SendMsgView_(disableSendButton = disableSendButton, placeholder = broadcastPlaceholder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OwnerChannelRelayBar(
|
||||
chatModel: ChatModel,
|
||||
relays: List<GroupRelay>,
|
||||
activeCount: Int,
|
||||
failedCount: Int,
|
||||
relayListExpanded: MutableState<Boolean>
|
||||
) {
|
||||
val total = relays.size
|
||||
val sorted = relays.sortedBy { relayDisplayName(it) }
|
||||
Column(Modifier.background(MaterialTheme.colors.surface)) {
|
||||
RelayBarHeader(relayListExpanded) {
|
||||
if (activeCount + failedCount < total) {
|
||||
RelayProgressIndicator(active = activeCount, total = total)
|
||||
}
|
||||
val statusText = if (failedCount > 0) {
|
||||
String.format(generalGetString(MR.strings.relay_bar_active_with_failures), activeCount, total, failedCount)
|
||||
} else {
|
||||
String.format(generalGetString(MR.strings.relay_bar_active), activeCount, total)
|
||||
}
|
||||
Text(statusText, modifier = Modifier.weight(1f), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
if (relayListExpanded.value) {
|
||||
sorted.forEach { relay ->
|
||||
val failedErr = relayMemberConnFailed(chatModel, relay)
|
||||
RelayBarDetailRow(
|
||||
onClick = if (failedErr != null) {
|
||||
{
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.relay_connection_failed),
|
||||
text = failedErr
|
||||
)
|
||||
}
|
||||
} else null
|
||||
) {
|
||||
Text(
|
||||
relayDisplayName(relay),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
RelayStatusIndicator(relay.relayStatus, connFailed = failedErr != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubscriberChannelRelayBar(
|
||||
hostnames: List<String>,
|
||||
relayMembers: List<GroupMember>,
|
||||
connectedCount: Int,
|
||||
errorCount: Int,
|
||||
total: Int,
|
||||
showProgress: Boolean,
|
||||
relayListExpanded: MutableState<Boolean>
|
||||
) {
|
||||
Column(Modifier.background(MaterialTheme.colors.surface)) {
|
||||
RelayBarHeader(relayListExpanded) {
|
||||
if (showProgress && connectedCount + errorCount < total) {
|
||||
RelayProgressIndicator(active = connectedCount, total = total)
|
||||
}
|
||||
val statusText = if (showProgress) {
|
||||
if (errorCount > 0) {
|
||||
String.format(generalGetString(MR.strings.relay_bar_connected_with_errors), connectedCount, total, errorCount)
|
||||
} else {
|
||||
String.format(generalGetString(MR.strings.relay_bar_connected), connectedCount, total)
|
||||
}
|
||||
} else {
|
||||
String.format(generalGetString(MR.strings.relay_bar_count), total)
|
||||
}
|
||||
Text(statusText, modifier = Modifier.weight(1f), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
if (relayListExpanded.value) {
|
||||
if (relayMembers.isEmpty()) {
|
||||
hostnames.forEach { relay ->
|
||||
RelayBarDetailRow {
|
||||
Text(
|
||||
String.format(generalGetString(MR.strings.via_relay_hostname), hostFromRelayLink(relay)),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
relayMembers.forEach { m ->
|
||||
val host = m.relayLink?.let { hostFromRelayLink(it) }
|
||||
val failedErr = m.activeConn?.connFailedErr
|
||||
RelayBarDetailRow(
|
||||
onClick = if (failedErr != null) {
|
||||
{
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.relay_connection_failed),
|
||||
text = failedErr
|
||||
)
|
||||
}
|
||||
} else null
|
||||
) {
|
||||
Text(
|
||||
String.format(generalGetString(MR.strings.via_relay_hostname), host ?: m.chatViewName),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
val (statusText, statusColor) = relayConnStatus(m)
|
||||
androidx.compose.foundation.Canvas(Modifier.size(8.dp)) {
|
||||
drawCircle(color = statusColor)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(statusText, color = MaterialTheme.colors.secondary, fontSize = 12.sp)
|
||||
if (failedErr != null) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(
|
||||
painterResource(MR.images.ic_error),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayBarHeader(
|
||||
expanded: MutableState<Boolean>,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expanded.value = !expanded.value }
|
||||
.padding(start = 12.dp, end = DEFAULT_PADDING_HALF, top = 8.dp, bottom = if (expanded.value) 4.dp else 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
content()
|
||||
Icon(
|
||||
painterResource(if (expanded.value) MR.images.ic_chevron_down else MR.images.ic_chevron_up),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayBarDetailRow(
|
||||
onClick: (() -> Unit)? = null,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 2.dp)
|
||||
Row(
|
||||
modifier = if (onClick != null) modifier.clickable(onClick = onClick) else modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
private fun relayMemberConnFailed(chatModel: ChatModel, relay: GroupRelay): String? {
|
||||
return chatModel.groupMembers.value
|
||||
.firstOrNull { it.groupMemberId == relay.groupMemberId }
|
||||
?.activeConn?.connFailedErr
|
||||
}
|
||||
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package chat.simplex.common.views.chat.group
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ChannelMembersView(
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
chatModel: ChatModel,
|
||||
close: () -> Unit,
|
||||
showMemberInfo: (GroupMember) -> Unit
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val allMembers = remember { chatModel.groupMembers }.value
|
||||
.filter { m ->
|
||||
m.memberStatus != GroupMemberStatus.MemLeft
|
||||
&& m.memberStatus != GroupMemberStatus.MemRemoved
|
||||
&& m.groupMemberId != groupInfo.membership.groupMemberId
|
||||
}
|
||||
val owners = allMembers.filter { it.memberRole >= GroupMemberRole.Owner }
|
||||
// TODO [relays] subscriber/owner counts require backend support for accurate totals
|
||||
val subscribers = allMembers.filter { it.memberRole < GroupMemberRole.Owner && it.memberRole != GroupMemberRole.Relay }
|
||||
|
||||
ColumnWithScrollBar {
|
||||
val title = if (groupInfo.isOwner) {
|
||||
generalGetString(MR.strings.channel_members_title_owners_and_subscribers)
|
||||
} else {
|
||||
generalGetString(MR.strings.channel_members_section_owners)
|
||||
}
|
||||
AppBarTitle(title)
|
||||
|
||||
SectionView(title = generalGetString(MR.strings.channel_members_section_owners)) {
|
||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Owner) {
|
||||
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
ChannelMemberRow(groupInfo.membership)
|
||||
}
|
||||
}
|
||||
owners.forEachIndexed { index, member ->
|
||||
if (index > 0 || groupInfo.membership.memberRole >= GroupMemberRole.Owner) {
|
||||
Divider()
|
||||
}
|
||||
SectionItemView(
|
||||
click = { showMemberInfo(member) },
|
||||
minHeight = 54.dp,
|
||||
padding = PaddingValues(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
ChannelMemberRow(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groupInfo.isOwner) {
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
SectionView(title = String.format(generalGetString(MR.strings.channel_members_num_subscribers), subscribers.size)) {
|
||||
if (subscribers.isEmpty()) {
|
||||
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(
|
||||
generalGetString(MR.strings.channel_members_no_subscribers),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
subscribers.forEachIndexed { index, member ->
|
||||
if (index > 0) {
|
||||
Divider()
|
||||
}
|
||||
SectionItemView(
|
||||
click = { showMemberInfo(member) },
|
||||
minHeight = 54.dp,
|
||||
padding = PaddingValues(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
ChannelMemberRow(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelMemberRow(member: GroupMember) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
MemberProfileImage(size = 38.dp, member)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (member.verified) {
|
||||
MemberVerifiedShield()
|
||||
}
|
||||
Text(
|
||||
member.chatViewName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (member.memberIncognito) Indigo else Color.Unspecified
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package chat.simplex.common.views.chat.group
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.setGroupMembers
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ChannelRelaysView(
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
chatModel: ChatModel,
|
||||
close: () -> Unit,
|
||||
showMemberInfo: (GroupMember, GroupRelay?) -> Unit
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
var groupRelays by remember { mutableStateOf<List<GroupRelay>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
if (groupInfo.isOwner) {
|
||||
groupRelays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId)
|
||||
}
|
||||
}
|
||||
|
||||
ChannelRelaysLayout(
|
||||
groupInfo = groupInfo,
|
||||
chatModel = chatModel,
|
||||
groupRelays = groupRelays,
|
||||
showMemberInfo = showMemberInfo
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelRelaysLayout(
|
||||
groupInfo: GroupInfo,
|
||||
chatModel: ChatModel,
|
||||
groupRelays: List<GroupRelay>,
|
||||
showMemberInfo: (GroupMember, GroupRelay?) -> Unit
|
||||
) {
|
||||
val relayMembers = remember { chatModel.groupMembers }.value
|
||||
.filter { it.memberRole == GroupMemberRole.Relay }
|
||||
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(generalGetString(MR.strings.channel_relays_title))
|
||||
|
||||
if (relayMembers.isEmpty()) {
|
||||
SectionView {
|
||||
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_chat_relays),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SectionView {
|
||||
relayMembers.forEachIndexed { index, member ->
|
||||
if (index > 0) {
|
||||
Divider()
|
||||
}
|
||||
SectionItemView(
|
||||
click = { showMemberInfo(member, groupRelays.firstOrNull { it.groupMemberId == member.groupMemberId }) },
|
||||
minHeight = 54.dp,
|
||||
padding = PaddingValues(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
val statusText = if (groupInfo.isOwner) {
|
||||
ownerRelayStatusText(member, groupRelays)
|
||||
} else {
|
||||
subscriberRelayStatusText(member)
|
||||
}
|
||||
RelayMemberRow(member, statusText)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTextFooter(generalGetString(MR.strings.chat_relays_forward_messages))
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayMemberRow(member: GroupMember, statusText: String) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
MemberProfileImage(size = 38.dp, member)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
member.chatViewName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Text(
|
||||
statusText,
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscriberRelayStatusText(member: GroupMember): String {
|
||||
return if (member.activeConn?.connDisabled == true) {
|
||||
generalGetString(MR.strings.member_info_member_disabled)
|
||||
} else if (member.activeConn?.connInactive == true) {
|
||||
generalGetString(MR.strings.member_info_member_inactive)
|
||||
} else {
|
||||
relayConnStatus(member).first
|
||||
}
|
||||
}
|
||||
|
||||
private fun ownerRelayStatusText(member: GroupMember, groupRelays: List<GroupRelay>): String {
|
||||
return if (member.activeConn?.connStatus is ConnStatus.Failed) {
|
||||
generalGetString(MR.strings.relay_conn_status_failed)
|
||||
} else if (member.activeConn?.connDisabled == true) {
|
||||
generalGetString(MR.strings.member_info_member_disabled)
|
||||
} else if (member.activeConn?.connInactive == true) {
|
||||
generalGetString(MR.strings.member_info_member_inactive)
|
||||
} else {
|
||||
groupRelays.firstOrNull { it.groupMemberId == member.groupMemberId }?.relayStatus?.text
|
||||
?: relayConnStatus(member).first
|
||||
}
|
||||
}
|
||||
|
||||
fun relayConnStatus(member: GroupMember): Pair<String, Color> {
|
||||
return when (member.activeConn?.connStatus) {
|
||||
is ConnStatus.Ready -> generalGetString(MR.strings.relay_conn_status_connected) to Color.Green
|
||||
is ConnStatus.Deleted -> generalGetString(MR.strings.relay_conn_status_deleted) to Color.Red
|
||||
is ConnStatus.Failed -> generalGetString(MR.strings.relay_conn_status_failed) to Color.Red
|
||||
else -> generalGetString(MR.strings.relay_conn_status_connecting) to WarningYellow
|
||||
}
|
||||
}
|
||||
|
||||
fun hostFromRelayLink(link: String): String {
|
||||
val ft = parseToMarkdown(link)
|
||||
if (ft != null) {
|
||||
for (f in ft) {
|
||||
val format = f.format
|
||||
if (format is Format.SimplexLink) {
|
||||
val host = format.smpHosts.firstOrNull()
|
||||
if (host != null) return host
|
||||
}
|
||||
}
|
||||
}
|
||||
return link
|
||||
}
|
||||
+198
-59
@@ -41,6 +41,7 @@ import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.database.TtlOptions
|
||||
import chat.simplex.common.views.newchat.SimpleXLinkQRCode
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
@@ -114,7 +115,7 @@ fun ModalData.GroupChatInfoView(
|
||||
}
|
||||
}
|
||||
},
|
||||
showMemberInfo = { member ->
|
||||
showMemberInfo = { member, groupRelay ->
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
@@ -126,7 +127,7 @@ fun ModalData.GroupChatInfoView(
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { closeCurrent ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, closeCurrent) {
|
||||
GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, groupRelay = groupRelay, close = closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
@@ -165,7 +166,7 @@ fun ModalData.GroupChatInfoView(
|
||||
clearChat = { clearChatDialog(chat, close) },
|
||||
leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) },
|
||||
manageGroupLink = {
|
||||
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated) }
|
||||
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated, isChannel = groupInfo.useRelays) }
|
||||
},
|
||||
onSearchClicked = onSearchClicked,
|
||||
deletingItems = deletingItems
|
||||
@@ -175,9 +176,14 @@ fun ModalData.GroupChatInfoView(
|
||||
|
||||
fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
val titleId = if (groupInfo.businessChat == null) MR.strings.delete_group_question else MR.strings.delete_chat_question
|
||||
val titleId = if (groupInfo.useRelays) MR.strings.delete_channel_question
|
||||
else if (groupInfo.businessChat == null) MR.strings.delete_group_question
|
||||
else MR.strings.delete_chat_question
|
||||
val messageId =
|
||||
if (groupInfo.businessChat == null) {
|
||||
if (groupInfo.useRelays) {
|
||||
if (groupInfo.membership.memberCurrent) MR.strings.delete_channel_for_all_subscribers_cannot_undo_warning
|
||||
else MR.strings.delete_channel_for_self_cannot_undo_warning
|
||||
} else if (groupInfo.businessChat == null) {
|
||||
if (groupInfo.membership.memberCurrent) MR.strings.delete_group_for_all_members_cannot_undo_warning
|
||||
else MR.strings.delete_group_for_self_cannot_undo_warning
|
||||
} else {
|
||||
@@ -209,8 +215,12 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl
|
||||
}
|
||||
|
||||
fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val titleId = if (groupInfo.businessChat == null) MR.strings.leave_group_question else MR.strings.leave_chat_question
|
||||
val messageId = if (groupInfo.businessChat == null)
|
||||
val titleId = if (groupInfo.useRelays) MR.strings.leave_channel_question
|
||||
else if (groupInfo.businessChat == null) MR.strings.leave_group_question
|
||||
else MR.strings.leave_chat_question
|
||||
val messageId = if (groupInfo.useRelays)
|
||||
MR.strings.you_will_stop_receiving_messages_from_this_channel_chat_history_will_be_preserved
|
||||
else if (groupInfo.businessChat == null)
|
||||
MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved
|
||||
else
|
||||
MR.strings.you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved
|
||||
@@ -229,12 +239,16 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
|
||||
}
|
||||
|
||||
private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMember) {
|
||||
val messageId = if (groupInfo.businessChat == null)
|
||||
val titleId = if (groupInfo.useRelays) MR.strings.button_remove_subscriber_question
|
||||
else MR.strings.button_remove_member_question
|
||||
val messageId = if (groupInfo.useRelays)
|
||||
MR.strings.subscriber_will_be_removed_from_channel_cannot_be_undone
|
||||
else if (groupInfo.businessChat == null)
|
||||
MR.strings.member_will_be_removed_from_group_cannot_be_undone
|
||||
else
|
||||
MR.strings.member_will_be_removed_from_chat_cannot_be_undone
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
generalGetString(MR.strings.button_remove_member_question),
|
||||
generalGetString(titleId),
|
||||
generalGetString(messageId),
|
||||
buttons = {
|
||||
Column {
|
||||
@@ -358,6 +372,22 @@ fun AddGroupMembersButton(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelLinkActionButton(
|
||||
modifier: Modifier,
|
||||
groupInfo: GroupInfo,
|
||||
manageGroupLink: () -> Unit
|
||||
) {
|
||||
InfoViewActionButton(
|
||||
modifier = modifier,
|
||||
icon = painterResource(MR.images.ic_link),
|
||||
title = stringResource(MR.strings.action_button_channel_link),
|
||||
disabled = !groupInfo.ready,
|
||||
disabledLook = !groupInfo.ready,
|
||||
onClick = manageGroupLink
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserSupportChatButton(
|
||||
chat: Chat,
|
||||
@@ -409,7 +439,7 @@ fun ModalData.GroupChatInfoLayout(
|
||||
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
addMembers: () -> Unit,
|
||||
showMemberInfo: (GroupMember) -> Unit,
|
||||
showMemberInfo: (GroupMember, GroupRelay?) -> Unit,
|
||||
editGroupProfile: () -> Unit,
|
||||
addOrEditWelcomeMessage: () -> Unit,
|
||||
openMemberSupport: () -> Unit,
|
||||
@@ -478,14 +508,19 @@ fun ModalData.GroupChatInfoLayout(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val showThreeButtons = if (groupInfo.useRelays) groupInfo.isOwner else groupInfo.canAddMembers
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = if (groupInfo.canAddMembers) 320.dp else 230.dp)
|
||||
.widthIn(max = if (showThreeButtons) 320.dp else 230.dp)
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (groupInfo.canAddMembers) {
|
||||
if (groupInfo.useRelays && groupInfo.isOwner) {
|
||||
SearchButton(modifier = Modifier.fillMaxWidth(0.33f), chat, groupInfo, close, onSearchClicked)
|
||||
ChannelLinkActionButton(modifier = Modifier.fillMaxWidth(0.5f), groupInfo, manageGroupLink)
|
||||
MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo)
|
||||
} else if (!groupInfo.useRelays && groupInfo.canAddMembers) {
|
||||
SearchButton(modifier = Modifier.fillMaxWidth(0.33f), chat, groupInfo, close, onSearchClicked)
|
||||
AddGroupMembersButton(modifier = Modifier.fillMaxWidth(0.5f), chat, groupInfo)
|
||||
MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo)
|
||||
@@ -499,58 +534,88 @@ fun ModalData.GroupChatInfoLayout(
|
||||
SectionSpacer()
|
||||
|
||||
var anyTopSectionRowShow = false
|
||||
SectionView {
|
||||
if (groupInfo.canAddMembers && groupInfo.businessChat == null) {
|
||||
anyTopSectionRowShow = true
|
||||
if (groupLink == null) {
|
||||
CreateGroupLinkButton(manageGroupLink)
|
||||
} else {
|
||||
GroupLinkButton(manageGroupLink)
|
||||
if (groupInfo.useRelays) {
|
||||
SectionView {
|
||||
if (groupInfo.isOwner && groupLink != null) {
|
||||
anyTopSectionRowShow = true
|
||||
ChannelLinkButton(manageGroupLink)
|
||||
} else if (groupInfo.groupProfile.groupLink != null) {
|
||||
anyTopSectionRowShow = true
|
||||
ChannelLinkQRCodeSection(groupInfo.groupProfile.groupLink!!)
|
||||
}
|
||||
if (groupInfo.isOwner || activeSortedMembers.any { it.memberRole >= GroupMemberRole.Owner }) {
|
||||
anyTopSectionRowShow = true
|
||||
ChannelMembersButton(chat.remoteHostId, groupInfo, showMemberInfo)
|
||||
}
|
||||
}
|
||||
if (groupInfo.businessChat == null && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||
anyTopSectionRowShow = true
|
||||
MemberSupportButton(chat, openMemberSupport)
|
||||
if (!groupInfo.isOwner && groupInfo.groupProfile.groupLink != null) {
|
||||
SectionTextFooter(stringResource(MR.strings.you_can_share_channel_link_anybody_will_be_able_to_connect))
|
||||
}
|
||||
if (groupInfo.canModerate) {
|
||||
anyTopSectionRowShow = true
|
||||
GroupReportsButton(chat) {
|
||||
scope.launch {
|
||||
showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
|
||||
} else {
|
||||
SectionView {
|
||||
if (groupInfo.canAddMembers && groupInfo.businessChat == null) {
|
||||
anyTopSectionRowShow = true
|
||||
if (groupLink == null) {
|
||||
CreateGroupLinkButton(manageGroupLink)
|
||||
} else {
|
||||
GroupLinkButton(manageGroupLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
groupInfo.membership.memberActive &&
|
||||
(groupInfo.membership.memberRole < GroupMemberRole.Moderator || groupInfo.membership.supportChat != null)
|
||||
) {
|
||||
anyTopSectionRowShow = true
|
||||
UserSupportChatButton(chat, groupInfo, scrollToItemId)
|
||||
if (groupInfo.businessChat == null && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||
anyTopSectionRowShow = true
|
||||
MemberSupportButton(chat, openMemberSupport)
|
||||
}
|
||||
if (groupInfo.canModerate) {
|
||||
anyTopSectionRowShow = true
|
||||
GroupReportsButton(chat) {
|
||||
scope.launch {
|
||||
showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
groupInfo.membership.memberActive &&
|
||||
(groupInfo.membership.memberRole < GroupMemberRole.Moderator || groupInfo.membership.supportChat != null)
|
||||
) {
|
||||
anyTopSectionRowShow = true
|
||||
UserSupportChatButton(chat, groupInfo, scrollToItemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
val showEditSection = (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)
|
||||
|| groupInfo.groupProfile.description != null
|
||||
|| !groupInfo.useRelays
|
||||
if (anyTopSectionRowShow) {
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
}
|
||||
|
||||
SectionView {
|
||||
if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) {
|
||||
EditGroupProfileButton(editGroupProfile)
|
||||
if (showEditSection) {
|
||||
SectionView {
|
||||
if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) {
|
||||
val editProfileTitleId = if (groupInfo.useRelays) MR.strings.button_edit_channel_profile else MR.strings.button_edit_group_profile
|
||||
EditGroupProfileButton(editProfileTitleId, editGroupProfile)
|
||||
}
|
||||
if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) {
|
||||
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
|
||||
}
|
||||
if (!groupInfo.useRelays) {
|
||||
val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
|
||||
GroupPreferencesButton(prefsTitleId, openPreferences)
|
||||
}
|
||||
}
|
||||
if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) {
|
||||
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
|
||||
if (!groupInfo.useRelays) {
|
||||
val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs
|
||||
SectionTextFooter(stringResource(footerId))
|
||||
}
|
||||
val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
|
||||
GroupPreferencesButton(prefsTitleId, openPreferences)
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
}
|
||||
val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs
|
||||
SectionTextFooter(stringResource(footerId))
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
|
||||
SectionView {
|
||||
if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
} else {
|
||||
SendReceiptsOptionDisabled()
|
||||
if (!groupInfo.useRelays) {
|
||||
if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
} else {
|
||||
SendReceiptsOptionDisabled()
|
||||
}
|
||||
}
|
||||
WallpaperButton {
|
||||
ModalManager.end.showModal {
|
||||
@@ -566,7 +631,7 @@ fun ModalData.GroupChatInfoLayout(
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true)
|
||||
|
||||
if (!groupInfo.nextConnectPrepared) {
|
||||
if (!groupInfo.nextConnectPrepared && !groupInfo.useRelays) {
|
||||
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) {
|
||||
if (groupInfo.canAddMembers) {
|
||||
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
|
||||
@@ -589,7 +654,7 @@ fun ModalData.GroupChatInfoLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!groupInfo.nextConnectPrepared) {
|
||||
if (!groupInfo.nextConnectPrepared && !groupInfo.useRelays) {
|
||||
items(filteredMembers.value, key = { it.groupMemberId }) { member ->
|
||||
Divider()
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
@@ -601,7 +666,7 @@ fun ModalData.GroupChatInfoLayout(
|
||||
toggleItemSelection(member.groupMemberId, selectedItems)
|
||||
}
|
||||
} else {
|
||||
showMemberInfo(member)
|
||||
showMemberInfo(member, null)
|
||||
}
|
||||
},
|
||||
longClick = { showMenu.value = true },
|
||||
@@ -622,18 +687,30 @@ fun ModalData.GroupChatInfoLayout(
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (!groupInfo.nextConnectPrepared) {
|
||||
if (!groupInfo.nextConnectPrepared && !groupInfo.useRelays) {
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
}
|
||||
SectionView {
|
||||
if (groupInfo.useRelays && (groupInfo.isOwner || activeSortedMembers.any { it.memberRole == GroupMemberRole.Relay })) {
|
||||
ChannelRelaysButton(chat.remoteHostId, groupInfo, showMemberInfo)
|
||||
}
|
||||
ClearChatButton(clearChat)
|
||||
if (groupInfo.canDelete) {
|
||||
val titleId = if (groupInfo.businessChat == null) MR.strings.button_delete_group else MR.strings.button_delete_chat
|
||||
val titleId = if (groupInfo.useRelays) MR.strings.button_delete_channel
|
||||
else if (groupInfo.businessChat == null) MR.strings.button_delete_group
|
||||
else MR.strings.button_delete_chat
|
||||
DeleteGroupButton(titleId, deleteGroup)
|
||||
}
|
||||
if (groupInfo.membership.memberCurrentOrPending) {
|
||||
val titleId = if (groupInfo.businessChat == null) MR.strings.button_leave_group else MR.strings.button_leave_chat
|
||||
LeaveGroupButton(titleId, leaveGroup)
|
||||
val hasOtherOwner = activeSortedMembers.any {
|
||||
it.memberRole == GroupMemberRole.Owner && it.groupMemberId != groupInfo.membership.groupMemberId
|
||||
}
|
||||
if (!groupInfo.useRelays || !groupInfo.isOwner || hasOtherOwner) {
|
||||
val titleId = if (groupInfo.useRelays) MR.strings.button_leave_channel
|
||||
else if (groupInfo.businessChat == null) MR.strings.button_leave_group
|
||||
else MR.strings.button_leave_chat
|
||||
LeaveGroupButton(titleId, leaveGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1016,10 +1093,72 @@ private fun CreateGroupLinkButton(onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditGroupProfileButton(onClick: () -> Unit) {
|
||||
private fun ChannelLinkButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_link),
|
||||
stringResource(MR.strings.channel_link),
|
||||
onClick,
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelLinkQRCodeSection(groupLink: String) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SimpleXLinkQRCode(connReq = groupLink)
|
||||
SectionItemView({
|
||||
clipboard.shareText(simplexChatLink(groupLink))
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_share), null, tint = MaterialTheme.colors.primary)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(MR.strings.share_link), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelMembersButton(rhId: Long?, groupInfo: GroupInfo, showMemberInfo: (GroupMember, GroupRelay?) -> Unit) {
|
||||
val title = if (groupInfo.isOwner) {
|
||||
stringResource(MR.strings.channel_members_title_owners_and_subscribers)
|
||||
} else {
|
||||
stringResource(MR.strings.channel_members_section_owners)
|
||||
}
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_group),
|
||||
title,
|
||||
click = {
|
||||
withBGApi {
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
ChannelMembersView(rhId, groupInfo, chatModel, close) { member -> showMemberInfo(member, null) }
|
||||
}
|
||||
}
|
||||
},
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelRelaysButton(rhId: Long?, groupInfo: GroupInfo, showMemberInfo: (GroupMember, GroupRelay?) -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_wifi_tethering),
|
||||
stringResource(MR.strings.button_channel_relays),
|
||||
click = {
|
||||
withBGApi {
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
ChannelRelaysView(rhId, groupInfo, chatModel, close, showMemberInfo)
|
||||
}
|
||||
}
|
||||
},
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditGroupProfileButton(titleId: StringResource = MR.strings.button_edit_group_profile, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_edit),
|
||||
stringResource(MR.strings.button_edit_group_profile),
|
||||
stringResource(titleId),
|
||||
onClick,
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
)
|
||||
@@ -1147,7 +1286,7 @@ fun PreviewGroupChatInfoLayout() {
|
||||
appBar = remember { mutableStateOf(null) },
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
addMembers = {},
|
||||
showMemberInfo = {},
|
||||
showMemberInfo = { _, _ -> },
|
||||
editGroupProfile = {},
|
||||
addOrEditWelcomeMessage = {},
|
||||
openMemberSupport = {},
|
||||
|
||||
+17
-12
@@ -32,6 +32,7 @@ fun GroupLinkView(
|
||||
groupLink: GroupLink?,
|
||||
onGroupLinkUpdated: ((GroupLink?) -> Unit)?,
|
||||
creatingGroup: Boolean = false,
|
||||
isChannel: Boolean = false,
|
||||
close: (() -> Unit)? = null
|
||||
) {
|
||||
var groupLinkVar by rememberSaveable(stateSaver = GroupLink.nullableStateSaver) { mutableStateOf(groupLink) }
|
||||
@@ -122,6 +123,7 @@ fun GroupLinkView(
|
||||
groupInfo,
|
||||
groupLinkMemberRole,
|
||||
creatingLink,
|
||||
isChannel = isChannel,
|
||||
createLink = ::createLink,
|
||||
showAddShortLinkAlert = ::showAddShortLinkAlert,
|
||||
updateLink = {
|
||||
@@ -168,6 +170,7 @@ fun GroupLinkLayout(
|
||||
groupInfo: GroupInfo,
|
||||
groupLinkMemberRole: MutableState<GroupMemberRole?>,
|
||||
creatingLink: Boolean,
|
||||
isChannel: Boolean = false,
|
||||
createLink: () -> Unit,
|
||||
showAddShortLinkAlert: ((() -> Unit)?) -> Unit,
|
||||
updateLink: () -> Unit,
|
||||
@@ -185,9 +188,9 @@ fun GroupLinkLayout(
|
||||
}
|
||||
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.group_link))
|
||||
AppBarTitle(stringResource(if (isChannel) MR.strings.channel_link else MR.strings.group_link))
|
||||
Text(
|
||||
stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect),
|
||||
stringResource(if (isChannel) MR.strings.you_can_share_channel_link_anybody_will_be_able_to_connect else MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect),
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = 12.dp),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
@@ -208,7 +211,9 @@ fun GroupLinkLayout(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
RoleSelectionRow(groupInfo, groupLinkMemberRole)
|
||||
if (!isChannel) {
|
||||
RoleSelectionRow(groupInfo, groupLinkMemberRole)
|
||||
}
|
||||
var initialLaunch by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(groupLinkMemberRole.value) {
|
||||
if (!initialLaunch) {
|
||||
@@ -218,12 +223,12 @@ fun GroupLinkLayout(
|
||||
}
|
||||
val showShortLink = remember { mutableStateOf(true) }
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
if (groupLink.connLinkContact.connShortLink == null) {
|
||||
SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = false)
|
||||
} else {
|
||||
SectionViewWithButton(titleButton = { ToggleShortLinkButton(showShortLink) }) {
|
||||
SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = showShortLink.value)
|
||||
}
|
||||
SectionViewWithButton(
|
||||
titleButton =
|
||||
if (!isChannel && groupLink.connLinkContact.connShortLink != null) {
|
||||
{ ToggleShortLinkButton(showShortLink) }
|
||||
} else null) {
|
||||
SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = showShortLink.value)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
@@ -235,7 +240,7 @@ fun GroupLinkLayout(
|
||||
stringResource(MR.strings.share_link),
|
||||
icon = painterResource(MR.images.ic_share),
|
||||
click = {
|
||||
if (groupLink.shouldBeUpgraded) {
|
||||
if (!isChannel && groupLink.shouldBeUpgraded) {
|
||||
showAddShortLinkAlert {
|
||||
clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value))
|
||||
}
|
||||
@@ -246,7 +251,7 @@ fun GroupLinkLayout(
|
||||
)
|
||||
if (creatingGroup && close != null) {
|
||||
ContinueButton(close)
|
||||
} else {
|
||||
} else if (!isChannel) {
|
||||
SimpleButton(
|
||||
stringResource(MR.strings.delete_link),
|
||||
icon = painterResource(MR.images.ic_delete),
|
||||
@@ -255,7 +260,7 @@ fun GroupLinkLayout(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (groupLink.shouldBeUpgraded) {
|
||||
if (!isChannel && groupLink.shouldBeUpgraded) {
|
||||
AddShortLinkButton(text = stringResource(MR.strings.upgrade_group_link)) {
|
||||
showAddShortLinkAlert(null)
|
||||
}
|
||||
|
||||
+134
-76
@@ -50,6 +50,7 @@ fun GroupMemberInfoView(
|
||||
connectionCode: String?,
|
||||
chatModel: ChatModel,
|
||||
openedFromSupportChat: Boolean,
|
||||
groupRelay: GroupRelay? = null,
|
||||
close: () -> Unit,
|
||||
closeAll: () -> Unit, // Close all open windows up to ChatView
|
||||
) {
|
||||
@@ -90,6 +91,7 @@ fun GroupMemberInfoView(
|
||||
newRole,
|
||||
developerTools,
|
||||
connectionCode,
|
||||
groupRelay = groupRelay,
|
||||
getContactChat = { chatModel.getContactChat(it) },
|
||||
openDirectChat = { contactId ->
|
||||
scope.launch {
|
||||
@@ -311,6 +313,7 @@ fun GroupMemberInfoLayout(
|
||||
newRole: MutableState<GroupMemberRole>,
|
||||
developerTools: Boolean,
|
||||
connectionCode: String?,
|
||||
groupRelay: GroupRelay? = null,
|
||||
getContactChat: (Long) -> Chat?,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
createMemberContact: () -> Unit,
|
||||
@@ -365,7 +368,7 @@ fun GroupMemberInfoLayout(
|
||||
@Composable
|
||||
fun ModeratorDestructiveSection() {
|
||||
val canBlockForAll = member.canBlockForAll(groupInfo)
|
||||
val canRemove = member.canBeRemoved(groupInfo)
|
||||
val canRemove = member.canBeRemoved(groupInfo) && member.memberRole != GroupMemberRole.Relay
|
||||
if (canBlockForAll || canRemove) {
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
SectionView {
|
||||
@@ -380,7 +383,7 @@ fun GroupMemberInfoLayout(
|
||||
if (member.memberStatus == GroupMemberStatus.MemRemoved || member.memberStatus == GroupMemberStatus.MemLeft) {
|
||||
DeleteMemberMessagesButton(deleteMemberMessages)
|
||||
} else {
|
||||
RemoveMemberButton(removeMember)
|
||||
RemoveMemberButton(groupInfo.useRelays, removeMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,77 +420,80 @@ fun GroupMemberInfoLayout(
|
||||
|
||||
val contactId = member.memberContactId
|
||||
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = 320.dp)
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
if (!groupInfo.useRelays) {
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
|
||||
if (knownChat != null) {
|
||||
val (chat, contact) = knownChat
|
||||
val knownContactConnectionStats: MutableState<ConnectionStats?> = remember { mutableStateOf(null) }
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = 320.dp)
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
|
||||
if (knownChat != null) {
|
||||
val (chat, contact) = knownChat
|
||||
val knownContactConnectionStats: MutableState<ConnectionStats?> = remember { mutableStateOf(null) }
|
||||
|
||||
LaunchedEffect(contact.contactId) {
|
||||
withBGApi {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, chat.chatInfo.apiId)
|
||||
if (contactInfo != null) {
|
||||
knownContactConnectionStats.value = contactInfo.first
|
||||
LaunchedEffect(contact.contactId) {
|
||||
withBGApi {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, chat.chatInfo.apiId)
|
||||
if (contactInfo != null) {
|
||||
knownContactConnectionStats.value = contactInfo.first
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contact.contactId) })
|
||||
AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact, knownContactConnectionStats)
|
||||
VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact, knownContactConnectionStats)
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group
|
||||
} else {
|
||||
OpenChatButton(
|
||||
modifier = Modifier.fillMaxWidth(0.33f),
|
||||
disabledLook = !(member.sendMsgEnabled || (member.activeConn?.connectionStats?.ratchetSyncAllowed ?: false)),
|
||||
onClick = { createMemberContact() }
|
||||
)
|
||||
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contact.contactId) })
|
||||
AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact, knownContactConnectionStats)
|
||||
VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact, knownContactConnectionStats)
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group
|
||||
} else {
|
||||
OpenChatButton(
|
||||
modifier = Modifier.fillMaxWidth(0.33f),
|
||||
disabledLook = !(member.sendMsgEnabled || (member.activeConn?.connectionStats?.ratchetSyncAllowed ?: false)),
|
||||
onClick = { createMemberContact() }
|
||||
)
|
||||
}
|
||||
InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
|
||||
showSendMessageToEnableCallsAlert()
|
||||
})
|
||||
InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
|
||||
showSendMessageToEnableCallsAlert()
|
||||
})
|
||||
} else { // no known contact chat && directMessages are off
|
||||
val messageId = if (groupInfo.businessChat == null) MR.strings.direct_messages_are_prohibited_in_group else MR.strings.direct_messages_are_prohibited_in_chat
|
||||
InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.33f), painterResource(MR.images.ic_chat_bubble), generalGetString(MR.strings.info_view_message_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title), messageId)
|
||||
})
|
||||
InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title), messageId)
|
||||
})
|
||||
InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title), messageId)
|
||||
})
|
||||
}
|
||||
InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
|
||||
showSendMessageToEnableCallsAlert()
|
||||
})
|
||||
InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
|
||||
showSendMessageToEnableCallsAlert()
|
||||
})
|
||||
} else { // no known contact chat && directMessages are off
|
||||
val messageId = if (groupInfo.businessChat == null) MR.strings.direct_messages_are_prohibited_in_group else MR.strings.direct_messages_are_prohibited_in_chat
|
||||
InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.33f), painterResource(MR.images.ic_chat_bubble), generalGetString(MR.strings.info_view_message_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title), messageId)
|
||||
})
|
||||
InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title), messageId)
|
||||
})
|
||||
InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title), messageId)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
SectionSpacer()
|
||||
}
|
||||
|
||||
if (member.memberActive) {
|
||||
SectionView {
|
||||
if (
|
||||
!openedFromSupportChat &&
|
||||
groupInfo.membership.memberRole >= GroupMemberRole.Moderator &&
|
||||
member.memberRole != GroupMemberRole.Relay &&
|
||||
(member.memberRole < GroupMemberRole.Moderator || member.supportChat != null)
|
||||
) {
|
||||
SupportChatButton()
|
||||
}
|
||||
if (connectionCode != null) {
|
||||
if (connectionCode != null && !(groupInfo.useRelays && member.memberRole == GroupMemberRole.Relay)) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
}
|
||||
if (cStats != null && cStats.ratchetSyncAllowed) {
|
||||
@@ -517,15 +523,46 @@ fun GroupMemberInfoLayout(
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
SectionView(title = stringResource(MR.strings.member_info_section_title_member)) {
|
||||
val titleId = if (groupInfo.businessChat == null) MR.strings.info_row_group else MR.strings.info_row_chat
|
||||
val memberSectionTitle = if (groupInfo.useRelays) {
|
||||
when (member.memberRole) {
|
||||
GroupMemberRole.Relay -> stringResource(MR.strings.member_info_section_title_relay)
|
||||
GroupMemberRole.Owner -> stringResource(MR.strings.member_info_section_title_owner)
|
||||
else -> stringResource(MR.strings.member_info_section_title_subscriber)
|
||||
}
|
||||
} else {
|
||||
stringResource(MR.strings.member_info_section_title_member)
|
||||
}
|
||||
SectionView(title = memberSectionTitle) {
|
||||
val titleId = if (groupInfo.useRelays) MR.strings.info_row_channel
|
||||
else if (groupInfo.businessChat == null) MR.strings.info_row_group
|
||||
else MR.strings.info_row_chat
|
||||
InfoRow(stringResource(titleId), groupInfo.displayName)
|
||||
val roles = remember { member.canChangeRoleTo(groupInfo) }
|
||||
if (roles != null) {
|
||||
RoleSelectionRow(roles, newRole, onRoleSelected)
|
||||
if (!groupInfo.useRelays) {
|
||||
val roles = remember { member.canChangeRoleTo(groupInfo) }
|
||||
if (roles != null) {
|
||||
RoleSelectionRow(roles, newRole, onRoleSelected)
|
||||
} else {
|
||||
InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text)
|
||||
}
|
||||
} else {
|
||||
InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text)
|
||||
}
|
||||
val relayLink = member.relayLink
|
||||
if (relayLink != null) {
|
||||
InfoRow(stringResource(MR.strings.info_row_relay_link), String.format(generalGetString(MR.strings.via_relay_hostname), hostFromRelayLink(relayLink)))
|
||||
}
|
||||
val relayAddress = groupRelay?.userChatRelay?.address
|
||||
if (relayAddress != null) {
|
||||
InfoRow(stringResource(MR.strings.info_row_relay_address), String.format(generalGetString(MR.strings.via_relay_hostname), hostFromRelayLink(relayAddress)))
|
||||
val clipboard = LocalClipboardManager.current
|
||||
ShareRelayAddressButton { clipboard.shareText(simplexChatLink(relayAddress)) }
|
||||
}
|
||||
}
|
||||
if (groupInfo.useRelays && member.memberRole == GroupMemberRole.Relay) {
|
||||
SectionTextFooter(
|
||||
if (groupInfo.isOwner) stringResource(MR.strings.relay_section_footer_owner)
|
||||
else stringResource(MR.strings.relay_section_footer_subscriber)
|
||||
)
|
||||
}
|
||||
if (cStats != null) {
|
||||
SectionDividerSpaced()
|
||||
@@ -565,14 +602,19 @@ fun GroupMemberInfoLayout(
|
||||
val connFailedErr = member.activeConn?.connFailedErr
|
||||
if (connFailedErr != null) {
|
||||
SectionDividerSpaced()
|
||||
SectionView {
|
||||
InfoRow(stringResource(MR.strings.info_row_connection_failed), connFailedErr)
|
||||
SectionView(title = stringResource(MR.strings.info_row_connection_failed), icon = painterResource(MR.images.ic_warning), iconTint = Color.Red, leadingIcon = true) {
|
||||
SectionItemView {
|
||||
Text(
|
||||
connFailedErr,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||
ModeratorDestructiveSection()
|
||||
} else {
|
||||
} else if (!groupInfo.useRelays) {
|
||||
NonAdminBlockSection()
|
||||
}
|
||||
|
||||
@@ -588,18 +630,20 @@ fun GroupMemberInfoLayout(
|
||||
else String.format(generalGetString(MR.strings.conn_level_desc_indirect), conn.connLevel)
|
||||
InfoRow(stringResource(MR.strings.info_row_connection), connLevelDesc)
|
||||
}
|
||||
SectionItemView({
|
||||
withBGApi {
|
||||
val info = controller.apiGroupMemberQueueInfo(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (info != null) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.message_queue_info),
|
||||
text = queueInfoText(info)
|
||||
)
|
||||
if (!groupInfo.useRelays || member.memberRole == GroupMemberRole.Relay) {
|
||||
SectionItemView({
|
||||
withBGApi {
|
||||
val info = controller.apiGroupMemberQueueInfo(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (info != null) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.message_queue_info),
|
||||
text = queueInfoText(info)
|
||||
)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(MR.strings.info_row_debug_delivery))
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(MR.strings.info_row_debug_delivery))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -703,10 +747,11 @@ fun UnblockForAllButton(onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RemoveMemberButton(onClick: () -> Unit) {
|
||||
fun RemoveMemberButton(useRelays: Boolean = false, onClick: () -> Unit) {
|
||||
val label = if (useRelays) MR.strings.button_remove_subscriber else MR.strings.button_remove_member
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_delete),
|
||||
stringResource(MR.strings.button_remove_member),
|
||||
stringResource(label),
|
||||
click = onClick,
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
@@ -724,6 +769,17 @@ fun DeleteMemberMessagesButton(onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShareRelayAddressButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_share_filled),
|
||||
stringResource(MR.strings.share_relay_address),
|
||||
onClick,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OpenChatButton(
|
||||
modifier: Modifier,
|
||||
@@ -908,8 +964,9 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem
|
||||
}
|
||||
|
||||
fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
||||
val titleId = if (gInfo.useRelays) MR.strings.block_subscriber_for_all_question else MR.strings.block_for_all_question
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.block_for_all_question),
|
||||
title = generalGetString(titleId),
|
||||
text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.block_for_all),
|
||||
onConfirm = {
|
||||
@@ -932,8 +989,9 @@ fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, onSuc
|
||||
}
|
||||
|
||||
fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
||||
val titleId = if (gInfo.useRelays) MR.strings.unblock_subscriber_for_all_question else MR.strings.unblock_for_all_question
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.unblock_for_all_question),
|
||||
title = generalGetString(titleId),
|
||||
text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.unblock_for_all),
|
||||
onConfirm = {
|
||||
|
||||
+2
-2
@@ -316,7 +316,7 @@ fun GroupMenuItems(
|
||||
}
|
||||
}
|
||||
GroupMemberStatus.MemAccepted -> {
|
||||
if (groupInfo.membership.memberCurrentOrPending) {
|
||||
if (groupInfo.membership.memberCurrentOrPending && !(groupInfo.useRelays && groupInfo.isOwner)) {
|
||||
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
|
||||
}
|
||||
if (groupInfo.canDelete) {
|
||||
@@ -338,7 +338,7 @@ fun GroupMenuItems(
|
||||
}
|
||||
}
|
||||
ClearChatAction(chat, showMenu)
|
||||
if (groupInfo.membership.memberCurrentOrPending) {
|
||||
if (groupInfo.membership.memberCurrentOrPending && !(groupInfo.useRelays && groupInfo.isOwner)) {
|
||||
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
|
||||
}
|
||||
if (groupInfo.canDelete) {
|
||||
|
||||
+581
@@ -0,0 +1,581 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.getUserServers
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.*
|
||||
import chat.simplex.common.views.chat.group.GroupLinkView
|
||||
import chat.simplex.common.views.chatlist.openGroupChat
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.NetworkAndServersView
|
||||
import chat.simplex.common.views.chat.group.hostFromRelayLink
|
||||
import chat.simplex.res.MR
|
||||
import java.net.URI
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@Composable
|
||||
fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit) {
|
||||
val view = LocalMultiplatformView()
|
||||
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val displayName = rememberSaveable { mutableStateOf("") }
|
||||
val chosenImage = rememberSaveable { mutableStateOf<URI?>(null) }
|
||||
val profileImage = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val hasRelays = rememberSaveable { mutableStateOf(true) }
|
||||
val groupInfo = remember { mutableStateOf<GroupInfo?>(null) }
|
||||
val groupLink = rememberSaveable(stateSaver = GroupLink.nullableStateSaver) { mutableStateOf<GroupLink?>(null) }
|
||||
val groupRelays = remember { mutableStateOf<List<GroupRelay>>(emptyList()) }
|
||||
val creationInProgress = rememberSaveable { mutableStateOf(false) }
|
||||
val showLinkStep = rememberSaveable { mutableStateOf(false) }
|
||||
val relayListExpanded = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val gInfo = groupInfo.value
|
||||
if (showLinkStep.value && gInfo != null) {
|
||||
LinkStepView(chatModel, gInfo, groupLink, closeAll)
|
||||
} else if (gInfo != null) {
|
||||
ProgressStepView(
|
||||
chatModel, gInfo, groupRelays, relayListExpanded,
|
||||
onLinkReady = if (appPlatform.isDesktop) {
|
||||
{
|
||||
chatModel.creatingChannelId.value = null
|
||||
closeAll()
|
||||
withBGApi {
|
||||
openGroupChat(null, gInfo.groupId)
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
GroupLinkView(chatModel, rhId = null, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, close = close)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
{ showLinkStep.value = true }
|
||||
},
|
||||
cancelChannelCreation = {
|
||||
chatModel.creatingChannelId.value = null
|
||||
ChannelRelaysModel.reset()
|
||||
closeAll()
|
||||
withBGApi {
|
||||
try {
|
||||
chatModel.controller.apiDeleteChat(rh = null, type = ChatType.Group, id = gInfo.apiId)
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.removeChat(null, gInfo.id)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "cancelChannelCreation error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
ProfileStepView(
|
||||
chatModel = chatModel,
|
||||
displayName = displayName,
|
||||
profileImage = profileImage,
|
||||
chosenImage = chosenImage,
|
||||
focusRequester = focusRequester,
|
||||
hasRelays = hasRelays,
|
||||
creationInProgress = creationInProgress,
|
||||
bottomSheetModalState = bottomSheetModalState,
|
||||
scope = scope,
|
||||
view = view,
|
||||
close = close,
|
||||
createChannel = {
|
||||
hideKeyboard(view)
|
||||
val trimmedName = displayName.value.trim()
|
||||
displayName.value = trimmedName
|
||||
val profile = GroupProfile(
|
||||
displayName = trimmedName,
|
||||
fullName = "",
|
||||
shortDescr = null,
|
||||
image = profileImage.value,
|
||||
groupPreferences = GroupPreferences(history = GroupPreference(GroupFeatureEnabled.ON))
|
||||
)
|
||||
creationInProgress.value = true
|
||||
withBGApi {
|
||||
try {
|
||||
val enabledRelays = getEnabledRelays()
|
||||
val relayIds = enabledRelays.mapNotNull { it.chatRelayId }
|
||||
if (relayIds.isEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
creationInProgress.value = false
|
||||
hasRelays.value = false
|
||||
}
|
||||
return@withBGApi
|
||||
}
|
||||
val result = chatModel.controller.apiNewPublicGroup(
|
||||
rh = null,
|
||||
incognito = false,
|
||||
relayIds = relayIds,
|
||||
groupProfile = profile
|
||||
)
|
||||
if (result != null) {
|
||||
val (gI, gL, gR) = result
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId = null, gI)
|
||||
chatModel.creatingChannelId.value = gI.id
|
||||
groupInfo.value = gI
|
||||
groupLink.value = gL
|
||||
groupRelays.value = gR.sortedBy { relayDisplayName(it) }
|
||||
ChannelRelaysModel.set(gI.groupId, gR)
|
||||
creationInProgress.value = false
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) { creationInProgress.value = false }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
creationInProgress.value = false
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.error_creating_channel),
|
||||
text = e.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getEnabledRelays(): List<UserChatRelay> {
|
||||
val servers = getUserServers(rh = null) ?: return emptyList()
|
||||
val all = servers.flatMap { op ->
|
||||
op.chatRelays.filter { it.enabled && !it.deleted && it.chatRelayId != null }
|
||||
}
|
||||
return all.shuffled().take(3)
|
||||
}
|
||||
|
||||
private suspend fun checkHasRelays(): Boolean {
|
||||
val servers = try { getUserServers(rh = null) } catch (_: Exception) { null } ?: return false
|
||||
return servers.any { op ->
|
||||
op.chatRelays.any { it.enabled && !it.deleted && it.chatRelayId != null }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileStepView(
|
||||
chatModel: ChatModel,
|
||||
displayName: MutableState<String>,
|
||||
profileImage: MutableState<String?>,
|
||||
chosenImage: MutableState<URI?>,
|
||||
focusRequester: FocusRequester,
|
||||
hasRelays: MutableState<Boolean>,
|
||||
creationInProgress: MutableState<Boolean>,
|
||||
bottomSheetModalState: ModalBottomSheetState,
|
||||
scope: CoroutineScope,
|
||||
view: Any?,
|
||||
close: () -> Unit,
|
||||
createChannel: () -> Unit
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
hasRelays.value = checkHasRelays()
|
||||
}
|
||||
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
modifier = Modifier.imePadding(),
|
||||
sheetContent = {
|
||||
GetImageBottomSheet(
|
||||
chosenImage,
|
||||
onImageChange = { bitmap -> profileImage.value = resizeImageToStrSize(cropToSquare(bitmap), maxDataSize = 12500) },
|
||||
hideBottomSheet = {
|
||||
scope.launch { bottomSheetModalState.hide() }
|
||||
}
|
||||
)
|
||||
},
|
||||
sheetState = bottomSheetModalState,
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
ModalView(close = close) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(generalGetString(MR.strings.create_channel_title))
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
ProfileImage(108.dp, image = profileImage.value)
|
||||
EditImageButton { scope.launch { bottomSheetModalState.show() } }
|
||||
}
|
||||
if (profileImage.value != null) {
|
||||
DeleteImageButton { profileImage.value = null }
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.channel_display_name_field),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
if (!isValidDisplayName(displayName.value.trim())) {
|
||||
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
|
||||
IconButton({ showInvalidNameAlert(mkValidName(displayName.value.trim()), displayName) }, Modifier.size(20.dp)) {
|
||||
Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_wifi_tethering),
|
||||
generalGetString(MR.strings.configure_relays),
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
NetworkAndServersView(close)
|
||||
}
|
||||
},
|
||||
textColor = if (hasRelays.value) MaterialTheme.colors.primary else WarningOrange,
|
||||
iconColor = if (hasRelays.value) MaterialTheme.colors.primary else WarningOrange
|
||||
)
|
||||
|
||||
val canCreate = canCreateProfile(displayName.value) && hasRelays.value && !creationInProgress.value
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_check),
|
||||
generalGetString(MR.strings.create_channel_button),
|
||||
click = createChannel,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
disabled = !canCreate
|
||||
)
|
||||
|
||||
SectionTextFooter(
|
||||
if (!hasRelays.value) {
|
||||
generalGetString(MR.strings.enable_at_least_one_chat_relay)
|
||||
} else {
|
||||
val name = chatModel.currentUser.value?.displayName ?: ""
|
||||
String.format(generalGetString(MR.strings.your_profile_shared_with_channel_relays), name)
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(1000)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressStepView(
|
||||
chatModel: ChatModel,
|
||||
gInfo: GroupInfo,
|
||||
groupRelays: MutableState<List<GroupRelay>>,
|
||||
relayListExpanded: MutableState<Boolean>,
|
||||
onLinkReady: () -> Unit,
|
||||
cancelChannelCreation: () -> Unit
|
||||
) {
|
||||
val failedCount = groupRelays.value.count { relayMemberConnFailed(chatModel, it) != null }
|
||||
val activeCount = groupRelays.value.count { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null }
|
||||
val total = groupRelays.value.size
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
DisposableEffect(Unit) {
|
||||
chatModel.centerPanelBackgroundClickHandler = {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.cancel_creating_channel_question),
|
||||
confirmText = generalGetString(MR.strings.cancel_creating_channel_confirm),
|
||||
onConfirm = cancelChannelCreation,
|
||||
dismissText = generalGetString(MR.strings.wait_verb),
|
||||
destructive = true,
|
||||
)
|
||||
true
|
||||
}
|
||||
onDispose {
|
||||
chatModel.centerPanelBackgroundClickHandler = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(gInfo.groupId) {
|
||||
snapshotFlow { ChannelRelaysModel.groupRelays.toList() }
|
||||
.collect { relays ->
|
||||
if (ChannelRelaysModel.groupId.value != gInfo.groupId) return@collect
|
||||
groupRelays.value = relays.sortedBy { relayDisplayName(it) }
|
||||
if (relays.all { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null }) {
|
||||
onLinkReady()
|
||||
ChannelRelaysModel.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModalView(
|
||||
close = cancelChannelCreation,
|
||||
showClose = false,
|
||||
endButtons = {
|
||||
TextButton(onClick = cancelChannelCreation) {
|
||||
Text(generalGetString(MR.strings.cancel_verb))
|
||||
}
|
||||
}
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(generalGetString(MR.strings.creating_channel))
|
||||
|
||||
Box(
|
||||
Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProfileImage(108.dp, image = gInfo.groupProfile.image)
|
||||
}
|
||||
Text(
|
||||
gInfo.groupProfile.displayName,
|
||||
style = MaterialTheme.typography.h6,
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
SectionView {
|
||||
SectionItemView(click = { relayListExpanded.value = !relayListExpanded.value }) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (activeCount + failedCount < total) {
|
||||
RelayProgressIndicator(active = activeCount, total = total)
|
||||
}
|
||||
val statusText = if (failedCount > 0) {
|
||||
String.format(generalGetString(MR.strings.relay_bar_active_with_failures), activeCount, total, failedCount)
|
||||
} else {
|
||||
String.format(generalGetString(MR.strings.relay_bar_active), activeCount, total)
|
||||
}
|
||||
Text(statusText, modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
painterResource(if (relayListExpanded.value) MR.images.ic_chevron_up else MR.images.ic_chevron_down),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (relayListExpanded.value) {
|
||||
groupRelays.value.forEach { relay ->
|
||||
val failedErr = relayMemberConnFailed(chatModel, relay)
|
||||
if (failedErr != null) {
|
||||
SectionItemView(
|
||||
click = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.relay_connection_failed),
|
||||
text = failedErr
|
||||
)
|
||||
},
|
||||
minHeight = 30.dp,
|
||||
padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = 4.dp)
|
||||
) {
|
||||
RelayRow(relay, connFailed = true)
|
||||
}
|
||||
} else {
|
||||
SectionItemView(
|
||||
minHeight = 30.dp,
|
||||
padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = 4.dp)
|
||||
) {
|
||||
RelayRow(relay, connFailed = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
SectionView {
|
||||
val enabled = activeCount > 0
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_link),
|
||||
generalGetString(MR.strings.channel_link),
|
||||
click = {
|
||||
if (activeCount >= total) {
|
||||
onLinkReady()
|
||||
} else if (activeCount > 0) {
|
||||
val alertText = String.format(
|
||||
generalGetString(MR.strings.channel_will_start_with_relays),
|
||||
activeCount, total
|
||||
)
|
||||
if (activeCount + failedCount < total) {
|
||||
AlertManager.shared.showAlertDialogButtons(
|
||||
title = generalGetString(MR.strings.not_all_relays_connected),
|
||||
text = alertText,
|
||||
buttons = {
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
|
||||
Text(generalGetString(MR.strings.wait_verb))
|
||||
}
|
||||
TextButton(onClick = {
|
||||
AlertManager.shared.hideAlert()
|
||||
onLinkReady()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.proceed_verb))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.not_all_relays_connected),
|
||||
text = alertText,
|
||||
confirmText = generalGetString(MR.strings.proceed_verb),
|
||||
onConfirm = { onLinkReady() }
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
textColor = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
iconColor = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
disabled = !enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun relayMemberConnFailed(chatModel: ChatModel, relay: GroupRelay): String? {
|
||||
return chatModel.groupMembers.value
|
||||
.firstOrNull { it.groupMemberId == relay.groupMemberId }
|
||||
?.activeConn?.connFailedErr
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayRow(relay: GroupRelay, connFailed: Boolean) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(relayDisplayName(relay))
|
||||
RelayStatusIndicator(relay.relayStatus, connFailed = connFailed)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LinkStepView(
|
||||
chatModel: ChatModel,
|
||||
gInfo: GroupInfo,
|
||||
groupLink: MutableState<GroupLink?>,
|
||||
closeAll: () -> Unit
|
||||
) {
|
||||
val close: () -> Unit = {
|
||||
chatModel.creatingChannelId.value = null
|
||||
withBGApi {
|
||||
delay(500)
|
||||
withContext(Dispatchers.Main) {
|
||||
ModalManager.start.closeModals()
|
||||
openGroupChat(null, gInfo.groupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
ModalView(close = close, showClose = false) {
|
||||
GroupLinkView(
|
||||
chatModel = chatModel,
|
||||
rhId = null,
|
||||
groupInfo = gInfo,
|
||||
groupLink = groupLink.value,
|
||||
onGroupLinkUpdated = { groupLink.value = it },
|
||||
creatingGroup = true,
|
||||
isChannel = true,
|
||||
close = close
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun relayDisplayName(relay: GroupRelay): String {
|
||||
if (relay.userChatRelay.name.isNotEmpty()) return relay.userChatRelay.name
|
||||
relay.userChatRelay.domains.firstOrNull()?.let { return it }
|
||||
relay.relayLink?.let { return hostFromRelayLink(it) }
|
||||
return "relay ${relay.groupRelayId}"
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun RelayStatusIndicator(status: RelayStatus, connFailed: Boolean = false) {
|
||||
val color = if (connFailed) Color.Red else if (status == RelayStatus.RsActive) Color.Green else WarningYellow
|
||||
val text = if (connFailed) generalGetString(MR.strings.relay_status_failed) else status.text
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Canvas(Modifier.size(8.dp)) {
|
||||
drawCircle(color = color)
|
||||
}
|
||||
Text(
|
||||
text,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
if (connFailed) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_error),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RelayProgressIndicator(active: Int, total: Int) {
|
||||
if (active == 0) {
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(20.dp),
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
} else {
|
||||
val progress = active.toFloat() / total.coerceAtLeast(1).toFloat()
|
||||
Box(Modifier.size(20.dp)) {
|
||||
Canvas(Modifier.fillMaxSize()) {
|
||||
// Background circle
|
||||
drawCircle(
|
||||
color = Color.Gray.copy(alpha = 0.3f),
|
||||
style = Stroke(width = 2.5.dp.toPx())
|
||||
)
|
||||
// Progress arc
|
||||
drawArc(
|
||||
color = Color(0xFF2196F3), // accent blue
|
||||
startAngle = -90f,
|
||||
sweepAngle = 360f * progress,
|
||||
useCenter = false,
|
||||
style = Stroke(width = 2.5.dp.toPx(), cap = StrokeCap.Round)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewAddChannelView() {
|
||||
SimpleXTheme {
|
||||
AddChannelView(chatModel = ChatModel, close = {}, closeAll = {})
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -56,7 +56,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c
|
||||
}
|
||||
} else {
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
GroupLinkView(chatModel, rhId, groupInfo, groupLink = null, onGroupLinkUpdated = null, creatingGroup = true, close)
|
||||
GroupLinkView(chatModel, rhId, groupInfo, groupLink = null, onGroupLinkUpdated = null, creatingGroup = true, close = close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+97
-43
@@ -28,6 +28,15 @@ suspend fun planAndConnect(
|
||||
filterKnownContact: ((Contact) -> Unit)? = null,
|
||||
filterKnownGroup: ((GroupInfo) -> Unit)? = null,
|
||||
): CompletableDeferred<Boolean> {
|
||||
val link = strHasSingleSimplexLink(shortOrFullLink.trim())
|
||||
if (link?.format is Format.SimplexLink && (link.format as Format.SimplexLink).linkType == SimplexLinkType.relay) {
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.relay_address_alert_title),
|
||||
generalGetString(MR.strings.relay_address_alert_message),
|
||||
)
|
||||
cleanup?.invoke()
|
||||
return CompletableDeferred(false)
|
||||
}
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
val inProgress = mutableStateOf(true)
|
||||
connectProgressManager.startConnectProgress(generalGetString(MR.strings.loading_profile)) {
|
||||
@@ -203,6 +212,7 @@ private suspend fun planAndConnectTask(
|
||||
showPrepareGroupAlert(
|
||||
rhId,
|
||||
connectionLink,
|
||||
connectionPlan.groupLinkPlan.groupSLinkInfo_,
|
||||
connectionPlan.groupLinkPlan.groupSLinkData_,
|
||||
close,
|
||||
cleanup
|
||||
@@ -421,49 +431,75 @@ fun ownGroupLinkConfirmConnect(
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)?,
|
||||
) {
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.connect_plan_join_your_group),
|
||||
text = String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName) + linkText,
|
||||
buttons = {
|
||||
Column {
|
||||
// Open group
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
openKnownGroup(chatModel, rhId, close, groupInfo)
|
||||
cleanup?.invoke()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
// Use current profile
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withBGApi {
|
||||
connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup)
|
||||
if (groupInfo.useRelays) {
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.connect_plan_this_is_your_link_for_channel),
|
||||
text = String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_channel_vName), groupInfo.displayName),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
openKnownGroup(chatModel, rhId, close, groupInfo)
|
||||
cleanup?.invoke()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_plan_open_channel), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Use new incognito profile
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withBGApi {
|
||||
connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup)
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
cleanup?.invoke()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
cleanup?.invoke()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
},
|
||||
onDismissRequest = cleanup,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
} else {
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.connect_plan_join_your_group),
|
||||
text = String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName) + linkText,
|
||||
buttons = {
|
||||
Column {
|
||||
// Open group
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
openKnownGroup(chatModel, rhId, close, groupInfo)
|
||||
cleanup?.invoke()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
// Use current profile
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withBGApi {
|
||||
connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Use new incognito profile
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withBGApi {
|
||||
connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
cleanup?.invoke()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = cleanup,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
},
|
||||
onDismissRequest = cleanup,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo) {
|
||||
@@ -478,7 +514,9 @@ private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: ((
|
||||
)
|
||||
},
|
||||
confirmText = generalGetString(
|
||||
if (groupInfo.businessChat == null) {
|
||||
if (groupInfo.useRelays) {
|
||||
if (groupInfo.nextConnectPrepared) MR.strings.connect_plan_open_new_channel else MR.strings.connect_plan_open_channel
|
||||
} else if (groupInfo.businessChat == null) {
|
||||
if (groupInfo.nextConnectPrepared) MR.strings.connect_plan_open_new_group else MR.strings.connect_plan_open_group
|
||||
} else {
|
||||
if (groupInfo.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat
|
||||
@@ -544,21 +582,37 @@ fun showPrepareContactAlert(
|
||||
fun showPrepareGroupAlert(
|
||||
rhId: Long?,
|
||||
connectionLink: CreatedConnLink,
|
||||
groupShortLinkInfo: GroupShortLinkInfo?,
|
||||
groupShortLinkData: GroupShortLinkData,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)?
|
||||
) {
|
||||
val isChannel = !(groupShortLinkInfo?.direct ?: true)
|
||||
AlertManager.privacySensitive.showOpenChatAlert(
|
||||
profileName = groupShortLinkData.groupProfile.displayName,
|
||||
profileFullName = groupShortLinkData.groupProfile.fullName,
|
||||
profileImage = { ProfileImage(size = alertProfileImageSize, image = groupShortLinkData.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled) },
|
||||
confirmText = generalGetString(MR.strings.connect_plan_open_new_group),
|
||||
profileImage = {
|
||||
ProfileImage(
|
||||
size = alertProfileImageSize,
|
||||
image = groupShortLinkData.groupProfile.image,
|
||||
icon = if (isChannel) MR.images.ic_bigtop_updates else MR.images.ic_supervised_user_circle_filled
|
||||
)
|
||||
},
|
||||
confirmText = generalGetString(if (isChannel) MR.strings.connect_plan_open_new_channel else MR.strings.connect_plan_open_new_group),
|
||||
onConfirm = {
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withBGApi {
|
||||
val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, groupShortLinkData)
|
||||
val directLink = groupShortLinkInfo?.direct ?: true
|
||||
val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, directLink = directLink, groupShortLinkData)
|
||||
if (chat != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val relays = groupShortLinkInfo?.groupRelays
|
||||
if (!relays.isNullOrEmpty()) {
|
||||
val chatInfo = chat.chatInfo
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
chatModel.channelRelayHostnames[chatInfo.groupInfo.groupId] = relays
|
||||
}
|
||||
}
|
||||
ChatController.chatModel.chatsContext.addChat(chat)
|
||||
openChat_(chatModel, rhId, close, chat)
|
||||
}
|
||||
|
||||
+9
@@ -63,6 +63,9 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
createGroup = {
|
||||
ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) }
|
||||
},
|
||||
createChannel = {
|
||||
ModalManager.start.showCustomModal { close -> AddChannelView(chatModel, close, closeAll) }
|
||||
},
|
||||
rh = rh,
|
||||
close = close
|
||||
)
|
||||
@@ -110,6 +113,7 @@ private fun ModalData.NewChatSheetLayout(
|
||||
addContact: () -> Unit,
|
||||
scanPaste: () -> Unit,
|
||||
createGroup: () -> Unit,
|
||||
createChannel: () -> Unit,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
@@ -193,6 +197,11 @@ private fun ModalData.NewChatSheetLayout(
|
||||
painterResource(MR.images.ic_group),
|
||||
stringResource(MR.strings.create_group_button),
|
||||
createGroup,
|
||||
),
|
||||
Triple(
|
||||
painterResource(MR.images.ic_bigtop_updates),
|
||||
stringResource(MR.strings.create_channel_beta_button),
|
||||
createChannel,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@Composable
|
||||
fun ShowRelayTestStatus(relay: UserChatRelay, modifier: Modifier = Modifier) =
|
||||
when (relay.tested) {
|
||||
true -> Icon(painterResource(MR.images.ic_check), null, modifier, tint = SimplexGreen)
|
||||
false -> Icon(painterResource(MR.images.ic_close), null, modifier, tint = MaterialTheme.colors.error)
|
||||
else -> Icon(painterResource(MR.images.ic_check), null, modifier, tint = Color.Transparent)
|
||||
}
|
||||
|
||||
fun validRelayName(name: String): Boolean =
|
||||
name.isNotEmpty() && isValidDisplayName(name)
|
||||
|
||||
fun showInvalidRelayNameAlert(name: MutableState<String>) {
|
||||
val validName = mkValidName(name.value)
|
||||
if (validName.isEmpty()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_name)
|
||||
)
|
||||
} else {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.invalid_name),
|
||||
text = String.format(generalGetString(MR.strings.correct_name_to), validName),
|
||||
onConfirm = {
|
||||
name.value = validName
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun validRelayAddress(address: String): Boolean {
|
||||
val parsedMd = parseToMarkdown(address)
|
||||
return parsedMd != null &&
|
||||
parsedMd.size == 1 &&
|
||||
parsedMd.first().format is Format.SimplexLink &&
|
||||
(parsedMd.first().format as Format.SimplexLink).linkType == SimplexLinkType.relay
|
||||
}
|
||||
|
||||
// TODO [relays] TBC matching relay to operator by domain (relay address can be hosted on operator server)
|
||||
fun addChatRelay(
|
||||
relay: UserChatRelay,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>?,
|
||||
rhId: Long?,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val nameEmpty = relay.name.trim().isEmpty()
|
||||
val addressEmpty = relay.address.trim().isEmpty()
|
||||
if (nameEmpty && addressEmpty) {
|
||||
close()
|
||||
} else if (!validRelayName(relay.name)) {
|
||||
close()
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_relay_name),
|
||||
text = generalGetString(MR.strings.check_relay_name)
|
||||
)
|
||||
} else if (!validRelayAddress(relay.address)) {
|
||||
close()
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_relay_address),
|
||||
text = generalGetString(MR.strings.check_relay_address)
|
||||
)
|
||||
} else {
|
||||
val i = userServers.value.indexOfFirst { it.operator == null }
|
||||
if (i != -1) {
|
||||
val updatedUserServers = userServers.value.toMutableList()
|
||||
val operatorServers = updatedUserServers[i]
|
||||
updatedUserServers[i] = operatorServers.copy(
|
||||
chatRelays = operatorServers.chatRelays + relay
|
||||
)
|
||||
userServers.value = updatedUserServers
|
||||
withBGApi {
|
||||
validateServers_(rhId, userServers.value, serverErrors, serverWarnings)
|
||||
}
|
||||
close()
|
||||
} else { // Shouldn't happen
|
||||
close()
|
||||
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error_adding_relay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatRelayView(
|
||||
relay: UserChatRelay,
|
||||
onDelete: () -> Unit,
|
||||
onUpdate: (UserChatRelay) -> Unit,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val relayToEdit = remember { mutableStateOf(relay) }
|
||||
|
||||
ModalView(
|
||||
close = {
|
||||
val validName = validRelayName(relayToEdit.value.name)
|
||||
val validAddress = validRelayAddress(relayToEdit.value.address)
|
||||
if (validName && validAddress) {
|
||||
onUpdate(relayToEdit.value)
|
||||
close()
|
||||
} else if (!validName) {
|
||||
close()
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_relay_name),
|
||||
text = generalGetString(MR.strings.check_relay_name)
|
||||
)
|
||||
} else {
|
||||
close()
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_relay_address),
|
||||
text = generalGetString(MR.strings.check_relay_address)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
ChatRelayLayout(
|
||||
relayToEdit,
|
||||
onDelete = onDelete
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatRelayLayout(
|
||||
relay: MutableState<UserChatRelay>,
|
||||
onDelete: (() -> Unit)?
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.chat_relay))
|
||||
if (relay.value.preset) {
|
||||
PresetRelay(relay)
|
||||
} else {
|
||||
CustomRelay(relay, onDelete)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PresetRelay(relay: MutableState<UserChatRelay>) {
|
||||
SectionView(stringResource(MR.strings.preset_relay_name).uppercase()) {
|
||||
SectionItemView {
|
||||
Text(relay.value.name)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.preset_relay_address).uppercase()) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
relay.value.address,
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
UseRelaySection(relay)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomRelay(
|
||||
relay: MutableState<UserChatRelay>,
|
||||
onDelete: (() -> Unit)?
|
||||
) {
|
||||
val relayName = remember { mutableStateOf(relay.value.name) }
|
||||
val relayAddress = remember { mutableStateOf(relay.value.address) }
|
||||
val validName = remember { derivedStateOf { validRelayName(relayName.value) } }
|
||||
val validAddress = remember { derivedStateOf { validRelayAddress(relayAddress.value) } }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { relayName.value }
|
||||
.distinctUntilChanged()
|
||||
.collect { relay.value = relay.value.copy(name = it) }
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { relayAddress.value }
|
||||
.distinctUntilChanged()
|
||||
.collect { relay.value = relay.value.copy(address = it) }
|
||||
}
|
||||
|
||||
Column {
|
||||
val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
|
||||
Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
stringResource(MR.strings.your_relay_name).uppercase(),
|
||||
color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp
|
||||
)
|
||||
IconButton(
|
||||
onClick = { if (!validName.value) showInvalidRelayNameAlert(relayName) },
|
||||
enabled = !validName.value,
|
||||
modifier = Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_error), null,
|
||||
tint = if (!validName.value) MaterialTheme.colors.error else Color.Transparent
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
TextEditor(
|
||||
relayName,
|
||||
Modifier,
|
||||
placeholder = generalGetString(MR.strings.enter_relay_name)
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
SectionView(
|
||||
stringResource(MR.strings.your_relay_address).uppercase(),
|
||||
icon = painterResource(MR.images.ic_error),
|
||||
iconTint = if (!validAddress.value) MaterialTheme.colors.error else Color.Transparent,
|
||||
) {
|
||||
TextEditor(
|
||||
relayAddress,
|
||||
Modifier.height(144.dp)
|
||||
)
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
UseRelaySection(relay, validAddress.value)
|
||||
|
||||
if (onDelete != null) {
|
||||
SectionDividerSpaced()
|
||||
SectionView {
|
||||
SectionItemView(onDelete) {
|
||||
Text(stringResource(MR.strings.delete_relay), color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseRelaySection(
|
||||
relay: MutableState<UserChatRelay>,
|
||||
valid: Boolean = true
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.use_relay).uppercase()) {
|
||||
SectionItemViewSpaceBetween(
|
||||
click = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.not_implemented),
|
||||
text = generalGetString(MR.strings.relay_testing_not_available)
|
||||
)
|
||||
},
|
||||
disabled = !valid
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.test_relay),
|
||||
color = if (valid) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary
|
||||
)
|
||||
ShowRelayTestStatus(relay.value)
|
||||
}
|
||||
|
||||
val enabled = rememberUpdatedState(relay.value.enabled)
|
||||
PreferenceToggle(
|
||||
stringResource(MR.strings.use_for_new_channels),
|
||||
checked = enabled.value
|
||||
) {
|
||||
relay.value = relay.value.copy(enabled = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatRelayViewLink(
|
||||
relay: UserChatRelay,
|
||||
duplicateRelayNames: Set<String>,
|
||||
duplicateRelayAddresses: Set<String>,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
SectionItemView(onClick) {
|
||||
Box(Modifier.width(16.dp)) {
|
||||
when {
|
||||
relay.name in duplicateRelayNames || relay.address in duplicateRelayAddresses -> InvalidServer()
|
||||
!relay.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary)
|
||||
else -> ShowRelayTestStatus(relay)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
val displayName = relay.name.ifEmpty { relay.domains.firstOrNull() ?: relay.address }
|
||||
if (relay.enabled) {
|
||||
Text(displayName, color = MaterialTheme.colors.onBackground, maxLines = 1)
|
||||
} else {
|
||||
Text(displayName, maxLines = 1, color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.NewChatRelayView(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
rhId: Long?,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val relayToEdit = remember {
|
||||
mutableStateOf(
|
||||
UserChatRelay(
|
||||
chatRelayId = null, address = "", name = "", domains = emptyList(),
|
||||
preset = false, tested = null, enabled = true, deleted = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ModalView(close = {
|
||||
addChatRelay(relayToEdit.value, userServers, serverErrors, serverWarnings, rhId, close)
|
||||
}) {
|
||||
NewChatRelayLayout(relayToEdit)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewChatRelayLayout(relay: MutableState<UserChatRelay>) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.new_chat_relay))
|
||||
CustomRelay(relay, onDelete = null)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
+58
-4
@@ -54,6 +54,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
|
||||
val currUserServers = remember { stateGetOrPut("currUserServers") { emptyList<UserOperatorServers>() } }
|
||||
val userServers = remember { stateGetOrPut("userServers") { emptyList<UserOperatorServers>() } }
|
||||
val serverErrors = remember { stateGetOrPut("serverErrors") { emptyList<UserServersError>() } }
|
||||
val serverWarnings = remember { stateGetOrPut("serverWarnings") { emptyList<UserServersWarning>() } }
|
||||
|
||||
val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } }
|
||||
fun onClose(close: () -> Unit): Boolean = if (!serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value)) {
|
||||
@@ -91,6 +92,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
|
||||
currUserServers = currUserServers,
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
serverWarnings = serverWarnings,
|
||||
toggleSocksProxy = { enable ->
|
||||
val def = NetCfg.defaults
|
||||
val proxyDef = NetCfg.proxyDefaults
|
||||
@@ -158,6 +160,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
) {
|
||||
@@ -209,7 +212,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SectionView(generalGetString(MR.strings.network_preset_servers_title).uppercase()) {
|
||||
userServers.value.forEachIndexed { index, srv ->
|
||||
srv.operator?.let { ServerOperatorRow(index, it, currUserServers, userServers, serverErrors, currentRemoteHost?.remoteHostId) }
|
||||
srv.operator?.let { ServerOperatorRow(index, it, currUserServers, userServers, serverErrors, serverWarnings, currentRemoteHost?.remoteHostId) }
|
||||
}
|
||||
}
|
||||
if (conditionsAction != null && anyOperatorEnabled.value) {
|
||||
@@ -234,6 +237,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
|
||||
YourServersView(
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
serverWarnings = serverWarnings,
|
||||
operatorIndex = nullOperatorIndex,
|
||||
rhId = currentRemoteHost?.remoteHostId
|
||||
)
|
||||
@@ -284,6 +288,12 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
|
||||
ServersErrorFooter(generalGetString(MR.strings.errors_in_servers_configuration))
|
||||
}
|
||||
}
|
||||
val serversWarn = globalServersWarning(serverWarnings.value)
|
||||
if (serversWarn != null) {
|
||||
SectionCustomFooter {
|
||||
ServersWarningFooter(serversWarn)
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
@@ -664,6 +674,7 @@ private fun ServerOperatorRow(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
rhId: Long?
|
||||
) {
|
||||
SectionItemView(
|
||||
@@ -673,6 +684,7 @@ private fun ServerOperatorRow(
|
||||
currUserServers,
|
||||
userServers,
|
||||
serverErrors,
|
||||
serverWarnings,
|
||||
index,
|
||||
rhId
|
||||
)
|
||||
@@ -848,6 +860,30 @@ fun ServersErrorFooter(errStr: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ServersWarningFooter(warnStr: String) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_warning),
|
||||
contentDescription = stringResource(MR.strings.server_warning),
|
||||
tint = WarningOrange,
|
||||
modifier = Modifier
|
||||
.size(19.sp.toDp())
|
||||
.offset(x = 2.sp.toDp())
|
||||
)
|
||||
TextIconSpaced()
|
||||
Text(
|
||||
warnStr,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
lineHeight = 18.sp,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.smp_save_servers_question),
|
||||
@@ -887,11 +923,13 @@ fun updateOperatorsConditionsAcceptance(usvs: MutableState<List<UserOperatorServ
|
||||
suspend fun validateServers_(
|
||||
rhId: Long?,
|
||||
userServersToValidate: List<UserOperatorServers>,
|
||||
serverErrors: MutableState<List<UserServersError>>
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>? = null
|
||||
) {
|
||||
try {
|
||||
val errors = chatController.validateServers(rhId, userServersToValidate) ?: return
|
||||
val (errors, warnings) = chatController.validateServers(rhId, userServersToValidate) ?: return
|
||||
serverErrors.value = errors
|
||||
serverWarnings?.value = warnings
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, ex.stackTraceToString())
|
||||
}
|
||||
@@ -914,6 +952,15 @@ fun globalServersError(serverErrors: List<UserServersError>): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun globalServersWarning(serverWarnings: List<UserServersWarning>): String? {
|
||||
for (warn in serverWarnings) {
|
||||
if (warn.globalWarning != null) {
|
||||
return warn.globalWarning
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun globalSMPServersError(serverErrors: List<UserServersError>): String? {
|
||||
for (err in serverErrors) {
|
||||
if (err.globalSMPError != null) {
|
||||
@@ -943,6 +990,12 @@ fun findDuplicateHosts(serverErrors: List<UserServersError>): Set<String> {
|
||||
return duplicateHostsList.toSet()
|
||||
}
|
||||
|
||||
fun findDuplicateRelayNames(serverErrors: List<UserServersError>): Set<String> =
|
||||
serverErrors.mapNotNull { (it as? UserServersError.DuplicateChatRelayName)?.duplicateChatRelay }.toSet()
|
||||
|
||||
fun findDuplicateRelayAddresses(serverErrors: List<UserServersError>): Set<String> =
|
||||
serverErrors.mapNotNull { (it as? UserServersError.DuplicateChatRelayAddress)?.duplicateAddress }.toSet()
|
||||
|
||||
private suspend fun saveServers(
|
||||
rhId: Long?,
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
@@ -987,7 +1040,8 @@ fun PreviewNetworkAndServersLayout() {
|
||||
toggleSocksProxy = {},
|
||||
currUserServers = remember { mutableStateOf(emptyList()) },
|
||||
userServers = remember { mutableStateOf(emptyList()) },
|
||||
serverErrors = remember { mutableStateOf(emptyList()) }
|
||||
serverErrors = remember { mutableStateOf(emptyList()) },
|
||||
serverWarnings = remember { mutableStateOf(emptyList()) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -15,6 +15,7 @@ import kotlinx.coroutines.*
|
||||
fun ModalData.NewServerView(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
rhId: Long?,
|
||||
close: () -> Unit
|
||||
) {
|
||||
@@ -28,6 +29,7 @@ fun ModalData.NewServerView(
|
||||
newServer.value,
|
||||
userServers,
|
||||
serverErrors,
|
||||
serverWarnings,
|
||||
rhId,
|
||||
close = close
|
||||
)
|
||||
@@ -101,6 +103,7 @@ fun addServer(
|
||||
server: UserServer,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>? = null,
|
||||
rhId: Long?,
|
||||
close: () -> Unit
|
||||
) {
|
||||
|
||||
+62
-2
@@ -47,6 +47,7 @@ fun OperatorView(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?
|
||||
) {
|
||||
@@ -57,7 +58,7 @@ fun OperatorView(
|
||||
LaunchedEffect(userServers) {
|
||||
snapshotFlow { userServers.value }
|
||||
.collect { updatedServers ->
|
||||
validateServers_(rhId = rhId, userServersToValidate = updatedServers, serverErrors = serverErrors)
|
||||
validateServers_(rhId = rhId, userServersToValidate = updatedServers, serverErrors = serverErrors, serverWarnings = serverWarnings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,9 +69,10 @@ fun OperatorView(
|
||||
currUserServers,
|
||||
userServers,
|
||||
serverErrors,
|
||||
serverWarnings,
|
||||
operatorIndex,
|
||||
navigateToProtocolView = { serverIndex, server, protocol ->
|
||||
navigateToProtocolView(userServers, serverErrors, operatorIndex, rhId, serverIndex, server, protocol)
|
||||
navigateToProtocolView(userServers, serverErrors, serverWarnings, operatorIndex, rhId, serverIndex, server, protocol)
|
||||
},
|
||||
currentUser,
|
||||
rhId,
|
||||
@@ -87,6 +89,7 @@ fun OperatorView(
|
||||
fun navigateToProtocolView(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?,
|
||||
serverIndex: Int,
|
||||
@@ -100,6 +103,7 @@ fun navigateToProtocolView(
|
||||
serverProtocol = protocol,
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
serverWarnings = serverWarnings,
|
||||
onDelete = {
|
||||
if (protocol == ServerProtocol.SMP) {
|
||||
deleteSMPServer(userServers, operatorIndex, serverIndex)
|
||||
@@ -130,11 +134,42 @@ fun navigateToProtocolView(
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToChatRelayView(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
operatorIndex: Int,
|
||||
relayIndex: Int,
|
||||
relay: UserChatRelay,
|
||||
rhId: Long?
|
||||
) {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
ChatRelayView(
|
||||
relay = relay,
|
||||
onDelete = {
|
||||
deleteChatRelay(userServers, operatorIndex, relayIndex)
|
||||
close()
|
||||
},
|
||||
onUpdate = { updatedRelay ->
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorIndex] = this[operatorIndex].copy(
|
||||
chatRelays = this[operatorIndex].chatRelays.toMutableList().apply {
|
||||
this[relayIndex] = updatedRelay
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
close = close
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OperatorViewLayout(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
operatorIndex: Int,
|
||||
navigateToProtocolView: (Int, UserServer, ServerProtocol) -> Unit,
|
||||
currentUser: User?,
|
||||
@@ -170,15 +205,21 @@ fun OperatorViewLayout(
|
||||
currUserServers = currUserServers,
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
serverWarnings = serverWarnings,
|
||||
operatorIndex = operatorIndex,
|
||||
rhId = rhId
|
||||
)
|
||||
}
|
||||
val serversErr = globalServersError(serverErrors.value)
|
||||
val serversWarn = globalServersWarning(serverWarnings.value)
|
||||
if (serversErr != null) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(serversErr)
|
||||
}
|
||||
} else if (serversWarn != null) {
|
||||
SectionCustomFooter {
|
||||
ServersWarningFooter(serversWarn)
|
||||
}
|
||||
} else {
|
||||
val footerText = when (val c = operator.conditionsAcceptance) {
|
||||
is ConditionsAcceptance.Accepted -> if (c.acceptedAt != null) {
|
||||
@@ -194,6 +235,22 @@ fun OperatorViewLayout(
|
||||
}
|
||||
|
||||
if (operator.enabled) {
|
||||
if (userServers.value[operatorIndex].chatRelays.any { !it.deleted }) {
|
||||
val duplicateRelayNames = findDuplicateRelayNames(serverErrors.value)
|
||||
val duplicateRelayAddresses = findDuplicateRelayAddresses(serverErrors.value)
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.chat_relays).uppercase()) {
|
||||
userServers.value[operatorIndex].chatRelays.forEachIndexed { index, relay ->
|
||||
if (!relay.deleted) {
|
||||
ChatRelayViewLink(relay, duplicateRelayNames, duplicateRelayAddresses) {
|
||||
navigateToChatRelayView(userServers, serverErrors, serverWarnings, operatorIndex, index, relay, rhId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTextFooter(generalGetString(MR.strings.chat_relays_forward_messages_in_channels))
|
||||
}
|
||||
|
||||
if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.operator_use_for_messages).uppercase()) {
|
||||
@@ -458,6 +515,7 @@ private fun UseOperatorToggle(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?
|
||||
) {
|
||||
@@ -485,6 +543,7 @@ private fun UseOperatorToggle(
|
||||
currUserServers = currUserServers,
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
serverWarnings = serverWarnings,
|
||||
operatorIndex = operatorIndex,
|
||||
rhId = rhId,
|
||||
close = close
|
||||
@@ -510,6 +569,7 @@ private fun SingleOperatorUsageConditionsView(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?,
|
||||
close: () -> Unit
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ fun ProtocolServerView(
|
||||
serverProtocol: ServerProtocol,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
onDelete: () -> Unit,
|
||||
onUpdate: (UserServer) -> Unit,
|
||||
close: () -> Unit,
|
||||
|
||||
+65
-5
@@ -30,6 +30,7 @@ import kotlinx.coroutines.launch
|
||||
fun ModalData.YourServersView(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?
|
||||
) {
|
||||
@@ -40,7 +41,7 @@ fun ModalData.YourServersView(
|
||||
LaunchedEffect(userServers) {
|
||||
snapshotFlow { userServers.value }
|
||||
.collect { updatedServers ->
|
||||
validateServers_(rhId = rhId, userServersToValidate = updatedServers, serverErrors = serverErrors)
|
||||
validateServers_(rhId = rhId, userServersToValidate = updatedServers, serverErrors = serverErrors, serverWarnings = serverWarnings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +52,10 @@ fun ModalData.YourServersView(
|
||||
scope,
|
||||
userServers,
|
||||
serverErrors,
|
||||
serverWarnings,
|
||||
operatorIndex,
|
||||
navigateToProtocolView = { serverIndex, server, protocol ->
|
||||
navigateToProtocolView(userServers, serverErrors, operatorIndex, rhId, serverIndex, server, protocol)
|
||||
navigateToProtocolView(userServers, serverErrors, serverWarnings, operatorIndex, rhId, serverIndex, server, protocol)
|
||||
},
|
||||
currentUser,
|
||||
rhId,
|
||||
@@ -72,6 +74,7 @@ fun YourServersViewLayout(
|
||||
scope: CoroutineScope,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
operatorIndex: Int,
|
||||
navigateToProtocolView: (Int, UserServer, ServerProtocol) -> Unit,
|
||||
currentUser: User?,
|
||||
@@ -81,7 +84,22 @@ fun YourServersViewLayout(
|
||||
val duplicateHosts = findDuplicateHosts(serverErrors.value)
|
||||
|
||||
Column {
|
||||
if (userServers.value[operatorIndex].chatRelays.any { !it.deleted }) {
|
||||
val duplicateRelayNames = findDuplicateRelayNames(serverErrors.value)
|
||||
val duplicateRelayAddresses = findDuplicateRelayAddresses(serverErrors.value)
|
||||
SectionView(generalGetString(MR.strings.chat_relays).uppercase()) {
|
||||
userServers.value[operatorIndex].chatRelays.forEachIndexed { i, relay ->
|
||||
if (relay.deleted) return@forEachIndexed
|
||||
ChatRelayViewLink(relay, duplicateRelayNames, duplicateRelayAddresses) {
|
||||
navigateToChatRelayView(userServers, serverErrors, serverWarnings, operatorIndex, i, relay, rhId)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTextFooter(generalGetString(MR.strings.chat_relays_forward_messages_in_channels))
|
||||
}
|
||||
|
||||
if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.message_servers).uppercase()) {
|
||||
userServers.value[operatorIndex].smpServers.forEachIndexed { i, server ->
|
||||
if (server.deleted) return@forEachIndexed
|
||||
@@ -150,7 +168,8 @@ fun YourServersViewLayout(
|
||||
|
||||
if (
|
||||
userServers.value[operatorIndex].smpServers.any { !it.deleted } ||
|
||||
userServers.value[operatorIndex].xftpServers.any { !it.deleted }
|
||||
userServers.value[operatorIndex].xftpServers.any { !it.deleted } ||
|
||||
userServers.value[operatorIndex].chatRelays.any { !it.deleted }
|
||||
) {
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
}
|
||||
@@ -159,7 +178,7 @@ fun YourServersViewLayout(
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_add),
|
||||
stringResource(MR.strings.smp_servers_add),
|
||||
click = { showAddServerDialog(scope, userServers, serverErrors, rhId) },
|
||||
click = { showAddServerDialog(scope, userServers, serverErrors, serverWarnings, rhId) },
|
||||
disabled = testing.value,
|
||||
textColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
iconColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
@@ -171,6 +190,12 @@ fun YourServersViewLayout(
|
||||
ServersErrorFooter(serversErr)
|
||||
}
|
||||
}
|
||||
val serversWarn = globalServersWarning(serverWarnings.value)
|
||||
if (serversWarn != null) {
|
||||
SectionCustomFooter {
|
||||
ServersWarningFooter(serversWarn)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
|
||||
SectionView {
|
||||
@@ -226,6 +251,7 @@ fun showAddServerDialog(
|
||||
scope: CoroutineScope,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
serverWarnings: MutableState<List<UserServersWarning>>,
|
||||
rhId: Long?
|
||||
) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
@@ -235,7 +261,7 @@ fun showAddServerDialog(
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
NewServerView(userServers, serverErrors, rhId, close)
|
||||
NewServerView(userServers, serverErrors, serverWarnings, rhId, close)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(MR.strings.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
@@ -250,6 +276,7 @@ fun showAddServerDialog(
|
||||
server,
|
||||
userServers,
|
||||
serverErrors,
|
||||
serverWarnings,
|
||||
rhId,
|
||||
close = close
|
||||
)
|
||||
@@ -260,6 +287,14 @@ fun showAddServerDialog(
|
||||
Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
NewChatRelayView(userServers, serverErrors, serverWarnings, rhId, close)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(MR.strings.chat_relay), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -405,3 +440,28 @@ fun deleteSMPServer(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteChatRelay(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
operatorServersIndex: Int,
|
||||
relayIndex: Int
|
||||
) {
|
||||
val relay = userServers.value[operatorServersIndex].chatRelays[relayIndex]
|
||||
if (relay.chatRelayId == null) {
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorServersIndex] = this[operatorServersIndex].copy(
|
||||
chatRelays = this[operatorServersIndex].chatRelays.toMutableList().apply {
|
||||
this.removeAt(relayIndex)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorServersIndex] = this[operatorServersIndex].copy(
|
||||
chatRelays = this[operatorServersIndex].chatRelays.toMutableList().apply {
|
||||
this[relayIndex] = this[relayIndex].copy(deleted = true)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
<string name="simplex_link_invitation">SimpleX one-time invitation</string>
|
||||
<string name="simplex_link_group">SimpleX group link</string>
|
||||
<string name="simplex_link_channel">SimpleX channel link</string>
|
||||
<string name="simplex_link_relay">SimpleX relay link</string>
|
||||
<string name="simplex_link_relay">SimpleX relay address</string>
|
||||
<string name="simplex_link_connection">via %1$s</string>
|
||||
<string name="simplex_link_mode">SimpleX links</string>
|
||||
<string name="simplex_link_mode_description">Description</string>
|
||||
@@ -141,6 +141,8 @@
|
||||
<string name="no_media_servers_configured_for_private_routing">No servers to receive files.</string>
|
||||
<string name="for_chat_profile">For chat profile %s:</string>
|
||||
<string name="errors_in_servers_configuration">Errors in servers configuration.</string>
|
||||
<string name="no_chat_relays_enabled">No chat relays enabled.</string>
|
||||
<string name="server_warning">Server warning</string>
|
||||
<string name="error_accepting_operator_conditions">Error accepting conditions</string>
|
||||
<string name="blocking_reason_spam">Spam</string>
|
||||
<string name="blocking_reason_content">Content violates conditions of use</string>
|
||||
@@ -514,8 +516,11 @@
|
||||
<string name="chat_banner_your_contact">Your contact</string>
|
||||
<string name="chat_banner_bot">Bot</string>
|
||||
<string name="chat_banner_join_group">Tap Join group</string>
|
||||
<string name="chat_banner_join_channel">Tap Join channel</string>
|
||||
<string name="chat_banner_your_group">Your group</string>
|
||||
<string name="chat_banner_your_channel">Your channel</string>
|
||||
<string name="chat_banner_group">Group</string>
|
||||
<string name="chat_banner_channel">Channel</string>
|
||||
<string name="chat_banner_business_connection">Business connection</string>
|
||||
<string name="chat_banner_your_business_contact">Your business contact</string>
|
||||
|
||||
@@ -561,6 +566,8 @@
|
||||
<string name="report_sent_alert_title">Report sent to moderators</string>
|
||||
<string name="report_sent_alert_msg_view_in_support_chat">You can view your reports in Chat with admins.</string>
|
||||
<string name="compose_view_join_group">Join group</string>
|
||||
<string name="compose_view_join_channel">Join channel</string>
|
||||
<string name="compose_view_broadcast">Broadcast</string>
|
||||
<string name="compose_view_add_message">Add message</string>
|
||||
<string name="compose_view_connect">Connect</string>
|
||||
<string name="compose_view_send_contact_request_alert_question">Send contact request?</string>
|
||||
@@ -575,7 +582,9 @@
|
||||
<string name="cant_send_message_contact_not_synchronized">not synchronized</string>
|
||||
<string name="cant_send_message_contact_disabled">contact disabled</string>
|
||||
<string name="observer_cant_send_message_title">you are observer</string>
|
||||
<string name="you_are_subscriber">you are subscriber</string>
|
||||
<string name="observer_cant_send_message_desc">Please contact group admin.</string>
|
||||
<string name="channel_role_label">channel</string>
|
||||
<string name="cant_send_message_rejected">request to join rejected</string>
|
||||
<string name="cant_send_message_group_deleted">group is deleted</string>
|
||||
<string name="cant_send_message_mem_removed">removed from group</string>
|
||||
@@ -1655,8 +1664,10 @@
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">You joined this group. Connecting to inviting group member.</string>
|
||||
<string name="leave_group_button">Leave</string>
|
||||
<string name="leave_group_question">Leave group?</string>
|
||||
<string name="leave_channel_question">Leave channel?</string>
|
||||
<string name="leave_chat_question">Leave chat?</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">You will stop receiving messages from this group. Chat history will be preserved.</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_channel_chat_history_will_be_preserved">You will stop receiving messages from this channel. Chat history will be preserved.</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">You will stop receiving messages from this chat. Chat history will be preserved.</string>
|
||||
<string name="icon_descr_add_members">Invite members</string>
|
||||
<string name="icon_descr_group_inactive">Group inactive</string>
|
||||
@@ -1755,6 +1766,7 @@
|
||||
<string name="group_member_role_moderator">moderator</string>
|
||||
<string name="group_member_role_admin">admin</string>
|
||||
<string name="group_member_role_owner">owner</string>
|
||||
<string name="group_member_role_relay">relay</string>
|
||||
|
||||
<!-- GroupMemberStatus -->
|
||||
<string name="group_member_status_rejected">rejected</string>
|
||||
@@ -1803,24 +1815,32 @@
|
||||
<string name="group_info_section_title_num_members">%1$s MEMBERS</string>
|
||||
<string name="group_info_member_you">you: %1$s</string>
|
||||
<string name="button_delete_group">Delete group</string>
|
||||
<string name="button_delete_channel">Delete channel</string>
|
||||
<string name="button_delete_chat">Delete chat</string>
|
||||
<string name="delete_group_question">Delete group?</string>
|
||||
<string name="delete_channel_question">Delete channel?</string>
|
||||
<string name="delete_chat_question">Delete chat?</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">Group will be deleted for all members - this cannot be undone!</string>
|
||||
<string name="delete_channel_for_all_subscribers_cannot_undo_warning">Channel will be deleted for all subscribers - this cannot be undone!</string>
|
||||
<string name="delete_chat_for_all_members_cannot_undo_warning">Chat will be deleted for all members - this cannot be undone!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Group will be deleted for you - this cannot be undone!</string>
|
||||
<string name="delete_channel_for_self_cannot_undo_warning">Channel will be deleted for you - this cannot be undone!</string>
|
||||
<string name="delete_chat_for_self_cannot_undo_warning">Chat will be deleted for you - this cannot be undone!</string>
|
||||
<string name="button_leave_group">Leave group</string>
|
||||
<string name="button_leave_channel">Leave channel</string>
|
||||
<string name="button_leave_chat">Leave chat</string>
|
||||
<string name="button_edit_group_profile">Edit group profile</string>
|
||||
<string name="button_edit_channel_profile">Edit channel profile</string>
|
||||
<string name="button_add_welcome_message">Add welcome message</string>
|
||||
<string name="button_welcome_message">Welcome message</string>
|
||||
<string name="group_link">Group link</string>
|
||||
<string name="channel_link">Channel link</string>
|
||||
<string name="create_group_link">Create group link</string>
|
||||
<string name="button_create_group_link">Create link</string>
|
||||
<string name="delete_link_question">Delete link?</string>
|
||||
<string name="delete_link">Delete link</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">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.</string>
|
||||
<string name="you_can_share_channel_link_anybody_will_be_able_to_connect">You can share a link or a QR code - anybody will be able to join the channel.</string>
|
||||
<string name="all_group_members_will_remain_connected">All group members will remain connected.</string>
|
||||
<string name="error_creating_link_for_group">Error creating group link</string>
|
||||
<string name="error_updating_link_for_group">Error updating group link</string>
|
||||
@@ -1837,7 +1857,10 @@
|
||||
<string name="send_receipts_disabled_alert_title">Receipts are disabled</string>
|
||||
<string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string>
|
||||
<string name="action_button_add_members">Invite</string>
|
||||
<string name="action_button_channel_link">Link</string>
|
||||
<string name="button_support_chat">Chat with admins</string>
|
||||
<string name="button_channel_members">Channel members</string>
|
||||
<string name="button_channel_relays">Chat relays</string>
|
||||
|
||||
<!-- Chat / Chat item info -->
|
||||
<string name="section_title_for_console">FOR CONSOLE</string>
|
||||
@@ -1872,6 +1895,7 @@
|
||||
|
||||
<!-- GroupMemberInfoView.kt -->
|
||||
<string name="button_remove_member_question">Remove member?</string>
|
||||
<string name="button_remove_subscriber_question">Remove subscriber?</string>
|
||||
<string name="button_remove_members_question">Remove members?</string>
|
||||
<string name="button_delete_member_messages_question">Delete member messages?</string>
|
||||
<string name="button_remove_member">Remove member</string>
|
||||
@@ -1879,6 +1903,7 @@
|
||||
<string name="button_support_chat_member">Chat with member</string>
|
||||
<string name="button_send_direct_message">Send direct message</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string>
|
||||
<string name="subscriber_will_be_removed_from_channel_cannot_be_undone">Subscriber will be removed from channel - this cannot be undone!</string>
|
||||
<string name="members_will_be_removed_from_group_cannot_be_undone">Members will be removed from group - this cannot be undone!</string>
|
||||
<string name="member_will_be_removed_from_chat_cannot_be_undone">Member will be removed from chat - this cannot be undone!</string>
|
||||
<string name="members_will_be_removed_from_chat_cannot_be_undone">Members will be removed from chat - this cannot be undone!</string>
|
||||
@@ -1925,7 +1950,7 @@
|
||||
<string name="info_row_group">Group</string>
|
||||
<string name="info_row_chat">Chat</string>
|
||||
<string name="info_row_connection">Connection</string>
|
||||
<string name="info_row_connection_failed">Connection failed</string>
|
||||
<string name="info_row_connection_failed">CONNECTION FAILED</string>
|
||||
<string name="conn_level_desc_direct">direct</string>
|
||||
<string name="conn_level_desc_indirect">indirect (%1$s)</string>
|
||||
<string name="message_queue_info">Message queue info</string>
|
||||
@@ -2794,4 +2819,99 @@
|
||||
|
||||
<!-- GroupMentions.kt -->
|
||||
<string name="max_group_mentions_per_message_reached">You can mention up to %1$s members per message!</string>
|
||||
|
||||
<!-- ChannelMembersView.kt -->
|
||||
<string name="channel_members_title_owners_and_subscribers">Owners & subscribers</string>
|
||||
<string name="channel_members_section_owners">Owners</string>
|
||||
<string name="channel_members_num_subscribers">%1$d subscribers</string>
|
||||
<string name="channel_members_no_subscribers">No subscribers</string>
|
||||
|
||||
<!-- ChatRelayView.kt -->
|
||||
<string name="chat_relay">Chat relay</string>
|
||||
<string name="new_chat_relay">New chat relay</string>
|
||||
<string name="preset_relay_name">Preset relay name</string>
|
||||
<string name="preset_relay_address">Preset relay address</string>
|
||||
<string name="your_relay_name">Your relay name</string>
|
||||
<string name="your_relay_address">Your relay address</string>
|
||||
<string name="enter_relay_name">Enter relay name…</string>
|
||||
<string name="use_relay">Use relay</string>
|
||||
<string name="test_relay">Test relay</string>
|
||||
<string name="use_for_new_channels">Use for new channels</string>
|
||||
<string name="delete_relay">Delete relay</string>
|
||||
<string name="not_implemented">Not implemented</string>
|
||||
<string name="relay_testing_not_available">Relay testing is not yet available.</string>
|
||||
<string name="invalid_relay_name">Invalid relay name!</string>
|
||||
<string name="check_relay_name">Check relay name and try again.</string>
|
||||
<string name="invalid_relay_address">Invalid relay address!</string>
|
||||
<string name="check_relay_address">Check relay address and try again.</string>
|
||||
<string name="error_adding_relay">Error adding relay</string>
|
||||
|
||||
<!-- OperatorView.kt / ProtocolServersView.kt -->
|
||||
<string name="chat_relays">Chat relays</string>
|
||||
<string name="chat_relays_forward_messages_in_channels">Chat relays forward messages in channels you create.</string>
|
||||
|
||||
<!-- ChannelRelaysView.kt -->
|
||||
<string name="channel_relays_title">Chat relays</string>
|
||||
<string name="no_chat_relays">No chat relays</string>
|
||||
<string name="chat_relays_forward_messages">Chat relays forward messages to channel subscribers.</string>
|
||||
<string name="relay_conn_status_connected">connected</string>
|
||||
<string name="relay_conn_status_connecting">connecting</string>
|
||||
<string name="relay_conn_status_deleted">deleted</string>
|
||||
<string name="relay_conn_status_failed">failed</string>
|
||||
<string name="relay_status_new">new</string>
|
||||
<string name="relay_status_invited">invited</string>
|
||||
<string name="relay_status_accepted">accepted</string>
|
||||
<string name="relay_status_active">active</string>
|
||||
|
||||
<!-- ComposeView.kt channel relay bars -->
|
||||
<string name="relay_bar_active_with_failures">%1$d/%2$d relays active, %3$d failed</string>
|
||||
<string name="relay_bar_active">%1$d/%2$d relays active</string>
|
||||
<string name="relay_bar_connected_with_errors">%1$d/%2$d relays connected, %3$d errors</string>
|
||||
<string name="relay_bar_connected">%1$d/%2$d relays connected</string>
|
||||
<string name="relay_bar_count">%1$d relays</string>
|
||||
|
||||
<!-- GroupMemberInfoView.kt channel-related -->
|
||||
<string name="member_info_section_title_relay">RELAY</string>
|
||||
<string name="member_info_section_title_owner">OWNER</string>
|
||||
<string name="member_info_section_title_subscriber">SUBSCRIBER</string>
|
||||
<string name="info_row_channel">Channel</string>
|
||||
<string name="info_row_relay_link">Relay link</string>
|
||||
<string name="info_row_relay_address">Relay address</string>
|
||||
<string name="via_relay_hostname">via %1$s</string>
|
||||
<string name="share_relay_address">Share relay address</string>
|
||||
<string name="relay_section_footer_owner">Subscribers use relay link to connect to the channel.\nRelay address was used to set up this relay for the channel.</string>
|
||||
<string name="relay_section_footer_subscriber">You connected to the channel via this relay link.</string>
|
||||
<string name="button_remove_subscriber">Remove subscriber</string>
|
||||
<string name="block_subscriber_for_all_question">Block subscriber for all?</string>
|
||||
|
||||
<!-- AddChannelView.kt -->
|
||||
<string name="create_channel_title">Create channel</string>
|
||||
<string name="create_channel_button">Create channel</string>
|
||||
<string name="create_channel_beta_button">Create channel (BETA)</string>
|
||||
<string name="channel_display_name_field">Channel name</string>
|
||||
<string name="creating_channel">Creating channel</string>
|
||||
<string name="error_creating_channel">Error creating channel</string>
|
||||
<string name="cancel_creating_channel_question">Cancel creating channel?</string>
|
||||
<string name="cancel_creating_channel_confirm">Cancel</string>
|
||||
<string name="enable_at_least_one_chat_relay">Enable at least one chat relay to create a channel.</string>
|
||||
<string name="your_profile_shared_with_channel_relays">Your profile %1$s will be shared with channel relays and subscribers.</string>
|
||||
<string name="configure_relays">Configure relays</string>
|
||||
<string name="relay_status_failed">failed</string>
|
||||
<string name="relay_connection_failed">Relay connection failed</string>
|
||||
<string name="not_all_relays_connected">Not all relays connected</string>
|
||||
<string name="wait_verb">Wait</string>
|
||||
<string name="proceed_verb">Proceed</string>
|
||||
<string name="channel_will_start_with_relays">Channel will start working with %1$d of %2$d relays. Proceed?</string>
|
||||
|
||||
<!-- ConnectPlan.kt channel-related -->
|
||||
<string name="relay_address_alert_title">Relay address</string>
|
||||
<string name="relay_address_alert_message">This is a chat relay address, it cannot be used to connect.</string>
|
||||
<string name="connect_plan_open_channel">Open channel</string>
|
||||
<string name="connect_plan_open_new_channel">Open new channel</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel">Your channel</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[This is your link for channel <b>%1$s</b>!]]></string>
|
||||
<string name="error_opening_channel">Error opening channel</string>
|
||||
|
||||
<!-- ChatListNavLinkView.kt channel-related -->
|
||||
<string name="unblock_subscriber_for_all_question">Unblock subscriber for all?</string>
|
||||
</resources>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M129-560q0 63 21.5 121.25T213-333q7.5 8 8.25 18.75T214.5-296q-8 8-17.5 6t-17-10q-48-53.5-73.5-120.75T81-560q0-72 25.5-139.25T180-820.5q7.5-7.5 17-9t17.25 6.25q7.75 7.75 7 18T213-787.5q-41 48-62.5 106.25T129-560Zm143.75 71.75q11.75 34.75 34.75 63.75 6.5 8 7 18t-7.25 17.75q-7.75 7.75-17.5 7.25T273.5-390q-29-36-44.75-79.75T213-560q0-46.5 15.75-90.25T273.5-730q6.5-8 16.25-9t17.5 6.75q7.75 7.75 7.25 17.75t-7 17.5q-23 29-34.75 64.5T261-560q0 37 11.75 71.75ZM451.5-150v-325q-28-8.5-44.5-32.25T390.5-560q0-37.5 26-63.75T480-650q37.5 0 63.75 26.25T570-560q0 29-16.75 52.75T509-475v325q0 12.5-8.25 20.5t-20.75 8q-12.5 0-20.5-8t-8-20.5Zm235.75-481.75Q675.5-666.5 653-696q-7-7.5-7.5-17.5t7.25-17.75q7.75-7.75 17.5-7.25t16.25 8.5q29 36 44.75 79.75T747-560q0 46.5-15.75 90.25T686.5-390q-6.5 8-15.75 8.5t-17-7.25Q646-396.5 646-406.5t7-18q22.5-29 34.25-63.75T699-560q0-37-11.75-71.75ZM831-560q0-63-21.5-121.25T747-787.5q-7.5-7.5-8.25-18.25t7-18.5Q753.5-832 763-830t17.5 9.5q47.5 54 73 121.25T879-560q0 72-25.5 139.25T780.5-300q-8 8-17.5 9.5t-17.25-6.25q-7.75-7.75-7-18T747-333q41-47.5 62.5-105.75T831-560Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Reference in New Issue
Block a user