document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{ const scroller=workspace.querySelector('[data-conversation]'); const conversationEnd=workspace.querySelector('[data-conversation-end]'); const jump=workspace.querySelector('[data-jump-latest]'); const form=workspace.querySelector('[data-ticket-composer]'); const input=form?.querySelector('textarea'); const composer=workspace.querySelector('[data-composer-shell]'); const composerToggle=workspace.querySelector('[data-composer-toggle]'); const baseKey=workspace.dataset.draftKey||location.pathname; let mode='reply'; const atBottom=()=>scroller.scrollHeight-scroller.scrollTop-scroller.clientHeight<80; const scrollLatest=(smooth=false)=>{const top=scroller.scrollHeight;if(smooth&&typeof scroller.scrollTo==='function')scroller.scrollTo({top,behavior:'smooth'});else scroller.scrollTop=top}; let initialPin=true; const scheduleLatest=()=>{[0,40,120,300,700,1200].forEach(delay=>setTimeout(()=>{if(initialPin)scrollLatest(false)},delay))}; scheduleLatest(); window.addEventListener('load',scheduleLatest,{once:true}); window.addEventListener('pageshow',scheduleLatest); document.fonts?.ready?.then(scheduleLatest); scroller.querySelectorAll('img,audio').forEach(media=>{const event=media.tagName==='IMG'?'load':'loadedmetadata';media.addEventListener(event,()=>{if(initialPin)scrollLatest(false)},{once:true})}); const sizeObserver='ResizeObserver' in window?new ResizeObserver(()=>{if(initialPin)scrollLatest(false)}):null; sizeObserver?.observe(scroller);if(conversationEnd?.parentElement)sizeObserver?.observe(conversationEnd.parentElement); setTimeout(()=>{initialPin=false;sizeObserver?.disconnect()},1800); ['wheel','touchstart','pointerdown','keydown'].forEach(eventName=>scroller.addEventListener(eventName,()=>{initialPin=false;sizeObserver?.disconnect()},{once:true,passive:true})); scroller.addEventListener('scroll',()=>{jump.hidden=atBottom()}); jump?.addEventListener('click',()=>{scrollLatest(true);jump.hidden=true}); const groupDates=()=>{scroller.querySelectorAll('.date-divider').forEach(item=>item.remove());let previousDate='';scroller.querySelectorAll('[data-message]').forEach(message=>{if(message.dataset.date!==previousDate){const divider=document.createElement('div');divider.className='date-divider';divider.innerHTML=`${message.dataset.dayLabel}`;message.before(divider);previousDate=message.dataset.date}})}; groupDates(); document.body.addEventListener('htmx:beforeSwap',event=>{if(event.detail.target?.matches('[data-load-older]')){scroller.dataset.previousHeight=scroller.scrollHeight}}); document.body.addEventListener('htmx:afterSwap',event=>{if(event.detail.target?.matches('[data-load-older]')){const previous=Number(scroller.dataset.previousHeight||0);groupDates();scroller.scrollTop+=scroller.scrollHeight-previous}}); const resize=()=>{if(!input)return;input.style.height='auto';input.style.height=`${Math.min(Math.max(input.scrollHeight,112),240)}px`}; const draftKey=()=>`${baseKey}-${mode}`; const status=workspace.querySelector('[data-draft-status]'); const replyResult=workspace.dataset.replyResult||new URLSearchParams(location.search).get('reply_result')||''; const submissionSucceeded=replyResult==='reply'||replyResult==='note'; if(submissionSucceeded){sessionStorage.removeItem(`${baseKey}-reply`);sessionStorage.removeItem(`${baseKey}-note`)} const setComposerOpen=(open,focus=true)=>{composer?.classList.toggle('is-collapsed',!open);composerToggle?.setAttribute('aria-expanded',String(open));if(open&&focus)setTimeout(()=>input?.focus(),80)}; const saveDraft=()=>{if(!input)return;sessionStorage.setItem(draftKey(),input.value);if(status){status.textContent=input.value?'پیش‌نویس ذخیره شد':'';clearTimeout(saveDraft.timer);saveDraft.timer=setTimeout(()=>status.textContent='',1800)}}; const loadDraft=()=>{if(!input)return;input.value=sessionStorage.getItem(draftKey())||'';resize();input.dispatchEvent(new Event('input',{bubbles:true}))}; if(input){input.setAttribute('rows','5');input.setAttribute('placeholder','شرح کامل پاسخ، اطلاعات تکمیلی و نتیجه مورد انتظار خود را بنویسید…');input.addEventListener('input',()=>{resize();saveDraft()});loadDraft()} const hasDraft=Boolean(sessionStorage.getItem(`${baseKey}-reply`)||sessionStorage.getItem(`${baseKey}-note`)); if(submissionSucceeded){form?.reset();if(input){input.value='';resize()}setComposerOpen(false,false)}else setComposerOpen(hasDraft||replyResult==='error',false); composerToggle?.addEventListener('click',()=>setComposerOpen(true)); workspace.querySelectorAll('[data-composer-close]').forEach(button=>button.addEventListener('click',()=>setComposerOpen(false,false))); workspace.querySelector('[data-dismiss-dispatch-context]')?.addEventListener('click',event=>event.currentTarget.closest('.ticket-dispatch-context')?.remove()); composer?.addEventListener('keydown',event=>{if(event.key==='Escape'){setComposerOpen(false,false);composerToggle?.focus()}}); const primarySubmit=form?.querySelector('[data-primary-submit]'); const guideToPrimarySubmit=event=>{event.preventDefault();event.stopPropagation();if(!primarySubmit)return;primarySubmit.scrollIntoView({behavior:'smooth',block:'center'});primarySubmit.classList.remove('submit-attention');void primarySubmit.offsetWidth;primarySubmit.classList.add('submit-attention');setTimeout(()=>primarySubmit.focus({preventScroll:true}),450)}; workspace.querySelectorAll('[data-scroll-to-submit]').forEach(control=>{control.addEventListener('click',guideToPrimarySubmit);control.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){guideToPrimarySubmit(event)}})}); workspace.querySelectorAll('[data-composer-mode]').forEach(button=>button.addEventListener('click',()=>{saveDraft();mode=button.dataset.composerMode;loadDraft();setTimeout(()=>input?.focus(),0)})); workspace.querySelector('[data-toggle-recorder]')?.addEventListener('click',()=>{const recorder=workspace.querySelector('[data-voice-recorder]');recorder.hidden=!recorder.hidden;if(!recorder.hidden)recorder.querySelector('[data-record]')?.focus()}); workspace.querySelector('[data-toggle-extra]')?.addEventListener('click',()=>{const extra=workspace.querySelector('[data-composer-extra]');extra.hidden=!extra.hidden}); 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. // navigator.onLine only reports the browser's network-interface heuristic. // It can be false while this Django origin is still reachable (notably on // 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 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; const wasAtBottom=atBottom(); const fragment=document.createRange().createContextualFragment(data.html||''); if(data.replace)scroller.querySelectorAll('[data-message],.date-divider,.conversation-empty').forEach(item=>item.remove()); else scroller.querySelectorAll('.conversation-empty').forEach(item=>item.remove()); scroller.insertBefore(fragment,conversationEnd); workspace.dataset.pollVersion=data.version||'';workspace.dataset.pollCursor=data.cursor||''; 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; }; const poll=async()=>{ 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 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(!response.ok)throw new Error(`HTTP ${response.status}`); applyPollUpdate(await response.json());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(){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()}}); window.addEventListener('focus',startPolling); window.addEventListener('pageshow',restartPolling); window.addEventListener('online',restartPolling); // One independent heartbeat cannot be lost in the load/pageshow race that can // cancel a recursively scheduled timeout. pollRunning prevents overlap and // nextPollAt preserves the normal interval and network-error backoff. const pollHeartbeat=setInterval(startPolling,1000); window.addEventListener('beforeunload',()=>{clearInterval(pollHeartbeat);stopPolling()},{once:true}); startPolling(); const resetMessageMenu=menu=>{if(!menu)return;const editor=menu.querySelector('[data-message-editor]');const choices=menu.querySelector('[data-message-menu-choices]');const input=menu.querySelector('[data-message-edit-input]');if(editor)editor.hidden=true;if(choices)choices.hidden=false;if(input){input.value=input.dataset.originalValue||'';input.dispatchEvent(new Event('input',{bubbles:true}))}}; const closeMessageMenu=menu=>{if(!menu)return;menu.open=false;resetMessageMenu(menu)}; workspace.addEventListener('toggle',event=>{const menu=event.target.closest?.('[data-message-menu]');if(menu?.open)workspace.querySelectorAll('[data-message-menu][open]').forEach(other=>{if(other!==menu)closeMessageMenu(other)})},true); workspace.addEventListener('click',event=>{const close=event.target.closest('[data-message-menu-close]');if(close){event.preventDefault();closeMessageMenu(close.closest('[data-message-menu]'));return}const edit=event.target.closest('[data-message-edit-open]');if(edit){const menu=edit.closest('[data-message-menu]');menu.querySelector('[data-message-menu-choices]').hidden=true;menu.querySelector('[data-message-editor]').hidden=false;const input=menu.querySelector('[data-message-edit-input]');input.focus();input.setSelectionRange(input.value.length,input.value.length);return}const back=event.target.closest('[data-message-edit-back]');if(back){event.preventDefault();resetMessageMenu(back.closest('[data-message-menu]'))}}); workspace.addEventListener('input',event=>{if(!event.target.matches('[data-message-edit-input]'))return;const count=event.target.closest('[data-message-editor]').querySelector('[data-message-edit-count]');if(count)count.textContent=`${event.target.value.length.toLocaleString('fa-IR')} / ۴۰۰۰`}); document.addEventListener('click',event=>{workspace.querySelectorAll('[data-message-menu][open]').forEach(menu=>{if(!menu.contains(event.target))closeMessageMenu(menu)})}); workspace.addEventListener('keydown',event=>{if(event.key==='Escape'){const menu=event.target.closest('[data-message-menu]')||workspace.querySelector('[data-message-menu][open]');if(menu){event.preventDefault();closeMessageMenu(menu);menu.querySelector(':scope > summary')?.focus()}}}); workspace.querySelectorAll('[data-toggle-panel]').forEach(button=>button.addEventListener('click',()=>{const panel=workspace.querySelector(`#${button.dataset.togglePanel}`);workspace.querySelectorAll('[data-workspace-panel]').forEach(item=>item.classList.toggle('panel-open',item===panel&&!item.classList.contains('panel-open')))})); workspace.querySelectorAll('[data-close-panel]').forEach(button=>button.addEventListener('click',()=>button.closest('[data-workspace-panel]').classList.remove('panel-open'))); });