, groupInfo: GroupInfo): Stri
data-channel-link="${pg.groupLink}"
data-channel-id="${pg.publicGroupId}"
data-relay-domains="$domains"
+ data-app-download-buttons="on"
>
"""
}
diff --git a/website/channel_sample.html b/website/channel_sample.html
index d102c76ceb..86ed9d4813 100644
--- a/website/channel_sample.html
+++ b/website/channel_sample.html
@@ -15,6 +15,7 @@
data-channel-link="YOUR_CHANNEL_LINK"
data-channel-id="YOUR_CHANNEL_ID"
data-relay-domains="relay1.example.com"
+ data-app-download-buttons="on"
>
diff --git a/website/src/js/channel-preview.jsc b/website/src/js/channel-preview.jsc
index 27682a3439..30894b5ae2 100644
--- a/website/src/js/channel-preview.jsc
+++ b/website/src/js/channel-preview.jsc
@@ -2,6 +2,8 @@
(function() {
+#include "qrcode.js"
+
const STYLE = `
.simplex-preview-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
@@ -72,6 +74,7 @@ const STYLE = `
cursor: pointer;
text-decoration: none;
display: inline-block;
+ font-family: inherit;
}
.simplex-preview-join-btn:hover {
@@ -497,6 +500,122 @@ const STYLE = `
width: 100%;
}
+.simplex-preview-conversion {
+ margin-top: 20px;
+}
+
+.simplex-preview-qr-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.simplex-preview-qr-container canvas {
+ border-radius: 8px;
+}
+
+.simplex-preview-qr-caption {
+ font-size: 13px;
+ color: #8b8786;
+ text-align: center;
+ margin: 8px 0 0;
+}
+
+.simplex-preview-badges {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin: 16px 0 0;
+}
+
+.simplex-preview-badges a {
+ display: block;
+}
+
+.simplex-preview-badges a img {
+ height: 36px;
+ width: auto;
+ display: block;
+}
+
+.simplex-preview-copy-section {
+ font-size: 13px;
+ color: #8b8786;
+ margin-top: 16px;
+}
+
+.simplex-preview-copy-section a {
+ color: #0088ff;
+ text-decoration: none;
+}
+
+.simplex-preview-copy-section a:hover {
+ text-decoration: underline;
+}
+
+.simplex-preview-copy-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 6px;
+ background: #f5f5f6;
+ border-radius: 8px;
+ padding: 6px 10px;
+}
+
+.simplex-preview-copy-link {
+ flex: 1;
+ font-size: 11px;
+ color: #333;
+ word-break: break-all;
+ font-family: monospace;
+ min-width: 0;
+ line-height: 1.3;
+}
+
+.simplex-preview-copy-btn {
+ flex-shrink: 0;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 2px;
+ color: #8b8786;
+ line-height: 1;
+}
+
+.simplex-preview-copy-btn:hover {
+ color: #333;
+}
+
+.simplex-preview-step-label {
+ font-size: 16px;
+ font-weight: 500;
+ text-align: center;
+ margin: 20px 0 8px;
+ color: #000;
+}
+
+.simplex-preview-open-btn {
+ display: block;
+ text-align: center;
+ background: #0053D0;
+ color: #fff;
+ border: none;
+ border-radius: 34px;
+ padding: 12px 32px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ text-decoration: none;
+ font-family: inherit;
+}
+
+.simplex-preview-open-btn:hover {
+ background: #0047b3;
+}
+
@media (min-width: 1000px) {
.simplex-preview-info {
width: 320px;
@@ -562,11 +681,14 @@ const VOICE_ICON_SVG = ``;
+const COPY_ICON_SVG = ``;
+
function initChannelPreview(container) {
const relayDomains = (container.dataset.relayDomains || '').split(',').map(u => u.trim()).filter(Boolean);
const relayScheme = container.dataset.relayScheme || 'https';
const channelId = container.dataset.channelId || '';
const channelLink = container.dataset.channelLink || '';
+ const showAppBadges = container.dataset.appDownloadButtons !== 'off';
if (!relayDomains.length || !channelId) {
container.innerHTML = 'Missing configuration: data-relay-domains and data-channel-id required.
';
@@ -586,7 +708,7 @@ function initChannelPreview(container) {
container.innerHTML = 'Failed to load channel preview.
';
return;
}
- render(container, data, channelLink);
+ render(container, data, channelLink, showAppBadges);
});
}
@@ -620,7 +742,7 @@ async function fetchPreview(relayScheme, relayDomains, channelLink, channelId) {
return linkMismatch ? 'link_mismatch' : null;
}
-function render(container, data, channelLink) {
+function render(container, data, channelLink, showAppBadges) {
const { channel, members, messages } = data;
const membersMap = {};
for (const m of members) {
@@ -652,13 +774,12 @@ function render(container, data, channelLink) {
const infoContent = document.createElement('div');
infoContent.className = 'simplex-preview-info-content';
- renderInfoContent(infoContent, data, channelLink, members.length);
+ renderInfoContent(infoContent, data, channelLink, members.length, showAppBadges);
info.appendChild(infoContent);
container.appendChild(info);
- header.addEventListener('click', (e) => {
- if (e.target.closest('.simplex-preview-join-btn')) return;
+ header.addEventListener('click', () => {
if (window.innerWidth < 1000) {
info.classList.add('open');
main.style.overflow = 'hidden';
@@ -704,22 +825,16 @@ function renderHeader(channel, channelLink, subscriberCount) {
header.appendChild(info);
if (channelLink) {
- const btn = document.createElement('a');
+ const btn = document.createElement('button');
btn.className = 'simplex-preview-join-btn';
btn.textContent = 'Join';
- try {
- btn.href = platformSimplexUri(channelLink);
- } catch(e) {
- btn.href = channelLink;
- }
- btn.target = '_blank';
header.appendChild(btn);
}
return header;
}
-function renderInfoContent(container, data, channelLink, subscriberCount) {
+function renderInfoContent(container, data, channelLink, subscriberCount, showAppBadges) {
const { channel } = data;
const avatar = document.createElement('img');
@@ -765,19 +880,116 @@ function renderInfoContent(container, data, channelLink, subscriberCount) {
}
if (channelLink) {
- const btn = document.createElement('a');
- btn.className = 'simplex-preview-join-btn';
- btn.textContent = 'Join';
- try {
- btn.href = platformSimplexUri(channelLink);
- } catch(e) {
- btn.href = channelLink;
+ const conversion = document.createElement('div');
+ conversion.className = 'simplex-preview-conversion';
+ if (isMobile.any()) {
+ renderMobileConversion(conversion, channelLink, showAppBadges);
+ } else {
+ renderDesktopConversion(conversion, channelLink, showAppBadges);
}
- btn.target = '_blank';
- container.appendChild(btn);
+ container.appendChild(conversion);
}
}
+function renderDesktopConversion(container, channelLink, showAppBadges) {
+ const qrContainer = document.createElement('div');
+ qrContainer.className = 'simplex-preview-qr-container';
+ const canvas = document.createElement('canvas');
+ qrContainer.appendChild(canvas);
+
+ const caption = document.createElement('p');
+ caption.className = 'simplex-preview-qr-caption';
+ caption.textContent = 'Scan from mobile app to join';
+ qrContainer.appendChild(caption);
+ container.appendChild(qrContainer);
+
+ try {
+ QRCode.toCanvas(canvas, channelLink, {
+ errorCorrectionLevel: 'M',
+ color: { dark: '#062D56' },
+ width: 400,
+ margin: 1
+ }).then(function() {
+ canvas.style.width = '200px';
+ canvas.style.height = '200px';
+ }).catch(function() {
+ qrContainer.style.display = 'none';
+ });
+ } catch(e) {
+ qrContainer.style.display = 'none';
+ }
+
+ if (showAppBadges) {
+ const badges = document.createElement('div');
+ badges.className = 'simplex-preview-badges';
+ badges.innerHTML =
+ '
' +
+ '
';
+ container.appendChild(badges);
+ }
+
+ const copySection = document.createElement('div');
+ copySection.className = 'simplex-preview-copy-section';
+ copySection.innerHTML = 'Or copy link for desktop app:';
+
+ const copyRow = document.createElement('div');
+ copyRow.className = 'simplex-preview-copy-row';
+
+ const linkText = document.createElement('span');
+ linkText.className = 'simplex-preview-copy-link';
+ linkText.textContent = channelLink;
+ copyRow.appendChild(linkText);
+
+ const copyBtn = document.createElement('button');
+ copyBtn.className = 'simplex-preview-copy-btn';
+ copyBtn.innerHTML = COPY_ICON_SVG;
+ copyBtn.title = 'Copy to clipboard';
+ copyBtn.addEventListener('click', function() {
+ navigator.clipboard.writeText(channelLink).then(function() {
+ copyBtn.title = 'Copied!';
+ setTimeout(function() { copyBtn.title = 'Copy to clipboard'; }, 2000);
+ });
+ });
+ copyRow.appendChild(copyBtn);
+
+ copySection.appendChild(copyRow);
+ container.appendChild(copySection);
+}
+
+function renderMobileConversion(container, channelLink, showAppBadges) {
+ if (showAppBadges) {
+ const stepLabel = document.createElement('p');
+ stepLabel.className = 'simplex-preview-step-label';
+ stepLabel.textContent = 'Install SimpleX App';
+ container.appendChild(stepLabel);
+
+ const badges = document.createElement('div');
+ badges.className = 'simplex-preview-badges';
+
+ if (isMobile.Android()) {
+ badges.innerHTML =
+ '
' +
+ '
' +
+ '
';
+ } else if (isMobile.iOS()) {
+ badges.innerHTML =
+ '
' +
+ '
';
+ } else {
+ badges.innerHTML =
+ '
' +
+ '
';
+ }
+ container.appendChild(badges);
+ }
+
+ const openBtn = document.createElement('a');
+ openBtn.className = 'simplex-preview-open-btn';
+ openBtn.textContent = 'Open SimpleX App';
+ openBtn.href = channelLink;
+ container.appendChild(openBtn);
+}
+
function setupExpandCollapse(section, readMore) {
if (section._measured) return;
const lineHeight = parseFloat(getComputedStyle(section).lineHeight);