diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/emoji.js | 2 | ||||
| -rw-r--r-- | src/features/messages.js | 53 | ||||
| -rw-r--r-- | src/i18n.js | 1 | ||||
| -rw-r--r-- | src/style.css | 10 |
4 files changed, 48 insertions, 18 deletions
diff --git a/src/emoji.js b/src/emoji.js index 80ccdd2..01db8bf 100644 --- a/src/emoji.js +++ b/src/emoji.js @@ -26,7 +26,7 @@ export const EMOJI_TOKEN_RE = new RegExp('(:[' + CODE + ']+:)', 'g'); export const EMOJI_PARTIAL_RE = new RegExp('(?:^|[^:' + CODE + ']):([' + CODE + ']{2,})$'); // Vector's share link for a pack; the naddr inside is the pack coordinate. -export const PACK_LINK_RE = /^https?:\/\/(?:www\.)?vectorapp\.io\/emojis\/pack\/(naddr1[a-z0-9]+)\/?$/i; +export const PACK_LINK_RE = /^https?:\/\/[^\s/]+\/emojis\/pack\/(naddr1[a-z0-9]+)\/?$/i; const okUrl = (u) => typeof u === 'string' && /^https?:\/\/\S+$/i.test(u.trim()); 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); diff --git a/src/i18n.js b/src/i18n.js index 84aa76b..d7781cd 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -207,6 +207,7 @@ const STR = { msgEmojiPackAdd: 'Add', msgEmojiPackHave: 'Added', msgEmojiPackRemove: 'Remove this pack', + msgEmojiPackShare: 'Copy a link to this pack', msgEmojiPackAdded: 'Emoji pack added: {name}', msgEmojiPackBad: "Couldn't find an emoji pack there", msgEmojiPackLink: 'Emoji pack', diff --git a/src/style.css b/src/style.css index f1670ce..bcec4a5 100644 --- a/src/style.css +++ b/src/style.css @@ -1036,9 +1036,13 @@ body::after { .emoji-packs { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } .emoji-pack { display: inline-flex; align-items: center; gap: 2px; } .emoji-pack .linklike { padding: 0 2px; font-size: 14px; color: var(--muted); } -/* Vector's pack share link: named, with its Add button */ -.pack-link { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; } -.pack-link .btn-sm { padding: 3px 10px; } +/* a pack share link (Vector's or ours) as a chip: one tap adds the pack */ +.pack-link { + display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; font-size: 14px; line-height: 1.3; + border: 1px solid var(--line-strong); border-radius: 10px; background: var(--paper); color: inherit; cursor: pointer; +} +.pack-link:disabled { opacity: 0.7; cursor: default; } +.chat-bubble.me .pack-link { background: transparent; border-color: color-mix(in srgb, currentColor 45%, transparent); } /* a custom emoji whose picture won't load falls back to its :code: alt text; in the 40px grid cell that text must not spill into the next cell */ .emoji-grid button { overflow: hidden; } |
