diff --git a/.loki/reference/chrome_Components_Icons_all.png b/.loki/reference/chrome_Components_Icons_all.png
index 9ee15f4ff..173c74e32 100644
Binary files a/.loki/reference/chrome_Components_Icons_all.png and b/.loki/reference/chrome_Components_Icons_all.png differ
diff --git a/.loki/reference/chrome_Components_Icons_phone.png b/.loki/reference/chrome_Components_Icons_phone.png
new file mode 100644
index 000000000..ea142fac6
Binary files /dev/null and b/.loki/reference/chrome_Components_Icons_phone.png differ
diff --git a/.loki/reference/chrome_Components_Icons_phoneOff.png b/.loki/reference/chrome_Components_Icons_phoneOff.png
new file mode 100644
index 000000000..d621a0f3a
Binary files /dev/null and b/.loki/reference/chrome_Components_Icons_phoneOff.png differ
diff --git a/src/components/Calls/CallIFrame.js b/src/components/Calls/CallIFrame.js
new file mode 100644
index 000000000..84ead3627
--- /dev/null
+++ b/src/components/Calls/CallIFrame.js
@@ -0,0 +1,14 @@
+import { h } from 'preact';
+
+import { Screen } from '../Screen';
+import { createClassName } from '../helpers';
+import styles from './styles.scss';
+
+
+export const CallIFrame = (url) => (
+
+
+
+
+
+);
diff --git a/src/components/Calls/CallNotification.js b/src/components/Calls/CallNotification.js
new file mode 100644
index 000000000..d163857ee
--- /dev/null
+++ b/src/components/Calls/CallNotification.js
@@ -0,0 +1,64 @@
+import { h } from 'preact';
+import { useState } from 'preact/compat';
+
+import I18n from '../../i18n';
+import PhoneAccept from '../../icons/phone.svg';
+import PhoneDecline from '../../icons/phoneOff.svg';
+import constants from '../../lib/constants';
+import { Avatar } from '../Avatar';
+import { Button } from '../Button';
+import { createClassName, getAvatarUrl } from '../helpers';
+import styles from './styles.scss';
+
+
+export const CallNotification = ({ callProvider, callerUsername, url, dispatch } = { callProvider: undefined, callerUsername: undefined, dispatch: undefined }) => {
+ const [show, setShow] = useState(!!callProvider && !!callerUsername && !!dispatch && !!url);
+
+ const acceptClick = async () => {
+ setShow(!{ show });
+
+ switch (callProvider) {
+ case constants.jitsiCallStartedMessageType: {
+ window.open(url);
+ await dispatch({ incomingCallAlert: null });
+ break;
+ }
+ case constants.webrtcCallStartedMessageType: {
+ // TODO: add webrtc code here
+ break;
+ }
+ }
+ };
+
+ const declineClick = async () => {
+ await dispatch({ incomingCallAlert: null });
+ };
+
+ return (
+
+ { show
+ ? (
+
+
+
+ { I18n.t('Incoming video Call') }
+
+
+
+
+
+
+ )
+ : null
+ }
+
);
+};
diff --git a/src/components/Calls/styles.scss b/src/components/Calls/styles.scss
new file mode 100644
index 000000000..f9127ce56
--- /dev/null
+++ b/src/components/Calls/styles.scss
@@ -0,0 +1,78 @@
+@import '../../styles/colors';
+@import '../../styles/variables';
+
+.call-notification {
+ position: relative;
+
+ display: flex;
+
+ width: 100%;
+ height: 50%;
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+
+ width: 100%;
+ height: 100%;
+
+ background: #1f2329;
+
+ font-weight: 600;
+ justify-content: space-evenly;
+
+ &-avatar {
+ display: flex;
+
+ margin: 0 auto;
+ align-self: flex-end;
+ }
+
+ &-message {
+ margin: 0 auto;
+
+ color: #ffffff;
+ }
+
+ &-actions {
+ display: flex;
+ flex-direction: row;
+
+ margin: 0 auto;
+ margin-bottom: 15px;
+
+ color: white;
+
+ align-items: flex-end;
+
+ > button {
+ margin-bottom: 0;
+ margin-left: 10px;
+ }
+
+ &-accept {
+ border-color: green;
+ background-color: #2de0a5;
+ }
+
+ &-decline {
+ border-color: red;
+ background-color: #f5455c;
+ }
+ }
+ }
+}
+
+.call-iframe {
+ position: relative;
+ top: 0;
+
+ display: flex;
+
+ width: 100%;
+ height: 50%;
+
+ &__content {
+ height: 100%;
+ }
+}
diff --git a/src/components/Messages/Message/index.js b/src/components/Messages/Message/index.js
index 70d815955..bd9540c03 100644
--- a/src/components/Messages/Message/index.js
+++ b/src/components/Messages/Message/index.js
@@ -1,7 +1,7 @@
import { h } from 'preact';
import I18n from '../../../i18n';
-import { getAttachmentUrl, memo, normalizeTransferHistoryMessage } from '../../helpers';
+import { getAttachmentUrl, memo, normalizeTransferHistoryMessage, normalizeCallTimeMessage } from '../../helpers';
import { AudioAttachment } from '../AudioAttachment';
import { FileAttachment } from '../FileAttachment';
import { ImageAttachment } from '../ImageAttachment';
@@ -22,6 +22,8 @@ import {
MESSAGE_TYPE_WELCOME,
MESSAGE_TYPE_LIVECHAT_CLOSED,
MESSAGE_TYPE_LIVECHAT_STARTED,
+ MESSAGE_WEBRTC_CALL,
+ MESSAGE_JITSI_CALL,
MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY,
} from '../constants';
@@ -80,7 +82,7 @@ const renderContent = ({
),
].filter(Boolean);
-const getSystemMessageText = ({ t, conversationFinishedMessage, transferData }) =>
+const getSystemMessageText = ({ t, conversationFinishedMessage, transferData, callStatus }) =>
(t === MESSAGE_TYPE_ROOM_NAME_CHANGED && I18n.t('Room name changed'))
|| (t === MESSAGE_TYPE_USER_ADDED && I18n.t('User added by'))
|| (t === MESSAGE_TYPE_USER_REMOVED && I18n.t('User removed by'))
@@ -89,6 +91,8 @@ const getSystemMessageText = ({ t, conversationFinishedMessage, transferData })
|| (t === MESSAGE_TYPE_WELCOME && I18n.t('Welcome'))
|| (t === MESSAGE_TYPE_LIVECHAT_CLOSED && (conversationFinishedMessage || I18n.t('Conversation finished')))
|| (t === MESSAGE_TYPE_LIVECHAT_STARTED && I18n.t('Chat started'))
+ || (t === MESSAGE_WEBRTC_CALL && normalizeCallTimeMessage(callStatus))
+ || (t === MESSAGE_JITSI_CALL && normalizeCallTimeMessage(callStatus))
|| (t === MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY && normalizeTransferHistoryMessage(transferData));
const getMessageUsernames = (compact, message) => {
diff --git a/src/components/Messages/constants.js b/src/components/Messages/constants.js
index 94be2ca13..6c8bc70ea 100644
--- a/src/components/Messages/constants.js
+++ b/src/components/Messages/constants.js
@@ -7,3 +7,5 @@ export const MESSAGE_TYPE_WELCOME = 'wm';
export const MESSAGE_TYPE_LIVECHAT_CLOSED = 'livechat-close';
export const MESSAGE_TYPE_LIVECHAT_STARTED = 'livechat-started';
export const MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY = 'livechat_transfer_history';
+export const MESSAGE_JITSI_CALL = 'webrtc_call_started';
+export const MESSAGE_WEBRTC_CALL = 'jitsi_call_started';
diff --git a/src/components/helpers.js b/src/components/helpers.js
index 7544d0d07..1fc372e1f 100644
--- a/src/components/helpers.js
+++ b/src/components/helpers.js
@@ -1,3 +1,6 @@
+import format from 'date-fns/format';
+import { parseISO } from 'date-fns/fp';
+import isToday from 'date-fns/isToday';
import { Component } from 'preact';
import { Livechat } from '../api';
@@ -113,7 +116,7 @@ export const createToken = () => Math.random().toString(36).substring(2, 15) + M
export const getAvatarUrl = (username) => (username ? `${ Livechat.client.host }/avatar/${ username }` : null);
-export const msgTypesNotRendered = ['livechat_video_call', 'livechat_navigation_history', 'au', 'command', 'uj', 'ul', 'livechat-close'];
+export const msgTypesNotRendered = ['livechat_video_call', 'livechat_navigation_history', 'au', 'command', 'uj', 'ul', 'livechat-close', 'webRTC_call_started', 'jitsi_call_started'];
export const canRenderMessage = ({ t }) => !msgTypesNotRendered.includes(t);
@@ -126,6 +129,20 @@ export const sortArrayByColumn = (array, column, inverted) => array.sort((a, b)
return 1;
});
+export const normalizeCallTimeMessage = (callStatus) => {
+ const timestamp = new Date().toISOString();
+ const time = format(parseISO(timestamp), isToday(parseISO(timestamp)) ? 'HH:mm' : 'dddd HH:mm');
+ if (!callStatus) {
+ return;
+ }
+ if (callStatus === 'accept') {
+ return I18n.t('call_start_time', { time });
+ }
+ if (callStatus === 'endCall') {
+ return I18n.t('call_end_time', { time });
+ }
+};
+
export const normalizeTransferHistoryMessage = (transferData) => {
if (!transferData) {
return;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index c5ed5d2cb..8654754c5 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -23,6 +23,7 @@
"drop_here_to_upload_a_file_e5f4dd60": "Drop here to upload a file",
"email_22a7d52d": "Email",
"enable_notifications_a3daf4b1": "Enable notifications",
+ "error_getting_call_alert": "Error occurred while receiving a call notification",
"error_closing_chat_4c5e29d7": "Error closing chat.",
"error_removing_user_data_ce507478": "Error removing user data.",
"error_starting_a_new_conversation_reason_a1b491a1": "Error starting a new conversation: %{reason}",
@@ -36,6 +37,8 @@
"from_transferred_the_chat_to_the_department_to_752ab298": "%{from} transferred the chat to the department %{to}",
"from_transferred_the_chat_to_to_15bdcb11": "%{from} transferred the chat to %{to}",
"gdpr_8b366c2b": "GDPR",
+ "call_start_time": "Call Started at %{time}",
+ "call_end_time": "Call Ended at %{time}",
"go_to_menu_options_forget_remove_my_personal_data__99c40934": "Go to **menu options → Forget/Remove my personal data** to request the immediate removal of your data.",
"hiddenelementscount_more_c017d614": "+ %{hiddenElementsCount} more",
"i_agree_df2ecbd4": "I Agree",
@@ -84,4 +87,4 @@
"your_spot_is_spot_a35cd288": "Your spot is #%{spot}",
"your_spot_is_spot_estimated_wait_time_estimatedwai_d0ff46e0": "Your spot is #%{spot} (Estimated wait time: %{estimatedWaitTime})"
}
-}
\ No newline at end of file
+}
diff --git a/src/icons/phone.svg b/src/icons/phone.svg
new file mode 100644
index 000000000..074a7c13c
--- /dev/null
+++ b/src/icons/phone.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/src/icons/phoneOff.svg b/src/icons/phoneOff.svg
new file mode 100644
index 000000000..a92af93a4
--- /dev/null
+++ b/src/icons/phoneOff.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/lib/constants.js b/src/lib/constants.js
index 014215c02..fd3463eb9 100644
--- a/src/lib/constants.js
+++ b/src/lib/constants.js
@@ -4,4 +4,6 @@ export default {
livechatConnectedAlertId: 'LIVECHAT_CONNECTED',
livechatDisconnectedAlertId: 'LIVECHAT_DISCONNECTED',
livechatQueueMessageId: 'LIVECHAT_QUEUE_MESSAGE',
+ webrtcCallStartedMessageType: 'webRTC_call_started',
+ jitsiCallStartedMessageType: 'jitsi_call_started',
};
diff --git a/src/lib/room.js b/src/lib/room.js
index e4e6c0e0b..076c16904 100644
--- a/src/lib/room.js
+++ b/src/lib/room.js
@@ -1,15 +1,18 @@
import { route } from 'preact-router';
import { Livechat } from '../api';
-import { setCookies, upsert, canRenderMessage } from '../components/helpers';
+import { setCookies, upsert, canRenderMessage, createToken } from '../components/helpers';
+import I18n from '../i18n';
import { store } from '../store';
import { normalizeAgent } from './api';
import Commands from './commands';
+import constants from './constants';
import { loadConfig, processUnread } from './main';
import { parentCall } from './parentCall';
import { normalizeMessage, normalizeMessages } from './threads';
import { handleTranscript } from './transcript';
+
const commands = new Commands();
export const closeChat = async ({ transcriptRequested } = {}) => {
@@ -22,11 +25,36 @@ export const closeChat = async ({ transcriptRequested } = {}) => {
route('/chat-finished');
};
+// TODO: use a separate event to listen to call start event. Listening on the message type isn't a good solution
+export const processCallMessage = async (message) => {
+ const { alerts } = store.state;
+ try {
+ await store.setState({ incomingCallAlert: {
+ show: true,
+ callProvider: message.t,
+ callerUsername: message.u.username,
+ ...message.customFields.jitsiCallUrl && { url: message.customFields.jitsiCallUrl },
+ } });
+ } catch (err) {
+ console.error(err);
+ const alert = { id: createToken(), children: I18n.t('error_getting_call_alert'), error: true, timeout: 5000 };
+ await store.setState({ alerts: (alerts.push(alert), alerts) });
+ }
+};
+
const processMessage = async (message) => {
+ const { incomingCallAlert } = store.state;
+ if (incomingCallAlert) {
+ // TODO: create a new event to handle the call dismiss event, currently we're just dismissing the call alert if a new message is sent which is not a good solution
+ await store.setState({ incomingCallAlert: null });
+ }
+
if (message.t === 'livechat-close') {
closeChat(message);
} else if (message.t === 'command') {
commands[message.msg] && commands[message.msg]();
+ } else if (message.t === constants.webrtcCallStartedMessageType || message.t === constants.jitsiCallStartedMessageType) {
+ await processCallMessage(message);
}
};
@@ -171,6 +199,11 @@ export const loadMessages = async () => {
if (messages && messages.length) {
const lastMessage = messages[messages.length - 1];
await store.setState({ lastReadMessageId: lastMessage && lastMessage._id });
+
+ // TODO: create a separate event for starting the call and checking if the call is ongoing
+ if (lastMessage.t === constants.webrtcCallStartedMessageType || lastMessage.t === constants.jitsiCallStartedMessageType) {
+ await processCallMessage(lastMessage);
+ }
}
};
diff --git a/src/routes/Chat/component.js b/src/routes/Chat/component.js
index 0fa0774b3..16152897b 100644
--- a/src/routes/Chat/component.js
+++ b/src/routes/Chat/component.js
@@ -2,6 +2,7 @@ import { Picker } from 'emoji-mart';
import { h, Component } from 'preact';
import { Button } from '../../components/Button';
+import { CallNotification } from '../../components/Calls/CallNotification';
import { Composer, ComposerAction, ComposerActions } from '../../components/Composer';
import { FilesDropTarget } from '../../components/FilesDropTarget';
import { FooterOptions, CharCounter } from '../../components/Footer';
@@ -118,6 +119,8 @@ export default class Chat extends Component {
registrationRequired,
onRegisterUser,
limitTextLength,
+ incomingCallAlert,
+ dispatch,
...props
}, {
atBottom = true,
@@ -144,6 +147,7 @@ export default class Chat extends Component {
onUpload={onUpload}
>
+ { incomingCallAlert && !!incomingCallAlert.show && }
(
lastReadMessageId,
triggerAgent,
queueInfo,
+ incomingCallAlert,
}) => (
(
nameFieldRegistrationForm={nameFieldRegistrationForm}
emailFieldRegistrationForm={emailFieldRegistrationForm}
limitTextLength={limitTextLength}
+ incomingCallAlert={incomingCallAlert}
/>
)}
diff --git a/src/store/index.js b/src/store/index.js
index 47d031d06..fdbc7b13e 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -34,9 +34,11 @@ const initialState = {
visible: true,
minimized: true,
unread: null,
+ incomingCallAlert: null,
+ ongoingCall: null, // TODO: store call info like url, startTime, timeout, etc here
};
-const dontPersist = ['messages', 'typing', 'loading', 'alerts', 'unread', 'noMoreMessages', 'modal'];
+const dontPersist = ['messages', 'typing', 'loading', 'alerts', 'unread', 'noMoreMessages', 'modal', 'incomingCallAlert', 'ongoingCall'];
export const store = new Store(initialState, { dontPersist });
if (process.env.NODE_ENV === 'development') {