Версия:
Фрагменты кода для Роспатент и объем занимаемой памяти.
Введение
Для регистрации продукта требуется предоставить фрагменты кода программного продукта, а также размер приложения.
Размер приложения
Из формы не ясно, какой именно размер приложения должен быть указан: общий объем занимаемой памяти исходников или скомпилированных/собранных файлов.
ПО на языке JavaScript не предполагает компиляции, а собрано может быть по разному, с различной степенью оптимизации.
Поэтому предоставим объем занимаемой памяти всех модулей при скачивании из репозиториев в GitFlic в zip архивах на текущий момент.
Основной проект: 141 MB
Модуль регистрации: 1,53 MB
Модуль консультанта: 12 MB
Модуль Виджета: 6,57 MB
ИТОГО, СУММАРНЫЙ РАЗМЕР: 161,1 MB
Фрагменты кода
Основной проект
Блоки кода ядра GoCore, на котором построено приложение
// MySQLModel
var MySQLModel = function (obj) {
if (debug) console.log('creating new MySQLModel ....', obj.name, obj.client_object || 'no_CO')
var _t = this
if (typeof obj !== 'object') {
throw new MyError('Не верно вызвана функция конструктор в MySQLModel.js')
}
_t.tableName = obj.name.toLowerCase()
_t.user = obj.user || {sid: '0'}
_t.name = obj.name
_t.data_cache_alias = obj.name + '_' + (obj.client_object || '0') + '_' + _t.user.sid
_t.client_object = obj.client_object
_t.cache = {}
_t.columns = []
_t.uniqueColumns = []
_t.required_fields = []
_t.is_inherit_fields = {} // Все которые нужны в конечном запросе (columns)
_t.is_inherit_fields_real_value = {} // Только те которые проверяются на значения. Отдельный объект, чтобы был короче цикл
_t.not_insertable = ['created']
_t.validation = {}
_t.init_params = obj.params || {}
_t.dynamic_field_input_arr = []
_t.dynamic_field_tables_for_clear_cache = {}
_t.current_dynamic_field_alias = ''
_t.dynamic_field_pair = []
_t.history_fields = []
_t.always_save_log_fields = []
this.validationFormats = {
notNull: {
format: '<значение>',
example: 'строка, число, дата...'
},
number: {
format: '<число>',
example: '10'
},
url: {
format: '<Протокол>://<адрес>',
example: 'http://example.ru'
},
email: {
format: '<Имя>@<домен>',
example: 'user@example.ru'
}
}
this.beforeFunction = {
get: function (obj, cb) {
cb(null, null)
},
add: function (obj, cb) {
cb(null, null)
},
modify: function (obj, cb) {
cb(null, null)
},
remove: function (obj, cb) {
cb(null, null)
}
}
this.prepareResult = function (rows, profile) {
for (var i in rows) {
// Берем тип данных для этого поля и преобразуем null -> '', 1/0 --> true/false
if (typeof profile[i] != 'object') continue
if (rows[i] === 'NULL') rows[i] = null
if (rows[i] === null && profile[i].type !== 'tinyint') {
rows[i] = ''
continue
}
if (profile[i].type == 'tinyint' && profile[i].field_length == 1) rows[i] = !!rows[i]
}
return rows
}
this.getFormatingFunc = function (rows, params) {
if (!Array.isArray(rows)) {
var isSingleValue = true
rows = [rows]
}
var formatData = function (field, field_type, profile) {
var formatFuncName = profile.get_formating || (function () {
switch (field_type) {
case "date":
return 'userFriendlyDate'
break
case "datetime":
return 'userFriendlyDateTime'
break
case "blob":
case "mediumblob":
case "longblob":
return 'parseBlob'
break
case "tinyint":
return (profile.field_length == 1) ? 'parseBool' : null
break
case "DECIMAL(10, 2)":
case "DECIMAL(15, 2)":
case "DECIMAL(50, 2)":
return 'formatMoney'
break
case "DECIMAL(3, 2)":
return 'formatPercent'
break
default :
return null
break
}
})()
if (formatFuncName) {
if (typeof _t[formatFuncName] == 'function') return _t[formatFuncName](field, {
params: params,
profile: profile
})
if (typeof funcs[formatFuncName] == 'function') return funcs[formatFuncName](field)
}
return field
}
for (var i in rows) {
/*if (rows[i].name.indexOf('MySQLvariable')!==-1) {
delete rows[i];
continue;
}*/
var row = rows[i]
for (var j in row) {
if (row[j] === null) row[j] = ''
var field = row[j]
if (j.indexOf('MySQLvariable') > -1) {
delete rows[i][j]
continue
}
var profile = _t.class_fields_profile[j]
if (!profile) continue
var field_type = profile.type
rows[i][j] = formatData(field, field_type, profile)
}
}
return (isSingleValue) ? (rows.length) ? rows[0] : [] : rows
}
this.setFormatingFunc = function (row) {
for (var i in row) {
var field = row[i]
if (field === null) continue
var profile = _t.class_fields_profile[i]
var field_type = profile.type.replace(/\W|[0-9]/ig, '').toLowerCase()
var formatFuncName = profile.set_formating || (function () {
switch (field_type) {
case "date":
return 'getDateMySQL'
break
case "datetime":
return 'getDateTimeMySQL'
break
case "int":
case "bigint":
case "decimal":
if (field === '') row[i] = null
return null
break
default :
return null
break
}
})()
if (formatFuncName) {
if (typeof _t[formatFuncName] == 'function') {
row[i] = [formatFuncName](field)
} else if (typeof funcs[formatFuncName] == 'function') {
row[i] = funcs[formatFuncName](field)
}
}
}
return row
}
this.loadDefaultValues = function (obj, cb, additional_params) {
if (typeof additional_params !== 'object') additional_params = {}
var standart = additional_params.standart
var is_virtual = additional_params.is_virtual || true
if (standart) {
// загрузим стандартные значения
for (var i in _t.class_fields_profile) {
var colProfile = _t.class_fields_profile[i]
var columnName = colProfile.column_name
if (typeof colProfile !== 'object') continue
var colValue = obj[columnName]
if (typeof colValue === 'undefined' && colProfile.default_value && !colProfile.is_virtual) {
obj[columnName] = colProfile.default_value
}
}
}
if (!is_virtual) return cb(null, obj)
// Загрузим значения по умолчанию для is_virtual полей
async.eachSeries(_t.class_fields_profile, function (item, cb) {
var columnName = item.column_name
if (typeof item !== 'object') {
if (excludeCheckExistFields.includes(i)) return cb(null)
return cb(new MyError('Не удалось получить профайл колонки...', i))
}
var colValue = obj[columnName]
if (typeof colValue === 'undefined' && item.is_virtual && item.default_value) {
// Загрузим необходимый default
if (typeof obj[item.keyword] !== 'undefined') return cb(null)
var o = {
command: 'getPrototype',
object: item.from_table,
params: {
columns: ['id'],
collapseData: false,
where: [
{
key: 'sysname',
val1: item.default_value
}
]
}
}
_t.api(o, function (err, res) {
if (err || typeof res !== 'object') {
console.log('Не удалось получить значение по умолчанию для поля ' + columnName, err, res)
return cb(null)
}
if (!res.length) {
console.log('Нет записей в ' + item.from_table + 'с sysname = ' + item.default_value + ' для поля ' + columnName)
return cb(null)
}
obj[item.keyword] = res[0].id
// Сохраним значение и виртуального поля, для возможности проверки ролевой модели например
obj[columnName] = item.default_value
cb(null)
})
} else if (typeof colValue === 'undefined' && item.default_value) {
obj[columnName] = item.default_value
return cb(null)
} else {
cb(null)
}
}, function (err) {
return cb(err, obj)
})
}
}
// КОНЕЦ ФРАГМЕНТА
MySQLModel.prototype.get = function (params, cb) {
if (arguments.length == 1) {
cb = arguments[0]
params = {}
}
if (typeof cb !== 'function') throw new MyError('В метод не передан cb')
if (typeof params !== 'object') return cb(new MyError('В метод не переданы params'))
var _t = this
var doNotLog = params.doNotLog || !debugSQL
delete params.doNotLog
let cacheAlias = _t.data_cache_alias // Уже содержит User sid
let cacheAliasHash, realSQL
var use_cache = (typeof params.use_cache !== 'undefined') ? params.use_cache : _t.use_cache
if (use_cache) {
cacheAlias += hashObj(params)
cacheAlias += _t.user.user_data ? ('---' + _t.user.user_data.id) : ''
cacheAliasHash = cacheAlias
if (!global.classesCache[_t.name]) global.classesCache[_t.name] = {}
if (global.classesCache[_t.name][cacheAliasHash]) {
// if (!doNotLog) console.log('\n========USE_CACHE=======', global.classesCache[_t.name][cacheAliasHash].alias)
if (!doNotLog) console.log('USE_CACHE:', _t.name)
// if (!doNotLog) console.error('\n========USE_CACHE=======', global.classesCache[_t.name][cacheAliasHash].alias)
let results = global.classesCache[_t.name][cacheAliasHash].results
&& Array.isArray(global.classesCache[_t.name][cacheAliasHash].results)
? [...global.classesCache[_t.name][cacheAliasHash].results]
: global.classesCache[_t.name][cacheAliasHash].results
if (results) results = funcs.cloneObj(results)
let additionalDataRes = global.classesCache[_t.name][cacheAliasHash].additionalData
if (additionalDataRes) additionalDataRes = funcs.cloneObj(additionalDataRes)
return cb(null, results, additionalDataRes)
}
}
var fromClient = params.fromClient
delete params.fromClient
var doNotUseDefaultWhere = typeof params.doNotUseDefaultWhere !== 'undefined' && !fromClient
? params.doNotUseDefaultWhere
: null
delete params.doNotUseDefaultWhere
var list_of_access_res_ids = []
var doNotCheckList = fromClient ? false : params.doNotCheckList
var skipCheckRoleModel = fromClient ? false : params.skipCheckRoleModel
var skipCheckBlocked = fromClient ? false : params.skipCheckBlocked
var skipCheckRoleModelAdmin = (_t.user.authorized && _t.user.user_data.user_type_sysname === 'ADMIN')
var role_model_where = []
delete params.doNotCheckList
delete params.skipCheckRoleModel
delete params.skipCheckBlocked
// Будет использоваться для подзапросов типа "Select ... from .... where some_table_id in (select id from some_table)
const prepareSQL = params.prepareSQL
const prepareSQLField = params.prepareSQLField || 'id' // Поле, которое запрашивается при подготовки подзапроса
const countOnly = params.countOnly
if (fromClient && params.specColumns) {
return cb(new MyError('specColumns is not available for client request'))
}
if (fromClient && params.groupBy) {
return cb(new MyError('groupBy is not available for client request'))
}
if (fromClient && params.prepareSQL) {
return cb(new MyError('prepareSQL is not available for client request'))
}
var multi_value_separator = '-|-';
// КОНЕЦ ФРАГМЕНТА
async.series({
// ...
formatWhere: (cb) => {
let new_where = []
for (var ii0 in where) {
if (typeof where[ii0] === 'object') new_where.push(where[ii0])
}
where = new_where
for (var ii in where) {
if (where[ii].group === 'Access_by_list_SYSTEM') where[ii].group = 'Access_by_list_SYSTEM_replaced'
}
if (list_of_access_res_ids.length) {
where.push(
{
key: 'id',
type: 'in',
group: 'Access_by_list_SYSTEM',
val1: list_of_access_res_ids
}
)
}
where = where.sort(function (a, b) {
if (!a.group) a.group = a.key + '_serverAutoGroup'
if (!b.group) b.group = b.key + '_serverAutoGroup'
a.groupTrim = a.group.replace(/\.\w+/ig, '')
b.groupTrim = b.group.replace(/\.\w+/ig, '')
if (a.groupTrim > b.groupTrim) return 1
if (a.groupTrim < b.groupTrim) return -1
if (a.role_group > b.role_group) return 1
if (a.role_group < b.role_group) return -1
return 0
})
var p = {
tableName: tableName,
class_fields_profile: _t.class_fields_profile,
columns: columns,
where: where,
ready_columns_by_colName: ready_columns_by_colName
}
getWhereStrCb(p, (err, res) => {
if (err) {
console.error('getWhereStr', err)
return cb(err)
}
whereStr = res.str
return cb(null)
})
},
// ...
query: (cb) => {
pool.getConn((err, conn) => {
if (err) return cb(new MyError('Не удалось получить подключение к БД', {err: err}))
const t1 = Date.now()
conn.query(realSQL, [], function (err, res) {
conn.release()
const cnt = !isNaN(+debugSQL) ? +debugSQL : false
const diff = Date.now() - t1
if (!cnt || String(diff).length >= cnt) {
// если debugSQL указан как число, то будут выводиться только те,
// у кого длительность больше или равна Х символов
if (diff > debugSQLConsoleErrTime) {
console.error('LONG realSQL. SQL_TIME:', diff, realSQL)
} else {
if (!doNotLog) console.log('realSQL. SQL_TIME:', diff, realSQL)
}
}
if (err) {
err.msg = err.message
if (err.code === 'ER_BAD_FIELD_ERROR') {
var err2 = err
err = new MyError('ER_BAD_FIELD_ERROR. Возможно, Вы добавили поле, которое использует для своего построения другие поля, ' +
'но его sort_no меньше чем у них. Проверьте что все используемые им поля имеют sort_no меньше. ' +
'Внимание! SORT_NO не синхронизируется! ' +
'Если изначально проставлено не верно, то надо залесть в class_profile и установить sort_no (функция в контекстном меню).'
, {
name: _t.name,
err: err2,
realSQL
})
}
console.error('GET ERROR', err, realSQL)
return cb(err)
}
rows = res
cb(null)
})
})
},
}, cb);
}
// Внутренний api
var api = function (obj, cb, user) {
// if (obj.object == 'tangibles') {
// console.log(obj)
// debugger
// }
if (typeof cb !== 'function') throw new MyError('The API did not pass the callback function')
if (typeof obj !== 'object') return cb(new MyError('The API is not passed to obj'))
obj = funcs.cloneObj(obj)
const objOrig = funcs.cloneObj(obj)
obj.params = obj.params || {}
var _t = this
if (!user) throw new MyError('API not passed to user')
let sid = user.sid
var command, object
var object_params = obj.object_params || {}
var params = obj.params || {}
var paramsConsoleJSON
var client_object = obj.client_object || params.client_object
var fromClient = obj.params.fromClient
//if (client_object) params.client_object = client_object;
var t1 = moment()
if (obj.command == '_CLEAR') {
global.classes = {}
return cb(null, new UserOk('Cache dropped.'))
}
var apiRID = funcs.guidShort() // API Request ID
var fromClientConsole = (fromClient) ? '==> ' : ''
var now_time = moment()
var is_access_important // Определяет был ли дан доступ или требуются доп проверки
var is_access_by_list // Определяет что доступ выдан но требеется проверить по списку
var noToastr = params.noToastr
delete params.noToastr
var skipCheckRoleModel = (!fromClient) ? params.skipCheckRoleModel : false
// var checkAccess = (fromClient)? false : params.checkAccess;
var checkAccess = params.checkAccess
var skipCheckRoleModelAdmin = (user.authorized && user.user_data.user_type_sysname === 'ADMIN')
// if (user.authorized && user.user_data.user_type_sysname === 'ADMIN'){
// skipCheckRoleModel = true;
// }
delete params.checkAccess
delete params.noToastr
var accesses = []
var denies = []
var _class
async.series({
checkServicesProblem:async cb => {
// ...
},
checkArgs: cb => {
// Проверим аргументы
// ...
},
logInCosole: cb => {
// log in console all requests
var excludedParams = ['password']
var paramsConsole = funcs.cloneObj(params)
// paramsConsole
for (var i in excludedParams) {
if (typeof paramsConsole[excludedParams[i]]!== 'undefined') paramsConsole[excludedParams[i]] = '***'
}
if (paramsConsole)
paramsConsoleJSON = JSON.stringify(paramsConsole).substr(0, 5000)
if (debug) console.info(moment().format('DD.MM.YYYY HH:mm:ss'),fromClientConsole + apiRID, ' → ', checkAccess ? 'checkAccess': '', now_time.format('DD.MM.YYYY HH:mm:ss'), 'API LOG (' + sid + '):', command, object, paramsConsoleJSON)
if (debug) global.times.log_time += moment().diff(now_time)
cb(null)
},
checkAccess: cb => {
//checkAccess
// ...
},
prepareAlias: cb => {
// Подготовим инстанс класса (из кэша или новый)
// ...
},
// ...
checkAccessByList: cb => {
// Проверка доступа по спискам для команд не 'get'. Для 'get' отдельный механизм
// ...
},
// ...
checkRoleModel: cb => {
if (!useRoleModel) return cb(null)
// ...
},
do: cb => {
if (checkAccess) return cb(null, new UserOk('noToastr'))
// Выполнить действие или вернуть класс
if (command == '_getClass') return cb(null, _class)
if (command == '_clearCache') {
for (var key in global.classes) {
if (key.indexOf(object + '_-_') === 0) {
global.classesCache[object] = {}
if (client_object) delete global.classes[key][client_object]
else global.classes[key] = {}
}
}
return cb(null, new UserOk('The class / client cache has been successfully cleared.'))
}
if (command == '_clearCacheAll') {
for (var key2 in global.classes) {
if (key2.indexOf(object) === 0) {
global.classesCache[object] = {}
global.classes[key2] = {}
delete global.classes[key2]
}
}
return cb(null, new UserOk('Class cache successfully cleared for all client objects.')) //Кеш класса успешно очищен для всех клиентских объектов
}
if (command == '_getTimes') {
return cb(null, new UserOk('See console', {times: global.times}))
}
if (typeof _class !== 'object') return cb(new MyError('The class is not an object.'))
if (typeof _class[command] !== 'function') return cb(new MyError('The class does not have this method.', {
method: command,
object: object
}))
_class.is_busy = true
if (fromClient) delete params.doNotCheckList
if (!is_access_by_list) {
params.doNotCheckList = true
} // Доступ выдан на высоком уровне, проверять список не нужно.
_class[command](params, function (err, res, additionalData) {
if (err) {
console.log(moment().format('DD.MM.YYYY HH:mm:ss'), err)
}
delete _class.is_busy
cb(err, res)
// cb(err, res, additionalData);
})
}
}, (err, resAll, additionalData) => {
if (err instanceof UserOk && err.message === 'goNext') {
resAll.do = err
err = null
}
// Проверить на ошибки
let res = resAll.do
var end_time = moment()
var request_time = end_time.diff(t1)
if (debug) console.info(fromClientConsole + apiRID, ' ← Time:', checkAccess ? 'checkAccess' : '', request_time, end_time.format('DD.MM.YYYY HH:mm:ss'), 'API LOG END(' + sid + '):', command, object, paramsConsoleJSON)
if (err) {
if (config.get('debugMyError')){
if (err instanceof MyError){
console.error('ERR:MyError', err)
}
}
if (config.get('debugUserError')){
if (err instanceof UserError){
console.error('ERR:UserError', err)
}
}
if (err instanceof UserOk) {
// ...
return
// return cb(null, getCode('ok', err), request_time);
}
if (err instanceof UserError && !!fromClient) {
getCode({name: err.message, params: err.data, user: user}, function (code_err, code_res) {
if (code_err) return cb(code_err, request_time)
return cb(null, code_res, request_time)
})
return
} else {
// ...
return
}
}
// выполнить форматирование результата
if (res instanceof UserOk) {
// ...
}
if (additionalData) return cb(null, res, additionalData, request_time)
cb(null, res, request_time)
})
}
Блоки кода специфичные для проекта Voda
// Widget.js (бэк)
/**
* Функция будет отслеживать состояние подключения WebRTC соединения и отсылать соответствующие команды
* Здесь же будут выполняться подписки/отписки сокетов для обмена
* @param info
*/
const processWebRTC = (info) => {
let _t = this
// отправим консультанту инфу необходимую для начала инициализации webRTC
const consultant = info.consultant
const widget = info.widget
if (!widget.connected || !consultant.connected) {
if (!widget.processWebRTCCounter) widget.processWebRTCCounter = 0
if (widget.processWebRTCCounter >= 50) return
widget.processWebRTCCounter++
setTimeout(() => {
processWebRTC(info)
}, 200)
return
} else {
widget.processWebRTCCounter = 0
}
if (info.communication_session?.is_finished) {
return
}
if (consultant.local.webRTC?.data?.connected || consultant.local.webRTC?.data?.failed) {
// ...
}
if (widget.local.webRTC?.data?.connected || widget.local.webRTC?.data?.failed) {
if (typeof widget.unsubscribeWebRTC === "function") {
// ...
}
if (widget.local.webRTC?.data?.failed) {
console.error('ERR:processWebRTC: widget.failed', {webRTC:widget.local.webRTC?.data})
widget.endWebRTC()
}
}
const route = getWebRTCAction(consultant, widget)
// ...
if (route.isStart){
const id = info?.widget?.remote?.communicationSessions?.data?.id
// ...
}
let targetInstance
if (route.target === CONSULTANT) targetInstance = consultant
else if (route.target === WIDGET) targetInstance = widget
const session = info.communication_session
const targetWebRTC = targetInstance.local.webRTC?.data || {}
// const confCounterKey = `${route.toState}_counter`
if (!session.connectWebRTCProcess) session.connectWebRTCProcess = []
if (!route.isStart) session.connectWebRTCProcess = []
session.connectWebRTCProcess.push(`${route.target}.${route.toState}`)
let data_
if (route.target === CONSULTANT) {
// Определим данные для передачи в зависимости от того в какое состояние переводим
switch (route.toState) {
case 'start':
if (!targetWebRTC[route.toState]) {
}
data_ = {
start: true,
createOffer: true,
session: {is_share:session.is_share, is_video: session.is_video, is_audio: session.is_audio},
iceServers
}
break
// ...
case 'reanswer_confirmed':
widget.setData('webRTC', {reoffer:null, reanswer:null, reoffer_confirmed:null, reanswer_confirmed:null}, null, true, true)
consultant.setData('webRTC', {reoffer:null, reanswer:null, reoffer_confirmed:null, reanswer_confirmed:null}, null, true, true)
default:
break
}
} else if (route.target === WIDGET) {
// Определим данные для передачи в зависимости от того в какое состояние переводим
switch (route.toState) {
// ...
case 'offer':
if (!consultant.local.webRTC.data.offer) break // Оффера еще нет. Придем сюда же при получении оффера
data_ = {offer: consultant.local.webRTC.data.offer}
break
case 'consultantReadyToIce':
data_ = {consultantReadyToIce: true}
break
// ...
default:
break
}
}
// Если есть что установить - установим
if (data_) {
targetInstance.setData('webRTC', data_, null, true, true)
}
}
Model.prototype.call = function (obj, cb) {
if (arguments.length === 1) {
cb = arguments[0]
obj = {}
}
var _t = this
var rollback_key = obj.rollback_key || rollback.create()
var doNotSaveRollback = obj.doNotSaveRollback || !!obj.rollback_key
// Создадим crm
// Создадим communication_session,
// Синхронизируем консультантов
let crm_user_id = this.user.sessionData ? this.user.sessionData.crm_user_id : null
const uuid = this.user.sessionData?.uuid
const aMacAddr = this.user?.mac
const aDOM = this.user?.DOM ?? ""
const onlyAudio = obj.onlyAudio
const oSConsultant_id = obj.consultant_id
let widget, communication_session, communication_session_id
const users = getAuthorizedUsers()
const admins = users.filter(one => one.user_data.user_type_sysname === 'ADMIN')
let tokens = null
let FCMtokens = null
let user_id
let consultants_for_sites, consultants, stack
let device, deviceId, macAddr, is_exist_user
async.series({
createCrm: cb => {
async.series({
checkExist: cb => {
if (!crm_user_id) return cb(null)
// ...
},
add: cb => {
if (crm_user_id) return cb(null)
// ...
},
}, cb)
},
getOrCreateCommunicationSession: cb => {
// ...
},
syncCommunicationSession: cb => {
const o = {
command: 'syncCommunicationSessionLocal',
object: 'Widget',
params: {
widgetKey: widget.widgetKey,
communication_session,
syncCSConsultantStack: true
}
}
_t.apiSys(o, (err, res) => {
if (err) console.error(new MyError('Не удалось syncCommunicationSessionLocal', {o, err}))
consultants = res.consultants
stack = res.stack
cb(null) // Если не выполняем в фоне
})
},
syncConsultants: cb => {
// ...
},
// ...
sendCallNotification: async cb => {
const storage = getConsultantStorage()
// ...
},
sendMailNotification: cb => {
// ...
},
}, function (err, res) {
// ...
})
}
// Org_site_consultant.js (бэк)
Model.prototype.initConsultant = function (obj, cb) {
if (arguments.length === 1) {
cb = arguments[0]
obj = {}
}
var _t = this
var rollback_key = obj.rollback_key || rollback.create()
var doNotSaveRollback = obj.doNotSaveRollback || !!obj.rollback_key
let consultant = this.getConsultant()
let consultantInfo
const disconnectHandler = e => {
// запустим таймер и если не произойдет переподключения того же сокета
//, остановим сеанс и webRTC
consultant.disconnected = Date.now()
consultant.connected = null
const i = setInterval(() => {
if (consultant.socket.connected || !consultant.disconnected) {
// Успешно переподключились. Остановим
clearInterval(i)
return
}
if (Date.now() - consultant.disconnected >= 120000) {
// if (!consultant.connected) consultant.connected = Date.now()
clearInterval(i)
async.series({
endSession: cb => {
// Проверим что надо завершать
if (!consultant.communication_id) return cb(null)
const [err, info] =
this.getInfoByCommunicationId(consultant.communication_id)
if (err) {
console.error('Не удалось получить communication_session по консультанту (1054)', err)
return cb(null)
}
const communication_session = info.communication_session
if (finished_communication_session_statuses.includes(communication_session.status_sysname)) {
return cb(null)
}
communication_session.to_status = 'CONSULTANT_LEFT'
const o = {
command: 'syncConsultantStack',
object: 'Communication_session',
params: {
communication_session
}
}
_t.apiSys(o, (err, res) => {
consultant.endWebRTC()
if (err) console.error(new MyError('Не удалось syncConsultantStack', {o, err}))
cb(null)
})
},
}, err => {
// Ничего не надо
})
}
}, 100)
// ...
}
const pushConfirmHandler = data => {
const hash = data.hash
if (!hash) {
console.error('Consultant. pushConfirm. hash not passed')
return
}
const key = data.key
if (!key) {
console.error('Consultant. pushConfirm. key not passed')
return
}
if (consultant.remote[key]) {
if (consultant.remote[key].hash === hash) {
if (!consultant.remote[key].confirm) consultant.remote[key].confirm = true
consultantOnConfirm.call(this, consultant, key)
}
}
}
const handleWebRTCConsultant = data => {
consultant.setData('webRTC', {...data}, null, true, true)
}
if (consultant && !obj.useHere) {
if (consultant.socket.id !== this.user.socket.id && consultant.socket.connected) {
// Если это новый сокет, а старый при этом подключен (то есть вкладка не закрыта).
this.user.socket.removeAllListeners('disconnect')
// this.user.socket.removeListener('disconnect', disconnectHandler)
return cb(new UserError('Модуль используется в другой вкладке..',
{
usedElsewhere: true,
anotherSocketId: consultant.socket?.id
}))
}
} else if (consultant && obj.useHere) {
// Старому сокету нужно все завершить и перевести его в режим usedElsewhere
// Возможно в дальнейшем с запросом к пользователю. Перейти на другую вкладку? Да! Нет!
// ...
}
async.series({
createConsultant: cb => {
if (consultant) return cb(null)
_t.createConsultant({}, (err, res) => {
if (err) return cb(err)
consultant = res.consultant
cb(null)
})
},
addListenersAndResetRemoteAndSocket: cb => {
// ...
},
loadPrevFiles: async cb => {
// ...
},
syncLocal: cb => {
const user_ids = [consultant.user_id]
const params = {
user_ids,
}
skipConsultantRemote(params)
async.series({
sites: cb => {
// cb(null)
_t.syncConsultantSitesLocal(params, (err, res) => {
if (err) console.error(new MyError('Не удалось syncConsultantSitesLocal', {params, err}))
cb(null)
})
},
stack: cb => {
// ...
},
}, err => {
if (err) console.error(new MyError('Не удалось syncConsultantLocal', {o, err}))
cb(null)
})
},
getOrganizationOptions: cb => {
// ...
},
getSitesSettings: cb => {
// ...
},
// ...
syncRemote_:cb => {
cb(null)
},
}, function (err, res) {
// ...
})
}
Модуль консультанта
// CallScreen.js
const CallScreen = () => {
const selfMedia = 'consultMedia'
const serverClassName = 'Org_site_consultant'
const isCamExist = true // Мы отдельно заранее не проверяем наличие, как это сделано в виджете.
const {state, isIOS} = useApp()
const {
webRTC,
emitWebRTC,
answerSession,
chats,
chatId,
setChatId,
setCrmUserId,
options
} = useApp()
const [activeCamera, setActiveCamera] = useState(false)
// ...
const communicationSessionData = useRef()
useEffect(() => {
// if (!state.communications?.data) return
// const activeSessionMedia = state.communications?.data[state.communications?.data.length - 1]?.[selfMedia]?.is_audio
const activeSessionMedia = state.communications?.data
? state.communications?.data
.filter(session => ['ACCEPTED', 'IN_PROGRESS'].includes(session.status_sysname))
: null
communicationSessionData.current = activeSessionMedia?.length ? activeSessionMedia[0] : null
}, [state.communications?.data])
const getConstrainsSettingsImportantRef = useRef(null)
const isDisplayStreamedRef = useRef(false)
const [isDisplayStreamed, setIsDisplayStreamed] = useState(false)
const isVideoStreamedRef = useRef(communicationSessionData.current?.[selfMedia]?.is_video)
const [isVideoStreamed, setIsVideoStreamed] = useState(communicationSessionData.current?.[selfMedia]?.is_video)
// ...
//=================Set WebTRC Connecntion=================
const [crmUserIdFromComSession, setCrmUserIdFromComSession] = useState([])
const currentSessionIdRef = useRef()
const oldSessStatusRef = useRef()
// ...
const webRTCLocalRef = useRef({})
const clearWebRTCLocalTimerRef = useRef(null)
const [retryState, setRetryState] = useState({})
const webRTCData = webRTC?.data // webRTC?.data // !webRTC ? false : webRTC.data
const webRTCDataRef = useRef(webRTCData)
useEffect(() => {
webRTCDataRef.current = webRTCData
}, [webRTCData])
const endWebRTC = () => {
if (peerConnection.current) {
peerConnection.current.getSenders()?.forEach(sender => sender?.track?.stop())
stopShareScreen()
peerConnection.current.close()
peerConnection.current = null
}
if (localMediaStream.current) {
console.log('endWebRTC: CLEAR localMediaStream')
localMediaStream.current.getTracks().forEach(track => track.stop())
localMediaStream.current = null
}
if (localMediaElem.current) {
console.log('endWebRTC: CLEAR localMediaElem srcObject')
localMediaElem.current.srcObject = null
}
if (shareScreenStreamRef.current) {
shareScreenStreamRef.current.getTracks().forEach(track => track.stop())
shareScreenStreamRef.current = null
}
if (localMediaElemHandlersRef.current) {
localMediaElemHandlersRef.current.forEach(item => {
if (localMediaElem.current) localMediaElem.current?.removeEventListener(item.type, item.handler)
})
localMediaElemHandlersRef.current = []
}
if (remoteMediaElemHandlersRef.current) {
remoteMediaElemHandlersRef.current.forEach(item => {
if (remoteMediaElem.current) remoteMediaElem.current?.removeEventListener(item.type, item.handler)
})
remoteMediaElemHandlersRef.current = []
}
// setShareScreen(false)
if (remoteMediaElem.current) {
remoteMediaElem.current.srcObject = null
}
console.log('endWebRTC: webRTCLocalRef (before clear):', webRTCLocalRef.current)
if (clearWebRTCLocalTimerRef.current) clearTimeout(clearWebRTCLocalTimerRef.current)
webRTCLocalRef.current = {}
if (rebootWebRTCTimerRef.current) clearTimeout(rebootWebRTCTimerRef.current)
if (waitConnectionRestoreTimeoutRef.current) clearTimeout(waitConnectionRestoreTimeoutRef.current)
if (waitConnectionRestoreBeforeFinishTimeoutRef.current) {
clearTimeout(waitConnectionRestoreBeforeFinishTimeoutRef.current)
}
if (canvasAnimationIdRef.current) {
cancelAnimationFrame(canvasAnimationIdRef.current)
canvasAnimationIdRef.current = null
}
if (!communicationSessionData.current) stopRecording()
}
const rebootWebRTCHandler = async () => {
const o = {
command: 'rebootWebRTC',
object: 'Org_site_consultant'
}
const res = await socketQuery(o)
}
// Завершение webRTC по unmount и Скрытие карточки клиента
useEffect(() => {
// Реакция на unmount компонента.
return () => {
// Завершим webRTC
endWebRTC()
// Отдельно завершим все потоки, так как не всегда при endWebRTC надо завершать потоки
if (shareScreenStreamRef.current) {
shareScreenStreamRef.current.getTracks().forEach(track => track.stop())
shareScreenStreamRef.current = null
}
if (localMediaStream.current) {
localMediaStream.current.getTracks().forEach(track => track.stop())
localMediaStream.current = null
}
setCrmUserId(null)
}
}, [])
const defineDevices = async (options) => {
// Выполним заполнение справочников устройств
// ...
// logD('DEBUG:Devices==>', devices_)
audioinput_ = devices_.filter(d => d.kind === 'audioinput')
audiooutput_ = devices_.filter(d => d.kind === 'audiooutput')
videoinput_ = devices_.filter(d => d.kind === 'videoinput')
// Почему это так, см описание выше
audioComplexDevices_ = isMobile ? audioinput_ : audiooutput_
setDevices(devices_)
setAudioinput(audioinput_)
setAudiooutput(audiooutput_)
setVideoinput(videoinput_)
setAudioComplexDevices(audioComplexDevices_)
}
const getConstrains = (constrains = {}) => {
// const isVideo = constrains.is_video ?? webRTCData.session?.is_video
// ...
}
/**
* Попробует получить доступ к медиа ресурсам. Попробует несколько раз
* Если выйдет, то вернет поток, если нет, то залогирует и вернет null
* @param constrains
* @param params
* @return {Promise<ok|err>}
*/
async function getLocalMedia(constrains, params = {}) {
let stream
if (!constrains) {
return err('Не переданы defauldConstrains')
}
try {
stream = await navigator.mediaDevices.getUserMedia(constrains)
} catch (e) {
// Повторим попытку, спустя X.
// Если не выйдет попробуем взять только часть аудио (если было и то и то)
// ...
}
await defineDevices(options)
return ok('ok', {stream})
}
const waitConnectionRestoreTimeoutRef = useRef()
const waitConnectionRestoreBeforeFinishTimeoutRef = useRef()
function handleConnectionLoss() {
setLoaderText('Собеседник ушел. Подождем возвращения...')
waitConnectionRestoreTimeoutRef.current = setTimeout(() => {
setLoaderText('Собеседник не вернулся. Завершаем сеанс...')
waitConnectionRestoreBeforeFinishTimeoutRef.current = setTimeout(()=>{
if (state.currentVideoSession) finishCurrentCall(state.currentVideoSession)
}, 5000)
}, 12000)
}
function handleConnectionRestored() {
setLoaderText(null)
if (canvasAnimationIdRef.current) {
cancelAnimationFrame(canvasAnimationIdRef.current)
canvasAnimationIdRef.current = null
}
if (waitConnectionRestoreTimeoutRef.current) clearTimeout(waitConnectionRestoreTimeoutRef.current)
if (waitConnectionRestoreBeforeFinishTimeoutRef.current) {
clearTimeout(waitConnectionRestoreBeforeFinishTimeoutRef.current)
}
void rebuildMixer();
}
/**
* Создаст новый peer и установит подписки на события получения системных данных и треков от WebRTC
* Если передан офер, то создаст answer
* @return {Promise<{msg: string, code: number, data: {}, type: string}>}
*/
async function handleNewPeer({createOffer}) {
if (peerConnection.current) {
return console.warn(`Already connected`)
}
peerConnection.current = new RTCPeerConnection({
iceServers: webRTCData.iceServers
})
// ...
}
const firstTimeCamOrScreenOnForIOSRef = useRef(true)
/**
* Отправит новый дескрипшн.
* @return {Promise<{msg: string, code: number, data: {}, type: string}>}
*/
async function handleUpdatePeer() {
if (!peerConnection.current) {
console.warn('Connection is not set')
return
}
await peerConnection.current.setLocalDescription() // сбрасываем предыдущее описание
const offer = await peerConnection.current.createOffer()
await peerConnection.current.setLocalDescription(offer)
// ...
}
//=======================ON WebRTCData REACTIONS=================================
// реакция на start
useEffect(() => {
// Завершение webRTC Если пришел пустой
if (!webRTCData || !Object.keys(webRTCData).length) {
endWebRTC()
return
}
if (!webRTCData) return
if (webRTCData.connected) return
if ((webRTCData.start && webRTCLocalRef.current.started
&& webRTCLocalRef.current.started !== currentSessionIdRef.current)
|| (webRTCData.start && !currentSessionIdRef.current)) {
// ...
}
// if (webRTCData.start && !webRTCLocal.started) {
if (webRTCData.start && !webRTCLocalRef.current.started
&& !webRTCLocalRef.current.reoffer && !webRTCLocalRef.current.reanswer) {
console.log('СТАРТУЕМ', webRTCData, webRTCLocalRef.current, currentSessionIdRef.current)
// ...
startCapture()
.then(() => {
handleNewPeer({createOffer: webRTCData.createOffer})
.then(() => {
emitWebRTC({start_confirmed: Date.now()}) // Отправим на сервер подтверждение
})
.catch(e => {
logD('ERROR handleNewPeer', e)
endWebRTC()
})
})
.catch(e => {
logD('Error getting user media', e)
setHint(true)
})
// } else if (webRTCLocal.started) {
}
// }, [webRTC, reconnectWebRTCCounter])
}, [webRTCData, retryState])
// реакция на answer
useEffect(() => {
if (!webRTCData) return
if (webRTCData.connected) return
// ...
}, [webRTC])
// ...
// реакция на нового iceCandidate
useEffect(() => {
// ...
}, [])
//===================ANOTHER REACTIONS=======================================
// ...
/**
* Позволяет включить камеру или демонстрацию экрана заменив или добавив поток
* @param newTrack
* @return boolean true if added
*/
const addOrReplaceTrack = (newTrack) => {
if (!peerConnection.current) return false
const sender = peerConnection.current.getSenders().find(sender =>
sender.track?.kind === newTrack.kind
)
if (!sender) {
console.log('DEBUG:addOrReplaceTrack: peerConnection.current.addTrack. newTrack=', newTrack)
peerConnection.current.addTrack(newTrack, localMediaStream.current)
setLocalMediaFromPeer()
return true
}
// В будущем можно заменять только однотипные (экран/камера) треки, разделяя их например по label 'window...'
// Тогда можно будет передавать сразу оба трека и камера и экран
sender.replaceTrack(newTrack).then()
setLocalMediaFromPeer()
return false
}
//=======================SHARE============================
const shareScreenStreamRef = useRef(null)
// const endedDisplayMediaListener = async () => {
// // здесь обрабатываем случай отключения
// console.log('Screen sharing stopped')
// setIsDisplayStreamed(false)
// }
const isCamBeforeShare = useRef(false) // Была ли вкл камера, до демонстрации
// ...
//====================ANOTHER===========================
const finishCurrentCall = async id => {
if (!id) return
setFinishLoader(true)
const o = {
command: 'finishCommunication',
object: serverClassName,
params: {
communication_id: id,
noToastr: true
}
}
const res = await socketQuery(o)
answerSession(null)
}
// ...
const [consultantMedia, setConsultantMedia] = useState({})
// ...
useEffect(() => {
if (state.currentVideoSession === null) setFinishLoader(false)
}, [state.currentVideoSession])
// ...
// ================CHAT=========================
// ================RENDER======================
// Вывод экрана активной видео сессии
const displayCurrentVideoSession = (state.currentVideoSession === null)
? false
: <div className="callscreen__wrapper">
</div>
return (
<>
{displayCurrentVideoSession}
{(!state.warnMessage) ? false : <Warning/>}
</>
)
}
Модуль виджета
// ConsultantPreview.js
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
const ConsultantPreview = ({consult, startCallHandler, isCurrent, getActiveConsultantId, isWidgetResized, showWidgetHandler, isSmallWidth, callBtnColor, callBtnText}) => {
const serverParams = getServerParams()
if (!isCurrent) {
return (
<div className="slide" style={{minWidth: isWidgetResized ? 'auto' : '326px'}}>
<p className="slide-nickname"
style={{opacity: '0', display: isWidgetResized ? "none" : "block"}}>{consult.nickname}</p>
<img src={`${serverParams.url}/upload/${consult.user_image}`} alt={'preview'}/>
</div>
)
}
const previewImg = useRef(null)
// ...
const setNowPlay = (toPlay) => {
nowPlay.current = toPlay
setShowEl(toPlay ? toPlay.videoEl : previewImg)
}
const LOADER_PAUSE = 'LOADER_PAUSE'
const LOADER_LOAD_VIDEO = 'LOADER_LOAD_VIDEO'
const loader = useRef(false)
// ...
const [videoVolume, setVideoVolume] = useState(0)
const [videoBtnUnmute, setVideoBtnUnmute] = useState(false)
const [videoAutoUnmute, setVideoAutoUnmute] = useState(false)
useEffect(()=> {
setVideoVolume(parsedOptions?.video_volume)
setVideoBtnUnmute(parsedOptions?.video_btn_unmute)
setVideoAutoUnmute(parsedOptions?.video_auto_unmute)
setShowCalendar(parsedOptions?.calendar_init)
}, [ parsedOptions ])
let previews = null
const allVideoElements = [videoEl1]
const lockEls = useRef([]) // Заблокировать элементы, когда его уже выбрали но еще не использовали
const unlockEl = el => {
lockEls.current = lockEls.current.filter(one => one !== el)
}
const getNextVideoEl = () => {
const busyEls = [...lockEls.current]
const lockEl = () => {
const freeEls = allVideoElements.filter(one => !busyEls.includes(one))
const el = freeEls[0]
if (!el) return null
lockEls.current.push(el)
return el
}
if (!nowPlay.current) {
return lockEl()
}
// Найдем video которое сейчас играет. Исключим его DOM элемент и элементы всех последующих
let start = videosRef.current.indexOf(nowPlay.current)
if (start < 0) start = 0
for (let i = start; i < videosRef.current.length - 1; i++) {
const item = videosRef.current[i]
if (!busyEls.includes(item.videoEl)) {
busyEls.push(item.videoEl)
}
}
return lockEl()
}
const processPlay = () => {
if (!videosRef.current.length) return
const playing = videosRef.current.find(one => one.playing)
if (playing) return
let toPlay
for (const item of videosRef.current) {
if (item.played || item.error) continue
toPlay = item
break
}
// Нет видео в очереди готовых к воспроизведению.
// Выставим лоадер и запустим через две сек.
if (!toPlay) {
// Если лоадер === Pause то значит мы уже ждали пару сек и можно старотовать последнее
// ...
}
if (toPlay) {
if (!toPlay.loaded) return
// ...
}
}
const prepareVideo = (video) => {
const found = videosRef.current.find(one => one.video.filename === video.filename)
if (found) return true
// Если пока нет свобдных видеоэлементов, то тоже выходим
const videoEl = getNextVideoEl()
// ...
function onloadHandler(e) {
// ...
}
function onendHandler(e) {
// ...
}
videoEl.current.addEventListener('loadeddata', onloadHandler)
videoEl.current.addEventListener('ended', onendHandler)
try {
videoEl.current.src = video.url
videoEl.current.load()
} catch (e) {
console.warn('Не удалось загрузить файл', video.url)
videosRef.current = videosRef.current.map(one => {
if (one === videoObj) {
one.error = true
}
return one
})
processPlay()
if (previews && Array.isArray(previews)) processPreview(previews)
}
}
const processPreview = (previews) => {
// ...
}
useEffect(() => {
getActiveConsultantId(consult.id)
}, [isCurrent])
const muteRef = useRef(null)
// ...
return (
<div
className="slide"
style={{minWidth: isWidgetResized ? 'auto' : '326px'}}
key={consult.id}
id={consult.id}
onClick={(e) => {
if (isWidgetResized) {
if (options.data?.muted_preview) setMuted(false)
widgetResizeHandler(isWidgetResized)
return
}
startCallHandler(consult.id, consult)
}}>
{isWidgetResized && isSmallWidth
?
<span></span>
:
<p className={widgetColor !== ""
? `voda-slide-nickname voda-slide-nickname-color-${widgetColor}`
: "voda-slide-nickname"}
>
{
["IN_PROGRESS", "ACCEPTED"].includes(consult.call_status) ?
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="4" cy="4" r="4" fill="#ffb300"/>
</svg>
:
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="4" cy="4" r="4" fill="#2CE829"/>
</svg>
}
<span>{consult.nickname}</span>
</p>
}
{
["IN_PROGRESS", "ACCEPTED"].includes(consult.call_status) ?
<div className="voda-slide-busy-note" style={{bottom: busy_note_bottom}}>
<span>{contentSettings.consultant_preview[widgetLanguage].title} </span>
<span>{contentSettings.consultant_preview[widgetLanguage].note}</span>
</div>
: ''
}
<LiveMessage liveMsgs={consult?.liveMsgs}/>
<div className="voda-priview-btn-container" style={{bottom: consult_small_bottom}}>
<CalendarBtn addClass="preview" show={showCalendar} />
{
isWidgetResized && isSmallWidth
?
<div
style={ callBtnColor ? {backgroundColor: callBtnColor} : {} }
className={
consultVideo?.isYoutube
? "voda-preview-call-btn voda-preview-call-btn-is-youtube voda-is-small-width"
: "voda-preview-call-btn voda-is-small-width"
}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M14.062 5.62001C11.6967 5.37156 9.31285 5.35314 6.94398 5.56501L5.36198 5.70601C4.72717 5.76295 4.13007 6.03213 3.66704 6.47012C3.20401 6.90811 2.90208 7.48934 2.80998 8.12001C2.43378 10.693 2.43378 13.307 2.80998 15.88C2.9023 16.5108 3.20442 17.092 3.66762 17.53C4.13081 17.968 4.72805 18.2371 5.36298 18.294L6.94498 18.434C9.31381 18.6463 11.6976 18.6282 14.063 18.38L14.671 18.316C15.2745 18.2527 15.842 17.9981 16.2905 17.5893C16.739 17.1805 17.0451 16.6391 17.164 16.044L20.211 17.662C20.3204 17.7201 20.4425 17.7502 20.5663 17.7497C20.6902 17.7491 20.812 17.7178 20.9208 17.6587C21.0297 17.5996 21.1222 17.5144 21.1901 17.4108C21.258 17.3072 21.2992 17.1884 21.31 17.065L21.335 16.781C21.6139 13.5994 21.6139 10.3996 21.335 7.21801L21.31 6.93401C21.2991 6.81056 21.2578 6.69172 21.1898 6.58813C21.1218 6.48454 21.0291 6.39942 20.9202 6.34038C20.8112 6.28134 20.6893 6.25022 20.5654 6.24981C20.4414 6.24939 20.3193 6.2797 20.21 6.33801L17.164 7.95601C17.0451 7.36094 16.739 6.81948 16.2905 6.41072C15.842 6.00196 15.2745 5.74733 14.671 5.68401L14.062 5.62001ZM7.07798 7.05901C9.35001 6.8559 11.6364 6.87365 13.905 7.11201L14.513 7.17601C14.8101 7.20747 15.0882 7.33685 15.3037 7.54379C15.5191 7.75072 15.6596 8.02345 15.703 8.31901C16.061 10.76 16.061 13.239 15.703 15.681C15.6596 15.9766 15.5191 16.2493 15.3037 16.4562C15.0882 16.6632 14.8101 16.7926 14.513 16.824L13.905 16.888C11.6364 17.1264 9.35001 17.1441 7.07798 16.941L5.49598 16.799C5.197 16.7723 4.91571 16.6457 4.69746 16.4397C4.47921 16.2336 4.33673 15.96 4.29298 15.663C3.93782 13.2339 3.93782 10.7661 4.29298 8.33701C4.33636 8.03979 4.4787 7.76588 4.69699 7.55956C4.91529 7.35324 5.19679 7.22656 5.49598 7.20001L7.07798 7.05901ZM17.36 9.55001C17.509 11.18 17.509 12.82 17.36 14.45L19.907 15.803C20.0883 13.2709 20.0883 10.7291 19.907 8.19701L17.36 9.55001Z" fill="black"/>
</svg>
</div>
:
<div className={
consultVideo?.isYoutube ? "voda-preview-call-btn voda-preview-call-btn-is-youtube" : "voda-preview-call-btn"
}
style={
callBtnColor
? {bottom: consult_small_bottom, backgroundColor: callBtnColor}
: {bottom: consult_small_bottom}
}
>
{
callBtnText ? callBtnText : contentSettings.preview_call_btn[widgetLanguage].text
}
</div>
}
</div>
{!consultVideo
? <img
alt={'preview'}
src={`${serverParams.url}/upload/${consult.user_image}`}
className="voda-img_preview"
ref={previewImg}
/>
: <>
{!options.data?.muted_preview || consultVideo?.isYoutube
? false
: <MuteBtn onClick={(e)=>{
if (isWidgetResized) return
e.stopPropagation()
setMuted(!muted)
}} muted={muted} isWidgetResized={isWidgetResized} isSmallWidth={isSmallWidth} />}
{consultVideo?.isYoutube && (() => {
// Это для отладки
console.log('RENDER:', {isYoutube: consultVideo?.isYoutube, consultYoutubeVideoIsLoading})
return true
})()
?
<div className="voda-iframe-video-holder">
<div className="voda-iframe-video-overlay">
{
consultYoutubeVideoIsLoading
? <img
alt={'preview'}
src={`${serverParams.url}/upload/${consult.user_image}`}
className="voda-img_preview"
ref={previewImg}
/>
: false
}
</div>
<div className="voda-iframe-video-holder-inner" id="voda-iframe-video-holder-inner"></div>
</div>
:
<video
preload="none" playsInline autoPlay="autoplay" muted={muted} loop="loop"
id={`voda-video-layer-${1}`}>
<source src={consultVideo.url} type="video/mp4"/>
</video>
}
</>}
<span style={{
position: 'absolute',
width: '1px',
height: '1px',
background: '#ff0000',
left: '50%',
bottom: 0,
zIndex: 1000,
}}></span>
{options.data?.is_tablet ? <div className="voda-tablet-call-btn">Связаться с консультантом</div> : false}
</div>
)
}
export default ConsultantPreview
Модуль регистрации
// Registration.js
const REQUEST_TIMEOUT = 70000;
const Registration = () => {
const [currentSection, setCurrentSection] = useState(0)
const [isLoading, setIsLoading] = useState(false);
const [remainingTime, setRemainingTime] = useState(null);
const [serverMessage, setServerMessage] = useState('');
const [formData, setFormData] = useState({
greetings: {
name: '',
// phone: '',
email: '',
about_web: ''
},
platform: 'welcome'
})
const nextStepHandler = async () => {
const cfg = window.WELCOME_CONFIG || {}
if (currentSection === regStructure.length - 1) return;
// ...
const apiRequest = socketQuery({
command: 'createDemoCrmUser',
object: 'Quiz_registration',
params: { ...formData }
});
try {
// ...
} catch (err) {
clearInterval(countdownInterval);
setIsLoading(false);
setRemainingTime(null);
if (err.message === 'timeout') {
setHint(true);
setServerMessage('Сервер не ответил вовремя, попробуйте позже.');
setTimeout(() => {
setHint(false);
setServerMessage('');
}, 10000);
}
// Если таймаут, просто гасим лоадер, ответ игнорируется
}
if (currentScreen.screen === 'consultants') {
addConsultantHandler()
}
}
// ...
const validateData = (inputData, screen) => {
// ...
}
// ...
return (
<div className="voda-registration">
<Logo />
{/*// ...*/}
</div>
)
}
export default Registration