GIFs from Wikimedia Commons (MediaWiki search) with no key; shared places link to Apple Maps
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
584a95753d
commit
651d0def2f
9 files changed
+43
-28
No files matched your search
+4
-11
@@ -1,10 +1,10 @@
|
||||
'use strict';
|
||||
// ---- Family Chat extras: a GIF picker (kept family-friendly by the hub; phones never contact the GIF service) and sharing a place (your location, or a place you search for).
|
||||
// The Chat list also shows Signal conversations, labelled, when Signal is available on this screen.
|
||||
async function chGifOn() { if (ch.gifOn === undefined) { try { ch.gifOn = !!(await chApi('/gif/status')).enabled; ch.gifBy = (await chApi('/gif/status')).provider; } catch { ch.gifOn = false; } } return ch.gifOn; }
|
||||
async function chGifOn() { if (ch.gifOn === undefined) { try { const st = await chApi('/gif/status'); ch.gifOn = !!st.enabled; ch.gifBy = st.provider; } catch { ch.gifOn = false; } } return ch.gifOn; }
|
||||
function chGifPicker() {
|
||||
const to = ch.cur, by = ch.gifBy === 'giphy' ? 'GIPHY' : 'Tenor';
|
||||
const el = openSheet('GIF', 'Family-friendly only', `<form id="gpf" class="bar" style="flex-wrap:nowrap;gap:6px"><input type="search" id="gpq" placeholder="Search GIFs" autocomplete="off" maxlength="60" style="flex:1;min-width:0"><button class="btn ghost">Search</button></form><div class="gifgrid" id="gpg" role="list"></div><p class="err" id="gpe"></p><div class="bar" style="justify-content:center"><button class="btn ghost" id="gpm" hidden>More</button></div><p class="sub" style="text-align:center;margin:4px 0 0;font-size:12px">Powered by ${by}</p>`);
|
||||
const to = ch.cur, by = ch.gifBy === 'giphy' ? 'GIPHY' : ch.gifBy === 'wikimedia' ? 'Wikimedia Commons (free pictures; mostly animals, nature, space and science)' : 'Tenor';
|
||||
const el = openSheet('GIF', 'Family-friendly only', `<form id="gpf" class="bar" style="flex-wrap:nowrap;gap:6px"><input type="search" id="gpq" placeholder="Search GIFs" autocomplete="off" maxlength="60" style="flex:1;min-width:0"><button class="btn ghost">Search</button></form>${ch.gifBy === 'wikimedia' ? `<div class="seg" id="gpc" style="flex-wrap:wrap">${['animals', 'space', 'weather', 'ocean', 'science', 'dance', 'cartoon', 'sports'].map(w => `<a href="#" data-w="${w}">${w}</a>`).join('')}</div>` : ''}<div class="gifgrid" id="gpg" role="list"></div><p class="err" id="gpe"></p><div class="bar" style="justify-content:center"><button class="btn ghost" id="gpm" hidden>More</button></div><p class="sub" style="text-align:center;margin:4px 0 0;font-size:12px">Powered by ${by}</p>`);
|
||||
el.querySelector('.sheet').classList.add('wide'); let next = '', q = '', busy = false;
|
||||
const load = async more => {
|
||||
if (busy) return; busy = true; el.querySelector('#gpe').textContent = '';
|
||||
@@ -14,6 +14,7 @@ function chGifPicker() {
|
||||
};
|
||||
el.querySelector('#gpf').onsubmit = e => { e.preventDefault(); q = el.querySelector('#gpq').value.trim(); next = ''; load(false); };
|
||||
el.querySelector('#gpm').onclick = () => load(true);
|
||||
el.querySelectorAll('#gpc [data-w]').forEach(a => a.onclick = e => { e.preventDefault(); el.querySelector('#gpq').value = a.dataset.w; el.querySelector('#gpf').requestSubmit(); });
|
||||
el.querySelector('#gpg').onclick = async e => { const b = e.target.closest('.gifc'); if (!b || b.disabled) return; b.disabled = true; b.style.opacity = '.5';
|
||||
try { await chPost('/gif/send', { conv: to, url: b.dataset.u, title: b.dataset.t }); closeSheet(); } catch (er) { b.disabled = false; b.style.opacity = ''; el.querySelector('#gpe').textContent = er.message; } };
|
||||
load(false);
|
||||
@@ -32,14 +33,6 @@ function chPlacePicker() {
|
||||
try { const r = await chApi('/places?q=' + encodeURIComponent(q)); box.innerHTML = r.items.map((x, i) => `<a href="#" class="row" data-i="${i}"><span class="grow" style="white-space:normal"><b>${esc(x.title)}</b><small>${esc(x.sub)}</small></span></a>`).join('') || '<div class="row"><span class="grow"><small>No places found.</small></span></div>';
|
||||
box.querySelectorAll('[data-i]').forEach(a => a.onclick = ev => { ev.preventDefault(); const x = r.items[+a.dataset.i]; send(x.title, x.sub, x.lat, x.lng); }); } catch (er) { box.innerHTML = ''; el.querySelector('#ple').textContent = er.message; } };
|
||||
}
|
||||
/** Where a shared place opens: the phone's own maps app when there is one, otherwise OpenStreetMap. */
|
||||
function chPlaceHref(s) {
|
||||
const lat = +s.lat, lng = +s.lng; if (!isFinite(lat) || !isFinite(lng)) return s.link;
|
||||
const ua = navigator.userAgent || '', t = encodeURIComponent(s.title || 'Place');
|
||||
if (/iPhone|iPad|iPod|Macintosh/.test(ua) && 'ontouchend' in document) return `https://maps.apple.com/?ll=${lat},${lng}&q=${t}`;
|
||||
if (/Android/.test(ua)) return `geo:${lat},${lng}?q=${lat},${lng}(${t})`;
|
||||
return s.link;
|
||||
}
|
||||
// ---- one list for Chat and Signal
|
||||
async function chLoadSignal() {
|
||||
ch.sig = []; if (typeof allowed !== 'function' || !allowed('signal') || typeof sgApi !== 'function') return;
|
||||
|
||||
+2
-2
@@ -82,12 +82,12 @@ function chPoll(m) {
|
||||
// ---- share cards: a song, film, book... sent from its own screen. Tapping opens it, unless that part of the hub is not turned on for this person.
|
||||
const CH_SHARE = { song: ['🎵', 'Song'], album: ['💿', 'Album'], artist: ['🎤', 'Artist'], playlist: ['📃', 'Playlist'], movie: ['🎬', 'Movie'], show: ['📺', 'TV show'], episode: ['📺', 'Episode'], book: ['📖', 'Book'], audiobook: ['🎧', 'Audiobook'], podcast: ['🎙️', 'Podcast'], podcastep: ['🎙️', 'Podcast episode'], radio: ['📻', 'Radio'], place: ['📍', 'Place'] };
|
||||
/** The tab a deep link lives in decides whether this person can open it (a child account only gets the tabs a parent allowed). */
|
||||
const chShareOk = link => { if (/^https:\/\/www\.openstreetmap\.org\/\?mlat=/.test(String(link || ''))) return true; const tab = String(link || '').split('/')[1]; return !!tab && !!routesOf()[tab] && allowed(tab); };
|
||||
const chShareOk = link => { if (/^https:\/\/maps\.apple\.com\/\?ll=/.test(String(link || ''))) return true; const tab = String(link || '').split('/')[1]; return !!tab && !!routesOf()[tab] && allowed(tab); };
|
||||
function chShareCard(m) {
|
||||
const s = m.share, k = CH_SHARE[s.kind] || ['🔗', 'Shared'], ok = chShareOk(s.link), art = /^\/(plex|api)\//.test(s.art || '') ? s.art : '';
|
||||
const body = `${art ? `<img class="sgshare-art" alt="" loading="lazy" src="${esc(art)}" onerror="this.replaceWith(Object.assign(document.createElement('i'),{className:'sgshare-art',textContent:'${k[0]}'}))">` : `<i class="sgshare-art">${k[0]}</i>`}
|
||||
<span class="sgshare-t"><small>${k[0]} ${esc(k[1])}</small><b>${esc(s.title)}</b>${s.sub ? `<em>${esc(s.sub)}</em>` : ''}${ok ? '' : '<small class="sgshare-off">Not available for you</small>'}</span>`;
|
||||
if (s.kind === 'place') return `<a class="sgshare" href="${esc(chPlaceHref(s))}" target="_blank" rel="noopener noreferrer">${body}<span class="sgshare-go">›</span></a>`;
|
||||
if (s.kind === 'place') return `<a class="sgshare" href="${esc(s.link)}" target="_blank" rel="noopener noreferrer">${body}<span class="sgshare-go">›</span></a>`;
|
||||
return ok ? `<a class="sgshare" href="${esc(s.link)}">${body}<span class="sgshare-go">›</span></a>` : `<div class="sgshare off" role="note">${body}</div>`;
|
||||
}
|
||||
function chBubble(m, c, prev) {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// App-shell cache only; Seafile/API traffic always goes to the network.
|
||||
const CACHE = 'hub-v129';
|
||||
const CACHE = 'hub-v130';
|
||||
const KEEP = /^hub-(offline|share|who|data-)/; // caches that outlive a new version
|
||||
const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder
|
||||
const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];
|
||||
|
||||
Reference in new issue
Block a user