fix payement link and gallery image

This commit is contained in:
javad committed 2026-10-06 11:18:31 +03:30
1 parent de3135b588
commit 1cbbbbd0dc
24 files changed
+654 -116

No files matched your search

+77
View File
@@ -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
View File
@@ -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
View File
@@ -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();
})();