diff --git a/src/components/AlertRenderer/CustomizedAlert.tsx b/src/components/AlertRenderer/CustomizedAlert.tsx new file mode 100644 index 000000000..fab982bef --- /dev/null +++ b/src/components/AlertRenderer/CustomizedAlert.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { + styled, + RcTypography, + RcLink, + RcIcon, + palette2, +} from '@ringcentral/juno'; +import { + ReportAnIssue, + InfoBorder, + Check, +} from '@ringcentral/juno-icon'; + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledIcon = styled(RcIcon)` + margin-right: 6px; + vertical-align: middle; + display: inline-block; +`; + +const DetailContainer = styled.div` + margin-top: 4px; +`; + +const DetailTitle = styled(RcTypography)` + margin: 4px 0; +`; + +const ItemText = styled(RcTypography)` + margin-bottom: 4px; +`; + +function Detail({ title, items, onLinkClick }) { + return ( + + + {title} + + { + items.map((item, idx) => { + if (item.type === 'text' && item.text) { + return ( + + {item.text} + + ) + } + if (item.type === 'link' && item.text) { + return ( + + onLinkClick(item.id)} + target="_blank" + > + {item.text} + + + ); + } + return null; + }) + } + + ); +} + +const StyleMessage = styled(RcTypography)` + font-size: 1rem; + line-height: 1.5rem; +`; + +export function CustomizedAlert({ + message, + showMore = false, + onLinkClick, +}) { + const { payload, level } = message; + let icon = InfoBorder; + let iconColor; + if (level === 'danger') { + icon = ReportAnIssue; + iconColor = 'danger.f02'; + } else if (level === 'success') { + icon = Check; + iconColor = 'success.f02'; + } + return ( + + + + {payload && payload.alertMessage} + + {showMore && payload.details && payload.details.length > 0 && ( + payload.details.map((detail, idx) => ( + + )) + )} + + ) +} \ No newline at end of file diff --git a/src/components/AlertRenderer/index.js b/src/components/AlertRenderer/index.tsx similarity index 86% rename from src/components/AlertRenderer/index.js rename to src/components/AlertRenderer/index.tsx index 3ab82b579..69fc0d937 100644 --- a/src/components/AlertRenderer/index.js +++ b/src/components/AlertRenderer/index.tsx @@ -3,12 +3,15 @@ import React from 'react'; import FormattedMessage from '@ringcentral-integration/widgets/components/FormattedMessage'; import { RcLink } from '@ringcentral/juno'; import { styled } from '@ringcentral/juno/foundation'; +import { CustomizedAlert } from './CustomizedAlert'; const StyledLink = styled(RcLink)` font-size: 13px; `; -export function getAlertRenderer() { +export function getAlertRenderer({ + onThirdPartyLinkClick, +}) { return (message) => { if (message.message === 'allowMicrophonePermissionOnInactiveTab') { return () => 'Please go to your first opened tab with this widget to allow microphone permission for this call.'; @@ -23,7 +26,15 @@ export function getAlertRenderer() { return () => 'Sorry, stopping recording is not supported for this call. Please contact your account administrator to enable "Allow mute in auto recording".'; } if (message.message === 'showCustomAlertMessage') { - return () => message.payload.alertMessage; + return ({ message, showMore }) => { + return ( + + ); + } } if (message.message === 'noUnreadForOldMessages') { return () => 'Sorry, app can\'t mark old messages as unread.'; diff --git a/src/components/NotificationPanel/NotificationItem.tsx b/src/components/NotificationPanel/NotificationItem.tsx new file mode 100644 index 000000000..119011c58 --- /dev/null +++ b/src/components/NotificationPanel/NotificationItem.tsx @@ -0,0 +1,288 @@ +import type { FunctionComponent } from 'react'; +import React, { memo, useMemo, useState, useEffect} from 'react'; + +import classNames from 'classnames'; + +import type { RcSnackbarContentType } from '@ringcentral/juno'; +import { + combineProps, + RcSnackbarAction, + RcSnackbarContent, + RcText, + RcLink, + styled, + getParsePaletteColor, + css, +} from '@ringcentral/juno'; +import { Close as closeSvg } from '@ringcentral/juno-icon'; + +import type { + NotificationItemProps, + NotificationMessage, +} from '@ringcentral-integration/widgets/components/NotificationPanel/NotificationPanel.interface'; +import styles from '@ringcentral-integration/widgets/components/NotificationPanel/styles.scss'; + +export function getLevelType(level: NotificationMessage['level']) { + let type: RcSnackbarContentType; + switch (level) { + case 'warning': + type = 'warn'; + break; + case 'danger': + type = 'error'; + break; + default: + type = level; + } + return type; +} + +const RcSnackbarContentColors = { + success: { + textColor: 'neutral.f06', + bgColor: 'success.b01', + }, + error: { + textColor: 'danger.f02', + bgColor: 'danger.b01', + }, + info: { + textColor: 'neutral.f06', + bgColor: 'neutral.b03', + }, + warn: { + textColor: 'neutral.f06', + bgColor: 'warning.b02', + }, +}; + +const MoreLinkColors = { + success: 'action.primary', + error: 'action.primary', + info: 'action.primary', + warn: 'action.primary', +}; + +const StyledSnackbarContent = styled(RcSnackbarContent)` + color: ${({ type }) => getParsePaletteColor(RcSnackbarContentColors[type].textColor)}; + background-color: ${({ type }) => getParsePaletteColor(RcSnackbarContentColors[type].bgColor)}; + + .RcSnackbarContent-message { + a { + color: ${({ type }) => getParsePaletteColor(MoreLinkColors[type])}; + font-style: normal!important; + text-decoration: underline; + } + + /* .RcSnackbarAction-text { + a { + color: ${({ type }) => getParsePaletteColor(MoreLinkColors[type])}; + font-style: normal!important; + + } + } */ + } +`; + +const ShowMoreLink = styled(RcLink)` + font-size: 0.815rem; + line-height: 24px; +`; + +const CounterText = styled(RcText)` + font-size: 0.815rem; + line-height: 24px; + text-align: right; + flex: 1; +`; + +function ClosingCounter({ + ttl, +}) { + const [time, setTime] = useState(ttl / 1000); + useEffect(() => { + let newTime = ttl / 1000; + let timer; + function startTimer() { + timer = setTimeout(() => { + newTime -= 1; + setTime(newTime); + if (newTime > 0) { + startTimer(); + } + }, 1000); + }; + startTimer(); + return () => { + clearTimeout(timer); + newTime = 0; + }; + }, [ttl]); + return ( + + Closing in {time} sec... + + ); +} + +const StyledFooter = styled.div` + display: flex; + align-items: center; + flex-direction: row; + margin-right: -30px; + margin-top: 5px; + margin-bottom: 5px; +`; + +function Footer({ + showMore, + onShowMore, + ttl, + type, +}) { + return ( + + {showMore && ( + + + Show more + + + )} + {ttl > 0 && } + + ); +} + +interface NewNotificationItemProps extends NotificationItemProps { + cancelAutoDismiss: (id: string) => void; +} + +export const NotificationItem: FunctionComponent = memo( + ({ + data, + currentLocale, + brand, + dismiss, + getRenderer, + duration = 0, + animation: defaultAnimation, + backdropAnimation: defaultBackdropAnimation, + classes: { + snackbar: snackbarClass = {}, + backdrop: backdropClass = undefined, + } = {}, + size, + messageAlign, + fullWidth, + cancelAutoDismiss, + }) => { + const [showMore, setShowMore] = useState(false); + const Message = getRenderer(data); + const second = duration / 1000; + const { + id, + level, + classes = {}, + loading, + action, + animation = defaultAnimation, + backdropAnimation = defaultBackdropAnimation, + backdrop, + onBackdropClick, + ttl, + payload, + } = data; + + const type: RcSnackbarContentType = getLevelType(level); + + const animationStyle = useMemo( + () => ({ + animationDuration: `${second}s`, + }), + [second], + ); + return ( +
+ {backdrop && ( +
+ )} + + + { + ( + !showMore && payload && payload.details && payload.details.length > 0 + ) && ( +
0} + onShowMore={() => { + cancelAutoDismiss(id); + setShowMore(true); + }} + ttl={ttl} + type={type} + /> + ) + } + + } + action={ + action === undefined ? ( + { + dismiss(id); + }} + /> + ) : ( + action + ) + } + /> +
+ ); + }, +); + +NotificationItem.defaultProps = { + duration: 500, + classes: {}, + size: 'small', + messageAlign: 'left', + fullWidth: true, +}; diff --git a/src/components/NotificationPanel/index.tsx b/src/components/NotificationPanel/index.tsx new file mode 100644 index 000000000..d337f3a66 --- /dev/null +++ b/src/components/NotificationPanel/index.tsx @@ -0,0 +1,86 @@ +// TODO: should use juno animation to do that +import 'animate.css/animate.min.css'; + +import type { FunctionComponent } from 'react'; +import React, { useEffect, useState } from 'react'; + +import classNames from 'classnames'; + +import { useSleep } from '@ringcentral/juno'; + +import { NotificationItem } from './NotificationItem'; +import type { + NotificationMessage, + NotificationPanelProps, +} from '@ringcentral-integration/widgets/components/NotificationPanel/NotificationPanel.interface'; +import styles from '@ringcentral-integration/widgets/components/NotificationPanel/styles.scss'; + +export const NotificationPanel: FunctionComponent = ({ + entranceAnimation = 'fadeInDown', + exitAnimation = 'fadeOutUp', + backdropEntranceAnimation = 'fadeIn', + backdropExitAnimation = 'fadeOut', + duration = 500, + messages, + className, + ...rest +}) => { + const [currentMessages, setCurrentMessages] = useState(messages); + const { sleep, cancel } = useSleep(); + + useEffect(() => { + const updatedMessages: NotificationMessage[] = []; + // if length is grater means that is delete item. + if (currentMessages.length > messages.length) { + currentMessages.forEach((currentMessage) => { + const updatedMessage = { + ...currentMessage, + }; + // if that can't find this id, that means that is delete + if (!messages.some((m) => m.id === currentMessage.id)) { + updatedMessage.animation = exitAnimation; + updatedMessage.backdropAnimation = backdropExitAnimation; + } else { + updatedMessage.animation = ''; + updatedMessage.backdropAnimation = ''; + } + updatedMessages.push(updatedMessage); + }); + + setCurrentMessages(updatedMessages); + + if (duration > 0) { + sleep(duration) + .then(() => { + setCurrentMessages(messages); + }) + .catch(() => { + // ignore cancel + }); + } + } else { + cancel(); + setCurrentMessages(messages); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [messages]); + + return ( +
+ {currentMessages.map((data, i) => { + return ( + + ); + })} +
+ ); +}; diff --git a/src/containers/App/index.js b/src/containers/App/index.js index 27047332c..fc4c00c12 100644 --- a/src/containers/App/index.js +++ b/src/containers/App/index.js @@ -9,7 +9,6 @@ import { import GlipChat from '@ringcentral-integration/glip-widgets/containers/GlipChat'; import GlipGroups from '@ringcentral-integration/glip-widgets/containers/GlipGroups'; -import { NotificationContainer } from '@ringcentral-integration/widgets/containers/NotificationContainer'; import CallBadgeContainer from '@ringcentral-integration/widgets/containers/CallBadgeContainer'; import CallingSettingsPage from '@ringcentral-integration/widgets/containers/CallingSettingsPage'; import { CallsOnholdPage } from '@ringcentral-integration/widgets/containers/CallsOnholdPage'; @@ -24,7 +23,6 @@ import { ThemeContainer } from '@ringcentral-integration/widgets/containers/Them import { PhoneContext } from '@ringcentral-integration/widgets/lib/phoneContext'; -import { getAlertRenderer } from '../../components/AlertRenderer'; import ThirdPartyContactSourceIcon from '../../components/ThirdPartyContactSourceIcon'; import GenericMeetingPage from '../GenericMeetingPage'; import { formatMeetingInfo } from '../../lib/formatMeetingInfo'; @@ -55,6 +53,8 @@ import MeetingScheduleButton from '../ThirdPartyMeetingScheduleButton'; import ThirdPartySettingSectionPage from '../ThirdPartySettingSectionPage'; import ContactsPage from '../ContactsPage'; import CustomizedPage from '../CustomizedPage'; +import { NotificationContainer } from '../NotificationContainer'; + export default function App({ phone, showCallBadge, @@ -135,7 +135,6 @@ export default function App({ diff --git a/src/containers/NotificationContainer/index.ts b/src/containers/NotificationContainer/index.ts new file mode 100644 index 000000000..9f77717f2 --- /dev/null +++ b/src/containers/NotificationContainer/index.ts @@ -0,0 +1,8 @@ +import { connectModule } from '@ringcentral-integration/widgets/lib/phoneContext'; +import type { NotificationContainerProps } from '@ringcentral-integration/widgets/containers/NotificationContainer/NotificationContainer.interface'; +import { NotificationPanel } from '../../components/NotificationPanel'; + +export const NotificationContainer = connectModule< + any, + NotificationContainerProps +>((phone) => phone.alertUI)(NotificationPanel); diff --git a/src/lib/Adapter/index.js b/src/lib/Adapter/index.js index fb4811937..9a3729394 100644 --- a/src/lib/Adapter/index.js +++ b/src/lib/Adapter/index.js @@ -703,9 +703,9 @@ class Adapter extends AdapterCore { return this._widgetCurrentPath === '/history'; } - alertMessage({ message, level, ttl }) { + alertMessage({ message, level, ttl, details }) { return this._requestWithPostMessage('/custom-alert-message', { - message, level, ttl, + message, level, ttl, details, }); } diff --git a/src/modules/Adapter/index.js b/src/modules/Adapter/index.js index 42a7c813d..8f12aac10 100644 --- a/src/modules/Adapter/index.js +++ b/src/modules/Adapter/index.js @@ -393,6 +393,7 @@ export default class Adapter extends AdapterModuleCore { message: 'showCustomAlertMessage', payload: { alertMessage: data.alertMessage || data.body && data.body.message, + details: data.body && data.body.details, } }); this._postRCAdapterMessageResponse({ diff --git a/src/modules/AlertUI/index.ts b/src/modules/AlertUI/index.ts new file mode 100644 index 000000000..527d14d12 --- /dev/null +++ b/src/modules/AlertUI/index.ts @@ -0,0 +1,38 @@ +import { AlertUI as AlertUIBase } from '@ringcentral-integration/widgets/modules/AlertUI'; +import { Module } from '@ringcentral-integration/commons/lib/di'; +import type { + NotificationMessage, +} from '@ringcentral-integration/widgets/components/NotificationPanel/NotificationPanel.interface'; +import { getAlertRenderer } from '../../components/AlertRenderer'; + +@Module({ + name: 'AlertUI', + deps: ['ThirdPartyService'], +}) +export class AlertUI extends AlertUIBase { + constructor(deps) { + super(deps); + this._ignoreModuleReadiness(deps.thirdPartyService); + } + + getUIFunctions(options) { + const functions = super.getUIFunctions(options); + return { + ...functions, + getRenderer: (message: NotificationMessage) => { + const renderer = getAlertRenderer({ + onThirdPartyLinkClick: (id)=> this._deps.thirdPartyService.onClickLinkInAlertDetail(id), + })(message); + if (renderer) { + return renderer; + } + return functions.getRenderer(message); + }, + cancelAutoDismiss: (id) => { + this._deps.alert.update(id, { + ttl: 0, + }); + }, + }; + } +} \ No newline at end of file diff --git a/src/modules/Phone/index.js b/src/modules/Phone/index.js index d4e65abd0..0bb5bacc3 100644 --- a/src/modules/Phone/index.js +++ b/src/modules/Phone/index.js @@ -110,7 +110,6 @@ import { RingCentralExtensions } from '@ringcentral-integration/commons/modules/ import { ActiveCallsUI, } from '@ringcentral-integration/widgets/modules/ActiveCallsUI'; -import { AlertUI } from '@ringcentral-integration/widgets/modules/AlertUI'; import { CallBadgeUI, } from '@ringcentral-integration/widgets/modules/CallBadgeUI'; @@ -167,6 +166,7 @@ import hackSend from '../../lib/hackSend'; import lockRefresh from '../../lib/lockRefresh'; import { ActiveCallControl } from '../ActiveCallControl'; import Adapter from '../Adapter'; +import { AlertUI } from '../AlertUI'; import { AccountContacts } from '../AccountContacts'; import { AddressBook } from '../AddressBook'; import { Analytics } from '../Analytics'; diff --git a/src/modules/ThirdPartyService/index.ts b/src/modules/ThirdPartyService/index.ts index c598b25e8..c252d1d79 100644 --- a/src/modules/ThirdPartyService/index.ts +++ b/src/modules/ThirdPartyService/index.ts @@ -1415,6 +1415,19 @@ export default class ThirdPartyService extends RcModuleV2 { }); } + async onClickLinkInAlertDetail(id) { + if (!this._additionalButtonPath) { + console.warn('Button event is not registered, '); + return; + } + await requestWithPostMessage(this._additionalButtonPath, { + button: { + id, + type: 'linkInAlertDetail', + }, + }); + } + @computed(that => [that.customizedPages]) get customizedTabs() { return this.customizedPages.filter(x => x.type === 'tab').map(tab => ({