Share works in the iOS app on an episode of a private feed (#452)
With no ipx-play link (a private feed, or no ipx-play set up), Share fell back to the share sheet after the request for the link, which WebKit refuses once an await has come between the press and it, and a Patreon address went through confirm() first, which the iOS app answers Cancel without showing anything (#453). Taps sent the request and nothing appeared. That address now goes through the same menu as an ipx-play link, so the sheet opens from a press of its own, and the warning about an address that carries your access is written in the menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Share on an episode offers Share link, Share from where you are, and Copy link, with a link to the episode's page on ipx-play that anyone can play; an admin sets ipx-play up in Admin, Directory (#447).
|
||||
- Share on an episode offers Share link, Share from where you are, and Copy link, with a link to the episode's page on ipx-play that anyone can play; an episode of a private feed offers its own address in the same menu, saying so when the address carries your own access to the feed. An admin sets ipx-play up in Admin, Directory (#447, #452).
|
||||
- In the iOS app, Settings has a This device section for the app's server and player token, with a button that makes a token and hands it to the app (#445).
|
||||
- In the iOS app, the page registers the phone with ipx for its icon's count, and signing out removes it (#433).
|
||||
- In the iOS app, the player bar shows what CarPlay plays, so it can be paused on the phone after leaving the car, including when the app was opened by the car (#420, #442).
|
||||
|
||||
@@ -524,8 +524,8 @@ const shareAt=(url: string, secs: number)=>secs>=5 ? `${url}?t=${Math.floor(secs
|
||||
|
||||
/// An episode shared as Overcast shares one (#447): a link to its page on ipx-play, where anyone
|
||||
/// can play it, from where you are in it if you like. A private feed, a post, or a server with no
|
||||
/// ipx-play shares the item's own address, as before.
|
||||
async function shareEpisode(e: Entry, encId: number|null, btn?: HTMLElement){
|
||||
/// ipx-play shares the item's own address, through the same menu.
|
||||
async function shareEpisode(e: Entry, encId: number|null){
|
||||
const name=entryName(e).text;
|
||||
const own=encId!=null ? e.enclosures.find(x=>x.id===encId)?.url : e.link;
|
||||
let link: {url: string|null, why: string|null}={url:null, why:null};
|
||||
@@ -533,26 +533,26 @@ async function shareEpisode(e: Entry, encId: number|null, btn?: HTMLElement){
|
||||
link=await api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/share`,
|
||||
{method:'POST', body:JSON.stringify(encId!=null ? {enc:encId} : {})});
|
||||
}catch{}
|
||||
if(!link.url){
|
||||
if(own) await share(name, own, btn); else toast('There is nothing to share for this item.', true);
|
||||
return;
|
||||
}
|
||||
const url=link.url;
|
||||
// Where you are: the player's clock when it has this one, else where you left off.
|
||||
const at=player.guid===e.guid && player.feed===e.feed_id ? audio.currentTime : e.position;
|
||||
// A menu rather than the share sheet at once: the sheet needs a press of its own, and the
|
||||
// request above has used up the one that opened this.
|
||||
const url=link.url ?? own;
|
||||
if(!url){ toast('There is nothing to share for this item.', true); return; }
|
||||
// Where you are: the player's clock when it has this one, else where you left off. Only
|
||||
// ipx-play's page reads a time.
|
||||
const at=!link.url ? 0 : player.guid===e.guid && player.feed===e.feed_id ? audio.currentTime : e.position;
|
||||
// A menu rather than the share sheet at once, for the item's own address too: the sheet needs a
|
||||
// press of its own, and the request above has used up the one that opened this (#452). The
|
||||
// warning is written here, not asked with confirm(), which the iOS app answers Cancel (#453).
|
||||
openModal(`<button class="iconbtn cardx" onclick="closeModal()" title="Close" aria-label="Close">${ICON.close}</button>
|
||||
<h3>Share</h3><p class="hint">${esc(name)}</p>
|
||||
${!link.url && carriesAccess(url) ? `<p class="hint">${esc(ACCESS_WARNING)}</p>` : ''}
|
||||
<div class="sharemenu">
|
||||
<button class="btn primary" id="shlink">${ICON.share} Share link</button>
|
||||
${at>=5?`<button class="btn" id="shat">${ICON.share} Share from ${clock(at)}</button>`:''}
|
||||
<button class="btn" id="shcopy">${ICON.copy} Copy link</button>
|
||||
</div>
|
||||
<p class="hint">Anyone with the link can play it, and subscribe in their own podcast app.</p>`);
|
||||
$<HTMLButtonElement>('#shlink').onclick=()=>{ closeModal(); share(name, url); };
|
||||
${link.url ? '<p class="hint">Anyone with the link can play it, and subscribe in their own podcast app.</p>' : ''}`);
|
||||
$<HTMLButtonElement>('#shlink').onclick=()=>{ closeModal(); sendLink(name, url); };
|
||||
const timed=$<HTMLButtonElement>('#shat');
|
||||
if(timed) timed.onclick=()=>{ closeModal(); share(name, shareAt(url, at)); };
|
||||
if(timed) timed.onclick=()=>{ closeModal(); sendLink(name, shareAt(url, at)); };
|
||||
$<HTMLButtonElement>('#shcopy').onclick=async()=>{ await copyText(url, $<HTMLButtonElement>('#shcopy')); toast('Link copied'); closeModal(); };
|
||||
}
|
||||
|
||||
@@ -564,7 +564,7 @@ async function epAction(a: string, e: Entry, el: HTMLElement|null, encId?: numbe
|
||||
try{
|
||||
if(a==='play'){ haptic('tap'); play(e, encId!=null ? enc : undefined); }
|
||||
if(a==='offline' && enc) await toggleKept(enc, entryName(e).text);
|
||||
if(a==='share') await shareEpisode(e, encId??null, el??undefined);
|
||||
if(a==='share') await shareEpisode(e, encId??null);
|
||||
if(a==='flag'){ e.flagged=!e.flagged; await api(path+'/flags',{method:'POST',body:JSON.stringify({flagged:e.flagged})}); haptic('tap'); redraw(); }
|
||||
if(a==='read'){ await setRead(e,!e.read); haptic(e.read?'done':'tap'); redraw(); }
|
||||
if(a==='get'){
|
||||
|
||||
@@ -212,9 +212,17 @@ async function copyText(text: string,btn?: HTMLElement){
|
||||
/// ponytail: a guess from the address. A private feed whose key has another name is shared
|
||||
/// without asking; add its pattern here when one turns up.
|
||||
async function share(title: string, url: string, btn?: HTMLElement){
|
||||
if(/patreon\.com\/|\/\/[^/]*@|[?&][^=]*(auth|token|key|secret|pass|sig)[^=]*=/i.test(url)
|
||||
&& !confirm('This address looks like it includes your own access to the feed, and anyone you '
|
||||
+'send it to could use it as you.\n\nShare it anyway?')) return;
|
||||
if(carriesAccess(url) && !confirm(ACCESS_WARNING+'\n\nShare it anyway?')) return;
|
||||
await sendLink(title, url, btn);
|
||||
}
|
||||
|
||||
const carriesAccess=(url: string)=>/patreon\.com\/|\/\/[^/]*@|[?&][^=]*(auth|token|key|secret|pass|sig)[^=]*=/i.test(url);
|
||||
const ACCESS_WARNING='This address looks like it includes your own access to the feed, and anyone you '
|
||||
+'send it to could use it as you.';
|
||||
|
||||
/// The share sheet or the clipboard, asking nothing. Call it straight from a press: WebKit refuses
|
||||
/// the sheet once an await has come between the press and it (#452).
|
||||
async function sendLink(title: string, url: string, btn?: HTMLElement){
|
||||
if(navigator.share){
|
||||
// Cancelling the sheet rejects too, and is not a failure worth a word.
|
||||
try{ await navigator.share({title, url}); }
|
||||
|
||||
Reference in New Issue
Block a user