Missing configuration: data-relay-domains and data-channel-id required.
'; return; } injectStyles(); container.classList.add('simplex-preview-container', 'simplex-scheme-' + colorScheme); if (container.dataset.lightBackground) { container.style.setProperty('--sp-light-bg', container.dataset.lightBackground); } if (container.dataset.darkBackground) { container.style.setProperty('--sp-dark-bg', container.dataset.darkBackground); } const topOffset = parseInt(container.dataset.topOffset || '0', 10); if (topOffset > 0) { container.style.setProperty('--sp-top-offset', topOffset + 'px'); container.style.marginTop = topOffset + 'px'; container.style.height = 'calc(100vh - ' + topOffset + 'px)'; container.style.height = 'calc(100dvh - ' + topOffset + 'px)'; } container.innerHTML = 'Loading channel...
'; fetchPreview(relayScheme, relayDomains, channelLink, channelId).then(data => { if (data === 'link_mismatch') { container.innerHTML = 'All relays returned a different channel link from specified in the page.
'; return; } if (!data) { container.innerHTML = 'Failed to load channel preview.
'; return; } render(container, data, channelLink, showAppBadges); }); } let stylesInjected = false; function injectStyles() { if (stylesInjected) return; stylesInjected = true; const style = document.createElement('style'); style.textContent = STYLE; document.head.appendChild(style); } async function fetchPreview(relayScheme, relayDomains, channelLink, channelId) { let linkMismatch = false; for (const domain of relayDomains) { try { const url = `${relayScheme}://${domain}/channel/${channelId}.json`; const resp = await fetch(url); if (!resp.ok) continue; const data = await resp.json(); const relayLink = data.channel?.publicGroup?.groupLink; if (channelLink && relayLink && channelLink !== relayLink) { linkMismatch = true; continue; } return data; } catch(e) { continue; } } return linkMismatch ? 'link_mismatch' : null; } function render(container, data, channelLink, showAppBadges) { const { channel, members, messages } = data; const membersMap = {}; for (const m of members) { membersMap[m.memberId] = m; } container.innerHTML = ''; const main = document.createElement('div'); main.className = 'simplex-preview-main'; const header = renderHeader(channel, channelLink, data.subscribers); main.appendChild(header); const messagesDiv = document.createElement('div'); messagesDiv.className = 'simplex-preview-messages'; const welcome = data.welcomeMessage || channel.description; var allMessages = messages; if (welcome) { var welcomeMsg = { sender: null, ts: messages.length > 0 ? messages[0].ts : new Date().toISOString(), content: { type: 'text', text: typeof welcome === 'string' ? welcome : '' }, formattedText: Array.isArray(welcome) ? welcome : null, reactions: [] }; allMessages = [welcomeMsg].concat(messages); } renderMessages(messagesDiv, allMessages, membersMap, channel); main.appendChild(messagesDiv); container.appendChild(main); const info = document.createElement('div'); info.className = 'simplex-preview-info'; const closeBtn = document.createElement('button'); closeBtn.className = 'simplex-preview-info-close'; closeBtn.innerHTML = '✕'; info.appendChild(closeBtn); const infoContent = document.createElement('div'); infoContent.className = 'simplex-preview-info-content'; renderInfoContent(infoContent, data, channelLink, data.subscribers, showAppBadges); info.appendChild(infoContent); container.appendChild(info); header.addEventListener('click', (e) => { if (e.target.closest('.simplex-preview-join-btn')) return; if (window.innerWidth < 1000) { info.classList.add('open'); main.style.overflow = 'hidden'; document.documentElement.classList.add('simplex-preview-scroll-lock'); document.body.classList.add('simplex-preview-scroll-lock'); } }); closeBtn.addEventListener('click', () => { info.classList.remove('open'); main.style.overflow = ''; document.documentElement.classList.remove('simplex-preview-scroll-lock'); document.body.classList.remove('simplex-preview-scroll-lock'); }); setupSecretToggles(container); setTimeout(() => { main.scrollTop = main.scrollHeight; }, 0); } function renderHeader(channel, channelLink, subscriberCount) { const header = document.createElement('div'); header.className = 'simplex-preview-header'; const avatar = document.createElement('img'); avatar.className = 'simplex-preview-header-avatar'; avatar.src = isDataImage(channel.image) ? channel.image : DEFAULT_AVATAR; avatar.alt = channel.displayName; header.appendChild(avatar); const info = document.createElement('div'); info.className = 'simplex-preview-header-info'; const name = document.createElement('h1'); name.className = 'simplex-preview-header-name'; name.textContent = channel.displayName; info.appendChild(name); if (subscriberCount > 0) { const desc = document.createElement('p'); desc.className = 'simplex-preview-header-description'; desc.textContent = subscriberCount + ' subscribers'; info.appendChild(desc); } header.appendChild(info); if (channelLink) { const btn = document.createElement('a'); btn.className = 'simplex-preview-join-btn'; btn.textContent = 'Join'; appendSimplexLogo(btn); btn.href = channelLink; header.appendChild(btn); } return header; } function renderInfoContent(container, data, channelLink, subscriberCount, showAppBadges) { const { channel } = data; const avatar = document.createElement('img'); avatar.className = 'simplex-preview-info-avatar'; avatar.src = isDataImage(channel.image) ? channel.image : DEFAULT_AVATAR; avatar.alt = channel.displayName; container.appendChild(avatar); const name = document.createElement('h2'); name.className = 'simplex-preview-info-name'; name.textContent = channel.displayName; container.appendChild(name); const shortDescr = data.shortDescription || channel.shortDescr; if (shortDescr) { const descrDiv = document.createElement('div'); descrDiv.className = 'simplex-preview-info-descr'; descrDiv.innerHTML = Array.isArray(shortDescr) ? renderMarkdown(shortDescr) : escapeHtml(shortDescr); container.appendChild(descrDiv); } if (subscriberCount > 0) { const subs = document.createElement('p'); subs.className = 'simplex-preview-info-subscribers'; subs.textContent = subscriberCount + ' subscribers'; container.appendChild(subs); } if (channelLink) { if (!isMobile.any()) { const openBtn = document.createElement('a'); openBtn.className = 'simplex-preview-open-btn'; openBtn.style.display = 'flex'; openBtn.style.width = 'fit-content'; openBtn.style.margin = '32px auto 0'; openBtn.textContent = 'Join in SimpleX Chat'; appendSimplexLogo(openBtn); openBtn.href = channelLink; container.appendChild(openBtn); } const showJoinSection = !isMobile.any() || showAppBadges; if (showJoinSection) { const divider = document.createElement('div'); divider.className = 'simplex-preview-divider'; container.appendChild(divider); const joinTitle = document.createElement('p'); joinTitle.className = 'simplex-preview-conversion-title'; joinTitle.textContent = 'To join this channel'; container.appendChild(joinTitle); } const conversion = document.createElement('div'); conversion.className = 'simplex-preview-conversion'; if (!showJoinSection) { conversion.style.marginTop = '28px'; } if (isMobile.any()) { renderMobileConversion(conversion, channelLink, showAppBadges); } else { renderDesktopConversion(conversion, channelLink, showAppBadges); } container.appendChild(conversion); } } var BADGE_APPLE = '
';
var BADGE_TESTFLIGHT = '
';
function renderAppBadges(container) {
const title = document.createElement('p');
title.className = 'simplex-preview-step-title';
title.textContent = 'Install SimpleX Chat app';
container.appendChild(title);
const badges = document.createElement('div');
badges.className = 'simplex-preview-badges';
if (isMobile.Android()) {
badges.innerHTML = BADGE_GOOGLE + BADGE_FDROID + BADGE_APK;
} else if (isMobile.iOS()) {
badges.innerHTML = BADGE_APPLE + BADGE_TESTFLIGHT;
} else {
badges.innerHTML = BADGE_APPLE + BADGE_GOOGLE;
}
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);
}
const qrToggle = document.createElement('a');
qrToggle.className = 'simplex-preview-qr-toggle';
qrToggle.textContent = 'Show QR code for mobile app';
qrToggle.href = '#';
container.appendChild(qrToggle);
const qrPopup = document.createElement('div');
qrPopup.className = 'simplex-preview-qr-popup';
qrPopup.style.display = 'none';
const caption = document.createElement('p');
caption.className = 'simplex-preview-qr-caption';
caption.textContent = 'Scan from SimpleX Chat app';
qrPopup.appendChild(caption);
const canvas = document.createElement('canvas');
qrPopup.appendChild(canvas);
const qrHide = document.createElement('a');
qrHide.className = 'simplex-preview-qr-toggle';
qrHide.textContent = 'Hide QR code';
qrHide.href = '#';
qrPopup.appendChild(qrHide);
container.appendChild(qrPopup);
function toggleQr(e) {
e.preventDefault();
if (qrPopup.style.display === 'none') {
qrPopup.style.display = 'flex';
qrToggle.style.display = 'none';
if (!canvas._rendered) {
canvas._rendered = true;
try {
var cs = getComputedStyle(container);
QRCode.toCanvas(canvas, channelLink, {
errorCorrectionLevel: 'M',
color: {
dark: qrColor(cs.getPropertyValue('--sp-qr-fg'), '#062D56'),
light: qrColor(cs.getPropertyValue('--sp-qr-bg'), '#ffffff')
},
width: 400,
margin: 1
}).then(function() {
canvas.style.width = '200px';
canvas.style.height = '200px';
}).catch(function() {
canvas._rendered = false;
qrPopup.style.display = 'none';
qrToggle.style.display = '';
});
} catch(err) {
canvas._rendered = false;
qrPopup.style.display = 'none';
qrToggle.style.display = '';
}
}
} else {
qrPopup.style.display = 'none';
qrToggle.style.display = '';
}
}
qrToggle.addEventListener('click', toggleQr);
qrHide.addEventListener('click', toggleQr);
const copyAction = document.createElement('p');
copyAction.className = 'simplex-preview-copy-action';
const copyLink = document.createElement('a');
copyLink.textContent = 'copy link';
copyLink.addEventListener('click', 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);
copyAction.appendChild(document.createTextNode(' for desktop app'));
container.appendChild(copyAction);
}
function renderMobileConversion(container, channelLink, showAppBadges) {
if (showAppBadges) {
renderAppBadges(container);
}
const openBtn = document.createElement('a');
openBtn.className = 'simplex-preview-open-btn';
openBtn.textContent = 'Join in SimpleX Chat';
appendSimplexLogo(openBtn);
openBtn.href = channelLink;
container.appendChild(openBtn);
}
function setupSecretToggles(container) {
container.addEventListener('click', (e) => {
const secret = e.target.closest('.secret');
if (secret) secret.classList.toggle('visible');
});
}
function renderMessages(container, messages, membersMap, channel) {
const hasAnySender = messages.some(function(m) { return m.sender; });
let prevMsg = null;
let prevDate = null;
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
const nextMsg = i < messages.length - 1 ? messages[i + 1] : null;
const msgDate = formatDateLabel(msg.ts);
if (msgDate !== prevDate) {
const dateSep = document.createElement('div');
dateSep.className = 'simplex-preview-date-separator';
dateSep.textContent = msgDate;
container.appendChild(dateSep);
prevDate = msgDate;
}
const separation = getItemSeparation(msg, prevMsg);
const nextSeparation = getItemSeparation(nextMsg, msg);
const showAvatar = hasAnySender && (!prevMsg || msg.sender !== prevMsg.sender);
const showName = showAvatar;
const showTail = nextSeparation.largeGap;
const member = msg.sender ? membersMap[msg.sender] : null;
const senderName = member ? member.displayName : channel.displayName;
const senderImage = member ? member.image : channel.image;
const row = document.createElement('div');
row.className = 'simplex-preview-msg-row' + (nextSeparation.largeGap ? ' has-gap' : '');
if (hasAnySender) {
if (showName) {
const nameDiv = document.createElement('div');
nameDiv.className = 'simplex-preview-msg-name';
nameDiv.textContent = senderName;
container.appendChild(nameDiv);
}
if (showAvatar) {
const avatarImg = document.createElement('img');
avatarImg.className = 'simplex-preview-msg-avatar';
avatarImg.src = isDataImage(senderImage) ? senderImage : DEFAULT_AVATAR;
avatarImg.alt = senderName;
row.appendChild(avatarImg);
} else {
const spacer = document.createElement('div');
spacer.className = 'simplex-preview-msg-avatar-placeholder';
row.appendChild(spacer);
}
}
const col = document.createElement('div');
const bubble = renderBubble(msg, member, showTail, membersMap, channel);
col.appendChild(bubble);
if (msg.reactions && msg.reactions.length > 0) {
col.appendChild(renderReactions(msg.reactions));
}
row.appendChild(col);
container.appendChild(row);
prevMsg = msg;
}
}
function renderBubble(msg, member, showTail, membersMap, channel) {
const mc = msg.content;
const mediaOnly = (mc.type === 'image' || mc.type === 'video') && !mc.text && !msg.quote && !msg.forward;
const noTailContent = (mc.type === 'image' || mc.type === 'video' || mc.type === 'voice') && !mc.text;
const hasTail = showTail && !noTailContent;
const bubble = document.createElement('div');
bubble.className = 'simplex-preview-bubble' + (hasTail ? ' has-tail' : '') + (mediaOnly ? ' media-only' : '');
if (hasTail) {
const tail = document.createElement('div');
tail.className = 'simplex-preview-bubble-tail';
tail.innerHTML = tailSvg();
bubble.appendChild(tail);
}
const inner = document.createElement('div');
inner.className = 'simplex-preview-bubble-inner';
if (msg.forward) {
const fwd = document.createElement('div');
fwd.className = 'simplex-preview-forwarded-header';
fwd.innerHTML = FORWARD_ICON_SVG + ' Forwarded';
inner.appendChild(fwd);
}
if (msg.quote) {
inner.appendChild(renderQuote(msg.quote, membersMap, channel));
}
switch (mc.type) {
case 'image':
renderImageContent(inner, mc, msg, mediaOnly);
break;
case 'video':
renderVideoContent(inner, mc, msg, mediaOnly);
break;
case 'link':
renderLinkContent(inner, mc, msg);
break;
case 'voice':
renderVoiceContent(inner, mc, msg);
break;
case 'file':
renderFileContent(inner, mc, msg);
break;
default:
renderTextContent(inner, msg);
break;
}
bubble.appendChild(inner);
if (mediaOnly) {
const overlay = document.createElement('div');
overlay.className = 'simplex-preview-meta-overlay';
if (msg.edited) overlay.innerHTML = '';
overlay.innerHTML += formatTime(msg.ts);
bubble.appendChild(overlay);
}
return bubble;
}
function renderQuote(quote, membersMap, channel) {
const quoteDiv = document.createElement('div');
quoteDiv.className = 'simplex-preview-quote';
const contentDiv = document.createElement('div');
contentDiv.className = 'simplex-preview-quote-content';
const ref = quote.msgRef;
let senderName = '';
if (ref) {
if (ref.memberId) {
const quotedMember = membersMap[ref.memberId];
senderName = quotedMember ? quotedMember.displayName : '';
} else if (ref.sent) {
senderName = channel.displayName;
}
}
if (senderName) {
const sender = document.createElement('div');
sender.className = 'simplex-preview-quote-sender';
sender.textContent = senderName;
contentDiv.appendChild(sender);
}
const textDiv = document.createElement('div');
textDiv.className = 'simplex-preview-quote-text';
textDiv.textContent = quote.content ? (quote.content.text || '') : '';
contentDiv.appendChild(textDiv);
quoteDiv.appendChild(contentDiv);
if (quote.content) {
if ((quote.content.type === 'image' || quote.content.type === 'video') && isDataImage(quote.content.image)) {
const thumb = document.createElement('img');
thumb.className = 'simplex-preview-quote-thumb';
thumb.src = quote.content.image;
quoteDiv.appendChild(thumb);
} else if (quote.content.type === 'file') {
const icon = document.createElement('div');
icon.className = 'simplex-preview-quote-file-icon';
icon.innerHTML = FILE_ICON_SVG;
quoteDiv.appendChild(icon);
} else if (quote.content.type === 'voice') {
const icon = document.createElement('div');
icon.className = 'simplex-preview-quote-file-icon';
icon.innerHTML = VOICE_ICON_SVG;
quoteDiv.appendChild(icon);
}
}
return quoteDiv;
}
function classifyImage(img) {
const w = img.naturalWidth;
const h = img.naturalHeight;
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) {
if (isDataImage(mc.image)) {
const img = document.createElement('img');
img.className = 'simplex-preview-image';
img.src = mc.image;
img.alt = 'Image';
img.addEventListener('load', () => classifyImage(img));
inner.appendChild(img);
} else {
const ph = document.createElement('div');
ph.className = 'simplex-preview-image-placeholder';
ph.innerHTML = IMAGE_PLACEHOLDER_SVG;
inner.appendChild(ph);
}
if (mc.text) {
appendTextBlock(inner, msg);
} else if (!mediaOnly) {
appendMetaOnly(inner, msg);
}
}
function renderVideoContent(inner, mc, msg, mediaOnly) {
if (isDataImage(mc.image)) {
const wrapper = document.createElement('div');
wrapper.style.position = 'relative';
const img = document.createElement('img');
img.className = 'simplex-preview-image';
img.src = mc.image;
img.alt = 'Video';
img.addEventListener('load', () => classifyImage(img));
wrapper.appendChild(img);
const dur = document.createElement('span');
dur.style.cssText = 'position:absolute;bottom:6px;left:12px;color:#fff;font-size:12px;text-shadow:0 0 4px rgba(0,0,0,0.7),0 0 2px rgba(0,0,0,0.9);';
dur.textContent = formatDuration(mc.duration || 0);
wrapper.appendChild(dur);
inner.appendChild(wrapper);
} else {
const ph = document.createElement('div');
ph.className = 'simplex-preview-image-placeholder';
ph.innerHTML = IMAGE_PLACEHOLDER_SVG;
inner.appendChild(ph);
}
if (mc.text) {
appendTextBlock(inner, msg);
} else if (!mediaOnly) {
appendMetaOnly(inner, msg);
}
}
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)) {
const img = document.createElement('img');
img.className = 'simplex-preview-link-card-image';
img.src = mc.preview.image;
img.alt = mc.preview.title || '';
card.appendChild(img);
}
const body = document.createElement('div');
body.className = 'simplex-preview-link-card-body';
if (mc.preview.title) {
const title = document.createElement('div');
title.className = 'simplex-preview-link-card-title';
title.textContent = mc.preview.title;
body.appendChild(title);
}
if (mc.preview.description) {
const desc = document.createElement('div');
desc.className = 'simplex-preview-link-card-description';
desc.textContent = mc.preview.description;
body.appendChild(desc);
}
if (mc.preview.uri) {
const uri = document.createElement('div');
uri.className = 'simplex-preview-link-card-uri';
uri.textContent = mc.preview.uri;
body.appendChild(uri);
}
card.appendChild(body);
bubble.appendChild(card);
}
appendTextBlock(bubble, msg);
}
function renderVoiceContent(bubble, mc, msg) {
const voiceDiv = document.createElement('div');
voiceDiv.className = 'simplex-preview-voice';
voiceDiv.innerHTML = VOICE_ICON_SVG + ' ' + formatDuration(mc.duration || 0) + '';
bubble.appendChild(voiceDiv);
if (mc.text) {
appendTextBlock(bubble, msg);
} else {
appendMetaOnly(bubble, msg);
}
}
function renderFileContent(bubble, mc, msg) {
const fileDiv = document.createElement('div');
fileDiv.className = 'simplex-preview-file-indicator';
fileDiv.innerHTML = FILE_ICON_SVG;
const info = document.createElement('div');
if (msg.file) {
const nameSpan = document.createElement('div');
nameSpan.className = 'simplex-preview-file-name';
nameSpan.textContent = msg.file.fileName;
info.appendChild(nameSpan);
const sizeSpan = document.createElement('div');
sizeSpan.className = 'simplex-preview-file-size';
sizeSpan.textContent = formatFileSize(msg.file.fileSize);
info.appendChild(sizeSpan);
}
fileDiv.appendChild(info);
bubble.appendChild(fileDiv);
if (mc.text) {
appendTextBlock(bubble, msg);
} else {
appendMetaOnly(bubble, msg);
}
}
function renderTextContent(bubble, msg) {
appendTextBlock(bubble, msg);
}
function appendTextBlock(bubble, msg) {
const textDiv = document.createElement('div');
textDiv.className = 'simplex-preview-text';
const meta = renderMetaHTML(msg);
if (msg.formattedText && msg.formattedText.length > 0) {
textDiv.innerHTML = renderMarkdown(msg.formattedText) + meta;
} else {
textDiv.innerHTML = escapeHtml(msg.content.text || '') + meta;
}
bubble.appendChild(textDiv);
}
function appendMetaOnly(bubble, msg) {
const metaDiv = document.createElement('div');
metaDiv.style.cssText = 'padding: 0 8px 4px; text-align: right;';
metaDiv.innerHTML = renderMetaHTML(msg);
bubble.appendChild(metaDiv);
}
function renderMetaHTML(msg) {
let html = '';
return html;
}
function renderReactions(reactions) {
const div = document.createElement('div');
div.className = 'simplex-preview-reactions';
for (const r of reactions) {
if (r.totalReacted < 1) continue;
const badge = document.createElement('span');
badge.className = 'simplex-preview-reaction';
const emoji = r.reaction && r.reaction.emoji ? r.reaction.emoji : '?';
badge.appendChild(document.createTextNode(emoji));
if (r.totalReacted > 1) {
const count = document.createElement('span');
count.className = 'simplex-preview-reaction-count';
count.textContent = r.totalReacted;
badge.appendChild(count);
}
div.appendChild(badge);
}
return div;
}
function getItemSeparation(msg, prevMsg) {
if (!prevMsg || !msg) return { largeGap: true };
const sameSender = msg.sender === prevMsg.sender;
if (!sameSender) return { largeGap: true };
const t1 = new Date(prevMsg.ts).valueOf();
const t2 = new Date(msg.ts).valueOf();
if (Math.abs(t2 - t1) >= 60000) return { largeGap: true };
return { largeGap: false };
}
function formatTime(ts) {
try {
const d = new Date(ts);
const h = d.getHours().toString().padStart(2, '0');
const m = d.getMinutes().toString().padStart(2, '0');
return h + ':' + m;
} catch(e) {
return '';
}
}
function formatDateLabel(ts) {
try {
const d = new Date(ts);
const now = new Date();
const weekday = d.toLocaleDateString(undefined, { weekday: 'short' });
const dayMonth = d.toLocaleDateString(undefined, {
day: 'numeric',
month: 'short',
year: d.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
});
return weekday + ', ' + dayMonth;
} catch(e) {
return '';
}
}
function formatDuration(secs) {
const m = Math.floor(secs / 60);
const s = secs % 60;
return m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0');
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
document.querySelectorAll('[data-simplex-channel-preview]').forEach(initChannelPreview);
})();