mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-27 22:34:51 +00:00
core: render web previews for channels (#7029)
* plan: web previews for channels
* types for recipient side to support channel web previews and domain names
* fix
* migrations
* update schema and api types
* update schema
* rename migrations
* core: render channel preview data
* core: render channel preview data in relays
* website: use cpp to inject JS functions
* JSC files
* remove directory.js
* channel preview renderer
* Revert "cli: fix redraw slowness (#6735)"
This reverts commit b801d77c74.
* sample channel page
* default avatar
* rename options
* better layout
* layout
* images
* some fixes
* tails
* markdown colors
* image sizes
* reactions
* fix reactions
* fewer avatars
* forward icon
* command to change group access parameters
* view public group access changes in CLI
* media metadata color
* ios: group web access ui
* update ui
* add init
* kotlin, labels
* update page
* update relay base URL
* fix
* ios update channel web page info
* update kotlin layout
* use cards
* update layout
* use domains for relay data, path is fixed
* update embed code
* fix bots api
* include only history items and senders
* update preview JS/HTML
* show different error if link is different
* remove stale json files
* better layout
* layout fixes
* improve layout
* improve layout
* update embed code
* web cta
* better layout
* buttons
* layout
* paddings
* desktop cta
* desktop cta
* cta layout
* fonts
* paddings
* paddings
* more paddings
* copy link
* read more
* hide avatar and placeholder when all messages are from channel
* color scheme
* fix color
* improve
* layout
* welcome message
* dark mode colors
* padding
* font size
* overscroll
* font
* logo on button
* better join
* buttons
* refactor
* another logo
* text
* desktop button
* button text
* center
* fix svg
* padding
* smaller gap
* render channel on any message changes etc
* fixes
* atomic file updates, escape attributes
* fix tests
* more tests
* more efficient rendering
* improve security
* sanitize links, include mentioned members
* schema
* fixes
* improve rendering
* fix showing correct subscribers count
* fix member names
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
parent
43904dd0dc
commit
adb3fb8cb2
+186
@@ -0,0 +1,186 @@
|
||||
package chat.simplex.common.views.chat.group
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
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.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@Composable
|
||||
fun ChannelWebPageView(
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
chatModel: ChatModel,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val isChannel = groupInfo.isChannel
|
||||
val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess
|
||||
val webPage = rememberSaveable { mutableStateOf(access?.groupWebPage ?: "") }
|
||||
val allowEmbedding = rememberSaveable { mutableStateOf(access?.allowEmbedding ?: false) }
|
||||
val groupRelays = remember { mutableStateListOf<GroupRelay>() }
|
||||
|
||||
val dataUnchanged = webPage.value.trim() == (access?.groupWebPage ?: "") &&
|
||||
allowEmbedding.value == (access?.allowEmbedding ?: false)
|
||||
|
||||
val save: () -> Unit = {
|
||||
withBGApi {
|
||||
val trimmedPage = webPage.value.trim()
|
||||
val newAccess = PublicGroupAccess(
|
||||
groupWebPage = trimmedPage.ifEmpty { null },
|
||||
groupDomain = access?.groupDomain,
|
||||
domainWebPage = access?.domainWebPage ?: false,
|
||||
allowEmbedding = allowEmbedding.value
|
||||
)
|
||||
val gp = groupInfo.groupProfile.copy(
|
||||
publicGroup = groupInfo.groupProfile.publicGroup?.copy(publicGroupAccess = newAccess)
|
||||
)
|
||||
val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, gp, isChannel)
|
||||
if (gInfo != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId, gInfo)
|
||||
}
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val closeWithAlert = {
|
||||
if (dataUnchanged) {
|
||||
close()
|
||||
} else {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.save_preferences_question),
|
||||
confirmText = generalGetString(if (isChannel) MR.strings.save_and_notify_channel_subscribers else MR.strings.save_and_notify_group_members),
|
||||
dismissText = generalGetString(MR.strings.exit_without_saving),
|
||||
onConfirm = save,
|
||||
onDismiss = close,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val relays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId)
|
||||
groupRelays.clear()
|
||||
groupRelays.addAll(relays)
|
||||
}
|
||||
|
||||
BackHandler(onBack = closeWithAlert)
|
||||
ModalView(close = closeWithAlert, cardScreen = true) {
|
||||
ChannelWebPageLayout(
|
||||
isChannel = isChannel,
|
||||
webPage = webPage,
|
||||
allowEmbedding = allowEmbedding,
|
||||
groupRelays = groupRelays,
|
||||
groupInfo = groupInfo,
|
||||
dataUnchanged = dataUnchanged,
|
||||
save = save
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelWebPageLayout(
|
||||
isChannel: Boolean,
|
||||
webPage: MutableState<String>,
|
||||
allowEmbedding: MutableState<Boolean>,
|
||||
groupRelays: List<GroupRelay>,
|
||||
groupInfo: GroupInfo,
|
||||
dataUnchanged: Boolean,
|
||||
save: () -> Unit
|
||||
) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(if (isChannel) MR.strings.channel_webpage else MR.strings.group_webpage))
|
||||
|
||||
val embedCode = embedCode(groupRelays, groupInfo)
|
||||
if (embedCode != null) {
|
||||
SectionTextFooter(stringResource(MR.strings.webpage_info))
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.webpage_code)) {
|
||||
SectionItemView {
|
||||
Text(
|
||||
embedCode,
|
||||
style = MaterialTheme.typography.body2.copy(fontFamily = FontFamily.Monospace, fontSize = 12.sp),
|
||||
maxLines = 6,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
SectionItemView({
|
||||
clipboard.setText(AnnotatedString(embedCode))
|
||||
showToast(generalGetString(MR.strings.copied))
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_content_copy), null, tint = MaterialTheme.colors.primary)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(MR.strings.copy_code), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.webpage_code_footer))
|
||||
} else {
|
||||
SectionTextFooter(stringResource(MR.strings.relays_no_web_support))
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.enter_webpage_url)) {
|
||||
PlainTextEditor(webPage, placeholder = stringResource(MR.strings.web_page_url_placeholder))
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.webpage_url_footer))
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView {
|
||||
PreferenceToggle(stringResource(MR.strings.allow_anyone_to_embed), checked = allowEmbedding.value) {
|
||||
allowEmbedding.value = it
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(if (allowEmbedding.value) MR.strings.embed_any_webpage_can_show else MR.strings.embed_only_your_page))
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView {
|
||||
SectionItemView(save, disabled = dataUnchanged) {
|
||||
Text(
|
||||
stringResource(MR.strings.save_verb),
|
||||
color = if (dataUnchanged) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun embedCode(groupRelays: List<GroupRelay>, groupInfo: GroupInfo): String? {
|
||||
val pg = groupInfo.groupProfile.publicGroup ?: return null
|
||||
val relayDomains = groupRelays.mapNotNull { it.relayCap.webDomain }
|
||||
if (relayDomains.isEmpty()) return null
|
||||
val domains = relayDomains.joinToString(",")
|
||||
return """<div data-simplex-channel-preview
|
||||
data-channel-link="${pg.groupLink}"
|
||||
data-channel-id="${pg.publicGroupId}"
|
||||
data-relay-domains="$domains"
|
||||
data-app-download-buttons="on"
|
||||
data-color-scheme="light"
|
||||
></div>
|
||||
<script src="https://simplex.chat/js/channel-preview.js"></script>"""
|
||||
}
|
||||
+22
@@ -175,6 +175,9 @@ fun ModalData.GroupChatInfoView(
|
||||
manageGroupLink = {
|
||||
ModalManager.end.showModal(cardScreen = true) { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated, isChannel = groupInfo.useRelays, shareGroupInfo = groupInfo) }
|
||||
},
|
||||
manageWebPage = {
|
||||
ModalManager.end.showCustomModal { close -> ChannelWebPageView(rhId, groupInfo, chatModel, close) }
|
||||
},
|
||||
onSearchClicked = onSearchClicked,
|
||||
deletingItems = deletingItems
|
||||
)
|
||||
@@ -506,6 +509,7 @@ fun ModalData.GroupChatInfoLayout(
|
||||
clearChat: () -> Unit,
|
||||
leaveGroup: () -> Unit,
|
||||
manageGroupLink: () -> Unit,
|
||||
manageWebPage: () -> Unit,
|
||||
close: () -> Unit = { ModalManager.closeAllModalsEverywhere()},
|
||||
onSearchClicked: () -> Unit,
|
||||
deletingItems: State<Boolean>
|
||||
@@ -796,6 +800,13 @@ fun ModalData.GroupChatInfoLayout(
|
||||
}
|
||||
}
|
||||
|
||||
if (groupInfo.useRelays && groupInfo.isOwner) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.advanced_options)) {
|
||||
ChannelWebPageButton(groupInfo, manageWebPage)
|
||||
}
|
||||
}
|
||||
|
||||
if (developerTools) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
|
||||
@@ -1209,6 +1220,16 @@ private fun ChannelLinkButton(onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelWebPageButton(groupInfo: GroupInfo, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_travel_explore),
|
||||
stringResource(if (groupInfo.isChannel) MR.strings.channel_webpage else MR.strings.group_webpage),
|
||||
onClick,
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelLinkQRCodeSection(groupLink: String) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
@@ -1413,6 +1434,7 @@ fun PreviewGroupChatInfoLayout() {
|
||||
clearChat = {},
|
||||
leaveGroup = {},
|
||||
manageGroupLink = {},
|
||||
manageWebPage = {},
|
||||
onSearchClicked = {},
|
||||
deletingItems = remember { mutableStateOf(true) }
|
||||
)
|
||||
|
||||
+22
@@ -102,6 +102,28 @@ fun TextEditor(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlainTextEditor(
|
||||
value: MutableState<String>,
|
||||
placeholder: String? = null,
|
||||
singleLine: Boolean = true
|
||||
) {
|
||||
BasicTextField(
|
||||
value = value.value,
|
||||
onValueChange = { value.value = it },
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = 12.dp),
|
||||
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
|
||||
singleLine = singleLine,
|
||||
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
|
||||
decorationBox = { innerTextField ->
|
||||
if (value.value.isEmpty() && placeholder != null) {
|
||||
Text(placeholder, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ParsedFormattedText(
|
||||
val formattedText: List<FormattedText>? = null
|
||||
|
||||
@@ -1925,6 +1925,20 @@
|
||||
<string name="button_welcome_message">Welcome message</string>
|
||||
<string name="group_link">Group link</string>
|
||||
<string name="channel_link">Channel link</string>
|
||||
<string name="channel_webpage">Channel webpage</string>
|
||||
<string name="group_webpage">Group webpage</string>
|
||||
<string name="advanced_options">Advanced options</string>
|
||||
<string name="web_page_url_placeholder">https://</string>
|
||||
<string name="allow_anyone_to_embed">Allow anyone to embed</string>
|
||||
<string name="enter_webpage_url">Enter webpage URL</string>
|
||||
<string name="webpage_url_footer">It will be shown to subscribers and used to allow loading the preview.</string>
|
||||
<string name="webpage_code">Webpage code</string>
|
||||
<string name="webpage_code_footer">Add this code to your webpage. It will display the preview of your channel / group.</string>
|
||||
<string name="copy_code">Copy code</string>
|
||||
<string name="webpage_info">Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting.</string>
|
||||
<string name="relays_no_web_support">Used chat relays do not support webpages.</string>
|
||||
<string name="embed_any_webpage_can_show">Any webpage can show the preview.</string>
|
||||
<string name="embed_only_your_page">Only your page above can show the preview.</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>
|
||||
|
||||
Reference in New Issue
Block a user