diff --git a/src/api.js b/src/api.js index 85f731619..a12962a7c 100644 --- a/src/api.js +++ b/src/api.js @@ -1,9 +1,9 @@ import queryString from 'query-string'; import LivechatClient from '@rocket.chat/sdk/lib/clients/Livechat'; -const host = window.SERVER_URL - || queryString.parse(window.location.search).serverUrl - || (process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null); +const host = window.SERVER_URL + || queryString.parse(window.location.search).serverUrl + || (process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null); const useSsl = host && host.match(/^https:/) !== null; export const Livechat = new LivechatClient({ host, protocol: 'ddp', useSsl }); diff --git a/src/components/App/index.js b/src/components/App/index.js index 2f3822ce0..8a80194b4 100644 --- a/src/components/App/index.js +++ b/src/components/App/index.js @@ -2,6 +2,8 @@ import { Component } from 'preact'; import { Router, route } from 'preact-router'; import queryString from 'query-string'; +import { Livechat } from '../../api'; +import { sessionUpdate, userSessionWithoutLocation, userSessionPresence } from '../../lib/userSession'; import history from '../../history'; import Chat from '../../routes/Chat'; import LeaveMessage from '../../routes/LeaveMessage'; @@ -133,9 +135,25 @@ export class App extends Component { I18n.on('change', this.handleLanguageChange); } + async initSession() { + const { config: { settings: { allowCollectGuestLocation }, session } = {} } = this.props; + if (!session) { + if (allowCollectGuestLocation) { + sessionUpdate(); + } else { + await Livechat.sendSessionData(userSessionWithoutLocation); + userSessionPresence.init(); + } + } else { + // Update visit count for user + Livechat.updateVisitCount(session.token); + } + } + async initialize() { // TODO: split these behaviors into composable components await Connection.init(); + this.initSession(); this.handleTriggers(); CustomFields.init(); Hooks.init(); diff --git a/src/i18n/pt.json b/src/i18n/pt.json index feaaf7832..962482d9d 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -77,4 +77,4 @@ "your_spot_is_spot_a35cd288": "Seu lugar é #%{spot}", "your_spot_is_spot_estimated_wait_time_estimatedwai_d0ff46e0": "Seu lugar é #%{spot} (Tempo estimado: %{estimatedWaitTime})" } -} +} \ No newline at end of file diff --git a/src/i18n/pt_BR.json b/src/i18n/pt_BR.json index 987b5cb4a..425da239c 100644 --- a/src/i18n/pt_BR.json +++ b/src/i18n/pt_BR.json @@ -77,4 +77,4 @@ "your_spot_is_spot_a35cd288": "Seu lugar é #%{spot}", "your_spot_is_spot_estimated_wait_time_estimatedwai_d0ff46e0": "Seu lugar é #%{spot} (Tempo estimado: %{estimatedWaitTime})" } -} +} \ No newline at end of file diff --git a/src/lib/connection.js b/src/lib/connection.js index 2231fd646..bed1d4126 100644 --- a/src/lib/connection.js +++ b/src/lib/connection.js @@ -39,7 +39,7 @@ const Connection = { if (timer) { return; } - timer = setTimeout(async() => { + timer = setTimeout(async () => { try { clearTimeout(timer); timer = false; diff --git a/src/lib/main.js b/src/lib/main.js index 0a2b3c391..888df4cf7 100644 --- a/src/lib/main.js +++ b/src/lib/main.js @@ -36,6 +36,11 @@ export const loadConfig = async () => { }); }; +export const getToken = () => { + const { token } = store.state; + return token; +}; + export const processUnread = async () => { const { minimized, visible, messages } = store.state; if (minimized || !visible) { @@ -61,4 +66,3 @@ export const processUnread = async () => { await store.setState({ unread: unreadMessages.length }); } }; - diff --git a/src/lib/userPresence.js b/src/lib/userPresence.js index c1c5d1533..eb2ed2e1d 100644 --- a/src/lib/userPresence.js +++ b/src/lib/userPresence.js @@ -8,7 +8,7 @@ const awayTime = 300000; let self; let oldStatus; -const userPrensence = { +const userPresence = { init() { if (initiated) { @@ -80,4 +80,4 @@ const userPrensence = { }, }; -export default userPrensence; +export default userPresence; diff --git a/src/lib/userSession.js b/src/lib/userSession.js new file mode 100644 index 000000000..3a58f3858 --- /dev/null +++ b/src/lib/userSession.js @@ -0,0 +1,244 @@ +/* eslint-disable no-lonely-if */ +/* eslint-disable no-alert */ +import { getToken } from './main'; +import { Livechat } from '../api'; +import store from '../store'; + +const docActivityEvents = ['mousemove', 'mousedown', 'touchend', 'keydown']; +const token = getToken(); +let timer; +let initiated = false; +const awayTime = 300000; +let self; +let oldStatus; + +export const userSessionPresence = { + + init() { + if (initiated) { + return; + } + + initiated = true; + self = this; + store.on('change', this.handleStoreChange); + }, + + reset() { + initiated = false; + this.stopEvents(); + store.off('change', this.handleStoreChange); + }, + + stopTimer() { + timer && clearTimeout(timer); + }, + + startTimer() { + this.stopTimer(); + timer = setTimeout(this.setAway, awayTime); + }, + + handleStoreChange(state) { + if (!initiated) { + return; + } + + const { token } = state; + token ? self.startEvents() : self.stopEvents(); + }, + + startEvents() { + docActivityEvents.forEach((event) => { + document.addEventListener(event, this.setOnline); + }); + + window.addEventListener('focus', this.setOnline); + }, + + stopEvents() { + docActivityEvents.forEach((event) => { + document.removeEventListener(event, this.setOnline); + }); + + window.removeEventListener('focus', this.setOnline); + this.stopTimer(); + }, + + async setOnline() { + self.startTimer(); + if (oldStatus === 'online') { + return; + } + oldStatus = 'online'; + + await Livechat.updateSessionStatus('online', token); + }, + + async setAway() { + self.stopTimer(); + if (oldStatus === 'away') { + return; + } + oldStatus = 'away'; + await Livechat.updateSessionStatus('away', token); + }, +}; + +const deviceInfo = () => { + const module = { + options: [], + header: [navigator.platform, navigator.userAgent, navigator.appVersion, navigator.vendor, window.opera], + dataos: [ + { name: 'Windows Phone', value: 'Windows Phone', version: 'OS' }, + { name: 'Windows', value: 'Win', version: 'NT' }, + { name: 'iPhone', value: 'iPhone', version: 'OS' }, + { name: 'iPad', value: 'iPad', version: 'OS' }, + { name: 'Kindle', value: 'Silk', version: 'Silk' }, + { name: 'Android', value: 'Android', version: 'Android' }, + { name: 'PlayBook', value: 'PlayBook', version: 'OS' }, + { name: 'BlackBerry', value: 'BlackBerry', version: '/' }, + { name: 'Macintosh', value: 'Mac', version: 'OS X' }, + { name: 'Linux', value: 'Linux', version: 'rv' }, + { name: 'Palm', value: 'Palm', version: 'PalmOS' }, + ], + databrowser: [ + { name: 'Chrome', value: 'Chrome', version: 'Chrome' }, + { name: 'Firefox', value: 'Firefox', version: 'Firefox' }, + { name: 'Safari', value: 'Safari', version: 'Version' }, + { name: 'Internet Explorer', value: 'MSIE', version: 'MSIE' }, + { name: 'Opera', value: 'Opera', version: 'Opera' }, + { name: 'BlackBerry', value: 'CLDC', version: 'CLDC' }, + { name: 'Mozilla', value: 'Mozilla', version: 'Mozilla' }, + ], + init() { + const agent = this.header.join(' '); + const os = this.matchItem(agent, this.dataos); + const browser = this.matchItem(agent, this.databrowser); + + return { os, browser }; + }, + matchItem(string, data) { + let i = 0; + let j = 0; + let regex; + let regexv; + let match; + let matches; + let version; + + for (i = 0; i < data.length; i += 1) { + regex = new RegExp(data[i].value, 'i'); + match = regex.test(string); + if (match) { + regexv = new RegExp(`${ data[i].version }[- /:;]([\\d._]+)`, 'i'); + matches = string.match(regexv); + version = ''; + if (matches) { if (matches[1]) { matches = matches[1]; } } + if (matches) { + matches = matches.split(/[._]+/); + for (j = 0; j < matches.length; j += 1) { + if (j === 0) { + version += `${ matches[j] }.`; + } else { + version += matches[j]; + } + } + } else { + version = '0'; + } + return { + name: data[i].name, + version: parseFloat(version), + }; + } + } + return { name: 'unknown', version: 0 }; + }, + }; + + const info = module.init(); + return { + os: info.os.name, + osVersion: info.os.version, + browserName: info.browser.name, + browserVersion: info.browser.version, + }; +}; + +export const userSessionWithoutLocation = { + token, + deviceInfo: deviceInfo(), +}; + + +/** + * This is used to convert location to a default type we want to send to server + * @param {Object} location + * @returns {Object} + */ +const convertLocationToSend = (location) => ( + { + countryName: location.country || location.country_name, + countryCode: location.country_code, + city: location.city || location.state, + latitude: location.latitude, + longitude: location.longitude, + completLocation: `${ location.country }, ${ location.state }, ${ location.city }`, + }); + +/** + * This is used to get location details for user + * @param {Number} latitude + * @param {Number} longitude + * @returns {Object} + */ +const sessionInfo = async (latitude, longitude) => { + const { address } = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${ latitude }&lon=${ longitude }`, { + mode: 'cors', + headers: { + 'Access-Control-Allow-Origin': '*', + }, + }).then((res) => res.json()); + + const location = convertLocationToSend(address); + location.latitude = latitude; + location.longitude = longitude; + + return { + location, + token, + deviceInfo: deviceInfo(), + }; +}; + +/** + * This function works in following way + * 1. Asks for user location access + * 2. If not granted, sets locationAccess in store as false, just send the session information + * 3. If granted, sets location of user info to DB + */ +export const sessionUpdate = async () => { + if (navigator.geolocation) { + store.setState({ + locationAccess: true, + }); + navigator.geolocation.getCurrentPosition(async (position) => { + const userSession = await sessionInfo(position.coords.latitude, position.coords.longitude); + await Livechat.sendSessionData(userSession); + userSessionPresence.init(); + }, async (err) => { + // This means user has denied location access + // We need then to confirm location before starting the chat + // Save state of location access inside store. + if (err) { + store.setState({ + locationAccess: false, + }); + userSessionPresence.init(); + // Send user data without location + await Livechat.sendSessionData(userSessionWithoutLocation); + } + }); + } +}; diff --git a/src/routes/Chat/container.js b/src/routes/Chat/container.js index ee9b985cc..8171cae10 100644 --- a/src/routes/Chat/container.js +++ b/src/routes/Chat/container.js @@ -100,8 +100,12 @@ export class ChatContainer extends Component { } await this.grantUser(); - const { _id: rid } = await this.getRoom(); + const { _id: rid, msgs } = await this.getRoom(); const { alerts, dispatch, token, user } = this.props; + // This is a hack to fix room state in session when new chat is started + if (msgs === 1) { + await Livechat.updateSessionStatus('online', token); + } try { this.stopTypingDebounced.stop(); await Promise.all([ diff --git a/src/routes/Register/container.js b/src/routes/Register/container.js index fbb3b4ee2..a449d2d7e 100644 --- a/src/routes/Register/container.js +++ b/src/routes/Register/container.js @@ -30,6 +30,7 @@ export class RegisterContainer extends Component { await dispatch({ loading: true, department }); try { await Livechat.grantVisitor({ visitor: { ...fields, token } }); + await Livechat.updateVisitorSessionOnRegister({ visitor: { ...fields, token } }); parentCall('callback', ['pre-chat-form-submit', fields]); await loadConfig(); } finally { diff --git a/widget-demo.html b/widget-demo.html index 85cb7197c..4c674c551 100644 --- a/widget-demo.html +++ b/widget-demo.html @@ -1,7 +1,8 @@ - + + This is home page