From 546306fa1692db7ac062024ce8c99ebbb90fc536 Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:07:38 +0000 Subject: [PATCH] web cta --- .../Chat/Group/ChannelWebAccessView.swift | 1 + .../views/chat/group/ChannelWebPageView.kt | 1 + website/channel_sample.html | 1 + website/src/js/channel-preview.jsc | 256 ++++++++++++++++-- 4 files changed, 237 insertions(+), 22 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift index f84dce87bf..594a058aef 100644 --- a/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift +++ b/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift @@ -128,6 +128,7 @@ struct ChannelWebAccessView: View { data-channel-link="\(pg.groupLink)" data-channel-id="\(pg.publicGroupId)" data-relay-domains="\(relayDomains.joined(separator: ","))" + data-app-download-buttons="on" > """ diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt index a948f0830a..ead19a0981 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt @@ -179,6 +179,7 @@ private fun embedCode(groupRelays: List, 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 = + 'App Store' + + 'Google Play'; + 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 = + 'Google Play' + + 'F-Droid' + + 'APK Download'; + } else if (isMobile.iOS()) { + badges.innerHTML = + 'App Store' + + 'TestFlight'; + } else { + badges.innerHTML = + 'App Store' + + 'Google Play'; + } + 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);