diff options
| author | Adam Soltys <[email protected]> | 2026-09-15 09:38:47 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-15 09:38:47 -0700 |
| commit | dce411ae0a5f4f9475786b3bd4f419db0a371709 (patch) | |
| tree | b71e479843808b1e881d4ba4e053f852ac16e299 /src/features | |
| parent | 8f3ef61e4fbaf8c7cf586ed1280fd8f4bdc624ae (diff) | |
| download | coinosv3-dce411ae0a5f4f9475786b3bd4f419db0a371709.tar.gz coinosv3-dce411ae0a5f4f9475786b3bd4f419db0a371709.zip | |
A pack link in chat adds the pack in place; coinos has its own share link.
Tapping "Emoji pack" on a phone opened Vector: the chip was an outbound
link to vectorapp.io, a domain the Vector app owns. The chip is now a
button — one tap adds the pack right here, nothing leaves coinos.
Packs also get a coinos share link, v3.coinos.io/emojis/pack/<naddr>
(copy it from the picker's pack row). The Android app already claims
every v3.coinos.io path, so on a phone that link opens coinos, which adds
the pack once the wallet is open. Pack links from any host render as the
chip; the naddr's kind is what's checked.
Pack changes now save at once (learned shortcodes stay coalesced) — a
remove followed straight away by a navigation was lost to the debounce.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 53 |
1 files changed, 39 insertions, 14 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index cae2ba7..4b7be5e 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -26,7 +26,7 @@ import { makeDMRumor, makeDMReaction, unwrapDM, wrapDM } from '../dm.js'; import { EMOJI_SET_KIND, EMOJI_LIST_KIND, EMOJI_PARTIAL_RE, PACK_LINK_RE, emojiTagMap, splitEmoji, emojiOnlyCount, outboundEmojiTags, shortcodeOf, - packAddr, parsePackRef, parsePackAddr, parseEmojiSet, + packAddr, packNaddr, parsePackRef, parsePackAddr, parseEmojiSet, } from '../emoji.js'; import { saveInbox } from '../dm-inbox.js'; import { makeSearcher, resultRows, fallbackAvatar, warmSearch } from '../recipient-search.js'; @@ -114,10 +114,14 @@ export function messagesFeature(ctx) { } return emojiState; }; + // Learned shortcodes arrive in bursts (a backfill), so those writes are + // coalesced; a pack change is one deliberate act and lands at once — a + // tap-then-navigate must never lose it. let emojiSaveTimer = 0; - const saveEmoji = () => { + const saveEmoji = (now = false) => { clearTimeout(emojiSaveTimer); - emojiSaveTimer = setTimeout(() => { try { wallet.saveFeatureState(EMOJI_STATE, emojiSt()); } catch {} }, 500); + const write = () => { try { wallet.saveFeatureState(EMOJI_STATE, emojiSt()); } catch {} }; + if (now) write(); else emojiSaveTimer = setTimeout(write, 500); }; // code → image url: held packs first (in order), then what's been seen const emojiUrl = (code) => { @@ -179,7 +183,7 @@ export function messagesFeature(ctx) { s.packs[pack.addr] = { title: pack.title, image: pack.image, emojis: pack.emojis, at: pack.at }; if (!s.order.includes(pack.addr)) s.order.push(pack.addr); s.unfetched = s.unfetched.filter((a) => a !== pack.addr); - saveEmoji(); + saveEmoji(true); } const hasPack = (addr) => !!emojiSt().packs[addr]; // A pasted naddr / Vector share link, or the Add button on a pack link in @@ -204,7 +208,7 @@ export function messagesFeature(ctx) { delete s.packs[addr]; s.order = s.order.filter((a) => a !== addr); s.unfetched = s.unfetched.filter((a) => a !== addr); - saveEmoji(); + saveEmoji(true); render(); publishEmojiList().catch(() => {}); } @@ -222,7 +226,7 @@ export function messagesFeature(ctx) { const tags = [...kept, ...[...new Set([...s.order, ...s.unfetched])].map((a) => ['a', a])]; const evt = await signPlain(id, { kind: EMOJI_LIST_KIND, content: '', tags, created_at }); s.list = { at: created_at, tags }; - saveEmoji(); + saveEmoji(true); publishOn(emojiRelays(), evt); } // Adopt the newest kind 10030 on the relays when it's newer than what we @@ -252,7 +256,7 @@ export function messagesFeature(ctx) { } // the list's own order wins over arrival order s.order = want.filter((a) => s.packs[a]); - saveEmoji(); + saveEmoji(true); if (changed) scheduleRepaint(); } catch { emojiSyncAt = 0; } } @@ -277,18 +281,21 @@ export function messagesFeature(ctx) { const n = emojiOnlyCount(splitEmoji(text, (c) => em.get(c))); return n > 0 && n <= 3; }; - // Vector's pack share link, rendered as what it is — with an Add button. + // A pack share link (Vector's, or ours) rendered as what it is: one tap + // adds the pack right here. Never an outbound link — on a phone the + // vectorapp.io address belongs to the Vector app, and a tap on it walked + // out of coinos into Vector. function packLinkNode(url, naddr) { const ref = parsePackRef(naddr); const addr = ref && packAddr(ref.pubkey, ref.identifier); const have = !!addr && hasPack(addr); - return h('span', { class: 'pack-link' }, - h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, '🎨 ' + t('msgEmojiPackLink')), - h('button', { - class: 'btn-sm', type: 'button', disabled: have || !!ui.emojiBusy, - onClick: (e) => { e.stopPropagation(); addPack(naddr); }, - }, have ? t('msgEmojiPackHave') : t('msgEmojiPackAdd'))); + return h('button', { + class: 'pack-link', type: 'button', title: url, disabled: have || !!ui.emojiBusy, + onClick: (e) => { e.stopPropagation(); addPack(naddr); }, + }, '🎨 ' + t('msgEmojiPackLink'), ' · ', h('b', {}, have ? t('msgEmojiPackHave') : t('msgEmojiPackAdd'))); } + // our share link for a pack — opens in the coinos app on a phone + const packShareLink = (addr) => APP_BASE + '/emojis/pack/' + packNaddr(addr); // ---- unread ------------------------------------------------------------- // A conversation is unread when its newest message from someone else is @@ -1253,6 +1260,11 @@ export function messagesFeature(ctx) { h('div', { class: 'emoji-packs' }, packs.map((p) => h('span', { class: 'chat-react emoji-pack', title: p.addr }, p.image ? h('img', { class: 'cemoji', src: p.image, alt: '' }) : null, ' ', p.title || '…', + // its share link, ours: a tap on a phone opens the coinos app + h('button', { + type: 'button', class: 'linklike', title: t('msgEmojiPackShare'), + onClick: async () => { try { await navigator.clipboard.writeText(packShareLink(p.addr)); toast(t('copied')); } catch {} }, + }, '⧉'), h('button', { type: 'button', class: 'linklike', title: t('msgEmojiPackRemove'), onClick: () => removePack(p.addr) }, '×'))), ui.emojiPick.addPack ? h('form', { @@ -1631,6 +1643,15 @@ export function messagesFeature(ctx) { // An invite link opened in the browser lands here before the wallet exists. const urlInvite = typeof location !== 'undefined' ? parseInviteLink(location.href) : null; if (urlInvite) { try { history.replaceState(null, '', '/'); } catch {} } + // coinos's own pack share link, /emojis/pack/<naddr> — the Android app + // owns every v3.coinos.io path, so one tapped on a phone lands here, not + // in a browser; added once a wallet is open (init below). + const urlPack = (() => { + if (typeof location === 'undefined') return null; + const m = location.pathname.match(/^\/emojis\/pack\/(naddr1[a-z0-9]+)\/?$/i); + return m && parsePackRef(m[1]) ? m[1] : null; + })(); + if (urlPack) { try { history.replaceState(null, '', '/'); } catch {} } // coinos.io/<username> parity: a claimed name (or a raw npub / hex key) as // the whole URL path deep-links to that profile. Real files never reach the @@ -6557,6 +6578,10 @@ export function messagesFeature(ctx) { loadLinkInvite(urlInvite); setTimeout(() => { ui.chatOpen = true; ui.msgView = 'home'; render(); }, 0); } + if (urlPack) { + addPack(urlPack).catch(() => {}); + setTimeout(() => { ui.chatOpen = true; ui.msgView = 'home'; render(); }, 0); + } ui.pubProf = null; // a wallet is open now — its chrome owns the profile window.addEventListener('resize', onViewportResize); window.visualViewport?.addEventListener('resize', onViewportResize); |
