mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-26 19:35:48 +00:00
android, desktop: sharing channel links (#6828)
* android, desktop: sharing channel links - types, api, strings * implementation * fix build * improve layout * improve card layouts * better divider * preview image * icon, preview * better icons * bigger icon * darker icons * better icon colors * better layouts * compose preview for chat links * sizes * fix editing --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
+88
-3
@@ -2113,7 +2113,7 @@ data class GroupInfo (
|
||||
|
||||
val chatIconName: ImageResource
|
||||
get() = if (useRelays) {
|
||||
MR.images.ic_bigtop_updates_padded
|
||||
MR.images.ic_bigtop_updates_circle_filled
|
||||
} else when (businessChat?.chatType) {
|
||||
null -> MR.images.ic_supervised_user_circle_filled
|
||||
BusinessChatType.Business -> MR.images.ic_work_filled_padded
|
||||
@@ -4307,7 +4307,7 @@ sealed class MsgContent {
|
||||
@Serializable(with = MsgContentSerializer::class) class MCVoice(override val text: String, val duration: Int): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCReport(override val text: String, val reason: ReportReason): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCChat(override val text: String, val chatLink: MsgChatLink): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCChat(override val text: String, val chatLink: MsgChatLink, val ownerSig: LinkOwnerSig? = null): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
|
||||
|
||||
val isVoice: Boolean get() =
|
||||
@@ -4428,7 +4428,8 @@ object MsgContentSerializer : KSerializer<MsgContent> {
|
||||
}
|
||||
"chat" -> {
|
||||
val chatLink = decoder.json.decodeFromString<MsgChatLink>(json["chatLink"].toString())
|
||||
MsgContent.MCChat(text, chatLink)
|
||||
val ownerSig = json["ownerSig"]?.let { decoder.json.decodeFromJsonElement<LinkOwnerSig>(it) }
|
||||
MsgContent.MCChat(text, chatLink, ownerSig)
|
||||
}
|
||||
else -> MsgContent.MCUnknown(t, text, json)
|
||||
}
|
||||
@@ -4489,6 +4490,7 @@ object MsgContentSerializer : KSerializer<MsgContent> {
|
||||
put("type", "chat")
|
||||
put("text", value.text)
|
||||
put("chatLink", json.encodeToJsonElement(value.chatLink))
|
||||
value.ownerSig?.let { put("ownerSig", json.encodeToJsonElement(it)) }
|
||||
}
|
||||
is MsgContent.MCUnknown -> value.json
|
||||
}
|
||||
@@ -4548,8 +4550,91 @@ sealed class MsgChatLink {
|
||||
@Serializable @SerialName("contact") data class Contact(val connLink: String, val profile: Profile, val business: Boolean) : MsgChatLink()
|
||||
@Serializable @SerialName("invitation") data class Invitation(val invLink: String, val profile: Profile) : MsgChatLink()
|
||||
@Serializable @SerialName("group") data class Group(val connLink: String, val groupProfile: GroupProfile) : MsgChatLink()
|
||||
|
||||
val isPublicGroup: Boolean
|
||||
get() = (this as? Group)?.groupProfile?.publicGroup != null
|
||||
|
||||
val connLinkStr: String
|
||||
get() = when (this) {
|
||||
is Group -> connLink
|
||||
is Contact -> connLink
|
||||
is Invitation -> invLink
|
||||
}
|
||||
|
||||
val image: String?
|
||||
get() = when (this) {
|
||||
is Group -> groupProfile.image
|
||||
is Contact -> profile.image
|
||||
is Invitation -> profile.image
|
||||
}
|
||||
|
||||
val displayName: String
|
||||
get() = when (this) {
|
||||
is Group -> groupProfile.displayName
|
||||
is Contact -> profile.displayName
|
||||
is Invitation -> profile.displayName
|
||||
}
|
||||
|
||||
val fullName: String
|
||||
get() = when (this) {
|
||||
is Group -> groupProfile.fullName
|
||||
is Contact -> profile.fullName
|
||||
is Invitation -> profile.fullName
|
||||
}
|
||||
|
||||
val shortDescription: String?
|
||||
get() {
|
||||
val s = when (this) {
|
||||
is Group -> groupProfile.shortDescr
|
||||
is Contact -> profile.shortDescr
|
||||
is Invitation -> profile.shortDescr
|
||||
}
|
||||
return s?.trim()?.ifEmpty { null }
|
||||
}
|
||||
|
||||
val iconRes: ImageResource
|
||||
get() = when (this) {
|
||||
is Group -> when (groupProfile.publicGroup?.groupType) {
|
||||
GroupType.Channel -> MR.images.ic_bigtop_updates_circle_filled
|
||||
else -> MR.images.ic_supervised_user_circle_filled
|
||||
}
|
||||
is Contact -> if (business) MR.images.ic_work_filled_padded else MR.images.ic_account_circle_filled
|
||||
is Invitation -> MR.images.ic_account_circle_filled
|
||||
}
|
||||
|
||||
val smallIconRes: ImageResource
|
||||
get() = when (this) {
|
||||
is Group -> when (groupProfile.publicGroup?.groupType) {
|
||||
GroupType.Channel -> MR.images.ic_bigtop_updates
|
||||
else -> MR.images.ic_group
|
||||
}
|
||||
is Contact -> if (business) MR.images.ic_work else MR.images.ic_person
|
||||
is Invitation -> MR.images.ic_person
|
||||
}
|
||||
|
||||
fun infoLine(signed: Boolean): String {
|
||||
var s = when (this) {
|
||||
is Group -> when (groupProfile.publicGroup?.groupType) {
|
||||
GroupType.Channel -> generalGetString(MR.strings.chat_link_channel)
|
||||
else -> generalGetString(MR.strings.chat_link_group)
|
||||
}
|
||||
is Contact -> if (business) generalGetString(MR.strings.chat_link_business_address) else generalGetString(MR.strings.chat_link_contact_address)
|
||||
is Invitation -> generalGetString(MR.strings.chat_link_one_time)
|
||||
}
|
||||
if (signed) {
|
||||
s += " " + if (isPublicGroup) generalGetString(MR.strings.chat_link_from_owner) else generalGetString(MR.strings.chat_link_signed)
|
||||
}
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class LinkOwnerSig(
|
||||
val ownerId: String? = null,
|
||||
val chatBinding: String,
|
||||
val ownerSig: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class FormattedText(val text: String, val format: Format? = null) {
|
||||
val linkUri: String? get() =
|
||||
|
||||
+31
-7
@@ -1135,6 +1135,13 @@ object ChatController {
|
||||
return processSendMessageCmd(rh, cmd)?.map { it.chatItem }
|
||||
}
|
||||
|
||||
suspend fun apiShareChatMsgContent(rh: Long?, shareChatType: ChatType, shareChatId: Long, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, sendAsGroup: Boolean): MsgContent? {
|
||||
val r = sendCmd(rh, CC.ApiShareChatMsgContent(shareChatType, shareChatId, toChatType, toChatId, toScope, sendAsGroup))
|
||||
if (r is API.Result && r.res is CR.ChatMsgContent) return r.res.msgContent
|
||||
apiErrorAlert("apiShareChatMsgContent", generalGetString(MR.strings.error_sharing_channel), r)
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List<Long>): CR.ForwardPlan? {
|
||||
val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, chatItemIds))
|
||||
if (r is API.Result && r.res is CR.ForwardPlan) return r.res
|
||||
@@ -1485,9 +1492,9 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiConnectPlan(rh: Long?, connLink: String, inProgress: MutableState<Boolean>): Pair<CreatedConnLink, ConnectionPlan>? {
|
||||
suspend fun apiConnectPlan(rh: Long?, connLink: String, linkOwnerSig: LinkOwnerSig? = null, inProgress: MutableState<Boolean>): Pair<CreatedConnLink, ConnectionPlan>? {
|
||||
val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null }
|
||||
val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink), inProgress = inProgress)
|
||||
val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink, linkOwnerSig), inProgress = inProgress)
|
||||
if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan
|
||||
if (inProgress.value && r != null) apiConnectResponseAlert(r)
|
||||
return null
|
||||
@@ -3624,6 +3631,7 @@ sealed class 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 sendAsGroup: Boolean, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List<Long>, val ttl: Int?): CC()
|
||||
class ApiShareChatMsgContent(val shareChatType: ChatType, val shareChatId: Long, val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean): 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()
|
||||
@@ -3683,7 +3691,7 @@ sealed class CC {
|
||||
class APIAddContact(val userId: Long, val incognito: Boolean): CC()
|
||||
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
|
||||
class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC()
|
||||
class APIConnectPlan(val userId: Long, val connLink: String): CC()
|
||||
class APIConnectPlan(val userId: Long, val connLink: String, val linkOwnerSig: LinkOwnerSig? = null): CC()
|
||||
class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData): 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()
|
||||
@@ -3822,6 +3830,9 @@ sealed class CC {
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_forward ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) " as_group=on" else ""} ${chatRef(fromChatType, fromChatId, fromScope)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
|
||||
}
|
||||
is ApiShareChatMsgContent -> {
|
||||
"/_share chat content ${chatRef(shareChatType, shareChatId, null)} ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) "(as_group=on)" else ""}"
|
||||
}
|
||||
is ApiPlanForwardChatItems -> {
|
||||
"/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${chatItemIds.joinToString(",")}"
|
||||
}
|
||||
@@ -3884,7 +3895,10 @@ sealed class CC {
|
||||
is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}"
|
||||
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
|
||||
is ApiChangeConnectionUser -> "/_set conn user :$connId $userId"
|
||||
is APIConnectPlan -> "/_connect plan $userId $connLink"
|
||||
is APIConnectPlan -> {
|
||||
val sigStr = if (linkOwnerSig != null) " sig=${json.encodeToString(linkOwnerSig)}" else ""
|
||||
"/_connect plan $userId $connLink$sigStr"
|
||||
}
|
||||
is APIPrepareContact -> "/_prepare contact $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(contactShortLinkData)}"
|
||||
is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} direct=${onOff(directLink)} ${json.encodeToString(groupShortLinkData)}"
|
||||
is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId"
|
||||
@@ -4003,6 +4017,7 @@ sealed class CC {
|
||||
is ApiChatItemReaction -> "apiChatItemReaction"
|
||||
is ApiGetReactionMembers -> "apiGetReactionMembers"
|
||||
is ApiForwardChatItems -> "apiForwardChatItems"
|
||||
is ApiShareChatMsgContent -> "apiShareChatMsgContent"
|
||||
is ApiPlanForwardChatItems -> "apiPlanForwardChatItems"
|
||||
is ApiNewGroup -> "apiNewGroup"
|
||||
is ApiNewPublicGroup -> "apiNewPublicGroup"
|
||||
@@ -6318,6 +6333,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("subscriptionStatus") class SubscriptionStatusEvt(val subscriptionStatus: SubscriptionStatus, val connections: List<String>): CR()
|
||||
@Serializable @SerialName("chatInfoUpdated") class ChatInfoUpdated(val user: UserRef, val chatInfo: ChatInfo): CR()
|
||||
@Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List<AChatItem>): CR()
|
||||
@Serializable @SerialName("chatMsgContent") class ChatMsgContent(val user: UserRef, val msgContent: MsgContent): CR()
|
||||
@Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List<AChatItem>): CR()
|
||||
@Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@@ -6506,6 +6522,7 @@ sealed class CR {
|
||||
is SubscriptionStatusEvt -> "subscriptionStatus"
|
||||
is ChatInfoUpdated -> "chatInfoUpdated"
|
||||
is NewChatItems -> "newChatItems"
|
||||
is ChatMsgContent -> "chatMsgContent"
|
||||
is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated"
|
||||
is ChatItemUpdated -> "chatItemUpdated"
|
||||
is ChatItemNotChanged -> "chatItemNotChanged"
|
||||
@@ -6686,6 +6703,7 @@ sealed class CR {
|
||||
is SubscriptionStatusEvt -> "subscriptionStatus $subscriptionStatus\nconnections: $connections"
|
||||
is ChatInfoUpdated -> withUser(user, json.encodeToString(chatInfo))
|
||||
is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) })
|
||||
is ChatMsgContent -> withUser(user, msgContent.toString())
|
||||
is ChatItemsStatusesUpdated -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) })
|
||||
is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem))
|
||||
is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem))
|
||||
@@ -6840,6 +6858,12 @@ fun simplexChatLink(uri: String): String =
|
||||
if (uri.startsWith("simplex:/")) uri.replace("simplex:/", "https://simplex.chat/")
|
||||
else uri
|
||||
|
||||
@Serializable
|
||||
sealed class OwnerVerification {
|
||||
@Serializable @SerialName("verified") object Verified : OwnerVerification()
|
||||
@Serializable @SerialName("failed") class Failed(val reason: String) : OwnerVerification()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ConnectionPlan {
|
||||
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
|
||||
@@ -6850,7 +6874,7 @@ sealed class ConnectionPlan {
|
||||
|
||||
@Serializable
|
||||
sealed class InvitationLinkPlan {
|
||||
@Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): InvitationLinkPlan()
|
||||
@Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null): InvitationLinkPlan()
|
||||
@Serializable @SerialName("ownLink") object OwnLink: InvitationLinkPlan()
|
||||
@Serializable @SerialName("connecting") class Connecting(val contact_: Contact? = null): InvitationLinkPlan()
|
||||
@Serializable @SerialName("known") class Known(val contact: Contact): InvitationLinkPlan()
|
||||
@@ -6858,7 +6882,7 @@ sealed class InvitationLinkPlan {
|
||||
|
||||
@Serializable
|
||||
sealed class ContactAddressPlan {
|
||||
@Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): ContactAddressPlan()
|
||||
@Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null): ContactAddressPlan()
|
||||
@Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan()
|
||||
@Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan()
|
||||
@Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan()
|
||||
@@ -6868,7 +6892,7 @@ sealed class ContactAddressPlan {
|
||||
|
||||
@Serializable
|
||||
sealed class GroupLinkPlan {
|
||||
@Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan()
|
||||
@Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null, val ownerVerification: OwnerVerification? = 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()
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ val GroupDark = Color(80, 80, 80, 60)
|
||||
val IncomingCallLight = Color(239, 237, 236, 255)
|
||||
val WarningOrange = Color(255, 127, 0, 255)
|
||||
val WarningYellow = Color(255, 192, 0, 255)
|
||||
val FileLight = Color(183, 190, 199, 255)
|
||||
val FileDark = Color(101, 101, 106, 255)
|
||||
val FileLight = Color(191, 194, 199, 255)
|
||||
val FileDark = Color(94, 94, 98, 255)
|
||||
|
||||
val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black
|
||||
val NoteFolderIconColor: Color @Composable get() = MaterialTheme.appColors.primaryVariant2
|
||||
|
||||
+1
-1
@@ -3204,7 +3204,7 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (
|
||||
val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId)
|
||||
close?.invoke()
|
||||
ModalManager.end.showModalCloseable(true) {
|
||||
GroupLinkView(chatModel, rhId, groupInfo, link, onGroupLinkUpdated = null, isChannel = groupInfo.useRelays)
|
||||
GroupLinkView(chatModel, rhId, groupInfo, link, onGroupLinkUpdated = null, isChannel = groupInfo.useRelays, shareGroupInfo = groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package chat.simplex.common.views.chat
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.MsgChatLink
|
||||
import chat.simplex.common.ui.theme.appColors
|
||||
import chat.simplex.common.views.helpers.ProfileImage
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ComposeChatLinkView(
|
||||
chatLink: MsgChatLink,
|
||||
cancelEnabled: Boolean,
|
||||
cancelPreview: () -> Unit
|
||||
) {
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.background(sentColor)
|
||||
.padding(start = 8.dp, top = 6.dp, bottom = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ProfileImage(size = 54.dp, image = chatLink.image, icon = chatLink.iconRes)
|
||||
Column(
|
||||
Modifier.fillMaxWidth().weight(1f).padding(horizontal = 8.dp)
|
||||
) {
|
||||
Text(chatLink.displayName, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
chatLink.shortDescription?.let { descr ->
|
||||
Text(
|
||||
descr,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (cancelEnabled) {
|
||||
IconButton(onClick = cancelPreview) {
|
||||
Icon(painterResource(MR.images.ic_close), null, tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-3
@@ -57,6 +57,7 @@ const val MAX_NUMBER_OF_MENTIONS = 3
|
||||
sealed class ComposePreview {
|
||||
@Serializable object NoPreview: ComposePreview()
|
||||
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
|
||||
@Serializable class ChatLinkPreview(val chatLink: MsgChatLink, val ownerSig: LinkOwnerSig? = null): ComposePreview()
|
||||
@Serializable class MediaPreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
|
||||
@Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
|
||||
@Serializable class FilePreview(val fileName: String, val uri: URI): ComposePreview()
|
||||
@@ -112,7 +113,12 @@ data class ComposeState(
|
||||
val mentions: MentionedMembers = emptyMap()
|
||||
) {
|
||||
constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this(
|
||||
ComposeMessage(editingItem.content.text),
|
||||
ComposeMessage(
|
||||
when (val mc = editingItem.content.msgContent) {
|
||||
is MsgContent.MCChat -> stripTextLink(mc.text, mc.chatLink.connLinkStr)
|
||||
else -> editingItem.content.text
|
||||
}
|
||||
),
|
||||
editingItem.formattedText ?: FormattedText.plain(editingItem.content.text),
|
||||
liveMessage,
|
||||
chatItemPreview(editingItem),
|
||||
@@ -163,6 +169,7 @@ data class ComposeState(
|
||||
val hasContent = when (preview) {
|
||||
is ComposePreview.MediaPreview -> true
|
||||
is ComposePreview.VoicePreview -> true
|
||||
is ComposePreview.ChatLinkPreview -> true
|
||||
is ComposePreview.FilePreview -> true
|
||||
else -> !whitespaceOnly || forwarding || liveMessage != null || submittingValidReport
|
||||
}
|
||||
@@ -174,6 +181,7 @@ data class ComposeState(
|
||||
val linkPreviewAllowed: Boolean
|
||||
get() =
|
||||
when (preview) {
|
||||
is ComposePreview.ChatLinkPreview -> false
|
||||
is ComposePreview.MediaPreview -> false
|
||||
is ComposePreview.VoicePreview -> false
|
||||
is ComposePreview.FilePreview -> false
|
||||
@@ -200,6 +208,7 @@ data class ComposeState(
|
||||
get() = when (preview) {
|
||||
ComposePreview.NoPreview -> false
|
||||
is ComposePreview.CLinkPreview -> false
|
||||
is ComposePreview.ChatLinkPreview -> false
|
||||
is ComposePreview.MediaPreview -> preview.content.isNotEmpty()
|
||||
is ComposePreview.VoicePreview -> false
|
||||
is ComposePreview.FilePreview -> true
|
||||
@@ -468,6 +477,7 @@ fun ComposeView(
|
||||
is SharedContent.File -> listOf(shared.uri.toString())
|
||||
is SharedContent.Text -> emptyList()
|
||||
is SharedContent.Forward -> emptyList()
|
||||
is SharedContent.ChatLink -> emptyList()
|
||||
}
|
||||
// When sharing a file and pasting it in SimpleX itself, the file shouldn't be deleted before sending or before leaving the chat after sharing
|
||||
chatModel.filesToDelete.removeAll { file ->
|
||||
@@ -672,8 +682,11 @@ fun ComposeView(
|
||||
is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration)
|
||||
is MsgContent.MCFile -> MsgContent.MCFile(msgText)
|
||||
is MsgContent.MCReport -> MsgContent.MCReport(msgText, reason = msgContent.reason)
|
||||
// TODO [short links] update chat link
|
||||
is MsgContent.MCChat -> MsgContent.MCChat(msgText, chatLink = msgContent.chatLink)
|
||||
is MsgContent.MCChat -> {
|
||||
val linkStr = msgContent.chatLink.connLinkStr
|
||||
val text = if (msgText.isEmpty()) linkStr else "$msgText\n$linkStr"
|
||||
MsgContent.MCChat(text, chatLink = msgContent.chatLink, ownerSig = msgContent.ownerSig)
|
||||
}
|
||||
is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json)
|
||||
}
|
||||
}
|
||||
@@ -760,6 +773,11 @@ fun ComposeView(
|
||||
when (val preview = cs.preview) {
|
||||
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
|
||||
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
|
||||
is ComposePreview.ChatLinkPreview -> {
|
||||
val linkStr = preview.chatLink.connLinkStr
|
||||
val text = if (msgText.isEmpty()) linkStr else "$msgText\n$linkStr"
|
||||
msgs.add(MsgContent.MCChat(text, preview.chatLink, preview.ownerSig))
|
||||
}
|
||||
is ComposePreview.MediaPreview -> {
|
||||
// TODO batch send: batch media previews
|
||||
preview.content.forEachIndexed { index, it ->
|
||||
@@ -1060,6 +1078,11 @@ fun ComposeView(
|
||||
::cancelLinkPreview,
|
||||
cancelEnabled = !composeState.value.inProgress
|
||||
)
|
||||
is ComposePreview.ChatLinkPreview -> ComposeChatLinkView(
|
||||
chatLink = preview.chatLink,
|
||||
cancelEnabled = !composeState.value.inProgress,
|
||||
cancelPreview = { composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) }
|
||||
)
|
||||
is ComposePreview.MediaPreview -> ComposeImageView(
|
||||
preview,
|
||||
::cancelImages,
|
||||
@@ -1440,6 +1463,22 @@ fun ComposeView(
|
||||
contextItem = ComposeContextItem.ForwardingItems(shared.chatItems, shared.fromChatInfo),
|
||||
preview = if (composeState.value.preview is ComposePreview.CLinkPreview) composeState.value.preview else ComposePreview.NoPreview
|
||||
)
|
||||
is SharedContent.ChatLink -> {
|
||||
val cInfo = chat.chatInfo
|
||||
val sendAsGroup = (cInfo as? ChatInfo.Group)?.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false
|
||||
withBGApi {
|
||||
val mc = chatModel.controller.apiShareChatMsgContent(
|
||||
chat.remoteHostId, ChatType.Group, shared.groupInfo.groupId,
|
||||
cInfo.chatType, cInfo.apiId,
|
||||
cInfo.groupChatScope(), sendAsGroup
|
||||
)
|
||||
if (mc is MsgContent.MCChat) {
|
||||
composeState.value = composeState.value.copy(
|
||||
preview = ComposePreview.ChatLinkPreview(mc.chatLink, mc.ownerSig)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
chatModel.sharedContent.value = null
|
||||
|
||||
+15
-4
@@ -39,7 +39,7 @@ fun ContextItemView(
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
|
||||
@Composable
|
||||
fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int) {
|
||||
fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int, prefix: AnnotatedString? = null, stripLink: String? = null) {
|
||||
val inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = if (attachment != null) {
|
||||
remember(contextItem.id) {
|
||||
val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = {
|
||||
@@ -68,24 +68,35 @@ fun ContextItemView(
|
||||
userMemberId = when {
|
||||
chatInfo is ChatInfo.Group -> chatInfo.groupInfo.membership.memberId
|
||||
else -> null
|
||||
}
|
||||
},
|
||||
prefix = prefix,
|
||||
stripLink = stripLink,
|
||||
)
|
||||
}
|
||||
|
||||
fun attachment(contextItem: ChatItem): ImageResource? {
|
||||
val fileIsLoaded = getLoadedFilePath(contextItem.file) != null
|
||||
|
||||
return when (contextItem.content.msgContent) {
|
||||
val mc = contextItem.content.msgContent
|
||||
return when (mc) {
|
||||
is MsgContent.MCFile -> if (fileIsLoaded) MR.images.ic_draft_filled else null
|
||||
is MsgContent.MCImage -> MR.images.ic_image
|
||||
is MsgContent.MCVoice -> if (fileIsLoaded) MR.images.ic_play_arrow_filled else null
|
||||
is MsgContent.MCChat -> mc.chatLink.smallIconRes
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContextMsgPreview(contextItem: ChatItem, lines: Int) {
|
||||
MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines)
|
||||
val mc = contextItem.content.msgContent
|
||||
if (mc is MsgContent.MCChat) {
|
||||
val hasText = contextItem.text != mc.chatLink.connLinkStr
|
||||
val prefix = buildAnnotatedString { append(mc.chatLink.displayName + if (hasText) " - " else "") }
|
||||
MessageText(contextItem, remember(contextItem.id) { mc.chatLink.smallIconRes }, lines, prefix = prefix, stripLink = mc.chatLink.connLinkStr)
|
||||
} else {
|
||||
MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines)
|
||||
}
|
||||
}
|
||||
|
||||
val sent = contextItems[0].chatDir.sent
|
||||
|
||||
+15
-1
@@ -167,7 +167,7 @@ fun ModalData.GroupChatInfoView(
|
||||
clearChat = { clearChatDialog(chat, close) },
|
||||
leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) },
|
||||
manageGroupLink = {
|
||||
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated, isChannel = groupInfo.useRelays) }
|
||||
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated, isChannel = groupInfo.useRelays, shareGroupInfo = groupInfo) }
|
||||
},
|
||||
onSearchClicked = onSearchClicked,
|
||||
deletingItems = deletingItems
|
||||
@@ -554,6 +554,11 @@ fun ModalData.GroupChatInfoLayout(
|
||||
} else if (channelLink != null) {
|
||||
anyTopSectionRowShow = true
|
||||
ChannelLinkQRCodeSection(channelLink)
|
||||
ShareViaChatButton {
|
||||
chatModel.sharedContent.value = SharedContent.ChatLink(groupInfo)
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
}
|
||||
}
|
||||
if (groupInfo.isOwner || activeSortedMembers.any { it.memberRole >= GroupMemberRole.Owner }) {
|
||||
anyTopSectionRowShow = true
|
||||
@@ -1138,6 +1143,15 @@ private fun ChannelLinkQRCodeSection(groupLink: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShareViaChatButton(onClick: () -> Unit) {
|
||||
SectionItemView(onClick) {
|
||||
Icon(painterResource(MR.images.ic_forward), null, tint = MaterialTheme.colors.primary)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(MR.strings.share_via_chat), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelMembersButton(rhId: Long?, groupInfo: GroupInfo, showMemberInfo: (GroupMember, GroupRelay?) -> Unit) {
|
||||
val title = if (groupInfo.isOwner) {
|
||||
|
||||
+48
-40
@@ -33,6 +33,7 @@ fun GroupLinkView(
|
||||
onGroupLinkUpdated: ((GroupLink?) -> Unit)?,
|
||||
creatingGroup: Boolean = false,
|
||||
isChannel: Boolean = false,
|
||||
shareGroupInfo: GroupInfo? = null,
|
||||
close: (() -> Unit)? = null
|
||||
) {
|
||||
var groupLinkVar by rememberSaveable(stateSaver = GroupLink.nullableStateSaver) { mutableStateOf(groupLink) }
|
||||
@@ -124,6 +125,7 @@ fun GroupLinkView(
|
||||
groupLinkMemberRole,
|
||||
creatingLink,
|
||||
isChannel = isChannel,
|
||||
shareGroupInfo = shareGroupInfo,
|
||||
createLink = ::createLink,
|
||||
showAddShortLinkAlert = ::showAddShortLinkAlert,
|
||||
updateLink = {
|
||||
@@ -171,6 +173,7 @@ fun GroupLinkLayout(
|
||||
groupLinkMemberRole: MutableState<GroupMemberRole?>,
|
||||
creatingLink: Boolean,
|
||||
isChannel: Boolean = false,
|
||||
shareGroupInfo: GroupInfo? = null,
|
||||
createLink: () -> Unit,
|
||||
showAddShortLinkAlert: ((() -> Unit)?) -> Unit,
|
||||
updateLink: () -> Unit,
|
||||
@@ -230,40 +233,56 @@ fun GroupLinkLayout(
|
||||
} else null) {
|
||||
SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = showShortLink.value)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp)
|
||||
) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SimpleButton(
|
||||
stringResource(MR.strings.share_link),
|
||||
icon = painterResource(MR.images.ic_share),
|
||||
click = {
|
||||
if (!isChannel && groupLink.shouldBeUpgraded) {
|
||||
showAddShortLinkAlert {
|
||||
clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value))
|
||||
}
|
||||
} else {
|
||||
if (!isChannel && groupLink.shouldBeUpgraded) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_add),
|
||||
stringResource(MR.strings.upgrade_group_link),
|
||||
click = { showAddShortLinkAlert(null) },
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_share),
|
||||
stringResource(MR.strings.share_link),
|
||||
click = {
|
||||
if (!isChannel && groupLink.shouldBeUpgraded) {
|
||||
showAddShortLinkAlert {
|
||||
clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value))
|
||||
}
|
||||
} else {
|
||||
clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value))
|
||||
}
|
||||
},
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
if (shareGroupInfo != null) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_forward),
|
||||
stringResource(MR.strings.share_via_chat),
|
||||
click = {
|
||||
chatModel.sharedContent.value = SharedContent.ChatLink(shareGroupInfo)
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
},
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
if (creatingGroup && close != null) {
|
||||
ContinueButton(close)
|
||||
} else if (!isChannel) {
|
||||
SimpleButton(
|
||||
stringResource(MR.strings.delete_link),
|
||||
icon = painterResource(MR.images.ic_delete),
|
||||
color = Color.Red,
|
||||
click = deleteLink
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!isChannel && groupLink.shouldBeUpgraded) {
|
||||
AddShortLinkButton(text = stringResource(MR.strings.upgrade_group_link)) {
|
||||
showAddShortLinkAlert(null)
|
||||
}
|
||||
if (!creatingGroup && !isChannel) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_delete),
|
||||
stringResource(MR.strings.delete_link),
|
||||
click = deleteLink,
|
||||
iconColor = Color.Red,
|
||||
textColor = Color.Red,
|
||||
)
|
||||
}
|
||||
if (creatingGroup && close != null) {
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
ContinueButton(close)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,17 +290,6 @@ fun GroupLinkLayout(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddShortLinkButton(text: String, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_add),
|
||||
text,
|
||||
onClick,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole?>, enabled: Boolean = true) {
|
||||
Row(
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ fun GroupProfileLayout(
|
||||
) {
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
ProfileImage(108.dp, profileImage.value, color = MaterialTheme.colors.secondary.copy(alpha = 0.1f))
|
||||
ProfileImage(108.dp, profileImage.value, icon = groupInfo.chatIconName, color = MaterialTheme.colors.secondary.copy(alpha = 0.1f))
|
||||
EditImageButton { scope.launch { bottomSheetModalState.show() } }
|
||||
}
|
||||
if (profileImage.value != null) {
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.ProfileImage
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun CIChatLinkHeader(
|
||||
chatLink: MsgChatLink,
|
||||
ownerSig: LinkOwnerSig?,
|
||||
hasText: Boolean,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(start = 8.dp, end = 12.dp, top = 8.dp, bottom = 4.dp)
|
||||
) {
|
||||
Row(
|
||||
Modifier.defaultMinSize(minWidth = 220.dp)
|
||||
) {
|
||||
ProfileImage(
|
||||
size = 54.dp,
|
||||
image = chatLink.image,
|
||||
icon = chatLink.iconRes,
|
||||
color = if (isInDarkTheme()) FileDark else FileLight
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column(
|
||||
Modifier.defaultMinSize(minHeight = 54.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
chatLink.displayName,
|
||||
style = MaterialTheme.typography.caption,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
val fn = chatLink.fullName
|
||||
if (fn.isNotEmpty() && fn != chatLink.displayName) {
|
||||
Text(fn, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider(Modifier.fillMaxWidth().padding(top = 8.dp))
|
||||
Column(Modifier.padding(top = 8.dp, bottom = 4.dp, start = 4.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
chatLink.shortDescription?.let { descr ->
|
||||
Text(
|
||||
descr,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 18.sp,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
chatLink.infoLine(signed = ownerSig != null),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 18.sp,
|
||||
)
|
||||
Text(
|
||||
stringResource(MR.strings.tap_to_open),
|
||||
color = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -127,7 +127,8 @@ fun CIFileView(
|
||||
fun fileIndicator() {
|
||||
Box(
|
||||
Modifier
|
||||
.size(42.sp.toDp() * sizeMultiplier)
|
||||
.padding(top = 2.sp.toDp())
|
||||
.size(40.sp.toDp() * sizeMultiplier)
|
||||
.clip(RoundedCornerShape(4.sp.toDp() * sizeMultiplier)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
+12
-13
@@ -15,6 +15,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
@@ -52,15 +53,12 @@ fun CIGroupInvitationView(
|
||||
else if (isInDarkTheme()) FileDark else FileLight
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(vertical = 4.dp)
|
||||
.padding(end = 2.dp)
|
||||
Modifier.defaultMinSize(minWidth = 220.dp)
|
||||
) {
|
||||
ProfileImage(size = 60.dp, image = groupInvitation.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled, color = iconColor)
|
||||
Spacer(Modifier.padding(horizontal = 3.dp))
|
||||
ProfileImage(size = 54.dp, image = groupInvitation.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled, color = iconColor)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column(
|
||||
Modifier.defaultMinSize(minHeight = 60.dp),
|
||||
Modifier.defaultMinSize(minHeight = 54.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(p.displayName, style = MaterialTheme.typography.caption, fontWeight = FontWeight.Medium, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
@@ -98,8 +96,7 @@ fun CIGroupInvitationView(
|
||||
Box(
|
||||
Modifier
|
||||
.width(IntrinsicSize.Min)
|
||||
.padding(vertical = 3.dp)
|
||||
.padding(start = 8.dp, end = 12.dp),
|
||||
.padding(start = 8.dp, end = 12.dp, top = 8.dp, bottom = 4.dp),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
Box(
|
||||
@@ -112,10 +109,10 @@ fun CIGroupInvitationView(
|
||||
) {
|
||||
groupInfoView()
|
||||
val secondaryColor = MaterialTheme.colors.secondary
|
||||
Column(Modifier.padding(top = 2.dp, start = 5.dp)) {
|
||||
Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp))
|
||||
Divider(Modifier.fillMaxWidth().padding(top = 8.dp))
|
||||
Column(Modifier.padding(top = 8.dp, bottom = 4.dp, start = 4.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
if (action) {
|
||||
Text(groupInvitationStr())
|
||||
Text(groupInvitationStr(), fontSize = 13.sp, lineHeight = 18.sp)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join))
|
||||
@@ -131,7 +128,9 @@ fun CIGroupInvitationView(
|
||||
buildAnnotatedString {
|
||||
append(groupInvitationStr())
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) }
|
||||
}
|
||||
},
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 18.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+40
-5
@@ -23,6 +23,7 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.planAndConnect
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -55,7 +56,7 @@ fun FramedItemView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean) {
|
||||
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean, stripLink: String? = null, prefix: AnnotatedString? = null) {
|
||||
MarkdownText(
|
||||
qi.text,
|
||||
qi.formattedText,
|
||||
@@ -66,11 +67,13 @@ fun FramedItemView(
|
||||
linkMode = linkMode,
|
||||
uriHandler = if (appPlatform.isDesktop) uriHandler else null,
|
||||
showTimestamp = showTimestamp,
|
||||
prefix = prefix,
|
||||
stripLink = stripLink,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ciQuotedMsgView(qi: CIQuote) {
|
||||
fun ciQuotedMsgView(qi: CIQuote, stripLink: String? = null, prefix: AnnotatedString? = null) {
|
||||
Box(
|
||||
Modifier
|
||||
// this width limitation prevents crash on calculating constraints that may happen if you post veeeery long message and then quote it.
|
||||
@@ -89,10 +92,10 @@ fun FramedItemView(
|
||||
style = TextStyle(fontSize = 13.5.sp, color = if (qi.chatDir is CIDirection.GroupSnd) CurrentColors.value.colors.primary else CurrentColors.value.colors.secondary),
|
||||
maxLines = 1
|
||||
)
|
||||
ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp)
|
||||
ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp, stripLink = stripLink, prefix = prefix)
|
||||
}
|
||||
} else {
|
||||
ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp)
|
||||
ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp, stripLink = stripLink, prefix = prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,6 +180,20 @@ fun FramedItemView(
|
||||
tint = if (isInDarkTheme()) FileDark else FileLight
|
||||
)
|
||||
}
|
||||
is MsgContent.MCChat -> {
|
||||
val prefix = buildAnnotatedString {
|
||||
append(qi.content.chatLink.displayName + if (qi.content.text != qi.content.chatLink.connLinkStr) " - " else "")
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().weight(1f)) {
|
||||
ciQuotedMsgView(qi, stripLink = qi.content.chatLink.connLinkStr, prefix = prefix)
|
||||
}
|
||||
Icon(
|
||||
painterResource(qi.content.chatLink.smallIconRes),
|
||||
null,
|
||||
Modifier.padding(top = 6.dp, end = 4.dp).size(22.dp),
|
||||
tint = if (isInDarkTheme()) FileDark else FileLight
|
||||
)
|
||||
}
|
||||
else -> ciQuotedMsgView(qi)
|
||||
}
|
||||
}
|
||||
@@ -329,6 +346,22 @@ fun FramedItemView(
|
||||
CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCChat -> {
|
||||
val hasText = mc.text != mc.chatLink.connLinkStr
|
||||
Box(
|
||||
Modifier.combinedClickable(
|
||||
onClick = {
|
||||
withBGApi { planAndConnect(chat.remoteHostId, mc.chatLink.connLinkStr, linkOwnerSig = mc.ownerSig, close = null) }
|
||||
},
|
||||
onLongClick = { showMenu.value = true }
|
||||
)
|
||||
) {
|
||||
CIChatLinkHeader(chatLink = mc.chatLink, ownerSig = mc.ownerSig, hasText = hasText)
|
||||
}
|
||||
if (hasText) {
|
||||
CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp, stripLink = mc.chatLink.connLinkStr)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCReport -> {
|
||||
val prefix = buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = Color.Red, fontStyle = FontStyle.Italic)) {
|
||||
@@ -366,7 +399,8 @@ fun CIMarkdownText(
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
prefix: AnnotatedString? = null
|
||||
prefix: AnnotatedString? = null,
|
||||
stripLink: String? = null
|
||||
) {
|
||||
val chatInfo = chat.chatInfo
|
||||
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
|
||||
@@ -382,6 +416,7 @@ fun CIMarkdownText(
|
||||
else -> null
|
||||
},
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix,
|
||||
stripLink = stripLink,
|
||||
selectionRange = selection.highlightRange,
|
||||
onTextLayoutResult = selection.onTextLayoutResult
|
||||
)
|
||||
|
||||
+20
@@ -109,9 +109,12 @@ fun MarkdownText (
|
||||
showViaProxy: Boolean = false,
|
||||
showTimestamp: Boolean = true,
|
||||
prefix: AnnotatedString? = null,
|
||||
stripLink: String? = null,
|
||||
selectionRange: IntRange? = null,
|
||||
onTextLayoutResult: ((TextLayoutResult) -> Unit)? = null
|
||||
) {
|
||||
val text = if (stripLink != null) stripTextLink(text.toString(), stripLink) else text
|
||||
val formattedText = if (stripLink != null) stripFormattedTextLink(formattedText, stripLink) else formattedText
|
||||
val textLayoutDirection = remember (text) {
|
||||
if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
}
|
||||
@@ -532,3 +535,20 @@ private fun isRtl(s: CharSequence): Boolean {
|
||||
}
|
||||
|
||||
fun mentionText(name: String): String = if (name.contains(" @")) "@'$name'" else "@$name"
|
||||
|
||||
fun stripTextLink(text: String, link: String): String =
|
||||
if (text == link) ""
|
||||
else if (text.endsWith("\n$link")) text.dropLast(link.length + 1)
|
||||
else text
|
||||
|
||||
fun stripFormattedTextLink(ft: List<FormattedText>?, link: String): List<FormattedText>? {
|
||||
if (ft == null || ft.isEmpty() || ft.last().text != link) return ft
|
||||
val result = ft.toMutableList()
|
||||
result.removeLast()
|
||||
val i = result.lastIndex
|
||||
if (i >= 0 && result[i].format == null && result[i].text.endsWith("\n")) {
|
||||
result[i] = FormattedText(result[i].text.dropLast(1), null)
|
||||
if (result[i].text.isEmpty()) result.removeLast()
|
||||
}
|
||||
return result.ifEmpty { null }
|
||||
}
|
||||
|
||||
+27
-7
@@ -31,6 +31,7 @@ import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.newchat.planAndConnect
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
@@ -241,12 +242,18 @@ fun ChatPreviewView(
|
||||
Text(previewText.first, color = previewText.second)
|
||||
} else if (ci != null && showChatPreviews) {
|
||||
val (text: CharSequence, inlineTextContent) = when {
|
||||
ci.meta.itemDeleted == null -> ci.text(chat.chatInfo.isChannel) to null
|
||||
else -> markedDeletedText(ci, chat.chatInfo) to null
|
||||
ci.meta.itemDeleted != null -> markedDeletedText(ci, chat.chatInfo) to null
|
||||
ci.content.msgContent is MsgContent.MCChat -> {
|
||||
val chatLink = (ci.content.msgContent as MsgContent.MCChat).chatLink
|
||||
val descr = chatLink.shortDescription?.let { "\n$it" } ?: ""
|
||||
(chatLink.displayName + descr) to null
|
||||
}
|
||||
else -> ci.text(chat.chatInfo.isChannel) to null
|
||||
}
|
||||
val formattedText = when {
|
||||
ci.meta.itemDeleted == null -> ci.formattedText
|
||||
else -> null
|
||||
val formattedText: List<FormattedText>? = when {
|
||||
ci.meta.itemDeleted != null -> null
|
||||
ci.content.msgContent is MsgContent.MCChat -> null
|
||||
else -> ci.formattedText
|
||||
}
|
||||
val prefix = when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCReport ->
|
||||
@@ -332,6 +339,19 @@ fun ChatPreviewView(
|
||||
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
|
||||
}
|
||||
}
|
||||
is MsgContent.MCChat -> SmallContentPreview(borderColor = if (mc.chatLink.image != null) MaterialTheme.colors.onSurface.copy(alpha = 0.12f) else Color.Transparent) {
|
||||
Box(
|
||||
Modifier.fillMaxSize().clickable { withBGApi { planAndConnect(chat.remoteHostId, mc.chatLink.connLinkStr, linkOwnerSig = mc.ownerSig, close = null) } },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val image = mc.chatLink.image
|
||||
if (image != null) {
|
||||
Image(base64ToBitmap(image), null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize())
|
||||
} else {
|
||||
Icon(painterResource(mc.chatLink.iconRes), null, Modifier.size(44.sp.toDp()), tint = if (isInDarkTheme()) FileDark else FileLight)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@@ -500,8 +520,8 @@ fun ChatPreviewView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SmallContentPreview(content: @Composable BoxScope.() -> Unit) {
|
||||
Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).size(36.sp.toDp()).border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(22)).clip(RoundedCornerShape(22))) {
|
||||
private fun SmallContentPreview(borderColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f), content: @Composable BoxScope.() -> Unit) {
|
||||
Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).size(36.sp.toDp()).border(0.5.dp, borderColor, RoundedCornerShape(22)).clip(RoundedCornerShape(22))) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -51,6 +51,9 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
}
|
||||
}
|
||||
}
|
||||
is SharedContent.ChatLink -> {
|
||||
hasSimplexLink = true
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
if (chatModel.chats.value.isNotEmpty()) {
|
||||
@@ -98,7 +101,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
||||
val navButton: @Composable RowScope.() -> Unit = {
|
||||
when {
|
||||
showSearch -> NavigationButtonBack(hideSearchOnBack)
|
||||
(users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward -> {
|
||||
(users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward && remember { chatModel.sharedContent }.value !is SharedContent.ChatLink -> {
|
||||
val allRead = users
|
||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
@@ -129,6 +132,8 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
||||
chatModel.sharedContent.value = null
|
||||
if (sharedContent is SharedContent.Forward) {
|
||||
chatModel.chatId.value = sharedContent.fromChatInfo.id
|
||||
} else if (sharedContent is SharedContent.ChatLink) {
|
||||
chatModel.chatId.value = sharedContent.groupInfo.id
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -144,6 +149,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
||||
is SharedContent.Media -> stringResource(MR.strings.share_image)
|
||||
is SharedContent.File -> stringResource(MR.strings.share_file)
|
||||
is SharedContent.Forward -> if (v.chatItems.size > 1) stringResource(MR.strings.forward_multiple) else stringResource(MR.strings.forward_message)
|
||||
is SharedContent.ChatLink -> stringResource(MR.strings.share_channel)
|
||||
null -> stringResource(MR.strings.share_message)
|
||||
},
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
|
||||
+11
@@ -273,6 +273,7 @@ class AlertManager {
|
||||
profileFullName: String,
|
||||
profileImage: @Composable () -> Unit,
|
||||
subtitle: String? = null,
|
||||
information: String? = null,
|
||||
confirmText: String? = generalGetString(MR.strings.connect_plan_open_chat),
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
dismissText: String = generalGetString(MR.strings.cancel_verb),
|
||||
@@ -329,6 +330,16 @@ class AlertManager {
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
if (information != null) {
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
Text(
|
||||
information,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.body2,
|
||||
maxLines = 3,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ sealed class SharedContent {
|
||||
data class Media(val text: String, val uris: List<URI>): SharedContent()
|
||||
data class File(val text: String, val uri: URI): SharedContent()
|
||||
data class Forward(val chatItems: List<ChatItem>, val fromChatInfo: ChatInfo): SharedContent()
|
||||
data class ChatLink(val groupInfo: GroupInfo): SharedContent()
|
||||
}
|
||||
|
||||
enum class AnimatedViewState {
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
|
||||
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)
|
||||
GroupLinkView(chatModel, rhId = null, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-5
@@ -24,6 +24,7 @@ enum class ConnectionLinkType {
|
||||
suspend fun planAndConnect(
|
||||
rhId: Long?,
|
||||
shortOrFullLink: String,
|
||||
linkOwnerSig: LinkOwnerSig? = null,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)? = null,
|
||||
filterKnownContact: ((Contact) -> Unit)? = null,
|
||||
@@ -44,12 +45,13 @@ suspend fun planAndConnect(
|
||||
inProgress.value = false
|
||||
cleanup?.invoke()
|
||||
}
|
||||
return planAndConnectTask(rhId, shortOrFullLink, close, cleanup, filterKnownContact, filterKnownGroup, inProgress)
|
||||
return planAndConnectTask(rhId, shortOrFullLink, linkOwnerSig, close, cleanup, filterKnownContact, filterKnownGroup, inProgress)
|
||||
}
|
||||
|
||||
private suspend fun planAndConnectTask(
|
||||
rhId: Long?,
|
||||
shortOrFullLink: String,
|
||||
linkOwnerSig: LinkOwnerSig? = null,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)? = null,
|
||||
filterKnownContact: ((Contact) -> Unit)? = null,
|
||||
@@ -66,7 +68,7 @@ private suspend fun planAndConnectTask(
|
||||
cleanup?.invoke()
|
||||
completable.complete(!completable.isActive)
|
||||
}
|
||||
val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, inProgress = inProgress)
|
||||
val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, linkOwnerSig, inProgress = inProgress)
|
||||
connectProgressManager.stopConnectProgress()
|
||||
if (!inProgress.value) { return completable }
|
||||
if (result != null) {
|
||||
@@ -85,6 +87,7 @@ private suspend fun planAndConnectTask(
|
||||
rhId,
|
||||
connectionLink,
|
||||
connectionPlan.invitationLinkPlan.contactSLinkData_,
|
||||
ownerVerification = connectionPlan.invitationLinkPlan.ownerVerification,
|
||||
close,
|
||||
cleanup
|
||||
)
|
||||
@@ -96,6 +99,7 @@ private suspend fun planAndConnectTask(
|
||||
text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText,
|
||||
connectDestructive = false,
|
||||
cleanup = cleanup,
|
||||
ownerVerification = connectionPlan.invitationLinkPlan.ownerVerification,
|
||||
)
|
||||
}
|
||||
InvitationLinkPlan.OwnLink -> {
|
||||
@@ -146,6 +150,7 @@ private suspend fun planAndConnectTask(
|
||||
rhId,
|
||||
connectionLink,
|
||||
connectionPlan.contactAddressPlan.contactSLinkData_,
|
||||
ownerVerification = connectionPlan.contactAddressPlan.ownerVerification,
|
||||
close,
|
||||
cleanup
|
||||
)
|
||||
@@ -157,6 +162,7 @@ private suspend fun planAndConnectTask(
|
||||
text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText,
|
||||
connectDestructive = false,
|
||||
cleanup,
|
||||
ownerVerification = connectionPlan.contactAddressPlan.ownerVerification,
|
||||
)
|
||||
}
|
||||
ContactAddressPlan.OwnLink -> {
|
||||
@@ -215,6 +221,7 @@ private suspend fun planAndConnectTask(
|
||||
connectionLink,
|
||||
connectionPlan.groupLinkPlan.groupSLinkInfo_,
|
||||
connectionPlan.groupLinkPlan.groupSLinkData_,
|
||||
ownerVerification = connectionPlan.groupLinkPlan.ownerVerification,
|
||||
close,
|
||||
cleanup
|
||||
)
|
||||
@@ -226,6 +233,7 @@ private suspend fun planAndConnectTask(
|
||||
text = generalGetString(MR.strings.you_will_join_group) + linkText,
|
||||
connectDestructive = false,
|
||||
cleanup = cleanup,
|
||||
ownerVerification = connectionPlan.groupLinkPlan.ownerVerification,
|
||||
)
|
||||
}
|
||||
is GroupLinkPlan.OwnLink -> {
|
||||
@@ -292,7 +300,7 @@ private suspend fun planAndConnectTask(
|
||||
ProfileImage(
|
||||
size = alertProfileImageSize,
|
||||
image = groupSLinkData.groupProfile.image,
|
||||
icon = MR.images.ic_bigtop_updates_padded
|
||||
icon = MR.images.ic_bigtop_updates_circle_filled
|
||||
)
|
||||
},
|
||||
subtitle = generalGetString(MR.strings.channel_no_active_relays_try_later),
|
||||
@@ -375,10 +383,12 @@ fun askCurrentOrIncognitoProfileAlert(
|
||||
text: String? = null,
|
||||
connectDestructive: Boolean,
|
||||
cleanup: (() -> Unit)?,
|
||||
ownerVerification: OwnerVerification? = null,
|
||||
) {
|
||||
val fullText = listOfNotNull(text, ownerVerificationMessage(ownerVerification)).joinToString("\n\n").ifEmpty { null }
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
|
||||
title = title,
|
||||
text = text,
|
||||
text = fullText,
|
||||
buttons = {
|
||||
Column {
|
||||
val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary
|
||||
@@ -573,6 +583,7 @@ fun showPrepareContactAlert(
|
||||
rhId: Long?,
|
||||
connectionLink: CreatedConnLink,
|
||||
contactShortLinkData: ContactShortLinkData,
|
||||
ownerVerification: OwnerVerification? = null,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)?
|
||||
) {
|
||||
@@ -589,6 +600,7 @@ fun showPrepareContactAlert(
|
||||
else MR.images.ic_account_circle_filled
|
||||
)
|
||||
},
|
||||
information = ownerVerificationMessage(ownerVerification),
|
||||
confirmText = generalGetString(MR.strings.connect_plan_open_new_chat),
|
||||
onConfirm = {
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
@@ -614,6 +626,7 @@ fun showPrepareGroupAlert(
|
||||
connectionLink: CreatedConnLink,
|
||||
groupShortLinkInfo: GroupShortLinkInfo?,
|
||||
groupShortLinkData: GroupShortLinkData,
|
||||
ownerVerification: OwnerVerification? = null,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)?
|
||||
) {
|
||||
@@ -626,10 +639,11 @@ fun showPrepareGroupAlert(
|
||||
ProfileImage(
|
||||
size = alertProfileImageSize,
|
||||
image = groupShortLinkData.groupProfile.image,
|
||||
icon = if (isChannel) MR.images.ic_bigtop_updates_padded else MR.images.ic_supervised_user_circle_filled
|
||||
icon = if (isChannel) MR.images.ic_bigtop_updates_circle_filled else MR.images.ic_supervised_user_circle_filled
|
||||
)
|
||||
},
|
||||
subtitle = subscriberCount,
|
||||
information = ownerVerificationMessage(ownerVerification),
|
||||
confirmText = generalGetString(if (isChannel) MR.strings.connect_plan_open_new_channel else MR.strings.connect_plan_open_new_group),
|
||||
onConfirm = {
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
@@ -657,3 +671,9 @@ fun showPrepareGroupAlert(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun ownerVerificationMessage(ov: OwnerVerification?): String? = when (ov) {
|
||||
is OwnerVerification.Verified -> generalGetString(MR.strings.owner_verification_passed)
|
||||
is OwnerVerification.Failed -> String.format(generalGetString(MR.strings.owner_verification_failed), ov.reason)
|
||||
null -> null
|
||||
}
|
||||
|
||||
@@ -533,8 +533,21 @@
|
||||
<string name="share_file">Share file…</string>
|
||||
<string name="forward_message">Forward message…</string>
|
||||
<string name="forward_multiple">Forward messages…</string>
|
||||
<string name="share_channel">Share channel…</string>
|
||||
<string name="cannot_share_message_alert_title">Cannot send message</string>
|
||||
<string name="cannot_share_message_alert_text">Selected chat preferences prohibit this message.</string>
|
||||
<string name="share_via_chat">Share via chat</string>
|
||||
<string name="tap_to_open">Tap to open</string>
|
||||
<string name="chat_link_channel">Channel link</string>
|
||||
<string name="chat_link_group">Group link</string>
|
||||
<string name="chat_link_business_address">Business address</string>
|
||||
<string name="chat_link_contact_address">Contact address</string>
|
||||
<string name="chat_link_one_time">One-time link</string>
|
||||
<string name="chat_link_from_owner">(from owner)</string>
|
||||
<string name="chat_link_signed">(signed)</string>
|
||||
<string name="error_sharing_channel">Error sharing channel</string>
|
||||
<string name="owner_verification_passed">Link signature verified.</string>
|
||||
<string name="owner_verification_failed">⚠️ Signature verification failed: %s.</string>
|
||||
|
||||
<!-- ComposeView.kt, helpers -->
|
||||
<string name="attach">Attach</string>
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path fill-rule="evenodd" d="M480-85A395 395 0 1 0 479.99-85Z M234.3-480q0 44.1 15.05 84.88T293.1-321.1q5.25 5.6 5.77 13.12T294.15-295.2q-5.6 5.6-12.25 4.2t-11.9-7q-33.6-37.45-51.45-84.52T200.7-480q0-50.4 17.85-97.47T270-662.35q5.25-5.25 11.9-6.3t12.07 4.38q5.42 5.42 4.9 12.6T293.1-639.25q-28.7 33.6-43.75 74.38T234.3-480Zm100.62 50.22q8.22 24.32 24.32 44.62 4.55 5.6 4.9 12.6t-5.07 12.42q-5.42 5.42-12.25 5.07T335.45-361q-20.3-25.2-31.32-55.82T293.1-480q0-32.55 11.02-63.17T335.45-599q4.55-5.6 11.38-6.3t12.25 4.72q5.42 5.42 5.07 12.42t-4.9 12.25q-16.1 20.3-24.32 45.15T326.7-480q0 25.9 8.22 50.22ZM460.05-193v-227.5q-19.6-5.95-31.15-22.57T417.35-480q0-26.25 18.2-44.62T480-543q26.25 0 44.62 18.38T543-480q0 20.3-11.72 36.92T500.3-420.5v227.5q0 8.75-5.77 14.35t-14.52 5.6q-8.75 0-14.35-5.6t-5.6-14.35Zm165.02-337.22Q616.85-554.55 601.1-575.2q-4.9-5.25-5.25-12.25t5.07-12.42q5.42-5.42 12.25-5.07t11.38 5.95q20.3 25.2 31.32 55.82T666.9-480q0 32.55-11.02 63.17T624.55-361q-4.55 5.6-11.02 5.95t-11.9-5.07Q596.2-365.55 596.2-372.55t4.9-12.6q15.75-20.3 23.97-44.62T633.3-480q0-25.9-8.22-50.22ZM725.7-480q0-44.1-15.05-84.88T666.9-639.25q-5.25-5.25-5.77-12.77t4.9-12.95Q671.45-670.4 678.1-669t12.25 6.65q33.25 37.8 51.1 84.88T759.3-480q0 50.4-17.85 97.47T690.35-298q-5.6 5.6-12.25 6.65t-12.07-4.38q-5.42-5.42-4.9-12.6T666.9-321.1q28.7-33.25 43.75-74.02T725.7-480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
-8
@@ -1,8 +0,0 @@
|
||||
<svg width="1240" height="1240" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g>
|
||||
<g transform="translate(164, 1076) scale(0.95)">
|
||||
<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"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
Reference in New Issue
Block a user