diff --git a/apps/meteor/client/contexts/ServerContext/methods.ts b/apps/meteor/client/contexts/ServerContext/methods.ts index 59d38536a493..760087052b24 100644 --- a/apps/meteor/client/contexts/ServerContext/methods.ts +++ b/apps/meteor/client/contexts/ServerContext/methods.ts @@ -73,7 +73,7 @@ export type ServerMethods = { 'getUsersOfRoom': (...args: any[]) => any; 'hideRoom': (...args: any[]) => any; 'ignoreUser': (...args: any[]) => any; - 'insertOrUpdateSound': (...args: any[]) => any; + 'insertOrUpdateSound': (args: { previousName?: string; name?: string; _id?: string; extension: string }) => string; 'insertOrUpdateUserStatus': (...args: any[]) => any; 'instances/get': (...args: any[]) => any; 'jitsi:generateAccessToken': (...args: any[]) => any; diff --git a/apps/meteor/client/views/admin/customSounds/AddCustomSound.js b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx similarity index 52% rename from apps/meteor/client/views/admin/customSounds/AddCustomSound.js rename to apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx index 4dcc7d495b1f..344c655f06ec 100644 --- a/apps/meteor/client/views/admin/customSounds/AddCustomSound.js +++ b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx @@ -1,5 +1,5 @@ import { Field, TextInput, Box, Icon, Margins, Button, ButtonGroup } from '@rocket.chat/fuselage'; -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, ReactElement, FormEvent } from 'react'; import VerticalBar from '../../../components/VerticalBar'; import { useMethod } from '../../../contexts/ServerContext'; @@ -8,12 +8,18 @@ import { useTranslation } from '../../../contexts/TranslationContext'; import { useFileInput } from '../../../hooks/useFileInput'; import { validate, createSoundData } from './lib'; -function AddCustomSound({ goToNew, close, onChange, ...props }) { +type AddCustomSoundProps = { + goToNew: (where: string) => () => void; + close: () => void; + onChange: () => void; +}; + +const AddCustomSound = function AddCustomSound({ goToNew, close, onChange, ...props }: AddCustomSoundProps): ReactElement { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const [name, setName] = useState(''); - const [sound, setSound] = useState(); + const [sound, setSound] = useState<{ name: string }>(); const uploadCustomSound = useMethod('uploadCustomSound'); @@ -26,42 +32,41 @@ function AddCustomSound({ goToNew, close, onChange, ...props }) { const [clickUpload] = useFileInput(handleChangeFile, 'audio/mp3'); const saveAction = useCallback( - async (name, soundFile) => { - const soundData = createSoundData(soundFile, name); - const validation = validate(soundData, soundFile); - if (validation.length === 0) { - let soundId; - try { - soundId = await insertOrUpdateSound(soundData); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } + async (name, soundFile): Promise => { + const soundData = createSoundData(soundFile, name) as { extension: string; _id?: string; name: string; newFile?: true }; + const validation = validate(soundData, soundFile) as Array[0]>; + + validation.forEach((error) => { + throw new Error(t('error-the-field-is-required', { field: t(error) })); + }); + + try { + const soundId = await insertOrUpdateSound(soundData); - soundData._id = soundId; - soundData.random = Math.round(Math.random() * 1000); - - if (soundId) { - dispatchToastMessage({ type: 'success', message: t('Uploading_file') }); - - const reader = new FileReader(); - reader.readAsBinaryString(soundFile); - reader.onloadend = () => { - try { - uploadCustomSound(reader.result, soundFile.type, soundData); - dispatchToastMessage({ type: 'success', message: t('File_uploaded') }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }; + if (!soundId) { + return undefined; } + + dispatchToastMessage({ type: 'success', message: t('Uploading_file') }); + + const reader = new FileReader(); + reader.readAsBinaryString(soundFile); + reader.onloadend = (): void => { + try { + uploadCustomSound(reader.result, soundFile.type, { + ...soundData, + _id: soundId, + random: Math.round(Math.random() * 1000), + }); + dispatchToastMessage({ type: 'success', message: t('File_uploaded') }); + } catch (error) { + (typeof error === 'string' || error instanceof Error) && dispatchToastMessage({ type: 'error', message: error }); + } + }; return soundId; + } catch (error) { + (typeof error === 'string' || error instanceof Error) && dispatchToastMessage({ type: 'error', message: error }); } - validation.forEach((error) => { - throw new Error({ - type: 'error', - message: t('error-the-field-is-required', { field: t(error) }), - }); - }); }, [dispatchToastMessage, insertOrUpdateSound, t, uploadCustomSound], ); @@ -69,11 +74,14 @@ function AddCustomSound({ goToNew, close, onChange, ...props }) { const handleSave = useCallback(async () => { try { const result = await saveAction(name, sound); + if (!result) { + throw new Error('error-something-went-wrong'); + } + goToNew(result); dispatchToastMessage({ type: 'success', message: t('Custom_Sound_Saved_Successfully') }); - goToNew(result)(); onChange(); } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); + (typeof error === 'string' || error instanceof Error) && dispatchToastMessage({ type: 'error', message: error }); } }, [dispatchToastMessage, goToNew, name, onChange, saveAction, sound, t]); @@ -82,7 +90,11 @@ function AddCustomSound({ goToNew, close, onChange, ...props }) { {t('Name')} - setName(e.currentTarget.value)} placeholder={t('Name')} /> + ): void => setName(e.currentTarget.value)} + placeholder={t('Name')} + /> @@ -92,7 +104,7 @@ function AddCustomSound({ goToNew, close, onChange, ...props }) { - {(sound && sound.name) || 'none'} + {sound?.name || t('None')} @@ -110,6 +122,6 @@ function AddCustomSound({ goToNew, close, onChange, ...props }) { ); -} +}; export default AddCustomSound; diff --git a/apps/meteor/client/views/admin/customSounds/AdminSounds.js b/apps/meteor/client/views/admin/customSounds/AdminSounds.js deleted file mode 100644 index 645e3311e762..000000000000 --- a/apps/meteor/client/views/admin/customSounds/AdminSounds.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Box, Table, Icon, Button } from '@rocket.chat/fuselage'; -import React, { useMemo, useCallback } from 'react'; - -import FilterByText from '../../../components/FilterByText'; -import GenericTable from '../../../components/GenericTable'; -import { useCustomSound } from '../../../contexts/CustomSoundContext'; -import { useTranslation } from '../../../contexts/TranslationContext'; - -function AdminSounds({ data, sort, onClick, onHeaderClick, setParams, params }) { - const t = useTranslation(); - - const header = useMemo( - () => [ - - {t('Name')} - , - , - ], - [onHeaderClick, sort, t], - ); - - const customSound = useCustomSound(); - - const handlePlay = useCallback( - (sound) => { - customSound.play(sound); - }, - [customSound], - ); - - const renderRow = (sound) => { - const { _id, name } = sound; - - return ( - - - {name} - - - - - - ); - }; - - return ( - } - /> - ); -} - -export default AdminSounds; diff --git a/apps/meteor/client/views/admin/customSounds/AdminSoundsRoute.js b/apps/meteor/client/views/admin/customSounds/AdminSoundsRoute.js deleted file mode 100644 index a6b2105c2a69..000000000000 --- a/apps/meteor/client/views/admin/customSounds/AdminSoundsRoute.js +++ /dev/null @@ -1,111 +0,0 @@ -import { Button, Icon } from '@rocket.chat/fuselage'; -import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo, useState, useCallback } from 'react'; - -import Page from '../../../components/Page'; -import VerticalBar from '../../../components/VerticalBar'; -import { usePermission } from '../../../contexts/AuthorizationContext'; -import { useRoute, useRouteParameter } from '../../../contexts/RouterContext'; -import { useTranslation } from '../../../contexts/TranslationContext'; -import { useEndpointData } from '../../../hooks/useEndpointData'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; -import AddCustomSound from './AddCustomSound'; -import AdminSounds from './AdminSounds'; -import EditCustomSound from './EditCustomSound'; - -function CustomSoundsRoute() { - const route = useRoute('custom-sounds'); - const context = useRouteParameter('context'); - const id = useRouteParameter('id'); - const canManageCustomSounds = usePermission('manage-sounds'); - - const t = useTranslation(); - - const [params, setParams] = useState(() => ({ text: '', current: 0, itemsPerPage: 25 })); - const [sort, setSort] = useState(() => ['name', 'asc']); - - const { text, itemsPerPage, current } = useDebouncedValue(params, 500); - const [column, direction] = useDebouncedValue(sort, 500); - const query = useMemo( - () => ({ - query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), - sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), - ...(itemsPerPage && { count: itemsPerPage }), - ...(current && { offset: current }), - }), - [text, itemsPerPage, current, column, direction], - ); - - const { value: data, reload } = useEndpointData('custom-sounds.list', query); - - const handleItemClick = useCallback( - (_id) => () => { - route.push({ - context: 'edit', - id: _id, - }); - }, - [route], - ); - - const handleHeaderClick = (id) => { - setSort(([sortBy, sortDirection]) => { - if (sortBy === id) { - return [id, sortDirection === 'asc' ? 'desc' : 'asc']; - } - - return [id, 'asc']; - }); - }; - - const handleNewButtonClick = useCallback(() => { - route.push({ context: 'new' }); - }, [route]); - - const handleClose = useCallback(() => { - route.push({}); - }, [route]); - - const handleChange = useCallback(() => { - reload(); - }, [reload]); - - if (!canManageCustomSounds) { - return ; - } - - return ( - - - - - - - - - - {context && ( - - - {context === 'edit' && t('Custom_Sound_Edit')} - {context === 'new' && t('Custom_Sound_Add')} - - - {context === 'edit' && } - {context === 'new' && } - - )} - - ); -} - -export default CustomSoundsRoute; diff --git a/apps/meteor/client/views/admin/customSounds/AdminSoundsRoute.tsx b/apps/meteor/client/views/admin/customSounds/AdminSoundsRoute.tsx new file mode 100644 index 000000000000..4b6a70e2c4ac --- /dev/null +++ b/apps/meteor/client/views/admin/customSounds/AdminSoundsRoute.tsx @@ -0,0 +1,168 @@ +import { Box, Button, Icon, Pagination } from '@rocket.chat/fuselage'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import React, { useMemo, useState, useCallback, ReactElement } from 'react'; + +import FilterByText from '../../../components/FilterByText'; +import { GenericTable } from '../../../components/GenericTable/V2/GenericTable'; +import { GenericTableBody } from '../../../components/GenericTable/V2/GenericTableBody'; +import { GenericTableCell } from '../../../components/GenericTable/V2/GenericTableCell'; +import { GenericTableHeader } from '../../../components/GenericTable/V2/GenericTableHeader'; +import { GenericTableHeaderCell } from '../../../components/GenericTable/V2/GenericTableHeaderCell'; +import { GenericTableLoadingTable } from '../../../components/GenericTable/V2/GenericTableLoadingTable'; +import { GenericTableRow } from '../../../components/GenericTable/V2/GenericTableRow'; +import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../../components/GenericTable/hooks/useSort'; +import Page from '../../../components/Page'; +import VerticalBar from '../../../components/VerticalBar'; +import { usePermission } from '../../../contexts/AuthorizationContext'; +import { useCustomSound } from '../../../contexts/CustomSoundContext'; +import { useRoute, useRouteParameter } from '../../../contexts/RouterContext'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import { useEndpointData } from '../../../hooks/useEndpointData'; +import { AsyncStatePhase } from '../../../lib/asyncState'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; +import AddCustomSound from './AddCustomSound'; +import EditCustomSound from './EditCustomSound'; + +function CustomSoundsRoute(): ReactElement { + const route = useRoute('custom-sounds'); + const context = useRouteParameter('context'); + const id = useRouteParameter('id'); + const canManageCustomSounds = usePermission('manage-sounds'); + + const t = useTranslation(); + const customSound = useCustomSound(); + + const { sortBy, sortDirection, setSort } = useSort<'name'>('name'); + const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); + + const [text, setParams] = useState(''); + + const query = useDebouncedValue( + useMemo( + () => ({ + query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), + sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, + ...(itemsPerPage && { count: itemsPerPage }), + ...(current && { offset: current }), + }), + [text, itemsPerPage, current, sortBy, sortDirection], + ), + 500, + ); + + const { reload, ...result } = useEndpointData('custom-sounds.list', query); + + const handleItemClick = useCallback( + (_id) => (): void => { + route.push({ + context: 'edit', + id: _id, + }); + }, + [route], + ); + + const handlePlay = useCallback( + (sound) => { + customSound.play(sound); + }, + [customSound], + ); + + const handleNewButtonClick = useCallback(() => { + route.push({ context: 'new' }); + }, [route]); + + const handleClose = useCallback(() => { + route.push({}); + }, [route]); + + const handleChange = useCallback(() => { + reload(); + }, [reload]); + + if (!canManageCustomSounds) { + return ; + } + + return ( + + + + + + + setParams(text)} /> + + + + {t('Name')} + + + + + {result.phase === AsyncStatePhase.LOADING && } + {result.phase === AsyncStatePhase.RESOLVED && + result.value.sounds.map((sound) => ( + + + {sound.name} + + + + + + ))} + + + {result.phase === AsyncStatePhase.RESOLVED && ( + + )} + + + {context && ( + + + {context === 'edit' && t('Custom_Sound_Edit')} + {context === 'new' && t('Custom_Sound_Add')} + + + {context === 'edit' && } + {context === 'new' && } + + )} + + ); +} + +export default CustomSoundsRoute; diff --git a/apps/meteor/definition/rest/index.ts b/apps/meteor/definition/rest/index.ts index 9e2fbc2e1789..05ced47f0be8 100644 --- a/apps/meteor/definition/rest/index.ts +++ b/apps/meteor/definition/rest/index.ts @@ -6,6 +6,7 @@ import type { BannersEndpoints } from './v1/banners'; import type { ChannelsEndpoints } from './v1/channels'; import type { ChatEndpoints } from './v1/chat'; import type { CloudEndpoints } from './v1/cloud'; +import { CustomSoundEndpoint } from './v1/customSounds'; import type { CustomUserStatusEndpoints } from './v1/customUserStatus'; import type { DmEndpoints } from './v1/dm'; import type { DnsEndpoints } from './v1/dns'; @@ -53,7 +54,8 @@ type CommunityEndpoints = BannersEndpoints & InstancesEndpoints & VoipEndpoints & InvitesEndpoints & - E2eEndpoints; + E2eEndpoints & + CustomSoundEndpoint; type Endpoints = CommunityEndpoints & EnterpriseEndpoints; diff --git a/apps/meteor/definition/rest/v1/customSounds.ts b/apps/meteor/definition/rest/v1/customSounds.ts new file mode 100644 index 000000000000..1c03d02416af --- /dev/null +++ b/apps/meteor/definition/rest/v1/customSounds.ts @@ -0,0 +1,14 @@ +import { PaginatedRequest } from '../helpers/PaginatedRequest'; +import { PaginatedResult } from '../helpers/PaginatedResult'; + +export type CustomSoundEndpoint = { + 'custom-sounds.list': { + GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ + sounds: { + _id: string; + name: string; + extension: string; + }[]; + }>; + }; +}; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 703448860f51..cb97dc01c201 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3348,6 +3348,7 @@ "Outgoing_WebHook_Description": "Get data out of Rocket.Chat in real-time.", "Output_format": "Output format", "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "Override URL to which files are uploaded. This url also used for downloads unless a CDN is given", + "Play": "Play", "Page_title": "Page title", "Page_URL": "Page URL", "Pages": "Pages",