mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-02 04:51:49 +00:00
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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user