diff --git a/manifest.json b/manifest.json index 3f376db..12389ed 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,7 @@ { "requiredSdkVersion": "~0.0.59", "name": "TypedCaptions", + "version": "0.0.4", "javascriptEntrypointUrl": "TypedCaptions.js", "localesBaseUrl": "locales", "dataChannels": [ diff --git a/package-lock.json b/package-lock.json index 014ebbe..05857fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.93", + "bigbluebutton-html-plugin-sdk": "0.0.94", "path": "^0.12.7", "react": "^18.2.0", "react-chat-elements": "^12.0.14", @@ -3787,9 +3787,9 @@ "license": "MIT" }, "node_modules/bigbluebutton-html-plugin-sdk": { - "version": "0.0.93", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.93.tgz", - "integrity": "sha512-oA6qdOmNm93tYGo0xedHF+tAeTrq3rgd7es7WP/VBWP/vMfLNOCjhLQ4BM55tckA1xhAGuEnwR0+SXBE7ot9hg==", + "version": "0.0.94", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.94.tgz", + "integrity": "sha512-sBTrpD03CdQ11CH6L1OA7gDe6vBqJ1RxHkNY68pNwhlB7pdrVwdhW++YzGLCTyBcih+TCetupAZ/dixM1a7IdQ==", "dependencies": { "@apollo/client": "^3.8.7", "@browser-bunyan/console-formatted-stream": "^1.8.0", diff --git a/package.json b/package.json index cd2ec22..18c3355 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.93", + "bigbluebutton-html-plugin-sdk": "0.0.94", "path": "^0.12.7", "react-intl": "^6.6.8", "react": "^18.2.0", diff --git a/public/locales/index.json b/public/locales/index.json new file mode 100644 index 0000000..1d234d3 --- /dev/null +++ b/public/locales/index.json @@ -0,0 +1,7 @@ +[ + "en.json", + "fr-FR.json", + "fr.json", + "index.json", + "pt-BR.json" +] diff --git a/src/common/types.ts b/src/common/types.ts index 9fc39a4..7599a7c 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -3,8 +3,9 @@ export interface CaptionMessage { locale: string; } -export interface CaptionMenu { +export interface ActiveCaptionMenuInformation { captionLocale: string; + userId: string; } export interface sidekickMenuLocale { diff --git a/src/components/main/component.tsx b/src/components/main/component.tsx deleted file mode 100644 index 9301538..0000000 --- a/src/components/main/component.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; -import { createIntl, defineMessages } from 'react-intl'; - -import { - ActionButtonDropdownOption, - ActionButtonDropdownSeparator, - BbbPluginSdk, - DataChannelTypes, - GenericContentSidekickArea, - PluginApi, -} from 'bigbluebutton-html-plugin-sdk'; - -import { TypedCaptionsProps } from './types'; -import { TypedCaptionsModal } from '../modal/component'; -import { CaptionMenu } from '../../common/types'; -import { TypedCaptionsSidekickArea } from '../typed-captions-sidekick-content/component'; - -const intlMessages = defineMessages({ - writeCC: { - id: 'plugin.actionButtonDropdown.write', - description: 'action button dropdown label to start writing', - }, - stopCC: { - id: 'plugin.actionButtonDropdown.remove', - description: 'action button dropdown label to start writing', - }, - sectionName: { - id: 'plugin.actionButtonDropdown.sidekickComponent.sectionName', - description: 'name of the sidekick component section', - }, - menuTitle: { - id: 'plugin.actionButtonDropdown.sidekickComponent.menuTitle', - description: 'title of the sidekick component menu (internal part)', - }, -}); - -function TypedCaptions( - { pluginUuid: uuid }: TypedCaptionsProps, -): React.ReactElement { - BbbPluginSdk.initialize(uuid); - - const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); - - const { - data: captionMenusResponseFromDataChannel, - pushEntry: pushCaptionMenuResponseFromDataChannel, - deleteEntry: excludeCaptionMenuResponseFromDataChannel, - } = pluginApi.useDataChannel('typed-captions-data-channel', DataChannelTypes.ALL_ITEMS, 'caption-menus'); - - const { - messages, - currentLocale, - loading: localeMessagesLoading, - } = pluginApi.useLocaleMessages({ - headers: { - 'ngrok-skip-browser-warning': 'any', - }, - }); - - const intl = (!localeMessagesLoading && messages) ? createIntl({ - locale: currentLocale, - messages, - fallbackOnEmptyString: true, - }) : null; - - const [captionLocale, setCaptionLocale] = React.useState(''); - - const currentUserResponse = pluginApi.useCurrentUser(); - - const [isModalOpen, setIsModalOpen] = React.useState(false); - - const onRequestClose = () => { - setIsModalOpen(false); - }; - - /// contentFunction, name, section, buttonIcon - React.useEffect(() => { - if (!localeMessagesLoading && captionMenusResponseFromDataChannel?.data && currentUserResponse?.data?.role === 'MODERATOR') { - const sectionName = intl.formatMessage(intlMessages.sectionName); - const sidekickMenuComponentList = captionMenusResponseFromDataChannel?.data - .map((menu) => new GenericContentSidekickArea({ - name: intl.formatMessage(intlMessages.menuTitle, { - 0: menu.payloadJson.captionLocale, - }), - buttonIcon: 'closed_caption', - section: sectionName, - open: true, - contentFunction: (element: HTMLElement) => { - const root = ReactDOM.createRoot(element); - root.render( - - - , - ); - return root; - }, - })); - pluginApi.setGenericContentItems(sidekickMenuComponentList); - } - }, [captionMenusResponseFromDataChannel, localeMessagesLoading, messages]); - - React.useEffect(() => { - if (currentUserResponse?.data?.role === 'MODERATOR') { - let captionLocaleFromMenus = ''; - if (captionLocale === '') { - captionMenusResponseFromDataChannel?.data?.forEach((item) => { - if (item.fromUserId === currentUserResponse?.data?.userId) { - setCaptionLocale(item.payloadJson.captionLocale); - captionLocaleFromMenus = item.payloadJson.captionLocale; - } - }); - } - const entryIdToRemove = captionMenusResponseFromDataChannel?.data?.filter( - (item) => item.payloadJson.captionLocale === captionLocale - || captionLocaleFromMenus === item.payloadJson.captionLocale, - )[0]?.entryId; - let actionButtonDropdownOnClick = () => { - excludeCaptionMenuResponseFromDataChannel([entryIdToRemove]); - }; - let actionButtonDropdownLabel = ''; - if (intl) { - if (!entryIdToRemove) { - actionButtonDropdownLabel = intl.formatMessage(intlMessages.writeCC); - actionButtonDropdownOnClick = () => { - setIsModalOpen(true); - }; - } else actionButtonDropdownLabel = intl.formatMessage(intlMessages.stopCC); - } - if (!localeMessagesLoading) { - pluginApi.setActionButtonDropdownItems([ - new ActionButtonDropdownSeparator(), - new ActionButtonDropdownOption({ - icon: 'closed_caption', - label: actionButtonDropdownLabel, - tooltip: 'this is a button injected by plugin', - allowed: true, - onClick: actionButtonDropdownOnClick, - }), - ]); - } - } else { - pluginApi.setActionButtonDropdownItems([]); - pluginApi.setGenericContentItems([]); - } - }, [currentUserResponse, captionMenusResponseFromDataChannel, localeMessagesLoading, messages]); - - return ( - - ); -} - -export default TypedCaptions; diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 588bddc..e1d5ba3 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -1,26 +1,20 @@ -import * as BbbPluginSdk from 'bigbluebutton-html-plugin-sdk'; import { defineMessages, IntlShape } from 'react-intl'; import * as React from 'react'; -import Styled from './styles'; +import * as Styled from './styles'; import LocalesDropdown from './locales-dropdown/component'; -import './styles.css'; -import { AVAILABLE_LOCALES, CAPTIONS_CONFIG_LANGUAGES } from './constants'; -import { AvailableLocaleObject, CaptionMenu } from '../../common/types'; +import { AvailableLocaleObject } from '../../common/types'; -interface TypedCaptionsModalProps { +interface TypedCaptionsModalComponentProps { isOpen: boolean; intl: IntlShape; onRequestClose: () => void; - setIsOpen: (value: boolean) => void; - availableCaptionMenus: BbbPluginSdk.DataChannelEntryResponseType[]; - pushCaptionMenu: BbbPluginSdk.PushEntryFunction; + handleStart: React.MouseEventHandler; + handleChange: (event: React.ChangeEvent) => void; + availableLocales: AvailableLocaleObject[]; captionLocale: string; - setCaptionLocale: (value: string) => void; - pluginApi: BbbPluginSdk.PluginApi; + errorMessage: string; } -const TIMEOUT_RENDER_ERROR = 3000; - const intlMessages = defineMessages({ selectorLabel: { id: 'plugin.actionButtonDropdown.modal.selectorLabel', @@ -36,82 +30,33 @@ const intlMessages = defineMessages({ }, }); -function TypedCaptionsModal(props: TypedCaptionsModalProps) { +function TypedCaptionsModalComponent(props: TypedCaptionsModalComponentProps) { const { isOpen, onRequestClose, - setIsOpen, - availableCaptionMenus, - pushCaptionMenu, - captionLocale: locale, - setCaptionLocale: setLocale, - pluginApi, intl, + handleChange, + availableLocales, + captionLocale, + errorMessage, + handleStart, } = props; - const [availableLocales, setAvailableLocales] = React.useState([]); - const [errorMessage, setErrorMessage] = React.useState(''); - - React.useEffect(() => { - const filteredLocales = AVAILABLE_LOCALES.filter( - (l) => CAPTIONS_CONFIG_LANGUAGES.includes(l?.locale), - ); - setAvailableLocales(filteredLocales as AvailableLocaleObject[]); - - return () => { - setIsOpen(false); - setAvailableLocales([]); - }; - }, []); - - const setError = (message: string) => { - setErrorMessage(message); - setTimeout(() => { - setErrorMessage(''); - }, TIMEOUT_RENDER_ERROR); - }; - - const handleStart: React.MouseEventHandler = () => { - const alreadyUsedEntryId = availableCaptionMenus?.filter( - (item) => item.payloadJson.captionLocale === locale, - )[0]?.entryId; - if (locale !== '' && !alreadyUsedEntryId) { - pluginApi.serverCommands.caption.addLocale(locale); - pushCaptionMenu({ captionLocale: locale }); - setIsOpen(false); - } else if (locale === '') { - setError('Please select a language!'); - } else if (alreadyUsedEntryId) { - setError('This language is already in the typed-captions menu'); - } - }; - - const handleCloseModal = () => { - setIsOpen(false); - }; - - const handleChange: React.EventHandler> = (event) => { - setLocale(event.target.value); - }; - if (!intl) return null; const selectorLabel = intl.formatMessage(intlMessages.selectorLabel); return isOpen && ( document.querySelector('#modals-container')} - overlayClassName="modal-overlay" - {...{ - isOpen, - onRequestClose, - setIsOpen, - }} + parentSelector={() => document.querySelector('#modals-container') as HTMLElement} + overlayClassName="modalOverlay" + isOpen={isOpen} + onRequestClose={onRequestClose} > { - handleCloseModal(); + onRequestClose(); }} > ('typed-captions-data-channel', BbbPluginSdk.DataChannelTypes.ALL_ITEMS, 'caption-menus'); + + const availableCaptionMenus = activeCaptionMenusResponseFromDataChannel?.data || []; + + const currentUser = pluginApi.useCurrentUser!(); + + const userId = currentUser?.data?.userId || ''; + + const [captionLocale, setCaptionLocale] = useState(''); + + const [availableLocales, setAvailableLocales] = React.useState([]); + const [errorMessage, setErrorMessage] = React.useState(''); + + useEffect(() => { + const filteredLocales = AVAILABLE_LOCALES.filter( + (l) => CAPTIONS_CONFIG_LANGUAGES.includes(l?.locale), + ); + setAvailableLocales(filteredLocales as AvailableLocaleObject[]); + + return () => { + onRequestClose(); + setAvailableLocales([]); + }; + }, []); + + const setError = (message: string) => { + setErrorMessage(message); + setTimeout(() => { + setErrorMessage(''); + }, TIMEOUT_RENDER_ERROR); + }; + + const handleStart: React.MouseEventHandler = () => { + const alreadyUsedEntryId = availableCaptionMenus?.filter( + (item) => item.payloadJson.captionLocale === captionLocale, + )[0]?.entryId; + if (captionLocale !== '' && !alreadyUsedEntryId) { + pluginApi.serverCommands!.caption.addLocale(captionLocale); + pushActiveCaptionMenu({ + captionLocale, + userId, + }); + onRequestClose(); + } else if (captionLocale === '') { + setError('Please select a language!'); + } else if (alreadyUsedEntryId) { + setError('This language is already in the typed-captions menu'); + } + }; + + const handleChange: React.EventHandler> = (event) => { + setCaptionLocale(event.target.value); + }; + + return ( + + ); +} + +export { TypedCaptionsModalContainer }; diff --git a/src/components/modal/hooks.ts b/src/components/modal/hooks.ts new file mode 100644 index 0000000..681b19f --- /dev/null +++ b/src/components/modal/hooks.ts @@ -0,0 +1,99 @@ +import { + ActionButtonDropdownOption, + ActionButtonDropdownSeparator, + DataChannelTypes, + PluginApi, +} from 'bigbluebutton-html-plugin-sdk'; +import { useEffect, useState } from 'react'; +import { + defineMessages, + IntlShape, +} from 'react-intl'; +import { ActiveCaptionMenuInformation } from '../../common/types'; + +const intlMessages = defineMessages({ + writeCC: { + id: 'plugin.actionButtonDropdown.write', + description: 'action button dropdown label to start writing', + }, + stopCC: { + id: 'plugin.actionButtonDropdown.remove', + description: 'action button dropdown label to start writing', + }, +}); + +export const useActionsButtonManager = ( + pluginApi: PluginApi, + intl: IntlShape | null, + localeMessagesLoading: boolean, +) => { + const { + data: activeCaptionMenusResponseFromDataChannel, + deleteEntry: deleteActiveCaptionMenuResponseFromDataChannel, + } = pluginApi.useDataChannel!( + 'typed-captions-data-channel', + DataChannelTypes.ALL_ITEMS, + 'caption-menus', + ); + + const [captionLocale, setCaptionLocale] = useState(''); + + const currentUserResponse = pluginApi.useCurrentUser!(); + + const currentUserId = currentUserResponse?.data?.userId || ''; + + const [isModalOpen, setIsModalOpen] = useState(false); + + const onRequestClose = () => { + setIsModalOpen(false); + }; + useEffect(() => { + if (currentUserResponse?.data?.role === 'MODERATOR') { + if (captionLocale === '') { + activeCaptionMenusResponseFromDataChannel?.data?.forEach((item) => { + if (item.payloadJson.userId === currentUserId) { + setCaptionLocale(item.payloadJson.captionLocale); + } + }); + } + const entryIdToRemove = activeCaptionMenusResponseFromDataChannel?.data?.filter( + (item) => item.payloadJson.userId === currentUserId, + )[0]?.entryId || ''; + let actionButtonDropdownOnClick = () => { + deleteActiveCaptionMenuResponseFromDataChannel([entryIdToRemove]); + }; + let actionButtonDropdownLabel = ''; + if (intl) { + if (!entryIdToRemove) { + actionButtonDropdownLabel = intl.formatMessage(intlMessages.writeCC); + actionButtonDropdownOnClick = () => { + setIsModalOpen(true); + }; + } else actionButtonDropdownLabel = intl.formatMessage(intlMessages.stopCC); + } + if (!localeMessagesLoading && intl) { + pluginApi.setActionButtonDropdownItems([ + new ActionButtonDropdownSeparator(), + new ActionButtonDropdownOption({ + icon: 'closed_caption', + label: actionButtonDropdownLabel, + tooltip: 'this is a button injected by plugin', + allowed: true, + onClick: actionButtonDropdownOnClick, + }), + ]); + } + } else { + pluginApi.setActionButtonDropdownItems([]); + pluginApi.setGenericContentItems([]); + } + }, [ + currentUserResponse, + activeCaptionMenusResponseFromDataChannel, + localeMessagesLoading, intl]); + + return { + isModalOpen, + onRequestClose, + }; +}; diff --git a/src/components/modal/styles.css b/src/components/modal/styles.css deleted file mode 100644 index 8c6c40a..0000000 --- a/src/components/modal/styles.css +++ /dev/null @@ -1,12 +0,0 @@ -.modal-overlay { - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(6, 23, 42, 0.75); -} diff --git a/src/components/modal/styles.ts b/src/components/modal/styles.ts index b6a94a2..18804c2 100644 --- a/src/components/modal/styles.ts +++ b/src/components/modal/styles.ts @@ -66,7 +66,7 @@ export { borderSizeSmall, }; -const ModalScrollboxVertical = styled(ReactModal)` +const ModalScrollBoxVertical = styled(ReactModal)` position: relative; z-index: 1000 !important; @@ -123,7 +123,7 @@ const ModalScrollboxVertical = styled(ReactModal)` } `; -const TypedCaptionsModal = styled(ModalScrollboxVertical)` +const TypedCaptionsModal = styled(ModalScrollBoxVertical)` min-height: 30vh; `; @@ -259,7 +259,7 @@ const WriterMenuSelect = styled.div` } `; -export default { +export { ErrorLabel, CloseButton, BaseButton, diff --git a/src/components/typed-captions-sidekick-content/caption-messages-list/component.tsx b/src/components/typed-captions-panel/caption-messages-list/component.tsx similarity index 89% rename from src/components/typed-captions-sidekick-content/caption-messages-list/component.tsx rename to src/components/typed-captions-panel/caption-messages-list/component.tsx index d8e96ec..16500fa 100644 --- a/src/components/typed-captions-sidekick-content/caption-messages-list/component.tsx +++ b/src/components/typed-captions-panel/caption-messages-list/component.tsx @@ -1,15 +1,18 @@ import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; import * as React from 'react'; +import { IntlShape } from 'react-intl'; import { CaptionMessage } from '../../../common/types'; import Styled from './styles'; interface CaptionMessagesListProps { captionMessagesResponse: DataChannelEntryResponseType[]; + intl: IntlShape; } export function CaptionMessagesList(props: CaptionMessagesListProps) { const { captionMessagesResponse, + intl, } = props; return ( @@ -17,7 +20,7 @@ export function CaptionMessagesList(props: CaptionMessagesListProps) { {captionMessagesResponse?.slice(0).reverse().map((item) => { const { createdAt, payloadJson } = item; const dateCreatedAt = new Date(createdAt); - const time = `${dateCreatedAt.getHours()}:${dateCreatedAt.getMinutes()}`; + const time = intl.formatTime(dateCreatedAt); const { text } = payloadJson; return ( ); } + +export const renderComponent = ( + uuid: string, + intl: IntlShape, + captionLocale: string, +) => (element: HTMLElement) => { + const root = ReactDOM.createRoot(element); + root.render( + + + , + ); + return root; +}; diff --git a/src/components/typed-captions-sidekick-content/input-captions/component.tsx b/src/components/typed-captions-panel/input-captions/component.tsx similarity index 100% rename from src/components/typed-captions-sidekick-content/input-captions/component.tsx rename to src/components/typed-captions-panel/input-captions/component.tsx diff --git a/src/components/typed-captions-sidekick-content/input-captions/styles.ts b/src/components/typed-captions-panel/input-captions/styles.ts similarity index 89% rename from src/components/typed-captions-sidekick-content/input-captions/styles.ts rename to src/components/typed-captions-panel/input-captions/styles.ts index ee14f12..be6b073 100644 --- a/src/components/typed-captions-sidekick-content/input-captions/styles.ts +++ b/src/components/typed-captions-panel/input-captions/styles.ts @@ -9,7 +9,6 @@ import { colorBlueLight, colorGrayLighter, colorLink, - colorPrimary, colorText, colorWhite, fontSizeBase, @@ -42,26 +41,25 @@ const Input = styled(TextareaAutosize)` -webkit-appearance: none; padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25); resize: none; - transition: none; + transition: box-shadow 0.1s ease-in-out; border-radius: ${borderRadius}; font-size: ${fontSizeBase}; line-height: 1; min-height: 2.5rem; max-height: 10rem; border: 1px solid ${colorGrayLighter}; - box-shadow: 0 0 0 1px ${colorGrayLighter}; + box-shadow: none; &:focus { - border-radius: ${borderSize}; - box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary}; + border-radius: ${borderRadius}; + box-shadow: 0 0 0 1px ${colorBlueLight} inset; + border-color: ${colorBlueLight}; } &:hover, &:active, &:focus { - outline: transparent; - outline-style: dotted; - outline-width: ${borderSize}; + outline: none; } `; diff --git a/src/components/typed-captions-sidekick-content/styles.ts b/src/components/typed-captions-panel/styles.ts similarity index 100% rename from src/components/typed-captions-sidekick-content/styles.ts rename to src/components/typed-captions-panel/styles.ts diff --git a/src/index.tsx b/src/index.tsx index 0d50eda..57d8d8f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import TypedCaptions from './components/main/component'; +import TypedCaptions from './main/component'; const uuid = document.currentScript?.getAttribute('uuid') || 'root'; diff --git a/src/main/component.tsx b/src/main/component.tsx new file mode 100644 index 0000000..9b9f7c3 --- /dev/null +++ b/src/main/component.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { + BbbPluginSdk, + PluginApi, +} from 'bigbluebutton-html-plugin-sdk'; + +import { TypedCaptionsProps } from './types'; +import { TypedCaptionsModalContainer } from '../components/modal/container'; +import { useGetInternationalization, useTypedCaptionsPanelManager } from './hooks'; + +function TypedCaptions( + { pluginUuid: uuid }: TypedCaptionsProps, +): React.ReactElement { + BbbPluginSdk.initialize(uuid); + + const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); + + const { + intl, + localeMessagesLoading, + } = useGetInternationalization(pluginApi); + + useTypedCaptionsPanelManager( + pluginApi, + intl, + localeMessagesLoading, + uuid, + ); + + return (intl + ? ( + + ) : null + ); +} + +export default TypedCaptions; diff --git a/src/main/hooks.ts b/src/main/hooks.ts new file mode 100644 index 0000000..11c6227 --- /dev/null +++ b/src/main/hooks.ts @@ -0,0 +1,97 @@ +import { + DataChannelTypes, + GenericContentSidekickArea, + PluginApi, +} from 'bigbluebutton-html-plugin-sdk'; +import { useEffect, useState } from 'react'; +import { + createIntl, createIntlCache, defineMessages, IntlShape, +} from 'react-intl'; +import { ActiveCaptionMenuInformation } from '../common/types'; +import { renderComponent } from '../components/typed-captions-panel/component'; + +const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') + ? { + headers: { + 'ngrok-skip-browser-warning': 'any', + }, + } : undefined; + +const intlMessages = defineMessages({ + sectionName: { + id: 'plugin.actionButtonDropdown.sidekickComponent.sectionName', + description: 'name of the sidekick component section', + }, + menuTitle: { + id: 'plugin.actionButtonDropdown.sidekickComponent.menuTitle', + description: 'title of the sidekick component menu (internal part)', + }, +}); + +export const useGetInternationalization = (pluginApi: PluginApi) => { + const { + messages: localeMessages, + currentLocale, + loading: localeMessagesLoading, + } = pluginApi.useLocaleMessages!(LOCALE_REQUEST_OBJECT); + + const [intl, setIntl] = useState(null); + + useEffect(() => { + if (!localeMessagesLoading && localeMessages) { + const cache = createIntlCache(); + setIntl(createIntl({ + locale: currentLocale, + messages: localeMessages, + fallbackOnEmptyString: true, + }, cache)); + } + }, [localeMessagesLoading, localeMessages]); + + return { + intl, + localeMessagesLoading, + }; +}; + +export const useTypedCaptionsPanelManager = ( + pluginApi: PluginApi, + intl: IntlShape | null, + localeMessagesLoading: boolean, + pluginUuid: string, +) => { + const { + data: activeCaptionMenusResponseFromDataChannel, + } = pluginApi.useDataChannel!('typed-captions-data-channel', DataChannelTypes.ALL_ITEMS, 'caption-menus'); + + const currentUserResponse = pluginApi.useCurrentUser!(); + + useEffect(() => { + if ((intl && !localeMessagesLoading) + && activeCaptionMenusResponseFromDataChannel?.data + && currentUserResponse?.data?.role === 'MODERATOR') { + const sectionName = intl.formatMessage(intlMessages.sectionName); + const currentUserId = currentUserResponse?.data?.userId || ''; + const sidekickMenuComponentList = activeCaptionMenusResponseFromDataChannel?.data + .filter((menu) => menu.payloadJson.userId === currentUserId) + .map((menu) => new GenericContentSidekickArea({ + id: `transcription-${pluginUuid}`, + name: intl.formatMessage(intlMessages.menuTitle, { + 0: menu.payloadJson.captionLocale, + }), + buttonIcon: 'closed_caption', + section: sectionName, + open: true, + contentFunction: renderComponent( + pluginUuid, + intl, + menu.payloadJson.captionLocale, + ), + })); + pluginApi.setGenericContentItems(sidekickMenuComponentList); + } + }, [ + currentUserResponse, + activeCaptionMenusResponseFromDataChannel, + localeMessagesLoading, intl]); +}; diff --git a/src/components/main/types.ts b/src/main/types.ts similarity index 68% rename from src/components/main/types.ts rename to src/main/types.ts index 8936ce8..769c926 100644 --- a/src/components/main/types.ts +++ b/src/main/types.ts @@ -1,3 +1,5 @@ +import { ActiveCaptionMenuInformation } from '../common/types'; + interface TypedCaptionsProps { pluginName: string, pluginUuid: string, @@ -12,4 +14,6 @@ interface ExternalVideoMeetingSubscription { }[] } +export type PushActiveCaptionFunction = (args: ActiveCaptionMenuInformation) => void; + export { TypedCaptionsProps, ExternalVideoMeetingSubscription };