fix payement link and gallery image
This commit is contained in:
1 parent
de3135b588
commit
1cbbbbd0dc
24 files changed
+654
-116
No files matched your search
@@ -0,0 +1,77 @@
|
||||
/* Receiving a message is not evidence of seeing it. No receipts are cached on disk. */
|
||||
document.querySelectorAll('[data-conversation-workspace][data-read-url]').forEach(workspace=>{
|
||||
const scroller=workspace.querySelector('[data-conversation]');
|
||||
if(!scroller)return;
|
||||
const acknowledged=new Set(),tracked=new Set();
|
||||
let running=false,timer=0,controller=null;
|
||||
const cookie=name=>document.cookie.split('; ').find(row=>row.startsWith(`${name}=`))?.split('=').slice(1).join('=')||'';
|
||||
const visibleIds=()=>{
|
||||
if(document.visibilityState!=='visible' || workspace.dataset.accessDenied)return [];
|
||||
const bounds=scroller.getBoundingClientRect();
|
||||
const viewport=window.visualViewport;
|
||||
const viewTop=viewport?.offsetTop||0,viewLeft=viewport?.offsetLeft||0;
|
||||
const top=Math.max(bounds.top,viewTop),bottom=Math.min(bounds.bottom,viewTop+(viewport?.height||innerHeight));
|
||||
const left=Math.max(bounds.left,viewLeft),right=Math.min(bounds.right,viewLeft+(viewport?.width||innerWidth));
|
||||
return [...scroller.querySelectorAll('[data-message-id]')].filter(message=>{
|
||||
if(acknowledged.has(message.dataset.messageId) || message.hasAttribute('data-message-deleted'))return false;
|
||||
const box=message.getBoundingClientRect();
|
||||
const height=Math.min(box.bottom,bottom)-Math.max(box.top,top);
|
||||
if(box.height<=0 || box.width<=0 || height<Math.min(48,box.height) || Math.min(box.right,right)<=Math.max(box.left,left))return false;
|
||||
const x=(Math.max(box.left,left)+Math.min(box.right,right))/2;
|
||||
const y=(Math.max(box.top,top)+Math.min(box.bottom,bottom))/2;
|
||||
return message.contains(document.elementFromPoint(x,y));
|
||||
}).slice(0,100).map(message=>message.dataset.messageId);
|
||||
};
|
||||
const schedule=()=>{
|
||||
clearTimeout(timer);
|
||||
timer=setTimeout(send,250);
|
||||
};
|
||||
const denyAccess=()=>{
|
||||
workspace.dataset.accessDenied='true';
|
||||
delete workspace.dataset.pollUrl;
|
||||
controller?.abort();clearTimeout(timer);
|
||||
scroller.querySelectorAll('[data-load-older]').forEach(button=>window.htmx?.trigger(button,'htmx:abort'));
|
||||
scroller.querySelectorAll('[data-message],.date-divider,.conversation-empty,[data-load-older]').forEach(item=>item.remove());
|
||||
const notice=document.createElement('p');
|
||||
notice.className='conversation-empty';notice.textContent='دسترسی به این مکاتبات لغو شده است.';
|
||||
scroller.append(notice);
|
||||
const composer=workspace.querySelector('[data-composer-shell]');if(composer)composer.hidden=true;
|
||||
const jump=workspace.querySelector('[data-jump-latest]');if(jump)jump.hidden=true;
|
||||
};
|
||||
async function send(){
|
||||
if(running)return;
|
||||
const ids=visibleIds();if(!ids.length)return;
|
||||
running=true;controller=new AbortController();
|
||||
try{
|
||||
const response=await fetch(workspace.dataset.readUrl,{method:'POST',credentials:'same-origin',cache:'no-store',signal:controller.signal,
|
||||
headers:{'Content-Type':'application/json','X-CSRFToken':decodeURIComponent(cookie('csrftoken'))},body:JSON.stringify({message_ids:ids})});
|
||||
if(response.redirected || [401,403,404].includes(response.status)){workspace.dispatchEvent(new Event('conversation:access-denied'));return}
|
||||
if(!response.ok)throw new Error(`HTTP ${response.status}`);
|
||||
const data=await response.json();
|
||||
if(workspace.dataset.accessDenied)return;
|
||||
(data.read_ids||[]).forEach(id=>acknowledged.add(id));
|
||||
if(typeof data.unread_shares==='number')document.querySelectorAll('[data-share-unread-count]').forEach(badge=>{
|
||||
badge.textContent=String(data.unread_shares);badge.hidden=!data.unread_shares;
|
||||
});
|
||||
}catch(error){if(error.name!=='AbortError')console.error('Message read acknowledgement failed',error)}
|
||||
finally{running=false;controller=null;if(visibleIds().length)timer=setTimeout(send,3000)}
|
||||
}
|
||||
const observer=new IntersectionObserver(schedule,{root:scroller,threshold:[0,0.1,0.5,1]});
|
||||
const observeMessages=()=>{
|
||||
tracked.forEach(message=>{if(!scroller.contains(message)){observer.unobserve(message);tracked.delete(message)}});
|
||||
scroller.querySelectorAll('[data-message-id]').forEach(message=>{if(!tracked.has(message)){tracked.add(message);observer.observe(message)}});
|
||||
schedule();
|
||||
};
|
||||
new MutationObserver(observeMessages).observe(scroller,{childList:true,subtree:true});
|
||||
scroller.addEventListener('scroll',schedule,{passive:true});
|
||||
new ResizeObserver(schedule).observe(scroller);
|
||||
document.addEventListener('click',schedule);
|
||||
document.addEventListener('keydown',schedule);
|
||||
window.addEventListener('resize',schedule);
|
||||
window.visualViewport?.addEventListener('resize',schedule);
|
||||
document.addEventListener('visibilitychange',schedule);
|
||||
window.addEventListener('pageshow',schedule);
|
||||
workspace.addEventListener('conversation:access-denied',denyAccess);
|
||||
window.addEventListener('pagehide',()=>{clearTimeout(timer);controller?.abort()});
|
||||
observeMessages();
|
||||
});
|
||||
+10
-18
@@ -87,20 +87,8 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
|
||||
form?.addEventListener('submit',()=>{form.dataset.submitting='true'});
|
||||
window.addEventListener('pageshow',()=>{if(submissionSucceeded){form?.reset();if(input){input.value='';resize()}setComposerOpen(false,false)}});
|
||||
|
||||
const pollUrl=workspace.dataset.pollUrl;
|
||||
const readUrl=workspace.dataset.readUrl;
|
||||
const pollInterval=3000;
|
||||
let pollController=null,pollRunning=false,pollGeneration=0,pollErrors=0,nextPollAt=0;
|
||||
let readRunning=false;
|
||||
const cookie=name=>document.cookie.split('; ').find(row=>row.startsWith(`${name}=`))?.split('=').slice(1).join('=')||'';
|
||||
const acknowledgeVisibleMessages=async()=>{
|
||||
if(!readUrl||readRunning||document.visibilityState!=='visible')return;
|
||||
const messageIds=[...scroller.querySelectorAll('[data-message-id]')].map(item=>item.dataset.messageId).filter(Boolean);
|
||||
if(!messageIds.length)return;
|
||||
readRunning=true;
|
||||
try{await fetch(readUrl,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':decodeURIComponent(cookie('csrftoken'))},body:JSON.stringify({message_ids:messageIds})})}catch(error){console.error('Message read acknowledgement failed',error)}finally{readRunning=false}
|
||||
};
|
||||
acknowledgeVisibleMessages();
|
||||
// Keep the staff/customer conversation synchronized even while another tab is
|
||||
// active. Browsers may throttle background timers, so visibility is used only
|
||||
// to trigger an immediate catch-up, never as a hard polling gate.
|
||||
@@ -109,7 +97,7 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
|
||||
// local networks, installed PWAs and some mobile browsers). Let fetch be the
|
||||
// source of truth and use the existing error backoff when the origin really
|
||||
// is unavailable.
|
||||
const canPoll=()=>Boolean(pollUrl);
|
||||
const canPoll=()=>Boolean(workspace.dataset.pollUrl) && !workspace.dataset.accessDenied;
|
||||
const schedulePoll=()=>{const delay=pollErrors?Math.min(30000,pollInterval*(2**Math.min(pollErrors,3))):pollInterval;nextPollAt=Date.now()+delay};
|
||||
const applyPollUpdate=data=>{
|
||||
if(!data.changed)return;
|
||||
@@ -124,7 +112,6 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
|
||||
const count=workspace.querySelector('[data-conversation-count]');if(count)count.textContent=String(data.count??'');
|
||||
workspace.querySelectorAll('[data-ticket-status]').forEach(item=>item.textContent=data.status||item.textContent);
|
||||
groupDates();
|
||||
acknowledgeVisibleMessages();
|
||||
scroller.querySelectorAll('img:not([data-poll-bound]),audio:not([data-poll-bound])').forEach(media=>{media.dataset.pollBound='true';const event=media.tagName==='IMG'?'load':'loadedmetadata';media.addEventListener(event,()=>{if(wasAtBottom)scrollLatest(false)},{once:true})});
|
||||
if(wasAtBottom){scrollLatest(true);jump.hidden=true}else jump.hidden=false;
|
||||
};
|
||||
@@ -132,17 +119,22 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
|
||||
if(!canPoll()||pollRunning)return;
|
||||
const generation=++pollGeneration;pollRunning=true;const controller=new AbortController();pollController=controller;
|
||||
try{
|
||||
const url=new URL(pollUrl,location.origin);url.searchParams.set('version',workspace.dataset.pollVersion||'');url.searchParams.set('cursor',workspace.dataset.pollCursor||'');
|
||||
const url=new URL(workspace.dataset.pollUrl,location.origin);url.searchParams.set('version',workspace.dataset.pollVersion||'');url.searchParams.set('cursor',workspace.dataset.pollCursor||'');
|
||||
const response=await fetch(url,{method:'GET',signal:controller.signal,cache:'no-store',headers:{Accept:'application/json'}});
|
||||
if(response.redirected){location.assign(response.url);return}
|
||||
if(generation!==pollGeneration || !canPoll())return;
|
||||
if(response.redirected){workspace.dispatchEvent(new Event('conversation:access-denied'));location.assign(response.url);return}
|
||||
if([401,403,404].includes(response.status)){workspace.dispatchEvent(new Event('conversation:access-denied'));return}
|
||||
if(!response.ok)throw new Error(`HTTP ${response.status}`);
|
||||
applyPollUpdate(await response.json());pollErrors=0;
|
||||
const data=await response.json();
|
||||
if(generation!==pollGeneration || !canPoll())return;
|
||||
applyPollUpdate(data);pollErrors=0;
|
||||
}catch(error){if(error.name!=='AbortError'){pollErrors+=1;console.error('Ticket polling failed',error)}}finally{if(generation===pollGeneration){pollRunning=false;pollController=null;schedulePoll()}}
|
||||
};
|
||||
const startPolling=()=>{if(!canPoll()||pollRunning||Date.now()<nextPollAt)return;poll()};
|
||||
const stopPolling=()=>{pollGeneration+=1;if(pollController){pollController.abort();pollController=null}pollRunning=false;nextPollAt=0};
|
||||
const restartPolling=()=>{stopPolling();startPolling()};
|
||||
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible'){restartPolling();acknowledgeVisibleMessages()}});
|
||||
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')restartPolling()});
|
||||
workspace.addEventListener('conversation:access-denied',stopPolling);
|
||||
window.addEventListener('focus',startPolling);
|
||||
window.addEventListener('pageshow',restartPolling);
|
||||
window.addEventListener('online',restartPolling);
|
||||
|
||||
+19
-50
@@ -1,54 +1,23 @@
|
||||
(() => {
|
||||
const badges = document.querySelectorAll('[data-share-unread-count]');
|
||||
if (!badges.length) return;
|
||||
const workspace = document.querySelector('[data-share-read-url]');
|
||||
const scroller = workspace?.querySelector('[data-conversation]');
|
||||
const countUrl = badges[0].dataset.countUrl;
|
||||
let running = false;
|
||||
const cookie = name => document.cookie.split('; ').find(row => row.startsWith(`${name}=`))?.split('=').slice(1).join('=') || '';
|
||||
const refresh = async () => {
|
||||
if (running || document.visibilityState !== 'visible') return;
|
||||
running = true;
|
||||
try {
|
||||
const ids = [];
|
||||
if (scroller) {
|
||||
const bounds = scroller.getBoundingClientRect();
|
||||
scroller.querySelectorAll('[data-message-id]').forEach(message => {
|
||||
const box = message.getBoundingClientRect();
|
||||
if (box.bottom > Math.max(bounds.top, 0) && box.top < Math.min(bounds.bottom, innerHeight)) ids.push(message.dataset.messageId);
|
||||
});
|
||||
}
|
||||
const body = new URLSearchParams();
|
||||
ids.forEach(id => body.append('message_ids', id));
|
||||
const response = await fetch(workspace?.dataset.shareReadUrl || countUrl, workspace ? {
|
||||
method: 'POST', body, headers: {'X-CSRFToken': cookie('csrftoken')}, cache: 'no-store', credentials: 'same-origin'
|
||||
} : {cache: 'no-store', credentials: 'same-origin'});
|
||||
if (response.status === 403 && workspace) {
|
||||
scroller.querySelectorAll('[data-message], .date-divider, .conversation-empty').forEach(item => item.remove());
|
||||
const notice = document.createElement('p');
|
||||
notice.className = 'conversation-empty';
|
||||
notice.textContent = 'دسترسی به این اشتراک لغو شده است.';
|
||||
scroller.append(notice);
|
||||
workspace.removeAttribute('data-poll-url');
|
||||
const counts = await fetch(countUrl, {cache: 'no-store', credentials: 'same-origin'});
|
||||
if (counts.ok) {
|
||||
const data = await counts.json();
|
||||
badges.forEach(badge => {badge.textContent = data.unread_shares; badge.hidden = !data.unread_shares;});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
badges.forEach(badge => {badge.textContent = data.unread_shares; badge.hidden = !data.unread_shares;});
|
||||
} catch (_) {
|
||||
// A failed request leaves the count and read status available for retry.
|
||||
} finally { running = false; }
|
||||
const badges=document.querySelectorAll('[data-share-unread-count]');
|
||||
if(!badges.length)return;
|
||||
const countUrl=badges[0].dataset.countUrl;
|
||||
let running=false;
|
||||
const refresh=async()=>{
|
||||
if(running || document.visibilityState!=='visible')return;
|
||||
running=true;
|
||||
try{
|
||||
const response=await fetch(countUrl,{cache:'no-store',credentials:'same-origin'});
|
||||
if(!response.ok || response.redirected)return;
|
||||
const data=await response.json();
|
||||
badges.forEach(badge=>{badge.textContent=String(data.unread_shares);badge.hidden=!data.unread_shares});
|
||||
}catch(_){
|
||||
// Retain the last count until the next successful refresh.
|
||||
}finally{running=false}
|
||||
};
|
||||
scroller?.addEventListener('scroll', refresh, {passive: true});
|
||||
document.addEventListener('visibilitychange', refresh);
|
||||
window.addEventListener('pageshow', refresh);
|
||||
window.addEventListener('focus', refresh);
|
||||
document.body.addEventListener('htmx:afterSwap', refresh);
|
||||
setInterval(refresh, workspace ? 3000 : 15000);
|
||||
document.addEventListener('visibilitychange',refresh);
|
||||
window.addEventListener('pageshow',refresh);
|
||||
window.addEventListener('focus',refresh);
|
||||
setInterval(refresh,15000);
|
||||
refresh();
|
||||
})();
|
||||
Reference in new issue
Block a user