2747 lines
86 KiB
JavaScript
2747 lines
86 KiB
JavaScript
( function () {
|
||
'use strict';
|
||
|
||
if ( typeof HESABIX_CHAT === 'undefined' ) {
|
||
if ( typeof console !== 'undefined' && console.warn ) {
|
||
console.warn( '[HesabixChat] HESABIX_CHAT تعریف نشد — اسکریپت localize نشده، public_key خالی است، یا ترتیب لود (فایل JS قبل از HESABIX_CHAT) اشتباه است.' );
|
||
}
|
||
return;
|
||
}
|
||
|
||
var cfg = HESABIX_CHAT;
|
||
var STORAGE_VERSION = 1;
|
||
|
||
function remembersPanelAcrossPages() {
|
||
var r = cfg.rememberPanelBetweenPages;
|
||
if ( typeof r === 'undefined' ) {
|
||
return true;
|
||
}
|
||
return r === true || r === 1 || r === '1';
|
||
}
|
||
|
||
function chatIsVerbose() {
|
||
if ( cfg && cfg.debug ) {
|
||
return true;
|
||
}
|
||
try {
|
||
if ( typeof localStorage !== 'undefined' && localStorage.getItem( 'hesabix_chat_debug' ) === '1' ) {
|
||
return true;
|
||
}
|
||
if ( typeof location !== 'undefined' && /[?&]hesabix_chat_debug=1(?:&|$)/.test( String( location.search || '' ) ) ) {
|
||
return true;
|
||
}
|
||
} catch ( e0 ) {}
|
||
return false;
|
||
}
|
||
function chatLog() {
|
||
var a = [ '[HesabixChat]' ];
|
||
for ( var i = 0; i < arguments.length; i++ ) {
|
||
a.push( arguments[ i ] );
|
||
}
|
||
if ( typeof console !== 'undefined' && console.log ) {
|
||
console.log.apply( console, a );
|
||
}
|
||
}
|
||
function chatLogV() {
|
||
if ( ! chatIsVerbose() ) {
|
||
return;
|
||
}
|
||
var b = [ '[HesabixChat:verbose]' ];
|
||
for ( var j = 0; j < arguments.length; j++ ) {
|
||
b.push( arguments[ j ] );
|
||
}
|
||
if ( typeof console !== 'undefined' && console.log ) {
|
||
console.log.apply( console, b );
|
||
}
|
||
}
|
||
function chatWarn() {
|
||
var c = [ '[HesabixChat]' ];
|
||
for ( var k = 0; k < arguments.length; k++ ) {
|
||
c.push( arguments[ k ] );
|
||
}
|
||
if ( typeof console !== 'undefined' && console.warn ) {
|
||
console.warn.apply( console, c );
|
||
}
|
||
}
|
||
|
||
var widgetDebugPending = [];
|
||
var widgetDebugFlushTimer = null;
|
||
|
||
function remoteDebugSecretKey( lk ) {
|
||
if ( lk === 'visitor_token' || lk === 'authorization' ) {
|
||
return true;
|
||
}
|
||
if ( lk.indexOf( 'password' ) >= 0 ) {
|
||
return true;
|
||
}
|
||
var i = lk.length - 6;
|
||
if ( i >= 0 && lk.slice( i ) === '_token' ) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function sanitizeForRemoteDebugValue( data, depth ) {
|
||
if ( typeof depth !== 'number' ) {
|
||
depth = 0;
|
||
}
|
||
if ( depth > 5 ) {
|
||
return '[max-depth]';
|
||
}
|
||
if ( data === null || data === undefined ) {
|
||
return data;
|
||
}
|
||
if ( typeof data === 'string' ) {
|
||
if ( data.length > 800 ) {
|
||
return data.slice( 0, 800 ) + '…';
|
||
}
|
||
return data;
|
||
}
|
||
if ( typeof data !== 'object' ) {
|
||
return data;
|
||
}
|
||
if ( Array.isArray( data ) ) {
|
||
var outa = [];
|
||
var mx = Math.min( data.length, 40 );
|
||
for ( var ia = 0; ia < mx; ia++ ) {
|
||
outa.push( sanitizeForRemoteDebugValue( data[ ia ], depth + 1 ) );
|
||
}
|
||
return outa;
|
||
}
|
||
var outo = {};
|
||
for ( var dk in data ) {
|
||
if ( ! Object.prototype.hasOwnProperty.call( data, dk ) ) {
|
||
continue;
|
||
}
|
||
var lk2 = String( dk ).toLowerCase();
|
||
var vv = data[ dk ];
|
||
if ( remoteDebugSecretKey( lk2 ) ) {
|
||
outo[ dk ] =
|
||
typeof vv === 'string' && vv.length
|
||
? vv.slice( 0, Math.min( 4, vv.length ) ) + '…'
|
||
: '[redacted]';
|
||
continue;
|
||
}
|
||
outo[ dk ] = sanitizeForRemoteDebugValue( vv, depth + 1 );
|
||
}
|
||
return outo;
|
||
}
|
||
|
||
function widgetDebugConfigured() {
|
||
return !! (
|
||
cfg.widgetDebugLogging &&
|
||
cfg.widgetDebugAjaxUrl &&
|
||
cfg.widgetDebugNonce
|
||
);
|
||
}
|
||
|
||
function pushWidgetRemoteDebug( topic, payload ) {
|
||
if ( ! widgetDebugConfigured() ) {
|
||
return;
|
||
}
|
||
try {
|
||
var tpk = typeof topic === 'string' ? topic.slice( 0, 80 ) : 'event';
|
||
widgetDebugPending.push( {
|
||
topic: tpk || 'event',
|
||
client_ts: Date.now(),
|
||
payload: sanitizeForRemoteDebugValue( payload, 0 )
|
||
} );
|
||
if ( widgetDebugPending.length > 120 ) {
|
||
widgetDebugPending.splice(
|
||
0,
|
||
widgetDebugPending.length - 120
|
||
);
|
||
}
|
||
scheduleWidgetRemoteDebugFlush();
|
||
} catch ( eWd0 ) {}
|
||
}
|
||
|
||
function scheduleWidgetRemoteDebugFlush() {
|
||
if ( widgetDebugFlushTimer !== null ) {
|
||
return;
|
||
}
|
||
widgetDebugFlushTimer = window.setTimeout( function () {
|
||
widgetDebugFlushTimer = null;
|
||
flushWidgetRemoteDebugNow();
|
||
}, 2000 );
|
||
}
|
||
|
||
function flushWidgetRemoteDebugNow() {
|
||
if ( ! widgetDebugConfigured() || ! widgetDebugPending.length ) {
|
||
return;
|
||
}
|
||
var slice = widgetDebugPending.splice( 0, widgetDebugPending.length );
|
||
var packs;
|
||
try {
|
||
packs = JSON.stringify( {
|
||
referer:
|
||
typeof location !== 'undefined'
|
||
? String( location.href || '' ).split( '#' )[ 0 ]
|
||
: '',
|
||
items: slice
|
||
} );
|
||
} catch ( eWdJson ) {
|
||
return;
|
||
}
|
||
var ajaxU = cfg.widgetDebugAjaxUrl;
|
||
var ajaxN = cfg.widgetDebugNonce;
|
||
try {
|
||
if ( typeof fetch !== 'undefined' ) {
|
||
var fd = new FormData();
|
||
fd.append( 'action', 'hesabix_chat_debug_push' );
|
||
fd.append( 'nonce', ajaxN );
|
||
fd.append( 'batch', packs );
|
||
fetch( ajaxU, {
|
||
method: 'POST',
|
||
body: fd,
|
||
credentials: 'same-origin',
|
||
cache: 'no-store'
|
||
} ).catch( function () {} );
|
||
return;
|
||
}
|
||
} catch ( eWdF ) {}
|
||
try {
|
||
if ( typeof navigator !== 'undefined' && navigator.sendBeacon ) {
|
||
var fdBeacon = new FormData();
|
||
fdBeacon.append( 'action', 'hesabix_chat_debug_push' );
|
||
fdBeacon.append( 'nonce', ajaxN );
|
||
fdBeacon.append( 'batch', packs );
|
||
navigator.sendBeacon( ajaxU, fdBeacon );
|
||
}
|
||
} catch ( eWdB ) {}
|
||
}
|
||
|
||
if (
|
||
typeof document !== 'undefined' &&
|
||
document.addEventListener &&
|
||
document.visibilityState !== undefined
|
||
) {
|
||
document.addEventListener(
|
||
'visibilitychange',
|
||
function () {
|
||
if ( document.visibilityState === 'hidden' ) {
|
||
flushWidgetRemoteDebugNow();
|
||
}
|
||
},
|
||
false
|
||
);
|
||
}
|
||
|
||
chatLogV( 'راه\u200cاندازی', {
|
||
loadMode: cfg.loadMode,
|
||
hasApiBase: Boolean( cfg.apiBase && String( cfg.apiBase ).length ),
|
||
hasPublicKey: Boolean( cfg.publicKey && String( cfg.publicKey ).length ),
|
||
rememberPanel: remembersPanelAcrossPages(),
|
||
hint: 'برای لاگ بیشتر: ?hesabix_chat_debug=1 در URL یا localStorage.hesabix_chat_debug=1'
|
||
} );
|
||
|
||
function simpleKey( s ) {
|
||
var h = 0;
|
||
for ( var i = 0; i < s.length; i++ ) {
|
||
h = ( ( h << 5 ) - h ) + s.charCodeAt( i );
|
||
h |= 0;
|
||
}
|
||
return 'h' + Math.abs( h ).toString( 36 );
|
||
}
|
||
|
||
function storageKey() {
|
||
return 'hesabix_wp_crm_' + STORAGE_VERSION + '_' + simpleKey( cfg.apiBase + '|' + cfg.publicKey );
|
||
}
|
||
|
||
function panelUiStorageKey() {
|
||
return 'hesabix_wp_panel_' + STORAGE_VERSION + '_' + simpleKey( cfg.apiBase + '|' + cfg.publicKey );
|
||
}
|
||
|
||
function readRememberedPanelOpen() {
|
||
if ( ! remembersPanelAcrossPages() ) {
|
||
return null;
|
||
}
|
||
try {
|
||
if ( typeof sessionStorage === 'undefined' ) {
|
||
return null;
|
||
}
|
||
var raw = sessionStorage.getItem( panelUiStorageKey() );
|
||
if ( raw === null || raw === '' ) {
|
||
return null;
|
||
}
|
||
if ( raw === '1' ) {
|
||
return true;
|
||
}
|
||
if ( raw === '0' ) {
|
||
return false;
|
||
}
|
||
} catch ( ePan ) {}
|
||
return null;
|
||
}
|
||
|
||
function persistRememberedPanelOpen( opened ) {
|
||
if ( ! remembersPanelAcrossPages() ) {
|
||
return;
|
||
}
|
||
try {
|
||
if ( typeof sessionStorage === 'undefined' ) {
|
||
return;
|
||
}
|
||
sessionStorage.setItem( panelUiStorageKey(), opened ? '1' : '0' );
|
||
} catch ( ePan2 ) {}
|
||
}
|
||
|
||
function clearRememberedPanelOpen() {
|
||
try {
|
||
if ( typeof sessionStorage !== 'undefined' ) {
|
||
sessionStorage.removeItem( panelUiStorageKey() );
|
||
}
|
||
} catch ( ePan3 ) {}
|
||
}
|
||
|
||
function getWsUrl() {
|
||
var base = ( cfg.apiBase || '' ).replace( /\/$/, '' );
|
||
if ( base.indexOf( 'https://' ) === 0 ) {
|
||
return base.replace( 'https://', 'wss://' ) + '/ws/crm-chat';
|
||
}
|
||
if ( base.indexOf( 'http://' ) === 0 ) {
|
||
return base.replace( 'http://', 'ws://' ) + '/ws/crm-chat';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function apiPath( p ) {
|
||
return ( cfg.apiBase || '' ).replace( /\/$/, '' ) + p;
|
||
}
|
||
|
||
function loadSession() {
|
||
try {
|
||
var raw = localStorage.getItem( storageKey() );
|
||
if ( ! raw ) {
|
||
return null;
|
||
}
|
||
var o = JSON.parse( raw );
|
||
if ( ! o || o.v !== STORAGE_VERSION || o.apiBase !== cfg.apiBase || o.publicKey !== cfg.publicKey ) {
|
||
return null;
|
||
}
|
||
if ( ! o.visitor_token || ! o.conversation_id ) {
|
||
return null;
|
||
}
|
||
return o;
|
||
} catch ( e ) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function saveSession( convId, token ) {
|
||
var payload = {
|
||
v: STORAGE_VERSION,
|
||
apiBase: cfg.apiBase,
|
||
publicKey: cfg.publicKey,
|
||
conversation_id: convId,
|
||
visitor_token: token
|
||
};
|
||
try {
|
||
localStorage.setItem( storageKey(), JSON.stringify( payload ) );
|
||
} catch ( e ) {}
|
||
}
|
||
|
||
function clearSession() {
|
||
try {
|
||
localStorage.removeItem( storageKey() );
|
||
} catch ( e ) {}
|
||
clearRememberedPanelOpen();
|
||
}
|
||
|
||
function historyStorageKey() {
|
||
return 'hesabix_wp_hist_1_' + simpleKey( cfg.apiBase + '|' + cfg.publicKey );
|
||
}
|
||
|
||
function loadThreadHistory() {
|
||
try {
|
||
var raw = localStorage.getItem( historyStorageKey() );
|
||
if ( ! raw ) {
|
||
return [];
|
||
}
|
||
var o = JSON.parse( raw );
|
||
if ( ! o || o.v !== 1 || ! Array.isArray( o.items ) ) {
|
||
return [];
|
||
}
|
||
return o.items;
|
||
} catch ( eH ) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function saveThreadHistory( items ) {
|
||
try {
|
||
localStorage.setItem(
|
||
historyStorageKey(),
|
||
JSON.stringify( { v: 1, items: items } )
|
||
);
|
||
} catch ( eS ) {}
|
||
}
|
||
|
||
function pushThreadHistory( sess ) {
|
||
if ( ! sess || sess.conversation_id == null || ! sess.visitor_token ) {
|
||
return;
|
||
}
|
||
var arr = loadThreadHistory();
|
||
var filt = arr.filter( function ( x ) {
|
||
return String( x.conversation_id ) !== String( sess.conversation_id );
|
||
} );
|
||
filt.unshift( {
|
||
conversation_id: sess.conversation_id,
|
||
visitor_token: sess.visitor_token,
|
||
saved_at: Date.now()
|
||
} );
|
||
if ( filt.length > 20 ) {
|
||
filt = filt.slice( 0, 20 );
|
||
}
|
||
saveThreadHistory( filt );
|
||
}
|
||
|
||
function detectVisitorDeviceType() {
|
||
try {
|
||
var ua = navigator.userAgent || '';
|
||
var uaTablet = /iPad|Tablet|PlayBook|Silk|Nexus (7|10)/i.test( ua );
|
||
var uaMobile = /Mobi|Android.*Mobile|iPhone|iPod|webOS|BlackBerry|IEMobile|Opera Mini/i.test( ua );
|
||
if ( typeof window.matchMedia === 'function' ) {
|
||
try {
|
||
if (
|
||
window.matchMedia( '(pointer: coarse)' ).matches &&
|
||
Math.min( screen.width, screen.height ) >= 600 &&
|
||
/Tablet|iPad|PlayBook/i.test( ua )
|
||
) {
|
||
return 'tablet';
|
||
}
|
||
} catch ( eMm ) {}
|
||
}
|
||
if ( uaTablet ) {
|
||
return 'tablet';
|
||
}
|
||
if ( /Android/i.test( ua ) && ! /Mobile/i.test( ua ) ) {
|
||
return 'tablet';
|
||
}
|
||
if ( uaMobile ) {
|
||
return 'mobile';
|
||
}
|
||
return 'desktop';
|
||
} catch ( eDt ) {
|
||
return 'desktop';
|
||
}
|
||
}
|
||
|
||
function parseJsonSafe( text ) {
|
||
try {
|
||
return JSON.parse( text );
|
||
} catch ( e ) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function sameConversationId( a, b ) {
|
||
return a != null && b != null && String( a ) === String( b );
|
||
}
|
||
|
||
function wsFromRoleLooksAgent( role ) {
|
||
var r = String( role == null ? '' : role )
|
||
.toLowerCase()
|
||
.replace( /^\s+|\s+$/g, '' );
|
||
if ( ! r ) {
|
||
return false;
|
||
}
|
||
switch ( r ) {
|
||
case 'agent':
|
||
case 'staff':
|
||
case 'operator':
|
||
case 'support':
|
||
case 'supporter':
|
||
case 'consultant':
|
||
case 'advisor':
|
||
case 'employee':
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function extractApiError( j ) {
|
||
if ( ! j ) {
|
||
return cfg.strings.errorGeneric;
|
||
}
|
||
var d = j.detail;
|
||
if ( d && typeof d === 'object' && d.error && d.error.message ) {
|
||
return d.error.message;
|
||
}
|
||
if ( d && d.message ) {
|
||
return d.message;
|
||
}
|
||
if ( j.error && j.error.message ) {
|
||
return j.error.message;
|
||
}
|
||
if ( j.message && typeof j.message === 'string' ) {
|
||
return j.message;
|
||
}
|
||
return cfg.strings.errorGeneric;
|
||
}
|
||
|
||
function apiFetch( url, options ) {
|
||
return fetch( url, options ).then( function ( res ) {
|
||
return res.text().then( function ( text ) {
|
||
var j = parseJsonSafe( text );
|
||
if ( ! res.ok ) {
|
||
throw new Error( extractApiError( j ) );
|
||
}
|
||
if ( j && j.success === false ) {
|
||
throw new Error( extractApiError( j ) );
|
||
}
|
||
return j;
|
||
} );
|
||
} );
|
||
}
|
||
|
||
function startConversation( body ) {
|
||
return apiFetch( apiPath( '/api/v1/public/crm-chat/conversations/start' ), {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify( {
|
||
public_key: cfg.publicKey,
|
||
first_name: body.first_name,
|
||
last_name: body.last_name,
|
||
email: body.email,
|
||
phone: body.phone,
|
||
page_url: window.location.href,
|
||
device_type: detectVisitorDeviceType()
|
||
} )
|
||
} ).then( function ( j ) {
|
||
return j.data || j;
|
||
} );
|
||
}
|
||
|
||
function postMessage( convId, token, body ) {
|
||
return apiFetch( apiPath( '/api/v1/public/crm-chat/messages' ), {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify( {
|
||
visitor_token: token,
|
||
conversation_id: convId,
|
||
body: body
|
||
} )
|
||
} ).then( function ( j ) {
|
||
return j.data || j;
|
||
} );
|
||
}
|
||
|
||
function patchVisitorCurrentPage( convId, token, pageUrl ) {
|
||
var path = '/api/v1/public/crm-chat/conversations/' + encodeURIComponent( convId ) + '/current-page';
|
||
return apiFetch( apiPath( path ), {
|
||
method: 'PATCH',
|
||
headers: Object.assign( { 'Content-Type': 'application/json' }, visitorTokenHeaders( token ) ),
|
||
body: JSON.stringify( { page_url: pageUrl } )
|
||
} ).then( function ( j ) {
|
||
return j.data || j;
|
||
} );
|
||
}
|
||
|
||
function visitorTokenHeaders( token ) {
|
||
return { 'X-Visitor-Token': token };
|
||
}
|
||
|
||
function listMessages( convId, token, limit ) {
|
||
var q = '?limit=' + ( limit || 100 );
|
||
return apiFetch(
|
||
apiPath( '/api/v1/public/crm-chat/conversations/' + convId + '/messages' ) + q,
|
||
{ method: 'GET', headers: visitorTokenHeaders( token ) }
|
||
).then( function ( j ) {
|
||
var d = j.data || j;
|
||
return ( d && d.items ) ? d.items : [];
|
||
} );
|
||
}
|
||
|
||
/** علامتگذاری پیامهای پشتیبان بهعنوان خواندهشده (تا شناسه). */
|
||
function markAgentMessagesRead( convId, token, upToId ) {
|
||
if ( ! upToId || upToId < 1 ) {
|
||
return Promise.resolve();
|
||
}
|
||
return apiFetch( apiPath( '/api/v1/public/crm-chat/conversations/' + convId + '/read' ), {
|
||
method: 'POST',
|
||
headers: Object.assign( { 'Content-Type': 'application/json' }, visitorTokenHeaders( token ) ),
|
||
body: JSON.stringify( { up_to_message_id: upToId } )
|
||
} ).catch( function () {} );
|
||
}
|
||
|
||
function maxAgentMessageId( items ) {
|
||
var m = 0;
|
||
( items || [] ).forEach( function ( x ) {
|
||
if ( ( ( x.sender_role || '' ).toString() ) !== 'agent' || x.id == null ) {
|
||
return;
|
||
}
|
||
var id = +x.id;
|
||
if ( id > m ) {
|
||
m = id;
|
||
}
|
||
} );
|
||
return m;
|
||
}
|
||
|
||
function playAgentReplySound() {
|
||
var url = ( cfg.agentReplySoundUrl || '' ).toString();
|
||
if ( ! url ) {
|
||
return;
|
||
}
|
||
try {
|
||
var a = new Audio( url );
|
||
a.volume = 0.85;
|
||
var p = a.play();
|
||
if ( p && typeof p.catch === 'function' ) {
|
||
p.catch( function () {} );
|
||
}
|
||
} catch ( ePlay ) {}
|
||
}
|
||
|
||
function primeAgentSoundForAutoplay() {
|
||
var url = ( cfg.agentReplySoundUrl || '' ).toString();
|
||
if ( ! url ) {
|
||
return;
|
||
}
|
||
try {
|
||
var a = new Audio( url );
|
||
a.volume = 0.001;
|
||
var p = a.play();
|
||
if ( p && typeof p.catch === 'function' ) {
|
||
p.catch( function () {} );
|
||
}
|
||
} catch ( eIgn ) {}
|
||
}
|
||
|
||
function maybePlayAgentReplySound( items ) {
|
||
var maxId = maxAgentMessageId( items );
|
||
var url = ( cfg.agentReplySoundUrl || '' ).toString();
|
||
if ( ! state.agentSoundPrimed ) {
|
||
state.lastAgentMsgNotifiedId = maxId;
|
||
state.agentSoundPrimed = true;
|
||
return;
|
||
}
|
||
if ( maxId > state.lastAgentMsgNotifiedId ) {
|
||
state.lastAgentMsgNotifiedId = maxId;
|
||
if ( url ) {
|
||
playAgentReplySound();
|
||
}
|
||
}
|
||
}
|
||
|
||
function syncMarkAgentRead( items ) {
|
||
if ( ! state.session ) {
|
||
return;
|
||
}
|
||
var up = maxAgentMessageId( items );
|
||
if ( up > 0 ) {
|
||
markAgentMessagesRead( state.session.conversation_id, state.session.visitor_token, up );
|
||
}
|
||
}
|
||
|
||
function visitorFileDownloadUrl( conversationId, fileId ) {
|
||
return apiPath( '/api/v1/public/crm-chat/conversations/' + conversationId + '/files/' + encodeURIComponent( fileId ) + '/download' );
|
||
}
|
||
|
||
/** دانلود فایل ضمیمه بدون گذاشتن توکن در URL (هدر X-Visitor-Token). */
|
||
function downloadVisitorFile( convId, token, fileId, displayName ) {
|
||
var u = visitorFileDownloadUrl( convId, fileId );
|
||
return fetch( u, { method: 'GET', headers: visitorTokenHeaders( token ) } ).then( function ( res ) {
|
||
if ( ! res.ok ) {
|
||
return res.text().then( function ( text ) {
|
||
var j = parseJsonSafe( text );
|
||
throw new Error( extractApiError( j ) );
|
||
} );
|
||
}
|
||
var fname = displayName || 'file';
|
||
var cd = res.headers.get( 'Content-Disposition' );
|
||
if ( cd ) {
|
||
var mStar = cd.match( /filename\*=UTF-8''([^;\n]+)/i );
|
||
var mQ = cd.match( /filename="([^"]+)"/i );
|
||
if ( mStar && mStar[1] ) {
|
||
try {
|
||
fname = decodeURIComponent( mStar[1].trim() );
|
||
} catch ( e0 ) {
|
||
fname = mStar[1].trim();
|
||
}
|
||
} else if ( mQ && mQ[1] ) {
|
||
fname = mQ[1].trim();
|
||
}
|
||
}
|
||
return res.blob().then( function ( blob ) {
|
||
var objUrl = URL.createObjectURL( blob );
|
||
var a = document.createElement( 'a' );
|
||
a.href = objUrl;
|
||
a.download = fname;
|
||
a.rel = 'noopener';
|
||
document.body.appendChild( a );
|
||
a.click();
|
||
a.remove();
|
||
URL.revokeObjectURL( objUrl );
|
||
} );
|
||
} );
|
||
}
|
||
|
||
function postFile( convId, token, file, caption ) {
|
||
var fd = new FormData();
|
||
fd.append( 'visitor_token', token );
|
||
fd.append( 'conversation_id', String( convId ) );
|
||
fd.append( 'caption', caption || '' );
|
||
fd.append( 'file', file );
|
||
return apiFetch( apiPath( '/api/v1/public/crm-chat/messages/file' ), {
|
||
method: 'POST',
|
||
body: fd
|
||
} ).then( function ( j ) {
|
||
return j.data || j;
|
||
} );
|
||
}
|
||
|
||
function posClass( pos ) {
|
||
var map = {
|
||
'bottom-right': 'hesabix-pos-bottom-right',
|
||
'bottom-left': 'hesabix-pos-bottom-left',
|
||
'top-right': 'hesabix-pos-top-right',
|
||
'top-left': 'hesabix-pos-top-left'
|
||
};
|
||
return map[ pos ] || 'hesabix-pos-bottom-right';
|
||
}
|
||
|
||
function formatTime( iso ) {
|
||
if ( ! iso ) {
|
||
return '';
|
||
}
|
||
try {
|
||
var d = new Date( iso );
|
||
return d.toLocaleString();
|
||
} catch ( e ) {
|
||
return String( iso );
|
||
}
|
||
}
|
||
|
||
var state = {
|
||
open: false,
|
||
session: null,
|
||
messages: [],
|
||
localCannedAfterVisitor: [],
|
||
agentSoundPrimed: false,
|
||
lastAgentMsgNotifiedId: 0,
|
||
ws: null,
|
||
pollTimer: null,
|
||
usePoll: false,
|
||
agentTyping: false,
|
||
agentTypingTimer: null,
|
||
agentTypingName: '',
|
||
seenAgentJoinIds: {},
|
||
agentJoinBannerText: '',
|
||
visitorTypingSendTimer: null,
|
||
visitorTypingStopTimer: null,
|
||
pageUrlReportTimer: null,
|
||
visitorOpts: { allowFile: false, allowVoice: false },
|
||
slowReplyTimer: null,
|
||
slowReplyBaselineAgentId: 0,
|
||
slowReplyVisible: false,
|
||
agentAttendanceNoticeShown: false,
|
||
businessHoursSticky: '',
|
||
businessHoursSendAck: ''
|
||
};
|
||
|
||
if ( cfg.businessHours && ( cfg.businessHours.enabled === true || cfg.businessHours.enabled === 1 || cfg.businessHours.enabled === '1' ) ) {
|
||
if ( cfg.businessHours.snapshotOutside && cfg.businessHours.snapshotMessage ) {
|
||
var snapM = String( cfg.businessHours.snapshotMessage || '' ).replace( /^\s+|\s+$/g, '' );
|
||
if ( snapM ) {
|
||
state.businessHoursSticky = snapM;
|
||
}
|
||
}
|
||
}
|
||
|
||
function resolveOperatorPresenceName( apiNameRaw ) {
|
||
var mode = ( cfg.operatorLabelMode || 'real' ) + '';
|
||
if ( mode === 'unified' ) {
|
||
var u = ( cfg.operatorUnifiedDisplayName || '' ).toString().trim();
|
||
if ( u ) {
|
||
return u;
|
||
}
|
||
}
|
||
return ( apiNameRaw || '' ).toString().trim();
|
||
}
|
||
|
||
function agentPresenceBubbleLabel( snFromMsg ) {
|
||
var r = resolveOperatorPresenceName( snFromMsg || '' );
|
||
if ( r ) {
|
||
return r;
|
||
}
|
||
var sup = cfg.strings && cfg.strings.support ? String( cfg.strings.support ) : '';
|
||
return sup.replace ? sup.replace( /^\s+|\s+$/g, '' ) : sup;
|
||
}
|
||
|
||
function noticeTplOrFallback( tpl, fallback ) {
|
||
var t = ( tpl != null ? String( tpl ) : '' ).replace( /^\s+|\s+$/g, '' );
|
||
return t ? t : ( fallback ? String( fallback ) : '' );
|
||
}
|
||
|
||
function maybeClearAgentTypingLive() {
|
||
if ( state.agentTypingTimer ) {
|
||
clearTimeout( state.agentTypingTimer );
|
||
state.agentTypingTimer = null;
|
||
}
|
||
state.agentTyping = false;
|
||
state.agentTypingName = '';
|
||
updatePeerTypingUI();
|
||
}
|
||
|
||
function scheduleSlowReplyChecks() {
|
||
var sec = +( cfg.slowReplyTimeoutSec || 0 );
|
||
var txtRaw = cfg.slowReplyMessage != null ? String( cfg.slowReplyMessage ) : '';
|
||
var txt = txtRaw.replace( /^\s+|\s+$/g, '' );
|
||
if ( state.slowReplyTimer ) {
|
||
clearTimeout( state.slowReplyTimer );
|
||
state.slowReplyTimer = null;
|
||
}
|
||
if ( sec <= 0 || ! txt ) {
|
||
return;
|
||
}
|
||
state.slowReplyTimer = setTimeout( function () {
|
||
state.slowReplyTimer = null;
|
||
if ( ! state.session ) {
|
||
return;
|
||
}
|
||
listMessages( state.session.conversation_id, state.session.visitor_token, 100 )
|
||
.then( function ( items ) {
|
||
var ax = maxAgentMessageId( items );
|
||
var base = +( state.slowReplyBaselineAgentId || 0 );
|
||
if ( ax > base ) {
|
||
return;
|
||
}
|
||
state.slowReplyVisible = true;
|
||
state.messages = items;
|
||
renderMessages();
|
||
} )
|
||
.catch( function () {} );
|
||
}, sec * 1000 );
|
||
}
|
||
|
||
function baselineSlowReplyAfterVisitorSend() {
|
||
state.slowReplyBaselineAgentId = maxAgentMessageId( state.messages );
|
||
state.slowReplyVisible = false;
|
||
scheduleSlowReplyChecks();
|
||
}
|
||
|
||
function businessHoursAjaxEnabled() {
|
||
var bh = cfg.businessHours;
|
||
return !!( bh && ( bh.enabled === true || bh.enabled === 1 || bh.enabled === '1' ) );
|
||
}
|
||
|
||
function fetchBusinessHoursStatusParsed() {
|
||
if ( ! businessHoursAjaxEnabled() ) {
|
||
return Promise.resolve( { outside_hours: false, message: '' } );
|
||
}
|
||
var bhc = cfg.businessHours;
|
||
var fd = new FormData();
|
||
fd.append( 'action', bhc.action );
|
||
fd.append( 'nonce', bhc.nonce );
|
||
return fetch( bhc.ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' } )
|
||
.then( function ( res ) {
|
||
return res.json();
|
||
} )
|
||
.then( function ( j ) {
|
||
if ( j && j.success && j.data ) {
|
||
return {
|
||
outside_hours: !! j.data.outside_hours,
|
||
message: j.data.message != null ? String( j.data.message ) : ''
|
||
};
|
||
}
|
||
return { outside_hours: false, message: '' };
|
||
} )
|
||
.catch( function () {
|
||
return { outside_hours: false, message: '' };
|
||
} );
|
||
}
|
||
|
||
function afterVisitorOutboundSuccess() {
|
||
baselineSlowReplyAfterVisitorSend();
|
||
if ( ! businessHoursAjaxEnabled() ) {
|
||
return;
|
||
}
|
||
fetchBusinessHoursStatusParsed().then( function ( r ) {
|
||
if ( r.outside_hours && r.message ) {
|
||
state.businessHoursSendAck = String( r.message ).replace( /^\s+|\s+$/g, '' );
|
||
} else {
|
||
state.businessHoursSendAck = '';
|
||
}
|
||
renderMessages();
|
||
} );
|
||
}
|
||
|
||
function refreshBusinessHoursStickyForPanel() {
|
||
if ( ! businessHoursAjaxEnabled() ) {
|
||
return;
|
||
}
|
||
fetchBusinessHoursStatusParsed().then( function ( r ) {
|
||
if ( r.outside_hours && r.message ) {
|
||
state.businessHoursSticky = String( r.message ).replace( /^\s+|\s+$/g, '' );
|
||
} else {
|
||
state.businessHoursSticky = '';
|
||
}
|
||
renderMessages();
|
||
} );
|
||
}
|
||
|
||
function bumpSlowReplyIfAgentAnswered() {
|
||
var ax = maxAgentMessageId( state.messages );
|
||
if ( ax > +( state.slowReplyBaselineAgentId || 0 ) ) {
|
||
state.slowReplyVisible = false;
|
||
if ( state.slowReplyTimer ) {
|
||
clearTimeout( state.slowReplyTimer );
|
||
state.slowReplyTimer = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
function maybeShowAgentAttendanceNotice( tpl, actorNameRaw ) {
|
||
var nm = resolveOperatorPresenceName( actorNameRaw );
|
||
var t = tpl != null ? String( tpl ) : '';
|
||
if ( ! nm || ! t ) {
|
||
return;
|
||
}
|
||
if ( state.agentAttendanceNoticeShown ) {
|
||
return;
|
||
}
|
||
state.agentAttendanceNoticeShown = true;
|
||
state.agentJoinBannerText = t.replace( /\%s/g, nm );
|
||
renderMessages();
|
||
}
|
||
|
||
/** همگامسازی نشانی صفحهٔ فعلی با سرور (SPA: pushState/popstate/hashchange + interval). */
|
||
var visitorPageUrlHook = {
|
||
installed: false,
|
||
lastSent: '',
|
||
interval: null,
|
||
pushState: null,
|
||
replaceState: null
|
||
};
|
||
|
||
function scheduleVisitorPageUrlReport() {
|
||
if ( state.pageUrlReportTimer ) {
|
||
clearTimeout( state.pageUrlReportTimer );
|
||
}
|
||
state.pageUrlReportTimer = setTimeout( function () {
|
||
state.pageUrlReportTimer = null;
|
||
reportVisitorPageUrlIfChanged();
|
||
}, 380 );
|
||
}
|
||
|
||
function reportVisitorPageUrlIfChanged() {
|
||
if ( ! state.session || typeof window === 'undefined' || typeof location === 'undefined' ) {
|
||
return;
|
||
}
|
||
var href = String( location.href || '' );
|
||
if ( ! href || href === visitorPageUrlHook.lastSent ) {
|
||
return;
|
||
}
|
||
visitorPageUrlHook.lastSent = href;
|
||
var cid = state.session.conversation_id;
|
||
var tok = state.session.visitor_token;
|
||
patchVisitorCurrentPage( cid, tok, href ).catch( function ( e ) {
|
||
visitorPageUrlHook.lastSent = '';
|
||
chatLogV( 'current-page PATCH', e && e.message );
|
||
} );
|
||
}
|
||
|
||
function installVisitorPageUrlTracking() {
|
||
if ( typeof window === 'undefined' ) {
|
||
return;
|
||
}
|
||
if ( ! visitorPageUrlHook.installed ) {
|
||
window.addEventListener( 'popstate', scheduleVisitorPageUrlReport );
|
||
window.addEventListener( 'hashchange', scheduleVisitorPageUrlReport );
|
||
visitorPageUrlHook.pushState = history.pushState;
|
||
visitorPageUrlHook.replaceState = history.replaceState;
|
||
history.pushState = function () {
|
||
var r = visitorPageUrlHook.pushState.apply( history, arguments );
|
||
scheduleVisitorPageUrlReport();
|
||
return r;
|
||
};
|
||
history.replaceState = function () {
|
||
var r = visitorPageUrlHook.replaceState.apply( history, arguments );
|
||
scheduleVisitorPageUrlReport();
|
||
return r;
|
||
};
|
||
visitorPageUrlHook.installed = true;
|
||
}
|
||
visitorPageUrlHook.lastSent = '';
|
||
if ( ! visitorPageUrlHook.interval ) {
|
||
visitorPageUrlHook.interval = window.setInterval( scheduleVisitorPageUrlReport, 3200 );
|
||
}
|
||
scheduleVisitorPageUrlReport();
|
||
}
|
||
|
||
function teardownVisitorPageUrlTracking() {
|
||
if ( typeof window === 'undefined' ) {
|
||
return;
|
||
}
|
||
if ( visitorPageUrlHook.interval ) {
|
||
window.clearInterval( visitorPageUrlHook.interval );
|
||
visitorPageUrlHook.interval = null;
|
||
}
|
||
if ( state.pageUrlReportTimer ) {
|
||
clearTimeout( state.pageUrlReportTimer );
|
||
state.pageUrlReportTimer = null;
|
||
}
|
||
if ( visitorPageUrlHook.installed ) {
|
||
window.removeEventListener( 'popstate', scheduleVisitorPageUrlReport );
|
||
window.removeEventListener( 'hashchange', scheduleVisitorPageUrlReport );
|
||
if ( visitorPageUrlHook.pushState ) {
|
||
history.pushState = visitorPageUrlHook.pushState;
|
||
visitorPageUrlHook.pushState = null;
|
||
}
|
||
if ( visitorPageUrlHook.replaceState ) {
|
||
history.replaceState = visitorPageUrlHook.replaceState;
|
||
visitorPageUrlHook.replaceState = null;
|
||
}
|
||
visitorPageUrlHook.installed = false;
|
||
}
|
||
visitorPageUrlHook.lastSent = '';
|
||
}
|
||
|
||
var launcherAttentionTimer = null;
|
||
|
||
function stopPoll() {
|
||
if ( state.pollTimer ) {
|
||
clearInterval( state.pollTimer );
|
||
state.pollTimer = null;
|
||
}
|
||
}
|
||
|
||
function startPoll( convId, token ) {
|
||
stopPoll();
|
||
state.pollTimer = setInterval( function () {
|
||
listMessages( convId, token, 100 ).then( function ( items ) {
|
||
maybePlayAgentReplySound( items );
|
||
var prevMax = maxAgentMessageId( state.messages );
|
||
var nextMax = maxAgentMessageId( items );
|
||
var changed = items.length !== state.messages.length || nextMax !== prevMax;
|
||
if ( changed ) {
|
||
state.messages = items;
|
||
bumpSlowReplyIfAgentAnswered();
|
||
renderMessages();
|
||
syncMarkAgentRead( items );
|
||
}
|
||
} ).catch( function () {} );
|
||
}, 4000 );
|
||
}
|
||
|
||
var host = document.getElementById( 'hesabix-chat-host' );
|
||
if ( ! host ) {
|
||
chatWarn( 'عنصر DOM با id برابر hesabix-chat-host یافت نشد. در حالت shortcode باید شورتکد [hesabix_chat] در همان صفحه باشد؛ در بیلدر ممکن است asset لود نشود. در load_mode=global باید فوتر قالب همان id را چاپ کند.' );
|
||
return;
|
||
}
|
||
|
||
var surface;
|
||
var poweredByEl;
|
||
|
||
function syncPoweredByVisibility() {
|
||
if ( ! poweredByEl || ! surface ) {
|
||
return;
|
||
}
|
||
if ( surface.classList.contains( 'hesabix-chat--step-form' ) ) {
|
||
poweredByEl.classList.remove( 'hesabix-chat--hidden' );
|
||
} else {
|
||
poweredByEl.classList.add( 'hesabix-chat--hidden' );
|
||
}
|
||
}
|
||
|
||
/* نوع تعریف function داخل بلاک try در حالت strict فقط محدود به همان بلاک است؛
|
||
تابعهای ارسال کامپوزر بیرون از try به setComposerSending نیاز دارند → تخصیص به var بیرونی */
|
||
var setComposerSending;
|
||
var setComposerFileInputsDisabled;
|
||
|
||
try {
|
||
function applyTplClassCsvToElement( el, csv ) {
|
||
if ( ! el || csv == null ) {
|
||
return;
|
||
}
|
||
var pts = String( csv ).split( /[\s,]+/ );
|
||
for ( var pi = 0; pi < pts.length; pi++ ) {
|
||
var tk = pts[ pi ].replace( /^\s+|\s+$/g, '' );
|
||
if ( ! tk ) {
|
||
continue;
|
||
}
|
||
try {
|
||
el.classList.add( tk );
|
||
} catch ( tcx ) {}
|
||
}
|
||
}
|
||
|
||
var tplCx = cfg.tplExtraClasses || {};
|
||
var root = document.createElement( 'div' );
|
||
var uiPreset = ( cfg.uiPreset && cfg.uiPreset.toString() ) || 'default';
|
||
if ( [ 'default', 'minimal', 'colorful' ].indexOf( uiPreset ) < 0 ) {
|
||
uiPreset = 'default';
|
||
}
|
||
var themeKey = ( cfg.theme && cfg.theme.toString() ) || 'light';
|
||
var themeClasses;
|
||
if ( themeKey === 'cream' ) {
|
||
themeClasses = [ 'hesabix-chat--theme-light', 'hesabix-chat--theme-cream' ];
|
||
} else if ( themeKey === 'ocean' ) {
|
||
themeClasses = [ 'hesabix-chat--theme-light', 'hesabix-chat--theme-ocean' ];
|
||
} else if ( themeKey === 'midnight' ) {
|
||
themeClasses = [ 'hesabix-chat--theme-dark', 'hesabix-chat--theme-midnight' ];
|
||
} else if ( themeKey === 'dark' ) {
|
||
themeClasses = [ 'hesabix-chat--theme-dark' ];
|
||
} else {
|
||
themeClasses = [ 'hesabix-chat--theme-light' ];
|
||
}
|
||
root.className = 'hesabix-chat-root ' + themeClasses.join( ' ' ) + ' hesabix-chat--preset-' + uiPreset;
|
||
root.setAttribute( 'dir', cfg.dir || 'rtl' );
|
||
root.style.setProperty( '--hesabix-btn', cfg.buttonColor );
|
||
root.style.setProperty( '--hesabix-btn-txt', cfg.buttonTextColor );
|
||
root.style.setProperty( '--hesabix-panel-w', ( cfg.panelWidth || 380 ) + 'px' );
|
||
root.style.setProperty( '--hesabix-panel-h', ( cfg.panelHeight || 520 ) + 'px' );
|
||
root.style.setProperty( '--hesabix-z', String( cfg.zIndex || 99999 ) );
|
||
root.style.setProperty( '--hesabix-bottom', ( cfg.offsetBottom || 24 ) + 'px' );
|
||
var sideDesk = cfg.offsetSideDesktop != null ? cfg.offsetSideDesktop : ( cfg.offsetSide != null ? cfg.offsetSide : 24 );
|
||
var sideMob = cfg.offsetSideMobile != null ? cfg.offsetSideMobile : sideDesk;
|
||
root.style.setProperty( '--hesabix-side', sideDesk + 'px' );
|
||
root.style.setProperty( '--hesabix-side-mobile', sideMob + 'px' );
|
||
root.style.setProperty( '--hesabix-margin-left', ( cfg.marginLeftDesktop || 0 ) + 'px' );
|
||
root.style.setProperty( '--hesabix-margin-right', ( cfg.marginRightDesktop || 0 ) + 'px' );
|
||
root.style.setProperty( '--hesabix-margin-left-mobile', ( cfg.marginLeftMobile || 0 ) + 'px' );
|
||
root.style.setProperty( '--hesabix-margin-right-mobile', ( cfg.marginRightMobile || 0 ) + 'px' );
|
||
root.style.setProperty( '--hesabix-radius', ( cfg.borderRadius || 12 ) + 'px' );
|
||
root.style.setProperty( '--hesabix-accent', cfg.buttonColor );
|
||
|
||
applyTplClassCsvToElement( root, tplCx.root );
|
||
|
||
var pos = posClass( cfg.buttonPosition );
|
||
|
||
var wrap = document.createElement( 'div' );
|
||
wrap.className = 'hesabix-chat-floating ' + pos;
|
||
applyTplClassCsvToElement( wrap, tplCx.launcherWrap );
|
||
|
||
var btn = document.createElement( 'button' );
|
||
btn.type = 'button';
|
||
btn.className = 'hesabix-chat-launcher';
|
||
btn.setAttribute( 'aria-expanded', 'false' );
|
||
btn.setAttribute( 'aria-haspopup', 'dialog' );
|
||
btn.style.backgroundColor = cfg.buttonColor;
|
||
btn.style.color = cfg.buttonTextColor;
|
||
btn.insertAdjacentHTML(
|
||
'afterbegin',
|
||
'<span class="hesabix-chat-launcher-ico" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4 4h11a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H8l-4 3.5V7a3 3 0 0 1 3-3Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M8 8.5h7M8 12h4.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>'
|
||
);
|
||
var launcherLab = document.createElement( 'span' );
|
||
launcherLab.className = 'hesabix-chat-launcher-txt';
|
||
launcherLab.textContent = cfg.buttonText;
|
||
btn.appendChild( launcherLab );
|
||
applyTplClassCsvToElement( btn, tplCx.launcher );
|
||
|
||
var panel = document.createElement( 'div' );
|
||
panel.className = 'hesabix-chat-panel hesabix-chat--hidden ' + pos;
|
||
panel.setAttribute( 'role', 'dialog' );
|
||
panel.setAttribute( 'aria-modal', 'true' );
|
||
panel.setAttribute( 'aria-label', cfg.chatTitle );
|
||
applyTplClassCsvToElement( panel, tplCx.panel );
|
||
|
||
surface = document.createElement( 'div' );
|
||
surface.className = 'hesabix-chat-surface hesabix-chat-surface--' + uiPreset;
|
||
applyTplClassCsvToElement( surface, tplCx.surface );
|
||
|
||
var header = document.createElement( 'div' );
|
||
header.className = 'hesabix-chat-header';
|
||
var htitle = document.createElement( 'h2' );
|
||
htitle.textContent = cfg.chatTitle;
|
||
var hBrand = document.createElement( 'div' );
|
||
hBrand.className = 'hesabix-chat-header-brand';
|
||
if ( cfg.headerLogoUrl && cfg.headerLogoUrl.toString().length ) {
|
||
var hImg = document.createElement( 'img' );
|
||
hImg.className = 'hesabix-chat-header-logo';
|
||
hImg.src = cfg.headerLogoUrl;
|
||
hImg.alt = '';
|
||
hImg.setAttribute( 'width', '36' );
|
||
hImg.setAttribute( 'height', '36' );
|
||
hImg.decoding = 'async';
|
||
hImg.loading = 'lazy';
|
||
hBrand.appendChild( hImg );
|
||
}
|
||
hBrand.appendChild( htitle );
|
||
var hact = document.createElement( 'div' );
|
||
hact.className = 'hesabix-chat-header-actions';
|
||
var wsConnLabel = document.createElement( 'span' );
|
||
wsConnLabel.className = 'hesabix-chat-ws-conn';
|
||
wsConnLabel.setAttribute( 'role', 'status' );
|
||
var btnNew = document.createElement( 'button' );
|
||
btnNew.type = 'button';
|
||
btnNew.textContent = cfg.strings.back;
|
||
btnNew.title = cfg.strings.newChatHint;
|
||
var btnClose = document.createElement( 'button' );
|
||
btnClose.type = 'button';
|
||
btnClose.className = 'hesabix-chat-header-close';
|
||
btnClose.setAttribute( 'aria-label', cfg.strings.close || '' );
|
||
btnClose.setAttribute(
|
||
'title',
|
||
( cfg.strings.closeTooltip && String( cfg.strings.closeTooltip ) ) || ( cfg.strings.close || '' )
|
||
);
|
||
btnClose.innerHTML =
|
||
'<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6L6 18M6 6l12 12"/></svg>';
|
||
hact.appendChild( wsConnLabel );
|
||
hact.appendChild( btnNew );
|
||
hact.appendChild( btnClose );
|
||
header.appendChild( hBrand );
|
||
header.appendChild( hact );
|
||
|
||
var threadBar = document.createElement( 'div' );
|
||
threadBar.className = 'hesabix-chat-thread-bar hesabix-chat--hidden';
|
||
var threadBarInner = document.createElement( 'div' );
|
||
threadBarInner.className = 'hesabix-chat-thread-inner';
|
||
var threadBarLbl = document.createElement( 'span' );
|
||
threadBarLbl.className = 'hesabix-chat-thread-label';
|
||
threadBarLbl.textContent =
|
||
( cfg.strings && cfg.strings.threadPickerLabel ) ? String( cfg.strings.threadPickerLabel ) : '';
|
||
var threadSel = document.createElement( 'select' );
|
||
threadSel.className = 'hesabix-chat-thread-select';
|
||
threadSel.setAttribute(
|
||
'aria-label',
|
||
( cfg.strings && cfg.strings.threadPickerLabel ) ? String( cfg.strings.threadPickerLabel ) : 'Conversation'
|
||
);
|
||
threadBarInner.appendChild( threadBarLbl );
|
||
threadBarInner.appendChild( threadSel );
|
||
threadBar.appendChild( threadBarInner );
|
||
|
||
var msgBox = document.createElement( 'div' );
|
||
msgBox.className = 'hesabix-chat-messages';
|
||
|
||
function esc( t ) {
|
||
var d = document.createElement( 'div' );
|
||
d.textContent = t;
|
||
return d.innerHTML;
|
||
}
|
||
|
||
var formEl = document.createElement( 'form' );
|
||
formEl.className = 'hesabix-chat-form';
|
||
var emailMode = ( cfg.emailField || 'required' ).toString();
|
||
var showPageCtx = cfg.showPageContext === true || cfg.showPageContext === 1 || cfg.showPageContext === '1';
|
||
var pageTitle = ( typeof document !== 'undefined' && document.title ) ? document.title : '';
|
||
var pageHref = ( typeof window !== 'undefined' && window.location && window.location.href ) ? window.location.href : '';
|
||
var pageCtxBlock = '';
|
||
if ( showPageCtx ) {
|
||
pageCtxBlock =
|
||
'<div class="hesabix-chat-page-ctx" role="note">' +
|
||
'<div class="hesabix-chat-page-ctx-row"><span class="hesabix-chat-page-ctx-l">' + esc( ( cfg.strings && cfg.strings.pageContextLabel ) || '' ) + ': </span>' +
|
||
'<a class="hesabix-chat-page-ctx-a" href="' + esc( pageHref ) + '" target="_blank" rel="noopener noreferrer">' + esc( pageTitle || pageHref ) + '</a></div>' +
|
||
'<div class="hesabix-chat-page-ctx-url" title="' + esc( pageHref ) + '">' + esc( pageHref ) + '</div></div>';
|
||
}
|
||
var emailPart = '';
|
||
if ( emailMode === 'hidden' ) {
|
||
emailPart = '<input type="hidden" name="email" value="" />';
|
||
} else {
|
||
var emReq = ( emailMode === 'required' ) ? ' required' : '';
|
||
var emHint = '';
|
||
if ( emailMode === 'optional' && cfg.strings && cfg.strings.emailOptionalHint ) {
|
||
emHint = '<p class="hesabix-chat-fld-hint hesabix-chat-fld-hint--email">' + esc( cfg.strings.emailOptionalHint ) + '</p>';
|
||
}
|
||
emailPart =
|
||
'<label class="hesabix-chat-fld"><span class="hesabix-chat-fld-l">' + esc( cfg.strings.email ) + '</span>' +
|
||
'<input type="email" name="email" autocomplete="email"' + emReq + ' /></label>' + emHint;
|
||
}
|
||
formEl.innerHTML =
|
||
'<div class="hesabix-chat-form-lead"><h3 class="hesabix-chat-form-title">' + esc( cfg.strings.formTitle || '' ) + '</h3>' +
|
||
'<p class="hesabix-chat-form-hint">' + esc( cfg.strings.formSubtitle || '' ) + '</p></div>' +
|
||
pageCtxBlock +
|
||
'<div class="hesabix-chat-error hesabix-chat--hidden" data-err></div>' +
|
||
'<div class="hesabix-chat-form-fields">' +
|
||
'<label class="hesabix-chat-fld"><span class="hesabix-chat-fld-l">' + esc( cfg.strings.firstName ) + '</span><input type="text" name="first_name" required autocomplete="given-name" /></label>' +
|
||
'<label class="hesabix-chat-fld"><span class="hesabix-chat-fld-l">' + esc( cfg.strings.lastName ) + '</span><input type="text" name="last_name" required autocomplete="family-name" /></label>' +
|
||
emailPart +
|
||
'<label class="hesabix-chat-fld"><span class="hesabix-chat-fld-l">' + esc( cfg.strings.phone ) + '</span><input type="tel" name="phone" required autocomplete="tel" /></label></div>' +
|
||
'<button type="submit" class="hesabix-chat-form-submit">' + esc( cfg.strings.start ) + '</button>';
|
||
|
||
poweredByEl = null;
|
||
( function buildPoweredFooter() {
|
||
if ( ! ( cfg.showPoweredByHesabix === true || cfg.showPoweredByHesabix === 1 || cfg.showPoweredByHesabix === '1' ) ) {
|
||
return;
|
||
}
|
||
var pUrlRaw = cfg.poweredByHesabixUrl != null ? String( cfg.poweredByHesabixUrl ) : '';
|
||
var pLblRaw = cfg.poweredByHesabixText != null ? String( cfg.poweredByHesabixText ) : '';
|
||
var pUrl = pUrlRaw.replace( /^\s+|\s+$/g, '' );
|
||
var pLbl = pLblRaw.replace( /^\s+|\s+$/g, '' );
|
||
if ( ! pUrl || ! /^https?:\/\//i.test( pUrl ) ) {
|
||
return;
|
||
}
|
||
if ( ! pLbl ) {
|
||
return;
|
||
}
|
||
try {
|
||
var u_chk = document.createElement( 'a' );
|
||
u_chk.href = pUrl;
|
||
if ( ! u_chk.host ) {
|
||
return;
|
||
}
|
||
} catch ( ePw0 ) {
|
||
return;
|
||
}
|
||
var wrap = document.createElement( 'p' );
|
||
wrap.className = 'hesabix-chat-powered-by';
|
||
var lk = document.createElement( 'a' );
|
||
lk.href = pUrl;
|
||
lk.rel = 'noopener noreferrer sponsored';
|
||
lk.target = '_blank';
|
||
lk.textContent = pLbl;
|
||
wrap.appendChild( lk );
|
||
poweredByEl = wrap;
|
||
formEl.appendChild( poweredByEl );
|
||
} )();
|
||
|
||
var comp = document.createElement( 'div' );
|
||
comp.className = 'hesabix-chat-composer hesabix-chat--hidden';
|
||
var errC = document.createElement( 'div' );
|
||
errC.className = 'hesabix-chat-error hesabix-chat--hidden';
|
||
var row = document.createElement( 'div' );
|
||
row.className = 'hesabix-chat-composer-row';
|
||
var ta = document.createElement( 'textarea' );
|
||
ta.className = 'hesabix-chat-composer-input';
|
||
ta.rows = 2;
|
||
ta.placeholder = cfg.strings.placeholder;
|
||
var sendBtn = document.createElement( 'button' );
|
||
sendBtn.type = 'button';
|
||
sendBtn.className = 'hesabix-chat-send';
|
||
var sendLabel = ( cfg.strings && cfg.strings.send ) ? String( cfg.strings.send ) : '';
|
||
var sendTip = ( cfg.strings && cfg.strings.sendTooltip ) ? String( cfg.strings.sendTooltip ) : sendLabel;
|
||
var sendingAria = cfg.strings && cfg.strings.sending ? String( cfg.strings.sending ) : sendLabel;
|
||
sendBtn.setAttribute( 'aria-label', sendLabel );
|
||
sendBtn.setAttribute( 'title', sendTip );
|
||
var sendBtnIconSvg =
|
||
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' +
|
||
'<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>';
|
||
var sendBtnSpinnerSvg =
|
||
'<svg class="hesabix-chat-send-spinner" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true" focusable="false">' +
|
||
'<path d="M12 3a9 9 0 1 1 0 18" /></svg>';
|
||
sendBtn.innerHTML = sendBtnIconSvg;
|
||
|
||
var composerSendInFlight = false;
|
||
|
||
setComposerFileInputsDisabled = function ( dis ) {
|
||
try {
|
||
var q = comp.querySelectorAll(
|
||
'input[type="file"].hesabix-chat-attach, input[type="file"].hesabix-chat-voice-file'
|
||
);
|
||
for ( var qi = 0; qi < q.length; qi++ ) {
|
||
q[ qi ].disabled = !! dis;
|
||
}
|
||
} catch ( fi0 ) {}
|
||
};
|
||
|
||
setComposerSending = function ( busy ) {
|
||
busy = !! busy;
|
||
composerSendInFlight = busy;
|
||
comp.classList.toggle( 'hesabix-chat-composer--sending', busy );
|
||
try {
|
||
ta.disabled = busy;
|
||
} catch ( eTa ) {}
|
||
sendBtn.disabled = busy;
|
||
sendBtn.setAttribute( 'aria-busy', busy ? 'true' : 'false' );
|
||
sendBtn.classList.toggle( 'hesabix-chat-send--busy', busy );
|
||
if ( busy ) {
|
||
sendBtn.innerHTML = sendBtnSpinnerSvg;
|
||
sendBtn.setAttribute( 'aria-label', sendingAria );
|
||
sendBtn.setAttribute( 'title', sendingAria );
|
||
} else {
|
||
sendBtn.innerHTML = sendBtnIconSvg;
|
||
sendBtn.setAttribute( 'aria-label', sendLabel );
|
||
sendBtn.setAttribute( 'title', sendTip );
|
||
}
|
||
setComposerFileInputsDisabled( busy );
|
||
};
|
||
|
||
row.appendChild( ta );
|
||
row.appendChild( sendBtn );
|
||
comp.appendChild( errC );
|
||
comp.appendChild( row );
|
||
|
||
var peerTypingEl = document.createElement( 'div' );
|
||
peerTypingEl.className = 'hesabix-chat-peer-typing hesabix-chat--hidden';
|
||
peerTypingEl.setAttribute( 'role', 'status' );
|
||
peerTypingEl.setAttribute( 'aria-live', 'polite' );
|
||
|
||
surface.appendChild( header );
|
||
surface.appendChild( threadBar );
|
||
surface.appendChild( formEl );
|
||
surface.appendChild( msgBox );
|
||
surface.appendChild( peerTypingEl );
|
||
surface.appendChild( comp );
|
||
panel.appendChild( surface );
|
||
surface.classList.add( 'hesabix-chat--step-form' );
|
||
syncPoweredByVisibility();
|
||
wrap.appendChild( btn );
|
||
wrap.appendChild( panel );
|
||
/* پرتال پنل: در حالت global دکمه fixed است و پنل absolute نسبت به host با ارتفاع صفر اشتباه جا میگرفت؛ پنل در body. */
|
||
{
|
||
var hesabixChatVars = [
|
||
'--hesabix-btn', '--hesabix-btn-txt', '--hesabix-panel-w', '--hesabix-panel-h', '--hesabix-z', '--hesabix-bottom',
|
||
'--hesabix-side', '--hesabix-side-mobile', '--hesabix-margin-left', '--hesabix-margin-right',
|
||
'--hesabix-margin-left-mobile', '--hesabix-margin-right-mobile', '--hesabix-radius', '--hesabix-accent'
|
||
];
|
||
for ( var vi = 0; vi < hesabixChatVars.length; vi++ ) {
|
||
var vnm = hesabixChatVars[ vi ];
|
||
panel.style.setProperty( vnm, root.style.getPropertyValue( vnm ) );
|
||
}
|
||
panel.setAttribute( 'dir', root.getAttribute( 'dir' ) || 'rtl' );
|
||
for ( var ti = 0; ti < themeClasses.length; ti++ ) {
|
||
panel.classList.add( themeClasses[ ti ] );
|
||
}
|
||
panel.classList.add( 'hesabix-chat--preset-' + uiPreset, 'hesabix-chat-panel--portaled' );
|
||
wrap.removeChild( panel );
|
||
document.body.appendChild( panel );
|
||
}
|
||
root.appendChild( wrap );
|
||
host.appendChild( root );
|
||
|
||
function setupVisitorFileUpload() {
|
||
var wantsFileUi = !!( cfg.showFileUpload );
|
||
var wantsVoiceUi = !!( cfg.showVoiceMessage );
|
||
if ( ! wantsFileUi && ! wantsVoiceUi ) {
|
||
return;
|
||
}
|
||
var base = ( cfg.apiBase || '' ).replace( /\/$/, '' );
|
||
if ( ! base || ! cfg.publicKey ) {
|
||
return;
|
||
}
|
||
var optUrl = base + '/api/v1/public/crm-chat/widget-options?public_key=' + encodeURIComponent( cfg.publicKey );
|
||
var runFetch = function () {
|
||
fetch( optUrl, { method: 'GET', credentials: 'omit' } )
|
||
.then( function ( res ) {
|
||
return res.text().then( function ( t ) {
|
||
return { ok: res.ok, t: t };
|
||
} );
|
||
} )
|
||
.then( function ( pack ) {
|
||
var j = parseJsonSafe( pack.t );
|
||
if ( ! pack.ok || ! j ) {
|
||
return;
|
||
}
|
||
var wrapD = j.data !== undefined && j.data !== null ? j.data : j;
|
||
if ( ! wrapD ) {
|
||
return;
|
||
}
|
||
state.visitorOpts.allowFile = !! wrapD.allow_file_upload;
|
||
state.visitorOpts.allowVoice = !! wrapD.allow_voice;
|
||
var effFile = !!( wantsFileUi && state.visitorOpts.allowFile );
|
||
var effVoice = !!( wantsVoiceUi && state.visitorOpts.allowVoice );
|
||
if ( effFile ) {
|
||
var fileRow = document.createElement( 'div' );
|
||
fileRow.className = 'hesabix-chat-file-row';
|
||
var finp = document.createElement( 'input' );
|
||
finp.type = 'file';
|
||
finp.className = 'hesabix-chat-attach';
|
||
finp.setAttribute( 'aria-label', ( cfg.strings && cfg.strings.attach ) || '' );
|
||
fileRow.appendChild( finp );
|
||
comp.appendChild( fileRow );
|
||
finp.addEventListener( 'change', function ( ev ) {
|
||
var f = ev.target && ev.target.files && ev.target.files[0];
|
||
if ( ! f || ! state.session ) {
|
||
return;
|
||
}
|
||
if ( composerSendInFlight ) {
|
||
ev.target.value = '';
|
||
return;
|
||
}
|
||
showFormError( errC, '' );
|
||
setComposerSending( true );
|
||
postFile( state.session.conversation_id, state.session.visitor_token, f, '' )
|
||
.then( function () {
|
||
ev.target.value = '';
|
||
return refreshMessages().then( function () {
|
||
afterVisitorOutboundSuccess();
|
||
} );
|
||
} )
|
||
.catch( function ( e ) {
|
||
showFormError( errC, e.message || cfg.strings.errorGeneric );
|
||
} )
|
||
.finally( function () {
|
||
setComposerSending( false );
|
||
} );
|
||
} );
|
||
}
|
||
if ( effVoice ) {
|
||
var micRow = document.createElement( 'div' );
|
||
micRow.className = 'hesabix-chat-voice-row';
|
||
var vin = document.createElement( 'input' );
|
||
vin.type = 'file';
|
||
vin.accept = 'audio/*';
|
||
try {
|
||
vin.capture = 'user';
|
||
} catch ( ig ) {}
|
||
vin.className = 'hesabix-chat-voice-file';
|
||
vin.setAttribute( 'aria-label', ( cfg.strings && cfg.strings.voicePick ) ? String( cfg.strings.voicePick ) : 'Voice' );
|
||
micRow.appendChild( vin );
|
||
comp.appendChild( micRow );
|
||
vin.addEventListener( 'change', function ( ev ) {
|
||
var f = ev.target && ev.target.files && ev.target.files[0];
|
||
if ( ! f || ! state.session ) {
|
||
return;
|
||
}
|
||
if ( composerSendInFlight ) {
|
||
ev.target.value = '';
|
||
return;
|
||
}
|
||
showFormError( errC, '' );
|
||
setComposerSending( true );
|
||
postFile( state.session.conversation_id, state.session.visitor_token, f, '' )
|
||
.then( function () {
|
||
ev.target.value = '';
|
||
return refreshMessages().then( function () {
|
||
afterVisitorOutboundSuccess();
|
||
} );
|
||
} )
|
||
.catch( function ( e ) {
|
||
showFormError( errC, e.message || cfg.strings.errorGeneric );
|
||
} )
|
||
.finally( function () {
|
||
setComposerSending( false );
|
||
} );
|
||
} );
|
||
}
|
||
if ( effFile || effVoice ) {
|
||
comp.classList.add( 'hesabix-chat-composer--dropzone' );
|
||
if ( ta._hesabixPasteBound !== true ) {
|
||
ta._hesabixPasteBound = true;
|
||
ta.addEventListener( 'paste', function ( pe ) {
|
||
if ( ! effFile || ! state.session ) {
|
||
return;
|
||
}
|
||
if ( composerSendInFlight ) {
|
||
return;
|
||
}
|
||
var dt = pe.clipboardData || window.clipboardData;
|
||
if ( ! dt || ! dt.files || ! dt.files.length ) {
|
||
return;
|
||
}
|
||
var pf = dt.files[0];
|
||
if ( ! pf ) {
|
||
return;
|
||
}
|
||
pe.preventDefault();
|
||
showFormError( errC, '' );
|
||
setComposerSending( true );
|
||
postFile( state.session.conversation_id, state.session.visitor_token, pf, '' )
|
||
.then( function () {
|
||
return refreshMessages().then( function () {
|
||
afterVisitorOutboundSuccess();
|
||
} );
|
||
} )
|
||
.catch( function ( e ) {
|
||
showFormError( errC, e.message || cfg.strings.errorGeneric );
|
||
} )
|
||
.finally( function () {
|
||
setComposerSending( false );
|
||
} );
|
||
} );
|
||
ta.addEventListener( 'dragover', function ( de ) {
|
||
de.preventDefault();
|
||
comp.classList.add( 'hesabix-chat-dragover' );
|
||
} );
|
||
ta.addEventListener( 'dragleave', function () {
|
||
comp.classList.remove( 'hesabix-chat-dragover' );
|
||
} );
|
||
ta.addEventListener( 'drop', function ( de ) {
|
||
de.preventDefault();
|
||
comp.classList.remove( 'hesabix-chat-dragover' );
|
||
if ( ! state.session ) {
|
||
return;
|
||
}
|
||
if ( composerSendInFlight ) {
|
||
return;
|
||
}
|
||
var dt2 = de.dataTransfer;
|
||
if ( ! dt2 || ! dt2.files || ! dt2.files.length ) {
|
||
return;
|
||
}
|
||
var df = dt2.files[0];
|
||
if ( ! df ) {
|
||
return;
|
||
}
|
||
showFormError( errC, '' );
|
||
setComposerSending( true );
|
||
postFile( state.session.conversation_id, state.session.visitor_token, df, '' )
|
||
.then( function () {
|
||
return refreshMessages().then( function () {
|
||
afterVisitorOutboundSuccess();
|
||
} );
|
||
} )
|
||
.catch( function ( e ) {
|
||
showFormError( errC, e.message || cfg.strings.errorGeneric );
|
||
} )
|
||
.finally( function () {
|
||
setComposerSending( false );
|
||
} );
|
||
} );
|
||
}
|
||
}
|
||
} )
|
||
.catch( function () {} );
|
||
};
|
||
if ( typeof window.requestIdleCallback === 'function' ) {
|
||
window.requestIdleCallback( runFetch, { timeout: 4000 } );
|
||
} else {
|
||
setTimeout( runFetch, 1 );
|
||
}
|
||
}
|
||
setupVisitorFileUpload();
|
||
|
||
chatLogV( 'DOM آماده', {
|
||
hostId: host.id,
|
||
hostClass: host.className,
|
||
shortcode: host.classList.contains( 'hesabix-chat-host--shortcode' ),
|
||
portaled: panel.classList.contains( 'hesabix-chat-panel--portaled' ),
|
||
panelParent: panel.parentNode && panel.parentNode.nodeName
|
||
} );
|
||
chatLogV( 'نمونه', {
|
||
panelRect: ( typeof panel.getBoundingClientRect === 'function' ) ? panel.getBoundingClientRect() : null,
|
||
launcherInDom: document.body.contains( btn )
|
||
} );
|
||
} catch ( initEx ) {
|
||
chatWarn( 'خطا حین ساخت ویجت (قبل از اتصال رویدادها)', ( initEx && initEx.message ) || initEx, initEx );
|
||
return;
|
||
}
|
||
|
||
/* بیرون از try تا در strict mode همان scope تابعهای enterChatMode / afterSessionReady باشد */
|
||
var threadBarSwitching = false;
|
||
|
||
function buildThreadOptions() {
|
||
var opts = [];
|
||
var curId = state.session ? String( state.session.conversation_id ) : null;
|
||
if ( state.session && state.session.visitor_token ) {
|
||
var pfx = ( cfg.strings && cfg.strings.threadCurrentPrefix ) || '';
|
||
opts.push( {
|
||
conversation_id: state.session.conversation_id,
|
||
visitor_token: state.session.visitor_token,
|
||
label: pfx + ' #' + state.session.conversation_id,
|
||
saved_at: Date.now()
|
||
} );
|
||
}
|
||
var hist = loadThreadHistory();
|
||
for ( var hi = 0; hi < hist.length; hi++ ) {
|
||
var h = hist[ hi ];
|
||
if ( ! h || h.conversation_id == null || ! h.visitor_token ) {
|
||
continue;
|
||
}
|
||
if ( curId && String( h.conversation_id ) === curId ) {
|
||
continue;
|
||
}
|
||
var ts = h.saved_at ? new Date( h.saved_at ) : null;
|
||
var st =
|
||
ts && ! isNaN( ts.getTime() )
|
||
? ts.toLocaleDateString() +
|
||
' · ' +
|
||
ts.toLocaleTimeString( [], { hour: '2-digit', minute: '2-digit' } )
|
||
: '';
|
||
var lb = st ? '#' + h.conversation_id + ' · ' + st : '#' + h.conversation_id;
|
||
opts.push( {
|
||
conversation_id: h.conversation_id,
|
||
visitor_token: h.visitor_token,
|
||
label: lb,
|
||
saved_at: h.saved_at || 0
|
||
} );
|
||
}
|
||
return opts;
|
||
}
|
||
|
||
function refreshThreadBar() {
|
||
if ( ! threadBar || ! threadSel ) {
|
||
return;
|
||
}
|
||
var opts = buildThreadOptions();
|
||
if ( opts.length <= 1 ) {
|
||
threadBar.classList.add( 'hesabix-chat--hidden' );
|
||
return;
|
||
}
|
||
threadBar.classList.remove( 'hesabix-chat--hidden' );
|
||
var selId = state.session ? String( state.session.conversation_id ) : '';
|
||
threadSel.innerHTML = '';
|
||
for ( var oi = 0; oi < opts.length; oi++ ) {
|
||
var it = opts[ oi ];
|
||
var opt = document.createElement( 'option' );
|
||
opt.value = String( it.conversation_id );
|
||
opt.textContent = it.label;
|
||
opt.setAttribute( 'data-vtok', it.visitor_token );
|
||
if ( String( it.conversation_id ) === selId ) {
|
||
opt.selected = true;
|
||
}
|
||
threadSel.appendChild( opt );
|
||
}
|
||
}
|
||
|
||
threadSel.addEventListener( 'change', function () {
|
||
if ( threadBarSwitching ) {
|
||
return;
|
||
}
|
||
var opt = threadSel.options[ threadSel.selectedIndex ];
|
||
if ( ! opt ) {
|
||
return;
|
||
}
|
||
var vid = opt.value;
|
||
var vtk = opt.getAttribute( 'data-vtok' );
|
||
if ( ! vid || ! vtk ) {
|
||
return;
|
||
}
|
||
if ( state.session && String( state.session.conversation_id ) === vid ) {
|
||
return;
|
||
}
|
||
threadBarSwitching = true;
|
||
state.session = { conversation_id: +vid, visitor_token: vtk };
|
||
saveSession( +vid, vtk );
|
||
unbindRealtime();
|
||
afterSessionReady( false )
|
||
.then( function () {
|
||
threadBarSwitching = false;
|
||
refreshThreadBar();
|
||
} )
|
||
.catch( function () {
|
||
threadBarSwitching = false;
|
||
} );
|
||
} );
|
||
|
||
function showFormError( el, msg ) {
|
||
if ( ! el ) {
|
||
return;
|
||
}
|
||
if ( msg ) {
|
||
el.textContent = msg;
|
||
el.classList.remove( 'hesabix-chat--hidden' );
|
||
} else {
|
||
el.textContent = '';
|
||
el.classList.add( 'hesabix-chat--hidden' );
|
||
}
|
||
}
|
||
|
||
function ensureWsConnDot() {
|
||
var dot = wsConnLabel.querySelector( '.hesabix-chat-ws-dot' );
|
||
if ( ! dot ) {
|
||
dot = document.createElement( 'span' );
|
||
dot.className = 'hesabix-chat-ws-dot';
|
||
dot.setAttribute( 'aria-hidden', 'true' );
|
||
wsConnLabel.appendChild( dot );
|
||
}
|
||
return dot;
|
||
}
|
||
|
||
function updateWsConn( code ) {
|
||
var s = ( cfg.strings && cfg.strings ) || {};
|
||
var isRtl = ( ( cfg.dir || 'rtl' ) + '' ) === 'rtl';
|
||
if ( ! wsConnLabel ) {
|
||
return;
|
||
}
|
||
wsConnLabel.textContent = '';
|
||
ensureWsConnDot();
|
||
if ( code === 'connecting' ) {
|
||
wsConnLabel.className = 'hesabix-chat-ws-conn hesabix-chat-ws-conn--connecting';
|
||
var tCon = s.wsConnecting || ( isRtl ? 'در حال اتصال…' : 'Connecting…' );
|
||
wsConnLabel.title = tCon;
|
||
wsConnLabel.setAttribute( 'aria-label', tCon );
|
||
} else if ( code === 'live' ) {
|
||
wsConnLabel.className = 'hesabix-chat-ws-conn hesabix-chat-ws-conn--live';
|
||
var tLive = s.wsLiveHint || s.wsLive || ( isRtl ? 'اتصال لحظهای برقرار است' : 'Real-time link active' );
|
||
wsConnLabel.title = tLive;
|
||
wsConnLabel.setAttribute( 'aria-label', tLive );
|
||
} else {
|
||
wsConnLabel.className = 'hesabix-chat-ws-conn hesabix-chat-ws-conn--offline';
|
||
var tOff = s.wsOfflineHint || s.wsOffline || ( isRtl ? 'بهروزرسانی ممکن است با تأخیر باشد' : 'Polling; messages still work' );
|
||
wsConnLabel.title = tOff;
|
||
wsConnLabel.setAttribute( 'aria-label', tOff );
|
||
}
|
||
}
|
||
|
||
function updatePeerTypingUI() {
|
||
var s2 = ( cfg.strings && cfg.strings ) || {};
|
||
var isRtl2 = ( ( cfg.dir || 'rtl' ) + '' ) === 'rtl';
|
||
var fallback = s2.agentTyping || ( isRtl2 ? 'پشتیبان در حال تایپ…' : 'Support is typing…' );
|
||
var name = ( state.agentTypingName || '' ).trim();
|
||
var tpl = s2.agentTypingNamed;
|
||
var t = fallback;
|
||
if ( name && tpl ) {
|
||
t = tpl.replace( /\%s/g, name );
|
||
} else if ( name ) {
|
||
t = name + ( isRtl2 ? ' در حال تایپ است…' : ' is typing…' );
|
||
}
|
||
if ( state.agentTyping && peerTypingEl && t ) {
|
||
peerTypingEl.textContent = t;
|
||
peerTypingEl.classList.remove( 'hesabix-chat--hidden' );
|
||
} else if ( peerTypingEl ) {
|
||
peerTypingEl.classList.add( 'hesabix-chat--hidden' );
|
||
}
|
||
}
|
||
|
||
function applyMessagesReadFromEvent( msg ) {
|
||
if ( ! state.session || ! msg || ! sameConversationId( msg.conversation_id, state.session.conversation_id ) ) {
|
||
return;
|
||
}
|
||
var rr = String( msg.reader_role || '' ).toLowerCase().replace( /^\s+|\s+$/g, '' );
|
||
if ( rr !== 'agent' ) {
|
||
return;
|
||
}
|
||
var ids = msg.message_ids;
|
||
var at = msg.read_at;
|
||
if ( ! ids || ! ids.length ) {
|
||
return;
|
||
}
|
||
var idset = {};
|
||
ids.forEach( function ( id ) {
|
||
if ( id != null ) {
|
||
idset[ id ] = 1;
|
||
}
|
||
} );
|
||
var changed = false;
|
||
state.messages.forEach( function ( m ) {
|
||
var sr = String( m.sender_role || '' ).toLowerCase().replace( /^\s+|\s+$/g, '' );
|
||
if ( sr !== 'visitor' ) {
|
||
return;
|
||
}
|
||
if ( m && m.id != null && idset[ m.id ] && at != null && at !== '' ) {
|
||
m.read_at = at;
|
||
changed = true;
|
||
}
|
||
} );
|
||
if ( changed ) {
|
||
renderMessages();
|
||
}
|
||
}
|
||
|
||
function sendVisitorTyping( active ) {
|
||
if ( ! state.ws || state.ws.readyState !== 1 ) {
|
||
return;
|
||
}
|
||
try {
|
||
state.ws.send( JSON.stringify( { type: 'typing', active: Boolean( active ) } ) );
|
||
} catch ( e0 ) {}
|
||
}
|
||
|
||
function scheduleVisitorTypingStop() {
|
||
if ( state.visitorTypingStopTimer ) {
|
||
clearTimeout( state.visitorTypingStopTimer );
|
||
}
|
||
state.visitorTypingStopTimer = setTimeout( function () {
|
||
state.visitorTypingStopTimer = null;
|
||
sendVisitorTyping( false );
|
||
}, 800 );
|
||
}
|
||
|
||
function disconnectWs() {
|
||
if ( state.agentTypingTimer ) {
|
||
clearTimeout( state.agentTypingTimer );
|
||
state.agentTypingTimer = null;
|
||
}
|
||
try {
|
||
if ( state.ws ) {
|
||
state.ws.close();
|
||
}
|
||
} catch ( e1 ) {}
|
||
state.ws = null;
|
||
}
|
||
|
||
function connectWs( convId, token ) {
|
||
disconnectWs();
|
||
updateWsConn( 'connecting' );
|
||
var u = getWsUrl();
|
||
if ( ! u ) {
|
||
chatLogV( 'WebSocket: آدرس خالی (apiBase / پروتکل؟)' );
|
||
updateWsConn( 'offline' );
|
||
return;
|
||
}
|
||
var ws;
|
||
try {
|
||
ws = new WebSocket( u );
|
||
} catch ( e2 ) {
|
||
chatLogV( 'WebSocket: ساخت ناموفق', u, e2 );
|
||
updateWsConn( 'offline' );
|
||
return;
|
||
}
|
||
chatLogV( 'WebSocket: اتصال', u );
|
||
state.ws = ws;
|
||
ws.onopen = function () {
|
||
ws.send(
|
||
JSON.stringify( {
|
||
type: 'auth',
|
||
role: 'visitor',
|
||
visitor_token: token,
|
||
conversation_id: convId
|
||
} )
|
||
);
|
||
};
|
||
ws.onmessage = function ( ev ) {
|
||
var msg = parseJsonSafe( ev.data );
|
||
pushWidgetRemoteDebug( 'ws.in', msg != null ? msg : { raw: typeof ev.data === 'string' ? ev.data.slice( 0, 500 ) : '' } );
|
||
if ( ! msg ) {
|
||
return;
|
||
}
|
||
if ( msg.type === 'auth_ok' ) {
|
||
updateWsConn( 'live' );
|
||
pushWidgetRemoteDebug( 'ws.auth_ok', { conversation_hint: convId } );
|
||
return;
|
||
}
|
||
if ( msg.type === 'crm_chat.event' && msg.event === 'message.created' ) {
|
||
var created = msg.message;
|
||
try {
|
||
if (
|
||
state.session &&
|
||
created &&
|
||
sameConversationId( created.conversation_id, convId ) &&
|
||
String( created.sender_role || '' ).toLowerCase().replace( /^\s+|\s+$/g, '' ) === 'agent'
|
||
) {
|
||
maybeClearAgentTypingLive();
|
||
}
|
||
} catch ( eCre ) {}
|
||
refreshMessages().catch( function () {} );
|
||
return;
|
||
}
|
||
if ( msg.type === 'crm_chat.event' && msg.event === 'message.updated' ) {
|
||
refreshMessages().catch( function () {} );
|
||
return;
|
||
}
|
||
if ( msg.type === 'crm_chat.event' && msg.event === 'message.deleted' ) {
|
||
refreshMessages().catch( function () {} );
|
||
return;
|
||
}
|
||
if ( msg.type === 'crm_chat.event' && msg.event === 'conversation.deleted' ) {
|
||
if ( state.session && sameConversationId( msg.conversation_id, convId ) ) {
|
||
clearSession();
|
||
state.session = null;
|
||
enterFormMode();
|
||
}
|
||
return;
|
||
}
|
||
if ( msg.type === 'crm_chat.event' && msg.event === 'messages.read' ) {
|
||
applyMessagesReadFromEvent( msg );
|
||
if ( cfg.showAgentAttendanceOnRead ) {
|
||
if ( sameConversationId( msg.conversation_id, convId ) ) {
|
||
var rr = String( msg.reader_role || '' ).toLowerCase().replace( /^\s+|\s+$/g, '' );
|
||
var midr = msg.message_ids;
|
||
var readerDispCore = agentPresenceBubbleLabel(
|
||
msg.reader_display_name != null ? String( msg.reader_display_name ) : ''
|
||
);
|
||
var tplBaseR = cfg.strings && cfg.strings.agentJoinedNotice ? String( cfg.strings.agentJoinedNotice ) : '';
|
||
var tplR = noticeTplOrFallback( cfg.agentReadNoticeTemplate, tplBaseR || '%s به گفتگو پیوست' );
|
||
if ( rr === 'agent' && midr && midr.length && tplR && readerDispCore ) {
|
||
maybeShowAgentAttendanceNotice( tplR, msg.reader_display_name );
|
||
}
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
if ( msg.type === 'crm_chat.event' && msg.event === 'typing' ) {
|
||
pushWidgetRemoteDebug( 'evt.typing.recv', {
|
||
active: !! msg.active,
|
||
from_role: msg.from_role,
|
||
cid_live: convId,
|
||
cid_msg: msg.conversation_id,
|
||
ok: Boolean(
|
||
sameConversationId( msg.conversation_id, convId ) && wsFromRoleLooksAgent( msg.from_role )
|
||
)
|
||
} );
|
||
if ( sameConversationId( msg.conversation_id, convId ) && wsFromRoleLooksAgent( msg.from_role ) ) {
|
||
if ( state.agentTypingTimer ) {
|
||
clearTimeout( state.agentTypingTimer );
|
||
state.agentTypingTimer = null;
|
||
}
|
||
state.agentTyping = Boolean( msg.active );
|
||
state.agentTypingName = msg.active
|
||
? agentPresenceBubbleLabel(
|
||
( msg.actor_name && String( msg.actor_name ) ) || ''
|
||
)
|
||
: '';
|
||
updatePeerTypingUI();
|
||
if ( state.agentTyping ) {
|
||
state.agentTypingTimer = setTimeout( function () {
|
||
state.agentTypingTimer = null;
|
||
state.agentTyping = false;
|
||
state.agentTypingName = '';
|
||
updatePeerTypingUI();
|
||
}, 4000 );
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
if ( msg.type === 'crm_chat.event' && msg.event === 'agent.joined' ) {
|
||
pushWidgetRemoteDebug( 'evt.agent_joined.recv', {
|
||
ok: !!( cfg.showAgentJoinWs &&
|
||
sameConversationId( msg.conversation_id, convId ) &&
|
||
msg.agent ),
|
||
agent: msg.agent,
|
||
conversation_id: msg.conversation_id,
|
||
panel_open: state.open,
|
||
active_conv: convId
|
||
} );
|
||
if (
|
||
cfg.showAgentJoinWs &&
|
||
sameConversationId( msg.conversation_id, convId ) &&
|
||
msg.agent
|
||
) {
|
||
var ag = msg.agent;
|
||
var aid = ag.id != null ? String( ag.id ) : '';
|
||
var tplJb = cfg.strings && cfg.strings.agentJoinedNotice ? String( cfg.strings.agentJoinedNotice ) : '';
|
||
var tplJ = noticeTplOrFallback( cfg.agentJoinNoticeTemplate, tplJb || '%s به گفتگو پیوست' );
|
||
var nmJoin = agentPresenceBubbleLabel( ag.name != null ? String( ag.name ) : '' );
|
||
if ( aid && tplJ && nmJoin && ! state.seenAgentJoinIds[ aid ] ) {
|
||
state.seenAgentJoinIds[ aid ] = 1;
|
||
maybeShowAgentAttendanceNotice(
|
||
tplJ,
|
||
ag.name != null ? String( ag.name ) : ''
|
||
);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
};
|
||
ws.onerror = function () {
|
||
pushWidgetRemoteDebug( 'ws.error', { url: u } );
|
||
updateWsConn( 'offline' );
|
||
};
|
||
ws.onclose = function () {
|
||
pushWidgetRemoteDebug( 'ws.close', { hadSocket: Boolean( ws ) } );
|
||
if ( state.ws === ws ) {
|
||
state.ws = null;
|
||
}
|
||
updateWsConn( 'offline' );
|
||
};
|
||
}
|
||
|
||
function renderMessages() {
|
||
msgBox.innerHTML = '';
|
||
if ( ! state.session ) {
|
||
return;
|
||
}
|
||
var sUi = ( cfg.strings && cfg.strings ) || {};
|
||
var isRtl3 = ( ( cfg.dir || 'rtl' ) + '' ) === 'rtl';
|
||
var wmsg = ( cfg.welcomeMessage && cfg.welcomeMessage.toString() ) || '';
|
||
wmsg = wmsg.replace( /^\s+|\s+$/g, '' );
|
||
if ( wmsg ) {
|
||
var wel = document.createElement( 'div' );
|
||
wel.className = 'hesabix-chat-welcome';
|
||
var wlab = document.createElement( 'div' );
|
||
wlab.className = 'hesabix-chat-welcome-kicker';
|
||
wlab.textContent = cfg.chatTitle || cfg.strings.support;
|
||
var wbody = document.createElement( 'div' );
|
||
wbody.className = 'hesabix-chat-welcome-text';
|
||
wmsg.split( /\r?\n/ ).forEach( function ( line ) {
|
||
var p = document.createElement( 'p' );
|
||
p.textContent = line;
|
||
wbody.appendChild( p );
|
||
} );
|
||
var wic = document.createElement( 'div' );
|
||
wic.className = 'hesabix-chat-welcome-ico';
|
||
wic.setAttribute( 'aria-hidden', 'true' );
|
||
wic.innerHTML =
|
||
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M7 3h6a3 3 0 0 1 2.2 1l2 2.2A3 3 0 0 1 18 7.7V19a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z" stroke="currentColor" stroke-width="1.3" fill="currentColor" fill-opacity="0.1"/><path d="M8.5 8.5H13M8.5 12h3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>';
|
||
wel.appendChild( wic );
|
||
var wmain = document.createElement( 'div' );
|
||
wmain.className = 'hesabix-chat-welcome-main';
|
||
wmain.appendChild( wlab );
|
||
wmain.appendChild( wbody );
|
||
wel.appendChild( wmain );
|
||
msgBox.appendChild( wel );
|
||
}
|
||
var rtt = ( cfg.responseTimeText && cfg.responseTimeText.toString() ) || '';
|
||
rtt = rtt.replace( /^\s+|\s+$/g, '' );
|
||
if ( rtt ) {
|
||
var eta = document.createElement( 'div' );
|
||
eta.className = 'hesabix-chat-response-eta';
|
||
eta.setAttribute( 'role', 'status' );
|
||
var etIco = document.createElement( 'span' );
|
||
etIco.className = 'hesabix-chat-response-eta-ico';
|
||
etIco.setAttribute( 'aria-hidden', 'true' );
|
||
etIco.innerHTML =
|
||
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2.5"/></svg>';
|
||
var etx = document.createElement( 'span' );
|
||
etx.className = 'hesabix-chat-response-eta-txt';
|
||
etx.textContent = rtt;
|
||
eta.appendChild( etIco );
|
||
eta.appendChild( etx );
|
||
msgBox.appendChild( eta );
|
||
}
|
||
if ( state.agentJoinBannerText ) {
|
||
var noticeEl = document.createElement( 'div' );
|
||
noticeEl.className = 'hesabix-chat-chat-notice';
|
||
noticeEl.setAttribute( 'role', 'status' );
|
||
noticeEl.textContent = state.agentJoinBannerText;
|
||
msgBox.appendChild( noticeEl );
|
||
}
|
||
|
||
function appendCannedAnswerBubble( cannedBody ) {
|
||
var divS = document.createElement( 'div' );
|
||
divS.className = 'hesabix-chat-bubble hesabix-chat-bubble--agent hesabix-chat-bubble--canned';
|
||
var strongS = document.createElement( 'strong' );
|
||
strongS.textContent = agentPresenceBubbleLabel( '' );
|
||
var bodyS = document.createElement( 'div' );
|
||
bodyS.className = 'hesabix-chat-bubble-body';
|
||
bodyS.textContent = ( cannedBody || '' ).toString();
|
||
bodyS.style.whiteSpace = 'pre-wrap';
|
||
var metaS = document.createElement( 'div' );
|
||
metaS.className = 'hesabix-chat-bubble-meta hesabix-chat-bubble-meta--agent';
|
||
var cannedLbl = document.createElement( 'span' );
|
||
cannedLbl.className = 'hesabix-chat-canned-badge';
|
||
cannedLbl.textContent = sUi.cannedAnswerLabel || '';
|
||
metaS.appendChild( cannedLbl );
|
||
divS.appendChild( strongS );
|
||
divS.appendChild( bodyS );
|
||
divS.appendChild( metaS );
|
||
msgBox.appendChild( divS );
|
||
}
|
||
|
||
state.messages.forEach( function ( m ) {
|
||
var role = ( m.sender_role || '' ).toString();
|
||
var div = document.createElement( 'div' );
|
||
div.className = 'hesabix-chat-bubble hesabix-chat-bubble--' + ( role === 'visitor' ? 'visitor' : 'agent' );
|
||
var snRaw = ( m.sender_name && String( m.sender_name ).trim() ) || '';
|
||
var label = role === 'visitor' ? cfg.strings.you : agentPresenceBubbleLabel( snRaw );
|
||
var strong = document.createElement( 'strong' );
|
||
strong.textContent = label;
|
||
var bodyP = document.createElement( 'div' );
|
||
bodyP.textContent = ( m.body || '' ).toString();
|
||
if ( m.file && m.file.original_name && m.file.id ) {
|
||
var fm = ( m.file.mime_type || '' ).toString().toLowerCase();
|
||
var fileDiv = document.createElement( 'div' );
|
||
fileDiv.className = 'hesabix-chat-file';
|
||
var sid = state.session ? state.session.conversation_id : null;
|
||
var stok = state.session ? state.session.visitor_token : null;
|
||
if ( fm.indexOf( 'image/' ) === 0 && sid && stok ) {
|
||
var thumb = document.createElement( 'div' );
|
||
thumb.className = 'hesabix-chat-thumb';
|
||
var vu = visitorFileDownloadUrl( sid, m.file.id );
|
||
fetch( vu, {
|
||
method: 'GET',
|
||
headers: visitorTokenHeaders( stok ),
|
||
} )
|
||
.then( function ( r ) {
|
||
return r.blob();
|
||
} )
|
||
.then( function ( b ) {
|
||
var url = URL.createObjectURL( b );
|
||
var im = document.createElement( 'img' );
|
||
im.className = 'hesabix-chat-thumb-img';
|
||
im.alt = m.file.original_name;
|
||
im.src = url;
|
||
im.onload = function () {
|
||
try {
|
||
URL.revokeObjectURL( url );
|
||
} catch ( eR ) {}
|
||
};
|
||
im.addEventListener( 'click', function () {
|
||
downloadVisitorFile( sid, stok, m.file.id, m.file.original_name ).catch( function () {} );
|
||
} );
|
||
thumb.appendChild( im );
|
||
} )
|
||
.catch( function () {} );
|
||
fileDiv.appendChild( thumb );
|
||
} else if ( fm.indexOf( 'audio/' ) === 0 && sid && stok ) {
|
||
var ap = visitorFileDownloadUrl( sid, m.file.id );
|
||
fetch( ap, { method: 'GET', headers: visitorTokenHeaders( stok ) } )
|
||
.then( function ( r ) {
|
||
return r.blob();
|
||
} )
|
||
.then( function ( blob ) {
|
||
var urlA = URL.createObjectURL( blob );
|
||
var aud = document.createElement( 'audio' );
|
||
aud.controls = true;
|
||
aud.preload = 'metadata';
|
||
aud.src = urlA;
|
||
fileDiv.appendChild( aud );
|
||
} )
|
||
.catch( function () {} );
|
||
} else {
|
||
var dlBtn0 = document.createElement( 'button' );
|
||
dlBtn0.type = 'button';
|
||
dlBtn0.className = 'hesabix-chat-file-dl';
|
||
dlBtn0.textContent = m.file.original_name;
|
||
dlBtn0.addEventListener( 'click', function () {
|
||
if ( ! state.session ) {
|
||
return;
|
||
}
|
||
downloadVisitorFile(
|
||
state.session.conversation_id,
|
||
state.session.visitor_token,
|
||
m.file.id,
|
||
m.file.original_name
|
||
).catch( function ( e ) {
|
||
showFormError( errC, e.message || cfg.strings.errorGeneric );
|
||
} );
|
||
} );
|
||
fileDiv.appendChild( dlBtn0 );
|
||
}
|
||
if ( fm.indexOf( 'image/' ) === 0 || fm.indexOf( 'audio/' ) === 0 ) {
|
||
var dlLink = document.createElement( 'button' );
|
||
dlLink.type = 'button';
|
||
dlLink.className = 'hesabix-chat-file-dl hesabix-chat-file-dl--sub';
|
||
dlLink.textContent = m.file.original_name;
|
||
dlLink.addEventListener( 'click', function () {
|
||
if ( ! state.session ) {
|
||
return;
|
||
}
|
||
downloadVisitorFile(
|
||
state.session.conversation_id,
|
||
state.session.visitor_token,
|
||
m.file.id,
|
||
m.file.original_name
|
||
).catch( function ( e ) {
|
||
showFormError( errC, e.message || cfg.strings.errorGeneric );
|
||
} );
|
||
} );
|
||
fileDiv.appendChild( dlLink );
|
||
}
|
||
bodyP.appendChild( fileDiv );
|
||
}
|
||
var small = document.createElement( 'small' );
|
||
small.className = 'hesabix-chat-msg-time';
|
||
small.textContent = formatTime( m.created_at );
|
||
var meta = document.createElement( 'div' );
|
||
meta.className =
|
||
'hesabix-chat-bubble-meta' +
|
||
( role === 'visitor' ? ' hesabix-chat-bubble-meta--visitor' : ' hesabix-chat-bubble-meta--agent' );
|
||
if ( m.edited_at ) {
|
||
var edSpan = document.createElement( 'span' );
|
||
edSpan.className = 'hesabix-chat-msg-edited';
|
||
edSpan.textContent = ( sUi.msgEdited || ( isRtl3 ? 'ویرایششده' : 'edited' ) ) + ' · ';
|
||
meta.appendChild( edSpan );
|
||
}
|
||
if ( role === 'visitor' ) {
|
||
var readBy = m.read_at;
|
||
var tSent = sUi.msgDelivered || ( isRtl3 ? 'ارسال شد' : 'Sent' );
|
||
var tRead = sUi.msgReadBySupport || ( isRtl3 ? 'پشتیبان خواند' : 'Read by support' );
|
||
var receipt = document.createElement( 'span' );
|
||
receipt.className = 'hesabix-chat-msg-receipt' + ( readBy ? ' hesabix-chat-msg-receipt--read' : '' );
|
||
receipt.setAttribute( 'title', readBy ? tRead : tSent );
|
||
receipt.setAttribute( 'aria-label', readBy ? tRead : tSent );
|
||
receipt.innerHTML =
|
||
'<svg class="hesabix-chat-tick-ico" width="20" height="12" viewBox="0 0 28 12" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">' +
|
||
'<path d="M1 6.2l2.3 2.2L7.8 2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="hesabix-chat-tick-1st"/>' +
|
||
'<path d="M6.5 6.2l2.3 2.2L13.2 2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="hesabix-chat-tick-2nd"/>' +
|
||
'</svg>';
|
||
meta.appendChild( receipt );
|
||
}
|
||
meta.appendChild( small );
|
||
div.appendChild( strong );
|
||
div.appendChild( bodyP );
|
||
div.appendChild( meta );
|
||
msgBox.appendChild( div );
|
||
|
||
var mid = m.id != null ? +m.id : null;
|
||
if ( mid != null ) {
|
||
for ( var ci = 0; ci < state.localCannedAfterVisitor.length; ci++ ) {
|
||
var ce = state.localCannedAfterVisitor[ ci ];
|
||
if ( ce && +ce.afterVisitorMessageId === mid && ce.body ) {
|
||
appendCannedAnswerBubble( ce.body );
|
||
}
|
||
}
|
||
}
|
||
} );
|
||
|
||
var qrList = cfg.quickReplies;
|
||
if ( qrList && qrList.length && surface.classList.contains( 'hesabix-chat--step-chat' ) ) {
|
||
var qrRow = document.createElement( 'div' );
|
||
qrRow.className = 'hesabix-chat-quick-replies-row';
|
||
qrList.forEach( function ( item ) {
|
||
if ( ! item || ! item.q ) {
|
||
return;
|
||
}
|
||
var chip = document.createElement( 'button' );
|
||
chip.type = 'button';
|
||
chip.className = 'hesabix-chat-quick-chip';
|
||
chip.textContent = item.q;
|
||
chip.addEventListener( 'click', function () {
|
||
pickQuickReply( item.q, item.a || '' );
|
||
} );
|
||
qrRow.appendChild( chip );
|
||
} );
|
||
if ( qrRow.firstChild ) {
|
||
var qrWrap = document.createElement( 'div' );
|
||
qrWrap.className = 'hesabix-chat-quick-replies';
|
||
var qrTitle = document.createElement( 'div' );
|
||
qrTitle.className = 'hesabix-chat-quick-replies-title';
|
||
qrTitle.textContent = sUi.quickRepliesTitle || '';
|
||
qrWrap.appendChild( qrTitle );
|
||
qrWrap.appendChild( qrRow );
|
||
msgBox.appendChild( qrWrap );
|
||
}
|
||
}
|
||
var bhLine = '';
|
||
var sendAckTrim = state.businessHoursSendAck ? String( state.businessHoursSendAck ).replace( /^\s+|\s+$/g, '' ) : '';
|
||
var stickyTrim = state.businessHoursSticky ? String( state.businessHoursSticky ).replace( /^\s+|\s+$/g, '' ) : '';
|
||
if ( sendAckTrim ) {
|
||
bhLine = sendAckTrim;
|
||
} else if ( stickyTrim ) {
|
||
bhLine = stickyTrim;
|
||
}
|
||
if ( bhLine ) {
|
||
var bhEl = document.createElement( 'div' );
|
||
bhEl.className = 'hesabix-chat-chat-notice hesabix-chat-offline-hours-notice';
|
||
bhEl.setAttribute( 'role', 'status' );
|
||
bhEl.textContent = bhLine;
|
||
msgBox.appendChild( bhEl );
|
||
}
|
||
if ( state.slowReplyVisible && cfg.slowReplyMessage ) {
|
||
var sm = ( cfg.slowReplyMessage + '' ).replace( /^\s+|\s+$/g, '' );
|
||
if ( sm ) {
|
||
var slo = document.createElement( 'div' );
|
||
slo.className = 'hesabix-chat-chat-notice hesabix-chat-slow-reply-notice';
|
||
slo.setAttribute( 'role', 'status' );
|
||
slo.textContent = sm;
|
||
msgBox.appendChild( slo );
|
||
}
|
||
}
|
||
msgBox.scrollTop = msgBox.scrollHeight;
|
||
}
|
||
function pickQuickReply( questionText, answerText ) {
|
||
if ( ! state.session ) {
|
||
return;
|
||
}
|
||
if ( composerSendInFlight ) {
|
||
return;
|
||
}
|
||
showFormError( errC, '' );
|
||
setComposerSending( true );
|
||
postMessage( state.session.conversation_id, state.session.visitor_token, questionText )
|
||
.then( function ( msgData ) {
|
||
var vid = msgData && msgData.id != null ? +msgData.id : null;
|
||
var ans = ( answerText || '' ).toString().trim();
|
||
return refreshMessages().then( function () {
|
||
if ( vid != null && ans ) {
|
||
state.localCannedAfterVisitor.push( { afterVisitorMessageId: vid, body: ans } );
|
||
}
|
||
renderMessages();
|
||
afterVisitorOutboundSuccess();
|
||
} );
|
||
} )
|
||
.catch( function ( e ) {
|
||
showFormError( errC, e.message || cfg.strings.errorGeneric );
|
||
} )
|
||
.finally( function () {
|
||
setComposerSending( false );
|
||
} );
|
||
}
|
||
|
||
function refreshMessages() {
|
||
if ( ! state.session ) {
|
||
return;
|
||
}
|
||
return listMessages( state.session.conversation_id, state.session.visitor_token, 100 ).then( function ( items ) {
|
||
maybePlayAgentReplySound( items );
|
||
state.messages = items;
|
||
bumpSlowReplyIfAgentAnswered();
|
||
var have = {};
|
||
( items || [] ).forEach( function ( x ) {
|
||
if ( x && x.id != null ) {
|
||
have[ +x.id ] = 1;
|
||
}
|
||
} );
|
||
state.localCannedAfterVisitor = ( state.localCannedAfterVisitor || [] ).filter( function ( e ) {
|
||
return e && e.afterVisitorMessageId != null && have[ +e.afterVisitorMessageId ];
|
||
} );
|
||
renderMessages();
|
||
syncMarkAgentRead( items );
|
||
} );
|
||
}
|
||
|
||
function enterChatMode() {
|
||
formEl.classList.add( 'hesabix-chat--hidden' );
|
||
comp.classList.remove( 'hesabix-chat--hidden' );
|
||
surface.classList.remove( 'hesabix-chat--step-form' );
|
||
surface.classList.add( 'hesabix-chat--step-chat' );
|
||
syncPoweredByVisibility();
|
||
if ( wsConnLabel ) {
|
||
updateWsConn( 'offline' );
|
||
}
|
||
refreshThreadBar();
|
||
}
|
||
|
||
function enterFormMode() {
|
||
teardownVisitorPageUrlTracking();
|
||
setComposerSending( false );
|
||
state.messages = [];
|
||
state.localCannedAfterVisitor = [];
|
||
state.agentSoundPrimed = false;
|
||
state.lastAgentMsgNotifiedId = 0;
|
||
state.seenAgentJoinIds = {};
|
||
state.agentJoinBannerText = '';
|
||
state.agentAttendanceNoticeShown = false;
|
||
state.slowReplyVisible = false;
|
||
state.slowReplyBaselineAgentId = 0;
|
||
if ( state.slowReplyTimer ) {
|
||
clearTimeout( state.slowReplyTimer );
|
||
state.slowReplyTimer = null;
|
||
}
|
||
state.agentTypingName = '';
|
||
state.businessHoursSticky = '';
|
||
state.businessHoursSendAck = '';
|
||
msgBox.innerHTML = '';
|
||
formEl.classList.remove( 'hesabix-chat--hidden' );
|
||
comp.classList.add( 'hesabix-chat--hidden' );
|
||
surface.classList.remove( 'hesabix-chat--step-chat' );
|
||
surface.classList.add( 'hesabix-chat--step-form' );
|
||
syncPoweredByVisibility();
|
||
if ( wsConnLabel ) {
|
||
wsConnLabel.innerHTML = '';
|
||
wsConnLabel.className = 'hesabix-chat-ws-conn';
|
||
wsConnLabel.title = '';
|
||
wsConnLabel.removeAttribute( 'aria-label' );
|
||
}
|
||
if ( threadBar ) {
|
||
threadBar.classList.add( 'hesabix-chat--hidden' );
|
||
}
|
||
unbindRealtime();
|
||
}
|
||
|
||
function bindRealtime() {
|
||
if ( ! state.session || ! state.open ) {
|
||
chatLogV( 'bindRealtime: رد (session/open)', { hasSession: Boolean( state.session ), open: state.open } );
|
||
return;
|
||
}
|
||
var cid = state.session.conversation_id;
|
||
var tok = state.session.visitor_token;
|
||
chatLogV( 'bindRealtime: شروع', { conversation: cid } );
|
||
connectWs( cid, tok );
|
||
startPoll( cid, tok );
|
||
}
|
||
|
||
function unbindRealtime() {
|
||
if ( state.visitorTypingStopTimer ) {
|
||
clearTimeout( state.visitorTypingStopTimer );
|
||
state.visitorTypingStopTimer = null;
|
||
}
|
||
sendVisitorTyping( false );
|
||
state.agentTyping = false;
|
||
state.agentTypingName = '';
|
||
if ( state.agentTypingTimer ) {
|
||
clearTimeout( state.agentTypingTimer );
|
||
state.agentTypingTimer = null;
|
||
}
|
||
if ( peerTypingEl ) {
|
||
peerTypingEl.classList.add( 'hesabix-chat--hidden' );
|
||
}
|
||
disconnectWs();
|
||
stopPoll();
|
||
}
|
||
|
||
function afterSessionReady( skipRealtime ) {
|
||
enterChatMode();
|
||
installVisitorPageUrlTracking();
|
||
return refreshMessages().then( function () {
|
||
if ( ! skipRealtime && state.open ) {
|
||
bindRealtime();
|
||
}
|
||
} );
|
||
}
|
||
|
||
var pf = cfg.prefill || {};
|
||
if ( pf.first_name ) {
|
||
formEl.querySelector( '[name="first_name"]' ).value = pf.first_name;
|
||
}
|
||
if ( pf.last_name ) {
|
||
formEl.querySelector( '[name="last_name"]' ).value = pf.last_name;
|
||
}
|
||
if ( pf.email ) {
|
||
var preEl = formEl.querySelector( '[name="email"]' );
|
||
if ( preEl ) {
|
||
preEl.value = pf.email;
|
||
}
|
||
}
|
||
|
||
var existing = loadSession();
|
||
if ( existing ) {
|
||
state.session = {
|
||
conversation_id: existing.conversation_id,
|
||
visitor_token: existing.visitor_token
|
||
};
|
||
afterSessionReady( true )
|
||
.then( function () {
|
||
deferUntilPagePaint( function () {
|
||
runLauncherBoot( true );
|
||
} );
|
||
} )
|
||
.catch( function ( se ) {
|
||
chatLogV( 'بازیابی نشست محلی ناموفق', se );
|
||
clearSession();
|
||
state.session = null;
|
||
enterFormMode();
|
||
deferUntilPagePaint( function () {
|
||
runLauncherBoot( false );
|
||
} );
|
||
} );
|
||
} else {
|
||
deferUntilPagePaint( function () {
|
||
runLauncherBoot( false );
|
||
} );
|
||
}
|
||
|
||
formEl.addEventListener( 'submit', function ( ev ) {
|
||
ev.preventDefault();
|
||
var fd = new FormData( formEl );
|
||
var errEl = formEl.querySelector( '[data-err]' );
|
||
showFormError( errEl, '' );
|
||
startConversation( {
|
||
first_name: ( fd.get( 'first_name' ) || '' ).toString().trim(),
|
||
last_name: ( fd.get( 'last_name' ) || '' ).toString().trim(),
|
||
email: ( fd.get( 'email' ) || '' ).toString().trim(),
|
||
phone: ( fd.get( 'phone' ) || '' ).toString().trim()
|
||
} )
|
||
.then( function ( data ) {
|
||
var cid = data.conversation_id;
|
||
var tok = data.visitor_token;
|
||
if ( ! cid || ! tok ) {
|
||
throw new Error( cfg.strings.errorGeneric );
|
||
}
|
||
var prevSess = loadSession();
|
||
state.seenAgentJoinIds = {};
|
||
state.agentJoinBannerText = '';
|
||
state.agentTypingName = '';
|
||
saveSession( cid, tok );
|
||
state.session = { conversation_id: cid, visitor_token: tok };
|
||
if ( prevSess && String( prevSess.conversation_id ) !== String( cid ) ) {
|
||
pushThreadHistory( prevSess );
|
||
}
|
||
state.agentAttendanceNoticeShown = false;
|
||
state.slowReplyVisible = false;
|
||
if ( state.slowReplyTimer ) {
|
||
clearTimeout( state.slowReplyTimer );
|
||
state.slowReplyTimer = null;
|
||
}
|
||
return afterSessionReady( false );
|
||
} )
|
||
.catch( function ( e ) {
|
||
chatLogV( 'startConversation خطا', e && e.message, e );
|
||
showFormError( errEl, e.message || cfg.strings.errorGeneric );
|
||
} );
|
||
} );
|
||
|
||
function submitComposerMessage() {
|
||
if ( ! state.session || composerSendInFlight ) {
|
||
return;
|
||
}
|
||
var t = ( ta.value || '' ).trim();
|
||
showFormError( errC, '' );
|
||
if ( ! t ) {
|
||
return;
|
||
}
|
||
setComposerSending( true );
|
||
postMessage( state.session.conversation_id, state.session.visitor_token, t )
|
||
.then( function () {
|
||
ta.value = '';
|
||
if ( state.visitorTypingStopTimer ) {
|
||
clearTimeout( state.visitorTypingStopTimer );
|
||
state.visitorTypingStopTimer = null;
|
||
}
|
||
sendVisitorTyping( false );
|
||
return refreshMessages();
|
||
} )
|
||
.then( function () {
|
||
afterVisitorOutboundSuccess();
|
||
} )
|
||
.catch( function ( e ) {
|
||
showFormError( errC, e.message || cfg.strings.errorGeneric );
|
||
} )
|
||
.finally( function () {
|
||
setComposerSending( false );
|
||
} );
|
||
}
|
||
|
||
sendBtn.addEventListener( 'click', submitComposerMessage );
|
||
ta.addEventListener( 'keydown', function ( e ) {
|
||
if ( e.key !== 'Enter' ) {
|
||
return;
|
||
}
|
||
if ( ! ( e.ctrlKey || e.metaKey ) ) {
|
||
return;
|
||
}
|
||
if ( composerSendInFlight ) {
|
||
e.preventDefault();
|
||
return;
|
||
}
|
||
e.preventDefault();
|
||
submitComposerMessage();
|
||
} );
|
||
|
||
ta.addEventListener( 'input', function () {
|
||
if ( ! state.session ) {
|
||
return;
|
||
}
|
||
var v = ( ta.value || '' );
|
||
if ( ! v.length ) {
|
||
if ( state.visitorTypingStopTimer ) {
|
||
clearTimeout( state.visitorTypingStopTimer );
|
||
state.visitorTypingStopTimer = null;
|
||
}
|
||
sendVisitorTyping( false );
|
||
return;
|
||
}
|
||
sendVisitorTyping( true );
|
||
scheduleVisitorTypingStop();
|
||
} );
|
||
ta.addEventListener( 'blur', function () {
|
||
if ( state.visitorTypingStopTimer ) {
|
||
clearTimeout( state.visitorTypingStopTimer );
|
||
state.visitorTypingStopTimer = null;
|
||
}
|
||
sendVisitorTyping( false );
|
||
} );
|
||
|
||
btnNew.addEventListener( 'click', function () {
|
||
if ( ! window.confirm( cfg.strings.newChatHint ) ) {
|
||
return;
|
||
}
|
||
if ( state.session ) {
|
||
pushThreadHistory( state.session );
|
||
}
|
||
clearSession();
|
||
state.session = null;
|
||
enterFormMode();
|
||
} );
|
||
|
||
function applyLauncherIdleIfNeeded() {
|
||
if ( ! btn ) {
|
||
return;
|
||
}
|
||
btn.classList.remove( 'hesabix-chat-launcher--idle' );
|
||
btn.removeAttribute( 'data-hesabix-idle-anim' );
|
||
var anim = ( cfg.launcherIdleAnimation || 'none' ).toString();
|
||
if ( state.open || anim === 'none' || ! anim ) {
|
||
return;
|
||
}
|
||
btn.classList.add( 'hesabix-chat-launcher--idle' );
|
||
btn.setAttribute( 'data-hesabix-idle-anim', anim );
|
||
}
|
||
|
||
function clearLauncherAttentionTimer() {
|
||
if ( launcherAttentionTimer ) {
|
||
clearTimeout( launcherAttentionTimer );
|
||
launcherAttentionTimer = null;
|
||
}
|
||
}
|
||
|
||
function scheduleLauncherAttention() {
|
||
clearLauncherAttentionTimer();
|
||
if ( ! btn ) {
|
||
return;
|
||
}
|
||
var anim = ( cfg.launcherIdleAnimation || 'none' ).toString();
|
||
if ( anim === 'none' || ! anim ) {
|
||
return;
|
||
}
|
||
btn.classList.remove( 'hesabix-chat-launcher--idle' );
|
||
btn.removeAttribute( 'data-hesabix-idle-anim' );
|
||
var d = Number( cfg.launcherAttentionDelaySec );
|
||
if ( isNaN( d ) || d < 0 ) {
|
||
d = 0;
|
||
}
|
||
if ( d > 600 ) {
|
||
d = 600;
|
||
}
|
||
launcherAttentionTimer = setTimeout( function () {
|
||
launcherAttentionTimer = null;
|
||
applyLauncherIdleIfNeeded();
|
||
}, d * 1000 );
|
||
}
|
||
|
||
function setOpen( v, skipDataRefresh ) {
|
||
state.open = v;
|
||
btn.setAttribute( 'aria-expanded', v ? 'true' : 'false' );
|
||
if ( v ) {
|
||
primeAgentSoundForAutoplay();
|
||
clearLauncherAttentionTimer();
|
||
btn.classList.remove( 'hesabix-chat-launcher--idle' );
|
||
btn.removeAttribute( 'data-hesabix-idle-anim' );
|
||
panel.classList.remove( 'hesabix-chat--hidden' );
|
||
( function () {
|
||
var st = ( typeof getComputedStyle === 'function' ) ? getComputedStyle( panel ) : null;
|
||
var r = ( typeof panel.getBoundingClientRect === 'function' ) ? panel.getBoundingClientRect() : null;
|
||
chatLogV( 'پنل باز', {
|
||
hasSession: Boolean( state.session ),
|
||
className: panel.className,
|
||
rect: r ? { w: r.width, h: r.height, top: r.top, left: r.left } : null,
|
||
css: st ? { zIndex: st.zIndex, display: st.display, visibility: st.visibility, opacity: st.opacity, pointerEvents: st.pointerEvents } : null
|
||
} );
|
||
} )();
|
||
if ( state.session ) {
|
||
if ( skipDataRefresh ) {
|
||
bindRealtime();
|
||
} else {
|
||
refreshMessages()
|
||
.then( function () {
|
||
bindRealtime();
|
||
} )
|
||
.catch( function () {} );
|
||
}
|
||
}
|
||
refreshBusinessHoursStickyForPanel();
|
||
} else {
|
||
panel.classList.add( 'hesabix-chat--hidden' );
|
||
state.businessHoursSticky = '';
|
||
chatLogV( 'پنل بسته' );
|
||
unbindRealtime();
|
||
applyLauncherIdleIfNeeded();
|
||
}
|
||
persistRememberedPanelOpen( v );
|
||
}
|
||
|
||
btn.addEventListener( 'click', function ( ev ) {
|
||
primeAgentSoundForAutoplay();
|
||
chatLog( 'کلیک لانچر', { wasOpen: state.open, target: ev.target && ev.target.nodeName } );
|
||
setOpen( ! state.open );
|
||
} );
|
||
|
||
btnClose.addEventListener( 'click', function () {
|
||
setOpen( false );
|
||
} );
|
||
|
||
document.addEventListener( 'keydown', function ( e ) {
|
||
if ( e.key === 'Escape' && state.open ) {
|
||
setOpen( false );
|
||
}
|
||
} );
|
||
|
||
function deferUntilPagePaint( fn ) {
|
||
var run = function () {
|
||
if ( typeof window.requestAnimationFrame === 'function' ) {
|
||
window.requestAnimationFrame( function () {
|
||
window.requestAnimationFrame( fn );
|
||
} );
|
||
} else {
|
||
setTimeout( fn, 0 );
|
||
}
|
||
};
|
||
if ( document.readyState === 'complete' ) {
|
||
run();
|
||
} else {
|
||
window.addEventListener( 'load', run, { once: true } );
|
||
}
|
||
}
|
||
|
||
function wantsOpenOnLoad() {
|
||
var opl = cfg.openPanelOnLoad;
|
||
return opl === true || opl === 1 || opl === '1';
|
||
}
|
||
|
||
function openPanelDelayMs() {
|
||
var odl = Number( cfg.openPanelDelaySec );
|
||
if ( isNaN( odl ) || odl < 0 ) {
|
||
odl = 0;
|
||
}
|
||
if ( odl > 120 ) {
|
||
odl = 120;
|
||
}
|
||
return odl * 1000;
|
||
}
|
||
|
||
function runLauncherBoot( skipRefreshOnOpen ) {
|
||
scheduleLauncherAttention();
|
||
var remembered = readRememberedPanelOpen();
|
||
if ( remembered === false ) {
|
||
return;
|
||
}
|
||
if ( remembered === true ) {
|
||
setTimeout( function () {
|
||
setOpen( true, skipRefreshOnOpen === true );
|
||
}, 0 );
|
||
return;
|
||
}
|
||
if ( ! wantsOpenOnLoad() ) {
|
||
return;
|
||
}
|
||
var ms = openPanelDelayMs();
|
||
setTimeout( function () {
|
||
setOpen( true, skipRefreshOnOpen === true );
|
||
}, ms );
|
||
}
|
||
} )();
|