web: improve channel layout, fix subscriber count (#7092)

* web: improve channel layout

* limit link preview width

* fix

* update subscriber count in relays

* catch worker errors

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
Evgeny
2026-06-18 08:41:05 +01:00
committed by GitHub
parent 72e6a696eb
commit a6bc9da009
3 changed files with 60 additions and 17 deletions
+3
View File
@@ -1376,6 +1376,9 @@ updatePublicGroupData user gInfo
pure (gInfo', gLink)
setGroupLinkDataAsync user gInfo' gLink
pure gInfo'
| useRelays' gInfo && isRelay (membership gInfo) = do
cxt <- chatStoreCxt
withStore $ \db -> updatePublicMemberCount db cxt user gInfo
| otherwise = pure gInfo
updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> CM (GroupInfo, Bool)
+13 -8
View File
@@ -26,7 +26,7 @@ where
import Control.Concurrent.STM (check, flushTQueue)
import Control.Exception (SomeException, catch)
import Control.Logger.Simple
import Control.Monad (forM_, void, when)
import Control.Monad
import Control.Monad.Except (runExceptT)
import Data.Either (rights)
import Data.Int (Int64)
@@ -42,7 +42,7 @@ import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Data.Time.Clock (UTCTime, getCurrentTime)
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), CorsOrigin (..), PublishableGroup (..), WebPreviewConfig (..), WebPreviewState (..), mkStoreCxt)
import Simplex.Chat.Controller (ChatController (..), CorsOrigin (..), PublishableGroup (..), WebPreviewConfig (..), WebPreviewState (..), mkStoreCxt)
import Simplex.Chat.Markdown (FormattedText (..), MarkdownList, parseMaybeMarkdownList)
import Simplex.Chat.Messages
( CChatItem (..),
@@ -57,7 +57,7 @@ import Simplex.Chat.Messages
)
import Simplex.Chat.Messages.CIContent (ciMsgContent)
import Simplex.Chat.Protocol (MsgContent, MsgRef (..), QuotedMsg (..), isReport)
import Simplex.Chat.Store.Groups (getGroupOwners, getRelayPublishableGroups)
import Simplex.Chat.Store.Groups (getGroupOwners, getRelayPublishableGroups, updatePublicMemberCount)
import Simplex.Chat.Store.Messages (getGroupWebPreviewItems)
import Simplex.Chat.Store.Shared (getGroupInfo)
import Simplex.Chat.Types
@@ -75,11 +75,11 @@ import Simplex.Chat.Types
)
import Simplex.Messaging.Agent.Store.Common (withTransaction)
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Util (safeDecodeUtf8)
import qualified URI.ByteString as U
import Simplex.Messaging.Util (catchOwn, eitherToMaybe, safeDecodeUtf8, tshow)
import Simplex.Messaging.Parsers (defaultJSON)
import System.Directory (createDirectoryIfMissing, listDirectory, removeFile, renameFile)
import System.FilePath (dropExtension, takeExtension, (</>))
import qualified URI.ByteString as U
import UnliftIO.STM
data WebFileInfo = WebFileInfo
@@ -135,7 +135,7 @@ webPreviewWorker cfg@WebPreviewConfig {webJsonDir, webCorsFile, webUpdateInterva
cleanStaleFiles wps
regenerateCors wps
seedRoutinePending wps
workerLoop wps
forever $ workerLoop wps `catchOwn` \e -> logError ("web preview worker error: " <> tshow e)
where
cxt = mkStoreCxt (config cc)
@@ -146,7 +146,6 @@ webPreviewWorker cfg@WebPreviewConfig {webJsonDir, webCorsFile, webUpdateInterva
renderRoutine
noRoutine <- atomically $ S.null <$> readTVar routinePending
when noRoutine waitRefresh
workerLoop wps
where
drainRemovals = atomically (tryReadTQueue filesToRemove) >>= \case
Nothing -> pure ()
@@ -233,6 +232,12 @@ renderGroupPreview WebPreviewConfig {webJsonDir, webPreviewItemCount} cc user gI
case publicGroup of
Just PublicGroupProfile {publicGroupId, publicGroupAccess} -> do
let fName = publicGroupIdFileName publicGroupId <> ".json"
-- backfill the subscriber count for channels created before it was tracked
subscribers <- case publicMemberCount of
Just _ -> pure publicMemberCount
Nothing -> do
g_ <- withTransaction (chatStore cc) (\db -> runExceptT $ updatePublicMemberCount db cxt user gInfo)
pure $ eitherToMaybe g_ >>= \GroupInfo {groupSummary = GroupSummary {publicMemberCount = pmc}} -> pmc
(items, owners) <- withTransaction (chatStore cc) $ \db -> do
is <- getGroupWebPreviewItems db user gInfo webPreviewItemCount
os <- getGroupOwners db cxt user gInfo
@@ -246,7 +251,7 @@ renderGroupPreview WebPreviewConfig {webJsonDir, webPreviewItemCount} cc user gI
shortDescription = toFormattedText =<< sd,
welcomeMessage = toFormattedText =<< wd,
members = senders,
subscribers = publicMemberCount,
subscribers,
messages = msgs,
updatedAt = ts
}
+44 -9
View File
@@ -1,7 +1,7 @@
#include "simplex-lib.jsc"
(function() {
#include "simplex-lib.jsc"
#include "qrcode.js"
const STYLE = `
@@ -290,6 +290,7 @@ const STYLE = `
.simplex-preview-quote-text {
font-size: 15px;
overflow: hidden;
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
@@ -1051,6 +1052,32 @@ function renderAppBadges(container) {
container.appendChild(badges);
}
// the QR library only parses hex colors; map 'transparent' (used in dark mode) to a transparent hex
function qrColor(value, fallback) {
value = (value || '').trim();
if (!value) return fallback;
return value === 'transparent' ? '#0000' : value;
}
// navigator.clipboard is undefined outside secure contexts; fall back to execCommand
function copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
}
return new Promise(function(resolve, reject) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.top = '-9999px';
document.body.appendChild(ta);
ta.select();
let ok = false;
try { ok = document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
ok ? resolve() : reject(new Error('copy failed'));
});
}
function renderDesktopConversion(container, channelLink, showAppBadges) {
if (showAppBadges) {
renderAppBadges(container);
@@ -1093,8 +1120,8 @@ function renderDesktopConversion(container, channelLink, showAppBadges) {
QRCode.toCanvas(canvas, channelLink, {
errorCorrectionLevel: 'M',
color: {
dark: cs.getPropertyValue('--sp-qr-fg').trim() || '#062D56',
light: cs.getPropertyValue('--sp-qr-bg').trim() || '#ffffff'
dark: qrColor(cs.getPropertyValue('--sp-qr-fg'), '#062D56'),
light: qrColor(cs.getPropertyValue('--sp-qr-bg'), '#ffffff')
},
width: 400,
margin: 1
@@ -1102,12 +1129,14 @@ function renderDesktopConversion(container, channelLink, showAppBadges) {
canvas.style.width = '200px';
canvas.style.height = '200px';
}).catch(function() {
canvas._rendered = false;
qrPopup.style.display = 'none';
qrToggle.style.display = 'none';
qrToggle.style.display = '';
});
} catch(err) {
canvas._rendered = false;
qrPopup.style.display = 'none';
qrToggle.style.display = 'none';
qrToggle.style.display = '';
}
}
} else {
@@ -1123,10 +1152,10 @@ function renderDesktopConversion(container, channelLink, showAppBadges) {
const copyLink = document.createElement('a');
copyLink.textContent = 'copy link';
copyLink.addEventListener('click', function() {
navigator.clipboard.writeText(channelLink).then(function() {
copyToClipboard(channelLink).then(function() {
copyLink.textContent = 'Copied!';
setTimeout(function() { copyLink.textContent = 'copy link'; }, 2000);
});
}).catch(function() {});
});
copyAction.appendChild(document.createTextNode('Or '));
copyAction.appendChild(copyLink);
@@ -1341,7 +1370,11 @@ function renderQuote(quote, membersMap, channel) {
function classifyImage(img) {
const w = img.naturalWidth;
const h = img.naturalHeight;
img.classList.add(w * 0.97 <= h ? 'portrait' : 'landscape');
const portrait = w * 0.97 <= h;
img.classList.add(portrait ? 'portrait' : 'landscape');
// constrain the bubble to the image width so long text wraps instead of widening the bubble
const inner = img.closest('.simplex-preview-bubble-inner');
if (inner) inner.style.maxWidth = portrait ? '300px' : '400px';
}
function renderImageContent(inner, mc, msg, mediaOnly) {
@@ -1395,6 +1428,8 @@ function renderVideoContent(inner, mc, msg, mediaOnly) {
function renderLinkContent(bubble, mc, msg) {
if (mc.preview) {
// clamp the bubble to the link card width so long text wraps instead of widening the bubble
bubble.style.maxWidth = '400px';
const card = document.createElement('div');
card.className = 'simplex-preview-link-card';
if (isDataImage(mc.preview.image)) {