From d9995a2151dae649dbfc74e1dc561cfd9bf52a88 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 13 Aug 2024 15:34:31 -0300 Subject: [PATCH 01/49] refactor: Cover implicit `any` types (#33030) --- packages/fuselage-ui-kit/src/blocks/InputBlock.tsx | 10 +++++++++- .../ChannelsSelectElement/ChannelsSelectElement.tsx | 4 ++-- .../MultiChannelsSelectElement.tsx | 4 ++-- .../src/elements/MultiStaticSelectElement.tsx | 2 +- .../src/elements/StaticSelectElement.tsx | 2 +- .../UsersSelectElement/MultiUsersSelectElement.tsx | 4 ++-- .../UsersSelectElement/UsersSelectElement.tsx | 4 ++-- .../src/surfaces/createSurfaceRenderer.tsx | 4 ++-- .../src/elements/Timestamp/ErrorBoundary.tsx | 11 ++++++++--- .../ui-client/src/components/CustomFieldsForm.tsx | 6 +++--- .../MultiSelectCustom/MultiSelectCustom.tsx | 4 ++-- .../MultiSelectCustom/MultiSelectCustomList.tsx | 2 +- .../src/Components/FlowContainer/FlowContainer.tsx | 12 +++++++----- .../src/Components/SurfaceSelect/SurfaceSelect.tsx | 2 +- .../src/components/LoginSwitchLanguageFooter.tsx | 5 ++++- 15 files changed, 47 insertions(+), 29 deletions(-) diff --git a/packages/fuselage-ui-kit/src/blocks/InputBlock.tsx b/packages/fuselage-ui-kit/src/blocks/InputBlock.tsx index 979e04e808c7..9844372a3060 100644 --- a/packages/fuselage-ui-kit/src/blocks/InputBlock.tsx +++ b/packages/fuselage-ui-kit/src/blocks/InputBlock.tsx @@ -46,7 +46,15 @@ const InputBlock = ({ {surfaceRenderer.renderInputBlockElement(inputElement, 0)} {error && {error}} - {block.hint && {block.hint}} + {block.hint && ( + + {surfaceRenderer.renderTextObject( + block.hint, + 0, + UiKit.BlockContext.NONE + )} + + )} ); }; diff --git a/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.tsx b/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.tsx index 49b24545dbf1..a26037373aa7 100644 --- a/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.tsx +++ b/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.tsx @@ -22,8 +22,8 @@ const ChannelsSelectElement = ({ const options = useChannelsData({ filter: filterDebounced }); const handleChange = useCallback( - (value) => { - action({ target: { value } }); + (value: string | string[]) => { + if (!Array.isArray(value)) action({ target: { value } }); }, [action] ); diff --git a/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/MultiChannelsSelectElement.tsx b/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/MultiChannelsSelectElement.tsx index 78bf07bbf599..b050a137860d 100644 --- a/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/MultiChannelsSelectElement.tsx +++ b/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/MultiChannelsSelectElement.tsx @@ -22,8 +22,8 @@ const MultiChannelsSelectElement = ({ const options = useChannelsData({ filter: filterDebounced }); const handleChange = useCallback( - (value) => { - action({ target: { value } }); + (value: string | string[]) => { + if (Array.isArray(value)) action({ target: { value } }); }, [action] ); diff --git a/packages/fuselage-ui-kit/src/elements/MultiStaticSelectElement.tsx b/packages/fuselage-ui-kit/src/elements/MultiStaticSelectElement.tsx index a1948a921f49..9bb5746f7ef0 100644 --- a/packages/fuselage-ui-kit/src/elements/MultiStaticSelectElement.tsx +++ b/packages/fuselage-ui-kit/src/elements/MultiStaticSelectElement.tsx @@ -27,7 +27,7 @@ const MultiStaticSelectElement = ({ ); const handleChange = useCallback( - (value) => { + (value: string[]) => { action({ target: { value } }); }, [action] diff --git a/packages/fuselage-ui-kit/src/elements/StaticSelectElement.tsx b/packages/fuselage-ui-kit/src/elements/StaticSelectElement.tsx index c68a7f067a8d..549ef2d8bd6e 100644 --- a/packages/fuselage-ui-kit/src/elements/StaticSelectElement.tsx +++ b/packages/fuselage-ui-kit/src/elements/StaticSelectElement.tsx @@ -26,7 +26,7 @@ const StaticSelectElement = ({ ); const handleChange = useCallback( - (value) => { + (value: string) => { action({ target: { value } }); }, [action] diff --git a/packages/fuselage-ui-kit/src/elements/UsersSelectElement/MultiUsersSelectElement.tsx b/packages/fuselage-ui-kit/src/elements/UsersSelectElement/MultiUsersSelectElement.tsx index bdf7bfef4a31..3fd3351c2bb6 100644 --- a/packages/fuselage-ui-kit/src/elements/UsersSelectElement/MultiUsersSelectElement.tsx +++ b/packages/fuselage-ui-kit/src/elements/UsersSelectElement/MultiUsersSelectElement.tsx @@ -31,8 +31,8 @@ const MultiUsersSelectElement = ({ const data = useUsersData({ filter: debouncedFilter }); const handleChange = useCallback( - (value) => { - action({ target: { value } }); + (value: string | string[]) => { + if (Array.isArray(value)) action({ target: { value } }); }, [action] ); diff --git a/packages/fuselage-ui-kit/src/elements/UsersSelectElement/UsersSelectElement.tsx b/packages/fuselage-ui-kit/src/elements/UsersSelectElement/UsersSelectElement.tsx index 39e71bd4fce8..03306a36905c 100644 --- a/packages/fuselage-ui-kit/src/elements/UsersSelectElement/UsersSelectElement.tsx +++ b/packages/fuselage-ui-kit/src/elements/UsersSelectElement/UsersSelectElement.tsx @@ -24,8 +24,8 @@ const UsersSelectElement = ({ block, context }: UsersSelectElementProps) => { const data = useUsersData({ filter: debouncedFilter }); const handleChange = useCallback( - (value) => { - action({ target: { value } }); + (value: string | string[]) => { + if (!Array.isArray(value)) action({ target: { value } }); }, [action] ); diff --git a/packages/fuselage-ui-kit/src/surfaces/createSurfaceRenderer.tsx b/packages/fuselage-ui-kit/src/surfaces/createSurfaceRenderer.tsx index 2f6115f8ca66..0208198f0268 100644 --- a/packages/fuselage-ui-kit/src/surfaces/createSurfaceRenderer.tsx +++ b/packages/fuselage-ui-kit/src/surfaces/createSurfaceRenderer.tsx @@ -1,11 +1,11 @@ import type * as UiKit from '@rocket.chat/ui-kit'; -import type { ComponentType, ReactElement } from 'react'; +import type { ComponentType, ReactElement, ReactNode } from 'react'; export const createSurfaceRenderer = < S extends UiKit.SurfaceRenderer >( // eslint-disable-next-line @typescript-eslint/naming-convention - SurfaceComponent: ComponentType, + SurfaceComponent: ComponentType<{ children: ReactNode }>, surfaceRenderer: S ) => function Surface( diff --git a/packages/gazzodown/src/elements/Timestamp/ErrorBoundary.tsx b/packages/gazzodown/src/elements/Timestamp/ErrorBoundary.tsx index 453853275f8a..a659adec2aa7 100644 --- a/packages/gazzodown/src/elements/Timestamp/ErrorBoundary.tsx +++ b/packages/gazzodown/src/elements/Timestamp/ErrorBoundary.tsx @@ -1,7 +1,12 @@ -import React, { Component, ReactNode } from 'react'; +import { Component, ReactNode } from 'react'; -export class ErrorBoundary extends Component<{ fallback: React.ReactNode }, { hasError: boolean }> { - constructor(props: { fallback: React.ReactNode }) { +interface ErrorBoundaryProps { + fallback: ReactNode; + children: ReactNode; +} + +export class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false }; } diff --git a/packages/ui-client/src/components/CustomFieldsForm.tsx b/packages/ui-client/src/components/CustomFieldsForm.tsx index 0d1ba2ca5b1b..3a9c66014e4d 100644 --- a/packages/ui-client/src/components/CustomFieldsForm.tsx +++ b/packages/ui-client/src/components/CustomFieldsForm.tsx @@ -5,7 +5,7 @@ import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; import { useCallback, useMemo } from 'react'; -import type { Control, FieldValues } from 'react-hook-form'; +import type { Control, FieldValues, FieldError as RHFFieldError } from 'react-hook-form'; import { Controller, useFormState, get } from 'react-hook-form'; type CustomFieldFormProps = { @@ -46,10 +46,10 @@ const CustomField = ({ [defaultValue, options], ); - const validateRequired = useCallback((value) => (required ? typeof value === 'string' && !!value.trim() : true), [required]); + const validateRequired = useCallback((value: string) => (required ? typeof value === 'string' && !!value.trim() : true), [required]); const getErrorMessage = useCallback( - (error) => { + (error: RHFFieldError) => { switch (error?.type) { case 'required': return t('The_field_is_required', label || name); diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx index 317f56899d05..a90cfb1bd1c6 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx @@ -64,8 +64,8 @@ export const MultiSelectCustom = ({ const [collapsed, toggleCollapsed] = useToggle(false); const onClose = useCallback( - (e) => { - if (isValidReference(reference, e)) { + (e: MouseEvent) => { + if (isValidReference(reference, e as { target: Node | null })) { toggleCollapsed(false); return; } diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx index 71cb54f81aa5..4afe036e74d2 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx @@ -20,7 +20,7 @@ const MultiSelectCustomList = ({ const [text, setText] = useState(''); - const handleChange = useCallback((event) => setText(event.currentTarget.value), []); + const handleChange = useCallback((event: FormEvent) => setText(event.currentTarget.value), []); const filteredOptions = useFilteredOptions(text, options); diff --git a/packages/uikit-playground/src/Components/FlowContainer/FlowContainer.tsx b/packages/uikit-playground/src/Components/FlowContainer/FlowContainer.tsx index f04d95b274c2..311ddaae795c 100644 --- a/packages/uikit-playground/src/Components/FlowContainer/FlowContainer.tsx +++ b/packages/uikit-playground/src/Components/FlowContainer/FlowContainer.tsx @@ -8,6 +8,8 @@ import ReactFlow, { Viewport, ReactFlowInstance, useReactFlow, + Connection, + Edge, } from 'reactflow'; import 'reactflow/dist/style.css'; @@ -39,10 +41,10 @@ const FlowContainer = () => { const edgeUpdateSuccessful = useRef(true); const onConnect = useCallback( - (params) => { - if (params.source === params.target) return; + (connection: Connection) => { + if (connection.source === connection.target) return; const newEdge = { - ...params, + ...connection, type: FlowParams.edgeType, markerEnd: FlowParams.markerEnd, style: FlowParams.style, @@ -57,7 +59,7 @@ const FlowContainer = () => { }, []); const onEdgeUpdate = useCallback( - (oldEdge, newConnection) => { + (oldEdge: Edge, newConnection: Connection) => { edgeUpdateSuccessful.current = true; setEdges((els) => updateEdge(oldEdge, newConnection, els)); }, @@ -65,7 +67,7 @@ const FlowContainer = () => { ); const onEdgeUpdateEnd = useCallback( - (_, edge) => { + (_: MouseEvent | TouchEvent, edge: Edge) => { if (!edgeUpdateSuccessful.current) { setEdges((eds) => { return eds.filter((e) => e.id !== edge.id); diff --git a/packages/uikit-playground/src/Components/SurfaceSelect/SurfaceSelect.tsx b/packages/uikit-playground/src/Components/SurfaceSelect/SurfaceSelect.tsx index d0a59cc42a45..e6c0abba57e7 100644 --- a/packages/uikit-playground/src/Components/SurfaceSelect/SurfaceSelect.tsx +++ b/packages/uikit-playground/src/Components/SurfaceSelect/SurfaceSelect.tsx @@ -16,7 +16,7 @@ const SurfaceSelect: FC = () => { value={`${screens[activeScreen].payload.surface}`} placeholder="Surface" onChange={(e) => { - dispatch(surfaceAction(typeof e === 'string' ? parseInt(e) : e)); + dispatch(surfaceAction(typeof e === 'string' ? parseInt(e) : Number(e))); }} /> ); diff --git a/packages/web-ui-registration/src/components/LoginSwitchLanguageFooter.tsx b/packages/web-ui-registration/src/components/LoginSwitchLanguageFooter.tsx index 0d08fba8a3ac..46ce5112b9d4 100644 --- a/packages/web-ui-registration/src/components/LoginSwitchLanguageFooter.tsx +++ b/packages/web-ui-registration/src/components/LoginSwitchLanguageFooter.tsx @@ -62,7 +62,10 @@ const LoginSwitchLanguageFooter = ({ {suggestions.map((suggestion) => ( ))} From 938337f585c1a44546edf44bcba8f75693786491 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 13 Aug 2024 16:31:49 -0300 Subject: [PATCH 02/49] ci: fix prs from forks (#32998) --- .github/actions/setup-node/action.yml | 1 + .github/workflows/ci-test-e2e.yml | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index caa3c63e00f0..60d54ab896dd 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -32,6 +32,7 @@ runs: uses: actions/cache@v3 with: path: | + .turbo/cache node_modules ${{ env.DENO_DIR }} apps/meteor/node_modules diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index c700512ec1ef..e8dd480d5b27 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -188,10 +188,8 @@ jobs: docker compose -f docker-compose-ci.yml up -d - name: Clean up temporary files - # remove all folders inside /tmp except /tmp/coverage run: | - cd /tmp - sudo find . -mindepth 1 -maxdepth 1 -type d | grep -v './coverage' | sudo xargs rm -rf + sudo rm -rf /tmp/bundle - name: Cache Playwright binaries if: inputs.type == 'ui' From 1f061a1aa54960ba2440da3795d10a3a2e1b4c7f Mon Sep 17 00:00:00 2001 From: Lucas Pelegrino Date: Tue, 13 Aug 2024 17:48:36 -0300 Subject: [PATCH 03/49] fix(i18n): fixes onboarding.component.emailCodeFallback translation (#33029) --- .changeset/proud-years-buy.md | 5 +++++ packages/i18n/src/locales/de.i18n.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/proud-years-buy.md diff --git a/.changeset/proud-years-buy.md b/.changeset/proud-years-buy.md new file mode 100644 index 000000000000..94f4ab0df736 --- /dev/null +++ b/.changeset/proud-years-buy.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/i18n': patch +--- + +Fixes a typo in german translation and fixes the broken hyperlink for Resend and Change Email diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 2208fe4a6d81..a67509672d33 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -5432,7 +5432,7 @@ "onboarding.component.form.action.confirm": "Bestätigen", "onboarding.component.form.action.pasteHere": "Hier einfügen...", "onboarding.component.form.termsAndConditions": "Ich bin mit den Nutzungsvereinbarung und den Datenschutzbestimmungen einverstanden", - "onboarding.component.emailCodeFallback": "Keine E-Mail erhalten? Noch einemal versenden oder E-Mailadresse ändern", + "onboarding.component.emailCodeFallback": "Keine E-Mail erhalten? <1>Noch einmal versenden oder <3>E-Mailadresse ändern", "onboarding.page.form.title": "Starten wir Ihren Arbeitsbereich", "onboarding.page.emailConfirmed.title": "E-Mail bestätigt", "onboarding.page.emailConfirmed.subtitle": "Sie können zu Ihrer Rocket.Chat-Anwendung zurückkehren - wir haben Ihren Arbeitsbereich bereits gestartet.", From 284b5cecd20dcd8d80c179a226f0c084c5ca30bb Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:47:21 -0300 Subject: [PATCH 04/49] fix: Retention policy cron max age settings reactivity (#33025) --- .../server/cronPruneMessages.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/meteor/app/retention-policy/server/cronPruneMessages.ts b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts index 337691bfbe57..640aa517a679 100644 --- a/apps/meteor/app/retention-policy/server/cronPruneMessages.ts +++ b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts @@ -6,13 +6,20 @@ import { getCronAdvancedTimerFromPrecisionSetting } from '../../../lib/getCronAd import { cleanRoomHistory } from '../../lib/server/functions/cleanRoomHistory'; import { settings } from '../../settings/server'; -const maxTimes = { - c: 0, - p: 0, - d: 0, +type RetentionRoomTypes = 'c' | 'p' | 'd'; + +const getMaxAgeSettingIdByRoomType = (type: RetentionRoomTypes) => { + switch (type) { + case 'c': + return settings.get('RetentionPolicy_TTL_Channels'); + case 'p': + return settings.get('RetentionPolicy_TTL_Groups'); + case 'd': + return settings.get('RetentionPolicy_TTL_DMs'); + } }; -let types: (keyof typeof maxTimes)[] = []; +let types: RetentionRoomTypes[] = []; const oldest = new Date('0001-01-01T00:00:00Z'); @@ -29,7 +36,7 @@ async function job(): Promise { // get all rooms with default values for await (const type of types) { - const maxAge = maxTimes[type] || 0; + const maxAge = getMaxAgeSettingIdByRoomType(type) || 0; const latest = new Date(now.getTime() - maxAge); const rooms = await Rooms.find( @@ -95,9 +102,6 @@ settings.watchMultiple( 'RetentionPolicy_AppliesToChannels', 'RetentionPolicy_AppliesToGroups', 'RetentionPolicy_AppliesToDMs', - 'RetentionPolicy_TTL_Channels', - 'RetentionPolicy_TTL_Groups', - 'RetentionPolicy_TTL_DMs', 'RetentionPolicy_Advanced_Precision', 'RetentionPolicy_Advanced_Precision_Cron', 'RetentionPolicy_Precision', @@ -120,10 +124,6 @@ settings.watchMultiple( types.push('d'); } - maxTimes.c = settings.get('RetentionPolicy_TTL_Channels'); - maxTimes.p = settings.get('RetentionPolicy_TTL_Groups'); - maxTimes.d = settings.get('RetentionPolicy_TTL_DMs'); - const precision = (settings.get('RetentionPolicy_Advanced_Precision') && settings.get('RetentionPolicy_Advanced_Precision_Cron')) || getCronAdvancedTimerFromPrecisionSetting(settings.get('RetentionPolicy_Precision')); From 19824bde976dae0b74f01d88fb3b3342cfef58db Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 14 Aug 2024 10:51:58 -0300 Subject: [PATCH 05/49] chore: remove `persists` from updater (#33037) --- .../federation/server/endpoints/dispatch.js | 2 +- .../lib/server/lib/notifyUsersOnMessage.ts | 2 +- .../hooks/afterSaveOmnichannelMessage.ts | 2 +- apps/meteor/server/models/dummy/BaseDummy.ts | 6 +- apps/meteor/server/models/raw/BaseRaw.ts | 10 +- .../model-typings/src/models/IBaseModel.ts | 1 + packages/model-typings/src/updater.ts | 3 +- packages/models/src/updater.spec.ts | 170 ++++++------------ packages/models/src/updater.ts | 45 +++-- 9 files changed, 96 insertions(+), 145 deletions(-) diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js index a7ab98accd36..7090f053a22b 100644 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ b/apps/meteor/app/federation/server/endpoints/dispatch.js @@ -296,7 +296,7 @@ const eventHandlers = { const roomUpdater = Rooms.getUpdater(); await notifyUsersOnMessage(denormalizedMessage, room, roomUpdater); if (roomUpdater.hasChanges()) { - await roomUpdater.persist({ _id: room._id }); + await Rooms.updateFromUpdater({ _id: room._id }, roomUpdater); } sendAllNotifications(denormalizedMessage, room); diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts index 0b80b9bb06df..a05c05b4bb94 100644 --- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts @@ -188,7 +188,7 @@ callbacks.add( await notifyUsersOnMessage(message, room, roomUpdater); if (roomUpdater.hasChanges()) { - await roomUpdater.persist({ _id: room._id }); + await Rooms.updateFromUpdater({ _id: room._id }, roomUpdater); } return message; diff --git a/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts b/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts index 372704d339bb..07ce7fe08573 100644 --- a/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts +++ b/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts @@ -14,7 +14,7 @@ callbacks.add( const result = await callbacks.run('afterOmnichannelSaveMessage', message, { room, roomUpdater: updater }); if (updater.hasChanges()) { - await updater.persist({ _id: room._id }); + await LivechatRooms.updateFromUpdater({ _id: room._id }, updater); } return result; diff --git a/apps/meteor/server/models/dummy/BaseDummy.ts b/apps/meteor/server/models/dummy/BaseDummy.ts index 9ba590151c02..c3052ede9487 100644 --- a/apps/meteor/server/models/dummy/BaseDummy.ts +++ b/apps/meteor/server/models/dummy/BaseDummy.ts @@ -42,7 +42,11 @@ export class BaseDummy< } public getUpdater(): Updater { - return new UpdaterImpl(this.col as unknown as IBaseModel); + return new UpdaterImpl(); + } + + public updateFromUpdater(query: Filter, updater: Updater): Promise { + return this.updateOne(query, updater); } getCollectionName(): string { diff --git a/apps/meteor/server/models/raw/BaseRaw.ts b/apps/meteor/server/models/raw/BaseRaw.ts index 0a41a0fbb3eb..1a3dd1a3eb4c 100644 --- a/apps/meteor/server/models/raw/BaseRaw.ts +++ b/apps/meteor/server/models/raw/BaseRaw.ts @@ -111,7 +111,15 @@ export abstract class BaseRaw< } public getUpdater(): Updater { - return new UpdaterImpl(this.col as unknown as IBaseModel); + return new UpdaterImpl(); + } + + public updateFromUpdater(query: Filter, updater: Updater): Promise { + const updateFilter = updater.getUpdateFilter(); + return this.updateOne(query, updateFilter).catch((e) => { + console.warn(e, updateFilter); + return Promise.reject(e); + }); } private doNotMixInclusionAndExclusionFields(options: FindOptions = {}): FindOptions { diff --git a/packages/model-typings/src/models/IBaseModel.ts b/packages/model-typings/src/models/IBaseModel.ts index 59a5f67273eb..246c3ae253dd 100644 --- a/packages/model-typings/src/models/IBaseModel.ts +++ b/packages/model-typings/src/models/IBaseModel.ts @@ -51,6 +51,7 @@ export interface IBaseModel< getCollectionName(): string; getUpdater(): Updater; + updateFromUpdater(query: Filter, updater: Updater): Promise; findOneAndUpdate(query: Filter, update: UpdateFilter | T, options?: FindOneAndUpdateOptions): Promise>; diff --git a/packages/model-typings/src/updater.ts b/packages/model-typings/src/updater.ts index fe8354479c1f..7743430ad4b8 100644 --- a/packages/model-typings/src/updater.ts +++ b/packages/model-typings/src/updater.ts @@ -1,12 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type { Join, NestedPaths, PropertyType, ArrayElement, NestedPathsOfType, Filter, UpdateFilter } from 'mongodb'; +import type { Join, NestedPaths, PropertyType, ArrayElement, NestedPathsOfType, UpdateFilter } from 'mongodb'; export interface Updater { set

, K extends keyof P>(key: K, value: P[K]): Updater; unset>(key: K): Updater; inc>(key: K, value: number): Updater; addToSet>(key: K, value: ArrayElementType[K]>): Updater; - persist(query: Filter): Promise; hasChanges(): boolean; getUpdateFilter(): UpdateFilter; } diff --git a/packages/models/src/updater.spec.ts b/packages/models/src/updater.spec.ts index ae75400bd9ee..bdae43e338f3 100644 --- a/packages/models/src/updater.spec.ts +++ b/packages/models/src/updater.spec.ts @@ -15,63 +15,67 @@ test('updater typings', () => { e: string; }; e: string[]; - }>({} as any); + }>(); - const omnichannel = new UpdaterImpl({} as any); - omnichannel.addToSet('v.activity', 'asd'); - // @ts-expect-error - omnichannel.addToSet('v.activity', 1); - // @ts-expect-error - omnichannel.addToSet('v.activity', { - asdas: 1, - }); - - // @ts-expect-error - omnichannel.addToSet('v.activity.asd', { - asdas: 1, - }); - - updater.addToSet('e', 'a'); - - // @ts-expect-error - updater.addToSet('e', 1); - // @ts-expect-error - updater.addToSet('a', 'b'); - - // @ts-expect-error - updater.set('njame', 1); - // @ts-expect-error - updater.set('ttes', 1); - // @ts-expect-error + // @ts-expect-error: it should not allow any string to `t` only `l` is allowed updater.set('t', 'a'); + // `l` is allowed updater.set('t', 'l'); - // @ts-expect-error - updater.set('a', 'b'); - // @ts-expect-error - updater.set('c', 'b'); - updater.set('c', 1); - updater.set('a', { - b: 'set', - }); + // `a` is { b: string } + updater.set('a', { b: 'test' }); updater.set('a.b', 'test'); - - // @ts-expect-error + // @ts-expect-error: it should not allow strings to `a`, a is an object containing `b: string` + updater.set('a', 'b'); + // @ts-expect-error: `a` is not optional so unset is not allowed updater.unset('a'); + // @ts-expect-error: strings cannot be incremented + updater.inc('a', 1); + // `c` is number but it should be optional, so unset is allowed updater.unset('c'); + updater.set('c', 1); + // @ts-expect-error: `c` is a number + updater.set('c', 'b'); + // inc is allowed for numbers + updater.inc('c', 1); + // `d` is { e: string } but it should be optional, so unset is allowed updater.unset('d'); + updater.set('d', { e: 'a' }); + // @ts-expect-error: `d` is an object + updater.set('d', 'a'); + + // @ts-expect-error: it should not allow numbers, since e is a string + updater.addToSet('e', 1); + // @ts-expect-error: it should not allow strings, since a is an object + updater.addToSet('a', 'b'); + updater.addToSet('e', 'a'); + // @ts-expect-error: it should not allow `njame` its not specified in the model + updater.set('njame', 1); + + // `d` is { e: string } and also it should be optional, so unset is allowed updater.unset('d.e'); - // @ts-expect-error + // @ts-expect-error: `d` is an object cannot be incremented updater.inc('d', 1); - updater.inc('c', 1); + + // `activity` is a string + const omnichannel = new UpdaterImpl(); + omnichannel.addToSet('v.activity', 'asd'); + // @ts-expect-error: it should not allow numbers, since activity is a string + omnichannel.addToSet('v.activity', 1); + // @ts-expect-error: it should not allow objects, since activity is a string + omnichannel.addToSet('v.activity', { + asdas: 1, + }); + // @ts-expect-error: it should not allow sub properties, since activity is a string + omnichannel.addToSet('v.activity.asd', { + asdas: 1, + }); }); test('updater $set operations', async () => { - const updateOne = jest.fn(); - const updater = new UpdaterImpl<{ _id: string; t: 'l'; @@ -79,29 +83,16 @@ test('updater $set operations', async () => { b: string; }; c?: number; - }>({ - updateOne, - } as any); + }>(); updater.set('a', { b: 'set', }); - await updater.persist({ - _id: 'test', - }); - - expect(updateOne).toBeCalledWith( - { - _id: 'test', - }, - { $set: { a: { b: 'set' } } }, - ); + expect(updater.getUpdateFilter()).toEqual({ $set: { a: { b: 'set' } } }); }); test('updater $unset operations', async () => { - const updateOne = jest.fn(); - const updater = new UpdaterImpl<{ _id: string; t: 'l'; @@ -109,27 +100,12 @@ test('updater $unset operations', async () => { b: string; }; c?: number; - }>({ - updateOne, - } as any); - + }>(); updater.unset('c'); - - await updater.persist({ - _id: 'test', - }); - - expect(updateOne).toBeCalledWith( - { - _id: 'test', - }, - { $unset: { c: 1 } }, - ); + expect(updater.getUpdateFilter()).toEqual({ $unset: { c: 1 } }); }); test('updater inc multiple operations', async () => { - const updateOne = jest.fn(); - const updater = new UpdaterImpl<{ _id: string; t: 'l'; @@ -137,52 +113,27 @@ test('updater inc multiple operations', async () => { b: string; }; c?: number; - }>({ - updateOne, - } as any); + }>(); updater.inc('c', 1); updater.inc('c', 1); - await updater.persist({ - _id: 'test', - }); - - expect(updateOne).toBeCalledWith( - { - _id: 'test', - }, - { $inc: { c: 2 } }, - ); + expect(updater.getUpdateFilter()).toEqual({ $inc: { c: 2 } }); }); test('it should add items to array', async () => { - const updateOne = jest.fn(); const updater = new UpdaterImpl<{ _id: string; a: string[]; - }>({ - updateOne, - } as any); + }>(); updater.addToSet('a', 'b'); updater.addToSet('a', 'c'); - await updater.persist({ - _id: 'test', - }); - - expect(updateOne).toBeCalledWith( - { - _id: 'test', - }, - { $addToSet: { a: { $each: ['b', 'c'] } } }, - ); + expect(updater.getUpdateFilter()).toEqual({ $addToSet: { a: { $each: ['b', 'c'] } } }); }); -test('it should persist only once', async () => { - const updateOne = jest.fn(); - +test('it should getUpdateFilter only once', async () => { const updater = new UpdaterImpl<{ _id: string; t: 'l'; @@ -190,19 +141,12 @@ test('it should persist only once', async () => { b: string; }; c?: number; - }>({ - updateOne, - } as any); + }>(); updater.set('a', { b: 'set', }); - await updater.persist({ - _id: 'test', - }); - - expect(updateOne).toBeCalledTimes(1); - - expect(() => updater.persist({ _id: 'test' })).rejects.toThrow(); + expect(updater.getUpdateFilter()).toEqual({ $set: { a: { b: 'set' } } }); + expect(() => updater.getUpdateFilter()).toThrow(); }); diff --git a/packages/models/src/updater.ts b/packages/models/src/updater.ts index 361e228e65ac..4f7ad271f397 100644 --- a/packages/models/src/updater.ts +++ b/packages/models/src/updater.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type { IBaseModel, Updater, SetProps, UnsetProps, IncProps, AddToSetProps } from '@rocket.chat/model-typings'; -import type { UpdateFilter, Filter } from 'mongodb'; +import type { Updater, SetProps, UnsetProps, IncProps, AddToSetProps } from '@rocket.chat/model-typings'; +import type { UpdateFilter } from 'mongodb'; type ArrayElementType = T extends (infer E)[] ? E : T; @@ -17,8 +17,6 @@ export class UpdaterImpl implements Updater { private dirty = false; - constructor(private model: IBaseModel) {} - set

, K extends keyof P>(key: K, value: P[K]) { this._set = this._set ?? new Map, any>(); this._set.set(key as Keys, value); @@ -47,31 +45,16 @@ export class UpdaterImpl implements Updater { return this; } - async persist(query: Filter): Promise { - if (this.dirty) { - throw new Error('Updater is not dirty'); - } - - if ((process.env.NODE_ENV === 'development' || process.env.TEST_MODE) && !this.hasChanges()) { - throw new Error('Nothing to update'); - } - - this.dirty = true; - - const update = this.getUpdateFilter(); - try { - await this.model.updateOne(query, update); - } catch (error) { - console.error('Failed to update', JSON.stringify(query), JSON.stringify(update, null, 2)); - throw error; - } + hasChanges() { + const filter = this._getUpdateFilter(); + return this._hasChanges(filter); } - hasChanges() { - return Object.keys(this.getUpdateFilter()).length > 0; + private _hasChanges(filter: UpdateFilter) { + return Object.keys(filter).length > 0; } - getUpdateFilter() { + private _getUpdateFilter() { return { ...(this._set && { $set: Object.fromEntries(this._set) }), ...(this._unset && { $unset: Object.fromEntries([...this._unset.values()].map((k) => [k, 1])) }), @@ -79,6 +62,18 @@ export class UpdaterImpl implements Updater { ...(this._addToSet && { $addToSet: Object.fromEntries([...this._addToSet.entries()].map(([k, v]) => [k, { $each: v }])) }), } as unknown as UpdateFilter; } + + getUpdateFilter() { + if (this.dirty) { + throw new Error('Updater is dirty'); + } + this.dirty = true; + const filter = this._getUpdateFilter(); + if (!this._hasChanges(filter)) { + throw new Error('No changes to update'); + } + return filter; + } } export { Updater }; From f88212491e0ef4e6b3b5f14f51e4f77ad51c9acb Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 14 Aug 2024 21:48:45 +0530 Subject: [PATCH 06/49] fix: disable deletion for federated users (#32852) --- .changeset/empty-toys-smell.md | 5 +++++ .../app/lib/server/functions/deleteUser.ts | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .changeset/empty-toys-smell.md diff --git a/.changeset/empty-toys-smell.md b/.changeset/empty-toys-smell.md new file mode 100644 index 000000000000..043d9c19567d --- /dev/null +++ b/.changeset/empty-toys-smell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Federated users can no longer be deleted. diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index d6457664671a..e66c8c2d5eef 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -1,5 +1,5 @@ import { api } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; +import { isUserFederated, type IUser } from '@rocket.chat/core-typings'; import { Integrations, FederationServers, @@ -12,6 +12,7 @@ import { ReadReceipts, LivechatUnitMonitors, ModerationReports, + MatrixBridgedUser, } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -46,6 +47,19 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele return; } + if (isUserFederated(user)) { + throw new Meteor.Error('error-not-allowed', 'Deleting federated, external user is not allowed', { + method: 'deleteUser', + }); + } + + const remoteUser = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId); + if (remoteUser) { + throw new Meteor.Error('error-not-allowed', 'User participated in federation, this user can only be deactivated permanently', { + method: 'deleteUser', + }); + } + const subscribedRooms = await getSubscribedRoomsForUserWithDetails(userId); if (shouldRemoveOrChangeOwner(subscribedRooms) && !confirmRelinquish) { From 1b30512b25d3ab9c8f6a8d808322202dce00ec0d Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 14 Aug 2024 16:15:51 -0300 Subject: [PATCH 07/49] feat: Replaces outdated retention policy warning in favor of `Bubble` (#33044) --- .changeset/large-geese-ring.md | 5 +++++ .../app/theme/client/imports/general/base_old.css | 15 --------------- .../views/room/body/RetentionPolicyWarning.tsx | 15 ++++++--------- apps/meteor/client/views/room/body/RoomBody.tsx | 4 ++-- apps/meteor/client/views/room/body/RoomBodyV2.tsx | 4 ++-- 5 files changed, 15 insertions(+), 28 deletions(-) create mode 100644 .changeset/large-geese-ring.md diff --git a/.changeset/large-geese-ring.md b/.changeset/large-geese-ring.md new file mode 100644 index 000000000000..9b36edf1c02d --- /dev/null +++ b/.changeset/large-geese-ring.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Replaces an outdated banner with the Bubble component in order to display retention policy warning diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index 20b023cc61aa..3120d9c05ff0 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -776,21 +776,6 @@ padding: 21px 0 10px; } - & .start { - margin-top: 44px; - - text-align: center; - - & .start__purge-warning { - margin-top: -33px; - margin-bottom: 0.5rem; - padding: 1rem; - - border-width: 1px 0 0; - background: linear-gradient(to bottom, var(--rc-color-alert-message-warning-background) 0%, rgba(255, 255, 255, 0) 100%); - } - } - & .editing .body { border-radius: var(--border-radius); } diff --git a/apps/meteor/client/views/room/body/RetentionPolicyWarning.tsx b/apps/meteor/client/views/room/body/RetentionPolicyWarning.tsx index 12fdff976a1f..f4939a261145 100644 --- a/apps/meteor/client/views/room/body/RetentionPolicyWarning.tsx +++ b/apps/meteor/client/views/room/body/RetentionPolicyWarning.tsx @@ -1,5 +1,5 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { Icon } from '@rocket.chat/fuselage'; +import { Bubble, MessageDivider } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -13,14 +13,11 @@ const RetentionPolicyWarning = ({ room }: { room: IRoom }): ReactElement => { const message = usePruneWarningMessage(room); return ( -

- {message} -
+ + + {message} + + ); }; diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 31f8440643b7..a592bb1fa2c0 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -290,9 +290,9 @@ const RoomBody = (): ReactElement => { {hasMorePreviousMessages ? (
  • {isLoadingMoreMessages ? : null}
  • ) : ( -
  • - {retentionPolicy?.isActive ? : null} +
  • + {retentionPolicy?.isActive ? : null}
  • )} diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 32b4288b3b0e..cfd6cb94cb51 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -262,9 +262,9 @@ const RoomBody = (): ReactElement => { {hasMorePreviousMessages ? (
  • {isLoadingMoreMessages ? : null}
  • ) : ( -
  • - {retentionPolicy?.isActive ? : null} +
  • + {retentionPolicy?.isActive ? : null}
  • )} From ec0cec69e796f92726c87f6545510e7163c9dd50 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 14 Aug 2024 17:00:18 -0300 Subject: [PATCH 08/49] chore: turbo env-mode: loose for dev (#33056) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6f7980d8bfc9..29de436373e2 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "build:services": "turbo run build --filter=rocketchat-services...", "build:ci": "turbo run build:ci", "testunit": "turbo run testunit", - "dev": "turbo run dev --parallel --filter=@rocket.chat/meteor...", - "dsv": "turbo run dsv --filter=@rocket.chat/meteor...", + "dev": "turbo run dev --env-mode=loose --parallel --filter=@rocket.chat/meteor...", + "dsv": "turbo run dsv --env-mode=loose --filter=@rocket.chat/meteor...", "lint": "turbo run lint", "storybook": "yarn workspace @rocket.chat/meteor run storybook", "fuselage": "./fuselage.sh", From 447888779d00bb91d6ee0b234a9f0686c60440be Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 15 Aug 2024 10:23:32 -0300 Subject: [PATCH 09/49] ci: fix external pull requests (#33063) --- .github/actions/build-docker/action.yml | 26 ++++++++++++++++++++++--- .github/workflows/ci-test-e2e.yml | 15 +++++++------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 75673c15bfd6..6f8250d2acd4 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -17,13 +17,25 @@ inputs: required: false description: 'Containers to build along with Rocket.Chat' type: string + turbo-cache: + required: false + description: 'Enable turbo cache' + default: 'true' + publish-image: + required: false + description: 'Publish image' + default: 'true' + setup: + required: false + description: 'Setup node.js' + default: 'true' runs: using: composite steps: - name: Login to GitHub Container Registry - if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + if: inputs.publish-image == 'true' &&(github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') uses: docker/login-action@v2 with: registry: ghcr.io @@ -42,17 +54,20 @@ runs: cd /tmp/build tar xzf Rocket.Chat.tar.gz rm Rocket.Chat.tar.gz - - uses: rharkor/caching-for-turbo@v1.5 + # if we are testing a PR from a fork, we already called the turbo cache at this point, so it should be false + if: inputs.turbo-cache == 'true' - name: Setup NodeJS uses: ./.github/actions/setup-node + if: inputs.setup == 'true' with: node-version: ${{ inputs.node-version }} cache-modules: true install: true - run: yarn build + if: inputs.setup == 'true' shell: bash - name: Build Docker images @@ -63,9 +78,14 @@ runs: docker compose -f docker-compose-ci.yml build "${args[@]}" - name: Publish Docker images to GitHub Container Registry - if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + if: inputs.publish-image == 'true' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') shell: bash run: | args=(rocketchat ${{ inputs.build-containers }}) docker compose -f docker-compose-ci.yml push "${args[@]}" + + - name: Clean up temporary files + shell: bash + run: | + sudo rm -rf /tmp/bundle diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index e8dd480d5b27..31a8bc2ea2b6 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -130,7 +130,9 @@ jobs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + - uses: rharkor/caching-for-turbo@v1.5 + - run: yarn build # if we are testing a PR from a fork, we need to build the docker image at this point - uses: ./.github/actions/build-docker if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository @@ -138,8 +140,11 @@ jobs: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ inputs.node-version }} - - - uses: rharkor/caching-for-turbo@v1.5 + # we already called the turbo cache at this point, so it should be false + turbo-cache: false + # the same reason we need to rebuild the docker image at this point is the reason we dont want to publish it + publish-image: false + setup: false - name: Start httpbin container and wait for it to be ready if: inputs.type == 'api' @@ -159,8 +164,6 @@ jobs: exit 1 fi - - run: yarn build - - name: Prepare code coverage directory if: inputs.release == 'ee' run: | @@ -187,10 +190,6 @@ jobs: run: | docker compose -f docker-compose-ci.yml up -d - - name: Clean up temporary files - run: | - sudo rm -rf /tmp/bundle - - name: Cache Playwright binaries if: inputs.type == 'ui' uses: actions/cache@v3 From 77989f51dd6e932344267cdb3b784d9234e8c3de Mon Sep 17 00:00:00 2001 From: Kunal Agrawal <92196937+its-kunal@users.noreply.github.com> Date: Thu, 15 Aug 2024 19:34:56 +0530 Subject: [PATCH 10/49] i18n: more Hindi translation keys added (#30927) Co-authored-by: Douglas Fabris Co-authored-by: Guilherme Gazzo --- packages/i18n/src/locales/hi-IN.i18n.json | 5929 ++++++++++++++++++++- 1 file changed, 5926 insertions(+), 3 deletions(-) diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index d68f9e2d8d08..1049d8495d86 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -1,9 +1,29 @@ { "500": "आंतरिक सर्वर त्रुटि", + "__agents__agents_and__count__conversations__period__": "{{agents}} एजेंट और {{count}} बातचीत, {{period}}", + "__count__empty_rooms_will_be_removed_automatically": "{{count}} खाली कमरे स्वचालित रूप से हटा दिए जाएंगे।", + "__count__empty_rooms_will_be_removed_automatically__rooms__": "{{count}} खाली कमरे स्वचालित रूप से हटा दिए जाएंगे:
    {{rooms}}।", + "__count__message_pruned": "{{count}} संदेश काट दिया गया", + "__count__conversations__period__": "{{count}} बातचीत, {{period}}", + "__count__tags__and__count__conversations__period__": "{{count}} टैग और {{conversations}} बातचीत, {{period}}", + "__departments__departments_and__count__conversations__period__": "{{departments}} विभाग और {{count}} बातचीत, {{period}}", + "__usersCount__member_joined": "+ {{usersCount}} सदस्य शामिल हुए", + "__usersCount__people_will_be_invited": "{{usersCount}} लोगों को आमंत्रित किया जाएगा", "__username__is_no_longer__role__defined_by__user_by_": "{{username}} is no longer {{role}} by {{user_by}}", "__username__was_set__role__by__user_by_": "{{username}} was set {{role}} by {{user_by}}", + "__count__without__department__": "बिना विभाग के {{count}}", + "__count__without__tags__": "बिना टैग के {{count}}", + "__count__without__assignee__": "{{count}} बिना असाइनी के", + "removed__username__as__role_": "{{username}} को {{role}} के रूप में हटा दिया गया", + "set__username__as__role_": "{{username}} को {{role}} के रूप में सेट करें", + "This_room_encryption_has_been_enabled_by__username_": "इस कमरे का एन्क्रिप्शन {{username}} द्वारा सक्षम किया गया है", + "This_room_encryption_has_been_disabled_by__username_": "इस कमरे का एन्क्रिप्शन {{username}} द्वारा अक्षम कर दिया गया है", + "Third_party_login": "तृतीय-पक्ष लॉगिन", + "Enabled_E2E_Encryption_for_this_room": "इस कमरे के लिए E2E एन्क्रिप्शन सक्षम किया गया", + "disabled": "अक्षम", + "Disabled_E2E_Encryption_for_this_room": "इस कमरे के लिए अक्षम E2E एन्क्रिप्शन", "@username": "@यूज़रनेम", - "@username_message": "@यूज़रनेम ", + "@username_message": "@यूज़रनेम ", "#channel": "#चैनल", "%_of_conversations": "% बातचीत", "0_Errors_Only": "0 - त्रुटियां केवल", @@ -11,18 +31,38 @@ "2_Erros_Information_and_Debug": "2 - त्रुटियां, सूचना और डिबग", "12_Hour": "12-घंटे की घड़ी", "24_Hour": "24-घंटे की घड़ी", + "A_cloud-based_platform_for_those_needing_a_plug-and-play_app": "प्लग-एंड-प्ले ऐप की आवश्यकता वाले लोगों के लिए एक क्लाउड-आधारित प्लेटफ़ॉर्म।", + "A_new_owner_will_be_assigned_automatically_to__count__rooms": "एक नए मालिक को स्वचालित रूप से {{count}} कमरों को सौंपा जाएगा।", + "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "एक नए मालिक को स्वचालित रूप से {{roomName}} कमरे का कार्यभार सौंपा जाएगा।", + "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "एक नए मालिक को स्वचालित रूप से उन {{count}} कमरों को सौंपा जाएगा:
    {{rooms}}।", + "A_secure_and_highly_private_self-managed_solution_for_conference_calls": "कॉन्फ़्रेंस कॉल के लिए एक सुरक्षित और अत्यधिक निजी स्व-प्रबंधित समाधान।", + "A_workspace_admin_needs_to_install_and_configure_a_conference_call_app": "एक कार्यस्थान व्यवस्थापक को एक कॉन्फ़्रेंस कॉल ऐप इंस्टॉल और कॉन्फ़िगर करने की आवश्यकता होती है।", + "An_app_needs_to_be_installed_and_configured": "एक ऐप इंस्टॉल और कॉन्फ़िगर करना होगा.", + "Accessibility": "सरल उपयोग", + "Accessibility_and_Appearance": "पहुंच एवं उपस्थिति", + "Accessibility_activation": "यहां आप अपने ब्राउज़िंग अनुभव को बेहतर बनाने के लिए कई प्रकार की सुविधाएं सक्रिय कर सकते हैं।", + "Accept_Call": "कॉल लेना", "Accept": "स्वीकार करें", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "यदि कोई ऑनलाइन एजेंट नहीं हैं, तो भी इनकमिंग लाइवचैट अनुरोध स्वीकार करें", + "Accept_new_livechats_when_agent_is_idle": "जब एजेंट निष्क्रिय हो तो नए ओमनीचैनल अनुरोध स्वीकार करें", "Accept_with_no_online_agents": "कोई ऑनलाइन एजेंटों के साथ स्वीकार करें", "Access_not_authorized": "प्रवेश अधिकृत नहीं है", "Access_Token_URL": "एक्सेस टोकन URL", + "Access_Your_Account": "अपने खाते पर पहुंच", + "access_your_basic_information": "अपनी बुनियादी सूचना का आंकलन करें", "access-mailer": "मेलर स्क्रीन एक्सेस करें", "access-mailer_description": "सभी उपयोगकर्ताओं को बड़े पैमाने पर ईमेल भेजने की अनुमति।", + "access-marketplace": "बाज़ार तक पहुंचें", + "access-marketplace_description": "बाज़ार से ऐप्स ब्राउज़ करने और प्राप्त करने की अनुमति", "access-permissions": "अनुमतियाँ स्क्रीन एक्सेस करें", "access-permissions_description": "विभिन्न भूमिकाओं के लिए अनुमतियों को संशोधित करें।", + "access-setting-permissions": "सेटिंग-आधारित अनुमतियाँ संशोधित करें", + "access-setting-permissions_description": "सेटिंग-आधारित अनुमतियों को संशोधित करने की अनुमति", "Accessing_permissions": "अक्सेस्सिंग की अनुमति", "Account_SID": "खाता एसआईडी", + "Account": "खाता", "Accounts": "खाता", + "Accounts_Description": "कार्यस्थान सदस्य खाता सेटिंग संशोधित करें.", "Accounts_Admin_Email_Approval_Needed_Default": "

    The user [name] ([email]) has been registered.

    Please check \"Administration -> Users\" to activate or delete it.

    ", "Accounts_Admin_Email_Approval_Needed_Subject_Default": "एक नया उपयोगकर्ता पंजीकृत है और उसे अनुमोदन की आवश्यकता है", "Accounts_Admin_Email_Approval_Needed_With_Reason_Default": "

    The user [name] ([email]) has been registered.

    Reason: [reason]

    Please check \"Administration -> Users\" to activate or delete it.

    ", @@ -31,12 +71,17 @@ "Accounts_AllowDeleteOwnAccount": "उपयोगकर्ताओं को स्वयं का खाता हटाने की अनुमति दें", "Accounts_AllowedDomainsList": "अनुमत डोमेन सूची", "Accounts_AllowedDomainsList_Description": "अनुमत डोमेन की कोमा-पृथक सूची", + "Accounts_AllowInvisibleStatusOption": "अदृश्य स्थिति विकल्प की अनुमति दें", "Accounts_AllowEmailChange": "ईमेल परिवर्तन की अनुमति दें", + "Accounts_AllowEmailNotifications": "ईमेल सूचनाओं की अनुमति दें", + "Accounts_AllowFeaturePreview": "फ़ीचर पूर्वावलोकन की अनुमति दें", "Accounts_AllowPasswordChange": "पासवर्ड बदलने की अनुमति दें", + "Accounts_AllowPasswordChangeForOAuthUsers": "OAuth उपयोगकर्ताओं के लिए पासवर्ड बदलने की अनुमति दें", "Accounts_AllowRealNameChange": "नाम बदलने की अनुमति दें", "Accounts_AllowUserAvatarChange": "उपयोगकर्ता अवतार परिवर्तन की अनुमति दें", "Accounts_AllowUsernameChange": "उपयोगकर्ता नाम बदलने की अनुमति दें", "Accounts_AllowUserProfileChange": "उपयोगकर्ता प्रोफ़ाइल बदलने की अनुमति दें", + "Accounts_AllowUserStatusMessageChange": "कस्टम स्थिति संदेश की अनुमति दें", "Accounts_AvatarBlockUnauthenticatedAccess": "अपुष्ट एक्सेस को अवतारों से ब्लॉक करें", "Accounts_AvatarCacheTime": "अवतार कैश समय", "Accounts_AvatarCacheTime_description": "HTTP प्रोटोकॉल को अवतार छवियों को कैश करने के लिए सेकंड की संख्या बताई गई है।", @@ -52,11 +97,14 @@ "Accounts_CustomFieldsToShowInUserInfo": "कस्टम फ़ील्ड उपयोगकर्ता जानकारी में दिखाने के लिए", "Accounts_Default_User_Preferences": "डिफ़ॉल्ट उपयोगकर्ता प्राथमिकताएं", "Accounts_Default_User_Preferences_audioNotifications": "ऑडियो सूचनाएं डिफ़ॉल्ट चेतावनी", + "Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "उपयोगकर्ताओं को चैनल को भी भेजें व्यवहार का चयन करने की अनुमति दें", "Accounts_Default_User_Preferences_desktopNotifications": "डेस्कटॉप सूचनाएं डिफ़ॉल्ट चेतावनी", "Accounts_Default_User_Preferences_pushNotifications": "मोबाइल सूचनाएं डिफ़ॉल्ट चेतावनी", "Accounts_Default_User_Preferences_not_available": "उपयोगकर्ता प्राथमिकताएँ प्राप्त करने में विफल, क्योंकि वे उपयोगकर्ता द्वारा अभी तक सेट नहीं किए गए हैं", + "Accounts_Default_User_Preferences_showThreadsInMainChannel_Description": "सक्षम होने पर, थ्रेड के अंतर्गत सभी उत्तर भी सीधे मुख्य कक्ष में प्रदर्शित किए जाएंगे। अक्षम होने पर, प्रेषक की पसंद के आधार पर थ्रेड उत्तर प्रदर्शित किए जाएंगे।", "Accounts_DefaultUsernamePrefixSuggestion": "डिफ़ॉल्ट उपयोगकर्ता नाम उपसर्ग सुझाव", "Accounts_denyUnverifiedEmail": "अयोग्य ईमेल अस्वीकार करें", + "Accounts_Directory_DefaultView": "डिफ़ॉल्ट निर्देशिका सूची", "Accounts_Email_Activated": "[name]

    आपका खाता सक्रिय हो गया था।

    ", "Accounts_Email_Activated_Subject": "खाता सक्रिय किया गया", "Accounts_Email_Approved": "[name]

    आपका खाता स्वीकृत हो गया।

    ", @@ -76,18 +124,36 @@ "Accounts_iframe_url": "Iframe URL", "Accounts_LoginExpiration": "दिन में प्रवेश की समाप्ति", "Accounts_ManuallyApproveNewUsers": "नए उपयोगकर्ताओं को मैन्युअल रूप से अनुमोदित करें", + "Accounts_OAuth_Apple": "Apple के साथ साइन इन करें", + "Accounts_OAuth_Apple_Description": "यदि आप चाहते हैं कि Apple लॉगिन केवल मोबाइल पर सक्षम हो, तो आप सभी फ़ील्ड खाली छोड़ सकते हैं।", + "Accounts_OAuth_Custom_Access_Token_Param": "एक्सेस टोकन के लिए परम नाम", "Accounts_OAuth_Custom_Authorize_Path": "पथ अधिकृत करें", + "Accounts_OAuth_Custom_Avatar_Field": "अवतार क्षेत्र", "Accounts_OAuth_Custom_Button_Color": "बटन का रंग", "Accounts_OAuth_Custom_Button_Label_Color": "बटन टेक्स्ट का रंग", "Accounts_OAuth_Custom_Button_Label_Text": "बटन टेक्स्ट", + "Accounts_OAuth_Custom_Channel_Admin": "उपयोगकर्ता डेटा समूह मानचित्र", + "Accounts_OAuth_Custom_Channel_Map": "OAuth समूह चैनल मानचित्र", + "Accounts_OAuth_Custom_Email_Field": "ईमेल फ़ील्ड", "Accounts_OAuth_Custom_Enable": "सक्षम करें", + "Accounts_OAuth_Custom_Groups_Claim": "चैनल मैपिंग के लिए भूमिकाएँ/समूह फ़ील्ड", "Accounts_OAuth_Custom_id": "Id", "Accounts_OAuth_Custom_Identity_Path": "पहचान पथ", - "Accounts_OAuth_Custom_Identity_Token_Sent_Via": "पहचान टोकन भेजा गया", + "Accounts_OAuth_Custom_Identity_Token_Sent_Via": "के जरिए पहचान टोकन भेजा गया", + "Accounts_OAuth_Custom_Key_Field": "कुंजी क्षेत्र", "Accounts_OAuth_Custom_Login_Style": "लॉगिन शैली", + "Accounts_OAuth_Custom_Map_Channels": "भूमिकाओं/समूहों को चैनलों पर मैप करें", + "Accounts_OAuth_Custom_Merge_Roles": "SSO से भूमिकाएँ मर्ज करें", "Accounts_OAuth_Custom_Merge_Users": "उपयोगकर्ताओं को मर्ज करें", + "Accounts_OAuth_Custom_Merge_Users_Distinct_Services": "उपयोगकर्ताओं को अलग-अलग सेवाओं से मर्ज करें", + "Accounts_OAuth_Custom_Merge_Users_Distinct_Services_Description": "जब दिया गया कुंजी फ़ील्ड किसी मौजूदा उपयोगकर्ता से मेल खाता है, तो इस OAuth सेवा के उपयोगकर्ताओं को उनकी मूल सेवा की परवाह किए बिना मौजूदा उपयोगकर्ताओं में विलय करने की अनुमति दें।", + "Accounts_OAuth_Custom_Name_Field": "नाम फ़ील्ड", + "Accounts_OAuth_Custom_Roles_Claim": "भूमिकाएँ/समूह फ़ील्ड नाम", + "Accounts_OAuth_Custom_Roles_To_Sync": "सिंक करने के लिए भूमिकाएँ", + "Accounts_OAuth_Custom_Roles_To_Sync_Description": "उपयोगकर्ता लॉगिन और निर्माण पर सिंक करने के लिए OAuth भूमिकाएँ (अल्पविराम से अलग)।", "Accounts_OAuth_Custom_Scope": "क्षेत्र", "Accounts_OAuth_Custom_Secret": "गुप्त", + "Accounts_OAuth_Custom_Show_Button_On_Login_Page": "लॉगिन पेज पर बटन दिखाएँ", "Accounts_OAuth_Custom_Token_Path": "टोकन पथ", "Accounts_OAuth_Custom_Token_Sent_Via": "के जरिए टोकन भेजा गया", "Accounts_OAuth_Custom_Username_Field": "उपयोगकर्ता नाम फ़ील्ड", @@ -111,6 +177,7 @@ "Accounts_OAuth_Gitlab_callback_url": "GitLab कॉलबैक URL", "Accounts_OAuth_Gitlab_id": "Gitlab Id", "Accounts_OAuth_Gitlab_identity_path": "पहचान पथ", + "Accounts_OAuth_Gitlab_merge_users": "उपयोगकर्ताओं को मर्ज करें", "Accounts_OAuth_Gitlab_secret": "क्लाइंट Secret", "Accounts_OAuth_Google": "Google लॉगिन", "Accounts_OAuth_Google_callback_url": "Google कॉलबैक URL", @@ -125,7 +192,10 @@ "Accounts_OAuth_Meteor_id": "Meteor Id", "Accounts_OAuth_Meteor_secret": "Meteor Secret", "Accounts_OAuth_Nextcloud": "OAuth सक्षम", + "Accounts_OAuth_Nextcloud_callback_url": "नेक्स्टक्लाउड कॉलबैक यूआरएल", + "Accounts_OAuth_Nextcloud_id": "नेक्स्टक्लाउड आईडी", "Accounts_OAuth_Nextcloud_secret": "क्लाइंट Secret", + "Accounts_OAuth_Nextcloud_URL": "नेक्स्टक्लाउड सर्वर यूआरएल", "Accounts_OAuth_Proxy_host": "प्रॉक्सी होस्ट", "Accounts_OAuth_Proxy_services": "प्रॉक्सी सेवाएँ", "Accounts_OAuth_Tokenpass": "Tokenpass लॉगइन", @@ -152,65 +222,5918 @@ "Accounts_Password_Policy_AtLeastOneLowercase_Description": "लागू करें कि पासवर्ड में कम से कम एक लोअरकेस वर्ण हो।", "Accounts_Password_Policy_AtLeastOneNumber": "कम से कम एक नंबर", "Accounts_Password_Policy_AtLeastOneNumber_Description": "लागू करें कि एक पासवर्ड में कम से कम एक संख्यात्मक चरित्र होता है।", + "Accounts_Password_Policy_AtLeastOneSpecialCharacter": "कम से कम एक प्रतीक", + "Accounts_Password_Policy_AtLeastOneSpecialCharacter_Description": "यह सुनिश्चित करें कि पासवर्ड में कम से कम एक विशेष अक्षर हो।", + "Accounts_Password_Policy_AtLeastOneUppercase": "कम से कम एक अपरकेस", "Accounts_Password_Policy_AtLeastOneUppercase_Description": "लागू करें कि पासवर्ड में कम से कम एक लोअरकेस वर्ण हो।", + "Accounts_Password_Policy_Enabled": "पासवर्ड नीति सक्षम करें", + "Accounts_Password_Policy_Enabled_Description": "सक्षम होने पर, उपयोगकर्ता पासवर्ड को निर्धारित नीतियों का पालन करना होगा। ध्यान दें: यह केवल नए पासवर्ड पर लागू होता है, मौजूदा पासवर्ड पर नहीं।", + "Accounts_Password_Policy_ForbidRepeatingCharacters": "अक्षरों को दोहराने से मना करें", + "Accounts_Password_Policy_ForbidRepeatingCharacters_Description": "यह सुनिश्चित करता है कि पासवर्ड में एक-दूसरे के बगल में दोहराए जाने वाले समान अक्षर न हों।", + "Accounts_Password_Policy_ForbidRepeatingCharactersCount": "अधिकतम दोहराव वाले अक्षर", + "Accounts_Password_Policy_ForbidRepeatingCharactersCount_Description": "किसी पात्र को पहले कितनी बार दोहराया जा सकता है इसकी अनुमति नहीं है।", + "Accounts_Password_Policy_MaxLength": "ज्यादा से ज्यादा लंबाई", + "Accounts_Password_Policy_MaxLength_Description": "यह सुनिश्चित करता है कि पासवर्ड में इस संख्या से अधिक अक्षर न हों। अक्षम करने के लिए `-1` का उपयोग करें.", + "Accounts_Password_Policy_MinLength": "न्यूनतम लंबाई", + "Accounts_Password_Policy_MinLength_Description": "यह सुनिश्चित करता है कि पासवर्ड में कम से कम इतने अक्षर होने चाहिए। अक्षम करने के लिए `-1` का उपयोग करें.", + "Accounts_PasswordReset": "पासवर्ड रीसेट", + "Accounts_Registration_AuthenticationServices_Default_Roles": "प्रमाणीकरण सेवाओं के लिए डिफ़ॉल्ट भूमिकाएँ", + "Accounts_Registration_AuthenticationServices_Default_Roles_Description": "प्रमाणीकरण सेवाओं के माध्यम से पंजीकरण करते समय उपयोगकर्ताओं को डिफ़ॉल्ट भूमिकाएँ (अल्पविराम से अलग) दी जाएंगी", + "Accounts_Registration_AuthenticationServices_Enabled": "प्रमाणीकरण सेवाओं के साथ पंजीकरण", + "Accounts_Registration_Users_Default_Roles": "उपयोगकर्ताओं के लिए डिफ़ॉल्ट भूमिकाएँ", + "Accounts_Registration_Users_Default_Roles_Description": "मैन्युअल पंजीकरण (एपीआई सहित) के माध्यम से पंजीकरण करते समय उपयोगकर्ताओं को डिफ़ॉल्ट भूमिकाएं (अल्पविराम से अलग) दी जाएंगी", + "Accounts_Registration_Users_Default_Roles_Enabled": "मैन्युअल पंजीकरण के लिए डिफ़ॉल्ट भूमिकाएँ सक्षम करें", + "Accounts_Registration_InviteUrlType": "आमंत्रण URL प्रकार", "Accounts_Registration_InviteUrlType_Direct": "सीधा", + "Accounts_Registration_InviteUrlType_Proxy": "प्रतिनिधि", "Accounts_RegistrationForm": "पंजीकरण पत्र", "Accounts_RegistrationForm_Disabled": "उपयोग करने की अनुमति नहीं है", + "Accounts_RegistrationForm_LinkReplacementText": "पंजीकरण फॉर्म लिंक प्रतिस्थापन पाठ", "Accounts_RegistrationForm_Public": "जनता", + "Accounts_RegistrationForm_Secret_URL": "गुप्त यूआरएल", + "Accounts_RegistrationForm_SecretURL": "पंजीकरण प्रपत्र गुप्त यूआरएल", + "Accounts_RegistrationForm_SecretURL_Description": "आपको एक यादृच्छिक स्ट्रिंग प्रदान करनी होगी जो आपके पंजीकरण URL में जोड़ी जाएगी। उदाहरण: `https://open.rocket.chat/register/[secret_hash]`", + "Accounts_RequireNameForSignUp": "साइनअप के लिए नाम की आवश्यकता है", + "Accounts_RequirePasswordConfirmation": "पासवर्ड पुष्टिकरण की आवश्यकता है", + "Accounts_RoomAvatarExternalProviderUrl": "कक्ष अवतार बाहरी प्रदाता यूआरएल", + "Accounts_RoomAvatarExternalProviderUrl_Description": "उदाहरण: `https://acme.com/api/v1/{roomId}`", + "Accounts_SearchFields": "खोज में विचार करने योग्य फ़ील्ड", + "Accounts_Send_Email_When_Activating": "उपयोगकर्ता सक्रिय होने पर उपयोगकर्ता को ईमेल भेजें", + "Accounts_Send_Email_When_Deactivating": "उपयोगकर्ता के निष्क्रिय होने पर उपयोगकर्ता को ईमेल भेजें", + "Accounts_Set_Email_Of_External_Accounts_as_Verified": "बाहरी खातों के ईमेल को सत्यापित के रूप में सेट करें", + "Accounts_Set_Email_Of_External_Accounts_as_Verified_Description": "एलडीएपी, ओएथ आदि जैसी बाहरी सेवाओं से बनाए गए खातों के ईमेल स्वचालित रूप से सत्यापित हो जाएंगे", + "Accounts_SetDefaultAvatar": "डिफ़ॉल्ट अवतार सेट करें", + "Accounts_SetDefaultAvatar_Description": "OAuth खाते या Gravatar के आधार पर डिफ़ॉल्ट अवतार निर्धारित करने का प्रयास करता है", + "Accounts_ShowFormLogin": "डिफ़ॉल्ट लॉगिन फॉर्म दिखाएँ", + "Accounts_TwoFactorAuthentication_By_TOTP_Enabled": "टीओटीपी के माध्यम से दो कारक प्रमाणीकरण सक्षम करें", + "Accounts_TwoFactorAuthentication_By_TOTP_Enabled_Description": "उपयोगकर्ता Google Authenticator या Authy जैसे किसी भी TOTP ऐप का उपयोग करके अपना टू फैक्टर ऑथेंटिकेशन सेटअप कर सकते हैं।", + "Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In": "ईमेल के माध्यम से टू फैक्टर के लिए नए उपयोगकर्ताओं को ऑटो ऑप्ट इन करें", + "Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In_Description": "नए उपयोगकर्ताओं के पास ईमेल के माध्यम से दो कारक प्रमाणीकरण डिफ़ॉल्ट रूप से सक्षम होगा। वे इसे अपने प्रोफ़ाइल पृष्ठ में अक्षम कर सकेंगे.", + "Accounts_TwoFactorAuthentication_By_Email_Code_Expiration": "ईमेल के माध्यम से भेजे गए कोड को सेकंडों में समाप्त करने का समय", + "Accounts_TwoFactorAuthentication_By_Email_Enabled": "ईमेल के माध्यम से दो कारक प्रमाणीकरण सक्षम करें", + "Accounts_TwoFactorAuthentication_By_Email_Enabled_Description": "जिन उपयोगकर्ताओं का ईमेल सत्यापित है और उनके प्रोफ़ाइल पृष्ठ में विकल्प सक्षम है, उन्हें कुछ कार्यों जैसे लॉगिन, प्रोफ़ाइल सहेजना आदि को अधिकृत करने के लिए एक अस्थायी कोड के साथ एक ईमेल प्राप्त होगा।", + "Accounts_TwoFactorAuthentication_Enabled": "दो कारक प्रमाणीकरण सक्षम करें", + "Accounts_TwoFactorAuthentication_Enabled_Description": "निष्क्रिय होने पर, यह सेटिंग सभी दो कारक प्रमाणीकरण को निष्क्रिय कर देगी।\nउपयोगकर्ताओं को दो कारक प्रमाणीकरण का उपयोग करने के लिए बाध्य करने के लिए, व्यवस्थापक को इसे लागू करने के लिए 'उपयोगकर्ता' भूमिका को कॉन्फ़िगर करना होगा।", + "Accounts_TwoFactorAuthentication_Enforce_Password_Fallback": "पासवर्ड फ़ॉलबैक लागू करें", + "Accounts_TwoFactorAuthentication_Enforce_Password_Fallback_Description": "यदि उस उपयोगकर्ता के लिए कोई अन्य दो कारक प्रमाणीकरण विधि सक्षम नहीं है और उसके लिए एक पासवर्ड सेट किया गया है, तो महत्वपूर्ण कार्यों के लिए उपयोगकर्ताओं को अपना पासवर्ड दर्ज करने के लिए मजबूर किया जाएगा।", "Accounts_TwoFactorAuthentication_MaxDelta": "soochna", + "Accounts_TwoFactorAuthentication_MaxDelta_Description": "अधिकतम डेल्टा यह निर्धारित करता है कि किसी भी समय कितने टोकन वैध हैं। टोकन हर 30 सेकंड में उत्पन्न होते हैं, और (30 * अधिकतम डेल्टा) सेकंड के लिए वैध होते हैं।\nउदाहरण: अधिकतम डेल्टा 10 पर सेट होने पर, प्रत्येक टोकन का उपयोग उसके टाइमस्टैम्प से 300 सेकंड पहले या बाद तक किया जा सकता है। यह तब उपयोगी होता है जब क्लाइंट की घड़ी सर्वर के साथ ठीक से समन्वयित नहीं होती है।", + "Accounts_TwoFactorAuthentication_RememberFor": "(सेकंड) के लिए दो कारक याद रखें", + "Accounts_TwoFactorAuthentication_RememberFor_Description": "यदि दो कारक प्राधिकरण कोड पहले ही दिए गए समय में प्रदान किया गया हो तो उसका अनुरोध न करें।", + "Accounts_UseDefaultBlockedDomainsList": "डिफ़ॉल्ट अवरुद्ध डोमेन सूची का उपयोग करें", + "Accounts_UseDNSDomainCheck": "DNS डोमेन जाँच का उपयोग करें", + "API_EmbedDisabledFor": "उपयोगकर्ताओं के लिए एंबेड अक्षम करें", + "Accounts_UserAddedEmail_Default": "

    [साइट_नाम] में आपका स्वागत है

    [Site_URL] पर जाएँ और आज उपलब्ध सर्वोत्तम ओपन सोर्स चैट समाधान आज़माएँ!

    आप अपने ईमेल: [ईमेल] और पासवर्ड: [पासवर्ड] का उपयोग करके लॉगिन कर सकते हैं। आपको अपने पहले लॉगिन के बाद इसे बदलने की आवश्यकता हो सकती है।", + "Accounts_UserAddedEmail_Description": "आप निम्नलिखित प्लेसहोल्डर्स का उपयोग कर सकते हैं:\n - `[नाम]`, `[fname]`, `[lname]` क्रमशः उपयोगकर्ता के पूर्ण नाम, प्रथम नाम या अंतिम नाम के लिए।\n - `[ईमेल]` उपयोगकर्ता के ईमेल के लिए।\n - उपयोगकर्ता के पासवर्ड के लिए `[पासवर्ड]`।\n - एप्लिकेशन नाम और यूआरएल के लिए क्रमशः `[Site_Name]` और `[Site_URL]`।", + "API_EmbedDisabledFor_Description": "एम्बेडेड लिंक पूर्वावलोकन को अक्षम करने के लिए उपयोगकर्ता नामों की अल्पविराम से अलग की गई सूची।", + "Accounts_UserAddedEmailSubject_Default": "आपको [Site_Name] में जोड़ दिया गया है", + "Accounts_Verify_Email_For_External_Accounts": "सत्यापित बाहरी खातों के लिए ईमेल चिह्नित करें", + "Action": "कार्रवाई", + "Action_required": "कार्रवाई आवश्यक है", + "Action_Available_After_Custom_Content_Added": "कस्टम सामग्री जोड़े जाने के बाद यह क्रिया उपलब्ध हो जाएगी", + "Action_Available_After_Custom_Content_Added_And_Visible": "यह क्रिया कस्टम सामग्री जोड़े जाने और सभी के लिए दृश्यमान होने के बाद उपलब्ध हो जाएगी", + "Activate": "सक्रिय", + "Active": "सक्रिय", + "Active_users": "सक्रिय उपयोगकर्ता", + "Activity": "गतिविधि", + "Add": "जोड़ना", + "Add_a_Message": "कोई संदेश जोड़ें", + "Add_agent": "एजेंट जोड़ें", + "Add_custom_oauth": "कस्टम OAuth जोड़ें", + "Add_Domain": "डोमेन जोड़ें", + "Add_emoji": "इमोजी जोड़ें", + "Add_files_from": "से फ़ाइलें जोड़ें", + "Add_manager": "प्रबंधक जोड़ें", + "Add_monitor": "मॉनिटर जोड़ें", + "Add_Reaction": "प्रतिक्रिया जोड़ें", + "Add_Role": "भूमिका जोड़ें", + "Add_Sender_To_ReplyTo": "प्रेषक को उत्तर-प्रति में जोड़ें", + "Add_Server": "सर्वर जोड़े", + "Add_URL": "यूआरएल जोड़ें", + "Add_user": "उपयोगकर्ता जोड़ें", + "Add_User": "उपयोगकर्ता जोड़ें", + "Add_users": "उपयोगकर्ता जोड़ें", + "Add_members": "सदस्य जोड़ें", + "add-all-to-room": "सभी उपयोगकर्ताओं को एक कमरे में जोड़ें", + "add-all-to-room_description": "सभी उपयोगकर्ताओं को एक कमरे में जोड़ने की अनुमति", + "add-livechat-department-agents": "विभागों में ओमनीचैनल एजेंट जोड़ें", + "add-livechat-department-agents_description": "विभागों में ओमनीचैनल एजेंटों को जोड़ने की अनुमति", + "add-oauth-service": "OAuth सेवा जोड़ें", + "add-oauth-service_description": "नई OAuth सेवा जोड़ने की अनुमति", + "bypass-time-limit-edit-and-delete": "समय सीमा को बायपास करें", + "bypass-time-limit-edit-and-delete_description": "संदेशों को संपादित करने और हटाने के लिए समय सीमा को बायपास करने की अनुमति", + "add-team-channel": "टीम चैनल जोड़ें", + "add-team-channel_description": "किसी टीम में चैनल जोड़ने की अनुमति", + "add-team-member": "टीम सदस्य जोड़ें", + "add-team-member_description": "किसी टीम में सदस्यों को जोड़ने की अनुमति", + "add-user": "उपयोगकर्ता जोड़ें", + "add-user_description": "उपयोगकर्ता स्क्रीन के माध्यम से सर्वर पर नए उपयोगकर्ता जोड़ने की अनुमति", + "add-user-to-any-c-room": "किसी भी सार्वजनिक चैनल में उपयोगकर्ता जोड़ें", + "add-user-to-any-c-room_description": "किसी उपयोगकर्ता को किसी सार्वजनिक चैनल में जोड़ने की अनुमति", + "add-user-to-any-p-room": "किसी भी निजी चैनल में उपयोगकर्ता जोड़ें", + "add-user-to-any-p-room_description": "किसी निजी चैनल में उपयोगकर्ता जोड़ने की अनुमति", + "add-user-to-joined-room": "किसी भी जुड़े हुए चैनल में उपयोगकर्ता जोड़ें", + "add-user-to-joined-room_description": "किसी उपयोगकर्ता को वर्तमान में शामिल चैनल में जोड़ने की अनुमति", + "added__roomName__to_team": "इस टीम में #{{roomName}} जोड़ा गया", + "Added__username__to_team": "इस टीम में @{{user_added}} जोड़ा गया", + "added__roomName__to_this_team": "इस टीम में #{{roomName}} जोड़ा गया", + "Apps_Framework_enabled": "ऐप फ़्रेमवर्क सक्षम करें", + "Added__username__to_this_team": "इस टीम में @{{user_added}} जोड़ा गया", + "Adding_OAuth_Services": "OAuth सेवाएँ जोड़ना", + "Adding_permission": "अनुमति जोड़ी जा रही है", + "Adjustable_layout": "समायोज्य लेआउट", + "Adding_user": "उपयोगकर्ता जोड़ा जा रहा है", + "Additional_emails": "अतिरिक्त ईमेल", "Additional_Feedback": "अतिरिक्त प्रतिक्रिया", + "additional_integrations_Bots": "यदि आप यह खोज रहे हैं कि अपने स्वयं के बॉट को कैसे एकीकृत किया जाए, तो हमारे हबोट एडॉप्टर के अलावा कहीं और न देखें। https://github.com/RocketChat/hubot-rocketchat", + "Admin_disabled_encryption": "आपके व्यवस्थापक ने E2E एन्क्रिप्शन सक्षम नहीं किया है.", + "Admin_Info": "व्यवस्थापक जानकारी", + "admin-no-active-video-conf-provider": "**कॉन्फ़्रेंस कॉल सक्षम नहीं है**: इस कार्यस्थान पर उपलब्ध कराने के लिए कॉन्फ़्रेंस कॉल कॉन्फ़िगर करें।", + "admin-video-conf-provider-not-configured": "**कॉन्फ़्रेंस कॉल सक्षम नहीं है**: इस कार्यस्थान पर उपलब्ध कराने के लिए कॉन्फ़्रेंस कॉल कॉन्फ़िगर करें।", + "admin-no-videoconf-provider-app": "**कॉन्फ्रेंस कॉल सक्षम नहीं**: कॉन्फ्रेंस कॉल ऐप्स रॉकेट.चैट मार्केटप्लेस में उपलब्ध हैं।", + "Administration": "प्रशासन", + "Address": "पता", + "Adjustable_font_size": "समायोज्य फ़ॉन्ट आकार", + "Adjustable_font_size_description": "उन लोगों के लिए डिज़ाइन किया गया है जो बेहतर पठनीयता के लिए बड़े या छोटे पाठ को पसंद करते हैं। यह लचीलापन उपयोगकर्ताओं को सॉफ़्टवेयर इंटरफ़ेस को उनकी विशिष्ट आवश्यकताओं के अनुरूप बनाने के लिए सशक्त बनाकर समावेशिता को बढ़ावा देता है।", + "Adult_images_are_not_allowed": "वयस्क छवियों की अनुमति नहीं है", + "Aerospace_and_Defense": "विमानन व रक्षा", + "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "OAuth2 प्रमाणीकरण के बाद, उपयोगकर्ताओं को इस सूची के एक URL पर पुनः निर्देशित किया जाएगा। आप प्रति पंक्ति एक URL जोड़ सकते हैं.", + "After_guest_registration": "अतिथि पंजीकरण के बाद", + "Agent": "प्रतिनिधि", + "Agent_added": "एजेंट जोड़ा गया", + "Agent_Info": "एजेंट की जानकारी", + "Agent_messages": "एजेंट संदेश", + "Agent_Name": "एजेंट का नाम", + "Agent_Name_Placeholder": "कृपया एजेंट का नाम दर्ज करें...", + "Agent_removed": "एजेंट हटा दिया गया", + "Agent_deactivated": "एजेंट निष्क्रिय कर दिया गया", + "Agent_Without_Extensions": "एक्सटेंशन के बिना एजेंट", + "Agents": "एजेंटों", + "Agree": "सहमत", + "Alerts": "अलर्ट", + "Alias": "उपनाम", + "Alias_Format": "अन्य प्रारूप", + "Alias_Format_Description": "उपनाम के साथ स्लैक से संदेश आयात करें; %s को उपयोक्ता के उपयोक्तानाम से बदल दिया जाता है। यदि खाली है, तो किसी उपनाम का उपयोग नहीं किया जाएगा।", + "Alias_Set": "उपनाम सेट", + "AutoLinker_Email": "ऑटोलिंकर ईमेल", + "Aliases": "उपनाम", + "AutoLinker_Phone": "ऑटोलिंकर फ़ोन", + "AutoLinker_Phone_Description": "फ़ोन नंबरों के लिए स्वचालित रूप से लिंक किया गया. जैसे `(123)456-7890`", + "All": "सभी", + "AutoLinker_StripPrefix": "ऑटोलिंकर स्ट्रिप उपसर्ग", + "All_Apps": "सभी एप्लीकेशन", + "AutoLinker_StripPrefix_Description": "लघु प्रदर्शन. जैसे https://rocket.chat => रॉकेट.चैट", + "All_added_tokens_will_be_required_by_the_user": "उपयोगकर्ता को सभी जोड़े गए टोकन की आवश्यकता होगी", + "All_categories": "सब वर्ग", + "AutoLinker_Urls_Scheme": "ऑटोलिंकर योजना: // यूआरएल", + "All_channels": "सभी चैनल", + "AutoLinker_Urls_TLD": "ऑटोलिंकर टीएलडी यूआरएल", + "All_closed_chats_have_been_removed": "सभी बंद चैट हटा दिए गए हैं", + "AutoLinker_Urls_www": "ऑटोलिंक 'www' यूआरएल", + "All_logs": "सभी लॉग", + "AutoLinker_UrlsRegExp": "ऑटोलिंकर यूआरएल नियमित अभिव्यक्ति", + "All_messages": "सभी संदेश", + "All_Prices": "सभी कीमतें", + "All_status": "सभी स्थिति", + "All_users": "सभी उपयोगकर्ता", + "All_users_in_the_channel_can_write_new_messages": "चैनल के सभी उपयोगकर्ता नए संदेश लिख सकते हैं", + "Allow_collect_and_store_HTTP_header_informations": "HTTP हेडर जानकारी एकत्र करने और संग्रहीत करने की अनुमति दें", + "Allow_collect_and_store_HTTP_header_informations_description": "यह सेटिंग निर्धारित करती है कि क्या लाइवचैट को HTTP हेडर डेटा से एकत्र की गई जानकारी, जैसे आईपी पता, उपयोगकर्ता-एजेंट, आदि को संग्रहीत करने की अनुमति है।", + "Allow_Invalid_SelfSigned_Certs": "अमान्य स्व-हस्ताक्षरित प्रमाणपत्र की अनुमति दें", + "Allow_Invalid_SelfSigned_Certs_Description": "लिंक सत्यापन और पूर्वावलोकन के लिए अमान्य और स्व-हस्ताक्षरित एसएसएल प्रमाणपत्र की अनुमति दें।", + "Allow_Marketing_Emails": "मार्केटिंग ईमेल की अनुमति दें", + "Allow_Online_Agents_Outside_Business_Hours": "व्यावसायिक घंटों के बाहर ऑनलाइन एजेंटों को अनुमति दें", + "Allow_Online_Agents_Outside_Office_Hours": "कार्यालय समय के बाहर ऑनलाइन एजेंटों को अनुमति दें", + "Allow_Save_Media_to_Gallery": "मीडिया को गैलरी में सहेजने की अनुमति दें", + "Allow_switching_departments": "आगंतुक को विभाग बदलने की अनुमति दें", + "Almost_done": "लगभग हो गया", + "Alphabetical": "वर्णमाला", + "bold": "बोल्ड", + "Also_send_thread_message_to_channel_behavior": "चैनल व्यवहार के लिए थ्रेड संदेश भी भेजें", + "Also_send_to_channel": "चैनल को भी भेजें", + "Always_open_in_new_window": "हमेशा नई विंडो में खोलें", + "Always_show_thread_replies_in_main_channel": "थ्रेड उत्तरों को हमेशा मुख्य चैनल में दिखाएं", + "Analytic_reports": "विश्लेषणात्मक रिपोर्ट", + "Analytics": "एनालिटिक्स", + "Analytics_Description": "देखें कि उपयोगकर्ता आपके कार्यक्षेत्र के साथ कैसे इंटरैक्ट करते हैं।", + "Analytics_features_enabled": "सुविधाएँ सक्षम", + "Analytics_features_messages_Description": "उपयोगकर्ता द्वारा संदेशों पर की जाने वाली कार्रवाइयों से संबंधित कस्टम ईवेंट को ट्रैक करता है।", + "Analytics_features_rooms_Description": "किसी चैनल या समूह पर गतिविधियों से संबंधित कस्टम ईवेंट को ट्रैक करता है (बनाएं, छोड़ें, हटाएं)।", + "Analytics_features_users_Description": "उपयोगकर्ताओं से संबंधित कार्यों से संबंधित कस्टम ईवेंट को ट्रैक करता है (पासवर्ड रीसेट समय, प्रोफ़ाइल चित्र परिवर्तन, आदि)।", + "Analytics_Google": "गूगल विश्लेषिकी", + "Analytics_Google_id": "ट्रैकिंग आईडी", + "Analytics_page_briefing_first_paragraph": "Rocket.Chat सभी के लिए उत्पाद को बेहतर बनाने के लिए अनाम उपयोग डेटा, जैसे सुविधा उपयोग और सत्र की लंबाई, एकत्र करता है।", + "Analytics_page_briefing_second_paragraph": "हम कभी भी व्यक्तिगत या संवेदनशील डेटा एकत्र न करके आपकी गोपनीयता की रक्षा करते हैं। यह अनुभाग दिखाता है कि क्या एकत्र किया गया है, जो पारदर्शिता और विश्वास के प्रति हमारी प्रतिबद्धता को मजबूत करता है।", + "Analyze_practical_usage": "उपयोगकर्ताओं, संदेशों और चैनलों के बारे में व्यावहारिक उपयोग के आँकड़ों का विश्लेषण करें", + "and": "और", + "And_more": "और {{length}} और भी", + "Animals_and_Nature": "पशु और प्रकृति", + "Announcement": "घोषणा", + "Anonymous": "गुमनाम", + "Answer_call": "कॉल का उत्तर दें", + "API": "एपीआई", + "API_Add_Personal_Access_Token": "नया व्यक्तिगत एक्सेस टोकन जोड़ें", + "API_Allow_Infinite_Count": "सब कुछ पाने की अनुमति दें", + "API_Allow_Infinite_Count_Description": "क्या REST API पर कॉल को एक कॉल में सब कुछ वापस करने की अनुमति दी जानी चाहिए?", + "API_Analytics": "एनालिटिक्स", + "API_CORS_Origin": "कॉर्स उत्पत्ति", + "API_Apply_permission_view-outside-room_on_users-list": "एपीआई `users.list` पर `view-outside-room` अनुमति लागू करें", + "API_Apply_permission_view-outside-room_on_users-list_Description": "अनुमति लागू करने के लिए अस्थायी सेटिंग. अनुमति को हमेशा लागू करने के लिए परिवर्तन के अंतर्गत अगली प्रमुख रिलीज़ पर हटा दिया जाएगा", + "API_Default_Count": "डिफ़ॉल्ट count", + "API_Default_Count_Description": "यदि उपभोक्ता ने कोई प्रदान नहीं किया है तो REST API परिणामों के लिए डिफ़ॉल्ट गणना।", + "API_Drupal_URL": "ड्रूपल सर्वर यूआरएल", + "API_Drupal_URL_Description": "उदाहरण: `https://domain.com` (अनुगामी स्लैश को छोड़कर)", + "API_Embed": "लिंक पूर्वावलोकन एम्बेड करें", + "API_Embed_Description": "जब कोई उपयोगकर्ता किसी वेबसाइट पर लिंक पोस्ट करता है तो एम्बेडेड लिंक पूर्वावलोकन सक्षम होते हैं या नहीं।", + "API_EmbedIgnoredHosts": "उपेक्षित होस्ट एम्बेड करें", + "API_EmbedIgnoredHosts_Description": "होस्ट या सीआईडीआर पतों की अल्पविराम से अलग की गई सूची, उदाहरण के लिए। लोकलहोस्ट, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16", + "API_EmbedSafePorts": "सुरक्षित बंदरगाह", + "API_EmbedSafePorts_Description": "पूर्वावलोकन के लिए अनुमति प्राप्त बंदरगाहों की अल्पविराम से अलग की गई सूची।", + "API_Embed_UserAgent": "एंबेड अनुरोध उपयोगकर्ता एजेंट", + "API_EmbedCacheExpirationDays": "एंबेड कैश समाप्ति दिवस", + "API_Enable_CORS": "CORS सक्षम करें", + "API_Enable_Direct_Message_History_EndPoint": "सीधा संदेश इतिहास समापन बिंदु सक्षम करें", + "API_Enable_Direct_Message_History_EndPoint_Description": "यह `/api/v1/im.history.others` को सक्षम करता है जो अन्य उपयोगकर्ताओं द्वारा भेजे गए सीधे संदेशों को देखने की अनुमति देता है जिनका कॉलर हिस्सा नहीं है।", + "API_Enable_Personal_Access_Tokens": "REST API में व्यक्तिगत एक्सेस टोकन सक्षम करें", + "API_Enable_Personal_Access_Tokens_Description": "REST API के साथ उपयोग के लिए व्यक्तिगत एक्सेस टोकन सक्षम करें", + "API_Enable_Rate_Limiter": "दर सीमक सक्षम करें", + "API_Enable_Rate_Limiter_Dev": "विकास में दर सीमक सक्षम करें", + "API_Enable_Rate_Limiter_Dev_Description": "क्या विकास परिवेश में कॉल की मात्रा को अंतिम बिंदुओं तक सीमित किया जाना चाहिए?", + "API_Enable_Rate_Limiter_Limit_Calls_Default": "रेट लिमिटर पर डिफ़ॉल्ट नंबर कॉल", + "API_Enable_Rate_Limiter_Limit_Calls_Default_Description": "REST API के प्रत्येक समापन बिंदु के लिए डिफ़ॉल्ट कॉल की संख्या, नीचे परिभाषित समय सीमा के भीतर अनुमत है", + "API_Enable_Rate_Limiter_Limit_Time_Default": "दर सीमक के लिए डिफ़ॉल्ट समय सीमा (एमएस में)", + "API_Enable_Rate_Limiter_Limit_Time_Default_Description": "REST API के प्रत्येक समापन बिंदु पर कॉल की संख्या सीमित करने के लिए डिफ़ॉल्ट टाइमआउट (एमएस में)", + "API_Enable_Shields": "शील्ड्स सक्षम करें", + "API_Enable_Shields_Description": "`/api/v1/shield.svg` पर उपलब्ध शील्ड सक्षम करें", + "API_GitHub_Enterprise_URL": "सर्वर यूआरएल", + "API_GitHub_Enterprise_URL_Description": "उदाहरण: `https://domain.com` (अनुगामी स्लैश को छोड़कर)", + "API_Gitlab_URL": "गिटलैब यूआरएल", + "API_Personal_Access_Token_Generated": "पर्सनल एक्सेस टोकन सफलतापूर्वक जनरेट हुआ", + "API_Personal_Access_Token_Generated_Text_Token_s_UserId_s": "कृपया अपना टोकन सावधानी से सहेजें क्योंकि इसके बाद आप इसे नहीं देख पाएंगे।
    टोकन: {{token}}
    आपकी उपयोगकर्ता आईडी: {{userId}}", + "API_Personal_Access_Token_Name": "व्यक्तिगत पहुँच टोकन नाम", + "API_Personal_Access_Tokens_Regenerate_It": "टोकन पुन: उत्पन्न करें", + "API_Personal_Access_Tokens_Regenerate_Modal": "यदि आपने अपना टोकन खो दिया है या भूल गए हैं, तो आप इसे पुन: उत्पन्न कर सकते हैं, लेकिन याद रखें कि इस टोकन का उपयोग करने वाले सभी एप्लिकेशन को अपडेट किया जाना चाहिए", + "API_Personal_Access_Tokens_Remove_Modal": "क्या आप वाकई इस व्यक्तिगत एक्सेस टोकन को हटाना चाहते हैं?", + "API_Personal_Access_Tokens_To_REST_API": "REST API तक व्यक्तिगत पहुंच टोकन", + "API_Rate_Limiter": "एपीआई दर सीमक", + "API_Shield_Types": "ढाल के प्रकार", + "API_Shield_Types_Description": "अल्पविराम से अलग की गई सूची के रूप में सक्षम करने के लिए शील्ड के प्रकार, सभी के लिए `ऑनलाइन`, `चैनल` या `*` में से चुनें", + "Apps_Framework_Development_Mode": "विकास मोड सक्षम करें", + "API_Shield_user_require_auth": "उपयोगकर्ता शील्ड के लिए प्रमाणीकरण की आवश्यकता है", + "API_Token": "एपीआई टोकन", + "Apps_Framework_Development_Mode_Description": "डेवलपमेंट मोड उन ऐप्स को इंस्टॉल करने की अनुमति देता है जो Rocket.Chat के मार्केटप्लेस से नहीं हैं।", + "API_Tokenpass_URL": "टोकनपास सर्वर यूआरएल", + "API_Tokenpass_URL_Description": "उदाहरण: `https://domain.com` (अनुगामी स्लैश को छोड़कर)", + "API_Upper_Count_Limit": "अधिकतम रिकार्ड राशि", + "API_Upper_Count_Limit_Description": "REST API को अधिकतम कितने रिकॉर्ड लौटाने चाहिए (जब असीमित न हो)?", + "API_Use_REST_For_DDP_Calls": "उल्का कॉल के लिए वेबसोकेट के बजाय REST का उपयोग करें", + "API_User_Limit": "सभी उपयोगकर्ताओं को चैनल में जोड़ने के लिए उपयोगकर्ता सीमा", + "API_Wordpress_URL": "वर्डप्रेस यूआरएल", + "api-bypass-rate-limit": "REST API के लिए बाईपास दर सीमा", + "api-bypass-rate-limit_description": "दर सीमा के बिना एपीआई कॉल करने की अनुमति", + "Apiai_Key": "एपीआई.एआई कुंजी", + "Apiai_Language": "एपीआई.एआई भाषा", + "APIs": "शहद की मक्खी", + "App_author_homepage": "लेखक मुखपृष्ठ", + "App_Details": "ऐप विवरण", + "App_Info": "अनुप्रयोग की जानकारी", + "App_Information": "ऐप की जानकारी", + "App_Installation": "ऐप इंस्टालेशन", + "App_not_enabled": "ऐप सक्षम नहीं है", + "App_not_found": "ऐप नहीं मिला", "App_status_auto_enabled": "सक्रिय", + "App_status_constructed": "निर्माण", "App_status_disabled": "उपयोग करने की अनुमति नहीं है", + "App_status_error_disabled": "अक्षम: ध्यान में न आई त्रुटि", + "App_status_initialized": "प्रारंभ", + "App_status_invalid_license_disabled": "विकलांग: अमान्य लाइसेंस", + "App_status_invalid_settings_disabled": "अक्षम: कॉन्फ़िगरेशन की आवश्यकता है", + "App_status_manually_disabled": "अक्षम: मैन्युअल रूप से", "App_status_manually_enabled": "सक्रिय", + "App_status_unknown": "अज्ञात", + "App_Store": "ऐप स्टोर", + "App_support_url": "यूआरएल का समर्थन करें", + "App_Url_to_Install_From": "यूआरएल से इंस्टॉल करें", + "App_Url_to_Install_From_File": "फ़ाइल से इंस्टॉल करें", + "App_user_not_allowed_to_login": "ऐप उपयोगकर्ताओं को सीधे लॉग इन करने की अनुमति नहीं है।", "Appearance": "दिखावट", + "Application_added": "एप्लिकेशन जोड़ा गया", + "Application_delete_warning": "आप इस एप्लिकेशन को पुनर्प्राप्त नहीं कर पाएंगे!", + "Application_Name": "आवेदन का नाम", + "Application_updated": "एप्लिकेशन अपडेट किया गया", + "Apply": "आवेदन करना", + "Apply_and_refresh_all_clients": "सभी ग्राहकों को लागू करें और ताज़ा करें", + "Apps": "ऐप्स", + "Apps_context_explore": "अन्वेषण करना", + "Apps_context_installed": "स्थापित", + "Apps_context_requested": "का अनुरोध किया", + "Apps_context_private": "निजी ऐप्स", + "Apps_context_premium": "अधिमूल्य", + "Apps_Count_Enabled": "{{count}} ऐप सक्षम", + "Private_Apps_Count_Enabled": "{{count}} निजी ऐप सक्षम", + "Apps_Count_Enabled_tooltip": "सामुदायिक कार्यस्थान अधिकतम {{number}} {{context}} ऐप्स सक्षम कर सकते हैं", + "Apps_disabled_when_Premium_trial_ended": "प्रीमियम योजना का परीक्षण समाप्त होने पर ऐप्स अक्षम हो गए", + "Apps_disabled_when_Premium_trial_ended_description": "समुदाय पर कार्यस्थानों में अधिकतम 5 मार्केटप्लेस ऐप्स और 3 निजी ऐप्स सक्षम हो सकते हैं। अपने कार्यक्षेत्र व्यवस्थापक से ऐप्स को पुनः सक्षम करने के लिए कहें।", + "Apps_disabled_when_Premium_trial_ended_description_admin": "समुदाय पर कार्यस्थानों में अधिकतम 5 मार्केटप्लेस ऐप्स और 3 निजी ऐप्स सक्षम हो सकते हैं। आपके लिए आवश्यक ऐप्स को पुनः सक्षम करें.", + "Apps_Engine_Version": "ऐप्स इंजन संस्करण", + "Apps_Error_private_app_install_disabled": "इस कार्यक्षेत्र में निजी ऐप इंस्टॉलेशन और अपडेट अक्षम हैं", + "Apps_Essential_Alert": "यह ऐप निम्नलिखित घटनाओं के लिए आवश्यक है:", + "Apps_Essential_Disclaimer": "यदि यह ऐप अक्षम है तो ऊपर सूचीबद्ध ईवेंट बाधित हो जाएंगे। यदि आप चाहते हैं कि Rocket.Chat इस ऐप की कार्यक्षमता के बिना काम करे, तो आपको इसे अनइंस्टॉल करना होगा", + "Apps_Framework_Source_Package_Storage_Type": "ऐप्स का स्रोत पैकेज संग्रहण प्रकार", + "Apps_Framework_Source_Package_Storage_Type_Description": "चुनें कि सभी ऐप्स का स्रोत कोड कहाँ संग्रहीत किया जाएगा। प्रत्येक ऐप का आकार कई मेगाबाइट हो सकता है।", + "Apps_Framework_Source_Package_Storage_Type_Alert": "ऐप्स को संग्रहीत करने का स्थान बदलने से वहां पहले से इंस्टॉल किए गए ऐप्स में अस्थिरता उत्पन्न हो सकती है", + "Apps_Framework_Source_Package_Storage_FileSystem_Path": "ऐप्स स्रोत पैकेज संग्रहीत करने के लिए निर्देशिका", + "Apps_Framework_Source_Package_Storage_FileSystem_Path_Description": "ऐप्स के स्रोत कोड को संग्रहीत करने के लिए फ़ाइल सिस्टम में पूर्ण पथ (ज़िप फ़ाइल प्रारूप में)", + "Apps_Framework_Source_Package_Storage_FileSystem_Alert": "सुनिश्चित करें कि चुनी गई निर्देशिका मौजूद है और Rocket.Chat उस तक पहुंच सकता है (उदाहरण के लिए पढ़ने/लिखने की अनुमति)", + "Apps_Game_Center": "खेल केंद्र", + "Apps_Game_Center_Back": "गेम सेंटर पर वापस जाएँ", + "Apps_Game_Center_Invite_Friends": "शामिल होने के लिए अपने दोस्तों को आमंत्रित कीजिए", + "Apps_Game_Center_Play_Game_Together": "@यहाँ आइए एक साथ {{name}} खेलें!", + "Apps_Interface_IPostExternalComponentClosed": "किसी बाहरी घटक के बंद होने के बाद होने वाली घटना", + "Apps_Interface_IPostExternalComponentOpened": "किसी बाहरी घटक के खुलने के बाद होने वाली घटना", + "Apps_Interface_IPostMessageDeleted": "संदेश हटाए जाने के बाद होने वाली घटना", + "Apps_Interface_IPostMessageSent": "संदेश भेजे जाने के बाद होने वाली घटना", + "Apps_Interface_IPostMessageUpdated": "किसी संदेश के अद्यतन होने के बाद होने वाली घटना", + "Apps_Interface_IPostRoomCreate": "रूम बनने के बाद होने वाला इवेंट", + "Apps_Interface_IPostRoomDeleted": "एक कमरा हटाए जाने के बाद होने वाली घटना", + "Apps_Interface_IPostRoomUserJoined": "किसी उपयोगकर्ता के कमरे में शामिल होने के बाद होने वाली घटना (निजी समूह, सार्वजनिक चैनल)", + "Apps_Interface_IPreMessageDeletePrevent": "संदेश हटाए जाने से पहले होने वाली घटना", + "Apps_Interface_IPreMessageSentExtend": "संदेश भेजे जाने से पहले होने वाली घटना", + "Apps_Interface_IPreMessageSentModify": "संदेश भेजे जाने से पहले होने वाली घटना", + "Apps_Interface_IPreMessageSentPrevent": "संदेश भेजे जाने से पहले होने वाली घटना", + "Apps_Interface_IPreMessageUpdatedExtend": "किसी संदेश के अपडेट होने से पहले होने वाली घटना", + "Apps_Interface_IPreMessageUpdatedModify": "किसी संदेश के अपडेट होने से पहले होने वाली घटना", + "Apps_Interface_IPreMessageUpdatedPrevent": "किसी संदेश के अपडेट होने से पहले होने वाली घटना", + "Apps_Interface_IPreRoomCreateExtend": "रूम बनने से पहले होने वाली घटना", + "Apps_Interface_IPreRoomCreateModify": "रूम बनने से पहले होने वाली घटना", + "Apps_Interface_IPreRoomCreatePrevent": "रूम बनने से पहले होने वाली घटना", + "Apps_Interface_IPreRoomDeletePrevent": "किसी कमरे को हटाए जाने से पहले होने वाली घटना", + "Apps_Interface_IPreRoomUserJoined": "किसी उपयोगकर्ता के कमरे में शामिल होने से पहले होने वाली घटना (निजी समूह, सार्वजनिक चैनल)", + "Apps_License_Message_appId": "इस ऐप के लिए लाइसेंस जारी नहीं किया गया है", + "Apps_License_Message_bundle": "ऐसे बंडल के लिए लाइसेंस जारी किया गया जिसमें ऐप शामिल नहीं है", + "Apps_License_Message_expire": "लाइसेंस अब वैध नहीं है और इसे नवीनीकृत करने की आवश्यकता है", + "Apps_License_Message_maxSeats": "लाइसेंस सक्रिय उपयोगकर्ताओं की वर्तमान संख्या को समायोजित नहीं करता है। कृपया सीटों की संख्या बढ़ाएँ", + "Apps_License_Message_publicKey": "लाइसेंस को डिक्रिप्ट करने का प्रयास करते समय एक त्रुटि हुई है। कृपया अपने कार्यक्षेत्र को कनेक्टिविटी सेवाओं में सिंक करें और पुनः प्रयास करें", + "Apps_License_Message_renewal": "लाइसेंस समाप्त हो गया है और नवीनीकरण की आवश्यकता है", + "Apps_License_Message_seats": "सक्रिय उपयोगकर्ताओं की वर्तमान संख्या को समायोजित करने के लिए लाइसेंस में पर्याप्त सीटें नहीं हैं। कृपया सीटों की संख्या बढ़ाएँ", + "Apps_Logs_TTL": "ऐप्स से लॉग संग्रहीत रखने के लिए दिनों की संख्या", + "Apps_Logs_TTL_7days": "7 दिन", + "Apps_Logs_TTL_14days": "14 दिन", + "Apps_Logs_TTL_30days": "तीस दिन", + "Apps_Logs_TTL_Alert": "लॉग संग्रह के आकार के आधार पर, इस सेटिंग को बदलने से कुछ क्षणों के लिए धीमापन आ सकता है", + "Apps_Marketplace_Deactivate_App_Prompt": "क्या आप वाकई इस ऐप को अक्षम करना चाहते हैं?", + "Apps_Marketplace_Login_Required_Description": "Rocket.Chat मार्केटप्लेस से ऐप्स खरीदने के लिए आपके कार्यक्षेत्र को पंजीकृत करने और लॉग इन करने की आवश्यकता होती है।", + "Apps_Marketplace_Login_Required_Title": "मार्केटप्लेस लॉगिन आवश्यक", + "Apps_Marketplace_Modify_App_Subscription": "सदस्यता संशोधित करें", + "Apps_Marketplace_pricingPlan_monthly": "{{price}} /माह", + "Apps_Marketplace_pricingPlan_monthly_perUser": "{{price}} / प्रति उपयोगकर्ता माह", + "Apps_Marketplace_pricingPlan_monthly_trialDays": "{{price}} / माह-{{trialDays}}-दिन का परीक्षण", + "Apps_Marketplace_pricingPlan_monthly_perUser_trialDays": "{{price}}/माह प्रति उपयोगकर्ता-{{trialDays}}-दिन का परीक्षण", + "Apps_Marketplace_pricingPlan_+*_monthly": " {{price}}+* /माह", + "Apps_Marketplace_pricingPlan_+*_monthly_trialDays": " {{price}}+* / माह-{{trialDays}}-दिन का परीक्षण", + "Apps_Marketplace_pricingPlan_+*_monthly_perUser": " {{price}}+* / प्रति उपयोगकर्ता माह", + "Apps_Marketplace_pricingPlan_+*_monthly_perUser_trialDays": " {{price}}+* / प्रति उपयोगकर्ता माह-{{trialDays}}-दिन का परीक्षण", + "Apps_Marketplace_pricingPlan_+*_yearly": " {{price}}+* / वर्ष", + "Apps_Marketplace_pricingPlan_+*_yearly_trialDays": " {{price}}+* / वर्ष-{{trialDays}}-दिन का परीक्षण", + "Apps_Marketplace_pricingPlan_+*_yearly_perUser": " {{price}}+* / वर्ष प्रति उपयोगकर्ता", + "Apps_Marketplace_pricingPlan_+*_yearly_perUser_trialDays": " {{price}}+* / वर्ष प्रति उपयोगकर्ता-{{trialDays}}-दिन का परीक्षण", + "Apps_Marketplace_pricingPlan_yearly_trialDays": "{{price}} / वर्ष-{{trialDays}}-दिन का परीक्षण", + "Apps_Marketplace_pricingPlan_yearly_perUser_trialDays": "{{price}} / वर्ष प्रति उपयोगकर्ता-{{trialDays}}-दिन का परीक्षण", + "Apps_Marketplace_Uninstall_App_Prompt": "क्या आप वाकई इस ऐप को अनइंस्टॉल करना चाहते हैं?", + "Apps_Marketplace_Uninstall_Subscribed_App_Anyway": "फिर भी इसे अनइंस्टॉल करें", + "Apps_Marketplace_Uninstall_Subscribed_App_Prompt": "इस ऐप की सक्रिय सदस्यता है और अनइंस्टॉल करने से यह रद्द नहीं होगी। यदि आप ऐसा करना चाहते हैं, तो कृपया अनइंस्टॉल करने से पहले अपनी सदस्यता संशोधित करें।", + "Apps_Permissions_Review_Modal_Title": "आवश्यक अनुमतियाँ", + "Apps_Permissions_Review_Modal_Subtitle": "यह ऐप निम्नलिखित अनुमतियों तक पहुंच चाहता है। क्या आप सहमत हैं?", + "Apps_Permissions_No_Permissions_Required": "ऐप को अतिरिक्त अनुमतियों की आवश्यकता नहीं है", + "Apps_Permissions_cloud_workspace-token": "इस सर्वर की ओर से क्लाउड सेवाओं के साथ बातचीत करें", + "Apps_Permissions_user_read": "उपयोगकर्ता जानकारी तक पहुंचें", + "Apps_Permissions_user_write": "उपयोगकर्ता जानकारी संशोधित करें", + "Apps_Permissions_upload_read": "इस सर्वर पर अपलोड की गई एक्सेस फ़ाइलें", + "Apps_Permissions_upload_write": "इस सर्वर पर फ़ाइलें अपलोड करें", + "Apps_Permissions_server-setting_read": "इस सर्वर में सेटिंग्स तक पहुंचें", + "Apps_Permissions_server-setting_write": "इस सर्वर में सेटिंग्स संशोधित करें", + "Apps_Permissions_room_read": "कमरे की जानकारी तक पहुंचें", + "Apps_Permissions_room_write": "कमरे बनाएं और संशोधित करें", + "Apps_Permissions_message_read": "संदेशों तक पहुंचें", + "Apps_Permissions_message_write": "संदेश भेजें और संशोधित करें", + "Apps_Permissions_livechat-status_read": "लाइवचैट स्थिति की जानकारी तक पहुंचें", + "Apps_Permissions_livechat-custom-fields_write": "लाइवचैट कस्टम फ़ील्ड कॉन्फ़िगरेशन को संशोधित करें", + "Apps_Permissions_livechat-visitor_read": "लाइवचैट विज़िटर जानकारी तक पहुंचें", + "Apps_Permissions_livechat-visitor_write": "लाइवचैट विज़िटर जानकारी संशोधित करें", + "Apps_Permissions_livechat-message_read": "लाइवचैट संदेश जानकारी तक पहुंचें", + "Apps_Permissions_livechat-message_write": "लाइवचैट संदेश जानकारी संशोधित करें", + "Apps_Permissions_livechat-room_read": "लाइवचैट रूम की जानकारी तक पहुंचें", + "Apps_Permissions_livechat-room_write": "लाइवचैट रूम की जानकारी संशोधित करें", + "Apps_Permissions_livechat-department_read": "लाइवचैट विभाग की जानकारी तक पहुंचें", + "Apps_Permissions_livechat-department_multiple": "कई लाइवचैट विभागों की जानकारी तक पहुंच", + "Apps_Permissions_livechat-department_write": "लाइवचैट विभाग की जानकारी संशोधित करें", + "Apps_Permissions_slashcommand": "नए स्लैश कमांड पंजीकृत करें", + "Apps_Permissions_api": "नए HTTP समापनबिंदु पंजीकृत करें", + "Apps_Permissions_env_read": "इस सर्वर वातावरण के बारे में न्यूनतम जानकारी तक पहुँचें", + "Apps_Permissions_networking": "इस सर्वर नेटवर्क तक पहुंच", + "Apps_Permissions_persistence": "डेटाबेस में आंतरिक डेटा संग्रहीत करें", + "Apps_Permissions_scheduler": "निर्धारित नौकरियों को पंजीकृत करें और बनाए रखें", + "Apps_Permissions_ui_interact": "यूआई के साथ इंटरैक्ट करें", + "Apps_Settings": "ऐप की सेटिंग्स", + "Apps_Manual_Update_Modal_Title": "यह ऐप पहले से इंस्टॉल है", + "Apps_Manual_Update_Modal_Body": "क्या आप इसे अपडेट करना चाहते हैं?", + "Apps_User_Already_Exists": "उपयोक्तानाम \"{{username}}\" पहले से ही प्रयोग किया जा रहा है। इस ऐप को इंस्टॉल करने के लिए इसका उपयोग करने वाले उपयोगकर्ता का नाम बदलें या उसे हटा दें", + "AutoLinker": "ऑटोलिंकर", + "Apps_WhatIsIt": "ऐप्स: वे क्या हैं?", + "Apps_WhatIsIt_paragraph1": "प्रशासन क्षेत्र में एक नया आइकन! इसका क्या मतलब है और ऐप्स क्या हैं?", + "Apps_WhatIsIt_paragraph2": "सबसे पहले, इस संदर्भ में ऐप्स का तात्पर्य मोबाइल एप्लिकेशन से नहीं है। वास्तव में, प्लगइन्स या उन्नत एकीकरण के संदर्भ में उनके बारे में सोचना सबसे अच्छा होगा।", + "Apps_WhatIsIt_paragraph3": "दूसरे, वे गतिशील स्क्रिप्ट या पैकेज हैं जो आपको कोडबेस को फोर्क किए बिना अपने रॉकेट.चैट इंस्टेंस को अनुकूलित करने की अनुमति देंगे। लेकिन ध्यान रखें, यह एक नया फीचर सेट है और इसके कारण यह 100% स्थिर नहीं हो सकता है। साथ ही, हम अभी भी फीचर सेट विकसित कर रहे हैं इसलिए इस समय हर चीज को अनुकूलित नहीं किया जा सकता है। किसी ऐप को विकसित करना शुरू करने के बारे में अधिक जानकारी के लिए, यहां जाकर पढ़ें:", + "Apps_WhatIsIt_paragraph4": "लेकिन इसके साथ ही, यदि आप इस सुविधा को सक्षम करने और इसे आज़माने में रुचि रखते हैं तो ऐप्स सिस्टम को सक्षम करने के लिए यहां इस बटन पर क्लिक करें।", + "Archive": "पुरालेख", + "Archived": "संग्रहीत", + "archive-room": "पुरालेख कक्ष", + "archive-room_description": "किसी चैनल को संग्रहित करने की अनुमति", + "are_typing": "टाइप कर रहे हैं", + "are_playing": "खेल रहे हैं", + "is_playing": "खेल रहे है", + "are_uploading": "अपलोड कर रहे हैं", + "are_recording": "रिकॉर्डिंग कर रहे हैं", + "is_uploading": "अपलोड कर रहा है", + "is_recording": "रिकॉर्डिंग कर रहा है", + "Are_you_sure": "क्या आपको यकीन है?", + "Are_you_sure_delete_department": "क्या आप वाकई इस विभाग को हटाना चाहते हैं? इस एक्शन को वापस नहीं किया जा सकता। पुष्टि करने के लिए कृपया विभाग का नाम दर्ज करें।", + "Are_you_sure_you_want_to_clear_all_unread_messages": "क्या आप वाकई सभी अपठित संदेशों को साफ़ करना चाहते हैं?", + "Are_you_sure_you_want_to_close_this_chat": "क्या आप वाकई इस चैट को बंद करना चाहते हैं?", + "Are_you_sure_you_want_to_delete_this_record": "क्या आप वाकई यह रिकॉर्ड हटाना चाहते हैं?", + "Are_you_sure_you_want_to_delete_your_account": "क्या आप इस खाते को हटाने के लिए सुनिश्चित हैं?", + "Are_you_sure_you_want_to_disable_Facebook_integration": "क्या आप वाकई फेसबुक एकीकरण को अक्षम करना चाहते हैं?", + "Are_you_sure_you_want_to_reset_the_name_of_all_priorities": "क्या आप वाकई सभी प्राथमिकताओं का नाम रीसेट करना चाहते हैं?", + "Assets": "संपत्ति", + "Assets_Description": "अपने कार्यक्षेत्र का लोगो, आइकन, फ़ेविकॉन और बहुत कुछ संशोधित करें।", + "Asset_preview": "संपत्ति पूर्वावलोकन", + "Assign_admin": "व्यवस्थापक नियुक्त करना", + "Assign_new_conversations_to_bot_agent": "बॉट एजेंट को नई बातचीत सौंपें", + "Assign_new_conversations_to_bot_agent_description": "रूटिंग सिस्टम किसी मानव एजेंट को नई बातचीत को संबोधित करने से पहले एक बॉट एजेंट को खोजने का प्रयास करेगा।", + "assign-admin-role": "व्यवस्थापक भूमिका निर्दिष्ट करें", + "assign-admin-role_description": "अन्य उपयोगकर्ताओं को व्यवस्थापक भूमिका सौंपने की अनुमति", + "assign-roles": "भूमिकाएँ सौंपें", + "assign-roles_description": "अन्य उपयोगकर्ताओं को भूमिकाएँ आवंटित करने की अनुमति", + "Associate": "संबंद्ध करना", + "Associate_Agent": "सहयोगी एजेंट", + "Associate_Agent_to_Extension": "एक्सटेंशन के लिए एसोसिएट एजेंट", + "at": "पर", + "At_least_one_added_token_is_required_by_the_user": "उपयोगकर्ता को कम से कम एक अतिरिक्त टोकन की आवश्यकता है", + "AtlassianCrowd": "एटलसियन भीड़", + "AtlassianCrowd_Description": "एटलसियन भीड़ को एकीकृत करें।", + "Attachment_File_Uploaded": "फ़ाइल अपलोड की गई", + "Attribute_handling": "विशेषता प्रबंधन", + "Audio": "ऑडियो", + "Audio_message": "ऑडियो संदेश", + "Audio_Notification_Value_Description": "कोई भी कस्टम ध्वनि या डिफ़ॉल्ट ध्वनि हो सकती है: बीप, चेले, डिंग, ड्रॉपलेट, हाईबेल, सीज़न", "Audio_Notifications_Default_Alert": "ऑडियो सूचनाएं डिफ़ॉल्ट चेतावनी", + "Audio_Notifications_Value": "डिफ़ॉल्ट संदेश अधिसूचना ऑडियो", + "Audio_record": "ऑडियो रिकॉर्ड", + "Audios": "ऑडियो", + "Audit": "अंकेक्षण", + "Auditing": "लेखा परीक्षा", + "Auth": "प्रमाणीकरण", + "Auth_Token": "प्रामाणिक टोकन", + "Authentication": "प्रमाणीकरण", + "Author": "लेखक", + "Author_Information": "लेखक की जानकारी", + "Author_Site": "लेखक साइट", + "Authorization_URL": "प्राधिकरण यूआरएल", + "Authorize": "अधिकृत", + "Authorize_access_to_your_account": "अपने खाते तक पहुंच अधिकृत करें", + "Automatic_translation_not_available": "स्वचालित अनुवाद उपलब्ध नहीं है", + "Automatic_translation_not_available_info": "इस कमरे में E2E एन्क्रिप्शन सक्षम है, अनुवाद एन्क्रिप्टेड संदेशों के साथ काम नहीं कर सकता है", + "Auto_Load_Images": "छवियाँ स्वतः लोड करें", + "Auto_Selection": "स्वतः चयन", + "Auto_Translate": "ऑटो का अनुवाद", + "auto-translate": "स्वतः अनुवाद", + "auto-translate_description": "ऑटो ट्रांसलेशन टूल का उपयोग करने की अनुमति", + "Automatic_Translation": "स्वचालित अनुवाद", + "AutoTranslate": "ऑटो का अनुवाद", + "AutoTranslate_APIKey": "एपीआई कुंजी", + "AutoTranslate_Change_Language_Description": "ऑटो-अनुवाद भाषा बदलने से पिछले संदेशों का अनुवाद नहीं होता है।", + "AutoTranslate_DeepL": "डीपएल", + "AutoTranslate_Disabled_for_room": "#{{roomName}} के लिए स्वतः-अनुवाद अक्षम किया गया", + "AutoTranslate_Enabled": "स्वतः-अनुवाद सक्षम करें", + "AutoTranslate_Enabled_Description": "ऑटो-ट्रांसलेशन सक्षम करने से 'ऑटो-ट्रांसलेट' अनुमति वाले लोगों को सभी संदेशों को स्वचालित रूप से उनकी चयनित भाषा में अनुवाद करने की अनुमति मिल जाएगी। शुल्क लागू हो सकता है.", + "AutoTranslate_Enabled_for_room": "#{{roomName}} के लिए स्वतः-अनुवाद सक्षम किया गया", + "AutoTranslate_AutoEnableOnJoinRoom": "गैर-डिफ़ॉल्ट भाषा सदस्यों के लिए स्वचालित अनुवाद", + "AutoTranslate_AutoEnableOnJoinRoom_Description": "सक्षम होने पर, जब भी कार्यस्थान डिफ़ॉल्ट से भिन्न भाषा प्राथमिकता वाला कोई उपयोगकर्ता किसी कमरे में शामिल होता है, तो यह स्वचालित रूप से उनके लिए अनुवादित हो जाएगा।", + "AutoTranslate_Google": "गूगल", + "AutoTranslate_language_set_to": "स्वतः-अनुवाद भाषा को {{language}} पर सेट किया गया", + "AutoTranslate_Microsoft": "माइक्रोसॉफ्ट", + "AutoTranslate_Microsoft_API_Key": "Ocp-एपिम-सदस्यता-कुंजी", + "AutoTranslate_ServiceProvider": "सेवा प्रदाता", + "Available": "उपलब्ध", + "Available_agents": "उपलब्ध एजेंट", + "Available_departments": "उपलब्ध विभाग", + "Avatar": "अवतार", + "Avatars": "अवतारों", + "Avatar_changed_successfully": "अवतार सफलतापूर्वक बदला गया", + "Avatar_URL": "अवतार यूआरएल", + "Avatar_format_invalid": "अवैध प्रारूप। केवल छवि प्रकार की अनुमति है", + "Avatar_url_invalid_or_error": "प्रदान किया गया यूआरएल अमान्य है या पहुंच योग्य नहीं है। कृपया पुनः प्रयास करें, लेकिन एक अलग यूआरएल के साथ।", + "Avg_chat_duration": "चैट period का औसत", + "Avg_first_response_time": "प्रथम प्रतिक्रिया समय का औसत", + "Avg_of_abandoned_chats": "छोड़ी गई चैट का औसत", + "Avg_of_available_service_time": "सेवा उपलब्ध समय का औसत", + "Avg_of_chat_duration_time": "चैट period का औसत समय", + "Avg_of_service_time": "सेवा समय का औसत", + "Avg_of_waiting_time": "प्रतीक्षा समय का औसत", + "Avg_reaction_time": "प्रतिक्रिया समय का औसत", + "Avg_response_time": "प्रतिक्रिया समय का औसत", + "away": "दूर", + "Away": "दूर", + "Back": "पीछे", + "Back_to_applications": "अनुप्रयोगों पर वापस जाएँ", + "Back_to_calendar": "कैलेंडर पर वापस जाएँ", + "Back_to_chat": "चैट पर वापस जाएँ", + "Back_to_imports": "आयात पर वापस जाएँ", + "Back_to_integration_detail": "एकीकरण विवरण पर वापस जाएँ", + "Back_to_integrations": "एकीकरण पर वापस जाएँ", + "Back_to_login": "लॉगिन पर वापस जाएं", + "Back_to_Manage_Apps": "ऐप्स प्रबंधित करने के लिए वापस जाएं", + "Back_to_permissions": "अनुमतियों पर वापस जाएँ", + "Back_to_room": "कक्ष में वापस", + "Back_to_threads": "धागों पर वापस जाएँ", + "Backup_codes": "बैकअप कोड", + "ban-user": "प्रतिबंध उपयोगकर्ता", + "ban-user_description": "किसी उपयोगकर्ता को किसी चैनल से प्रतिबंधित करने की अनुमति", + "BBB_End_Meeting": "बैठक समाप्त", + "BBB_Enable_Teams": "टीमों के लिए सक्षम करें", + "BBB_Join_Meeting": "बैठक में शामिल", + "BBB_Start_Meeting": "मीटिंग प्रारंभ करें", + "BBB_Video_Call": "बीबीबी वीडियो कॉल", + "BBB_You_have_no_permission_to_start_a_call": "आपको कॉल शुरू करने की कोई अनुमति नहीं है", + "Be_the_first_to_join": "शामिल होने वाले पहले व्यक्ति बनें", + "Belongs_To": "से संबंधित", + "Best_first_response_time": "सर्वोत्तम प्रथम प्रतिक्रिया समय", + "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "बीटा सुविधा. सक्षम होने के लिए वीडियो कॉन्फ़्रेंस पर निर्भर करता है।", + "Better": "बेहतर", + "Bio": "वह था", + "Bio_Placeholder": "बायो प्लेसहोल्डर", + "Block": "अवरोध पैदा करना", + "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "आईपी एड्रेस को ब्लॉक करने से पहले असफल प्रयासों की मात्रा", + "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "उपयोगकर्ता को ब्लॉक करने से पहले विफल प्रयासों की मात्रा", + "Block_Multiple_Failed_Logins_By_Ip": "आईपी द्वारा विफल लॉगिन प्रयासों को ब्लॉक करें", + "Block_Multiple_Failed_Logins_By_User": "उपयोगकर्ता नाम द्वारा विफल लॉगिन प्रयासों को ब्लॉक करें", + "Block_Multiple_Failed_Logins_Enable_Collect_Login_data_Description": "लॉग इन प्रयासों से लेकर डेटाबेस पर संग्रह तक आईपी और उपयोगकर्ता नाम संग्रहीत करता है", + "Block_Multiple_Failed_Logins_Enabled": "लॉग इन डेटा एकत्रित करना सक्षम करें", + "Block_Multiple_Failed_Logins_Ip_Whitelist": "आईपी श्वेतसूची", + "Block_Multiple_Failed_Logins_Ip_Whitelist_Description": "श्वेतसूचीबद्ध आईपी की अल्पविराम से अलग की गई सूची", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "आईपी एड्रेस ब्लॉक की period (मिनटों में)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes_Description": "यह वह समय है जब आईपी एड्रेस को ब्लॉक किया जाता है, और वह समय जिसमें काउंटर रीसेट होने से पहले असफल प्रयास हो सकते हैं", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "उपयोगकर्ता ब्लॉक की period (मिनटों में)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes_Description": "यह वह समय है जब उपयोगकर्ता को ब्लॉक किया जाता है, और वह समय जिसमें काउंटर रीसेट होने से पहले विफल प्रयास हो सकते हैं", + "Block_Multiple_Failed_Logins_Notify_Failed": "विफल लॉगिन प्रयासों की सूचना दें", + "Block_Multiple_Failed_Logins_Notify_Failed_Channel": "सूचनाएं भेजने के लिए चैनल", + "Block_Multiple_Failed_Logins_Notify_Failed_Channel_Desc": "यहीं पर सूचनाएं प्राप्त होंगी. सुनिश्चित करें कि चैनल मौजूद है. चैनल के नाम में # चिन्ह शामिल नहीं होना चाहिए", + "Block_User": "खंड उपयोगकर्ता", + "Blockchain": "ब्लॉकचेन", + "block-ip-device-management": "आईपी डिवाइस प्रबंधन को ब्लॉक करें", + "block-ip-device-management_description": "आईपी एड्रेस को ब्लॉक करने की अनुमति", + "Block_IP_Address": "आईपी एड्रेस को ब्लॉक करें", + "Blocked_IP_Addresses": "अवरुद्ध आईपी पते", + "Blockstack": "ब्लॉकस्टैक", + "Blockstack_Description": "कार्यक्षेत्र के सदस्यों को किसी तीसरे पक्ष या दूरस्थ सर्वर पर भरोसा किए बिना साइन इन करने की क्षमता दें।", + "Blockstack_Auth_Description": "प्रामाणिक विवरण", + "Blockstack_ButtonLabelText": "बटन लेबल टेक्स्ट", + "Blockstack_Generate_Username": "उपयोक्तानाम उत्पन्न करें", + "Body": "शरीर", + "Bold": "बोल्ड", + "bot_request": "बॉट अनुरोध", + "BotHelpers_userFields": "उपयोगकर्ता फ़ील्ड", + "BotHelpers_userFields_Description": "उपयोगकर्ता फ़ील्ड का CSV जिसे बॉट्स सहायक विधियों द्वारा एक्सेस किया जा सकता है।", + "Bot": "बीओटी", + "Bots": "बॉट", + "Bots_Description": "वे फ़ील्ड सेट करें जिन्हें बॉट विकसित करते समय संदर्भित और उपयोग किया जा सकता है।", + "Branch": "शाखा", + "Broadcast": "प्रसारण", + "Broadcast_channel": "प्रसारण चैनल", + "Broadcast_channel_Description": "केवल अधिकृत उपयोगकर्ता ही नए संदेश लिख सकते हैं, लेकिन अन्य उपयोगकर्ता उत्तर दे सकेंगे", + "Broadcast_Connected_Instances": "कनेक्टेड इंस्टेंस प्रसारित करें", + "Broadcasting_api_key": "प्रसारण एपीआई कुंजी", + "Broadcasting_client_id": "प्रसारण क्लाइंट आईडी", + "Broadcasting_client_secret": "प्रसारण ग्राहक रहस्य", + "Broadcasting_enabled": "प्रसारण सक्षम", + "Broadcasting_media_server_url": "प्रसारण मीडिया सर्वर यूआरएल", + "Browse_Files": "फ़ाइलों को ब्राउज़ करें", + "Browser_does_not_support_audio_element": "आपका ब्राउजर में ऑडियो तत्व समर्थित नहीं है।", + "Browser_does_not_support_video_element": "आपका ब्राउज़र वीडियो तत्व का समर्थन नहीं करता.", + "Browser_does_not_support_recording_video": "आपका ब्राउज़र वीडियो रिकॉर्ड करने का समर्थन नहीं करता", + "Bugsnag_api_key": "बगस्नाग एपीआई कुंजी", + "Build_Environment": "पर्यावरण का निर्माण करें", + "bulk-register-user": "थोक में उपयोगकर्ता बनाएँ", + "bulk-register-user_description": "बड़ी संख्या में उपयोगकर्ता बनाने की अनुमति", + "Bundles": "बंडल", + "Busiest_day": "सबसे व्यस्त दिन", + "Busiest_time": "व्यस्ततम समय", + "Business_Hour": "व्यवसाय का समय", + "Business_Hour_Removed": "व्यावसायिक समय हटा दिया गया", + "Business_Hours": "काम करने के घंटे", + "Business_hours_enabled": "व्यावसायिक घंटे सक्षम", + "Business_hours_updated": "व्यावसायिक घंटे अपडेट किए गए", + "busy": "व्यस्त", + "Busy": "व्यस्त", + "Buy": "खरीदना", + "By": "द्वारा", + "by": "द्वारा", + "cache_cleared": "कैश साफ़ किया गया", + "Calendar_MeetingUrl_Regex": "मीटिंग यूआरएल रेगुलर एक्सप्रेशन", + "Calendar_MeetingUrl_Regex_Description": "घटना विवरण में मीटिंग यूआरएल का पता लगाने के लिए अभिव्यक्ति का उपयोग किया जाता है। वैध यूआरएल वाले पहले मिलान समूह का उपयोग किया जाएगा। HTML एन्कोडेड यूआरएल स्वचालित रूप से डीकोड हो जाएंगे।", + "Calendar_settings": "कैलेंडर सेटिंग", + "Call": "पुकारना", + "Call_again": "दोबारा फोन करें", + "Call_back": "वापस बुलाओ", + "Call_not_found": "कॉल नहीं मिली", + "Call_not_found_error": "ऐसा तब हो सकता है जब कॉल यूआरएल मान्य नहीं है, या आपको कनेक्शन संबंधी समस्याएं आ रही हैं। कृपया कॉल यूआरएल के स्रोत की जांच करें और पुनः प्रयास करें, या यदि समस्या बनी रहती है तो अपने कार्यक्षेत्र व्यवस्थापक से बात करें", + "Calling": "कॉलिंग", + "Call_Center": "आवाज चैनल", + "Call_Center_Description": "रॉकेट.चैट के वॉयस चैनल कॉन्फ़िगर करें", + "Call_ended": "कॉल समाप्त", + "Calls": "कॉल", + "Calls_in_queue": "{{calls}} कतार में कॉल करें", + "Call_declined": "कॉल अस्वीकृत!", + "Call_history_provides_a_record_of_when_calls_took_place_and_who_joined": "कॉल इतिहास इस बात का रिकॉर्ड प्रदान करता है कि कॉल कब हुई और कौन शामिल हुआ।", + "Call_Information": "कॉल सूचना", + "Call_provider": "कॉल प्रदाता", + "Call_Already_Ended": "कॉल पहले ही समाप्त हो चुकी है", + "Call_number": "कॉल नंबर", + "Call_number_premium_only": "कॉल नंबर (केवल प्रीमियम प्लान)", + "call-management": "कॉल प्रबंधन", + "call-management_description": "बैठक शुरू करने की अनुमति", + "Call_ongoing": "कॉल जारी है", + "Call_started": "कॉल शुरू हुई", + "Call_unavailable_for_federation": "फ़ेडरेटेड रूम के लिए कॉल उपलब्ध नहीं है", + "Call_was_not_answered": "कॉल का उत्तर नहीं दिया गया", + "Caller": "कोलर", + "Caller_Id": "कॉलर आईडी", + "Camera_access_not_allowed": "कैमरा एक्सेस की अनुमति नहीं थी, कृपया अपनी ब्राउज़र सेटिंग जांचें।", + "Cam_on": "कैम ऑन", + "Cam_off": "कैम बंद", + "can-audit": "ऑडिट कर सकते हैं", + "can-audit_description": "ऑडिट तक पहुंचने की अनुमति", + "can-audit-log": "ऑडिट लॉग कर सकते हैं", + "can-audit-log_description": "ऑडिट लॉग तक पहुंचने की अनुमति", "Cancel": "रद्द करना", "Cancel_message_input": "रद्द करना", + "Canceled": "रद्द", + "Canned_Response_Created": "डिब्बाबंद प्रतिक्रिया बनाई गई", + "Canned_Response_Updated": "डिब्बाबंद प्रतिक्रिया अद्यतन की गई", + "Canned_Response_Delete_Warning": "डिब्बाबंद प्रतिक्रिया को हटाना पूर्ववत नहीं किया जा सकता।", + "Canned_Response_Removed": "डिब्बाबंद प्रतिक्रिया हटा दी गई", + "Canned_Response_Sharing_Department_Description": "चयनित विभाग में कोई भी इस डिब्बाबंद प्रतिक्रिया तक पहुंच सकता है", + "Canned_Response_Sharing_Private_Description": "केवल आप और ओमनीचैनल प्रबंधक ही इस डिब्बाबंद प्रतिक्रिया तक पहुंच सकते हैं", + "Canned_Response_Sharing_Public_Description": "कोई भी इस डिब्बाबंद प्रतिक्रिया तक पहुंच सकता है", + "Canned_Responses": "डिब्बाबंद प्रतिक्रियाएं", + "Canned_Responses_Enable": "डिब्बाबंद प्रत्युत्तर सक्षम करें", + "Create_department": "विभाग बनाएं", + "Create_direct_message": "सीधा संदेश बनाएं", + "Create_tag": "टैग बनाएं", + "Create_trigger": "ट्रिगर बनाएं", + "Create_SLA_policy": "SLA नीति बनाएं", + "Cannot_invite_users_to_direct_rooms": "उपयोगकर्ताओं को सीधे रूम में आमंत्रित नहीं किया जा सकता", + "Cannot_open_conversation_with_yourself": "अपने आप से सीधे संदेश नहीं भेजा जा सकता", + "Cannot_share_your_location": "आपका स्थान साझा नहीं किया जा सकता...", + "Cannot_disable_while_on_call": "कॉल के दौरान स्थिति नहीं बदल सकते", + "Cant_join": "शामिल नहीं हो सकते", + "CAS": "कैस", + "CAS_Description": "केंद्रीय प्रमाणीकरण सेवा सदस्यों को कई प्रोटोकॉल पर कई साइटों पर साइन इन करने के लिए क्रेडेंशियल्स के एक सेट का उपयोग करने की अनुमति देती है।", + "CAS_autoclose": "लॉगिन पॉपअप स्वतः बंद करें", + "CAS_base_url": "एसएसओ बेस यूआरएल", + "CAS_base_url_Description": "आपकी बाहरी SSO सेवा का आधार URL जैसे: `https://sso.example.undef/sso/`", + "CAS_button_color": "लॉगिन बटन पृष्ठभूमि रंग", + "CAS_button_label_color": "लॉगिन बटन टेक्स्ट का रंग", + "CAS_button_label_text": "लॉगिन बटन लेबल", + "CAS_Creation_User_Enabled": "उपयोगकर्ता निर्माण की अनुमति दें", + "CAS_Creation_User_Enabled_Description": "CAS टिकट द्वारा उपलब्ध कराए गए डेटा से CAS उपयोगकर्ता निर्माण की अनुमति दें।", "CAS_enabled": "सक्रिय", + "CAS_Login_Layout": "CAS लॉगिन लेआउट", + "CAS_login_url": "एसएसओ लॉगिन यूआरएल", + "CAS_login_url_Description": "आपकी बाहरी SSO सेवा का लॉगिन URL जैसे: `https://sso.example.undef/sso/login`", + "CAS_popup_height": "लॉगिन पॉपअप ऊंचाई", + "CAS_popup_width": "लॉगिन पॉपअप चौड़ाई", + "CAS_Sync_User_Data_Enabled": "उपयोगकर्ता डेटा को हमेशा सिंक करें", + "CAS_Sync_User_Data_Enabled_Description": "लॉगिन पर बाहरी CAS उपयोगकर्ता डेटा को हमेशा उपलब्ध विशेषताओं में सिंक्रनाइज़ करें। ध्यान दें: खाता बनाते समय विशेषताएँ हमेशा समन्वयित होती हैं।", + "CAS_Sync_User_Data_FieldMap": "गुण मानचित्र", + "CAS_Sync_User_Data_FieldMap_Description": "बाहरी विशेषताओं (मान) से आंतरिक विशेषताएँ (कुंजी) बनाने के लिए इस JSON इनपुट का उपयोग करें। '%' के साथ संलग्न बाहरी विशेषता नाम मूल्य स्ट्रिंग में प्रक्षेपित होंगे।\nउदाहरण, `{\"ईमेल\":\"%ईमेल%\", \"नाम\":\"%पहला नाम%, %अंतिमनाम%\"}`\n \nविशेषता मानचित्र हमेशा प्रक्षेपित होता है। CAS 1.0 में केवल `उपयोगकर्ता नाम` विशेषता उपलब्ध है। उपलब्ध आंतरिक विशेषताएँ हैं: उपयोगकर्ता नाम, नाम, ईमेल, कमरे; रूम उपयोगकर्ता के निर्माण पर शामिल होने के लिए कमरों की एक अल्पविराम से अलग की गई सूची है, उदाहरण के लिए: `{\"rooms\": \"%team%,%department%\"}` निर्माण पर CAS उपयोगकर्ताओं को उनकी टीम और विभाग चैनल में शामिल करेगा।", + "CAS_trust_username": "CAS उपयोगकर्ता नाम पर भरोसा करें", + "CAS_trust_username_description": "सक्षम होने पर, Rocket.Chat को भरोसा होगा कि CAS का कोई भी उपयोगकर्ता नाम Rocket.Chat पर उसी उपयोगकर्ता का है।\nयदि किसी उपयोगकर्ता का नाम CAS पर बदला जाता है तो इसकी आवश्यकता हो सकती है, लेकिन यह लोगों को अपने CAS उपयोगकर्ताओं का नाम बदलकर Rocket.Chat खातों पर नियंत्रण लेने की अनुमति भी दे सकता है।", + "CAS_version": "कैस संस्करण", + "CAS_version_Description": "केवल आपकी CAS SSO सेवा द्वारा समर्थित CAS संस्करण का उपयोग करें।", + "Categories": "श्रेणियाँ", + "Categories*": "श्रेणियाँ*", + "CDN_JSCSS_PREFIX": "जेएस/सीएसएस के लिए सीडीएन उपसर्ग", + "CDN_PREFIX": "सीडीएन उपसर्ग", + "CDN_PREFIX_ALL": "सभी संपत्तियों के लिए सीडीएन उपसर्ग का उपयोग करें", + "Certificates_and_Keys": "प्रमाणपत्र और चाबियाँ", + "changed_room_announcement_to__room_announcement_": "कमरे की घोषणा को इसमें बदला गया: {{room_announcement}}", + "changed_room_description_to__room_description_": "कमरे के विवरण को इसमें बदल दिया गया: {{room_description}}", + "change-livechat-room-visitor": "लाइवचैट रूम विज़िटर बदलें", + "change-livechat-room-visitor_description": "लाइवचैट रूम विज़िटर के लिए अतिरिक्त जानकारी जोड़ने की अनुमति", + "Change_Room_Type": "कमरे का प्रकार बदलना", + "Changing_email": "ईमेल बदलना", + "channel": "चैनल", + "Channel": "चैनल", + "Channel_already_exist": "चैनल `#%s` पहले से मौजूद है।", + "Channel_already_exist_static": "चैनल पहले से मौजूद है.", + "Channel_already_Unarchived": "`#%s` नाम वाला चैनल पहले से ही अनारक्षित स्थिति में है", + "Channel_Archived": "`#%s` नाम वाला चैनल सफलतापूर्वक संग्रहीत किया गया है", + "Channel_created": "चैनल `#%s` बनाया गया.", + "Channel_doesnt_exist": "चैनल `#%s` मौजूद नहीं है।", + "Channel_Export": "चैनल निर्यात", + "Channel_name": "चैनल का नाम", + "Channel_Name_Placeholder": "कृपया चैनल का नाम दर्ज करें...", + "Channel_to_listen_on": "सुनने के लिए चैनल", + "Channel_Unarchived": "`#%s` नाम वाला चैनल सफलतापूर्वक अनारक्षित कर दिया गया है", + "Channels": "चैनल", + "Channels_added": "चैनल सफलतापूर्वक जोड़े गए", + "Channels_are_where_your_team_communicate": "चैनल वे हैं जहां आपकी टीम संवाद करती है", + "Channels_list": "सार्वजनिक चैनलों की सूची", + "Channel_what_is_this_channel_about": "यह चैनल किस बारे में है?", + "Chart": "चार्ट", + "Chat_button": "चैट बटन", + "Chat_close": "चैट बंद करें", + "Chat_closed": "चैट बंद", + "Chat_closed_by_agent": "एजेंट द्वारा चैट बंद कर दी गई", + "Chat_closed_successfully": "चैट सफलतापूर्वक बंद हुई", + "Chat_History": "चैट का इतिहास", + "Chat_Now": "अभी बातचीत करें", + "chat_on_hold_due_to_inactivity": "निष्क्रियता के कारण यह चैट रुकी हुई है", + "Chat_On_Hold": "चैट ऑन-होल्ड", + "Chat_On_Hold_Successfully": "इस चैट को सफलतापूर्वक ऑन-होल्ड पर रखा गया था", + "Chat_queued": "चैट पंक्तिबद्ध", + "Chat_removed": "चैट हटा दी गई", + "Chat_resumed": "चैट फिर से शुरू हुई", + "Chat_start": "चैट प्रारंभ", + "Chat_started": "चैट शुरू हुई", + "Chat_taken": "चैट लिया गया", + "Chat_window": "चैट विंडो", + "Chatops_Enabled": "चैटॉप्स सक्षम करें", + "Chatops_Title": "चैटॉप्स पैनल", + "Chatops_Username": "चैटॉप्स उपयोगकर्ता नाम", + "Chat_Duration": "चैट की period", + "Chats_removed": "चैट हटा दी गईं", + "Check_All": "सभी चेक करें", + "Check_if_the_spelling_is_correct": "जांचें कि क्या वर्तनी सही है", + "Check_Progress": "प्रगति की जाँच करें", + "Check_device_activity": "डिवाइस गतिविधि की जाँच करें", + "Choose_a_room": "एक कमरा चुनें", + "Choose_messages": "संदेश चुनें", + "Choose_the_alias_that_will_appear_before_the_username_in_messages": "वह उपनाम चुनें जो संदेशों में उपयोगकर्ता नाम से पहले दिखाई देगा।", + "Choose_the_username_that_this_integration_will_post_as": "वह उपयोक्तानाम चुनें जिसके रूप में यह एकीकरण पोस्ट किया जाएगा.", + "Choose_users": "उपयोगकर्ता चुनें", + "Clean_History_unavailable_for_federation": "महासंघ के लिए स्वच्छ इतिहास अनुपलब्ध है", + "Clean_Usernames": "उपयोक्तानाम साफ़ करें", + "clean-channel-history": "स्वच्छ चैनल इतिहास", + "clean-channel-history_description": "चैनलों से इतिहास साफ़ करने की अनुमति", + "clear": "स्पष्ट", + "Clear_all_unreads_question": "सभी अपठित साफ़ करें?", + "clear_cache_now": "अभी कैश साफ़ करें", + "Clear_filters": "फ़िल्टर साफ़ करें", + "clear_history": "इतिहास मिटा दें", + "Clear_livechat_session_when_chat_ended": "चैट समाप्त होने पर अतिथि सत्र साफ़ करें", + "clear-oembed-cache": "OEmbed कैश साफ़ करें", + "clear-oembed-cache_description": "OEmbed कैश साफ़ करने की अनुमति", + "Click_here": "यहाँ क्लिक करें", + "Click_here_for_more_details_or_contact_sales_for_a_new_license": "अधिक जानकारी के लिए यहां क्लिक करें या नए लाइसेंस के लिए {{email}} से संपर्क करें।", + "Click_here_for_more_info": "अधिक जानकारी के लिए यहां क्लिक करें", + "Click_here_to_clear_the_selection": "चयन साफ़ करने के लिए यहां क्लिक करें", + "Click_here_to_enter_your_encryption_password": "अपना एन्क्रिप्शन पासवर्ड दर्ज करने के लिए यहां क्लिक करें", + "Click_here_to_view_and_copy_your_password": "अपना पासवर्ड देखने और कॉपी करने के लिए यहां क्लिक करें।", + "Click_the_messages_you_would_like_to_send_by_email": "उन संदेशों पर क्लिक करें जिन्हें आप ई-मेल द्वारा भेजना चाहते हैं", + "Click_to_join": "शामिल होने के लिए क्लिक करें!", + "Click_to_load": "लोड करने के लिए क्लिक करें", + "Client_ID": "ग्राहक ID", "Client_Secret": "क्लाइंट Secret", + "Client": "ग्राहक", + "Clients_will_refresh_in_a_few_seconds": "ग्राहक कुछ ही सेकंड में ताज़ा हो जाएंगे", + "close": "बंद करना", + "Close": "बंद करना", + "Close_chat": "चैट बंद करें", + "Close_room_description": "आप इस चैट को बंद करने वाले हैं. क्या आप वाकई जारी रखना चाहते हैं?", + "close-livechat-room": "ओमनीचैनल कक्ष बंद करें", + "close-livechat-room_description": "वर्तमान ओमनीचैनल कक्ष को बंद करने की अनुमति", + "close-others-livechat-room": "अन्य ओमनीचैनल कक्ष बंद करें", + "close-others-livechat-room_description": "अन्य ओमनीचैनल कमरों को बंद करने की अनुमति", + "Close_Window": "विंडो बंद", + "Closed": "बंद किया हुआ", + "Closed_At": "पर बंद हुआ", + "Closed_automatically": "सिस्टम द्वारा स्वचालित रूप से बंद कर दिया गया", + "Closed_automatically_because_chat_was_onhold_for_seconds": "स्वचालित रूप से बंद हो गया क्योंकि चैट {{onHoldTime}} सेकंड के लिए होल्ड पर थी", + "Closed_automatically_chat_queued_too_long": "सिस्टम द्वारा स्वचालित रूप से बंद (कतार का अधिकतम समय पार हो गया)", + "Closed_by_visitor": "आगंतुक द्वारा बंद कर दिया गया", + "Wrap_up_conversation": "बातचीत समाप्त करें", + "These_options_affect_this_conversation_only_To_set_default_selections_go_to_My_Account_Omnichannel": "ये विकल्प केवल इस वार्तालाप को प्रभावित करते हैं. डिफ़ॉल्ट चयन सेट करने के लिए, मेरा खाता > ओमनीचैनल पर जाएँ।", + "This_option_affect_this_conversation_only_To_set_default_selection_go_to_My_Account_Omnichannel": "यह विकल्प केवल इस वार्तालाप को प्रभावित करता है. डिफ़ॉल्ट चयन सेट करने के लिए, मेरा खाता > ओमनीचैनल पर जाएँ।", + "Closing_chat": "चैट बंद हो रही है", + "Closing_chat_message": "चैट बंद करने का संदेश", + "Cloud": "बादल", + "Cloud_Apply_Offline_License": "ऑफ़लाइन लाइसेंस लागू करें", + "Cloud_Change_Offline_License": "ऑफ़लाइन लाइसेंस बदलें", + "Cloud_License_applied_successfully": "लाइसेंस सफलतापूर्वक लागू हो गया!", + "Cloud_Invalid_license": "अवैध लाइसेंस!", + "Cloud_Apply_license": "लाइसेंस लागू करें", + "Cloud_connectivity": "क्लाउड कनेक्टिविटी", + "Cloud_address_to_send_registration_to": "अपना क्लाउड पंजीकरण ईमेल भेजने का पता।", + "Cloud_click_here": "टेक्स्ट कॉपी करने के बाद, [क्लाउड कंसोल (यहां क्लिक करें)]({{cloudConsoleUrl}}) पर जाएं।", + "Cloud_console": "क्लाउड कंसोल", + "Cloud_error_code": "कोड: {{errorCode}}", + "Cloud_error_in_authenticating": "प्रमाणीकरण करते समय त्रुटि प्राप्त हुई", + "Cloud_Info": "क्लाउड जानकारी", + "Cloud_login_to_cloud": "Rocket.Chat क्लाउड में लॉग इन करें", + "Cloud_logout": "रॉकेट.चैट क्लाउड से लॉगआउट करें", + "Cloud_manually_input_token": "क्लाउड कंसोल से प्राप्त टोकन दर्ज करें।", + "Cloud_register_error": "आपके अनुरोध को संसाधित करने का प्रयास करते समय एक त्रुटि हुई है। कृपया बाद में पुन: प्रयास करें।", + "Cloud_Register_manually": "ऑफ़लाइन पंजीकरण करें", + "Cloud_register_offline_finish_helper": "क्लाउड कंसोल में पंजीकरण प्रक्रिया पूरी करने के बाद आपको कुछ टेक्स्ट प्रस्तुत किया जाना चाहिए। पंजीकरण समाप्त करने के लिए कृपया इसे यहां पेस्ट करें।", + "Cloud_register_offline_helper": "यदि एयरगैप या नेटवर्क पहुंच प्रतिबंधित है तो कार्यस्थानों को मैन्युअल रूप से पंजीकृत किया जा सकता है। प्रक्रिया को पूरा करने के लिए नीचे दिए गए टेक्स्ट को कॉपी करें और हमारे क्लाउड कंसोल पर जाएं।", + "Cloud_register_success": "आपका कार्यक्षेत्र सफलतापूर्वक पंजीकृत हो गया है!", + "Cloud_registration_required": "पंजीकरण आवश्यक", + "Cloud_registration_required_description": "ऐसा लगता है कि सेटअप के दौरान आपने अपना कार्यक्षेत्र पंजीकृत करना नहीं चुना।", + "Cloud_registration_required_link_text": "अपना कार्यक्षेत्र पंजीकृत करने के लिए यहां क्लिक करें।", + "Cloud_resend_email": "ईमेल दुबारा भेजें", + "Cloud_Service_Agree_PrivacyTerms": "क्लाउड सेवा गोपनीयता शर्तें अनुबंध", + "Cloud_Service_Agree_PrivacyTerms_Description": "मैं [शर्तें](https://rocket.chat/terms) और [गोपनीयता नीति](https://rocket.chat/privacy) से सहमत हूं", + "Cloud_Service_Agree_PrivacyTerms_Login_Disabled_Warning": "आपको अपने क्लाउड कार्यक्षेत्र से जुड़ने के लिए क्लाउड गोपनीयता शर्तों (सेटअप विज़ार्ड > क्लाउड जानकारी > क्लाउड सेवा गोपनीयता शर्तें अनुबंध) को स्वीकार करना चाहिए", + "Cloud_status_page_description": "यदि किसी विशेष क्लाउड सेवा में समस्या आ रही है तो आप हमारे स्थिति पृष्ठ पर ज्ञात समस्याओं की जांच कर सकते हैं", + "Cloud_token_instructions": "अपने कार्यक्षेत्र को पंजीकृत करने के लिए क्लाउड कंसोल पर जाएं। लॉग इन करें या एक खाता बनाएं और स्व-प्रबंधित रजिस्टर पर क्लिक करें। नीचे दिए गए टोकन को चिपकाएँ", + "Cloud_troubleshooting": "समस्या निवारण", + "Cloud_update_email": "ईमेल अपडेट करें", + "Cloud_what_is_it": "यह क्या है?", + "Copy_Link": "लिंक की प्रतिलिपि करें", + "Copy_password": "पासवर्ड कॉपी करें", + "Cloud_what_is_it_additional": "इसके अलावा आप Rocket.Chat क्लाउड कंसोल से लाइसेंस, बिलिंग और समर्थन का प्रबंधन करने में सक्षम होंगे।", + "Cloud_what_is_it_description": "Rocket.Chat क्लाउड कनेक्ट आपको अपने स्व-होस्ट किए गए Rocket.Chat वर्कस्पेस को हमारे क्लाउड में प्रदान की जाने वाली सेवाओं से कनेक्ट करने की अनुमति देता है।", + "Cloud_what_is_it_services_like": "सेवाएँ जैसे:", + "Cloud_workspace_connected": "आपका कार्यक्षेत्र Rocket.Chat Cloud से जुड़ा है। यहां अपने Rocket.Chat क्लाउड खाते में लॉग इन करने से आप मार्केटप्लेस जैसी कुछ सेवाओं के साथ बातचीत कर सकेंगे।", + "Cloud_workspace_connected_plus_account": "आपका कार्यक्षेत्र अब Rocket.Chat क्लाउड से जुड़ा है और एक खाता संबद्ध है।", + "Cloud_workspace_connected_without_account": "आपका कार्यक्षेत्र अब Rocket.Chat क्लाउड से कनेक्ट हो गया है। यदि आप चाहें, तो आप Rocket.Chat क्लाउड में लॉग इन कर सकते हैं और अपने कार्यक्षेत्र को अपने क्लाउड खाते से जोड़ सकते हैं।", + "Cloud_workspace_disconnect": "यदि आप अब क्लाउड सेवाओं का उपयोग नहीं करना चाहते हैं तो आप अपने कार्यक्षेत्र को Rocket.Chat Cloud से डिस्कनेक्ट कर सकते हैं।", + "Cloud_workspace_support": "यदि आपको क्लाउड सेवा में परेशानी हो रही है, तो कृपया पहले सिंक करने का प्रयास करें। यदि समस्या बनी रहती है, तो कृपया क्लाउड कंसोल में एक सहायता टिकट खोलें।", + "Collaborative": "सहयोगात्मक", + "Collapse": "गिर जाना", + "Collapse_Embedded_Media_By_Default": "एंबेडेड मीडिया को डिफ़ॉल्ट रूप से संक्षिप्त करें", + "color": "रंग", + "Color": "रंग", + "Colors": "रंग की", + "Commands": "आदेश", + "Comment_to_leave_on_closing_session": "समापन सत्र पर जाने के लिए टिप्पणी करें", + "Comment": "टिप्पणी", + "Common_Access": "सामान्य पहुंच", + "Commit": "प्रतिबद्ध", + "Community": "समुदाय", + "Free_Edition": "निशुल्क संस्करण", + "Composer_not_available_phone_calls": "फ़ोन कॉल पर संदेश उपलब्ध नहीं हैं", + "Condensed": "संघनित", + "Condition": "स्थिति", + "Commit_details": "प्रतिबद्ध विवरण", + "Completed": "पुरा होना।", + "Computer": "कंप्यूटर", + "Conference_call_apps": "कॉन्फ़्रेंस कॉल ऐप्स", + "Conference_call_has_ended": "_कॉल समाप्त हो गया है._", + "Conference_name": "सम्मेलन का नाम", + "Configure_Incoming_Mail_IMAP": "इनकमिंग मेल कॉन्फ़िगर करें (IMAP)", + "Configure_Outgoing_Mail_SMTP": "आउटगोइंग मेल कॉन्फ़िगर करें (एसएमटीपी)", + "Configure_video_conference_to_make_it_available_on_this_workspace": "इसे इस कार्यक्षेत्र पर उपलब्ध कराने के लिए वीडियो कॉन्फ़्रेंस कॉन्फ़िगर करें", + "Confirm": "पुष्टि करना", + "Confirm_new_encryption_password": "नये एन्क्रिप्शन पासवर्ड की पुष्टि करें", + "Confirm_new_password": "नए पासवर्ड की पुष्टि करें", + "Confirm_New_Password_Placeholder": "कृपया नया पासवर्ड दोबारा दर्ज करें...", + "Confirm_password": "पासवर्ड की पुष्टि कीजिये", + "Confirm_your_password": "अपने पासवर्ड की पुष्टि करें", + "Confirm_configuration_update_description": "पहचान डेटा और क्लाउड कनेक्शन डेटा बरकरार रखा जाएगा।

    चेतावनी : यदि यह वास्तव में एक नया कार्यक्षेत्र है, तो कृपया वापस जाएं और संचार विवादों से बचने के लिए नए कार्यक्षेत्र विकल्प का चयन करें।", + "Confirm_configuration_update": "कॉन्फ़िगरेशन अद्यतन की पुष्टि करें", + "Confirm_new_workspace_description": "पहचान डेटा और क्लाउड कनेक्शन डेटा रीसेट कर दिया जाएगा।

    चेतावनी : कार्यक्षेत्र यूआरएल बदलने पर लाइसेंस प्रभावित हो सकता है।", + "Confirm_new_workspace": "नए कार्यक्षेत्र की पुष्टि करें", + "Confirmation": "पुष्टीकरण", + "Configure_video_conference": "कॉन्फ़्रेंस कॉल कॉन्फ़िगर करें", + "Configuration_update_confirmed": "कॉन्फ़िगरेशन अद्यतन की पुष्टि की गई", + "Configuration_update": "कॉन्फ़िगरेशन अद्यतन", + "Connect": "जोड़ना", + "Connected": "जुड़े हुए", + "Connect_SSL_TLS": "एसएसएल/टीएलएस से जुड़ें", + "Connection_Closed": "कनेक्शन बंद", + "Connection_Reset": "सम्बन्ध फिरसे बनाना", + "Connection_error": "संपर्क त्रुटि", + "Connection_failed": "एलडीएपी कनेक्शन विफल", + "Connectivity_Services": "कनेक्टिविटी सेवाएँ", + "Consulting": "CONSULTING", + "Consumer_Packaged_Goods": "उपभोक्ता के लिए पैक की गई वस्तुएं", + "Contact": "संपर्क", + "Contacts": "संपर्क", + "Contact_Name": "संपर्क नाम", + "Contact_Center": "संपर्क केंद्र", + "Contact_Chat_History": "संपर्क चैट इतिहास", + "Contains_Security_Fixes": "सुरक्षा सुधार शामिल हैं", + "Contact_Manager": "प्रबंधक से संपर्क करें", + "Contact_not_found": "संपर्क नहीं मिला", + "Contact_Profile": "प्रोफ़ाइल से संपर्क करें", + "Contact_Info": "संपर्क जानकारी", + "Content": "सामग्री", + "Continue": "जारी रखना", + "Continuous_sound_notifications_for_new_livechat_room": "नए ओमनीचैनल कक्ष के लिए निरंतर ध्वनि सूचनाएं", + "convert-team": "टीम परिवर्तित करें", + "convert-team_description": "टीम को चैनल में बदलने की अनुमति", + "Conversation": "बातचीत", + "Conversation_closed": "बातचीत बंद: {{comment}}.", + "Conversation_closed_without_comment": "बातचीत बंद", + "Conversation_closing_tags": "वार्तालाप समापन टैग", + "Conversation_closing_tags_description": "समापन टैग स्वचालित रूप से समापन पर वार्तालापों को असाइन किए जाएंगे।", + "Conversation_finished": "बातचीत ख़त्म", + "Conversation_finished_message": "बातचीत समाप्त संदेश", + "Conversation_finished_text": "बातचीत समाप्त पाठ", + "conversation_with_s": "%s के साथ बातचीत", + "Conversations": "बात चिट", + "Conversations_per_day": "प्रति दिन बातचीत", + "Convert": "बदलना", + "Convert_Ascii_Emojis": "ASCII को इमोजी में बदलें", + "Convert_to_channel": "चैनल में कनवर्ट करें", + "Converting_channel_to_a_team": "आप इस चैनल को एक टीम में परिवर्तित कर रहे हैं। सभी सदस्यों को रखा जाएगा.", + "Converted__roomName__to_team": "#{{roomName}} को एक टीम में परिवर्तित किया गया", + "Converted__roomName__to_channel": "#{{roomName}} को एक चैनल में परिवर्तित किया गया", + "Converted__roomName__to_a_team": "#{{roomName}} को एक टीम में परिवर्तित किया गया", + "Converted__roomName__to_a_channel": "#{{roomName}} को चैनल में परिवर्तित किया गया", + "Converting_team_to_channel": "टीम को चैनल में परिवर्तित करना", + "Copied": "कॉपी किया गया", + "Copy": "प्रतिलिपि", + "Copy_text": "पाठ कॉपी करें", + "Copy_to_clipboard": "क्लिपबोर्ड पर कॉपी करें", + "COPY_TO_CLIPBOARD": "क्लिपबोर्ड पर कॉपी करें", + "could-not-access-webdav": "WebDAV तक नहीं पहुंच सका", + "Count": "count करना", + "Counters": "काउंटर", + "Country": "देश", + "Country_Afghanistan": "अफ़ग़ानिस्तान", + "Country_Albania": "अल्बानिया", + "Country_Algeria": "एलजीरिया", + "Country_American_Samoa": "अमेरिकी समोआ", + "Country_Andorra": "एंडोरा", + "Country_Angola": "अंगोला", + "Country_Anguilla": "एंगुइला", + "Country_Antarctica": "अंटार्कटिका", + "Country_Antigua_and_Barbuda": "अण्टीगुआ और बारबूडा", + "Country_Argentina": "अर्जेंटीना", + "Country_Armenia": "आर्मीनिया", + "Country_Aruba": "अरूबा", + "Country_Australia": "ऑस्ट्रेलिया", + "Country_Austria": "ऑस्ट्रिया", + "Country_Azerbaijan": "आज़रबाइजान", + "Country_Bahamas": "बहामा", + "Country_Bahrain": "बहरीन", + "Country_Bangladesh": "बांग्लादेश", + "Country_Barbados": "बारबाडोस", + "Country_Belarus": "बेलोरूस", + "Country_Belgium": "बेल्जियम", + "Country_Belize": "बेलीज़", + "Country_Benin": "बेनिन", + "Country_Bermuda": "बरमूडा", + "Country_Bhutan": "भूटान", + "Country_Bolivia": "बोलीविया", + "Country_Bosnia_and_Herzegovina": "बोस्निया और हर्जेगोविना", + "Country_Botswana": "बोत्सवाना", + "Country_Bouvet_Island": "बाउवेट द्वीप", + "Country_Brazil": "ब्राज़िल", + "Country_British_Indian_Ocean_Territory": "ब्रिटेन और भारतीय समुद्री क्षेत्र", + "Country_Brunei_Darussalam": "ब्रूनेइ्र दारएस्सलाम", + "Country_Bulgaria": "बुल्गारिया", + "Country_Burkina_Faso": "बुर्किना फासो", + "Country_Burundi": "बुस्र्न्दी", + "Country_Cambodia": "कंबोडिया", + "Country_Cameroon": "कैमरून", + "Country_Canada": "कनाडा", + "Country_Cape_Verde": "केप वर्ड", + "Country_Cayman_Islands": "केमन द्वीपसमूह", + "Country_Central_African_Republic": "केन्द्रीय अफ़्रीकी गणराज्य", + "Country_Chad": "काग़ज़ का टुकड़ा", + "Country_Chile": "चिली", + "Country_China": "चीन", + "Country_Christmas_Island": "क्रिसमस द्वीप", + "Country_Cocos_Keeling_Islands": "कोकोस (कीलिंग) द्वीप समूह", + "Country_Colombia": "कोलंबिया", + "Country_Comoros": "कोमोरोस", + "Country_Congo": "कांगो", + "Country_Congo_The_Democratic_Republic_of_The": "कांगो, लोकतांत्रिक गणराज्य", + "Country_Cook_Islands": "कुक द्वीपसमूह", + "Country_Costa_Rica": "कोस्टा रिका", + "Country_Cote_Divoire": "हाथीदांत का किनारा", + "Country_Croatia": "क्रोएशिया", + "Country_Cuba": "क्यूबा", + "Country_Cyprus": "साइप्रस", + "Country_Czech_Republic": "चेक रिपब्लिक", + "Country_Denmark": "डेनमार्क", + "Country_Djibouti": "ज़िबूटी", + "Country_Dominica": "डोमिनिका", + "Country_Dominican_Republic": "डोमिनिकन गणराज्य", + "Country_Ecuador": "इक्वेडोर", + "Country_Egypt": "मिस्र", + "Country_El_Salvador": "अल साल्वाडोर", + "Country_Equatorial_Guinea": "भूमध्यवर्ती गिनी", + "Country_Eritrea": "इरिट्रिया", + "Country_Estonia": "एस्तोनिया", + "Country_Ethiopia": "इथियोपिया", + "Country_Falkland_Islands_Malvinas": "फ़ॉकलैंड द्वीप समूह (माल्विनास)", + "Country_Faroe_Islands": "फ़ैरो द्वीप", + "Country_Fiji": "फ़िजी", + "Country_Finland": "फिनलैंड", + "Country_France": "फ्रांस", + "Country_French_Guiana": "फ्रेंच गयाना", + "Country_French_Polynesia": "फ़्रेंच पोलिनेशिया", + "Country_French_Southern_Territories": "दक्षिणी फ्राँसिसी क्षेत्र", + "Country_Gabon": "गैबॉन", + "Country_Gambia": "गाम्बिया", + "Country_Georgia": "जॉर्जिया", + "Country_Germany": "जर्मनी", + "Country_Ghana": "घाना", + "Country_Gibraltar": "जिब्राल्टर", + "Country_Greece": "यूनान", + "Country_Greenland": "ग्रीनलैंड", + "Country_Grenada": "ग्रेनेडा", + "Country_Guadeloupe": "ग्वाडेलोप", + "Country_Guam": "गुआम", + "Country_Guatemala": "ग्वाटेमाला", + "Country_Guinea": "गिनी", + "Country_Guinea_bissau": "गिनी-बिसाऊ", + "Country_Guyana": "गुयाना", + "Country_Haiti": "हैती", + "Country_Heard_Island_and_Mcdonald_Islands": "हर्ड द्वीप और मैकडोनाल्ड द्वीप समूह", + "Country_Holy_See_Vatican_City_State": "होली सी (वेटिकन सिटी राज्य)", + "Country_Honduras": "होंडुरस", + "Country_Hong_Kong": "हांगकांग", + "Country_Hungary": "हंगरी", + "Country_Iceland": "आइसलैंड", + "Country_India": "भारत", + "Country_Indonesia": "इंडोनेशिया", + "Country_Iran_Islamic_Republic_of": "ईरान (इस्लामिक रिपब्लिक ऑफ", + "Country_Iraq": "इराक", + "Country_Ireland": "आयरलैंड", + "Country_Israel": "इजराइल", + "Country_Italy": "इटली", + "Country_Jamaica": "जमैका", + "Country_Japan": "जापान", + "Country_Jordan": "जॉर्डन", + "Country_Kazakhstan": "कजाखस्तान", + "Country_Kenya": "केन्या", + "Country_Kiribati": "किरिबाती", + "Country_Korea_Democratic_Peoples_Republic_of": "कोरिया प्रजातात्रिक जनवादी गणतंत्र", + "Country_Korea_Republic_of": "कोरिया गणराज्य", + "Country_Kuwait": "कुवैट", + "Country_Kyrgyzstan": "किर्गिज़स्तान", + "Country_Lao_Peoples_Democratic_Republic": "लाओ पीपुल्स डेमोक्रेटिक रिपब्लिक", + "Country_Latvia": "लातविया", + "Country_Lebanon": "लेबनान", + "Country_Lesotho": "लिसोटो", + "Country_Liberia": "लाइबेरिया", + "Country_Libyan_Arab_Jamahiriya": "लीबिया का अरब जमहिरिया", + "Country_Liechtenstein": "लिकटेंस्टाइन", + "Country_Lithuania": "लिथुआनिया", + "Country_Luxembourg": "लक्समबर्ग", + "Country_Macao": "मकाओ", + "Country_Macedonia_The_Former_Yugoslav_Republic_of": "मैसेडोनिया, पूर्व यूगोस्लाव गणराज्य", + "Country_Madagascar": "मेडागास्कर", + "Country_Malawi": "मलावी", + "Country_Malaysia": "मलेशिया", + "Country_Maldives": "मालदीव", + "Country_Mali": "वे थे", + "Country_Malta": "माल्टा", + "Country_Marshall_Islands": "मार्शल द्वीपसमूह", + "Country_Martinique": "मार्टीनिक", + "Country_Mauritania": "मॉरिटानिया", + "Country_Mauritius": "मॉरीशस", + "Country_Mayotte": "मैयट", + "Country_Mexico": "मेक्सिको", + "Country_Micronesia_Federated_States_of": "माइक्रोनेशिया, संघीय राज्य", + "Country_Moldova_Republic_of": "मोल्दोवा, गणराज्य", + "Country_Monaco": "मोनाको", + "Country_Mongolia": "मंगोलिया", + "Country_Montserrat": "मोंटेसेराट", + "Country_Morocco": "मोरक्को", + "Country_Mozambique": "मोज़ाम्बिक", + "Country_Myanmar": "म्यांमार", + "Country_Namibia": "नामिबिया", + "Country_Nauru": "नाउरू", + "Country_Nepal": "नेपाल", + "Country_Netherlands": "नीदरलैंड", + "Country_Netherlands_Antilles": "नीदरलैंड्स एंटाइल्स", + "If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours": "यदि आपके पास कोई नहीं है तो अपना पाने के लिए [omni@rocket.chat](mailto:omni@rocket.chat) पर एक ईमेल भेजें।", + "Country_New_Caledonia": "नया केलडोनिया", + "Country_New_Zealand": "न्यूज़ीलैंड", + "Country_Nicaragua": "निकारागुआ", + "Country_Niger": "नाइजर", + "Country_Nigeria": "नाइजीरिया", + "Country_Niue": "नियू", + "Country_Norfolk_Island": "नॉरफ़ॉक द्वीप", + "Country_Northern_Mariana_Islands": "उत्तरी मरीयाना द्वीप समूह", + "Country_Norway": "नॉर्वे", + "Country_Oman": "अपने मन", + "Country_Pakistan": "पाकिस्तान", + "Country_Palau": "पलाउ", + "Country_Palestinian_Territory_Occupied": "अधिकृत फ़िलिस्तीन क्षेत्र", + "Country_Panama": "पनामा", + "Country_Papua_New_Guinea": "पापुआ न्यू गिनी", + "Country_Paraguay": "परागुआ", + "Country_Peru": "पेरू", + "Country_Philippines": "फिलिपींस", + "Country_Pitcairn": "पिटकेर्न", + "Country_Poland": "पोलैंड", + "Country_Portugal": "पुर्तगाल", + "Country_Puerto_Rico": "प्यूर्टो रिको", + "Country_Qatar": "कतर", + "Country_Reunion": "रीयूनियन", + "Country_Romania": "रोमानिया", + "Country_Russian_Federation": "रूसी संघ", + "Country_Rwanda": "रवांडा", + "Country_Saint_Helena": "Saint Helena", + "Country_Saint_Kitts_and_Nevis": "संत किट्ट्स और नेविस", + "Country_Saint_Lucia": "सेंट लूसिया", + "Country_Saint_Pierre_and_Miquelon": "सेंट पियरे और मिकेलॉन", + "Country_Saint_Vincent_and_The_Grenadines": "संत विंसेंट अँड थे ग्रेनडीनेस", + "Country_Samoa": "समोआ", + "Country_San_Marino": "सैन मारिनो", + "Country_Sao_Tome_and_Principe": "साओ टोमे और प्रिंसिपे", + "Country_Saudi_Arabia": "सऊदी अरब", + "Country_Senegal": "सेनेगल", + "Country_Serbia_and_Montenegro": "सर्बिया और मोंटेनेग्रो", + "inline_code": "इनलाइन कोड", + "Country_Seychelles": "सेशल्स", + "Country_Sierra_Leone": "सेरा लिओन", + "Country_Singapore": "सिंगापुर", + "Country_Slovakia": "स्लोवाकिया", + "Country_Slovenia": "स्लोवेनिया", + "Country_Solomon_Islands": "सोलोमन इस्लैंडस", + "Country_Somalia": "सोमालिया", + "Country_South_Africa": "दक्षिण अफ्रीका", + "Country_South_Georgia_and_The_South_Sandwich_Islands": "दक्षिण जॉर्जिया और दक्षिण सैंडविच द्वीप समूह", + "Country_Spain": "स्पेन", + "Country_Sri_Lanka": "श्रीलंका", + "Country_Sudan": "सूडान", + "Country_Suriname": "सूरीनाम", + "Country_Svalbard_and_Jan_Mayen": "स्वालबार्ड और जान मायेन", + "Country_Swaziland": "स्वाजीलैंड", + "Country_Sweden": "स्वीडन", + "Country_Switzerland": "स्विट्ज़रलैंड", + "Country_Syrian_Arab_Republic": "सीरियाई अरब गणराज्य", + "Country_Taiwan_Province_of_China": "ताइवान, चीन प्रांत", + "Country_Tajikistan": "तजाकिस्तान", + "Country_Tanzania_United_Republic_of": "तंजानिया, संयुक्त गणराज्य", + "Country_Thailand": "थाईलैंड", + "Country_Timor_leste": "तिमोर ने पढ़ा", + "Country_Togo": "चल देना", + "Country_Tokelau": "टोकेलाऊ", + "Country_Tonga": "पहुँचा", + "Country_Trinidad_and_Tobago": "त्रिनिदाद और टोबैगो", + "Country_Tunisia": "ट्यूनीशिया", + "Country_Turkey": "टर्की", + "Country_Turkmenistan": "तुर्कमेनिस्तान", + "Country_Turks_and_Caicos_Islands": "तुर्क और कैकोस द्वीप समूह", + "Country_Tuvalu": "तुवालू", + "Country_Uganda": "युगांडा", + "Country_Ukraine": "यूक्रेन", + "Country_United_Arab_Emirates": "संयुक्त अरब अमीरात", + "Country_United_Kingdom": "यूनाइटेड किंगडम", + "Country_United_States": "संयुक्त राज्य अमेरिका", + "Country_United_States_Minor_Outlying_Islands": "संयुक्त राज्य अमेरिका के छोटे दूरस्थ द्वीपसमूह", + "Country_Uruguay": "उरुग्वे", + "Country_Uzbekistan": "उज़्बेकिस्तान", + "Country_Vanuatu": "वानुअतु", + "Country_Venezuela": "वेनेज़ुएला", + "Country_Viet_Nam": "वियतनाम", + "Country_Virgin_Islands_British": "वर्जिन द्वीप समूह, ब्रिटिश", + "Country_Virgin_Islands_US": "वर्जिन द्वीप समूह, यू.एस.", + "Country_Wallis_and_Futuna": "वाली और फ़्युटुना", + "Country_Western_Sahara": "पश्चिमी सहारा", + "Country_Yemen": "यमन", + "Country_Zambia": "जाम्बिया", + "Country_Zimbabwe": "ज़िम्बाब्वे", + "Create": "बनाएं", + "Create_canned_response": "डिब्बाबंद प्रतिक्रिया बनाएँ", + "Create_custom_field": "कस्टम फ़ील्ड बनाएं", + "Create_channel": "चैनल बनाएं", + "Create_channels": "चैनल बनाएं", + "Create_a_public_channel_that_new_workspace_members_can_join": "एक सार्वजनिक चैनल बनाएं जिसमें नए कार्यक्षेत्र सदस्य शामिल हो सकें।", + "Create_A_New_Channel": "एक नया चैनल बनाएं", + "Create_new": "नया निर्माण", + "Create_new_members": "नए सदस्य बनाएं", + "Create_unique_rules_for_this_channel": "इस चैनल के लिए अद्वितीय नियम बनाएं", + "Create_unit": "इकाई बनाएं", + "create-c": "सार्वजनिक चैनल बनाएं", + "create-c_description": "सार्वजनिक चैनल बनाने की अनुमति", + "create-d": "सीधे संदेश बनाएं", + "create-d_description": "सीधे संदेश प्रारंभ करने की अनुमति", + "create-invite-links": "आमंत्रण लिंक बनाएं", + "create-invite-links_description": "चैनलों के लिए आमंत्रण लिंक बनाने की अनुमति", + "create-p": "निजी चैनल बनाएं", + "create-p_description": "निजी चैनल बनाने की अनुमति", + "create-personal-access-tokens": "व्यक्तिगत एक्सेस टोकन बनाएं", + "create-personal-access-tokens_description": "व्यक्तिगत एक्सेस टोकन बनाने की अनुमति", + "create-team": "टीम बनाएं", + "create-team_description": "टीमें बनाने की अनुमति", + "create-user": "उपयोगकर्ता बनाइये", + "create-user_description": "उपयोगकर्ता बनाने की अनुमति", + "Created": "बनाया था", + "Created_as": "के रूप में बनाया गया", + "Created_at": "पर बनाया गया", + "Created_at_s_by_s": "%s द्वारा % s पर बनाया गया", + "Created_at_s_by_s_triggered_by_s": "%s द्वारा %s पर बनाया गया , %s द्वारा ट्रिगर किया गया", + "Created_by": "के द्वारा बनाई गई", + "CRM_Integration": "सीआरएम एकीकरण", + "CROWD_Allow_Custom_Username": "Rocket.Chat में कस्टम उपयोगकर्ता नाम की अनुमति दें", + "CROWD_Reject_Unauthorized": "अनधिकृत अस्वीकार करें", + "Crowd_Remove_Orphaned_Users": "अनाथ उपयोगकर्ताओं को हटाएँ", + "Crowd_sync_interval_Description": "तुल्यकालन के बीच का अंतराल. उदाहरण `हर 24 घंटे` या `सप्ताह के पहले दिन`, अधिक उदाहरण [क्रोन टेक्स्ट पार्सर](http://bunkat.github.io/later/parsers.html#text) पर", + "Current_Chats": "वर्तमान चैट", + "Current_File": "मौजूदा फ़ाइल", + "Current_Import_Operation": "वर्तमान आयात परिचालन", + "Current_Status": "वर्तमान स्थिति", + "Currently_we_dont_support_joining_servers_with_this_many_people": "वर्तमान में हम इतने सारे लोगों के साथ सर्वर से जुड़ने का समर्थन नहीं करते हैं", "Custom": "कस्टम", + "Custom CSS": "कस्टम सीएसएस", + "Custom_agent": "कस्टम एजेंट", + "Custom_dates": "कस्टम तिथियाँ", + "Custom_Emoji": "कस्टम इमोजी", + "Custom_Emoji_Add": "नया इमोजी जोड़ें", + "Custom_Emoji_Added_Successfully": "कस्टम इमोजी सफलतापूर्वक जोड़ा गया", + "Custom_Emoji_Delete_Warning": "किसी इमोजी को हटाना पूर्ववत नहीं किया जा सकता.", + "Custom_Emoji_Error_Invalid_Emoji": "अमान्य इमोजी", + "Custom_Emoji_Error_Name_Or_Alias_Already_In_Use": "कस्टम इमोजी या उसका कोई उपनाम पहले से ही उपयोग में है।", + "Custom_Emoji_Error_Same_Name_And_Alias": "कस्टम इमोजी नाम और उनके उपनाम अलग-अलग होने चाहिए.", + "Custom_Emoji_Has_Been_Deleted": "कस्टम इमोजी हटा दिया गया है.", + "Custom_Emoji_Info": "कस्टम इमोजी जानकारी", + "Custom_Emoji_Updated_Successfully": "कस्टम इमोजी सफलतापूर्वक अपडेट किया गया", + "Custom_Fields": "तटकर क्षेत्र", + "Custom_Field_Removed": "कस्टम फ़ील्ड हटा दी गई", + "Custom_Field_Not_Found": "कस्टम फ़ील्ड नहीं मिला", + "Custom_Integration": "कस्टम एकीकरण", + "Custom_OAuth_has_been_added": "कस्टम OAuth जोड़ा गया है", + "Custom_OAuth_has_been_removed": "कस्टम OAuth हटा दिया गया है", + "Custom_oauth_helper": "अपना OAuth प्रदाता स्थापित करते समय, आपको एक कॉलबैक URL सूचित करना होगा। उपयोग

     %एस
    .", + "Custom_oauth_unique_name": "कस्टम OAuth अद्वितीय नाम", + "Custom_roles": "कस्टम भूमिकाएँ", + "Custom_roles_upsell_add_custom_roles_workspace": "अपने कार्यक्षेत्र के अनुरूप कस्टम भूमिकाएँ जोड़ें", + "Custom_roles_upsell_add_custom_roles_workspace_description": "कस्टम भूमिकाएँ आपको अपने कार्यक्षेत्र में लोगों के लिए अनुमतियाँ सेट करने की अनुमति देती हैं। यह सुनिश्चित करने के लिए कि लोगों को काम करने के लिए सुरक्षित वातावरण मिले, सभी भूमिकाएँ निर्धारित करें।", + "Custom_Script_Logged_In": "लॉग इन उपयोगकर्ताओं के लिए कस्टम स्क्रिप्ट", + "Custom_Script_Logged_In_Description": "कस्टम स्क्रिप्ट जो हमेशा और लॉग इन किए गए किसी भी उपयोगकर्ता पर चलेगी। (जब भी आप चैट में प्रवेश करते हैं और आप लॉग इन होते हैं)", + "Custom_Script_Logged_Out": "लॉग आउट उपयोगकर्ताओं के लिए कस्टम स्क्रिप्ट", + "Custom_Script_Logged_Out_Description": "कस्टम स्क्रिप्ट जो हमेशा चलेगी और किसी भी उपयोगकर्ता के लिए जो लॉग इन नहीं है। (जब भी आप लॉगिन पेज दर्ज करें)", + "Custom_Script_On_Logout": "लॉगआउट फ़्लो के लिए कस्टम स्क्रिप्ट", + "Custom_Script_On_Logout_Description": "कस्टम स्क्रिप्ट जो केवल निष्पादन लॉगआउट प्रवाह पर चलेगी", + "Custom_Scripts": "कस्टम स्क्रिप्ट", + "Custom_Sound_Add": "कस्टम ध्वनि जोड़ें", + "Custom_Sound_Delete_Warning": "किसी ध्वनि को हटाना पूर्ववत नहीं किया जा सकता.", + "Custom_Sound_Edit": "कस्टम ध्वनि संपादित करें", + "Custom_Sound_Error_Invalid_Sound": "अमान्य ध्वनि", + "Custom_Sound_Error_Name_Already_In_Use": "कस्टम ध्वनि नाम पहले से ही उपयोग में है.", + "Custom_Sound_Has_Been_Deleted": "कस्टम ध्वनि हटा दी गई है.", + "Custom_Sound_Info": "कस्टम ध्वनि जानकारी", + "Custom_Sound_Saved_Successfully": "कस्टम ध्वनि सफलतापूर्वक सहेजी गई", + "Custom_Status": "कस्टम स्थिति", + "Custom_Translations": "कस्टम अनुवाद", + "Custom_Translations_Description": "एक वैध JSON होना चाहिए जहां कुंजी ऐसी भाषाएं हैं जिनमें कुंजी और अनुवाद का शब्दकोश होता है। उदाहरण: `{\"en\": {\"चैनल\": \"कमरे\"},\"pt\": {\"चैनल\": \"सलास\"}}`", + "Custom_User_Status": "कस्टम उपयोगकर्ता स्थिति", + "Custom_User_Status_Add": "कस्टम उपयोगकर्ता स्थिति जोड़ें", + "Custom_User_Status_Added_Successfully": "कस्टम उपयोगकर्ता स्थिति सफलतापूर्वक जोड़ी गई", + "Custom_User_Status_Delete_Warning": "कस्टम उपयोगकर्ता स्थिति को हटाना पूर्ववत नहीं किया जा सकता।", + "Custom_User_Status_Edit": "कस्टम उपयोगकर्ता स्थिति संपादित करें", + "Custom_User_Status_Error_Invalid_User_Status": "अमान्य उपयोगकर्ता स्थिति", + "Custom_User_Status_Error_Name_Already_In_Use": "कस्टम उपयोगकर्ता स्थिति नाम पहले से ही उपयोग में है।", + "Custom_User_Status_Has_Been_Deleted": "कस्टम उपयोगकर्ता स्थिति हटा दी गई है", + "Custom_User_Status_Info": "कस्टम उपयोगकर्ता स्थिति जानकारी", + "Custom_User_Status_Updated_Successfully": "कस्टम उपयोगकर्ता स्थिति सफलतापूर्वक अपडेट की गई", + "Customer_without_registered_email": "ग्राहक के पास पंजीकृत ईमेल पता नहीं है", + "Customize": "अनुकूलित करें", + "Customize_Content": "सामग्री को अनुकूलित करें", + "CustomSoundsFilesystem": "कस्टम ध्वनि फ़ाइल सिस्टम", + "CustomSoundsFilesystem_Description": "निर्दिष्ट करें कि कस्टम ध्वनियाँ कैसे संग्रहीत की जाती हैं।", + "Daily_Active_Users": "दैनिक सक्रिय उपयोगकर्ता", + "Dashboard": "डैशबोर्ड", + "Data_modified": "डेटा संशोधित", + "Data_processing_consent_text": "डेटा प्रोसेसिंग सहमति पाठ", + "Data_processing_consent_text_description": "इस सेटिंग का उपयोग यह समझाने के लिए करें कि आप बातचीत के दौरान ग्राहक की व्यक्तिगत जानकारी एकत्र, संग्रहीत और संसाधित कर सकते हैं।", + "Date": "तारीख", + "Date_From": "से", + "Date_to": "को", + "DAU_value": "डीएयू {{price}}", + "days": "दिन", + "Days": "दिन", + "DB_Migration": "डेटाबेस माइग्रेशन", + "DB_Migration_Date": "डेटाबेस माइग्रेशन तिथि", + "DDP_Rate_Limiter": "डीडीपी दर सीमा", + "DDP_Rate_Limit_Connection_By_Method_Enabled": "प्रति विधि कनेक्शन द्वारा सीमा: सक्षम", + "DDP_Rate_Limit_Connection_By_Method_Interval_Time": "प्रति विधि कनेक्शन द्वारा सीमा: अंतराल समय", + "DDP_Rate_Limit_Connection_By_Method_Requests_Allowed": "प्रति विधि कनेक्शन द्वारा सीमा: अनुरोधों की अनुमति है", + "DDP_Rate_Limit_Connection_Enabled": "कनेक्शन द्वारा सीमा: सक्षम", + "DDP_Rate_Limit_Connection_Interval_Time": "कनेक्शन द्वारा सीमा: अंतराल समय", + "DDP_Rate_Limit_Connection_Requests_Allowed": "कनेक्शन द्वारा सीमा: अनुरोधों की अनुमति है", + "DDP_Rate_Limit_IP_Enabled": "आईपी द्वारा सीमा: सक्षम", + "DDP_Rate_Limit_IP_Interval_Time": "आईपी द्वारा सीमा: अंतराल समय", + "DDP_Rate_Limit_IP_Requests_Allowed": "आईपी द्वारा सीमा: अनुरोधों की अनुमति है", + "DDP_Rate_Limit_User_By_Method_Enabled": "प्रति विधि उपयोगकर्ता द्वारा सीमा: सक्षम", + "DDP_Rate_Limit_User_By_Method_Interval_Time": "प्रति विधि उपयोगकर्ता द्वारा सीमा: अंतराल समय", + "DDP_Rate_Limit_User_By_Method_Requests_Allowed": "प्रति विधि उपयोगकर्ता द्वारा सीमा: अनुरोधों की अनुमति है", + "DDP_Rate_Limit_User_Enabled": "उपयोगकर्ता द्वारा सीमा: सक्षम", + "DDP_Rate_Limit_User_Interval_Time": "उपयोगकर्ता द्वारा सीमा: अंतराल समय", + "DDP_Rate_Limit_User_Requests_Allowed": "उपयोगकर्ता द्वारा सीमा: अनुरोधों की अनुमति है", + "Deactivate": "निष्क्रिय करें", + "Decline": "गिरावट", + "default": "गलती करना", + "Default": "गलती करना", + "Default_provider": "डिफ़ॉल्ट प्रदाता", + "Default_value": "डिफ़ॉल्ट मान", + "Delete": "मिटाना", + "Deleting": "हटाया जा रहा है", + "Delete_account": "खाता हटा दो", + "Delete_account?": "खाता हटा दो?", + "Delete_all_closed_chats": "सभी बंद चैट हटाएं", + "Delete_Department?": "विभाग हटाएं?", + "Delete_File_Warning": "किसी फ़ाइल को हटाने से वह हमेशा के लिए हट जाएगी. इसे असंपादित नहीं किया जा सकता है।", + "Delete_message": "संदेश को हटाएं", + "Delete_my_account": "मेरा एकाउंट हटा दो", + "Delete_Role_Warning": "इसे असंपादित नहीं किया जा सकता है", + "Delete_Room_Warning": "किसी रूम को हटाने से रूम के भीतर पोस्ट किए गए सभी संदेश हट जाएंगे। इसे असंपादित नहीं किया जा सकता है।", + "Delete_User_Warning": "किसी उपयोगकर्ता को हटाने से उस उपयोगकर्ता के सभी संदेश भी हट जाएंगे। इसे असंपादित नहीं किया जा सकता है।", + "Delete_User_Warning_Delete": "किसी उपयोगकर्ता को हटाने से उस उपयोगकर्ता के सभी संदेश भी हट जाएंगे। इसे असंपादित नहीं किया जा सकता है।", + "Delete_User_Warning_Keep": "उपयोगकर्ता को हटा दिया जाएगा, लेकिन उनके संदेश दृश्यमान रहेंगे. इसे असंपादित नहीं किया जा सकता है।", + "Delete_User_Warning_Unlink": "किसी उपयोगकर्ता को हटाने से उनके सभी संदेशों से उपयोगकर्ता नाम हटा दिया जाएगा। इसे असंपादित नहीं किया जा सकता है।", + "delete-c": "सार्वजनिक चैनल हटाएँ", + "delete-c_description": "सार्वजनिक चैनलों को हटाने की अनुमति", + "delete-d": "सीधे संदेश हटाएँ", + "delete-d_description": "सीधे संदेशों को हटाने की अनुमति", + "delete-message": "संदेश को हटाएं", + "delete-message_description": "एक कमरे के भीतर एक संदेश को हटाने की अनुमति", + "delete-own-message": "स्वयं का संदेश हटाएँ", + "delete-own-message_description": "स्वयं का संदेश हटाने की अनुमति", + "delete-p": "निजी चैनल हटाएँ", + "delete-p_description": "निजी चैनल हटाने की अनुमति", + "delete-team": "टीम हटाएँ", + "delete-team_description": "टीमों को हटाने की अनुमति", + "delete-user": "उपभोक्ता मिटायें", + "delete-user_description": "उपयोगकर्ताओं को हटाने की अनुमति", + "Deleted": "हटा दिया गया!", + "Deleted_user": "हटाया हुआ उपयोगकर्ता", + "Deleted__roomName__": "#{{roomName}} हटा दिया गया", + "Deleted__roomName__room": "#{{roomName}} हटा दिया गया", + "Department": "विभाग", + "Department_archived": "विभाग संग्रहीत", + "Department_name": "विभाग का नाम", + "Department_not_found": "विभाग नहीं मिला", + "Department_removed": "विभाग हटा दिया गया", + "Department_Removal_Disabled": "व्यवस्थापक द्वारा हटाएं विकल्प अक्षम कर दिया गया है", + "Department_unarchived": "विभाग अनारक्षित", + "Departments": "विभागों", + "Deployment_ID": "परिनियोजन आईडी", + "Deployment": "तैनाती", + "Description": "विवरण", + "Desktop": "डेस्कटॉप", + "Desktop_apps": "डेस्कटॉप ऐप्स", + "Desktop_Notification_Test": "डेस्कटॉप अधिसूचना परीक्षण", + "Desktop_Notifications": "डेस्कटॉप सूचनाएं", "Desktop_Notifications_Default_Alert": "डेस्कटॉप सूचनाएं डिफ़ॉल्ट चेतावनी", + "Desktop_Notifications_Disabled": "डेस्कटॉप सूचनाएं अक्षम हैं. यदि आपको सूचनाएं सक्षम करने की आवश्यकता है तो अपनी ब्राउज़र प्राथमिकताएं बदलें।", + "Desktop_Notifications_Duration": "डेस्कटॉप अधिसूचना period", + "Desktop_Notifications_Duration_Description": "डेस्कटॉप अधिसूचना प्रदर्शित करने के लिए सेकंड। यह OS X अधिसूचना केंद्र को प्रभावित कर सकता है। डिफ़ॉल्ट ब्राउज़र सेटिंग्स का उपयोग करने और ओएस एक्स अधिसूचना केंद्र को प्रभावित न करने के लिए 0 दर्ज करें।", + "Desktop_Notifications_Enabled": "डेस्कटॉप सूचनाएं सक्षम हैं", + "Desktop_Notifications_Not_Enabled": "डेस्कटॉप सूचनाएं सक्षम नहीं हैं", + "Unselected_by_default": "डिफ़ॉल्ट रूप से अचयनित", + "Unseen_features": "अनदेखी विशेषताएं", + "Details": "विवरण", + "Device_Changes_Not_Available": "इस ब्राउज़र में डिवाइस परिवर्तन उपलब्ध नहीं हैं. गारंटीकृत उपलब्धता के लिए, कृपया Rocket.Chat के आधिकारिक डेस्कटॉप ऐप का उपयोग करें।", + "Device_Changes_Not_Available_Insecure_Context": "डिवाइस परिवर्तन केवल सुरक्षित संदर्भों पर उपलब्ध हैं (जैसे https://)", + "Device_Management": "डिवाइस प्रबंधन", + "Device_Management_Allow_Login_Email_preference": "कार्यस्थान सदस्यों को लॉगिन पहचान ईमेल बंद करने की अनुमति दें", + "Device_Management_Allow_Login_Email_preference_Description": "व्यक्तिगत सदस्य अपनी प्राथमिकता निर्धारित कर सकते हैं। तब उपयोगी जब बार-बार लॉगिन समाप्ति तिथि निर्धारित की जाती है जिससे सदस्यों को बार-बार लॉगिन करना पड़ता है।", + "Device_Management_Client": "ग्राहक", + "Device_Management_Description": "सुरक्षा और पहुंच नियंत्रण नीतियां कॉन्फ़िगर करें.", + "Device_Management_Device": "उपकरण", + "line": "रेखा", + "Device_Management_Device_Unknown": "अज्ञात", + "Device_Management_Email_Subject": "[साइट_नाम] - लॉगिन का पता चला", + "Device_Management_Email_Body": "आप निम्नलिखित प्लेसहोल्डर्स का उपयोग कर सकते हैं: `

    {लॉगिन_डिटेक्टेड}

    [नाम] ([उपयोगकर्ता नाम]) {Logged_In_Via}

    {डिवाइस_मैनेजमेंट_क्लाइंट}: [ब्राउज़रइन्फो]
    {डिवाइस_मैनेजमेंट_ओएस}: [osInfo]
    {डिवाइस_मैनेजमेंट_डिवाइस}: [डिवाइसइन्फो]
    {डिवाइस_मैनेजमेंट_आईपी}: [आईपीइन्फो]

    [उपयोगकर्ता एजेंट]

    {अपने खाते पर पहुंच}

    {Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}
    [साइट URL]

    {Thank_You_For_Choosing_RocketChat}

    `", + "Device_Management_Enable_Login_Emails": "लॉगिन पहचान ईमेल सक्षम करें", + "Device_Management_Enable_Login_Emails_Description": "कार्यस्थल के सदस्यों को हर बार उनके खातों में नए लॉगिन का पता चलने पर ईमेल भेजे जाते हैं।", + "Device_Management_IP": "आई पी", + "Device_Management_OS": "आप", + "Device_ID": "डिवाइस आईडी", + "Device_Info": "डिवाइस जानकारी", + "Device_Logged_Out": "डिवाइस लॉग आउट हो गया", + "Device_Logout_Text": "डिवाइस कार्यक्षेत्र से लॉग आउट हो जाएगा और वर्तमान सत्र समाप्त हो जाएगा। उपयोगकर्ता उसी डिवाइस से दोबारा लॉग इन कर सकेगा।", + "Devices": "उपकरण", + "Devices_Set": "डिवाइस सेट", + "Device_settings": "उपकरण सेटिंग्स", + "Dialed_number_doesnt_exist": "डायल किया गया नंबर मौजूद नहीं है", + "Dialed_number_is_incomplete": "डायल किया गया नंबर पूरा नहीं है", + "Different_Style_For_User_Mentions": "उपयोगकर्ता उल्लेखों के लिए अलग शैली", + "Livechat_Facebook_API_Key": "ओमनीचैनल एपीआई कुंजी", + "Direct": "प्रत्यक्ष", + "Direction": "दिशा", + "Livechat_Facebook_API_Secret": "ओमनीचैनल एपीआई रहस्य", + "Direct_Message": "सीधा संदेश", + "Livechat_Facebook_Enabled": "फेसबुक एकीकरण सक्षम", + "Direct_message_creation_description": "आप एकाधिक उपयोगकर्ताओं के साथ चैट बनाने वाले हैं. जिन लोगों से आप बात करना चाहते हैं, उन सभी को सीधे संदेशों का उपयोग करके एक ही स्थान पर जोड़ें।", + "Direct_message_someone": "किसी को सीधा संदेश भेजें", + "Direct_message_you_have_joined": "आप एक नए डायरेक्ट मैसेज से जुड़े हैं", + "Direct_Messages": "सीधे संदेश", + "Direct_Reply": "सीधा उत्तर", + "Direct_Reply_Advice": "आप सीधे इस ईमेल का उत्तर दे सकते हैं. थ्रेड में पिछले ईमेल को संशोधित न करें.", + "Direct_Reply_Debug": "सीधा उत्तर डिबग करें", + "Direct_Reply_Debug_Description": "[सावधान] डिबग मोड सक्षम करने से आपका 'प्लेन टेक्स्ट पासवर्ड' एडमिन कंसोल में प्रदर्शित होगा।", + "Direct_Reply_Delete": "ईमेल हटाएँ", + "Direct_Reply_Delete_Description": "[ध्यान दें!] यदि यह विकल्प सक्रिय है, तो सभी अपठित संदेश अपरिवर्तनीय रूप से हटा दिए जाते हैं, यहां तक कि वे भी जो सीधे उत्तर नहीं हैं। कॉन्फ़िगर किया गया ई-मेल मेलबॉक्स हमेशा खाली रहता है और इसे मनुष्यों द्वारा \"समानांतर\" में संसाधित नहीं किया जा सकता है।", + "Direct_Reply_Enable": "सीधा उत्तर सक्षम करें", + "Direct_Reply_Enable_Description": "[ध्यान दें!] यदि \"डायरेक्ट रिप्लाई\" सक्षम है, तो Rocket.Chat कॉन्फ़िगर किए गए ईमेल मेलबॉक्स को नियंत्रित करेगा। सभी अपठित ई-मेल पुनर्प्राप्त किए जाते हैं, पढ़े गए के रूप में चिह्नित किए जाते हैं और संसाधित किए जाते हैं। \"डायरेक्ट रिप्लाई\" केवल तभी सक्रिय किया जाना चाहिए जब उपयोग किया गया मेलबॉक्स विशेष रूप से Rocket.Chat द्वारा पहुंच के लिए है और मनुष्यों द्वारा \"समानांतर में\" पढ़ा/संसाधित नहीं किया गया है।", + "Direct_Reply_Frequency": "ईमेल जाँच आवृत्ति", + "Direct_Reply_Frequency_Description": "(मिनटों में, डिफ़ॉल्ट/न्यूनतम 2)", + "Direct_Reply_Host": "डायरेक्ट रिप्लाई होस्ट", + "Direct_Reply_IgnoreTLS": "टीएलएस पर ध्यान न दें", + "Direct_Reply_Password": "पासवर्ड", + "Direct_Reply_Port": "डायरेक्ट_रिप्लाई_पोर्ट", + "Direct_Reply_Protocol": "प्रत्यक्ष उत्तर प्रोटोकॉल", + "Direct_Reply_Separator": "सेपरेटर", + "Direct_Reply_Separator_Description": "[केवल तभी परिवर्तन करें जब आप ठीक-ठीक जानते हों कि आप क्या कर रहे हैं, दस्तावेज़ देखें]\nईमेल के आधार और टैग भाग के बीच विभाजक", + "Direct_Reply_Username": "उपयोगकर्ता नाम", + "Direct_Reply_Username_Description": "कृपया संपूर्ण ईमेल का उपयोग करें, टैगिंग की अनुमति नहीं है, इसे अधिक लिखा जाएगा", + "Directory": "निर्देशिका", + "Disable": "अक्षम करना", + "Disable_Facebook_integration": "फेसबुक एकीकरण अक्षम करें", + "Disable_Notifications": "नोटीफिकेशन निष्क्रिय किया गया", + "Disable_two-factor_authentication": "TOTP के माध्यम से दो-कारक प्रमाणीकरण अक्षम करें", + "Disable_two-factor_authentication_email": "ईमेल के माध्यम से दो-कारक प्रमाणीकरण अक्षम करें", "Disabled": "उपयोग करने की अनुमति नहीं है", + "Disallow_reacting": "प्रतिक्रिया करने की अनुमति न दें", + "Disallow_reacting_Description": "प्रतिक्रिया करने की अनुमति नहीं देता", + "Discard": "खारिज करना", + "Disconnect": "डिस्कनेक्ट", + "Discover_public_channels_and_teams_in_the_workspace_directory": "कार्यक्षेत्र निर्देशिका में सार्वजनिक चैनल और टीमें खोजें।", + "Discussion": "बहस", + "Discussion_Description": "चर्चाएँ वार्तालापों को व्यवस्थित करने का एक अतिरिक्त तरीका है जो बाहरी चैनलों के उपयोगकर्ताओं को विशिष्ट वार्तालापों में भाग लेने के लिए आमंत्रित करने की अनुमति देता है।", + "Discussion_description": "क्या हो रहा है इसका अवलोकन रखने में सहायता करें! एक चर्चा बनाने से, आपके द्वारा चुने गए चैनल का एक उप-चैनल बनाया जाता है और दोनों लिंक हो जाते हैं।", + "Discussion_first_message_disabled_due_to_e2e": "आप इसके निर्माण के बाद इस चर्चा में एंड-टू-एंड एन्क्रिप्टेड संदेश भेजना शुरू कर सकते हैं।", + "Discussion_first_message_title": "आपका संदेश", + "Discussion_name": "चर्चा का नाम", + "Discussion_start": "चर्चा प्रारंभ करें", + "Discussion_target_channel": "मूल चैनल या समूह", + "Discussion_target_channel_description": "एक चैनल चुनें जो आप जो पूछना चाहते हैं उससे संबंधित हो", + "Discussion_target_channel_prefix": "आप एक चर्चा बना रहे हैं", + "Discussion_title": "चर्चा बनाएं", + "Discussions_unavailable_for_federation": "फेडरेटेड रूम के लिए चर्चाएँ उपलब्ध नहीं हैं", + "discussion-created": "{{message}}", + "Discussions": "चर्चाएँ", + "Display": "प्रदर्शन", + "Display_avatars": "अवतार प्रदर्शित करें", + "Display_Avatars_Sidebar": "साइडबार में अवतार प्रदर्शित करें", + "Display_chat_permissions": "चैट अनुमतियाँ प्रदर्शित करें", + "Display_mentions_counter": "केवल प्रत्यक्ष उल्लेख के लिए बैज प्रदर्शित करें", + "Display_offline_form": "ऑफ़लाइन फॉर्म प्रदर्शित करें", + "Display_setting_permissions": "सेटिंग्स बदलने के लिए अनुमतियाँ प्रदर्शित करें", + "Display_unread_counter": "अपठित संदेश होने पर रूम को अपठित के रूप में प्रदर्शित करें", + "Displays_action_text": "क्रिया पाठ प्रदर्शित करता है", + "Do_It_Later": "इसे बाद में करें", + "Do_not_display_unread_counter": "इस चैनल का कोई भी काउंटर प्रदर्शित न करें", + "Do_not_provide_this_code_to_anyone": "यह कोड किसी को न दें.", + "Do_Nothing": "कुछ भी नहीं है", + "Do_you_have_any_notes_for_this_conversation": "क्या आपके पास इस बातचीत के लिए कोई नोट्स हैं?", + "Do_you_want_to_accept": "क्या आप स्वीकार करना चाहते हैं?", + "Do_you_want_to_change_to_s_question": "क्या आप %s में बदलना चाहते हैं?", + "Documentation": "प्रलेखन", + "Document_Domain": "दस्तावेज़ डोमेन", + "Domain": "कार्यक्षेत्र", + "Domain_added": "डोमेन जोड़ा गया", + "Domain_removed": "डोमेन हटाया गया", + "Domains": "डोमेन", + "Domains_allowed_to_embed_the_livechat_widget": "लाइवचैट विजेट को एम्बेड करने की अनुमति वाले डोमेन की अल्पविराम से अलग की गई सूची। सभी डोमेन को अनुमति देने के लिए खाली छोड़ें।", + "Done": "हो गया", + "Dont_ask_me_again": "मुझसे दोबारा मत पूछो!", + "Dont_ask_me_again_list": "मुझसे दुबारा सूची मत पूछो", + "Download": "डाउनलोड करना", + "Download_Destkop_App": "डेस्कटॉप ऐप डाउनलोड करें", + "Download_Info": "जानकारी डाउनलोड करें", + "Download_My_Data": "मेरा डेटा डाउनलोड करें (HTML)", + "Download_Pending_Avatars": "लंबित अवतार डाउनलोड करें", + "Download_Pending_Files": "लंबित फ़ाइलें डाउनलोड करें", + "Download_Snippet": "डाउनलोड करना", + "Downloading_file_from_external_URL": "बाहरी URL से फ़ाइल डाउनलोड हो रही है", + "Drop_to_upload_file": "फ़ाइल अपलोड करने के लिए छोड़ें", + "Dry_run": "पूर्वाभ्यास", + "Dry_run_description": "प्रेषक के समान पते पर केवल एक ईमेल भेजा जाएगा। ईमेल किसी वैध उपयोगकर्ता का होना चाहिए.", + "Duplicate_archived_channel_name": "`#%s` नाम से एक संग्रहीत चैनल मौजूद है", + "Markdown_Headers": "संदेशों में मार्कडाउन हेडर की अनुमति दें", + "Markdown_Marked_Breaks": "चिह्नित ब्रेक सक्षम करें", + "Duplicate_archived_private_group_name": "'%s' नाम से एक संग्रहीत निजी समूह मौजूद है", + "Duplicate_channel_name": "'%s' नाम का एक चैनल मौजूद है", + "Markdown_Marked_GFM": "चिह्नित जीएफएम सक्षम करें", + "Duplicate_file_name_found": "डुप्लिकेट फ़ाइल नाम मिला.", + "Markdown_Marked_Pedantic": "चिह्नित पेडेंटिक सक्षम करें", + "Markdown_Marked_SmartLists": "चिह्नित स्मार्ट सूचियाँ सक्षम करें", + "Duplicate_private_group_name": "'%s' नाम से एक निजी समूह मौजूद है", + "Markdown_Marked_Smartypants": "चिह्नित स्मार्टपैंट सक्षम करें", + "Duplicated_Email_address_will_be_ignored": "डुप्लिकेट ईमेल पते पर ध्यान नहीं दिया जाएगा.", + "Markdown_Marked_Tables": "चिह्नित तालिकाएँ सक्षम करें", + "duplicated-account": "डुप्लिकेट खाता", + "E2E Encryption": "E2E एन्क्रिप्शन", + "E2E_Encryption_enabled_for_room": "#{{roomName}} के लिए एंड-टू-एंड एन्क्रिप्शन सक्षम किया गया", + "E2E_Encryption_disabled_for_room": "#{{roomName}} के लिए एंड-टू-एंड एन्क्रिप्शन अक्षम किया गया", + "Markdown_Parser": "मार्कडाउन पार्सर", + "Markdown_SupportSchemesForLink": "लिंक के लिए मार्कडाउन सहायता योजनाएँ", + "E2E Encryption_Description": "बातचीत को निजी रखें, यह सुनिश्चित करते हुए कि केवल प्रेषक और इच्छित प्राप्तकर्ता ही उन्हें पढ़ सकें।", + "Markdown_SupportSchemesForLink_Description": "अनुमत योजनाओं की अल्पविराम से अलग की गई सूची", + "E2E_enable": "E2E सक्षम करें", + "E2E_disable": "E2E अक्षम करें", + "E2E_Enable_alert": "यह विशेषता अभी बीटा संस्करण में है! कृपया github.com/RocketChat/Rocket.Chat/issues पर बग की रिपोर्ट करें और इनसे अवगत रहें:
    - एन्क्रिप्टेड रूम के एन्क्रिप्टेड संदेश सर्च ऑपरेशन से नहीं मिलेंगे।
    - मोबाइल ऐप्स एन्क्रिप्टेड संदेशों का समर्थन नहीं कर सकते (वे इसे लागू कर रहे हैं)।
    - बॉट एन्क्रिप्टेड संदेशों को तब तक नहीं देख पाएंगे जब तक वे इसके लिए समर्थन लागू नहीं करते।
    - इस संस्करण में अपलोड एन्क्रिप्टेड नहीं होंगे.", + "E2E_Enable_description": "एन्क्रिप्टेड समूह बनाने का विकल्प सक्षम करें और समूहों को बदलने और एन्क्रिप्ट किए जाने वाले संदेशों को निर्देशित करने में सक्षम हों", + "E2E_Enabled": "E2E सक्षम", + "E2E_Enabled_Default_DirectRooms": "डिफ़ॉल्ट रूप से डायरेक्ट रूम के लिए एन्क्रिप्शन सक्षम करें", + "E2E_Enabled_Default_PrivateRooms": "निजी कमरों के लिए डिफ़ॉल्ट रूप से एन्क्रिप्शन सक्षम करें", + "E2E_Encryption_Password_Change": "एन्क्रिप्शन पासवर्ड बदलें", + "E2E_Encryption_Password_Explanation": "अब आप एन्क्रिप्टेड निजी समूह और सीधे संदेश बना सकते हैं। आप मौजूदा निजी समूहों या डीएम को एन्क्रिप्टेड में भी बदल सकते हैं।

    यह एंड-टू-एंड एन्क्रिप्शन है इसलिए आपके संदेशों को एनकोड/डीकोड करने की कुंजी सर्वर पर सहेजी नहीं जाएगी। इस कारण से आपको अपना पासवर्ड किसी सुरक्षित स्थान पर संग्रहीत करना होगा। आपको इसे अन्य डिवाइसों पर दर्ज करना होगा जिन पर आप e2e एन्क्रिप्शन का उपयोग करना चाहते हैं।", + "E2E_key_reset_email": "E2E कुंजी रीसेट अधिसूचना", + "E2E_message_encrypted_placeholder": "यह संदेश एंड-टू-एंड एन्क्रिप्टेड है. इसे देखने के लिए, आपको अपनी खाता सेटिंग में अपनी एन्क्रिप्शन कुंजी दर्ज करनी होगी।", + "E2E_password_request_text": "अपने एन्क्रिप्टेड निजी समूहों और सीधे संदेशों तक पहुंचने के लिए, अपना एन्क्रिप्शन पासवर्ड दर्ज करें।
    आपके द्वारा उपयोग किए जाने वाले प्रत्येक क्लाइंट पर अपने संदेशों को एनकोड/डीकोड करने के लिए आपको यह पासवर्ड दर्ज करना होगा, क्योंकि कुंजी सर्वर पर संग्रहीत नहीं है।", + "E2E_password_reveal_text": "एंड-टू-एंड एन्क्रिप्शन के साथ सुरक्षित निजी कमरे और सीधे संदेश बनाएं।

    अपना पासवर्ड सुरक्षित रूप से सहेजें, क्योंकि आपके संदेशों को एन्कोड/डीकोड करने की कुंजी सर्वर पर सहेजी नहीं जाएगी। e2e एन्क्रिप्शन का उपयोग करने के लिए आपको इसे अन्य डिवाइस पर दर्ज करना होगा। और अधिक जानें

    अपना पासवर्ड किसी भी ब्राउज़र से, जिस पर आपने दर्ज किया है, कभी भी बदलें। इस संदेश को ख़ारिज करने से पहले अपना पासवर्ड संग्रहीत करना याद रखें।

    आपका पासवर्ड है: {{randomPassword}}", + "E2E_Reset_Email_Content": "आप स्वचालित रूप से लॉग आउट हो गए हैं. जब आप दोबारा लॉगिन करते हैं, तो Rocket.Chat एक नई कुंजी उत्पन्न करेगा और किसी भी एन्क्रिप्टेड कमरे तक आपकी पहुंच बहाल करेगा जिसमें एक या अधिक सदस्य ऑनलाइन हैं। E2E एन्क्रिप्शन की प्रकृति के कारण, Rocket.Chat किसी भी एन्क्रिप्टेड कमरे तक पहुंच बहाल करने में सक्षम नहीं होगा जिसमें कोई भी सदस्य ऑनलाइन नहीं है।", + "E2E_Reset_Key_Explanation": "यह विकल्प आपकी वर्तमान E2E कुंजी को हटा देगा और आपको लॉग आउट कर देगा।
    जब आप दोबारा लॉगिन करते हैं, तो Rocket.Chat आपके लिए एक नई कुंजी उत्पन्न करेगा और किसी भी एन्क्रिप्टेड कमरे तक आपकी पहुंच बहाल करेगा जिसमें एक या अधिक सदस्य ऑनलाइन हैं।
    E2E एन्क्रिप्शन की प्रकृति के कारण, Rocket.Chat किसी भी एन्क्रिप्टेड कमरे तक पहुंच बहाल करने में सक्षम नहीं होगा जिसमें कोई भी सदस्य ऑनलाइन नहीं है।", + "E2E_Reset_Other_Key_Warning": "वर्तमान E2E कुंजी को रीसेट करने से उपयोगकर्ता लॉग आउट हो जाएगा। जब उपयोगकर्ता दोबारा लॉगिन करेगा, तो Rocket.Chat एक नई कुंजी उत्पन्न करेगा और उपयोगकर्ता को किसी भी एन्क्रिप्टेड कमरे तक पहुंच बहाल करेगा जिसमें एक या अधिक सदस्य ऑनलाइन होंगे। E2E एन्क्रिप्शन की प्रकृति के कारण, Rocket.Chat किसी भी एन्क्रिप्टेड कमरे तक पहुंच बहाल करने में सक्षम नहीं होगा जिसमें कोई भी सदस्य ऑनलाइन नहीं है।", + "E2E_unavailable_for_federation": "E2E फ़ेडरेटेड कमरों के लिए उपलब्ध नहीं है", + "ECDH_Enabled": "डेटा परिवहन के लिए दूसरी परत एन्क्रिप्शन सक्षम करें", + "Edit": "संपादन करना", + "Edit_Business_Hour": "व्यावसायिक समय संपादित करें", + "Edit_Canned_Response": "डिब्बाबंद प्रतिक्रिया संपादित करें", + "Edit_Canned_Responses": "डिब्बाबंद प्रतिक्रियाएँ संपादित करें", + "Edit_Custom_Field": "कस्टम फ़ील्ड संपादित करें", + "Edit_Department": "विभाग संपादित करें", + "Edit_Federated_User_Not_Allowed": "फ़ेडरेटेड उपयोगकर्ता को संपादित करना संभव नहीं है", + "Message_AllowSnippeting": "संदेश स्निपेटिंग की अनुमति दें", + "Edit_Invite": "आमंत्रण संपादित करें", + "Edit_previous_message": "`%s` - पिछला संदेश संपादित करें", + "Edit_Priority": "प्राथमिकता संपादित करें", + "Edit_SLA_Policy": "SLA नीति संपादित करें", "Edit_Status": "स्थिति संपादित करें", + "Edit_Tag": "टैग संपादित करें", + "Edit_Trigger": "ट्रिगर संपादित करें", + "Edit_Unit": "इकाई संपादित करें", + "Message_Attachments_GroupAttach": "समूह अनुलग्नक बटन", + "Message_Attachments_GroupAttachDescription": "यह आइकनों को एक विस्तार योग्य मेनू के अंतर्गत समूहित करता है। कम स्क्रीन स्पेस लेता है.", + "Edit_User": "यूजर को संपादित करो", + "edit-livechat-room-customfields": "लाइवचैट रूम कस्टम फ़ील्ड संपादित करें", + "edit-livechat-room-customfields_description": "लाइवचैट रूम के कस्टम फ़ील्ड को संपादित करने की अनुमति", + "edit-message": "संदेश संपादित करें", + "edit-message_description": "एक कमरे के भीतर किसी संदेश को संपादित करने की अनुमति", + "edit-other-user-active-status": "अन्य उपयोगकर्ता सक्रिय स्थिति संपादित करें", + "edit-other-user-active-status_description": "अन्य खातों को सक्षम या अक्षम करने की अनुमति", + "edit-other-user-avatar": "अन्य उपयोगकर्ता अवतार संपादित करें", + "edit-other-user-avatar_description": "अन्य उपयोगकर्ता का अवतार बदलने की अनुमति.", + "edit-other-user-e2ee": "अन्य उपयोगकर्ता E2E एन्क्रिप्शन संपादित करें", + "edit-other-user-e2ee_description": "अन्य उपयोगकर्ता के E2E एन्क्रिप्शन को संशोधित करने की अनुमति।", + "edit-other-user-info": "अन्य उपयोगकर्ता जानकारी संपादित करें", + "edit-other-user-info_description": "अन्य उपयोगकर्ता का नाम, उपयोगकर्ता नाम या ईमेल पता बदलने की अनुमति।", + "edit-other-user-password": "अन्य उपयोगकर्ता पासवर्ड संपादित करें", + "edit-other-user-password_description": "अन्य उपयोगकर्ता के पासवर्ड को संशोधित करने की अनुमति। अन्य-उपयोगकर्ता-जानकारी संपादित करने की अनुमति की आवश्यकता है।", + "edit-other-user-totp": "अन्य उपयोगकर्ता दो कारक TOTP संपादित करें", + "edit-other-user-totp_description": "अन्य उपयोगकर्ता के टू फैक्टर टीओटीपी को संपादित करने की अनुमति", + "edit-privileged-setting": "विशेषाधिकार प्राप्त सेटिंग संपादित करें", + "edit-privileged-setting_description": "सेटिंग्स संपादित करने की अनुमति", + "edit-team": "टीम संपादित करें", + "edit-team_description": "टीमों को संपादित करने की अनुमति", + "edit-team-channel": "टीम चैनल संपादित करें", + "edit-team-channel_description": "किसी टीम के चैनल को संपादित करने की अनुमति", + "edit-team-member": "टीम सदस्य संपादित करें", + "edit-team-member_description": "किसी टीम के सदस्यों को संपादित करने की अनुमति", + "edit-room": "कक्ष संपादित करें", + "edit-room_description": "किसी कमरे का नाम, विषय, प्रकार (निजी या सार्वजनिक स्थिति) और स्थिति (सक्रिय या संग्रहीत) संपादित करने की अनुमति", + "edit-room-avatar": "कक्ष अवतार संपादित करें", + "edit-room-avatar_description": "किसी कमरे का अवतार संपादित करने की अनुमति.", + "edit-room-retention-policy": "कक्ष की अवधारण नीति संपादित करें", + "edit-room-retention-policy_description": "किसी कमरे की अवधारण नीति को संपादित करने, उसमें मौजूद संदेशों को स्वचालित रूप से हटाने की अनुमति", + "edit-omnichannel-contact": "ओमनीचैनल संपर्क संपादित करें", + "Use_Legacy_Message_Template": "लीगेसी संदेश टेम्पलेट का उपयोग करें", + "multi_line": "मल्टी लाइन", + "edit-omnichannel-contact_description": "ओमनीचैनल संपर्क को संपादित करने की अनुमति", + "Edit_Contact_Profile": "संपर्क प्रोफ़ाइल संपादित करें", + "edited": "संपादित", + "Editing_room": "संपादन कक्ष", + "Editing_user": "उपयोगकर्ता का संपादन", + "Editor": "संपादक", + "Message_ShowEditedStatus": "संपादित स्थिति दिखाएँ", + "Education": "शिक्षा", + "Message_ShowFormattingTips": "फ़ॉर्मेटिंग युक्तियाँ दिखाएँ", + "Email": "ईमेल", + "Email_Description": "Rocket.Chat के अंदर से प्रसारण ईमेल भेजने के लिए कॉन्फ़िगरेशन।", + "Email_address_to_send_offline_messages": "ऑफ़लाइन संदेश भेजने के लिए ईमेल पता", + "Email_already_exists": "ईमेल पहले से ही मौजूद है", + "Email_body": "ईमेल बॉडी", + "Email_Change_Disabled": "आपके Rocket.Chat व्यवस्थापक ने ईमेल बदलना अक्षम कर दिया है", + "Email_Changed_Description": "आप निम्नलिखित प्लेसहोल्डर्स का उपयोग कर सकते हैं:\n - `[ईमेल]` उपयोगकर्ता के ईमेल के लिए।\n- एप्लिकेशन नाम और यूआरएल के लिए क्रमशः `[Site_Name]` और `[Site_URL]`।", + "Email_Changed_Email_Subject": "[साइट_नाम] - ईमेल पता बदल दिया गया है", + "Email_changed_section": "ईमेल पता बदल गया", + "Email_Footer_Description": "आप निम्नलिखित प्लेसहोल्डर्स का उपयोग कर सकते हैं:\n - एप्लिकेशन नाम और यूआरएल के लिए क्रमशः `[Site_Name]` और `[Site_URL]`।", + "Email_from": "से", + "Email_Header_Description": "आप निम्नलिखित प्लेसहोल्डर्स का उपयोग कर सकते हैं:\n - एप्लिकेशन नाम और यूआरएल के लिए क्रमशः `[Site_Name]` और `[Site_URL]`।", + "Email_Inbox": "ईमेल इनबॉक्स", + "Email_Inboxes": "ईमेल इनबॉक्स", + "Email_Inbox_has_been_added": "ईमेल इनबॉक्स जोड़ा गया है", + "Email_Inbox_has_been_removed": "ईमेल इनबॉक्स हटा दिया गया है", + "Email_Notification_Mode": "ऑफ़लाइन ईमेल सूचनाएं", + "Email_Notification_Mode_All": "प्रत्येक उल्लेख/डीएम", "Email_Notification_Mode_Disabled": "उपयोग करने की अनुमति नहीं है", + "Email_notification_show_message": "ईमेल अधिसूचना में संदेश दिखाएँ", + "Email_Notifications_Change_Disabled": "आपके Rocket.Chat व्यवस्थापक ने ईमेल सूचनाएं अक्षम कर दी हैं", + "Email_or_username": "ईमेल या उपयोगकर्ता का नाम", + "Email_Placeholder": "कृपया अपना ईमेल एड्रेस इंटर करें...", + "Email_Placeholder_any": "कृपया ईमेल पते दर्ज करें...", + "email_plain_text_only": "केवल सादा पाठ ईमेल भेजें", + "email_style_description": "नेस्टेड चयनकर्ताओं से बचें", + "email_style_label": "ईमेल शैली", + "Email_subject": "ईमेल विषय", + "Email_verified": "ईमेल सत्यापित हुआ", + "Email_sent": "ईमेल भेजा", + "Emoji": "इमोजी", + "Emoji_picker": "इमोजी पिकर", + "EmojiCustomFilesystem": "कस्टम इमोजी फ़ाइल सिस्टम", + "EmojiCustomFilesystem_Description": "निर्दिष्ट करें कि इमोजी कैसे संग्रहीत किए जाते हैं।", + "Empty_no_agent_selected": "खाली, कोई एजेंट चयनित नहीं", + "Empty_title": "ख़ाली शीर्षक", "Enable": "सक्षम करें", + "Enable_Auto_Away": "ऑटो अवे सक्षम करें", + "Enable_CSP": "सामग्री-सुरक्षा-नीति सक्षम करें", + "Enable_CSP_Description": "इस विकल्प को तब तक अक्षम न करें जब तक आपके पास कोई कस्टम बिल्ड न हो और इनलाइन-स्क्रिप्ट के कारण समस्याएँ न आ रही हों", + "Extra_CSP_Domains": "अतिरिक्त सीएसपी डोमेन", + "Extra_CSP_Domains_Description": "सामग्री-सुरक्षा-नीति में जोड़ने के लिए अतिरिक्त डोमेन", + "Enable_Desktop_Notifications": "डेस्कटॉप सूचनाएं सक्षम करें", + "Enable_inquiry_fetch_by_stream": "स्ट्रीम का उपयोग करके सर्वर से पूछताछ डेटा लाने में सक्षम करें", + "Enable_omnichannel_auto_close_abandoned_rooms": "आगंतुक द्वारा छोड़े गए कमरों को स्वचालित रूप से बंद करने में सक्षम करें", + "Enable_Password_History": "पासवर्ड इतिहास सक्षम करें", + "Enable_Password_History_Description": "सक्षम होने पर, उपयोगकर्ता अपने पासवर्ड को अपने हाल ही में उपयोग किए गए कुछ पासवर्डों में अपडेट नहीं कर पाएंगे।", + "Enable_Svg_Favicon": "एसवीजी फ़ेविकॉन सक्षम करें", + "Enable_two-factor_authentication": "TOTP के माध्यम से दो-कारक प्रमाणीकरण सक्षम करें", + "Enable_two-factor_authentication_email": "ईमेल के माध्यम से दो-कारक प्रमाणीकरण सक्षम करें", + "Enable_unlimited_apps": "असीमित ऐप्स सक्षम करें", "Enabled": "सक्रिय", + "Encrypted": "कूट रूप दिया गया", + "Encrypted_channel_Description": "एंड-टू-एंड एन्क्रिप्टेड चैनल। खोज एन्क्रिप्टेड चैनलों के साथ काम नहीं करेगी और सूचनाएं संदेश सामग्री नहीं दिखा सकती हैं।", + "Encrypted_key_title": "इस चैनल के लिए एंड-टू-एंड एन्क्रिप्शन अक्षम करने के लिए यहां क्लिक करें (e2ee-अनुमति की आवश्यकता है)", + "Encrypted_message": "एन्क्रिप्टेड संदेश", + "Encrypted_setting_changed_successfully": "एन्क्रिप्टेड सेटिंग सफलतापूर्वक बदल दी गई", + "Encrypted_not_available": "सार्वजनिक चैनलों के लिए उपलब्ध नहीं है", + "Encryption_key_saved_successfully": "आपकी एन्क्रिप्शन कुंजी सफलतापूर्वक सहेजी गई थी.", + "EncryptionKey_Change_Disabled": "आप अपनी एन्क्रिप्शन कुंजी के लिए पासवर्ड सेट नहीं कर सकते क्योंकि आपकी निजी कुंजी इस क्लाइंट पर मौजूद नहीं है। नया पासवर्ड सेट करने के लिए आपको अपने मौजूदा पासवर्ड का उपयोग करके अपनी निजी कुंजी लोड करनी होगी या किसी क्लाइंट का उपयोग करना होगा जहां कुंजी पहले से ही लोड है।", + "End": "अंत", + "End_suspicious_sessions": "किसी भी संदिग्ध सत्र को समाप्त करें", + "End_call": "कॉल समाप्त करें", + "End_conversation": "बातचीत समाप्त करें", + "Expand_view": "दृश्य का विस्तार करें", + "Explore": "अन्वेषण करना", + "Explore_marketplace": "बाज़ार का अन्वेषण करें", + "Explore_the_marketplace_to_find_awesome_apps": "Rocket.Chat के लिए शानदार ऐप्स ढूंढने के लिए बाज़ार का अन्वेषण करें", + "Export": "निर्यात", + "End_Call": "कॉल समाप्त करें", + "End_OTR": "ओटीआर समाप्त करें", + "Engagement": "सगाई", + "Engagement_Dashboard": "सगाई डैशबोर्ड", + "Enrich_your_workspace": "सहभागिता डैशबोर्ड के साथ अपने कार्यक्षेत्र परिप्रेक्ष्य को समृद्ध करें। अपने उपयोगकर्ताओं, संदेशों और चैनलों के बारे में व्यावहारिक उपयोग आंकड़ों का विश्लेषण करें। प्रीमियम योजनाओं में शामिल.", + "Ensure_secure_workspace_access": "कार्यस्थल तक सुरक्षित पहुंच सुनिश्चित करें", + "Enter": "प्रवेश करना", + "Enter_a_custom_message": "एक कस्टम संदेश दर्ज करें", + "Enter_a_department_name": "विभाग का नाम दर्ज करें", + "Enter_a_name": "नाम डालें", + "Enter_a_regex": "रेगेक्स दर्ज करें", + "Enter_a_room_name": "कमरे का नाम दर्ज करें", + "Enter_a_tag": "एक टैग दर्ज करें", + "Enter_a_username": "एक उपयोगकर्ता नाम दर्ज करें", + "Enter_Alternative": "वैकल्पिक मोड (एंटर + Ctrl/Alt/Shift/CMD के साथ भेजें)", + "Enter_authentication_code": "प्रमाणीकरण कोड दर्ज करें", + "Enter_Behaviour": "कुंजी व्यवहार दर्ज करें", + "Enter_Behaviour_Description": "यदि एंटर कुंजी एक संदेश भेजेगी या लाइन ब्रेक करेगी तो यह बदल जाएगा", + "Enter_E2E_password": "E2E पासवर्ड दर्ज करें", + "Enter_name_here": "यहां नाम दर्ज करें", + "Enter_Normal": "सामान्य मोड (एंटर के साथ भेजें)", + "Enter_to": "में दर्ज", + "Enter_your_E2E_password": "अपना E2E पासवर्ड दर्ज करें", + "Enter_your_password_to_delete_your_account": "अपना खाता हटाने के लिए अपना पासवर्ड दर्ज करें। इसे असंपादित नहीं किया जा सकता है।", + "Enter_your_username_to_delete_your_account": "अपना खाता हटाने के लिए अपना उपयोगकर्ता नाम दर्ज करें। इसे असंपादित नहीं किया जा सकता है।", + "Premium_capabilities": "प्रीमियम क्षमताएं", + "Premium_Departments_title": "ग्राहकों को कतार में लगाएं और एजेंट उत्पादकता में सुधार करें", + "Premium_Departments_description_upgrade": "समुदाय पर कार्यस्थान केवल एक विभाग बना सकते हैं। सीमाएं हटाने और अपने कार्यक्षेत्र को सुपरचार्ज करने के लिए प्रीमियम योजना में अपग्रेड करें।", + "Premium_Departments_description_free_trial": "समुदाय पर कार्यस्थान एक विभाग बना सकते हैं। अनेक विभाग बनाने के लिए आज ही निःशुल्क प्रीमियम परीक्षण प्रारंभ करें!", + "Premium_License": "प्रीमियम लाइसेंस", + "Premium_only": "केवल प्रीमियम", + "Entertainment": "मनोरंजन", + "Error": "गलती", + "Error_something_went_wrong": "उफ़! कुछ गलत हो गया। कृपया पृष्ठ पुनः लोड करें या किसी व्यवस्थापक से संपर्क करें।", + "Error_404": "त्रुटि 404", + "Error_changing_password": "पासवर्ड बदलने में त्रुटि", + "Error_loading_pages": "पेज लोड करने में त्रुटि", + "Error_login_blocked_for_ip": "इस आईपी के लिए लॉगिन अस्थायी रूप से अवरुद्ध कर दिया गया है", + "Error_login_blocked_for_user": "इस उपयोगकर्ता के लिए लॉगिन अस्थायी रूप से अवरुद्ध कर दिया गया है", + "Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances": "त्रुटि: रॉकेट.चैट को कई उदाहरणों में चलाने पर ओप्लॉग टेलिंग की आवश्यकता होती है", + "Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances_details": "कृपया सुनिश्चित करें कि आपका MongoDB रेप्लिकासेट मोड पर है और MONGO_OPLOG_URL पर्यावरण चर एप्लिकेशन सर्वर पर सही ढंग से परिभाषित है", + "Error_sending_livechat_offline_message": "ओमनीचैनल ऑफ़लाइन संदेश भेजने में त्रुटि", + "Error_sending_livechat_transcript": "ओमनीचैनल प्रतिलेख भेजने में त्रुटि", + "Error_Site_URL": "अमान्य साइट_यूआरएल", + "Error_Site_URL_description": "कृपया, अपनी \"साइट_यूआरएल\" सेटिंग अपडेट करें और अधिक जानकारी पाएं [यहां](https://go.rocket.chat/i/invalid-site-url)", + "error-action-not-allowed": "{{action}} की अनुमति नहीं है", + "error-agent-offline": "एजेंट ऑफ़लाइन है", + "error-agent-status-service-offline": "एजेंट की स्थिति ऑफ़लाइन है या ओमनीचैनल सेवा सक्रिय नहीं है", + "error-application-not-found": "अनुप्रयोग नहीं मिला", + "error-archived-duplicate-name": "'{{room_name}}' नाम से एक संग्रहीत चैनल है", + "error-avatar-invalid-url": "अमान्य अवतार URL: {{url}}", + "error-avatar-url-handling": "{{username}} के लिए URL ({{url}}) से अवतार सेटिंग को संभालते समय त्रुटि", + "error-business-hours-are-closed": "व्यावसायिक घंटे बंद हैं", + "error-business-hour-finish-time-before-start-time": "समाप्ति का समय प्रारंभ समय के बाद का होना चाहिए", + "error-business-hour-finish-time-equals-start-time": "प्रारंभ और समाप्ति का समय एक समान नहीं हो सकता", + "error-blocked-username": "{{field}} अवरुद्ध है और इसका उपयोग नहीं किया जा सकता!", + "error-canned-response-not-found": "डिब्बाबंद प्रतिक्रिया नहीं मिली", + "error-cannot-delete-app-user": "ऐप उपयोगकर्ता को हटाने की अनुमति नहीं है, इसे हटाने के लिए संबंधित ऐप को अनइंस्टॉल करें।", + "error-cant-add-federated-users": "फ़ेडरेटेड उपयोगकर्ताओं को गैर-फ़ेडरेटेड रूम में नहीं जोड़ा जा सकता", + "error-cant-invite-for-direct-room": "उपयोगकर्ता को सीधे रूम में आमंत्रित नहीं किया जा सकता", + "error-channels-setdefault-is-same": "चैनल की डिफ़ॉल्ट सेटिंग वही है जिसमें इसे बदला जाएगा।", + "error-channels-setdefault-missing-default-param": "बॉडीपरम 'डिफ़ॉल्ट' आवश्यक है", + "error-could-not-change-email": "ईमेल नहीं बदला जा सका", + "error-could-not-change-name": "नाम नहीं बदला जा सका", + "error-could-not-change-username": "उपयोक्तानाम नहीं बदला जा सका", + "error-comment-is-required": "टिप्पणी आवश्यक है", + "error-custom-field-name-already-exists": "कस्टम फ़ील्ड नाम पहले से मौजूद है", + "error-delete-protected-role": "संरक्षित भूमिका को हटाया नहीं जा सकता", + "error-department-not-found": "विभाग नहीं मिला", + "error-department-removal-disabled": "विभाग निष्कासन प्रशासन द्वारा अक्षम कर दिया गया है, कृपया अपने व्यवस्थापक से संपर्क करें", + "error-direct-message-file-upload-not-allowed": "सीधे संदेशों में फ़ाइल साझाकरण की अनुमति नहीं है", + "error-duplicate-channel-name": "'{{channel_name}}' नाम से एक चैनल मौजूद है", + "error-duplicate-priority-name": "समान नाम वाली प्राथमिकता पहले से मौजूद है", + "error-edit-permissions-not-allowed": "संपादन अनुमति की अनुमति नहीं है", + "error-email-domain-blacklisted": "ईमेल डोमेन ब्लैकलिस्टेड है", + "error-email-body-not-initialized": "ईमेल का मुख्य भाग प्रारंभ नहीं किया गया. रिच ईमेल भेजने से पहले ईमेल सेटिंग्स पर ईमेल के हेडर और फुटर को सेटअप करें", + "error-email-send-failed": "ईमेल भेजने का प्रयास करने में त्रुटि: {{message}}", + "error-essential-app-disabled": "त्रुटि: एक Rocket.Chat ऐप जो इसके लिए आवश्यक है, अक्षम है। कृपया अपने व्यवस्थापक से संपर्क करें", + "error-failed-to-delete-department": "विभाग हटाने में विफल", + "error-field-unavailable": "{{field}} पहले से ही उपयोग में है :(", + "error-file-too-large": "फ़ाइल बहुत बड़ी है", + "error-forwarding-chat": "चैट अग्रेषित करते समय कुछ गलत हो गया, कृपया बाद में पुनः प्रयास करें।", + "error-forwarding-chat-same-department": "चयनित विभाग और वर्तमान कक्ष विभाग समान हैं", + "error-forwarding-department-target-not-allowed": "लक्ष्य विभाग को अग्रेषित करने की अनुमति नहीं है.", + "error-guests-cant-have-other-roles": "अतिथि उपयोगकर्ताओं की कोई अन्य भूमिका नहीं हो सकती.", + "error-import-file-extract-error": "आयात फ़ाइल निकालने में विफल.", + "error-import-file-is-empty": "आयातित फ़ाइल खाली प्रतीत होती है.", + "error-import-file-missing": "आयात की जाने वाली फ़ाइल निर्दिष्ट पथ पर नहीं मिली।", + "error-importer-not-defined": "आयातक को सही ढंग से परिभाषित नहीं किया गया था, इसमें आयात वर्ग गुम है।", + "error-input-is-not-a-valid-field": "{{input}} मान्य {{field}} नहीं है", + "error-insufficient-permission": "गलती! आपके पास इस ऑपरेशन को करने के लिए आवश्यक '{{permission}}' अनुमति नहीं है", + "error-inquiry-taken": "पूछताछ हो चुकी है", + "error-invalid-account": "अवैध खाता", + "error-invalid-actionlink": "अमान्य क्रिया लिंक", + "error-invalid-arguments": "अमान्य तर्क", + "error-invalid-asset": "अमान्य संपत्ति", + "error-invalid-channel": "अमान्य चैनल.", + "error-invalid-channel-start-with-chars": "अमान्य चैनल. @ या # से प्रारंभ करें", + "error-invalid-custom-field": "अमान्य कस्टम फ़ील्ड", + "error-invalid-custom-field-name": "अमान्य कस्टम फ़ील्ड नाम. केवल अक्षरों, संख्याओं, हाइफ़न और अंडरस्कोर का उपयोग करें।", + "error-invalid-custom-field-value": "{{field}} फ़ील्ड के लिए अमान्य मान", + "error-invalid-date": "अमान्य दिनांक प्रदान की गई.", + "error-invalid-dates": "दिनांक से दिनांक के बाद नहीं हो सकता", + "error-invalid-description": "अमान्य विवरण", + "error-invalid-domain": "अमान्य डोमेन", + "error-invalid-email": "अमान्य ईमेल {{email}}", + "error-invalid-email-address": "अमान्य ईमेल पता", + "error-invalid-email-inbox": "अमान्य ईमेल इनबॉक्स", + "error-email-inbox-not-found": "ईमेल इनबॉक्स नहीं मिला", + "error-invalid-file-height": "अमान्य फ़ाइल ऊंचाई", + "error-invalid-file-type": "अमान्य फ़ाइल प्रकार", + "error-invalid-file-width": "अमान्य फ़ाइल चौड़ाई", + "error-invalid-from-address": "आपने एक अमान्य FROM पता सूचित किया.", + "error-invalid-inquiry": "अमान्य पूछताछ", + "error-invalid-integration": "अमान्य एकीकरण", + "error-invalid-message": "अमान्य संदेश", + "error-invalid-method": "अमान्य विधि", + "error-invalid-name": "अमान्य नाम", + "error-invalid-password": "अवैध पासवर्ड", + "error-invalid-param": "अमान्य पैरामीटर", + "error-invalid-params": "अमान्य पैरामीटर", + "error-invalid-permission": "अमान्य अनुमति", + "error-invalid-port-number": "अमान्य पोर्ट नंबर", + "error-invalid-priority": "अमान्य प्राथमिकता", + "error-invalid-redirectUri": "अमान्य रीडायरेक्टयूरी", + "error-invalid-role": "अमान्य भूमिका", + "error-invalid-room": "अमान्य कमरा", + "error-invalid-room-name": "{{room_name}} कमरे का वैध नाम नहीं है", + "error-invalid-room-type": "{{type}} मान्य कमरे का प्रकार नहीं है।", + "error-invalid-settings": "अमान्य सेटिंग्स प्रदान की गईं", + "error-invalid-subscription": "अमान्य सदस्यता", + "error-invalid-token": "अमान्य टोकन", + "error-invalid-triggerWords": "अमान्य ट्रिगर शब्द", + "error-invalid-urls": "अमान्य यूआरएल", + "error-invalid-user": "अमान्य उपयोगकर्ता", + "error-invalid-username": "अमान्य उपयोगकर्ता नाम", + "error-invalid-value": "अमान्य मूल्य", + "error-invalid-webhook-response": "वेबहुक यूआरएल ने 200 के अलावा किसी अन्य स्थिति के साथ प्रतिक्रिया दी", + "error-license-user-limit-reached": "उपयोगकर्ताओं की अधिकतम संख्या तक पहुँच गया है.", + "error-logged-user-not-in-room": "आप `%s` कमरे में नहीं हैं", + "error-max-departments-number-reached": "आप अपने लाइसेंस द्वारा अनुमत विभागों की अधिकतम संख्या तक पहुँच गए। नए लाइसेंस के लिए sales@rocket.chat से संपर्क करें।", + "error-max-guests-number-reached": "आप अपने लाइसेंस द्वारा अनुमत अतिथि उपयोगकर्ताओं की अधिकतम संख्या तक पहुँच गए हैं। नए लाइसेंस के लिए sales@rocket.chat से संपर्क करें।", + "error-max-number-simultaneous-chats-reached": "प्रति एजेंट एक साथ चैट की अधिकतम संख्या तक पहुंच गई है।", + "error-max-rooms-per-guest-reached": "प्रति अतिथि कमरों की अधिकतम संख्या तक पहुँच गई है।", + "error-mac-limit-reached": "इस कार्यक्षेत्र के लिए मासिक सक्रिय संपर्कों की अधिकतम संख्या तक पहुंच गई है।", + "error-message-deleting-blocked": "संदेश हटाना अवरुद्ध है", + "error-message-editing-blocked": "संदेश संपादन अवरुद्ध है", + "error-message-size-exceeded": "संदेश का आकार Message_MaxAllowedSize से अधिक है", + "error-missing-unsubscribe-link": "आपको [सदस्यता समाप्त करें] लिंक प्रदान करना होगा।", + "error-no-tokens-for-this-user": "इस उपयोगकर्ता के लिए कोई टोकन नहीं हैं", + "error-no-agents-online-in-department": "विभाग में कोई एजेंट ऑनलाइन नहीं है", + "error-no-message-for-unread": "अपठित चिह्नित करने के लिए कोई संदेश नहीं हैं", + "error-not-allowed": "अनुमति नहीं", + "error-not-authorized": "अधिकृत नहीं हैं", + "error-office-hours-are-closed": "कार्यालय समय बंद है.", + "Estimated_due_time": "अनुमानित नियत समय", + "error-password-in-history": "दर्ज किया गया पासवर्ड पहले इस्तेमाल किया जा चुका है", + "error-password-policy-not-met": "पासवर्ड सर्वर की नीति के अनुरूप नहीं है", + "Estimated_due_time_in_minutes": "अनुमानित नियत समय (मिनटों में समय)", + "error-password-policy-not-met-maxLength": "पासवर्ड सर्वर की अधिकतम लंबाई की नीति के अनुरूप नहीं है (पासवर्ड बहुत लंबा है)", + "error-password-policy-not-met-minLength": "पासवर्ड सर्वर की न्यूनतम लंबाई की नीति को पूरा नहीं करता (पासवर्ड बहुत छोटा है)", + "error-password-policy-not-met-oneLowercase": "पासवर्ड सर्वर की कम से कम एक लोअरकेस वर्ण की नीति को पूरा नहीं करता है", + "error-password-policy-not-met-oneNumber": "पासवर्ड सर्वर की कम से कम एक संख्यात्मक वर्ण की नीति को पूरा नहीं करता है", + "error-password-policy-not-met-oneSpecial": "पासवर्ड सर्वर की कम से कम एक विशेष वर्ण की नीति को पूरा नहीं करता है", + "Please_go_to_the_Administration_page_then_Livechat_Facebook": "कृपया प्रशासन पृष्ठ पर जाएं, फिर ओमनीचैनल > फेसबुक पर जाएं", + "error-password-policy-not-met-oneUppercase": "पासवर्ड सर्वर की कम से कम एक बड़े अक्षर की नीति को पूरा नहीं करता है", + "error-password-policy-not-met-repeatingCharacters": "पासवर्ड सर्वर की वर्जित दोहराए जाने वाले वर्णों की नीति के अनुरूप नहीं है (आपके पास एक-दूसरे के बगल में समान वर्णों के बहुत सारे हैं)", + "error-password-same-as-current": "वर्तमान पासवर्ड के समान ही दर्ज किया गया पासवर्ड", + "error-personal-access-tokens-are-current-disabled": "व्यक्तिगत एक्सेस टोकन वर्तमान में अक्षम हैं", + "error-pinning-message": "संदेश पिन नहीं किया जा सका", + "error-push-disabled": "पुश अक्षम है", + "error-remove-last-owner": "यह आखिरी मालिक है. कृपया इसे हटाने से पहले एक नया स्वामी निर्धारित करें।", + "error-returning-inquiry": "पूछताछ को कतार में लौटाने में त्रुटि", + "error-role-in-use": "भूमिका को हटाया नहीं जा सकता क्योंकि यह उपयोग में है", + "error-role-name-required": "भूमिका का नाम आवश्यक है", + "error-room-does-not-exist": "यह कमरा मौजूद नहीं है", + "error-role-already-present": "इस नाम की एक भूमिका पहले से मौजूद है", + "error-room-already-closed": "कमरा पहले से ही बंद है", + "error-room-is-not-closed": "कमरा बंद नहीं है", + "error-room-onHold": "गलती! कमरा रुका हुआ है", + "error-room-is-already-on-hold": "गलती! कमरा पहले से ही होल्ड पर है", + "error-room-not-on-hold": "गलती! कमरा होल्ड पर नहीं है", + "error-selected-agent-room-agent-are-same": "चयनित एजेंट और रूम एजेंट समान हैं", + "error-starring-message": "संदेश को घूरा नहीं जा सका", + "error-tags-must-be-assigned-before-closing-chat": "चैट बंद करने से पहले टैग असाइन किया जाना चाहिए", + "error-the-field-is-required": "फ़ील्ड {{field}} आवश्यक है.", + "error-this-is-not-a-livechat-room": "यह एक ओमनीचैनल कक्ष नहीं है", + "error-this-is-a-premium-feature": "यह एक प्रीमियम फीचर से है", + "error-token-already-exists": "इस नाम का एक टोकन पहले से मौजूद है", + "error-token-does-not-exists": "टोकन मौजूद नहीं है", + "error-too-many-requests": "त्रुटि, बहुत सारे अनुरोध. कृप्या धीरें करो। दोबारा प्रयास करने से पहले आपको {{seconds}} सेकंड तक प्रतीक्षा करनी होगी।", + "error-transcript-already-requested": "प्रतिलिपि का अनुरोध पहले ही किया जा चुका है", + "error-unpinning-message": "संदेश अनपिन नहीं किया जा सका", + "error-user-deactivated": "उपयोगकर्ता सक्रिय नहीं है", + "error-user-has-no-roles": "उपयोगकर्ता की कोई भूमिका नहीं है", + "error-user-is-not-activated": "उपयोगकर्ता सक्रिय नहीं है", + "error-user-is-not-agent": "उपयोगकर्ता एक ओमनीचैनल एजेंट नहीं है", + "error-user-is-offline": "उपयोगकर्ता ऑफ़लाइन है", + "error-user-limit-exceeded": "आप जिन उपयोगकर्ताओं को #channel_name पर आमंत्रित करने का प्रयास कर रहे हैं, उनकी संख्या व्यवस्थापक द्वारा निर्धारित सीमा से अधिक है", + "error-user-not-belong-to-department": "उपयोगकर्ता इस विभाग से संबंधित नहीं है", + "error-user-not-in-room": "उपयोगकर्ता इस कमरे में नहीं है", + "error-user-registration-disabled": "उपयोगकर्ता पंजीकरण अक्षम है", + "error-user-registration-secret": "उपयोगकर्ता पंजीकरण की अनुमति केवल गुप्त यूआरएल के माध्यम से है", + "error-validating-department-chat-closing-tags": "जब विभाग को बातचीत बंद करने के लिए टैग की आवश्यकता होती है तो कम से कम एक समापन टैग की आवश्यकता होती है।", + "error-no-permission-team-channel": "आपको इस चैनल को टीम में जोड़ने की अनुमति नहीं है", + "error-no-owner-channel": "केवल मालिक ही इस चैनल को टीम में जोड़ सकते हैं", + "error-unable-to-update-priority": "प्राथमिकता अद्यतन करने में असमर्थ", + "error-you-are-last-owner": "आप आखिरी मालिक हैं. कृपया कमरा छोड़ने से पहले नए मालिक का चयन करें।", + "error-saving-sla": "SLA सहेजते समय एक त्रुटि उत्पन्न हुई", + "error-duplicated-sla": "समान नाम या नियत समय वाला एक SLA पहले से मौजूद है", + "error-cannot-place-chat-on-hold": "आप चैट को होल्ड पर नहीं रख सकते", + "error-contact-sent-last-message-so-cannot-place-on-hold": "जब संपर्क ने आखिरी संदेश भेज दिया हो तो आप चैट को होल्ड पर नहीं रख सकते", + "error-unserved-rooms-cannot-be-placed-onhold": "परोसे जाने से पहले कमरे को होल्ड पर नहीं रखा जा सकता", + "Workspace_exceeded_MAC_limit_disclaimer": "कार्यक्षेत्र सक्रिय संपर्कों की मासिक सीमा को पार कर गया है. इस समस्या के समाधान के लिए अपने कार्यक्षेत्र व्यवस्थापक से बात करें।", + "You_do_not_have_permission_to_do_this": "तुमको यह करने की इजाजत नहीं है", + "You_do_not_have_permission_to_execute_this_command": "आपके पास कमांड निष्पादित करने के लिए पर्याप्त अनुमतियाँ नहीं हैं: `/{{command}}`", + "You_have_reached_the_limit_active_costumers_this_month": "आप इस महीने सक्रिय ग्राहकों की सीमा तक पहुंच गए हैं", + "Errors_and_Warnings": "त्रुटियाँ और चेतावनियाँ", + "Esc_to": "Esc को", + "Estimated_wait_time": "अनुमानित प्रतीक्षा समय", + "Estimated_wait_time_in_minutes": "अनुमानित प्रतीक्षा समय (मिनटों में समय)", + "Event_notifications": "घटना सूचनाएं", + "Event_notifications_description": "इस सेटिंग को अक्षम करके आप ऐप को आगामी घटनाओं के बारे में सूचित करने से रोकेंगे।", + "Event_Trigger": "इवेंट ट्रिगर", + "Event_Trigger_Description": "चुनें कि किस प्रकार का ईवेंट इस आउटगोइंग वेबहुक इंटीग्रेशन को ट्रिगर करेगा", + "every_5_minutes": "हर 5 मिनट में एक बार", + "every_10_seconds": "हर 10 सेकंड में एक बार", + "every_30_seconds": "हर 30 सेकंड में एक बार", + "every_10_minutes": "हर 10 मिनट में एक बार", + "every_30_minutes": "हर 30 मिनट में एक बार", + "every_day": "हर दिन एक बार", + "every_hour": "हर घंटे में एक बार", + "every_minute": "हर मिनट में एक बार", + "every_second": "हर सेकंड एक बार", + "every_six_hours": "हर छह घंटे में एक बार", + "every_12_hours": "हर 12 घंटे में एक बार", + "every_24_hours": "हर 24 घंटे में एक बार", + "every_48_hours": "हर 48 घंटे में एक बार", + "Everyone_can_access_this_channel": "हर कोई इस चैनल तक पहुंच सकता है", + "Exact": "एकदम सही", + "Example_payload": "उदाहरण पेलोड", + "Example_s": "उदाहरण: %s", + "except_pinned": "(उन्हें छोड़कर जिन्हें पिन किया गया है)", + "Exclude_Botnames": "बॉट्स को बाहर निकालें", + "Exclude_Botnames_Description": "उन बॉट्स से संदेशों का प्रचार-प्रसार न करें जिनका नाम उपरोक्त रेगुलर एक्सप्रेशन से मेल खाता हो। यदि खाली छोड़ दिया जाए, तो बॉट्स के सभी संदेश प्रसारित हो जाएंगे।", + "Exclude_pinned": "पिन किए गए संदेशों को बाहर निकालें", + "Execute_Synchronization_Now": "अभी सिंक्रोनाइज़ेशन निष्पादित करें", + "Exit_Full_Screen": "पूर्ण स्क्रीन से बाहर निकलें", + "Expand": "बढ़ाना", + "Experimental_Feature_Alert": "यह एक प्रायोगिक सुविधा है! कृपया ध्यान रखें कि यह भविष्य में बिना किसी सूचना के बदल सकता है, टूट सकता है या हटाया भी जा सकता है।", + "Expired": "खत्म हो चुका", + "Expiration": "समय सीमा समाप्ति", + "Expiration_(Days)": "समाप्ति (दिन)", + "Export_as_file": "फ़ाइल के रूप में निर्यात करें", + "Export_Messages": "संदेश निर्यात करें", + "Export_My_Data": "मेरा डेटा निर्यात करें (JSON)", + "expression": "अभिव्यक्ति", + "Extended": "विस्तारित", + "Extensions": "एक्सटेंशन", + "Extension_Number": "विस्तारण क्रमांक", + "Extension_Status": "विस्तार स्थिति", + "External": "बाहरी", + "External_Domains": "बाहरी डोमेन", + "External_Queue_Service_URL": "बाहरी कतार सेवा यूआरएल", + "External_Service": "बाह्य सेवा", + "External_Users": "बाहरी उपयोगकर्ताओं", + "Extremely_likely": "बहुत ज्यादा संभावना", + "Facebook": "फेसबुक", + "Facebook_Page": "फेसबुक पेज", + "Failed": "असफल", + "Failed_to_activate_invite_token": "आमंत्रण टोकन सक्रिय करने में विफल", + "Failed_to_add_monitor": "मॉनिटर जोड़ने में विफल", + "Failed_To_Download_Files": "फ़ाइलें डाउनलोड करने में विफल", + "Failed_to_generate_invite_link": "आमंत्रण लिंक जनरेट करने में विफल", + "Failed_To_Load_Import_Data": "आयात डेटा लोड करने में विफल", + "Failed_To_Load_Import_History": "आयात इतिहास लोड करने में विफल", + "Failed_To_Load_Import_Operation": "आयात कार्रवाई लोड करने में विफल", + "Failed_To_Start_Import": "आयात कार्रवाई प्रारंभ करने में विफल", + "Failed_to_validate_invite_token": "आमंत्रण टोकन सत्यापित करने में विफल", + "Failure": "असफलता", + "False": "असत्य", + "Fallback_forward_department": "अग्रेषण के लिए फ़ॉलबैक विभाग", + "Fallback_forward_department_description": "आपको एक फ़ॉलबैक विभाग को परिभाषित करने की अनुमति देता है जो इस समय कोई ऑनलाइन एजेंट न होने की स्थिति में इस पर अग्रेषित चैट प्राप्त करेगा", + "Favorite": "पसंदीदा", + "Favorite_Rooms": "पसंदीदा कमरे सक्षम करें", + "Favorites": "पसंदीदा", + "Feature_preview": "फ़ीचर पूर्वावलोकन", + "Feature_preview_page_description": "फीचर पूर्वावलोकन पृष्ठ पर आपका स्वागत है! यहां, आप नवीनतम अत्याधुनिक सुविधाओं को सक्षम कर सकते हैं जो वर्तमान में विकास के अधीन हैं और अभी तक आधिकारिक तौर पर जारी नहीं की गई हैं।\n\nकृपया ध्यान दें कि ये कॉन्फ़िगरेशन अभी भी परीक्षण चरण में हैं और स्थिर या पूरी तरह कार्यात्मक नहीं हो सकते हैं।", + "featured": "प्रदर्शित", + "Featured": "प्रदर्शित", + "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "यह सुविधा प्रशासन सेटिंग्स (एडमिन -> वीडियो कॉन्फ्रेंस) से सक्षम होने के लिए उपरोक्त चयनित कॉल प्रदाता पर निर्भर करती है।", + "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "यह सुविधा सक्षम होने के लिए \"विज़िटर नेविगेशन इतिहास को संदेश के रूप में भेजें\" पर निर्भर करती है।", + "Feature_Limiting": "सुविधा सीमित करना", + "Features": "विशेषताएँ", + "Federation": "फेडरेशन", + "Federation_Description": "फ़ेडरेशन असीमित संख्या में कार्यस्थानों को एक-दूसरे के साथ संचार करने की अनुमति देता है।", + "Federation_Enable": "फ़ेडरेशन सक्षम करें", + "Federation_Example_matrix_server": "उदाहरण: मैट्रिक्स.ऑर्ग", + "Federation_Federated_room_search": "फ़ेडरेटेड कमरे की खोज", + "Federation_Public_key": "सार्वजनिक कुंजी", + "Federation_Search_federated_rooms": "फ़ेडरेटेड कमरे खोजें", + "Federation_slash_commands": "फेडरेशन का आदेश", + "FEDERATION_Discovery_Method": "खोज विधि", + "FEDERATION_Discovery_Method_Description": "आप अपने DNS रिकॉर्ड पर हब या SRV और TXT प्रविष्टि का उपयोग कर सकते हैं।", + "FEDERATION_Domain": "कार्यक्षेत्र", + "FEDERATION_Domain_Alert": "सुविधा सक्षम करने के बाद इसे न बदलें, हम अभी तक डोमेन परिवर्तनों को संभाल नहीं सकते हैं।", + "FEDERATION_Domain_Description": "वह डोमेन जोड़ें जिससे यह सर्वर लिंक होना चाहिए - उदाहरण के लिए: @rocket.chat.", + "FEDERATION_Enabled": "फेडरेशन समर्थन को एकीकृत करने का प्रयास।", + "FEDERATION_Enabled_Alert": "फेडरेशन सपोर्ट का कार्य प्रगति पर है। इस समय उत्पादन प्रणाली पर उपयोग की अनुशंसा नहीं की जाती है।", + "FEDERATION_Public_Key": "सार्वजनिक कुंजी", + "FEDERATION_Public_Key_Description": "यह वह कुंजी है जिसे आपको अपने साथियों के साथ साझा करने की आवश्यकता है।", + "FEDERATION_Status": "स्थिति", + "FEDERATION_Test_Setup": "परीक्षण व्यवस्था", + "FEDERATION_Test_Setup_Error": "आपके सेटअप का उपयोग करके आपका सर्वर नहीं मिल सका, कृपया अपनी सेटिंग्स की समीक्षा करें।", + "FEDERATION_Test_Setup_Success": "आपका फ़ेडरेशन सेटअप काम कर रहा है और अन्य सर्वर आपको ढूंढ सकते हैं!", + "Retry_Count": "count पुनः प्रयास करें", + "Federation_Matrix": "फेडरेशन V2", "Federation_Matrix_enabled": "सक्रिय", + "Federation_Matrix_Enabled_Alert": "मैट्रिक्स फेडरेशन समर्थन के बारे में अधिक जानकारी यहां पाई जा सकती है (किसी भी कॉन्फ़िगरेशन के बाद, परिवर्तनों को प्रभावी करने के लिए पुनः आरंभ करना आवश्यक है)", + "Federation_Matrix_Federated": "संघीय", + "Federation_Matrix_Federated_Description": "फ़ेडरेटेड रूम बनाकर आप न तो एन्क्रिप्शन सक्षम कर पाएंगे और न ही प्रसारण", + "Federation_Matrix_Federated_Description_disabled": "फ़ेडरेशन वर्तमान में इस कार्यक्षेत्र में अक्षम है.", + "Federation_Matrix_id": "ऐपसेवा आईडी", + "Federation_Matrix_hs_token": "होमसर्वर टोकन", + "Federation_Matrix_as_token": "ऐपसर्विस टोकन", + "Federation_Matrix_homeserver_url": "होमसर्वर यूआरएल", + "Federation_Matrix_homeserver_url_alert": "हम अपने फेडरेशन के साथ उपयोग करने के लिए एक नए, खाली होमसर्वर की अनुशंसा करते हैं", + "Federation_Matrix_homeserver_domain": "होमसर्वर डोमेन", + "Federation_Matrix_homeserver_domain_alert": "किसी भी उपयोगकर्ता को केवल रॉकेट.चैट के अलावा तीसरे पक्ष के ग्राहकों के साथ होमसर्वर से नहीं जुड़ना चाहिए", + "Federation_Matrix_bridge_url": "ब्रिज यूआरएल", + "Federation_Matrix_bridge_localpart": "ऐपसर्विस उपयोगकर्ता लोकलपार्ट", + "Federation_Matrix_registration_file": "पंजीकरण फ़ाइल", + "Federation_Matrix_registration_file_Alert": "महत्वपूर्ण: अल्पकालिक घटनाओं को सक्षम करने से सर्वर उन सभी सर्वरों से सभी उपयोगकर्ताओं की टाइपिंग स्थिति प्राप्त कर लेगा जिनसे आप जुड़े हुए हैं। इसे सक्षम करने के लिए, कृपया अपनी पंजीकरण फ़ाइल (.yaml फ़ाइल जिसे आप Rocket.Chat को पंजीकृत करने के लिए उपयोग कर रहे हैं) अपडेट करें। अपने होम सर्वर पर), निम्नलिखित जोड़ें:
    de.sorunome.msc2409.push_epheral: true", + "Federation_Matrix_error_applying_room_roles": "फ़ेडरेटेड नेटवर्क पर रूम भूमिकाएँ लागू करते समय कुछ गलत हो गया", + "Federation_Matrix_giving_same_permission_warning": "आप इस उपयोगकर्ता को अपने जैसे ही विशेषाधिकार दे रहे हैं, आप इस परिवर्तन को पूर्ववत नहीं कर पाएंगे। क्या आपकी आगे बढ़ने की इच्छा है?", + "Federation_Matrix_losing_privileges": "विशेषाधिकार खोना", + "Federation_Matrix_losing_privileges_warning": "आप इस कार्रवाई को पूर्ववत नहीं कर पाएंगे, क्योंकि आप स्वयं को पदावनत कर रहे हैं। यदि आप अंतिम विशेषाधिकार प्राप्त उपयोगकर्ता हैं तो आप यह विशेषाधिकार पुनः प्राप्त नहीं कर पाएंगे। क्या आप अब भी आगे बढ़ना चाहते हैं?", + "Federation_Matrix_not_allowed_to_change_moderator": "आपको मॉडरेटर बदलने की अनुमति नहीं है", + "Federation_Matrix_not_allowed_to_change_owner": "आपको स्वामी बदलने की अनुमति नहीं है", + "Federation_Matrix_join_public_rooms_is_premium": "फ़ेडरेटेड रूम से जुड़ें एक प्रीमियम सुविधा है", + "Federation_Matrix_max_size_of_public_rooms_users": "किसी दूरस्थ सर्वर में सार्वजनिक कक्ष से जुड़ने पर उपयोगकर्ताओं की अधिकतम संख्या", + "Federation_Matrix_max_size_of_public_rooms_users_desc": "किसी दूरस्थ सर्वर में सार्वजनिक कक्ष से जुड़ने पर अधिकतम उपयोगकर्ताओं की संख्या। अधिक उपयोगकर्ताओं वाले सार्वजनिक कमरों को शामिल होने वाले सार्वजनिक कमरों की सूची में नजरअंदाज कर दिया जाएगा।", + "Federation_Matrix_max_size_of_public_rooms_users_Alert": "ध्यान रखें, आप उपयोगकर्ताओं को शामिल होने के लिए जितना बड़ा कमरा देंगे, उस कमरे में शामिल होने में उतना ही अधिक समय लगेगा, साथ ही इसमें संसाधन की मात्रा भी उपयोग होगी। और पढ़ें", + "Field": "मैदान", + "Field_removed": "फ़ील्ड हटा दिया गया", + "Field_required": "आवश्यक क्षेत्र", + "File": "फ़ाइल", + "File_Downloads_Started": "फ़ाइल डाउनलोड प्रारंभ हो गए", + "File_exceeds_allowed_size_of_bytes": "फ़ाइल स्वीकृत आकार {{size}} से अधिक है।", + "File_name_Placeholder": "फ़ाइल ढूंढो...", + "File_not_allowed_direct_messages": "सीधे संदेशों में फ़ाइल साझाकरण की अनुमति नहीं है.", + "File_Path": "दस्तावेज पथ", + "file_pruned": "फ़ाइल की छँटाई की गई", + "File_removed_by_automatic_prune": "स्वचालित छँटाई द्वारा फ़ाइल हटा दी गई", + "File_removed_by_prune": "फ़ाइल को प्रून द्वारा हटा दिया गया", + "File_Type": "फाइल का प्रकार", + "File_type_is_not_accepted": "फ़ाइल प्रकार स्वीकार नहीं किया जाता है.", + "File_uploaded": "फ़ाइल अपलोड की गई", + "File_Upload_Disabled": "फ़ाइल अपलोड अक्षम किया गया", + "File_uploaded_successfully": "फ़ाइल सफलतापूर्वक अपलोड की गई", + "File_URL": "फ़ाइल यूआरएल", + "FileType": "फाइल का प्रकार", + "files": "फ़ाइलें", + "Files": "फ़ाइलें", + "Files_only": "केवल संलग्न फ़ाइलें हटाएँ, संदेश रखें", + "FileSize_Bytes": "{{fileSize}} बाइट्स", + "FileSize_KB": "{{fileSize}} केबी", + "FileSize_MB": "{{fileSize}} एमबी", + "FileUpload": "फाइल अपलोड", + "FileUpload_Description": "फ़ाइल अपलोड और भंडारण कॉन्फ़िगर करें.", + "FileUpload_Cannot_preview_file": "फ़ाइल का पूर्वावलोकन नहीं किया जा सकता", + "FileUpload_Disabled": "फ़ाइल अपलोड अक्षम हैं.", + "FileUpload_Enable_json_web_token_for_files": "फ़ाइल अपलोड करने के लिए Json वेब टोकन सुरक्षा सक्षम करें", + "FileUpload_Enable_json_web_token_for_files_description": "अपलोड की गई फ़ाइलों के यूआरएल में एक JWT जोड़ता है", + "FileUpload_Restrict_to_room_members": "फ़ाइलों को कमरों के सदस्यों तक ही सीमित रखें", + "FileUpload_Restrict_to_room_members_Description": "कमरों पर अपलोड की गई फ़ाइलों की पहुंच केवल कमरों के सदस्यों तक ही सीमित रखें", + "FileUpload_Enabled": "फ़ाइल अपलोड सक्षम", + "FileUpload_Enabled_Direct": "सीधे संदेशों में फ़ाइल अपलोड सक्षम", + "FileUpload_Error": "फ़ाइल अपलोड करने में त्रुटि", + "FileUpload_File_Empty": "फ़ाइल खाली", + "FileUpload_FileSystemPath": "सिस्टम पथ", + "FileUpload_GoogleStorage_AccessId": "Google संग्रहण एक्सेस आईडी", + "FileUpload_GoogleStorage_AccessId_Description": "एक्सेस आईडी आम तौर पर ईमेल प्रारूप में होती है, उदाहरण के लिए: \"`example-test@example.iam.gserviceaccount.com`\"", + "FileUpload_GoogleStorage_Bucket": "Google संग्रहण बकेट नाम", + "FileUpload_GoogleStorage_Bucket_Description": "बकेट का नाम जिस पर फ़ाइलें अपलोड की जानी चाहिए.", + "FileUpload_GoogleStorage_ProjectId": "प्रोजेक्ट आईडी", + "FileUpload_GoogleStorage_ProjectId_Description": "Google डेवलपर कंसोल से प्रोजेक्ट आईडी", + "FileUpload_GoogleStorage_Proxy_Avatars": "प्रॉक्सी अवतार", + "FileUpload_GoogleStorage_Proxy_Avatars_Description": "प्रॉक्सी अवतार फ़ाइल संपत्ति के यूआरएल तक सीधी पहुंच के बजाय आपके सर्वर के माध्यम से प्रसारित होती है", + "FileUpload_GoogleStorage_Proxy_Uploads": "प्रॉक्सी अपलोड", + "FileUpload_GoogleStorage_Proxy_Uploads_Description": "संपत्ति के यूआरएल तक सीधी पहुंच के बजाय आपके सर्वर के माध्यम से प्रॉक्सी अपलोड फ़ाइल ट्रांसमिशन", + "FileUpload_GoogleStorage_Secret": "गूगल स्टोरेज सीक्रेट", + "FileUpload_GoogleStorage_Secret_Description": "कृपया [इन निर्देशों](https://github.com/CulturalMe/meteor-slingshot#google-cloud) का पालन करें और परिणाम यहां पेस्ट करें।", + "FileUpload_json_web_token_secret_for_files": "फ़ाइल अपलोड JSON वेब टोकन रहस्य", + "FileUpload_json_web_token_secret_for_files_description": "फ़ाइल अपलोड JSON वेब टोकन सीक्रेट (प्रमाणीकरण के बिना अपलोड की गई फ़ाइलों तक पहुँचने में सक्षम होने के लिए उपयोग किया जाता है)", + "FileUpload_MaxFileSize": "अधिकतम फ़ाइल अपलोड आकार (बाइट्स में)", + "FileUpload_MaxFileSizeDescription": "फ़ाइल आकार की सीमा को हटाने के लिए इसे -1 पर सेट करें।", + "FileUpload_MediaType_NotAccepted__type__": "मीडिया प्रकार स्वीकृत नहीं: {{type}}", + "FileUpload_MediaType_NotAccepted": "मीडिया प्रकार स्वीकृत नहीं", + "FileUpload_MediaTypeBlackList": "अवरुद्ध मीडिया प्रकार", + "FileUpload_MediaTypeBlackListDescription": "मीडिया प्रकारों की अल्पविराम से अलग की गई सूची। इस सेटिंग को स्वीकृत मीडिया प्रकारों पर प्राथमिकता है।", + "FileUpload_MediaTypeWhiteList": "स्वीकृत मीडिया प्रकार", + "FileUpload_MediaTypeWhiteListDescription": "मीडिया प्रकारों की अल्पविराम से अलग की गई सूची। सभी मीडिया प्रकारों को स्वीकार करने के लिए इसे खाली छोड़ दें।", + "FileUpload_ProtectFiles": "अपलोड की गई फ़ाइलों को सुरक्षित रखें", + "FileUpload_ProtectFilesDescription": "केवल प्रमाणित उपयोगकर्ताओं को ही पहुंच प्राप्त होगी", + "FileUpload_ProtectFilesEnabled_JWTNotSet": "अपलोड की गई फ़ाइलें सुरक्षित हैं, लेकिन JWT एक्सेस सेटअप नहीं है, मीडिया संदेश भेजने के लिए ट्विलियो के लिए यह आवश्यक है। सेटिंग्स में सेटअप -> फ़ाइल अपलोड करें", + "FileUpload_RotateImages": "अपलोड पर छवियाँ घुमाएँ", + "FileUpload_RotateImages_Description": "इस सेटिंग को सक्षम करने से छवि गुणवत्ता हानि हो सकती है", + "FileUpload_S3_Acl": "एसीएल", + "FileUpload_S3_AWSAccessKeyId": "प्रवेश की चाबी", + "FileUpload_S3_AWSSecretAccessKey": "गुप्त कुंजी", + "FileUpload_S3_Bucket": "बाल्टी का नाम", + "FileUpload_S3_BucketURL": "बकेट यूआरएल", + "FileUpload_S3_CDN": "डाउनलोड के लिए सीडीएन डोमेन", + "FileUpload_S3_ForcePathStyle": "बल पथ शैली", + "FileUpload_S3_Proxy_Avatars": "प्रॉक्सी अवतार", + "FileUpload_S3_Proxy_Avatars_Description": "प्रॉक्सी अवतार फ़ाइल संपत्ति के यूआरएल तक सीधी पहुंच के बजाय आपके सर्वर के माध्यम से प्रसारित होती है", + "FileUpload_S3_Proxy_Uploads": "प्रॉक्सी अपलोड", + "FileUpload_S3_Proxy_Uploads_Description": "संपत्ति के यूआरएल तक सीधी पहुंच के बजाय आपके सर्वर के माध्यम से प्रॉक्सी अपलोड फ़ाइल ट्रांसमिशन", + "FileUpload_S3_Region": "क्षेत्र", + "FileUpload_S3_SignatureVersion": "हस्ताक्षर संस्करण", + "FileUpload_S3_URLExpiryTimeSpan": "यूआरएल समाप्ति समय period", + "FileUpload_S3_URLExpiryTimeSpan_Description": "वह समय जिसके बाद Amazon S3 द्वारा जेनरेट किए गए URL मान्य नहीं होंगे (सेकंड में)। यदि 5 सेकंड से कम पर सेट किया जाता है, तो इस फ़ील्ड को अनदेखा कर दिया जाएगा।", + "FileUpload_Storage_Type": "भण्डारण प्रकार", + "FileUpload_Webdav_Password": "वेबडीएवी पासवर्ड", + "FileUpload_Webdav_Proxy_Avatars": "प्रॉक्सी अवतार", + "FileUpload_Webdav_Proxy_Avatars_Description": "प्रॉक्सी अवतार फ़ाइल संपत्ति के यूआरएल तक सीधी पहुंच के बजाय आपके सर्वर के माध्यम से प्रसारित होती है", + "FileUpload_Webdav_Proxy_Uploads": "प्रॉक्सी अपलोड", + "FileUpload_Webdav_Proxy_Uploads_Description": "संपत्ति के यूआरएल तक सीधी पहुंच के बजाय आपके सर्वर के माध्यम से प्रॉक्सी अपलोड फ़ाइल ट्रांसमिशन", + "FileUpload_Webdav_Server_URL": "WebDAV सर्वर एक्सेस यूआरएल", + "FileUpload_Webdav_Upload_Folder_Path": "फ़ोल्डर पथ अपलोड करें", + "FileUpload_Webdav_Upload_Folder_Path_Description": "WebDAV फ़ोल्डर पथ जिस पर फ़ाइलें अपलोड की जानी चाहिए", + "FileUpload_Webdav_Username": "वेबडीएवी उपयोगकर्ता नाम", + "Filter": "फ़िल्टर", + "Filter_by_category": "श्रेणी के अनुसार फ़िल्टर करें", + "Filter_by_Custom_Fields": "कस्टम फ़ील्ड द्वारा फ़िल्टर करें", + "Filter_By_Price": "कीमत के अनुसार फ़िल्टर करें", + "Filter_By_Status": "स्थिति के अनुसार फ़िल्टर करें", "Filters": "फिल्टर", + "Filters_applied": "फ़िल्टर लागू किए गए", + "Financial_Services": "वित्तीय सेवाएं", + "Finish": "खत्म करना", + "Finish_Registration": "पंजीकरण समाप्त करें", + "First_Channel_After_Login": "लॉगिन के बाद पहला चैनल", + "First_response_time": "प्रथम प्रतिक्रिया समय", + "Flags": "झंडे", + "Follow_message": "संदेश का पालन करें", + "Follow_social_profiles": "हमारे सामाजिक प्रोफाइल का अनुसरण करें, हमें जीथब पर फोर्क करें और हमारे ट्रेलो बोर्ड पर रॉकेट.चैट ऐप के बारे में अपने विचार साझा करें।", + "Following": "अगले", + "Fonts": "फोंट्स", + "Food_and_Drink": "भोजन पेय", + "Footer": "फ़ुटबाल", + "Footer_Direct_Reply": "प्रत्यक्ष उत्तर सक्षम होने पर पादलेख", + "For_more_details_please_check_our_docs": "अधिक जानकारी के लिए कृपया हमारे दस्तावेज़ देखें।", + "For_your_security_you_must_enter_your_current_password_to_continue": "आपकी सुरक्षा के लिए, जारी रखने के लिए आपको अपना वर्तमान पासवर्ड दर्ज करना होगा", + "Force_Disable_OpLog_For_Cache": "कैश के लिए ओपलॉग को बलपूर्वक अक्षम करें", + "Force_Disable_OpLog_For_Cache_Description": "कैश उपलब्ध होने पर भी उसे सिंक करने के लिए OpLog का उपयोग नहीं किया जाएगा", + "Force_Screen_Lock": "बलपूर्वक स्क्रीन लॉक करें", + "Force_Screen_Lock_After": "इसके बाद फोर्स स्क्रीन लॉक करें", + "Force_Screen_Lock_After_description": "नवीनतम सत्र की समाप्ति के बाद दोबारा पासवर्ड का अनुरोध करने का समय, सेकंड में।", + "Force_Screen_Lock_description": "सक्षम होने पर, आप अपने उपयोगकर्ताओं को ऐप को अनलॉक करने के लिए पिन/बायोमेट्री/फेसआईडी का उपयोग करने के लिए बाध्य करेंगे।", + "Force_SSL": "एसएसएल को बाध्य करें", + "Force_SSL_Description": "*सावधान!* _Force SSL_ का उपयोग कभी भी रिवर्स प्रॉक्सी के साथ नहीं किया जाना चाहिए। यदि आपके पास रिवर्स प्रॉक्सी है, तो आपको वहां रीडायरेक्ट करना चाहिए। यह विकल्प हेरोकू जैसे परिनियोजन के लिए मौजूद है, जो रिवर्स प्रॉक्सी पर रीडायरेक्ट कॉन्फ़िगरेशन की अनुमति नहीं देता है।", + "Force_visitor_to_accept_data_processing_consent": "विज़िटर को डेटा प्रोसेसिंग सहमति स्वीकार करने के लिए बाध्य करें", + "Force_visitor_to_accept_data_processing_consent_description": "आगंतुकों को सहमति के बिना चैटिंग शुरू करने की अनुमति नहीं है।", + "Force_visitor_to_accept_data_processing_consent_enabled_alert": "डेटा प्रोसेसिंग के साथ समझौता प्रोसेसिंग के कारण की पारदर्शी समझ पर आधारित होना चाहिए। इस वजह से, आपको नीचे दी गई सेटिंग भरनी होगी जो आपकी व्यक्तिगत जानकारी एकत्र करने और संसाधित करने के कारण बताने के लिए उपयोगकर्ताओं को प्रदर्शित की जाएगी।", + "force-delete-message": "संदेश को बलपूर्वक हटाएं", + "force-delete-message_description": "सभी प्रतिबंधों को दरकिनार करते हुए किसी संदेश को हटाने की अनुमति", + "Font_size": "फ़ॉन्ट आकार", + "Forgot_password": "अपना कूट शब्द भूल गए?", + "Forgot_Password_Description": "आप निम्नलिखित प्लेसहोल्डर्स का उपयोग कर सकते हैं:\n - पासवर्ड पुनर्प्राप्ति URL के लिए `[Forgot_Password_Url]`।\n - `[नाम]`, `[fname]`, `[lname]` क्रमशः उपयोगकर्ता के पूर्ण नाम, प्रथम नाम या अंतिम नाम के लिए।\n - `[ईमेल]` उपयोगकर्ता के ईमेल के लिए।\n - एप्लिकेशन नाम और यूआरएल के लिए क्रमशः `[Site_Name]` और `[Site_URL]`।", + "Forgot_Password_Email": "अपना पासवर्ड रीसेट करने के लिए यहां क्लिक करें।", + "Forgot_Password_Email_Subject": "[साइट_नाम] - पासवर्ड पुनर्प्राप्ति", + "Forgot_password_section": "पासवर्ड भूल गए", + "Format": "प्रारूप", + "Forward": "आगे", + "Forward_chat": "चैट अग्रेषित करें", + "Forward_message": "अग्रेषित संदेश", + "Forward_to_department": "विभाग को अग्रेषित करें", + "Forward_to_user": "उपयोगकर्ता को अग्रेषित करें", + "Forwarding": "अग्रेषित करना", + "Free": "मुक्त", + "Free_Extension_Numbers": "निःशुल्क एक्सटेंशन नंबर", + "Free_Apps": "मुक्त एप्लिकेशन्स", + "Frequently_Used": "बहुधा प्रयुक्त", + "Friday": "शुक्रवार", + "From": "से", + "From_Email": "ई - मेल से", + "From_email_warning": "चेतावनी : फ़ील्ड आपकी मेल सर्वर सेटिंग्स के अधीन है।", + "Full_Name": "पूरा नाम", + "Full_Screen": "पूर्ण स्क्रीन", + "Gaming": "जुआ", + "General": "सामान्य", + "General_Description": "सामान्य कार्यस्थान सेटिंग्स कॉन्फ़िगर करें.", + "General_Settings": "सामान्य सेटिंग्स", + "Generate_new_key": "एक नई कुंजी जनरेट करें", + "Generate_New_Link": "नया लिंक जनरेट करें", + "Generating_key": "कुंजी उत्पन्न करना", + "Copy_link": "लिंक की प्रतिलिपि करें", + "get-password-policy-forbidRepeatingCharacters": "पासवर्ड में दोहराए जाने वाले अक्षर नहीं होने चाहिए", + "get-password-policy-forbidRepeatingCharactersCount": "पासवर्ड में {{forbidRepeatingCharactersCount}} से अधिक दोहराव वाले अक्षर नहीं होने चाहिए", + "get-password-policy-maxLength": "पासवर्ड अधिकतम {{maxLength}} अक्षर लंबा होना चाहिए", + "get-password-policy-minLength": "पासवर्ड न्यूनतम {{minLength}} अक्षर लंबा होना चाहिए", + "get-password-policy-mustContainAtLeastOneLowercase": "पासवर्ड में कम से कम एक लोअरकेस अक्षर होना चाहिए", + "get-password-policy-mustContainAtLeastOneNumber": "पासवर्ड में कम से कम एक नंबर होना चाहिए", + "get-password-policy-mustContainAtLeastOneSpecialCharacter": "पासवर्ड में कम से कम एक विशेष अक्षर होना चाहिए", + "get-password-policy-mustContainAtLeastOneUppercase": "पासवर्ड में कम से कम एक बड़ा अक्षर होना चाहिए", + "get-password-policy-minLength-label": "कम से कम {{limit}} अक्षर", + "get-password-policy-maxLength-label": "अधिकतम {{limit}} अक्षर", + "get-password-policy-forbidRepeatingCharactersCount-label": "अधिकतम. {{limit}} दोहराए जाने वाले अक्षर", + "get-password-policy-mustContainAtLeastOneLowercase-label": "कम से कम एक छोटा अक्षर", + "get-password-policy-mustContainAtLeastOneUppercase-label": "कम से कम एक बड़ा अक्षर", + "get-password-policy-mustContainAtLeastOneNumber-label": "कम से कम एक नंबर", + "get-password-policy-mustContainAtLeastOneSpecialCharacter-label": "कम से कम एक प्रतीक", + "get-server-info": "सर्वर जानकारी प्राप्त करें", + "get-server-info_description": "सर्वर जानकारी प्राप्त करने की अनुमति", + "github_no_public_email": "आपके GitHub खाते में सार्वजनिक ईमेल के रूप में कोई ईमेल नहीं है", + "github_HEAD": "सिर", + "Give_a_unique_name_for_the_custom_oauth": "कस्टम OAuth के लिए एक अद्वितीय नाम दें", + "strike": "हड़ताल", + "Give_the_application_a_name_This_will_be_seen_by_your_users": "एप्लिकेशन को एक नाम दें. यह आपके उपयोगकर्ताओं को दिखाई देगा.", + "Global": "वैश्विक", + "Global Policy": "वैश्विक नीति", + "Global_purge_override_warning": "एक वैश्विक अवधारण नीति लागू है। यदि आप \"ओवरराइड ग्लोबल रिटेंशन पॉलिसी\" को बंद कर देते हैं, तो आप केवल वही पॉलिसी लागू कर सकते हैं जो ग्लोबल पॉलिसी से अधिक सख्त है।", + "Global_Search": "वैश्विक खोज", + "Go_to_your_workspace": "अपने कार्यस्थल पर जाएँ", + "Go_to_accessibility_and_appearance": "पहुंच और उपस्थिति पर जाएं", + "Google_Meet_Premium_only": "Google मीट (केवल प्रीमियम)", + "Google_Play": "गूगल प्ले", + "Hold_Call": "कॉल होल्ड करें", + "Hold_Call_Premium_only": "कॉल होल्ड करें (केवल प्रीमियम प्लान)", + "GoogleCloudStorage": "गूगल क्लाउड स्टोरेज", + "GoogleNaturalLanguage_ServiceAccount_Description": "सेवा खाता कुंजी JSON फ़ाइल. अधिक जानकारी [यहां] (https://cloud.google.com/प्राकृतिक-भाषा/docs/common/auth#set_up_a_service_account) पाई जा सकती है", + "GoogleTagManager_id": "Google टैग प्रबंधक आईडी", + "Got_it": "समझ गया", + "Government": "सरकार", + "Grandfathered_app": "दादाजी ऐप - ऐप सीमा में गिना जाता है लेकिन इस ऐप पर सीमा लागू नहीं होती है", + "Graphql_CORS": "ग्राफक्यूएल कॉर्स", + "Graphql_Enabled": "ग्राफक्यूएल सक्षम", + "Graphql_Subscription_Port": "ग्राफक्यूएल सदस्यता पोर्ट", + "Grid_view": "जालक दृश्य", + "Snippet_Messages": "स्निपेट संदेश", + "Group": "समूह", + "Group_by": "द्वारा समूह बनाएं", + "Group_by_Type": "प्रकार के अनुसार समूह बनाएं", + "snippet-message": "स्निपेट संदेश", + "snippet-message_description": "स्निपेट संदेश बनाने की अनुमति", + "Group_discussions": "समूह चर्चा", + "Group_favorites": "समूह पसंदीदा", + "Group_mentions_disabled_x_members": "समूह का उल्लेख है कि `@all` और `@here` को उन कमरों के लिए अक्षम कर दिया गया है जिनमें {{total}} से अधिक सदस्य हैं।", + "Group_mentions_only": "समूह का केवल उल्लेख है", + "Grouping": "समूहन", + "Guest": "अतिथि", + "Hash": "हैश", + "Header": "हैडर", + "Header_and_Footer": "शीर्षक और पृष्ठांक", + "Pharmaceutical": "फार्मास्युटिकल", + "Healthcare": "स्वास्थ्य देखभाल", + "Helpers": "सहायकों", + "Here_is_your_authentication_code": "यहां आपका प्रमाणीकरण कोड है:", + "Hex_Color_Preview": "हेक्स रंग पूर्वावलोकन", + "Hi": "नमस्ते", + "Hi_username": "नमस्ते [नाम]", + "Hidden": "छिपा हुआ", + "Hide": "छिपाना", + "Hide_counter": "काउंटर छुपाएं", + "Hide_flextab": "प्रासंगिक बार के बाहर क्लिक करके उसे छिपाएँ", + "Hide_Group_Warning": "क्या आप वाकई समूह \"%s\" को छिपाना चाहते हैं?", + "Hide_Livechat_Warning": "क्या आप वाकई \"%s\" के साथ चैट छिपाना चाहते हैं?", + "Hide_On_Workspace": "कार्यस्थल पर छुपें", + "Hide_Private_Warning": "क्या आप वाकई \"%s\" के साथ चर्चा छिपाना चाहते हैं?", + "Hide_roles": "भूमिकाएँ छिपाएँ", + "Hide_room": "छिपाना", + "Hide_Room_Warning": "क्या आप वाकई चैनल \"%s\" को छिपाना चाहते हैं?", + "Hide_System_Messages": "सिस्टम संदेश छिपाएँ", + "Hide_Unread_Room_Status": "अपठित कक्ष की स्थिति छिपाएँ", + "Hide_usernames": "उपयोक्तानाम छिपाएँ", + "Hide_video": "वीडियो छिपाएँ", + "High": "उच्च", + "Highest": "उच्चतम", + "Highlights": "हाइलाइट", + "Highlights_How_To": "जब कोई किसी शब्द या वाक्यांश का उल्लेख करता है तो उसे सूचित करने के लिए उसे यहां जोड़ें। आप शब्दों या वाक्यांशों को अल्पविराम से अलग कर सकते हैं। हाइलाइट शब्द केस संवेदी नहीं होते.", + "Highlights_List": "शब्दों को हाइलाइट करें", + "History": "इतिहास", + "Hold_Time": "समय पकड़", + "Hold": "पकड़ना", + "Hold_Premium_only": "होल्ड करें (केवल प्रीमियम योजनाएं)", "Home": "होम", + "Homepage": "मुखपृष्ठ", + "Homepage_Custom_Content_Default_Message": "व्यवस्थापक इस सफ़ेद स्थान में प्रस्तुत करने के लिए सामग्री html सम्मिलित कर सकते हैं।", + "Host": "मेज़बान", + "Hospitality_Businness": "खातिरदारी का व्यवसाय", + "hours": "घंटे", + "Hours": "घंटे", + "How_and_why_we_collect_usage_data": "उपयोग डेटा कैसे और क्यों एकत्र किया जाता है", "How_friendly_was_the_chat_agent": "चैट एजेंट कितना दोस्ताना था?", "How_knowledgeable_was_the_chat_agent": "चैट एजेंट कितना जानकार था?", + "How_long_to_wait_after_agent_goes_offline": "एजेंट के ऑफ़लाइन हो जाने के बाद कितनी देर तक प्रतीक्षा करनी होगी", + "How_long_to_wait_to_consider_visitor_abandonment": "आगंतुक परित्याग पर विचार करने के लिए कब तक प्रतीक्षा करनी होगी?", + "How_long_to_wait_to_consider_visitor_abandonment_in_seconds": "आगंतुक परित्याग पर विचार करने के लिए कब तक प्रतीक्षा करनी होगी?", "How_responsive_was_the_chat_agent": "चैट एजेंट कितना उत्तरदायी था?", "How_satisfied_were_you_with_this_chat": "आप इस चैट से कितने संतुष्ट थे?", + "How_to_handle_open_sessions_when_agent_goes_offline": "जब एजेंट ऑफ़लाइन हो जाए तो खुले सत्र को कैसे संभालें", + "Http_timeout": "HTTP टाइमआउट (मिलीसेकंड में)", + "Http_timeout_value": "5000", + "HTML": "एचटीएमएल", + "Icon": "आइकन", + "I_Saved_My_Password": "मैंने अपना पासवर्ड सहेज लिया", + "Idle_Time_Limit": "निष्क्रिय समय सीमा", + "Idle_Time_Limit_Description": "स्थिति बदलने तक की समयावधि। मान सेकंड में होना चाहिए.", + "if_they_are_from": "(यदि वे %s से हैं)", + "If_this_email_is_registered": "यदि यह ईमेल पंजीकृत है, तो हम आपका पासवर्ड रीसेट करने के तरीके पर निर्देश भेजेंगे। यदि आपको शीघ्र ही कोई ईमेल प्राप्त नहीं होता है, तो कृपया वापस आएं और पुनः प्रयास करें।", + "If_you_didnt_ask_for_reset_ignore_this_email": "यदि आपने अपना पासवर्ड रीसेट करने के लिए नहीं कहा है, तो आप इस ईमेल को अनदेखा कर सकते हैं।", + "If_you_didnt_try_to_login_in_your_account_please_ignore_this_email": "यदि आपने अपने खाते में लॉगिन करने का प्रयास नहीं किया है तो कृपया इस ईमेल को अनदेखा करें।", + "Iframe_Integration": "आईफ्रेम एकीकरण", + "Iframe_Integration_receive_enable": "प्राप्त करना सक्षम करें", + "Iframe_Integration_receive_enable_Description": "मूल विंडो को Rocket.Chat पर आदेश भेजने की अनुमति दें।", + "Iframe_Integration_receive_origin": "मूल प्राप्त करें", + "Iframe_Integration_receive_origin_Description": "प्रोटोकॉल उपसर्ग के साथ मूल, अल्पविराम द्वारा अलग किए गए, जिन्हें आदेश प्राप्त करने की अनुमति है जैसे। कहीं से भी प्राप्त करने की अनुमति देने के लिए `https://localhost, http://localhost`, या *।", + "Iframe_Integration_send_enable": "भेजें सक्षम करें", + "Iframe_Integration_send_enable_Description": "ईवेंट को मूल विंडो पर भेजें", + "Iframe_Integration_send_target_origin": "लक्ष्य उत्पत्ति भेजें", + "Iframe_Integration_send_target_origin_Description": "प्रोटोकॉल उपसर्ग के साथ उत्पत्ति, उदाहरण के लिए कौन से आदेश भेजे जाते हैं। `https://localhost`, या * कहीं भी भेजने की अनुमति देने के लिए।", + "Iframe_Restrict_Access": "किसी भी Iframe के अंदर पहुंच प्रतिबंधित करें", + "Iframe_Restrict_Access_Description": "यह सेटिंग किसी भी आईफ्रेम के अंदर आरसी को लोड करने के लिए प्रतिबंधों को सक्षम/अक्षम करती है", + "Iframe_X_Frame_Options": "एक्स-फ़्रेम-विकल्प के विकल्प", + "Iframe_X_Frame_Options_Description": "एक्स-फ़्रेम-विकल्प के विकल्प। [आप यहां सभी विकल्प देख सकते हैं।](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options#Syntax)", + "Ignore": "अनदेखा करना", + "Ignored": "अवहेलना करना", + "Ignore_Two_Factor_Authentication": "टू फैक्टर ऑथेंटिकेशन को नजरअंदाज करें", + "Images": "इमेजिस", + "IMAP_intercepter_already_running": "IMAP इंटरसेप्टर पहले से ही चल रहा है", + "IMAP_intercepter_Not_running": "IMAP इंटरसेप्टर नहीं चल रहा है", + "Impersonate_next_agent_from_queue": "कतार से अगले एजेंट का प्रतिरूपण करें", + "Impersonate_user": "उपयोगकर्ता का प्रतिरूपण करें", + "Impersonate_user_description": "सक्षम होने पर, एकीकरण उस उपयोगकर्ता के रूप में पोस्ट होता है जिसने एकीकरण को ट्रिगर किया है", + "Import": "आयात", + "Import_New_File": "नई फ़ाइल आयात करें", + "Import_requested_successfully": "आयात का सफलतापूर्वक अनुरोध किया गया", + "Import_Type": "आयात प्रकार", + "Importer_Archived": "संग्रहीत", + "Importer_CSV_Information": "CSV आयातक को एक विशिष्ट प्रारूप की आवश्यकता होती है, कृपया अपनी ज़िप फ़ाइल की संरचना कैसे करें, इसके लिए दस्तावेज़ पढ़ें:", + "Importer_done": "आयात पूरा हो गया!", + "Importer_ExternalUrl_Description": "आप सार्वजनिक रूप से पहुंच योग्य फ़ाइल के लिए URL का भी उपयोग कर सकते हैं:", + "Importer_finishing": "आयात समाप्त करना.", + "Importer_From_Description": "Rocket.Chat में {{from}} डेटा आयात करता है।", + "Importer_From_Description_CSV": "Rocket.Chat में CSV डेटा आयात करता है। अपलोड की गई फ़ाइल एक ज़िप फ़ाइल होनी चाहिए.", + "Importer_HipChatEnterprise_BetaWarning": "कृपया ध्यान रखें कि इस आयात पर अभी भी काम चल रहा है, कृपया GitHub में होने वाली किसी भी त्रुटि की रिपोर्ट करें:", + "Importer_HipChatEnterprise_Information": "अपलोड की गई फ़ाइल डिक्रिप्टेड tar.gz होनी चाहिए, कृपया अधिक जानकारी के लिए दस्तावेज़ पढ़ें:", + "Importer_import_cancelled": "आयात रद्द कर दिया गया.", + "Importer_import_failed": "आयात चलाते समय एक त्रुटि उत्पन्न हुई.", + "Importer_importing_channels": "चैनल आयात करना.", + "Importer_importing_files": "फ़ाइलें आयात करना.", + "Importer_importing_messages": "संदेश आयात करना.", + "Importer_importing_started": "आयात प्रारंभ करना.", + "Importer_importing_users": "उपयोगकर्ताओं को आयात करना.", + "Importer_not_in_progress": "आयातक वर्तमान में नहीं चल रहा है.", + "Importer_not_setup": "आयातक सही ढंग से सेटअप नहीं है, क्योंकि उसने कोई डेटा नहीं लौटाया।", + "Importer_Prepare_Restart_Import": "आयात पुनः प्रारंभ करें", + "Importer_Prepare_Start_Import": "आयात करना प्रारंभ करें", + "Importer_Prepare_Uncheck_Archived_Channels": "संग्रहीत चैनल अनचेक करें", + "Importer_Prepare_Uncheck_Deleted_Users": "हटाए गए उपयोगकर्ताओं को अनचेक करें", + "Importer_progress_error": "आयात के लिए प्रगति प्राप्त करने में विफल.", + "Importer_setup_error": "आयातक को सेट करते समय एक त्रुटि उत्पन्न हुई.", + "Importer_Slack_Users_CSV_Information": "अपलोड की गई फ़ाइल स्लैक की उपयोगकर्ता निर्यात फ़ाइल होनी चाहिए, जो एक CSV फ़ाइल है। अधिक जानकारी के लिए यहां देखें:", + "Importer_Source_File": "स्रोत फ़ाइल चयन", + "importer_status_done": "सफलतापूर्वक पूरा", + "importer_status_downloading_file": "फ़ाइल डाउनलोड हो रही है", + "importer_status_file_loaded": "फ़ाइल लोड की गई", + "importer_status_finishing": "लगभग हो गया", + "importer_status_import_cancelled": "रद्द", + "importer_status_import_failed": "गलती", + "importer_status_importing_channels": "चैनल आयात करना", + "importer_status_importing_files": "फ़ाइलें आयात करना", + "importer_status_importing_messages": "संदेश आयात करना", + "importer_status_importing_started": "डेटा आयात करना", + "importer_status_importing_users": "उपयोगकर्ताओं को आयात करना", + "importer_status_new": "शुरू नहीं", + "importer_status_preparing_channels": "चैनल फ़ाइल पढ़ना", + "importer_status_preparing_messages": "संदेश फ़ाइलें पढ़ना", + "importer_status_preparing_started": "फ़ाइलें पढ़ना", + "importer_status_preparing_users": "उपयोगकर्ता फ़ाइल पढ़ना", + "importer_status_uploading": "फ़ाइल अपलोड हो रही है", + "importer_status_user_selection": "क्या आयात करना है यह चुनने के लिए तैयार हैं", + "Importer_Upload_FileSize_Message": "आपकी सर्वर सेटिंग्स {{maxFileSize}} तक किसी भी आकार की फ़ाइलें अपलोड करने की अनुमति देती हैं।", + "Importer_Upload_Unlimited_FileSize": "आपकी सर्वर सेटिंग्स किसी भी आकार की फ़ाइलें अपलोड करने की अनुमति देती हैं।", + "Importing_channels": "चैनल आयात करना", + "Importing_Data": "डेटा आयात करना", + "Importing_messages": "संदेश आयात करना", + "Importing_users": "उपयोगकर्ताओं को आयात करना", + "Inactivity_Time": "निष्क्रियता का समय", + "In_progress": "प्रगति पर है", + "inbound-voip-calls": "इनबाउंड वीओआईपी कॉल", + "inbound-voip-calls_description": "इनबाउंड वीओआईपी कॉल की अनुमति", + "Inbox_Info": "इनबॉक्स जानकारी", + "Include_Offline_Agents": "ऑफ़लाइन एजेंटों को शामिल करें", + "Inclusive": "सहित", + "Incoming": "आने वाली", + "Incoming_call_from": "से आने वाली कॉल", + "Incoming_Livechats": "पंक्तिबद्ध चैट", + "Incoming_WebHook": "आने वाली वेबहुक", + "Industry": "उद्योग", + "Info": "जानकारी", + "initials_avatar": "प्रारंभिक अवतार", + "Inline_code": "इनलाइन कोड", + "Install": "स्थापित करना", + "Install_anyway": "फिर भी इंस्टॉल करें", + "Install_Extension": "एक्सटेंशन इंस्टॉल करें", + "Install_FxOs": "अपने फ़ायरफ़ॉक्स पर Rocket.Chat इंस्टॉल करें", + "Install_FxOs_done": "महान! अब आप अपने होमस्क्रीन पर आइकन के माध्यम से Rocket.Chat का उपयोग कर सकते हैं। रॉकेट.चैट के साथ आनंद लें!", + "Install_FxOs_error": "क्षमा करें, यह इच्छानुसार काम नहीं किया! निम्न त्रुटि दिखाई दी:", + "Install_FxOs_follow_instructions": "कृपया अपने डिवाइस पर ऐप इंस्टॉलेशन की पुष्टि करें (संकेत मिलने पर \"इंस्टॉल करें\" दबाएं)।", + "Installing": "स्थापित कर रहा है", + "Install_package": "पैकेज स्थापित करे", "Installation": "स्थापना", + "Installed": "स्थापित", + "Installed_at": "पर स्थापित किया गया", + "Instance": "उदाहरण", + "Instances": "उदाहरण", + "Instances_health": "उदाहरण स्वास्थ्य", + "Instance_Record": "उदाहरण रिकार्ड", + "Instructions": "निर्देश", + "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "अपने विज़िटर को संदेश भेजने के लिए फ़ॉर्म भरने के निर्देश", + "Insert_Contact_Name": "संपर्क नाम डालें", + "Insert_Placeholder": "प्लेसहोल्डर डालें", + "Install_rocket_chat_on_your_preferred_desktop_platform": "अपने पसंदीदा डेस्कटॉप प्लेटफ़ॉर्म पर Rocket.Chat इंस्टॉल करें।", + "Insurance": "बीमा", + "Integration_added": "एकीकरण जोड़ा गया है", + "Integration_Advanced_Settings": "एडवांस सेटिंग", + "Integration_Delete_Warning": "किसी एकीकरण को हटाना पूर्ववत नहीं किया जा सकता.", + "Integration_disabled": "एकीकरण अक्षम किया गया", + "Integration_History_Cleared": "एकीकरण इतिहास सफलतापूर्वक साफ़ किया गया", + "Integration_Incoming_WebHook": "आने वाली वेबहुक एकीकरण", + "Integration_New": "नया एकीकरण", + "integration-scripts-disabled": "एकीकरण स्क्रिप्ट अक्षम हैं", + "integration-scripts-isolated-vm-disabled": "\"सिक्योर सैंडबॉक्स\" का उपयोग नई या संशोधित स्क्रिप्ट पर नहीं किया जा सकता है।", + "integration-scripts-vm2-disabled": "\"संगत सैंडबॉक्स\" का उपयोग नई या संशोधित स्क्रिप्ट पर नहीं किया जा सकता है।", + "Integration_Outgoing_WebHook": "आउटगोइंग वेबहुक एकीकरण", + "Integration_Outgoing_WebHook_History": "आउटगोइंग वेबहुक एकीकरण इतिहास", + "Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger": "डेटा एकीकरण के लिए पारित किया गया", + "Integration_Outgoing_WebHook_History_Data_Passed_To_URL": "डेटा यूआरएल को भेजा गया", + "Integration_Outgoing_WebHook_History_Error_Stacktrace": "त्रुटि स्टैकट्रेस", + "Integration_Outgoing_WebHook_History_Http_Response": "HTTP प्रतिक्रिया", + "Integration_Outgoing_WebHook_History_Http_Response_Error": "HTTP प्रतिक्रिया त्रुटि", + "Integration_Outgoing_WebHook_History_Messages_Sent_From_Prepare_Script": "तैयारी चरण से भेजे गए संदेश", + "Integration_Outgoing_WebHook_History_Messages_Sent_From_Process_Script": "प्रक्रिया प्रतिक्रिया चरण से भेजे गए संदेश", + "Integration_Outgoing_WebHook_History_Time_Ended_Or_Error": "इसके समाप्त होने या त्रुटि होने का समय", + "Integration_Outgoing_WebHook_History_Time_Triggered": "समय एकीकरण ट्रिगर हुआ", + "Integration_Outgoing_WebHook_History_Trigger_Step": "अंतिम ट्रिगर चरण", + "Integration_Outgoing_WebHook_No_History": "इस निवर्तमान वेबहुक एकीकरण का अभी तक कोई इतिहास दर्ज नहीं किया गया है।", + "Integration_Retry_Count": "count पुनः प्रयास करें", + "Integration_Retry_Count_Description": "यदि यूआरएल पर कॉल विफल हो जाती है तो कितनी बार एकीकरण का प्रयास किया जाना चाहिए?", + "Integration_Retry_Delay": "विलंब पुनः प्रयास करें", + "Integration_Retry_Delay_Description": "पुनः प्रयास करने वालों को किस विलंब एल्गोरिदम का उपयोग करना चाहिए? 10 ^ x या 2 ^ x या x * 2", + "Integration_Retry_Failed_Url_Calls": "विफल यूआरएल कॉल पुनः प्रयास करें", + "Integration_Retry_Failed_Url_Calls_Description": "यदि यूआरएल पर कॉल आउट विफल रहता है तो क्या एकीकरण को उचित समय तक प्रयास करना चाहिए?", + "Integration_Run_When_Message_Is_Edited": "संपादनों पर चलाएँ", + "Integration_Run_When_Message_Is_Edited_Description": "क्या संदेश संपादित होने पर एकीकरण चलना चाहिए? इसे गलत पर सेट करने से एकीकरण केवल **नए** संदेशों पर चलेगा।", + "Integration_updated": "एकीकरण अद्यतन किया गया है.", + "Integration_Word_Trigger_Placement": "कहीं भी शब्द प्लेसमेंट", + "Integration_Word_Trigger_Placement_Description": "क्या शुरुआत के अलावा वाक्य में कहीं भी रखे जाने पर शब्द को ट्रिगर किया जाना चाहिए?", + "Integrations": "एकीकरण", + "Integrations_for_all_channels": "सभी सार्वजनिक चैनलों पर सुनने के लिए all_public_channels , सभी निजी समूहों पर सुनने के लिए all_private_groups , और सभी प्रत्यक्ष संदेशों को सुनने के लिए all_direct_messages दर्ज करें।", + "Integrations_Outgoing_Type_FileUploaded": "फ़ाइल अपलोड की गई", + "Integrations_Outgoing_Type_RoomArchived": "कक्ष संग्रहीत", + "Integrations_Outgoing_Type_RoomCreated": "कक्ष बनाया गया (सार्वजनिक और निजी)", + "Integrations_Outgoing_Type_RoomJoined": "उपयोगकर्ता से जुड़ा कक्ष", + "Integrations_Outgoing_Type_RoomLeft": "उपयोगकर्ता बायां कमरा", + "Integrations_Outgoing_Type_SendMessage": "संदेश भेजा गया", + "Integrations_Outgoing_Type_UserCreated": "उपयोगकर्ता बनाया गया", + "InternalHubot": "आंतरिक धारीदार", + "InternalHubot_EnableForChannels": "सार्वजनिक चैनलों के लिए सक्षम करें", + "InternalHubot_EnableForDirectMessages": "सीधे संदेशों के लिए सक्षम करें", + "InternalHubot_EnableForPrivateGroups": "निजी चैनलों के लिए सक्षम करें", + "InternalHubot_PathToLoadCustomScripts": "स्क्रिप्ट लोड करने के लिए फ़ोल्डर", + "InternalHubot_reload": "स्क्रिप्ट पुनः लोड करें", + "InternalHubot_ScriptsToLoad": "लोड करने के लिए स्क्रिप्ट", + "InternalHubot_ScriptsToLoad_Description": "कृपया अपने कस्टम फ़ोल्डर से लोड करने के लिए स्क्रिप्ट की अल्पविराम से अलग की गई सूची दर्ज करें", + "InternalHubot_Username_Description": "यह आपके सर्वर पर पंजीकृत बॉट का वैध उपयोगकर्ता नाम होना चाहिए।", + "Invalid Canned Response": "अमान्य डिब्बाबंद प्रतिक्रिया", + "Invalid_confirm_pass": "पासवर्ड पुष्टिकरण पासवर्ड से मेल नहीं खाता", + "Invalid_Department": "अमान्य विभाग", + "Invalid_email": "दर्ज किया गया ईमेल अमान्य है", + "Invalid_Export_File": "अपलोड की गई फ़ाइल वैध %s निर्यात फ़ाइल नहीं है.", + "Invalid_field": "फ़ील्ड ख़ाली नहीं होनी चाहिए", + "Invalid_Import_File_Type": "अमान्य आयात फ़ाइल प्रकार.", + "Invalid_name": "नाम खाली नहीं होना चाहिए", + "Invalid_notification_setting_s": "अमान्य अधिसूचना सेटिंग: %s", + "Invalid_OAuth_client": "अमान्य OAuth क्लाइंट", + "Invalid_or_expired_invite_token": "अमान्य या समाप्त आमंत्रण टोकन", + "Invalid_pass": "पासवर्ड खाली नहीं होना चाहिए", + "Invalid_password": "अवैध पासवर्ड", + "Invalid_reason": "शामिल होने का कारण खाली नहीं होना चाहिए", + "Invalid_room_name": "%s मान्य कमरे का नाम नहीं है", + "Invalid_secret_URL_message": "प्रदान किया गया यूआरएल अमान्य है.", + "Invalid_setting_s": "अमान्य सेटिंग: %s", + "Invalid_two_factor_code": "अमान्य दो कारक कोड", + "Invalid_username": "दर्ज किया गया उपयोक्तानाम अमान्य है", + "invisible": "अदृश्य", + "Invisible": "अदृश्य", + "Invitation": "आमंत्रण", + "Invitation_Email_Description": "आप निम्नलिखित प्लेसहोल्डर्स का उपयोग कर सकते हैं:\n - प्राप्तकर्ता ईमेल के लिए `[ईमेल]`।\n - एप्लिकेशन नाम और यूआरएल के लिए क्रमशः `[Site_Name]` और `[Site_URL]`।", + "Invitation_HTML": "आमंत्रण HTML", + "Invitation_HTML_Default": "

    आपको [Site_Name] पर आमंत्रित किया गया है

    [Site_URL] पर जाएँ और आज उपलब्ध सर्वोत्तम ओपन सोर्स चैट समाधान आज़माएँ!

    ", + "Invitation_Subject": "आमंत्रण विषय", + "Invitation_Subject_Default": "आपको [Site_Name] पर आमंत्रित किया गया है", + "Invite": "आमंत्रित करना", + "Invites": "आमंत्रण", + "Invite_and_add_members_to_this_workspace_to_start_communicating": "संचार शुरू करने के लिए इस कार्यक्षेत्र में सदस्यों को आमंत्रित करें और जोड़ें।", + "Invite_Link": "लिंक आमंत्रित करें", + "link": "जोड़ना", + "Invite_link_generated": "आमंत्रण लिंक जनरेट कर दिया गया है", + "Invite_removed": "आमंत्रण सफलतापूर्वक हटा दिया गया", + "Invite_user_to_join_channel": "इस चैनल से जुड़ने के लिए एक उपयोगकर्ता को आमंत्रित करें", + "Invite_user_to_join_channel_all_from": "इस चैनल से जुड़ने के लिए [#चैनल] के सभी उपयोगकर्ताओं को आमंत्रित करें", + "Invite_user_to_join_channel_all_to": "इस चैनल के सभी उपयोगकर्ताओं को [#चैनल] से जुड़ने के लिए आमंत्रित करें", + "Invite_Users": "सदस्यों को आमंत्रित करो", + "IP": "आई पी", + "IP_Address": "आईपी पता", + "IRC_Channel_Join": "JOIN कमांड का आउटपुट।", + "IRC_Channel_Leave": "पार्ट कमांड का आउटपुट।", + "IRC_Channel_Users": "NAMES कमांड का आउटपुट।", + "IRC_Channel_Users_End": "NAMES कमांड के आउटपुट का अंत।", + "IRC_Description": "इंटरनेट रिले चैट (आईआरसी) एक टेक्स्ट-आधारित समूह संचार उपकरण है। उपयोगकर्ता खुली चर्चा के लिए विशिष्ट रूप से नामित चैनलों या कमरों से जुड़ते हैं। आईआरसी व्यक्तिगत उपयोगकर्ताओं और फ़ाइल साझाकरण क्षमताओं के बीच निजी संदेशों का भी समर्थन करता है। यह पैकेज कार्यक्षमता की इन परतों को Rocket.Chat के साथ एकीकृत करता है।", + "IRC_Enabled": "आईआरसी समर्थन को एकीकृत करने का प्रयास। इस मान को बदलने के लिए Rocket.Chat को पुनः आरंभ करने की आवश्यकता है।", + "IRC_Enabled_Alert": "आईआरसी समर्थन का कार्य प्रगति पर है। इस समय उत्पादन प्रणाली पर उपयोग की अनुशंसा नहीं की जाती है।", + "IRC_Federation": "आईआरसी फेडरेशन", + "IRC_Federation_Description": "अन्य आईआरसी सर्वर से कनेक्ट करें।", + "IRC_Federation_Disabled": "आईआरसी फेडरेशन अक्षम है.", + "IRC_Hostname": "कनेक्ट करने के लिए आईआरसी होस्ट सर्वर।", + "IRC_Login_Fail": "आईआरसी सर्वर से कनेक्शन विफल होने पर आउटपुट।", + "IRC_Login_Success": "आईआरसी सर्वर से सफल कनेक्शन पर आउटपुट।", + "IRC_Message_Cache_Size": "आउटबाउंड संदेश प्रबंधन के लिए कैश सीमा।", + "IRC_Port": "आईआरसी होस्ट सर्वर पर बाइंड करने के लिए पोर्ट।", + "IRC_Private_Message": "PRIVMSG कमांड का आउटपुट।", + "IRC_Quit": "आईआरसी सत्र छोड़ने पर आउटपुट।", + "is_typing": "टाइप कर रहा है", + "Issue_Links": "ट्रैकर लिंक जारी करें", + "IssueLinks_Incompatible": "चेतावनी: इसे और 'हेक्स कलर प्रीव्यू' को एक ही समय में सक्षम न करें।", + "IssueLinks_LinkTemplate": "समस्या लिंक के लिए टेम्पलेट", + "IssueLinks_LinkTemplate_Description": "समस्या लिंक के लिए टेम्पलेट; %s को इश्यू नंबर से बदल दिया जाएगा.", + "It_Will_Hide_All_Other_Content_Blocks_In_The_Homepage": "यह मुखपृष्ठ में अन्य सभी सामग्री ब्लॉक छिपा देगा", + "It_Will_Show_All_Other_Content_Blocks_In_The_Homepage": "यह मुखपृष्ठ पर अन्य सभी सामग्री ब्लॉक दिखाएगा", + "It_works": "यह काम करता है", + "It_Security": "आईटी सुरक्षा", + "Italic": "तिरछा", + "italics": "तिर्छा", + "Items_per_page:": "आइटम प्रति पेज:", + "Jitsi_included_with_Community": "जित्सी, समुदाय के साथ शामिल", + "Job_Title": "नौकरी का नाम", + "Join": "जोड़ना", + "Join_with_password": "पासवर्ड के साथ जुड़ें", + "Join_audio_call": "ऑडियो कॉल में शामिल हों", + "Join_call": "कॉल में शामिल हों", + "Join_Chat": "चैट में शामिल हों", + "Join_conference": "सम्मेलन में शामिल हों", + "Join_default_channels": "डिफ़ॉल्ट चैनल से जुड़ें", + "Join_the_Community": "समुदाय में शामिल हों", + "Join_the_given_channel": "दिए गए चैनल से जुड़ें", + "Join_rooms": "कमरों से जुड़ें", + "Join_video_call": "वीडियो कॉल में शामिल हों", + "Join_my_room_to_start_the_video_call": "वीडियो कॉल शुरू करने के लिए मेरे कमरे से जुड़ें", + "join-without-join-code": "बिना जॉइन कोड के शामिल हों", + "join-without-join-code_description": "जॉइन कोड सक्षम वाले चैनलों में जॉइन कोड को बायपास करने की अनुमति", + "Joined": "में शामिल हो गए", + "joined": "में शामिल हो गए", + "Joined_at": "पर शामिल हुए", + "JSON": "JSON", + "Jump": "कूदना", + "Jump_to_first_unread": "पहले अपठित पर जाएँ", + "Jump_to_message": "संदेश पर जाएं", + "Jump_to_recent_messages": "हाल के संदेशों पर जाएँ", + "Just_invited_people_can_access_this_channel": "केवल आमंत्रित लोग ही इस चैनल तक पहुँच सकते हैं।", + "kick-user-from-any-c-room": "किसी भी सार्वजनिक चैनल से उपयोगकर्ता को लात मारो", + "kick-user-from-any-c-room_description": "किसी उपयोगकर्ता को किसी भी सार्वजनिक चैनल से बाहर निकालने की अनुमति", + "kick-user-from-any-p-room": "किसी भी निजी चैनल से उपयोगकर्ता को लात मारो", + "kick-user-from-any-p-room_description": "किसी उपयोगकर्ता को किसी निजी चैनल से बाहर निकालने की अनुमति", + "Katex_Dollar_Syntax": "डॉलर सिंटैक्स की अनुमति दें", + "Katex_Dollar_Syntax_Description": "$$katex ब्लॉक$$ और $inline katex$ सिंटैक्स का उपयोग करने की अनुमति दें", + "Katex_Enabled": "केटेक्स सक्षम", + "Katex_Enabled_Description": "संदेशों में गणित टाइपसेटिंग के लिए [katex](http://खान.github.io/KaTeX/) का उपयोग करने की अनुमति दें", + "Katex_Parenthesis_Syntax": "कोष्ठक सिंटैक्स की अनुमति दें", + "Katex_Parenthesis_Syntax_Description": "\\[katex ब्लॉक\\] और \\(इनलाइन katex\\) सिंटैक्स का उपयोग करने की अनुमति दें", + "Keep_default_user_settings": "डिफ़ॉल्ट सेटिंग्स रखें", + "Keyboard_Shortcuts_Edit_Previous_Message": "पिछला संदेश संपादित करें", + "Keyboard_Shortcuts_Keys_1": "कमांड (या Ctrl) + p या कमांड (या Ctrl) + k", + "Keyboard_Shortcuts_Keys_2": "ऊपर की ओर तीर", + "Keyboard_Shortcuts_Keys_3": "कमांड (या Alt) + बायाँ तीर", + "Keyboard_Shortcuts_Keys_4": "कमांड (या Alt) + ऊपर तीर", + "Keyboard_Shortcuts_Keys_5": "कमांड (या Alt) + दायां तीर", + "Keyboard_Shortcuts_Keys_6": "कमांड (या Alt) + डाउन एरो", + "Keyboard_Shortcuts_Keys_7": "शिफ्ट + एंटर", + "Keyboard_Shortcuts_Keys_8": "शिफ्ट (या Ctrl) + ESC", + "Keyboard_Shortcuts_Mark_all_as_read": "सभी संदेशों को (सभी चैनलों में) पठित के रूप में चिह्नित करें", + "Keyboard_Shortcuts_Move_To_Beginning_Of_Message": "संदेश की शुरुआत में जाएँ", + "Keyboard_Shortcuts_Move_To_End_Of_Message": "संदेश के अंत में जाएँ", + "Keyboard_Shortcuts_New_Line_In_Message": "संदेश लिखें इनपुट में नई पंक्ति", + "Keyboard_Shortcuts_Open_Channel_Slash_User_Search": "चैनल/उपयोगकर्ता खोज खोलें", + "Keyboard_Shortcuts_Title": "कुंजीपटल अल्प मार्ग", + "Knowledge_Base": "ज्ञानधार", + "Label": "लेबल", + "Language": "भाषा", + "Language_Bulgarian": "बल्गेरियाई", + "Language_Chinese": "चीनी", + "Language_Czech": "चेक", + "Language_Danish": "दानिश", + "Language_Dutch": "डच", + "Language_English": "अंग्रेज़ी", + "Language_Estonian": "एस्तोनियावासी", + "Language_Finnish": "फिनिश", + "Language_French": "फ़्रेंच", + "Language_German": "जर्मन", + "Language_Greek": "यूनानी", + "Language_Hungarian": "हंगेरी", + "Language_Italian": "इतालवी", + "Language_Japanese": "जापानी", + "Language_Latvian": "लात्वीयावासी", + "Language_Lithuanian": "लिथुआनियाई", + "Language_Not_set": "कोई विशेष नहीं", + "Language_Polish": "पोलिश", + "Language_Portuguese": "पुर्तगाली", + "Language_Romanian": "रोमानियाई", + "Language_Russian": "रूसी", + "Language_Slovak": "स्लोवाक", + "Language_Slovenian": "स्लोवेनियाई", + "Language_Spanish": "स्पैनिश", + "Language_Swedish": "स्वीडिश", + "Language_Version": "अंग्रेजी संस्करण", + "Last_7_days": "पिछले 7 दिन", + "Last_15_days": "पिछले 15 दिन", + "Last_30_days": "पिछले 30 दिनों में", + "Last_90_days": "पिछले 90 दिन", + "Last_6_months": "पिछले 6 महीने", + "Last_year": "पिछले साल", + "Last_active": "अंतिम सक्रिय", + "Last_Call": "आखिरी कॉल", + "Last_Chat": "आखिरी चैट", + "Last_Heartbeat_Time": "आखिरी दिल की धड़कन का समय", + "Last_login": "आखरी लॉगइन", + "Last_Message": "अंतिम संदेश", + "Last_Message_At": "अंतिम संदेश पर", + "Last_seen": "अंतिम बार देखा गया", + "Last_Status": "अंतिम स्थिति", + "Last_token_part": "अंतिम सांकेतिक भाग", + "Last_Updated": "आखरी अपडेट", + "Launched_successfully": "सफलतापूर्वक लॉन्च किया गया", + "Layout": "लेआउट", + "Layout_Login_Hide_Logo": "लोगो छिपाएँ", + "Layout_Login_Hide_Logo_Description": "लॉगिन पेज पर लोगो छिपाएँ.", + "Layout_Login_Hide_Title": "शीर्षक छिपाएँ", + "Layout_Login_Hide_Title_Description": "लॉगिन पेज पर शीर्षक छिपाएँ.", + "Layout_Login_Hide_Powered_By": "\"इसके द्वारा संचालित\" छुपाएं", + "Layout_Login_Hide_Powered_By_Description": "लॉगिन पेज पर \"संचालित द्वारा\" छुपाएं।", + "Layout_Login_Template": "लॉगिन टेम्प्लेट", + "Layout_Login_Template_Description": "लॉगिन पेज का स्वरूप अनुकूलित करें.", + "Layout_Login_Template_Vertical": "खड़ा", + "Layout_Login_Template_Horizontal": "क्षैतिज", + "Layout_Description": "अपने कार्यक्षेत्र का स्वरूप अनुकूलित करें.", + "Layout_Home_Body": "सामग्री ब्लॉक", + "Layout_Home_Page_Content": "लेआउट/होम पेज सामग्री", + "Layout_Home_Page_Content_Title": "मुख पृष्ठ सामग्री", + "Layout_Home_Title": "गृह शीर्षक", + "Layout_Legal_Notice": "कानूनी नोटिस", + "Layout_Login_Terms": "लॉगिन शर्तें", + "Layout_Login_Terms_Content": "आगे बढ़कर आप हमारी सेवा की शर्तों , गोपनीयता नीति और कानूनी नोटिस से सहमत हैं।", + "Layout_Privacy_Policy": "गोपनीयता नीति", + "Layout_Show_Home_Button": "साइडबार हेडर पर होम पेज बटन दिखाएँ", + "Layout_Custom_Content_Description": "यहां आपकी कस्टम सामग्री है। यदि आप प्रीमियम योजना पर हैं, तो इसे एक सफेद ब्लॉक के अंदर रखा जा सकता है या होमपेज पर उपलब्ध सभी जगह ले सकता है।", + "Layout_Home_Custom_Block_Visible": "मुखपृष्ठ पर कस्टम सामग्री दिखाएं", + "Layout_Custom_Body_Only": "केवल कस्टम सामग्री दिखाएं", + "Layout_Custom_Body_Only_Description": "यह मुखपृष्ठ में अन्य सभी सामग्री ब्लॉक छिपा देगा।", + "Layout_Sidenav_Footer": "साइड नेविगेशन फ़ुटर", + "Layout_Sidenav_Footer_Dark": "साइड नेविगेशन फ़ुटर - डार्क थीम", + "Layout_Sidenav_Footer_description": "फ़ुटर का आकार 260 x 70px है", + "Layout_Sidenav_Footer_Dark_description": "फ़ुटर का आकार 260 x 70px है", + "Layout_Terms_of_Service": "सेवा की शर्तें", + "LDAP": "एलडीएपी", + "LDAP_Description": "लाइटवेट डायरेक्ट्री एक्सेस प्रोटोकॉल किसी को भी आपके सर्वर या कंपनी के बारे में डेटा का पता लगाने में सक्षम बनाता है।", + "LDAP_Documentation": "एलडीएपी दस्तावेज़ीकरण", + "LDAP_Connection": "संबंध", + "LDAP_Connection_Authentication": "प्रमाणीकरण", + "LDAP_Connection_Encryption": "कूटलेखन", + "LDAP_Connection_Timeouts": "समय समाप्ति", + "LDAP_UserSearch": "उपयोगकर्ता खोज", + "LDAP_UserSearch_Filter": "फ़िल्टर खोजें", + "LDAP_UserSearch_GroupFilter": "समूह फ़िल्टर", + "LDAP_DataSync": "डेटा सिंक", + "LDAP_DataSync_DataMap": "मानचित्रण", + "LDAP_DataSync_Avatar": "अवतार", + "LDAP_DataSync_Advanced": "उन्नत सिंक", + "LDAP_DataSync_CustomFields": "कस्टम फ़ील्ड सिंक करें", + "LDAP_DataSync_Roles": "भूमिकाएँ सिंक करें", + "LDAP_DataSync_Channels": "चैनल सिंक करें", + "LDAP_DataSync_Teams": "टीमों को सिंक करें", + "LDAP_DataSync_BackgroundSync": "पृष्ठभूमि समन्वयन", + "LDAP_Server_Type": "सर्वर प्रकार", + "LDAP_Server_Type_AD": "सक्रिय निर्देशिका", + "LDAP_Server_Type_Other": "अन्य", + "LDAP_Name_Field": "नाम फ़ील्ड", + "LDAP_Email_Field": "ईमेल फ़ील्ड", + "LDAP_Update_Data_On_Login": "लॉगिन पर उपयोगकर्ता डेटा अपडेट करें", + "LDAP_Update_Data_On_OAuth_Login": "OAuth सेवाओं के साथ लॉगिन पर उपयोगकर्ता डेटा अपडेट करें", + "LDAP_Advanced_Sync": "उन्नत सिंक", "LDAP_Authentication": "सक्षम करें", + "LDAP_Authentication_Password": "पासवर्ड", + "LDAP_Authentication_UserDN": "उपयोगकर्ता डी.एन", + "LDAP_Authentication_UserDN_Description": "एलडीएपी उपयोगकर्ता जो अन्य उपयोगकर्ताओं के साइन इन करने पर उन्हें प्रमाणित करने के लिए उपयोगकर्ता लुकअप करता है।\n यह आमतौर पर तृतीय-पक्ष एकीकरण के लिए विशेष रूप से बनाया गया एक सेवा खाता है। पूर्णतः योग्य नाम का उपयोग करें, जैसे `cn=Administrator,cn=Users,dc=Example,dc=com`.", + "LDAP_Avatar_Field": "उपयोगकर्ता अवतार फ़ील्ड", + "You_have_to_set_an_API_token_first_in_order_to_use_the_integration": "एकीकरण का उपयोग करने के लिए आपको पहले एक एपीआई टोकन सेट करना होगा।", + "LDAP_Avatar_Field_Description": " उपयोगकर्ताओं के लिए किस फ़ील्ड को *अवतार* के रूप में उपयोग किया जाएगा। पहले `थंबनेलफोटो` और `जेपीईजीफोटो` को फ़ॉलबैक के रूप में उपयोग करने के लिए खाली छोड़ दें।", + "LDAP_Background_Sync": "पृष्ठभूमि समन्वयन", + "LDAP_Background_Sync_Avatars": "अवतार पृष्ठभूमि सिंक", + "LDAP_Background_Sync_Avatars_Description": "उपयोगकर्ता अवतारों को सिंक करने के लिए एक अलग पृष्ठभूमि प्रक्रिया सक्षम करें।", + "LDAP_Background_Sync_Avatars_Interval": "अवतार पृष्ठभूमि सिंक अंतराल", + "LDAP_Background_Sync_Import_New_Users": "पृष्ठभूमि सिंक नए उपयोगकर्ताओं को आयात करें", + "LDAP_Background_Sync_Import_New_Users_Description": "उन सभी उपयोगकर्ताओं को आयात करेगा (आपके फ़िल्टर मानदंड के आधार पर) जो एलडीएपी में मौजूद हैं और रॉकेट.चैट में मौजूद नहीं हैं", + "LDAP_Background_Sync_Interval": "पृष्ठभूमि सिंक अंतराल", + "LDAP_Background_Sync_Interval_Description": "तुल्यकालन के बीच का अंतराल. उदाहरण `हर 24 घंटे` या `सप्ताह के पहले दिन`, अधिक उदाहरण [क्रोन टेक्स्ट पार्सर](http://bunkat.github.io/later/parsers.html#text) पर", + "LDAP_Background_Sync_Keep_Existant_Users_Updated": "मौजूदा उपयोगकर्ताओं का बैकग्राउंड सिंक अपडेट करें", + "LDAP_Background_Sync_Keep_Existant_Users_Updated_Description": "प्रत्येक **सिंक अंतराल** पर पहले से ही एलडीएपी से आयातित सभी उपयोगकर्ताओं के अवतार, फ़ील्ड, उपयोगकर्ता नाम इत्यादि (आपके कॉन्फ़िगरेशन के आधार पर) को सिंक करेगा।", + "LDAP_Background_Sync_Merge_Existent_Users": "बैकग्राउंड सिंक मौजूदा उपयोगकर्ताओं को मर्ज करता है", + "LDAP_Background_Sync_Merge_Existent_Users_Description": "सभी उपयोगकर्ताओं (आपके फ़िल्टर मानदंड के आधार पर) को मर्ज कर देगा जो एलडीएपी में मौजूद हैं और रॉकेट.चैट में भी मौजूद हैं। इसे सक्षम करने के लिए, डेटा सिंक टैब में 'मौजूदा उपयोगकर्ताओं को मर्ज करें' सेटिंग सक्रिय करें।", + "LDAP_BaseDN": "बेस डी.एन", + "LDAP_BaseDN_Description": "एलडीएपी सबट्री का पूर्णतः योग्य विशिष्ट नाम (डीएन) जिसे आप उपयोगकर्ताओं और समूहों के लिए खोजना चाहते हैं। आप जितने चाहें उतने जोड़ सकते हैं; हालाँकि, प्रत्येक समूह को उसी डोमेन आधार में परिभाषित किया जाना चाहिए जिसमें उसके उपयोगकर्ता शामिल हैं। उदाहरण: `ou=उपयोगकर्ता+ou=प्रोजेक्ट्स,dc=उदाहरण,dc=com`। यदि आप प्रतिबंधित उपयोगकर्ता समूह निर्दिष्ट करते हैं, तो केवल उन समूहों से संबंधित उपयोगकर्ता ही दायरे में होंगे। हम अनुशंसा करते हैं कि आप अपने एलडीएपी निर्देशिका ट्री के शीर्ष स्तर को अपने डोमेन आधार के रूप में निर्दिष्ट करें और पहुंच को नियंत्रित करने के लिए खोज फ़िल्टर का उपयोग करें।", + "LDAP_CA_Cert": "सीए सर्टिफिकेट", + "LDAP_Connect_Timeout": "कनेक्शन टाइमआउट (एमएस)", + "LDAP_DataSync_AutoLogout": "ऑटो लॉगआउट निष्क्रिय उपयोगकर्ता", + "LDAP_Default_Domain": "डिफ़ॉल्ट डोमेन", + "LDAP_Default_Domain_Description": "यदि प्रदान किया गया है तो डिफ़ॉल्ट डोमेन का उपयोग उन उपयोगकर्ताओं के लिए एक अद्वितीय ईमेल बनाने के लिए किया जाएगा जहां ईमेल एलडीएपी से आयात नहीं किया गया था। ईमेल को `username@default_domain` या `unique_id@default_domain` के रूप में माउंट किया जाएगा।\n उदाहरण: `रॉकेट.चैट`", "LDAP_Enable": "सक्षम करें", + "LDAP_Enable_Description": "प्रमाणीकरण के लिए एलडीएपी का उपयोग करने का प्रयास करें।", + "LDAP_Enable_LDAP_Groups_To_RC_Teams": "LDAP से Rocket.Chat तक टीम मैपिंग सक्षम करें", + "LDAP_Encryption": "कूटलेखन", + "LDAP_Encryption_Description": "एलडीएपी सर्वर पर संचार सुरक्षित करने के लिए एन्क्रिप्शन विधि का उपयोग किया जाता है। उदाहरणों में `प्लेन` (कोई एन्क्रिप्शन नहीं), `एसएसएल/एलडीएपीएस` (शुरुआत से एन्क्रिप्टेड), और `स्टार्टटीएलएस` (कनेक्ट होने के बाद एन्क्रिप्टेड संचार में अपग्रेड) शामिल हैं।", + "LDAP_Find_User_After_Login": "लॉग इन करने के बाद उपयोगकर्ता ढूंढें", + "LDAP_Find_User_After_Login_Description": "बाइंड के बाद उपयोगकर्ता के डीएन की खोज करेगा ताकि यह सुनिश्चित किया जा सके कि एडी कॉन्फ़िगरेशन द्वारा अनुमति दिए जाने पर बाइंड खाली पासवर्ड के साथ लॉगिन को रोकने में सफल रहा।", + "LDAP_Group_Filter_Enable": "एलडीएपी उपयोगकर्ता समूह फ़िल्टर सक्षम करें", + "LDAP_Group_Filter_Enable_Description": "एलडीएपी समूह में उपयोगकर्ताओं तक पहुंच प्रतिबंधित करें\n समूहों द्वारा पहुंच को प्रतिबंधित करने के लिए *memberOf* फ़िल्टर के बिना OpenLDAP सर्वर को अनुमति देने के लिए उपयोगी", + "LDAP_Group_Filter_Group_Id_Attribute": "समूह आईडी विशेषता", + "LDAP_Group_Filter_Group_Id_Attribute_Description": "जैसे **ओपनएलडीएपी:** `सीएन`", + "LDAP_Group_Filter_Group_Member_Attribute": "समूह सदस्य विशेषता", + "LDAP_Group_Filter_Group_Member_Attribute_Description": "जैसे **ओपनएलडीएपी:** `यूनीकमेम्बर`", + "LDAP_Group_Filter_Group_Member_Format": "समूह सदस्य प्रारूप", + "LDAP_Group_Filter_Group_Member_Format_Description": "जैसे **OpenLDAP:** `uid=#{username},ou=users,o=Company,c=com`", + "LDAP_Group_Filter_Group_Name": "समूह नाम", + "LDAP_Group_Filter_Group_Name_Description": "समूह का नाम जिससे उपयोगकर्ता संबंधित है", + "LDAP_Group_Filter_ObjectClass": "समूह ऑब्जेक्टक्लास", + "LDAP_Group_Filter_ObjectClass_Description": "*ऑब्जेक्टक्लास* जो समूहों की पहचान करता है।\n जैसे **OpenLDAP:** `groupOfUniqueNames`", + "LDAP_Groups_To_Rocket_Chat_Teams": "एलडीएपी से रॉकेट.चैट तक टीम मैपिंग।", + "LDAP_Host": "मेज़बान", + "LDAP_Host_Description": "एलडीएपी होस्ट, उदा. `ldap.example.com` या `10.0.0.30`.", + "LDAP_Idle_Timeout": "निष्क्रिय समयबाह्य (एमएस)", + "LDAP_Idle_Timeout_Description": "नवीनतम एलडीएपी ऑपरेशन के बाद कनेक्शन बंद होने तक कितने मिलीसेकंड प्रतीक्षा करें। (प्रत्येक ऑपरेशन एक नया कनेक्शन खोलेगा)", + "LDAP_Import_Users_Description": "यह ट्रू सिंक प्रक्रिया सभी एलडीएपी उपयोगकर्ताओं को आयात करेगी\n *सावधान!* अतिरिक्त उपयोगकर्ताओं को आयात न करने के लिए खोज फ़िल्टर निर्दिष्ट करें।", + "LDAP_Internal_Log_Level": "आंतरिक लॉग स्तर", + "LDAP_Login_Fallback": "फ़ॉलबैक लॉगिन करें", + "LDAP_Login_Fallback_Description": "यदि एलडीएपी पर लॉगिन सफल नहीं होता है तो डिफ़ॉल्ट/स्थानीय खाता सिस्टम में लॉगिन करने का प्रयास करें। किसी कारण से एलडीएपी डाउन होने पर मदद करता है।", + "LDAP_Merge_Existing_Users": "मौजूदा उपयोगकर्ताओं को मर्ज करें", + "LDAP_Merge_Existing_Users_Description": "*सावधान!* एलडीएपी से एक उपयोगकर्ता आयात करते समय और समान उपयोगकर्ता नाम वाला एक उपयोगकर्ता पहले से मौजूद है तो एलडीएपी जानकारी और पासवर्ड मौजूदा उपयोगकर्ता में सेट किया जाएगा।", + "LDAP_Port": "पत्तन", + "LDAP_Port_Description": "एलडीएपी तक पहुंचने के लिए पोर्ट। उदाहरण के लिए: एलडीएपीएस के लिए `389` या `636`", + "LDAP_Prevent_Username_Changes": "एलडीएपी उपयोगकर्ताओं को अपना Rocket.Chat उपयोगकर्ता नाम बदलने से रोकें", + "LDAP_Query_To_Get_User_Teams": "उपयोगकर्ता समूह प्राप्त करने के लिए एलडीएपी क्वेरी", + "LDAP_Reconnect": "रिकनेक्ट", + "LDAP_Reconnect_Description": "संचालन निष्पादित करते समय किसी कारण से कनेक्शन बाधित होने पर स्वचालित रूप से पुन: कनेक्ट करने का प्रयास करें", + "LDAP_Reject_Unauthorized": "अनधिकृत अस्वीकार करें", + "LDAP_Reject_Unauthorized_Description": "जिन प्रमाणपत्रों को सत्यापित नहीं किया जा सकता, उन्हें अनुमति देने के लिए इस विकल्प को अक्षम करें। आमतौर पर स्व-हस्ताक्षरित प्रमाणपत्रों को काम करने के लिए इस विकल्प को अक्षम करना होगा", + "LDAP_Search_Page_Size": "पृष्ठ आकार खोजें", + "LDAP_Search_Page_Size_Description": "प्रत्येक परिणाम पृष्ठ पर संसाधित होने के लिए प्रविष्टियों की अधिकतम संख्या वापस आएगी", + "LDAP_Search_Size_Limit": "खोज आकार सीमा", + "LDAP_Search_Size_Limit_Description": "वापस आने वाली प्रविष्टियों की अधिकतम संख्या.\n **ध्यान दें** यह संख्या **खोज पृष्ठ आकार** से अधिक होनी चाहिए", + "LDAP_Sync_Custom_Fields": "कस्टम फ़ील्ड सिंक करें", + "LDAP_CustomFieldMap": "कस्टम फ़ील्ड मैपिंग", + "LDAP_Sync_AutoLogout_Enabled": "ऑटो लॉगआउट सक्षम करें", + "LDAP_Sync_AutoLogout_Interval": "ऑटो लॉगआउट अंतराल", + "LDAP_Sync_Now": "अभी सिंक करें", + "LDAP_Sync_Now_Description": "यह अगले शेड्यूल किए गए सिंक की प्रतीक्षा किए बिना, अब **बैकग्राउंड सिंक** ऑपरेशन शुरू कर देगा।\nयह क्रिया अतुल्यकालिक है, कृपया अधिक जानकारी के लिए लॉग देखें।", + "LDAP_Sync_User_Active_State": "उपयोगकर्ता सक्रिय स्थिति सिंक करें", + "LDAP_Sync_User_Active_State_Both": "उपयोगकर्ताओं को सक्षम और अक्षम करें", + "LDAP_Sync_User_Active_State_Description": "एलडीएपी स्थिति के आधार पर निर्धारित करें कि उपयोगकर्ताओं को Rocket.Chat पर सक्षम या अक्षम किया जाना चाहिए या नहीं। 'pwdAccountLockedTime' विशेषता का उपयोग यह निर्धारित करने के लिए किया जाएगा कि उपयोगकर्ता अक्षम है या नहीं।", + "LDAP_Sync_User_Active_State_Disable": "उपयोगकर्ताओं को अक्षम करें", + "LDAP_Sync_User_Active_State_Nothing": "कुछ भी नहीं है", + "LDAP_Sync_User_Avatar": "उपयोगकर्ता अवतार सिंक करें", + "LDAP_Sync_User_Data_Roles": "एलडीएपी समूह सिंक करें", + "LDAP_Sync_User_Data_Channels": "एलडीएपी समूहों को चैनलों के साथ ऑटो सिंक करें", + "LDAP_Sync_User_Data_Channels_Admin": "चैनल व्यवस्थापक", + "LDAP_Sync_User_Data_Channels_Admin_Description": "जब चैनल स्वतः निर्मित होते हैं जो सिंक के दौरान मौजूद नहीं होते हैं, तो यह उपयोगकर्ता स्वचालित रूप से चैनल का व्यवस्थापक बन जाएगा।", + "LDAP_Sync_User_Data_Channels_BaseDN": "एलडीएपी ग्रुप बेसडीएन", + "LDAP_Sync_User_Data_Channels_Description": "उपयोगकर्ताओं को उनके एलडीएपी समूह के आधार पर किसी चैनल में स्वचालित रूप से जोड़ने के लिए इस सुविधा को सक्षम करें। यदि आप भी किसी चैनल से उपयोगकर्ताओं को हटाना चाहते हैं, तो उपयोगकर्ताओं को स्वत: हटाने के बारे में नीचे दिया गया विकल्प देखें।", + "LDAP_Sync_User_Data_Channels_Enforce_AutoChannels": "चैनलों से उपयोगकर्ताओं को स्वतः हटाएँ", + "LDAP_Sync_User_Data_Channels_Enforce_AutoChannels_Description": "**ध्यान दें**: इसे सक्षम करने से चैनल के किसी भी उपयोगकर्ता को हटा दिया जाएगा जिसके पास संबंधित एलडीएपी समूह नहीं है! इसे केवल तभी सक्षम करें यदि आप जानते हैं कि आप क्या कर रहे हैं।", + "LDAP_Sync_User_Data_Channels_Filter": "उपयोगकर्ता समूह फ़िल्टर", + "LDAP_Sync_User_Data_Channels_Filter_Description": "एलडीएपी खोज फ़िल्टर का उपयोग यह जाँचने के लिए किया जाता है कि कोई उपयोगकर्ता किसी समूह में है या नहीं।", + "LDAP_Sync_User_Data_ChannelsMap": "एलडीएपी समूह चैनल मानचित्र", + "LDAP_Sync_User_Data_ChannelsMap_Default": "// उपरोक्त चैनलों के लिए एलडीएपी समूहों को ऑटो सिंक सक्षम करें", + "LDAP_Sync_User_Data_ChannelsMap_Description": "एलडीएपी समूहों को रॉकेट.चैट चैनलों पर मैप करें।\n उदाहरण के तौर पर, `{\"कर्मचारी\":\"सामान्य\"}` एलडीएपी समूह कर्मचारी में किसी भी उपयोगकर्ता को सामान्य चैनल में जोड़ देगा।", + "LDAP_Sync_User_Data_Roles_AutoRemove": "उपयोगकर्ता भूमिकाएँ स्वतः हटाएँ", + "LDAP_Sync_User_Data_Roles_AutoRemove_Description": "**ध्यान दें**: इसे सक्षम करने से उपयोगकर्ता स्वचालित रूप से किसी भूमिका से हटा दिए जाएंगे यदि उन्हें एलडीएपी में असाइन नहीं किया गया है! यह केवल उन भूमिकाओं को स्वचालित रूप से हटा देगा जो नीचे उपयोगकर्ता डेटा समूह मानचित्र के अंतर्गत सेट की गई हैं।", + "LDAP_Sync_User_Data_Roles_BaseDN": "एलडीएपी ग्रुप बेसडीएन", + "LDAP_Sync_User_Data_Roles_BaseDN_Description": "LDAP BaseDN का उपयोग उपयोगकर्ताओं को खोजने के लिए किया जाता है।", + "LDAP_Sync_User_Data_Roles_Filter": "उपयोगकर्ता समूह फ़िल्टर", + "LDAP_Sync_User_Data_Roles_Filter_Description": "एलडीएपी खोज फ़िल्टर का उपयोग यह जाँचने के लिए किया जाता है कि कोई उपयोगकर्ता किसी समूह में है या नहीं।", + "LDAP_Sync_User_Data_RolesMap": "उपयोगकर्ता डेटा समूह मानचित्र", + "LDAP_Sync_User_Data_RolesMap_Description": "LDAP समूहों को Rocket.Chat उपयोगकर्ता भूमिकाओं में मैप करें\n उदाहरण के तौर पर, `{\"रॉकेट-एडमिन\":\"एडमिन\", \"टेक-सपोर्ट\":\"सपोर्ट\", \"मैनेजर\":[\"लीडर\", \"मॉडरेटर\"]}` रॉकेट-एडमिन एलडीएपी ग्रुप को मैप करेगा रॉकेट की \"व्यवस्थापक\" भूमिका.", + "LDAP_Teams_BaseDN": "एलडीएपी टीमें बेसडीएन", + "LDAP_Teams_BaseDN_Description": "एलडीएपी बेसडीएन का उपयोग उपयोगकर्ता टीमों को देखने के लिए किया जाता है।", + "LDAP_Teams_Name_Field": "एलडीएपी टीम का नाम विशेषता", + "LDAP_Teams_Name_Field_Description": "LDAP विशेषता जिसका उपयोग Rocket.Chat को टीम का नाम लोड करने के लिए करना चाहिए। यदि आप उन्हें अल्पविराम से अलग करते हैं तो आप एक से अधिक संभावित विशेषता नाम निर्दिष्ट कर सकते हैं।", + "LDAP_Timeout": "टाइमआउट (एमएस)", + "LDAP_Timeout_Description": "कोई त्रुटि लौटाने से पहले खोज परिणाम के लिए कितने मीलसेकंड प्रतीक्षा करते हैं", + "LDAP_Unique_Identifier_Field": "अद्वितीय पहचानकर्ता फ़ील्ड", + "LDAP_Unique_Identifier_Field_Description": "एलडीएपी उपयोगकर्ता और रॉकेट.चैट उपयोगकर्ता को लिंक करने के लिए किस फ़ील्ड का उपयोग किया जाएगा। आप एलडीएपी रिकॉर्ड से मूल्य प्राप्त करने का प्रयास करने के लिए अल्पविराम से अलग किए गए कई मानों को सूचित कर सकते हैं।\n डिफ़ॉल्ट मान `ऑब्जेक्टGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber` है", + "LDAP_User_Found": "एलडीएपी उपयोगकर्ता मिला", + "LDAP_User_Search_AttributesToQuery": "क्वेरी के गुण", + "LDAP_User_Search_AttributesToQuery_Description": "निर्दिष्ट करें कि एलडीएपी प्रश्नों पर कौन सी विशेषताएँ लौटाई जानी चाहिए, उन्हें अल्पविराम से अलग करें। हर चीज़ के लिए डिफ़ॉल्ट. `*` सभी नियमित विशेषताओं का प्रतिनिधित्व करता है और `+` सभी परिचालन विशेषताओं का प्रतिनिधित्व करता है। प्रत्येक Rocket.Chat सिंक विकल्प द्वारा उपयोग की जाने वाली प्रत्येक विशेषता को शामिल करना सुनिश्चित करें।", + "LDAP_User_Search_Field": "खोज क्षेत्र", + "LDAP_User_Search_Field_Description": "एलडीएपी विशेषता जो प्रमाणीकरण का प्रयास करने वाले एलडीएपी उपयोगकर्ता की पहचान करती है। अधिकांश सक्रिय निर्देशिका स्थापनाओं के लिए यह फ़ील्ड `sAMAccountName` होना चाहिए, लेकिन यह अन्य LDAP समाधानों, जैसे OpenLDAP, के लिए `uid` हो सकता है। आप ईमेल या अपनी इच्छित विशेषता के आधार पर उपयोगकर्ताओं की पहचान करने के लिए `मेल` का उपयोग कर सकते हैं।\n आप उपयोगकर्ताओं को उपयोगकर्ता नाम या ईमेल जैसे कई पहचानकर्ताओं का उपयोग करके लॉगिन करने की अनुमति देने के लिए अल्पविराम से अलग किए गए कई मानों का उपयोग कर सकते हैं।", + "LDAP_User_Search_Filter": "फ़िल्टर", + "LDAP_User_Search_Filter_Description": "यदि निर्दिष्ट किया गया है, तो केवल इस फ़िल्टर से मेल खाने वाले उपयोगकर्ताओं को ही लॉग इन करने की अनुमति दी जाएगी। यदि कोई फ़िल्टर निर्दिष्ट नहीं है, तो निर्दिष्ट डोमेन आधार के दायरे में सभी उपयोगकर्ता साइन इन करने में सक्षम होंगे।\n जैसे सक्रिय निर्देशिका के लिए `memberOf=cn=ROCKET_CHAT,ou=सामान्य समूह`।\n जैसे OpenLDAP (एक्स्टेंसिबल मैच सर्च) के लिए `ou:dn:=ROCKET_CHAT`।", "LDAP_User_Search_Scope": "क्षेत्र", + "LDAP_Username_Field": "उपयोक्तानाम फ़ील्ड", + "LDAP_Username_Field_Description": "नए उपयोगकर्ताओं के लिए किस फ़ील्ड का उपयोग *उपयोगकर्ता नाम* के रूप में किया जाएगा. लॉगिन पेज पर सूचित उपयोगकर्ता नाम का उपयोग करने के लिए खाली छोड़ दें।\n आप टेम्प्लेट टैग का भी उपयोग कर सकते हैं, जैसे `#{givenName}.#{sn}`.\n डिफ़ॉल्ट मान `sAMAccountName` है।", + "LDAP_Username_To_Search": "खोजने के लिए उपयोगकर्ता नाम", + "LDAP_Validate_Teams_For_Each_Login": "प्रत्येक लॉगिन के लिए मैपिंग मान्य करें", + "LDAP_Validate_Teams_For_Each_Login_Description": "निर्धारित करें कि क्या हर बार Rocket.Chat पर लॉगिन करने पर उपयोगकर्ताओं की टीमों को अपडेट किया जाना चाहिए। यदि इसे बंद कर दिया जाता है तो टीम को केवल उनके पहले लॉगिन पर ही लोड किया जाएगा।", + "Lead_capture_email_regex": "लीड कैप्चर ईमेल रेगेक्स", + "Lead_capture_phone_regex": "लीड कैप्चर फ़ोन रेगेक्स", + "Learn_more": "और अधिक जानें", + "Learn_more_about_agents": "एजेंटों के बारे में और जानें", + "Learn_more_about_canned_responses": "डिब्बाबंद प्रतिक्रियाओं के बारे में और जानें", + "Learn_more_about_contacts": "संपर्कों के बारे में और जानें", + "Learn_more_about_current_chats": "वर्तमान चैट के बारे में और जानें", + "Learn_more_about_custom_fields": "कस्टम फ़ील्ड के बारे में और जानें", + "Learn_more_about_conversations": "बातचीत के बारे में और जानें", + "Learn_more_about_departments": "विभागों के बारे में और जानें", + "Learn_more_about_managers": "प्रबंधकों के बारे में और जानें", + "Learn_more_about_monitors": "मॉनिटर के बारे में और जानें", + "Learn_more_about_SLA_policies": "SLA नीतियों के बारे में और जानें", + "Learn_more_about_tags": "टैग के बारे में और जानें", + "Learn_more_about_triggers": "ट्रिगर्स के बारे में और जानें", + "Learn_more_about_units": "इकाइयों के बारे में और जानें", + "Learn_more_about_voice_channel": "वॉइस चैनल के बारे में और जानें", + "Least_recent_updated": "कम से कम हाल ही में अद्यतन किया गया", + "Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat": "जानें कि Rocket.Chat की असंख्य संभावनाओं को कैसे अनलॉक किया जाए।", + "Leave": "छुट्टी", + "Leave_a_comment": "एक टिप्पणी छोड़ें", + "Leave_Group_Warning": "क्या आप वाकई समूह \"%s\" छोड़ना चाहते हैं?", + "Leave_Livechat_Warning": "क्या आप वाकई \"%s\" के साथ ओमनीचैनल छोड़ना चाहते हैं?", + "Leave_Private_Warning": "क्या आप वाकई \"%s\" के साथ चर्चा छोड़ना चाहते हैं?", + "Leave_room": "छुट्टी", + "Leave_Room_Warning": "क्या आप वाकई चैनल \"%s\" छोड़ना चाहते हैं?", + "Leave_the_current_channel": "वर्तमान चैनल छोड़ें", + "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "यदि आप भूमिका नहीं दिखाना चाहते तो विवरण फ़ील्ड खाली छोड़ दें", + "leave-c": "चैनल छोड़ें", + "leave-c_description": "चैनल छोड़ने की अनुमति", + "leave-p": "निजी समूह छोड़ें", + "leave-p_description": "निजी समूह छोड़ने की अनुमति", + "Lets_get_you_new_one_": "आइए आपके लिए एक नया लेकर आएं!", + "License": "लाइसेंस", + "Line": "रेखा", + "Link": "जोड़ना", + "Link_Preview": "लिंक पूर्वावलोकन", + "List_of_Channels": "चैनलों की सूची", + "List_of_departments_for_forward": "अग्रेषण हेतु अनुमत विभागों की सूची (वैकल्पिक)", + "List_of_departments_for_forward_description": "उन विभागों की एक प्रतिबंधित सूची सेट करने की अनुमति दें जो इस विभाग से चैट प्राप्त कर सकते हैं", + "List_of_departments_to_apply_this_business_hour": "इस व्यावसायिक घंटे को लागू करने वाले विभागों की सूची", + "List_of_Direct_Messages": "सीधे संदेशों की सूची", + "List_view": "लिस्ट व्यू", + "Livechat": "सीधी बातचीत", + "Livechat_abandoned_rooms_action": "आगंतुक परित्याग को कैसे संभालें", + "Livechat_abandoned_rooms_closed_custom_message": "कस्टम संदेश जब आगंतुक निष्क्रियता के कारण कमरा स्वचालित रूप से बंद हो जाता है", + "Livechat_agents": "ओमनीचैनल एजेंट", + "Livechat_Agents": "एजेंटों", + "Livechat_allow_manual_on_hold": "एजेंटों को चैट को मैन्युअल रूप से होल्ड पर रखने की अनुमति दें", + "Livechat_allow_manual_on_hold_Description": "सक्षम होने पर, एजेंट को चैट को होल्ड पर रखने का विकल्प मिलेगा", + "Livechat_allow_manual_on_hold_upon_agent_engagement_only": "एजेंट संलग्न होने के बाद ही चैट होल्ड पर रहती है", + "Livechat_allow_manual_on_hold_upon_agent_engagement_only_Description": "केवल तभी चैट को होल्ड पर रखने की अनुमति दें यदि एजेंट वही है जिसने बातचीत में अंतिम संदेश भेजा है।", + "Livechat_AllowedDomainsList": "लाइवचैट अनुमत डोमेन", + "Livechat_Appearance": "लाइवचैट उपस्थिति", + "Livechat_auto_close_on_hold_chats_custom_message": "ऑन होल्ड कतार में बंद चैट के लिए कस्टम संदेश", + "Livechat_auto_close_on_hold_chats_custom_message_Description": "जब ऑन-होल्ड कतार में कोई कमरा सिस्टम द्वारा स्वचालित रूप से बंद हो जाता है तो कस्टम संदेश भेजा जाता है", + "Livechat_auto_close_on_hold_chats_timeout": "ऑन होल्ड क्यू में चैट बंद करने से पहले कितनी देर तक इंतजार करना होगा?", + "Livechat_auto_close_on_hold_chats_timeout_Description": "परिभाषित करें कि चैट सिस्टम द्वारा स्वचालित रूप से बंद होने तक ऑन होल्ड कतार में कितनी देर तक रहेगी। समय सेकंड में", + "Livechat_auto_transfer_chat_timeout": "किसी अन्य एजेंट को अनुत्तरित चैट के स्वचालित स्थानांतरण के लिए टाइमआउट (सेकंड में)।", + "Livechat_auto_transfer_chat_timeout_Description": "यह इवेंट तभी होता है जब चैट अभी शुरू हुई हो. निष्क्रियता के लिए पहली बार स्थानांतरण के बाद, कमरे की निगरानी नहीं की जाती है।", + "Livechat_business_hour_type": "व्यावसायिक घंटे का प्रकार (एकल या एकाधिक)", + "Livechat_chat_transcript_sent": "चैट प्रतिलेख भेजा गया: {{transcript}}", + "Livechat_close_chat": "चैट बंद करें", + "Livechat_custom_fields_options_placeholder": "पूर्व-कॉन्फ़िगर मान का चयन करने के लिए अल्पविराम से अलग की गई सूची का उपयोग किया जाता है। तत्वों के बीच रिक्त स्थान स्वीकार नहीं किया जाता है।", + "Livechat_custom_fields_public_description": "सार्वजनिक कस्टम फ़ील्ड बाहरी अनुप्रयोगों, जैसे लाइवचैट, आदि में प्रदर्शित किए जाएंगे।", + "Livechat_Dashboard": "ओमनीचैनल डैशबोर्ड", + "Livechat_DepartmentOfflineMessageToChannel": "इस विभाग के लाइवचैट ऑफ़लाइन संदेशों को एक चैनल पर भेजें", + "Livechat_enable_message_character_limit": "संदेश वर्ण सीमा सक्षम करें", + "Livechat_enabled": "ओमनीचैनल सक्षम", + "Livechat_forward_open_chats": "खुली हुई चैट को अग्रेषित करें", + "Livechat_forward_open_chats_timeout": "चैट अग्रेषित करने के लिए टाइमआउट (सेकंड में)।", + "Livechat_guest_count": "अतिथि काउंटर", + "Livechat_Inquiry_Already_Taken": "ओम्नीचैनल पूछताछ पहले ही ले ली गई है", + "Livechat_Installation": "लाइवचैट इंस्टालेशन", + "Livechat_last_chatted_agent_routing": "अंतिम बार चैट किए गए एजेंट को प्राथमिकता", + "Livechat_last_chatted_agent_routing_Description": "यदि चैट शुरू होने पर एजेंट उपलब्ध है तो लास्ट-चैट एजेंट सेटिंग उस एजेंट को चैट आवंटित करती है जिसने पहले उसी विज़िटर के साथ बातचीत की थी।", + "Livechat_managers": "ओमनीचैनल प्रबंधक", + "Livechat_Managers": "प्रबंधकों", + "Livechat_max_queue_wait_time_action": "अधिकतम प्रतीक्षा समय तक पहुंचने पर कतारबद्ध चैट को कैसे संभालें", + "Livechat_maximum_queue_wait_time": "कतार में अधिकतम प्रतीक्षा समय", + "Livechat_maximum_queue_wait_time_description": "चैट को कतार में रखने का अधिकतम समय (मिनटों में)। -1 का मतलब असीमित है", + "Livechat_message_character_limit": "लाइवचैट संदेश वर्ण सीमा", + "Livechat_monitors": "लाइवचैट मॉनिटर", + "Livechat_Monitors": "पर नज़र रखता है", + "Livechat_offline": "ओमनीचैनल ऑफ़लाइन", + "Livechat_offline_message_sent": "लाइवचैट ऑफ़लाइन संदेश भेजा गया", + "Livechat_OfflineMessageToChannel_enabled": "किसी चैनल पर लाइवचैट ऑफ़लाइन संदेश भेजें", + "Omnichannel_chat_closed_due_to_inactivity": "चैट स्वचालित रूप से बंद हो गई क्योंकि हमें {{timeout}} सेकंड में {{guest}} से कोई उत्तर नहीं मिला", + "Omnichannel_on_hold_chat_resumed": "होल्ड पर चैट फिर से शुरू: {{comment}}", + "Omnichannel_on_hold_chat_automatically": "{{guest}} से एक नया संदेश प्राप्त होने पर चैट स्वचालित रूप से ऑन होल्ड से फिर से शुरू हो गई थी", + "Omnichannel_on_hold_chat_resumed_manually": "चैट को मैन्युअल रूप से ऑन होल्ड से {{user}} द्वारा फिर से शुरू किया गया था", + "Omnichannel_On_Hold_due_to_inactivity": "चैट को स्वचालित रूप से होल्ड पर रखा गया था क्योंकि हमें {{timeout}} सेकंड में {{guest}} से कोई उत्तर नहीं मिला था", + "Omnichannel_On_Hold_manually": "चैट को {{user}} द्वारा मैन्युअल रूप से होल्ड पर रखा गया था", + "Omnichannel_onHold_Chat": "चैट को होल्ड पर रखें", + "Omnichannel_quick_actions": "ओमनीचैनल त्वरित कार्यवाही", + "Omnichannel_sorting_disclaimer": "ओमनीचैनल वार्तालापों को {{sortingMechanism}} द्वारा क्रमबद्ध किया जाता है, लागू करने के लिए एक कक्ष संपादित करें।", + "Livechat_online": "ओमनीचैनल ऑन-लाइन", + "Omnichannel_placed_chat_on_hold": "चैट ऑन होल्ड: {{comment}}", + "Omnichannel_hide_conversation_after_closing": "बंद करने के बाद बातचीत छिपाएँ", + "Omnichannel_hide_conversation_after_closing_description": "बातचीत बंद करने के बाद आपको होम पर रीडायरेक्ट कर दिया जाएगा।", + "Livechat_Queue": "ओमनीचैनल कतार", "Livechat_registration_form": "पंजीकरण ", + "Livechat_registration_form_message": "पंजीकरण प्रपत्र संदेश", + "Livechat_room_count": "ओमनीचैनल कक्ष संख्या", + "Livechat_Routing_Method": "ओमनीचैनल रूटिंग विधि", + "Livechat_status": "लाइवचैट स्थिति", + "Livechat_Take_Confirm": "क्या आप इस ग्राहक को लेना चाहते हैं?", + "Livechat_title": "लाइवचैट शीर्षक", + "Livechat_title_color": "लाइवचैट शीर्षक पृष्ठभूमि रंग", + "Livechat_transcript_already_requested_warning": "इस चैट की प्रतिलेख पहले ही अनुरोध किया जा चुका है और बातचीत समाप्त होते ही भेज दी जाएगी।", + "Livechat_transcript_has_been_requested": "निर्यात का अनुरोध किया गया. इसमें कुछ सेकंड लग सकते हैं.", + "Livechat_email_transcript_has_been_requested": "प्रतिलेख का अनुरोध किया गया है. इसमें कुछ सेकंड लग सकते हैं.", + "Livechat_transcript_request_has_been_canceled": "चैट ट्रांस्क्रिप्शन अनुरोध रद्द कर दिया गया है.", + "Livechat_transcript_sent": "ओमनीचैनल प्रतिलेख भेजा गया", + "Livechat_transfer_return_to_the_queue": "{{from}} ने चैट को कतार में लौटा दिया", + "Livechat_transfer_return_to_the_queue_with_a_comment": "{{from}} ने एक टिप्पणी के साथ चैट को कतार में लौटा दिया: {{comment}}", + "Livechat_transfer_return_to_the_queue_auto_transfer_unanswered_chat": "{{from}} ने चैट को कतार में वापस कर दिया क्योंकि यह {{period}} सेकंड तक अनुत्तरित थी", + "Livechat_transfer_to_agent": "{{from}} ने चैट को {{to}} में स्थानांतरित कर दिया", + "Livechat_transfer_to_agent_with_a_comment": "{{from}} ने एक टिप्पणी के साथ चैट को {{to}} में स्थानांतरित कर दिया: {{comment}}", + "Livechat_transfer_to_agent_auto_transfer_unanswered_chat": "{{from}} ने चैट को {{to}} में स्थानांतरित कर दिया क्योंकि यह {{period}} सेकंड तक अनुत्तरित थी", + "Livechat_transfer_to_department": "{{to}} ने चैट को विभाग में स्थानांतरित कर दिया {{to}}", + "Livechat_transfer_to_department_with_a_comment": "{{to}} ने एक टिप्पणी के साथ चैट को विभाग में स्थानांतरित कर दिया।", + "Livechat_transfer_failed_fallback": "मूल विभाग ({{from}} ) में ऑनलाइन एजेंट नहीं हैं। चैट सफलतापूर्वक {{to}} में स्थानांतरित हो गई", + "Livechat_Triggers": "लाइवचैट ट्रिगर", + "Livechat_user_sent_chat_transcript_to_visitor": "{{agent}} ने चैट ट्रांसक्रिप्ट को {{guest}} को भेजा", + "Livechat_Users": "ओमनीचैनल उपयोगकर्ता", + "Livechat_Calls": "लाइवचैट कॉल", + "Livechat_visitor_email_and_transcript_email_do_not_match": "विज़िटर का ईमेल और प्रतिलेख ईमेल मेल नहीं खाते", + "Livechat_visitor_transcript_request": "{{guest}} ने चैट प्रतिलेख का अनुरोध किया", + "LiveStream & Broadcasting": "लाइवस्ट्रीम और प्रसारण", + "LiveStream & Broadcasting_Description": "Rocket.Chat और YouTube लाइव के बीच यह एकीकरण चैनल मालिकों को एक चैनल के अंदर लाइवस्ट्रीम के लिए अपने कैमरा फ़ीड को लाइव प्रसारित करने की अनुमति देता है।", + "Livestream": "लाइव स्ट्रीम", + "Livestream_close": "लाइवस्ट्रीम बंद करें", + "Livestream_enable_audio_only": "केवल ऑडियो मोड सक्षम करें", + "Livestream_enabled": "लाइवस्ट्रीम सक्षम", + "Livestream_not_found": "लाइवस्ट्रीम उपलब्ध नहीं है", + "Livestream_unavailable_for_federation": "फ़ेडरेटेड कमरों के लिए लिवेस्ट्रम अनुपलब्ध है", + "Livestream_popout": "लाइवस्ट्रीम खोलें", + "Livestream_source_changed_succesfully": "लाइवस्ट्रीम स्रोत सफलतापूर्वक बदला गया", + "Livestream_switch_to_room": "वर्तमान कमरे की लाइवस्ट्रीम पर स्विच करें", + "Livestream_url": "लाइवस्ट्रीम स्रोत यूआरएल", + "Livestream_url_incorrect": "लाइवस्ट्रीम यूआरएल ग़लत है", + "Livestream_live_now": "अब सीधा प्रसारण हो रहा है!", + "Load_Balancing": "भार का संतुलन", + "Load_more": "और लोड करें", + "Load_Rotation": "लोड रोटेशन", + "Loading": "लोड हो रहा है", + "Loading_more_from_history": "इतिहास से और अधिक लोड हो रहा है", + "Loading_suggestion": "सुझाव लोड हो रहे हैं", + "Loading...": "लोड हो रहा है...", + "Local": "स्थानीय", + "Local_Domains": "स्थानीय डोमेन", + "Local_Password": "स्थानीय पासवर्ड", + "Local_Time": "स्थानीय समय", + "Local_Timezone": "स्थानीय समय क्षेत्र", + "Local_Time_time": "स्थानीय समय: {{time}}", + "Localization": "स्थानीयकरण", + "Location": "जगह", + "Log_Exceptions_to_Channel": "चैनल में अपवाद लॉग करें", + "Log_Exceptions_to_Channel_Description": "एक चैनल जो सभी कैप्चर किए गए अपवाद प्राप्त करेगा। अपवादों को नज़रअंदाज करने के लिए खाली छोड़ें।", + "Log_File": "फ़ाइल और लाइन दिखाएँ", + "Log_Level": "छांटने का स्तर", + "Log_Package": "पैकेज दिखाएँ", + "Log_Trace_Methods": "ट्रेस विधि कॉल", + "Log_Trace_Methods_Filter": "ट्रेस विधि फ़िल्टर", + "Log_Trace_Methods_Filter_Description": "यहां टेक्स्ट का मूल्यांकन रेगएक्सपी (`नया रेगएक्सपी('टेक्स्ट')`) के रूप में किया जाएगा। प्रत्येक कॉल का ट्रेस दिखाने के लिए इसे खाली रखें।", + "Log_Trace_Subscriptions": "सदस्यता कॉल ट्रेस करें", + "Log_Trace_Subscriptions_Filter": "सदस्यता फ़िल्टर ट्रेस करें", + "Log_Trace_Subscriptions_Filter_Description": "यहां टेक्स्ट का मूल्यांकन रेगएक्सपी (`नया रेगएक्सपी('टेक्स्ट')`) के रूप में किया जाएगा। प्रत्येक कॉल का ट्रेस दिखाने के लिए इसे खाली रखें।", + "Log_View_Limit": "लॉग दृश्य सीमा", + "Logged_Out_Banner_Text": "आपके कार्यक्षेत्र व्यवस्थापक ने इस उपकरण पर आपका सत्र समाप्त कर दिया। जारी रखने के लिए कृपया दोबारा लॉग इन करें।", + "Logged_out_of_other_clients_successfully": "अन्य ग्राहकों से सफलतापूर्वक लॉग आउट हो गया", + "Login": "लॉग इन करें", + "Log_in_to_sync": "सिंक करने के लिए लॉग इन करें", + "Login_Attempts": "लॉगिन प्रयास विफल", + "Login_Detected": "लॉगिन का पता चला", + "Logged_In_Via": "के माध्यम से लॉग इन किया गया", + "Login_Logs": "लॉगइन लॉग्स", + "Login_Logs_ClientIp": "विफल लॉगिन प्रयास लॉग पर क्लाइंट आईपी दिखाएं", + "Login_Logs_Enabled": "लॉग (कंसोल पर) विफल लॉगिन प्रयास", + "Login_Logs_ForwardedForIp": "विफल लॉगिन प्रयास लॉग पर अग्रेषित आईपी दिखाएं", + "Login_Logs_UserAgent": "विफल लॉगिन प्रयास लॉग पर UserAgent दिखाएं", + "Login_Logs_Username": "विफल लॉगिन प्रयास लॉग पर उपयोगकर्ता नाम दिखाएं", + "Login_with": "%s के साथ लॉगिन करें", + "Logistics": "रसद", + "Logout": "लॉग आउट", + "Logout_Others": "अन्य लॉग इन स्थानों से लॉगआउट करें", + "Logout_Device": "डिवाइस लॉग आउट करें", + "Log_out_devices_remotely": "डिवाइसों को दूरस्थ रूप से लॉग आउट करें", + "logout-device-management": "लॉगआउट डिवाइस प्रबंधन", + "logout-device-management_description": "डिवाइस प्रबंधन डैशबोर्ड से अन्य उपयोगकर्ताओं को लॉगआउट करने की अनुमति", + "logout-other-user": "अन्य उपयोगकर्ता को लॉगआउट करें", + "logout-other-user_description": "अन्य उपयोगकर्ताओं को लॉगआउट करने की अनुमति", + "Logs": "लॉग्स", + "Logs_Description": "कॉन्फ़िगर करें कि सर्वर लॉग कैसे प्राप्त होते हैं।", + "Longest_chat_duration": "सबसे लंबी चैट period", + "Longest_reaction_time": "सबसे लंबा प्रतिक्रिया समय", + "Longest_response_time": "सबसे लंबा प्रतिक्रिया समय", + "Looked_for": "ढ़ूढ़ा", + "Low": "कम", + "Lowest": "निम्नतम", + "Mail_Message_Invalid_emails": "आपने एक या अधिक अमान्य ईमेल प्रदान किए हैं: %s", + "Mail_Message_Missing_subject": "आपको एक ईमेल विषय प्रदान करना होगा.", + "Mail_Message_Missing_to": "आपको एक या अधिक उपयोगकर्ताओं का चयन करना होगा या अल्पविराम से अलग करके एक या अधिक ईमेल पते प्रदान करने होंगे।", + "Mail_Message_No_messages_selected_select_all": "आपने कोई संदेश नहीं चुना है", + "Mail_Messages": "मेल संदेश", + "Mail_Messages_Instructions": "संदेशों पर क्लिक करके चुनें कि आप कौन से संदेश ईमेल के माध्यम से भेजना चाहते हैं", + "Mail_Messages_Subject": "यहां %s संदेशों का चयनित भाग है", + "mail-messages": "मेल संदेश", + "mail-messages_description": "मेल संदेश विकल्प का उपयोग करने की अनुमति", + "Mailer": "मेलर", + "Mailer_body_tags": "आपको अनसब्सक्रिप्शन लिंक के लिए [अनसब्सक्राइब] का उपयोग करना होगा
    आप उपयोगकर्ता के पूर्ण नाम, प्रथम नाम या अंतिम नाम के लिए क्रमशः `[name]`, `[fname]`, `[lname]` का उपयोग कर सकते हैं।
    आप उपयोगकर्ता के ईमेल के लिए [ईमेल] का उपयोग कर सकते हैं।", + "Mailing": "डाक", + "Make_Admin": "एडमिन बनाओ", + "Make_sure_you_have_a_copy_of_your_codes_1": "सुनिश्चित करें कि आपके पास अपने कोड की एक प्रति है:", + "Make_sure_you_have_a_copy_of_your_codes_2": "यदि आप अपने प्रमाणक ऐप तक पहुंच खो देते हैं, तो आप लॉग इन करने के लिए इनमें से किसी एक कोड का उपयोग कर सकते हैं।", + "Manage": "प्रबंधित करना", + "manage-agent-extension-association": "एजेंट एक्सटेंशन एसोसिएशन का प्रबंधन करें", + "manage-agent-extension-association_description": "एजेंट एक्सटेंशन एसोसिएशन को प्रबंधित करने की अनुमति", + "manage-apps": "एप्लिकेशन प्रबंधित", + "manage-apps_description": "सभी ऐप्स को प्रबंधित करने की अनुमति", + "manage-assets": "संपत्ति का प्रबंधन करें", + "manage-assets_description": "सर्वर संपत्तियों को प्रबंधित करने की अनुमति", + "manage-cloud": "बादल प्रबंधित करें", + "manage-cloud_description": "क्लाउड को प्रबंधित करने की अनुमति", + "Manage_Devices": "डिवाइस प्रबंधित करें", + "manage-email-inbox": "ईमेल इनबॉक्स प्रबंधित करें", + "manage-email-inbox_description": "ईमेल इनबॉक्स प्रबंधित करने की अनुमति", + "manage-emoji": "इमोजी प्रबंधित करें", + "manage-emoji_description": "सर्वर इमोजी को प्रबंधित करने की अनुमति", + "messages_pruned": "संदेशों की काट-छाँट की गई", + "manage-incoming-integrations": "आने वाले एकीकरणों को प्रबंधित करें", + "manage-incoming-integrations_description": "सर्वर आने वाली एकीकरणों को प्रबंधित करने की अनुमति", + "manage-integrations": "एकीकरण प्रबंधित करें", + "manage-integrations_description": "सर्वर एकीकरण को प्रबंधित करने की अनुमति", + "manage-livechat-agents": "ओमनीचैनल एजेंटों को प्रबंधित करें", + "manage-livechat-agents_description": "सर्वचैनल एजेंटों को प्रबंधित करने की अनुमति", + "manage-livechat-canned-responses": "ओमनीचैनल डिब्बाबंद प्रतिक्रियाएँ प्रबंधित करें", + "manage-livechat-canned-responses_description": "सर्वचैनल डिब्बाबंद प्रतिक्रियाओं को प्रबंधित करने की अनुमति", + "manage-livechat-departments": "ओमनीचैनल विभागों का प्रबंधन करें", + "manage-livechat-departments_description": "सर्वचैनल विभागों को प्रबंधित करने की अनुमति", + "manage-livechat-managers": "ओमनीचैनल प्रबंधकों को प्रबंधित करें", + "manage-livechat-managers_description": "सर्वचैनल प्रबंधकों को प्रबंधित करने की अनुमति", + "manage-livechat-monitors": "ओमनीचैनल मॉनिटर्स प्रबंधित करें", + "manage-livechat-monitors_description": "ओमनीचैनल मॉनिटर प्रबंधित करने की अनुमति", + "manage-livechat-priorities": "ओमनीचैनल प्राथमिकताएँ प्रबंधित करें", + "manage-livechat-priorities_description": "सर्वचैनल प्राथमिकताओं को प्रबंधित करने की अनुमति", + "manage-livechat-sla": "ओमनीचैनल SLA प्रबंधित करें", + "manage-livechat-sla_description": "सर्वचैनल एसएलए को प्रबंधित करने की अनुमति", + "manage-livechat-tags": "ओमनीचैनल टैग प्रबंधित करें", + "manage-livechat-tags_description": "ओमनीचैनल टैग प्रबंधित करने की अनुमति", + "manage-livechat-units": "ओमनीचैनल इकाइयों का प्रबंधन करें", + "manage-livechat-units_description": "सर्वचैनल इकाइयों को प्रबंधित करने की अनुमति", + "manage-oauth-apps": "OAuth ऐप्स प्रबंधित करें", + "manage-oauth-apps_description": "सर्वर OAuth ऐप्स को प्रबंधित करने की अनुमति", + "manage-outgoing-integrations": "आउटगोइंग एकीकरण प्रबंधित करें", + "manage-outgoing-integrations_description": "सर्वर आउटगोइंग एकीकरणों को प्रबंधित करने की अनुमति", + "manage-own-incoming-integrations": "स्वयं के आने वाले एकीकरणों को प्रबंधित करें", + "manage-own-incoming-integrations_description": "उपयोगकर्ताओं को अपने स्वयं के आने वाले एकीकरण या वेबहुक बनाने और संपादित करने की अनुमति", + "manage-own-integrations": "स्वयं के एकीकरण प्रबंधित करें", + "manage-own-integrations_description": "उपयोगकर्ताओं को अपना स्वयं का एकीकरण या वेबहुक बनाने और संपादित करने की अनुमति", + "manage-own-outgoing-integrations": "स्वयं के आउटगोइंग एकीकरणों को प्रबंधित करें", + "manage-own-outgoing-integrations_description": "उपयोगकर्ताओं को अपने स्वयं के आउटगोइंग एकीकरण या वेबहुक बनाने और संपादित करने की अनुमति", + "manage-selected-settings": "कुछ सेटिंग्स बदलें", + "manage-selected-settings_description": "सेटिंग्स को बदलने की अनुमति जो स्पष्ट रूप से बदलने के लिए दी गई है", + "manage-sounds": "ध्वनियाँ प्रबंधित करें", + "manage-sounds_description": "सर्वर ध्वनियों को प्रबंधित करने की अनुमति", + "manage-the-app": "ऐप प्रबंधित करें", + "manage-user-status": "उपयोगकर्ता स्थिति प्रबंधित करें", + "manage-user-status_description": "सर्वर कस्टम उपयोगकर्ता स्थितियों को प्रबंधित करने की अनुमति", + "manage-voip-call-settings": "वीओआईपी कॉल सेटिंग्स प्रबंधित करें", + "manage-voip-call-settings_description": "वीओआईपी कॉल सेटिंग प्रबंधित करने की अनुमति", + "manage-voip-contact-center-settings": "वीओआईपी संपर्क केंद्र सेटिंग्स प्रबंधित करें", + "manage-voip-contact-center-settings_description": "वीओआईपी संपर्क केंद्र सेटिंग्स को प्रबंधित करने की अनुमति", + "Manage_Omnichannel": "ओमनीचैनल प्रबंधित करें", + "Manage_workspace": "कार्यक्षेत्र प्रबंधित करें", + "Manager_added": "प्रबंधक जोड़ा गया", + "Manager_removed": "मैनेजर को हटा दिया गया", + "Managers": "प्रबंधकों", + "Manage_server_list": "सर्वर सूची प्रबंधित करें", + "Manage_servers": "सर्वर प्रबंधित करें", + "Manage_which_devices": "सुरक्षा सुनिश्चित करने में सहायता के लिए प्रबंधित करें कि कौन से उपकरण इस कार्यक्षेत्र से कनेक्ट हो रहे हैं। डिवाइस आईडी, लॉगिन डेटा जैसी जानकारी शामिल है और डिवाइस को दूरस्थ रूप से लॉग आउट करने की क्षमता भी शामिल है।", + "Management_Server": "तारांकन प्रबंधक इंटरफ़ेस (एएमआई)", + "Managing_assets": "संपत्ति का प्रबंधन", + "Managing_integrations": "एकीकरण का प्रबंधन", + "Manual_Selection": "मैन्युअल चयन", + "Manufacturing": "उत्पादन", + "MapView_Enabled": "मैपव्यू सक्षम करें", + "MapView_Enabled_Description": "मैपव्यू सक्षम करने से चैट इनपुट फ़ील्ड के दाईं ओर एक स्थान साझा बटन प्रदर्शित होगा।", + "MapView_GMapsAPIKey": "गूगल स्टेटिक मैप्स एपीआई कुंजी", + "MapView_GMapsAPIKey_Description": "इसे Google डेवलपर्स कंसोल से निःशुल्क प्राप्त किया जा सकता है।", + "Mark_all_as_read": "`%s` - सभी संदेशों को (सभी चैनलों में) पढ़े गए के रूप में चिह्नित करें", + "Mark_as_read": "पढ़े हुए का चिह्न", + "Mark_as_unread": "अपठित के रूप में चिह्नित करें", + "Mark_read": "पढ़ा हुआ चिह्नित करें", + "Mark_unread": "अपठित चिन्हित करो", + "Marketplace": "बाजार", + "Marketplace_app_last_updated": "अंतिम बार अद्यतन किया गया {{lastUpdated}}", + "Marketplace_view_marketplace": "बाज़ार देखें", + "Marketplace_error": "इंटरनेट से कनेक्ट नहीं हो सकता या आपका कार्यक्षेत्र ऑफ़लाइन इंस्टॉल हो सकता है।", + "MAU_value": "हमेशा {{price}}", + "Max_length_is": "अधिकतम लंबाई %s है", + "Max_number_incoming_livechats_displayed": "कतार में प्रदर्शित वस्तुओं की अधिकतम संख्या", + "Max_number_incoming_livechats_displayed_description": "(वैकल्पिक) आने वाली ओमनीचैनल कतार में प्रदर्शित आइटमों की अधिकतम संख्या।", + "Max_number_of_chats_per_agent": "अधिकतम. एक साथ चैट की संख्या", + "Max_number_of_chats_per_agent_description": "अधिकतम. एक साथ होने वाली चैट की संख्या जिसमें एजेंट भाग ले सकते हैं", + "Max_number_of_uses": "उपयोग की अधिकतम संख्या", + "Max_Retry": "सर्वर से पुनः कनेक्ट करने का अधिकतम प्रयास", + "Maximum": "अधिकतम", + "Maximum_number_of_guests_reached": "सबसे ज्यादा संख्या में मेहमान पहुंचे", + "Me": "मुझे", + "Media": "मिडिया", + "Medium": "मध्यम", + "Members": "सदस्यों", + "Members_List": "सदस्यों की सूची", + "mention-all": "सभी का उल्लेख करें", + "mention-all_description": "@all उल्लेख का उपयोग करने की अनुमति", + "Mentions_all_room_members": "कक्ष के सभी सदस्यों का उल्लेख करता है", + "Mentions_online_room_members": "ऑनलाइन रूम के सदस्यों का उल्लेख करता है", + "Mentions_user": "उपयोगकर्ता का उल्लेख करता है", + "Mentions_channel": "चैनल का उल्लेख है", + "Mentions_you": "आपका जिक्र करता हूं", + "mention-here": "यहां उल्लेख करें", + "mention-here_description": "@यहाँ उल्लेख का उपयोग करने की अनुमति", + "Mentions": "का उल्लेख है", + "Mentions_default": "उल्लेख (डिफ़ॉल्ट)", + "Mentions_only": "केवल उल्लेख है", + "Mentions_with_@_symbol": "@ चिन्ह के साथ उल्लेख", + "Mentions_with_@_symbol_description": "लक्षित संचार की सुविधा प्रदान करते हुए, समूहों या विशिष्ट उपयोगकर्ताओं के लिए संदेशों को सूचित और हाइलाइट किया जाता है।\n\nजब उल्लेख सुविधा में \"@\" प्रतीक का उपयोग किया जाता है तो स्क्रीन रीडर की कार्यक्षमता अनुकूलित हो जाती है। यह सुनिश्चित करता है कि स्क्रीन रीडर पर भरोसा करने वाले उपयोगकर्ता इन उल्लेखों की आसानी से व्याख्या कर सकते हैं और उनसे जुड़ सकते हैं।", + "Merge_Channels": "चैनल मर्ज करें", + "message": "संदेश", + "Message": "संदेश", + "Message_Description": "संदेश सेटिंग कॉन्फ़िगर करें.", + "Message_AllowBadWordsFilter": "संदेश को बुरे शब्दों को फ़िल्टर करने की अनुमति दें", + "Message_AllowConvertLongMessagesToAttachment": "लंबे संदेशों को अनुलग्नक में परिवर्तित करने की अनुमति दें", + "Message_AllowDeleting": "संदेश हटाने की अनुमति दें", + "Message_AllowDeleting_BlockDeleteInMinutes": "(एन) मिनट के बाद संदेश को ब्लॉक करें", + "Message_AllowDeleting_BlockDeleteInMinutes_Description": "अवरोधन अक्षम करने के लिए 0 दर्ज करें.", + "Message_AllowDirectMessagesToYourself": "उपयोगकर्ता को अपने लिए सीधे संदेश भेजने की अनुमति दें", + "Message_AllowEditing": "संदेश संपादन की अनुमति दें", + "Message_AllowEditing_BlockEditInMinutes": "(n) मिनट के बाद संदेश संपादन को ब्लॉक करें", + "Message_AllowEditing_BlockEditInMinutesDescription": "अवरोधन अक्षम करने के लिए 0 दर्ज करें.", + "Message_AllowPinning": "संदेश पिन करने की अनुमति दें", + "Message_AllowPinning_Description": "संदेशों को किसी भी चैनल पर पिन करने की अनुमति दें।", + "Message_AllowStarring": "संदेश को तारांकित करने की अनुमति दें", + "Message_AllowUnrecognizedSlashCommand": "अज्ञात स्लैश कमांड की अनुमति दें", + "Message_Already_Sent": "यह संदेश पहले ही भेजा जा चुका है और सर्वर द्वारा संसाधित किया जा रहा है", + "Message_AlwaysSearchRegExp": "हमेशा RegExp का उपयोग करके खोजें", + "Message_AlwaysSearchRegExp_Description": "यदि आपकी भाषा [MongoDB टेक्स्ट सर्च](https://docs.mongodb.org/manual/reference/text-search-भाषाओं/#text-search-भाषाओं) पर समर्थित नहीं है, तो हम `True` सेट करने की अनुशंसा करते हैं।", + "Message_Attachments": "संदेश अनुलग्नक", + "Message_Attachments_Thumbnails_Enabled": "बैंडविथ को बचाने के लिए छवि थंबनेल सक्षम करें", + "Message_Attachments_Thumbnails_Width": "थंबनेल की अधिकतम चौड़ाई (पिक्सेल में)", + "Message_Attachments_Thumbnails_Height": "थंबनेल की अधिकतम ऊंचाई (पिक्सेल में)", + "Message_with_attachment": "अनुलग्नक के साथ संदेश", + "Report_sent": "सूचना भेजी गई", + "Message_Attachments_Thumbnails_EnabledDesc": "बैंडविथ उपयोग को कम करने के लिए मूल छवि के स्थान पर थंबनेल प्रस्तुत किए जाएंगे। अनुलग्नक के नाम के आगे वाले आइकन का उपयोग करके मूल रिज़ॉल्यूशन वाली छवियां डाउनलोड की जा सकती हैं।", + "Message_Attachments_Strip_Exif": "समर्थित फ़ाइलों से EXIF मेटाडेटा हटाएँ", + "Message_Attachments_Strip_ExifDescription": "छवि फ़ाइलों (jpeg, tiff, आदि) से EXIF मेटाडेटा को हटा देता है। यह सेटिंग पूर्वव्यापी नहीं है, इसलिए अक्षम होने पर अपलोड की गई फ़ाइलों में EXIF डेटा होगा", + "Message_Audio": "ऑडियो संदेश", + "Message_Audio_bitRate": "ऑडियो संदेश बिट दर", + "Message_AudioRecorderEnabled": "ऑडियो रिकॉर्डर सक्षम", + "Message_AudioRecorderEnabled_Description": "'फ़ाइल अपलोड' सेटिंग्स के अंतर्गत 'ऑडियो/एमपी3' फ़ाइलों को एक स्वीकृत मीडिया प्रकार होना आवश्यक है।", + "Message_Audio_Recording_Disabled": "संदेश ऑडियो रिकॉर्डिंग अक्षम की गई", + "Message_auditing": "संदेशों का ऑडिट करें", + "Message_auditing_log": "ऑडिट लॉग", + "Message_BadWordsFilterList": "बुरे शब्दों को काली सूची में जोड़ें", + "Message_BadWordsFilterListDescription": "फ़िल्टर करने के लिए बुरे शब्दों की अल्पविराम से अलग की गई सूची जोड़ें", + "Message_BadWordsWhitelist": "ब्लैकलिस्ट से शब्द हटाएँ", + "Message_BadWordsWhitelistDescription": "फ़िल्टर से हटाए जाने वाले शब्दों की अल्पविराम से अलग की गई सूची जोड़ें", + "Message_Characther_Limit": "संदेश वर्ण सीमा", + "Message_Code_highlight": "कोड हाइलाइटिंग भाषाओं की सूची", + "Message_Code_highlight_Description": "अल्पविराम से अलग की गई भाषाओं की सूची (सभी समर्थित भाषाएं [highlight.js](https://github.com/highlightjs/highlight.js/tree/11.6.0#supported-भाषाएं) पर) जिनका उपयोग कोड ब्लॉक को हाइलाइट करने के लिए किया जाएगा", + "Message_CustomDomain_AutoLink": "ऑटो लिंक के लिए कस्टम डोमेन श्वेतसूची", + "Message_CustomDomain_AutoLink_Description": "यदि आप `https://internaltool.intranet` या `internaltool.intranet` जैसे आंतरिक लिंक को ऑटो लिंक करना चाहते हैं, तो आपको फ़ील्ड में `इंट्रानेट` डोमेन जोड़ना होगा, कई डोमेन को अल्पविराम से अलग करना होगा।", + "message_counter": "{{counter}} संदेश", + "Message_DateFormat": "तारिख का प्रारूप", + "Message_DateFormat_Description": "यह भी देखें: [Moment.js](http://momentjs.com/docs/#/displaying/format/)", + "Message_deleting_blocked": "यह संदेश अब हटाया नहीं जा सकता", + "Message_editing": "संदेश संपादन", + "Message_ErasureType": "संदेश मिटाने का प्रकार", + "Message_ErasureType_Delete": "सभी संदेश हटाएँ", + "Message_ErasureType_Description": "निर्धारित करें कि उन उपयोगकर्ताओं के संदेशों का क्या करना है जो अपना खाता हटाते हैं।\n - **संदेश और उपयोगकर्ता नाम रखें:** उपयोगकर्ता का संदेश और फ़ाइल इतिहास सीधे संदेशों से हटा दिया जाएगा लेकिन अन्य कमरों में रखा जाएगा।\n - **सभी संदेश हटाएं:** उपयोगकर्ता के सभी संदेश और फ़ाइलें डेटाबेस से हटा दी जाएंगी और अब उपयोगकर्ता का पता लगाना संभव नहीं होगा।\n - **उपयोगकर्ता और संदेशों के बीच लिंक हटाएं:** यह विकल्प उपयोगकर्ता के सभी संदेशों और फ़ाइलों को Rocket.Cat बॉट को सौंप देगा और डायरेक्ट संदेश हटा दिए जाएंगे।", + "Message_ErasureType_Keep": "संदेश और उपयोगकर्ता नाम रखें", + "Message_ErasureType_Unlink": "उपयोगकर्ता और संदेशों के बीच लिंक हटाएँ", + "Message_GlobalSearch": "वैश्विक खोज", + "Message_GroupingPeriod": "समूहीकरण period (सेकंड में)", + "Message_GroupingPeriodDescription": "संदेशों को पिछले संदेश के साथ समूहीकृत किया जाएगा यदि दोनों एक ही उपयोगकर्ता के हैं और बीता हुआ समय सेकंड में सूचित समय से कम था।", + "Message_has_been_edited": "संदेश संपादित कर दिया गया है", + "Message_has_been_edited_at": "संदेश को {{date}} पर संपादित किया गया है", + "Message_has_been_edited_by": "संदेश को {{username}} द्वारा संपादित किया गया है", + "Message_has_been_edited_by_at": "संदेश को {{username}} द्वारा {{date}} पर संपादित किया गया है", + "Message_has_been_forwarded": "संदेश अग्रेषित कर दिया गया है", + "Message_has_been_pinned": "संदेश पिन कर दिया गया है", + "Message_has_been_starred": "संदेश तारांकित कर दिया गया है", + "Message_has_been_unpinned": "संदेश अनपिन कर दिया गया है", + "Message_has_been_unstarred": "संदेश अतारांकित कर दिया गया है", + "Message_HideType_au": "\"उपयोगकर्ता द्वारा जोड़े गए\" संदेशों को छिपाएँ", + "Message_HideType_added_user_to_team": "\"उपयोगकर्ता को टीम में जोड़ा गया\" संदेश छिपाएँ", + "Message_HideType_mute_unmute": "\"उपयोगकर्ता द्वारा म्यूट/अनम्यूट किए गए\" संदेशों को छुपाएं", + "Message_HideType_r": "\"कमरे का नाम बदला गया\" संदेश छिपाएँ", + "Message_HideType_rm": "\"संदेश हटाया गया\" संदेश छिपाएँ", + "Message_HideType_room_allowed_reacting": "\"कमरे में प्रतिक्रिया देने की अनुमति है\" संदेश छिपाएँ", + "Message_HideType_room_archived": "\"कक्ष संग्रहीत\" संदेश छिपाएँ", + "Message_HideType_room_changed_avatar": "\"कक्ष का अवतार बदल गया\" संदेश छिपाएँ", + "Message_HideType_room_changed_privacy": "\"कमरे का प्रकार बदल गया\" संदेश छिपाएँ", + "Message_HideType_room_changed_topic": "\"कक्ष का विषय बदल गया\" संदेश छिपाएँ", + "Message_HideType_room_disallowed_reacting": "\"कमरे में प्रतिक्रिया की अनुमति नहीं\" संदेश छिपाएँ", + "Message_HideType_room_enabled_encryption": "\"कक्ष एन्क्रिप्शन सक्षम\" संदेश छिपाएँ", + "Message_HideType_room_disabled_encryption": "\"कक्ष एन्क्रिप्शन अक्षम\" संदेश छिपाएँ", + "Message_HideType_room_set_read_only": "\"रूम सेट केवल पढ़ने के लिए\" संदेश छिपाएँ", + "Message_HideType_room_removed_read_only": "\"कमरा जोड़ा गया लेखन अनुमति\" संदेश छिपाएँ", + "Message_HideType_room_unarchived": "\"कक्ष अनासंग्रहीत\" संदेश छिपाएँ", + "Message_HideType_ru": "\"उपयोगकर्ता द्वारा निकाले गए\" संदेश छिपाएँ", + "Message_HideType_removed_user_from_team": "\"उपयोगकर्ता को टीम से निकाला गया\" संदेश छिपाएँ", + "Message_HideType_subscription_role_added": "\"क्या भूमिका निर्धारित थी\" संदेश छिपाएँ", + "Message_HideType_subscription_role_removed": "\"भूमिका अब परिभाषित नहीं\" संदेश छिपाएँ", + "Message_HideType_uj": "\"उपयोगकर्ता जुड़ें\" संदेश छिपाएँ", + "Message_HideType_ujt": "\"टीम में शामिल उपयोगकर्ता\" संदेश छिपाएँ", + "Message_HideType_ul": "\"उपयोगकर्ता छोड़ें\" संदेश छुपाएं", + "Message_HideType_ult": "\"उपयोगकर्ता बाएँ टीम\" संदेश छिपाएँ", + "Message_HideType_user_added_room_to_team": "\"उपयोगकर्ता द्वारा टीम में जोड़ा गया कमरा\" संदेश छिपाएँ", + "Message_HideType_user_converted_to_channel": "\"उपयोगकर्ता द्वारा एक चैनल में परिवर्तित टीम\" संदेशों को छुपाएं", + "Message_HideType_user_converted_to_team": "\"उपयोगकर्ता द्वारा टीम में परिवर्तित चैनल\" संदेशों को छुपाएं", + "Message_HideType_user_deleted_room_from_team": "\"उपयोगकर्ता द्वारा टीम से हटाया गया कमरा\" संदेश छिपाएँ", + "Message_HideType_user_removed_room_from_team": "\"उपयोगकर्ता ने टीम से कमरा हटा दिया\" संदेश छुपाएं", + "Message_HideType_changed_description": "\"कमरे का विवरण बदल गया\" संदेशों को छिपाएँ", + "Message_HideType_changed_announcement": "\"कक्ष घोषणा परिवर्तित में\" संदेश छिपाएँ", + "Message_HideType_ut": "\"उपयोगकर्ता सम्मिलित वार्तालाप\" संदेश छिपाएँ", + "Message_HideType_wm": "\"स्वागत\" संदेश छिपाएँ", + "Message_Id": "संदेश आईडी", + "Message_Ignored": "इस संदेश को नजरअंदाज कर दिया गया", + "message-impersonate": "अन्य उपयोगकर्ताओं का प्रतिरूपण करें", + "message-impersonate_description": "संदेश उपनाम का उपयोग करके अन्य उपयोगकर्ताओं का प्रतिरूपण करने की अनुमति", + "Message_info": "संदेश जानकारी", + "Message_KeepHistory": "प्रति संदेश संपादन इतिहास रखें", + "Message_MaxAll": "सभी संदेशों के लिए अधिकतम चैनल आकार", + "Message_MaxAllowedSize": "प्रति संदेश अधिकतम अनुमत वर्ण", + "Message_pinning": "संदेश पिन करना", + "message_pruned": "संदेश काट दिया गया", + "Message_QuoteChainLimit": "जंजीरदार उद्धरणों की अधिकतम संख्या", + "Message_Read_Receipt_Enabled": "पढ़ी गई रसीदें दिखाएँ", + "Message_Read_Receipt_Store_Users": "विस्तृत पठन प्राप्तियाँ", + "Message_Read_Receipt_Store_Users_Description": "प्रत्येक उपयोगकर्ता की पढ़ी गई रसीदें दिखाता है", + "Message_removed": "संदेश हटा दिया गया", + "Message_is_removed": "संदेश हटा दिया गया", + "Message_sent_by_email": "ईमेल द्वारा भेजा गया संदेश", + "Message_ShowDeletedStatus": "हटाई गई स्थिति दिखाएँ", + "Message_Formatting_Toolbox": "फ़ॉर्मेटिंग टूलबॉक्स", + "Message_composer_toolbox_primary_actions": "संगीतकार प्राथमिक क्रियाएँ", + "Message_composer_toolbox_secondary_actions": "संगीतकार माध्यमिक क्रियाएँ", + "Message_starring": "संदेश अभिनीत", + "Message_Time": "संदेश का समय", + "Message_TimeAndDateFormat": "समय और दिनांक प्रारूप", + "Message_TimeAndDateFormat_Description": "यह भी देखें: [Moment.js](http://momentjs.com/docs/#/displaying/format/)", + "Message_TimeFormat": "समय स्वरूप", + "Message_TimeFormat_Description": "यह भी देखें: [Moment.js](http://momentjs.com/docs/#/displaying/format/)", + "Message_too_long": "संदेश बहुत लंबा है", + "Message_UserId": "उपयोगकर्ता पहचान", + "Message_view_mode_info": "इससे स्क्रीन पर संदेशों द्वारा ली जाने वाली जगह की मात्रा बदल जाती है।", + "Message_VideoRecorderEnabled": "वीडियो रिकॉर्डर सक्षम", + "Message_Video_Recording_Disabled": "संदेश वीडियो रिकॉर्डिंग अक्षम की गई", + "MessageBox_view_mode": "संदेशबॉक्स दृश्य मोड", + "Message_VideoRecorderEnabledDescription": "'फ़ाइल अपलोड' सेटिंग्स के अंतर्गत 'वीडियो/वेबएम' फ़ाइलों को एक स्वीकृत मीडिया प्रकार होना आवश्यक है।", + "messages": "संदेशों", + "Messages": "संदेशों", + "Messages_selected": "संदेश चयनित", + "Messages_sent": "संदेश भेजे गए", + "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "इनकमिंग वेबहुक पर भेजे गए संदेश यहां पोस्ट किए जाएंगे।", + "Meta": "मेटा", + "Meta_Description": "कस्टम मेटा गुण सेट करें.", + "Meta_custom": "कस्टम मेटा टैग", + "Meta_fb_app_id": "फेसबुक ऐप आईडी", + "Meta_google-site-verification": "Google साइट सत्यापन", + "Meta_language": "भाषा", + "Meta_msvalidate01": "MSValidate.01", + "Meta_robots": "रोबोटों", + "meteor_status_connected": "जुड़े हुए", + "meteor_status_connecting": "कनेक्ट हो रहा है...", + "meteor_status_failed": "सर्वर कनेक्शन विफल रहा", + "meteor_status_offline": "ऑफ़लाइन मोड।", + "meteor_status_reconnect_in": "एक सेकंड में पुनः प्रयास कर रहा हूँ...", + "meteor_status_try_now_offline": "पुनः कनेक्ट करें", + "meteor_status_try_now_waiting": "अब कोशिश करो", + "meteor_status_waiting": "सर्वर कनेक्शन की प्रतीक्षा में,", + "Method": "तरीका", + "Mic_on": "माइक ऑन", + "Microphone": "माइक्रोफ़ोन", + "Microphone_access_not_allowed": "माइक्रोफ़ोन एक्सेस की अनुमति नहीं थी, कृपया अपनी ब्राउज़र सेटिंग जांचें।", + "Mic_off": "माइक बंद", + "Min_length_is": "न्यूनतम लंबाई %s है", + "Minimum": "न्यूनतम", + "Minimum_balance": "न्यूनतम शेष", + "minute": "मिनट", + "minutes": "मिनट", + "Missing_configuration": "अनुपलब्ध कॉन्फ़िगरेशन", + "Mobex_sms_gateway_address": "मोबेक्स एसएमएस गेटवे पता", + "Mobex_sms_gateway_address_desc": "निर्दिष्ट पोर्ट के साथ आपकी मोबेक्स सेवा का आईपी या होस्ट। जैसे `http://192.168.1.1:1401` या `https://www.example.com:1401`", + "Mobex_sms_gateway_from_number": "से", + "Mobex_sms_gateway_from_number_desc": "लाइवचैट क्लाइंट को नया एसएमएस भेजते समय मूल पता/फोन नंबर", + "Mobex_sms_gateway_from_numbers_list": "एसएमएस भेजने के लिए नंबरों की सूची", + "Mobex_sms_gateway_from_numbers_list_desc": "बिल्कुल नए संदेश भेजने में उपयोग करने के लिए संख्याओं की अल्पविराम से अलग की गई सूची, उदाहरण के लिए। 123456789, 123456788, 123456888", + "Mobex_sms_gateway_password": "पासवर्ड", + "Mobex_sms_gateway_restful_address": "मोबेक्स एसएमएस रेस्ट एपीआई पता", + "Mobex_sms_gateway_restful_address_desc": "आपके Mobex REST API का IP या होस्ट। जैसे `http://192.168.1.1:8080` या `https://www.example.com:8080`", + "Mobex_sms_gateway_username": "उपयोगकर्ता नाम", + "Mobile": "गतिमान", + "Mobile_apps": "मोबाइल क्षुधा", + "Mobile_Description": "मोबाइल उपकरणों से अपने कार्यक्षेत्र से जुड़ने के लिए व्यवहार को परिभाषित करें।", + "mobile-upload-file": "मोबाइल उपकरणों पर फ़ाइल अपलोड करने की अनुमति दें", + "mobile-upload-file_description": "मोबाइल उपकरणों पर फ़ाइल अपलोड करने की अनुमति", "Mobile_Push_Notifications_Default_Alert": "मोबाइल सूचनाएं डिफ़ॉल्ट चेतावनी", + "Moderation": "संयम", + "Moderation_Show_reports": "रिपोर्ट दिखाएँ", + "Moderation_Go_to_message": "संदेश पर जाएँ", + "Moderation_Delete_message": "संदेश को हटाएं", + "Moderation_Dismiss_and_delete": "ख़ारिज करें और हटाएं", + "Moderation_Delete_this_message": "इस संदेश को हटा दें", + "Moderation_Message_context_header": "रिपोर्ट किए गए संदेश", + "Moderation_Message_deleted": "संदेश हटा दिया गया और रिपोर्ट खारिज कर दी गईं", + "Moderation_Messages_deleted": "संदेश हटा दिए गए और रिपोर्ट खारिज कर दी गईं", + "Moderation_Action_View_reports": "रिपोर्ट किए गए संदेश देखें", + "Moderation_Hide_reports": "रिपोर्ट छुपाएं", + "Moderation_Dismiss_all_reports": "सभी रिपोर्ट खारिज करें", + "Moderation_Deactivate_User": "उपयोगकर्ता को निष्क्रिय करें", + "Moderation_User_deactivated": "उपयोगकर्ता निष्क्रिय कर दिया गया", + "Moderation_Delete_all_messages": "सभी संदेश हटाएँ", + "Moderation_Dismiss_reports": "रिपोर्ट खारिज करें", + "Moderation_Duplicate_messages": "डुप्लिकेट किए गए संदेश", + "Moderation_Duplicate_messages_warning": "निम्नलिखित में कई कमरों में भेजे गए समान संदेश शामिल हो सकते हैं।", + "Moderation_Report_date": "रिपोर्ट तिथि", + "Moderation_Reported_message": "रिपोर्ट किया गया संदेश", + "Moderation_Reports_dismissed": "रिपोर्ट खारिज कर दी गईं", + "Moderation_Message_already_deleted": "संदेश पहले ही हटा दिया गया है", + "Moderation_Reset_user_avatar": "उपयोगकर्ता अवतार रीसेट करें", + "Moderation_See_messages": "संदेश देखें", + "Moderation_Avatar_reset_success": "अवतार रीसेट", + "Moderation_Dismiss_reports_confirm": "रिपोर्टें हटा दी जाएंगी और रिपोर्ट किया गया संदेश प्रभावित नहीं होगा.", + "Moderation_Dismiss_all_reports_confirm": "सभी रिपोर्टें हटा दी जाएंगी और रिपोर्ट किए गए संदेश प्रभावित नहीं होंगे.", + "Moderation_Are_you_sure_you_want_to_delete_this_message": "यह संदेश उसके संबंधित कक्ष से स्थायी रूप से हटा दिया जाएगा और रिपोर्ट खारिज कर दी जाएगी।", + "Moderation_Are_you_sure_you_want_to_reset_the_avatar": "उपयोगकर्ता अवतार को रीसेट करने से उनका वर्तमान अवतार स्थायी रूप से हट जाएगा।", + "Moderation_Are_you_sure_you_want_to_deactivate_this_user": "पुनः सक्रिय होने तक उपयोगकर्ता लॉग इन नहीं कर पाएगा। सभी रिपोर्ट किए गए संदेशों को उनके संबंधित कमरे से स्थायी रूप से हटा दिया जाएगा।", + "Moderation_Are_you_sure_you_want_to_delete_all_reported_messages_from_this_user": "इस उपयोगकर्ता के सभी रिपोर्ट किए गए संदेशों को उनके संबंधित कमरे से स्थायी रूप से हटा दिया जाएगा और रिपोर्ट खारिज कर दी जाएगी।", + "Moderation_User_deleted_warning": "जिस उपयोगकर्ता ने संदेश भेजा था वह अब मौजूद नहीं है या उसे हटा दिया गया है।", + "Monday": "सोमवार", + "Mongo_storageEngine": "मोंगो स्टोरेज इंजन", + "Mongo_version": "मानगो संस्करण", + "MongoDB": "MongoDB", + "MongoDB_Deprecated": "MongoDB अस्वीकृत", + "MongoDB_version_s_is_deprecated_please_upgrade_your_installation": "MongoDB संस्करण %s अप्रचलित है, कृपया अपना इंस्टालेशन अपग्रेड करें।", + "Monitor_added": "मॉनिटर जोड़ा गया", + "Monitor_new_and_suspicious_logins": "नए और संदिग्ध लॉगिन की निगरानी करें", + "Monitor_history_for_changes_on": "परिवर्तनों के लिए इतिहास की निगरानी करें", + "Monitor_removed": "मॉनिटर हटा दिया गया", + "Monitors": "पर नज़र रखता है", + "Monthly_Active_Users": "मासिक सक्रिय उपयोगकर्ता", + "More": "अधिक", + "More_channels": "अधिक चैनल", + "More_direct_messages": "अधिक प्रत्यक्ष संदेश", + "More_groups": "अधिक निजी समूह", + "More_unreads": "अधिक अपठित", + "More_options": "अधिक विकल्प", + "Most_popular_channels_top_5": "सर्वाधिक लोकप्रिय चैनल (शीर्ष 5)", + "Most_recent_updated": "सबसे ताज़ा अपडेट किया गया", + "Most_recent_requested": "सबसे हाल ही में अनुरोध किया गया", + "Move_beginning_message": "`%s` - संदेश की शुरुआत में जाएँ", + "Move_end_message": "`%s` - संदेश के अंत में जाएँ", + "Move_queue": "कतार में जाएँ", + "Msgs": "संदेश", + "multi": "बहु", + "Multi_line": "मल्टी लाइन", + "Multiple_monolith_instances_alert": "आप सक्रिय प्रीमियम लाइसेंस के बिना कई इंस्टेंसेस का संचालन कर रहे हैं - हो सकता है कि कुछ सुविधाएँ डिज़ाइन के अनुसार व्यवहार न करें", + "Mute": "आवाज़ बंद करना", + "Mute_and_dismiss": "म्यूट करें और ख़ारिज करें", + "Mute_all_notifications": "सभी सूचनाएं म्यूट करें", + "Mute_Focused_Conversations": "केंद्रित वार्तालापों को म्यूट करें", + "Mute_Group_Mentions": "@सभी और @यहां उल्लेखों को म्यूट करें", + "Mute_someone_in_room": "कमरे में किसी को म्यूट करें", + "Mute_user": "उपयोगकर्ता को म्यूट करें", + "Mute_microphone": "माइक्रोफ़ोन म्यूट करें", + "mute-user": "उपयोगकर्ता को म्यूट करें", + "mute-user_description": "उसी चैनल में अन्य उपयोगकर्ताओं को म्यूट करने की अनुमति", + "Muted": "म्यूट किए गए", + "My Data": "मेरी जानकारी", + "My_Account": "मेरा खाता", + "My_location": "मेरा स्थान", + "n_messages": "%s संदेश", + "N_new_messages": "%s नए संदेश", + "Name": "नाम", + "Name_cant_be_empty": "नाम खाली नहीं हो सकता", + "Name_of_agent": "एजेंट का नाम", + "Name_optional": "नाम: (वैकल्पिक)", + "Name_Placeholder": "कृपया अपना नाम दर्ज करें...", + "Navigation": "मार्गदर्शन", + "Navigation_bar": "नेविगेशन पट्टी", + "Navigation_bar_description": "नेविगेशन बार का परिचय - एक उच्च-स्तरीय नेविगेशन जो उपयोगकर्ताओं को उनकी आवश्यकता के अनुसार शीघ्रता से ढूंढने में मदद करने के लिए डिज़ाइन किया गया है। अपने कॉम्पैक्ट डिज़ाइन और सहज संगठन के साथ, यह सुव्यवस्थित साइडबार आवश्यक सॉफ़्टवेयर सुविधाओं और अनुभागों तक आसान पहुँच प्रदान करते हुए स्क्रीन स्थान को अनुकूलित करता है।", + "Navigation_History": "नेविगेशन इतिहास", + "Next": "अगला", + "Never": "कभी नहीं", + "New": "नया", + "New_Application": "नए आवेदन", + "New_Business_Hour": "नया व्यावसायिक घंटा", + "New_Call": "नई कॉल", + "New_Call_Premium_Only": "नई कॉल (केवल प्रीमियम योजनाएं)", + "New_chat_in_queue": "कतार में नई चैट", + "New_chat_priority": "प्राथमिकता बदली गई: {{user}} ने प्राथमिकता को {{priority}} में बदल दिया", + "New_chat_transfer": "नया चैट स्थानांतरण: {{transfer}}", + "New_chat_transfer_fallback": "फ़ॉलबैक विभाग में स्थानांतरित: {{fallback}}", + "New_contact": "नया कॉन्ट्रैक्ट", + "New_Custom_Field": "नया कस्टम फ़ील्ड", + "New_Department": "नया विभाग", + "New_discussion": "नई चर्चा", + "New_discussion_first_message": "आमतौर पर, चर्चा एक प्रश्न से शुरू होती है, जैसे \"मैं एक तस्वीर कैसे अपलोड करूं?\"", + "New_discussion_name": "चर्चा कक्ष के लिए एक सार्थक नाम", + "New_Email_Inbox": "नया ईमेल इनबॉक्स", + "New_encryption_password": "नया एन्क्रिप्शन पासवर्ड", + "New_integration": "नया एकीकरण", + "New_line_message_compose_input": "`%s` - संदेश लिखें इनपुट में नई पंक्ति", + "New_Livechat_offline_message_has_been_sent": "एक नया लाइवचैट ऑफ़लाइन संदेश भेजा गया है", + "New_logs": "नये लॉग", + "New_Message_Notification": "नया संदेश अधिसूचना", "New_messages": "नए संदेश", + "New_OTR_Chat": "नई ओटीआर चैट", + "New_password": "नया पासवर्ड", + "New_Password_Placeholder": "कृपया नया पासवर्ड दर्ज करें...", + "New_Priority": "नई प्राथमिकता", + "New_SLA_Policy": "नई एसएलए नीति", + "New_role": "नयी भूमिका", + "New_Room_Notification": "नये कक्ष की अधिसूचना", + "New_Tag": "नया टैग", + "New_Trigger": "नया ट्रिगर", + "New_Unit": "नई इकाई", + "New_users": "नए उपयोगकर्ता", + "New_version_available_(s)": "नया संस्करण उपलब्ध है (%s)", + "New_videocall_request": "नया वीडियो कॉल अनुरोध", + "New_visitor_navigation": "नया नेविगेशन: {{history}}", + "New_workspace_confirmed": "नए कार्यक्षेत्र की पुष्टि की गई", + "New_workspace": "नया कार्यक्षेत्र", + "Newer_than": "से नया", + "Newer_than_may_not_exceed_Older_than": "\"इससे नया\" \"इससे पुराना\" से अधिक नहीं हो सकता", + "Nickname": "उपनाम", + "Nickname_Placeholder": "अपना उपनाम दर्ज करें...", "No": "नहीं", + "no-active-video-conf-provider": "**कॉन्फ़्रेंस कॉल सक्षम नहीं है**: कार्यस्थान व्यवस्थापक को पहले कॉन्फ़्रेंस कॉल सुविधा सक्षम करने की आवश्यकता है।", + "No_available_agents_to_transfer": "स्थानांतरण के लिए कोई एजेंट उपलब्ध नहीं है", + "No_app_matches": "कोई ऐप मेल नहीं खाता", + "No_app_matches_for": "कोई ऐप इससे मेल नहीं खाता", + "No_apps_installed": "कोई ऐप्स इंस्टॉल नहीं", + "No_Canned_Responses": "कोई डिब्बाबंद प्रतिक्रिया नहीं", + "No_Canned_Responses_Yet": "अभी तक कोई डिब्बाबंद प्रतिक्रिया नहीं", + "No_Canned_Responses_Yet-description": "अक्सर पूछे जाने वाले प्रश्नों के त्वरित और सुसंगत उत्तर प्रदान करने के लिए डिब्बाबंद प्रतिक्रियाओं का उपयोग करें।", + "No_channels_in_team": "इस टीम में कोई चैनल नहीं", + "No_agents_yet": "अभी तक कोई एजेंट नहीं", + "No_agents_yet_description": "अपने दर्शकों से जुड़ने और अनुकूलित ग्राहक सेवा प्रदान करने के लिए एजेंट जोड़ें।", + "No_channels_yet": "आप अभी तक किसी भी चैनल का हिस्सा नहीं हैं", + "No_chats_yet": "अभी तक कोई चैट नहीं", + "No_chats_yet_description": "आपकी सभी चैट यहां दिखाई देंगी.", + "No_calls_yet": "अभी तक कोई कॉल नहीं", + "No_calls_yet_description": "आपकी सभी कॉलें यहां दिखाई देंगी.", + "No_contacts_yet": "अभी तक कोई संपर्क नहीं", + "No_contacts_yet_description": "सभी संपर्क यहां दिखाई देंगे.", + "No_custom_fields_yet": "अभी तक कोई कस्टम फ़ील्ड नहीं", + "No_custom_fields_yet_description": "संपर्क या टिकट विवरण में कस्टम फ़ील्ड जोड़ें या उन्हें नए आगंतुकों के लिए लाइव चैट पंजीकरण फॉर्म पर प्रदर्शित करें।", + "No_departments_yet": "अभी तक कोई विभाग नहीं", + "No_departments_yet_description": "एजेंटों को विभागों में व्यवस्थित करें, टिकट कैसे अग्रेषित किए जाएं यह निर्धारित करें और उनके प्रदर्शन की निगरानी करें।", + "No_managers_yet": "अभी तक कोई प्रबंधक नहीं", + "No_managers_yet_description": "प्रबंधकों के पास सभी ओमनीचैनल नियंत्रणों तक पहुंच होती है, वे निगरानी करने और कार्रवाई करने में सक्षम होते हैं।", + "No_content_was_provided": "कोई सामग्री उपलब्ध नहीं करायी गयी", + "No_data_found": "डाटा प्राप्त नहीं हुआ", + "No_data_available_for_the_selected_period": "चयनित period के लिए कोई डेटा उपलब्ध नहीं है", + "No_direct_messages_yet": "कोई सीधा संदेश नहीं.", + "No_Discussions_found": "कोई चर्चा नहीं मिली", + "No_discussions_yet": "अभी तक कोई चर्चा नहीं", + "No_emojis_found": "कोई इमोजी नहीं मिला", + "No_Encryption": "कोई एन्क्रिप्शन नहीं", + "No_files_found": "कोई फाईल नहीं मिली", + "No_files_left_to_download": "डाउनलोड करने के लिए कोई फ़ाइल नहीं बची", + "No_groups_yet": "आपके पास अभी तक कोई निजी समूह नहीं है.", + "No_history": "कोई इतिहास नहीं", + "No_installed_app_matches": "कोई भी इंस्टॉल किया गया ऐप मेल नहीं खाता", + "No_integration_found": "प्रदत्त आईडी से कोई एकीकरण नहीं मिला।", + "No_Limit": "कोई सीमा नहीं", + "No_livechats": "आपके पास कोई लाइवचैट नहीं है", + "No_marketplace_matches_for": "इसके लिए कोई मार्केटप्लेस मेल नहीं खाता", + "No_members_found": "कोई सदस्य नहीं मिला", + "No_mentions_found": "कोई उल्लेख नहीं मिला", + "No_messages_found_to_prune": "काट-छाँट करने के लिए कोई संदेश नहीं मिला", + "No_messages_yet": "अभी तक कोई संदेश नहीं", + "No_monitors_yet": "अभी तक कोई मॉनिटर नहीं है", + "No_monitors_yet_description": "मॉनिटर्स के पास ओमनीचैनल का आंशिक नियंत्रण होता है। वे विभाग के विश्लेषण और उन्हें सौंपी गई व्यावसायिक इकाइयों की गतिविधियों को देख सकते हैं।", + "No_tags_yet": "अभी तक कोई टैग नहीं", + "No_tags_yet_description": "संबंधित वार्तालापों को व्यवस्थित करना और ढूंढना आसान बनाने के लिए टिकटों में टैग जोड़ें।", + "No_triggers_yet": "अभी तक कोई ट्रिगर नहीं", + "No_triggers_yet_description": "ट्रिगर ऐसी घटनाएँ हैं जो लाइव चैट विजेट को खोलने और स्वचालित रूप से संदेश भेजने का कारण बनती हैं।", + "No_units_yet": "अभी तक कोई इकाई नहीं", + "No_units_yet_description": "विभागों को समूहीकृत करने और उन्हें बेहतर ढंग से प्रबंधित करने के लिए इकाइयों का उपयोग करें।", + "No_pages_yet_Try_hitting_Reload_Pages_button": "अभी तक कोई पेज नहीं. \"रीलोड पेज\" बटन दबाने का प्रयास करें।", + "No_pinned_messages": "कोई पिन किया हुआ संदेश नहीं", + "No_previous_chat_found": "कोई पिछली चैट नहीं मिली", + "No_release_information_provided": "कोई रिलीज़ सूचना नहीं दी गई", + "No_requested_apps": "कोई अनुरोधित ऐप्स नहीं", + "No_requests": "कोई अनुरोध नहीं", + "No_results_found": "कोई परिणाम नहीं मिला", + "No_results_found_for": "इसके लिए कोई परिणाम नहीं मिला:", + "No_SLA_policies_yet": "अभी तक कोई SLA नीति नहीं", + "No_SLA_policies_yet_description": "अनुमानित प्रतीक्षा समय के आधार पर ओमनीचैनल कतारों का क्रम बदलने के लिए SLA नीतियों का उपयोग करें।", + "No_snippet_messages": "कोई स्निपेट नहीं", + "No_starred_messages": "कोई तारांकित संदेश नहीं", + "No_such_command": "ऐसा कोई आदेश नहीं: `/{{command}}`", + "No_Threads": "कोई सूत्र नहीं मिला", + "no-videoconf-provider-app": "**कॉन्फ़्रेंस कॉल उपलब्ध नहीं है**: कॉन्फ़्रेंस कॉल ऐप्स को कार्यस्थल व्यवस्थापक द्वारा रॉकेट.चैट मार्केटप्लेस में इंस्टॉल किया जा सकता है।", + "Nobody_available": "कोई भी उपलब्ध नहीं है", + "Node_version": "नोड संस्करण", + "None": "कोई नहीं", + "Nonprofit": "ग़ैर-लाभकारी", + "Not_authorized": "अधिकृत नहीं हैं", + "Normal": "सामान्य", + "Not_Available": "उपलब्ध नहीं है", + "Not_assigned": "सौंपा नहीं गया है", + "Not_enough_data": "पर्याप्त डेटा नहीं", + "Not_following": "पालन नहीं करते हुए", + "Not_Following": "पालन नहीं करते हुए", + "Not_found_or_not_allowed": "नहीं मिला या अनुमति नहीं है", + "Not_Imported_Messages_Title": "निम्नलिखित संदेश सफलतापूर्वक आयात नहीं किए गए", + "Not_in_channel": "चैनल में नहीं", + "Not_likely": "संभावना नहीं", + "Not_started": "शुरू नहीं", + "Not_verified": "सत्यापित नहीं है", + "Not_Visible_To_Workspace": "कार्यस्थल पर दिखाई नहीं देता", + "Nothing": "कुछ नहीं", + "Nothing_found": "कुछ भी नहीं मिला", + "Notice_that_public_channels_will_be_public_and_visible_to_everyone": "ध्यान दें कि सार्वजनिक चैनल सार्वजनिक होंगे और सभी को दिखाई देंगे।", + "Notification_Desktop_Default_For": "के लिए डेस्कटॉप सूचनाएं दिखाएं", + "Notification_Push_Default_For": "के लिए पुश सूचनाएँ भेजें", + "Notification_RequireInteraction": "डेस्कटॉप अधिसूचना को ख़ारिज करने के लिए सहभागिता की आवश्यकता है", + "Notification_RequireInteraction_Description": "केवल क्रोम ब्राउज़र संस्करण> 50 के साथ काम करता है। जब तक उपयोगकर्ता इसके साथ इंटरैक्ट नहीं करता तब तक डेस्कटॉप अधिसूचना को अनिश्चित काल तक दिखाने के लिए *requireInteraction* पैरामीटर का उपयोग करता है।", + "Notifications": "सूचनाएं", + "Notifications_Max_Room_Members": "सभी संदेश सूचनाओं को अक्षम करने से पहले मैक्स रूम के सदस्य", + "Notifications_Max_Room_Members_Description": "जब सभी संदेशों के लिए सूचनाएं अक्षम हो जाती हैं तो कमरे में सदस्यों की अधिकतम संख्या। उपयोगकर्ता व्यक्तिगत आधार पर सभी सूचनाएं प्राप्त करने के लिए अभी भी प्रति कमरा सेटिंग बदल सकते हैं। (0 अक्षम करने के लिए)", + "Notifications_Muted_Description": "यदि आप सब कुछ म्यूट करना चुनते हैं, तो उल्लेखों को छोड़कर, नए संदेश आने पर आपको सूची में रूम हाइलाइट नहीं दिखाई देगा। सूचनाओं को म्यूट करने से सूचना सेटिंग ओवरराइड हो जाएंगी.", + "Notifications_Preferences": "अधिसूचना प्राथमिकताएँ", + "Notifications_Sound_Volume": "सूचनाएं ध्वनि की मात्रा", + "Notify_active_in_this_room": "इस कक्ष में सक्रिय उपयोगकर्ताओं को सूचित करें", + "Notify_all_in_this_room": "इस कमरे में सभी को सूचित करें", + "Notify_Calendar_Events": "कैलेंडर घटनाओं को सूचित करें", + "Now_Its_Visible_For_Everyone": "अब यह सबके लिए दृश्यमान है", + "Now_Its_Visible_Only_For_Admins": "अब यह केवल व्यवस्थापकों के लिए दृश्यमान है", + "NPS_survey_enabled": "एनपीएस सर्वेक्षण सक्षम करें", + "NPS_survey_enabled_Description": "सभी उपयोगकर्ताओं के लिए एनपीएस सर्वेक्षण चलाने की अनुमति दें। सर्वेक्षण शुरू होने से 2 महीने पहले व्यवस्थापकों को एक अलर्ट प्राप्त होगा", + "NPS_survey_is_scheduled_to-run-at__date__for_all_users": "एनपीएस सर्वेक्षण सभी उपयोगकर्ताओं के लिए {{date}} पर चलने के लिए निर्धारित है। 'एडमिन > जनरल > एनपीएस' पर सर्वेक्षण को बंद करना संभव है", + "Default_Timezone_For_Reporting": "रिपोर्टिंग के लिए डिफ़ॉल्ट समयक्षेत्र", + "Default_Timezone_For_Reporting_Description": "डिफ़ॉल्ट समयक्षेत्र सेट करता है जिसका उपयोग डैशबोर्ड दिखाते समय या ईमेल भेजते समय किया जाएगा", + "Default_Server_Timezone": "सर्वर समय क्षेत्र", + "Default_Custom_Timezone": "कस्टम समय क्षेत्र", + "Default_User_Timezone": "उपयोगकर्ता का वर्तमान समय क्षेत्र", + "Num_Agents": "#एजेंट", + "Number_in_seconds": "सेकंड में नंबर", + "Number_of_events": "घटनाओं की संख्या", + "Number_of_federated_servers": "फ़ेडरेटेड सर्वरों की संख्या", + "Number_of_federated_users": "फ़ेडरेटेड उपयोगकर्ताओं की संख्या", + "Number_of_messages": "संदेशों की संख्या", + "Number_of_most_recent_chats_estimate_wait_time": "अनुमानित प्रतीक्षा समय की गणना करने के लिए हाल की चैट की संख्या", + "Number_of_most_recent_chats_estimate_wait_time_description": "यह संख्या अंतिम सेवा वाले कमरों की संख्या को परिभाषित करती है जिनका उपयोग कतार प्रतीक्षा समय की गणना के लिए किया जाएगा।", + "Number_of_users_autocomplete_suggestions": "उपयोगकर्ताओं के स्वत: पूर्ण सुझावों की संख्या", + "OAuth": "OAuth", + "OAuth_Description": "केवल उपयोगकर्ता नाम और पासवर्ड से परे प्रमाणीकरण विधियों को कॉन्फ़िगर करें।", + "OAuth_Application": "OAuth आवेदन", + "Objects": "वस्तुओं", + "Off": "बंद", + "Off_the_record_conversation": "ऑफ-द-रिकॉर्ड बातचीत", + "Off_the_record_conversation_is_not_available_for_your_browser_or_device": "ऑफ-द-रिकॉर्ड बातचीत आपके ब्राउज़र या डिवाइस के लिए उपलब्ध नहीं है।", + "Office_Hours": "कार्यालय period", + "Office_hours_enabled": "कार्यालय समय सक्षम", + "Office_hours_updated": "कार्यालय समय अद्यतन किया गया", + "offline": "ऑफलाइन", + "Offline": "ऑफलाइन", + "Offline_DM_Email": "सीधा संदेश ईमेल विषय", + "Offline_Email_Subject_Description": "आप निम्नलिखित प्लेसहोल्डर्स का उपयोग कर सकते हैं:\n - एप्लिकेशन नाम, यूआरएल, उपयोगकर्ता नाम और रूमनाम के लिए क्रमशः `[साइट_नाम]`, `[साइट_यूआरएल]`, `[उपयोगकर्ता]` और `[कक्ष]`।", + "Offline_form": "ऑफलाइन फॉर्म", + "Offline_form_unavailable_message": "ऑफ़लाइन फॉर्म अनुपलब्ध संदेश", + "Offline_Link_Message": "संदेश पर जाएँ", + "Offline_Mention_All_Email": "सभी ईमेल विषय का उल्लेख करें", + "Offline_Mention_Email": "ईमेल विषय का उल्लेख करें", + "Offline_message": "ऑफ़लाइन संदेश", + "Offline_Message": "ऑफ़लाइन संदेश", + "Offline_Message_Use_DeepLink": "डीप लिंक यूआरएल फ़ॉर्मेट का उपयोग करें", + "Offline_messages": "ऑफ़लाइन संदेश", + "Offline_success_message": "ऑफ़लाइन सफलता संदेश", + "Offline_unavailable": "ऑफ़लाइन अनुपलब्ध", + "Ok": "ठीक है", + "Old Colors": "पुराने रंग", + "Old Colors (minor)": "पुराने रंग (मामूली)", + "Older_than": "से अधिक पुराना", + "Omnichannel": "सर्वचैनल", + "Omnichannel_Description": "ग्राहकों के साथ एक ही स्थान से संवाद करने के लिए ओमनीचैनल सेट करें, भले ही वे आपके साथ कैसे भी जुड़े हों।", + "Omnichannel_Directory": "ओमनीचैनल निर्देशिका", + "Omnichannel_appearance": "ओम्नीचैनल उपस्थिति", + "Omnichannel_calculate_dispatch_service_queue_statistics": "ओमनीचैनल प्रतीक्षा कतार आँकड़ों की गणना और प्रेषण करें", + "Omnichannel_calculate_dispatch_service_queue_statistics_Description": "स्थिति और अनुमानित प्रतीक्षा समय जैसे प्रतीक्षा कतार आँकड़ों को संसाधित करना और भेजना। यदि *लाइवचैट चैनल* उपयोग में नहीं है, तो इस सेटिंग को अक्षम करने और सर्वर को अनावश्यक प्रक्रियाएं करने से रोकने की अनुशंसा की जाती है।", + "Omnichannel_Contact_Center": "ओमनीचैनल संपर्क केंद्र", + "Omnichannel_contact_manager_routing": "संपर्क प्रबंधक को नई बातचीत सौंपें", + "Omnichannel_contact_manager_routing_Description": "यह सेटिंग असाइन किए गए संपर्क प्रबंधक को एक चैट आवंटित करती है, जब तक कि चैट शुरू होने पर संपर्क प्रबंधक ऑनलाइन होता है", + "Omnichannel_External_Frame": "बाहरी फ़्रेम", + "Omnichannel_External_Frame_Enabled": "बाहरी फ़्रेम सक्षम", + "Omnichannel_External_Frame_Encryption_JWK": "एन्क्रिप्शन कुंजी (JWK)", + "Omnichannel_External_Frame_Encryption_JWK_Description": "यदि प्रदान किया गया है तो यह प्रदान की गई कुंजी के साथ उपयोगकर्ता के टोकन को एन्क्रिप्ट करेगा और बाहरी सिस्टम को टोकन तक पहुंचने के लिए डेटा को डिक्रिप्ट करने की आवश्यकता होगी", + "Omnichannel_External_Frame_URL": "बाहरी फ़्रेम यूआरएल", + "omnichannel_priority_change_history": "प्राथमिकता बदली गई: {{user}} ने प्राथमिकता को {{priority}} में बदल दिया", + "omnichannel_sla_change_history": "SLA नीति परिवर्तित: {{user}} ने SLA नीति को {{sla}} में बदल दिया", + "Omnichannel_enable_department_removal": "विभाग निष्कासन सक्षम करें", + "Omnichannel_enable_department_removal_alert": "हटाए गए विभागों को पुनर्स्थापित नहीं किया जा सकता, हम इसके बजाय विभाग को संग्रहीत करने की अनुशंसा करते हैं।", + "Omnichannel_Reports_Status_Open": "खुला", + "Omnichannel_Reports_Status_Closed": "बंद किया हुआ", + "Omnichannel_Reports_Channels_Empty_Subtitle": "यह चार्ट सबसे अधिक उपयोग किए जाने वाले चैनल दिखाता है.", + "Omnichannel_Reports_Departments_Empty_Subtitle": "यह चार्ट उन विभागों को प्रदर्शित करता है जो सबसे अधिक वार्तालाप प्राप्त करते हैं।", + "Omnichannel_Reports_Status_Empty_Subtitle": "बातचीत शुरू होते ही यह चार्ट अपडेट हो जाएगा.", + "Omnichannel_Reports_Tags_Empty_Subtitle": "यह चार्ट सबसे अधिक उपयोग किये जाने वाले टैग दिखाता है।", + "Omnichannel_Reports_Agents_Empty_Subtitle": "यह चार्ट प्रदर्शित करता है कि कौन से एजेंट सबसे अधिक मात्रा में वार्तालाप प्राप्त करते हैं।", + "Omnichannel_Reports_Summary": "अपने ऑपरेशन के बारे में जानकारी हासिल करें और अपने मेट्रिक्स निर्यात करें।", + "On": "पर", + "on-hold-livechat-room": "ऑन होल्ड ओमनीचैनल रूम", + "on-hold-livechat-room_description": "ओमनीचैनल रूम को होल्ड पर रखने की अनुमति", + "on-hold-others-livechat-room": "अन्य ओम्नीचैनल कक्ष को होल्ड पर रखें", + "on-hold-others-livechat-room_description": "अन्य सर्वचैनल कक्ष को रोकने की अनुमति", + "On_Hold": "होल्ड पर", + "On_Hold_Chats": "होल्ड पर", + "On_Hold_conversations": "बातचीत रुकी हुई है", + "online": "ऑनलाइन", + "Online": "ऑनलाइन", + "Only_authorized_users_can_write_new_messages": "केवल अधिकृत उपयोगकर्ता ही नये संदेश लिख सकते हैं", + "Only_authorized_users_can_react_to_messages": "केवल अधिकृत उपयोगकर्ता ही संदेशों पर प्रतिक्रिया दे सकते हैं", + "Only_from_users": "केवल इन उपयोगकर्ताओं की सामग्री को छाँटें (प्रत्येक की सामग्री को छाँटने के लिए खाली छोड़ दें)", + "Only_Members_Selected_Department_Can_View_Channel": "इस चैनल पर केवल चयनित विभाग के सदस्य ही चैट देख सकते हैं", + "Only_On_Desktop": "डेस्कटॉप मोड (केवल डेस्कटॉप पर एंटर के साथ भेजता है)", + "Only_works_with_chrome_version_greater_50": "केवल Chrome ब्राउज़र संस्करण > 50 के साथ काम करता है", + "Only_you_can_see_this_message": "यह संदेश केवल आप ही देख सकते हैं", + "Only_invited_users_can_acess_this_channel": "केवल आमंत्रित उपयोगकर्ता ही इस चैनल तक पहुंच सकते हैं", + "Oops_page_not_found": "उफ़, पेज नहीं मिला", + "Oops!": "उफ़", + "Person_Or_Channel": "व्यक्ति या चैनल", + "Open": "खुला", + "Open_call": "खुला आवाहन", + "Open_call_in_new_tab": "नए टैब में कॉल खोलें", + "Open_channel_user_search": "`%s` - चैनल/उपयोगकर्ता खोज खोलें", + "Open_conversations": "वार्तालाप खोलें", + "Open_Days": "खुले दिन", + "Open_days_of_the_week": "सप्ताह के खुले दिन", + "Open_Dialpad": "डायलपैड खोलें", + "Open_directory": "निर्देशिका खोलें", + "Open_Livechats": "बातचीत प्रगति पर है", + "Open_Outlook": "आउटलुक खोलें", + "Open_settings": "खुली सेटिंग", + "Open-source_conference_call_solution": "ओपन-सोर्स कॉन्फ़्रेंस कॉल समाधान।", + "Open_thread": "थ्रेड खोलें", + "Opened": "खुल गया", + "Opened_in_a_new_window": "एक नई विंडो में खोला गया.", + "Opens_a_channel_group_or_direct_message": "एक चैनल, समूह या सीधा संदेश खोलता है", + "Optional": "वैकल्पिक", + "optional": "वैकल्पिक", "Options": "विकल्प", + "or": "या", + "Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser": "या इस यूआरएल को कॉपी करके अपने ब्राउज़र के एक टैब में पेस्ट करें", + "Or_talk_as_anonymous": "या गुमनाम बनकर बात करें", + "Order": "आदेश", + "Organization_Email": "संगठन ईमेल", + "Organization_Info": "संगठन की जानकारी", + "Organization_Name": "संगठन का नाम", + "Organization_Type": "संगठन का प्रकार", + "Original": "मूल", + "OS": "आप", + "OS_Arch": "ओएस आर्क", + "OS_Cpus": "ओएस सीपीयू गणना", + "OS_Freemem": "ओएस फ्री मेमोरी", + "OS_Loadavg": "ओएस लोड औसत", + "OS_Platform": "ओएस प्लेटफार्म", + "OS_Release": "ओएस रिलीज", + "OS_Totalmem": "ओएस कुल मेमोरी", + "OS_Type": "ओएस प्रकार", + "OS_Uptime": "ओएस अपटाइम", + "Other": "अन्य", + "others": "अन्य", + "Others": "अन्य", + "OTR": "ओटीआर", + "OTR_unavailable_for_federation": "फ़ेडरेटेड कमरों के लिए ओटीआर उपलब्ध नहीं है", + "OTR_Description": "ऑफ-द-रिकॉर्ड चैट सुरक्षित, निजी होती हैं और समाप्त होने के बाद गायब हो जाती हैं।", + "OTR_Chat_Declined_Title": "ओटीआर चैट आमंत्रण अस्वीकृत", + "OTR_Chat_Declined_Description": "%s ने OTR चैट आमंत्रण अस्वीकार कर दिया. गोपनीयता सुरक्षा के लिए सभी संबंधित सिस्टम संदेशों सहित स्थानीय कैश हटा दिया गया था।", + "OTR_Chat_Error_Title": "कुंजी रीफ़्रेश विफल होने के कारण चैट समाप्त हो गई", + "OTR_Chat_Error_Description": "गोपनीयता सुरक्षा के लिए सभी संबंधित सिस्टम संदेशों सहित स्थानीय कैश हटा दिया गया था।", + "OTR_Chat_Timeout_Title": "ओटीआर चैट आमंत्रण समाप्त हो गया", + "OTR_Chat_Timeout_Description": "%s समय पर ओटीआर चैट आमंत्रण स्वीकार करने में विफल रहा। गोपनीयता सुरक्षा के लिए सभी संबंधित सिस्टम संदेशों सहित स्थानीय कैश हटा दिया गया था।", + "OTR_Enable_Description": "2 उपयोगकर्ताओं के बीच सीधे संदेशों में ऑफ-द-रिकॉर्ड (ओटीआर) संदेशों का उपयोग करने का विकल्प सक्षम करें। ओटीआर संदेशों को सर्वर पर रिकॉर्ड नहीं किया जाता है और दो उपयोगकर्ताओं के बीच सीधे आदान-प्रदान और एन्क्रिप्ट किया जाता है।", + "OTR_message": "ओटीआर संदेश", + "OTR_is_only_available_when_both_users_are_online": "ओटीआर केवल तभी उपलब्ध होता है जब दोनों उपयोगकर्ता ऑनलाइन हों", + "outbound-voip-calls": "आउटबाउंड वीओआईपी कॉल", + "outbound-voip-calls_description": "आउटबाउंड वीओआईपी कॉल की अनुमति", + "Out_of_seats": "सीटों से बाहर", + "Outgoing": "जावक", + "Outgoing_WebHook": "निवर्तमान वेबहुक", + "Outgoing_WebHook_Description": "वास्तविक समय में Rocket.Chat से डेटा प्राप्त करें।", + "Outlook_authentication": "आउटलुक प्रमाणीकरण", + "Outlook_authentication_disabled": "आउटलुक प्रमाणीकरण अक्षम किया गया", + "Outlook_authentication_description": "इस मशीन में संग्रहीत किसी भी आउटलुक क्रेडेंशियल को साफ़ करने के लिए इसे अक्षम करें।", + "Outlook_calendar": "आउटलुक कैलेंडर", + "Outlook_calendar_event": "आउटलुक कैलेंडर इवेंट", + "Outlook_calendar_settings": "आउटलुक कैलेंडर सेटिंग्स", + "Outlook_Calendar": "आउटलुक कैलेंडर", "Outlook_Calendar_Enabled": "सक्रिय", + "Outlook_Calendar_Exchange_Url": "एक्सचेंज यूआरएल", + "Outlook_Calendar_Exchange_Url_Description": "ईडब्ल्यूएस एपीआई के लिए होस्ट यूआरएल।", + "Outlook_Calendar_Outlook_Url": "आउटलुक यूआरएल", + "Outlook_Calendar_Outlook_Url_Description": "आउटलुक वेब ऐप लॉन्च करने के लिए यूआरएल का उपयोग किया जाता है।", + "Output_format": "आउटपुट स्वरूप", + "Outlook_Sync_Failed": "आउटलुक इवेंट लोड करने में विफल.", + "Outlook_Sync_Success": "आउटलुक इवेंट सिंक्रनाइज़।", + "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "जिस URL पर फ़ाइलें अपलोड की गई हैं उसे ओवरराइड करें। इस यूआरएल का उपयोग डाउनलोड के लिए भी किया जाता है जब तक कि सीडीएन न दिया गया हो", + "Override_Destination_Channel": "मुख्य पैरामीटर में गंतव्य चैनल को अधिलेखित करने की अनुमति दें", + "Owner": "मालिक", + "Play": "खेल", + "Page_not_exist_or_not_permission": "पेज मौजूद नहीं है या हो सकता है कि आपके पास एक्सेस की अनुमति न हो", + "Page_not_found": "पृष्ठ नहीं मिला", + "Page_title": "पृष्ठ का शीर्षक", + "Page_URL": "पेज यूआरएल", + "Pages": "पृष्ठों", + "Parent_channel_doesnt_exist": "चैनल मौजूद नहीं है.", + "Participants": "प्रतिभागियों", + "Password": "पासवर्ड", + "Password_Change_Disabled": "आपके Rocket.Chat व्यवस्थापक ने पासवर्ड बदलना अक्षम कर दिया है", + "Password_Changed_Description": "आप निम्नलिखित प्लेसहोल्डर्स का उपयोग कर सकते हैं:\n - अस्थायी पासवर्ड के लिए `[पासवर्ड]`।\n - `[नाम]`, `[fname]`, `[lname]` क्रमशः उपयोगकर्ता के पूर्ण नाम, प्रथम नाम या अंतिम नाम के लिए।\n - `[ईमेल]` उपयोगकर्ता के ईमेल के लिए।\n - एप्लिकेशन नाम और यूआरएल के लिए क्रमशः `[Site_Name]` और `[Site_URL]`।", + "Password_Changed_Email_Subject": "[साइट_नाम] - पासवर्ड बदला गया", + "Password_changed_section": "पासवर्ड बदला गया", + "Password_changed_successfully": "पासवर्ड सफलतापूर्वक बदला गया", + "Password_History": "पासवर्ड इतिहास", + "Password_History_Amount": "पासवर्ड इतिहास की लंबाई", + "Password_History_Amount_Description": "उपयोगकर्ताओं को पुन: उपयोग करने से रोकने के लिए हाल ही में उपयोग किए गए पासवर्ड की मात्रा।", + "Password_must_have": "पासवर्ड होना चाहिए:", + "Password_Policy": "पासवर्ड नीति", + "Password_Policy_Aria_Description": "इसके नीचे पासवर्ड आवश्यकता सत्यापन सूचीबद्ध है", + "Password_must_meet_the_complexity_requirements": "पासवर्ड को जटिलता आवश्यकताओं को पूरा करना होगा।", + "Password_to_access": "प्रवेश हेतु पासवर्ड", + "Passwords_do_not_match": "सांकेतिक शब्द मेल नहीं खाते", + "Past_Chats": "पिछली चैट", + "Paste_here": "यहां चिपकाएं...", + "Paste": "पेस्ट करें", + "Pause": "विराम", + "Paste_error": "क्लिपबोर्ड से पढ़ने में त्रुटि", + "Paid_Apps": "सशुल्क ऐप्स", + "Payload": "पेलोड", + "PDF": "पीडीएफ", + "pdf_success_message": "पीडीएफ प्रतिलेख सफलतापूर्वक तैयार किया गया", + "pdf_error_message": "पीडीएफ प्रतिलेख उत्पन्न करने में त्रुटि", + "Peer_Password": "सहकर्मी पासवर्ड", + "People": "लोग", + "Permalink": "स्थायी लिंक", + "Permissions": "अनुमतियां", + "Personal_Access_Tokens": "व्यक्तिगत पहुँच टोकन", + "Pexip_Premium_only": "पेक्सिप (केवल प्रीमियम)", + "Phone": "फ़ोन", + "Phone_call": "फोन कॉल", + "Phone_Number": "फ़ोन नंबर", + "Thank_you_exclamation_mark": "धन्यवाद!", + "Thank_You_For_Choosing_RocketChat": "रॉकेट.चैट चुनने के लिए धन्यवाद!", + "Phone_already_exists": "फ़ोन पहले से मौजूद है", + "Phone_number": "फ़ोन नंबर", + "PID": "पीआईडी", + "Pin": "नत्थी करना", + "Pin_Message": "संदेश पिन करें", + "pin-message": "संदेश पिन करें", + "pin-message_description": "किसी संदेश को किसी चैनल में पिन करने की अनुमति", + "Pinned_a_message": "एक संदेश पिन किया गया:", + "Pinned_Messages": "पिन किए गए संदेश", + "Pinned_messages_unavailable_for_federation": "फ़ेडरेटेड रूम के लिए पिन किए गए संदेश उपलब्ध नहीं हैं।", + "pinning-not-allowed": "पिन करने की अनुमति नहीं है", + "PiwikAdditionalTrackers": "अतिरिक्त पिविक साइटें", + "PiwikAdditionalTrackers_Description": "यदि आप एक ही डेटा को विभिन्न वेबसाइटों में ट्रैक करना चाहते हैं, तो निम्नलिखित प्रारूप में अतिरिक्त पिविक वेबसाइट यूआरएल और साइटआईडी दर्ज करें: `[ { \"ट्रैकरयूआरएल\": \"https://my.piwik.domain2/\", \"साइटआईडी\": 42 } , { \"trackerURL\" : \"https://my.piwik.domain3/\", \"siteId\" : 15 } ]`", + "PiwikAnalytics_cookieDomain": "सभी उपडोमेन", + "PiwikAnalytics_cookieDomain_Description": "सभी उपडोमेन पर विज़िटर ट्रैक करें", + "PiwikAnalytics_domains": "आउटगोइंग लिंक छुपाएं", + "PiwikAnalytics_domains_Description": "'आउटलिंक्स' रिपोर्ट में, ज्ञात उपनाम यूआरएल पर क्लिक छुपाएं। कृपया प्रति पंक्ति एक डोमेन डालें और किसी विभाजक का उपयोग न करें।", + "PiwikAnalytics_prependDomain": "डोमेन को प्रीपेन्ड करें", + "PiwikAnalytics_prependDomain_Description": "ट्रैकिंग करते समय साइट डोमेन को पृष्ठ शीर्षक से जोड़ें", + "PiwikAnalytics_siteId_Description": "इस साइट की पहचान करने के लिए उपयोग की जाने वाली साइट आईडी। उदाहरण: 17", + "PiwikAnalytics_url_Description": "यूआरएल जहां पिविक स्थित है, उसमें पिछला स्लैश शामिल करना सुनिश्चित करें। उदाहरण: `https://piwik.rocket.chat/`", + "Placeholder_for_email_or_username_login_field": "ईमेल या उपयोगकर्ता नाम लॉगिन फ़ील्ड के लिए प्लेसहोल्डर", + "Placeholder_for_password_login_confirm_field": "पासवर्ड लॉगिन फ़ील्ड के लिए प्लेसहोल्डर की पुष्टि करें", + "Placeholder_for_password_login_field": "पासवर्ड लॉगिन फ़ील्ड के लिए प्लेसहोल्डर", + "Platform_Windows": "खिड़कियाँ", + "Platform_Linux": "लिनक्स", + "Platform_Mac": "मैक", + "Please_add_a_comment": "कृपया एक टिप्पणी जोड़ें", + "Please_add_a_comment_to_close_the_room": "कृपया कमरा बंद करने के लिए एक टिप्पणी जोड़ें", "Please_answer_survey": "कृपया इस चैट के बारे में त्वरित सर्वेक्षण का उत्तर देने के लिए एक क्षण लें", + "Please_enter_usernames": "कृपया उपयोक्तानाम दर्ज करें...", + "please_enter_valid_domain": "कृपया एक मान्य डोमेन दर्ज करें", + "Please_enter_value_for_url": "कृपया अपने अवतार के यूआरएल के लिए एक मान दर्ज करें।", + "Please_enter_your_new_password_below": "कृपया अपना पासवर्ड नीचे डालें:", + "Please_enter_your_password": "अपना पासवर्ड दर्ज करें", + "Please_fill_a_label": "कृपया एक लेबल भरें", + "Please_fill_a_name": "कृपया एक नाम भरें", + "Please_fill_a_token_name": "कृपया एक वैध टोकन नाम भरें", + "Please_fill_a_username": "कृपया एक उपयोक्तानाम भरें", + "Please_fill_all_the_information": "कृपया सारी जानकारी भरें", + "Please_fill_an_email": "कृपया एक ईमेल भरें", "Please_fill_name_and_email": "कृपया नाम और ईमेल भरें", + "Please_fill_out_reason_for_report": "कृपया रिपोर्ट का कारण भरें", + "Please_select_an_user": "कृपया एक उपयोगकर्ता चुनें", + "Please_select_enabled_yes_or_no": "कृपया सक्षम के लिए एक विकल्प चुनें", + "Please_select_visibility": "कृपया एक दृश्यता चुनें", + "Please_wait": "कृपया प्रतीक्षा करें", + "Please_wait_activation": "कृपया प्रतीक्षा करें, इसमें कुछ समय लग सकता है.", + "Please_wait_while_OTR_is_being_established": "कृपया ओटीआर स्थापित होने तक प्रतीक्षा करें", + "Please_wait_while_your_account_is_being_deleted": "कृपया तब तक प्रतीक्षा करें जब तक आपका खाता हटाया जा रहा हो...", + "Please_wait_while_your_profile_is_being_saved": "कृपया तब तक प्रतीक्षा करें जब तक आपकी प्रोफ़ाइल सहेजी जा रही हो...", + "Policies": "नीतियों", + "Pool": "पूल", + "Port": "पत्तन", + "Post_as": "के रूप में पोस्ट करें", + "Post_to": "को पोस्ट", + "Post_to_Channel": "चैनल पर पोस्ट करें", + "Post_to_s_as_s": "%s को %s के रूप में पोस्ट करें", + "post-readonly": "पोस्ट केवल पढ़ने के लिए", + "post-readonly_description": "केवल पढ़ने योग्य चैनल में संदेश पोस्ट करने की अनुमति", + "Powered_by_JoyPixels": "जॉयपिक्सल्स द्वारा संचालित", + "Powered_by_RocketChat": "रॉकेट.चैट द्वारा संचालित", + "powers-of-ten": "दस की शक्तियाँ", + "powers-of-two": "दो की शक्तियाँ", + "increments-of-two": "दो की वृद्धि", + "Preferences": "पसंद", + "Preferences_saved": "प्राथमिकताएँ सहेजी गईं", + "Preparing_data_for_import_process": "आयात प्रक्रिया के लिए डेटा तैयार करना", + "Preparing_list_of_channels": "चैनलों की सूची तैयार की जा रही है", + "Preparing_list_of_messages": "संदेशों की सूची तैयार की जा रही है", + "Preparing_list_of_users": "उपभोक्ताओं की सूची तैयार की जा रही है", + "Presence": "उपस्थिति", + "Preview": "पूर्व दर्शन", + "preview-c-room": "सार्वजनिक चैनल का पूर्वावलोकन करें", + "preview-c-room_description": "शामिल होने से पहले किसी सार्वजनिक चैनल की सामग्री देखने की अनुमति", + "Previous_month": "पिछला महीना", + "Previous_week": "पिछला सप्ताह", + "Price": "कीमत", + "Priorities": "प्राथमिकताओं", + "Priority": "प्राथमिकता", + "Priority_saved": "प्राथमिकता सहेजी गई", + "Priority_removed": "प्राथमिकता हटा दी गई", + "Priorities_restored": "प्राथमिकताएँ बहाल की गईं", + "Privacy": "गोपनीयता", + "Privacy_Policy": "गोपनीयता नीति", + "Privacy_policy": "गोपनीयता नीति", + "Privacy_summary": "गोपनीयता सारांश", + "Private": "निजी", + "private": "निजी", + "Private_channels": "निजी चैनल", + "Private_Apps": "निजी ऐप्स", + "Private_Channel": "निजी चैनल", + "Private_Channels": "निजी चैनल", + "Private_Chats": "निजी चैट", + "Private_Group": "निजी समूह", + "Private_Groups": "निजी समूह", + "Private_Groups_list": "निजी समूहों की सूची", + "Private_Team": "निजी टीम", + "Productivity": "उत्पादकता", + "Profile": "प्रोफ़ाइल", + "Profile_details": "प्रोफ़ाइल विवरण", + "Profile_picture": "प्रोफ़ाइल फोटो", + "Profile_saved_successfully": "प्रोफ़ाइल सफलतापूर्वक सहेजी गई", + "Prometheus": "प्रोमेथियस", + "Prometheus_API_User_Agent": "एपीआई: उपयोगकर्ता एजेंट को ट्रैक करें", + "Prometheus_Garbage_Collector": "नोडजेएस जीसी लीजिए", + "Prometheus_Garbage_Collector_Alert": "निष्क्रिय करने के लिए पुनरारंभ करना आवश्यक है", + "Prometheus_Reset_Interval": "अंतराल रीसेट करें (एमएस)", + "Protocol": "शिष्टाचार", + "Prune": "कांट - छांट", + "Prune_finished": "प्रून ख़त्म", + "Prune_Messages": "संदेशों की छँटाई करें", + "Prune_Modal": "क्या आप वाकई इन संदेशों की काट-छाँट करना चाहते हैं? काटे गए संदेशों को पुनर्प्राप्त नहीं किया जा सकता.", + "Prune_Warning_after": "यह %s के बाद %s में सभी %s को हटा देगा।", + "Prune_Warning_all": "यह %s में सभी %s को हटा देगा!", + "Prune_Warning_before": "यह %s से पहले %s में सभी %s को हटा देगा।", + "Prune_Warning_between": "यह %s में %s और %s के बीच के सभी %s को हटा देगा।", + "Pruning_files": "फ़ाइलें काट-छाँट की जा रही हैं...", + "Pruning_messages": "संदेशों में काट-छाँट की जा रही है...", "Public": "जनता", + "public": "जनता", + "Public_Channel": "सार्वजनिक चैनल", + "Public_Channels": "सार्वजनिक चैनल", + "Public_Community": "सार्वजनिक समुदाय", + "Public_URL": "सार्वजनिक यूआरएल", + "Purchase_for_free": "मुफ़्त में खरीदारी करें", + "Purchase_for_price": "$%s के लिए खरीदारी", + "Purchased": "खरीदी", + "Push": "धकेलना", + "Push_Description": "मोबाइल उपकरणों का उपयोग करने वाले कार्यक्षेत्र सदस्यों के लिए पुश सूचनाओं को सक्षम और कॉन्फ़िगर करें।", + "Push_Notifications": "सूचनाएं धक्का", + "Push_apn_cert": "APN Cert", + "Push_apn_dev_cert": "APN Dev Cert", + "Push_apn_dev_key": "एपीएन देव कुंजी", + "Push_apn_dev_passphrase": "एपीएन देव पासफ़्रेज़", + "Push_apn_key": "एपीएन कुंजी", + "Push_apn_passphrase": "एपीएन पासफ़्रेज़", "Push_enable": "सक्षम करें", + "Push_enable_gateway": "गेटवे सक्षम करें", + "Push_enable_gateway_Description": "**चेतावनी:** आपको इस सेटिंग को सक्षम करने और हमारे गेटवे का उपयोग करने के लिए अपने सर्वर (सेटअप विज़ार्ड> संगठन जानकारी> रजिस्टर सर्वर) और हमारी गोपनीयता शर्तों (सेटअप विज़ार्ड> क्लाउड जानकारी> क्लाउड सेवा गोपनीयता शर्तें अनुबंध) को पंजीकृत करने की आवश्यकता है। भले ही यह सेटिंग उस पर मौजूद हो, यदि सर्वर पंजीकृत नहीं है तो **नहीं** काम करेगा।", + "Push_gateway": "द्वार", + "Push_gateway_description": "एकाधिक गेटवे निर्दिष्ट करने के लिए एकाधिक लाइनों का उपयोग किया जा सकता है", + "Push_gcm_api_key": "जीसीएम एपीआई कुंजी", + "Push_gcm_project_number": "जीसीएम परियोजना संख्या", + "Push_production": "उत्पादन", + "Push_request_content_from_server": "Apple और Google (और सक्षम होने पर गेटवे) से संदेश सामग्री छिपाएँ", + "Push_request_content_from_server_Description": "संदेश सामग्री को पुश अधिसूचना डेटा में शामिल करके Apple/Google के सामने उजागर करने के बजाय, केवल एक संदेश आईडी पुश करें। मोबाइल क्लाइंट गतिशील रूप से सर्वर से सामग्री लाएगा और इसे प्रदर्शित करने से पहले अधिसूचना को अपडेट करेगा। एपीआई त्रुटि की स्थिति में, यह \"आपके पास एक नया संदेश है\" प्रदर्शित करेगा। यह सेटिंग केवल प्रीमियम योजना पर प्रभावी होती है।", + "Push_Setting_Requires_Restart_Alert": "इस मान को बदलने के लिए Rocket.Chat को पुनः आरंभ करने की आवश्यकता है।", + "Push_show_message": "अधिसूचना में संदेश दिखाएँ", + "Push_show_username_room": "अधिसूचना में चैनल/समूह/उपयोगकर्ता नाम दिखाएँ", + "Push_test_push": "परीक्षा", + "Query": "सवाल", + "Query_description": "यह निर्धारित करने के लिए अतिरिक्त शर्तें कि किन उपयोगकर्ताओं को ईमेल भेजना है। सदस्यता समाप्त करने वाले उपयोगकर्ता स्वचालित रूप से क्वेरी से हटा दिए जाते हैं। यह एक वैध JSON होना चाहिए. उदाहरण: \"{\"createdAt\":{\"$gt\":{\"$date\": \"2015-01-01T00:00:00.000Z\"}}}\"", + "Query_is_not_valid_JSON": "क्वेरी मान्य JSON नहीं है", + "Queue": "कतार", + "Queued": "कतारबद्ध", + "Queues": "पूंछ", + "Queue_delay_timeout": "कतार प्रसंस्करण विलंब समयबाह्य", + "Queue_Time": "कतार समय", + "Queue_management": "कतार प्रबंधन", + "Quick_reactions": "त्वरित प्रतिक्रियाएँ", + "Quick_reactions_description": "जब आपका माउस संदेश पर होता है तो सबसे अधिक उपयोग की जाने वाली तीन प्रतिक्रियाओं तक आसान पहुंच मिलती है", + "quote": "उद्धरण", + "Quote": "उद्धरण", + "Random": "Random", + "Rate Limiter": "दर सीमक", + "Rate Limiter_Description": "साइबर हमलों और स्क्रैपिंग को रोकने के लिए अपने सर्वर द्वारा भेजे गए या प्राप्त अनुरोधों की दर को नियंत्रित करें।", + "Rate_Limiter_Limit_RegisterUser": "उपयोगकर्ता को पंजीकृत करने के लिए डिफ़ॉल्ट नंबर दर सीमक पर कॉल करता है", + "Rate_Limiter_Limit_RegisterUser_Description": "एपीआई रेट लिमिटर अनुभाग में परिभाषित समय सीमा के भीतर अनुमत अंतिम बिंदुओं (आरईएसटी और रीयल-टाइम एपीआई) को पंजीकृत करने वाले उपयोगकर्ता के लिए डिफ़ॉल्ट कॉल की संख्या।", + "React_when_read_only": "प्रतिक्रिया करने की अनुमति दें", + "React_when_read_only_changed_successfully": "केवल पढ़ने के लिए सफलतापूर्वक परिवर्तन होने पर प्रतिक्रिया करने की अनुमति दें", + "Reacted_with": "के साथ प्रतिक्रिया व्यक्त की", + "Reactions": "प्रतिक्रियाओं", + "Read_by": "द्वारा पढ़ें", + "Read_only": "केवल पढ़ने के लिए", + "Read_Receipts": "रसीदें पढ़ें", + "Readability": "पठनीयता", + "This_room_is_read_only": "यह कमरा केवल पढ़ने के लिए है", + "Only_people_with_permission_can_send_messages_here": "केवल अनुमति प्राप्त लोग ही यहां संदेश भेज सकते हैं", + "Read_only_changed_successfully": "केवल पढ़ने के लिए सफलतापूर्वक बदला गया", + "Read_only_channel": "केवल पढ़ने के लिए चैनल", + "Read_only_group": "केवल पढ़ने योग्य समूह", + "Real_Estate": "रियल एस्टेट", + "Real_Time_Monitoring": "वास्तविक समय में निगरानी", + "RealName_Change_Disabled": "आपके Rocket.Chat व्यवस्थापक ने नाम बदलना अक्षम कर दिया है", + "Reason_To_Join": "शामिल होने का कारण", + "Receive_alerts": "अलर्ट प्राप्त करें", + "Receive_Group_Mentions": "@सभी और @यहाँ उल्लेख प्राप्त करें", + "Receive_login_notifications": "लॉगिन सूचनाएं प्राप्त करें", + "Receive_Login_Detection_Emails": "लॉगिन पहचान ईमेल प्राप्त करें", + "Receive_Login_Detection_Emails_Description": "हर बार आपके खाते पर नए लॉगिन का पता चलने पर एक ईमेल प्राप्त करें।", + "Recent_Import_History": "हाल का आयात इतिहास", + "Record": "अभिलेख", + "Records": "अभिलेख", + "recording": "रिकॉर्डिंग", + "Redirect_URI": "यूआरआई को पुनर्निर्देशित करें", + "Redirect_URL_does_not_match": "रीडायरेक्ट यूआरएल मेल नहीं खाता", + "Refresh": "ताज़ा करना", + "Refresh_keys": "कुंजियाँ ताज़ा करें", + "Refresh_oauth_services": "OAuth सेवाएँ ताज़ा करें", + "Refresh_your_page_after_install_to_enable_screen_sharing": "स्क्रीन शेयरिंग सक्षम करने के लिए इंस्टॉल के बाद अपने पेज को रीफ्रेश करें", + "Refreshing": "रिफ्रेशिंग", + "Regenerate_codes": "कोड पुन: उत्पन्न करें", + "Regexp_validation": "नियमित अभिव्यक्ति द्वारा सत्यापन", + "Register": "पंजीकरण करवाना", + "Register_new_account": "एक नया खाता रजिस्टर करे", + "Register_Server": "सर्वर पंजीकृत करें", + "Register_Server_Info": "Rocket.Chat Technologies Corp. द्वारा उपलब्ध कराए गए पूर्व-कॉन्फ़िगर गेटवे और प्रॉक्सी का उपयोग करें।", + "Register_Server_Opt_In": "उत्पाद और सुरक्षा अद्यतन", + "Register_Server_Registered": "पहुंच के लिए पंजीकरण करें", + "Register_Server_Registered_I_Agree": "मैं इससे सहमत हूं", + "Register_Server_Registered_Livechat": "लाइवचैट ओमनीचैनल प्रॉक्सी", + "Register_Server_Registered_Marketplace": "ऐप्स बाज़ार", + "Register_Server_Registered_OAuth": "सामाजिक नेटवर्क के लिए OAuth प्रॉक्सी", + "Register_Server_Registered_Push_Notifications": "मोबाइल पुश नोटिफिकेशन गेटवे", + "Register_Server_Standalone": "स्टैंडअलोन रखें, आपको इसकी आवश्यकता होगी", + "Register_Server_Standalone_Own_Certificates": "अपने स्वयं के प्रमाणपत्रों के साथ मोबाइल ऐप्स को पुनः संकलित करें", + "Register_Server_Standalone_Service_Providers": "सेवा प्रदाताओं के साथ खाते बनाएँ", + "Register_Server_Standalone_Update_Settings": "पूर्व-कॉन्फ़िगर की गई सेटिंग्स को अपडेट करें", + "Register_Server_Terms_Alert": "कृपया पंजीकरण पूरा करने की शर्तों से सहमत हों", + "register-on-cloud": "क्लाउड पर रजिस्टर करें", + "register-on-cloud_description": "क्लाउड पर पंजीकरण करने की अनुमति", + "Registration": "पंजीकरण", + "Registration_Succeeded": "पंजीकरण सफल हुआ", + "Registration_via_Admin": "व्यवस्थापक के माध्यम से पंजीकरण", + "Regular_Expressions": "नियमित अभिव्यक्ति", + "Reject_call": "कॉल अस्वीकार करें", + "Release": "मुक्त करना", + "Releases": "विज्ञप्ति", + "Religious": "धार्मिक", + "Reload": "पुनः लोड करें", + "Reload_page": "पृष्ठ पुनः लोड करें", + "Reload_Pages": "पेज पुनः लोड करें", + "Remember_my_credentials": "मेरी साख याद रखें", + "Remove": "निकालना", + "Remove_Admin": "व्यवस्थापक हटाएँ", + "Remove_Association": "एसोसिएशन हटाएँ", + "Remove_as_leader": "नेता पद से हटाओ", + "Remove_as_moderator": "मॉडरेटर के रूप में हटाएँ", + "Remove_as_owner": "स्वामी के रूप में हटाएँ", + "remove-canned-responses": "डिब्बाबंद प्रत्युत्तर हटाएँ", + "remove-canned-responses_description": "डिब्बाबंद प्रत्युत्तरों को हटाने की अनुमति", + "Remove_Channel_Links": "चैनल लिंक हटाएँ", + "Remove_custom_oauth": "कस्टम OAuth हटाएँ", + "Remove_from_room": "कमरे से निकालो", + "Remove_from_team": "टीम से हटाओ", + "Remove_last_admin": "अंतिम व्यवस्थापक को हटाया जा रहा है", + "Remove_someone_from_room": "किसी को कमरे से बाहर निकालें", + "remove-closed-livechat-room": "बंद ओमनीचैनल कक्ष हटाएँ", + "remove-closed-livechat-room_description": "बंद ऑम्नीचैनल रूम को हटाने की अनुमति", + "remove-closed-livechat-rooms": "सभी बंद ओमनीचैनल कमरे हटाएँ", + "remove-closed-livechat-rooms_description": "सभी बंद ओमनीचैनल कमरों को हटाने की अनुमति", + "remove-livechat-department": "ओमनीचैनल विभाग हटाएँ", + "remove-livechat-department_description": "सर्वचैनल विभागों को हटाने की अनुमति", + "remove-slackbridge-links": "स्लैकब्रिज लिंक हटाएँ", + "remove-slackbridge-links_description": "स्लैकब्रिज लिंक हटाने की अनुमति", + "remove-team-channel": "टीम चैनल हटाएँ", + "remove-team-channel_description": "किसी टीम के चैनल को हटाने की अनुमति", + "remove-user": "उपयोगकर्ता को हटाएँ", + "remove-user_description": "किसी उपयोगकर्ता को कमरे से निकालने की अनुमति", + "Removed": "निकाला गया", + "Removed_User": "उपयोगकर्ता को हटा दिया गया", + "Removed__roomName__from_this_team": "इस टीम से #{{roomName}} हटा दिया गया", + "Removed__username__from_team": "@{{user_removed}} को इस टीम से हटा दिया गया", + "Removed__roomName__from_the_team": "इस टीम से #{{roomName}} हटा दिया गया", + "Removed__username__from_the_team": "@{{user_removed}} को इस टीम से हटा दिया गया", + "Replay": "REPLAY", + "Replied_on": "पर उत्तर दिया", + "Replies": "जवाब", + "Reply": "जवाब", + "Reply_in_direct_message": "सीधे संदेश में उत्तर दें", + "Reply_in_thread": "थ्रेड में उत्तर दें", + "Reply_via_Email": "ईमेल के माध्यम से उत्तर दें", + "ReplyTo": "को उत्तर", + "Report": "प्रतिवेदन", + "Reports": "रिपोर्टों", + "Report_Abuse": "दुरुपयोग होने की सूचना दें", + "Report_exclamation_mark": "प्रतिवेदन!", + "Report_has_been_sent": "रिपोर्ट भेज दी गई है", + "Report_Number": "रिपोर्ट संख्या", + "Report_this_message_question_mark": "इस संदेश की रिपोर्ट करें?", + "Report_User": "उपयोगकर्ता को रिपोर्ट करें", + "Reporting": "रिपोर्टिंग", + "Request": "अनुरोध", + "Request_comment_when_closing_conversation": "बातचीत बंद करते समय टिप्पणी का अनुरोध करें", + "Request_comment_when_closing_conversation_description": "यदि सक्षम किया गया है, तो एजेंट को बातचीत बंद होने से पहले एक टिप्पणी सेट करनी होगी।", + "Request_tag_before_closing_chat": "बातचीत बंद करने से पहले टैग का अनुरोध करें", + "request": "अनुरोध", + "requests": "अनुरोध", + "Requests": "अनुरोध", + "Requested": "का अनुरोध किया", + "Requested_apps_will_appear_here": "अनुरोधित ऐप्स यहां दिखाई देंगे", + "request-pdf-transcript": "पीडीएफ प्रतिलेख का अनुरोध करें", + "request-pdf-transcript_description": "किसी दिए गए ओमनीचैनल कक्ष के लिए पीडीएफ प्रतिलेख का अनुरोध करने की अनुमति", + "Requested_At": "पर अनुरोध किया गया", + "Requested_By": "द्वारा अनुरोध किया गया", + "Require": "ज़रूरत होना", + "Required": "आवश्यक", + "required": "आवश्यक", + "Require_all_tokens": "सभी टोकन की आवश्यकता है", + "Require_any_token": "किसी भी टोकन की आवश्यकता है", + "Require_password_change": "पासवर्ड परिवर्तन की आवश्यकता है", + "Resend_verification_email": "सत्यापन ईमेल पुनः भेजे", + "Reset": "रीसेट", + "Reset_priorities": "प्राथमिकताएँ रीसेट करें", + "Reset_Connection": "कनेक्शन रीसेट करें", + "Reset_E2E_Key": "E2E कुंजी रीसेट करें", + "Reset_password": "पासवर्ड रीसेट", + "Reset_section_settings": "डिफॉल्ट्स का पुनःस्थापन", + "Reset_TOTP": "टीओटीपी रीसेट करें", + "reset-other-user-e2e-key": "अन्य उपयोगकर्ता E2E कुंजी रीसेट करें", + "Responding": "जवाब", + "Response_description_post": "खाली बॉडी या खाली टेक्स्ट प्रॉपर्टी वाले बॉडी को आसानी से नजरअंदाज कर दिया जाएगा। गैर-200 प्रतिक्रियाओं का उचित संख्या में पुनः प्रयास किया जाएगा। ऊपर निर्दिष्ट उपनाम और अवतार का उपयोग करके एक प्रतिक्रिया पोस्ट की जाएगी। आप उपरोक्त उदाहरण के अनुसार इन सूचनाओं को ओवरराइड कर सकते हैं।", + "Response_description_pre": "यदि हैंडलर चैनल में प्रतिक्रिया वापस पोस्ट करना चाहता है, तो निम्नलिखित JSON को प्रतिक्रिया के मुख्य भाग के रूप में वापस किया जाना चाहिए:", + "Restart": "पुनः आरंभ करें", + "Restart_the_server": "सर्वर पुनः प्रारंभ करें", + "restart-server": "सर्वर पुनः प्रारंभ करें", + "restart-server_description": "सर्वर को पुनरारंभ करने की अनुमति", + "Results": "परिणाम", + "Resume": "फिर शुरू करना", + "Retail": "खुदरा", + "Retention_setting_changed_successfully": "अवधारण नीति सेटिंग सफलतापूर्वक बदल दी गई", + "RetentionPolicy": "अवधारण नीति", + "RetentionPolicy_Advanced_Precision": "उन्नत अवधारण नीति कॉन्फ़िगरेशन का उपयोग करें", + "RetentionPolicy_Advanced_Precision_Cron": "उन्नत अवधारण नीति क्रॉन का उपयोग करें", + "RetentionPolicy_Advanced_Precision_Cron_Description": "क्रॉन जॉब एक्सप्रेशन द्वारा परिभाषित प्रून टाइमर को कितनी बार चलाना चाहिए। इसे अधिक सटीक मान पर सेट करने से तेज़ रिटेंशन टाइमर वाले चैनल बेहतर काम करते हैं, लेकिन बड़े समुदायों पर अतिरिक्त प्रसंस्करण शक्ति खर्च हो सकती है।", + "RetentionPolicy_AppliesToChannels": "चैनलों पर लागू होता है", + "RetentionPolicy_AppliesToDMs": "सीधे संदेशों पर लागू होता है", + "RetentionPolicy_AppliesToGroups": "निजी समूहों पर लागू होता है", + "RetentionPolicy_Description": "आपके कार्यक्षेत्र में पुराने संदेशों और फ़ाइलों की स्वचालित रूप से छंटाई करें।", + "RetentionPolicy_DoNotPruneDiscussion": "चर्चा संदेशों की काट-छाँट न करें", + "RetentionPolicy_DoNotPrunePinned": "पिन किए गए संदेशों की काट-छांट न करें", + "RetentionPolicy_DoNotPruneThreads": "धागों की काट-छाँट न करें", + "RetentionPolicy_Enabled": "सक्रिय", + "RetentionPolicy_ExcludePinned": "पिन किए गए संदेशों को बाहर निकालें", + "RetentionPolicy_FilesOnly": "केवल फ़ाइलें हटाएँ", + "RetentionPolicy_FilesOnly_Description": "केवल फ़ाइलें हटाई जाएंगी, संदेश स्वयं यथावत रहेंगे।", + "RetentionPolicy_MaxAge": "अधिकतम संदेश आयु", + "RetentionPolicy_MaxAge_Channels": "चैनलों में अधिकतम संदेश आयु", + "RetentionPolicy_MaxAge_Description": "इस मान से पुराने सभी संदेशों को दिनों में छाँटें", + "RetentionPolicy_MaxAge_DMs": "प्रत्यक्ष संदेशों में अधिकतम संदेश आयु", + "RetentionPolicy_MaxAge_Groups": "निजी समूहों में अधिकतम संदेश आयु", + "RetentionPolicy_Precision": "टाइमर परिशुद्धता", + "RetentionPolicy_Precision_Description": "प्रून टाइमर कितनी बार चलना चाहिए. इसे अधिक सटीक मान पर सेट करने से तेज़ रिटेंशन टाइमर वाले चैनल बेहतर काम करते हैं, लेकिन बड़े समुदायों पर अतिरिक्त प्रसंस्करण शक्ति खर्च हो सकती है।", + "RetentionPolicyRoom_Enabled": "पुराने संदेशों को स्वचालित रूप से छाँटें", + "RetentionPolicyRoom_ExcludePinned": "पिन किए गए संदेशों को बाहर निकालें", + "RetentionPolicyRoom_FilesOnly": "केवल फाइलों की छँटाई करें, संदेश रखें", + "RetentionPolicyRoom_MaxAge": "अधिकतम संदेश आयु दिनों में (डिफ़ॉल्ट: {{max}})", + "RetentionPolicyRoom_OverrideGlobal": "वैश्विक प्रतिधारण नीति को ओवरराइड करें", + "RetentionPolicyRoom_ReadTheDocs": "ध्यान रहें! अत्यधिक सावधानी के बिना इन सेटिंग्स में बदलाव करने से सभी संदेश इतिहास नष्ट हो सकते हैं। कृपया यहां सुविधा चालू करने से पहले दस्तावेज़ पढ़ें।", + "Retry": "पुन: प्रयास करें", + "Return_to_home": "घर पर वापस", + "Return_to_previous_page": "पिछले पेज पर लौटें", + "Return_to_the_queue": "कतार में वापस लौटें", + "Review_devices": "समीक्षा करें कि डिवाइस कब और कहाँ से कनेक्ट हो रहे हैं", + "Ringing": "बज", + "Ringtones_and_visual_indicators_notify_people_of_incoming_calls": "रिंगटोन और दृश्य संकेतक लोगों को आने वाली कॉल के बारे में सूचित करते हैं।", + "Robot_Instructions_File_Content": "robots.txt फ़ाइल सामग्री", + "Root": "जड़", + "Required_action": "आवश्यक क्रिया", + "Default_Referrer_Policy": "डिफ़ॉल्ट रेफ़रर नीति", + "Default_Referrer_Policy_Description": "यह 'रेफ़रर' हेडर को नियंत्रित करता है जो अन्य सर्वर से एम्बेडेड मीडिया का अनुरोध करते समय भेजा जाता है। अधिक जानकारी के लिए, [एमडीएन से यह लिंक](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) देखें। याद रखें, इसे प्रभावी बनाने के लिए पूरे पृष्ठ को ताज़ा करना आवश्यक है", + "No_feature_to_preview": "पूर्वावलोकन करने की कोई सुविधा नहीं", + "No_Referrer": "रेफर न करें", + "No_Referrer_When_Downgrade": "डाउनग्रेड करते समय कोई रेफरर नहीं", + "Notes": "टिप्पणियाँ", + "Origin": "मूल", + "Origin_When_Cross_Origin": "उत्पत्ति जब क्रॉस उत्पत्ति", + "Same_Origin": "वही मूल", + "Strict_Origin": "सख्त उत्पत्ति", + "Strict_Origin_When_Cross_Origin": "क्रॉस मूल जब सख्त मूल", + "UIKit_Interaction_Timeout": "ऐप प्रतिक्रिया देने में विफल रहा है. कृपया पुनः प्रयास करें या अपने व्यवस्थापक से संपर्क करें", + "Unsafe_Url": "असुरक्षित यूआरएल", + "Rocket_Chat_Alert": "रॉकेट.चैट अलर्ट", + "Role": "भूमिका", + "Roles": "भूमिकाएँ", + "Role_Editing": "भूमिका संपादन", + "Role_Mapping": "भूमिका मानचित्रण", + "Role_removed": "भूमिका हटा दी गई", + "Room": "कमरा", + "room_allowed_reacting": "{{user_by}} द्वारा प्रतिक्रिया करते हुए कमरे की अनुमति दी गई", + "room_allowed_reactions": "अनुमत प्रतिक्रियाएँ", + "Room_announcement_changed_successfully": "कक्ष की घोषणा सफलतापूर्वक बदल दी गई", + "Room_archivation_state": "राज्य", + "Room_archivation_state_false": "सक्रिय", + "Room_archivation_state_true": "संग्रहीत", + "Room_archived": "कक्ष संग्रहीत", + "room_changed_announcement": "कमरे की घोषणा को बदलकर: {{room_announcement}} द्वारा {{user_by}} कर दिया गया है।", + "room_changed_avatar": "कमरे का अवतार {{user_by}} द्वारा बदला गया", + "room_avatar_changed": "बदला हुआ कमरे का अवतार", + "room_changed_description": "कमरे का विवरण इस प्रकार बदला गया: {{room_description}} द्वारा {{user_by}}", + "room_changed_privacy": "कमरे का प्रकार बदलकर: {{room_type}} द्वारा {{user_by}} कर दिया गया है।", + "room_changed_topic": "कमरे का विषय इस प्रकार बदला गया: {{room_topic}} द्वारा {{user_by}}", + "room_changed_type": "कमरा बदलकर {{room_type}} कर दिया गया", + "room_changed_topic_to": "कमरे का विषय बदलकर {{room_topic}} कर दिया गया", + "Room_default_change_to_private_will_be_default_no_more": "यह एक डिफ़ॉल्ट चैनल है और इसे निजी समूह में बदलने से यह डिफ़ॉल्ट चैनल नहीं रहेगा। क्या आपकी आगे बढ़ने की इच्छा है?", + "Room_description_changed_successfully": "कमरे का विवरण सफलतापूर्वक बदला गया", + "room_disallowed_reacting": "{{user_by}} द्वारा प्रतिक्रिया व्यक्त करते हुए कमरा अस्वीकृत कर दिया गया", + "room_disallowed_reactions": "अस्वीकृत प्रतिक्रियाएँ", + "Room_Edit": "कक्ष संपादित करें", + "Room_has_been_archived": "कमरा संग्रहीत कर लिया गया है", + "Room_has_been_converted": "कमरा परिवर्तित कर दिया गया है", + "Room_has_been_created": "कक्ष बनाया गया है", + "Room_has_been_removed": "कमरा हटा दिया गया है", + "Room_has_been_unarchived": "कमरा अनारक्षित कर दिया गया है", + "Room_Info": "कमरे की जानकारी", + "room_is_blocked": "यह कमरा अवरुद्ध है", + "room_account_deactivated": "यह खाता निष्क्रिय कर दिया गया है", + "room_is_read_only": "यह कमरा केवल पढ़ने के लिए है", + "room_name": "कमरे का नाम", + "Room_name_changed": "कमरे का नाम बदलकर: {{room_name}} द्वारा {{user_by}} कर दिया गया है", + "Room_name_changed_to": "कमरे का नाम बदलकर {{room_name}} कर दिया गया", + "Room_name_changed_successfully": "कमरे का नाम सफलतापूर्वक बदला गया", + "Room_not_exist_or_not_permission": "कमरा मौजूद नहीं है या हो सकता है कि आपके पास प्रवेश की अनुमति न हो", + "Room_not_found": "कमरा नहीं मिला", + "Room_password_changed_successfully": "कमरे का पासवर्ड सफलतापूर्वक बदला गया", + "room_removed_read_only": "कक्ष को लिखने की अनुमति {{user_by}} द्वारा जोड़ी गई", + "room_set_read_only": "{{user_by}} द्वारा कमरे को केवल पढ़ने के लिए सेट किया गया", + "room_removed_read_only_permission": "केवल पढ़ने की अनुमति हटा दी गई", + "room_set_read_only_permission": "केवल पढ़ने के लिए कमरा निर्धारित करें", + "Room_topic_changed_successfully": "कक्ष का विषय सफलतापूर्वक बदला गया", + "Room_type_changed_successfully": "कमरे का प्रकार सफलतापूर्वक बदला गया", + "Room_type_of_default_rooms_cant_be_changed": "यह एक डिफ़ॉल्ट कमरा है और इसका प्रकार बदला नहीं जा सकता, कृपया अपने व्यवस्थापक से परामर्श लें।", + "Room_unarchived": "कमरा अनारक्षित", + "Room_updated_successfully": "कमरा सफलतापूर्वक अपडेट किया गया!", + "Room_uploaded_file_list": "फ़ाइलें सूची", + "Room_uploaded_file_list_empty": "कोई फ़ाइल उपलब्ध नहीं.", + "Rooms": "कमरा", + "Rooms_added_successfully": "कमरे सफलतापूर्वक जोड़े गए", + "Routing": "मार्ग", + "Run_only_once_for_each_visitor": "प्रत्येक आगंतुक के लिए केवल एक बार चलाएँ", + "run-import": "आयात चलाएँ", + "run-import_description": "आयातकों को चलाने की अनुमति", + "run-migration": "माइग्रेशन चलाएँ", + "run-migration_description": "माइग्रेशन चलाने की अनुमति", + "Running_Instances": "चल रहे उदाहरण", + "Runtime_Environment": "क्रम पर्यावरण", + "S_new_messages_since_s": "%s के बाद से %s नये संदेश", + "Same_As_Token_Sent_Via": "\"टोकन के माध्यम से भेजा गया\" के समान", + "Same_Style_For_Mentions": "उल्लेख के लिए वही शैली", + "SAML": "एसएएमएल", + "SAML_Description": "प्रमाणीकरण और प्राधिकरण डेटा के आदान-प्रदान के लिए सुरक्षा अभिकथन मार्कअप भाषा का उपयोग किया जाता है।", + "SAML_Allowed_Clock_Drift": "पहचान प्रदाता से अनुमत क्लॉक ड्रिफ्ट", + "SAML_Allowed_Clock_Drift_Description": "पहचान प्रदाता की घड़ी आपके सिस्टम घड़ियों से थोड़ी आगे बढ़ सकती है। आप थोड़ी मात्रा में घड़ी के बहाव की अनुमति दे सकते हैं। इसका मान कई मिलीसेकंड (एमएस) में दिया जाना चाहिए। दिया गया मान उस वर्तमान समय में जोड़ा जाता है जिस पर प्रतिक्रिया सत्यापित की जाती है।", + "SAML_AuthnContext_Template": "AuthnContext टेम्पलेट", + "SAML_AuthnContext_Template_Description": "आप यहां AuthnRequest टेम्पलेट से किसी भी वेरिएबल का उपयोग कर सकते हैं।\n \n अतिरिक्त ऑथ्न संदर्भ जोड़ने के लिए, {{AuthnContextClassRef}} टैग को डुप्लिकेट करें और {{\\_\\_authnContext\\_\\}} वेरिएबल को नए संदर्भ से बदलें।", + "SAML_AuthnRequest_Template": "AuthnRequest टेम्पलेट", + "SAML_AuthnRequest_Template_Description": "निम्नलिखित चर उपलब्ध हैं:\n- **\\_\\_newId\\_\\_**: यादृच्छिक रूप से उत्पन्न आईडी स्ट्रिंग\n- **\\_\\_तत्काल\\_\\_**: वर्तमान टाइमस्टैम्प\n- **\\_\\_कॉलबैकयूआरएल\\_\\_**: रॉकेट.चैट कॉलबैक यूआरएल।\n- **\\_\\_entryPoint\\_\\_**: {{Custom Entry Point}} सेटिंग का मान।\n- **\\_\\_जारीकर्ता\\_\\_**: {{Custom Issuer}} सेटिंग का मान।\n- **\\_\\_identifierFormatTag\\_\\_**: यदि वैध {{Identifier Format}} कॉन्फ़िगर किया गया है तो {{NameID Policy Template}} की सामग्री कॉन्फ़िगर की गई है।\n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} सेटिंग का मान।\n- **\\_\\_authnContextTag\\_\\_**: यदि वैध {{Custom Authn Context}} कॉन्फ़िगर किया गया है, तो {{AuthnContext Template}} की सामग्री कॉन्फ़िगर की गई है।\n- **\\_\\_authnContextComparison\\_\\_**: {{Authn Context Compare}} सेटिंग का मान।\n- **\\_\\_authnContext\\_\\_**: {{Custom Authn Context}} सेटिंग का मान.", + "SAML_Connection": "संबंध", + "SAML_General": "सामान्य", + "SAML_Custom_Authn_Context": "कस्टम प्रमाणीकरण संदर्भ", + "SAML_Custom_Authn_Context_Comparison": "प्रामाणिक संदर्भ तुलना", + "SAML_Custom_Authn_Context_description": "अनुरोध से प्रामाणिक संदर्भ हटाने के लिए इसे खाली छोड़ दें।\n \n एकाधिक प्रामाणिक संदर्भ जोड़ने के लिए, अतिरिक्त संदर्भों को सीधे {{AuthnContext Template}} सेटिंग में जोड़ें।", + "SAML_Custom_Cert": "कस्टम प्रमाणपत्र", + "SAML_Custom_Debug": "डिबग सक्षम करें", + "SAML_Custom_EMail_Field": "ई-मेल फ़ील्ड का नाम", + "SAML_Custom_Entry_point": "कस्टम प्रवेश बिंदु", + "SAML_Custom_Generate_Username": "उपयोगकर्ता नाम उत्पन्न करें", + "SAML_Custom_IDP_SLO_Redirect_URL": "आईडीपी एसएलओ रीडायरेक्ट यूआरएल", + "SAML_Custom_Immutable_Property": "अपरिवर्तनीय फ़ील्ड नाम", + "SAML_Custom_Immutable_Property_EMail": "ईमेल", + "SAML_Custom_Immutable_Property_Username": "उपयोगकर्ता नाम", + "SAML_Custom_Issuer": "कस्टम जारीकर्ता", + "SAML_Custom_Logout_Behaviour": "लॉगआउट व्यवहार", + "SAML_Custom_Logout_Behaviour_End_Only_RocketChat": "केवल Rocket.Chat से लॉग आउट करें", + "SAML_Custom_Logout_Behaviour_Terminate_SAML_Session": "SAML-सत्र समाप्त करें", + "SAML_Custom_mail_overwrite": "उपयोगकर्ता मेल को अधिलेखित करें (आईडीपी विशेषता का उपयोग करें)", + "SAML_Custom_name_overwrite": "उपयोगकर्ता का पूरा नाम अधिलेखित करें (आईडीपी विशेषता का उपयोग करें)", + "SAML_Custom_Private_Key": "निजी कुंजी सामग्री", + "SAML_Custom_Provider": "कस्टम प्रदाता", + "SAML_Custom_Public_Cert": "सार्वजनिक प्रमाणपत्र सामग्री", + "SAML_Custom_signature_validation_all": "सभी हस्ताक्षर मान्य करें", + "SAML_Custom_signature_validation_assertion": "अभिकथन हस्ताक्षर मान्य करें", + "SAML_Custom_signature_validation_either": "किसी भी हस्ताक्षर को मान्य करें", + "SAML_Custom_signature_validation_response": "मान्य प्रतिक्रिया हस्ताक्षर", + "SAML_Custom_signature_validation_type": "हस्ताक्षर सत्यापन प्रकार", + "SAML_Custom_signature_validation_type_description": "यदि कोई कस्टम प्रमाणपत्र प्रदान नहीं किया गया है तो इस सेटिंग को अनदेखा कर दिया जाएगा।", + "SAML_Custom_user_data_fieldmap": "उपयोगकर्ता डेटा फ़ील्ड मानचित्र", + "SAML_Custom_user_data_fieldmap_description": "कॉन्फ़िगर करें कि एसएएमएल (एक बार मिल जाने पर) में रिकॉर्ड से उपयोगकर्ता खाता फ़ील्ड (जैसे ईमेल) कैसे पॉप्युलेट किए जाते हैं।\nउदाहरण के तौर पर, `{\"name\":\"cn\", \"email\":\"mail\"}` cn विशेषता से किसी व्यक्ति का मानव पठनीय नाम चुनेगा, और मेल विशेषता से उनका ईमेल चुनेगा।\nRocket.Chat में उपलब्ध फ़ील्ड: `नाम`, `ईमेल` और `उपयोगकर्ता नाम`, बाकी सब हटा दिया जाएगा।\n`{\"ईमेल\": \"मेल\",\"उपयोगकर्ता नाम\": {\"फ़ील्डनाम\": \"मेल\",\"रेगेक्स\": \"(.*)@.+$\",\"टेम्पलेट\": \"उपयोगकर्ता-रेगेक्स\"}, \" नाम\": { \"फ़ील्डनाम\": [\"पहला नाम\", \"अंतिम नाम\"], \"टेम्पलेट\": \"{{firstName}} {{lastName}}\"}, \"{{identifier}}\": \"uid\"}`", + "SAML_Custom_user_data_custom_fieldmap": "उपयोगकर्ता डेटा कस्टम फ़ील्ड मानचित्र", + "SAML_Custom_user_data_custom_fieldmap_description": "कॉन्फ़िगर करें कि SAML में रिकॉर्ड से उपयोगकर्ता कस्टम फ़ील्ड कैसे पॉप्युलेट किए जाते हैं (एक बार मिल जाने पर)।", + "SAML_Custom_Username_Field": "उपयोक्तानाम फ़ील्ड नाम", + "SAML_Custom_Username_Normalize": "उपयोक्तानाम सामान्यीकृत करें", + "SAML_Custom_Username_Normalize_Lowercase": "लोअरकेस करने के लिए", + "SAML_Custom_Username_Normalize_None": "कोई सामान्यीकरण नहीं", + "SAML_Default_User_Role": "डिफ़ॉल्ट उपयोगकर्ता भूमिका", + "SAML_Default_User_Role_Description": "आप एकाधिक भूमिकाएँ निर्दिष्ट कर सकते हैं, उन्हें अल्पविराम से अलग कर सकते हैं।", + "SAML_Identifier_Format": "पहचानकर्ता प्रारूप", + "SAML_Identifier_Format_Description": "अनुरोध से NameID नीति को हटाने के लिए इसे खाली छोड़ दें।", + "SAML_LogoutRequest_Template": "लॉगआउट अनुरोध टेम्पलेट", + "SAML_LogoutRequest_Template_Description": "निम्नलिखित चर उपलब्ध हैं:\n- **\\_\\_newId\\_\\_**: यादृच्छिक रूप से उत्पन्न आईडी स्ट्रिंग\n- **\\_\\_तत्काल\\_\\_**: वर्तमान टाइमस्टैम्प\n- **\\_\\_idpSLORedirectURL\\_\\_**: रीडायरेक्ट करने के लिए आईडीपी सिंगल लॉगआउट यूआरएल।\n- **\\_\\_जारीकर्ता\\_\\_**: {{Custom Issuer}} सेटिंग का मान।\n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} सेटिंग का मान।\n- **\\_\\_nameID\\_\\_**: उपयोगकर्ता द्वारा लॉग इन करने पर आईडीपी से प्राप्त NameID।\n- **\\_\\_sessionIndex\\_\\_**: उपयोगकर्ता द्वारा लॉग इन करने पर आईडीपी से सेशन इंडेक्स प्राप्त होता है।", + "SAML_LogoutResponse_Template": "लॉगआउट प्रतिक्रिया टेम्पलेट", + "SAML_LogoutResponse_Template_Description": "निम्नलिखित चर उपलब्ध हैं:\n- **\\_\\_newId\\_\\_**: यादृच्छिक रूप से उत्पन्न आईडी स्ट्रिंग\n- **\\_\\_inResponseToId\\_\\_**: आईडीपी से प्राप्त लॉगआउट अनुरोध की आईडी\n- **\\_\\_तत्काल\\_\\_**: वर्तमान टाइमस्टैम्प\n- **\\_\\_idpSLORedirectURL\\_\\_**: रीडायरेक्ट करने के लिए आईडीपी सिंगल लॉगआउट यूआरएल।\n- **\\_\\_जारीकर्ता\\_\\_**: {{Custom Issuer}} सेटिंग का मान।\n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} सेटिंग का मान।\n- **\\_\\_nameID\\_\\_**: IdP लॉगआउट अनुरोध से प्राप्त NameID।\n- **\\_\\_sessionIndex\\_\\_**: IdP लॉगआउट अनुरोध से प्राप्त sessionIndex।", + "SAML_Metadata_Certificate_Template_Description": "निम्नलिखित चर उपलब्ध हैं:\n- **\\_\\_प्रमाणपत्र\\_\\_**: दावा एन्क्रिप्शन के लिए निजी प्रमाणपत्र।", + "SAML_Metadata_Template": "मेटाडेटा टेम्पलेट", + "SAML_Metadata_Template_Description": "निम्नलिखित चर उपलब्ध हैं:\n- **\\_\\_sloLocation\\_\\_**: रॉकेट.चैट सिंगल लॉगआउट यूआरएल।\n- **\\_\\_जारीकर्ता\\_\\_**: {{Custom Issuer}} सेटिंग का मान।\n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} सेटिंग का मान।\n- **\\_\\_certificateTag\\_\\_**: यदि कोई निजी प्रमाणपत्र कॉन्फ़िगर किया गया है, तो इसमें {{Metadata Certificate Template}} शामिल होगा, अन्यथा इसे अनदेखा कर दिया जाएगा।\n- **\\_\\_कॉलबैकयूआरएल\\_\\_**: रॉकेट.चैट कॉलबैक यूआरएल।", + "SAML_MetadataCertificate_Template": "मेटाडेटा प्रमाणपत्र टेम्पलेट", + "SAML_NameIdPolicy_Template": "NameID नीति टेम्पलेट", + "SAML_NameIdPolicy_Template_Description": "आप यहां अधिकृत अनुरोध टेम्पलेट से किसी भी वेरिएबल का उपयोग कर सकते हैं।", + "SAML_Role_Attribute_Name": "भूमिका विशेषता का नाम", + "SAML_Role_Attribute_Name_Description": "यदि यह विशेषता SAML प्रतिक्रिया पर पाई जाती है, तो इसके मानों का उपयोग नए उपयोगकर्ताओं के लिए भूमिका नाम के रूप में किया जाएगा।", + "SAML_Role_Attribute_Sync": "उपयोगकर्ता भूमिकाएँ सिंक करें", + "SAML_Role_Attribute_Sync_Description": "लॉगिन पर SAML उपयोगकर्ता भूमिकाओं को सिंक करें (स्थानीय उपयोगकर्ता भूमिकाओं को अधिलेखित करता है)।", + "SAML_Section_1_User_Interface": "प्रयोक्ता इंटरफ़ेस", + "SAML_Section_2_Certificate": "प्रमाणपत्र", + "SAML_Section_3_Behavior": "व्यवहार", + "SAML_Section_4_Roles": "भूमिकाएँ", + "SAML_Section_5_Mapping": "मानचित्रण", + "SAML_Section_6_Advanced": "विकसित", + "SAML_Custom_channels_update": "प्रत्येक लॉगिन पर रूम सब्सक्रिप्शन अपडेट करें", + "SAML_Custom_channels_update_description": "यह सुनिश्चित करता है कि उपयोगकर्ता प्रत्येक लॉगिन पर SAML दावे में सभी चैनलों का सदस्य है।", + "SAML_Custom_include_private_channels_update": "रूम सब्सक्रिप्शन में निजी कमरे शामिल करें", + "SAML_Custom_include_private_channels_update_description": "उपयोगकर्ता को SAML दावे में मौजूद किसी भी निजी कमरे में जोड़ता है।", + "Saturday": "शनिवार", + "Save": "बचाना", + "Save_changes": "परिवर्तनों को सुरक्षित करें", + "Save_Mobile_Bandwidth": "मोबाइल बैंडविड्थ सहेजें", + "Save_to_enable_this_action": "इस क्रिया को सक्षम करने के लिए सहेजें", + "Save_To_Webdav": "WebDAV में सहेजें", + "Save_your_encryption_password": "अपना एन्क्रिप्शन पासवर्ड सहेजें", + "save-all-canned-responses": "सभी डिब्बाबंद प्रतिक्रियाएँ सहेजें", + "save-all-canned-responses_description": "सभी डिब्बाबंद प्रतिक्रियाओं को सहेजने की अनुमति", + "save-canned-responses": "डिब्बाबंद प्रतिक्रियाएँ सहेजें", + "save-canned-responses_description": "डिब्बाबंद प्रत्युत्तरों को सहेजने की अनुमति", + "save-department-canned-responses": "विभाग डिब्बाबंद प्रतिक्रियाएँ सहेजें", + "save-department-canned-responses_description": "विभाग द्वारा डिब्बाबंद प्रत्युत्तरों को सहेजने की अनुमति", + "save-others-livechat-room-info": "अन्य ओमनीचैनल कक्ष जानकारी सहेजें", + "save-others-livechat-room-info_description": "अन्य सर्वचैनल कक्षों से जानकारी सहेजने की अनुमति", + "Saved": "बचाया", + "Saving": "सहेजा जा रहा है", + "Scan_QR_code": "Google Authenticator, Authy या Duo जैसे प्रमाणक ऐप का उपयोग करके QR कोड को स्कैन करें। यह 6 अंकों का कोड प्रदर्शित करेगा जिसे आपको नीचे दर्ज करना होगा।", + "Scan_QR_code_alternative_s": "यदि आप क्यूआर कोड को स्कैन नहीं कर सकते हैं, तो आप इसके बजाय मैन्युअल रूप से कोड दर्ज कर सकते हैं:", "Scope": "क्षेत्र", + "Score": "अंक", + "Screen_Lock": "स्क्रीन लॉक है", + "Screen_Share": "स्क्रीन शेयर", + "Script": "लिखी हुई कहानी", + "Script_Enabled": "स्क्रिप्ट सक्षम", + "Script_Engine": "स्क्रिप्ट सैंडबॉक्स", + "Script_Engine_Description": "पुरानी स्क्रिप्ट को ठीक से चलाने के लिए संगत सैंडबॉक्स की आवश्यकता हो सकती है, लेकिन सभी नई स्क्रिप्ट को इसके बजाय सुरक्षित सैंडबॉक्स का उपयोग करने का प्रयास करना चाहिए।", + "Script_Engine_vm2": "संगत सैंडबॉक्स (अस्वीकृत)", + "Script_Engine_isolated_vm": "सुरक्षित सैंडबॉक्स", + "Search": "खोज", + "Searchable": "खोज सकने", + "Search_Apps": "ऐप्स खोजें", + "Search_Installed_Apps": "इंस्टॉल किए गए ऐप्स खोजें", + "Search_Private_apps": "निजी ऐप्स खोजें", + "Search_Requested_Apps": "अनुरोधित ऐप्स खोजें", + "Search_Premium_Apps": "प्रीमियम ऐप्स खोजें", + "Search_by_file_name": "फ़ाइल नाम से खोजें", + "Search_by_username": "उपयोगकर्ता नाम से खोजें", + "Search_by_category": "श्रेणी के आधार पर खोजें", + "Search_Channels": "चैनल खोजें", + "Search_Chat_History": "चैट इतिहास खोजें", + "Search_current_provider_not_active": "वर्तमान खोज प्रदाता सक्रिय नहीं है", + "Search_Description": "कार्यक्षेत्र खोज प्रदाता का चयन करें और खोज संबंधी सेटिंग्स कॉन्फ़िगर करें।", + "Search_Devices_Users": "डिवाइस या उपयोगकर्ता खोजें", + "Search_Files": "फ़ाइल ढूंढो", + "Search_for_a_more_general_term": "अधिक सामान्य शब्द खोजें", + "Search_for_a_more_specific_term": "अधिक विशिष्ट शब्द खोजें", + "Search_Integrations": "एकीकरण खोजें", + "Search_message_search_failed": "खोज अनुरोध विफल रहा", + "Search_Messages": "संदेश खोजें", + "Search_on_marketplace": "मार्केटप्लेस पर खोजें", + "Search_Page_Size": "पृष्ठ आकार", + "Search_Private_Groups": "निजी समूह खोजें", + "Search_Provider": "प्रदाता खोजें", + "Search_rooms": "कमरे खोजें", + "Search_Rooms": "कमरे खोजें", + "Search_Users": "उपयोगकर्ता खोजें", + "Seats_Available": "{{seatsLeft}} सीटें उपलब्ध हैं", + "Seats_usage": "सीटों का उपयोग", + "seconds": "सेकंड", + "Secret_token": "गुप्त टोकन", + "Secure_SaaS_solution": "सुरक्षित SaaS समाधान.", + "Security": "सुरक्षा", + "See_all_themes": "सभी थीम देखें", + "See_documentation": "दस्तावेज़ देखें", + "See_Paid_Plan": "सशुल्क योजना देखें", + "See_Pricing": "मूल्य निर्धारण देखें", + "See_full_profile": "पूरी प्रोफ़ाइल देखें", + "See_history": "इतिहास देखें", + "See_on_Engagement_Dashboard": "एंगेजमेंट डैशबोर्ड पर देखें", "Select_a_department": "एक विभाग का चयन करें", + "Select_a_room": "एक कमरा चुनें", + "Select_a_user": "एक उपयोगकर्ता चुनें", + "Select_a_webdav_server": "एक WebDAV सर्वर चुनें", + "Select_an_avatar": "एक अवतार चुनें", + "Select_an_option": "कोई विकल्प चुनें", + "Select_at_least_one_user": "कम से कम एक उपयोगकर्ता का चयन करें", + "Select_at_least_two_users": "कम से कम दो उपयोगकर्ता चुनें", "Select_department": "एक विभाग का चयन करें", + "Select_file": "फ़ाइल का चयन करें", + "Select_role": "एक भूमिका चुनें", + "Select_service_to_login": "अपनी तस्वीर लोड करने या सीधे अपने कंप्यूटर से अपलोड करने के लिए लॉगिन करने के लिए एक सेवा का चयन करें", + "Select_tag": "एक टैग चुनें", + "Select_the_channels_you_want_the_user_to_be_removed_from": "उन चैनलों का चयन करें जिनसे आप उपयोगकर्ता को हटाना चाहते हैं", + "Select_the_teams_channels_you_would_like_to_delete": "उस टीम के चैनल का चयन करें जिसे आप हटाना चाहते हैं, जिन्हें आप नहीं चुनेंगे उन्हें कार्यक्षेत्र में ले जाया जाएगा।", + "Select_atleast_one_channel_to_forward_the_messsage_to": "संदेश अग्रेषित करने के लिए कम से कम एक चैनल चुनें", + "Select_user": "उपयोगकर्ता का चयन करें", + "Select_users": "उपयोगकर्ताओं का चयन करें", + "Selected_agents": "चयनित एजेंट", + "Selected_by_default": "डिफ़ॉल्ट रूप से चयनित", + "Selected_departments": "चयनित विभाग", + "Selected_first_reply_unselected_following_replies": "पहले उत्तर के लिए चयनित, निम्नलिखित उत्तरों के लिए अचयनित", + "Selected_monitors": "चयनित मॉनिटर्स", + "Selecting_users": "उपयोगकर्ताओं का चयन करना", "Send": "भेजना", + "Send_a_message": "एक संदेश भेजो", + "Send_a_test_mail_to_my_user": "मेरे उपयोगकर्ता को एक परीक्षण मेल भेजें", + "Send_a_test_push_to_my_user": "मेरे उपयोगकर्ता को एक परीक्षण पुश भेजें", + "Send_confirmation_email": "पुष्टिकरण ईमेल भेजें", + "Send_data_into_RocketChat_in_realtime": "वास्तविक समय में Rocket.Chat में डेटा भेजें।", + "Send_email": "ईमेल भेजें", + "Send_Email_SMTP_Warning": "इस ईमेल को भेजने के लिए आपको SMTP ईमेलिंग सर्वर सेटअप करना होगा", + "Send_invitation_email": "आमंत्रण ईमेल भेजें", + "Send_invitation_email_error": "आपने कोई वैध ईमेल पता प्रदान नहीं किया है.", + "Send_invitation_email_info": "आप एक साथ अनेक ईमेल आमंत्रण भेज सकते हैं.", + "Send_invitation_email_success": "आपने निम्नलिखित पते पर सफलतापूर्वक आमंत्रण ईमेल भेज दिया है:", + "Send_it_as_attachment_instead_question": "इसके बजाय इसे अनुलग्नक के रूप में भेजें?", + "Send_me_the_code_again": "मुझे दोबारा कोड भेजें", + "Send_request_on": "पर अनुरोध भेजें", + "Send_request_on_agent_message": "एजेंट संदेशों पर अनुरोध भेजें", + "Send_request_on_chat_close": "चैट बंद करने पर अनुरोध भेजें", + "Send_request_on_chat_queued": "चैट कतार पर अनुरोध भेजें", + "Send_request_on_chat_start": "चैट प्रारंभ पर अनुरोध भेजें", + "Send_request_on_chat_taken": "ली गई चैट पर अनुरोध भेजें", + "Send_request_on_forwarding": "अग्रेषण पर अनुरोध भेजें", + "Send_request_on_lead_capture": "लीड कैप्चर पर अनुरोध भेजें", + "Send_request_on_offline_messages": "ऑफ़लाइन संदेशों पर अनुरोध भेजें", + "Send_request_on_visitor_message": "विज़िटर संदेशों पर अनुरोध भेजें", + "Send_Test": "परीक्षण भेजें", + "Send_Test_Email": "परीक्षण ईमेल भेजें", + "Send_via_email": "ईमेल द्वारा भेजें", + "Send_via_Email_as_attachment": "अनुलग्नक के रूप में ईमेल द्वारा भेजें", + "Export_as_PDF": "पीडीएफ के रूप में निर्यात करें", + "Export_enabled_at_the_end_of_the_conversation": "बातचीत के अंत में निर्यात सक्षम किया गया", + "Send_Visitor_navigation_history_as_a_message": "विज़िटर नेविगेशन इतिहास को संदेश के रूप में भेजें", + "Send_visitor_navigation_history_on_request": "अनुरोध पर विज़िटर नेविगेशन इतिहास भेजें", + "Send_welcome_email": "स्वागत ईमेल भेजें", + "Send_your_JSON_payloads_to_this_URL": "अपने JSON पेलोड इस URL पर भेजें।", + "send-mail": "ईमेल भेजो", + "send-mail_description": "ईमेल भेजने की अनुमति", + "send-many-messages": "अनेक संदेश भेजें", + "send-many-messages_description": "प्रति सेकंड 5 संदेशों की दर सीमा को बायपास करने की अनुमति", + "send-omnichannel-chat-transcript": "ओमनीचैनल वार्तालाप प्रतिलेख भेजें", + "send-omnichannel-chat-transcript_description": "सर्वचैनल वार्तालाप प्रतिलेख भेजने की अनुमति", + "Sender_Info": "चैनल की जानकारी", + "Sending": "भेजना...", + "Sending_Invitations": "निमंत्रण भेजा जा रहा है", + "Sending_your_mail_to_s": "आपका मेल %s पर भेजा जा रहा है", + "Sent_an_attachment": "एक अनुलग्नक भेजा", + "Sent_from": "प्रेषक", + "Separate_multiple_words_with_commas": "एकाधिक शब्दों को अल्पविराम से अलग करें", + "Served_By": "द्वारा सेवा", + "Server": "सर्वर", + "Server_already_added": "सर्वर पहले ही जोड़ा जा चुका है", + "Server_doesnt_exist": "सर्वर मौजूद नहीं है", + "Servers": "सर्वर", + "Server_Configuration": "सर्वर कॉन्फ़िगरेशन", + "Server_File_Path": "सर्वर फ़ाइल पथ", + "Server_Folder_Path": "सर्वर फ़ोल्डर पथ", + "Server_Info": "सर्वर जानकारी", + "Server_name": "सर्वर का नाम", + "Server_Type": "सर्वर प्रकार", + "Service": "सेवा", + "Service_account_key": "सेवा खाता कुंजी", + "Set_as_favorite": "पसंदीदा के रूप में सेट करें", + "Set_as_leader": "नेता के रूप में स्थापित करें", + "Set_as_moderator": "मॉडरेटर के रूप में सेट करें", + "Set_as_owner": "स्वामी के रूप में सेट करें", + "Upload_app": "ऐप अपलोड करें", + "Set_random_password_and_send_by_email": "यादृच्छिक पासवर्ड सेट करें और ईमेल द्वारा भेजें", + "set-leader": "नेता सेट करें", + "set-leader_description": "अन्य उपयोगकर्ताओं को किसी चैनल के लीडर के रूप में सेट करने की अनुमति", + "set-moderator": "मॉडरेटर सेट करें", + "set-moderator_description": "अन्य उपयोगकर्ताओं को किसी चैनल के मॉडरेटर के रूप में सेट करने की अनुमति", + "set-owner": "स्वामी सेट करें", + "set-owner_description": "अन्य उपयोगकर्ताओं को किसी चैनल के स्वामी के रूप में सेट करने की अनुमति", + "set-react-when-readonly": "केवल पढ़ने के लिए प्रतिक्रिया सेट करें", + "set-react-when-readonly_description": "केवल पढ़ने योग्य चैनल में संदेशों पर प्रतिक्रिया करने की क्षमता सेट करने की अनुमति", + "set-readonly": "केवल पढ़ने के लिए सेट करें", + "set-readonly_description": "किसी चैनल को केवल पढ़ने के लिए चैनल सेट करने की अनुमति", + "Settings": "समायोजन", + "Settings_updated": "सेटिंग को अद्यतन किया गया है", + "Setup_SMTP": "एसएमटीपी सेट करें", + "Setup_Wizard": "स्थापना विज़ार्ड", + "Setup_Wizard_Description": "आपके कार्यक्षेत्र के बारे में बुनियादी जानकारी जैसे संगठन का नाम और देश।", + "Setup_Wizard_Info": "हम आपका पहला व्यवस्थापक उपयोगकर्ता स्थापित करने, आपके संगठन को कॉन्फ़िगर करने और निःशुल्क पुश सूचनाएं प्राप्त करने के लिए आपके सर्वर को पंजीकृत करने आदि में आपका मार्गदर्शन करेंगे।", + "Share": "शेयर करना", + "Share_Location_Title": "स्थान साझा करें?", + "Share_screen": "स्क्रीन साझा करना", + "New_CannedResponse": "नई डिब्बाबंद प्रतिक्रिया", + "Edit_CannedResponse": "डिब्बाबंद प्रतिक्रिया संपादित करें", + "Sharing": "शेयरिंग", + "Shared_Location": "साझा स्थान", + "Shared_Secret": "साझा रहस्य", + "Shortcut": "छोटा रास्ता", + "shortcut_name": "शॉर्टकट नाम", + "Should_be_a_URL_of_an_image": "किसी छवि का URL होना चाहिए.", + "Should_exists_a_user_with_this_username": "उपयोगकर्ता पहले से मौजूद होना चाहिए.", + "Show_agent_email": "एजेंट का ईमेल दिखाएँ", + "Show_agent_info": "एजेंट की जानकारी दिखाएँ", + "Show_all": "सब दिखाएं", + "Show_Avatars": "अवतार दिखाएँ", + "Show_counter": "अपठित के रूप में चिह्नित करें", + "Show_default_content": "डिफ़ॉल्ट सामग्री दिखाएँ", + "Show_email_field": "ईमेल फ़ील्ड दिखाएँ", + "Show_mentions": "उल्लेख के लिए बैज दिखाएँ", + "Show_more": "और दिखाओ", + "Show_name_field": "नाम फ़ील्ड दिखाएँ", + "show_offline_users": "ऑफ़लाइन उपयोगकर्ता दिखाएं", + "Show_on_offline_page": "ऑफ़लाइन पेज पर दिखाएं", + "Show_on_registration_page": "पंजीकरण पृष्ठ पर दिखाएँ", + "Show_only_online": "केवल ऑनलाइन दिखाएँ", + "Show_Only_This_Content": "केवल यही सामग्री दिखाएँ", + "Show_preregistration_form": "प्री-रजिस्ट्रेशन फॉर्म दिखाएँ", + "Show_queue_list_to_all_agents": "सभी एजेंटों को कतार सूची दिखाएं", + "Show_room_counter_on_sidebar": "साइडबार पर शो रूम काउंटर", + "Show_Setup_Wizard": "सेटअप विज़ार्ड दिखाएँ", + "Show_the_keyboard_shortcut_list": "कुंजीपटल शॉर्टकट सूची दिखाएँ", + "Show_To_Workspace": "कार्यस्थल पर दिखाएँ", + "Show_video": "वीडियो दिखाओ", + "Showing": "दिखा", + "Showing_archived_results": "

    %s संग्रहीत परिणाम दिखा रहा है

    ", + "Showing_current_of_total": "{{total}} में से {{current}} दिखाया जा रहा है", + "Showing_online_users": "दिखाया जा रहा है: {{total_showing}} , ऑनलाइन: {{online}}, कुल: {{total}} उपयोगकर्ता", + "Showing_results": "

    %s परिणाम दिखा रहा है

    ", + "Showing_results_of": "%s - %s के %s परिणाम दिखा रहा है", + "Show_usernames": "उपयोक्तानाम दिखाएँ", + "Show_roles": "भूमिकाएँ दिखाएँ", + "Show_or_hide_the_user_roles_of_message_authors": "संदेश लेखकों की उपयोगकर्ता भूमिकाएँ दिखाएँ या छिपाएँ।", + "Show_or_hide_the_username_of_message_authors": "संदेश लेखकों का उपयोगकर्ता नाम दिखाएँ या छिपाएँ।", + "Sidebar": "साइड बार", + "Sidebar_list_mode": "साइडबार चैनल सूची मोड", + "Sign_in_to_start_talking": "बातचीत शुरू करने के लिए साइन इन करें", + "Sign_in_with__provider__": "{{provider}} के साथ साइन इन करें", + "since_creation": "%s के बाद से", + "Site_Name": "जगह का नाम", + "Site_Url": "साइट URL", + "Site_Url_Description": "उदाहरण: `https://chat.domain.com/`", + "Size": "आकार", + "Skin_tone": "त्वचा का रंग", "Skip": "छोड़ें", + "SLA_Policy": "एसएलए नीति", + "SLA_Policies": "एसएलए नीतियां", + "SLA_removed": "एसएलए हटा दिया गया", + "Slack_Users": "स्लैक के उपयोगकर्ता सीएसवी", + "SlackBridge_APIToken": "एपीआई टोकन (विरासत)", + "SlackBridge_UseLegacy": "लीगेसी एपीआई टोकन का उपयोग करें", + "SlackBridge_APIToken_Description": "आप प्रति पंक्ति एक एपीआई टोकन जोड़कर एकाधिक स्लैक सर्वर कॉन्फ़िगर कर सकते हैं।", + "SlackBridge_BotToken": "बॉट टोकन", + "SlackBridge_BotToken_Description": "आप प्रति पंक्ति एक बॉट टोकन जोड़कर एकाधिक स्लैक सर्वर कॉन्फ़िगर कर सकते हैं।", + "SlackBridge_AppToken": "ऐप टोकन", + "SlackBridge_AppToken_Description": "आप प्रति पंक्ति एक ऐप टोकन जोड़कर एकाधिक स्लैक सर्वर कॉन्फ़िगर कर सकते हैं।", + "SlackBridge_SigningSecret": "हस्ताक्षर गुप्त", + "SlackBridge_SigningSecret_Description": "आप प्रति पंक्ति एक हस्ताक्षर रहस्य जोड़कर एकाधिक स्लैक सर्वर कॉन्फ़िगर कर सकते हैं।", + "Slackbridge_channel_links_removed_successfully": "स्लैकब्रिज चैनल लिंक सफलतापूर्वक हटा दिए गए हैं।", + "SlackBridge_Description": "स्लैक के साथ सीधे संवाद करने के लिए Rocket.Chat को सक्षम करें।", + "SlackBridge_error": "आपके संदेशों को %s पर आयात करते समय स्लैकब्रिज को एक त्रुटि मिली: %s", + "SlackBridge_finish": "स्लैकब्रिज ने %s पर संदेशों का आयात पूरा कर लिया है। कृपया सभी संदेशों को देखने के लिए पुनः लोड करें।", + "SlackBridge_Out_All": "स्लैकब्रिज आउट ऑल", + "SlackBridge_Out_All_Description": "उन सभी चैनलों से संदेश भेजें जो स्लैक में मौजूद हैं और बॉट शामिल हो गया है", + "SlackBridge_Out_Channels": "स्लैकब्रिज आउट चैनल", + "SlackBridge_Out_Channels_Description": "चुनें कि कौन से चैनल स्लैक को संदेश वापस भेजेंगे", + "SlackBridge_Out_Enabled": "स्लैकब्रिज आउट सक्षम", + "SlackBridge_Out_Enabled_Description": "चुनें कि क्या स्लैकब्रिज को भी आपके संदेश स्लैक को वापस भेजने चाहिए", + "SlackBridge_Remove_Channel_Links_Description": "रॉकेट.चैट चैनलों और स्लैक चैनलों के बीच आंतरिक लिंक हटाएं। बाद में चैनल नामों के आधार पर लिंक फिर से बनाए जाएंगे।", + "SlackBridge_start": "@%s ने `#%s` पर स्लैकब्रिज आयात शुरू किया है। जब यह पूरा हो जाएगा तो हम आपको बताएंगे।", + "Slash_Gimme_Description": "आपके संदेश से पहले ༼ツ ◕_◕ ༽ツ प्रदर्शित करता है", + "Slash_LennyFace_Description": "आपके संदेश के बाद ( ͡° ͜ʖ ͡°) प्रदर्शित होता है", + "Slash_Shrug_Description": "आपके संदेश के बाद ¯\\_(ツ)_/¯ प्रदर्शित करता है", + "Slash_Status_Description": "अपना स्थिति संदेश सेट करें", + "Slash_Status_Params": "स्थिति संदेश", + "Slash_Tableflip_Description": "प्रदर्शित करता है (╯°□°)╯︵ ┻━┻", + "Slash_TableUnflip_Description": "प्रदर्शित करता है ┬─┬ ノ( ゜-゜ノ)", + "Slash_Topic_Description": "विषय निर्धारित करें", + "Slash_Topic_Params": "विषय संदेश", + "Smarsh": "Smarsh", + "Smarsh_Description": "ईमेल संचार को सुरक्षित रखने के लिए कॉन्फ़िगरेशन.", + "Smarsh_Email": "स्मर्श ईमेल", + "Smarsh_Email_Description": ".eml फ़ाइल भेजने के लिए स्मर्श ईमेल पता।", + "Smarsh_Enabled": "स्मर्श सक्षम", + "Smarsh_Enabled_Description": "क्या स्मर्श ईएमएल कनेक्टर सक्षम है या नहीं (ईमेल -> एसएमटीपी के तहत 'ईमेल से' भरने की जरूरत है)।", + "Smarsh_Interval": "स्मर्श अंतराल", + "Smarsh_Interval_Description": "चैट भेजने से पहले प्रतीक्षा करने की मात्रा (ईमेल -> एसएमटीपी के तहत 'ईमेल से' भरने की आवश्यकता है)।", + "Smarsh_MissingEmail_Email": "ईमेल गुम है", + "Smarsh_MissingEmail_Email_Description": "किसी उपयोगकर्ता खाते का ईमेल पता गायब होने पर उसे दिखाया जाने वाला ईमेल आम तौर पर बॉट खातों के साथ होता है।", + "Smarsh_Timezone": "स्मर्श टाइमज़ोन", + "Smileys_and_People": "स्माइलीज़ और लोग", + "SMS": "एसएमएस", + "SMS_Description": "अपने कार्यक्षेत्र पर एसएमएस गेटवे सक्षम और कॉन्फ़िगर करें।", + "SMS_Default_Omnichannel_Department": "ओमनीचैनल विभाग (डिफ़ॉल्ट)", + "SMS_Default_Omnichannel_Department_Description": "यदि सेट किया गया है, तो इस एकीकरण द्वारा शुरू की गई सभी नई आने वाली चैट इस विभाग में भेज दी जाएंगी।\nअनुरोध में विभाग क्वेरी पैरामीटर पास करके इस सेटिंग को ओवरराइट किया जा सकता है।\nजैसे `https://{{SERVER_URL}}/api/v1/livechat/sms-incoming/twilio?department={{Department Id or Name}}`।\nनोट: यदि आप विभाग नाम का उपयोग कर रहे हैं, तो यह यूआरएल सुरक्षित होना चाहिए।", + "SMS_Enabled": "एसएमएस सक्षम", + "SMS_Twilio_NotConfigured": "ट्विलियो एसएमएस अभी तक कॉन्फ़िगर नहीं किया गया है। इसे कॉन्फ़िगर करने के लिए सेटिंग्स -> एसएमएस पर जाएं", + "SMS_Twilio_InvalidCredentials": "ट्विलियो एसएमएस क्रेडेंशियल अमान्य हैं, संदेश नहीं भेज सकते", + "SMTP": "एसएमटीपी", + "SMTP_Host": "एसएमटीपी होस्ट", + "SMTP_Password": "एसएमटीपी पासवर्ड", + "SMTP_Port": "एसएमटीपी पोर्ट", + "SMTP_Server_Not_Setup_Title": "SMTP सर्वर अभी तक सेटअप नहीं हुआ है", + "SMTP_Server_Not_Setup_Description": "आमंत्रण भेजना शुरू करने या उपयोगकर्ताओं को मैन्युअल रूप से जोड़ने के लिए अपना एसएमटीपी ईमेलिंग सर्वर सेट करें", + "SMTP_Test_Button": "एसएमटीपी सेटिंग्स का परीक्षण करें", + "SMTP_Username": "एसएमटीपी उपयोगकर्ता नाम", + "Snippet_Added": "%s पर बनाया गया", + "Snippet_name": "स्निपेट नाम", + "Snippeted_a_message": "एक स्निपेट {{snippetLink}} बनाया गया", + "Social_Network": "सामाजिक नेटवर्क", + "Some_ideas_to_get_you_started": "आपको आरंभ करने के लिए कुछ विचार", + "Something_went_wrong": "कुछ गलत हो गया", + "Something_went_wrong_try_again_later": "कुछ गलत हो गया, बाद में पुनः प्रयास करें।", + "Something_went_wrong_while_executing_command": "कमांड निष्पादित करते समय कुछ गलत हो गया: `/{{command}}`", + "Sorry_page_you_requested_does_not_exist_or_was_deleted": "क्षमा करें, आपके द्वारा अनुरोधित पृष्ठ मौजूद नहीं है या हटा दिया गया है!", + "Sort": "क्रम से लगाना", + "Sort_By": "इसके अनुसार क्रमबद्ध करें", + "Sorting_mechanism": "छँटाई तंत्र", + "Service_level_agreements": "सेवा स्तर अनुबंध", + "Sort_by_activity": "गतिविधि के आधार पर क्रमबद्ध करें", + "Sound": "आवाज़", + "Sounds": "ध्वनि", + "Sound_File_mp3": "ध्वनि फ़ाइल (एमपी3)", + "Sound File": "ध्वनि फ़ाइल", + "Source": "स्रोत", + "Speakers": "वक्ताओं", + "spy-voip-calls": "जासूस वीओआईपी कॉल", + "spy-voip-calls_description": "वीओआईपी कॉल की जासूसी करने की अनुमति", + "SSL": "एसएसएल", + "Star": "तारा", + "Star_Message": "सितारा संदेश", + "Starred_Messages": "तारांकित संदेश", + "Start": "शुरू", + "Start_a_call": "कॉल प्रारंभ करें", + "Start_a_free_trial": "निःशुल्क परीक्षण प्रारंभ करें", + "Start_audio_call": "ऑडियो कॉल प्रारंभ करें", + "Start_call": "कॉल प्रारंभ करें", "Start_Chat": "बातचीत शुरू ", + "Start_conference_call": "कॉन्फ़्रेंस कॉल प्रारंभ करें", + "Start_free_trial": "निशुल्क आजमाइश शुरु करें", + "Start_of_conversation": "बातचीत की शुरुआत", + "Start_OTR": "ओटीआर प्रारंभ करें", + "Start_video_call": "वीडियो कॉल प्रारंभ करें", + "Start_video_conference": "कॉन्फ़्रेंस कॉल प्रारंभ करें?", + "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s": "उपयोगकर्ता के लिए %s या चैनल के लिए %s से प्रारंभ करें। जैसे: %s या %s", + "start-discussion": "चर्चा चलाना", + "start-discussion_description": "चर्चा शुरू करने की अनुमति", + "start-discussion-other-user": "चर्चा प्रारंभ करें (अन्य-उपयोगकर्ता)", + "start-discussion-other-user_description": "चर्चा शुरू करने की अनुमति, जो उपयोगकर्ता को किसी अन्य उपयोगकर्ता द्वारा भेजे गए संदेश से भी चर्चा बनाने की अनुमति देती है", + "Started": "शुरू कर दिया", + "Started_a_video_call": "एक वीडियो कॉल शुरू की", + "Started_At": "इस समय पर शुरू किया", + "Statistics": "आंकड़े", + "Statistics_reporting": "Rocket.Chat पर आँकड़े भेजें", + "Statistics_reporting_Description": "अपने आँकड़े भेजकर, आप हमें यह पहचानने में मदद करेंगे कि Rocket.Chat के कितने उदाहरण तैनात हैं, साथ ही सिस्टम कितना अच्छा व्यवहार कर रहा है, ताकि हम इसे और बेहतर बना सकें। चिंता न करें, क्योंकि कोई भी उपयोगकर्ता जानकारी नहीं भेजी जाती है और हमें प्राप्त होने वाली सभी जानकारी गोपनीय रखी जाती है।", + "Stats_Active_Guests": "सक्रिय अतिथि", + "Stats_Active_Users": "सक्रिय उपयोगकर्ता", + "Stats_App_Users": "Rocket.Chat ऐप उपयोगकर्ता", + "Stats_Avg_Channel_Users": "औसत चैनल उपयोगकर्ता", + "Stats_Avg_Private_Group_Users": "औसत निजी समूह उपयोगकर्ता", + "Stats_Away_Users": "दूर उपयोगकर्ता", + "Stats_Max_Room_Users": "अधिकतम कमरे उपयोगकर्ता", + "Stats_Non_Active_Users": "निष्क्रिय उपयोगकर्ता", + "Stats_Offline_Users": "ऑफ़लाइन उपयोगकर्ता", + "Stats_Online_Users": "ऑनलाइन उपयोगकर्ता", + "Stats_Total_Active_Apps": "कुल सक्रिय ऐप्स", + "Stats_Total_Active_Incoming_Integrations": "कुल सक्रिय आवक एकीकरण", + "Stats_Total_Active_Outgoing_Integrations": "कुल सक्रिय आउटगोइंग एकीकरण", + "Stats_Total_Channels": "चैनल", + "Stats_Total_Connected_Users": "कुल जुड़े हुए उपयोगकर्ता", + "Stats_Total_Direct_Messages": "सीधे संदेश", + "Stats_Total_Incoming_Integrations": "कुल आवक एकीकरण", + "Stats_Total_Installed_Apps": "कुल इंस्टॉल किए गए ऐप्स", + "Stats_Total_Integrations": "कुल एकीकरण", + "Stats_Total_Integrations_With_Script_Enabled": "स्क्रिप्ट सक्षम के साथ पूर्ण एकीकरण", + "Stats_Total_Livechat_Rooms": "ओमनीचैनल कमरे", + "Stats_Total_Messages": "संदेशों", + "Stats_Total_Messages_Channel": "चैनलों में", + "Stats_Total_Messages_Direct": "सीधे संदेशों में", + "Stats_Total_Messages_Livechat": "सर्वचैनल में", + "Stats_Total_Messages_PrivateGroup": "निजी समूहों में", + "Stats_Total_Messages_Discussions": "चर्चाओं में", + "Stats_Total_Outgoing_Integrations": "कुल आउटगोइंग एकीकरण", + "Stats_Total_Private_Groups": "निजी समूह", + "Stats_Total_Rooms": "कमरा", + "Stats_Total_Uploads": "कुल अपलोड", + "Stats_Total_Uploads_Size": "कुल अपलोड आकार", + "Stats_Total_Users": "कुल उपयोगकर्ता", + "Status": "स्थिति", + "StatusMessage": "स्थिति संदेश", + "StatusMessage_Change_Disabled": "आपके Rocket.Chat व्यवस्थापक ने स्थिति संदेशों को बदलना अक्षम कर दिया है", + "StatusMessage_Changed_Successfully": "स्थिति संदेश सफलतापूर्वक बदला गया.", + "StatusMessage_Placeholder": "आप अभी क्या कर रहे हैं?", + "StatusMessage_Too_Long": "स्थिति संदेश 120 अक्षरों से छोटा होना चाहिए.", + "Step": "कदम", + "Stop_call": "कॉल बंद करो", + "Stop_Recording": "रिकॉर्डिंग बंद करें", + "Store_Last_Message": "अंतिम संदेश संग्रहीत करें", + "Store_Last_Message_Sent_per_Room": "प्रत्येक कमरे पर भेजा गया अंतिम संदेश संग्रहीत करें।", + "Stream_Cast": "स्ट्रीम कास्ट", + "Stream_Cast_Address": "स्ट्रीम कास्ट पता", + "Stream_Cast_Address_Description": "आपके रॉकेट.चैट सेंट्रल स्ट्रीम कास्ट का आईपी या होस्ट। जैसे `192.168.1.1:3000` या `लोकलहोस्ट:4000`", + "Strike": "हड़ताल", + "Style": "शैली", + "Subject": "विषय", + "Submit": "जमा करना", + "Subscribe": "सदस्यता लें", + "Success": "सफलता", + "Success_message": "सफलता संदेश", + "Successfully_downloaded_file_from_external_URL_should_start_preparing_soon": "बाहरी यूआरएल से फ़ाइल सफलतापूर्वक डाउनलोड हो गई है, जल्द ही तैयारी शुरू कर देनी चाहिए", + "Suggestion_from_recent_messages": "हाल के संदेशों से सुझाव", + "Sunday": "रविवार", + "Support": "सहायता", "Survey": "सर्वेक्षण", "Survey_instructions": "प्रत्येक प्रश्न को अपनी संतुष्टि के अनुसार रेट करें, 1 मतलब कि आप पूरी तरह से असंतुष्ट हैं और 5 का अर्थ है कि आप पूरी तरह से संतुष्ट हैं।", + "Symbols": "प्रतीक", + "Sync": "साथ-साथ करना", + "Sync / Import": "सिंक/आयात करें", + "Sync_in_progress": "तुल्यकालन प्रगति पर है", + "Sync_Interval": "अंतराल सिंक करना", + "Sync_success": "समन्वयन सफल", + "Sync_Users": "उपयोगकर्ताओं को सिंक करें", + "sync-auth-services-users": "प्रमाणीकरण सेवाओं के उपयोगकर्ताओं को सिंक करें", + "sync-auth-services-users_description": "प्रमाणीकरण सेवाओं के उपयोगकर्ताओं को सिंक करने की अनुमति", + "System_messages": "सिस्टम संदेश", + "Tag": "टैग", + "Tags": "टैग", + "Tag_removed": "टैग हटा दिया गया", + "Tag_already_exists": "टैग पहले से मौजूद है", + "Take_it": "इसे लें!", + "Take_rocket_chat_with_you_with_mobile_applications": "मोबाइल एप्लिकेशन के साथ Rocket.Chat को अपने साथ ले जाएं।", + "Taken_at": "पर लिया गया", + "Talk_Time": "बात करने का समय", + "Talk_to_an_expert": "किसी विशेषज्ञ से बात करें", + "Talk_to_sales": "बिक्री से बात करें", + "Talk_to_your_workspace_administrator_about_enabling_video_conferencing": "वीडियो कॉन्फ्रेंसिंग सक्षम करने के बारे में अपने कार्यक्षेत्र व्यवस्थापक से बात करें", + "Talk_to_your_workspace_admin_to_address_this_issue": "इस समस्या के समाधान के लिए अपने कार्यक्षेत्र व्यवस्थापक से बात करें।", + "Target user not allowed to receive messages": "लक्षित उपयोगकर्ता को संदेश प्राप्त करने की अनुमति नहीं है", + "TargetRoom": "लक्ष्य कक्ष", + "TargetRoom_Description": "वह कमरा जहां संदेश भेजे जाएंगे जो इस घटना के परिणामस्वरूप निकाल दिए गए हैं। केवल एक लक्ष्य कक्ष की अनुमति है और वह मौजूद रहना चाहिए।", + "Team": "टीम", + "Team_Add_existing_channels": "मौजूदा चैनल जोड़ें", + "Team_Add_existing": "मौजूदा जोड़ें", + "Team_Auto-join": "ऑटो में शामिल हों", + "Team_Channels": "टीम चैनल", + "Team_Delete_Channel_modal_content_danger": "इसे पूर्ववत नहीं किया जा सकता.", + "Team_Delete_Channel_modal_content": "क्या आप इस चैनल को हटाना चाहेंगे?", + "Team_has_been_created": "टीम बनाई गई है", + "Team_has_been_deleted": "टीम हटा दी गई है", + "Team_Info": "टीम की जानकारी", + "Team_Mapping": "टीम मैपिंग", + "Team_Name": "टीम का नाम", + "Team_Remove_from_team_modal_content": "क्या आप इस चैनल को {{teamName}} से हटाना चाहेंगे? चैनल को वापस कार्यक्षेत्र में ले जाया जाएगा.", + "Team_Remove_from_team": "टीम से हटाओ", + "Team_what_is_this_team_about": "यह टीम किस बारे में है", + "Teams": "टीमें", + "Teams_about_the_channels": "और चैनलों के बारे में?", + "Teams_channels_didnt_leave": "आपने निम्नलिखित चैनलों का चयन नहीं किया है इसलिए आप उन्हें नहीं छोड़ रहे हैं:", + "Teams_channels_last_owner_delete_channel_warning": "आप इस चैनल के अंतिम मालिक हैं. एक बार जब आप टीम को एक चैनल में बदल देते हैं, तो चैनल को कार्यक्षेत्र में ले जाया जाएगा।", + "Teams_channels_last_owner_leave_channel_warning": "आप इस चैनल के अंतिम मालिक हैं. एक बार जब आप टीम छोड़ देते हैं, तो चैनल टीम के अंदर रखा जाएगा लेकिन आप इसे बाहर से प्रबंधित करेंगे।", + "Teams_leaving_team": "आप इस टीम को छोड़ रहे हैं.", + "Teams_channels": "टीम के चैनल", + "Teams_convert_channel_to_team": "टीम में कनवर्ट करें", + "Teams_delete_team_choose_channels": "वे चैनल चुनें जिन्हें आप हटाना चाहते हैं। जिन्हें आप रखने का निर्णय लेंगे, वे आपके कार्यक्षेत्र पर उपलब्ध रहेंगे।", + "Teams_delete_team_public_notice": "ध्यान दें कि सार्वजनिक चैनल अभी भी सार्वजनिक रहेंगे और सभी को दिखाई देंगे।", + "Teams_delete_team_Warning": "एक बार जब आप किसी टीम को हटा देते हैं, तो सभी चैट सामग्री और कॉन्फ़िगरेशन हटा दिए जाएंगे।", + "Teams_delete_team": "आप इस टीम को हटाने वाले हैं.", + "Teams_deleted_channels": "निम्नलिखित चैनल हटाए जा रहे हैं:", + "Teams_Errors_Already_exists": "टीम `{{name}}` पहले से मौजूद है।", + "Teams_Errors_team_name": "आप टीम के नाम के रूप में \"{{name}}\" का उपयोग नहीं कर सकते।", + "Teams_move_channel_to_team": "टीम में जाएँ", + "Teams_move_channel_to_team_description_first": "किसी चैनल को टीम के अंदर ले जाने का मतलब है कि इस चैनल को टीम के संदर्भ में जोड़ा जाएगा, हालांकि, चैनल के सभी सदस्य, जो संबंधित टीम के सदस्य नहीं हैं, उनके पास अभी भी इस चैनल तक पहुंच होगी, लेकिन उन्हें टीम के सदस्यों के रूप में नहीं जोड़ा जाएगा।", + "Teams_move_channel_to_team_description_second": "चैनल का सारा प्रबंधन अभी भी इस चैनल के मालिकों द्वारा किया जाएगा।", + "Teams_move_channel_to_team_description_third": "टीम के सदस्य और यहां तक कि टीम के मालिक, यदि इस चैनल के सदस्य नहीं हैं, तो चैनल की सामग्री तक पहुंच नहीं पा सकते हैं।", + "Teams_move_channel_to_team_description_fourth": "कृपया ध्यान दें कि टीम का मालिक सदस्यों को चैनल से हटा सकेगा।", + "Teams_move_channel_to_team_confirm_description": "इस व्यवहार के बारे में पिछले निर्देशों को पढ़ने के बाद, क्या आप इस कार्रवाई के साथ आगे बढ़ना चाहते हैं?", + "Teams_New_Title": "टीम बनाएं", + "Teams_New_Name_Label": "नाम", + "Teams_Info": "टीम सूचना", + "Teams_kept_channels": "आपने निम्नलिखित चैनलों का चयन नहीं किया है इसलिए उन्हें कार्यक्षेत्र में ले जाया जाएगा:", + "Teams_kept__username__channels": "आपने निम्नलिखित चैनलों का चयन नहीं किया है इसलिए उन पर {{username}} रखा जाएगा:", + "Teams_leave_channels": "उस टीम के चैनल का चयन करें जिसे आप छोड़ना चाहते हैं।", + "Teams_leave": "टीम छोड़ें", + "Teams_left_team_successfully": "टीम को सफलतापूर्वक छोड़ दिया", + "Teams_members": "टीमों के सदस्य", + "Teams_New_Add_members_Label": "सदस्य जोड़ें", + "Teams_New_Broadcast_Description": "केवल अधिकृत उपयोगकर्ता ही नए संदेश लिख सकते हैं, लेकिन अन्य उपयोगकर्ता उत्तर दे सकेंगे", + "Teams_New_Broadcast_Label": "प्रसारण", + "Teams_New_Description_Label": "विषय", + "Teams_New_Description_Placeholder": "यह टीम किस बारे में है", + "Teams_New_Encrypted_Description_Disabled": "केवल निजी टीम के लिए उपलब्ध है", + "Teams_New_Encrypted_Description_Enabled": "एंड-टू-एंड एन्क्रिप्टेड टीम। खोज एन्क्रिप्टेड टीमों के साथ काम नहीं करेगी और सूचनाएं संदेश सामग्री नहीं दिखा सकती हैं।", + "Teams_New_Encrypted_Label": "कूट रूप दिया गया", + "Teams_New_Private_Description_Disabled": "अक्षम होने पर, कोई भी टीम में शामिल हो सकता है", + "Teams_New_Private_Description_Enabled": "केवल आमंत्रित लोग ही शामिल हो सकते हैं", + "Teams_New_Private_Label": "निजी", + "Teams_New_Read_only_Description": "इस टीम के सभी उपयोगकर्ता संदेश लिख सकते हैं", + "Teams_Public_Team": "सार्वजनिक टीम", + "Teams_Private_Team": "निजी टीम", + "Teams_removing_member": "सदस्य को हटाया जा रहा है", + "Teams_removing__username__from_team": "आप इस टीम से {{username}} हटा रहे हैं", + "Teams_removing__username__from_team_and_channels": "आप इस टीम और इसके सभी चैनलों से {{username}} हटा रहे हैं।", + "Teams_Select_a_team": "एक टीम चुनें", + "Teams_Search_teams": "खोज दल", + "Teams_New_Read_only_Label": "केवल पढ़ने के लिए", + "Technology_Services": "प्रौद्योगिकी सेवाएँ", + "Terms": "शर्तें", + "Terms_of_use": "उपयोग की शर्तें", + "Test_Connection": "परीक्षण कनेक्शन", + "Test_Desktop_Notifications": "डेस्कटॉप सूचनाओं का परीक्षण करें", + "Test_LDAP_Search": "एलडीएपी खोज का परीक्षण करें", + "test-admin-options": "व्यवस्थापक पैनल पर परीक्षण विकल्प", + "test-admin-options_description": "एलडीएपी लॉगिन जैसे व्यवस्थापक पैनल पर विकल्पों का परीक्षण करने की अनुमति।", + "test-push-notifications": "पुश सूचनाओं का परीक्षण करें", + "test-push-notifications_description": "पुश सूचनाओं का परीक्षण करने की अनुमति", + "Texts": "ग्रंथों", "Thank_you_for_your_feedback": "आपकी प्रतिक्रिया के लिए आपका धन्यवाद", + "The_application_name_is_required": "एप्लिकेशन का नाम आवश्यक है", + "The_application_will_be_able_to": "<1>{{appName}} यह करने में सक्षम होगा:", + "The_channel_name_is_required": "चैनल का नाम आवश्यक है", + "The_emails_are_being_sent": "ईमेल भेजे जा रहे हैं.", + "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", + "The_field_is_required": "फ़ील्ड %s आवश्यक है.", + "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "छवि का आकार बदलना काम नहीं करेगा क्योंकि हम आपके सर्वर पर स्थापित ImageMagick या ग्राफ़िक्सMagick का पता नहीं लगा सकते हैं।", + "The_message_is_a_discussion_you_will_not_be_able_to_recover": "संदेश एक चर्चा है आप संदेशों को पुनर्प्राप्त नहीं कर पाएंगे!", + "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "मोबाइल सूचनाएं सभी उपयोगकर्ताओं के लिए अक्षम कर दी गई थीं, पुश गेटवे को फिर से सक्षम करने के लिए \"एडमिन > पुश\" पर जाएं", + "The_necessary_browser_permissions_for_location_sharing_are_not_granted": "स्थान साझाकरण के लिए आवश्यक ब्राउज़र अनुमतियाँ प्रदान नहीं की गई हैं", + "The_peer__peer__does_not_exist": "सहकर्मी {{peer}} मौजूद नहीं है।", + "The_redirectUri_is_required": "रीडायरेक्टयूरी आवश्यक है", + "The_selected_user_is_not_a_monitor": "चयनित उपयोगकर्ता मॉनिटर नहीं है", + "The_selected_user_is_not_an_agent": "चयनित उपयोगकर्ता कोई एजेंट नहीं है", + "The_server_will_restart_in_s_seconds": "सर्वर %s सेकंड में पुनरारंभ हो जाएगा", + "The_setting_s_is_configured_to_s_and_you_are_accessing_from_s": "सेटिंग %s को %s पर कॉन्फ़िगर किया गया है और आप %s से एक्सेस कर रहे हैं!", + "The_user_s_will_be_removed_from_role_s": "उपयोगकर्ता %s को भूमिका %s से हटा दिया जाएगा", + "The_user_will_be_removed_from_s": "उपयोगकर्ता को %s से हटा दिया जाएगा", + "The_user_wont_be_able_to_type_in_s": "उपयोगकर्ता %s टाइप नहीं कर पाएगा", + "The_workspace_has_exceeded_the_monthly_limit_of_active_contacts": "कार्यक्षेत्र सक्रिय संपर्कों की मासिक सीमा को पार कर गया है.", + "Theme": "विषय", + "Themes": "विषय-वस्तु", + "Choose_theme_description": "वह इंटरफ़ेस स्वरूप चुनें जो आपकी आवश्यकताओं के लिए सबसे उपयुक्त हो।", + "theme-color-attention-color": "ध्यान दें रंग", + "theme-color-component-color": "घटक रंग", + "theme-color-content-background-color": "सामग्री पृष्ठभूमि रंग", + "theme-color-custom-scrollbar-color": "कस्टम स्क्रॉलबार रंग", + "theme-color-error-color": "त्रुटि रंग", + "theme-color-info-font-color": "जानकारी फ़ॉन्ट रंग", + "theme-color-link-font-color": "लिंक फ़ॉन्ट रंग", + "theme-color-pending-color": "लंबित रंग", + "theme-color-primary-action-color": "प्राथमिक क्रिया रंग", + "theme-color-primary-background-color": "प्राथमिक पृष्ठभूमि रंग", + "theme-color-primary-font-color": "प्राथमिक फ़ॉन्ट रंग", + "theme-color-rc-color-alert": "चेतावनी", + "theme-color-rc-color-alert-light": "चेतावनी प्रकाश", + "theme-color-rc-color-alert-message-primary": "चेतावनी संदेश प्राथमिक", + "theme-color-rc-color-alert-message-primary-background": "चेतावनी संदेश प्राथमिक पृष्ठभूमि", + "theme-color-rc-color-alert-message-secondary": "चेतावनी संदेश माध्यमिक", + "theme-color-rc-color-alert-message-secondary-background": "चेतावनी संदेश द्वितीयक पृष्ठभूमि", + "theme-color-rc-color-alert-message-warning": "चेतावनी संदेश चेतावनी", + "theme-color-rc-color-alert-message-warning-background": "चेतावनी संदेश चेतावनी पृष्ठभूमि", + "theme-color-rc-color-announcement-text": "घोषणा पाठ का रंग", + "theme-color-rc-color-announcement-background": "घोषणा पृष्ठभूमि रंग", + "theme-color-rc-color-announcement-text-hover": "घोषणा पाठ रंग होवर", + "theme-color-rc-color-announcement-background-hover": "घोषणा पृष्ठभूमि रंग होवर", + "theme-color-rc-color-button-primary": "बटन प्राथमिक", + "theme-color-rc-color-button-primary-light": "बटन प्राइमरी लाइट", + "theme-color-rc-color-content": "सामग्री", + "theme-color-rc-color-error": "गलती", + "theme-color-rc-color-error-light": "त्रुटि प्रकाश", + "theme-color-rc-color-link-active": "लिंक सक्रिय", + "theme-color-rc-color-primary": "प्राथमिक", + "theme-color-rc-color-primary-background": "प्राथमिक पृष्ठभूमि", + "theme-color-rc-color-primary-dark": "प्राथमिक अंधेरा", + "theme-color-rc-color-primary-darkest": "प्राथमिक अंधकारमय", + "theme-color-rc-color-primary-light": "प्राथमिक प्रकाश", + "theme-color-rc-color-primary-light-medium": "प्राथमिक प्रकाश माध्यम", + "theme-color-rc-color-primary-lightest": "प्राथमिक सबसे हल्का", + "theme-color-rc-color-success": "सफलता", + "theme-color-rc-color-success-light": "सफलता प्रकाश", + "theme-color-secondary-action-color": "द्वितीयक क्रिया रंग", + "theme-color-secondary-background-color": "द्वितीयक पृष्ठभूमि रंग", + "theme-color-secondary-font-color": "द्वितीयक फ़ॉन्ट रंग", + "theme-color-selection-color": "चयन रंग", + "theme-color-status-away": "दूर स्थिति रंग", + "theme-color-status-busy": "व्यस्त स्थिति रंग", + "theme-color-status-offline": "ऑफ़लाइन स्थिति रंग", + "theme-color-status-online": "ऑनलाइन स्थिति का रंग", + "theme-color-success-color": "सफलता का रंग", + "theme-color-transparent-dark": "पारदर्शी अंधेरा", + "theme-color-transparent-darker": "पारदर्शी गहरा", + "theme-color-transparent-lightest": "पारदर्शी सबसे हल्का", + "theme-color-unread-notification-color": "अपठित सूचनाएं रंग", + "theme-custom-css": "कस्टम सीएसएस", + "theme-font-body-font-family": "बॉडी फ़ॉन्ट परिवार", + "There_are_no_agents_added_to_this_department_yet": "इस विभाग में अभी तक कोई एजेंट नहीं जोड़ा गया है.", + "There_are_no_applications": "अभी तक कोई OAuth एप्लिकेशन नहीं जोड़ा गया है.", + "There_are_no_applications_installed": "वर्तमान में कोई Rocket.Chat एप्लिकेशन इंस्टॉल नहीं हैं।", + "There_are_no_available_monitors": "कोई मॉनिटर उपलब्ध नहीं हैं", + "There_are_no_departments_added_to_this_tag_yet": "इस टैग में अभी तक कोई विभाग नहीं जोड़ा गया है", + "There_are_no_departments_added_to_this_unit_yet": "इस इकाई में अभी तक कोई विभाग नहीं जोड़ा गया है", + "There_are_no_departments_available": "कोई विभाग उपलब्ध नहीं है", + "There_are_no_integrations": "कोई एकीकरण नहीं हैं", + "There_are_no_monitors_added_to_this_unit_yet": "इस इकाई में अभी तक कोई मॉनिटर नहीं जोड़ा गया है", + "There_are_no_personal_access_tokens_created_yet": "अभी तक कोई व्यक्तिगत एक्सेस टोकन नहीं बनाया गया है।", + "There_are_no_rooms_for_the_given_search_criteria": "दिए गए खोज मानदंड के लिए कोई जगह नहीं है", + "There_are_no_users_in_this_role": "इस भूमिका में कोई उपयोगकर्ता नहीं है.", + "There_is_no_video_conference_history_in_this_room": "इस कमरे में कोई कॉन्फ़्रेंस कॉल इतिहास नहीं है", + "There_is_one_or_more_apps_in_an_invalid_state_Click_here_to_review": "एक या अधिक ऐप्स अमान्य स्थिति में हैं. समीक्षा के लिए यहां क्लिक करें.", + "There_has_been_an_error_installing_the_app": "ऐप इंस्टॉल करने में त्रुटि हुई है", + "These_notes_will_be_available_in_the_call_summary": "ये नोट्स कॉल सारांश में उपलब्ध होंगे", + "This_agent_was_already_selected": "यह एजेंट पहले ही चयनित हो चुका था", + "this_app_is_included_with_subscription": "यह ऐप {{bundleName}} योजनाओं के साथ शामिल है", + "This_cant_be_undone": "इसे पूर्ववत नहीं किया जा सकता.", + "This_conversation_is_already_closed": "यह बातचीत पहले ही बंद हो चुकी है.", + "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "यह ईमेल पहले ही उपयोग किया जा चुका है और सत्यापित नहीं किया गया है. कृपया अपना पासवर्ड बदलें.", + "This_feature_is_currently_in_alpha": "यह सुविधा फिलहाल अल्फ़ा में है!", + "This_is_a_desktop_notification": "यह एक डेस्कटॉप अधिसूचना है", + "This_is_a_deprecated_feature_alert": "यह एक बहिष्कृत सुविधा है. यह उम्मीद के मुताबिक काम नहीं कर पाएगा और नए अपडेट नहीं मिलेंगे।", + "Zapier_integration_has_been_deprecated": "जैपियर एकीकरण को अप्रचलित कर दिया गया है, हो सकता है कि यह अपेक्षा के अनुरूप काम न करे और अपडेट प्राप्त न हो", + "Install_Zapier_from_marketplace": "व्यवधानों से बचने के लिए मार्केटप्लेस से जैपियर ऐप इंस्टॉल करें", + "This_is_a_push_test_messsage": "यह एक पुश परीक्षण संदेश है", + "This_message_was_rejected_by__peer__peer": "इस संदेश को {{peer}} सहकर्मी द्वारा अस्वीकार कर दिया गया था।", + "This_monitor_was_already_selected": "यह मॉनीटर पहले ही चयनित था", + "This_month": "इस महीने", + "This_room_has_been_archived_by__username_": "यह कमरा {{username}} द्वारा संग्रहीत किया गया है", + "This_room_has_been_unarchived_by__username_": "इस कमरे को {{username}} द्वारा असंग्रहीत कर दिया गया है", + "This_room_has_been_archived": "संग्रहीत कक्ष", + "This_room_has_been_unarchived": "अनारक्षित कमरा", + "This_server_will_be_available_while_your_session_is_active": "यह सर्वर आपके सत्र के सक्रिय रहने के दौरान उपलब्ध रहेगा", + "This_week": "इस सप्ताह", + "thread": "धागा", + "Thread_message": "*{{username}} के* संदेश पर टिप्पणी की गई: _ {{msg}} _", + "Threads": "धागे", + "Threads_Description": "थ्रेड्स किसी विशिष्ट संदेश के इर्द-गिर्द संगठित चर्चा की अनुमति देते हैं।", + "Threads_unavailable_for_federation": "फेडरेटेड रूम के लिए थ्रेड्स उपलब्ध नहीं हैं", + "Thursday": "गुरुवार", + "Time_in_minutes": "समय मिनटों में", + "Time_in_seconds": "समय सेकंड में", + "Timeout": "समय समाप्त", + "Timeouts": "समय समाप्ति", + "Timezone": "समय क्षेत्र", + "Title": "शीर्षक", + "Title_bar_color": "टाइटल बार का रंग", + "Title_bar_color_offline": "टाइटल बार का रंग ऑफ़लाइन", + "Title_offline": "शीर्षक ऑफ़लाइन", + "To": "को", + "To_additional_emails": "अतिरिक्त ईमेल के लिए", + "To_install_RocketChat_Livechat_in_your_website_copy_paste_this_code_above_the_last_body_tag_on_your_site": "अपनी वेबसाइट में Rocket.Chat लाइवचैट स्थापित करने के लिए, इस कोड को अपनी साइट पर अंतिम </body> टैग के ऊपर कॉपी और पेस्ट करें।", + "To_prevent_seeing_this_message_again_allow_popups_from_workspace_URL": "इस संदेश को दोबारा देखने से रोकने के लिए, सुनिश्चित करें कि आपकी ब्राउज़र सेटिंग्स कार्यस्थान URL से पॉप-अप खोलने की अनुमति देती हैं:", + "to_see_more_details_on_how_to_integrate": "एकीकृत करने के तरीके के बारे में अधिक विवरण देखने के लिए।", + "To_users": "उपयोगकर्ताओं के लिए", + "Today": "आज", + "Toggle_original_translated": "मूल/अनुवादित टॉगल करें", + "toggle-room-e2e-encryption": "कक्ष E2E एन्क्रिप्शन टॉगल करें", + "toggle-room-e2e-encryption_description": "e2e एन्क्रिप्शन कक्ष को टॉगल करने की अनुमति", + "Token": "टोकन", + "Token_Access": "टोकन एक्सेस", + "Token_Controlled_Access": "टोकन नियंत्रित पहुंच", + "Token_has_been_removed": "टोकन हटा दिया गया है", + "Token_required": "टोकन आवश्यक है", + "Tokens_Minimum_Needed_Balance": "न्यूनतम आवश्यक टोकन बैलेंस", + "Tokens_Minimum_Needed_Balance_Description": "प्रत्येक टोकन पर न्यूनतम आवश्यक शेष राशि निर्धारित करें। सीमा नहीं के लिए रिक्त या \"0\"।", + "Tokens_Minimum_Needed_Balance_Placeholder": "संतुलन मूल्य", + "Tokens_Required": "टोकन आवश्यक है", + "Tokens_Required_Input_Description": "अल्पविराम से अलग किए गए एक या अधिक टोकन परिसंपत्ति नाम टाइप करें।", + "Tokens_Required_Input_Error": "अमान्य टाइप किए गए टोकन.", + "Tokens_Required_Input_Placeholder": "टोकन संपत्ति के नाम", + "Topic": "विषय", + "Top_5_agents_with_the_most_conversations": "सर्वाधिक बातचीत वाले शीर्ष 5 एजेंट", + "Total": "कुल", + "Total_abandoned_chats": "कुल छोड़ी गई चैट", + "Total_conversations": "कुल बातचीत", + "Total_Discussions": "चर्चाएँ", + "Total_messages": "कुल संदेश", + "Total_rooms": "कुल कमरे", + "Total_Threads": "धागे", + "Total_visitors": "कुल आगंतुक", + "TOTP Invalid [totp-invalid]": "कोड या पासवर्ड अमान्य", + "TOTP_reset_email": "दो कारक TOTP रीसेट अधिसूचना", + "TOTP_Reset_Other_Key_Warning": "वर्तमान टू फैक्टर TOTP को रीसेट करने से उपयोगकर्ता लॉग आउट हो जाएगा। यूजर बाद में टू फैक्टर को दोबारा सेट कर सकेगा।", + "totp-disabled": "आपके पास अपने उपयोगकर्ता के लिए 2FA लॉगिन सक्षम नहीं है", + "totp-invalid": "कोड या पासवर्ड अमान्य", + "totp-required": "टीओटीपी आवश्यक", + "Transcript": "प्रतिलिपि", + "Transcript_Enabled": "विज़िटर से पूछें कि क्या वे चैट बंद होने के बाद एक प्रतिलेख चाहेंगे", + "Transcript_message": "प्रतिलेख के बारे में पूछने पर दिखाने योग्य संदेश", + "Transcript_of_your_livechat_conversation": "आपकी सर्वचैनल बातचीत का प्रतिलेख।", + "Transcript_Request": "प्रतिलेख अनुरोध", + "onboarding.form.registeredServerForm.continueStandalone": "स्टैंडअलोन के रूप में जारी रखें", + "transfer-livechat-guest": "लाइवचैट मेहमानों को स्थानांतरित करें", + "transfer-livechat-guest_description": "लाइवचैट मेहमानों को स्थानांतरित करने की अनुमति", + "Transferred": "तबादला", + "Translate": "अनुवाद", + "Translated": "अनुवाद", + "Translate_to": "अनुवाद करने के लिए", + "Translations": "अनुवाद", + "Travel_and_Places": "यात्रा एवं स्थान", + "Trigger_removed": "ट्रिगर हटा दिया गया", + "Trigger_Words": "ट्रिगर शब्द", + "Trigger": "चालू कर देना", + "Triggers": "चलाता है", + "Troubleshoot": "समस्याओं का निवारण", + "Troubleshoot_Description": "कॉन्फ़िगर करें कि आपके कार्यक्षेत्र पर समस्या निवारण कैसे प्रबंधित किया जाता है।", + "Troubleshoot_Disable_Data_Exporter_Processor": "डेटा निर्यातक प्रोसेसर को अक्षम करें", + "Troubleshoot_Disable_Data_Exporter_Processor_Alert": "यह सेटिंग उपयोगकर्ताओं से सभी निर्यात अनुरोधों की प्रोसेसिंग रोक देती है, इसलिए उन्हें अपना डेटा डाउनलोड करने के लिए लिंक प्राप्त नहीं होगा!", + "Troubleshoot_Disable_Instance_Broadcast": "इंस्टेंस प्रसारण अक्षम करें", + "Troubleshoot_Disable_Instance_Broadcast_Alert": "यह सेटिंग Rocket.Chat इंस्टेंस को अन्य इंस्टेंस पर इवेंट भेजने से रोकती है, इससे सिंकिंग समस्याएं और दुर्व्यवहार हो सकता है!", + "Troubleshoot_Disable_Livechat_Activity_Monitor": "लाइवचैट गतिविधि मॉनिटर अक्षम करें", + "Troubleshoot_Disable_Livechat_Activity_Monitor_Alert": "यह सेटिंग लाइवचैट विज़िटर सत्रों की प्रोसेसिंग को रोक देती है जिससे आँकड़े सही ढंग से काम करना बंद कर देते हैं!", + "Troubleshoot_Disable_Notifications": "नोटीफिकेशन निष्क्रिय किया गया", + "Troubleshoot_Disable_Notifications_Alert": "यह सेटिंग अधिसूचना प्रणाली को पूरी तरह से अक्षम कर देती है; ध्वनियाँ, डेस्कटॉप सूचनाएं, मोबाइल सूचनाएं और ईमेल बंद हो जाएंगे!", + "Troubleshoot_Disable_Presence_Broadcast": "उपस्थिति प्रसारण अक्षम करें", + "Troubleshoot_Disable_Presence_Broadcast_Alert": "यह सेटिंग पहले लोड से सभी उपयोगकर्ताओं को उनकी उपस्थिति स्थिति के साथ रखते हुए, उनके क्लाइंट को उपयोगकर्ताओं की स्थिति में बदलाव भेजने वाले सभी उदाहरणों को रोकती है!", + "Troubleshoot_Disable_Sessions_Monitor": "सत्र मॉनिटर अक्षम करें", + "Troubleshoot_Disable_Sessions_Monitor_Alert": "यह सेटिंग उपयोगकर्ता सत्रों के प्रसंस्करण को रोक देती है जिससे आँकड़े सही ढंग से काम करना बंद कर देते हैं!", + "Troubleshoot_Disable_Teams_Mention": "अक्षम टीमों का उल्लेख", + "Troubleshoot_Disable_Teams_Mention_Alert": "यह सेटिंग टीम उल्लेख सुविधा को अक्षम कर देती है. उपयोगकर्ता किसी संदेश में नाम से किसी टीम का उल्लेख नहीं कर पाएंगे और उसके सदस्यों को सूचित नहीं कर पाएंगे।", + "Troubleshoot_Force_Caching_Version": "संस्करण परिवर्तन के आधार पर ब्राउज़रों को नेटवर्किंग कैश साफ़ करने के लिए बाध्य करें", + "Troubleshoot_Force_Caching_Version_Alert": "यदि प्रदान किया गया मान खाली नहीं है और पिछले वाले से भिन्न है तो ब्राउज़र कैश साफ़ करने का प्रयास करेंगे। यह सेटिंग लंबे समय तक सेट नहीं की जानी चाहिए क्योंकि यह ब्राउज़र के प्रदर्शन को प्रभावित करती है, कृपया इसे जल्द से जल्द साफ़ करें।", + "True": "सत्य", + "Try_now": "अब कोशिश करो", + "Try_searching_in_the_marketplace_instead": "इसके बजाय मार्केटप्लेस में खोजने का प्रयास करें", + "Tuesday": "मंगलवार", + "Turn_OFF": "बंद करें", + "Turn_ON": "चालू करो", + "Turn_on_video": "वीडियो चालू करें", + "Turn_on_answer_chats": "उत्तर चैट चालू करें", + "Turn_on_answer_calls": "कॉल का उत्तर देना चालू करें", + "Turn_on_microphone": "माइक्रोफ़ोन चालू करें", + "Turn_off_microphone": "माइक्रोफ़ोन बंद करें", + "Turn_off_answer_chats": "उत्तर चैट बंद करें", + "Turn_off_answer_calls": "उत्तर कॉल बंद करें", + "Turn_off_video": "वीडियो बंद करें", + "Two Factor Authentication": "दो तरीकों से प्रमाणीकरण", + "Two-factor_authentication": "टीओटीपी के माध्यम से दो-कारक प्रमाणीकरण", + "Two-factor_authentication_disabled": "दो-कारक प्रमाणीकरण अक्षम किया गया", + "Two-factor_authentication_email": "ईमेल के माध्यम से दो-कारक प्रमाणीकरण", + "Two-factor_authentication_email_is_currently_disabled": "ईमेल के माध्यम से दो-कारक प्रमाणीकरण वर्तमान में अक्षम है", + "Two-factor_authentication_enabled": "दो-कारक प्रमाणीकरण सक्षम किया गया", + "Two-factor_authentication_is_currently_disabled": "टीओटीपी के माध्यम से दो-कारक प्रमाणीकरण वर्तमान में अक्षम है", + "Two-factor_authentication_native_mobile_app_warning": "चेतावनी: एक बार जब आप इसे सक्षम कर लेते हैं, तो आप अपने पासवर्ड का उपयोग करके मूल मोबाइल ऐप्स (रॉकेट.चैट+) पर तब तक लॉगिन नहीं कर पाएंगे जब तक वे 2FA लागू नहीं कर देते।", + "Type": "प्रकार", + "typing": "टाइपिंग", + "Types": "प्रकार", + "Types_and_Distribution": "प्रकार और वितरण", "Type_your_email": "अपना ईमेल टाइप करें", + "Type_your_job_title": "अपनी नौकरी का शीर्षक टाइप करें", "Type_your_message": "अपना संदेश टाइप करें", "Type_your_name": "अपना नाम लिखें", + "Type_your_password": "अपना पासवर्ड टाइप करें", + "Type_your_username": "अपना उपयोगकर्ता नाम टाइप करें", + "UI_Allow_room_names_with_special_chars": "कमरे के नाम में विशेष वर्णों की अनुमति दें", + "UI_Click_Direct_Message": "सीधा संदेश बनाने के लिए क्लिक करें", + "UI_Click_Direct_Message_Description": "प्रोफ़ाइल टैब खोलना छोड़ें, इसके बजाय सीधे बातचीत पर जाएँ", + "UI_DisplayRoles": "भूमिकाएँ प्रदर्शित करें", + "UI_Group_Channels_By_Type": "चैनलों को प्रकार के अनुसार समूहित करें", + "UI_Merge_Channels_Groups": "निजी समूहों को चैनलों के साथ मिलाएं", + "UI_Show_top_navbar_embedded_layout": "एम्बेडेड लेआउट में शीर्ष नेवबार दिखाएं", + "UI_Unread_Counter_Style": "अपठित काउंटर शैली", + "UI_Use_Name_Avatar": "डिफ़ॉल्ट अवतार उत्पन्न करने के लिए पूरे नाम के पहले अक्षर का उपयोग करें", + "UI_Use_Real_Name": "वास्तविक नाम का प्रयोग करें", + "unable-to-get-file": "फ़ाइल प्राप्त करने में असमर्थ", + "Unable_to_load_active_connections": "सक्रिय कनेक्शन लोड करने में असमर्थ", + "Unarchive": "संग्रह से निकालें", + "unarchive-room": "कक्ष को असंग्रहीत करें", + "unarchive-room_description": "चैनलों को असंग्रहीत करने की अनुमति", + "Unassigned": "सौंपे नहीं गए", + "unauthorized": "अधिकृत नहीं हैं", + "Unavailable": "अनुपलब्ध", + "Unblock": "अनब्लॉक", + "Unblock_User": "उपयोगकर्ता को अनब्लॉक करें", + "Uncheck_All": "सब को अचयनित करें", + "Uncollapse": "खोलना", + "Undefined": "अपरिभाषित", + "Unfavorite": "नापसंद करें", + "Unfollow_message": "संदेश को अनफ़ॉलो करें", + "Unignore": "अनदेखा न करें", + "Uninstall": "स्थापना रद्द करें", + "Units": "इकाइयों", + "Unit_removed": "इकाई हटा दी गई", + "Unique_ID_change_detected_description": "इस कार्यक्षेत्र की पहचान करने वाली जानकारी बदल गई है. ऐसा तब हो सकता है जब साइट यूआरएल या डेटाबेस कनेक्शन स्ट्रिंग बदल दी जाती है या जब मौजूदा डेटाबेस की एक प्रति से एक नया कार्यक्षेत्र बनाया जाता है।

    क्या आप मौजूदा कार्यक्षेत्र में कॉन्फ़िगरेशन अपडेट के साथ आगे बढ़ना चाहेंगे या एक नया कार्यक्षेत्र और अद्वितीय आईडी बनाना चाहेंगे?", + "Unique_ID_change_detected_learn_more_link": "और अधिक जानें", + "Unique_ID_change_detected": "अद्वितीय आईडी परिवर्तन का पता चला", + "Unknown_Import_State": "अज्ञात आयात राज्य", + "Unknown_User": "अज्ञात उपयोगकर्ता", + "Unlimited": "असीमित", + "Unmute": "अनम्यूट", + "Unmute_someone_in_room": "कमरे में किसी को अनम्यूट करें", + "Unmute_user": "उपयोगकर्ता को अनम्यूट करें", + "Unnamed": "अज्ञात", + "Unpin": "अनपिन", + "Unpin_Message": "संदेश अनपिन करें", + "unpinning-not-allowed": "अनपिन करने की अनुमति नहीं है", + "Unprioritized": "प्राथमिकता रहित", + "Unread": "अपठित ग", + "Unread_Count": "अपठित count", + "Unread_Count_DM": "सीधे संदेशों के लिए अपठित गणना", + "Unread_Count_Omni": "ओमनीचैनल चैट के लिए अपठित गणना", + "Unread_Messages": "अपठित संदेश", + "Unread_on_top": "शीर्ष पर अपठित", + "Unread_Rooms": "अपठित कमरे", + "Unread_Rooms_Mode": "अपठित कमरे मोड", + "Unread_Requested_First": "पहले अपठित का अनुरोध किया गया", + "Unread_Requested_Last": "अंतिम बार अपठित का अनुरोध किया गया", + "Unread_Tray_Icon_Alert": "अपठित ट्रे चिह्न चेतावनी", + "Unstar_Message": "तारा हटाएँ", + "Unmute_microphone": "माइक्रोफ़ोन अनम्यूट करें", + "Update": "अद्यतन", + "Update_EnableChecker": "अपडेट चेकर सक्षम करें", + "Update_EnableChecker_Description": "Rocket.Chat डेवलपर्स से नए अपडेट/महत्वपूर्ण संदेशों के लिए स्वचालित रूप से जाँच करता है और उपलब्ध होने पर सूचनाएं प्राप्त करता है। अधिसूचना प्रति नए संस्करण में एक बार क्लिक करने योग्य बैनर के रूप में और रॉकेट.कैट बॉट से एक संदेश के रूप में दिखाई देती है, दोनों ही केवल प्रशासकों के लिए दृश्यमान होते हैं।", + "Update_every": "प्रत्येक को अद्यतन करें", + "Update_LatestAvailableVersion": "नवीनतम उपलब्ध संस्करण अपडेट करें", + "Update_to_version": "{{version}} पर अपडेट करें", + "Update_your_RocketChat": "अपने रॉकेट.चैट को अपडेट करें", + "Updated_at": "पर अद्यतन किया गया", + "Upgrade_tab_upgrade_your_plan": "अपनी योजना को अपग्रेड करें", + "Upload": "डालना", + "Uploads": "अपलोड", + "Upload_private_app": "निजी ऐप अपलोड करें", + "Upload_file_description": "फाइल विवरण", + "Upload_file_name": "फ़ाइल का नाम", "Upload_file_question": "दस्तावेज अपलोड करें?", + "Upload_Folder_Path": "फ़ोल्डर पथ अपलोड करें", + "Upload_From": "{{name}} से अपलोड करें", + "Upload_user_avatar": "अवतार अपलोड करें", + "Uploading_file": "फ़ाइल अपलोड हो रही है...", + "Uptime": "अपटाइम", + "URL": "यूआरएल", + "URLs": "यूआरएल", + "Usage": "प्रयोग", + "Use": "उपयोग", + "Use_account_preference": "खाता प्राथमिकता का उपयोग करें", + "Use_Emojis": "इमोजी का प्रयोग करें", + "Use_Global_Settings": "वैश्विक सेटिंग्स का प्रयोग करें", + "Use_initials_avatar": "अपने उपयोक्तानाम के आरंभिक अक्षरों का प्रयोग करें", + "Use_minor_colors": "छोटे रंग पैलेट का उपयोग करें (डिफ़ॉल्ट रूप से प्रमुख रंग प्राप्त होते हैं)", + "Use_Room_configuration": "सर्वर कॉन्फ़िगरेशन को अधिलेखित करता है और रूम कॉन्फ़िगरेशन का उपयोग करता है", + "Use_Server_configuration": "सर्वर कॉन्फ़िगरेशन का उपयोग करें", + "Use_service_avatar": "%s अवतार का प्रयोग करें", + "Use_this_response": "इस प्रतिक्रिया का प्रयोग करें", + "Use_response": "प्रतिक्रिया का प्रयोग करें", + "Use_this_username": "इस उपयोक्तानाम का प्रयोग करें", + "Use_uploaded_avatar": "अपलोड किए गए अवतार का उपयोग करें", + "Use_url_for_avatar": "अवतार के लिए यूआरएल का प्रयोग करें", + "Use_User_Preferences_or_Global_Settings": "उपयोगकर्ता प्राथमिकताएँ या वैश्विक सेटिंग्स का उपयोग करें", + "User": "उपयोगकर्ता", + "User_menu": "उपयोगकर्ता विकल्प सूची", + "User Search": "उपयोगकर्ता खोज", + "User Search (Group Validation)": "उपयोगकर्ता खोज (समूह सत्यापन)", + "User__username__is_now_a_leader_of__room_name_": "उपयोगकर्ता {{username}} अब {{room_name}} का लीडर है", + "User__username__is_now_a_moderator_of__room_name_": "उपयोगकर्ता {{username}} अब {{room_name}} का मॉडरेटर है", + "User__username__is_now_an_owner_of__room_name_": "उपयोगकर्ता {{username}} अब {{room_name}} का स्वामी है", + "User__username__muted_in_room__roomName__": "उपयोगकर्ता {{username}} को कक्ष {{roomName}} में म्यूट कर दिया गया है", + "User__username__removed_from__room_name__leaders": "उपयोगकर्ता {{username}} को {{room_name}} लीडरों से हटा दिया गया", + "User__username__removed_from__room_name__moderators": "उपयोगकर्ता {{username}} को {{room_name}} मॉडरेटर से हटा दिया गया", + "User__username__removed_from__room_name__owners": "उपयोगकर्ता {{username}} को {{room_name}} स्वामियों से हटा दिया गया", + "User__username__unmuted_in_room__roomName__": "उपयोगकर्ता {{username}} को कमरे में अनम्यूट किया गया है {{roomName}}", + "User_added": "उपयोगकर्ता जोड़ा गया", + "User_added_by": "उपयोगकर्ता {{user_added}} को {{user_by}} द्वारा जोड़ा गया।", + "User_added_to": "जोड़ा गया {{user_added}}", + "User_added_successfully": "उपयोगकर्ता सफलतापूर्वक जोड़ा गया", + "User_and_group_mentions_only": "केवल उपयोगकर्ता और समूह का उल्लेख है", + "User_cant_be_empty": "उपयोगकर्ता खाली नहीं हो सकता", + "User_created_successfully!": "उपयोगकर्ता सफलतापूर्वक बना!", + "User_default": "उपयोगकर्ता डिफ़ॉल्ट", + "User_doesnt_exist": "`@%s` नाम से कोई उपयोगकर्ता मौजूद नहीं है।", + "User_e2e_key_was_reset": "उपयोगकर्ता E2E कुंजी सफलतापूर्वक रीसेट कर दी गई थी।", + "User_has_been_activated": "उपयोगकर्ता सक्रिय कर दिया गया है", + "User_has_been_deactivated": "उपयोगकर्ता को निष्क्रिय कर दिया गया है", + "User_has_been_deleted": "उपयोगकर्ता हटा दिया गया है", + "User_has_been_ignored": "उपयोगकर्ता को नजरअंदाज कर दिया गया है", + "User_has_been_muted_in_s": "उपयोगकर्ता को %s में म्यूट कर दिया गया है", + "User_has_been_removed_from_s": "उपयोगकर्ता को %s से हटा दिया गया है", + "User_has_been_removed_from_team": "उपयोगकर्ता को टीम से हटा दिया गया है", + "User_has_been_unignored": "उपयोगकर्ता को अब अनदेखा नहीं किया जाएगा", + "User_Info": "उपयोगकर्ता जानकारी", + "User_Interface": "प्रयोक्ता इंटरफ़ेस", + "User_is_blocked": "उपयोगकर्ता अवरुद्ध है", + "User_is_no_longer_an_admin": "उपयोगकर्ता अब व्यवस्थापक नहीं है", + "User_is_now_an_admin": "उपयोगकर्ता अब एक व्यवस्थापक है", + "User_is_unblocked": "उपयोगकर्ता को अनब्लॉक कर दिया गया है", + "User_joined_channel": "चैनल से जुड़ गया है.", + "User_joined_conversation": "बातचीत में शामिल हो गए हैं", + "User_joined_team": "इस टीम में शामिल हुए", + "User_joined_the_channel": "चैनल से जुड़े", + "User_joined_the_conversation": "बातचीत में शामिल हुए", + "User_joined_the_team": "इस टीम में शामिल हुए", + "user_joined_otr": "ओटीआर चैट में शामिल हो गया है।", + "user_key_refreshed_successfully": "कुंजी सफलतापूर्वक ताज़ा हो गई", + "user_requested_otr_key_refresh": "कुंजी ताज़ा करने का अनुरोध किया है.", "User_left": "उपयोगकर्ता छोड़ दिया", + "User_left_team": "इस टीम को छोड़ दिया", + "User_left_this_channel": "चैनल छोड़ दिया", + "User_left_this_team": "इस टीम को छोड़ दिया", + "User_logged_out": "उपयोगकर्ता लॉग आउट हो गया है", + "User_management": "प्रयोक्ता प्रबंधन", + "User_mentions_only": "उपयोगकर्ता केवल उल्लेख करता है", + "User_muted": "उपयोगकर्ता म्यूट किया गया", + "User_muted_by": "उपयोगकर्ता {{user_muted}} को {{user_by}} द्वारा म्यूट कर दिया गया है।", + "User_has_been_muted": "म्यूट किया गया {{user_muted}}", + "User_not_found": "उपयोगकर्ता नहीं मिला", + "User_not_found_or_incorrect_password": "उपयोगकर्ता नहीं मिला या पासवर्ड ग़लत है", + "User_or_channel_name": "उपयोगकर्ता या चैनल का नाम", + "User_Presence": "उपयोगकर्ता की उपस्थिति", + "User_removed": "उपयोगकर्ता हटा दिया गया", + "User_removed_by": "उपयोगकर्ता {{user_removed}} को {{user_by}} द्वारा हटा दिया गया।", + "User_has_been_removed": "हटा दिया गया {{user_removed}}", + "User_sent_a_message_on_channel": "{{username}} ने {{channel}} पर एक संदेश भेजा", + "User_sent_a_message_to_you": "{{username}} ने आपको एक संदेश भेजा है", + "user_sent_an_attachment": "{{user}} ने एक अनुलग्नक भेजा", + "User_Settings": "उपयोगकर्ता सेटिंग", + "User_started_a_new_conversation": "{{username}} ने एक नई बातचीत शुरू की", + "User_unmuted_by": "उपयोगकर्ता {{user_unmuted}} को {{user_by}} द्वारा अनम्यूट किया गया।", + "User_has_been_unmuted": "अनम्यूट किया गया {{user_unmuted}}", + "User_unmuted_in_room": "उपयोगकर्ता को कमरे में अनम्यूट कर दिया गया", + "User_updated_successfully": "उपयोगकर्ता सफलतापूर्वक अपडेट किया गया", + "User_uploaded_a_file_on_channel": "{{username}} ने {{channel}} पर एक फ़ाइल अपलोड की", + "User_uploaded_a_file_to_you": "{{username}} ने आपको एक फ़ाइल भेजी है", + "User_uploaded_file": "एक फ़ाइल अपलोड की गई", + "User_uploaded_image": "एक छवि अपलोड की गई", + "user-generate-access-token": "उपयोगकर्ता एक्सेस टोकन जनरेट करें", + "user-generate-access-token_description": "उपयोगकर्ताओं को एक्सेस टोकन जनरेट करने की अनुमति", + "UserData_EnableDownload": "उपयोगकर्ता डेटा डाउनलोड सक्षम करें", + "UserData_FileSystemPath": "सिस्टम पथ (निर्यात फ़ाइलें)", + "view-livechat-facebook": "ओमनीचैनल फेसबुक देखें", + "UserData_FileSystemZipPath": "सिस्टम पथ (संपीड़ित फ़ाइल)", + "view-livechat-facebook_description": "ओमनीचैनल फेसबुक देखने की अनुमति", + "UserData_MessageLimitPerRequest": "प्रति अनुरोध संदेश सीमा", + "UserData_ProcessingFrequency": "प्रसंस्करण आवृत्ति (मिनट)", + "UserDataDownload": "उपयोगकर्ता डेटा डाउनलोड", + "UserDataDownload_Description": "कार्यस्थान सदस्यों को कार्यस्थान डेटा डाउनलोड करने की अनुमति देने या अस्वीकृत करने के लिए कॉन्फ़िगरेशन।", + "UserDataDownload_CompletedRequestExisted_Text": "आपकी डेटा फ़ाइल पहले ही जनरेट हो चुकी थी. डाउनलोड लिंक के लिए अपना ईमेल खाता जांचें।", + "UserDataDownload_CompletedRequestExistedWithLink_Text": "आपकी डेटा फ़ाइल पहले ही जनरेट हो चुकी थी. इसे डाउनलोड करने के लिए यहां क्लिक करें।", + "UserDataDownload_EmailBody": "आपकी डेटा फ़ाइल अब डाउनलोड करने के लिए तैयार है। इसे डाउनलोड करने के लिए यहां क्लिक करें।", + "UserDataDownload_EmailSubject": "आपकी डेटा फ़ाइल डाउनलोड करने के लिए तैयार है", + "UserDataDownload_Requested": "अनुरोधित फ़ाइल डाउनलोड करें", + "UserDataDownload_Requested_Text": "आपकी डेटा फ़ाइल तैयार हो जाएगी. तैयार होने पर इसे डाउनलोड करने का एक लिंक आपके ईमेल पते पर भेजा जाएगा। आपके सामने चलने के लिए कतारबद्ध {{pending_operations}} हैं।", + "UserDataDownload_RequestExisted_Text": "आपकी डेटा फ़ाइल पहले से ही जेनरेट की जा रही है. तैयार होने पर इसे डाउनलोड करने का एक लिंक आपके ईमेल पते पर भेजा जाएगा। आपके सामने चलने के लिए कतारबद्ध {{pending_operations}} हैं।", + "Username": "उपयोगकर्ता नाम", + "Username_already_exist": "उपयोगकर्ता का नाम पहले से मौजूद है। कृपया कोई अन्य उपयोक्तानाम आज़माएँ.", + "Username_and_message_must_not_be_empty": "उपयोगकर्ता नाम और संदेश खाली नहीं होना चाहिए.", + "Username_cant_be_empty": "उपयोक्तानाम खाली नहीं हो सकता", + "Username_Change_Disabled": "आपके Rocket.Chat व्यवस्थापक ने उपयोगकर्ता नाम बदलना अक्षम कर दिया है", + "Username_denied_the_OTR_session": "{{username}} ने ओटीआर सत्र अस्वीकृत कर दिया", + "Username_description": "उपयोगकर्ता नाम का उपयोग दूसरों को संदेशों में आपका उल्लेख करने की अनुमति देने के लिए किया जाता है।", + "Username_doesnt_exist": "उपयोक्तानाम `%s` मौजूद नहीं है.", + "Username_ended_the_OTR_session": "{{username}} ने ओटीआर सत्र समाप्त कर दिया", + "Username_invalid": "%s वैध उपयोक्तानाम नहीं है,
    केवल अक्षरों, संख्याओं, बिंदुओं, हाइफ़न और अंडरस्कोर का उपयोग करें", + "Username_is_already_in_here": "`@%s` पहले से ही यहां मौजूद है।", + "Username_Placeholder": "कृपया उपयोक्तानाम दर्ज करें...", + "Username_title": "उपयोक्तानाम पंजीकृत करें", + "Username_has_been_updated": "उपयोगकर्ता नाम अपडेट कर दिया गया है", + "Username_wants_to_start_otr_Do_you_want_to_accept": "{{username}} ओटीआर प्रारंभ करना चाहता है। क्या आप स्वीकार करना चाहते हैं?", + "Username_name_email": "उपयोगकर्ता नाम, नाम या ई-मेल", + "Users": "उपयोगकर्ताओं", + "Users must use Two Factor Authentication": "यूजर्स को टू फैक्टर ऑथेंटिकेशन का इस्तेमाल करना होगा", + "Users_added": "उपयोगकर्ताओं को जोड़ दिया गया है", + "Users_and_rooms": "उपयोगकर्ता और कमरे", + "Users_by_time_of_day": "दिन के समय के अनुसार उपयोगकर्ता", + "Users_in_role": "भूमिका में उपयोगकर्ता", + "Users_key_has_been_reset": "उपयोगकर्ता की कुंजी रीसेट कर दी गई है", + "Users_reacted": "जिन उपयोगकर्ताओं ने प्रतिक्रिया दी", + "Users_TOTP_has_been_reset": "उपयोगकर्ता का TOTP रीसेट कर दिया गया है", + "Uses": "उपयोग", + "Uses_left": "बाएँ उपयोग", + "UTC_Timezone": "यूटीसी समय क्षेत्र", + "Utilities": "उपयोगिताओं", + "UTF8_Names_Slugify": "UTF8 नाम Slugify", + "UTF8_User_Names_Validation": "UTF8 उपयोगकर्ता नाम सत्यापन", + "UTF8_User_Names_Validation_Description": "रेगएक्सपी जिसका उपयोग उपयोगकर्ता नाम सत्यापित करने के लिए किया जाएगा", + "UTF8_Channel_Names_Validation": "UTF8 चैनल नाम सत्यापन", + "UTF8_Channel_Names_Validation_Description": "रेगएक्सपी जिसका उपयोग चैनल नामों को मान्य करने के लिए किया जाएगा", + "Videocall_enabled": "वीडियो कॉल सक्षम", + "Validate_email_address": "ई - मेल पता की पुष्टि करें", + "Validation": "मान्यकरण", + "Value_messages": "{{price}} संदेश", + "Value_users": "{{price}} उपयोगकर्ता", + "Verification": "सत्यापन", + "Verification_Description": "आप निम्नलिखित प्लेसहोल्डर्स का उपयोग कर सकते हैं:\n - सत्यापन URL के लिए `[Verification_Url]`।\n - `[नाम]`, `[fname]`, `[lname]` क्रमशः उपयोगकर्ता के पूर्ण नाम, प्रथम नाम या अंतिम नाम के लिए।\n - `[ईमेल]` उपयोगकर्ता के ईमेल के लिए।\n - एप्लिकेशन नाम और यूआरएल के लिए क्रमशः `[Site_Name]` और `[Site_URL]`।", + "Verification_Email": "अपना ईमेल पता सत्यापित करने के लिए यहां क्लिक करें।", + "Verification_email_body": "कृपया, अपने ईमेल पते की पुष्टि करने के लिए नीचे दिए गए बटन पर क्लिक करें।", + "Verification_email_sent": "सत्यापन विद्युतडाक भेज दिया गया है", + "Verification_Email_Subject": "[साइट_नाम] - ईमेल पता सत्यापन", + "Verified": "सत्यापित", + "Verify": "सत्यापित करें", + "Verify_your_email": "अपना ईमेल सत्यापित करें", + "Version": "संस्करण", + "Version_version": "संस्करण {{version}}", + "App_Request_Admin_Message": "नमस्ते {{admin_name}}, {{user_name}} ने इस कार्यक्षेत्र पर {{app_name}} ऐप इंस्टॉल करने का अनुरोध सबमिट किया है।\n \n यह वह संदेश है जिसमें उन्होंने शामिल किया:\n>{{message}}\n \n अधिक जानने और {{app_name}} ऐप इंस्टॉल करने के लिए, [यहां क्लिक करें]({{learn_more}})।", + "App_version_incompatible_tooltip": "ऐप Rocket.Chat संस्करण के साथ असंगत है", + "App_request_enduser_message": "आपके द्वारा अनुरोधित ऐप, {{appName}}, अभी इस कार्यक्षेत्र पर इंस्टॉल किया गया है।\n [यहां क्लिक करें]({{learnmore}}) ऐप के बारे में जानने के लिए।", + "App_requests_by_workspace": "कार्यक्षेत्र के सदस्यों द्वारा किए गए ऐप अनुरोध यहां दिखाई देते हैं", + "Video_Conference_Description": "अपने कार्यक्षेत्र के लिए कॉन्फ्रेंसिंग कॉल कॉन्फ़िगर करें।", + "Video_Chat_Window": "वीडियो चैट", + "Video_Conference": "कांफ्रेंस कॉल", + "Video_Call_unavailable_for_this_type_of_room": "इस प्रकार के कमरे के लिए वीडियो कॉल उपलब्ध नहीं है", + "Video_Conferences": "सम्मेलन में बुलावा", + "Video_Conference_Info": "बैठक की जानकारी", + "Video_Conference_Url": "मीटिंग यूआरएल", + "video-conf-provider-not-configured": "**कॉन्फ़्रेंस कॉल सक्षम नहीं है**: कार्यस्थान व्यवस्थापक को पहले कॉन्फ़्रेंस कॉल सुविधा सक्षम करने की आवश्यकता है।", + "Video_message": "वीडियो संदेश", + "Videocall_declined": "वीडियो कॉल अस्वीकृत.", + "Video_and_Audio_Call": "वीडियो और ऑडियो कॉल", + "video_conference_started": "_कॉल प्रारंभ किया._", + "video_conference_started_by": "**{{username}}** _कॉल शुरू हुई।_", + "video_conference_ended": "_कॉल समाप्त हो गया है._", + "video_conference_ended_by": "**{{username}}** _कॉल समाप्त हुई।_", + "video_livechat_started": "_वीडियो कॉल शुरू की._", + "video_livechat_missed": "_एक वीडियो कॉल शुरू की जिसका उत्तर नहीं दिया गया।_", + "video_direct_calling": "_कॉल कर रहा है।_", + "video_direct_ended": "_कॉल समाप्त हो गया है._", + "video_direct_ended_by": "**{{username}}** _कॉल समाप्त हुई।_", + "video_direct_missed": "_एक कॉल शुरू हुई जिसका उत्तर नहीं दिया गया।_", + "video_direct_started": "_कॉल प्रारंभ किया._", + "VideoConf_Default_Provider": "डिफ़ॉल्ट प्रदाता", + "VideoConf_Default_Provider_Description": "यदि आपके पास एकाधिक प्रदाता ऐप्स इंस्टॉल हैं, तो चुनें कि नए कॉन्फ़्रेंस कॉल के लिए किसका उपयोग किया जाना चाहिए।", + "VideoConf_Enable_Channels": "सार्वजनिक चैनलों में सक्षम करें", + "VideoConf_Enable_Groups": "निजी चैनलों में सक्षम करें", + "VideoConf_Enable_DMs": "सीधे संदेशों में सक्षम करें", + "VideoConf_Enable_Teams": "टीमों में सक्षम करें", + "VideoConf_Mobile_Ringing": "मोबाइल रिंगिंग सक्षम करें", + "VideoConf_Mobile_Ringing_Description": "सक्षम होने पर, मोबाइल उपयोगकर्ताओं को सीधे कॉल उनके डिवाइस पर फ़ोन कॉल के रूप में सुनाई देगी।", + "VideoConf_Mobile_Ringing_Alert": "यह सुविधा अभी प्रायोगिक चरण में है और हो सकता है कि यह अभी तक मोबाइल ऐप द्वारा पूरी तरह से समर्थित न हो। सक्षम होने पर यह उपयोगकर्ताओं को अतिरिक्त पुश सूचनाएं भेजेगा।", + "videoconf-ring-users": "कॉल करते समय अन्य उपयोगकर्ताओं को रिंग करें", + "videoconf-ring-users_description": "कॉल करते समय अन्य उपयोगकर्ताओं को रिंग करने की अनुमति", + "Video_record": "चलचित्र आलेख", + "Videos": "वीडियो", + "View_mode": "दृश्य मोड", + "View_All": "सभी सदस्यों को देखें", + "View_channels": "चैनल देखें", + "view-agent-canned-responses": "एजेंट की डिब्बाबंद प्रतिक्रियाएँ देखें", + "view-agent-canned-responses_description": "एजेंट की डिब्बाबंद प्रतिक्रियाएँ देखने की अनुमति", + "view-agent-extension-association": "एजेंट एक्सटेंशन एसोसिएशन देखें", + "view-agent-extension-association_description": "एजेंट एक्सटेंशन एसोसिएशन देखने की अनुमति", + "view-all-canned-responses": "सभी डिब्बाबंद प्रतिक्रियाएँ देखें", + "view-all-canned-responses_description": "सभी डिब्बाबंद प्रतिक्रियाओं को देखने की अनुमति", + "view-import-operations": "आयात परिचालन देखें", + "view-import-operations_description": "आयात परिचालन देखने की अनुमति", + "view-omnichannel-contact-center": "ओमनीचैनल संपर्क केंद्र देखें", + "view-omnichannel-contact-center_description": "ओमनीचैनल संपर्क केंद्र को देखने और उसके साथ बातचीत करने की अनुमति", + "View_Logs": "लॉग्स को देखें", + "View_original": "मूल देखें", + "View_the_Logs_for": "इसके लिए लॉग देखें: \"{{name}}\"", + "view-all-teams": "सभी टीमें देखें", + "view-all-teams_description": "सभी टीमों को देखने की अनुमति", + "view-all-team-channels": "सभी टीम चैनल देखें", + "view-all-team-channels_description": "सभी टीम के चैनल देखने की अनुमति", + "view-broadcast-member-list": "प्रसारण कक्ष में सदस्यों की सूची देखें", + "view-broadcast-member-list_description": "प्रसारण चैनल में उपयोगकर्ताओं की सूची देखने की अनुमति", + "view-c-room": "सार्वजनिक चैनल देखें", + "view-c-room_description": "सार्वजनिक चैनल देखने की अनुमति", + "view-canned-responses": "डिब्बाबंद प्रतिक्रियाएँ देखें", + "view-canned-responses_description": "डिब्बाबंद प्रतिक्रियाएँ देखने की अनुमति", + "view-d-room": "सीधे संदेश देखें", + "view-d-room_description": "सीधे संदेश देखने की अनुमति", + "view-device-management": "डिवाइस प्रबंधन देखें", + "view-device-management_description": "डिवाइस प्रबंधन डैशबोर्ड देखने की अनुमति", + "view-engagement-dashboard": "सहभागिता डैशबोर्ड देखें", + "view-engagement-dashboard_description": "सहभागिता डैशबोर्ड देखने की अनुमति", + "view-federation-data": "फ़ेडरेशन डेटा देखें", + "view-federation-data_description": "फ़ेडरेशन डेटा देखने की अनुमति", + "View_full_conversation": "पूरी बातचीत देखें", + "view-full-other-user-info": "अन्य उपयोगकर्ता की पूरी जानकारी देखें", + "view-full-other-user-info_description": "खाता निर्माण तिथि, अंतिम लॉगिन आदि सहित अन्य उपयोगकर्ताओं की पूरी प्रोफ़ाइल देखने की अनुमति।", + "view-history": "इतिहास देखें", + "view-history_description": "चैनल इतिहास देखने की अनुमति", + "view-join-code": "जॉइन कोड देखें", + "view-join-code_description": "चैनल जॉइन कोड देखने की अनुमति", + "view-joined-room": "सम्मिलित कक्ष देखें", + "view-joined-room_description": "वर्तमान में शामिल चैनलों को देखने की अनुमति", + "view-l-room": "ओमनीचैनल कमरे देखें", + "view-l-room_description": "ओमनीचैनल कमरे देखने की अनुमति", + "view-livechat-analytics": "ओमनीचैनल एनालिटिक्स देखें", + "view-livechat-analytics_description": "लाइव चैट विश्लेषण देखने की अनुमति", + "view-livechat-appearance": "ओमनीचैनल उपस्थिति देखें", + "view-livechat-appearance_description": "लाइव चैट उपस्थिति देखने की अनुमति", + "view-livechat-business-hours": "ओमनीचैनल व्यावसायिक घंटे देखें", + "view-livechat-business-hours_description": "लाइव चैट व्यावसायिक घंटे देखने की अनुमति", + "view-livechat-current-chats": "ओमनीचैनल वर्तमान चैट देखें", + "view-livechat-current-chats_description": "लाइव चैट वर्तमान चैट देखने की अनुमति", + "view-livechat-customfields": "ओमनीचैनल कस्टम फ़ील्ड देखें", + "view-livechat-customfields_description": "ओमनीचैनल कस्टम फ़ील्ड देखने की अनुमति", + "view-livechat-departments": "ओमनीचैनल विभाग देखें", + "view-livechat-departments_description": "ओमनीचैनल विभागों को देखने की अनुमति", + "view-livechat-installation": "ओमनीचैनल इंस्टालेशन देखें", + "view-livechat-installation_description": "ओमनीचैनल स्थापना देखने की अनुमति", + "view-livechat-manager": "ओमनीचैनल प्रबंधक देखें", + "view-livechat-manager_description": "अन्य ओमनीचैनल प्रबंधकों को देखने की अनुमति", + "view-livechat-monitor": "लाइवचैट मॉनिटर्स देखें", + "view-livechat-queue": "ओमनीचैनल कतार देखें", + "view-livechat-queue_description": "ओमनीचैनल कतार देखने की अनुमति", + "view-livechat-real-time-monitoring": "ओमनीचैनल रीयल-टाइम मॉनिटरिंग देखें", + "view-livechat-room-closed-by-another-agent": "किसी अन्य एजेंट द्वारा बंद किए गए ओमनीचैनल रूम देखें", + "view-livechat-room-closed-by-another-agent_description": "किसी अन्य एजेंट द्वारा बंद किए गए लाइव चैट रूम देखने की अनुमति", + "view-livechat-room-closed-same-department": "उसी विभाग में किसी अन्य एजेंट द्वारा बंद किए गए ओमनीचैनल रूम देखें", + "view-livechat-room-closed-same-department_description": "उसी विभाग में किसी अन्य एजेंट द्वारा बंद किए गए लाइव चैट रूम देखने की अनुमति", + "view-livechat-room-customfields": "ओमनीचैनल कक्ष कस्टम फ़ील्ड देखें", + "view-livechat-room-customfields_description": "लाइव चैट रूम कस्टम फ़ील्ड देखने की अनुमति", + "view-livechat-rooms": "ओमनीचैनल कमरे देखें", + "view-livechat-rooms_description": "अन्य ओमनीचैनल कमरे देखने की अनुमति", + "view-livechat-triggers": "ओमनीचैनल ट्रिगर देखें", + "view-livechat-triggers_description": "लाइव चैट ट्रिगर देखने की अनुमति", + "view-livechat-webhooks": "ओमनीचैनल वेबहुक देखें", + "view-livechat-webhooks_description": "लाइव चैट वेबहुक देखने की अनुमति", + "view-livechat-unit": "लाइवचैट इकाइयाँ देखें", + "view-logs": "लॉग्स को देखें", + "view-logs_description": "सर्वर लॉग देखने की अनुमति", + "view-other-user-channels": "अन्य उपयोगकर्ता चैनल देखें", + "view-other-user-channels_description": "अन्य उपयोगकर्ताओं के स्वामित्व वाले चैनल देखने की अनुमति", + "view-outside-room": "बाहरी कक्ष का दृश्य", + "view-outside-room_description": "मौजूदा कमरे के बाहर के उपयोगकर्ताओं को देखने की अनुमति", + "view-p-room": "निजी कक्ष देखें", + "view-p-room_description": "निजी चैनल देखने की अनुमति", + "view-privileged-setting": "विशेषाधिकार प्राप्त सेटिंग देखें", + "view-privileged-setting_description": "सेटिंग्स देखने की अनुमति", + "view-moderation-console": "मॉडरेशन कंसोल देखें", + "view-moderation-console_description": "सर्वर का मॉडरेशन कंसोल देखने की अनुमति", + "manage-moderation-actions": "मॉडरेशन क्रियाएँ प्रबंधित करें", + "manage-moderation-actions_description": "मॉडरेशन कार्रवाइयों को प्रबंधित करने, रिपोर्ट किए गए उपयोगकर्ताओं पर कार्रवाई करने की अनुमति", + "view-room-administration": "कक्ष प्रशासन देखें", + "view-room-administration_description": "सार्वजनिक, निजी और प्रत्यक्ष संदेश आँकड़े देखने की अनुमति। इसमें वार्तालाप या संग्रह देखने की क्षमता शामिल नहीं है", + "view-statistics": "सांख्यिकी देखें", + "view-statistics_description": "सिस्टम आँकड़े देखने की अनुमति जैसे लॉग इन किए गए उपयोगकर्ताओं की संख्या, कमरों की संख्या, ऑपरेटिंग सिस्टम की जानकारी", + "view-user-administration": "उपयोगकर्ता प्रशासन देखें", + "view-user-administration_description": "वर्तमान में सिस्टम में लॉग इन अन्य उपयोगकर्ता खातों के आंशिक, केवल पढ़ने योग्य सूची दृश्य की अनुमति। इस अनुमति के साथ कोई भी उपयोगकर्ता खाता जानकारी पहुंच योग्य नहीं है", + "Viewing_room_administration": "देखने का कमरा प्रशासन", + "Visibility": "दृश्यता", + "Visible": "दृश्यमान", + "Visible_To_Workspace": "कार्यस्थल पर दृश्यमान", + "Visit_Site_Url_and_try_the_best_open_source_chat_solution_available_today": "[Site_URL] पर जाएँ और आज ही उपलब्ध सर्वोत्तम ओपन सोर्स चैट समाधान आज़माएँ!", + "Visitor": "आगंतुक", + "Visitor_Email": "आगंतुक ई-मेल", + "Visitor_Info": "आगंतुक जानकारी", + "Visitor_message": "आगंतुक संदेश", + "Visitor_Name": "आगंतुक का नाम", + "Visitor_Name_Placeholder": "कृपया विज़िटर का नाम दर्ज करें...", + "Visitor_not_found": "विज़िटर नहीं मिला", + "Visitor_does_not_exist": "विज़िटर मौजूद नहीं है!", + "Visitor_Navigation": "विज़िटर नेविगेशन", + "Visitor_page_URL": "विज़िटर पृष्ठ URL", + "Visitor_time_on_site": "साइट पर आगंतुक का समय", + "Voice_Call": "आवाज कॉल", + "VoIP_Enable_Keep_Alive_For_Unstable_Networks": "एसआईपी विकल्प सक्रिय रखें सक्षम करें", + "VoIP_Enable_Keep_Alive_For_Unstable_Networks_Description": "समय-समय पर एसआईपी विकल्प संदेश भेजकर कई बाहरी एसआईपी गेटवे की स्थिति की निगरानी करें। अस्थिर नेटवर्क के लिए उपयोग किया जाता है.", + "VoIP_Enabled": "ध्वनि चैनल सक्षम करें", + "VoIP_Enabled_Description": "आउटबाउंड और इनकमिंग कॉल के माध्यम से एजेंटों को ग्राहकों से जोड़ें", + "VoIP_Extension": "वीओआइपी एक्सटेंशन", + "Voip_Server_Configuration": "तारांकन वेबसॉकेट सर्वर", + "VoIP_Server_Websocket_Port": "वेबसॉकेट पोर्ट", + "VoIP_Server_Name": "सर्वर का नाम", + "VoIP_Server_Websocket_Path": "वेबसोकेट यूआरएल", + "VoIP_Retry_Count": "count पुनः प्रयास करें", + "VoIP_Retry_Count_Description": "यह परिभाषित करता है कि कनेक्शन खो जाने पर क्लाइंट कितनी बार वीओआईपी सर्वर से दोबारा कनेक्ट करने का प्रयास करेगा।", + "VoIP_Management_Server": "वीओआईपी प्रबंधन सर्वर", + "VoIP_Management_Server_Host": "सर्वर होस्ट", + "VoIP_Management_Server_Port": "सर्वर पोर्ट", + "VoIP_Management_Server_Name": "सर्वर का नाम", + "VoIP_Management_Server_Username": "उपयोगकर्ता नाम", + "VoIP_Management_Server_Password": "पासवर्ड", + "Voip_call_started": "पर कॉल शुरू हुई", + "Voip_call_duration": "कॉल {{period}} तक चली", + "Voip_call_declined": "एजेंट द्वारा फोन काट दिया गया", + "Voip_call_on_hold": "कॉल को होल्ड पर रखा गया", + "Voip_call_unhold": "पर कॉल फिर से शुरू हुई", + "Voip_call_ended": "कॉल समाप्त हो गई", + "Voip_call_ended_unexpectedly": "कॉल अप्रत्याशित रूप से समाप्त हुई: {{reason}}", + "Voip_call_wrapup": "कॉल रैपअप नोट्स जोड़े गए: {{comment}}", + "VoIP_JWT_Secret": "गुप्त कुंजी (JWT)", + "VoIP_JWT_Secret_description": "सादे पाठ के बजाय JWT के रूप में सर्वर से क्लाइंट तक एक्सटेंशन विवरण साझा करने के लिए एक गुप्त कुंजी सेट करें। यदि कोई गुप्त कुंजी सेट नहीं की गई है तो एक्सटेंशन पंजीकरण विवरण सादे पाठ के रूप में भेजा जाएगा।", + "Voip_is_disabled": "वीओआईपी अक्षम है", + "Voip_is_disabled_description": "एक्सटेंशन की सूची देखने के लिए वीओआईपी को सक्रिय करना आवश्यक है, सेटिंग्स टैब में ऐसा करें।", + "VoIP_Toggle": "वीओआईपी सक्षम/अक्षम करें", + "Chat_opened_by_visitor": "विज़िटर द्वारा चैट खोली गई", + "Wait_activation_warning": "इससे पहले कि आप लॉग इन कर सकें, आपका खाता किसी व्यवस्थापक द्वारा मैन्युअल रूप से सक्रिय होना चाहिए।", + "Waiting_for_answer": "जवाब का इंतज़ार रहा हूँ", + "Waiting_queue": "प्रतीक्षा कतार", + "Waiting_queue_message": "प्रतीक्षा कतार संदेश", + "Waiting_queue_message_description": "संदेश जो आगंतुकों को कतार में लगने पर प्रदर्शित किया जाएगा", + "Waiting_Time": "इंतज़ार का समय", + "Waiting_for_server_connection": "सर्वर कनेक्शन की प्रतीक्षा की जा रही है", + "Warning": "चेतावनी", + "Warnings": "चेतावनियाँ", + "WAU_value": "मैं कद्र करता हूं {{value}}", + "We_appreciate_your_feedback": "हम आपके फ़ीडबैक की सराहना करते हैं", "We_are_offline_Sorry_for_the_inconvenience": "हम ऑफ़लाइन हैं। असुविधा के लिए खेद है।", + "We_Could_not_retrive_any_data": "हम कोई डेटा पुनः प्राप्त नहीं कर सके", + "We_have_sent_password_email": "हमने आपको पासवर्ड रीसेट निर्देशों के साथ एक ईमेल भेजा है। यदि आपको शीघ्र ही कोई ईमेल प्राप्त नहीं होता है, तो कृपया वापस आएं और पुनः प्रयास करें।", + "We_have_sent_registration_email": "हमने आपके पंजीकरण की पुष्टि के लिए आपको एक ईमेल भेजा है। यदि आपको शीघ्र ही कोई ईमेल प्राप्त नहीं होता है, तो कृपया वापस आएं और पुनः प्रयास करें।", + "Webdav Integration": "वेबडाव एकीकरण", + "Webdav Integration_Description": "उपयोगकर्ताओं के लिए सर्वर पर दस्तावेज़ बनाने, बदलने और स्थानांतरित करने के लिए एक रूपरेखा। Nextcloud जैसे WebDAV सर्वर को लिंक करने के लिए उपयोग किया जाता है।", + "WebDAV_Accounts": "वेबडीएवी खाते", + "Webdav_add_new_account": "नया WebDAV खाता जोड़ें", + "Webdav_Integration_Enabled": "वेबडाव एकीकरण सक्षम", + "WebDAV_Integration_Not_Allowed": "WebDAV एकीकरण की अनुमति नहीं है", + "Webdav_Password": "वेबडीएवी पासवर्ड", + "Webdav_Server_URL": "WebDAV सर्वर एक्सेस यूआरएल", + "Webdav_Username": "वेबडीएवी उपयोगकर्ता नाम", + "Webdav_account_removed": "WebDAV खाता हटा दिया गया", + "webdav-account-saved": "WebDAV खाता सहेजा गया", + "webdav-account-updated": "WebDAV खाता अपडेट किया गया", + "webdav-server-not-found": "WebDAV सर्वर नहीं मिला", + "Webhook_Details": "वेबहुक विवरण", + "Webhook_URL": "वेबहुक यूआरएल", + "Webhook_URL_not_set": "वेबहुक यूआरएल सेट नहीं है", + "Webhooks": "वेबहुक", + "WebRTC": "वेबआरटीसी", + "WebRTC_Description": "ऑडियो और/या वीडियो सामग्री प्रसारित करें, साथ ही किसी बिचौलिए की आवश्यकता के बिना ब्राउज़रों के बीच मनमाना डेटा प्रसारित करें।", + "WebRTC_Call": "वेबआरटीसी कॉल", + "WebRTC_Call_unavailable_for_federation": "फ़ेडरेटेड रूम के लिए WebRTC कॉल उपलब्ध नहीं है", + "WebRTC_direct_audio_call_from_%s": "%s से सीधा ऑडियो कॉल", + "WebRTC_direct_video_call_from_%s": "%s से सीधा वीडियो कॉल", + "WebRTC_Enable_Channel": "सार्वजनिक चैनलों के लिए सक्षम करें", + "WebRTC_Enable_Direct": "सीधे संदेशों के लिए सक्षम करें", + "WebRTC_Enable_Private": "निजी चैनलों के लिए सक्षम करें", + "WebRTC_group_audio_call_from_%s": "%s से समूह ऑडियो कॉल", + "WebRTC_group_video_call_from_%s": "%s से समूह वीडियो कॉल", + "WebRTC_monitor_call_from_%s": "%s से कॉल की निगरानी करें", + "WebRTC_Servers": "स्टन/टर्न सर्वर", + "WebRTC_Servers_Description": "अल्पविराम द्वारा अलग किए गए STUN और TURN सर्वरों की एक सूची।\n उपयोगकर्ता नाम, पासवर्ड और पोर्ट को `username:password@stun:host:port` या `username:password@turn:host:port` प्रारूप में अनुमति दी जाती है।", + "WebRTC_call_ended_message": "कॉल {{endTime}} पर समाप्त हुई - {{callDuration}} तक चली", + "WebRTC_call_declined_message": "संपर्क द्वारा कॉल अस्वीकृत.", + "Website": "वेबसाइट", + "Wednesday": "बुधवार", + "Weekly_Active_Users": "साप्ताहिक सक्रिय उपयोगकर्ता", + "Welcome": "स्वागत है %s .", + "Welcome_to": "[साइट_नाम] में आपका स्वागत है", + "Welcome_to_workspace": "{{Site_Name}} में आपका स्वागत है", + "Welcome_to_the": "आपका स्वागत है", + "When": "कब", + "When_a_line_starts_with_one_of_there_words_post_to_the_URLs_below": "जब कोई पंक्ति इनमें से किसी एक शब्द से शुरू होती है, तो नीचे दिए गए यूआरएल पर पोस्ट करें", + "When_is_the_chat_busier?": "चैट कब व्यस्त है?", + "Where_are_the_messages_being_sent?": "संदेश कहां भेजे जा रहे हैं?", + "Why_did_you_chose__score__": "आपने {{score}} क्यों चुना?", + "Why_do_you_want_to_report_question_mark": "आप रिपोर्ट क्यों करना चाहते हैं?", + "Will_Appear_In_From": "आपके द्वारा भेजे गए ईमेल के प्रेषक: शीर्षक में दिखाई देगा।", + "will_be_able_to": "के लिए योग्य होगा", + "Will_be_available_here_after_saving": "सेव करने के बाद यहां उपलब्ध होगा.", + "Without_priority": "बिना प्राथमिकता के", + "Without_SLA": "एसएलए के बिना", + "Workspace_now_using_device_management": "कार्यक्षेत्र अब डिवाइस प्रबंधन का उपयोग कर रहा है", + "Worldwide": "दुनिया भर", + "Would_you_like_to_return_the_inquiry": "क्या आप पूछताछ वापस करना चाहेंगे?", + "Would_you_like_to_return_the_queue": "क्या आप इस कमरे को वापस कतार में ले जाना चाहेंगे? सारी बातचीत का इतिहास कमरे में रखा जाएगा.", + "Would_you_like_to_place_chat_on_hold": "क्या आप इस चैट को ऑन-होल्ड रखना चाहेंगे?", + "Wrap_up_the_call": "कॉल समाप्त करें", + "Wrap_Up_Notes": "समापन नोट्स", + "Workspace": "कार्यस्थान", "Yes": "हाँ", - "You": "आप" + "Yes_archive_it": "हाँ, इसे संग्रहित करें!", + "Yes_clear_all": "हाँ, सब साफ़ करें!", + "Yes_continue": "हाँ, जारी रखें!", + "Yes_deactivate_it": "हाँ, इसे निष्क्रिय करें!", + "Yes_delete_it": "हाँ, इसे हटा दें!", + "Yes_hide_it": "हाँ, छुपाओ!", + "Yes_leave_it": "हाँ, छोड़ो!", + "Yes_mute_user": "हाँ, उपयोगकर्ता को म्यूट करें!", + "Yes_prune_them": "हाँ, उनकी काट-छाँट करें!", + "Yes_remove_user": "हाँ, उपयोगकर्ता को हटा दें!", + "Yes_unarchive_it": "हाँ, इसे असंग्रहीत करें!", + "yesterday": "कल", + "Yesterday": "कल", + "You": "आप", + "You_reacted_with": "आपने {{emoji}} के साथ प्रतिक्रिया व्यक्त की", + "Users_reacted_with": "{{users}} ने {{emoji}} के साथ प्रतिक्रिया व्यक्त की", + "Users_and_more_reacted_with": "{{user}} और {{counter}} और अधिक लोगों ने {{emoji}} के साथ प्रतिक्रिया व्यक्त की", + "You_and_users_Reacted_with": "आपने और {{users}} ने {{emoji}} के साथ प्रतिक्रिया व्यक्त की", + "You_users_and_more_Reacted_with": "आपने, {{user}} और {{counter}} ने {{emoji}} के साथ प्रतिक्रिया व्यक्त की", + "You_are_converting_team_to_channel": "आप इस टीम को एक चैनल में परिवर्तित कर रहे हैं।", + "you_are_in_preview_mode_of": "आप चैनल # {{room_name}} के पूर्वावलोकन मोड में हैं", + "you_are_in_preview": "आप पूर्वावलोकन मोड में हैं", + "you_are_in_preview_please_insert_the_password": "कृपया पासवर्ड डालें", + "you_are_in_preview_mode_of_incoming_livechat": "आप इस चैट के पूर्वावलोकन मोड में हैं", + "You_are_logged_in_as": "आपने इसके रूप में लॉगिन किया है", + "You_are_not_authorized_to_view_this_page": "आप इस पृष्ठ को देखने के लिए अधिकृत नहीं हैं।", + "You_can_change_a_different_avatar_too": "आप इस एकीकरण से पोस्ट करने के लिए उपयोग किए गए अवतार को ओवरराइड कर सकते हैं।", + "You_can_close_this_window_now": "अब आप इस विंडो को बंद कर सकते हैं.", + "You_can_search_using_RegExp_eg": "आप रेगुलर एक्सप्रेशन का उपयोग करके खोज सकते हैं। उदाहरण के लिए /^text$/i", + "You_can_try_to": "आप कोशिश कर सकते हैं", + "You_can_use_an_emoji_as_avatar": "आप इमोजी को अवतार के तौर पर भी इस्तेमाल कर सकते हैं.", + "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "आप अपने सीआरएम के साथ ओमनीचैनल को आसानी से एकीकृत करने के लिए वेबहुक का उपयोग कर सकते हैं।", + "You_cant_leave_a_livechat_room_Please_use_the_close_button": "आप एक सर्वचैनल कमरा नहीं छोड़ सकते। कृपया, बंद करें बटन का उपयोग करें।", + "You_followed_this_message": "आपने इस संदेश का अनुसरण किया.", + "You_have_a_new_message": "आपको एक नया संदेश आया है", + "You_have_been_muted": "आपको मौन कर दिया गया है और आप इस कमरे में बोल नहीं सकते", + "You_have_been_removed_from__roomName_": "आपको कमरे {{roomName}} से निकाल दिया गया है", + "You_have_joined_a_new_call_with": "आप एक नई कॉल में शामिल हुए हैं", + "You_have_n_codes_remaining": "आपके पास {{number}} कोड शेष हैं।", + "You_have_not_verified_your_email": "आपने अपना ईमेल सत्यापित नहीं किया है.", + "You_have_successfully_unsubscribed": "आपने हमारी मेलिंग सूची से सफलतापूर्वक सदस्यता समाप्त कर दी है।", + "You_must_join_to_view_messages_in_this_channel": "इस चैनल में संदेश देखने के लिए आपको अवश्य शामिल होना चाहिए", + "You_need_confirm_email": "लॉगिन करने के लिए आपको अपने ईमेल की पुष्टि करनी होगी!", + "You_need_install_an_extension_to_allow_screen_sharing": "स्क्रीन शेयरिंग की अनुमति देने के लिए आपको एक एक्सटेंशन इंस्टॉल करना होगा", + "You_need_to_change_your_password": "आपको अपना पासवर्ड बदलना होगा", + "You_need_to_type_in_your_password_in_order_to_do_this": "ऐसा करने के लिए आपको अपना पासवर्ड टाइप करना होगा!", + "You_need_to_type_in_your_username_in_order_to_do_this": "ऐसा करने के लिए आपको अपना उपयोगकर्ता नाम टाइप करना होगा!", + "You_need_to_verifiy_your_email_address_to_get_notications": "सूचनाएं प्राप्त करने के लिए आपको अपना ईमेल पता सत्यापित करना होगा", + "You_need_to_write_something": "तुम्हें कुछ लिखना होगा!", + "You_reached_the_maximum_number_of_guest_users_allowed_by_your_license": "आप अपने लाइसेंस द्वारा अनुमत अतिथि उपयोगकर्ताओं की अधिकतम संख्या तक पहुँच गए हैं।", + "You_should_inform_one_url_at_least": "आपको कम से कम एक यूआरएल परिभाषित करना चाहिए.", + "You_should_name_it_to_easily_manage_your_integrations": "अपने एकीकरणों को आसानी से प्रबंधित करने के लिए आपको इसे नाम देना चाहिए।", + "You_unfollowed_this_message": "आपने इस संदेश को अनफ़ॉलो कर दिया है.", + "You_will_be_asked_for_permissions": "आपसे अनुमतियां मांगी जाएंगी", + "You_will_not_be_able_to_recover": "आप इस संदेश को पुनर्प्राप्त नहीं कर पाएंगे!", + "You_will_not_be_able_to_recover_email_inbox": "आप इस ईमेल इनबॉक्स को पुनर्प्राप्त नहीं कर पाएंगे", + "You_will_not_be_able_to_recover_file": "आप इस फ़ाइल को पुनर्प्राप्त नहीं कर पाएंगे!", + "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "आपको ईमेल सूचनाएं प्राप्त नहीं होंगी क्योंकि आपने अपना ईमेल सत्यापित नहीं किया है।", + "Your_e2e_key_has_been_reset": "आपकी e2e कुंजी रीसेट कर दी गई है.", + "Your_email_address_has_changed": "आपका ईमेल पता बदल दिया गया है.", + "Your_email_has_been_queued_for_sending": "आपका ईमेल भेजने के लिए कतारबद्ध है", + "Your_entry_has_been_deleted": "आपकी प्रविष्टि हटा दी गई है.", + "Your_file_has_been_deleted": "आपकी फ़ाइल हटा दी गई है.", + "Your_invite_link_will_expire_after__usesLeft__uses": "आपका आमंत्रण लिंक {{usesLeft}} के उपयोग के बाद समाप्त हो जाएगा।", + "Your_invite_link_will_expire_on__date__": "आपका आमंत्रण लिंक {{date}} को समाप्त हो जाएगा।", + "Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "आपका आमंत्रण लिंक {{date}} को या {{usesLeft}} उपयोग के बाद समाप्त हो जाएगा।", + "Your_invite_link_will_never_expire": "आपका आमंत्रण लिंक कभी समाप्त नहीं होगा.", + "your_message": "आपका संदेश", + "your_message_optional": "आपका संदेश (वैकल्पिक)", + "Your_new_email_is_email": "आपका नया ईमेल पता [ईमेल] है।", + "Your_password_is_wrong": "आपका पासवर्ड ग़लत है!", + "Your_password_was_changed_by_an_admin": "आपका पासवर्ड किसी व्यवस्थापक द्वारा बदल दिया गया था.", + "Your_push_was_sent_to_s_devices": "आपका पुश %s डिवाइस पर भेजा गया था", + "Your_request_to_join__roomName__has_been_made_it_could_take_up_to_15_minutes_to_be_processed": "{{roomName}} में शामिल होने के लिए आपका अनुरोध कर दिया गया है, इसे संसाधित होने में 15 मिनट तक का समय लग सकता है। जब यह जाने के लिए तैयार होगा तो आपको सूचित कर दिया जाएगा।", + "Your_question": "आपका प्रश्न", + "Your_server_link": "आपका सर्वर लिंक", + "Your_temporary_password_is_password": "आपका अस्थायी पासवर्ड [पासवर्ड] है।", + "Your_TOTP_has_been_reset": "आपका टू फैक्टर टीओटीपी रीसेट कर दिया गया है।", + "Your_web_browser_blocked_Rocket_Chat_from_opening_tab": "आपके वेब ब्राउज़र ने Rocket.Chat को नया टैब खोलने से रोक दिया है।", + "Your_workspace_is_ready": "आपका कार्यक्षेत्र उपयोग के लिए तैयार है 🎉", + "Zapier": "Zapier", + "registration.page.login.errors.wrongCredentials": "उपयोगकर्ता नहीं मिला या पासवर्ड ग़लत है", + "registration.page.login.errors.invalidEmail": "अमान्य ईमेल", + "registration.page.login.errors.loginBlockedForIp": "इस आईपी के लिए लॉगिन अस्थायी रूप से अवरुद्ध कर दिया गया है", + "registration.page.login.errors.loginBlockedForUser": "इस उपयोगकर्ता के लिए लॉगिन अस्थायी रूप से अवरुद्ध कर दिया गया है", + "registration.page.login.errors.licenseUserLimitReached": "उपयोगकर्ताओं की अधिकतम संख्या तक पहुँच गया है.", + "registration.page.login.errors.AppUserNotAllowedToLogin": "ऐप उपयोगकर्ताओं को सीधे लॉग इन करने की अनुमति नहीं है।", + "registration.page.registration.waitActivationWarning": "इससे पहले कि आप लॉग इन कर सकें, आपका खाता किसी व्यवस्थापक द्वारा मैन्युअल रूप से सक्रिय होना चाहिए।", + "registration.page.login.register": "अब यहां? <1>एक खाता बनाएं", + "registration.page.login.forgot": "अपना कूट शब्द भूल गए?", + "registration.page.register.back": "लॉगिन पर वापस जाएं", + "registration.page.emailVerification.subTitle": "इस सर्वर को सत्यापित ईमेल पते की आवश्यकता है। सत्यापन लिंक के लिए कृपया अपना ईमेल इनबॉक्स जांचें।", + "registration.page.emailVerification.sent": "सत्यापन ईमेल भेजा गया, कृपया अपना इनबॉक्स जांचें।", + "registration.page.resetPassword.sent": "यदि यह ईमेल पंजीकृत है, तो हम आपका पासवर्ड रीसेट करने के तरीके पर निर्देश भेजेंगे। यदि आपको शीघ्र ही कोई ईमेल प्राप्त नहीं होता है, तो कृपया वापस आएं और पुनः प्रयास करें।", + "registration.page.resetPassword.sendInstructions": "निर्देश भेजें", + "registration.page.resetPassword.errors.invalidEmail": "अमान्य ईमेल", + "registration.page.poweredBy": "<1>Rocket.Chat द्वारा संचालित", + "registration.page.guest.chooseHowToJoin": "चुनें कि आप कैसे शामिल होना चाहते हैं", + "registration.page.guest.loginWithRocketChat": "Rocket.Chat के साथ लॉगिन करें", + "registration.page.guest.continueAsGuest": "अतिथि के रूप में जारी रखें", + "registration.component.welcome": "<1>Rocket.Chat कार्यक्षेत्र में आपका स्वागत है", + "registration.component.login": "लॉग इन करें", + "registration.component.login.userNotFound": "उपयोगकर्ता नहीं मिला", + "registration.component.login.incorrectPassword": "गलत पासवर्ड", + "registration.component.switchLanguage": "<1>{{name}} में बदलें", + "registration.component.resetPassword": "पासवर्ड रीसेट", + "registration.component.form.emailOrUsername": "ईमेल या उपयोगकर्ता का नाम", + "registration.component.form.username": "उपयोगकर्ता नाम", + "registration.component.form.name": "नाम", + "registration.component.form.nameOptional": "नाम: (वैकल्पिक", + "registration.component.form.createAnAccount": "खाता बनाएं", + "registration.component.form.userAlreadyExist": "उपयोगकर्ता का नाम पहले से मौजूद है। कृपया कोई अन्य उपयोक्तानाम आज़माएँ.", + "registration.component.form.emailAlreadyExists": "ईमेल पहले से ही मौजूद है", + "registration.component.form.usernameAlreadyExists": "उपयोगकर्ता का नाम पहले से मौजूद है। कृपया कोई अन्य उपयोक्तानाम आज़माएँ.", + "registration.component.form.invalidEmail": "दर्ज किया गया ईमेल अमान्य है", + "registration.component.form.email": "ईमेल", + "registration.component.form.emailPlaceholder": "example@example.com", + "registration.component.form.password": "पासवर्ड", + "registration.component.form.divider": "या", + "registration.component.form.submit": "जमा करना", + "registration.component.form.requiredField": "यह फ़ील्ड आवश्यक है", + "registration.component.form.joinYourTeam": "अपनी टीम में शामिल हों", + "registration.component.form.reasonToJoin": "शामिल होने का कारण", + "registration.component.form.invalidConfirmPass": "पासवर्ड पुष्टिकरण पासवर्ड से मेल नहीं खाता", + "registration.component.form.confirmPassword": "अपने पासवर्ड की पुष्टि करें", + "registration.component.form.confirmation": "पुष्टीकरण", + "registration.component.form.sendConfirmationEmail": "पुष्टिकरण ईमेल भेजें", + "registration.component.form.register": "पंजीकरण करवाना", + "onboarding.component.form.requiredField": "यह फ़ील्ड आवश्यक है", + "onboarding.component.form.steps": "{{stepCount}} का चरण {{currentStep}}", + "onboarding.component.form.action.back": "पीछे", + "onboarding.component.form.action.next": "अगला", + "onboarding.component.form.action.skip": "इस स्टेप को छोड़ दें", + "onboarding.component.form.action.register": "पंजीकरण करवाना", + "onboarding.component.form.action.registerWorkspace": "कार्यक्षेत्र पंजीकृत करें", + "onboarding.component.form.action.registerOffline": "ऑफ़लाइन पंजीकरण करें", + "onboarding.component.form.action.confirm": "पुष्टि करना", + "onboarding.component.form.action.pasteHere": "यहां चिपकाएं...", + "onboarding.component.form.action.completeRegistration": "पूरा पंजीकरण", + "onboarding.component.form.termsAndConditions": "मैं <1>नियम एवं शर्तें और <3>गोपनीयता नीति से सहमत हूं", + "onboarding.component.emailCodeFallback": "ईमेल प्राप्त नहीं हुआ? <1>पुनः भेजें या <3>ईमेल बदलें।", + "onboarding.page.form.title": "आइए आपका कार्यक्षेत्र लॉन्च करें", + "onboarding.page.emailConfirmed.title": "ईमेल की पुष्टि!", + "onboarding.page.emailConfirmed.subtitle": "आप अपने Rocket.Chat एप्लिकेशन पर वापस लौट सकते हैं - हमने आपका कार्यक्षेत्र पहले ही लॉन्च कर दिया है।", + "onboarding.page.checkYourEmail.title": "अपने ईमेल की जाँच करें", + "onboarding.page.checkYourEmail.subtitle": "आपका अनुरोध सफलतापूर्वक भेज दिया गया है।<1>अपना प्रीमियम योजना परीक्षण शुरू करने के लिए अपना ईमेल इनबॉक्स जांचें।<1>लिंक 30 मिनट में समाप्त हो जाएगा।", + "onboarding.page.confirmationProcess.title": "पुष्टि प्रक्रिया में है", + "onboarding.page.cloudDescription.title": "आइए आपका कार्यक्षेत्र और <1>14-दिवसीय परीक्षण लॉन्च करें", + "onboarding.page.cloudDescription.tryGold": "14 दिनों के लिए हमारा सर्वोत्तम गोल्ड प्लान निःशुल्क आज़माएँ", + "onboarding.page.cloudDescription.numberOfIntegrations": "1,000 एकीकरण", + "onboarding.page.cloudDescription.availability": "उच्च उपलब्धता", + "onboarding.page.cloudDescription.auditing": "संदेश ऑडिट पैनल/ऑडिट लॉग", + "onboarding.page.cloudDescription.engagement": "सगाई डैशबोर्ड", + "onboarding.page.cloudDescription.ldap": "एलडीएपी उन्नत सिंक", + "onboarding.page.cloudDescription.omnichannel": "ओमनीचैनल प्रीमियम", + "onboarding.page.cloudDescription.sla": "एसएलए: प्रीमियम", + "onboarding.page.cloudDescription.push": "सुरक्षित पुश सूचनाएं", + "onboarding.page.cloudDescription.goldIncludes": "* गोल्डन प्लान में अन्य प्लान की सभी सुविधाएँ शामिल हैं", + "onboarding.page.alreadyHaveAccount": "क्या आपके पास पहले से एक खाता मौजूद है? <1>अपने कार्यस्थान प्रबंधित करें।", + "onboarding.page.invalidLink.title": "आपका लिंक अब मान्य नहीं है", + "onboarding.page.invalidLink.content": "ऐसा लगता है कि आप पहले ही आमंत्रण लिंक का उपयोग कर चुके हैं. यह एकल साइन इन के लिए तैयार किया गया है। अपने कार्यक्षेत्र में शामिल होने के लिए एक नए साइन इन का अनुरोध करें।", + "onboarding.page.invalidLink.button.text": "नए लिंक का अनुरोध करें", + "onboarding.page.requestTrial.title": "<1>30-दिवसीय परीक्षण का अनुरोध करें", + "onboarding.page.requestTrial.subtitle": "30 दिनों के लिए हमारी सर्वोत्तम प्रीमियम योजना निःशुल्क आज़माएँ", + "onboarding.page.magicLinkEmail.title": "हमने आपको एक लॉगिन लिंक ईमेल किया है", + "onboarding.page.magicLinkEmail.subtitle": "आपके कार्यक्षेत्र में साइन इन करने के लिए हमने आपको अभी जो ईमेल भेजा है उसमें दिए गए लिंक पर क्लिक करें। <1>लिंक 30 मिनट में समाप्त हो जाएगा।", + "onboarding.form.adminInfoForm.title": "व्यवस्थापक जानकारी", + "onboarding.form.adminInfoForm.subtitle": "आपके कार्यक्षेत्र के लिए एक व्यवस्थापक प्रोफ़ाइल बनाने के लिए हमें इस जानकारी की आवश्यकता है।", + "onboarding.form.adminInfoForm.fields.fullName.label": "पूरा नाम", + "onboarding.form.adminInfoForm.fields.fullName.placeholder": "पहला और आखिरी नाम", + "onboarding.form.adminInfoForm.fields.username.label": "उपयोगकर्ता नाम", + "onboarding.form.adminInfoForm.fields.username.placeholder": "@उपयोगकर्ता नाम", + "onboarding.form.adminInfoForm.fields.email.label": "ईमेल", + "onboarding.form.adminInfoForm.fields.email.placeholder": "ईमेल", + "onboarding.form.adminInfoForm.fields.password.label": "पासवर्ड", + "onboarding.form.adminInfoForm.fields.password.placeholder": "पासवर्ड बनाएं", + "onboarding.form.adminInfoForm.fields.keepPosted.label": "मुझे Rocket.Chat अपडेट के बारे में बताते रहें", + "onboarding.form.awaitConfirmationForm.title": "पुष्टिकरण की प्रतीक्षा", + "onboarding.form.awaitConfirmationForm.content.securityCode": "सुरक्षा कोड", + "onboarding.form.awaitConfirmationForm.content.sentEmail": "ईमेल एक पुष्टिकरण लिंक के साथ <1>{{emailAddress}} पर भेजा गया है। कृपया सत्यापित करें कि नीचे दिया गया सुरक्षा कोड ईमेल में दिए गए सुरक्षा कोड से मेल खाता है।", + "onboarding.form.organizationInfoForm.title": "संगठन की जानकारी", + "onboarding.form.organizationInfoForm.subtitle": "हमें यह जानना होगा कि आप कौन हैं.", + "onboarding.form.organizationInfoForm.fields.organizationName.label": "संगठन का नाम", + "onboarding.form.organizationInfoForm.fields.organizationName.placeholder": "संगठन का नाम", + "onboarding.form.organizationInfoForm.fields.organizationType.label": "संगठन का प्रकार", + "onboarding.form.organizationInfoForm.fields.organizationType.placeholder": "चुनना", + "onboarding.form.organizationInfoForm.fields.organizationIndustry.label": "संगठन उद्योग", + "onboarding.form.organizationInfoForm.fields.organizationIndustry.placeholder": "चुनना", + "onboarding.form.organizationInfoForm.fields.organizationSize.label": "संगठन का आकार", + "onboarding.form.organizationInfoForm.fields.organizationSize.placeholder": "चुनना", + "onboarding.form.organizationInfoForm.fields.country.label": "देश", + "onboarding.form.organizationInfoForm.fields.country.placeholder": "चुनना", + "onboarding.form.registerOfflineForm.title": "ऑफ़लाइन पंजीकरण करें", + "onboarding.form.registerOfflineForm.copyStep.description": "यदि किसी कारण से आपका कार्यक्षेत्र इंटरनेट से कनेक्ट नहीं हो पाता है, तो इन चरणों का पालन करें:<1>1. यहां जाएं: <2>cloud.rocket.chat > कार्यस्थान और \"<3>स्वयं-प्रबंधित पंजीकरण करें\"<4>2 पर क्लिक करें। “<5>ऑफ़लाइन जारी रखें”<6>3 पर क्लिक करें। Cloud.rocket.chat में <7>ऑफ़लाइन कार्यस्थान पंजीकृत करें संवाद में, नीचे दिए गए बॉक्स में टोकन पेस्ट करें", + "onboarding.form.registerOfflineForm.pasteStep.description": "1. <1>cloud.rocket.chat में जेनरेटेड टेक्स्ट प्राप्त करें और अपनी पंजीकरण प्रक्रिया पूरी करने के लिए नीचे पेस्ट करें", + "onboarding.form.registerOfflineForm.fields.registrationToken.inputLabel": "पंजीकरण टोकन", + "onboarding.form.registeredServerForm.title": "अपना कार्यक्षेत्र पंजीकृत करें", + "onboarding.form.registeredServerForm.included.push": "मोबाइल पुश सूचनाएँ", + "onboarding.form.registeredServerForm.included.externalProviders": "बाहरी प्रदाताओं के साथ एकीकरण (व्हाट्सएप, फेसबुक, टेलीग्राम, ट्विटर)", + "onboarding.form.registeredServerForm.included.apps": "बाज़ार ऐप्स तक पहुंच", + "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "व्यवस्थापक ईमेल", + "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "जारी रखने के लिए अपना ईमेल डालें", + "onboarding.form.registeredServerForm.keepInformed": "मुझे समाचारों और घटनाओं के बारे में सूचित रखें", + "onboarding.form.registeredServerForm.registerLater": "बाद में दर्ज करें", + "onboarding.form.registeredServerForm.notConnectedToInternet": "सर्वर इंटरनेट से कनेक्ट नहीं है, इसलिए आपको इस कार्यक्षेत्र के लिए ऑफ़लाइन पंजीकरण करना होगा।", + "onboarding.form.registeredServerForm.registrationEngagement": "पंजीकरण स्वचालित लाइसेंस अपडेट, महत्वपूर्ण कमजोरियों की अधिसूचना और रॉकेट.चैट क्लाउड सेवाओं तक पहुंच की अनुमति देता है। कोई संवेदनशील कार्यक्षेत्र डेटा साझा नहीं किया जाता है; Rocket.Chat पर भेजे गए आँकड़े आपको प्रशासन क्षेत्र के भीतर दिखाई देंगे।", + "onboarding.form.registeredServerForm.registrationKeepInformed": "इस फॉर्म को सबमिट करके आप हमारी <1>गोपनीयता नीति के अनुसार, Rocket.Chat उत्पादों, घटनाओं और अपडेट के बारे में अधिक जानकारी प्राप्त करने के लिए सहमति देते हैं। आप किसी भी समय सदस्यता वापस ले सकते हैं।", + "onboarding.form.standaloneServerForm.title": "स्टैंडअलोन सर्वर पुष्टिकरण", + "onboarding.form.standaloneServerForm.servicesUnavailable": "कुछ सेवाएँ अनुपलब्ध होंगी या मैन्युअल सेटअप की आवश्यकता होगी", + "onboarding.form.standaloneServerForm.publishOwnApp": "पुश सूचनाएं भेजने के लिए आपको अपना स्वयं का ऐप Google Play और App Store पर संकलित और प्रकाशित करना होगा", + "onboarding.form.standaloneServerForm.manuallyIntegrate": "बाहरी सेवाओं के साथ मैन्युअल रूप से एकीकृत करने की आवश्यकता है", + "Something_Went_Wrong": "कुछ गलत हो गया", + "Toolbox_room_actions": "प्राथमिक कक्ष क्रियाएँ", + "Theme_light": "रोशनी", + "Theme_light_description": "दृष्टिबाधित व्यक्तियों के लिए अधिक सुलभ और अच्छी रोशनी वाले वातावरण के लिए एक अच्छा विकल्प।", + "Theme_dark": "अँधेरा", + "Theme_dark_description": "स्क्रीन द्वारा उत्सर्जित प्रकाश की मात्रा को कम करके कम रोशनी की स्थिति में आंखों का तनाव और थकान कम करें।", + "Enable_of_limit_apps_currently_enabled": "** वर्तमान में {{limit}} {{context}} ऐप्स में से {{limit}} सक्षम हैं।**\n \nसमुदाय पर कार्यस्थानों में अधिकतम {{limit}} {{context}} ऐप्स सक्षम हो सकते हैं।\n \n**{{appName}} डिफ़ॉल्ट रूप से अक्षम कर दिया जाएगा।** इस ऐप को सक्षम करने के लिए किसी अन्य {{context}} ऐप को अक्षम करें या प्रीमियम में अपग्रेड करें।", + "Enable_of_limit_apps_currently_enabled_exceeded": "** वर्तमान में {{limit}} {{context}} ऐप्स में से {{limit}} सक्षम हैं।**\n \nसामुदायिक ऐप की सीमा पार हो गई है.\n \nसमुदाय पर कार्यस्थानों में अधिकतम {{limit}} {{context}} ऐप्स सक्षम हो सकते हैं।\n \n**{{appName}} डिफ़ॉल्ट रूप से अक्षम कर दिया जाएगा।** इस ऐप को सक्षम करने के लिए आपको कम से कम {{exceed}} अन्य {{context}} ऐप्स को अक्षम करना होगा या प्रीमियम प्लान में अपग्रेड करना होगा।", + "Workspaces_on_Community_edition_install_app": "सामुदायिक कार्यस्थानों में अधिकतम {{limit}} {{context}} ऐप्स सक्षम हो सकते हैं। असीमित ऐप्स सक्षम करने के लिए प्रीमियम प्लान में अपग्रेड करें।", + "Apps_Currently_Enabled": "{{limit}} {{context}} में से {{limit}} ऐप्स वर्तमान में सक्षम हैं", + "Disable_another_app": "इस ऐप को सक्षम करने के लिए किसी अन्य ऐप को अक्षम करें या प्रीमियम प्लान में अपग्रेड करें।", + "Upload_anyway": "फिर भी अपलोड करें", + "App_limit_reached": "ऐप की सीमा पूरी हो गई", + "App_limit_exceeded": "ऐप की सीमा पार हो गई", + "Private_apps_limit_reached": "निजी ऐप्स की सीमा पूरी हो गई", + "Private_apps_limit_exceeded": "निजी ऐप्स की सीमा पार हो गई", + "Disable_at_least_more_apps": "इस ऐप को सक्षम करने के लिए आपको कम से कम {{numberOfExceededApps}} अन्य ऐप्स को अक्षम करना होगा या प्रीमियम प्लान में अपग्रेड करना होगा।", + "Community_Private_apps_limit_exceeded": "सामुदायिक ऐप की सीमा पार हो गई है.", + "Theme_match_system": "मिलान प्रणाली", + "Theme_match_system_description": "स्वचालित रूप से आपके सिस्टम के स्वरूप का मिलान करें।", + "Theme_high_contrast": "हाई कॉन्ट्रास्ट", + "Theme_high_contrast_description": "बोल्ड रंगों और तीव्र विरोधाभासों के साथ अधिकतम तानवाला विभेदन बेहतर पहुंच प्रदान करता है।", + "Highlighted_chosen_word": "चयनित शब्द पर प्रकाश डाला गया", + "Join_your_team": "अपनी टीम में शामिल हों", + "Create_a_password": "एक पासवर्ड बनाएं", + "Create_an_account": "खाता बनाएं", + "Get_all_apps": "वे सभी ऐप्स प्राप्त करें जिनकी आपकी टीम को आवश्यकता है", + "Workspaces_on_community_edition_trial_on": "समुदाय पर कार्यस्थानों में अधिकतम 5 मार्केटप्लेस ऐप्स और 3 निजी ऐप्स सक्षम हो सकते हैं। इन सीमाओं को हटाने के लिए आज ही निःशुल्क प्रीमियम परीक्षण शुरू करें!", + "Workspaces_on_community_edition_trial_off": "समुदाय पर कार्यस्थानों में अधिकतम 5 मार्केटप्लेस ऐप्स और 3 निजी ऐप्स सक्षम हो सकते हैं। सीमाएं हटाने और अपने कार्यक्षेत्र को सुपरचार्ज करने के लिए प्रीमियम में अपग्रेड करें।", + "No_private_apps_installed": "कोई निजी ऐप्स इंस्टॉल नहीं", + "Private_apps_are_side-loaded": "निजी ऐप्स साइड-लोडेड हैं और मार्केटप्लेस पर उपलब्ध नहीं हैं।", + "Chat_transcript": "चैट प्रतिलेख", + "Conversational_transcript": "संवादी प्रतिलेख", + "Conversations_by_agents": "एजेंटों द्वारा बातचीत", + "Conversations_by_channel": "चैनल द्वारा बातचीत", + "Conversations_by_department": "विभाग द्वारा बातचीत", + "Conversations_by_status": "स्थिति के अनुसार बातचीत", + "Conversations_by_tag": "टैग द्वारा बातचीत", + "Send_conversation_transcript_via_email": "ईमेल के माध्यम से वार्तालाप प्रतिलेख भेजें", + "Always_send_the_transcript_to_contacts_at_the_end_of_the_conversations": "बातचीत के अंत में हमेशा संपर्कों को प्रतिलेख भेजें।", + "Export_conversation_transcript_as_PDF": "वार्तालाप प्रतिलेख को पीडीएफ के रूप में निर्यात करें", + "Omnichannel_transcript_email": "ईमेल के माध्यम से चैट प्रतिलेख भेजें.", + "Accounts_Default_User_Preferences_omnichannelTranscriptEmail_Description": "बातचीत के अंत में हमेशा संपर्कों को प्रतिलेख भेजें।", + "Omnichannel_transcript_pdf": "चैट प्रतिलेख को पीडीएफ के रूप में निर्यात करें।", + "Accounts_Default_User_Preferences_omnichannelTranscriptPDF_Description": "बातचीत के अंत में प्रतिलेख को हमेशा पीडीएफ के रूप में निर्यात करें।", + "Contact_email": "ई - मेल से संपर्क करे", + "Customer": "ग्राहक", + "Time": "समय", + "Omnichannel_Agent": "ओमनीचैनल एजेंट", + "This_attachment_is_not_supported": "अनुलग्नक प्रारूप समर्थित नहीं है", + "Send_transcript": "प्रतिलेख भेजें", + "Undo_request": "अनुरोध पूर्ववत करें", + "No_permission": "अनुमति नहीं", + "Community_cap_description": "सामुदायिक कार्यस्थानों में 200 समवर्ती कनेक्शन की सीमा होती है। यदि यह सीमा पार हो जाती है तो उपयोगकर्ताओं के लिए एक-दूसरे की स्थिति देखना संभव नहीं होगा। इससे संदेश भेजने और प्राप्त करने पर कोई प्रभाव नहीं पड़ता है.", + "Premium_cap_description": "प्रीमियम योजनाओं में उपस्थिति सेवा सीमा नहीं होती है।", + "Service_status": "सेवा की स्थिति", + "More_about_Premium_plans": "प्रीमियम योजनाओं के बारे में अधिक जानकारी", + "Presence_service_cap": "उपस्थिति सेवा कैप", + "User_Status": "उपयोगकर्ता की स्थिति", + "User_status_menu": "उपयोगकर्ता स्थिति मेनू", + "Active_connections": "सक्रिय कनेक्शन", + "Presence_service": "उपस्थिति सेवा", + "Presence_broadcast_disabled": "उपस्थिति प्रसारण आंतरिक रूप से अक्षम है", + "Presence_broadcast_disabled_Description": "इससे पता चलता है कि क्या उपस्थिति प्रसारण स्वचालित रूप से अक्षम कर दिया गया है। ऐसा तब हो सकता है जब आपके पास प्रीमियम लाइसेंस नहीं है और 200 से अधिक समवर्ती कनेक्शन हैं।", + "New_custom_status": "नई कस्टम स्थिति", + "Service_disabled": "सेवा अब अक्षम है", + "Service_disabled_description": "जब तक एक ही समय में 200 से कम सक्रिय कनेक्शन न हों तब तक आप इसे दोबारा सक्षम नहीं कर सकते", + "User_status_disabled": "प्रदर्शन को बनाए रखने के लिए उपयोगकर्ता स्थिति अस्थायी रूप से अक्षम कर दी गई है।", + "User_status_disabled_learn_more": "उपयोगकर्ता स्थिति अक्षम", + "User_status_disabled_learn_more_description": "सक्रिय कनेक्शनों की अधिक मात्रा के कारण, उपयोगकर्ता की स्थिति को संभालने वाली सेवा अस्थायी रूप से अक्षम है। व्यवस्थापक इसे कार्यस्थान सेटिंग्स में मैन्युअल रूप से पुनः सक्षम कर सकते हैं।", + "Go_to_workspace_settings": "कार्यस्थान सेटिंग्स पर जाएँ", + "User_status_temporarily_disabled": "उपयोगकर्ता स्थिति अस्थायी रूप से अक्षम है", + "Use_token": "टोकन का प्रयोग करें", + "Disconnected": "डिस्कनेक्ट किया गया", + "Disconnect_workspace": "कार्यस्थान को डिस्कनेक्ट करें", + "Awaiting_confirmation": "पुष्टिकरण की प्रतीक्षा", + "Security_code": "सुरक्षा कोड", + "Registration_Token": "पंजीकरण टोकन", + "RegisterWorkspace_Button": "कार्यक्षेत्र पंजीकृत करें", + "ConnectWorkspace_Button": "कार्यक्षेत्र कनेक्ट करें", + "Workspace_registered": "कार्यक्षेत्र पंजीकृत", + "Workspace_not_connected": "कार्यस्थल कनेक्ट नहीं है", + "Token_Not_Recognized": "टोकन पहचाना नहीं गया", + "RegisterWorkspace_Registered_Description": "ये सेवाएँ उपलब्ध हैं", + "RegisterWorkspace_Registered_Subtitle": "चूँकि यह कार्यस्थान पंजीकृत है इसलिए निम्नलिखित उपलब्ध है", + "RegisterWorkspace_Registered_Benefits": "पंजीकरण स्वचालित लाइसेंस अपडेट, महत्वपूर्ण कमजोरियों की अधिसूचना और रॉकेट.चैट क्लाउड सेवाओं तक पहुंच की अनुमति देता है। कोई भी संवेदनशील कार्यक्षेत्र डेटा Rocket.Chat के साथ साझा नहीं किया जाता है।", + "RegisterWorkspace_NotRegistered_Title": "कार्यस्थल पंजीकृत नहीं है", + "RegisterWorkspace_NotRegistered_Subtitle": "इस कार्यक्षेत्र को पंजीकृत करें और प्राप्त करें", + "RegisterWorkspace_NotConnected_Title": "कार्यस्थल डिस्कनेक्ट हो गया", + "RegisterWorkspace_NotConnected_Subtitle": "इस कार्यक्षेत्र को कनेक्ट करें और प्राप्त करें", + "RegisterWorkspace_NotRegistered_Description": "कार्यक्षेत्र पंजीकृत करने के लाभ", + "RegisterWorkspace_Disconnect_Subtitle": "आपके कार्यक्षेत्र को डिस्कनेक्ट करने से निम्नलिखित की हानि होगी", + "RegisterWorkspace_Disconnect_Error": "डिस्कनेक्ट करने में त्रुटि उत्पन्न हुई", + "RegisterWorkspace_Features_MobileNotifications_Title": "मोबाइल पुश सूचनाएँ", + "RegisterWorkspace_Features_MobileNotifications_Description": "कार्यक्षेत्र के सदस्यों को उनके मोबाइल उपकरणों पर सूचनाएं प्राप्त करने की अनुमति देता है।", + "RegisterWorkspace_Features_MobileNotifications_Disconnect": "कार्यक्षेत्र के सदस्यों को अब अपने मोबाइल उपकरणों पर सूचनाएं प्राप्त नहीं होंगी।", + "RegisterWorkspace_Features_Marketplace_Title": "बाजार", + "RegisterWorkspace_Features_Marketplace_Description": "इस कार्यक्षेत्र पर Rocket.Chat मार्केटप्लेस ऐप्स इंस्टॉल करें।", + "RegisterWorkspace_Features_Marketplace_Disconnect": "अब ऐप्स इंस्टॉल करना संभव नहीं होगा.", + "RegisterWorkspace_Features_Omnichannel_Title": "सर्वचैनल", + "RegisterWorkspace_Features_Omnichannel_Description": "दुनिया के सबसे लोकप्रिय सामाजिक चैनलों के माध्यम से अपने दर्शकों से, जहां वे हैं, बात करें।", + "RegisterWorkspace_Features_Omnichannel_Disconnect": "ओमनीचैनल क्षमताएं अब उपलब्ध नहीं होंगी.", + "RegisterWorkspace_Features_ThirdPartyLogin_Title": "तृतीय-पक्ष लॉगिन", + "RegisterWorkspace_Features_ThirdPartyLogin_Description": "कार्यस्थान सदस्यों को तृतीय-पक्ष एप्लिकेशन के सेट का उपयोग करके लॉग इन करने दें।", + "RegisterWorkspace_Features_ThirdPartyLogin_Disconnect": "तृतीय-पक्ष लॉगिन विकल्प अब उपलब्ध नहीं होंगे।", + "RegisterWorkspace_Token_Title": "टोकन के साथ कार्यक्षेत्र पंजीकृत करें", + "RegisterWorkspace_Token_Step_Two": "टोकन को कॉपी करें और नीचे पेस्ट करें।", + "RegisterWorkspace_with_email": "कार्यस्थल को ईमेल से पंजीकृत करें", + "RegisterWorkspace_Setup_Subtitle": "इस कार्यक्षेत्र को पंजीकृत करने के लिए इसे Rocket.Chat क्लाउड खाते से संबद्ध करना होगा।", + "RegisterWorkspace_Setup_Steps": "{{numberOfSteps}} का चरण {{step}}", + "RegisterWorkspace_Setup_Label": "क्लाउड खाता ईमेल", + "RegisterWorkspace_Setup_Have_Account_Title": "एक खाता है?", + "RegisterWorkspace_Setup_Have_Account_Subtitle": "इस कार्यक्षेत्र को अपने खाते से संबद्ध करने के लिए अपना क्लाउड खाता ईमेल दर्ज करें।", + "RegisterWorkspace_Setup_No_Account_Title": "कोई खाता नहीं है?", + "RegisterWorkspace_Setup_No_Account_Subtitle": "एक नया क्लाउड खाता बनाने और इस कार्यक्षेत्र को संबद्ध करने के लिए अपना ईमेल दर्ज करें।", + "cloud.RegisterWorkspace_Setup_Email_Confirmation": "पुष्टिकरण लिंक के साथ <1>ईमेल पर ईमेल भेजा गया।", + "RegisterWorkspace_Setup_Email_Verification": "कृपया सत्यापित करें कि नीचे दिया गया सुरक्षा कोड ईमेल में दिए गए सुरक्षा कोड से मेल खाता है।", + "RegisterWorkspace_Syncing_Error": "आपके कार्यस्थान को समन्वयित करते समय एक त्रुटि उत्पन्न हुई", + "RegisterWorkspace_Syncing_Complete": "सिंक पूर्ण", + "RegisterWorkspace_Connection_Error": "कनेक्ट करने में त्रुटि उत्पन्न हुई", + "cloud.RegisterWorkspace_Token_Step_One": "1. यहां जाएं: <1>cloud.rocket.chat > कार्यस्थान और <3>'स्वयं-प्रबंधित पंजीकरण करें' पर क्लिक करें।", + "cloud.RegisterWorkspace_Setup_Terms_Privacy": "मैं <1>नियम एवं शर्तें और <3>गोपनीयता नीति से सहमत हूं", + "Larger_amounts_of_active_connections": "बड़ी मात्रा में सक्रिय कनेक्शन के लिए आप हमारे <1>मल्टीपल इंस्टेंस समाधान पर विचार कर सकते हैं।", + "Uninstall_grandfathered_app": "{{appName}} अनइंस्टॉल करें?", + "App_will_lose_grandfathered_status": "**यह {{context}} ऐप अपना दादा दर्जा खो देगा।**\n \nसमुदाय पर कार्यस्थानों में अधिकतम {{limit}} {{context}} ऐप्स सक्षम हो सकते हैं। दादाजी ऐप्स को सीमा में गिना जाता है लेकिन सीमा उन पर लागू नहीं होती है।", + "All_rooms": "सभी कमरे", + "All_visible": "सब दिख रहा है", + "Filter_by_room": "कमरे के प्रकार के अनुसार फ़िल्टर करें", + "Filter_by_visibility": "दृश्यता के आधार पर फ़िल्टर करें", + "Theme_Appearence": "थीम उपस्थिति", + "mentions_counter": "{{count}} उल्लेख", + "threads_counter": "{{count}} अपठित थ्रेडेड संदेश", + "group_mentions_counter": "{{count}} समूह का उल्लेख", + "unread_messages_counter": "अपठित संदेश को {{count}}", + "Premium": "अधिमूल्य", + "Premium_capability": "प्रीमियम क्षमता", + "Operating_withing_plan_limits": "योजना सीमा के भीतर संचालन", + "Plan_limits_reached": "योजना की सीमा पूरी हो गई", + "Workspace_not_registered": "कार्यस्थल पंजीकृत नहीं है", + "Users_Connected": "उपयोगकर्ता जुड़े", + "Solve_issues": "मुद्दे सुलझाओ", + "Update_version": "नया संस्करण", + "Version_not_supported": "संस्करण <1>समर्थित नहीं", + "Version_supported_until": "संस्करण <1>समर्थित {{date}} तक", + "Check_support_availability": "<1>समर्थन उपलब्धता की जाँच करें", + "Outdated": "रगड़ा हुआ", + "Latest": "नवीनतम", + "New_version_available": "नया संस्करण उपलब्ध है", + "trial": "परीक्षण", + "Subscription": "अंशदान", + "Manage_subscription": "सदस्यता प्रबंधित करें", + "ActiveSessionsPeak": "सक्रिय सत्र चरम पर हैं", + "ActiveSessionsPeak_InfoText": "पिछले 30 दिनों में सक्रिय कनेक्शनों की सर्वाधिक संख्या", + "ActiveSessions": "सक्रिय सत्र", + "ActiveSessions_available": "सत्र उपलब्ध हैं", + "Monthly_active_contacts": "मासिक सक्रिय संपर्क", + "Upgrade": "उन्नत करना", + "Seats": "सीटें", + "Marketplace_apps": "मार्केटप्लेस ऐप्स", + "Private_apps": "निजी ऐप्स", + "Finish_your_purchase_trial": "<1>डाउनग्रेड परिणामों से बचने के लिए अपनी खरीदारी समाप्त करें", + "Contact_sales_trial": "अपनी खरीदारी पूरी करने और <1>डाउनग्रेड परिणामों से बचने के लिए सेल्स से संपर्क करें", + "Why_has_a_trial_been_applied_to_this_workspace": "<0>इस कार्यक्षेत्र पर परीक्षण क्यों लागू किया गया है?", + "Compare_plans": "योजनाओं की तुलना करें", + "n_days_left": "{{n}} दिन बचे हैं", + "Contact_sales": "बिक्री से संपर्क करें", + "Finish_purchase": "खरीदारी समाप्त करें", + "Self_managed_hosting": "स्व-प्रबंधित होस्टिंग", + "Cloud_hosting": "रॉकेट.चैट क्लाउड होस्टिंग", + "free_per_month_user": "$0 प्रति माह/उपयोगकर्ता", + "Trial_active": "परीक्षण सक्रिय", + "Contact_sales_renew_date": "योजना नवीनीकरण तिथि की जांच करने के लिए <0>बिक्री से संपर्क करें", + "Renews_DATE": "नवीनीकरण {{date}}", + "UpgradeToGetMore_Headline": "अधिक पाने के लिए अपग्रेड करें", + "UpgradeToGetMore_Subtitle": "उन्नत क्षमताओं के साथ अपने कार्यक्षेत्र को सुपरचार्ज करें।", + "UpgradeToGetMore_scalability_Title": "उच्च मापनीयता", + "UpgradeToGetMore_scalability_Body": "मोनोलिथिक से माइक्रोसर्विसेज या मल्टी-इंस्टेंस पर स्विच करके दक्षता में सुधार करें, लागत कम करें और समवर्ती उपयोगकर्ताओं का उपयोग बढ़ाएं।", + "UpgradeToGetMore_accessibility-certification_Title": "WCAG 2.1 और BITV 2.0", + "UpgradeToGetMore_accessibility-certification_Body": "Rocket.Chat के एक्सेसिबिलिटी प्रोग्राम के साथ WCAG और BITV मानकों का अनुपालन करें।", + "UpgradeToGetMore_engagement-dashboard_Title": "एनालिटिक्स", + "UpgradeToGetMore_engagement-dashboard_Body": "सहभागिता डैशबोर्ड के माध्यम से उपयोगकर्ता, संदेश और चैनल के उपयोग के बारे में जानकारी प्राप्त करें।", + "UpgradeToGetMore_oauth-enterprise_Title": "उन्नत प्रमाणीकरण", + "UpgradeToGetMore_oauth-enterprise_Body": "समूह भूमिका मैपिंग, चैनल सदस्यता, ऑटो लॉगआउट और बहुत कुछ के साथ एलडीएपी/एसएएमएल/ओथ के माध्यम से उचित पहुंच अनुमतियां सुनिश्चित करें।", + "UpgradeToGetMore_custom-roles_Title": "कस्टम भूमिकाएँ", + "UpgradeToGetMore_custom-roles_Body": "अपने कार्यक्षेत्र में लोगों के लिए विशिष्ट भूमिकाएँ और अनुमतियाँ निर्धारित करके एक सुरक्षित और उत्पादक कार्य वातावरण सुनिश्चित करें।", + "UpgradeToGetMore_auditing_Title": "संदेश ऑडिटिंग", + "UpgradeToGetMore_auditing_Body": "ग्राहकों, आपूर्तिकर्ताओं और आंतरिक टीमों के साथ संचार गुणवत्ता सुनिश्चित करने के लिए बातचीत को एक ही स्थान पर ऑडिट करें।", + "Seats_InfoText": "प्रत्येक अद्वितीय उपयोगकर्ता एक सीट पर रहता है। निष्क्रिय उपयोगकर्ता सीटों पर कब्जा नहीं करते हैं। सीटों की कुल संख्या सक्रिय लाइसेंस प्रकार द्वारा परिभाषित की जाती है।", + "CountSeats_InfoText": "प्रत्येक अद्वितीय उपयोगकर्ता एक सीट पर रहता है। निष्क्रिय उपयोगकर्ता सीटों पर कब्जा नहीं करते हैं।", + "MAC_InfoText": "(मैक) बिलिंग माह के दौरान जुड़े अद्वितीय सर्वचैनल संपर्कों की संख्या।", + "CountMAC_InfoText": "(मैक) कैलेंडर माह के दौरान जुड़े अद्वितीय ओमनीचैनल संपर्कों की संख्या।", + "ActiveSessions_InfoText": "कुल समवर्ती कनेक्शन. एक ही यूजर को कई बार कनेक्ट किया जा सकता है। प्रदर्शन समस्याओं को रोकने के लिए उपयोगकर्ता उपस्थिति सेवा 200 या उससे अधिक पर अक्षम है।", + "Apps_InfoText": "समुदाय 3 निजी ऐप्स और 5 मार्केटप्लेस ऐप्स को सक्षम करने की अनुमति देता है", + "Remove_RocketChat_Watermark_InfoText": "सशुल्क लाइसेंस सक्रिय होने पर वॉटरमार्क स्वचालित रूप से हटा दिया जाता है।", + "Remove_RocketChat_Watermark": "रॉकेट.चैट वॉटरमार्क हटाएँ", + "High_scalabaility": "उच्च मापनीयता", + "Premium_and_unlimited_apps": "प्रीमियम और असीमित ऐप्स", + "Message_audit": "संदेश ऑडिटिंग", + "Premium_omnichannel_capabilities": "प्रीमियम सर्वचैनल क्षमताएँ", + "Video_call_manager": "वीडियो कॉल प्रबंधक", + "Unlimited_push_notifications": "असीमित पुश सूचनाएं", + "Buy_more": "अधिक खरीदें", + "Upgrade_to_Pro": "प्रो में अपग्रेड", + "Sync_license_update": "सिंक लाइसेंस अद्यतन", + "Sync_license_update_Callout_Title": "हम आपका लाइसेंस अपडेट कर रहे हैं", + "Sync_license_update_Callout": "यदि आपको कुछ मिनटों के भीतर अपने कार्यक्षेत्र में कोई बदलाव नज़र नहीं आता है, तो लाइसेंस अपडेट को सिंक करें।", + "Includes": "शामिल", + "Unlock_premium_capabilities": "प्रीमियम क्षमताओं को अनलॉक करें", + "Unlimited_seats": "असीमित सीटें", + "Unlimited_MACs": "असीमित एमएसी", + "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" } \ No newline at end of file From c676b357c8d142fa0e08f209da7376f4c2e38f66 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 15 Aug 2024 13:55:50 -0300 Subject: [PATCH 11/49] chore(client): Remove Meteor.Error from `slashCommand` (#32915) --- .../client/slackbridge_import.client.js | 2 +- .../slashcommand-asciiarts/client/gimme.ts | 2 +- .../slashcommand-asciiarts/client/lenny.ts | 2 +- .../slashcommand-asciiarts/client/shrug.ts | 2 +- .../client/tableflip.ts | 2 +- .../slashcommand-asciiarts/client/unflip.ts | 2 +- .../slashcommand-asciiarts/server/gimme.ts | 2 +- .../slashcommand-asciiarts/server/lenny.ts | 2 +- .../slashcommand-asciiarts/server/shrug.ts | 2 +- .../server/tableflip.ts | 2 +- .../slashcommand-asciiarts/server/unflip.ts | 2 +- .../client/client.ts | 2 +- .../server/server.ts | 2 +- .../app/slashcommands-create/client/client.ts | 2 +- .../app/slashcommands-create/server/server.ts | 2 +- .../app/slashcommands-help/server/server.ts | 2 +- .../app/slashcommands-hide/client/hide.ts | 2 +- .../app/slashcommands-invite/client/client.ts | 2 +- .../app/slashcommands-invite/server/server.ts | 2 +- .../slashcommands-inviteall/client/client.ts | 2 +- .../slashcommands-inviteall/server/server.ts | 2 +- .../app/slashcommands-join/client/client.ts | 2 +- .../app/slashcommands-join/server/server.ts | 2 +- .../app/slashcommands-kick/client/client.ts | 2 +- .../app/slashcommands-kick/server/server.ts | 2 +- .../app/slashcommands-leave/server/leave.ts | 2 +- apps/meteor/app/slashcommands-me/server/me.ts | 2 +- .../app/slashcommands-msg/server/server.ts | 2 +- .../app/slashcommands-mute/server/mute.ts | 2 +- .../app/slashcommands-mute/server/unmute.ts | 2 +- .../app/slashcommands-open/client/client.ts | 2 +- .../app/slashcommands-status/client/status.ts | 2 +- .../app/slashcommands-status/server/status.ts | 2 +- .../app/slashcommands-topic/client/topic.ts | 2 +- .../app/slashcommands-topic/server/topic.ts | 2 +- .../client/client.ts | 2 +- .../server/server.ts | 2 +- apps/meteor/app/utils/client/index.ts | 2 +- .../app/utils/{lib => client}/slashCommand.ts | 11 +- apps/meteor/app/utils/server/slashCommand.ts | 136 +++++++++++++++++- .../client/hooks/useAppSlashCommands.ts | 2 +- .../client/lib/errors/InvalidCommandUsage.ts | 7 + .../client/lib/errors/InvalidPreview.ts | 7 + apps/meteor/client/lib/errors/index.ts | 2 + .../startup/slashCommands/federation.ts | 2 +- .../hooks/useComposerBoxPopupQueries.ts | 2 +- 46 files changed, 196 insertions(+), 49 deletions(-) rename apps/meteor/app/utils/{lib => client}/slashCommand.ts (85%) create mode 100644 apps/meteor/client/lib/errors/InvalidCommandUsage.ts create mode 100644 apps/meteor/client/lib/errors/InvalidPreview.ts create mode 100644 apps/meteor/client/lib/errors/index.ts diff --git a/apps/meteor/app/slackbridge/client/slackbridge_import.client.js b/apps/meteor/app/slackbridge/client/slackbridge_import.client.js index 6aeffb7bef45..eebc07ddb72d 100644 --- a/apps/meteor/app/slackbridge/client/slackbridge_import.client.js +++ b/apps/meteor/app/slackbridge/client/slackbridge_import.client.js @@ -1,5 +1,5 @@ import { settings } from '../../settings/client'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; settings.onload('SlackBridge_Enabled', (key, value) => { if (value) { diff --git a/apps/meteor/app/slashcommand-asciiarts/client/gimme.ts b/apps/meteor/app/slashcommand-asciiarts/client/gimme.ts index 4c9d6a4e40c8..7cd5edb6bb87 100644 --- a/apps/meteor/app/slashcommand-asciiarts/client/gimme.ts +++ b/apps/meteor/app/slashcommand-asciiarts/client/gimme.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { sdk } from '../../utils/client/lib/SDKClient'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; /* * Gimme is a named function that will replace /gimme commands * @param {Object} message - The message object diff --git a/apps/meteor/app/slashcommand-asciiarts/client/lenny.ts b/apps/meteor/app/slashcommand-asciiarts/client/lenny.ts index 99eaa03b9e59..0e3cc55f6b86 100644 --- a/apps/meteor/app/slashcommand-asciiarts/client/lenny.ts +++ b/apps/meteor/app/slashcommand-asciiarts/client/lenny.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { sdk } from '../../utils/client/lib/SDKClient'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; /* * Lenny is a named function that will replace /lenny commands * @param {Object} message - The message object diff --git a/apps/meteor/app/slashcommand-asciiarts/client/shrug.ts b/apps/meteor/app/slashcommand-asciiarts/client/shrug.ts index bc0fb300789e..c4bdec8f1a8c 100644 --- a/apps/meteor/app/slashcommand-asciiarts/client/shrug.ts +++ b/apps/meteor/app/slashcommand-asciiarts/client/shrug.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { sdk } from '../../utils/client/lib/SDKClient'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; /* * Shrug is a named function that will replace /shrug commands * @param {Object} message - The message object diff --git a/apps/meteor/app/slashcommand-asciiarts/client/tableflip.ts b/apps/meteor/app/slashcommand-asciiarts/client/tableflip.ts index 0d709760fe84..8820b81f7c4a 100644 --- a/apps/meteor/app/slashcommand-asciiarts/client/tableflip.ts +++ b/apps/meteor/app/slashcommand-asciiarts/client/tableflip.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { sdk } from '../../utils/client/lib/SDKClient'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; /* * Tableflip is a named function that will replace /Tableflip commands * @param {Object} message - The message object diff --git a/apps/meteor/app/slashcommand-asciiarts/client/unflip.ts b/apps/meteor/app/slashcommand-asciiarts/client/unflip.ts index a7dc0d257e78..6c02fa196052 100644 --- a/apps/meteor/app/slashcommand-asciiarts/client/unflip.ts +++ b/apps/meteor/app/slashcommand-asciiarts/client/unflip.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { sdk } from '../../utils/client/lib/SDKClient'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; /* * Unflip is a named function that will replace /unflip commands * @param {Object} message - The message object diff --git a/apps/meteor/app/slashcommand-asciiarts/server/gimme.ts b/apps/meteor/app/slashcommand-asciiarts/server/gimme.ts index f426d6cf85c0..f902d75f33d1 100644 --- a/apps/meteor/app/slashcommand-asciiarts/server/gimme.ts +++ b/apps/meteor/app/slashcommand-asciiarts/server/gimme.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { executeSendMessage } from '../../lib/server/methods/sendMessage'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Gimme is a named function that will replace /gimme commands * @param {Object} message - The message object diff --git a/apps/meteor/app/slashcommand-asciiarts/server/lenny.ts b/apps/meteor/app/slashcommand-asciiarts/server/lenny.ts index 878a10e356d4..e760b5a1169e 100644 --- a/apps/meteor/app/slashcommand-asciiarts/server/lenny.ts +++ b/apps/meteor/app/slashcommand-asciiarts/server/lenny.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { executeSendMessage } from '../../lib/server/methods/sendMessage'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Lenny is a named function that will replace /lenny commands * @param {Object} message - The message object diff --git a/apps/meteor/app/slashcommand-asciiarts/server/shrug.ts b/apps/meteor/app/slashcommand-asciiarts/server/shrug.ts index 1240027bb38f..c2e5d166bfd8 100644 --- a/apps/meteor/app/slashcommand-asciiarts/server/shrug.ts +++ b/apps/meteor/app/slashcommand-asciiarts/server/shrug.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { executeSendMessage } from '../../lib/server/methods/sendMessage'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Shrug is a named function that will replace /shrug commands * @param {Object} message - The message object diff --git a/apps/meteor/app/slashcommand-asciiarts/server/tableflip.ts b/apps/meteor/app/slashcommand-asciiarts/server/tableflip.ts index 34acef9805e2..ac3f599dff1d 100644 --- a/apps/meteor/app/slashcommand-asciiarts/server/tableflip.ts +++ b/apps/meteor/app/slashcommand-asciiarts/server/tableflip.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { executeSendMessage } from '../../lib/server/methods/sendMessage'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Tableflip is a named function that will replace /Tableflip commands * @param {Object} message - The message object diff --git a/apps/meteor/app/slashcommand-asciiarts/server/unflip.ts b/apps/meteor/app/slashcommand-asciiarts/server/unflip.ts index 689e7262eac0..b905ed567447 100644 --- a/apps/meteor/app/slashcommand-asciiarts/server/unflip.ts +++ b/apps/meteor/app/slashcommand-asciiarts/server/unflip.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { executeSendMessage } from '../../lib/server/methods/sendMessage'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Unflip is a named function that will replace /unflip commands * @param {Object} message - The message object diff --git a/apps/meteor/app/slashcommands-archiveroom/client/client.ts b/apps/meteor/app/slashcommands-archiveroom/client/client.ts index c24763106684..f5154fb32a5b 100644 --- a/apps/meteor/app/slashcommands-archiveroom/client/client.ts +++ b/apps/meteor/app/slashcommands-archiveroom/client/client.ts @@ -1,4 +1,4 @@ -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'archive', diff --git a/apps/meteor/app/slashcommands-archiveroom/server/server.ts b/apps/meteor/app/slashcommands-archiveroom/server/server.ts index 99bcec2cd7b3..f1b33c1022bb 100644 --- a/apps/meteor/app/slashcommands-archiveroom/server/server.ts +++ b/apps/meteor/app/slashcommands-archiveroom/server/server.ts @@ -10,7 +10,7 @@ import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { archiveRoom } from '../../lib/server/functions/archiveRoom'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; slashCommands.add({ command: 'archive', diff --git a/apps/meteor/app/slashcommands-create/client/client.ts b/apps/meteor/app/slashcommands-create/client/client.ts index 299db606db9c..7e8ba831dbd8 100644 --- a/apps/meteor/app/slashcommands-create/client/client.ts +++ b/apps/meteor/app/slashcommands-create/client/client.ts @@ -1,4 +1,4 @@ -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'create', diff --git a/apps/meteor/app/slashcommands-create/server/server.ts b/apps/meteor/app/slashcommands-create/server/server.ts index 104d50c56926..6abee71c56fd 100644 --- a/apps/meteor/app/slashcommands-create/server/server.ts +++ b/apps/meteor/app/slashcommands-create/server/server.ts @@ -6,7 +6,7 @@ import { i18n } from '../../../server/lib/i18n'; import { createChannelMethod } from '../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../lib/server/methods/createPrivateGroup'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; slashCommands.add({ command: 'create', diff --git a/apps/meteor/app/slashcommands-help/server/server.ts b/apps/meteor/app/slashcommands-help/server/server.ts index c24bfb22c6fe..80efaffeb852 100644 --- a/apps/meteor/app/slashcommands-help/server/server.ts +++ b/apps/meteor/app/slashcommands-help/server/server.ts @@ -4,7 +4,7 @@ import { Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Help is a named function that will replace /help commands diff --git a/apps/meteor/app/slashcommands-hide/client/hide.ts b/apps/meteor/app/slashcommands-hide/client/hide.ts index 99c1eaea7049..c6486053ecc2 100644 --- a/apps/meteor/app/slashcommands-hide/client/hide.ts +++ b/apps/meteor/app/slashcommands-hide/client/hide.ts @@ -1,4 +1,4 @@ -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'hide', diff --git a/apps/meteor/app/slashcommands-invite/client/client.ts b/apps/meteor/app/slashcommands-invite/client/client.ts index 729073b785d8..7c8af755d64d 100644 --- a/apps/meteor/app/slashcommands-invite/client/client.ts +++ b/apps/meteor/app/slashcommands-invite/client/client.ts @@ -1,4 +1,4 @@ -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'invite', diff --git a/apps/meteor/app/slashcommands-invite/server/server.ts b/apps/meteor/app/slashcommands-invite/server/server.ts index de525d8c6fc6..06a85301540c 100644 --- a/apps/meteor/app/slashcommands-invite/server/server.ts +++ b/apps/meteor/app/slashcommands-invite/server/server.ts @@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../server/lib/i18n'; import { addUsersToRoomMethod } from '../../lib/server/methods/addUsersToRoom'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Invite is a named function that will replace /invite commands diff --git a/apps/meteor/app/slashcommands-inviteall/client/client.ts b/apps/meteor/app/slashcommands-inviteall/client/client.ts index f8ab40953d27..5083cd4a83ab 100644 --- a/apps/meteor/app/slashcommands-inviteall/client/client.ts +++ b/apps/meteor/app/slashcommands-inviteall/client/client.ts @@ -1,4 +1,4 @@ -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'invite-all-to', diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts index bac4349ec72c..e74bb89899c2 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -15,7 +15,7 @@ import { addUsersToRoomMethod } from '../../lib/server/methods/addUsersToRoom'; import { createChannelMethod } from '../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../lib/server/methods/createPrivateGroup'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; function inviteAll(type: T): SlashCommand['callback'] { return async function inviteAll({ command, params, message, userId }: SlashCommandCallbackParams): Promise { diff --git a/apps/meteor/app/slashcommands-join/client/client.ts b/apps/meteor/app/slashcommands-join/client/client.ts index 417fe1e5cd47..bc8d589f51ac 100644 --- a/apps/meteor/app/slashcommands-join/client/client.ts +++ b/apps/meteor/app/slashcommands-join/client/client.ts @@ -1,6 +1,6 @@ import type { Meteor } from 'meteor/meteor'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'join', diff --git a/apps/meteor/app/slashcommands-join/server/server.ts b/apps/meteor/app/slashcommands-join/server/server.ts index 33d0278f81a3..6497324ae9e0 100644 --- a/apps/meteor/app/slashcommands-join/server/server.ts +++ b/apps/meteor/app/slashcommands-join/server/server.ts @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../server/lib/i18n'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; slashCommands.add({ command: 'join', diff --git a/apps/meteor/app/slashcommands-kick/client/client.ts b/apps/meteor/app/slashcommands-kick/client/client.ts index 475346216f1e..7fc167e17c88 100644 --- a/apps/meteor/app/slashcommands-kick/client/client.ts +++ b/apps/meteor/app/slashcommands-kick/client/client.ts @@ -1,6 +1,6 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'kick', diff --git a/apps/meteor/app/slashcommands-kick/server/server.ts b/apps/meteor/app/slashcommands-kick/server/server.ts index 5ca6b45ec835..fdde07b897bf 100644 --- a/apps/meteor/app/slashcommands-kick/server/server.ts +++ b/apps/meteor/app/slashcommands-kick/server/server.ts @@ -6,7 +6,7 @@ import { Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { removeUserFromRoomMethod } from '../../../server/methods/removeUserFromRoom'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; slashCommands.add({ command: 'kick', diff --git a/apps/meteor/app/slashcommands-leave/server/leave.ts b/apps/meteor/app/slashcommands-leave/server/leave.ts index 42dad0807246..fa108fe18c72 100644 --- a/apps/meteor/app/slashcommands-leave/server/leave.ts +++ b/apps/meteor/app/slashcommands-leave/server/leave.ts @@ -5,7 +5,7 @@ import { Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { leaveRoomMethod } from '../../lib/server/methods/leaveRoom'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Leave is a named function that will replace /leave commands diff --git a/apps/meteor/app/slashcommands-me/server/me.ts b/apps/meteor/app/slashcommands-me/server/me.ts index ba6a9f8c82cc..b8b4a593cb73 100644 --- a/apps/meteor/app/slashcommands-me/server/me.ts +++ b/apps/meteor/app/slashcommands-me/server/me.ts @@ -1,7 +1,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { executeSendMessage } from '../../lib/server/methods/sendMessage'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Me is a named function that will replace /me commands diff --git a/apps/meteor/app/slashcommands-msg/server/server.ts b/apps/meteor/app/slashcommands-msg/server/server.ts index c6a244b80207..e757938106eb 100644 --- a/apps/meteor/app/slashcommands-msg/server/server.ts +++ b/apps/meteor/app/slashcommands-msg/server/server.ts @@ -7,7 +7,7 @@ import { i18n } from '../../../server/lib/i18n'; import { createDirectMessage } from '../../../server/methods/createDirectMessage'; import { executeSendMessage } from '../../lib/server/methods/sendMessage'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Msg is a named function that will replace /msg commands diff --git a/apps/meteor/app/slashcommands-mute/server/mute.ts b/apps/meteor/app/slashcommands-mute/server/mute.ts index 03ce960496da..da20ff4fed47 100644 --- a/apps/meteor/app/slashcommands-mute/server/mute.ts +++ b/apps/meteor/app/slashcommands-mute/server/mute.ts @@ -5,7 +5,7 @@ import { Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { muteUserInRoom } from '../../../server/methods/muteUserInRoom'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Mute is a named function that will replace /mute commands diff --git a/apps/meteor/app/slashcommands-mute/server/unmute.ts b/apps/meteor/app/slashcommands-mute/server/unmute.ts index 25c0956d49e3..4dc683f4ca93 100644 --- a/apps/meteor/app/slashcommands-mute/server/unmute.ts +++ b/apps/meteor/app/slashcommands-mute/server/unmute.ts @@ -5,7 +5,7 @@ import { Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { unmuteUserInRoom } from '../../../server/methods/unmuteUserInRoom'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; /* * Unmute is a named function that will replace /unmute commands diff --git a/apps/meteor/app/slashcommands-open/client/client.ts b/apps/meteor/app/slashcommands-open/client/client.ts index 987df9599761..99438a24eeb0 100644 --- a/apps/meteor/app/slashcommands-open/client/client.ts +++ b/apps/meteor/app/slashcommands-open/client/client.ts @@ -5,7 +5,7 @@ import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; import { router } from '../../../client/providers/RouterProvider'; import { Subscriptions, ChatSubscription } from '../../models/client'; import { sdk } from '../../utils/client/lib/SDKClient'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'open', diff --git a/apps/meteor/app/slashcommands-status/client/status.ts b/apps/meteor/app/slashcommands-status/client/status.ts index 9136ef8f586f..3698b5fda4cb 100644 --- a/apps/meteor/app/slashcommands-status/client/status.ts +++ b/apps/meteor/app/slashcommands-status/client/status.ts @@ -2,7 +2,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { dispatchToastMessage } from '../../../client/lib/toast'; import { sdk } from '../../utils/client/lib/SDKClient'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'status', diff --git a/apps/meteor/app/slashcommands-status/server/status.ts b/apps/meteor/app/slashcommands-status/server/status.ts index 72d92afaf3f2..a2ff6483d398 100644 --- a/apps/meteor/app/slashcommands-status/server/status.ts +++ b/apps/meteor/app/slashcommands-status/server/status.ts @@ -5,7 +5,7 @@ import { Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { settings } from '../../settings/server'; import { setUserStatusMethod } from '../../user-status/server/methods/setUserStatus'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; slashCommands.add({ command: 'status', diff --git a/apps/meteor/app/slashcommands-topic/client/topic.ts b/apps/meteor/app/slashcommands-topic/client/topic.ts index f5f5ed58bb0f..f7e47c334b5a 100644 --- a/apps/meteor/app/slashcommands-topic/client/topic.ts +++ b/apps/meteor/app/slashcommands-topic/client/topic.ts @@ -5,7 +5,7 @@ import { callbacks } from '../../../lib/callbacks'; import { hasPermission } from '../../authorization/client'; import { ChatRoom } from '../../models/client/models/ChatRoom'; import { sdk } from '../../utils/client/lib/SDKClient'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'topic', diff --git a/apps/meteor/app/slashcommands-topic/server/topic.ts b/apps/meteor/app/slashcommands-topic/server/topic.ts index 24fd51d5f509..c1fa6ea283b7 100644 --- a/apps/meteor/app/slashcommands-topic/server/topic.ts +++ b/apps/meteor/app/slashcommands-topic/server/topic.ts @@ -2,7 +2,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { saveRoomSettings } from '../../channel-settings/server/methods/saveRoomSettings'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; slashCommands.add({ command: 'topic', diff --git a/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts b/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts index 2fed1e1c7802..7b65fc067031 100644 --- a/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts +++ b/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts @@ -1,4 +1,4 @@ -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ command: 'unarchive', diff --git a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts b/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts index d87981bd65a2..4c0c44269d2f 100644 --- a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts +++ b/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts @@ -10,7 +10,7 @@ import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { unarchiveRoom } from '../../lib/server/functions/unarchiveRoom'; import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/lib/slashCommand'; +import { slashCommands } from '../../utils/server/slashCommand'; slashCommands.add({ command: 'unarchive', diff --git a/apps/meteor/app/utils/client/index.ts b/apps/meteor/app/utils/client/index.ts index fd03ffc3d720..561a1116141b 100644 --- a/apps/meteor/app/utils/client/index.ts +++ b/apps/meteor/app/utils/client/index.ts @@ -2,6 +2,6 @@ export { Info } from '../rocketchat.info'; export { getUserPreference } from './lib/getUserPreference'; export { fileUploadIsValidContentType } from './restrictions'; export { getUserAvatarURL } from './getUserAvatarURL'; -export { slashCommands } from '../lib/slashCommand'; +export { slashCommands } from './slashCommand'; export { getURL } from './getURL'; export { APIClient } from './lib/RestApiClient'; diff --git a/apps/meteor/app/utils/lib/slashCommand.ts b/apps/meteor/app/utils/client/slashCommand.ts similarity index 85% rename from apps/meteor/app/utils/lib/slashCommand.ts rename to apps/meteor/app/utils/client/slashCommand.ts index 47149807bbd8..66e793012fac 100644 --- a/apps/meteor/app/utils/lib/slashCommand.ts +++ b/apps/meteor/app/utils/client/slashCommand.ts @@ -6,7 +6,8 @@ import type { SlashCommandPreviewItem, SlashCommandPreviews, } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; + +import { InvalidCommandUsage, InvalidPreview } from '../../../client/lib/errors'; interface ISlashCommandAddParams { command: string; @@ -69,7 +70,7 @@ export const slashCommands = { } if (!message?.rid) { - throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + throw new InvalidCommandUsage(); } return cmd.callback({ command, params, message, triggerId, userId }); @@ -85,7 +86,7 @@ export const slashCommands = { } if (!message?.rid) { - throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + throw new InvalidCommandUsage(); } const previewInfo = await cmd.previewer(command, params, message); @@ -114,12 +115,12 @@ export const slashCommands = { } if (!message?.rid) { - throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + throw new InvalidCommandUsage(); } // { id, type, value } if (!preview.id || !preview.type || !preview.value) { - throw new Meteor.Error('error-invalid-preview', 'Preview Item must have an id, type, and value.'); + throw new InvalidPreview(); } return cmd.previewCallback(command, params, message, preview, triggerId); diff --git a/apps/meteor/app/utils/server/slashCommand.ts b/apps/meteor/app/utils/server/slashCommand.ts index dc85fee9b671..27b3c81735f9 100644 --- a/apps/meteor/app/utils/server/slashCommand.ts +++ b/apps/meteor/app/utils/server/slashCommand.ts @@ -1,7 +1,139 @@ +import { MeteorError } from '@rocket.chat/core-services'; +import type { + IMessage, + SlashCommand, + SlashCommandOptions, + RequiredField, + SlashCommandPreviewItem, + SlashCommandPreviews, +} from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; -import { slashCommands } from '../lib/slashCommand'; +interface ISlashCommandAddParams { + command: string; + callback?: SlashCommand['callback']; + options?: SlashCommandOptions; + result?: SlashCommand['result']; + providesPreview?: boolean; + previewer?: SlashCommand['previewer']; + previewCallback?: SlashCommand['previewCallback']; + appId?: string; + description?: string; +} + +export const slashCommands = { + commands: {} as Record, + add({ + command, + callback, + options = {}, + result, + providesPreview = false, + previewer, + previewCallback, + appId, + description = '', + }: ISlashCommandAddParams): void { + if (this.commands[command]) { + return; + } + this.commands[command] = { + command, + callback, + params: options.params, + description: options.description || description, + permission: options.permission, + clientOnly: options.clientOnly || false, + result, + providesPreview: Boolean(providesPreview), + previewer, + previewCallback, + appId, + } as SlashCommand; + }, + async run({ + command, + message, + params, + triggerId, + userId, + }: { + command: string; + params: string; + message: RequiredField, 'rid' | '_id'>; + userId: string; + triggerId?: string | undefined; + }): Promise { + const cmd = this.commands[command]; + if (typeof cmd?.callback !== 'function') { + return; + } + + if (!message?.rid) { + throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + } + + return cmd.callback({ command, params, message, triggerId, userId }); + }, + async getPreviews( + command: string, + params: string, + message: RequiredField, 'rid'>, + ): Promise { + const cmd = this.commands[command]; + if (typeof cmd?.previewer !== 'function') { + return; + } + + if (!message?.rid) { + throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + } + + const previewInfo = await cmd.previewer(command, params, message); + + if (!previewInfo?.items?.length) { + return; + } + + // A limit of ten results, to save time and bandwidth + if (previewInfo.items.length >= 10) { + previewInfo.items = previewInfo.items.slice(0, 10); + } + + return previewInfo; + }, + async executePreview( + command: string, + params: string, + message: Pick & Partial>, + preview: SlashCommandPreviewItem, + triggerId?: string, + ) { + const cmd = this.commands[command]; + if (typeof cmd?.previewCallback !== 'function') { + return; + } + + if (!message?.rid) { + throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + } + + // { id, type, value } + if (!preview.id || !preview.type || !preview.value) { + throw new MeteorError('error-invalid-preview', 'Preview Item must have an id, type, and value.'); + } + + return cmd.previewCallback(command, params, message, preview, triggerId); + }, +}; + +declare module '@rocket.chat/ddp-client' { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface ServerMethods { + slashCommand(params: { cmd: string; params: string; msg: IMessage; triggerId: string }): unknown; + } +} Meteor.methods({ async slashCommand(command) { @@ -27,5 +159,3 @@ Meteor.methods({ }); }, }); - -export { slashCommands }; diff --git a/apps/meteor/client/hooks/useAppSlashCommands.ts b/apps/meteor/client/hooks/useAppSlashCommands.ts index c49c629a2a06..3a925cb24690 100644 --- a/apps/meteor/client/hooks/useAppSlashCommands.ts +++ b/apps/meteor/client/hooks/useAppSlashCommands.ts @@ -3,7 +3,7 @@ import { useEndpoint, useStream, useUserId } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; -import { slashCommands } from '../../app/utils/lib/slashCommand'; +import { slashCommands } from '../../app/utils/client/slashCommand'; export const useAppSlashCommands = () => { const queryClient = useQueryClient(); diff --git a/apps/meteor/client/lib/errors/InvalidCommandUsage.ts b/apps/meteor/client/lib/errors/InvalidCommandUsage.ts new file mode 100644 index 000000000000..66e240cf2804 --- /dev/null +++ b/apps/meteor/client/lib/errors/InvalidCommandUsage.ts @@ -0,0 +1,7 @@ +import { RocketChatError } from './RocketChatError'; + +export class InvalidCommandUsage extends RocketChatError<'invalid-command-usage'> { + constructor(message = 'Executing a command requires at least a message with a room id.', details?: string) { + super('invalid-command-usage', message, details); + } +} diff --git a/apps/meteor/client/lib/errors/InvalidPreview.ts b/apps/meteor/client/lib/errors/InvalidPreview.ts new file mode 100644 index 000000000000..2c56a74a88e4 --- /dev/null +++ b/apps/meteor/client/lib/errors/InvalidPreview.ts @@ -0,0 +1,7 @@ +import { RocketChatError } from './RocketChatError'; + +export class InvalidPreview extends RocketChatError<'error-invalid-preview'> { + constructor(message = 'Preview Item must have an id, type, and value.', details?: string) { + super('error-invalid-preview', message, details); + } +} diff --git a/apps/meteor/client/lib/errors/index.ts b/apps/meteor/client/lib/errors/index.ts new file mode 100644 index 000000000000..6c57c5f25da6 --- /dev/null +++ b/apps/meteor/client/lib/errors/index.ts @@ -0,0 +1,2 @@ +export * from './InvalidCommandUsage'; +export * from './InvalidPreview'; diff --git a/apps/meteor/client/startup/slashCommands/federation.ts b/apps/meteor/client/startup/slashCommands/federation.ts index 76f083c16468..25728ad4601a 100644 --- a/apps/meteor/client/startup/slashCommands/federation.ts +++ b/apps/meteor/client/startup/slashCommands/federation.ts @@ -1,4 +1,4 @@ -import { slashCommands } from '../../../app/utils/lib/slashCommand'; +import { slashCommands } from '../../../app/utils/client/slashCommand'; const callback = undefined; const result = undefined; diff --git a/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopupQueries.ts b/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopupQueries.ts index f5e0c7ca710c..492579f2738c 100644 --- a/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopupQueries.ts +++ b/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopupQueries.ts @@ -2,7 +2,7 @@ import type { QueriesResults } from '@tanstack/react-query'; import { useQueries } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; -import { slashCommands } from '../../../../../app/utils/lib/slashCommand'; +import { slashCommands } from '../../../../../app/utils/client/slashCommand'; import type { ComposerPopupOption } from '../../contexts/ComposerPopupContext'; import { useEnablePopupPreview } from './useEnablePopupPreview'; From c7bdb14b47f7c3fd5ae30c77d393befc9cd57954 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Thu, 15 Aug 2024 14:41:54 -0300 Subject: [PATCH 12/49] chore: promote roomUpdater to afterSaveMessage hook (#33034) Co-authored-by: Guilherme Gazzo --- .../app/autotranslate/server/autotranslate.ts | 4 +-- .../hooks/propagateDiscussionMetadata.ts | 2 +- .../server/methods/createDiscussion.ts | 5 +-- .../server/hooks/afterSaveMessage.js | 2 +- .../app/lib/server/functions/sendMessage.ts | 9 +++-- .../app/lib/server/functions/updateMessage.ts | 6 ++-- .../app/lib/server/lib/afterSaveMessage.ts | 35 +++++++++++++++++++ .../lib/server/lib/notifyUsersOnMessage.ts | 13 +++---- .../server/lib/sendNotificationsOnMessage.ts | 2 +- .../server/startup/mentionUserNotInChannel.ts | 2 +- .../hooks/afterSaveOmnichannelMessage.ts | 2 +- .../threads/server/hooks/aftersavemessage.ts | 2 +- .../client/hooks/useAnalyticsEventTracking.ts | 2 +- .../server/hooks/afterSaveMessage.ts | 3 +- .../lib/engagementDashboard/messages.ts | 2 +- apps/meteor/lib/callbacks.ts | 2 +- .../EmailInbox/EmailInbox_Outgoing.ts | 6 ++-- .../infrastructure/rocket-chat/hooks/index.ts | 4 +-- .../rocket-chat/hooks/hooks.spec.ts | 4 +-- 19 files changed, 72 insertions(+), 35 deletions(-) create mode 100644 apps/meteor/app/lib/server/lib/afterSaveMessage.ts diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index 1e6c224c4115..f3c6d9e55fdb 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -79,7 +79,7 @@ export class TranslationProviderRegistry { return null; } - return provider.translateMessage(message, room, targetLanguage); + return provider.translateMessage(message, { room, targetLanguage }); } static getProviders(): AutoTranslate[] { @@ -290,7 +290,7 @@ export abstract class AutoTranslate { * @param {object} targetLanguage * @returns {object} unmodified message object. */ - async translateMessage(message: IMessage, room: IRoom, targetLanguage?: string): Promise { + async translateMessage(message: IMessage, { room, targetLanguage }: { room: IRoom; targetLanguage?: string }): Promise { let targetLanguages: string[]; if (targetLanguage) { targetLanguages = [targetLanguage]; diff --git a/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts b/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts index 05cf2326156f..1ff9ed1dc1ba 100644 --- a/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts +++ b/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts @@ -22,7 +22,7 @@ const updateAndNotifyParentRoomWithParentMessage = async (room: IRoom): Promise< */ callbacks.add( 'afterSaveMessage', - async (message, { _id, prid }) => { + async (message, { room: { _id, prid } }) => { if (!prid) { return message; } diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts index 6e670d723ec9..7f18e5371a23 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -5,7 +5,6 @@ import { Messages, Rooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; @@ -14,6 +13,7 @@ import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; import { attachMessage } from '../../../lib/server/functions/attachMessage'; import { createRoom } from '../../../lib/server/functions/createRoom'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { afterSaveMessageAsync } from '../../../lib/server/lib/afterSaveMessage'; import { settings } from '../../../settings/server'; const getParentRoom = async (rid: IRoom['_id']) => { @@ -191,8 +191,9 @@ const create = async ({ } if (discussionMsg) { - callbacks.runAsync('afterSaveMessage', discussionMsg, parentRoom); + afterSaveMessageAsync(discussionMsg, parentRoom); } + return discussion; }; diff --git a/apps/meteor/app/federation/server/hooks/afterSaveMessage.js b/apps/meteor/app/federation/server/hooks/afterSaveMessage.js index 7f67f4770686..20c64f87dda8 100644 --- a/apps/meteor/app/federation/server/hooks/afterSaveMessage.js +++ b/apps/meteor/app/federation/server/hooks/afterSaveMessage.js @@ -6,7 +6,7 @@ import { getFederationDomain } from '../lib/getFederationDomain'; import { clientLogger } from '../lib/logger'; import { normalizers } from '../normalizers'; -async function afterSaveMessage(message, room) { +async function afterSaveMessage(message, { room }) { // If there are not federated users on this room, ignore it if (!hasExternalDomain(room)) { return message; diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 4a5b8313ebcd..aba5ddb7264c 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -4,12 +4,12 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; -import { callbacks } from '../../../../lib/callbacks'; import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; +import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; @@ -289,11 +289,10 @@ export const sendMessage = async function (user: any, message: any, room: any, u void Apps.getBridges()?.getListenerBridge().messageEvent('IPostMessageSent', message); } - await callbacks.run('afterSaveMessage', message, room); + // TODO: is there an opportunity to send returned data to notifyOnMessageChange? + await afterSaveMessage(message, room); - void notifyOnMessageChange({ - id: message._id, - }); + void notifyOnMessageChange({ id: message._id }); void notifyOnRoomChangedById(message.rid); diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index b0f2acd1f4ee..96683d40348f 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -4,8 +4,8 @@ import type { IMessage, IUser, AtLeast } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; +import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; @@ -99,11 +99,11 @@ export const updateMessage = async function ( // although this is an "afterSave" kind callback, we know they can extend message's properties // so we wait for it to run before broadcasting - const data = await callbacks.run('afterSaveMessage', msg, room, user._id); + const data = await afterSaveMessage(msg, room, user._id); void notifyOnMessageChange({ id: msg._id, - data: data as any, // TODO move "afterSaveMessage" type definition to specify a return value + data, }); if (room?.lastMessage?._id === msg._id) { diff --git a/apps/meteor/app/lib/server/lib/afterSaveMessage.ts b/apps/meteor/app/lib/server/lib/afterSaveMessage.ts new file mode 100644 index 000000000000..5b6e12b1e185 --- /dev/null +++ b/apps/meteor/app/lib/server/lib/afterSaveMessage.ts @@ -0,0 +1,35 @@ +import type { IMessage, IUser, IRoom } from '@rocket.chat/core-typings'; +import type { Updater } from '@rocket.chat/models'; +import { Rooms } from '@rocket.chat/models'; + +import { callbacks } from '../../../../lib/callbacks'; + +export async function afterSaveMessage( + message: IMessage, + room: IRoom, + uid?: IUser['_id'], + roomUpdater?: Updater, +): Promise { + const updater = roomUpdater ?? Rooms.getUpdater(); + const data = await callbacks.run('afterSaveMessage', message, { room, uid, roomUpdater: updater }); + + if (!roomUpdater && updater.hasChanges()) { + await Rooms.updateFromUpdater({ _id: room._id }, updater); + } + + // TODO: Fix type - callback configuration needs to be updated + return data as unknown as IMessage; +} + +export function afterSaveMessageAsync( + message: IMessage, + room: IRoom, + uid?: IUser['_id'], + roomUpdater: Updater = Rooms.getUpdater(), +): void { + callbacks.runAsync('afterSaveMessage', message, { room, uid, roomUpdater }); + + if (roomUpdater.hasChanges()) { + void Rooms.updateFromUpdater({ _id: room._id }, roomUpdater); + } +} diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts index a05c05b4bb94..990a1f2e4029 100644 --- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts @@ -143,6 +143,8 @@ export async function updateThreadUsersSubscriptions(message: IMessage, replies: } export async function notifyUsersOnMessage(message: IMessage, room: IRoom, roomUpdater: Updater): Promise { + console.log('notifyUsersOnMessage function'); + // Skips this callback if the message was edited and increments it if the edit was way in the past (aka imported) if (isEditedMessage(message)) { if (Math.abs(moment(message.editedAt).diff(Date.now())) > 60000) { @@ -183,14 +185,13 @@ export async function notifyUsersOnMessage(message: IMessage, room: IRoom, roomU callbacks.add( 'afterSaveMessage', - async (message, room) => { - const roomUpdater = Rooms.getUpdater(); - await notifyUsersOnMessage(message, room, roomUpdater); - - if (roomUpdater.hasChanges()) { - await Rooms.updateFromUpdater({ _id: room._id }, roomUpdater); + async (message, { room, roomUpdater }) => { + if (!roomUpdater) { + return message; } + await notifyUsersOnMessage(message, room, roomUpdater); + return message; }, callbacks.priority.MEDIUM, diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts index 49fcc0ea4725..94c25f476222 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts @@ -406,7 +406,7 @@ settings.watch('Troubleshoot_Disable_Notifications', (value) => { callbacks.add( 'afterSaveMessage', - (message, room) => sendAllNotifications(message, room), + (message, { room }) => sendAllNotifications(message, room), callbacks.priority.LOW, 'sendNotificationsOnMessage', ); diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index 962691a78bd8..8a17686ba158 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -54,7 +54,7 @@ const getBlocks = (mentions: IMessage['mentions'], messageId: string, lng: strin callbacks.add( 'afterSaveMessage', - async (message, room) => { + async (message, { room }) => { // TODO: check if I need to test this 60 second rule. // If the message was edited, or is older than 60 seconds (imported) // the notifications will be skipped, so we can also skip this validation diff --git a/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts b/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts index 07ce7fe08573..311343c4ad01 100644 --- a/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts +++ b/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts @@ -5,7 +5,7 @@ import { callbacks } from '../../../../lib/callbacks'; callbacks.add( 'afterSaveMessage', - async (message, room) => { + async (message, { room }) => { if (!isOmnichannelRoom(room)) { return message; } diff --git a/apps/meteor/app/threads/server/hooks/aftersavemessage.ts b/apps/meteor/app/threads/server/hooks/aftersavemessage.ts index 179cb5ec12b7..a938dadddb27 100644 --- a/apps/meteor/app/threads/server/hooks/aftersavemessage.ts +++ b/apps/meteor/app/threads/server/hooks/aftersavemessage.ts @@ -77,7 +77,7 @@ Meteor.startup(() => { } callbacks.add( 'afterSaveMessage', - async (message, room) => { + async (message, { room }) => { return processThreads(message, room); }, callbacks.priority.LOW, diff --git a/apps/meteor/client/hooks/useAnalyticsEventTracking.ts b/apps/meteor/client/hooks/useAnalyticsEventTracking.ts index 78e078ef0070..9d1acf7b4318 100644 --- a/apps/meteor/client/hooks/useAnalyticsEventTracking.ts +++ b/apps/meteor/client/hooks/useAnalyticsEventTracking.ts @@ -55,7 +55,7 @@ export const useAnalyticsEventTracking = () => { callbacks.add( 'afterSaveMessage', - (_message, room, _uid) => { + (_message, { room }) => { trackEvent('Message', 'Send', `${room.name} (${room._id})`); }, callbacks.priority.LOW, diff --git a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts index 5b7a720ba312..9180632768af 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts @@ -1,4 +1,3 @@ -import type { IRoom, IMessage } from '@rocket.chat/core-typings'; import { isEditedMessage, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Subscriptions } from '@rocket.chat/models'; @@ -7,7 +6,7 @@ import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadRec callbacks.add( 'afterSaveMessage', - async (message: IMessage, room: IRoom) => { + async (message, { room }) => { // skips this callback if the message was edited if (isEditedMessage(message)) { return message; diff --git a/apps/meteor/ee/server/lib/engagementDashboard/messages.ts b/apps/meteor/ee/server/lib/engagementDashboard/messages.ts index 19939ae6e4e1..2a4bf67c12c5 100644 --- a/apps/meteor/ee/server/lib/engagementDashboard/messages.ts +++ b/apps/meteor/ee/server/lib/engagementDashboard/messages.ts @@ -5,7 +5,7 @@ import moment from 'moment'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { convertDateToInt, diffBetweenDaysInclusive, convertIntToDate, getTotalOfWeekItems } from './date'; -export const handleMessagesSent = async (message: IMessage, room?: IRoom): Promise => { +export const handleMessagesSent = async (message: IMessage, { room }: { room?: IRoom }): Promise => { const roomTypesToShow = roomCoordinator.getTypesToShowOnDashboard(); if (!room || !roomTypesToShow.includes(room.t)) { return message; diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index eb8e032804f7..7eaa9ed7595d 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -50,7 +50,7 @@ interface EventLikeCallbackSignatures { 'afterDeleteUser': (user: IUser) => void; 'afterFileUpload': (params: { user: IUser; room: IRoom; message: IMessage }) => void; 'afterRoomNameChange': (params: { rid: string; name: string; oldName: string }) => void; - 'afterSaveMessage': (message: IMessage, room: IRoom, uid?: string) => void; + 'afterSaveMessage': (message: IMessage, params: { room: IRoom; uid?: string; roomUpdater?: Updater }) => void; 'afterOmnichannelSaveMessage': (message: IMessage, constant: { room: IOmnichannelRoom; roomUpdater: Updater }) => void; 'livechat.removeAgentDepartment': (params: { departmentId: ILivechatDepartmentRecord['_id']; agentsId: ILivechatAgent['_id'][] }) => void; 'livechat.saveAgentDepartment': (params: { departmentId: ILivechatDepartmentRecord['_id']; agentsId: ILivechatAgent['_id'][] }) => void; diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts index 80be176ada35..51718e4937d8 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts @@ -1,5 +1,5 @@ import { isIMessageInbox } from '@rocket.chat/core-typings'; -import type { IEmailInbox, IUser, IMessage, IOmnichannelRoom, SlashCommandCallbackParams } from '@rocket.chat/core-typings'; +import type { IEmailInbox, IUser, IOmnichannelRoom, SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { Messages, Uploads, LivechatRooms, Rooms, Users } from '@rocket.chat/models'; import { Match } from 'meteor/check'; import type Mail from 'nodemailer/lib/mailer'; @@ -190,7 +190,9 @@ slashCommands.add({ callbacks.add( 'afterSaveMessage', - async (message: IMessage, room: any) => { + async (message, { room: omnichannelRoom }) => { + const room = omnichannelRoom as IOmnichannelRoom; + if (!room?.email?.inbox) { return message; } diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts index 950aac23a39a..f14257512b11 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts @@ -150,7 +150,7 @@ export class FederationHooks { public static afterMessageUpdated(callback: (message: IMessage, roomId: IRoom['_id'], userId: string) => Promise): void { callbacks.add( 'afterSaveMessage', - async (message: IMessage, room: IRoom): Promise => { + async (message, { room }): Promise => { if ( !room || !isRoomFederated(room) || @@ -174,7 +174,7 @@ export class FederationHooks { public static afterMessageSent(callback: (message: IMessage, roomId: IRoom['_id'], userId: string) => Promise): void { callbacks.add( 'afterSaveMessage', - async (message: IMessage, room: IRoom): Promise => { + async (message, { room }): Promise => { if (!room || !isRoomFederated(room) || !message || !settings.get('Federation_Matrix_enabled')) { return message; } diff --git a/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts b/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts index 7d3e664022c8..c77f6e4993fa 100644 --- a/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts +++ b/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts @@ -507,7 +507,7 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageUpdated(stub); - hooks['federation-v2-after-room-message-updated'](message, { federated: true, _id: 'roomId' }); + hooks['federation-v2-after-room-message-updated'](message, { room: { federated: true, _id: 'roomId' } }); expect(stub.calledWith(message, 'roomId', 'userId')).to.be.true; }); }); @@ -551,7 +551,7 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageSent(stub); - hooks['federation-v2-after-room-message-sent']({ u: { _id: 'userId' } }, { federated: true, _id: 'roomId' }); + hooks['federation-v2-after-room-message-sent']({ u: { _id: 'userId' } }, { room: { federated: true, _id: 'roomId' } }); expect(stub.calledWith({ u: { _id: 'userId' } }, 'roomId', 'userId')).to.be.true; }); }); From 95178e09171469d2740f7fa076e7b58ad030c927 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 15 Aug 2024 15:25:54 -0300 Subject: [PATCH 13/49] fix: File uploads should only be allowed for room members (#32940) --- .changeset/gorgeous-hotels-attend.md | 5 ++++ .../body/hooks/useFileUploadDropTarget.ts | 7 ++--- apps/meteor/tests/e2e/file-upload.spec.ts | 26 ++++++++++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 .changeset/gorgeous-hotels-attend.md diff --git a/.changeset/gorgeous-hotels-attend.md b/.changeset/gorgeous-hotels-attend.md new file mode 100644 index 000000000000..fd858d7ace86 --- /dev/null +++ b/.changeset/gorgeous-hotels-attend.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Stopped non channel members from dragging and dropping files in a channel they do not belong diff --git a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts index 314eb64304b5..b97c0ad0866c 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts @@ -8,7 +8,7 @@ import { useIsRoomOverMacLimit } from '../../../../hooks/omnichannel/useIsRoomOv import { useReactiveValue } from '../../../../hooks/useReactiveValue'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; import { useChat } from '../../contexts/ChatContext'; -import { useRoom } from '../../contexts/RoomContext'; +import { useRoom, useRoomSubscription } from '../../contexts/RoomContext'; import { useDropTarget } from './useDropTarget'; export const useFileUploadDropTarget = (): readonly [ @@ -36,6 +36,7 @@ export const useFileUploadDropTarget = (): readonly [ ); const chat = useChat(); + const subscription = useRoomSubscription(); const onFileDrop = useMutableCallback(async (files: File[]) => { const { getMimeType } = await import('../../../../../app/utils/lib/mimeTypes'); @@ -70,7 +71,7 @@ export const useFileUploadDropTarget = (): readonly [ } as const; } - if (!fileUploadAllowedForUser) { + if (!fileUploadAllowedForUser || !subscription) { return { enabled: false, reason: t('error-not-allowed'), @@ -83,7 +84,7 @@ export const useFileUploadDropTarget = (): readonly [ onFileDrop, ...overlayProps, } as const; - }, [fileUploadAllowedForUser, fileUploadEnabled, isRoomOverMacLimit, onFileDrop, overlayProps, t]); + }, [fileUploadAllowedForUser, fileUploadEnabled, isRoomOverMacLimit, onFileDrop, overlayProps, subscription, t]); return [triggerProps, allOverlayProps] as const; }; diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index 0a5d1cfd2512..159b2650ac16 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -12,7 +12,7 @@ test.describe.serial('file-upload', () => { test.beforeAll(async ({ api }) => { await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml'); - targetChannel = await createTargetChannel(api); + targetChannel = await createTargetChannel(api, { members: ['user1'] }); }); test.beforeEach(async ({ page }) => { @@ -76,3 +76,27 @@ test.describe.serial('file-upload', () => { await expect(poHomeChannel.content.btnModalConfirm).not.toBeVisible(); }); }); +test.describe('file-upload-not-member', () => { + let poHomeChannel: HomeChannel; + let targetChannel: string; + + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + + await page.goto('/home'); + await poHomeChannel.sidenav.openChat(targetChannel); + }); + + test.afterAll(async ({ api }) => { + expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200); + }); + + test('expect not be able to upload if not a member', async () => { + await poHomeChannel.content.dragAndDropTxtFile(); + await expect(poHomeChannel.content.modalFilePreview).not.toBeVisible(); + }); +}); From 320485db2d461ad8aedd703d3c045463ac094908 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:11:00 -0400 Subject: [PATCH 14/49] chore(deps): bump actions/setup-node from 3.7.0 to 4.0.3 (#32965) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/update-version-durability.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml index e52b4870b369..90c835577dc1 100644 --- a/.github/workflows/update-version-durability.yml +++ b/.github/workflows/update-version-durability.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3.7.0 + uses: actions/setup-node@v4.0.3 with: node-version: '20.15.1' From 0b2af2bccec0f1290d25c272f185c02eff07620d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:12:33 -0400 Subject: [PATCH 15/49] chore(deps): bump thehanimo/pr-title-checker from 1.3.7 to 1.4.2 (#31704) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guilherme Gazzo --- .github/workflows/pr-title-checker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml index bc9d1f042d58..d8f6db97c455 100644 --- a/.github/workflows/pr-title-checker.yml +++ b/.github/workflows/pr-title-checker.yml @@ -12,6 +12,6 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: thehanimo/pr-title-checker@v1.4.1 + - uses: thehanimo/pr-title-checker@v1.4.2 with: GITHUB_TOKEN: ${{ secrets.RC_TITLE_CHECKER }} From 760b5aaa589ef8661f7c2fc445f5209ac230cb6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:13:44 -0400 Subject: [PATCH 16/49] chore(deps): bump github/codeql-action from 2 to 3 (#32964) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guilherme Gazzo --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 483b404a6dc8..202a02dd7785 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,7 +26,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 # Override language selection by uncommenting this and choosing your languages with: languages: javascript @@ -34,7 +34,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -48,4 +48,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 443eda1e453f49299f801242a671c62cb4df334b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:15:41 -0400 Subject: [PATCH 17/49] chore(deps): bump docker/login-action from 2 to 3 (#30378) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66cf1afcccfb..246c34423bb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -576,13 +576,13 @@ jobs: steps: - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASS }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ secrets.CR_USER }} @@ -683,13 +683,13 @@ jobs: steps: - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASS }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ secrets.CR_USER }} From 17f3d5e96ae53a45307176cc5f20a6e2870c0f08 Mon Sep 17 00:00:00 2001 From: csuadev <72958726+csuadev@users.noreply.github.com> Date: Thu, 15 Aug 2024 22:01:43 +0200 Subject: [PATCH 18/49] fix: Missing department names on OC edit agent view (#33033) --- .changeset/fast-lobsters-turn.md | 5 ++++ .../views/omnichannel/agents/AgentEdit.tsx | 17 +++++++++++--- .../omnichannel/omnichannel-agents.spec.ts | 23 +++++++++++++++++++ .../e2e/page-objects/omnichannel-agents.ts | 4 ++++ 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 .changeset/fast-lobsters-turn.md diff --git a/.changeset/fast-lobsters-turn.md b/.changeset/fast-lobsters-turn.md new file mode 100644 index 000000000000..ff1d97ea7289 --- /dev/null +++ b/.changeset/fast-lobsters-turn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue due to an endpoint pagination that was causing that when an agent have assigned more than 50 departments, the departments have a blank space instead of the name. diff --git a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx index 7b854b0f36c3..9e114b7a0c64 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx @@ -34,7 +34,7 @@ import { MaxChatsPerAgent } from '../additionalForms'; type AgentEditProps = { agentData: Pick; - userDepartments: Pick[]; + userDepartments: (Pick & { departmentName: string })[]; availableDepartments: Pick[]; }; @@ -50,15 +50,26 @@ const AgentEdit = ({ agentData, userDepartments, availableDepartments }: AgentEd const email = getUserEmailAddress(agentData); + const departments: Pick[] = useMemo(() => { + const pending = userDepartments + .filter(({ departmentId }) => !availableDepartments.find((dep) => dep._id === departmentId)) + .map((dep) => ({ + _id: dep.departmentId, + name: dep.departmentName, + })); + + return [...availableDepartments, ...pending]; + }, [availableDepartments, userDepartments]); + const departmentsOptions: SelectOption[] = useMemo(() => { const archivedDepartment = (name: string, archived?: boolean) => (archived ? `${name} [${t('Archived')}]` : name); return ( - availableDepartments.map(({ _id, name, archived }) => + departments.map(({ _id, name, archived }) => name ? [_id, archivedDepartment(name, archived)] : [_id, archivedDepartment(_id, archived)], ) || [] ); - }, [availableDepartments, t]); + }, [departments, t]); const statusOptions: SelectOption[] = useMemo( () => [ diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts index ad4657b1841c..239978928126 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts @@ -105,4 +105,27 @@ test.describe.serial('OC - Manage Agents', () => { await poOmnichannelAgents.btnSave.click(); }); }); + + test('OC - Edit agent - Manage departments', async ({ page }) => { + await poOmnichannelAgents.selectUsername('user1'); + await poOmnichannelAgents.btnAdd.click(); + await poOmnichannelAgents.inputSearch.fill('user1'); + await poOmnichannelAgents.findRowByUsername('user1').click(); + + await poOmnichannelAgents.btnEdit.click(); + await poOmnichannelAgents.selectDepartment(department.data.name); + await poOmnichannelAgents.btnSave.click(); + + await test.step('expect the selected department is visible', async () => { + await poOmnichannelAgents.findRowByUsername('user1').click(); + + // mock the endpoint to use the one without pagination + await page.route('/api/v1/livechat/department?showArchived=true', async (route) => { + await route.fulfill({ json: { departments: [] } }); + }); + + await poOmnichannelAgents.btnEdit.click(); + await expect(poOmnichannelAgents.findSelectedDepartment(department.data.name)).toBeVisible(); + }); + }); }); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts index d588e409423f..4bde20c1da20 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts @@ -93,4 +93,8 @@ export class OmnichannelAgents { findRowByName(name: string) { return this.page.locator('tr', { has: this.page.locator(`td >> text="${name}"`) }); } + + findSelectedDepartment(name: string) { + return this.page.locator(`role=option[name="${name}"]`); + } } From 90486928e584f5bb7f3ecb135b116e58c8a88854 Mon Sep 17 00:00:00 2001 From: "Julio A." <52619625+julio-cfa@users.noreply.github.com> Date: Fri, 16 Aug 2024 02:31:14 +0200 Subject: [PATCH 19/49] chore: change 'Accounts_AvatarBlockUnauthenticatedAccess' default value from false to true (#33035) --- .../server/routes/avatar/middlewares/auth.js | 15 ++++++++++++--- apps/meteor/server/settings/accounts.ts | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/meteor/server/routes/avatar/middlewares/auth.js b/apps/meteor/server/routes/avatar/middlewares/auth.js index 40eb072d405c..5a4ead7ed048 100644 --- a/apps/meteor/server/routes/avatar/middlewares/auth.js +++ b/apps/meteor/server/routes/avatar/middlewares/auth.js @@ -1,11 +1,20 @@ -import { userCanAccessAvatar } from '../utils'; +import { userCanAccessAvatar, renderSVGLetters } from '../utils'; // protect all avatar endpoints export const protectAvatars = async (req, res, next) => { if (!(await userCanAccessAvatar(req))) { - res.writeHead(403); - res.write('Forbidden'); + let roomOrUsername; + + if (req.url.startsWith('/room')) { + roomOrUsername = req.url.split('/')[2] || 'Room'; + } else { + roomOrUsername = req.url.split('/')[1] || 'Anonymous'; + } + + res.writeHead(200, { 'Content-Type': 'image/svg+xml' }); + res.write(renderSVGLetters(roomOrUsername, 200)); res.end(); + return; } diff --git a/apps/meteor/server/settings/accounts.ts b/apps/meteor/server/settings/accounts.ts index 39e4183dbf5f..a744c47b2a41 100644 --- a/apps/meteor/server/settings/accounts.ts +++ b/apps/meteor/server/settings/accounts.ts @@ -760,7 +760,7 @@ export const createAccountSettings = () => i18nDescription: 'Accounts_AvatarCacheTime_description', }); - await this.add('Accounts_AvatarBlockUnauthenticatedAccess', false, { + await this.add('Accounts_AvatarBlockUnauthenticatedAccess', true, { type: 'boolean', public: true, }); From c8dac9fa3ba9fa89da9b417a6b956f5e4a612aa6 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Fri, 16 Aug 2024 12:40:30 -0300 Subject: [PATCH 20/49] fix: Realtime Monitoring LineCharts not updating (#33023) --- .../RealTimeMonitoringPage.js | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js index 5b4d837d211c..b6e29530b5e7 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js @@ -18,11 +18,19 @@ import ChatsOverview from './overviews/ChatsOverview'; import ConversationOverview from './overviews/ConversationOverview'; import ProductivityOverview from './overviews/ProductivityOverview'; +const randomizeKeys = (keys) => { + keys.current = keys.current.map((_key, i) => { + return `${i}_${new Date().getTime()}`; + }); +}; + const dateRange = getDateRange(); const RealTimeMonitoringPage = () => { const t = useTranslation(); + const keys = useRef([...Array(10).keys()]); + const [reloadFrequency, setReloadFrequency] = useState(5); const [departmentId, setDepartment] = useState(''); @@ -43,6 +51,10 @@ const RealTimeMonitoringPage = () => { [departmentParams], ); + useEffect(() => { + randomizeKeys(keys); + }, [allParams]); + const reloadCharts = useMutableCallback(() => { Object.values(reloadRef.current).forEach((reload) => { reload(); @@ -53,6 +65,7 @@ const RealTimeMonitoringPage = () => { const interval = setInterval(reloadCharts, reloadFrequency * 1000); return () => { clearInterval(interval); + randomizeKeys(keys); }; }, [reloadCharts, reloadFrequency]); @@ -90,30 +103,54 @@ const RealTimeMonitoringPage = () => { - + - - + + - + - - + + - + - + - + - + From 683b55b9e2ec4a4d0bc6ee66d92dc515158e9078 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 16 Aug 2024 12:03:22 -0600 Subject: [PATCH 21/49] fix: Avoid `processRoomAbandonment` callback from erroring when Business Hours config is missing for day (#33058) --- .changeset/gentle-bugs-think.md | 5 +++++ .../app/livechat/server/hooks/processRoomAbandonment.ts | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/gentle-bugs-think.md diff --git a/.changeset/gentle-bugs-think.md b/.changeset/gentle-bugs-think.md new file mode 100644 index 000000000000..fc4738f3043a --- /dev/null +++ b/.changeset/gentle-bugs-think.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Prevent `processRoomAbandonment` callback from erroring out when a room was inactive during a day Business Hours was not configured for. diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts index 8a5a4c280670..8eb53fbb8fa7 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts @@ -43,7 +43,8 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas officeDays = (await businessHourManager.getBusinessHour())?.workHours.reduce(parseDays, {}); } - if (!officeDays) { + // Empty object we assume invalid config + if (!officeDays || !Object.keys(officeDays).length) { return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); } @@ -55,6 +56,11 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas for (let index = 0; index <= daysOfInactivity; index++) { const today = inactivityDay.clone().format('dddd'); const officeDay = officeDays[today]; + // Config doesnt have data for this day, we skip day + if (!officeDay) { + inactivityDay.add(1, 'days'); + continue; + } const startTodaysOfficeHour = moment(`${officeDay.start.day}:${officeDay.start.time}`, 'dddd:HH:mm').add(index, 'days'); const endTodaysOfficeHour = moment(`${officeDay.finish.day}:${officeDay.finish.time}`, 'dddd:HH:mm').add(index, 'days'); if (officeDays[today].open) { From bbdff10c5980368014f29dd95bc95c27c47f6d2a Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Fri, 16 Aug 2024 18:22:56 -0300 Subject: [PATCH 22/49] refactor(Livechat): transcript.js to TS (#32087) --- .../src/lib/{transcript.js => transcript.ts} | 21 ++++++++++++++----- packages/livechat/src/store/index.tsx | 1 + 2 files changed, 17 insertions(+), 5 deletions(-) rename packages/livechat/src/lib/{transcript.js => transcript.ts} (68%) diff --git a/packages/livechat/src/lib/transcript.js b/packages/livechat/src/lib/transcript.ts similarity index 68% rename from packages/livechat/src/lib/transcript.js rename to packages/livechat/src/lib/transcript.ts index 970aab2ee9bc..33260edd62e8 100644 --- a/packages/livechat/src/lib/transcript.js +++ b/packages/livechat/src/lib/transcript.ts @@ -9,9 +9,19 @@ const promptTranscript = async () => { config: { messages: { transcriptMessage }, }, - user: { token, visitorEmails }, - room: { _id }, + user, + room, } = store.state; + + if (!room || !user) { + console.warn('Only call promptTranscript when there is a room and a user'); + return; + } + + const { visitorEmails } = user; + + const { _id } = room; + const email = visitorEmails && visitorEmails.length > 0 ? visitorEmails[0].address : ''; if (!email) { return; @@ -23,12 +33,12 @@ const promptTranscript = async () => { text: message, }).then((result) => { if (typeof result.success === 'boolean' && result.success) { - return Livechat.requestTranscript(email, { token, rid: _id }); + return Livechat.requestTranscript(email, { rid: _id }); } }); }; -const transcriptSentAlert = (message) => +const transcriptSentAlert = (message: string) => ModalManager.alert({ text: message, timeout: 1000, @@ -45,7 +55,8 @@ export const handleTranscript = async () => { const result = await promptTranscript(); - if (result && result.success) { + // TODO: Check why the api results are not returning the correct type + if ((result as { message: string; success: boolean })?.success) { transcriptSentAlert(i18next.t('transcript_success')); } }; diff --git a/packages/livechat/src/store/index.tsx b/packages/livechat/src/store/index.tsx index abc05f7101a9..f8629ce693cc 100644 --- a/packages/livechat/src/store/index.tsx +++ b/packages/livechat/src/store/index.tsx @@ -58,6 +58,7 @@ export type StoreState = { hiddenSystemMessages?: LivechatHiddenSytemMessageType[]; hideWatermark?: boolean; livechatLogo?: { url: string }; + transcript?: boolean; }; online?: boolean; departments: Department[]; From 81629860557f051f7ed1d6d560f8885e587d9c9e Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 19 Aug 2024 10:32:30 -0300 Subject: [PATCH 23/49] refactor: move system message calls from sendMessage to saveSystemMessage (#32842) Co-authored-by: Guilherme Gazzo --- .../server/methods/createDiscussion.ts | 6 +- .../lib/server/lib/notifyUsersOnMessage.ts | 14 ++++ .../app/livechat/server/api/v1/pageVisited.ts | 9 +-- apps/meteor/app/livechat/server/lib/Helper.ts | 6 +- .../app/livechat/server/lib/LivechatTyped.ts | 64 ++++++------------- .../app/message-pin/server/pinMessage.ts | 4 +- .../server/services/messages/service.ts | 42 ++++++++++-- .../src/types/IMessageService.ts | 9 ++- 8 files changed, 84 insertions(+), 70 deletions(-) diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts index 7f18e5371a23..96e0bd846390 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -27,13 +27,11 @@ async function createDiscussionMessage( drid: IRoom['_id'], msg: IMessage['msg'], messageEmbedded?: MessageAttachmentDefault, -): Promise { - const msgId = await Message.saveSystemMessage('discussion-created', rid, msg, user, { +): Promise { + return Message.saveSystemMessage('discussion-created', rid, msg, user, { drid, ...(messageEmbedded && { attachments: [messageEmbedded] }), }); - - return Messages.findOneById(msgId); } async function mentionMessage( diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts index 990a1f2e4029..85f2ac52b702 100644 --- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts @@ -183,6 +183,20 @@ export async function notifyUsersOnMessage(message: IMessage, room: IRoom, roomU return message; } +export async function notifyUsersOnSystemMessage(message: IMessage, room: IRoom): Promise { + const roomUpdater = Rooms.getUpdater(); + Rooms.setIncMsgCountAndSetLastMessageUpdateQuery(1, message, !!settings.get('Store_Last_Message'), roomUpdater); + + if (roomUpdater.hasChanges()) { + await Rooms.updateFromUpdater({ _id: room._id }, roomUpdater); + } + + // TODO: Rewrite to use just needed calls from the function + await updateUsersSubscriptions(message, room); + + return message; +} + callbacks.add( 'afterSaveMessage', async (message, { room, roomUpdater }) => { diff --git a/apps/meteor/app/livechat/server/api/v1/pageVisited.ts b/apps/meteor/app/livechat/server/api/v1/pageVisited.ts index e89a3e17f0a1..2688ad673af0 100644 --- a/apps/meteor/app/livechat/server/api/v1/pageVisited.ts +++ b/apps/meteor/app/livechat/server/api/v1/pageVisited.ts @@ -1,5 +1,4 @@ import type { IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; -import { Messages } from '@rocket.chat/models'; import { isPOSTLivechatPageVisitedParams } from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; @@ -11,17 +10,13 @@ API.v1.addRoute( { async post() { const { token, rid, pageInfo } = this.bodyParams; - const msgId = await Livechat.savePageHistory(token, rid, pageInfo); - if (!msgId) { - return API.v1.success(); - } - const message = await Messages.findOneById(msgId); + const message = await Livechat.savePageHistory(token, rid, pageInfo); if (!message) { return API.v1.success(); } - const { msg, navigation } = message; + const { msg, navigation } = message as IOmnichannelSystemMessage; return API.v1.success({ page: { msg, navigation } }); }, }, diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index c0e85a8c7c2b..1ef572df3068 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -36,7 +36,6 @@ import { validateEmail as validatorFunc } from '../../../../lib/emailValidator'; import { i18n } from '../../../../server/lib/i18n'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { sendNotification } from '../../../lib/server'; -import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { notifyOnLivechatDepartmentAgentChanged, notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId, @@ -141,10 +140,7 @@ export const createLivechatRoom = async < } await callbacks.run('livechat.newRoom', room); - - // TODO: replace with `Message.saveSystemMessage` - - await sendMessage(guest, { t: 'livechat-started', msg: '', groupable: false, token: guest.token }, room); + await Message.saveSystemMessageAndNotifyUser('livechat-started', rid, '', { _id, username }, { groupable: false, token: guest.token }); return result.value as IOmnichannelRoom; }; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index ccca7a8eb68e..8b537a10a4f5 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -252,7 +252,6 @@ class LivechatClass { const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor => (params as CloseRoomParamsByVisitor).visitor !== undefined; - let chatCloser: any; if (isRoomClosedByUserParams(params)) { const { user } = params; this.logger.debug(`Closing by user ${user?._id}`); @@ -261,7 +260,6 @@ class LivechatClass { _id: user?._id || '', username: user?.username, }; - chatCloser = user; } else if (isRoomClosedByVisitorParams(params)) { const { visitor } = params; this.logger.debug(`Closing by visitor ${params.visitor._id}`); @@ -270,7 +268,6 @@ class LivechatClass { _id: visitor._id, username: visitor.username, }; - chatCloser = visitor; } else { throw new Error('Error: Please provide details of the user or visitor who closed the room'); } @@ -296,10 +293,6 @@ class LivechatClass { this.logger.debug(`DB updated for room ${room._id}`); - const transcriptRequested = - !!transcriptRequest || (!settings.get('Livechat_enable_transcript') && settings.get('Livechat_transcript_send_always')); - - // Retrieve the closed room const newRoom = await LivechatRooms.findOneById(rid); if (!newRoom) { @@ -307,24 +300,20 @@ class LivechatClass { } this.logger.debug(`Sending closing message to room ${room._id}`); - await sendMessage( - chatCloser, - { - t: 'livechat-close', - msg: comment, - groupable: false, - transcriptRequested, - ...(isRoomClosedByVisitorParams(params) && { token: chatCloser.token }), - }, - newRoom, - ); + + const transcriptRequested = + !!transcriptRequest || (!settings.get('Livechat_enable_transcript') && settings.get('Livechat_transcript_send_always')); + + await Message.saveSystemMessageAndNotifyUser('livechat-close', rid, comment ?? '', closeData.closedBy, { + groupable: false, + transcriptRequested, + ...(isRoomClosedByVisitorParams(params) && { token: params.visitor.token }), + }); if (settings.get('Livechat_enable_transcript') && !settings.get('Livechat_transcript_send_always')) { await Message.saveSystemMessage('command', rid, 'promptTranscript', closeData.closedBy); } - this.logger.debug(`Running callbacks for room ${newRoom._id}`); - process.nextTick(() => { /** * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed @@ -1254,31 +1243,20 @@ class LivechatClass { const scopeData = scope || (nextDepartment ? 'department' : 'agent'); this.logger.info(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`); - await sendMessage( - transferredBy, - { - t: 'livechat_transfer_history', - rid: room._id, + const transferMessage = { + ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }), + transferData: { + transferredBy, ts: new Date(), - msg: '', - u: { - _id, - username, - }, - groupable: false, - ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }), - transferData: { - transferredBy, - ts: new Date(), - scope: scopeData, - comment, - ...(previousDepartment && { previousDepartment }), - ...(nextDepartment && { nextDepartment }), - ...(transferredTo && { transferredTo }), - }, + scope: scopeData, + comment, + ...(previousDepartment && { previousDepartment }), + ...(nextDepartment && { nextDepartment }), + ...(transferredTo && { transferredTo }), }, - room, - ); + }; + + await Message.saveSystemMessageAndNotifyUser('livechat_transfer_history', room._id, '', { _id, username }, transferMessage); } async saveGuest(guestData: Pick & { email?: string; phone?: string }, userId: string) { diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index f691a775cb6a..9f3dd44cc16d 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.ts +++ b/apps/meteor/app/message-pin/server/pinMessage.ts @@ -134,7 +134,7 @@ Meteor.methods({ const pinMessageType = originalMessage.t === 'e2e' ? 'message_pinned_e2e' : 'message_pinned'; - const msgId = await Message.saveSystemMessage(pinMessageType, originalMessage.rid, '', me, { + return Message.saveSystemMessage(pinMessageType, originalMessage.rid, '', me, { attachments: [ { text: originalMessage.msg, @@ -145,8 +145,6 @@ Meteor.methods({ }, ], }); - - return Messages.findOneById(msgId); }, async unpinMessage(message) { check(message._id, String); diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 4485bb7ad93b..906868b6bb17 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -6,7 +6,8 @@ import { Messages, Rooms } from '@rocket.chat/models'; import { deleteMessage } from '../../../app/lib/server/functions/deleteMessage'; import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; import { updateMessage } from '../../../app/lib/server/functions/updateMessage'; -import { notifyOnMessageChange } from '../../../app/lib/server/lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnMessageChange } from '../../../app/lib/server/lib/notifyListener'; +import { notifyUsersOnSystemMessage } from '../../../app/lib/server/lib/notifyUsersOnMessage'; import { executeSendMessage } from '../../../app/lib/server/methods/sendMessage'; import { executeSetReaction } from '../../../app/reactions/server/setReaction'; import { settings } from '../../../app/settings/server'; @@ -97,19 +98,38 @@ export class MessageService extends ServiceClassInternal implements IMessageServ return executeSetReaction(userId, reaction, messageId, shouldReact); } + async saveSystemMessageAndNotifyUser( + type: MessageTypesValues, + rid: string, + messageText: string, + owner: Pick, + extraData?: Partial, + ): Promise { + const createdMessage = await this.saveSystemMessage(type, rid, messageText, owner, extraData); + + const room = await Rooms.findOneById(rid); + if (!room) { + throw new Error('Failed to find the room.'); + } + + await notifyUsersOnSystemMessage(createdMessage, room); + + return createdMessage; + } + async saveSystemMessage( type: MessageTypesValues, rid: string, message: string, owner: Pick, extraData?: Partial, - ): Promise { + ): Promise { const { _id: userId, username, name } = owner; if (!username) { throw new Error('The username cannot be empty.'); } - const [result] = await Promise.all([ + const [{ insertedId }] = await Promise.all([ Messages.createWithTypeRoomIdMessageUserAndUnread( type, rid, @@ -121,11 +141,19 @@ export class MessageService extends ServiceClassInternal implements IMessageServ Rooms.incMsgCountById(rid, 1), ]); - void notifyOnMessageChange({ - id: result.insertedId, - }); + if (!insertedId) { + throw new Error('Failed to save system message.'); + } + + const createdMessage = await Messages.findOneById(insertedId); + if (!createdMessage) { + throw new Error('Failed to find the created message.'); + } + + void notifyOnMessageChange({ id: createdMessage._id, data: createdMessage }); + void notifyOnRoomChangedById(rid); - return result.insertedId; + return createdMessage; } async beforeSave({ diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index b38d6a9559d6..0563fc6f148d 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -8,7 +8,14 @@ export interface IMessageService { message: string, user: Pick, extraData?: Partial, - ): Promise; + ): Promise; + saveSystemMessageAndNotifyUser( + type: MessageTypesValues, + rid: string, + message: string, + user: Pick, + extraData?: Partial, + ): Promise; beforeSave(param: { message: IMessage; room: IRoom; user: IUser }): Promise; sendMessageWithValidation(user: IUser, message: Partial, room: Partial, upsert?: boolean): Promise; deleteMessage(user: IUser, message: IMessage): Promise; From a40541b61676d23338d72d420995bbc5a211d152 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 19 Aug 2024 15:29:14 -0300 Subject: [PATCH 24/49] chore: apps-engine message converter cache by any obj (#33053) --- .../app/apps/server/bridges/livechat.ts | 15 ++++++++---- .../app/apps/server/converters/messages.js | 23 ++++++++++++------- .../app/livechat/server/lib/LivechatTyped.ts | 2 +- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index ec5cff29a99b..4f4794591e02 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -1,7 +1,7 @@ -import type { IAppServerOrchestrator, IAppsLivechatMessage } from '@rocket.chat/apps'; +import type { IAppServerOrchestrator, IAppsLivechatMessage, IAppsMessage } from '@rocket.chat/apps'; import type { IExtraRoomParams } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator'; import type { IVisitor, ILivechatRoom, ILivechatTransferData, IDepartment } from '@rocket.chat/apps-engine/definition/livechat'; -import type { IMessage as IAppsEngineMesage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IMessage as IAppsEngineMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import { LivechatBridge } from '@rocket.chat/apps-engine/server/bridges/LivechatBridge'; import type { ILivechatDepartment, IOmnichannelRoom, SelectedAgent, IMessage, ILivechatVisitor } from '@rocket.chat/core-typings'; @@ -13,6 +13,12 @@ import { deasyncPromise } from '../../../../server/deasync/deasync'; import { type ILivechatMessage, Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; import { settings } from '../../../settings/server'; +declare module '@rocket.chat/apps/dist/converters/IAppMessagesConverter' { + export interface IAppMessagesConverter { + convertMessage(message: IMessage, cacheObj?: object): Promise; + } +} + declare module '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator' { interface IExtraRoomParams { customFields?: Record; @@ -337,7 +343,7 @@ export class AppLivechatBridge extends LivechatBridge { return Promise.all((await LivechatDepartment.findEnabledWithAgents().toArray()).map(boundConverter)); } - protected async _fetchLivechatRoomMessages(appId: string, roomId: string): Promise> { + protected async _fetchLivechatRoomMessages(appId: string, roomId: string): Promise> { this.orch.debugLog(`The App ${appId} is getting the transcript for livechat room ${roomId}.`); const messageConverter = this.orch.getConverters()?.get('messages'); @@ -346,8 +352,7 @@ export class AppLivechatBridge extends LivechatBridge { } const livechatMessages = await LivechatTyped.getRoomMessages({ rid: roomId }); - - return Promise.all(livechatMessages.map((message) => messageConverter.convertMessage(message) as Promise)); + return Promise.all(await livechatMessages.map((message) => messageConverter.convertMessage(message, livechatMessages)).toArray()); } protected async setCustomFields( diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index d7dae512e9a8..89ef2454d895 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -52,19 +52,26 @@ export class AppMessagesConverter { return transformMappedData(message, map); } - async convertMessage(msgObj) { + async convertMessage(msgObj, cacheObj = msgObj) { if (!msgObj) { return undefined; } const cache = - this.mem.get(msgObj) ?? + this.mem.get(cacheObj) ?? new Map([ ['room', cachedFunction(this.orch.getConverters().get('rooms').convertById.bind(this.orch.getConverters().get('rooms')))], - ['user', cachedFunction(this.orch.getConverters().get('users').convertById.bind(this.orch.getConverters().get('users')))], + [ + 'user.convertById', + cachedFunction(this.orch.getConverters().get('users').convertById.bind(this.orch.getConverters().get('users'))), + ], + [ + 'user.convertToApp', + cachedFunction(this.orch.getConverters().get('users').convertToApp.bind(this.orch.getConverters().get('users'))), + ], ]); - this.mem.set(msgObj, cache); + this.mem.set(cacheObj, cache); const map = { id: '_id', @@ -96,7 +103,7 @@ export class AppMessagesConverter { return undefined; } - return cache.get('user')(editedBy._id); + return cache.get('user.convertById')(editedBy._id); }, attachments: async (message) => { const result = await this._convertAttachmentsToApp(message.attachments); @@ -110,8 +117,8 @@ export class AppMessagesConverter { // When the message contains token, means the message is from the visitor(omnichannel) const user = await (isMessageFromVisitor(msgObj) - ? this.orch.getConverters().get('users').convertToApp(message.u) - : cache.get('user')(message.u._id)); + ? cache.get('user.convertToApp')(message.u) + : cache.get('user.convertById')(message.u._id)); delete message.u; @@ -120,7 +127,7 @@ export class AppMessagesConverter { * `sender` as undefined, so we need to add this fallback here. */ - return user || this.orch.getConverters().get('users').convertToApp(message.u); + return user || cache.get('user.convertToApp')(message.u); }, }; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 8b537a10a4f5..bb8a3fd77ba2 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -867,7 +867,7 @@ class LivechatClass { return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { sort: { ts: 1 }, - }).toArray(); + }); } async archiveDepartment(_id: string) { From 7e2facc979488b059dd4ecf49fe53c48d3387141 Mon Sep 17 00:00:00 2001 From: Abhinav Kumar Date: Tue, 20 Aug 2024 01:33:07 +0530 Subject: [PATCH 25/49] fix: customFields ignored in livechat room creation (#33047) --- .changeset/twelve-windows-train.md | 5 ++ .../server/hooks/beforeNewRoom.ts | 9 ++-- apps/meteor/lib/utils/isPlainObject.ts | 3 ++ .../livechat/hooks/beforeNewRoom.spec.ts | 52 +++++++++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 .changeset/twelve-windows-train.md create mode 100644 apps/meteor/lib/utils/isPlainObject.ts create mode 100644 apps/meteor/tests/unit/server/livechat/hooks/beforeNewRoom.spec.ts diff --git a/.changeset/twelve-windows-train.md b/.changeset/twelve-windows-train.md new file mode 100644 index 000000000000..4c6ef548e650 --- /dev/null +++ b/.changeset/twelve-windows-train.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed: Custom fields in extraData now correctly added to extraRoomInfo by livechat.beforeRoom callback during livechat room creation. diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewRoom.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewRoom.ts index 35219fc6e03b..4b0db6814bf2 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewRoom.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewRoom.ts @@ -2,6 +2,7 @@ import { OmnichannelServiceLevelAgreements } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; +import { isPlainObject } from '../../../../../lib/utils/isPlainObject'; callbacks.add( 'livechat.beforeRoom', @@ -10,9 +11,11 @@ callbacks.add( return roomInfo; } - const { sla: searchTerm } = extraData; + const { sla: searchTerm, customFields } = extraData; + const roomInfoWithExtraData = { ...roomInfo, ...(isPlainObject(customFields) && { customFields }) }; + if (!searchTerm) { - return roomInfo; + return roomInfoWithExtraData; } const sla = await OmnichannelServiceLevelAgreements.findOneByIdOrName(searchTerm); @@ -23,7 +26,7 @@ callbacks.add( } const { _id: slaId } = sla; - return { ...roomInfo, slaId }; + return { ...roomInfoWithExtraData, slaId }; }, callbacks.priority.MEDIUM, 'livechat-before-new-room', diff --git a/apps/meteor/lib/utils/isPlainObject.ts b/apps/meteor/lib/utils/isPlainObject.ts new file mode 100644 index 000000000000..a2bcf15cc590 --- /dev/null +++ b/apps/meteor/lib/utils/isPlainObject.ts @@ -0,0 +1,3 @@ +export function isPlainObject(value: unknown) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} diff --git a/apps/meteor/tests/unit/server/livechat/hooks/beforeNewRoom.spec.ts b/apps/meteor/tests/unit/server/livechat/hooks/beforeNewRoom.spec.ts new file mode 100644 index 000000000000..9ba9ae73fe57 --- /dev/null +++ b/apps/meteor/tests/unit/server/livechat/hooks/beforeNewRoom.spec.ts @@ -0,0 +1,52 @@ +import { expect } from 'chai'; +import { describe, it, beforeEach } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +import { callbacks } from '../../../../../lib/callbacks'; + +const findStub = sinon.stub(); + +proxyquire.noCallThru().load('../../../../../ee/app/livechat-enterprise/server/hooks/beforeNewRoom.ts', { + 'meteor/meteor': { + Meteor: { + Error, + }, + }, + '@rocket.chat/models': { + OmnichannelServiceLevelAgreements: { + findOneByIdOrName: findStub, + }, + }, +}); + +describe('livechat.beforeRoom', () => { + beforeEach(() => findStub.withArgs('high').resolves({ _id: 'high' }).withArgs('invalid').resolves(null)); + + it('should return roomInfo with customFields when provided', async () => { + const roomInfo = { name: 'test' }; + const extraData = { customFields: { test: 'test' } }; + const result = await callbacks.run('livechat.beforeRoom', roomInfo, extraData); + expect(result).to.deep.equal({ ...roomInfo, customFields: extraData.customFields }); + }); + + it('should throw an error when provided with an invalid sla', async () => { + const roomInfo = { name: 'test' }; + const extraData = { customFields: { test: 'test' }, sla: 'invalid' }; + await expect(callbacks.run('livechat.beforeRoom', roomInfo, extraData)).to.be.rejectedWith(Error, 'error-invalid-sla'); + }); + + it('should not include field in roomInfo when extraData has field other than customFields, sla', async () => { + const roomInfo = { name: 'test' }; + const extraData = { customFields: { test: 'test' }, sla: 'high' }; + const result = await callbacks.run('livechat.beforeRoom', roomInfo, extraData); + expect(result).to.deep.equal({ ...roomInfo, customFields: extraData.customFields, slaId: 'high' }); + }); + + it('should return roomInfo with no customFields when customFields is not an object', async () => { + const roomInfo = { name: 'test' }; + const extraData = { customFields: 'not an object' }; + const result = await callbacks.run('livechat.beforeRoom', roomInfo, extraData); + expect(result).to.deep.equal({ ...roomInfo }); + }); +}); From 5d657eef559ff0bd5ac249bdb7af275886d2733b Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 19 Aug 2024 20:21:46 -0300 Subject: [PATCH 26/49] chore: Settings files sanitization (#33057) --- .../priorities/PriorityEditForm.tsx | 2 +- .../admin/settings/GroupPage.stories.tsx | 26 -------- .../admin/settings/GroupSelector.stories.tsx | 16 ----- .../views/admin/settings/GroupSelector.tsx | 43 -------------- .../views/admin/settings/Section.stories.tsx | 23 -------- .../views/admin/settings/Setting.stories.tsx | 59 ------------------- .../{ => Setting}/MemoizedSetting.tsx | 0 .../ResetSettingButton.stories.tsx | 0 .../ResetSettingButton.tsx | 0 .../Setting/ResetSettingButton/index.ts | 1 + .../settings/Setting/Setting.stories.tsx | 58 ++++++++++++++++++ .../admin/settings/{ => Setting}/Setting.tsx | 10 +--- .../{ => Setting}/SettingSkeleton.tsx | 0 .../views/admin/settings/Setting/index.ts | 1 + .../inputs/ActionSettingInput.stories.tsx | 0 .../inputs/ActionSettingInput.tsx | 0 .../inputs/AssetSettingInput.stories.tsx | 0 .../inputs/AssetSettingInput.styles.css | 0 .../inputs/AssetSettingInput.tsx | 0 .../inputs/BooleanSettingInput.stories.tsx | 0 .../inputs/BooleanSettingInput.tsx | 0 .../inputs/CodeMirror/CodeMirror.tsx | 2 +- .../inputs/CodeMirror/CodeMirrorBox.tsx | 0 .../{ => Setting}/inputs/CodeMirror/index.ts | 0 .../inputs/CodeSettingInput.stories.tsx | 0 .../{ => Setting}/inputs/CodeSettingInput.tsx | 0 .../inputs/ColorSettingInput.stories.tsx | 0 .../inputs/ColorSettingInput.tsx | 0 .../inputs/FontSettingInput.stories.tsx | 0 .../{ => Setting}/inputs/FontSettingInput.tsx | 0 .../inputs/GenericSettingInput.stories.tsx | 0 .../inputs/GenericSettingInput.tsx | 0 .../inputs/IntSettingInput.stories.tsx | 0 .../{ => Setting}/inputs/IntSettingInput.tsx | 0 .../inputs/LanguageSettingInput.stories.tsx | 0 .../inputs/LanguageSettingInput.tsx | 0 .../inputs/LookupSettingInput.tsx | 4 +- .../MultiSelectSettingInput.stories.tsx | 0 .../inputs/MultiSelectSettingInput.tsx | 0 .../inputs/PasswordSettingInput.stories.tsx | 0 .../inputs/PasswordSettingInput.tsx | 0 .../RelativeUrlSettingInput.stories.tsx | 0 .../inputs/RelativeUrlSettingInput.tsx | 0 .../inputs/RoomPickSettingInput.tsx | 2 +- .../inputs/SelectSettingInput.stories.tsx | 0 .../inputs/SelectSettingInput.tsx | 0 .../inputs/SelectTimezoneSettingInput.tsx | 0 .../inputs/StringSettingInput.stories.tsx | 0 .../inputs/StringSettingInput.tsx | 0 .../inputs/TimespanSettingInput.spec.tsx | 2 +- .../inputs/TimespanSettingInput.tsx | 2 +- .../settings/{ => Setting}/inputs/types.ts | 0 .../SettingsGroupPage.stories.tsx | 24 ++++++++ .../SettingsGroupPage.tsx} | 18 +++--- .../SettingsGroupPageSkeleton.tsx} | 10 ++-- .../admin/settings/SettingsGroupPage/index.ts | 1 + .../SettingsGroupSelector.stories.tsx | 16 +++++ .../SettingsGroupSelector.tsx | 42 +++++++++++++ .../settings/SettingsGroupSelector/index.ts | 1 + .../views/admin/settings/SettingsRoute.tsx | 4 +- .../SettingsSection.stories.tsx | 21 +++++++ .../SettingsSection.tsx} | 24 ++++---- .../SettingsSectionSkeleton.tsx} | 8 +-- .../admin/settings/SettingsSection/index.ts | 1 + .../admin/settings/groups/AssetsGroupPage.tsx | 26 -------- .../admin/settings/groups/BaseGroupPage.tsx | 28 +++++++++ .../settings/groups/GenericGroupPage.tsx | 35 +++++++---- .../admin/settings/groups/LDAPGroupPage.tsx | 7 ++- .../{ => OAuthGroupPage}/CreateOAuthModal.tsx | 2 +- .../{ => OAuthGroupPage}/OAuthGroupPage.tsx | 20 +++---- .../settings/groups/OAuthGroupPage/index.ts | 1 + .../admin/settings/groups/TabbedGroupPage.tsx | 49 +++++++-------- .../AssignAgentButton.tsx | 0 .../AssignAgentModal.tsx | 0 .../RemoveAgentButton.tsx | 0 .../VoipExtensionsPage.tsx | 0 .../{ => VoipGroupPage}/VoipGroupPage.tsx | 20 +++---- .../settings/groups/VoipGroupPage/index.ts | 1 + .../tabs/AppSettings/AppSetting.tsx | 2 +- 79 files changed, 305 insertions(+), 307 deletions(-) delete mode 100644 apps/meteor/client/views/admin/settings/GroupPage.stories.tsx delete mode 100644 apps/meteor/client/views/admin/settings/GroupSelector.stories.tsx delete mode 100644 apps/meteor/client/views/admin/settings/GroupSelector.tsx delete mode 100644 apps/meteor/client/views/admin/settings/Section.stories.tsx delete mode 100644 apps/meteor/client/views/admin/settings/Setting.stories.tsx rename apps/meteor/client/views/admin/settings/{ => Setting}/MemoizedSetting.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting/ResetSettingButton}/ResetSettingButton.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting/ResetSettingButton}/ResetSettingButton.tsx (100%) create mode 100644 apps/meteor/client/views/admin/settings/Setting/ResetSettingButton/index.ts create mode 100644 apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx rename apps/meteor/client/views/admin/settings/{ => Setting}/Setting.tsx (95%) rename apps/meteor/client/views/admin/settings/{ => Setting}/SettingSkeleton.tsx (100%) create mode 100644 apps/meteor/client/views/admin/settings/Setting/index.ts rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/ActionSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/ActionSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/AssetSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/AssetSettingInput.styles.css (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/AssetSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/BooleanSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/BooleanSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/CodeMirror/CodeMirror.tsx (97%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/CodeMirror/CodeMirrorBox.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/CodeMirror/index.ts (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/CodeSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/CodeSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/ColorSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/ColorSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/FontSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/FontSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/GenericSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/GenericSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/IntSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/IntSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/LanguageSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/LanguageSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/LookupSettingInput.tsx (91%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/MultiSelectSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/MultiSelectSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/PasswordSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/PasswordSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/RelativeUrlSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/RelativeUrlSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/RoomPickSettingInput.tsx (93%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/SelectSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/SelectSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/SelectTimezoneSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/StringSettingInput.stories.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/StringSettingInput.tsx (100%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/TimespanSettingInput.spec.tsx (98%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/TimespanSettingInput.tsx (99%) rename apps/meteor/client/views/admin/settings/{ => Setting}/inputs/types.ts (100%) create mode 100644 apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.stories.tsx rename apps/meteor/client/views/admin/settings/{GroupPage.tsx => SettingsGroupPage/SettingsGroupPage.tsx} (92%) rename apps/meteor/client/views/admin/settings/{GroupPageSkeleton.tsx => SettingsGroupPage/SettingsGroupPageSkeleton.tsx} (64%) create mode 100644 apps/meteor/client/views/admin/settings/SettingsGroupPage/index.ts create mode 100644 apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.stories.tsx create mode 100644 apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.tsx create mode 100644 apps/meteor/client/views/admin/settings/SettingsGroupSelector/index.ts create mode 100644 apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.stories.tsx rename apps/meteor/client/views/admin/settings/{Section.tsx => SettingsSection/SettingsSection.tsx} (86%) rename apps/meteor/client/views/admin/settings/{SectionSkeleton.tsx => SettingsSection/SettingsSectionSkeleton.tsx} (68%) create mode 100644 apps/meteor/client/views/admin/settings/SettingsSection/index.ts delete mode 100644 apps/meteor/client/views/admin/settings/groups/AssetsGroupPage.tsx create mode 100644 apps/meteor/client/views/admin/settings/groups/BaseGroupPage.tsx rename apps/meteor/client/views/admin/settings/groups/{ => OAuthGroupPage}/CreateOAuthModal.tsx (95%) rename apps/meteor/client/views/admin/settings/groups/{ => OAuthGroupPage}/OAuthGroupPage.tsx (88%) create mode 100644 apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/index.ts rename apps/meteor/client/views/admin/settings/groups/{voip => VoipGroupPage}/AssignAgentButton.tsx (100%) rename apps/meteor/client/views/admin/settings/groups/{voip => VoipGroupPage}/AssignAgentModal.tsx (100%) rename apps/meteor/client/views/admin/settings/groups/{voip => VoipGroupPage}/RemoveAgentButton.tsx (100%) rename apps/meteor/client/views/admin/settings/groups/{voip => VoipGroupPage}/VoipExtensionsPage.tsx (100%) rename apps/meteor/client/views/admin/settings/groups/{ => VoipGroupPage}/VoipGroupPage.tsx (70%) create mode 100644 apps/meteor/client/views/admin/settings/groups/VoipGroupPage/index.ts diff --git a/apps/meteor/client/omnichannel/priorities/PriorityEditForm.tsx b/apps/meteor/client/omnichannel/priorities/PriorityEditForm.tsx index d67f637a2b4e..592cd6b0f932 100644 --- a/apps/meteor/client/omnichannel/priorities/PriorityEditForm.tsx +++ b/apps/meteor/client/omnichannel/priorities/PriorityEditForm.tsx @@ -7,7 +7,7 @@ import type { ReactElement } from 'react'; import React, { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import StringSettingInput from '../../views/admin/settings/inputs/StringSettingInput'; +import StringSettingInput from '../../views/admin/settings/Setting/inputs/StringSettingInput'; export type PriorityFormData = { name: string; reset: boolean }; diff --git a/apps/meteor/client/views/admin/settings/GroupPage.stories.tsx b/apps/meteor/client/views/admin/settings/GroupPage.stories.tsx deleted file mode 100644 index 83112bc42550..000000000000 --- a/apps/meteor/client/views/admin/settings/GroupPage.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import React from 'react'; - -import GroupPage from './GroupPage'; - -export default { - title: 'Admin/Settings/GroupPage', - component: GroupPage, - subcomponents: { - 'GroupPage.Skeleton': GroupPage.Skeleton, - }, - parameters: { - layout: 'fullscreen', - controls: { hideNoControlsWarning: true }, - }, -} as ComponentMeta; - -export const Default: ComponentStory = (args) => ; - -export const WithGroup: ComponentStory = (args) => ; -WithGroup.args = { - _id: 'General', - i18nLabel: 'General', -}; - -export const Skeleton: ComponentStory = () => ; diff --git a/apps/meteor/client/views/admin/settings/GroupSelector.stories.tsx b/apps/meteor/client/views/admin/settings/GroupSelector.stories.tsx deleted file mode 100644 index 4b505dd19299..000000000000 --- a/apps/meteor/client/views/admin/settings/GroupSelector.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import React from 'react'; - -import GroupSelector from './GroupSelector'; - -export default { - title: 'Admin/Settings/GroupSelector', - component: GroupSelector, - parameters: { - layout: 'fullscreen', - controls: { hideNoControlsWarning: true }, - }, -} as ComponentMeta; - -export const Default: ComponentStory = (args) => ; -Default.storyName = 'GroupSelector'; diff --git a/apps/meteor/client/views/admin/settings/GroupSelector.tsx b/apps/meteor/client/views/admin/settings/GroupSelector.tsx deleted file mode 100644 index 6d6d90a566eb..000000000000 --- a/apps/meteor/client/views/admin/settings/GroupSelector.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { GroupId } from '@rocket.chat/core-typings'; -import { useSettingStructure } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import GroupPage from './GroupPage'; -import AssetsGroupPage from './groups/AssetsGroupPage'; -import LDAPGroupPage from './groups/LDAPGroupPage'; -import OAuthGroupPage from './groups/OAuthGroupPage'; -import TabbedGroupPage from './groups/TabbedGroupPage'; -import VoipGroupPage from './groups/VoipGroupPage'; - -type GroupSelectorProps = { - groupId: GroupId; - onClickBack?: () => void; -}; - -const GroupSelector = ({ groupId, onClickBack }: GroupSelectorProps) => { - const group = useSettingStructure(groupId); - - if (!group) { - return ; - } - - if (groupId === 'Assets') { - return ; - } - - if (groupId === 'OAuth') { - return ; - } - - if (groupId === 'LDAP') { - return ; - } - - if (groupId === 'Call_Center') { - return ; - } - - return ; -}; - -export default GroupSelector; diff --git a/apps/meteor/client/views/admin/settings/Section.stories.tsx b/apps/meteor/client/views/admin/settings/Section.stories.tsx deleted file mode 100644 index 05ec280ea74c..000000000000 --- a/apps/meteor/client/views/admin/settings/Section.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import React from 'react'; - -import Section from './Section'; - -export default { - title: 'Admin/Settings/Section', - component: Section, - subcomponents: { - 'Section.Skeleton': Section.Skeleton, - }, - parameters: { - layout: 'fullscreen', - controls: { hideNoControlsWarning: true }, - }, -} as ComponentMeta; - -export const Default: ComponentStory = (args) =>
    ; -Default.args = { - groupId: 'General', -}; - -export const Skeleton: ComponentStory = () => ; diff --git a/apps/meteor/client/views/admin/settings/Setting.stories.tsx b/apps/meteor/client/views/admin/settings/Setting.stories.tsx deleted file mode 100644 index 18ff3801dbc9..000000000000 --- a/apps/meteor/client/views/admin/settings/Setting.stories.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { FieldGroup } from '@rocket.chat/fuselage'; -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import React from 'react'; - -import Setting from './Setting'; - -export default { - title: 'Admin/Settings/Setting', - component: Setting, - subcomponents: { - 'Setting.Memoized': Setting.Memoized, - }, - parameters: { - layout: 'centered', - actions: { - argTypesRegex: '^on.*', - }, - }, - decorators: [ - (fn) => ( -
    -
    {fn()}
    -
    - ), - ], -} as ComponentMeta; - -export const Default: ComponentStory = (args) => ; -Default.args = { - _id: 'setting-id', - label: 'Label', - hint: 'Hint', -}; - -export const WithCallout: ComponentStory = (args) => ; -WithCallout.args = { - _id: 'setting-id', - label: 'Label', - hint: 'Hint', - callout: 'Callout text', -}; - -export const types = () => ( - - - - - - - - - - - - - -); - -export const skeleton = () => ; diff --git a/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx b/apps/meteor/client/views/admin/settings/Setting/MemoizedSetting.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/MemoizedSetting.tsx rename to apps/meteor/client/views/admin/settings/Setting/MemoizedSetting.tsx diff --git a/apps/meteor/client/views/admin/settings/ResetSettingButton.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/ResetSettingButton/ResetSettingButton.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/ResetSettingButton.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/ResetSettingButton/ResetSettingButton.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/ResetSettingButton.tsx b/apps/meteor/client/views/admin/settings/Setting/ResetSettingButton/ResetSettingButton.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/ResetSettingButton.tsx rename to apps/meteor/client/views/admin/settings/Setting/ResetSettingButton/ResetSettingButton.tsx diff --git a/apps/meteor/client/views/admin/settings/Setting/ResetSettingButton/index.ts b/apps/meteor/client/views/admin/settings/Setting/ResetSettingButton/index.ts new file mode 100644 index 000000000000..38d90229c1a9 --- /dev/null +++ b/apps/meteor/client/views/admin/settings/Setting/ResetSettingButton/index.ts @@ -0,0 +1 @@ +export { default } from './ResetSettingButton'; diff --git a/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx new file mode 100644 index 000000000000..e303ffa2c497 --- /dev/null +++ b/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx @@ -0,0 +1,58 @@ +import { FieldGroup } from '@rocket.chat/fuselage'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import React from 'react'; + +import MemoizedSetting from './MemoizedSetting'; +import Setting from './Setting'; +import SettingSkeleton from './SettingSkeleton'; + +export default { + title: 'Admin/Settings/Setting', + component: Setting, + parameters: { + layout: 'centered', + actions: { + argTypesRegex: '^on.*', + }, + }, + decorators: [ + (fn) => ( +
    +
    {fn()}
    +
    + ), + ], +} as ComponentMeta; + +export const Default: ComponentStory = (args) => ; +Default.args = { + _id: 'setting-id', + label: 'Label', + hint: 'Hint', +}; + +export const WithCallout: ComponentStory = (args) => ; +WithCallout.args = { + _id: 'setting-id', + label: 'Label', + hint: 'Hint', + callout: 'Callout text', +}; + +export const types = () => ( + + + + + + + + + + + + + +); + +export const Skeleton = () => ; diff --git a/apps/meteor/client/views/admin/settings/Setting.tsx b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx similarity index 95% rename from apps/meteor/client/views/admin/settings/Setting.tsx rename to apps/meteor/client/views/admin/settings/Setting/Setting.tsx index 6a08352b9180..19ed42927cfe 100644 --- a/apps/meteor/client/views/admin/settings/Setting.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx @@ -6,10 +6,9 @@ import { useSettingStructure, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect, useMemo, useState, useCallback } from 'react'; -import MarkdownText from '../../../components/MarkdownText'; -import { useEditableSetting, useEditableSettingsDispatch, useIsEnterprise } from '../EditableSettingsContext'; +import MarkdownText from '../../../../components/MarkdownText'; +import { useEditableSetting, useEditableSettingsDispatch, useIsEnterprise } from '../../EditableSettingsContext'; import MemoizedSetting from './MemoizedSetting'; -import SettingSkeleton from './SettingSkeleton'; type SettingProps = { className?: string; @@ -165,7 +164,4 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr ); } -export default Object.assign(Setting, { - Memoized: MemoizedSetting, - Skeleton: SettingSkeleton, -}); +export default Setting; diff --git a/apps/meteor/client/views/admin/settings/SettingSkeleton.tsx b/apps/meteor/client/views/admin/settings/Setting/SettingSkeleton.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/SettingSkeleton.tsx rename to apps/meteor/client/views/admin/settings/Setting/SettingSkeleton.tsx diff --git a/apps/meteor/client/views/admin/settings/Setting/index.ts b/apps/meteor/client/views/admin/settings/Setting/index.ts new file mode 100644 index 000000000000..11b0d2f07626 --- /dev/null +++ b/apps/meteor/client/views/admin/settings/Setting/index.ts @@ -0,0 +1 @@ +export { default } from './Setting'; diff --git a/apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/ActionSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/ActionSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/ActionSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/ActionSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.styles.css b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.styles.css similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.styles.css rename to apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.styles.css diff --git a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirror.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx similarity index 97% rename from apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirror.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx index b53fae98e6e0..929bf12e39ad 100644 --- a/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirror.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx @@ -57,7 +57,7 @@ function CodeMirror({ const setupCodeMirror = async (): Promise => { const CodeMirror = await import('codemirror'); await Promise.all([ - import('../../../../../../app/ui/client/lib/codeMirror/codeMirror'), + import('../../../../../../../app/ui/client/lib/codeMirror/codeMirror'), import('codemirror/addon/edit/matchbrackets'), import('codemirror/addon/edit/closebrackets'), import('codemirror/addon/edit/matchtags'), diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirrorBox.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirrorBox.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeMirror/index.ts b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/index.ts similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/CodeMirror/index.ts rename to apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/index.ts diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/CodeSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/CodeSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/ColorSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/ColorSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/ColorSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/ColorSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/FontSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/FontSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/FontSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/FontSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/FontSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/GenericSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/GenericSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/GenericSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/GenericSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/IntSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/IntSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/IntSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/IntSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/IntSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/LanguageSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/LanguageSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/LanguageSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/LanguageSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/LookupSettingInput.tsx similarity index 91% rename from apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/LookupSettingInput.tsx index d2c7029f994d..76d5b53d83f5 100644 --- a/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/LookupSettingInput.tsx @@ -3,8 +3,8 @@ import type { PathPattern } from '@rocket.chat/rest-typings'; import type { ReactElement } from 'react'; import React from 'react'; -import type { AsyncState } from '../../../../hooks/useAsyncState'; -import { useEndpointData } from '../../../../hooks/useEndpointData'; +import type { AsyncState } from '../../../../../hooks/useAsyncState'; +import { useEndpointData } from '../../../../../hooks/useEndpointData'; import ResetSettingButton from '../ResetSettingButton'; import type { SettingInputProps } from './types'; diff --git a/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/MultiSelectSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/MultiSelectSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/MultiSelectSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/MultiSelectSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/PasswordSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/PasswordSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/PasswordSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/PasswordSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/RelativeUrlSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/RelativeUrlSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/RelativeUrlSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/RelativeUrlSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/RoomPickSettingInput.tsx similarity index 93% rename from apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/RoomPickSettingInput.tsx index df2a4c1b0688..16dc748b62d6 100644 --- a/apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/RoomPickSettingInput.tsx @@ -3,7 +3,7 @@ import { Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; -import RoomAutoCompleteMultiple from '../../../../components/RoomAutoCompleteMultiple'; +import RoomAutoCompleteMultiple from '../../../../../components/RoomAutoCompleteMultiple'; import ResetSettingButton from '../ResetSettingButton'; import type { SettingInputProps } from './types'; diff --git a/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/SelectSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/SelectSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/SelectSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/SelectSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/SelectTimezoneSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/SelectTimezoneSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/StringSettingInput.stories.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/StringSettingInput.stories.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/StringSettingInput.stories.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/StringSettingInput.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/StringSettingInput.tsx diff --git a/apps/meteor/client/views/admin/settings/inputs/TimespanSettingInput.spec.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.spec.tsx similarity index 98% rename from apps/meteor/client/views/admin/settings/inputs/TimespanSettingInput.spec.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.spec.tsx index ee42bc8387f9..975a3171cbbf 100644 --- a/apps/meteor/client/views/admin/settings/inputs/TimespanSettingInput.spec.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.spec.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { TIMEUNIT } from '../../../../lib/convertTimeUnit'; +import { TIMEUNIT } from '../../../../../lib/convertTimeUnit'; import { default as TimespanSettingInput, getHighestTimeUnit } from './TimespanSettingInput'; global.ResizeObserver = jest.fn().mockImplementation(() => ({ diff --git a/apps/meteor/client/views/admin/settings/inputs/TimespanSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.tsx similarity index 99% rename from apps/meteor/client/views/admin/settings/inputs/TimespanSettingInput.tsx rename to apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.tsx index 14191d133c75..b15a353995c9 100644 --- a/apps/meteor/client/views/admin/settings/inputs/TimespanSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.tsx @@ -3,7 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FormEventHandler, ReactElement } from 'react'; import React, { useMemo, useState } from 'react'; -import { TIMEUNIT, timeUnitToMs, msToTimeUnit } from '../../../../lib/convertTimeUnit'; +import { TIMEUNIT, timeUnitToMs, msToTimeUnit } from '../../../../../lib/convertTimeUnit'; import ResetSettingButton from '../ResetSettingButton'; import type { SettingInputProps } from './types'; diff --git a/apps/meteor/client/views/admin/settings/inputs/types.ts b/apps/meteor/client/views/admin/settings/Setting/inputs/types.ts similarity index 100% rename from apps/meteor/client/views/admin/settings/inputs/types.ts rename to apps/meteor/client/views/admin/settings/Setting/inputs/types.ts diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.stories.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.stories.tsx new file mode 100644 index 000000000000..e84406ccc9a3 --- /dev/null +++ b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.stories.tsx @@ -0,0 +1,24 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import React from 'react'; + +import SettingsGroupPage from './SettingsGroupPage'; +import SettingsGroupPageSkeleton from './SettingsGroupPageSkeleton'; + +export default { + title: 'Admin/Settings/SettingsGroupPage', + component: SettingsGroupPage, + parameters: { + layout: 'fullscreen', + controls: { hideNoControlsWarning: true }, + }, +} as ComponentMeta; + +export const Default: ComponentStory = (args) => ; + +export const WithGroup: ComponentStory = (args) => ; +WithGroup.args = { + _id: 'General', + i18nLabel: 'General', +}; + +export const Skeleton: ComponentStory = () => ; diff --git a/apps/meteor/client/views/admin/settings/GroupPage.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx similarity index 92% rename from apps/meteor/client/views/admin/settings/GroupPage.tsx rename to apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx index 5946805a497e..884c9f6e67e9 100644 --- a/apps/meteor/client/views/admin/settings/GroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx @@ -6,12 +6,11 @@ import { useToastMessageDispatch, useSettingsDispatch, useSettings, useTranslati import type { ReactNode, FormEvent, MouseEvent } from 'react'; import React, { useMemo, memo } from 'react'; -import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; -import type { EditableSetting } from '../EditableSettingsContext'; -import { useEditableSettingsDispatch, useEditableSettings } from '../EditableSettingsContext'; -import GroupPageSkeleton from './GroupPageSkeleton'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../../components/Page'; +import type { EditableSetting } from '../../EditableSettingsContext'; +import { useEditableSettingsDispatch, useEditableSettings } from '../../EditableSettingsContext'; -type GroupPageProps = { +type SettingsGroupPageProps = { children: ReactNode; headerButtons?: ReactNode; onClickBack?: () => void; @@ -22,7 +21,7 @@ type GroupPageProps = { isCustom?: boolean; }; -const GroupPage = ({ +const SettingsGroupPage = ({ children = undefined, headerButtons = undefined, onClickBack, @@ -31,7 +30,7 @@ const GroupPage = ({ i18nDescription = undefined, tabs = undefined, isCustom = false, -}: GroupPageProps) => { +}: SettingsGroupPageProps) => { const t = useTranslation(); const dispatch = useSettingsDispatch(); const dispatchToastMessage = useToastMessageDispatch(); @@ -133,7 +132,6 @@ const GroupPage = ({ return {children}; } - // The settings const isTranslationKey = (key: string): key is TranslationKey => (key as TranslationKey) !== undefined; return ( @@ -178,6 +176,4 @@ const GroupPage = ({ ); }; -export default Object.assign(memo(GroupPage), { - Skeleton: GroupPageSkeleton, -}); +export default memo(SettingsGroupPage); diff --git a/apps/meteor/client/views/admin/settings/GroupPageSkeleton.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPageSkeleton.tsx similarity index 64% rename from apps/meteor/client/views/admin/settings/GroupPageSkeleton.tsx rename to apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPageSkeleton.tsx index 5817b85d6c64..ad6f93390c1d 100644 --- a/apps/meteor/client/views/admin/settings/GroupPageSkeleton.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPageSkeleton.tsx @@ -1,10 +1,10 @@ import { Accordion, Box, Skeleton } from '@rocket.chat/fuselage'; import React, { useMemo } from 'react'; -import { Page, PageHeader, PageContent } from '../../../components/Page'; -import Section from './Section'; +import { Page, PageHeader, PageContent } from '../../../../components/Page'; +import SettingsSectionSkeleton from '../SettingsSection/SettingsSectionSkeleton'; -const GroupPageSkeleton = () => ( +const SettingsGroupPageSkeleton = () => ( } /> @@ -15,11 +15,11 @@ const GroupPageSkeleton = () => ( - + ); -export default GroupPageSkeleton; +export default SettingsGroupPageSkeleton; diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupPage/index.ts b/apps/meteor/client/views/admin/settings/SettingsGroupPage/index.ts new file mode 100644 index 000000000000..95ff4dc9a06a --- /dev/null +++ b/apps/meteor/client/views/admin/settings/SettingsGroupPage/index.ts @@ -0,0 +1 @@ +export { default } from './SettingsGroupPage'; diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.stories.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.stories.tsx new file mode 100644 index 000000000000..997c842f2d60 --- /dev/null +++ b/apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.stories.tsx @@ -0,0 +1,16 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import React from 'react'; + +import SettingsGroupSelector from './SettingsGroupSelector'; + +export default { + title: 'Admin/Settings/SettingsGroupSelector', + component: SettingsGroupSelector, + parameters: { + layout: 'fullscreen', + controls: { hideNoControlsWarning: true }, + }, +} as ComponentMeta; + +export const Default: ComponentStory = (args) => ; +Default.storyName = 'GroupSelector'; diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.tsx new file mode 100644 index 000000000000..79ea4513f6d0 --- /dev/null +++ b/apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.tsx @@ -0,0 +1,42 @@ +import type { GroupId } from '@rocket.chat/core-typings'; +import { useSettingStructure } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import SettingsGroupPageSkeleton from '../SettingsGroupPage/SettingsGroupPageSkeleton'; +import BaseGroupPage from '../groups/BaseGroupPage'; +import LDAPGroupPage from '../groups/LDAPGroupPage'; +import OAuthGroupPage from '../groups/OAuthGroupPage'; +import VoipGroupPage from '../groups/VoipGroupPage'; + +type SettingsGroupSelectorProps = { + groupId: GroupId; + onClickBack?: () => void; +}; + +const SettingsGroupSelector = ({ groupId, onClickBack }: SettingsGroupSelectorProps) => { + const group = useSettingStructure(groupId); + + if (!group) { + return ; + } + + if (groupId === 'OAuth') { + return ; + } + + if (groupId === 'LDAP') { + return ; + } + + if (groupId === 'Call_Center') { + return ; + } + + if (groupId === 'Assets') { + return ; + } + + return ; +}; + +export default SettingsGroupSelector; diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupSelector/index.ts b/apps/meteor/client/views/admin/settings/SettingsGroupSelector/index.ts new file mode 100644 index 000000000000..9b252bf843aa --- /dev/null +++ b/apps/meteor/client/views/admin/settings/SettingsGroupSelector/index.ts @@ -0,0 +1 @@ +export { default } from './SettingsGroupSelector'; diff --git a/apps/meteor/client/views/admin/settings/SettingsRoute.tsx b/apps/meteor/client/views/admin/settings/SettingsRoute.tsx index c03aced8b5a0..e119c5817417 100644 --- a/apps/meteor/client/views/admin/settings/SettingsRoute.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsRoute.tsx @@ -4,7 +4,7 @@ import React from 'react'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import EditableSettingsProvider from './EditableSettingsProvider'; -import GroupSelector from './GroupSelector'; +import SettingsGroupSelector from './SettingsGroupSelector'; import SettingsPage from './SettingsPage'; export const SettingsRoute = (): ReactElement => { @@ -22,7 +22,7 @@ export const SettingsRoute = (): ReactElement => { return ( - router.navigate('/admin/settings')} /> + router.navigate('/admin/settings')} /> ); }; diff --git a/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.stories.tsx b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.stories.tsx new file mode 100644 index 000000000000..14210a285c44 --- /dev/null +++ b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.stories.tsx @@ -0,0 +1,21 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import React from 'react'; + +import SettingsSection from './SettingsSection'; +import SettingsSectionSkeleton from './SettingsSectionSkeleton'; + +export default { + title: 'Admin/Settings/SettingsSection', + component: SettingsSection, + parameters: { + layout: 'fullscreen', + controls: { hideNoControlsWarning: true }, + }, +} as ComponentMeta; + +export const Default: ComponentStory = (args) => ; +Default.args = { + groupId: 'General', +}; + +export const Skeleton: ComponentStory = () => ; diff --git a/apps/meteor/client/views/admin/settings/Section.tsx b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx similarity index 86% rename from apps/meteor/client/views/admin/settings/Section.tsx rename to apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx index ef1baf47f165..d26d80e88637 100644 --- a/apps/meteor/client/views/admin/settings/Section.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx @@ -6,29 +6,30 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import React, { useMemo } from 'react'; -import { useEditableSettings, useEditableSettingsDispatch } from '../EditableSettingsContext'; -import SectionSkeleton from './SectionSkeleton'; -import Setting from './Setting'; +import { useEditableSettings, useEditableSettingsDispatch } from '../../EditableSettingsContext'; +import Setting from '../Setting'; -type SectionProps = { +type SettingsSectionProps = { groupId: string; hasReset?: boolean; sectionName: string; - tabName?: string; + currentTab?: string; solo: boolean; help?: ReactNode; children?: ReactNode; }; -function Section({ groupId, hasReset = true, sectionName, tabName = '', solo, help, children }: SectionProps): ReactElement { +function SettingsSection({ groupId, hasReset = true, sectionName, currentTab, solo, help, children }: SettingsSectionProps): ReactElement { + const t = useTranslation(); + const editableSettings = useEditableSettings( useMemo( () => ({ group: groupId, section: sectionName, - tab: tabName, + tab: currentTab, }), - [groupId, sectionName, tabName], + [groupId, sectionName, currentTab], ), ); @@ -65,8 +66,6 @@ function Section({ groupId, hasReset = true, sectionName, tabName = '', solo, he ); }); - const t = useTranslation(); - const handleResetSectionClick = (): void => { reset(); }; @@ -82,7 +81,6 @@ function Section({ groupId, hasReset = true, sectionName, tabName = '', solo, he {help} )} - {editableSettings.map( (setting) => isSetting(setting) && , @@ -104,6 +102,4 @@ function Section({ groupId, hasReset = true, sectionName, tabName = '', solo, he ); } -export default Object.assign(Section, { - Skeleton: SectionSkeleton, -}); +export default SettingsSection; diff --git a/apps/meteor/client/views/admin/settings/SectionSkeleton.tsx b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSectionSkeleton.tsx similarity index 68% rename from apps/meteor/client/views/admin/settings/SectionSkeleton.tsx rename to apps/meteor/client/views/admin/settings/SettingsSection/SettingsSectionSkeleton.tsx index ff509e08fc45..69466eba374f 100644 --- a/apps/meteor/client/views/admin/settings/SectionSkeleton.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSectionSkeleton.tsx @@ -2,9 +2,9 @@ import { Accordion, Box, FieldGroup, Skeleton } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; -import Setting from './Setting'; +import SettingSkeleton from '../Setting/SettingSkeleton'; -function SectionSkeleton(): ReactElement { +function SettingsSectionSkeleton(): ReactElement { return ( }> @@ -13,11 +13,11 @@ function SectionSkeleton(): ReactElement { {Array.from({ length: 10 }).map((_, i) => ( - + ))} ); } -export default SectionSkeleton; +export default SettingsSectionSkeleton; diff --git a/apps/meteor/client/views/admin/settings/SettingsSection/index.ts b/apps/meteor/client/views/admin/settings/SettingsSection/index.ts new file mode 100644 index 000000000000..09694b6d000d --- /dev/null +++ b/apps/meteor/client/views/admin/settings/SettingsSection/index.ts @@ -0,0 +1 @@ +export { default } from './SettingsSection'; diff --git a/apps/meteor/client/views/admin/settings/groups/AssetsGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/AssetsGroupPage.tsx deleted file mode 100644 index a5935eb47bc8..000000000000 --- a/apps/meteor/client/views/admin/settings/groups/AssetsGroupPage.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { ISetting } from '@rocket.chat/core-typings'; -import type { ReactElement } from 'react'; -import React, { memo } from 'react'; - -import { useEditableSettingsGroupSections } from '../../EditableSettingsContext'; -import GroupPage from '../GroupPage'; -import Section from '../Section'; - -type AssetsGroupPageProps = ISetting & { - onClickBack?: () => void; -}; - -function AssetsGroupPage({ _id, onClickBack, ...group }: AssetsGroupPageProps): ReactElement { - const sections = useEditableSettingsGroupSections(_id); - const solo = sections.length === 1; - - return ( - - {sections.map((sectionName) => ( -
    - ))} - - ); -} - -export default memo(AssetsGroupPage); diff --git a/apps/meteor/client/views/admin/settings/groups/BaseGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/BaseGroupPage.tsx new file mode 100644 index 000000000000..2b2a35b00a63 --- /dev/null +++ b/apps/meteor/client/views/admin/settings/groups/BaseGroupPage.tsx @@ -0,0 +1,28 @@ +import type { ReactElement } from 'react'; +import React from 'react'; + +import { useEditableSettingsGroupSections, useEditableSettingsGroupTabs } from '../../EditableSettingsContext'; +import GenericGroupPage from './GenericGroupPage'; +import TabbedGroupPage from './TabbedGroupPage'; + +type BaseGroupPageProps = { + _id: string; + i18nLabel: string; + headerButtons?: ReactElement; + hasReset?: boolean; + onClickBack?: () => void; +}; +const BaseGroupPage = ({ _id, i18nLabel, headerButtons, hasReset, onClickBack, ...props }: BaseGroupPageProps) => { + const tabs = useEditableSettingsGroupTabs(_id); + const sections = useEditableSettingsGroupSections(_id); + + if (tabs.length > 1) { + return ( + + ); + } + + return ; +}; + +export default BaseGroupPage; diff --git a/apps/meteor/client/views/admin/settings/groups/GenericGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/GenericGroupPage.tsx index c9148547b955..6856e1bf2f20 100644 --- a/apps/meteor/client/views/admin/settings/groups/GenericGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/GenericGroupPage.tsx @@ -1,25 +1,38 @@ -import type { ISetting } from '@rocket.chat/core-typings'; -import type { ReactElement } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import React, { memo } from 'react'; -import { useEditableSettingsGroupSections } from '../../EditableSettingsContext'; -import GroupPage from '../GroupPage'; -import Section from '../Section'; +import SettingsGroupPage from '../SettingsGroupPage'; +import Section from '../SettingsSection'; -type GenericGroupPageProps = ISetting & { +type GenericGroupPageProps = { + _id: string; + i18nLabel: string; + tabs?: ReactNode; + currentTab?: string; + hasReset?: boolean; + sections: string[]; + headerButtons?: ReactNode; onClickBack?: () => void; }; -function GenericGroupPage({ _id, onClickBack, ...props }: GenericGroupPageProps): ReactElement { - const sections = useEditableSettingsGroupSections(_id); +function GenericGroupPage({ + _id, + i18nLabel, + sections, + tabs, + currentTab, + hasReset, + onClickBack, + ...props +}: GenericGroupPageProps): ReactElement { const solo = sections.length === 1; return ( - + {sections.map((sectionName) => ( -
    +
    ))} - + ); } diff --git a/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx index a497738b9541..ae8fb0dabf2b 100644 --- a/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx @@ -8,13 +8,13 @@ import React, { memo, useMemo } from 'react'; import GenericModal from '../../../../components/GenericModal'; import { useExternalLink } from '../../../../hooks/useExternalLink'; import { useEditableSettings } from '../../EditableSettingsContext'; -import TabbedGroupPage from './TabbedGroupPage'; +import BaseGroupPage from './BaseGroupPage'; type LDAPGroupPageProps = ISetting & { onClickBack?: () => void; }; -function LDAPGroupPage({ _id, onClickBack, ...group }: LDAPGroupPageProps) { +function LDAPGroupPage({ _id, i18nLabel, onClickBack, ...group }: LDAPGroupPageProps) { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const testConnection = useEndpoint('POST', '/v1/ldap.testConnection'); @@ -129,8 +129,9 @@ function LDAPGroupPage({ _id, onClickBack, ...group }: LDAPGroupPageProps) { }; return ( - Promise; diff --git a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx similarity index 88% rename from apps/meteor/client/views/admin/settings/groups/OAuthGroupPage.tsx rename to apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx index 713a26935994..a858555e46d3 100644 --- a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx @@ -5,11 +5,11 @@ import { useToastMessageDispatch, useAbsoluteUrl, useMethod, useTranslation, use import type { ReactElement } from 'react'; import React, { memo, useEffect, useState } from 'react'; -import { strRight } from '../../../../../lib/utils/stringUtils'; -import GenericModal from '../../../../components/GenericModal'; -import { useEditableSettingsGroupSections } from '../../EditableSettingsContext'; -import GroupPage from '../GroupPage'; -import Section from '../Section'; +import { strRight } from '../../../../../../lib/utils/stringUtils'; +import GenericModal from '../../../../../components/GenericModal'; +import { useEditableSettingsGroupSections } from '../../../EditableSettingsContext'; +import SettingsGroupPage from '../../SettingsGroupPage'; +import SettingsSection from '../../SettingsSection'; import CreateOAuthModal from './CreateOAuthModal'; type OAuthGroupPageProps = ISetting & { @@ -94,7 +94,7 @@ function OAuthGroupPage({ _id, onClickBack, ...group }: OAuthGroupPageProps): Re }; return ( - -
    + ); } - return
    ; + return ; })} - + ); } diff --git a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/index.ts b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/index.ts new file mode 100644 index 000000000000..468abb7a3f98 --- /dev/null +++ b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/index.ts @@ -0,0 +1 @@ +export { default } from './OAuthGroupPage'; diff --git a/apps/meteor/client/views/admin/settings/groups/TabbedGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/TabbedGroupPage.tsx index eeecf9cc3800..782bf34122bc 100644 --- a/apps/meteor/client/views/admin/settings/groups/TabbedGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/TabbedGroupPage.tsx @@ -1,54 +1,47 @@ -import type { ISetting } from '@rocket.chat/core-typings'; -import { Tabs } from '@rocket.chat/fuselage'; +import { Tabs, TabsItem } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo, useState, useMemo } from 'react'; -import { useEditableSettingsGroupSections, useEditableSettingsGroupTabs } from '../../EditableSettingsContext'; -import GroupPage from '../GroupPage'; -import Section from '../Section'; +import { useEditableSettingsGroupSections } from '../../EditableSettingsContext'; import GenericGroupPage from './GenericGroupPage'; -type TabbedGroupPageProps = ISetting & { +type TabbedGroupPageProps = { headerButtons?: ReactElement; + _id: string; + i18nLabel: string; + tabs: string[]; onClickBack?: () => void; }; -function TabbedGroupPage({ _id, onClickBack, ...props }: TabbedGroupPageProps): JSX.Element { +function TabbedGroupPage({ _id, tabs, i18nLabel, onClickBack, ...props }: TabbedGroupPageProps) { const t = useTranslation(); - const tabs = useEditableSettingsGroupTabs(_id); - const [tab, setTab] = useState(tabs[0]); - const handleTabClick = useMemo(() => (tab: string) => (): void => setTab(tab), [setTab]); - const sections = useEditableSettingsGroupSections(_id, tab); - - const solo = sections.length === 1; - - if (!tabs.length || (tabs.length === 1 && !tabs[0])) { - return ; - } - - if (!tab && tabs[0]) { - setTab(tabs[0]); - } + const [currentTab, setCurrentTab] = useState(tabs[0]); + const handleTabClick = useMemo(() => (tab: string) => (): void => setCurrentTab(tab), [setCurrentTab]); + const sections = useEditableSettingsGroupSections(_id, currentTab); const tabsComponent = ( {tabs.map((tabName) => ( - + {tabName ? t(tabName as TranslationKey) : t(_id as TranslationKey)} - + ))} ); return ( - - {sections.map((sectionName) => ( -
    - ))} - + ); } diff --git a/apps/meteor/client/views/admin/settings/groups/voip/AssignAgentButton.tsx b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentButton.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/groups/voip/AssignAgentButton.tsx rename to apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentButton.tsx diff --git a/apps/meteor/client/views/admin/settings/groups/voip/AssignAgentModal.tsx b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentModal.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/groups/voip/AssignAgentModal.tsx rename to apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentModal.tsx diff --git a/apps/meteor/client/views/admin/settings/groups/voip/RemoveAgentButton.tsx b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/RemoveAgentButton.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/groups/voip/RemoveAgentButton.tsx rename to apps/meteor/client/views/admin/settings/groups/VoipGroupPage/RemoveAgentButton.tsx diff --git a/apps/meteor/client/views/admin/settings/groups/voip/VoipExtensionsPage.tsx b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/VoipExtensionsPage.tsx similarity index 100% rename from apps/meteor/client/views/admin/settings/groups/voip/VoipExtensionsPage.tsx rename to apps/meteor/client/views/admin/settings/groups/VoipGroupPage/VoipExtensionsPage.tsx diff --git a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/VoipGroupPage.tsx similarity index 70% rename from apps/meteor/client/views/admin/settings/groups/VoipGroupPage.tsx rename to apps/meteor/client/views/admin/settings/groups/VoipGroupPage/VoipGroupPage.tsx index 3b7c873f2268..1056bd47c3d5 100644 --- a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/VoipGroupPage.tsx @@ -4,12 +4,12 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo, useMemo, useState } from 'react'; -import GenericNoResults from '../../../../components/GenericNoResults'; -import { PageScrollableContentWithShadow } from '../../../../components/Page'; -import { useEditableSettingsGroupSections } from '../../EditableSettingsContext'; -import GroupPage from '../GroupPage'; -import Section from '../Section'; -import VoipExtensionsPage from './voip/VoipExtensionsPage'; +import GenericNoResults from '../../../../../components/GenericNoResults'; +import { PageScrollableContentWithShadow } from '../../../../../components/Page'; +import { useEditableSettingsGroupSections } from '../../../EditableSettingsContext'; +import SettingsGroupPage from '../../SettingsGroupPage'; +import SettingsSection from '../../SettingsSection'; +import VoipExtensionsPage from './VoipExtensionsPage'; type VoipGroupPageProps = ISetting & { onClickBack?: () => void; @@ -44,13 +44,13 @@ function VoipGroupPage({ _id, onClickBack, ...group }: VoipGroupPageProps) { voipEnabled ? ( ) : ( - + ), [t, voipEnabled], ); return ( - + {tab === 'Extensions' ? ( ExtensionsPageComponent ) : ( @@ -58,13 +58,13 @@ function VoipGroupPage({ _id, onClickBack, ...group }: VoipGroupPageProps) { {sections.map((sectionName) => ( -
    + ))} )} - + ); } diff --git a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/index.ts b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/index.ts new file mode 100644 index 000000000000..4a658a36e572 --- /dev/null +++ b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/index.ts @@ -0,0 +1 @@ +export { default } from './VoipGroupPage'; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSetting.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSetting.tsx index 7269a07e9b3d..8e6d297e433e 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSetting.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSetting.tsx @@ -7,7 +7,7 @@ import { Controller, useFormContext } from 'react-hook-form'; import { Utilities } from '../../../../../../ee/lib/misc/Utilities'; import MarkdownText from '../../../../../components/MarkdownText'; -import MemoizedSetting from '../../../../admin/settings/MemoizedSetting'; +import MemoizedSetting from '../../../../admin/settings/Setting/MemoizedSetting'; type AppTranslationFunction = { (key: string, ...replaces: unknown[]): string; From f20be47bab3f6f83dfa4fd7bb750b5f4f5768955 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Tue, 20 Aug 2024 19:50:21 +0530 Subject: [PATCH 27/49] fix: deactivate federated internal users permanently with the external user (#32809) --- .changeset/new-mayflies-wait.md | 5 +++++ .../server/functions/setUserActiveStatus.ts | 19 ++++++++++++++++++- .../local-services/federation/service.ts | 4 ++++ .../federation/domain/IFederationBridge.ts | 1 + .../infrastructure/matrix/Bridge.ts | 13 +++++++++++++ .../server/services/federation/service.ts | 8 ++++++++ .../src/types/IFederationService.ts | 4 ++++ 7 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .changeset/new-mayflies-wait.md diff --git a/.changeset/new-mayflies-wait.md b/.changeset/new-mayflies-wait.md new file mode 100644 index 000000000000..832db68cecd4 --- /dev/null +++ b/.changeset/new-mayflies-wait.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Deactivating users who federated will now be permanent. diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index e3104db280dd..9d7a3e113fc4 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -1,6 +1,7 @@ +import { Federation, FederationEE, License } from '@rocket.chat/core-services'; import type { IUser, IUserEmail } from '@rocket.chat/core-typings'; import { isUserFederated, isDirectMessageRoom } from '@rocket.chat/core-typings'; -import { Rooms, Users, Subscriptions } from '@rocket.chat/models'; +import { Rooms, Users, Subscriptions, MatrixBridgedUser } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -58,6 +59,22 @@ export async function setUserActiveStatus(userId: string, active: boolean, confi }); } + if (user.active !== active) { + const remoteUser = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId); + + if (remoteUser) { + if (active) { + throw new Meteor.Error('error-not-allowed', 'Deactivated federated users can not be re-activated', { + method: 'setUserActiveStatus', + }); + } + + const federation = (await License.hasValidLicense()) ? FederationEE : Federation; + + await federation.deactivateRemoteUser(remoteUser); + } + } + // Users without username can't do anything, so there is no need to check for owned rooms if (user.username != null && !active) { const userAdmin = await Users.findOneAdmin(userId || ''); diff --git a/apps/meteor/ee/server/local-services/federation/service.ts b/apps/meteor/ee/server/local-services/federation/service.ts index 15f661a29e63..6397f01ee9ac 100644 --- a/apps/meteor/ee/server/local-services/federation/service.ts +++ b/apps/meteor/ee/server/local-services/federation/service.ts @@ -215,4 +215,8 @@ export class FederationServiceEE extends AbstractBaseFederationServiceEE impleme async stopped(): Promise { return super.stopped(); } + + async deactivateRemoteUser(userId: string) { + return super.deactivateRemoteUser(userId); + } } diff --git a/apps/meteor/server/services/federation/domain/IFederationBridge.ts b/apps/meteor/server/services/federation/domain/IFederationBridge.ts index 62036dfd817a..635202cdd6f4 100644 --- a/apps/meteor/server/services/federation/domain/IFederationBridge.ts +++ b/apps/meteor/server/services/federation/domain/IFederationBridge.ts @@ -110,4 +110,5 @@ export interface IFederationBridge { externalUserId: string, externalRoomId: string, ): Promise<{ creator: { id: string; username: string }; name: string; joinedMembers: string[] } | undefined>; + deactivateUser(externalUserId: string): Promise; } diff --git a/apps/meteor/server/services/federation/infrastructure/matrix/Bridge.ts b/apps/meteor/server/services/federation/infrastructure/matrix/Bridge.ts index 88090b34686d..f5eb049a7496 100644 --- a/apps/meteor/server/services/federation/infrastructure/matrix/Bridge.ts +++ b/apps/meteor/server/services/federation/infrastructure/matrix/Bridge.ts @@ -752,4 +752,17 @@ export class MatrixBridge implements IFederationBridge { 'de.sorunome.msc2409.push_ephemeral': registrationFile.enableEphemeralEvents, }; } + + public async deactivateUser(uid: string) { + /* + * https://spec.matrix.org/v1.11/client-server-api/#post_matrixclientv3accountdeactivate + * Using { erase: false } since rocket.chat side on deactivation we do not delete anything. + */ + const resp = await this.bridgeInstance + .getIntent() + .matrixClient.doRequest('POST', '/_matrix/client/v3/account/deactivate', { user_id: uid }, { erase: false }); + if (resp.id_server_unbind_result !== 'success') { + throw new Error('Failed to deactivate matrix user'); + } + } } diff --git a/apps/meteor/server/services/federation/service.ts b/apps/meteor/server/services/federation/service.ts index c25fd0d3a5a1..66d3fd0cb6ee 100644 --- a/apps/meteor/server/services/federation/service.ts +++ b/apps/meteor/server/services/federation/service.ts @@ -238,6 +238,10 @@ export abstract class AbstractFederationService extends ServiceClassInternal { protected async verifyMatrixIds(matrixIds: string[]): Promise> { return this.bridge.verifyInviteeIds(matrixIds); } + + protected async deactivateRemoteUser(remoteUserId: string) { + return this.bridge.deactivateUser(remoteUserId); + } } abstract class AbstractBaseFederationService extends AbstractFederationService { @@ -342,4 +346,8 @@ export class FederationService extends AbstractBaseFederationService implements public async created(): Promise { return super.created(); } + + public async deactivateRemoteUser(userId: string) { + return super.deactivateRemoteUser(userId); + } } diff --git a/packages/core-services/src/types/IFederationService.ts b/packages/core-services/src/types/IFederationService.ts index a30b03717822..5563bd60db40 100644 --- a/packages/core-services/src/types/IFederationService.ts +++ b/packages/core-services/src/types/IFederationService.ts @@ -4,6 +4,8 @@ export interface IFederationService { createDirectMessageRoomAndInviteUser(internalInviterId: string, internalRoomId: string, externalInviteeId: string): Promise; verifyMatrixIds(matrixIds: string[]): Promise>; + + deactivateRemoteUser(userId: string): Promise; } export interface IFederationJoinExternalPublicRoomInput { @@ -38,4 +40,6 @@ export interface IFederationServiceEE { joinExternalPublicRoom(input: IFederationJoinExternalPublicRoomInput): Promise; verifyMatrixIds(matrixIds: string[]): Promise>; + + deactivateRemoteUser(userId: string): Promise; } From 0e749e272c6ae9a8cfa284b27300b63abd81b7ee Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:30:22 -0300 Subject: [PATCH 28/49] fix: Following/unfollowing a message when the thread is closed does not update the UI (#32981) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .changeset/ten-bulldogs-clap.md | 5 ++ .../app/lib/server/lib/notifyListener.ts | 3 - .../threads/server/methods/followMessage.ts | 9 ++- .../threads/server/methods/unfollowMessage.ts | 9 ++- apps/meteor/tests/e2e/message-actions.spec.ts | 65 +++++++++++++++++++ 5 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 .changeset/ten-bulldogs-clap.md diff --git a/.changeset/ten-bulldogs-clap.md b/.changeset/ten-bulldogs-clap.md new file mode 100644 index 000000000000..15f88bb6bd97 --- /dev/null +++ b/.changeset/ten-bulldogs-clap.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed an issue with the "follow message" button not changing state after click diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index 83ab5774374a..934742945f2d 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -461,9 +461,6 @@ export async function getMessageToBroadcast({ id, data }: { id: IMessage['_id']; } export const notifyOnMessageChange = withDbWatcherCheck(async ({ id, data }: { id: IMessage['_id']; data?: IMessage }): Promise => { - if (!dbWatchersDisabled) { - return; - } const message = await getMessageToBroadcast({ id, data }); if (!message) { return; diff --git a/apps/meteor/app/threads/server/methods/followMessage.ts b/apps/meteor/app/threads/server/methods/followMessage.ts index 1790e0607a62..8ed7093e00d4 100644 --- a/apps/meteor/app/threads/server/methods/followMessage.ts +++ b/apps/meteor/app/threads/server/methods/followMessage.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { RateLimiter } from '../../../lib/server'; +import { notifyOnMessageChange } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { follow } from '../functions'; @@ -41,7 +42,13 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' }); } - const followResult = await follow({ tmid: message.tmid || message._id, uid }); + const id = message.tmid || message._id; + + const followResult = await follow({ tmid: id, uid }); + + void notifyOnMessageChange({ + id, + }); const isFollowed = true; await Apps.self?.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed); diff --git a/apps/meteor/app/threads/server/methods/unfollowMessage.ts b/apps/meteor/app/threads/server/methods/unfollowMessage.ts index 6371f40af6cb..de4f2683be41 100644 --- a/apps/meteor/app/threads/server/methods/unfollowMessage.ts +++ b/apps/meteor/app/threads/server/methods/unfollowMessage.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { RateLimiter } from '../../../lib/server'; +import { notifyOnMessageChange } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { unfollow } from '../functions'; @@ -41,7 +42,13 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'unfollowMessage' }); } - const unfollowResult = await unfollow({ rid: message.rid, tmid: message.tmid || message._id, uid }); + const id = message.tmid || message._id; + + const unfollowResult = await unfollow({ rid: message.rid, tmid: id, uid }); + + void notifyOnMessageChange({ + id, + }); const isFollowed = false; await Apps.self?.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed); diff --git a/apps/meteor/tests/e2e/message-actions.spec.ts b/apps/meteor/tests/e2e/message-actions.spec.ts index bc916af89bae..8c84d6205e8c 100644 --- a/apps/meteor/tests/e2e/message-actions.spec.ts +++ b/apps/meteor/tests/e2e/message-actions.spec.ts @@ -38,6 +38,71 @@ test.describe.serial('message-actions', () => { await expect(poHomeChannel.tabs.flexTabViewThreadMessage).toHaveText('this is a reply message'); }); + + // with thread open we listen to the subscription and update the collection from there + test('expect follow/unfollow message with thread open', async ({ page }) => { + await test.step('start thread', async () => { + await poHomeChannel.content.sendMessage('this is a message for reply'); + await page.locator('[data-qa-type="message"]').last().hover(); + await page.locator('role=button[name="Reply in thread"]').click(); + await page.getByRole('dialog').locator(`role=textbox[name="Message #${targetChannel}"]`).fill('this is a reply message'); + await page.keyboard.press('Enter'); + await expect(poHomeChannel.tabs.flexTabViewThreadMessage).toHaveText('this is a reply message'); + }); + + await test.step('unfollow thread', async () => { + const unFollowButton = page + .locator('[data-qa-type="message"]', { has: page.getByRole('button', { name: 'Following' }) }) + .last() + .getByRole('button', { name: 'Following' }); + await expect(unFollowButton).toBeVisible(); + await unFollowButton.click(); + }); + + await test.step('follow thread', async () => { + const followButton = page + .locator('[data-qa-type="message"]', { has: page.getByRole('button', { name: 'Not following' }) }) + .last() + .getByRole('button', { name: 'Not following' }); + await expect(followButton).toBeVisible(); + await followButton.click(); + await expect( + page + .locator('[data-qa-type="message"]', { has: page.getByRole('button', { name: 'Following' }) }) + .last() + .getByRole('button', { name: 'Following' }), + ).toBeVisible(); + }); + }); + + // with thread closed we depend on message changed updates + test('expect follow/unfollow message with thread closed', async ({ page }) => { + await test.step('start thread', async () => { + await poHomeChannel.content.sendMessage('this is a message for reply'); + await page.locator('[data-qa-type="message"]').last().hover(); + await page.locator('role=button[name="Reply in thread"]').click(); + await page.locator('.rcx-vertical-bar').locator(`role=textbox[name="Message #${targetChannel}"]`).fill('this is a reply message'); + await page.keyboard.press('Enter'); + await expect(poHomeChannel.tabs.flexTabViewThreadMessage).toHaveText('this is a reply message'); + }); + + // close thread before testing because the behavior changes + await page.getByRole('dialog').getByRole('button', { name: 'Close', exact: true }).click(); + + await test.step('unfollow thread', async () => { + const unFollowButton = page.locator('[data-qa-type="message"]').last().getByTitle('Following'); + await expect(unFollowButton).toBeVisible(); + await unFollowButton.click(); + }); + + await test.step('follow thread', async () => { + const followButton = page.locator('[data-qa-type="message"]').last().getByTitle('Not following'); + await expect(followButton).toBeVisible(); + await followButton.click(); + await expect(page.locator('[data-qa-type="message"]').last().getByTitle('Following')).toBeVisible(); + }); + }); + test('expect edit the message', async ({ page }) => { await poHomeChannel.content.sendMessage('This is a message to edit'); await poHomeChannel.content.openLastMessageMenu(); From 51f35b444009f1d9d6f94360eeb0182ff6160cfa Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:43:44 -0300 Subject: [PATCH 29/49] chore: Visitors' messages are being counted as agents' responses in livechat metrics (#33022) --- .../server/hooks/saveAnalyticsData.ts | 10 ++++++--- .../meteor/server/models/raw/LivechatRooms.ts | 22 +++++-------------- .../src/models/ILivechatRoomsModel.ts | 9 ++++++-- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index fef6ad0936f8..109f49f440b5 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts @@ -1,4 +1,4 @@ -import { isEditedMessage } from '@rocket.chat/core-typings'; +import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings'; import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; @@ -70,8 +70,12 @@ callbacks.add( message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } }; } - const analyticsData = getAnalyticsData(room, new Date()); - await LivechatRooms.getAnalyticsUpdateQueryByRoomId(room, message, analyticsData, roomUpdater); + if (isMessageFromVisitor(message)) { + LivechatRooms.getAnalyticsUpdateQueryBySentByVisitor(room, message, roomUpdater); + } else { + const analyticsData = getAnalyticsData(room, new Date()); + LivechatRooms.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData, roomUpdater); + } return message; }, diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index ebe10ec67fbc..731cbcebf593 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -9,7 +9,7 @@ import type { ReportResult, MACStats, } from '@rocket.chat/core-typings'; -import { isMessageFromVisitor, UserStatus } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; import type { ILivechatRoomsModel } from '@rocket.chat/model-typings'; import type { Updater } from '@rocket.chat/models'; import { Settings } from '@rocket.chat/models'; @@ -2010,7 +2010,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return updater; } - private getAnalyticsUpdateQueryBySentByAgent( + getAnalyticsUpdateQueryBySentByAgent( room: IOmnichannelRoom, message: IMessage, analyticsData: Record | undefined, @@ -2027,10 +2027,9 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.getAnalyticsUpdateQuery(analyticsData, updater); } - private getAnalyticsUpdateQueryBySentByVisitor( + getAnalyticsUpdateQueryBySentByVisitor( room: IOmnichannelRoom, message: IMessage, - analyticsData: Record | undefined, updater: Updater = this.getUpdater(), ) { // livechat analytics : update last message timestamps @@ -2039,21 +2038,10 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive // update visitor timestamp, only if its new inquiry and not continuing message if (agentLastReply >= visitorLastQuery) { - return this.getAnalyticsUpdateQuery(analyticsData, updater).set('metrics.v.lq', message.ts); + return updater.set('metrics.v.lq', message.ts); } - return this.getAnalyticsUpdateQuery(analyticsData, updater); - } - - async getAnalyticsUpdateQueryByRoomId( - room: IOmnichannelRoom, - message: IMessage, - analyticsData: Record | undefined, - updater: Updater = this.getUpdater(), - ) { - return isMessageFromVisitor(message) - ? this.getAnalyticsUpdateQueryBySentByVisitor(room, message, analyticsData, updater) - : this.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData, updater); + return updater; } getTotalConversationsBetweenDate(t: 'l', date: { gte: Date; lt: Date }, { departmentId }: { departmentId?: string } = {}) { diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index babfa4ea2165..3a9eb98d57c4 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -214,12 +214,17 @@ export interface ILivechatRoomsModel extends IBaseModel { ): Updater; getNotResponseByRoomIdUpdateQuery(updater: Updater): Updater; getAgentLastMessageTsUpdateQuery(updater?: Updater): Updater; - getAnalyticsUpdateQueryByRoomId( + getAnalyticsUpdateQueryBySentByAgent( room: IOmnichannelRoom, message: IMessage, analyticsData: Record | undefined, updater?: Updater, - ): Promise>; + ): Updater; + getAnalyticsUpdateQueryBySentByVisitor( + room: IOmnichannelRoom, + message: IMessage, + updater?: Updater, + ): Updater; getTotalConversationsBetweenDate(t: 'l', date: { gte: Date; lt: Date }, data?: { departmentId: string }): Promise; getAnalyticsMetricsBetweenDate( t: 'l', From d828b44c18b728ad2a7211b8408989a0acc9e525 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 20 Aug 2024 15:04:47 -0300 Subject: [PATCH 30/49] test(Omnichannel): Fix department flaky test (#33091) Co-authored-by: Kevin Aleman --- .../omnichannel-departaments.spec.ts | 146 ++++++++---------- 1 file changed, 68 insertions(+), 78 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts index 2d96aef8e365..872eafdfb2a2 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts @@ -22,101 +22,102 @@ test.describe('OC - Manage Departments', () => { test.beforeAll(async ({ api }) => { // turn on department removal - const statusCode = (await api.post('/settings/Omnichannel_enable_department_removal', { value: true })).status(); - await expect(statusCode).toBe(200); + await api.post('/settings/Omnichannel_enable_department_removal', { value: true }); }); test.afterAll(async ({ api }) => { // turn off department removal - const statusCode = (await api.post('/settings/Omnichannel_enable_department_removal', { value: false })).status(); - await expect(statusCode).toBe(200); + await api.post('/settings/Omnichannel_enable_department_removal', { value: false }); }); - test.beforeEach(async ({ page }: { page: Page }) => { - poOmnichannelDepartments = new OmnichannelDepartments(page); + test.describe('Create first department', async () => { + test.beforeEach(async ({ page }: { page: Page }) => { + poOmnichannelDepartments = new OmnichannelDepartments(page); - await page.goto('/omnichannel'); - await poOmnichannelDepartments.sidenav.linkDepartments.click(); - }); - - test('Create department', async () => { - const departmentName = faker.string.uuid(); + await page.goto('/omnichannel'); + await poOmnichannelDepartments.sidenav.linkDepartments.click(); + }); - await poOmnichannelDepartments.headingButtonNew('Create department').click(); + test('Create department', async () => { + const departmentName = faker.string.uuid(); - await test.step('expect name and email to be required', async () => { - await expect(poOmnichannelDepartments.invalidInputEmail).not.toBeVisible(); - await poOmnichannelDepartments.inputName.fill('any_text'); - await poOmnichannelDepartments.inputName.fill(''); - await expect(poOmnichannelDepartments.invalidInputName).toBeVisible(); - await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredName)).toBeVisible(); - await poOmnichannelDepartments.inputName.fill('any_text'); - await expect(poOmnichannelDepartments.invalidInputName).not.toBeVisible(); + await poOmnichannelDepartments.headingButtonNew('Create department').click(); - await poOmnichannelDepartments.inputEmail.fill('any_text'); - await expect(poOmnichannelDepartments.invalidInputEmail).toBeVisible(); - await expect(poOmnichannelDepartments.errorMessage(ERROR.invalidEmail)).toBeVisible(); + await test.step('expect name and email to be required', async () => { + await expect(poOmnichannelDepartments.invalidInputEmail).not.toBeVisible(); + await poOmnichannelDepartments.inputName.fill('any_text'); + await poOmnichannelDepartments.inputName.fill(''); + await expect(poOmnichannelDepartments.invalidInputName).toBeVisible(); + await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredName)).toBeVisible(); + await poOmnichannelDepartments.inputName.fill('any_text'); + await expect(poOmnichannelDepartments.invalidInputName).not.toBeVisible(); - await poOmnichannelDepartments.inputEmail.fill(''); - await expect(poOmnichannelDepartments.invalidInputEmail).toBeVisible(); - await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).toBeVisible(); + await poOmnichannelDepartments.inputEmail.fill('any_text'); + await expect(poOmnichannelDepartments.invalidInputEmail).toBeVisible(); + await expect(poOmnichannelDepartments.errorMessage(ERROR.invalidEmail)).toBeVisible(); - await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); - await expect(poOmnichannelDepartments.invalidInputEmail).not.toBeVisible(); - await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).not.toBeVisible(); - }); + await poOmnichannelDepartments.inputEmail.fill(''); + await expect(poOmnichannelDepartments.invalidInputEmail).toBeVisible(); + await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).toBeVisible(); - await test.step('expect create new department', async () => { - await poOmnichannelDepartments.btnEnabled.click(); - await poOmnichannelDepartments.inputName.fill(departmentName); - await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); - await poOmnichannelDepartments.btnSave.click(); - await poOmnichannelDepartments.btnCloseToastSuccess.click(); - - await poOmnichannelDepartments.search(departmentName); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); - }); + await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); + await expect(poOmnichannelDepartments.invalidInputEmail).not.toBeVisible(); + await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).not.toBeVisible(); + }); - await test.step('expect to delete department', async () => { - await poOmnichannelDepartments.search(departmentName); - await poOmnichannelDepartments.selectedDepartmentMenu(departmentName).click(); - await poOmnichannelDepartments.menuDeleteOption.click(); + await test.step('expect create new department', async () => { + await poOmnichannelDepartments.btnEnabled.click(); + await poOmnichannelDepartments.inputName.fill(departmentName); + await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); + await poOmnichannelDepartments.btnSave.click(); - await test.step('expect confirm delete department', async () => { - await expect(poOmnichannelDepartments.modalConfirmDelete).toBeVisible(); + await poOmnichannelDepartments.search(departmentName); + await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); + }); - await test.step('expect delete to be disabled when name is incorrect', async () => { - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); - await poOmnichannelDepartments.inputModalConfirmDelete.fill('someramdomname'); - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); + await test.step('expect to delete department', async () => { + await poOmnichannelDepartments.search(departmentName); + await poOmnichannelDepartments.selectedDepartmentMenu(departmentName).click(); + await poOmnichannelDepartments.menuDeleteOption.click(); + + await test.step('expect confirm delete department', async () => { + await test.step('expect delete to be disabled when name is incorrect', async () => { + await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); + await poOmnichannelDepartments.inputModalConfirmDelete.fill('someramdomname'); + await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); + }); + + await test.step('expect to successfuly delete if department name is correct', async () => { + await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); + await poOmnichannelDepartments.inputModalConfirmDelete.fill(departmentName); + await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeEnabled(); + await poOmnichannelDepartments.btnModalConfirmDelete.click(); + }); }); - await test.step('expect to successfuly delete if department name is correct', async () => { - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); - await poOmnichannelDepartments.inputModalConfirmDelete.fill(departmentName); - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeEnabled(); - await poOmnichannelDepartments.btnModalConfirmDelete.click(); + await test.step('expect department to have been deleted', async () => { + await poOmnichannelDepartments.search(departmentName); + await expect(poOmnichannelDepartments.firstRowInTable).toHaveCount(0); }); }); - - await test.step('expect department to have been deleted', async () => { - await poOmnichannelDepartments.search(departmentName); - await expect(poOmnichannelDepartments.firstRowInTable).toHaveCount(0); - }); }); }); test.describe('After creation', async () => { let department: Awaited>['data']; - test.beforeEach(async ({ api }) => { + + test.beforeEach(async ({ api, page }) => { + poOmnichannelDepartments = new OmnichannelDepartments(page); + department = await createDepartment(api).then((res) => res.data); + await page.goto('/omnichannel/departments'); }); test.afterEach(async ({ api }) => { await deleteDepartment(api, { id: department._id }); }); - test('Edit department', async ({ api }) => { + test('Edit department', async () => { await test.step('expect create new department', async () => { await poOmnichannelDepartments.search(department.name); await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); @@ -132,19 +133,13 @@ test.describe('OC - Manage Departments', () => { await poOmnichannelDepartments.inputName.fill(`edited-${department.name}`); await poOmnichannelDepartments.btnSave.click(); - await poOmnichannelDepartments.btnCloseToastSuccess.click(); await poOmnichannelDepartments.search(`edited-${department.name}`); await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); }); - - await test.step('expect to delete department', async () => { - const deleteRes = await deleteDepartment(api, { id: department._id }); - await expect(deleteRes.status()).toBe(200); - }); }); - test('Archive department', async ({ api }) => { + test('Archive department', async () => { await test.step('expect create new department', async () => { await poOmnichannelDepartments.search(department.name); await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); @@ -172,11 +167,6 @@ test.describe('OC - Manage Departments', () => { await poOmnichannelDepartments.menuUnarchiveOption.click(); await expect(poOmnichannelDepartments.firstRowInTable).toHaveCount(0); }); - - await test.step('expect to delete department', async () => { - const deleteRes = await deleteDepartment(api, { id: department._id }); - await expect(deleteRes.status()).toBe(200); - }); }); test('Request tag(s) before closing conversation', async () => { @@ -269,7 +259,7 @@ test.describe('OC - Manage Departments', () => { await test.step('expect to disable department removal setting', async () => { const statusCode = (await api.post('/settings/Omnichannel_enable_department_removal', { value: false })).status(); - await expect(statusCode).toBe(200); + expect(statusCode).toBe(200); }); await test.step('expect not to be able to delete department', async () => { @@ -280,12 +270,12 @@ test.describe('OC - Manage Departments', () => { await test.step('expect to enable department removal setting', async () => { const statusCode = (await api.post('/settings/Omnichannel_enable_department_removal', { value: true })).status(); - await expect(statusCode).toBe(200); + expect(statusCode).toBe(200); }); await test.step('expect to delete department', async () => { const deleteRes = await deleteDepartment(api, { id: department._id }); - await expect(deleteRes.status()).toBe(200); + expect(deleteRes.status()).toBe(200); }); }); }); From 51f2fc22feb12816acc4fe65d43a46e9bc7eb49b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 20 Aug 2024 14:27:39 -0600 Subject: [PATCH 31/49] fix: `processRoomAbandonment` callback not processing data correctly (#33036) --- .changeset/spicy-kings-think.md | 6 + .../server/hooks/processRoomAbandonment.ts | 111 ++-- .../hooks/processRoomAbandonment.spec.ts | 623 ++++++++++++++++++ 3 files changed, 695 insertions(+), 45 deletions(-) create mode 100644 .changeset/spicy-kings-think.md create mode 100644 apps/meteor/tests/unit/app/livechat/server/hooks/processRoomAbandonment.spec.ts diff --git a/.changeset/spicy-kings-think.md b/.changeset/spicy-kings-think.md new file mode 100644 index 000000000000..9e8f3648b28c --- /dev/null +++ b/.changeset/spicy-kings-think.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes multiple problems with the `processRoomAbandonment` hook. This hook is in charge of calculating the time a room has been abandoned (this means, the time that elapsed since a room was last answered by an agent until it was closed). However, when business hours were enabled and the user didn't open on one day, if an abandoned room happened to be abandoned _over_ the day there was no business hour configuration, then the process will error out. +Additionally, the values the code was calculating were not right. When business hours are enabled, this code should only count the abandonment time _while a business hour was open_. When rooms were left abandoned for days or weeks, this will also throw an error or output an invalid count. diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts index 8eb53fbb8fa7..a6031bd42efa 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts @@ -6,11 +6,12 @@ import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; +import type { CloseRoomParams } from '../lib/localTypes'; -const getSecondsWhenOfficeHoursIsDisabled = (room: IOmnichannelRoom, agentLastMessage: IMessage) => +export const getSecondsWhenOfficeHoursIsDisabled = (room: IOmnichannelRoom, agentLastMessage: IMessage) => moment(new Date(room.closedAt || new Date())).diff(moment(new Date(agentLastMessage.ts)), 'seconds'); -const parseDays = ( +export const parseDays = ( acc: Record, day: IBusinessHourWorkHour, ) => { @@ -22,7 +23,7 @@ const parseDays = ( return acc; }; -const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLastMessage: IMessage) => { +export const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLastMessage: IMessage) => { if (!settings.get('Livechat_enable_business_hours')) { return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); } @@ -49,65 +50,85 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas } let totalSeconds = 0; - const endOfConversation = moment(new Date(room.closedAt || new Date())); - const startOfInactivity = moment(new Date(agentLastMessage.ts)); + const endOfConversation = moment.utc(new Date(room.closedAt || new Date())); + const startOfInactivity = moment.utc(new Date(agentLastMessage.ts)); const daysOfInactivity = endOfConversation.clone().startOf('day').diff(startOfInactivity.clone().startOf('day'), 'days'); - const inactivityDay = moment(new Date(agentLastMessage.ts)); + const inactivityDay = moment.utc(new Date(agentLastMessage.ts)); + for (let index = 0; index <= daysOfInactivity; index++) { const today = inactivityDay.clone().format('dddd'); const officeDay = officeDays[today]; - // Config doesnt have data for this day, we skip day if (!officeDay) { inactivityDay.add(1, 'days'); continue; } - const startTodaysOfficeHour = moment(`${officeDay.start.day}:${officeDay.start.time}`, 'dddd:HH:mm').add(index, 'days'); - const endTodaysOfficeHour = moment(`${officeDay.finish.day}:${officeDay.finish.time}`, 'dddd:HH:mm').add(index, 'days'); - if (officeDays[today].open) { - const firstDayOfInactivity = startOfInactivity.clone().format('D') === inactivityDay.clone().format('D'); - const lastDayOfInactivity = endOfConversation.clone().format('D') === inactivityDay.clone().format('D'); - - if (!firstDayOfInactivity && !lastDayOfInactivity) { - totalSeconds += endTodaysOfficeHour.clone().diff(startTodaysOfficeHour, 'seconds'); - } else { - const end = endOfConversation.isBefore(endTodaysOfficeHour) ? endOfConversation : endTodaysOfficeHour; - const start = firstDayOfInactivity ? inactivityDay : startTodaysOfficeHour; - totalSeconds += end.clone().diff(start, 'seconds'); - } + if (!officeDay.open) { + inactivityDay.add(1, 'days'); + continue; + } + if (!officeDay?.start?.time || !officeDay?.finish?.time) { + inactivityDay.add(1, 'days'); + continue; } - inactivityDay.add(1, 'days'); - } - return totalSeconds; -}; -callbacks.add( - 'livechat.closeRoom', - async (params) => { - const { room } = params; + const [officeStartHour, officeStartMinute] = officeDay.start.time.split(':'); + const [officeCloseHour, officeCloseMinute] = officeDay.finish.time.split(':'); + // We should only take in consideration the time where the office is open and the conversation was inactive + const todayStartOfficeHours = inactivityDay + .clone() + .set({ hour: parseInt(officeStartHour, 10), minute: parseInt(officeStartMinute, 10) }); + const todayEndOfficeHours = inactivityDay.clone().set({ hour: parseInt(officeCloseHour, 10), minute: parseInt(officeCloseMinute, 10) }); - if (!isOmnichannelRoom(room)) { - return params; + // 1: Room was inactive the whole day, we add the whole time BH is inactive + if (startOfInactivity.isBefore(todayStartOfficeHours) && endOfConversation.isAfter(todayEndOfficeHours)) { + totalSeconds += todayEndOfficeHours.diff(todayStartOfficeHours, 'seconds'); } - const closedByAgent = room.closer !== 'visitor'; - const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token; - if (!closedByAgent || !wasTheLastMessageSentByAgent) { - return params; + // 2: Room was inactive before start but was closed before end of BH, we add the inactive time + if (startOfInactivity.isBefore(todayStartOfficeHours) && endOfConversation.isBefore(todayEndOfficeHours)) { + totalSeconds += endOfConversation.diff(todayStartOfficeHours, 'seconds'); } - if (!room.v?.lastMessageTs) { - return params; + // 3: Room was inactive after start and ended after end of BH, we add the inactive time + if (startOfInactivity.isAfter(todayStartOfficeHours) && endOfConversation.isAfter(todayEndOfficeHours)) { + totalSeconds += todayEndOfficeHours.diff(startOfInactivity, 'seconds'); } - const agentLastMessage = await Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs); - if (!agentLastMessage) { - return params; + // 4: Room was inactive after start and before end of BH, we add the inactive time + if (startOfInactivity.isAfter(todayStartOfficeHours) && endOfConversation.isBefore(todayEndOfficeHours)) { + totalSeconds += endOfConversation.diff(startOfInactivity, 'seconds'); } - const secondsSinceLastAgentResponse = await getSecondsSinceLastAgentResponse(room, agentLastMessage); - await LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse); + inactivityDay.add(1, 'days'); + } + return totalSeconds; +}; + +export const onCloseRoom = async (params: { room: IOmnichannelRoom; options: CloseRoomParams['options'] }) => { + const { room } = params; + + if (!isOmnichannelRoom(room)) { + return params; + } + + const closedByAgent = room.closer !== 'visitor'; + const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token; + if (!closedByAgent || !wasTheLastMessageSentByAgent) { + return params; + } + + if (!room.v?.lastMessageTs) { return params; - }, - callbacks.priority.HIGH, - 'process-room-abandonment', -); + } + + const agentLastMessage = await Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs); + if (!agentLastMessage) { + return params; + } + const secondsSinceLastAgentResponse = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + await LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse); + + return params; +}; + +callbacks.add('livechat.closeRoom', onCloseRoom, callbacks.priority.HIGH, 'process-room-abandonment'); diff --git a/apps/meteor/tests/unit/app/livechat/server/hooks/processRoomAbandonment.spec.ts b/apps/meteor/tests/unit/app/livechat/server/hooks/processRoomAbandonment.spec.ts new file mode 100644 index 000000000000..91f88c36b022 --- /dev/null +++ b/apps/meteor/tests/unit/app/livechat/server/hooks/processRoomAbandonment.spec.ts @@ -0,0 +1,623 @@ +import { expect } from 'chai'; +import { it, describe } from 'mocha'; +import p from 'proxyquire'; +import sinon from 'sinon'; + +const settingsStub = sinon.stub(); +const models = { + LivechatDepartment: { + findOneById: sinon.stub(), + }, + LivechatBusinessHours: { + findOneById: sinon.stub(), + }, + Messages: { + findAgentLastMessageByVisitorLastMessageTs: sinon.stub(), + }, + LivechatRooms: { + setVisitorInactivityInSecondsById: sinon.stub(), + }, +}; + +const businessHourManagerMock = { + getBusinessHour: sinon.stub(), +}; + +const { getSecondsWhenOfficeHoursIsDisabled, parseDays, getSecondsSinceLastAgentResponse, onCloseRoom } = p + .noCallThru() + .load('../../../../../../app/livechat/server/hooks/processRoomAbandonment.ts', { + '@rocket.chat/models': models, + '../../../../lib/callbacks': { + callbacks: { add: sinon.stub(), priority: { HIGH: 'high' } }, + }, + '../../../settings/server': { + settings: { get: settingsStub }, + }, + '../business-hour': { businessHourManager: businessHourManagerMock }, + }); + +describe('processRoomAbandonment', () => { + describe('getSecondsWhenOfficeHoursIsDisabled', () => { + it('should return the seconds since the agents last message till room was closed', () => { + const room = { + closedAt: new Date('2024-01-01T12:00:10Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T12:00:00Z'), + }; + const result = getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); + expect(result).to.be.equal(10); + }); + it('should return the seconds since agents last message till now when room.closedAt is undefined', () => { + const room = { + closedAt: undefined, + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); + expect(result).to.be.equal(10); + }); + }); + describe('parseDays', () => { + it('should properly return the days in the expected format', () => { + const days = [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + ]; + + const result = days.reduce(parseDays, {}); + expect(result).to.be.deep.equal({ + Monday: { + start: { day: 'Monday', time: '10:00' }, + finish: { day: 'Monday', time: '11:00' }, + open: true, + }, + Tuesday: { + start: { day: 'Tuesday', time: '10:00' }, + finish: { day: 'Tuesday', time: '11:00' }, + open: true, + }, + Wednesday: { + start: { day: 'Wednesday', time: '10:00' }, + finish: { day: 'Wednesday', time: '11:00' }, + open: true, + }, + }); + }); + it('should properly parse open/close days', () => { + const days = [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: false, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + ]; + + const result = days.reduce(parseDays, {}); + expect(result).to.be.deep.equal({ + Monday: { + start: { day: 'Monday', time: '10:00' }, + finish: { day: 'Monday', time: '11:00' }, + open: true, + }, + Tuesday: { + start: { day: 'Tuesday', time: '10:00' }, + finish: { day: 'Tuesday', time: '11:00' }, + open: false, + }, + Wednesday: { + start: { day: 'Wednesday', time: '10:00' }, + finish: { day: 'Wednesday', time: '11:00' }, + open: true, + }, + }); + }); + }); + describe('getSecondsSinceLastAgentResponse', () => { + beforeEach(() => { + settingsStub.reset(); + models.LivechatDepartment.findOneById.reset(); + models.LivechatBusinessHours.findOneById.reset(); + businessHourManagerMock.getBusinessHour.reset(); + }); + it('should return the seconds since agent last message when Livechat_enable_business_hours is false', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(false); + const room = { + closedAt: undefined, + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(10); + }); + it('should return the seconds since last agent message when room has a department but department has an invalid business hour attached', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + models.LivechatDepartment.findOneById.withArgs('departmentId').resolves({ + businessHourId: 'businessHourId', + }); + models.LivechatBusinessHours.findOneById.withArgs('businessHourId').resolves(null); + const room = { + closedAt: undefined, + departmentId: 'departmentId', + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(models.LivechatDepartment.findOneById.calledWith(room.departmentId)).to.be.true; + expect(result).to.be.equal(10); + }); + it('should return the seconds since last agent message when department has a valid business hour but business hour doest have work hours', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + models.LivechatDepartment.findOneById.withArgs('departmentId').resolves({ + businessHourId: 'businessHourId', + }); + models.LivechatBusinessHours.findOneById.withArgs('businessHourId').resolves({ + workHours: [], + }); + businessHourManagerMock.getBusinessHour.withArgs('businessHourId').resolves(null); + const room = { + closedAt: undefined, + departmentId: 'departmentId', + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(10); + }); + it('should return the seconds since last agent message when department has a valid business hour but business hour workhours is empty', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + models.LivechatDepartment.findOneById.withArgs('departmentId').resolves({ + businessHourId: 'businessHourId', + }); + models.LivechatBusinessHours.findOneById.withArgs('businessHourId').resolves({ + workHours: [], + }); + businessHourManagerMock.getBusinessHour.withArgs('businessHourId').resolves({ + workHours: [], + }); + const room = { + closedAt: undefined, + departmentId: 'departmentId', + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(10); + }); + it('should get the data from the default business hour when room has no department attached and return the seconds since last agent message when default bh has no workhours', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [], + }); + const room = { + closedAt: undefined, + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(models.LivechatDepartment.findOneById.called).to.be.false; + expect(models.LivechatBusinessHours.findOneById.called).to.be.false; + expect(businessHourManagerMock.getBusinessHour.called).to.be.true; + expect(businessHourManagerMock.getBusinessHour.getCall(0).args.length).to.be.equal(0); + expect(result).to.be.equal(10); + }); + it('should return the proper number of seconds the room was inactive considering business hours (inactive same day)', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-01T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + ], + }); + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(3600); + }); + it('should return the proper number of seconds the room was inactive considering business hours (inactive same day)', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-01T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '23:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(7200); + }); + it('should return 0 if a room happened to be inactive on a day outside of business hours', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-03T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-03T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(0); + }); + it('should return the proper number of seconds when a room was inactive for more than 1 day', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-03T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(7200); + }); + it('should return the proper number of seconds when a room was inactive for more than 1 day, and one of those days was a closed day', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-03T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: false, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(7200); + }); + it('should return the proper number of seconds when a room was inactive for more than 1 day and one of those days is not in configuration', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-03T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(7200); + }); + it('should return the proper number of seconds when a room has been inactive for more than a week', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-10T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + { + day: 'Thursday', + start: { utc: { dayOfWeek: 'Thursday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Thursday', time: '11:00' } }, + open: false, + }, + { + day: 'Saturday', + start: { utc: { dayOfWeek: 'Friday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Friday', time: '11:00' } }, + open: true, + }, + { + day: 'Sunday', + start: { utc: { dayOfWeek: 'Saturday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Saturday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(28800); + }); + it('should return 0 when room was inactive in the same day but the configuration for bh on that day is invalid', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-01T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: undefined } }, + finish: { utc: { dayOfWeek: 'Monday', time: undefined } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: false, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(0); + }); + it('should return the proper number of seconds when a room has been inactive for more than a day but the inactivity started after BH started', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-02T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T10:15:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(6300); + }); + it('should return the proper number of seconds when a room was inactive between a BH start and end', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-01T10:50:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T10:15:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(2100); + }); + }); + describe('onCloseRoom', () => { + beforeEach(() => { + models.Messages.findAgentLastMessageByVisitorLastMessageTs.reset(); + }); + it('should skip the hook if room is not an omnichannel room', async () => { + const param = { room: { t: 'd' } }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false; + expect(r).to.be.equal(param); + }); + it('should skip if room was not closed by agent', async () => { + const param = { room: { t: 'l' }, closer: 'visitor' }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false; + expect(r).to.be.equal(param); + }); + it('should skip if the last message on room was not from an agent', async () => { + const param = { room: { t: 'l' }, closer: 'user', lastMessage: { token: 'xxxx' } }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false; + expect(r).to.be.equal(param); + }); + it('should skip if the last message is not on db', async () => { + models.Messages.findAgentLastMessageByVisitorLastMessageTs.resolves(null); + const param = { room: { _id: 'xyz', t: 'l', v: { lastMessageTs: new Date() }, closer: 'user', lastMessage: { msg: 'test' } } }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.calledWith('xyz', param.room.v.lastMessageTs)).to.be.true; + expect(r).to.be.equal(param); + }); + it('should skip if the visitor has not send any messages', async () => { + models.Messages.findAgentLastMessageByVisitorLastMessageTs.resolves({ ts: undefined }); + const param = { room: { _id: 'xyz', t: 'l', v: { token: 'xfasfdsa' }, closer: 'user', lastMessage: { msg: 'test' } } }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false; + expect(r).to.be.equal(param); + }); + it('should set the visitor inactivity in seconds when all params are valid', async () => { + models.Messages.findAgentLastMessageByVisitorLastMessageTs.resolves({ ts: new Date('2024-01-01T10:15:00Z') }); + settingsStub.withArgs('Livechat_enable_business_hours').returns(false); + const param = { + room: { + _id: 'xyz', + t: 'l', + v: { lastMessageTs: new Date() }, + closedAt: new Date('2024-01-01T10:50:00Z'), + closer: 'user', + lastMessage: { msg: 'test' }, + }, + }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.calledWith('xyz', param.room.v.lastMessageTs)).to.be.true; + expect(models.LivechatRooms.setVisitorInactivityInSecondsById.calledWith('xyz', 2100)).to.be.true; + expect(r).to.be.equal(param); + }); + }); +}); From 61ff3232b1b903b140b2c5506756ca78617dea95 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 20 Aug 2024 18:30:52 -0300 Subject: [PATCH 32/49] chore: move ddp-client and api-client packages out of ee folder (#32897) --- {ee/packages => packages}/api-client/.eslintrc.json | 0 {ee/packages => packages}/api-client/CHANGELOG.md | 0 {ee/packages => packages}/api-client/LICENSE | 0 .../api-client/__tests__/2fahandling.spec.ts | 0 {ee/packages => packages}/api-client/jest.config.ts | 0 {ee/packages => packages}/api-client/package.json | 0 {ee/packages => packages}/api-client/src/Credentials.ts | 0 .../api-client/src/RestClientInterface.ts | 0 {ee/packages => packages}/api-client/src/errors.ts | 0 {ee/packages => packages}/api-client/src/index.ts | 0 {ee/packages => packages}/api-client/tsconfig.json | 2 +- {ee/packages => packages}/ddp-client/.eslintrc.json | 0 {ee/packages => packages}/ddp-client/CHANGELOG.md | 0 {ee/packages => packages}/ddp-client/LICENSE | 0 {ee/packages => packages}/ddp-client/README.md | 0 .../ddp-client/__examples__/simple.ts | 0 {ee/packages => packages}/ddp-client/__mocks__/ws.ts | 0 .../ddp-client/__tests__/Account.spec.ts | 0 .../ddp-client/__tests__/ClientStream.spec.ts | 0 .../ddp-client/__tests__/Connection.spec.ts | 0 .../ddp-client/__tests__/DDPDispatcher.spec.ts | 0 .../ddp-client/__tests__/DDPSDK.spec.ts | 0 .../ddp-client/__tests__/MinimalDDPClient.spec.ts | 0 .../ddp-client/__tests__/Timeout.spec.ts | 0 .../ddp-client/__tests__/helpers/index.ts | 0 .../ddp-client/__tests__/wrapOnceEventIntoPromise.spec.ts | 0 {ee/packages => packages}/ddp-client/jest.config.ts | 0 {ee/packages => packages}/ddp-client/package.json | 0 {ee/packages => packages}/ddp-client/src/ClientStream.ts | 0 {ee/packages => packages}/ddp-client/src/Connection.ts | 0 {ee/packages => packages}/ddp-client/src/DDPDispatcher.ts | 0 {ee/packages => packages}/ddp-client/src/DDPSDK.ts | 0 .../ddp-client/src/MinimalDDPClient.ts | 0 .../ddp-client/src/TimeoutControl.ts | 0 {ee/packages => packages}/ddp-client/src/index.ts | 0 .../ddp-client/src/legacy/RocketchatSDKLegacy.ts | 0 .../ddp-client/src/legacy/types/SDKLegacy.ts | 0 .../ddp-client/src/livechat/LivechatClientImpl.ts | 0 .../ddp-client/src/livechat/types/LivechatSDK.ts | 0 {ee/packages => packages}/ddp-client/src/types/Account.ts | 0 .../ddp-client/src/types/ClientStream.ts | 0 .../ddp-client/src/types/DDPClient.ts | 0 .../ddp-client/src/types/IncomingPayload.ts | 0 .../ddp-client/src/types/OutgoingPayload.ts | 0 .../ddp-client/src/types/RemoveListener.ts | 0 {ee/packages => packages}/ddp-client/src/types/SDK.ts | 0 .../ddp-client/src/types/Subscription.ts | 0 .../ddp-client/src/types/connectionPayloads.ts | 0 .../ddp-client/src/types/heartbeatsPayloads.ts | 0 {ee/packages => packages}/ddp-client/src/types/methods.ts | 0 .../ddp-client/src/types/methodsPayloads.ts | 0 .../ddp-client/src/types/publicationPayloads.ts | 0 {ee/packages => packages}/ddp-client/src/types/streams.ts | 0 .../ddp-client/src/wrapOnceEventIntoPromise.ts | 0 {ee/packages => packages}/ddp-client/tsconfig.json | 2 +- yarn.lock | 8 ++++---- 56 files changed, 6 insertions(+), 6 deletions(-) rename {ee/packages => packages}/api-client/.eslintrc.json (100%) rename {ee/packages => packages}/api-client/CHANGELOG.md (100%) rename {ee/packages => packages}/api-client/LICENSE (100%) rename {ee/packages => packages}/api-client/__tests__/2fahandling.spec.ts (100%) rename {ee/packages => packages}/api-client/jest.config.ts (100%) rename {ee/packages => packages}/api-client/package.json (100%) rename {ee/packages => packages}/api-client/src/Credentials.ts (100%) rename {ee/packages => packages}/api-client/src/RestClientInterface.ts (100%) rename {ee/packages => packages}/api-client/src/errors.ts (100%) rename {ee/packages => packages}/api-client/src/index.ts (100%) rename {ee/packages => packages}/api-client/tsconfig.json (71%) rename {ee/packages => packages}/ddp-client/.eslintrc.json (100%) rename {ee/packages => packages}/ddp-client/CHANGELOG.md (100%) rename {ee/packages => packages}/ddp-client/LICENSE (100%) rename {ee/packages => packages}/ddp-client/README.md (100%) rename {ee/packages => packages}/ddp-client/__examples__/simple.ts (100%) rename {ee/packages => packages}/ddp-client/__mocks__/ws.ts (100%) rename {ee/packages => packages}/ddp-client/__tests__/Account.spec.ts (100%) rename {ee/packages => packages}/ddp-client/__tests__/ClientStream.spec.ts (100%) rename {ee/packages => packages}/ddp-client/__tests__/Connection.spec.ts (100%) rename {ee/packages => packages}/ddp-client/__tests__/DDPDispatcher.spec.ts (100%) rename {ee/packages => packages}/ddp-client/__tests__/DDPSDK.spec.ts (100%) rename {ee/packages => packages}/ddp-client/__tests__/MinimalDDPClient.spec.ts (100%) rename {ee/packages => packages}/ddp-client/__tests__/Timeout.spec.ts (100%) rename {ee/packages => packages}/ddp-client/__tests__/helpers/index.ts (100%) rename {ee/packages => packages}/ddp-client/__tests__/wrapOnceEventIntoPromise.spec.ts (100%) rename {ee/packages => packages}/ddp-client/jest.config.ts (100%) rename {ee/packages => packages}/ddp-client/package.json (100%) rename {ee/packages => packages}/ddp-client/src/ClientStream.ts (100%) rename {ee/packages => packages}/ddp-client/src/Connection.ts (100%) rename {ee/packages => packages}/ddp-client/src/DDPDispatcher.ts (100%) rename {ee/packages => packages}/ddp-client/src/DDPSDK.ts (100%) rename {ee/packages => packages}/ddp-client/src/MinimalDDPClient.ts (100%) rename {ee/packages => packages}/ddp-client/src/TimeoutControl.ts (100%) rename {ee/packages => packages}/ddp-client/src/index.ts (100%) rename {ee/packages => packages}/ddp-client/src/legacy/RocketchatSDKLegacy.ts (100%) rename {ee/packages => packages}/ddp-client/src/legacy/types/SDKLegacy.ts (100%) rename {ee/packages => packages}/ddp-client/src/livechat/LivechatClientImpl.ts (100%) rename {ee/packages => packages}/ddp-client/src/livechat/types/LivechatSDK.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/Account.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/ClientStream.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/DDPClient.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/IncomingPayload.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/OutgoingPayload.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/RemoveListener.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/SDK.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/Subscription.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/connectionPayloads.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/heartbeatsPayloads.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/methods.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/methodsPayloads.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/publicationPayloads.ts (100%) rename {ee/packages => packages}/ddp-client/src/types/streams.ts (100%) rename {ee/packages => packages}/ddp-client/src/wrapOnceEventIntoPromise.ts (100%) rename {ee/packages => packages}/ddp-client/tsconfig.json (81%) diff --git a/ee/packages/api-client/.eslintrc.json b/packages/api-client/.eslintrc.json similarity index 100% rename from ee/packages/api-client/.eslintrc.json rename to packages/api-client/.eslintrc.json diff --git a/ee/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md similarity index 100% rename from ee/packages/api-client/CHANGELOG.md rename to packages/api-client/CHANGELOG.md diff --git a/ee/packages/api-client/LICENSE b/packages/api-client/LICENSE similarity index 100% rename from ee/packages/api-client/LICENSE rename to packages/api-client/LICENSE diff --git a/ee/packages/api-client/__tests__/2fahandling.spec.ts b/packages/api-client/__tests__/2fahandling.spec.ts similarity index 100% rename from ee/packages/api-client/__tests__/2fahandling.spec.ts rename to packages/api-client/__tests__/2fahandling.spec.ts diff --git a/ee/packages/api-client/jest.config.ts b/packages/api-client/jest.config.ts similarity index 100% rename from ee/packages/api-client/jest.config.ts rename to packages/api-client/jest.config.ts diff --git a/ee/packages/api-client/package.json b/packages/api-client/package.json similarity index 100% rename from ee/packages/api-client/package.json rename to packages/api-client/package.json diff --git a/ee/packages/api-client/src/Credentials.ts b/packages/api-client/src/Credentials.ts similarity index 100% rename from ee/packages/api-client/src/Credentials.ts rename to packages/api-client/src/Credentials.ts diff --git a/ee/packages/api-client/src/RestClientInterface.ts b/packages/api-client/src/RestClientInterface.ts similarity index 100% rename from ee/packages/api-client/src/RestClientInterface.ts rename to packages/api-client/src/RestClientInterface.ts diff --git a/ee/packages/api-client/src/errors.ts b/packages/api-client/src/errors.ts similarity index 100% rename from ee/packages/api-client/src/errors.ts rename to packages/api-client/src/errors.ts diff --git a/ee/packages/api-client/src/index.ts b/packages/api-client/src/index.ts similarity index 100% rename from ee/packages/api-client/src/index.ts rename to packages/api-client/src/index.ts diff --git a/ee/packages/api-client/tsconfig.json b/packages/api-client/tsconfig.json similarity index 71% rename from ee/packages/api-client/tsconfig.json rename to packages/api-client/tsconfig.json index b397e2c4421f..9d8ef0c3a373 100644 --- a/ee/packages/api-client/tsconfig.json +++ b/packages/api-client/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.client.json", + "extends": "../../tsconfig.base.client.json", "compilerOptions": { "module": "commonjs", "rootDir": "./src", diff --git a/ee/packages/ddp-client/.eslintrc.json b/packages/ddp-client/.eslintrc.json similarity index 100% rename from ee/packages/ddp-client/.eslintrc.json rename to packages/ddp-client/.eslintrc.json diff --git a/ee/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md similarity index 100% rename from ee/packages/ddp-client/CHANGELOG.md rename to packages/ddp-client/CHANGELOG.md diff --git a/ee/packages/ddp-client/LICENSE b/packages/ddp-client/LICENSE similarity index 100% rename from ee/packages/ddp-client/LICENSE rename to packages/ddp-client/LICENSE diff --git a/ee/packages/ddp-client/README.md b/packages/ddp-client/README.md similarity index 100% rename from ee/packages/ddp-client/README.md rename to packages/ddp-client/README.md diff --git a/ee/packages/ddp-client/__examples__/simple.ts b/packages/ddp-client/__examples__/simple.ts similarity index 100% rename from ee/packages/ddp-client/__examples__/simple.ts rename to packages/ddp-client/__examples__/simple.ts diff --git a/ee/packages/ddp-client/__mocks__/ws.ts b/packages/ddp-client/__mocks__/ws.ts similarity index 100% rename from ee/packages/ddp-client/__mocks__/ws.ts rename to packages/ddp-client/__mocks__/ws.ts diff --git a/ee/packages/ddp-client/__tests__/Account.spec.ts b/packages/ddp-client/__tests__/Account.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/Account.spec.ts rename to packages/ddp-client/__tests__/Account.spec.ts diff --git a/ee/packages/ddp-client/__tests__/ClientStream.spec.ts b/packages/ddp-client/__tests__/ClientStream.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/ClientStream.spec.ts rename to packages/ddp-client/__tests__/ClientStream.spec.ts diff --git a/ee/packages/ddp-client/__tests__/Connection.spec.ts b/packages/ddp-client/__tests__/Connection.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/Connection.spec.ts rename to packages/ddp-client/__tests__/Connection.spec.ts diff --git a/ee/packages/ddp-client/__tests__/DDPDispatcher.spec.ts b/packages/ddp-client/__tests__/DDPDispatcher.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/DDPDispatcher.spec.ts rename to packages/ddp-client/__tests__/DDPDispatcher.spec.ts diff --git a/ee/packages/ddp-client/__tests__/DDPSDK.spec.ts b/packages/ddp-client/__tests__/DDPSDK.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/DDPSDK.spec.ts rename to packages/ddp-client/__tests__/DDPSDK.spec.ts diff --git a/ee/packages/ddp-client/__tests__/MinimalDDPClient.spec.ts b/packages/ddp-client/__tests__/MinimalDDPClient.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/MinimalDDPClient.spec.ts rename to packages/ddp-client/__tests__/MinimalDDPClient.spec.ts diff --git a/ee/packages/ddp-client/__tests__/Timeout.spec.ts b/packages/ddp-client/__tests__/Timeout.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/Timeout.spec.ts rename to packages/ddp-client/__tests__/Timeout.spec.ts diff --git a/ee/packages/ddp-client/__tests__/helpers/index.ts b/packages/ddp-client/__tests__/helpers/index.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/helpers/index.ts rename to packages/ddp-client/__tests__/helpers/index.ts diff --git a/ee/packages/ddp-client/__tests__/wrapOnceEventIntoPromise.spec.ts b/packages/ddp-client/__tests__/wrapOnceEventIntoPromise.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/wrapOnceEventIntoPromise.spec.ts rename to packages/ddp-client/__tests__/wrapOnceEventIntoPromise.spec.ts diff --git a/ee/packages/ddp-client/jest.config.ts b/packages/ddp-client/jest.config.ts similarity index 100% rename from ee/packages/ddp-client/jest.config.ts rename to packages/ddp-client/jest.config.ts diff --git a/ee/packages/ddp-client/package.json b/packages/ddp-client/package.json similarity index 100% rename from ee/packages/ddp-client/package.json rename to packages/ddp-client/package.json diff --git a/ee/packages/ddp-client/src/ClientStream.ts b/packages/ddp-client/src/ClientStream.ts similarity index 100% rename from ee/packages/ddp-client/src/ClientStream.ts rename to packages/ddp-client/src/ClientStream.ts diff --git a/ee/packages/ddp-client/src/Connection.ts b/packages/ddp-client/src/Connection.ts similarity index 100% rename from ee/packages/ddp-client/src/Connection.ts rename to packages/ddp-client/src/Connection.ts diff --git a/ee/packages/ddp-client/src/DDPDispatcher.ts b/packages/ddp-client/src/DDPDispatcher.ts similarity index 100% rename from ee/packages/ddp-client/src/DDPDispatcher.ts rename to packages/ddp-client/src/DDPDispatcher.ts diff --git a/ee/packages/ddp-client/src/DDPSDK.ts b/packages/ddp-client/src/DDPSDK.ts similarity index 100% rename from ee/packages/ddp-client/src/DDPSDK.ts rename to packages/ddp-client/src/DDPSDK.ts diff --git a/ee/packages/ddp-client/src/MinimalDDPClient.ts b/packages/ddp-client/src/MinimalDDPClient.ts similarity index 100% rename from ee/packages/ddp-client/src/MinimalDDPClient.ts rename to packages/ddp-client/src/MinimalDDPClient.ts diff --git a/ee/packages/ddp-client/src/TimeoutControl.ts b/packages/ddp-client/src/TimeoutControl.ts similarity index 100% rename from ee/packages/ddp-client/src/TimeoutControl.ts rename to packages/ddp-client/src/TimeoutControl.ts diff --git a/ee/packages/ddp-client/src/index.ts b/packages/ddp-client/src/index.ts similarity index 100% rename from ee/packages/ddp-client/src/index.ts rename to packages/ddp-client/src/index.ts diff --git a/ee/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts b/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts similarity index 100% rename from ee/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts rename to packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts diff --git a/ee/packages/ddp-client/src/legacy/types/SDKLegacy.ts b/packages/ddp-client/src/legacy/types/SDKLegacy.ts similarity index 100% rename from ee/packages/ddp-client/src/legacy/types/SDKLegacy.ts rename to packages/ddp-client/src/legacy/types/SDKLegacy.ts diff --git a/ee/packages/ddp-client/src/livechat/LivechatClientImpl.ts b/packages/ddp-client/src/livechat/LivechatClientImpl.ts similarity index 100% rename from ee/packages/ddp-client/src/livechat/LivechatClientImpl.ts rename to packages/ddp-client/src/livechat/LivechatClientImpl.ts diff --git a/ee/packages/ddp-client/src/livechat/types/LivechatSDK.ts b/packages/ddp-client/src/livechat/types/LivechatSDK.ts similarity index 100% rename from ee/packages/ddp-client/src/livechat/types/LivechatSDK.ts rename to packages/ddp-client/src/livechat/types/LivechatSDK.ts diff --git a/ee/packages/ddp-client/src/types/Account.ts b/packages/ddp-client/src/types/Account.ts similarity index 100% rename from ee/packages/ddp-client/src/types/Account.ts rename to packages/ddp-client/src/types/Account.ts diff --git a/ee/packages/ddp-client/src/types/ClientStream.ts b/packages/ddp-client/src/types/ClientStream.ts similarity index 100% rename from ee/packages/ddp-client/src/types/ClientStream.ts rename to packages/ddp-client/src/types/ClientStream.ts diff --git a/ee/packages/ddp-client/src/types/DDPClient.ts b/packages/ddp-client/src/types/DDPClient.ts similarity index 100% rename from ee/packages/ddp-client/src/types/DDPClient.ts rename to packages/ddp-client/src/types/DDPClient.ts diff --git a/ee/packages/ddp-client/src/types/IncomingPayload.ts b/packages/ddp-client/src/types/IncomingPayload.ts similarity index 100% rename from ee/packages/ddp-client/src/types/IncomingPayload.ts rename to packages/ddp-client/src/types/IncomingPayload.ts diff --git a/ee/packages/ddp-client/src/types/OutgoingPayload.ts b/packages/ddp-client/src/types/OutgoingPayload.ts similarity index 100% rename from ee/packages/ddp-client/src/types/OutgoingPayload.ts rename to packages/ddp-client/src/types/OutgoingPayload.ts diff --git a/ee/packages/ddp-client/src/types/RemoveListener.ts b/packages/ddp-client/src/types/RemoveListener.ts similarity index 100% rename from ee/packages/ddp-client/src/types/RemoveListener.ts rename to packages/ddp-client/src/types/RemoveListener.ts diff --git a/ee/packages/ddp-client/src/types/SDK.ts b/packages/ddp-client/src/types/SDK.ts similarity index 100% rename from ee/packages/ddp-client/src/types/SDK.ts rename to packages/ddp-client/src/types/SDK.ts diff --git a/ee/packages/ddp-client/src/types/Subscription.ts b/packages/ddp-client/src/types/Subscription.ts similarity index 100% rename from ee/packages/ddp-client/src/types/Subscription.ts rename to packages/ddp-client/src/types/Subscription.ts diff --git a/ee/packages/ddp-client/src/types/connectionPayloads.ts b/packages/ddp-client/src/types/connectionPayloads.ts similarity index 100% rename from ee/packages/ddp-client/src/types/connectionPayloads.ts rename to packages/ddp-client/src/types/connectionPayloads.ts diff --git a/ee/packages/ddp-client/src/types/heartbeatsPayloads.ts b/packages/ddp-client/src/types/heartbeatsPayloads.ts similarity index 100% rename from ee/packages/ddp-client/src/types/heartbeatsPayloads.ts rename to packages/ddp-client/src/types/heartbeatsPayloads.ts diff --git a/ee/packages/ddp-client/src/types/methods.ts b/packages/ddp-client/src/types/methods.ts similarity index 100% rename from ee/packages/ddp-client/src/types/methods.ts rename to packages/ddp-client/src/types/methods.ts diff --git a/ee/packages/ddp-client/src/types/methodsPayloads.ts b/packages/ddp-client/src/types/methodsPayloads.ts similarity index 100% rename from ee/packages/ddp-client/src/types/methodsPayloads.ts rename to packages/ddp-client/src/types/methodsPayloads.ts diff --git a/ee/packages/ddp-client/src/types/publicationPayloads.ts b/packages/ddp-client/src/types/publicationPayloads.ts similarity index 100% rename from ee/packages/ddp-client/src/types/publicationPayloads.ts rename to packages/ddp-client/src/types/publicationPayloads.ts diff --git a/ee/packages/ddp-client/src/types/streams.ts b/packages/ddp-client/src/types/streams.ts similarity index 100% rename from ee/packages/ddp-client/src/types/streams.ts rename to packages/ddp-client/src/types/streams.ts diff --git a/ee/packages/ddp-client/src/wrapOnceEventIntoPromise.ts b/packages/ddp-client/src/wrapOnceEventIntoPromise.ts similarity index 100% rename from ee/packages/ddp-client/src/wrapOnceEventIntoPromise.ts rename to packages/ddp-client/src/wrapOnceEventIntoPromise.ts diff --git a/ee/packages/ddp-client/tsconfig.json b/packages/ddp-client/tsconfig.json similarity index 81% rename from ee/packages/ddp-client/tsconfig.json rename to packages/ddp-client/tsconfig.json index 29b8cb051fe3..b98ff74ba385 100644 --- a/ee/packages/ddp-client/tsconfig.json +++ b/packages/ddp-client/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.client.json", + "extends": "../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "./src", "outDir": "./dist", diff --git a/yarn.lock b/yarn.lock index de477be8048a..6a6c2a8ee9d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8463,9 +8463,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/api-client@workspace:^, @rocket.chat/api-client@workspace:ee/packages/api-client": +"@rocket.chat/api-client@workspace:^, @rocket.chat/api-client@workspace:packages/api-client": version: 0.0.0-use.local - resolution: "@rocket.chat/api-client@workspace:ee/packages/api-client" + resolution: "@rocket.chat/api-client@workspace:packages/api-client" dependencies: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" @@ -8657,9 +8657,9 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/ddp-client@workspace:^, @rocket.chat/ddp-client@workspace:ee/packages/ddp-client, @rocket.chat/ddp-client@workspace:~": +"@rocket.chat/ddp-client@workspace:^, @rocket.chat/ddp-client@workspace:packages/ddp-client, @rocket.chat/ddp-client@workspace:~": version: 0.0.0-use.local - resolution: "@rocket.chat/ddp-client@workspace:ee/packages/ddp-client" + resolution: "@rocket.chat/ddp-client@workspace:packages/ddp-client" dependencies: "@rocket.chat/api-client": "workspace:^" "@rocket.chat/core-typings": "workspace:~" From 908391ad1d49f00c94df805f5a6f43bbc9c22afd Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 20 Aug 2024 20:07:00 -0300 Subject: [PATCH 33/49] ci: yarn login (#33117) --- .github/actions/build-docker/action.yml | 4 ++++ .github/actions/meteor-build/action.yml | 4 ++++ .github/actions/setup-node/action.yml | 20 ++++++++++++++++---- .github/workflows/ci-code-check.yml | 1 + .github/workflows/ci-test-e2e.yml | 3 +++ .github/workflows/ci-test-unit.yml | 1 + .github/workflows/ci.yml | 4 ++++ .github/workflows/new-release.yml | 1 + .github/workflows/pr-update-description.yml | 1 + .github/workflows/publish-release.yml | 1 + 10 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 6f8250d2acd4..5af39b924057 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -29,6 +29,9 @@ inputs: required: false description: 'Setup node.js' default: 'true' + NPM_TOKEN: + required: false + description: 'NPM token' runs: using: composite @@ -65,6 +68,7 @@ runs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ inputs.NPM_TOKEN }} - run: yarn build if: inputs.setup == 'true' diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml index dfbc1cef4150..525595146700 100644 --- a/.github/actions/meteor-build/action.yml +++ b/.github/actions/meteor-build/action.yml @@ -13,6 +13,9 @@ inputs: required: true description: 'Node version' type: string + NPM_TOKEN: + required: false + description: 'NPM token' runs: using: composite @@ -29,6 +32,7 @@ runs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ inputs.NPM_TOKEN }} # - name: Free disk space # run: | diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 60d54ab896dd..1035e2835792 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -1,22 +1,27 @@ name: 'Setup Node' +description: 'Setup NodeJS' inputs: node-version: required: true - type: string + description: 'Node version' cache-modules: required: false - type: boolean + description: 'Cache node_modules' install: required: false - type: boolean + description: 'Install dependencies' deno-dir: required: false - type: string + description: 'Deno directory' default: ~/.deno-cache + NPM_TOKEN: + required: false + description: 'NPM token' outputs: node-version: + description: 'Node version' value: ${{ steps.node-version.outputs.node-version }} runs: @@ -49,6 +54,13 @@ runs: node-version: ${{ inputs.node-version }} cache: 'yarn' + - name: yarn login + shell: bash + if: inputs.NPM_TOKEN + run: | + echo "//registry.npmjs.org/:_authToken=${{ inputs.NPM_TOKEN }}" > ~/.npmrc + - name: yarn install + if: inputs.install shell: bash run: yarn diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index fd214bc39488..af50b3230ba7 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -35,6 +35,7 @@ jobs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # - name: Free disk space # run: | diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 31a8bc2ea2b6..e6c02b7b6417 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -130,6 +130,8 @@ jobs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - uses: rharkor/caching-for-turbo@v1.5 - run: yarn build @@ -145,6 +147,7 @@ jobs: # the same reason we need to rebuild the docker image at this point is the reason we dont want to publish it publish-image: false setup: false + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Start httpbin container and wait for it to be ready if: inputs.type == 'api' diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index a32c1e575b8f..840808ff5e31 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -39,6 +39,7 @@ jobs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: rharkor/caching-for-turbo@v1.5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 246c34423bb1..514dd6d1c518 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,6 +152,7 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Cache vite uses: actions/cache@v3 @@ -253,6 +254,7 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} platform: ${{ matrix.platform }} build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} build-gh-docker: name: 🚢 Build Docker Images for Production @@ -280,6 +282,7 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} platform: ${{ matrix.platform }} build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Rename official Docker tag to GitHub Container Registry if: matrix.platform == 'official' @@ -560,6 +563,7 @@ jobs: release: preview username: ${{ secrets.CR_USER }} password: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} docker-image-publish: name: 🚀 Publish Docker Image (main) diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 5ef8027b1467..b2eae5d90b92 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -37,6 +37,7 @@ jobs: node-version: 14.21.3 cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: rharkor/caching-for-turbo@v1.5 diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index e792127eac9d..084f2a383480 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -24,6 +24,7 @@ jobs: node-version: 14.21.3 cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: rharkor/caching-for-turbo@v1.5 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index ccc3408e194e..3f2067ac7ec3 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -27,6 +27,7 @@ jobs: node-version: 14.21.3 cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: rharkor/caching-for-turbo@v1.5 From a13417655ae84c35fd507bb2a2608550dc04a838 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 20 Aug 2024 21:35:44 -0300 Subject: [PATCH 34/49] refactor: `Realtime Monitoring/Chart` component to TS (#33076) --- .../analytics/InterchangeableChart.tsx | 2 +- .../charts/AgentStatusChart.js | 2 +- .../realTimeMonitoring/charts/Chart.js | 15 --------------- .../realTimeMonitoring/charts/Chart.tsx | 16 ++++++++++++++++ .../charts/ChatDurationChart.js | 2 +- .../realTimeMonitoring/charts/ChatsChart.js | 2 +- .../charts/ChatsPerAgentChart.js | 2 +- .../charts/ChatsPerDepartmentChart.js | 2 +- .../charts/ResponseTimesChart.js | 2 +- 9 files changed, 23 insertions(+), 22 deletions(-) delete mode 100644 apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/Chart.js create mode 100644 apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/Chart.tsx diff --git a/apps/meteor/client/views/omnichannel/analytics/InterchangeableChart.tsx b/apps/meteor/client/views/omnichannel/analytics/InterchangeableChart.tsx index 872bb4f05b0d..e51cacf76c83 100644 --- a/apps/meteor/client/views/omnichannel/analytics/InterchangeableChart.tsx +++ b/apps/meteor/client/views/omnichannel/analytics/InterchangeableChart.tsx @@ -94,7 +94,7 @@ const InterchangeableChart = ({ }); }, [chartName, departmentId, draw, end, start, t, loadData]); - return ; + return ; }; export default InterchangeableChart; diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/AgentStatusChart.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/AgentStatusChart.js index 4564a859ccf5..4724bea74350 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/AgentStatusChart.js +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/AgentStatusChart.js @@ -60,7 +60,7 @@ const AgentStatusChart = ({ params, reloadRef, ...props }) => { } }, [available, away, busy, offline, state, t, updateChartData]); - return ; + return ; }; export default AgentStatusChart; diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/Chart.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/Chart.js deleted file mode 100644 index 8ba5066c1706..000000000000 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/Chart.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React, { forwardRef } from 'react'; - -const style = { - minHeight: '250px', -}; -const Chart = forwardRef(function Chart(props, ref) { - return ( - - - - ); -}); - -export default Chart; diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/Chart.tsx b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/Chart.tsx new file mode 100644 index 000000000000..5a47906ce92d --- /dev/null +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/Chart.tsx @@ -0,0 +1,16 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { MutableRefObject } from 'react'; +import React from 'react'; + +type ChartProps = { canvasRef: MutableRefObject }; + +const style = { + minHeight: '250px', +}; +const Chart = ({ canvasRef, ...props }: ChartProps) => ( + + + +); + +export default Chart; diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatDurationChart.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatDurationChart.js index d85fe1d3799d..b4e155394f68 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatDurationChart.js +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatDurationChart.js @@ -72,7 +72,7 @@ const ChatDurationChart = ({ params, reloadRef, ...props }) => { } }, [avg, longest, state, t, updateChartData]); - return ; + return ; }; export default ChatDurationChart; diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsChart.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsChart.js index cbe1285931d7..5a540dcd2dbd 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsChart.js +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsChart.js @@ -60,7 +60,7 @@ const ChatsChart = ({ params, reloadRef, ...props }) => { } }, [closed, open, queued, onhold, state, t, updateChartData]); - return ; + return ; }; export default ChatsChart; diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js index 6c7741781e1b..48b0bdbf655e 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js @@ -56,7 +56,7 @@ const ChatsPerAgentChart = ({ params, reloadRef, ...props }) => { } }, [chartData, state, t, updateChartData]); - return ; + return ; }; export default ChatsPerAgentChart; diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerDepartmentChart.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerDepartmentChart.js index 030fcedc0576..fbfe91695626 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerDepartmentChart.js +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerDepartmentChart.js @@ -59,7 +59,7 @@ const ChatsPerDepartmentChart = ({ params, reloadRef, ...props }) => { } }, [chartData, state, t, updateChartData]); - return ; + return ; }; export default ChatsPerDepartmentChart; diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ResponseTimesChart.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ResponseTimesChart.js index ac0500ebf4da..cfc33687c8fc 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ResponseTimesChart.js +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/ResponseTimesChart.js @@ -78,7 +78,7 @@ const ResponseTimesChart = ({ params, reloadRef, ...props }) => { } }, [reactionAvg, reactionLongest, responseAvg, responseLongest, state, t, updateChartData]); - return ; + return ; }; export default ResponseTimesChart; From 1041e8c0d4a91fd3e52494ee9264ee7b9b3d6103 Mon Sep 17 00:00:00 2001 From: anicoa Date: Wed, 21 Aug 2024 04:54:42 +0200 Subject: [PATCH 35/49] feat: proxy avatars for Accounts_AvatarExternalProviderUrl (#32824) Co-authored-by: Tasso Co-authored-by: Guilherme Gazzo --- apps/meteor/client/providers/AvatarUrlProvider.tsx | 6 +----- apps/meteor/server/routes/avatar/user.js | 8 ++++++++ packages/i18n/src/locales/de.i18n.json | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/providers/AvatarUrlProvider.tsx b/apps/meteor/client/providers/AvatarUrlProvider.tsx index 6cdc9012f714..b5a92c9117f2 100644 --- a/apps/meteor/client/providers/AvatarUrlProvider.tsx +++ b/apps/meteor/client/providers/AvatarUrlProvider.tsx @@ -11,13 +11,9 @@ type AvatarUrlProviderProps = { const AvatarUrlProvider = ({ children }: AvatarUrlProviderProps) => { const cdnAvatarUrl = String(useSetting('CDN_PREFIX') || ''); - const externalProviderUrl = String(useSetting('Accounts_AvatarExternalProviderUrl') || ''); const contextValue = useMemo( () => ({ getUserPathAvatar: ((): ((uid: string, etag?: string) => string) => { - if (externalProviderUrl) { - return (uid: string): string => externalProviderUrl.trim().replace(/\/+$/, '').replace('{username}', uid); - } if (cdnAvatarUrl) { return (uid: string, etag?: string): string => `${cdnAvatarUrl}/avatar/${uid}${etag ? `?etag=${etag}` : ''}`; } @@ -26,7 +22,7 @@ const AvatarUrlProvider = ({ children }: AvatarUrlProviderProps) => { getRoomPathAvatar: ({ type, ...room }: any): string => roomCoordinator.getRoomDirectives(type || room.t).getAvatarPath({ username: room._id, ...room }) || '', }), - [externalProviderUrl, cdnAvatarUrl], + [cdnAvatarUrl], ); return ; diff --git a/apps/meteor/server/routes/avatar/user.js b/apps/meteor/server/routes/avatar/user.js index 0d86bc4a08cf..269c2e90019a 100644 --- a/apps/meteor/server/routes/avatar/user.js +++ b/apps/meteor/server/routes/avatar/user.js @@ -1,4 +1,5 @@ import { Avatars, Users } from '@rocket.chat/models'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { FileUpload } from '../../../app/file-upload/server'; import { settings } from '../../../app/settings/server'; @@ -51,6 +52,13 @@ export const userAvatar = async function (req, res) { return FileUpload.get(file, req, res); } + if (settings.get('Accounts_AvatarExternalProviderUrl')) { + const response = await fetch(settings.get('Accounts_AvatarExternalProviderUrl').replace('{username}', requestUsername)); + response.headers.forEach((value, key) => res.setHeader(key, value)); + response.body.pipe(res); + return; + } + // if still using "letters fallback" if (!wasFallbackModified(reqModifiedHeader, res)) { res.writeHead(304); diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index a67509672d33..72963c308e67 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -5530,4 +5530,4 @@ "Enterprise": "Unternehmen", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Nachrichtenüberprüfung" -} \ No newline at end of file +} From 0b0d4d8c6fb1f71d2ff7096577df240bc243f7c1 Mon Sep 17 00:00:00 2001 From: Heet Patel <118350153+heet434@users.noreply.github.com> Date: Wed, 21 Aug 2024 08:30:19 +0530 Subject: [PATCH 36/49] feat: Added Creation Date col in Rooms Table in Admin Panel (#32709) --- .changeset/rooms-table-ts.md | 5 +++++ .../client/views/admin/rooms/EditRoom.tsx | 2 +- .../views/admin/rooms/EditRoomWithData.tsx | 2 +- .../client/views/admin/rooms/RoomRow.tsx | 19 +++++++++++-------- .../client/views/admin/rooms/RoomsTable.tsx | 7 +++++-- apps/meteor/lib/rooms/adminFields.ts | 1 + apps/meteor/tests/end-to-end/api/rooms.ts | 18 ++++++++++++++++++ packages/core-typings/src/IRoom.ts | 1 + 8 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 .changeset/rooms-table-ts.md diff --git a/.changeset/rooms-table-ts.md b/.changeset/rooms-table-ts.md new file mode 100644 index 000000000000..b5055ad26f69 --- /dev/null +++ b/.changeset/rooms-table-ts.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Add "Created at" column to admin rooms table diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index cc165bca215b..1a993979eec8 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -27,7 +27,7 @@ import { useDeleteRoom } from '../../hooks/roomActions/useDeleteRoom'; import { useEditAdminRoomPermissions } from './useEditAdminRoomPermissions'; type EditRoomProps = { - room: Pick; + room: IRoom; onChange: () => void; onDelete: () => void; }; diff --git a/apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx b/apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx index 6bd487c8218a..54245d3d55a9 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx @@ -53,7 +53,7 @@ const EditRoomWithData = ({ rid, onReload }: EditRoomWithDataProps) => { {t('Room_Info')} router.navigate('/admin/rooms')} /> - + ) : null; }; diff --git a/apps/meteor/client/views/admin/rooms/RoomRow.tsx b/apps/meteor/client/views/admin/rooms/RoomRow.tsx index 73a30e647764..05b1079bfbde 100644 --- a/apps/meteor/client/views/admin/rooms/RoomRow.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomRow.tsx @@ -1,5 +1,5 @@ import { isDiscussion } from '@rocket.chat/core-typings'; -import type { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; +import type { IRoom, RoomAdminFieldsType, Serialized } from '@rocket.chat/core-typings'; import { Box, Icon } from '@rocket.chat/fuselage'; import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; @@ -7,6 +7,7 @@ import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; import { GenericTableCell, GenericTableRow } from '../../../components/GenericTable'; +import { useFormatDate } from '../../../hooks/useFormatDate'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; const roomTypeI18nMap = { @@ -16,25 +17,26 @@ const roomTypeI18nMap = { p: 'Private_Channel', } as const; -const getRoomDisplayName = (room: Pick): string | undefined => - room.t === 'd' ? room.usernames?.join(' x ') : roomCoordinator.getRoomName(room.t, room); +const getRoomDisplayName = (room: Pick, RoomAdminFieldsType>): string | undefined => + room.t === 'd' ? room.usernames?.join(' x ') : roomCoordinator.getRoomName(room.t, room as IRoom); -const RoomRow = ({ room }: { room: Pick }) => { +const RoomRow = ({ room }: { room: Pick, RoomAdminFieldsType> }) => { const t = useTranslation(); const mediaQuery = useMediaQuery('(min-width: 1024px)'); const router = useRouter(); + const formatDate = useFormatDate(); - const { _id, t: type, usersCount, msgs, default: isDefault, featured, ...args } = room; - const icon = roomCoordinator.getRoomDirectives(room.t).getIcon?.(room); + const { _id, t: type, usersCount, msgs, default: isDefault, featured, ts, ...args } = room; + const icon = roomCoordinator.getRoomDirectives(room.t).getIcon?.(room as IRoom); const roomName = getRoomDisplayName(room); const getRoomType = ( - room: Pick, + room: Pick, RoomAdminFieldsType>, ): (typeof roomTypeI18nMap)[keyof typeof roomTypeI18nMap] | 'Teams_Public_Team' | 'Teams_Private_Team' | 'Discussion' => { if (room.teamMain) { return room.t === 'c' ? 'Teams_Public_Team' : 'Teams_Private_Team'; } - if (isDiscussion(room)) { + if (isDiscussion(room as IRoom)) { return 'Discussion'; } return roomTypeI18nMap[(room as IRoom).t as keyof typeof roomTypeI18nMap]; @@ -83,6 +85,7 @@ const RoomRow = ({ room }: { room: Pick }) => { {mediaQuery && {msgs}} {mediaQuery && {isDefault ? t('True') : t('False')}} {mediaQuery && {featured ? t('True') : t('False')}} + {mediaQuery && {ts ? formatDate(ts) : ''}} ); }; diff --git a/apps/meteor/client/views/admin/rooms/RoomsTable.tsx b/apps/meteor/client/views/admin/rooms/RoomsTable.tsx index 094ccb95857a..b4906b6970d1 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsTable.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsTable.tsx @@ -34,7 +34,7 @@ const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): React const prevRoomFilterText = useRef(roomFilters.searchText); - const { sortBy, sortDirection, setSort } = useSort<'name' | 't' | 'usersCount' | 'msgs' | 'default' | 'featured'>('name'); + const { sortBy, sortDirection, setSort } = useSort<'name' | 't' | 'usersCount' | 'msgs' | 'default' | 'featured' | 'ts'>('name'); const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination(); const searchText = useDebouncedValue(roomFilters.searchText, 500); @@ -109,6 +109,9 @@ const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): React > {t('Featured')} + + {t('Created_at')} + )} @@ -121,7 +124,7 @@ const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): React {headers} - + )} diff --git a/apps/meteor/lib/rooms/adminFields.ts b/apps/meteor/lib/rooms/adminFields.ts index 89441f04c9ae..21353da84c5e 100644 --- a/apps/meteor/lib/rooms/adminFields.ts +++ b/apps/meteor/lib/rooms/adminFields.ts @@ -9,6 +9,7 @@ export const adminFields: Partial> = { cl: 1, u: 1, usernames: 1, + ts: 1, usersCount: 1, muted: 1, unmuted: 1, diff --git a/apps/meteor/tests/end-to-end/api/rooms.ts b/apps/meteor/tests/end-to-end/api/rooms.ts index d59d3722f1a4..fa5878cc3c01 100644 --- a/apps/meteor/tests/end-to-end/api/rooms.ts +++ b/apps/meteor/tests/end-to-end/api/rooms.ts @@ -1995,6 +1995,24 @@ describe('[Rooms]', () => { }) .end(done); }); + it('should return an array sorted by "ts" property', (done) => { + void request + .get(api('rooms.adminRooms')) + .set(credentials) + .query({ + sort: JSON.stringify({ + ts: -1, + }), + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('rooms').and.to.be.an('array'); + expect(res.body.rooms).to.have.lengthOf.at.least(1); + expect(res.body.rooms[0]).to.have.property('ts').that.is.a('string'); + }) + .end(done); + }); }); describe('update group dms name', () => { diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 442cac45fada..4a2124e98b98 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -373,6 +373,7 @@ export type RoomAdminFieldsType = | 'cl' | 'u' | 'usernames' + | 'ts' | 'usersCount' | 'muted' | 'unmuted' From 6cb79a0482eed9e350a445dc9629e5e85a4757c0 Mon Sep 17 00:00:00 2001 From: Kishan Lal Rai <85572761+Kishn0109@users.noreply.github.com> Date: Wed, 21 Aug 2024 08:35:27 +0530 Subject: [PATCH 37/49] fix: Inconsistent Markdown Formatting in Custom Status Field (#32574) --- .changeset/kind-drinks-joke.md | 5 +++++ apps/meteor/client/components/MarkdownText.tsx | 18 +++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 .changeset/kind-drinks-joke.md diff --git a/.changeset/kind-drinks-joke.md b/.changeset/kind-drinks-joke.md new file mode 100644 index 000000000000..b235f5556805 --- /dev/null +++ b/.changeset/kind-drinks-joke.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed issue with asterisk-wrapped text not becoming bold when user enters profile custom status. diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx index c9af942f6e1c..3670bcc7cec0 100644 --- a/apps/meteor/client/components/MarkdownText.tsx +++ b/apps/meteor/client/components/MarkdownText.tsx @@ -16,16 +16,21 @@ type MarkdownTextParams = { withTruncatedText: boolean; } & ComponentProps; +const walkTokens = (token: marked.Token) => { + const boldPattern = /^\*.*\*$|^\*.*|.*\*$/; + const italicPattern = /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/; + if (boldPattern.test(token.raw)) { + token.type = 'strong'; + } else if (italicPattern.test(token.raw)) { + token.type = 'em'; + } +}; + +marked.use({ walkTokens }); const documentRenderer = new marked.Renderer(); const inlineRenderer = new marked.Renderer(); const inlineWithoutBreaks = new marked.Renderer(); -marked.Lexer.rules.gfm = { - ...marked.Lexer.rules.gfm, - strong: /^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/, - em: /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/, -}; - const linkMarked = (href: string | null, _title: string | null, text: string): string => `${text} `; const paragraphMarked = (text: string): string => text; @@ -112,7 +117,6 @@ const MarkdownText = ({ const markedHtml = /inline/.test(variant) ? marked.parseInline(new Option(content).innerHTML, markedOptions) : marked.parse(new Option(content).innerHTML, markedOptions); - if (parseEmoji) { // We are using the old emoji parser here. This could come // with additional processing use, but is the workaround available right now. From 2b13061d172b73a9f2219c453ea27785027a2898 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 21 Aug 2024 19:20:12 +0530 Subject: [PATCH 38/49] feat: verify federation configuration before processing any events (#32535) --- .changeset/bright-humans-cross.md | 5 + .changeset/six-beers-fry.md | 5 + .gitignore | 3 + apps/meteor/app/api/server/v1/federation.ts | 18 ++ .../server/functions/saveRoomName.ts | 5 +- .../server/functions/saveRoomTopic.ts | 6 +- .../app/lib/server/functions/createRoom.ts | 11 +- .../app/lib/server/functions/deleteMessage.ts | 20 +- .../server/functions/removeUserFromRoom.ts | 5 +- apps/meteor/app/lib/server/index.ts | 1 + .../methods/checkFederationConfiguration.ts | 80 ++++++++ .../app/reactions/server/setReaction.ts | 4 +- .../infrastructure/rocket-chat/hooks/index.ts | 40 ++-- .../local-services/federation/service.ts | 29 ++- .../rocket-chat/hooks/hooks.spec.ts | 85 ++++----- .../meteor/server/methods/addRoomModerator.ts | 9 +- apps/meteor/server/methods/addRoomOwner.ts | 9 +- .../server/methods/removeUserFromRoom.ts | 4 +- .../federation/domain/IFederationBridge.ts | 1 + .../infrastructure/matrix/Bridge.ts | 64 ++++++- .../rocket-chat/adapters/Settings.ts | 39 +++- .../rocket-chat/adapters/logger.ts | 2 + .../infrastructure/rocket-chat/hooks/index.ts | 106 ++++++----- .../infrastructure/rocket-chat/well-known.ts | 2 +- .../server/services/federation/service.ts | 134 +++++++++++++- .../server/services/federation/utils.ts | 44 +++++ .../messages/hooks/BeforeFederationActions.ts | 13 ++ .../server/services/messages/service.ts | 18 ++ .../room/hooks/BeforeFederationActions.ts | 13 ++ apps/meteor/server/services/room/service.ts | 17 ++ .../meteor/tests/end-to-end/api/federation.ts | 2 +- .../rocket-chat/hooks/hooks.spec.ts | 171 +++++++++--------- .../unit/server/federation/utils.spec.ts | 76 ++++++++ .../hooks/BeforeFederationActions.tests.ts | 78 ++++++++ .../room/hooks/FederationActions.tests.ts | 0 packages/core-services/src/index.ts | 7 +- .../src/types/IFederationService.ts | 31 +++- .../src/types/IMessageService.ts | 2 + .../core-services/src/types/IRoomService.ts | 4 + packages/i18n/src/locales/en.i18n.json | 2 + 40 files changed, 924 insertions(+), 241 deletions(-) create mode 100644 .changeset/bright-humans-cross.md create mode 100644 .changeset/six-beers-fry.md create mode 100644 apps/meteor/app/lib/server/methods/checkFederationConfiguration.ts create mode 100644 apps/meteor/server/services/federation/utils.ts create mode 100644 apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts create mode 100644 apps/meteor/server/services/room/hooks/BeforeFederationActions.ts create mode 100644 apps/meteor/tests/unit/server/federation/utils.spec.ts create mode 100644 apps/meteor/tests/unit/server/services/messages/hooks/BeforeFederationActions.tests.ts create mode 100644 apps/meteor/tests/unit/server/services/room/hooks/FederationActions.tests.ts diff --git a/.changeset/bright-humans-cross.md b/.changeset/bright-humans-cross.md new file mode 100644 index 000000000000..aa0c4c658994 --- /dev/null +++ b/.changeset/bright-humans-cross.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Federation actions like sending message in a federated DM, reacting in a federated chat, etc, will no longer work if the configuration is invalid. diff --git a/.changeset/six-beers-fry.md b/.changeset/six-beers-fry.md new file mode 100644 index 000000000000..48409c2f8de5 --- /dev/null +++ b/.changeset/six-beers-fry.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +New button added to validate Matrix Federation configuration. A new field inside admin settings will reflect the configuration status being either 'Valid' or 'Invalid'. diff --git a/.gitignore b/.gitignore index fcf2b8cd07c7..4e6e4bb29da9 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ yarn-error.log* *.sublime-workspace **/.vim/ + +data/ +registration.yaml diff --git a/apps/meteor/app/api/server/v1/federation.ts b/apps/meteor/app/api/server/v1/federation.ts index 7be5b1fc13fe..5f998546cf3e 100644 --- a/apps/meteor/app/api/server/v1/federation.ts +++ b/apps/meteor/app/api/server/v1/federation.ts @@ -22,3 +22,21 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'federation/configuration.verify', + { authRequired: true, permissionsRequired: ['view-privileged-setting'] }, + { + async get() { + const service = License.hasValidLicense() ? FederationEE : Federation; + + const status = await service.configurationStatus(); + + if (!status.externalReachability.ok || !status.appservice.ok) { + return API.v1.failure(status); + } + + return API.v1.success(status); + }, + }, +); diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts index 0fc15f878bcf..c2af750ffa13 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts @@ -1,4 +1,4 @@ -import { Message } from '@rocket.chat/core-services'; +import { Message, Room } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { Integrations, Rooms, Subscriptions } from '@rocket.chat/models'; @@ -48,6 +48,9 @@ export async function saveRoomName( function: 'RocketChat.saveRoomdisplayName', }); } + + await Room.beforeNameChange(room); + if (displayName === room.name) { return; } diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts index 11b9b5b6e565..a59f2ba82fba 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts @@ -1,4 +1,4 @@ -import { Message } from '@rocket.chat/core-services'; +import { Message, Room } from '@rocket.chat/core-services'; import { Rooms } from '@rocket.chat/models'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -20,6 +20,10 @@ export const saveRoomTopic = async function ( }); } + const room = await Rooms.findOneById(rid); + + await Room.beforeTopicChange(room!); + const update = await Rooms.setTopicById(rid, roomTopic); if (update && sendMessage) { await Message.saveSystemMessage('room_changed_topic', rid, roomTopic || '', user); diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 183cb789051f..b339155775e6 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,7 +1,7 @@ /* eslint-disable complexity */ import { AppEvents, Apps } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; -import { Message, Team } from '@rocket.chat/core-services'; +import { Federation, FederationEE, License, Message, Team } from '@rocket.chat/core-services'; import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services'; import type { ICreatedRoom, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; @@ -224,6 +224,13 @@ export const createRoom = async ( Object.assign(roomProps, eventResult); } + const shouldBeHandledByFederation = roomProps.federated === true || owner.username.includes(':'); + + if (shouldBeHandledByFederation) { + const federation = (await License.hasValidLicense()) ? FederationEE : Federation; + await federation.beforeCreateRoom(roomProps); + } + if (type === 'c') { await callbacks.run('beforeCreateChannel', owner, roomProps); } @@ -232,8 +239,6 @@ export const createRoom = async ( void notifyOnRoomChanged(room, 'inserted'); - const shouldBeHandledByFederation = room.federated === true || owner.username.includes(':'); - await createUsersSubscriptions({ room, members, now, owner, options, shouldBeHandledByFederation }); if (type === 'c') { diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index 04542d5f1d27..a91e77858043 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -1,5 +1,5 @@ import { AppEvents, Apps } from '@rocket.chat/apps'; -import { api } from '@rocket.chat/core-services'; +import { api, Message } from '@rocket.chat/core-services'; import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; import { Messages, Rooms, Uploads, Users, ReadReceipts } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -35,10 +35,18 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise; + } +} + +Meteor.methods({ + async checkFederationConfiguration() { + const uid = Meteor.userId(); + + if (!uid) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'checkFederationConfiguration', + }); + } + + if (!(await Authorization.hasPermission(uid, 'view-privileged-setting'))) { + throw new Meteor.Error('error-not-allowed', 'Action not allowed', { + method: 'checkFederationConfiguration', + }); + } + + const errors: string[] = []; + + const successes: string[] = []; + + const service = License.hasValidLicense() ? FederationEE : Federation; + + const status = await service.configurationStatus(); + + if (status.externalReachability.ok) { + successes.push('homeserver configuration looks good'); + } else { + let err = 'external reachability could not be verified'; + + const { error } = status.externalReachability; + if (error) { + err += `, error: ${error}`; + } + + errors.push(err); + } + + const { + roundTrip: { durationMs: duration }, + } = status.appservice; + + if (status.appservice.ok) { + successes.push(`appservice configuration looks good, total round trip time to homeserver ${duration}ms`); + } else { + errors.push(`failed to verify appservice configuration: ${status.appservice.error}`); + } + + if (errors.length) { + void service.markConfigurationInvalid(); + + if (successes.length) { + const message = ['Configuration could only be partially verified'].concat(successes).concat(errors).join(', '); + + throw new Meteor.Error('error-invalid-configuration', message, { method: 'checkFederationConfiguration' }); + } + + throw new Meteor.Error('error-invalid-configuration', ['Invalid configuration'].concat(errors).join(', '), { + method: 'checkFederationConfiguration', + }); + } + + void service.markConfigurationValid(); + + return { + message: ['All configuration looks good'].concat(successes).join(', '), + }; + }, +}); diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index e35103e9d333..d513c8dda6a5 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -1,5 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import { api } from '@rocket.chat/core-services'; +import { api, Message } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models'; @@ -52,6 +52,8 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction // return; // } + await Message.beforeReacted(message, room); + const userAlreadyReacted = message.reactions && Boolean(message.reactions[reaction]) && diff --git a/apps/meteor/ee/server/local-services/federation/infrastructure/rocket-chat/hooks/index.ts b/apps/meteor/ee/server/local-services/federation/infrastructure/rocket-chat/hooks/index.ts index 13519e873dfb..760c8281af38 100644 --- a/apps/meteor/ee/server/local-services/federation/infrastructure/rocket-chat/hooks/index.ts +++ b/apps/meteor/ee/server/local-services/federation/infrastructure/rocket-chat/hooks/index.ts @@ -1,24 +1,20 @@ import type { IRoom, IUser, Username } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { settings } from '../../../../../../../app/settings/server'; import { callbacks } from '../../../../../../../lib/callbacks'; +import { throwIfFederationNotEnabledOrNotReady } from '../../../../../../../server/services/federation/utils'; export class FederationHooksEE { public static onFederatedRoomCreated(callback: (room: IRoom, owner: IUser, originalMemberList: string[]) => Promise): void { callbacks.add( 'federation.afterCreateFederatedRoom', async (room: IRoom, params: { owner: IUser; originalMemberList: string[] }) => { - if ( - !room || - !isRoomFederated(room) || - !params || - !params.owner || - !params.originalMemberList || - !settings.get('Federation_Matrix_enabled') - ) { + if (!room || !isRoomFederated(room) || !params || !params.owner || !params.originalMemberList) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(room, params.owner, params.originalMemberList); }, callbacks.priority.HIGH, @@ -30,16 +26,12 @@ export class FederationHooksEE { callbacks.add( 'federation.onAddUsersToARoom', async (params: { invitees: IUser[] | Username[]; inviter: IUser }, room: IRoom) => { - if ( - !room || - !isRoomFederated(room) || - !params || - !params.invitees || - !params.inviter || - !settings.get('Federation_Matrix_enabled') - ) { + if (!room || !isRoomFederated(room) || !params || !params.invitees || !params.inviter) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(room, params.invitees, params.inviter); }, callbacks.priority.HIGH, @@ -48,9 +40,12 @@ export class FederationHooksEE { callbacks.add( 'afterAddedToRoom', async (params: { user: IUser; inviter?: IUser }, room: IRoom) => { - if (!room || !isRoomFederated(room) || !params || !params.user || !settings.get('Federation_Matrix_enabled')) { + if (!room || !isRoomFederated(room) || !params || !params.user) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(room, [params.user], params?.inviter); }, callbacks.priority.HIGH, @@ -62,9 +57,10 @@ export class FederationHooksEE { callbacks.add( 'afterCreateDirectRoom', async (room: IRoom, params: { members: IUser[]; creatorId: IUser['_id'] }) => { - if (!room || !params || !params.creatorId || !params.creatorId || !settings.get('Federation_Matrix_enabled')) { + if (!room || !params || !params.creatorId || !params.creatorId) { return; } + throwIfFederationNotEnabledOrNotReady(); await callback(room, params.creatorId, params.members); }, callbacks.priority.HIGH, @@ -76,9 +72,10 @@ export class FederationHooksEE { callbacks.add( 'beforeCreateDirectRoom', async (members: IUser[]) => { - if (!members || !settings.get('Federation_Matrix_enabled')) { + if (!members) { return; } + throwIfFederationNotEnabledOrNotReady(); await callback(members); }, callbacks.priority.HIGH, @@ -90,9 +87,10 @@ export class FederationHooksEE { callbacks.add( 'federation.beforeAddUserToARoom', async (params: { user: IUser | string; inviter?: IUser }, room: IRoom) => { - if (!room || !isRoomFederated(room) || !params || !params.user || !settings.get('Federation_Matrix_enabled')) { + if (!room || !isRoomFederated(room) || !params || !params.user) { return; } + throwIfFederationNotEnabledOrNotReady(); await callback(params.user, room, params.inviter); }, callbacks.priority.HIGH, diff --git a/apps/meteor/ee/server/local-services/federation/service.ts b/apps/meteor/ee/server/local-services/federation/service.ts index 6397f01ee9ac..5c6e210aefb8 100644 --- a/apps/meteor/ee/server/local-services/federation/service.ts +++ b/apps/meteor/ee/server/local-services/federation/service.ts @@ -1,4 +1,9 @@ -import type { IFederationServiceEE, IFederationJoinExternalPublicRoomInput } from '@rocket.chat/core-services'; +import type { + IFederationServiceEE, + IFederationJoinExternalPublicRoomInput, + FederationConfigurationStatus, +} from '@rocket.chat/core-services'; +import type { IRoom } from '@rocket.chat/core-typings'; import type { FederationPaginatedResult, IFederationPublicRooms } from '@rocket.chat/rest-typings'; import { AbstractFederationService } from '../../../../server/services/federation/service'; @@ -216,7 +221,27 @@ export class FederationServiceEE extends AbstractBaseFederationServiceEE impleme return super.stopped(); } - async deactivateRemoteUser(userId: string) { + public async verifyConfiguration(): Promise { + return super.verifyConfiguration(); + } + + public async markConfigurationValid(): Promise { + return super.markConfigurationValid(); + } + + public async markConfigurationInvalid(): Promise { + return super.markConfigurationInvalid(); + } + + public async configurationStatus(): Promise { + return super.configurationStatus(); + } + + public async beforeCreateRoom(room: Partial): Promise { + return super.beforeCreateRoom(room); + } + + async deactivateRemoteUser(userId: string): Promise { return super.deactivateRemoteUser(userId); } } diff --git a/apps/meteor/ee/tests/unit/server/federation/server/infrastructure/rocket-chat/hooks/hooks.spec.ts b/apps/meteor/ee/tests/unit/server/federation/server/infrastructure/rocket-chat/hooks/hooks.spec.ts index 67294ca2f7c7..86ab9df628d8 100644 --- a/apps/meteor/ee/tests/unit/server/federation/server/infrastructure/rocket-chat/hooks/hooks.spec.ts +++ b/apps/meteor/ee/tests/unit/server/federation/server/infrastructure/rocket-chat/hooks/hooks.spec.ts @@ -3,7 +3,7 @@ import proxyquire from 'proxyquire'; import sinon from 'sinon'; const remove = sinon.stub(); -const get = sinon.stub(); +const throwIfFederationNotEnabledOrNotReady = sinon.stub(); const hooks: Record = {}; const { FederationHooksEE } = proxyquire @@ -28,20 +28,19 @@ const { FederationHooksEE } = proxyquire }, }, }, - '../../../../../../../app/settings/server': { - settings: { get }, + '../../../../../../../server/services/federation/utils': { + throwIfFederationNotEnabledOrNotReady, }, }); describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { afterEach(() => { remove.reset(); - get.reset(); + throwIfFederationNotEnabledOrNotReady.reset(); }); describe('#onFederatedRoomCreated()', () => { it('should NOT execute the callback if no room was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onFederatedRoomCreated(stub); hooks['federation-v2-after-create-room'](); @@ -49,7 +48,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided room is not federated', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onFederatedRoomCreated(stub); hooks['federation-v2-after-create-room']({}); @@ -57,7 +55,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no params were provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onFederatedRoomCreated(stub); hooks['federation-v2-after-create-room']({ federated: true }); @@ -65,7 +62,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no owner was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onFederatedRoomCreated(stub); hooks['federation-v2-after-create-room']({ federated: true }, {}); @@ -73,7 +69,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no member list was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onFederatedRoomCreated(stub); hooks['federation-v2-after-create-room']({ federated: true }, { owner: 'owner' }); @@ -81,15 +76,18 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooksEE.onFederatedRoomCreated(stub); - hooks['federation-v2-after-create-room']({ federated: true }, { owner: 'owner', originalMemberList: [] }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect( + hooks['federation-v2-after-create-room']({ federated: true }, { owner: 'owner', originalMemberList: [] }), + ).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onFederatedRoomCreated(stub); hooks['federation-v2-after-create-room']({ federated: true }, { owner: 'owner', originalMemberList: [] }); @@ -99,7 +97,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { describe('#onUsersAddedToARoom() - afterAddedToRoom', () => { it('should NOT execute the callback if no room was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-after-add-user-to-a-room'](); @@ -107,7 +104,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided room is not federated', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-after-add-user-to-a-room']({}, {}); @@ -115,7 +111,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no params were provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-after-add-user-to-a-room']({}, { federated: true }); @@ -123,7 +118,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no user was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-after-add-user-to-a-room']({}, { federated: true }, {}); @@ -131,15 +125,18 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); - hooks['federation-v2-after-add-user-to-a-room']({ user: 'user', inviter: 'inviter' }, { federated: true }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect( + hooks['federation-v2-after-add-user-to-a-room']({ user: 'user', inviter: 'inviter' }, { federated: true }), + ).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-after-add-user-to-a-room']({ user: 'user', inviter: 'inviter' }, { federated: true }); @@ -147,7 +144,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should execute the callback even if there is no inviter (when auto-joining)', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-after-add-user-to-a-room']({ user: 'user' }, { federated: true }); @@ -156,7 +152,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); describe('#onUsersAddedToARoom() - federation.onAddUsersToARoom', () => { it('should NOT execute the callback if no room was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-on-add-users-to-a-room'](); @@ -164,7 +159,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided room is not federated', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-on-add-users-to-a-room']({}, {}); @@ -172,7 +166,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no params were provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-on-add-users-to-a-room']({}, { federated: true }); @@ -180,7 +173,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no user was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-on-add-users-to-a-room']({}, { federated: true }, {}); @@ -188,7 +180,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no inviter was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-on-add-users-to-a-room']({ invitees: ['user'] }, { federated: true }); @@ -196,15 +187,18 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); - hooks['federation-v2-on-add-users-to-a-room']({ invitees: ['user'], inviter: 'inviter' }, { federated: true }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect( + hooks['federation-v2-on-add-users-to-a-room']({ invitees: ['user'], inviter: 'inviter' }, { federated: true }), + ).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onUsersAddedToARoom(stub); hooks['federation-v2-on-add-users-to-a-room']({ invitees: ['user'], inviter: 'inviter' }, { federated: true }); @@ -214,7 +208,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { describe('#onDirectMessageRoomCreated()', () => { it('should NOT execute the callback if no room was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onDirectMessageRoomCreated(stub); hooks['federation-v2-after-create-direct-message-room'](); @@ -222,7 +215,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided room is not federated', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onDirectMessageRoomCreated(stub); hooks['federation-v2-after-create-direct-message-room']({}, {}); @@ -230,7 +222,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no params were provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onDirectMessageRoomCreated(stub); hooks['federation-v2-after-create-direct-message-room']({ federated: true }); @@ -238,7 +229,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no members was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onDirectMessageRoomCreated(stub); hooks['federation-v2-after-create-direct-message-room']({ federated: true }); @@ -246,7 +236,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no creatorId was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onDirectMessageRoomCreated(stub); hooks['federation-v2-after-create-direct-message-room']({ federated: true }, { members: [] }); @@ -254,15 +243,18 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooksEE.onDirectMessageRoomCreated(stub); - hooks['federation-v2-after-create-direct-message-room']({ federated: true }, { creatorId: 'creatorId', members: [] }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect( + hooks['federation-v2-after-create-direct-message-room']({ federated: true }, { creatorId: 'creatorId', members: [] }), + ).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.onDirectMessageRoomCreated(stub); hooks['federation-v2-after-create-direct-message-room']({ federated: true }, { creatorId: 'creatorId', members: [] }); @@ -272,7 +264,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { describe('#beforeDirectMessageRoomCreate()', () => { it('should NOT execute the callback if no members was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.beforeDirectMessageRoomCreate(stub); hooks['federation-v2-before-create-direct-message-room'](); @@ -280,15 +271,16 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooksEE.beforeDirectMessageRoomCreate(stub); - hooks['federation-v2-before-create-direct-message-room']([]); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(hooks['federation-v2-before-create-direct-message-room']([])).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.beforeDirectMessageRoomCreate(stub); hooks['federation-v2-before-create-direct-message-room']([]); @@ -298,7 +290,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { describe('#beforeAddUserToARoom()', () => { it('should NOT execute the callback if no room was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.beforeAddUserToARoom(stub); hooks['federation-v2-before-add-user-to-the-room'](); @@ -306,7 +297,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided room is not federated', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.beforeAddUserToARoom(stub); hooks['federation-v2-before-add-user-to-the-room']({}, {}); @@ -314,7 +304,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no params were provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.beforeAddUserToARoom(stub); hooks['federation-v2-before-add-user-to-the-room']({}, { federated: true }); @@ -322,7 +311,6 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no user was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.beforeAddUserToARoom(stub); hooks['federation-v2-before-add-user-to-the-room']({}, { federated: true }, {}); @@ -330,15 +318,18 @@ describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooksEE.beforeAddUserToARoom(stub); - hooks['federation-v2-before-add-user-to-the-room']({ user: 'user', inviter: 'inviter' }, { federated: true }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect( + hooks['federation-v2-before-add-user-to-the-room']({ user: 'user', inviter: 'inviter' }, { federated: true }), + ).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooksEE.beforeAddUserToARoom(stub); hooks['federation-v2-before-add-user-to-the-room']({ user: 'user', inviter: 'inviter' }, { federated: true }); diff --git a/apps/meteor/server/methods/addRoomModerator.ts b/apps/meteor/server/methods/addRoomModerator.ts index ef64ced09423..a9cc21f30e0d 100644 --- a/apps/meteor/server/methods/addRoomModerator.ts +++ b/apps/meteor/server/methods/addRoomModerator.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { settings } from '../../app/settings/server'; +import { isFederationEnabled, isFederationReady, FederationMatrixInvalidConfigurationError } from '../services/federation/utils'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -36,12 +37,18 @@ Meteor.methods({ }); } - if (!(await hasPermissionAsync(uid, 'set-moderator', rid)) && !isRoomFederated(room)) { + const isFederated = isRoomFederated(room); + + if (!(await hasPermissionAsync(uid, 'set-moderator', rid)) && !isFederated) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'addRoomModerator', }); } + if (isFederated && (!isFederationEnabled() || !isFederationReady())) { + throw new FederationMatrixInvalidConfigurationError('unable to change room owners'); + } + const user = await Users.findOneById(userId); if (!user?.username) { diff --git a/apps/meteor/server/methods/addRoomOwner.ts b/apps/meteor/server/methods/addRoomOwner.ts index f64e6699a4cb..f59267f6719a 100644 --- a/apps/meteor/server/methods/addRoomOwner.ts +++ b/apps/meteor/server/methods/addRoomOwner.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { settings } from '../../app/settings/server'; +import { isFederationReady, isFederationEnabled, FederationMatrixInvalidConfigurationError } from '../services/federation/utils'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -36,12 +37,18 @@ Meteor.methods({ }); } - if (!(await hasPermissionAsync(uid, 'set-owner', rid)) && !isRoomFederated(room)) { + const isFederated = isRoomFederated(room); + + if (!(await hasPermissionAsync(uid, 'set-owner', rid)) && !isFederated) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'addRoomOwner', }); } + if (isFederated && (!isFederationEnabled() || !isFederationReady())) { + throw new FederationMatrixInvalidConfigurationError('unable to change room owners'); + } + const user = await Users.findOneById(userId); if (!user?.username) { diff --git a/apps/meteor/server/methods/removeUserFromRoom.ts b/apps/meteor/server/methods/removeUserFromRoom.ts index 2f0e703a3b66..b039beb7ce64 100644 --- a/apps/meteor/server/methods/removeUserFromRoom.ts +++ b/apps/meteor/server/methods/removeUserFromRoom.ts @@ -1,6 +1,6 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; -import { Message, Team } from '@rocket.chat/core-services'; +import { Message, Team, Room } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Subscriptions, Rooms, Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -56,6 +56,8 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri const removedUser = await Users.findOneByUsernameIgnoringCase(data.username); + await Room.beforeUserRemoved(room); + if (!canKickAnyUser) { const subscription = await Subscriptions.findOneByRoomIdAndUserId(data.rid, removedUser._id, { projection: { _id: 1 }, diff --git a/apps/meteor/server/services/federation/domain/IFederationBridge.ts b/apps/meteor/server/services/federation/domain/IFederationBridge.ts index 635202cdd6f4..1076888f511d 100644 --- a/apps/meteor/server/services/federation/domain/IFederationBridge.ts +++ b/apps/meteor/server/services/federation/domain/IFederationBridge.ts @@ -110,5 +110,6 @@ export interface IFederationBridge { externalUserId: string, externalRoomId: string, ): Promise<{ creator: { id: string; username: string }; name: string; joinedMembers: string[] } | undefined>; + ping(): Promise<{ durationMs: number }>; deactivateUser(externalUserId: string): Promise; } diff --git a/apps/meteor/server/services/federation/infrastructure/matrix/Bridge.ts b/apps/meteor/server/services/federation/infrastructure/matrix/Bridge.ts index f5eb049a7496..31c101bbfdac 100644 --- a/apps/meteor/server/services/federation/infrastructure/matrix/Bridge.ts +++ b/apps/meteor/server/services/federation/infrastructure/matrix/Bridge.ts @@ -24,6 +24,8 @@ let MatrixUserInstance: any; const DEFAULT_TIMEOUT_IN_MS_FOR_JOINING_ROOMS = 180000; +const DEFAULT_TIMEOUT_IN_MS_FOR_PING_EVENT = 60 * 1000; + export class MatrixBridge implements IFederationBridge { protected bridgeInstance: Bridge; @@ -44,6 +46,32 @@ export class MatrixBridge implements IFederationBridge { if (!this.isRunning) { await this.bridgeInstance.run(this.internalSettings.getBridgePort()); + + this.bridgeInstance.addAppServicePath({ + method: 'POST', + path: '/_matrix/app/v1/ping', + checkToken: true, + handler: (_req, res, _next) => { + /* + * https://spec.matrix.org/v1.11/application-service-api/#post_matrixappv1ping + * Spec does not talk about what to do with the id. It is safe to ignore it as we are already checking for + * homeserver token to be correct. + * From the spec this might be a bit confusing, as it shows a txn id for post, but app service doing nothing with it afterwards + * when receiving from the homeserver. + * From spec directly - + AS ---> HS : /_matrix/client/v1/appservice/{appserviceId}/ping {"transaction_id": "meow"} + HS ---> AS : /_matrix/app/v1/ping {"transaction_id": "meow"} + HS <--- AS : 200 OK {} + AS <--- HS : 200 OK {"duration_ms": 123} + * https://github.com/matrix-org/matrix-spec/blob/e53e6ea8764b95f0bdb738549fca6f9f3f901298/content/application-service-api.md?plain=1#L229-L232 + * Code - wise, also doesn't care what happens with the response. + * https://github.com/element-hq/synapse/blob/cb6f4a84a6a8f2b79b80851f37eb5fa4c7c5264a/synapse/rest/client/appservice_ping.py#L80 - nothing done on return + * https://github.com/element-hq/synapse/blob/cb6f4a84a6a8f2b79b80851f37eb5fa4c7c5264a/synapse/appservice/api.py#L321-L332 - not even returning the response, caring for just the http status code - https://github.com/element-hq/synapse/blob/cb6f4a84a6a8f2b79b80851f37eb5fa4c7c5264a/synapse/http/client.py#L532-L537 + */ + res.status(200).json({}); + }, + }); + this.isRunning = true; } } catch (err) { @@ -657,6 +685,10 @@ export class MatrixBridge implements IFederationBridge { return MatrixEnumSendMessageType.FILE; } + private getMyHomeServerOrigin() { + return new URL(`https://${this.internalSettings.getHomeServerDomain()}`).hostname; + } + public async uploadContent( externalSenderId: string, content: Buffer, @@ -724,6 +756,16 @@ export class MatrixBridge implements IFederationBridge { controller: { onEvent: (request) => { const event = request.getData() as unknown as AbstractMatrixEvent; + + // TODO: can we ignore all events from out homeserver? + // This was added particularly to avoid duplicating messages. + // Messages sent from rocket.chat also causes a m.room.message event, which if gets to this bridge + // before the event id promise is resolved, the respective message does not get event id attached to them any longer, + // thus this event handler "resends" the message to the rocket.chat room (not to matrix though). + if (event.type === 'm.room.message' && this.extractHomeserverOrigin(event.sender) === this.getMyHomeServerOrigin()) { + return; + } + this.eventHandler(event); }, onLog: (line, isError) => { @@ -753,7 +795,27 @@ export class MatrixBridge implements IFederationBridge { }; } - public async deactivateUser(uid: string) { + public async ping(): Promise<{ durationMs: number }> { + if (!this.isRunning || !this.bridgeInstance) { + throw new Error("matrix bridge isn't yet running"); + } + + const { duration_ms: durationMs } = await this.bridgeInstance.getIntent().matrixClient.doRequest( + 'POST', + `/_matrix/client/v1/appservice/${this.internalSettings.getApplicationServiceId()}/ping`, + {}, + /* + * Empty txn id as it is optional, neither does the spec says exactly what to do with it. + * https://github.com/matrix-org/matrix-spec/blob/1fc8f8856fe47849f90344cfa91601c984627acb/data/api/client-server/appservice_ping.yaml#L55-L56 + */ + {}, + DEFAULT_TIMEOUT_IN_MS_FOR_PING_EVENT, + ); + + return { durationMs }; + } + + public async deactivateUser(uid: string): Promise { /* * https://spec.matrix.org/v1.11/client-server-api/#post_matrixclientv3accountdeactivate * Using { erase: false } since rocket.chat side on deactivation we do not delete anything. diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts index 9d447e881e78..861137f15e47 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts @@ -68,6 +68,17 @@ export class RocketChatSettingsAdapter { return settings.get('Federation_Matrix_enable_ephemeral_events') === true; } + public isConfigurationValid(): boolean { + return settings.get('Federation_Matrix_configuration_status') === 'Valid'; + } + + public async setConfigurationStatus(status: 'Valid' | 'Invalid'): Promise { + const { modifiedCount } = await Settings.updateOne({ _id: 'Federation_Matrix_configuration_status' }, { $set: { value: status } }); + if (modifiedCount) { + void notifyOnSettingChangedById('Federation_Matrix_configuration_status'); + } + } + public onFederationEnabledStatusChanged( callback: ( enabled: boolean, @@ -205,7 +216,7 @@ export class RocketChatSettingsAdapter { const siteUrl = settings.get('Site_Url'); await settingsRegistry.add('Federation_Matrix_id', `rocketchat_${uniqueId}`, { - readonly: true, + readonly: process.env.NODE_ENV === 'production', type: 'string', i18nLabel: 'Federation_Matrix_id', i18nDescription: 'Federation_Matrix_id_desc', @@ -214,7 +225,7 @@ export class RocketChatSettingsAdapter { }); await settingsRegistry.add('Federation_Matrix_hs_token', homeserverToken, { - readonly: true, + readonly: process.env.NODE_ENV === 'production', type: 'string', i18nLabel: 'Federation_Matrix_hs_token', i18nDescription: 'Federation_Matrix_hs_token_desc', @@ -223,7 +234,7 @@ export class RocketChatSettingsAdapter { }); await settingsRegistry.add('Federation_Matrix_as_token', applicationServiceToken, { - readonly: true, + readonly: process.env.NODE_ENV === 'production', type: 'string', i18nLabel: 'Federation_Matrix_as_token', i18nDescription: 'Federation_Matrix_as_token_desc', @@ -287,5 +298,27 @@ export class RocketChatSettingsAdapter { group: 'Federation', section: 'Matrix Bridge', }); + + await settingsRegistry.add('Federation_Matrix_configuration_status', 'Invalid', { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_configuration_status', + i18nDescription: 'Federation_Matrix_configuration_status_desc', + public: false, + enterprise: false, + invalidValue: '', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_check_configuration_button', 'checkFederationConfiguration', { + type: 'action', + actionText: 'Federation_Matrix_check_configuration', + public: false, + enterprise: false, + invalidValue: '', + group: 'Federation', + section: 'Matrix Bridge', + }); } } diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/logger.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/logger.ts index ddb606d37df8..87412cc2071d 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/logger.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/logger.ts @@ -3,3 +3,5 @@ import { Logger } from '@rocket.chat/logger'; const logger = new Logger('Federation_Matrix'); export const federationBridgeLogger = logger.section('matrix_federation_bridge'); + +export const federationServiceLogger = logger.section('matrix_federation_service'); diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts index f14257512b11..8cac9bc9ffb0 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts @@ -1,19 +1,22 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { isMessageFromMatrixFederation, isRoomFederated, isEditedMessage } from '@rocket.chat/core-typings'; -import { settings } from '../../../../../../app/settings/server'; import { callbacks } from '../../../../../../lib/callbacks'; import { afterLeaveRoomCallback } from '../../../../../../lib/callbacks/afterLeaveRoomCallback'; import { afterRemoveFromRoomCallback } from '../../../../../../lib/callbacks/afterRemoveFromRoomCallback'; import type { FederationRoomServiceSender } from '../../../application/room/sender/RoomServiceSender'; +import { isFederationEnabled, throwIfFederationNotEnabledOrNotReady, throwIfFederationNotReady } from '../../../utils'; export class FederationHooks { public static afterUserLeaveRoom(callback: (user: IUser, room: IRoom) => Promise): void { afterLeaveRoomCallback.add( async (user: IUser, room?: IRoom): Promise => { - if (!room || !isRoomFederated(room) || !user || !settings.get('Federation_Matrix_enabled')) { + if (!room || !isRoomFederated(room) || !user) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(user, room); }, callbacks.priority.HIGH, @@ -24,16 +27,12 @@ export class FederationHooks { public static onUserRemovedFromRoom(callback: (removedUser: IUser, room: IRoom, userWhoRemoved: IUser) => Promise): void { afterRemoveFromRoomCallback.add( async (params, room): Promise => { - if ( - !room || - !isRoomFederated(room) || - !params || - !params.removedUser || - !params.userWhoRemoved || - !settings.get('Federation_Matrix_enabled') - ) { + if (!room || !isRoomFederated(room) || !params || !params.removedUser || !params.userWhoRemoved) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(params.removedUser, room, params.userWhoRemoved); }, callbacks.priority.HIGH, @@ -45,9 +44,10 @@ export class FederationHooks { callbacks.add( 'federation.beforeAddUserToARoom', async (params: { user: IUser | string; inviter?: IUser }, room: IRoom): Promise => { - if (!params?.user || !room) { + if (!params?.user || !room || !isFederationEnabled()) { return; } + await callback(params.user, room); }, callbacks.priority.HIGH, @@ -59,7 +59,7 @@ export class FederationHooks { callbacks.add( 'federation.beforeAddUserToARoom', async (params: { user: IUser | string; inviter: IUser }, room: IRoom): Promise => { - if (!params?.user || !params.inviter || !room || !settings.get('Federation_Matrix_enabled')) { + if (!params?.user || !params.inviter || !room || !isFederationEnabled()) { return; } @@ -74,9 +74,12 @@ export class FederationHooks { callbacks.add( 'federation.beforeCreateDirectMessage', async (members: IUser[]): Promise => { - if (!members || !settings.get('Federation_Matrix_enabled')) { + if (!members) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(members); }, callbacks.priority.HIGH, @@ -88,16 +91,12 @@ export class FederationHooks { callbacks.add( 'afterSetReaction', async (message: IMessage, params: { user: IUser; reaction: string }): Promise => { - if ( - !message || - !isMessageFromMatrixFederation(message) || - !params || - !params.user || - !params.reaction || - !settings.get('Federation_Matrix_enabled') - ) { + if (!message || !isMessageFromMatrixFederation(message) || !params || !params.user || !params.reaction) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(message, params.user, params.reaction); }, callbacks.priority.HIGH, @@ -109,17 +108,12 @@ export class FederationHooks { callbacks.add( 'afterUnsetReaction', async (message: IMessage, params: { user: IUser; reaction: string; oldMessage: IMessage }): Promise => { - if ( - !message || - !isMessageFromMatrixFederation(message) || - !params || - !params.user || - !params.reaction || - !params.oldMessage || - !settings.get('Federation_Matrix_enabled') - ) { + if (!message || !isMessageFromMatrixFederation(message) || !params || !params.user || !params.reaction || !params.oldMessage) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(params.oldMessage, params.user, params.reaction); }, callbacks.priority.HIGH, @@ -131,15 +125,12 @@ export class FederationHooks { callbacks.add( 'afterDeleteMessage', async (message: IMessage, room: IRoom): Promise => { - if ( - !room || - !message || - !isRoomFederated(room) || - !isMessageFromMatrixFederation(message) || - !settings.get('Federation_Matrix_enabled') - ) { + if (!room || !message || !isRoomFederated(room) || !isMessageFromMatrixFederation(message)) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(message, room._id); }, callbacks.priority.HIGH, @@ -150,16 +141,13 @@ export class FederationHooks { public static afterMessageUpdated(callback: (message: IMessage, roomId: IRoom['_id'], userId: string) => Promise): void { callbacks.add( 'afterSaveMessage', - async (message, { room }): Promise => { - if ( - !room || - !isRoomFederated(room) || - !message || - !isMessageFromMatrixFederation(message) || - !settings.get('Federation_Matrix_enabled') - ) { + async (message: IMessage, { room }): Promise => { + if (!room || !isRoomFederated(room) || !message || !isMessageFromMatrixFederation(message)) { return message; } + + throwIfFederationNotEnabledOrNotReady(); + if (!isEditedMessage(message)) { return message; } @@ -174,10 +162,13 @@ export class FederationHooks { public static afterMessageSent(callback: (message: IMessage, roomId: IRoom['_id'], userId: string) => Promise): void { callbacks.add( 'afterSaveMessage', - async (message, { room }): Promise => { - if (!room || !isRoomFederated(room) || !message || !settings.get('Federation_Matrix_enabled')) { + async (message: IMessage, { room }): Promise => { + if (!room || !isRoomFederated(room) || !message) { return message; } + + throwIfFederationNotEnabledOrNotReady(); + if (isEditedMessage(message)) { return message; } @@ -190,9 +181,16 @@ export class FederationHooks { } public static async afterRoomRoleChanged(federationRoomService: FederationRoomServiceSender, data?: Record) { - if (!data || !settings.get('Federation_Matrix_enabled')) { + if (!data) { + return; + } + + if (!isFederationEnabled()) { return; } + + throwIfFederationNotReady(); + const { _id: role, type: action, @@ -225,9 +223,12 @@ export class FederationHooks { callbacks.add( 'afterRoomNameChange', async (params: Record): Promise => { - if (!params?.rid || !params.name || !settings.get('Federation_Matrix_enabled')) { + if (!params?.rid || !params.name) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(params.rid, params.name); }, callbacks.priority.HIGH, @@ -239,9 +240,12 @@ export class FederationHooks { callbacks.add( 'afterRoomTopicChange', async (params: Record): Promise => { - if (!params?.rid || !params.topic || !settings.get('Federation_Matrix_enabled')) { + if (!params?.rid || !params.topic) { return; } + + throwIfFederationNotEnabledOrNotReady(); + await callback(params.rid, params.topic); }, callbacks.priority.HIGH, @@ -266,5 +270,7 @@ export class FederationHooks { callbacks.remove('afterSaveMessage', 'federation-v2-after-room-message-updated'); callbacks.remove('afterSaveMessage', 'federation-v2-after-room-message-sent'); callbacks.remove('afterSaveMessage', 'federation-v2-after-room-message-sent'); + callbacks.remove('afterRoomNameChange', 'federation-v2-after-room-name-changed'); + callbacks.remove('afterRoomTopicChange', 'federation-v2-after-room-topic-changed'); } } diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/well-known.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/well-known.ts index b94dfe6628c4..b1088c2f6ff9 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/well-known.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/well-known.ts @@ -30,7 +30,7 @@ async function returnMatrixClientJSON(_: IncomingMessage, res: ServerResponse) { res.setHeader('content-type', 'application/json'); - res.write(JSON.stringify({ 'm.homeserver': `${protocol}//${hostname}` })); + res.write(JSON.stringify({ 'm.homeserver': { base_url: `${protocol}//${hostname}` } })); res.end(); } diff --git a/apps/meteor/server/services/federation/service.ts b/apps/meteor/server/services/federation/service.ts index 66d3fd0cb6ee..904e73913a17 100644 --- a/apps/meteor/server/services/federation/service.ts +++ b/apps/meteor/server/services/federation/service.ts @@ -1,5 +1,10 @@ +import { IncomingMessage } from 'node:http'; +import { URL } from 'node:url'; + import { ServiceClassInternal } from '@rocket.chat/core-services'; -import type { IFederationService } from '@rocket.chat/core-services'; +import type { IFederationService, FederationConfigurationStatus } from '@rocket.chat/core-services'; +import { isRoomFederated, type IRoom } from '@rocket.chat/core-typings'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import type { FederationRoomServiceSender } from './application/room/sender/RoomServiceSender'; import type { FederationUserServiceSender } from './application/user/sender/UserServiceSender'; @@ -12,10 +17,28 @@ import type { RocketChatNotificationAdapter } from './infrastructure/rocket-chat import type { RocketChatRoomAdapter } from './infrastructure/rocket-chat/adapters/Room'; import type { RocketChatSettingsAdapter } from './infrastructure/rocket-chat/adapters/Settings'; import type { RocketChatUserAdapter } from './infrastructure/rocket-chat/adapters/User'; +import { federationServiceLogger } from './infrastructure/rocket-chat/adapters/logger'; import { FederationRoomSenderConverter } from './infrastructure/rocket-chat/converters/RoomSender'; import { FederationHooks } from './infrastructure/rocket-chat/hooks'; - import './infrastructure/rocket-chat/well-known'; +import { throwIfFederationNotEnabledOrNotReady } from './utils'; + +function extractError(e: unknown) { + if (e instanceof Error || (typeof e === 'object' && e && 'toString' in e)) { + if ('name' in e && e.name === 'AbortError') { + return 'Operation timed out'; + } + + return e.toString(); + } + + federationServiceLogger.error(e); + + return 'Unknown error'; +} + +// for airgapped deployments, use environment variable to override a local instance of federationtester +const federationTesterHost = process.env.FEDERATION_TESTER_HOST?.trim()?.replace(/\/$/, '') || 'https://federationtester.matrix.org'; export abstract class AbstractFederationService extends ServiceClassInternal { private cancelSettingsObserver: () => void; @@ -126,7 +149,9 @@ export abstract class AbstractFederationService extends ServiceClassInternal { if (isFederationEnabled) { await this.onDisableFederation(); - return this.onEnableFederation(); + await this.onEnableFederation(); + await this.verifyConfiguration(); + return; } return this.onDisableFederation(); @@ -180,6 +205,17 @@ export abstract class AbstractFederationService extends ServiceClassInternal { this.internalQueueInstance.setHandler(federationEventsHandler.handleEvent.bind(federationEventsHandler), this.PROCESSING_CONCURRENCY); } + private canOtherHomeserversFederate(): Promise { + const url = new URL(`https://${this.internalSettingsAdapter.getHomeServerDomain()}`); + + return new Promise((resolve, reject) => + fetch(`${federationTesterHost}/api/federation-ok?server_name=${url.host}`) + .then((response) => response.text()) + .then((text) => resolve(text === 'GOOD')) + .catch(reject), + ); + } + protected getInternalSettingsAdapter(): RocketChatSettingsAdapter { return this.internalSettingsAdapter; } @@ -239,7 +275,75 @@ export abstract class AbstractFederationService extends ServiceClassInternal { return this.bridge.verifyInviteeIds(matrixIds); } - protected async deactivateRemoteUser(remoteUserId: string) { + public async configurationStatus(): Promise { + const status: FederationConfigurationStatus = { + appservice: { + roundTrip: { durationMs: -1 }, + ok: false, + }, + externalReachability: { + ok: false, + }, + }; + + try { + const pingResponse = await this.bridge.ping(); + status.appservice.roundTrip.durationMs = pingResponse.durationMs; + status.appservice.ok = true; + } catch (error) { + if (error instanceof IncomingMessage) { + if (error.statusCode === 404) { + status.appservice.error = 'homeserver version must be >=1.84.x'; + } else { + status.appservice.error = `received unknown status from homeserver, message: ${error.statusMessage}`; + } + } else { + status.appservice.error = extractError(error); + } + } + + try { + status.externalReachability.ok = await this.canOtherHomeserversFederate(); + } catch (error) { + status.externalReachability.error = extractError(error); + } + + return status; + } + + public async markConfigurationValid(): Promise { + return this.internalSettingsAdapter.setConfigurationStatus('Valid'); + } + + public async markConfigurationInvalid(): Promise { + return this.internalSettingsAdapter.setConfigurationStatus('Invalid'); + } + + public async verifyConfiguration(): Promise { + try { + await this.bridge?.ping(); // throws error if fails + + if (!(await this.canOtherHomeserversFederate())) { + throw new Error('External reachability could not be verified'); + } + + void this.markConfigurationValid(); + } catch (error) { + federationServiceLogger.error(error); + + void this.markConfigurationInvalid(); + } + } + + public async beforeCreateRoom(room: Partial): Promise { + if (!isRoomFederated(room)) { + return; + } + + throwIfFederationNotEnabledOrNotReady(); + } + + protected async deactivateRemoteUser(remoteUserId: string): Promise { return this.bridge.deactivateUser(remoteUserId); } } @@ -347,7 +451,27 @@ export class FederationService extends AbstractBaseFederationService implements return super.created(); } - public async deactivateRemoteUser(userId: string) { + public async verifyConfiguration(): Promise { + return super.verifyConfiguration(); + } + + public async markConfigurationValid(): Promise { + return super.markConfigurationValid(); + } + + public async markConfigurationInvalid(): Promise { + return super.markConfigurationInvalid(); + } + + public async configurationStatus(): Promise { + return super.configurationStatus(); + } + + public async beforeCreateRoom(room: Partial): Promise { + return super.beforeCreateRoom(room); + } + + public async deactivateRemoteUser(userId: string): Promise { return super.deactivateRemoteUser(userId); } } diff --git a/apps/meteor/server/services/federation/utils.ts b/apps/meteor/server/services/federation/utils.ts new file mode 100644 index 000000000000..0256b4f04fe8 --- /dev/null +++ b/apps/meteor/server/services/federation/utils.ts @@ -0,0 +1,44 @@ +import { settings } from '../../../app/settings/server'; + +export function isFederationEnabled(): boolean { + return settings.get('Federation_Matrix_enabled'); +} + +export function isFederationReady(): boolean { + return settings.get('Federation_Matrix_configuration_status') === 'Valid'; +} + +export function throwIfFederationNotEnabledOrNotReady(): void { + if (!isFederationEnabled()) { + throw new Error('Federation is not enabled'); + } + + if (!isFederationReady()) { + throw new Error('Federation configuration is invalid'); + } +} + +export function throwIfFederationEnabledButNotReady(): void { + if (!isFederationEnabled()) { + return; + } + + throwIfFederationNotReady(); +} + +export function throwIfFederationNotReady(): void { + if (!isFederationReady()) { + throw new Error('Federation configuration is invalid'); + } +} + +export class FederationMatrixInvalidConfigurationError extends Error { + constructor(cause?: string) { + // eslint-disable-next-line prefer-template + const message = 'Federation configuration is invalid' + (cause ? ',' + cause[0].toLowerCase() + cause.slice(1) : ''); + + super(message); + + this.name = 'FederationMatrixInvalidConfiguration'; + } +} diff --git a/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts b/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts new file mode 100644 index 000000000000..a954e4899970 --- /dev/null +++ b/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts @@ -0,0 +1,13 @@ +import { type IMessage, type IRoom, isMessageFromMatrixFederation, isRoomFederated } from '@rocket.chat/core-typings'; + +import { isFederationEnabled, isFederationReady } from '../../federation/utils'; + +export class FederationActions { + public static shouldPerformAction(message: IMessage, room: IRoom): boolean { + if (isMessageFromMatrixFederation(message) || isRoomFederated(room)) { + return isFederationEnabled() && isFederationReady(); + } + + return true; + } +} diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 906868b6bb17..b20b5236b7fe 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -13,6 +13,8 @@ import { executeSetReaction } from '../../../app/reactions/server/setReaction'; import { settings } from '../../../app/settings/server'; import { getUserAvatarURL } from '../../../app/utils/server/getUserAvatarURL'; import { BeforeSaveCannedResponse } from '../../../ee/server/hooks/messages/BeforeSaveCannedResponse'; +import { FederationMatrixInvalidConfigurationError } from '../federation/utils'; +import { FederationActions } from './hooks/BeforeFederationActions'; import { BeforeSaveBadWords } from './hooks/BeforeSaveBadWords'; import { BeforeSaveCheckMAC } from './hooks/BeforeSaveCheckMAC'; import { BeforeSaveJumpToMessage } from './hooks/BeforeSaveJumpToMessage'; @@ -168,6 +170,10 @@ export class MessageService extends ServiceClassInternal implements IMessageServ // TODO looks like this one was not being used (so I'll left it commented) // await this.joinDiscussionOnMessage({ message, room, user }); + if (!FederationActions.shouldPerformAction(message, room)) { + throw new FederationMatrixInvalidConfigurationError('Unable to send message'); + } + message = await mentionServer.execute(message); message = await this.cannedResponse.replacePlaceholders({ message, room, user }); message = await this.badWords.filterBadWords({ message }); @@ -237,4 +243,16 @@ export class MessageService extends ServiceClassInternal implements IMessageServ // await Room.join({ room, user }); // } + + async beforeReacted(message: IMessage, room: IRoom) { + if (!FederationActions.shouldPerformAction(message, room)) { + throw new FederationMatrixInvalidConfigurationError('Unable to react to message'); + } + } + + async beforeDelete(message: IMessage, room: IRoom) { + if (!FederationActions.shouldPerformAction(message, room)) { + throw new FederationMatrixInvalidConfigurationError('Unable to delete message'); + } + } } diff --git a/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts b/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts new file mode 100644 index 000000000000..925fdfcbee32 --- /dev/null +++ b/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts @@ -0,0 +1,13 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + +import { throwIfFederationNotEnabledOrNotReady } from '../../federation/utils'; + +export class FederationActions { + public static blockIfRoomFederatedButServiceNotReady({ federated }: Pick) { + if (!federated) { + return; + } + + throwIfFederationNotEnabledOrNotReady(); + } +} diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index 3ba47284ddee..5bbde4a2814e 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -11,6 +11,7 @@ import { getValidRoomName } from '../../../app/utils/server/lib/getValidRoomName import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { createDirectMessage } from '../../methods/createDirectMessage'; +import { FederationActions } from './hooks/BeforeFederationActions'; export class RoomService extends ServiceClassInternal implements IRoomService { protected name = 'room'; @@ -121,4 +122,20 @@ export class RoomService extends ServiceClassInternal implements IRoomService { return addUserToRoom(room._id, user); } + + async beforeLeave(room: IRoom): Promise { + FederationActions.blockIfRoomFederatedButServiceNotReady(room); + } + + async beforeUserRemoved(room: IRoom): Promise { + FederationActions.blockIfRoomFederatedButServiceNotReady(room); + } + + async beforeNameChange(room: IRoom): Promise { + FederationActions.blockIfRoomFederatedButServiceNotReady(room); + } + + async beforeTopicChange(room: IRoom): Promise { + FederationActions.blockIfRoomFederatedButServiceNotReady(room); + } } diff --git a/apps/meteor/tests/end-to-end/api/federation.ts b/apps/meteor/tests/end-to-end/api/federation.ts index 9d832d9fc1ac..a1bfd92f1d29 100644 --- a/apps/meteor/tests/end-to-end/api/federation.ts +++ b/apps/meteor/tests/end-to-end/api/federation.ts @@ -67,7 +67,7 @@ describe('federation', () => { .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { - expect(res.body).to.have.property('m.homeserver', 'http://localhost'); + expect(res.body['m.homeserver']).to.have.property('base_url', 'http://localhost'); }); }); }); diff --git a/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts b/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts index c77f6e4993fa..94d8fa26bd9c 100644 --- a/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts +++ b/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts @@ -8,7 +8,9 @@ import { afterRemoveFromRoomCallback } from '../../../../../../../lib/callbacks/ import type * as hooksModule from '../../../../../../../server/services/federation/infrastructure/rocket-chat/hooks'; const remove = sinon.stub(); -const get = sinon.stub(); +const throwIfFederationNotEnabledOrNotReady = sinon.stub(); +const throwIfFederationNotReady = sinon.stub(); +const isFederationEnabled = sinon.stub(); const hooks: Record = {}; const { FederationHooks } = proxyquire @@ -35,8 +37,10 @@ const { FederationHooks } = proxyquire '../../../../../../lib/callbacks/afterRemoveFromRoomCallback': { afterRemoveFromRoomCallback, }, - '../../../../../../app/settings/server': { - settings: { get }, + '../../../utils': { + throwIfFederationNotEnabledOrNotReady, + throwIfFederationNotReady, + isFederationEnabled, }, }); @@ -44,12 +48,13 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { beforeEach(() => { FederationHooks.removeAllListeners(); remove.reset(); - get.reset(); + throwIfFederationNotEnabledOrNotReady.reset(); + throwIfFederationNotReady.reset(); + isFederationEnabled.reset(); }); describe('#afterUserLeaveRoom()', () => { it('should NOT execute the callback if no room was provided', async () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterUserLeaveRoom(stub); @@ -59,7 +64,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided room is not federated', async () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterUserLeaveRoom(stub); @@ -70,7 +74,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no user was provided', async () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterUserLeaveRoom(stub); @@ -81,18 +84,20 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', async () => { - get.returns(false); + const error = new Error(); + + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooks.afterUserLeaveRoom(stub); // @ts-expect-error - await afterLeaveRoomCallback.run({}, { federated: true }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(afterLeaveRoomCallback.run({}, { federated: true })).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', async () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterUserLeaveRoom(stub); @@ -105,7 +110,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#onUserRemovedFromRoom()', () => { it('should NOT execute the callback if no room was provided', async () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.onUserRemovedFromRoom(stub); @@ -116,7 +120,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided room is not federated', async () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.onUserRemovedFromRoom(stub); @@ -127,7 +130,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no params were provided', async () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.onUserRemovedFromRoom(stub); @@ -138,7 +140,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no removedUser was provided', async () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.onUserRemovedFromRoom(stub); // @ts-expect-error @@ -148,7 +149,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no userWhoRemoved was provided', async () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.onUserRemovedFromRoom(stub); // @ts-expect-error @@ -158,17 +158,21 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', async () => { - get.returns(false); + const error = new Error(); + + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooks.onUserRemovedFromRoom(stub); - // @ts-expect-error - await afterRemoveFromRoomCallback.run({ removedUser: 'removedUser', userWhoRemoved: 'userWhoRemoved' }, { federated: true }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect( + // @ts-ignore-error + afterRemoveFromRoomCallback.run({ removedUser: 'removedUser', userWhoRemoved: 'userWhoRemoved' }, { federated: true }), + ).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', async () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.onUserRemovedFromRoom(stub); // @ts-expect-error @@ -179,7 +183,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#canAddFederatedUserToNonFederatedRoom()', () => { it('should NOT execute the callback if no room was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.canAddFederatedUserToNonFederatedRoom(stub); hooks['federation-v2-can-add-federated-user-to-non-federated-room'](); @@ -187,7 +190,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no params were provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.canAddFederatedUserToNonFederatedRoom(stub); hooks['federation-v2-can-add-federated-user-to-non-federated-room']({}, { federated: true }); @@ -195,7 +197,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no user was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.canAddFederatedUserToNonFederatedRoom(stub); hooks['federation-v2-can-add-federated-user-to-non-federated-room']({}, { federated: true }, {}); @@ -203,7 +204,7 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should execute the callback when everything is correct', () => { - get.returns(true); + isFederationEnabled.returns(true); const stub = sinon.stub(); FederationHooks.canAddFederatedUserToNonFederatedRoom(stub); hooks['federation-v2-can-add-federated-user-to-non-federated-room']({ user: 'user' }, { federated: true }); @@ -213,7 +214,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#canAddFederatedUserToFederatedRoom()', () => { it('should NOT execute the callback if no room was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.canAddFederatedUserToFederatedRoom(stub); hooks['federation-v2-can-add-federated-user-to-federated-room'](); @@ -221,7 +221,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no params were provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.canAddFederatedUserToFederatedRoom(stub); hooks['federation-v2-can-add-federated-user-to-federated-room']({}, { federated: true }); @@ -229,7 +228,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no user was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.canAddFederatedUserToFederatedRoom(stub); hooks['federation-v2-can-add-federated-user-to-federated-room']({}, { federated: true }, {}); @@ -237,7 +235,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no inviter was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.canAddFederatedUserToFederatedRoom(stub); hooks['federation-v2-can-add-federated-user-to-federated-room']({ user: 'user' }, { federated: true }, {}); @@ -245,15 +242,15 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); const stub = sinon.stub(); FederationHooks.canAddFederatedUserToFederatedRoom(stub); + // eslint-disable-next-line @typescript-eslint/no-floating-promises hooks['federation-v2-can-add-federated-user-to-federated-room']({ user: 'user', inviter: 'inviter' }, { federated: true }); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); + isFederationEnabled.returns(true); const stub = sinon.stub(); FederationHooks.canAddFederatedUserToFederatedRoom(stub); hooks['federation-v2-can-add-federated-user-to-federated-room']({ user: 'user', inviter: 'inviter' }, { federated: true }); @@ -263,7 +260,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#canCreateDirectMessageFromUI()', () => { it('should NOT execute the callback if no members was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.canCreateDirectMessageFromUI(stub); hooks['federation-v2-can-create-direct-message-from-ui-ce'](); @@ -271,15 +267,16 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooks.canCreateDirectMessageFromUI(stub); - hooks['federation-v2-can-create-direct-message-from-ui-ce']([]); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(hooks['federation-v2-can-create-direct-message-from-ui-ce']([])).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.canCreateDirectMessageFromUI(stub); hooks['federation-v2-can-create-direct-message-from-ui-ce']([]); @@ -289,7 +286,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#afterMessageReacted()', () => { it('should NOT execute the callback if no message was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageReacted(stub); hooks['federation-v2-after-message-reacted'](); @@ -297,7 +293,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided message is not from a federated room', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageReacted(stub); hooks['federation-v2-after-message-reacted']({}); @@ -305,7 +300,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no params were provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageReacted(stub); hooks['federation-v2-after-message-reacted']({ federation: { eventId: 'eventId' } }, {}); @@ -313,7 +307,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no user was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageReacted(stub); hooks['federation-v2-after-message-reacted']({ federation: { eventId: 'eventId' } }, { federated: true }, {}); @@ -321,7 +314,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no reaction was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageReacted(stub); hooks['federation-v2-after-message-reacted']({ federation: { eventId: 'eventId' } }, { user: 'user' }); @@ -329,15 +321,18 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooks.afterMessageReacted(stub); - hooks['federation-v2-after-message-reacted']({ federation: { eventId: 'eventId' } }, { user: 'user', reaction: 'reaction' }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect( + hooks['federation-v2-after-message-reacted']({ federation: { eventId: 'eventId' } }, { user: 'user', reaction: 'reaction' }), + ).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageReacted(stub); hooks['federation-v2-after-message-reacted']({ federation: { eventId: 'eventId' } }, { user: 'user', reaction: 'reaction' }); @@ -347,7 +342,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#afterMessageunReacted()', () => { it('should NOT execute the callback if no message was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageunReacted(stub); hooks['federation-v2-after-message-unreacted'](); @@ -355,7 +349,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided message is not from a federated room', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageunReacted(stub); hooks['federation-v2-after-message-unreacted']({}); @@ -363,7 +356,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no params were provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageunReacted(stub); hooks['federation-v2-after-message-unreacted']({ federation: { eventId: 'eventId' } }, {}); @@ -371,7 +363,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no user was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageunReacted(stub); hooks['federation-v2-after-message-unreacted']({ federation: { eventId: 'eventId' } }, { federated: true }, {}); @@ -379,7 +370,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no reaction was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageunReacted(stub); hooks['federation-v2-after-message-unreacted']({ federation: { eventId: 'eventId' } }, { user: 'user' }); @@ -387,7 +377,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no oldMessage was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageunReacted(stub); hooks['federation-v2-after-message-unreacted']({ federation: { eventId: 'eventId' } }, { user: 'user', reaction: 'reaction' }); @@ -395,18 +384,21 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooks.afterMessageunReacted(stub); - hooks['federation-v2-after-message-unreacted']( - { federation: { eventId: 'eventId' } }, - { user: 'user', reaction: 'reaction', oldMessage: {} }, - ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect( + hooks['federation-v2-after-message-unreacted']( + { federation: { eventId: 'eventId' } }, + { user: 'user', reaction: 'reaction', oldMessage: {} }, + ), + ).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageunReacted(stub); hooks['federation-v2-after-message-unreacted']( @@ -419,7 +411,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#afterMessageDeleted()', () => { it('should NOT execute the callback if no room was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageDeleted(stub); hooks['federation-v2-after-room-message-deleted'](); @@ -427,7 +418,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided room is not federated', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageDeleted(stub); hooks['federation-v2-after-room-message-deleted']({}, {}); @@ -435,7 +425,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided message is not from a federated room', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageDeleted(stub); hooks['federation-v2-after-room-message-deleted']({}, { federated: true }); @@ -443,15 +432,18 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooks.afterMessageDeleted(stub); - hooks['federation-v2-after-room-message-deleted']({ federation: { eventId: 'eventId' } }, { federated: true }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect( + hooks['federation-v2-after-room-message-deleted']({ federation: { eventId: 'eventId' } }, { federated: true }), + ).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageDeleted(stub); hooks['federation-v2-after-room-message-deleted']({ federation: { eventId: 'eventId' } }, { federated: true, _id: 'roomId' }); @@ -461,7 +453,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#afterMessageUpdated()', () => { it('should NOT execute the callback if no room was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageUpdated(stub); hooks['federation-v2-after-room-message-updated'](); @@ -469,7 +460,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided room is not federated', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageUpdated(stub); hooks['federation-v2-after-room-message-updated']({}, {}); @@ -477,7 +467,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided message is not from a federated room', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageUpdated(stub); hooks['federation-v2-after-room-message-updated']({}, { federated: true }); @@ -485,15 +474,18 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooks.afterMessageUpdated(stub); - hooks['federation-v2-after-room-message-updated']({ federation: { eventId: 'eventId' } }, { federated: true }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect( + hooks['federation-v2-after-room-message-updated']({ federation: { eventId: 'eventId' } }, { federated: true }), + ).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should NOT execute the callback if the message is not a edited one', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageUpdated(stub); hooks['federation-v2-after-room-message-updated']({ federation: { eventId: 'eventId' } }, { federated: true }); @@ -504,7 +496,7 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { const editedAt = faker.date.recent(); const editedBy = { _id: 'userId' }; const message = { federation: { eventId: 'eventId' }, editedAt, editedBy }; - get.returns(true); + const stub = sinon.stub(); FederationHooks.afterMessageUpdated(stub); hooks['federation-v2-after-room-message-updated'](message, { room: { federated: true, _id: 'roomId' } }); @@ -514,7 +506,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#afterMessageSent()', () => { it('should NOT execute the callback if no room was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageSent(stub); hooks['federation-v2-after-room-message-sent'](); @@ -522,7 +513,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if the provided room is not federated', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageSent(stub); hooks['federation-v2-after-room-message-sent']({}, {}); @@ -530,15 +520,16 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooks.afterMessageSent(stub); - hooks['federation-v2-after-room-message-sent']({}, { federated: true }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(hooks['federation-v2-after-room-message-sent']({}, { federated: true })).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should NOT execute the callback if the message is edited one', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageSent(stub); const editedAt = faker.date.recent(); @@ -548,7 +539,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterMessageSent(stub); hooks['federation-v2-after-room-message-sent']({ u: { _id: 'userId' } }, { room: { federated: true, _id: 'roomId' } }); @@ -581,7 +571,8 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT call the Federation module is disabled', async () => { - get.returns(false); + isFederationEnabled.returns(false); + await FederationHooks.afterRoomRoleChanged(handlers, undefined); expect(handlers.onRoomOwnerAdded.called).to.be.false; @@ -591,7 +582,9 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT call the handler if the event is not for roles we are interested in on Federation', async () => { - get.returns(true); + isFederationEnabled.returns(true); + // verifyFederationReady doesn't throw by default in here + await FederationHooks.afterRoomRoleChanged(handlers, { _id: 'not-interested' }); expect(handlers.onRoomOwnerAdded.called).to.be.false; @@ -601,7 +594,8 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT call the handler there is no handler for the event', async () => { - get.returns(true); + isFederationEnabled.returns(true); + await FederationHooks.afterRoomRoleChanged(handlers, { _id: 'owner', type: 'not-existing-type' }); expect(handlers.onRoomOwnerAdded.called).to.be.false; @@ -615,7 +609,8 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { const internalTargetUserId = 'internalTargetUserId'; const internalUserId = 'internalUserId'; it(`should call the handler for the event ${type}`, async () => { - get.returns(true); + isFederationEnabled.returns(true); + await FederationHooks.afterRoomRoleChanged(handlers, { _id: type.split('-')[0], type: type.split('-')[1], @@ -637,7 +632,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#afterRoomNameChanged()', () => { it('should NOT execute the callback if no params was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterRoomNameChanged(stub); hooks['federation-v2-after-room-name-changed'](); @@ -645,7 +639,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no roomId was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterRoomNameChanged(stub); hooks['federation-v2-after-room-name-changed']({}); @@ -653,7 +646,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no roomName was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterRoomNameChanged(stub); hooks['federation-v2-after-room-name-changed']({ rid: 'roomId' }); @@ -661,15 +653,16 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooks.afterRoomNameChanged(stub); - hooks['federation-v2-after-room-name-changed']({ rid: 'roomId', name: 'roomName' }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(hooks['federation-v2-after-room-name-changed']({ rid: 'roomId', name: 'roomName' })).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterRoomNameChanged(stub); hooks['federation-v2-after-room-name-changed']({ rid: 'roomId', name: 'roomName' }); @@ -679,7 +672,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#afterRoomTopicChanged()', () => { it('should NOT execute the callback if no params was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterRoomTopicChanged(stub); hooks['federation-v2-after-room-topic-changed'](); @@ -687,7 +679,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no roomId was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterRoomTopicChanged(stub); hooks['federation-v2-after-room-topic-changed']({}); @@ -695,7 +686,6 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if no topic was provided', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterRoomTopicChanged(stub); hooks['federation-v2-after-room-topic-changed']({ rid: 'roomId' }); @@ -703,15 +693,16 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { }); it('should NOT execute the callback if federation module was disabled', () => { - get.returns(false); + const error = new Error(); + throwIfFederationNotEnabledOrNotReady.throws(error); const stub = sinon.stub(); FederationHooks.afterRoomTopicChanged(stub); - hooks['federation-v2-after-room-topic-changed']({ rid: 'roomId', topic: 'topic' }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(hooks['federation-v2-after-room-topic-changed']({ rid: 'roomId', topic: 'topic' })).to.have.rejectedWith(error); expect(stub.called).to.be.false; }); it('should execute the callback when everything is correct', () => { - get.returns(true); const stub = sinon.stub(); FederationHooks.afterRoomTopicChanged(stub); hooks['federation-v2-after-room-topic-changed']({ rid: 'roomId', topic: 'topic' }); @@ -735,7 +726,7 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { describe('#removeAllListeners()', () => { it('should remove all the listeners', () => { FederationHooks.removeAllListeners(); - expect(remove.callCount).to.be.equal(9); + expect(remove.callCount).to.be.equal(11); expect( remove.getCall(0).calledWith('federation.beforeAddUserToARoom', 'federation-v2-can-add-federated-user-to-non-federated-room'), ).to.be.equal(true); @@ -751,6 +742,8 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { expect(remove.getCall(6).calledWith('afterSaveMessage', 'federation-v2-after-room-message-updated')).to.be.equal(true); expect(remove.getCall(7).calledWith('afterSaveMessage', 'federation-v2-after-room-message-sent')).to.be.equal(true); expect(remove.getCall(8).calledWith('afterSaveMessage', 'federation-v2-after-room-message-sent')).to.be.equal(true); + expect(remove.getCall(9).calledWith('afterRoomNameChange', 'federation-v2-after-room-name-changed')).to.be.equal(true); + expect(remove.getCall(10).calledWith('afterRoomTopicChange', 'federation-v2-after-room-topic-changed')).to.be.equal(true); }); }); }); diff --git a/apps/meteor/tests/unit/server/federation/utils.spec.ts b/apps/meteor/tests/unit/server/federation/utils.spec.ts new file mode 100644 index 000000000000..cc024d93f7dd --- /dev/null +++ b/apps/meteor/tests/unit/server/federation/utils.spec.ts @@ -0,0 +1,76 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; + +import type * as federationUtilsModule from '../../../../server/services/federation/utils'; + +const settings = { + enabled: false, + ready: false, + + get(id: string) { + switch (id) { + case 'Federation_Matrix_enabled': + return this.enabled; + case 'Federation_Matrix_configuration_status': + return this.ready ? 'Valid' : 'Invalid'; + } + }, + + reset() { + this.enabled = false; + this.ready = false; + }, +}; + +const { throwIfFederationNotEnabledOrNotReady, throwIfFederationNotReady, throwIfFederationEnabledButNotReady } = proxyquire + .noCallThru() + .load('../../../../server/services/federation/utils', { + '../../../app/settings/server': { + settings, + }, + }); + +describe('Federation helper functions', () => { + afterEach(() => { + settings.reset(); + }); + + describe('#throwIfFederationNotReady', () => { + it('should throw if federation is not ready', () => { + expect(throwIfFederationNotReady).to.throw(); + }); + }); + + describe('#throwIfFederationNotEnabledOrNotReady', () => { + it('should throw if federation is not enabled', () => { + expect(throwIfFederationNotEnabledOrNotReady).to.throw(); + }); + + it('should throw if federation is enabled but configuration is invalid', () => { + settings.enabled = true; + expect(throwIfFederationNotEnabledOrNotReady).to.throw(); + }); + + it('should not throw if both federation is enabled and configuration is valid', () => { + settings.enabled = true; + settings.ready = true; + expect(throwIfFederationNotEnabledOrNotReady).to.not.throw(); + }); + }); + + describe('#throwIfFederationEnabledButNotReady', () => { + it('should throw if federation is enabled and configuration is invalid', () => { + settings.enabled = true; + settings.ready = false; + + expect(throwIfFederationEnabledButNotReady).to.throw(); + }); + + it('should not throw if federation is disabled', () => { + expect(throwIfFederationEnabledButNotReady).to.not.throw(); + + settings.ready = true; + expect(throwIfFederationEnabledButNotReady).to.not.throw(); + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/messages/hooks/BeforeFederationActions.tests.ts b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeFederationActions.tests.ts new file mode 100644 index 000000000000..1c48fae7d369 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeFederationActions.tests.ts @@ -0,0 +1,78 @@ +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +import type * as beforeFederationActionModule from '../../../../../../server/services/messages/hooks/BeforeFederationActions'; + +const isFederationReady = sinon.stub(); +const isFederationEnabled = sinon.stub(); + +const { FederationActions } = proxyquire + .noCallThru() + .load('../../../../../../server/services/messages/hooks/BeforeFederationActions', { + '../../federation/utils': { + isFederationEnabled, + isFederationReady, + }, + }); + +describe("Don't perform action depending on federation status", () => { + afterEach(() => { + isFederationReady.reset(); + isFederationEnabled.reset(); + }); + + it('should return true if neither message nor room is federated', () => { + expect(FederationActions.shouldPerformAction({} as IMessage, {} as IRoom)).to.be.true; + }); + + describe('Federation is enabled', () => { + it('should return true if message is federated and configuration is valid', () => { + isFederationEnabled.returns(true); + isFederationReady.returns(true); + + expect(FederationActions.shouldPerformAction({ federation: { eventId: Date.now().toString() } } as IMessage, {} as unknown as IRoom)) + .to.be.true; + }); + + it('should return true if room is federated and configuration is valid', () => { + isFederationEnabled.returns(true); + isFederationReady.returns(true); + + expect(FederationActions.shouldPerformAction({} as unknown as IMessage, { federated: true } as IRoom)).to.be.true; + }); + + it('should return false if message is federated and configuration is invalid', () => { + isFederationEnabled.returns(true); + isFederationReady.returns(false); + + expect(FederationActions.shouldPerformAction({ federation: { eventId: Date.now().toString() } } as IMessage, {} as unknown as IRoom)) + .to.be.false; + }); + + it('should return false if room is federated and configuration is invalid', () => { + isFederationEnabled.returns(true); + isFederationReady.returns(false); + + expect(FederationActions.shouldPerformAction({} as unknown as IMessage, { federated: true } as IRoom)).to.be.false; + }); + }); + + describe('Federation is disabled', () => { + it('should return false if room is federated', () => { + isFederationEnabled.returns(false); + isFederationReady.returns(false); + + expect(FederationActions.shouldPerformAction({} as unknown as IMessage, { federated: true } as IRoom)).to.be.false; + }); + + it('should return false if message is federated', () => { + isFederationEnabled.returns(false); + isFederationReady.returns(false); + + expect(FederationActions.shouldPerformAction({ federation: { eventId: Date.now().toString() } } as IMessage, {} as unknown as IRoom)) + .to.be.false; + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/room/hooks/FederationActions.tests.ts b/apps/meteor/tests/unit/server/services/room/hooks/FederationActions.tests.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index ce51f4695aec..0f93ccbee04c 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -59,7 +59,12 @@ export { IBroker, IBrokerNode, BaseMetricOptions, IServiceMetrics } from './type export { IServiceContext, ServiceClass, IServiceClass, ServiceClassInternal } from './types/ServiceClass'; -export { IFederationService, IFederationServiceEE, IFederationJoinExternalPublicRoomInput } from './types/IFederationService'; +export { + IFederationService, + IFederationServiceEE, + IFederationJoinExternalPublicRoomInput, + FederationConfigurationStatus, +} from './types/IFederationService'; export { ConversationData, diff --git a/packages/core-services/src/types/IFederationService.ts b/packages/core-services/src/types/IFederationService.ts index 5563bd60db40..ffd1c1b009f6 100644 --- a/packages/core-services/src/types/IFederationService.ts +++ b/packages/core-services/src/types/IFederationService.ts @@ -1,10 +1,35 @@ +import type { IRoom } from '@rocket.chat/core-typings'; import type { FederationPaginatedResult, IFederationPublicRooms } from '@rocket.chat/rest-typings'; -export interface IFederationService { - createDirectMessageRoomAndInviteUser(internalInviterId: string, internalRoomId: string, externalInviteeId: string): Promise; +export type FederationConfigurationStatus = { + appservice: { + error?: string; + ok: boolean; + roundTrip: { + durationMs: number; + }; + }; + + externalReachability: { + error?: string; + ok: boolean; + }; +}; +interface IFederationBaseService { verifyMatrixIds(matrixIds: string[]): Promise>; + configurationStatus(): Promise; + + markConfigurationValid(): Promise; + + markConfigurationInvalid(): Promise; + + beforeCreateRoom(room: Partial): Promise; +} + +export interface IFederationService extends IFederationBaseService { + createDirectMessageRoomAndInviteUser(internalInviterId: string, internalRoomId: string, externalInviteeId: string): Promise; deactivateRemoteUser(userId: string): Promise; } @@ -15,7 +40,7 @@ export interface IFederationJoinExternalPublicRoomInput { pageToken?: string; } -export interface IFederationServiceEE { +export interface IFederationServiceEE extends IFederationBaseService { createDirectMessageRoom(internalUserId: string, invitees: string[]): Promise; searchPublicRooms( diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index 0563fc6f148d..ca84f78ea677 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -21,4 +21,6 @@ export interface IMessageService { deleteMessage(user: IUser, message: IMessage): Promise; updateMessage(message: IMessage, user: IUser, originalMsg?: IMessage): Promise; reactToMessage(userId: string, reaction: string, messageId: IMessage['_id'], shouldReact?: boolean): Promise; + beforeReacted(message: IMessage, room: IRoom): Promise; + beforeDelete(message: IMessage, room: IRoom): Promise; } diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index 23186590af50..36bf5dff2564 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -53,4 +53,8 @@ export interface IRoomService { ): Promise; getRouteLink(room: AtLeast): Promise; join(param: { room: IRoom; user: Pick; joinCode?: string }): Promise; + beforeLeave(room: IRoom): Promise; + beforeUserRemoved(room: IRoom): Promise; + beforeNameChange(room: IRoom): Promise; + beforeTopicChange(room: IRoom): Promise; } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index c270bb9bffb1..a807629819a6 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2328,6 +2328,8 @@ "Federation_Matrix_serve_well_known": "Serve Well Known", "Federation_Matrix_serve_well_known_Description": "Serve /.well-known/matrix/server and /.well-known/matrix/client directly from within Rocket.Chat instead of reverse proxy for federation", "Federation_Matrix_serve_well_known_Alert": "Keep this off if using DNS srv records for federation, or use a reverse proxy to return static JSON if federation traffic is heavy. Read mode.", + "Federation_Matrix_check_configuration": "Verify configuration", + "Federation_Matrix_configuration_status": "Configuration status", "Field": "Field", "Field_removed": "Field removed", "Field_required": "Field required", From ed4ea307d8c73babc62ce23935550e7b573786a3 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 21 Aug 2024 10:37:21 -0600 Subject: [PATCH 39/49] feat: Allow apps to react/unreact to messages (#33001) --- .changeset/nasty-windows-smile.md | 5 ++++ .../app/apps/server/bridges/messages.ts | 22 ++++++++++++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- ee/apps/ddp-streamer/package.json | 2 +- ee/packages/presence/package.json | 2 +- packages/apps/package.json | 2 +- packages/core-services/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/rest-typings/package.json | 2 +- yarn.lock | 26 +++++++++---------- 12 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 .changeset/nasty-windows-smile.md diff --git a/.changeset/nasty-windows-smile.md b/.changeset/nasty-windows-smile.md new file mode 100644 index 000000000000..e80ec3db27a9 --- /dev/null +++ b/.changeset/nasty-windows-smile.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Allow apps to react/unreact to messages via bridge diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index 18a68220998f..5a60a37e8b0b 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -1,4 +1,5 @@ import type { IAppServerOrchestrator, IAppsMessage, IAppsUser } from '@rocket.chat/apps'; +import type { Reaction } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import type { ITypingDescriptor } from '@rocket.chat/apps-engine/server/bridges/MessageBridge'; import { MessageBridge } from '@rocket.chat/apps-engine/server/bridges/MessageBridge'; @@ -10,6 +11,7 @@ import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; import notifications from '../../../notifications/server/lib/Notifications'; +import { executeSetReaction } from '../../../reactions/server/setReaction'; export class AppMessageBridge extends MessageBridge { constructor(private readonly orch: IAppServerOrchestrator) { @@ -118,4 +120,24 @@ export class AppMessageBridge extends MessageBridge { throw new Error('Unrecognized typing scope provided'); } } + + private isValidReaction(reaction: Reaction): boolean { + return reaction.startsWith(':') && reaction.endsWith(':'); + } + + protected async addReaction(messageId: string, userId: string, reaction: Reaction): Promise { + if (!this.isValidReaction(reaction)) { + throw new Error('Invalid reaction'); + } + + return executeSetReaction(messageId, userId, reaction, true); + } + + protected async removeReaction(messageId: string, userId: string, reaction: Reaction): Promise { + if (!this.isValidReaction(reaction)) { + throw new Error('Invalid reaction'); + } + + return executeSetReaction(messageId, userId, reaction, false); + } } diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 76a0c59d54e6..4aaff739e4d0 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -18,7 +18,7 @@ "author": "Rocket.Chat", "license": "MIT", "dependencies": { - "@rocket.chat/apps-engine": "1.44.0", + "@rocket.chat/apps-engine": "1.45.0-alpha.864", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 5addaf756f8a..8f2138b47b7d 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -230,7 +230,7 @@ "@rocket.chat/agenda": "workspace:^", "@rocket.chat/api-client": "workspace:^", "@rocket.chat/apps": "workspace:^", - "@rocket.chat/apps-engine": "1.44.0", + "@rocket.chat/apps-engine": "1.45.0-alpha.864", "@rocket.chat/base64": "workspace:^", "@rocket.chat/cas-validate": "workspace:^", "@rocket.chat/core-services": "workspace:^", diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index fdee5d5d3b9a..9bf49018ed0a 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -15,7 +15,7 @@ ], "author": "Rocket.Chat", "dependencies": { - "@rocket.chat/apps-engine": "1.44.0", + "@rocket.chat/apps-engine": "1.45.0-alpha.864", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 21f1883b6704..ad160821647c 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@babel/preset-env": "~7.22.20", "@babel/preset-typescript": "~7.22.15", - "@rocket.chat/apps-engine": "1.44.0", + "@rocket.chat/apps-engine": "1.45.0-alpha.864", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@types/node": "^14.18.63", diff --git a/packages/apps/package.json b/packages/apps/package.json index 15289501be4c..0aca06fdf070 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -18,7 +18,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/apps-engine": "1.44.0", + "@rocket.chat/apps-engine": "1.45.0-alpha.864", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/model-typings": "workspace:^" } diff --git a/packages/core-services/package.json b/packages/core-services/package.json index d576f87bef27..02176d6bf88c 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -34,7 +34,7 @@ "extends": "../../package.json" }, "dependencies": { - "@rocket.chat/apps-engine": "1.44.0", + "@rocket.chat/apps-engine": "1.45.0-alpha.864", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/message-parser": "workspace:^", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 267e75e5c177..499dab0156b6 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -22,7 +22,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/apps-engine": "1.44.0", + "@rocket.chat/apps-engine": "1.45.0-alpha.864", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/ui-kit": "workspace:~" diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 6d6b882c89d2..101769768017 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -63,7 +63,7 @@ "@babel/preset-env": "~7.22.20", "@babel/preset-react": "~7.22.15", "@babel/preset-typescript": "~7.22.15", - "@rocket.chat/apps-engine": "1.44.0", + "@rocket.chat/apps-engine": "1.45.0-alpha.864", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "^0.57.0", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 6a5bf5464e98..896e78900626 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -23,7 +23,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/apps-engine": "1.44.0", + "@rocket.chat/apps-engine": "1.45.0-alpha.864", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/ui-kit": "workspace:~", diff --git a/yarn.lock b/yarn.lock index 6a6c2a8ee9d0..64d79a0d27e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8483,9 +8483,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/apps-engine@npm:1.44.0": - version: 1.44.0 - resolution: "@rocket.chat/apps-engine@npm:1.44.0" +"@rocket.chat/apps-engine@npm:1.45.0-alpha.864": + version: 1.45.0-alpha.864 + resolution: "@rocket.chat/apps-engine@npm:1.45.0-alpha.864" dependencies: "@msgpack/msgpack": 3.0.0-beta2 adm-zip: ^0.5.9 @@ -8501,7 +8501,7 @@ __metadata: uuid: ~8.3.2 peerDependencies: "@rocket.chat/ui-kit": "*" - checksum: f2b1b13c6a070c8d320a6d681ede6945a5882f9e2d42f2569bfc8c098229f761c7ef358589d3f1714d17b157fafa8e4869f28752408356f4a9286f62cb517f46 + checksum: 4f223dd0671d920e4eaafa465fe87584473f3295061252d1020c0d0e1c076c3b74ee98af1ee5aedfeb72b042e38c3f381d10a151b3a2abcf33a7de8ac6146fa1 languageName: node linkType: hard @@ -8509,7 +8509,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/apps@workspace:packages/apps" dependencies: - "@rocket.chat/apps-engine": 1.44.0 + "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/model-typings": "workspace:^" eslint: ~8.45.0 @@ -8582,7 +8582,7 @@ __metadata: "@babel/core": ~7.22.20 "@babel/preset-env": ~7.22.20 "@babel/preset-typescript": ~7.22.15 - "@rocket.chat/apps-engine": 1.44.0 + "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/icons": ^0.36.0 @@ -8609,7 +8609,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/core-typings@workspace:packages/core-typings" dependencies: - "@rocket.chat/apps-engine": 1.44.0 + "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/icons": ^0.36.0 "@rocket.chat/message-parser": "workspace:^" @@ -8681,7 +8681,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ddp-streamer@workspace:ee/apps/ddp-streamer" dependencies: - "@rocket.chat/apps-engine": 1.44.0 + "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/ddp-client": "workspace:~" @@ -8879,7 +8879,7 @@ __metadata: "@babel/preset-env": ~7.22.20 "@babel/preset-react": ~7.22.15 "@babel/preset-typescript": ~7.22.15 - "@rocket.chat/apps-engine": 1.44.0 + "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": ^0.57.0 @@ -9329,7 +9329,7 @@ __metadata: "@rocket.chat/agenda": "workspace:^" "@rocket.chat/api-client": "workspace:^" "@rocket.chat/apps": "workspace:^" - "@rocket.chat/apps-engine": 1.44.0 + "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/base64": "workspace:^" "@rocket.chat/cas-validate": "workspace:^" "@rocket.chat/core-services": "workspace:^" @@ -9953,7 +9953,7 @@ __metadata: "@babel/core": ~7.22.20 "@babel/preset-env": ~7.22.20 "@babel/preset-typescript": ~7.22.15 - "@rocket.chat/apps-engine": 1.44.0 + "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" @@ -10067,7 +10067,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/rest-typings@workspace:packages/rest-typings" dependencies: - "@rocket.chat/apps-engine": 1.44.0 + "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/message-parser": "workspace:^" @@ -36732,7 +36732,7 @@ __metadata: version: 0.0.0-use.local resolution: "rocketchat-services@workspace:apps/meteor/ee/server/services" dependencies: - "@rocket.chat/apps-engine": 1.44.0 + "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": ~0.31.25 From 2f402fd33b0814815d8967ba457e5f8b30c5630c Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 21 Aug 2024 13:49:43 -0300 Subject: [PATCH 40/49] chore: allow using db watchers in localhost (#33120) --- packages/core-services/src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 0f93ccbee04c..8eea19ea7405 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -143,8 +143,11 @@ export { IUserService, }; +const disabledEnvVar = String(process.env.DISABLE_DB_WATCHERS).toLowerCase(); + export const dbWatchersDisabled = - ['yes', 'true'].includes(String(process.env.DISABLE_DB_WATCHERS).toLowerCase()) || process.env.NODE_ENV !== 'production'; + (process.env.NODE_ENV === 'production' && ['yes', 'true'].includes(disabledEnvVar)) || + (process.env.NODE_ENV !== 'production' && !['no', 'false'].includes(disabledEnvVar)); // TODO think in a way to not have to pass the service name to proxify here as well export const Authorization = proxifyWithWait('authorization'); From a7f12cc0ac76c9a59f89a0f17d0dfd143602a7b8 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Wed, 21 Aug 2024 23:33:20 +0530 Subject: [PATCH 41/49] fix: Forget session on window close (#33040) --- .changeset/two-bikes-crash.md | 7 +++ apps/meteor/client/startup/accounts.ts | 13 +++++ .../externals/meteor/accounts-base.d.ts | 2 + ...account-forgetSessionOnWindowClose.spec.ts | 55 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 .changeset/two-bikes-crash.md create mode 100644 apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts diff --git a/.changeset/two-bikes-crash.md b/.changeset/two-bikes-crash.md new file mode 100644 index 000000000000..a120435e4a48 --- /dev/null +++ b/.changeset/two-bikes-crash.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed an issue related to setting Accounts_ForgetUserSessionOnWindowClose, this setting was not working as expected. + +The new meteor 2.16 release introduced a new option to configure the Accounts package and choose between the local storage or session storage. They also changed how Meteor.\_localstorage works internally. Due to these changes in Meteor, our setting to use session storage wasn't working as expected. This PR fixes this issue and configures the Accounts package according to the workspace settings. diff --git a/apps/meteor/client/startup/accounts.ts b/apps/meteor/client/startup/accounts.ts index 3be110bc0a09..60f2de02bde0 100644 --- a/apps/meteor/client/startup/accounts.ts +++ b/apps/meteor/client/startup/accounts.ts @@ -2,6 +2,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import { settings } from '../../app/settings/client'; import { mainReady } from '../../app/ui-utils/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { t } from '../../app/utils/lib/i18n'; @@ -24,3 +25,15 @@ Accounts.onEmailVerificationLink((token: string) => { }); }); }); + +Meteor.startup(() => { + Tracker.autorun(() => { + const forgetUserSessionOnWindowClose = settings.get('Accounts_ForgetUserSessionOnWindowClose'); + + if (forgetUserSessionOnWindowClose === undefined) { + return; + } + + Accounts.config({ clientStorage: forgetUserSessionOnWindowClose ? 'session' : 'local' }); + }); +}); diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 3f0b148120e7..31b70f7b7154 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -42,6 +42,8 @@ declare module 'meteor/accounts-base' { function _clearAllLoginTokens(userId: string | null): void; + function config(options: { clientStorage: 'session' | 'local' }): void; + class ConfigError extends Error {} class LoginCancelledError extends Error { diff --git a/apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts b/apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts new file mode 100644 index 000000000000..a19b0e9866da --- /dev/null +++ b/apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts @@ -0,0 +1,55 @@ +import { DEFAULT_USER_CREDENTIALS } from './config/constants'; +import { Registration } from './page-objects'; +import { test, expect } from './utils/test'; + +test.describe.serial('Forget session on window close setting', () => { + let poRegistration: Registration; + + test.beforeEach(async ({ page }) => { + poRegistration = new Registration(page); + + await page.goto('/home'); + }); + + test.describe('Setting off', async () => { + test.beforeAll(async ({ api }) => { + await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: false }); + }); + + test('Login using credentials and reload to stay logged in', async ({ page, context }) => { + await poRegistration.username.type('user1'); + await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password); + await poRegistration.btnLogin.click(); + + await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible(); + + const newPage = await context.newPage(); + await newPage.goto('/home'); + + await expect(newPage.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible(); + }); + }); + + test.describe('Setting on', async () => { + test.beforeAll(async ({ api }) => { + await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: true }); + }); + + test.afterAll(async ({ api }) => { + await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: false }); + }); + + test('Login using credentials and reload to get logged out', async ({ page, context }) => { + await poRegistration.username.type('user1'); + await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password); + await poRegistration.btnLogin.click(); + + await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible(); + + const newPage = await context.newPage(); + await newPage.goto('/home'); + + await expect(newPage.locator('role=button[name="Login"]')).toBeVisible(); + }); + }); +}); From 94518af19429c00808662e5bfe486599ad7bea25 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 21 Aug 2024 12:35:26 -0600 Subject: [PATCH 42/49] chore: Remove `googleapis` package from codebase (#33121) --- apps/meteor/package.json | 1 - yarn.lock | 49 ++-------------------------------------- 2 files changed, 2 insertions(+), 48 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 8f2138b47b7d..b8c35ceab8a7 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -343,7 +343,6 @@ "filesize": "9.0.11", "generate-password": "^1.7.1", "google-libphonenumber": "^3.2.33", - "googleapis": "^104.0.0", "gravatar": "^1.8.2", "he": "^1.2.0", "highlight.js": "^11.6.0", diff --git a/yarn.lock b/yarn.lock index 64d79a0d27e6..1698092642f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9551,7 +9551,6 @@ __metadata: filesize: 9.0.11 generate-password: ^1.7.1 google-libphonenumber: ^3.2.33 - googleapis: ^104.0.0 gravatar: ^1.8.2 he: ^1.2.0 highlight.js: ^11.6.0 @@ -24339,19 +24338,6 @@ __metadata: languageName: node linkType: hard -"gaxios@npm:^4.0.0": - version: 4.3.3 - resolution: "gaxios@npm:4.3.3" - dependencies: - abort-controller: ^3.0.0 - extend: ^3.0.2 - https-proxy-agent: ^5.0.0 - is-stream: ^2.0.0 - node-fetch: ^2.6.7 - checksum: 0b72a00875404e2c3d7aca9f32535e931d7b0ebb850dc92fafc1685b99a109b04205c63e4637a2d0d9a261ac50adf83f7d33435f73e256dcca32564ef9358fee - languageName: node - linkType: hard - "gaxios@npm:^5.0.0, gaxios@npm:^5.0.1": version: 5.1.0 resolution: "gaxios@npm:5.1.0" @@ -24901,7 +24887,7 @@ __metadata: languageName: node linkType: hard -"google-auth-library@npm:^8.0.1, google-auth-library@npm:^8.0.2": +"google-auth-library@npm:^8.0.1": version: 8.7.0 resolution: "google-auth-library@npm:8.7.0" dependencies: @@ -24936,30 +24922,6 @@ __metadata: languageName: node linkType: hard -"googleapis-common@npm:^6.0.0": - version: 6.0.0 - resolution: "googleapis-common@npm:6.0.0" - dependencies: - extend: ^3.0.2 - gaxios: ^4.0.0 - google-auth-library: ^8.0.2 - qs: ^6.7.0 - url-template: ^2.0.8 - uuid: ^8.0.0 - checksum: a6c697ac0c829f7bdfcfe32f5fb16fbf7b864cc173257c09eff6e4893f3bd56064904f7b6843d4c8ff074b128609c6cc2ac7490aaf9ed70cab417dc2fb54236b - languageName: node - linkType: hard - -"googleapis@npm:^104.0.0": - version: 104.0.0 - resolution: "googleapis@npm:104.0.0" - dependencies: - google-auth-library: ^8.0.2 - googleapis-common: ^6.0.0 - checksum: b6aabd6913daf4ebbdc5500907991560680e4bedda6852a03767c890467719369e7b7c8e9152bf77908345c7626f2465d329a916270c7bf277a4af0d26262ae1 - languageName: node - linkType: hard - "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -34843,7 +34805,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.11.0, qs@npm:^6.10.0, qs@npm:^6.10.3, qs@npm:^6.7.0, qs@npm:^6.9.4, qs@npm:^6.9.6": +"qs@npm:6.11.0, qs@npm:^6.10.0, qs@npm:^6.10.3, qs@npm:^6.9.4, qs@npm:^6.9.6": version: 6.11.0 resolution: "qs@npm:6.11.0" dependencies: @@ -41187,13 +41149,6 @@ __metadata: languageName: node linkType: hard -"url-template@npm:^2.0.8": - version: 2.0.8 - resolution: "url-template@npm:2.0.8" - checksum: 4183fccd74e3591e4154134d4443dccecba9c455c15c7df774f1f1e3fa340fd9bffb903b5beec347196d15ce49c34edf6dec0634a95d170ad6e78c0467d6e13e - languageName: node - linkType: hard - "url-to-options@npm:^1.0.1": version: 1.0.1 resolution: "url-to-options@npm:1.0.1" From 1e1e849e255e2f390b43d65908162e3926cf367a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 21 Aug 2024 17:11:58 -0300 Subject: [PATCH 43/49] fix: `ContextualbarHeader` expanded prop (#33109) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .changeset/rich-pillows-hang.md | 5 +++++ .../client/components/Contextualbar/ContextualbarHeader.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/rich-pillows-hang.md diff --git a/.changeset/rich-pillows-hang.md b/.changeset/rich-pillows-hang.md new file mode 100644 index 000000000000..b714a5e6acd9 --- /dev/null +++ b/.changeset/rich-pillows-hang.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the `expanded` prop being accidentally forwarded to `ContextualbarHeader` diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx index 795182df8465..ebd92f0095e3 100644 --- a/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx @@ -8,10 +8,10 @@ type ContextualbarHeaderProps = { children: ReactNode; } & ComponentPropsWithoutRef; -const ContextualbarHeader = (props: ContextualbarHeaderProps) => ( +const ContextualbarHeader = ({ expanded, ...props }: ContextualbarHeaderProps) => ( - + From dd37ea1b35eb142c43927f08b1f19576d42c05a3 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Wed, 21 Aug 2024 18:01:11 -0300 Subject: [PATCH 44/49] fix: validate username before registering user (#32743) --- .changeset/purple-dolls-serve.md | 7 ++ apps/meteor/app/api/server/v1/users.ts | 5 + .../app/lib/server/functions/setUsername.ts | 21 ++--- .../lib/server/functions/validateUsername.ts | 15 +++ apps/meteor/tests/end-to-end/api/users.ts | 31 ++++-- .../server/functions/validateUsername.spec.ts | 94 +++++++++++++++++++ packages/i18n/src/locales/en.i18n.json | 1 + packages/i18n/src/locales/pt-BR.i18n.json | 3 +- .../web-ui-registration/src/RegisterForm.tsx | 6 ++ 9 files changed, 161 insertions(+), 22 deletions(-) create mode 100644 .changeset/purple-dolls-serve.md create mode 100644 apps/meteor/app/lib/server/functions/validateUsername.ts create mode 100644 apps/meteor/tests/unit/app/lib/server/functions/validateUsername.spec.ts diff --git a/.changeset/purple-dolls-serve.md b/.changeset/purple-dolls-serve.md new file mode 100644 index 000000000000..fc44faa60a38 --- /dev/null +++ b/.changeset/purple-dolls-serve.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/web-ui-registration': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where creating a new user with an invalid username (containing special characters) resulted in an error message, but the user was still created. The user creation process now properly aborts when an invalid username is provided. diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 7ae585b89dfa..9c56ecac01cb 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -45,6 +45,7 @@ import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername'; import { validateCustomFields } from '../../../lib/server/functions/validateCustomFields'; import { validateNameChars } from '../../../lib/server/functions/validateNameChars'; +import { validateUsername } from '../../../lib/server/functions/validateUsername'; import { notifyOnUserChange, notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; import { generateAccessToken } from '../../../lib/server/methods/createToken'; import { settings } from '../../../settings/server'; @@ -651,6 +652,10 @@ API.v1.addRoute( return API.v1.failure('Name contains invalid characters'); } + if (!validateUsername(this.bodyParams.username)) { + return API.v1.failure(`The username provided is not valid`); + } + if (!(await checkUsernameAvailability(this.bodyParams.username))) { return API.v1.failure('Username is already in use'); } diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts index e19ef874db0f..5b2b1923da75 100644 --- a/apps/meteor/app/lib/server/functions/setUsername.ts +++ b/apps/meteor/app/lib/server/functions/setUsername.ts @@ -17,6 +17,7 @@ import { getAvatarSuggestionForUser } from './getAvatarSuggestionForUser'; import { joinDefaultChannels } from './joinDefaultChannels'; import { saveUserIdentity } from './saveUserIdentity'; import { setUserAvatar } from './setUserAvatar'; +import { validateUsername } from './validateUsername'; export const setUsernameWithValidation = async (userId: string, username: string, joinDefaultChannelsSilenced?: boolean): Promise => { if (!username) { @@ -37,14 +38,7 @@ export const setUsernameWithValidation = async (userId: string, username: string return; } - let nameValidation; - try { - nameValidation = new RegExp(`^${settings.get('UTF8_User_Names_Validation')}$`); - } catch (error) { - nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); - } - - if (!nameValidation.test(username)) { + if (!validateUsername(username)) { throw new Meteor.Error( 'username-invalid', `${_.escape(username)} is not a valid username, use only letters, numbers, dots, hyphens and underscores`, @@ -74,18 +68,15 @@ export const setUsernameWithValidation = async (userId: string, username: string export const _setUsername = async function (userId: string, u: string, fullUser: IUser): Promise { const username = u.trim(); + if (!userId || !username) { return false; } - let nameValidation; - try { - nameValidation = new RegExp(`^${settings.get('UTF8_User_Names_Validation')}$`); - } catch (error) { - nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); - } - if (!nameValidation.test(username)) { + + if (!validateUsername(username)) { return false; } + const user = fullUser || (await Users.findOneById(userId)); // User already has desired username, return if (user.username === username) { diff --git a/apps/meteor/app/lib/server/functions/validateUsername.ts b/apps/meteor/app/lib/server/functions/validateUsername.ts new file mode 100644 index 000000000000..523667282d22 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/validateUsername.ts @@ -0,0 +1,15 @@ +import { settings } from '../../../settings/server'; + +export const validateUsername = (username: string): boolean => { + const settingsRegExp = settings.get('UTF8_User_Names_Validation'); + const defaultPattern = /^[0-9a-zA-Z-_.]+$/; + + let usernameRegExp: RegExp; + try { + usernameRegExp = settingsRegExp ? new RegExp(`^${settingsRegExp}$`) : defaultPattern; + } catch (e) { + usernameRegExp = defaultPattern; + } + + return usernameRegExp.test(username); +}; diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index d6112dd2416b..e908baebd974 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -605,6 +605,25 @@ describe('[Users]', () => { }) .end(done); }); + + it('should return an error when trying register new user with an invalid username', (done) => { + void request + .post(api('users.register')) + .send({ + email, + name: 'name', + username: 'test$username<>', + pass: 'test', + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error').and.to.be.equal('The username provided is not valid'); + }) + .end(done); + }); + it('should return an error when trying register new user with an existing username', (done) => { void request .post(api('users.register')) @@ -3700,9 +3719,9 @@ describe('[Users]', () => { it('should invalidate all active sesions', (done) => { /* We want to validate that the login with the "old" credentials fails - However, the removal of the tokens is done asynchronously. - Thus, we check that within the next seconds, at least one try to - access an authentication requiring route fails */ + However, the removal of the tokens is done asynchronously. + Thus, we check that within the next seconds, at least one try to + access an authentication requiring route fails */ let counter = 0; async function checkAuthenticationFails() { @@ -4060,9 +4079,9 @@ describe('[Users]', () => { it('should invalidate all active sesions', (done) => { /* We want to validate that the login with the "old" credentials fails - However, the removal of the tokens is done asynchronously. - Thus, we check that within the next seconds, at least one try to - access an authentication requiring route fails */ + However, the removal of the tokens is done asynchronously. + Thus, we check that within the next seconds, at least one try to + access an authentication requiring route fails */ let counter = 0; async function checkAuthenticationFails() { diff --git a/apps/meteor/tests/unit/app/lib/server/functions/validateUsername.spec.ts b/apps/meteor/tests/unit/app/lib/server/functions/validateUsername.spec.ts new file mode 100644 index 000000000000..647873b8ffbd --- /dev/null +++ b/apps/meteor/tests/unit/app/lib/server/functions/validateUsername.spec.ts @@ -0,0 +1,94 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +describe('validateUsername', () => { + const getStub = sinon.stub(); + + const proxySettings = { + settings: { + get: getStub, + }, + }; + + const { validateUsername } = proxyquire.noCallThru().load('../../../../../../app/lib/server/functions/validateUsername', { + '../../../settings/server': proxySettings, + }); + + beforeEach(() => { + getStub.reset(); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('with default settings', () => { + beforeEach(() => { + getStub.withArgs('UTF8_User_Names_Validation').returns('[0-9a-zA-Z-_.]+'); + }); + + it('should return true for a valid username', () => { + const result = validateUsername('valid_username.123'); + expect(result).to.be.true; + }); + + it('should return false for an invalid username containing special HTML tags', () => { + const result = validateUsername('username
    $
    '); + expect(result).to.be.false; + }); + + it('should return false for an empty username', () => { + const result = validateUsername(''); + expect(result).to.be.false; + }); + + it('should return false for a username with invalid characters', () => { + const result = validateUsername('invalid*username!'); + expect(result).to.be.false; + }); + + it('should return true for a username with allowed special characters', () => { + const result = validateUsername('username-_.'); + expect(result).to.be.true; + }); + }); + + describe('with custom regex settings', () => { + beforeEach(() => { + getStub.withArgs('UTF8_User_Names_Validation').returns('[a-zA-Z]+'); + }); + + it('should return true for a username matching the custom regex', () => { + const result = validateUsername('ValidUsername'); + expect(result).to.be.true; + }); + + it('should return false for a username that does not match the custom regex', () => { + const result = validateUsername('username123'); + expect(result).to.be.false; + }); + }); + + describe('with null regex settings', () => { + beforeEach(() => { + getStub.withArgs('UTF8_User_Names_Validation').returns(null); + }); + + it('should fallback to the default regex pattern if the settings value is null', () => { + const result = validateUsername('username'); + expect(result).to.be.true; + }); + }); + + describe('with invalid regex settings', () => { + beforeEach(() => { + getStub.withArgs('UTF8_User_Names_Validation').returns('invalid['); + }); + + it('should fallback to the default regex pattern if the settings value is invalid', () => { + const result = validateUsername('username'); + expect(result).to.be.true; + }); + }); +}); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index a807629819a6..69cc6c43fa7f 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6122,6 +6122,7 @@ "registration.component.form.username": "Username", "registration.component.form.name": "Name", "registration.component.form.nameContainsInvalidChars": "Name contains invalid characters", + "registration.component.form.usernameContainsInvalidChars": "Username contains invalid characters", "registration.component.form.nameOptional": "Name optional", "registration.component.form.createAnAccount": "Create an account", "registration.component.form.userAlreadyExist": "Username already exists. Please try another username.", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index 67c8f46888ad..c1ebbc28ca3b 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -4914,6 +4914,7 @@ "registration.component.form.username": "Nome de usuário", "registration.component.form.name": "Nome", "registration.component.form.nameContainsInvalidChars": "O nome contém caracteres inválidos", + "registration.component.form.usernameContainsInvalidChars": "O nome de usuário contém caracteres inválidos", "registration.component.form.userAlreadyExist": "O nome de usuário já existe. Tente outro nome de usuário.", "registration.component.form.emailAlreadyExists": "E-mail já existe", "registration.component.form.usernameAlreadyExists": "O nome de usuário já existe. Tente outro nome de usuário.", @@ -5014,4 +5015,4 @@ "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Auditoria de mensagem" -} \ No newline at end of file +} diff --git a/packages/web-ui-registration/src/RegisterForm.tsx b/packages/web-ui-registration/src/RegisterForm.tsx index 57cf9378ab72..311593d8e9b7 100644 --- a/packages/web-ui-registration/src/RegisterForm.tsx +++ b/packages/web-ui-registration/src/RegisterForm.tsx @@ -100,6 +100,12 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo if (/Username is already in use/.test(error.error)) { setError('username', { type: 'username-already-exists', message: t('registration.component.form.userAlreadyExist') }); } + if (/The username provided is not valid/.test(error.error)) { + setError('username', { + type: 'username-contains-invalid-chars', + message: t('registration.component.form.usernameContainsInvalidChars'), + }); + } if (/Name contains invalid characters/.test(error.error)) { setError('name', { type: 'name-contains-invalid-chars', message: t('registration.component.form.nameContainsInvalidChars') }); } From eb5e60ef7c0b67c385159aa663da40d479a90871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=87=E3=83=AF=E3=83=B3=E3=82=B7=E3=83=A5?= <61188295+Dnouv@users.noreply.github.com> Date: Thu, 22 Aug 2024 03:12:52 +0530 Subject: [PATCH 45/49] chore: bump rocketchat-icons (#33119) --- apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- ee/packages/ui-theming/package.json | 2 +- packages/core-services/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/ui-client/package.json | 2 +- packages/ui-composer/package.json | 2 +- packages/ui-kit/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- packages/uikit-playground/package.json | 2 +- yarn.lock | 30 ++++++++++----------- 12 files changed, 26 insertions(+), 26 deletions(-) diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 4aaff739e4d0..52863be3e098 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -50,7 +50,7 @@ "ws": "^8.8.1" }, "devDependencies": { - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@types/cookie": "^0.5.3", "@types/cookie-parser": "^1.4.5", "@types/ejson": "^2.2.1", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index b8c35ceab8a7..c29d70127bd7 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -249,7 +249,7 @@ "@rocket.chat/fuselage-ui-kit": "workspace:^", "@rocket.chat/gazzodown": "workspace:^", "@rocket.chat/i18n": "workspace:^", - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/jwt": "workspace:^", "@rocket.chat/layout": "~0.31.26", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index d1929c8b93f0..713265b36bfa 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -6,7 +6,7 @@ "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/fuselage": "^0.57.0", "@rocket.chat/fuselage-hooks": "^0.33.1", - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@rocket.chat/ui-contexts": "workspace:~", "@types/react": "~17.0.69", "eslint": "~8.45.0", diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 02176d6bf88c..a975a2ef2541 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -36,7 +36,7 @@ "dependencies": { "@rocket.chat/apps-engine": "1.45.0-alpha.864", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 499dab0156b6..3759caa666b8 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -23,7 +23,7 @@ ], "dependencies": { "@rocket.chat/apps-engine": "1.45.0-alpha.864", - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/ui-kit": "workspace:~" }, diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 101769768017..9b35e473ce72 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -69,7 +69,7 @@ "@rocket.chat/fuselage": "^0.57.0", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/prettier-config": "~0.31.25", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 69ca7e8b7f5b..f43b2d8622b8 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -8,7 +8,7 @@ "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/fuselage": "^0.57.0", "@rocket.chat/fuselage-hooks": "^0.33.1", - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 2b3fae217aac..d854a6ffea86 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -20,7 +20,7 @@ "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "^0.57.0", - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", "@storybook/addon-essentials": "~6.5.16", diff --git a/packages/ui-kit/package.json b/packages/ui-kit/package.json index 44aac1e25d17..c7e8159d457d 100644 --- a/packages/ui-kit/package.json +++ b/packages/ui-kit/package.json @@ -40,7 +40,7 @@ "@babel/plugin-transform-runtime": "~7.21.4", "@babel/preset-env": "~7.21.4", "@rocket.chat/eslint-config": "workspace:~", - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.12", "babel-loader": "~9.1.2", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index d0dc218808c9..5e7d114a3576 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -8,7 +8,7 @@ "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "^0.57.0", "@rocket.chat/fuselage-hooks": "^0.33.1", - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/styled": "~0.31.25", "@rocket.chat/ui-avatar": "workspace:^", diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json index 750f60893188..f46f139a66f4 100644 --- a/packages/uikit-playground/package.json +++ b/packages/uikit-playground/package.json @@ -21,7 +21,7 @@ "@rocket.chat/fuselage-toastbar": "^0.33.0", "@rocket.chat/fuselage-tokens": "^0.33.1", "@rocket.chat/fuselage-ui-kit": "workspace:~", - "@rocket.chat/icons": "^0.36.0", + "@rocket.chat/icons": "~0.38.0", "@rocket.chat/logo": "^0.31.30", "@rocket.chat/styled": "~0.31.25", "@rocket.chat/ui-avatar": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 1698092642f6..5e5c61d59f73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8585,7 +8585,7 @@ __metadata: "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -8611,7 +8611,7 @@ __metadata: dependencies: "@rocket.chat/apps-engine": 1.45.0-alpha.864 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" eslint: ~8.45.0 @@ -8886,7 +8886,7 @@ __metadata: "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/gazzodown": "workspace:^" - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/mock-providers": "workspace:^" "@rocket.chat/prettier-config": ~0.31.25 @@ -9039,10 +9039,10 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/icons@npm:^0.36.0": - version: 0.36.0 - resolution: "@rocket.chat/icons@npm:0.36.0" - checksum: ebec57fdfc9bac3b0b29ba43d9ac316b55f6e4177fa4456de195352d6add1e15e25c4d72e6a4fdc3d33abaabf8af0ca7eb0d36badb360113a19c15a13d68aed5 +"@rocket.chat/icons@npm:~0.38.0": + version: 0.38.0 + resolution: "@rocket.chat/icons@npm:0.38.0" + checksum: 844d76d25bb64633a40e5e2b498dca0acc4b85be87ef8e5b9921c537772fee16a8fb2a9178ac01928d699a4bc5a9856f6c7488a03d59db14aade5379bd529c1b languageName: node linkType: hard @@ -9349,7 +9349,7 @@ __metadata: "@rocket.chat/fuselage-ui-kit": "workspace:^" "@rocket.chat/gazzodown": "workspace:^" "@rocket.chat/i18n": "workspace:^" - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/jwt": "workspace:^" @@ -10241,7 +10241,7 @@ __metadata: "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/fuselage": ^0.57.0 "@rocket.chat/fuselage-hooks": ^0.33.1 - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/mock-providers": "workspace:^" "@rocket.chat/ui-contexts": "workspace:~" @@ -10290,7 +10290,7 @@ __metadata: "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": ^0.57.0 - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 "@storybook/addon-essentials": ~6.5.16 @@ -10359,7 +10359,7 @@ __metadata: "@babel/plugin-transform-runtime": ~7.21.4 "@babel/preset-env": ~7.21.4 "@rocket.chat/eslint-config": "workspace:~" - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" "@types/jest": ~29.5.12 babel-loader: ~9.1.2 @@ -10386,7 +10386,7 @@ __metadata: "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/fuselage": ^0.57.0 "@rocket.chat/fuselage-hooks": ^0.33.1 - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@rocket.chat/ui-contexts": "workspace:~" "@types/react": ~17.0.69 eslint: ~8.45.0 @@ -10416,7 +10416,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": ^0.57.0 "@rocket.chat/fuselage-hooks": ^0.33.1 - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/styled": ~0.31.25 "@rocket.chat/ui-avatar": "workspace:^" @@ -10469,7 +10469,7 @@ __metadata: "@rocket.chat/fuselage-toastbar": ^0.33.0 "@rocket.chat/fuselage-tokens": ^0.33.1 "@rocket.chat/fuselage-ui-kit": "workspace:~" - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@rocket.chat/logo": ^0.31.30 "@rocket.chat/styled": ~0.31.25 "@rocket.chat/ui-avatar": "workspace:^" @@ -36698,7 +36698,7 @@ __metadata: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": ~0.31.25 - "@rocket.chat/icons": ^0.36.0 + "@rocket.chat/icons": ~0.38.0 "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" From 7937ff741a3a92c032a3a0f77d7dc726c676d165 Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Wed, 21 Aug 2024 20:40:54 -0300 Subject: [PATCH 46/49] fix: System messages are counted as agents' first responses in livechat rooms (#32846) --- .changeset/rotten-camels-pretend.md | 6 + .../server/hooks/markRoomResponded.ts | 6 +- .../server/hooks/saveAnalyticsData.ts | 4 +- .../app/livechat/server/lib/AnalyticsTyped.ts | 5 +- apps/meteor/tests/data/livechat/rooms.ts | 4 +- .../end-to-end/api/livechat/04-dashboards.ts | 201 +++++++++++++++++- .../core-typings/src/IMessage/IMessage.ts | 182 ++++++++-------- 7 files changed, 309 insertions(+), 99 deletions(-) create mode 100644 .changeset/rotten-camels-pretend.md diff --git a/.changeset/rotten-camels-pretend.md b/.changeset/rotten-camels-pretend.md new file mode 100644 index 000000000000..5145bbaa5050 --- /dev/null +++ b/.changeset/rotten-camels-pretend.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +--- + +Fixed issue with system messages being counted as agents' first responses in livechat rooms (which caused the "best first response time" and "average first response time" metrics to be unreliable for all agents) diff --git a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts index 69e9b11c57b9..6820bd4664bd 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts +++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts @@ -1,5 +1,5 @@ import type { IOmnichannelRoom, IMessage } from '@rocket.chat/core-typings'; -import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings'; +import { isEditedMessage, isMessageFromVisitor, isSystemMessage } from '@rocket.chat/core-typings'; import type { Updater } from '@rocket.chat/models'; import { LivechatRooms, LivechatVisitors, LivechatInquiry } from '@rocket.chat/models'; import moment from 'moment'; @@ -12,7 +12,7 @@ export async function markRoomResponded( room: IOmnichannelRoom, roomUpdater: Updater, ): Promise { - if (message.t || isEditedMessage(message) || isMessageFromVisitor(message)) { + if (isSystemMessage(message) || isEditedMessage(message) || isMessageFromVisitor(message)) { return; } @@ -62,7 +62,7 @@ export async function markRoomResponded( callbacks.add( 'afterOmnichannelSaveMessage', async (message, { room, roomUpdater }) => { - if (!message || message.t || isEditedMessage(message) || isMessageFromVisitor(message)) { + if (!message || isEditedMessage(message) || isMessageFromVisitor(message) || isSystemMessage(message)) { return; } diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index 109f49f440b5..9553e9fe981b 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts @@ -1,4 +1,4 @@ -import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings'; +import { isEditedMessage, isMessageFromVisitor, isSystemMessage } from '@rocket.chat/core-typings'; import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; @@ -62,7 +62,7 @@ const getAnalyticsData = (room: IOmnichannelRoom, now: Date): Record { - if (!message || isEditedMessage(message)) { + if (!message || isEditedMessage(message) || isSystemMessage(message)) { return message; } diff --git a/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts b/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts index 3b7c6a3051bf..c0be707ba212 100644 --- a/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts +++ b/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts @@ -1,7 +1,10 @@ import { OmnichannelAnalytics } from '@rocket.chat/core-services'; import mem from 'mem'; -export const getAgentOverviewDataCached = mem(OmnichannelAnalytics.getAgentOverviewData, { maxAge: 60000, cacheKey: JSON.stringify }); +export const getAgentOverviewDataCached = mem(OmnichannelAnalytics.getAgentOverviewData, { + maxAge: process.env.TEST_MODE === 'true' ? 1 : 60000, + cacheKey: JSON.stringify, +}); // Agent overview data on realtime is cached for 5 seconds // while the data on the overview page is cached for 1 minute export const getAnalyticsOverviewDataCached = mem(OmnichannelAnalytics.getAnalyticsOverviewData, { diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index e2084adda934..9532fd4214ab 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -240,11 +240,11 @@ export const uploadFile = (roomId: string, visitorToken: string): Promise => { +export const sendAgentMessage = (roomId: string, msg?: string, userCredentials: Credentials = credentials): Promise => { return new Promise((resolve, reject) => { void request .post(methodCall('sendMessage')) - .set(credentials) + .set(userCredentials) .send({ message: JSON.stringify({ method: 'sendMessage', diff --git a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts index c0a559bbcba7..52c405d4a922 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts @@ -3,7 +3,7 @@ import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { before, after, describe, it } from 'mocha'; import moment from 'moment'; import type { Response } from 'supertest'; @@ -19,6 +19,7 @@ import { import { createAnOnlineAgent } from '../../../data/livechat/users'; import { sleep } from '../../../data/livechat/utils'; import { removePermissionFromAllRoles, restorePermissionToRoles, updateSetting } from '../../../data/permissions.helper'; +import { deleteUser } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; describe('LIVECHAT - dashboards', function () { @@ -777,6 +778,198 @@ describe('LIVECHAT - dashboards', function () { }); }); + describe('[livechat/analytics/agent-overview] - Average first response time', () => { + let agent: { credentials: Credentials; user: IUser & { username: string } }; + let originalFirstResponseTimeInSeconds: number; + let roomId: string; + const firstDelayInSeconds = 4; + const secondDelayInSeconds = 8; + + before(async () => { + agent = await createAnOnlineAgent(); + }); + + after(async () => { + await deleteUser(agent.user); + }); + + it('should return no average response time for an agent if no response has been sent in the period', async () => { + await startANewLivechatRoomAndTakeIt({ agent: agent.credentials }); + + const today = moment().startOf('day').format('YYYY-MM-DD'); + + const result = await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: today, to: today, name: 'Avg_first_response_time' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('head'); + expect(result.body).to.have.property('data'); + expect(result.body.data).to.be.an('array'); + expect(result.body.data).to.not.deep.include({ name: agent.user.username }); + }); + + it("should not consider system messages in agents' first response time metric", async () => { + const response = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials }); + roomId = response.room._id; + + await sleep(firstDelayInSeconds * 1000); + await sendAgentMessage(roomId, 'first response from agent', agent.credentials); + + const today = moment().startOf('day').format('YYYY-MM-DD'); + const result = await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: today, to: today, name: 'Avg_first_response_time' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('head'); + expect(result.body).to.have.property('data'); + expect(result.body.data).to.be.an('array'); + + const agentData = result.body.data.find( + (agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username, + ); + expect(agentData).to.not.be.undefined; + expect(agentData).to.have.property('name', agent.user.username); + expect(agentData).to.have.property('value'); + originalFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds(); + expect(originalFirstResponseTimeInSeconds).to.be.greaterThanOrEqual(firstDelayInSeconds); + }); + + it('should correctly calculate the average time of first responses for an agent', async () => { + const response = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials }); + roomId = response.room._id; + + await sleep(secondDelayInSeconds * 1000); + await sendAgentMessage(roomId, 'first response from agent', agent.credentials); + + const today = moment().startOf('day').format('YYYY-MM-DD'); + const result = await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: today, to: today, name: 'Avg_first_response_time' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('head'); + expect(result.body).to.have.property('data'); + expect(result.body.data).to.be.an('array').that.is.not.empty; + + const agentData = result.body.data.find( + (agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username, + ); + expect(agentData).to.not.be.undefined; + expect(agentData).to.have.property('name', agent.user.username); + expect(agentData).to.have.property('value'); + const averageFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds(); + expect(averageFirstResponseTimeInSeconds).to.be.greaterThan(originalFirstResponseTimeInSeconds); + expect(averageFirstResponseTimeInSeconds).to.be.greaterThanOrEqual((firstDelayInSeconds + secondDelayInSeconds) / 2); + expect(averageFirstResponseTimeInSeconds).to.be.lessThan(secondDelayInSeconds); + }); + }); + + describe('[livechat/analytics/agent-overview] - Best first response time', () => { + let agent: { credentials: Credentials; user: IUser & { username: string } }; + let originalBestFirstResponseTimeInSeconds: number; + let roomId: string; + + before(async () => { + agent = await createAnOnlineAgent(); + }); + + after(() => deleteUser(agent.user)); + + it('should return no best response time for an agent if no response has been sent in the period', async () => { + await startANewLivechatRoomAndTakeIt({ agent: agent.credentials }); + + const today = moment().startOf('day').format('YYYY-MM-DD'); + + const result = await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: today, to: today, name: 'Best_first_response_time' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('head'); + expect(result.body).to.have.property('data'); + expect(result.body.data).to.be.an('array'); + expect(result.body.data).to.not.deep.include({ name: agent.user.username }); + }); + + it("should not consider system messages in agents' best response time metric", async () => { + const response = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials }); + roomId = response.room._id; + + const delayInSeconds = 4; + await sleep(delayInSeconds * 1000); + + await sendAgentMessage(roomId, 'first response from agent', agent.credentials); + + const today = moment().startOf('day').format('YYYY-MM-DD'); + const result = await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: today, to: today, name: 'Best_first_response_time' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('head'); + expect(result.body).to.have.property('data'); + expect(result.body.data).to.be.an('array').that.is.not.empty; + + const agentData = result.body.data.find( + (agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username, + ); + expect(agentData).to.not.be.undefined; + expect(agentData).to.have.property('name', agent.user.username); + expect(agentData).to.have.property('value'); + originalBestFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds(); + expect(originalBestFirstResponseTimeInSeconds).to.be.greaterThanOrEqual(delayInSeconds); + }); + + it('should correctly calculate the best first response time for an agent and there are multiple first responses in the period', async () => { + const response = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials }); + roomId = response.room._id; + + const delayInSeconds = 6; + await sleep(delayInSeconds * 1000); + + await sendAgentMessage(roomId, 'first response from agent', agent.credentials); + + const today = moment().startOf('day').format('YYYY-MM-DD'); + const result = await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: today, to: today, name: 'Best_first_response_time' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('head'); + expect(result.body).to.have.property('data'); + expect(result.body.data).to.be.an('array'); + + const agentData = result.body.data.find( + (agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username, + ); + expect(agentData).to.not.be.undefined; + expect(agentData).to.have.property('name', agent.user.username); + expect(agentData).to.have.property('value'); + const bestFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds(); + expect(bestFirstResponseTimeInSeconds).to.be.equal(originalBestFirstResponseTimeInSeconds); + }); + }); + describe('livechat/analytics/overview', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await removePermissionFromAllRoles('view-livechat-manager'); @@ -835,12 +1028,12 @@ describe('LIVECHAT - dashboards', function () { expect(result.body).to.be.an('array'); const expectedResult = [ - { title: 'Total_conversations', value: 7 }, - { title: 'Open_conversations', value: 4 }, + { title: 'Total_conversations', value: 13 }, + { title: 'Open_conversations', value: 10 }, { title: 'On_Hold_conversations', value: 1 }, // { title: 'Total_messages', value: 6 }, // { title: 'Busiest_day', value: moment().format('dddd') }, - { title: 'Conversations_per_day', value: '3.50' }, + { title: 'Conversations_per_day', value: '6.50' }, // { title: 'Busiest_time', value: '' }, ]; diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 694225dc71a4..205cbaccd466 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -22,90 +22,95 @@ export type MessageUrl = { parsedUrl?: Pick; }; -type VoipMessageTypesValues = - | 'voip-call-started' - | 'voip-call-declined' - | 'voip-call-on-hold' - | 'voip-call-unhold' - | 'voip-call-ended' - | 'voip-call-duration' - | 'voip-call-wrapup' - | 'voip-call-ended-unexpectedly'; - -type TeamMessageTypes = - | 'removed-user-from-team' - | 'added-user-to-team' - | 'ult' - | 'user-converted-to-team' - | 'user-converted-to-channel' - | 'user-removed-room-from-team' - | 'user-deleted-room-from-team' - | 'user-added-room-to-team' - | 'ujt'; - -type LivechatMessageTypes = - | 'livechat_navigation_history' - | 'livechat_transfer_history' - | 'omnichannel_priority_change_history' - | 'omnichannel_sla_change_history' - | 'livechat_transcript_history' - | 'livechat_video_call' - | 'livechat_transfer_history_fallback' - | 'livechat-close' - | 'livechat_webrtc_video_call' - | 'livechat-started'; - -type OmnichannelTypesValues = 'omnichannel_placed_chat_on_hold' | 'omnichannel_on_hold_chat_resumed'; - -type OtrMessageTypeValues = 'otr' | 'otr-ack'; - -export type OtrSystemMessages = 'user_joined_otr' | 'user_requested_otr_key_refresh' | 'user_key_refreshed_successfully'; - -export type MessageTypesValues = - | 'e2e' - | 'uj' - | 'ul' - | 'ru' - | 'au' - | 'mute_unmute' - | 'r' - | 'ut' - | 'wm' - | 'rm' - | 'subscription-role-added' - | 'subscription-role-removed' - | 'room-archived' - | 'room-unarchived' - | 'room_changed_privacy' - | 'room_changed_description' - | 'room_changed_announcement' - | 'room_changed_avatar' - | 'room_changed_topic' - | 'room_e2e_enabled' - | 'room_e2e_disabled' - | 'user-muted' - | 'user-unmuted' - | 'room-removed-read-only' - | 'room-set-read-only' - | 'room-allowed-reacting' - | 'room-disallowed-reacting' - | 'command' - | 'videoconf' - | 'message_pinned' - | 'message_pinned_e2e' - | 'new-moderator' - | 'moderator-removed' - | 'new-owner' - | 'owner-removed' - | 'new-leader' - | 'leader-removed' - | 'discussion-created' - | LivechatMessageTypes - | TeamMessageTypes - | VoipMessageTypesValues - | OmnichannelTypesValues - | OtrMessageTypeValues - | OtrSystemMessages; +const VoipMessageTypesValues = [ + 'voip-call-started', + 'voip-call-declined', + 'voip-call-on-hold', + 'voip-call-unhold', + 'voip-call-ended', + 'voip-call-duration', + 'voip-call-wrapup', + 'voip-call-ended-unexpectedly', +] as const; + +const TeamMessageTypesValues = [ + 'removed-user-from-team', + 'added-user-to-team', + 'ult', + 'user-converted-to-team', + 'user-converted-to-channel', + 'user-removed-room-from-team', + 'user-deleted-room-from-team', + 'user-added-room-to-team', + 'ujt', +] as const; + +const LivechatMessageTypesValues = [ + 'livechat_navigation_history', + 'livechat_transfer_history', + 'livechat_transcript_history', + 'livechat_video_call', + 'livechat_transfer_history_fallback', + 'livechat-close', + 'livechat_webrtc_video_call', + 'livechat-started', + 'omnichannel_priority_change_history', + 'omnichannel_sla_change_history', + 'omnichannel_placed_chat_on_hold', + 'omnichannel_on_hold_chat_resumed', +] as const; + +const OtrMessageTypeValues = ['otr', 'otr-ack'] as const; + +const OtrSystemMessagesValues = ['user_joined_otr', 'user_requested_otr_key_refresh', 'user_key_refreshed_successfully'] as const; +export type OtrSystemMessages = (typeof OtrSystemMessagesValues)[number]; + +const MessageTypes = [ + 'e2e', + 'uj', + 'ul', + 'ru', + 'au', + 'mute_unmute', + 'r', + 'ut', + 'wm', + 'rm', + 'subscription-role-added', + 'subscription-role-removed', + 'room-archived', + 'room-unarchived', + 'room_changed_privacy', + 'room_changed_description', + 'room_changed_announcement', + 'room_changed_avatar', + 'room_changed_topic', + 'room_e2e_enabled', + 'room_e2e_disabled', + 'user-muted', + 'user-unmuted', + 'room-removed-read-only', + 'room-set-read-only', + 'room-allowed-reacting', + 'room-disallowed-reacting', + 'command', + 'videoconf', + 'message_pinned', + 'message_pinned_e2e', + 'new-moderator', + 'moderator-removed', + 'new-owner', + 'owner-removed', + 'new-leader', + 'leader-removed', + 'discussion-created', + ...TeamMessageTypesValues, + ...LivechatMessageTypesValues, + ...VoipMessageTypesValues, + ...OtrMessageTypeValues, + ...OtrSystemMessagesValues, +] as const; +export type MessageTypesValues = (typeof MessageTypes)[number]; export type TokenType = 'code' | 'inlinecode' | 'bold' | 'italic' | 'strike' | 'link'; export type Token = { @@ -231,9 +236,9 @@ export interface IMessage extends IRocketChatRecord { }; } -export type MessageSystem = { - t: 'system'; -}; +export interface ISystemMessage extends IMessage { + t: MessageTypesValues; +} export interface IEditedMessage extends IMessage { editedAt: Date; @@ -249,6 +254,9 @@ export const isEditedMessage = (message: IMessage): message is IEditedMessage => '_id' in (message as IEditedMessage).editedBy && typeof (message as IEditedMessage).editedBy._id === 'string'; +export const isSystemMessage = (message: IMessage): message is ISystemMessage => + message.t !== undefined && MessageTypes.includes(message.t); + export const isDeletedMessage = (message: IMessage): message is IEditedMessage => isEditedMessage(message) && message.t === 'rm'; export const isMessageFromMatrixFederation = (message: IMessage): boolean => 'federation' in message && Boolean(message.federation?.eventId); From 150c7b11bb4b203bebe40404fd782b2415bd8059 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Wed, 21 Aug 2024 20:59:11 -0300 Subject: [PATCH 47/49] refactor: Subscriptions out of DB Watcher (#32540) Co-authored-by: Diego Sampaio --- .../meteor/app/api/server/v1/subscriptions.ts | 1 + .../server/methods/saveSettings.ts | 27 ++- .../server/functions/saveRoomCustomFields.ts | 9 +- .../server/functions/saveRoomEncrypted.ts | 7 +- .../server/functions/saveRoomName.ts | 18 +- .../server/functions/saveRoomType.ts | 9 +- .../functions/handleSuggestedGroupKey.ts | 7 +- .../app/e2e/server/methods/updateGroupKey.ts | 11 +- .../federation/server/endpoints/dispatch.js | 23 ++- .../server/classes/ImportDataConverter.ts | 9 +- .../functions/addUserToDefaultChannels.ts | 8 +- .../app/lib/server/functions/addUserToRoom.ts | 8 +- .../app/lib/server/functions/archiveRoom.ts | 9 +- .../lib/server/functions/cleanRoomHistory.ts | 12 +- .../lib/server/functions/closeLivechatRoom.ts | 10 +- .../lib/server/functions/createDirectRoom.ts | 12 +- .../app/lib/server/functions/createRoom.ts | 12 +- .../app/lib/server/functions/deleteRoom.ts | 19 +- .../app/lib/server/functions/deleteUser.ts | 2 +- .../functions/relinquishRoomOwnerships.ts | 7 +- .../server/functions/removeUserFromRoom.ts | 7 +- .../saveCustomFieldsWithoutValidation.ts | 6 +- .../lib/server/functions/saveUserIdentity.ts | 38 +++- .../server/functions/setUserActiveStatus.ts | 18 +- .../app/lib/server/functions/unarchiveRoom.ts | 9 +- .../server/functions/updateGroupDMsName.ts | 7 +- .../app/lib/server/lib/notifyListener.ts | 119 ++++++++++++ .../lib/server/lib/notifyUsersOnMessage.ts | 109 ++++++----- .../app/lib/server/methods/blockUser.ts | 17 +- .../app/lib/server/methods/unblockUser.ts | 18 +- .../app/livechat/server/lib/Contacts.ts | 21 ++- apps/meteor/app/livechat/server/lib/Helper.ts | 23 ++- .../app/livechat/server/lib/LivechatTyped.ts | 35 +++- .../server/unreadMessages.ts | 15 +- .../methods/saveNotificationSettings.ts | 14 +- apps/meteor/app/threads/server/functions.ts | 75 ++++---- .../server/hooks/onCloseLivechat.ts | 7 +- .../services/omnichannel.internalService.ts | 30 ++- .../server/hooks/afterSaveMessage.ts | 8 +- .../server/database/watchCollections.ts | 3 +- apps/meteor/server/lib/readMessages.ts | 6 +- apps/meteor/server/lib/resetUserE2EKey.ts | 11 +- .../meteor/server/methods/addAllUserToRoom.ts | 6 +- apps/meteor/server/methods/addRoomLeader.ts | 6 +- .../meteor/server/methods/addRoomModerator.ts | 6 +- apps/meteor/server/methods/addRoomOwner.ts | 6 +- apps/meteor/server/methods/hideRoom.ts | 10 +- apps/meteor/server/methods/ignoreUser.ts | 17 +- apps/meteor/server/methods/openRoom.ts | 10 +- .../meteor/server/methods/removeRoomLeader.ts | 6 +- .../server/methods/removeRoomModerator.ts | 6 +- apps/meteor/server/methods/removeRoomOwner.ts | 6 +- .../server/methods/removeUserFromRoom.ts | 7 +- .../server/methods/saveUserPreferences.ts | 98 ++++++---- apps/meteor/server/methods/toggleFavorite.ts | 10 +- apps/meteor/server/models/dummy/BaseDummy.ts | 7 + apps/meteor/server/models/raw/BaseRaw.ts | 36 +++- apps/meteor/server/models/raw/Roles.ts | 32 ++-- .../meteor/server/models/raw/Subscriptions.ts | 171 +++++++++++++++--- .../rocket-chat/adapters/Room.ts | 96 ++++++---- apps/meteor/server/services/team/service.ts | 7 +- .../functions/closeLivechatRoom.tests.ts | 14 +- .../model-typings/src/models/IBaseModel.ts | 4 +- .../src/models/ISubscriptionsModel.ts | 66 ++++++- 64 files changed, 1112 insertions(+), 331 deletions(-) diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts index 9d81fe6bef65..b92d9ba572fd 100644 --- a/apps/meteor/app/api/server/v1/subscriptions.ts +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -82,6 +82,7 @@ API.v1.addRoute( async post() { const { readThreads = false } = this.bodyParams; const roomId = 'rid' in this.bodyParams ? this.bodyParams.rid : this.bodyParams.roomId; + await readMessages(roomId, this.userId, readThreads); return API.v1.success(); diff --git a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts index 2f119c948263..3d4d15c0316e 100644 --- a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts +++ b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -44,6 +45,8 @@ Meteor.methods({ }); } + let shouldNotifySubscriptionChanged = false; + switch (field) { case 'autoTranslate': const room = await Rooms.findE2ERoomById(rid, { projection: { _id: 1 } }); @@ -53,16 +56,34 @@ Meteor.methods({ }); } - await Subscriptions.updateAutoTranslateById(subscription._id, value === '1'); + const updateAutoTranslateResponse = await Subscriptions.updateAutoTranslateById(subscription._id, value === '1'); + if (updateAutoTranslateResponse.modifiedCount) { + shouldNotifySubscriptionChanged = true; + } + if (!subscription.autoTranslateLanguage && options.defaultLanguage) { - await Subscriptions.updateAutoTranslateLanguageById(subscription._id, options.defaultLanguage); + const updateAutoTranslateLanguageResponse = await Subscriptions.updateAutoTranslateLanguageById( + subscription._id, + options.defaultLanguage, + ); + if (updateAutoTranslateLanguageResponse.modifiedCount) { + shouldNotifySubscriptionChanged = true; + } } + break; case 'autoTranslateLanguage': - await Subscriptions.updateAutoTranslateLanguageById(subscription._id, value); + const updateAutoTranslateLanguage = await Subscriptions.updateAutoTranslateLanguageById(subscription._id, value); + if (updateAutoTranslateLanguage.modifiedCount) { + shouldNotifySubscriptionChanged = true; + } break; } + if (shouldNotifySubscriptionChanged) { + void notifyOnSubscriptionChangedById(subscription._id); + } + return true; }, }); diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts index 55d40cf3d7e6..ef70ff65c067 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts @@ -3,21 +3,28 @@ import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { UpdateResult } from 'mongodb'; +import { notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener'; + export const saveRoomCustomFields = async function (rid: string, roomCustomFields: Record): Promise { if (!Match.test(rid, String)) { throw new Meteor.Error('invalid-room', 'Invalid room', { function: 'RocketChat.saveRoomCustomFields', }); } + if (!Match.test(roomCustomFields, Object)) { throw new Meteor.Error('invalid-roomCustomFields-type', 'Invalid roomCustomFields type', { function: 'RocketChat.saveRoomCustomFields', }); } + const ret = await Rooms.setCustomFieldsById(rid, roomCustomFields); // Update customFields of any user's Subscription related with this rid - await Subscriptions.updateCustomFieldsByRoomId(rid, roomCustomFields); + const { modifiedCount } = await Subscriptions.updateCustomFieldsByRoomId(rid, roomCustomFields); + if (modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } return ret; }; diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts index ed07540ba2b0..c1a441463a98 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts @@ -6,6 +6,8 @@ import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { UpdateResult } from 'mongodb'; +import { notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener'; + export const saveRoomEncrypted = async function (rid: string, encrypted: boolean, user: IUser, sendMessage = true): Promise { if (!Match.test(rid, String)) { throw new Meteor.Error('invalid-room', 'Invalid room', { @@ -27,7 +29,10 @@ export const saveRoomEncrypted = async function (rid: string, encrypted: boolean } if (encrypted) { - await Subscriptions.disableAutoTranslateByRoomId(rid); + const { modifiedCount } = await Subscriptions.disableAutoTranslateByRoomId(rid); + if (modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } } return update; }; diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts index c2af750ffa13..f4a5afbb6380 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts @@ -8,11 +8,17 @@ import type { Document, UpdateResult } from 'mongodb'; import { callbacks } from '../../../../lib/callbacks'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { checkUsernameAvailability } from '../../../lib/server/functions/checkUsernameAvailability'; -import { notifyOnIntegrationChangedByChannels } from '../../../lib/server/lib/notifyListener'; +import { notifyOnIntegrationChangedByChannels, notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; const updateFName = async (rid: string, displayName: string): Promise<(UpdateResult | Document)[]> => { - return Promise.all([Rooms.setFnameById(rid, displayName), Subscriptions.updateFnameByRoomId(rid, displayName)]); + const responses = await Promise.all([Rooms.setFnameById(rid, displayName), Subscriptions.updateFnameByRoomId(rid, displayName)]); + + if (responses[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + + return responses; }; const updateRoomName = async (rid: string, displayName: string, slugifiedRoomName: string) => { @@ -24,10 +30,16 @@ const updateRoomName = async (rid: string, displayName: string, slugifiedRoomNam }); } - return Promise.all([ + const responses = await Promise.all([ Rooms.setNameById(rid, slugifiedRoomName, displayName), Subscriptions.updateNameAndAlertByRoomId(rid, slugifiedRoomName, displayName), ]); + + if (responses[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + + return responses; }; export async function saveRoomName( diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts index e8a60d1ea0eb..4600d1d46a80 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts @@ -8,6 +8,7 @@ import type { UpdateResult, Document } from 'mongodb'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import { i18n } from '../../../../server/lib/i18n'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; +import { notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; export const saveRoomType = async function ( @@ -41,11 +42,16 @@ export const saveRoomType = async function ( }); } - const result = (await Rooms.setTypeById(rid, roomType)) && (await Subscriptions.updateTypeByRoomId(rid, roomType)); + const result = await Promise.all([Rooms.setTypeById(rid, roomType), Subscriptions.updateTypeByRoomId(rid, roomType)]); + if (!result) { return result; } + if (result[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + if (sendMessage) { let message; if (roomType === 'c') { @@ -59,5 +65,6 @@ export const saveRoomType = async function ( } await Message.saveSystemMessage('room_changed_privacy', rid, message, user); } + return result; }; diff --git a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts index 860051c04d4d..22eccf03f407 100644 --- a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts +++ b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts @@ -1,6 +1,8 @@ import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedById } from '../../../lib/server/lib/notifyListener'; + export async function handleSuggestedGroupKey( handle: 'accept' | 'reject', rid: string, @@ -30,5 +32,8 @@ export async function handleSuggestedGroupKey( await Rooms.addUserIdToE2EEQueueByRoomIds([sub.rid], userId); } - await Subscriptions.unsetGroupE2ESuggestedKey(sub._id); + const { modifiedCount } = await Subscriptions.unsetGroupE2ESuggestedKey(sub._id); + if (modifiedCount) { + void notifyOnSubscriptionChangedById(sub._id); + } } diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts index 5764a021f54c..87182f723e7d 100644 --- a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts +++ b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts @@ -3,6 +3,7 @@ import { Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { notifyOnSubscriptionChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -25,12 +26,18 @@ Meteor.methods({ if (mySub) { // Setting the key to myself, can set directly to the final field if (userId === uid) { - await Subscriptions.setGroupE2EKey(mySub._id, key); + const setGroupE2EKeyResponse = await Subscriptions.setGroupE2EKey(mySub._id, key); + if (setGroupE2EKeyResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(mySub._id); + } return; } // uid also has subscription to this room - await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key); + const { modifiedCount } = await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key); + if (modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } } }, }); diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js index 7090f053a22b..4f2a197b25ee 100644 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ b/apps/meteor/app/federation/server/endpoints/dispatch.js @@ -6,7 +6,13 @@ import EJSON from 'ejson'; import { API } from '../../../api/server'; import { FileUpload } from '../../../file-upload/server'; import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; -import { notifyOnMessageChange, notifyOnRoomChanged, notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; +import { + notifyOnMessageChange, + notifyOnRoomChanged, + notifyOnRoomChangedById, + notifyOnSubscriptionChanged, + notifyOnSubscriptionChangedById, +} from '../../../lib/server/lib/notifyListener'; import { notifyUsersOnMessage } from '../../../lib/server/lib/notifyUsersOnMessage'; import { sendAllNotifications } from '../../../lib/server/lib/sendNotificationsOnMessage'; import { processThreads } from '../../../threads/server/hooks/aftersavemessage'; @@ -141,7 +147,10 @@ const eventHandlers = { const denormalizedSubscription = normalizers.denormalizeSubscription(subscription); // Create the subscription - await Subscriptions.insertOne(denormalizedSubscription); + const { insertedId } = await Subscriptions.insertOne(denormalizedSubscription); + if (insertedId) { + void notifyOnSubscriptionChangedById(insertedId); + } federationAltered = true; } } catch (ex) { @@ -176,7 +185,10 @@ const eventHandlers = { } = event; // Remove the user's subscription - await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); + if (deletedSubscription) { + void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); + } // Refresh the servers list await FederationServers.refreshServers(); @@ -204,7 +216,10 @@ const eventHandlers = { } = event; // Remove the user's subscription - await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); + if (deletedSubscription) { + void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); + } // Refresh the servers list await FederationServers.refreshServers(); diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 7b1e71eaa0f0..6de47e33b2b6 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -28,7 +28,7 @@ import { generateUsernameSuggestion } from '../../../lib/server/functions/getUse import { insertMessage } from '../../../lib/server/functions/insertMessage'; import { saveUserIdentity } from '../../../lib/server/functions/saveUserIdentity'; import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus'; -import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; +import { notifyOnSubscriptionChangedByRoomId, notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { createChannelMethod } from '../../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; @@ -1161,8 +1161,11 @@ export class ImportDataConverter { } async archiveRoomById(rid: string) { - await Rooms.archiveById(rid); - await Subscriptions.archiveByRoomId(rid); + const responses = await Promise.all([Rooms.archiveById(rid), Subscriptions.archiveByRoomId(rid)]); + + if (responses[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } } async convertData(startedByUserId: string, callbacks: IConversionCallbacks = {}): Promise { diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index b6d977dc36e2..3fb9c419aa5f 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -5,6 +5,7 @@ import { Subscriptions } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; +import { notifyOnSubscriptionChangedById } from '../lib/notifyListener'; import { getDefaultChannels } from './getDefaultChannels'; export const addUserToDefaultChannels = async function (user: IUser, silenced?: boolean): Promise { @@ -14,8 +15,9 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: for await (const room of defaultRooms) { if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) { const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); + // Add a subscription to this user - await Subscriptions.createWithRoomAndUser(room, user, { + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, { ts: new Date(), open: true, alert: true, @@ -27,6 +29,10 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: ...getDefaultSubscriptionPref(user), }); + if (insertedId) { + void notifyOnSubscriptionChangedById(insertedId, 'inserted'); + } + // Insert user joined message if (!silenced) { await Message.saveSystemMessage('uj', room._id, user.username || '', user); diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index b6ffc0ca4629..e6ca7b2a8b4d 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -11,7 +11,7 @@ import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/li import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; export const addUserToRoom = async function ( rid: string, @@ -82,7 +82,7 @@ export const addUserToRoom = async function ( const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(userToBeAdded); - await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, { + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, { ts: now, open: true, alert: !skipAlertSound, @@ -93,6 +93,10 @@ export const addUserToRoom = async function ( ...getDefaultSubscriptionPref(userToBeAdded as IUser), }); + if (insertedId) { + void notifyOnSubscriptionChangedById(insertedId, 'inserted'); + } + void notifyOnRoomChangedById(rid); if (!userToBeAdded.username) { diff --git a/apps/meteor/app/lib/server/functions/archiveRoom.ts b/apps/meteor/app/lib/server/functions/archiveRoom.ts index 3378d69f99ff..46fd7a1ac35b 100644 --- a/apps/meteor/app/lib/server/functions/archiveRoom.ts +++ b/apps/meteor/app/lib/server/functions/archiveRoom.ts @@ -3,11 +3,16 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; -import { notifyOnRoomChanged } from '../lib/notifyListener'; +import { notifyOnRoomChanged, notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener'; export const archiveRoom = async function (rid: string, user: IMessage['u']): Promise { await Rooms.archiveById(rid); - await Subscriptions.archiveByRoomId(rid); + + const archiveResponse = await Subscriptions.archiveByRoomId(rid); + if (archiveResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + await Message.saveSystemMessage('room-archived', rid, '', user); const room = await Rooms.findOneById(rid); diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts index 2bfb1086c635..765a03cad87b 100644 --- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts @@ -4,7 +4,7 @@ import { Messages, Rooms, Subscriptions, ReadReceipts, Users } from '@rocket.cha import { i18n } from '../../../../server/lib/i18n'; import { FileUpload } from '../../../file-upload/server'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; import { deleteRoom } from './deleteRoom'; export async function cleanRoomHistory({ @@ -75,6 +75,7 @@ export async function cleanRoomHistory({ if (!ignoreThreads) { const threads = new Set(); + await Messages.findThreadsByRoomIdPinnedTimestampAndUsers( { rid, pinned: excludePinned, ignoreDiscussion, ts, users: fromUsers }, { projection: { _id: 1 } }, @@ -83,7 +84,14 @@ export async function cleanRoomHistory({ }); if (threads.size > 0) { - await Subscriptions.removeUnreadThreadsByRoomId(rid, [...threads]); + const subscriptionIds: string[] = ( + await Subscriptions.findUnreadThreadsByRoomId(rid, [...threads], { projection: { _id: 1 } }).toArray() + ).map(({ _id }) => _id); + + const { modifiedCount } = await Subscriptions.removeUnreadThreadsByRoomId(rid, [...threads]); + if (modifiedCount) { + subscriptionIds.forEach((id) => notifyOnSubscriptionChangedById(id)); + } } } diff --git a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts index b716be044d57..263b137ae00c 100644 --- a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts +++ b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts @@ -5,6 +5,7 @@ import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import type { CloseRoomParams } from '../../../livechat/server/lib/LivechatTyped'; import { Livechat } from '../../../livechat/server/lib/LivechatTyped'; +import { notifyOnSubscriptionChanged } from '../lib/notifyListener'; export const closeLivechatRoom = async ( user: IUser, @@ -34,9 +35,12 @@ export const closeLivechatRoom = async ( } if (!room.open) { - const subscriptionsLeft = await Subscriptions.countByRoomId(roomId); - if (subscriptionsLeft) { - await Subscriptions.removeByRoomId(roomId); + const { deletedCount } = await Subscriptions.removeByRoomId(roomId, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }); + if (deletedCount) { return; } throw new Error('error-room-already-closed'); diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 67c6328f38f4..f77ee1f55901 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -11,7 +11,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { isTruthy } from '../../../../lib/isTruthy'; import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../lib/notifyListener'; const generateSubscription = ( fname: string, @@ -135,7 +135,7 @@ export async function createDirectRoom( if (roomMembers.length === 1) { // dm to yourself - await Subscriptions.updateOne( + const { modifiedCount, upsertedCount } = await Subscriptions.updateOne( { rid, 'u._id': roomMembers[0]._id }, { $set: { open: true }, @@ -146,6 +146,9 @@ export async function createDirectRoom( }, { upsert: true }, ); + if (modifiedCount || upsertedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, roomMembers[0]._id, modifiedCount ? 'updated' : 'inserted'); + } } else { const memberIds = roomMembers.map((member) => member._id); const membersWithPreferences: IUser[] = await Users.find( @@ -155,7 +158,7 @@ export async function createDirectRoom( for await (const member of membersWithPreferences) { const otherMembers = sortedMembers.filter(({ _id }) => _id !== member._id); - await Subscriptions.updateOne( + const { modifiedCount, upsertedCount } = await Subscriptions.updateOne( { rid, 'u._id': member._id }, { ...(options?.creator === member._id && { $set: { open: true } }), @@ -166,6 +169,9 @@ export async function createDirectRoom( }, { upsert: true }, ); + if (modifiedCount || upsertedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, member._id, modifiedCount ? 'updated' : 'inserted'); + } } } diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index b339155775e6..769155b66b60 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -12,7 +12,7 @@ import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreate import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; -import { notifyOnRoomChanged } from '../lib/notifyListener'; +import { notifyOnRoomChanged, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; import { createDirectRoom } from './createDirectRoom'; const isValidName = (name: unknown): name is string => { @@ -47,7 +47,11 @@ async function createUsersSubscriptions({ ...getDefaultSubscriptionPref(owner), }; - await Subscriptions.createWithRoomAndUser(room, owner, extra); + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, owner, extra); + + if (insertedId) { + await notifyOnRoomChanged(room, 'inserted'); + } return; } @@ -98,7 +102,9 @@ async function createUsersSubscriptions({ await Users.addRoomByUserIds(memberIds, room._id); } - await Subscriptions.createWithRoomAndManyUsers(room, subs); + const { insertedIds } = await Subscriptions.createWithRoomAndManyUsers(room, subs); + + Object.values(insertedIds).forEach((subId) => notifyOnSubscriptionChangedById(subId, 'inserted')); await Rooms.incUsersCountById(room._id, subs.length); } diff --git a/apps/meteor/app/lib/server/functions/deleteRoom.ts b/apps/meteor/app/lib/server/functions/deleteRoom.ts index a605d82b08c2..386ba8da8b94 100644 --- a/apps/meteor/app/lib/server/functions/deleteRoom.ts +++ b/apps/meteor/app/lib/server/functions/deleteRoom.ts @@ -2,16 +2,27 @@ import { Messages, Rooms, Subscriptions } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { FileUpload } from '../../../file-upload/server'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChanged } from '../lib/notifyListener'; export const deleteRoom = async function (rid: string): Promise { await FileUpload.removeFilesByRoomId(rid); + await Messages.removeByRoomId(rid); + await callbacks.run('beforeDeleteRoom', rid); - await Subscriptions.removeByRoomId(rid); + + await Subscriptions.removeByRoomId(rid, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }); + await FileUpload.getStore('Avatars').deleteByRoomId(rid); + await callbacks.run('afterDeleteRoom', rid); - await Rooms.removeById(rid); - void notifyOnRoomChangedById(rid, 'removed'); + const { deletedCount } = await Rooms.removeById(rid); + if (deletedCount) { + void notifyOnRoomChangedById(rid, 'removed'); + } }; diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index e66c8c2d5eef..483085d40811 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -112,7 +112,7 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele const rids = subscribedRooms.map((room) => room.rid); void notifyOnRoomChangedById(rids); - await Subscriptions.removeByUserId(userId); // Remove user subscriptions + await Subscriptions.removeByUserId(userId); // Remove user as livechat agent if (user.roles.includes('livechat-agent')) { diff --git a/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts b/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts index 75b232462077..8f1981ca386d 100644 --- a/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts +++ b/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts @@ -1,6 +1,7 @@ import { Messages, Roles, Rooms, Subscriptions, ReadReceipts } from '@rocket.chat/models'; import { FileUpload } from '../../../file-upload/server'; +import { notifyOnSubscriptionChanged } from '../lib/notifyListener'; import type { SubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; const bulkRoomCleanUp = async (rids: string[]): Promise => { @@ -8,7 +9,11 @@ const bulkRoomCleanUp = async (rids: string[]): Promise => { await Promise.all(rids.map((rid) => FileUpload.removeFilesByRoomId(rid))); return Promise.all([ - Subscriptions.removeByRoomIds(rids), + Subscriptions.removeByRoomIds(rids, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), Messages.removeByRoomIds(rids), ReadReceipts.removeByRoomIds(rids), Rooms.removeByIds(rids), diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts index 686fdd9c0317..5800cb68af81 100644 --- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts +++ b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts @@ -8,7 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; import { beforeLeaveRoomCallback } from '../../../../lib/callbacks/beforeLeaveRoomCallback'; import { settings } from '../../../settings/server'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChanged } from '../lib/notifyListener'; export const removeUserFromRoom = async function (rid: string, user: IUser, options?: { byUser: IUser }): Promise { const room = await Rooms.findOneById(rid); @@ -59,7 +59,10 @@ export const removeUserFromRoom = async function (rid: string, user: IUser, opti await Message.saveSystemMessage('command', rid, 'survey', user); } - await Subscriptions.removeByRoomIdAndUserId(rid, user._id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(rid, user._id); + if (deletedSubscription) { + void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); + } if (room.teamId && room.teamMain) { await Team.removeMember(room.teamId, user._id); diff --git a/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts b/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts index 4a0ac005e55c..5383048f13bd 100644 --- a/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts +++ b/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts @@ -5,6 +5,7 @@ import type { UpdateFilter } from 'mongodb'; import { trim } from '../../../../lib/utils/stringUtils'; import { settings } from '../../../settings/server'; +import { notifyOnSubscriptionChangedByUserIdAndRoomType } from '../lib/notifyListener'; export const saveCustomFieldsWithoutValidation = async function (userId: string, formData: Record): Promise { if (trim(settings.get('Accounts_CustomFields')) !== '') { @@ -22,7 +23,10 @@ export const saveCustomFieldsWithoutValidation = async function (userId: string, await Users.setCustomFields(userId, customFields); // Update customFields of all Direct Messages' Rooms for userId - await Subscriptions.setCustomFieldsDirectMessagesByUserId(userId, customFields); + const setCustomFieldsResponse = await Subscriptions.setCustomFieldsDirectMessagesByUserId(userId, customFields); + if (setCustomFieldsResponse.modifiedCount) { + void notifyOnSubscriptionChangedByUserIdAndRoomType(userId, 'd'); + } for await (const fieldName of Object.keys(customFields)) { if (!customFieldsMeta[fieldName].modifyRecordField) { diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts index 0b9ff21e53e3..1729a1ba8abd 100644 --- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts +++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts @@ -3,7 +3,11 @@ import { Messages, VideoConference, LivechatDepartmentAgents, Rooms, Subscriptio import { SystemLogger } from '../../../../server/lib/logger/system'; import { FileUpload } from '../../../file-upload/server'; -import { notifyOnRoomChangedByUsernamesOrUids } from '../lib/notifyListener'; +import { + notifyOnRoomChangedByUsernamesOrUids, + notifyOnSubscriptionChangedByUserId, + notifyOnSubscriptionChangedByNameAndRoomType, +} from '../lib/notifyListener'; import { _setRealName } from './setRealName'; import { _setUsername } from './setUsername'; import { updateGroupDMsName } from './updateGroupDMsName'; @@ -129,20 +133,38 @@ async function updateUsernameReferences({ await Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg); } - await Rooms.replaceUsername(previousUsername, username); - await Rooms.replaceMutedUsername(previousUsername, username); - await Rooms.replaceUsernameOfUserByUserId(user._id, username); - await Subscriptions.setUserUsernameByUserId(user._id, username); + const responses = await Promise.all([ + Rooms.replaceUsername(previousUsername, username), + Rooms.replaceMutedUsername(previousUsername, username), + Rooms.replaceUsernameOfUserByUserId(user._id, username), + Subscriptions.setUserUsernameByUserId(user._id, username), + LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username), + ]); - await LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username); + if (responses[3]?.modifiedCount) { + void notifyOnSubscriptionChangedByUserId(user._id); + } - void notifyOnRoomChangedByUsernamesOrUids([user._id], [previousUsername, username]); + if (responses[0]?.modifiedCount || responses[1]?.modifiedCount || responses[2]?.modifiedCount) { + void notifyOnRoomChangedByUsernamesOrUids([user._id], [previousUsername, username]); + } } // update other references if either the name or username has changed if (usernameChanged || nameChanged) { // update name and fname of 1-on-1 direct messages - await Subscriptions.updateDirectNameAndFnameByName(previousUsername, rawUsername && username, rawName && name); + const updateDirectNameResponse = await Subscriptions.updateDirectNameAndFnameByName( + previousUsername, + rawUsername && username, + rawName && name, + ); + + if (updateDirectNameResponse?.modifiedCount) { + void notifyOnSubscriptionChangedByNameAndRoomType({ + t: 'd', + name: username, + }); + } // update name and fname of group direct messages await updateGroupDMsName(user); diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index 9d7a3e113fc4..929c24210d2d 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -9,7 +9,12 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; -import { notifyOnRoomChangedById, notifyOnRoomChangedByUserDM, notifyOnUserChange } from '../lib/notifyListener'; +import { + notifyOnRoomChangedById, + notifyOnRoomChangedByUserDM, + notifyOnSubscriptionChangedByNameAndRoomType, + notifyOnUserChange, +} from '../lib/notifyListener'; import { closeOmnichannelConversations } from './closeOmnichannelConversations'; import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; @@ -39,8 +44,10 @@ async function reactivateDirectConversations(userId: string) { return acc; }, []); - await Rooms.setDmReadOnlyByUserId(userId, roomsToReactivate, false, false); - void notifyOnRoomChangedById(roomsToReactivate); + const setDmReadOnlyResponse = await Rooms.setDmReadOnlyByUserId(userId, roomsToReactivate, false, false); + if (setDmReadOnlyResponse.modifiedCount) { + void notifyOnRoomChangedById(roomsToReactivate); + } } export async function setUserActiveStatus(userId: string, active: boolean, confirmRelinquish = false): Promise { @@ -118,7 +125,10 @@ export async function setUserActiveStatus(userId: string, active: boolean, confi } if (user.username) { - await Subscriptions.setArchivedByUsername(user.username, !active); + const { modifiedCount } = await Subscriptions.setArchivedByUsername(user.username, !active); + if (modifiedCount) { + void notifyOnSubscriptionChangedByNameAndRoomType({ t: 'd', name: user.username }); + } } if (active === false) { diff --git a/apps/meteor/app/lib/server/functions/unarchiveRoom.ts b/apps/meteor/app/lib/server/functions/unarchiveRoom.ts index 7db86ed933a3..699f9c3701b1 100644 --- a/apps/meteor/app/lib/server/functions/unarchiveRoom.ts +++ b/apps/meteor/app/lib/server/functions/unarchiveRoom.ts @@ -2,11 +2,16 @@ import { Message } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '@rocket.chat/models'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener'; export const unarchiveRoom = async function (rid: string, user: IMessage['u']): Promise { await Rooms.unarchiveById(rid); - await Subscriptions.unarchiveByRoomId(rid); + + const unarchiveResponse = await Subscriptions.unarchiveByRoomId(rid); + if (unarchiveResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + await Message.saveSystemMessage('room-unarchived', rid, '', user); void notifyOnRoomChangedById(rid); diff --git a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts index a0ad2eedcf55..feb26ce6a1b0 100644 --- a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts +++ b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts @@ -1,6 +1,8 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; +import { notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener'; + const getFname = (members: IUser[]): string => members.map(({ name, username }) => name || username).join(', '); const getName = (members: IUser[]): string => members.map(({ username }) => username).join(','); @@ -63,7 +65,10 @@ export const updateGroupDMsName = async (userThatChangedName: IUser): Promise _id !== sub.u._id); - await Subscriptions.updateNameAndFnameById(sub._id, getName(otherMembers), getFname(otherMembers)); + const updateNameRespose = await Subscriptions.updateNameAndFnameById(sub._id, getName(otherMembers), getFname(otherMembers)); + if (updateNameRespose.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(room._id); + } } } }; diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index 934742945f2d..778fe89dbbf4 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -15,6 +15,7 @@ import type { IEmailInbox, IIntegrationHistory, AtLeast, + ISubscription, ISettingColor, IUser, IMessage, @@ -30,6 +31,7 @@ import { Integrations, LoginServiceConfiguration, IntegrationHistory, + Subscriptions, LivechatInquiry, LivechatDepartmentAgents, Users, @@ -37,6 +39,7 @@ import { } from '@rocket.chat/models'; import mem from 'mem'; +import { subscriptionFields } from '../../../../lib/publishFields'; import { shouldHideSystemMessage } from '../../../../server/lib/systemMessage/hideSystemMessage'; type ClientAction = 'inserted' | 'updated' | 'removed'; @@ -467,3 +470,119 @@ export const notifyOnMessageChange = withDbWatcherCheck(async ({ id, data }: { i } void api.broadcast('watch.messages', { message }); }); + +export const notifyOnSubscriptionChanged = withDbWatcherCheck( + async (subscription: ISubscription, clientAction: ClientAction = 'updated'): Promise => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }, +); + +export const notifyOnSubscriptionChangedByRoomIdAndUserId = withDbWatcherCheck( + async ( + rid: ISubscription['rid'], + uid: ISubscription['u']['_id'], + clientAction: Exclude = 'updated', + ): Promise => { + const cursor = Subscriptions.findByUserIdAndRoomIds(uid, [rid], { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedById = withDbWatcherCheck( + async (id: ISubscription['_id'], clientAction: Exclude = 'updated'): Promise => { + const subscription = await Subscriptions.findOneById(id); + if (!subscription) { + return; + } + + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }, +); + +export const notifyOnSubscriptionChangedByUserPreferences = withDbWatcherCheck( + async ( + uid: ISubscription['u']['_id'], + notificationOriginField: keyof ISubscription, + originFieldNotEqualValue: 'user' | 'subscription', + clientAction: Exclude = 'updated', + ): Promise => { + const cursor = Subscriptions.findByUserPreferences(uid, notificationOriginField, originFieldNotEqualValue, { + projection: subscriptionFields, + }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByRoomId = withDbWatcherCheck( + async (rid: ISubscription['rid'], clientAction: Exclude = 'updated'): Promise => { + const cursor = Subscriptions.findByRoomId(rid, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByAutoTranslateAndUserId = withDbWatcherCheck( + async (uid: ISubscription['u']['_id'], clientAction: Exclude = 'updated'): Promise => { + const cursor = Subscriptions.findByAutoTranslateAndUserId(uid, true, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByUserIdAndRoomType = withDbWatcherCheck( + async ( + uid: ISubscription['u']['_id'], + t: ISubscription['t'], + clientAction: Exclude = 'updated', + ): Promise => { + const cursor = Subscriptions.findByUserIdAndRoomType(uid, t, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByNameAndRoomType = withDbWatcherCheck( + async (filter: Partial>, clientAction: Exclude = 'updated'): Promise => { + const cursor = Subscriptions.findByNameAndRoomType(filter, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByUserId = withDbWatcherCheck( + async (uid: ISubscription['u']['_id'], clientAction: Exclude = 'updated'): Promise => { + const cursor = Subscriptions.findByUserId(uid, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByRoomIdAndUserIds = withDbWatcherCheck( + async ( + rid: ISubscription['rid'], + uids: ISubscription['u']['_id'][], + clientAction: Exclude = 'updated', + ): Promise => { + const cursor = Subscriptions.findByRoomIdAndUserIds(rid, uids, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts index 85f2ac52b702..7551cabb6e63 100644 --- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts @@ -7,6 +7,11 @@ import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; +import { + notifyOnSubscriptionChanged, + notifyOnSubscriptionChangedByRoomIdAndUserId, + notifyOnSubscriptionChangedByRoomIdAndUserIds, +} from './notifyListener'; function messageContainsHighlight(message: IMessage, highlights: string[]): boolean { if (!highlights || highlights.length === 0) return false; @@ -51,26 +56,14 @@ export async function getMentions(message: IMessage): Promise<{ toAll: boolean; type UnreadCountType = 'all_messages' | 'user_mentions_only' | 'group_mentions_only' | 'user_and_group_mentions_only'; -const incGroupMentions = async ( - rid: IRoom['_id'], - roomType: RoomType, - excludeUserId: IUser['_id'], - unreadCount: Exclude, -): Promise => { +const getGroupMentions = (roomType: RoomType, unreadCount: Exclude): number => { const incUnreadByGroup = ['all_messages', 'group_mentions_only', 'user_and_group_mentions_only'].includes(unreadCount); - const incUnread = roomType === 'd' || roomType === 'l' || incUnreadByGroup ? 1 : 0; - await Subscriptions.incGroupMentionsAndUnreadForRoomIdExcludingUserId(rid, excludeUserId, 1, incUnread); + return roomType === 'd' || roomType === 'l' || incUnreadByGroup ? 1 : 0; }; -const incUserMentions = async ( - rid: IRoom['_id'], - roomType: RoomType, - uids: IUser['_id'][], - unreadCount: Exclude, -): Promise => { - const incUnreadByUser = new Set(['all_messages', 'user_mentions_only', 'user_and_group_mentions_only']).has(unreadCount); - const incUnread = roomType === 'd' || roomType === 'l' || incUnreadByUser ? 1 : 0; - await Subscriptions.incUserMentionsAndUnreadForRoomIdAndUserIds(rid, uids, 1, incUnread); +const getUserMentions = (roomType: RoomType, unreadCount: Exclude): number => { + const incUnreadByUser = ['all_messages', 'user_mentions_only', 'user_and_group_mentions_only'].includes(unreadCount); + return roomType === 'd' || roomType === 'l' || incUnreadByUser ? 1 : 0; }; export const getUserIdsFromHighlights = async (rid: IRoom['_id'], message: IMessage): Promise => { @@ -101,45 +94,77 @@ const getUnreadSettingCount = (roomType: RoomType): UnreadCountType => { }; async function updateUsersSubscriptions(message: IMessage, room: IRoom): Promise { - // Don't increase unread counter on thread messages - if (room != null && !message.tmid) { - const { toAll, toHere, mentionIds } = await getMentions(message); - - const userIds = new Set(mentionIds); - - const unreadCount = getUnreadSettingCount(room.t); + if (!room || message.tmid) { + return; + } - (await getUserIdsFromHighlights(room._id, message)).forEach((uid) => userIds.add(uid)); + const [mentions, highlightIds] = await Promise.all([getMentions(message), getUserIdsFromHighlights(room._id, message)]); + + const { toAll, toHere, mentionIds } = mentions; + const userIds = [...new Set([...mentionIds, ...highlightIds])]; + const unreadCount = getUnreadSettingCount(room.t); + + const userMentionInc = getUserMentions(room.t, unreadCount as Exclude); + const groupMentionInc = getGroupMentions(room.t, unreadCount as Exclude); + + void Subscriptions.findByRoomIdAndNotAlertOrOpenExcludingUserIds({ + roomId: room._id, + uidsExclude: [message.u._id], + uidsInclude: userIds, + onlyRead: !toAll && !toHere, + }).forEach((sub) => { + const hasUserMention = userIds.includes(sub.u._id); + const shouldIncUnread = hasUserMention || toAll || toHere || unreadCount === 'all_messages'; + void notifyOnSubscriptionChanged( + { + ...sub, + alert: true, + open: true, + ...(shouldIncUnread && { unread: sub.unread + 1 }), + ...(hasUserMention && { userMentions: sub.userMentions + 1 }), + ...((toAll || toHere) && { groupMentions: sub.groupMentions + 1 }), + }, + 'updated', + ); + }); - // Give priority to user mentions over group mentions - if (userIds.size > 0) { - await incUserMentions(room._id, room.t, [...userIds], unreadCount as Exclude); - } else if (toAll || toHere) { - await incGroupMentions(room._id, room.t, message.u._id, unreadCount as Exclude); - } + // Give priority to user mentions over group mentions + if (userIds.length) { + await Subscriptions.incUserMentionsAndUnreadForRoomIdAndUserIds(room._id, userIds, 1, userMentionInc); + } else if (toAll || toHere) { + await Subscriptions.incGroupMentionsAndUnreadForRoomIdExcludingUserId(room._id, message.u._id, 1, groupMentionInc); + } - // this shouldn't run only if has group mentions because it will already exclude mentioned users from the query - if (!toAll && !toHere && unreadCount === 'all_messages') { - await Subscriptions.incUnreadForRoomIdExcludingUserIds(room._id, [...userIds, message.u._id], 1); - } + if (!toAll && !toHere && unreadCount === 'all_messages') { + await Subscriptions.incUnreadForRoomIdExcludingUserIds(room._id, [...userIds, message.u._id], 1); } - // Update all other subscriptions to alert their owners but without incrementing - // the unread counter, as it is only for mentions and direct messages - // We now set alert and open properties in two separate update commands. This proved to be more efficient on MongoDB - because it uses a more efficient index. + // update subscriptions of other members of the room await Promise.all([ Subscriptions.setAlertForRoomIdExcludingUserId(message.rid, message.u._id), Subscriptions.setOpenForRoomIdExcludingUserId(message.rid, message.u._id), ]); + + // update subscription of the message sender + await Subscriptions.setAsReadByRoomIdAndUserId(message.rid, message.u._id); + const setAsReadResponse = await Subscriptions.setAsReadByRoomIdAndUserId(message.rid, message.u._id); + if (setAsReadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(message.rid, message.u._id); + } } export async function updateThreadUsersSubscriptions(message: IMessage, replies: IUser['_id'][]): Promise { // Don't increase unread counter on thread messages - - await Subscriptions.setAlertForRoomIdAndUserIds(message.rid, replies); const repliesPlusSender = [...new Set([message.u._id, ...replies])]; - await Subscriptions.setOpenForRoomIdAndUserIds(message.rid, repliesPlusSender); - await Subscriptions.setLastReplyForRoomIdAndUserIds(message.rid, repliesPlusSender, new Date()); + + const responses = await Promise.all([ + Subscriptions.setAlertForRoomIdAndUserIds(message.rid, replies), + Subscriptions.setOpenForRoomIdAndUserIds(message.rid, repliesPlusSender), + Subscriptions.setLastReplyForRoomIdAndUserIds(message.rid, repliesPlusSender, new Date()), + ]); + + responses.some((response) => response?.modifiedCount) && + void notifyOnSubscriptionChangedByRoomIdAndUserIds(message.rid, repliesPlusSender); } export async function notifyUsersOnMessage(message: IMessage, room: IRoom, roomUpdater: Updater): Promise { diff --git a/apps/meteor/app/lib/server/methods/blockUser.ts b/apps/meteor/app/lib/server/methods/blockUser.ts index b967e35d7bc1..7fe6ec803dd1 100644 --- a/apps/meteor/app/lib/server/methods/blockUser.ts +++ b/apps/meteor/app/lib/server/methods/blockUser.ts @@ -5,6 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; +import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -33,14 +34,22 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); - const subscription2 = await Subscriptions.findOneByRoomIdAndUserId(rid, blocked); + const [blockedUser, blockerUser] = await Promise.all([ + Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }), + Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }), + ]); - if (!subscription || !subscription2) { + if (!blockedUser || !blockerUser) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); } - await Subscriptions.setBlockedByRoomId(rid, blocked, userId); + const [blockedResponse, blockerResponse] = await Subscriptions.setBlockedByRoomId(rid, blocked, userId); + + const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])]; + + if (listenerUsers.length) { + void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers); + } return true; }, diff --git a/apps/meteor/app/lib/server/methods/unblockUser.ts b/apps/meteor/app/lib/server/methods/unblockUser.ts index 2eec5a082109..7b4bc5660010 100644 --- a/apps/meteor/app/lib/server/methods/unblockUser.ts +++ b/apps/meteor/app/lib/server/methods/unblockUser.ts @@ -3,6 +3,8 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -20,14 +22,22 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'blockUser' }); } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); - const subscription2 = await Subscriptions.findOneByRoomIdAndUserId(rid, blocked); + const [blockedUser, blockerUser] = await Promise.all([ + Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }), + Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }), + ]); - if (!subscription || !subscription2) { + if (!blockedUser || !blockerUser) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); } - await Subscriptions.unsetBlockedByRoomId(rid, blocked, userId); + const [blockedResponse, blockerResponse] = await Subscriptions.unsetBlockedByRoomId(rid, blocked, userId); + + const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])]; + + if (listenerUsers.length) { + void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers); + } return true; }, diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts index 2e648b02f5dd..c20b5dbdb661 100644 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -6,7 +6,11 @@ import type { MatchKeysAndValues, OnlyFieldsOfType } from 'mongodb'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; -import { notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByRoom } from '../../../lib/server/lib/notifyListener'; +import { + notifyOnRoomChangedById, + notifyOnSubscriptionChangedByRoomId, + notifyOnLivechatInquiryChangedByRoom, +} from '../../../lib/server/lib/notifyListener'; import { i18n } from '../../../utils/lib/i18n'; type RegisterContactProps = { @@ -138,14 +142,23 @@ export const Contacts = { for await (const room of rooms) { const { _id: rid } = room; - await Promise.all([ + const responses = await Promise.all([ Rooms.setFnameById(rid, name), LivechatInquiry.setNameByRoomId(rid, name), Subscriptions.updateDisplayNameByRoomId(rid, name), ]); - void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); - void notifyOnRoomChangedById(rid); + if (responses[0]?.modifiedCount) { + void notifyOnRoomChangedById(rid); + } + + if (responses[1]?.modifiedCount) { + void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); + } + + if (responses[2]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } } } diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 1ef572df3068..17f21d8d7b04 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -39,6 +39,9 @@ import { sendNotification } from '../../../lib/server'; import { notifyOnLivechatDepartmentAgentChanged, notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId, + notifyOnSubscriptionChangedById, + notifyOnSubscriptionChangedByRoomId, + notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { Livechat as LivechatTyped } from './LivechatTyped'; @@ -285,7 +288,13 @@ export const createLivechatSubscription = async ( ...(department && { department }), } as InsertionModel; - return Subscriptions.insertOne(subscriptionData); + const response = await Subscriptions.insertOne(subscriptionData); + + if (response?.insertedId) { + void notifyOnSubscriptionChangedById(response.insertedId, 'inserted'); + } + + return response; }; export const removeAgentFromSubscription = async (rid: string, { _id, username }: Pick) => { @@ -296,7 +305,11 @@ export const removeAgentFromSubscription = async (rid: string, { _id, username } return; } - await Subscriptions.removeByRoomIdAndUserId(rid, _id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(rid, _id); + if (deletedSubscription) { + void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); + } + await Message.saveSystemMessage('ul', rid, username || '', { _id: user._id, username: user.username, name: user.name }); setImmediate(() => { @@ -513,12 +526,16 @@ export const updateChatDepartment = async ({ newDepartmentId: string; oldDepartmentId?: string; }) => { - await Promise.all([ + const responses = await Promise.all([ LivechatRooms.changeDepartmentIdByRoomId(rid, newDepartmentId), LivechatInquiry.changeDepartmentIdByRoomId(rid, newDepartmentId), Subscriptions.changeDepartmentByRoomId(rid, newDepartmentId), ]); + if (responses[2].modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + setImmediate(() => { void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { type: LivechatTransferEventType.DEPARTMENT, diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index bb8a3fd77ba2..2745b37d1501 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -60,8 +60,10 @@ import { notifyOnLivechatInquiryChangedByRoom, notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByToken, - notifyOnLivechatDepartmentAgentChangedByDepartmentId, notifyOnUserChange, + notifyOnLivechatDepartmentAgentChangedByDepartmentId, + notifyOnSubscriptionChangedByRoomId, + notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; @@ -289,7 +291,11 @@ class LivechatClass { throw new Error('Error closing room'); } - await Subscriptions.removeByRoomId(rid); + await Subscriptions.removeByRoomId(rid, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }); this.logger.debug(`DB updated for room ${room._id}`); @@ -515,12 +521,16 @@ class LivechatClass { const result = await Promise.allSettled([ Messages.removeByRoomId(rid), ReadReceipts.removeByRoomId(rid), - Subscriptions.removeByRoomId(rid), + Subscriptions.removeByRoomId(rid, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), LivechatInquiry.removeByRoomId(rid), LivechatRooms.removeById(rid), ]); - if (inquiry) { + if (result[3]?.status === 'fulfilled' && result[3].value?.deletedCount && inquiry) { void notifyOnLivechatInquiryChanged(inquiry, 'removed'); } @@ -1143,13 +1153,18 @@ class LivechatClass { const cursor = LivechatRooms.findByVisitorToken(token); for await (const room of cursor) { await Promise.all([ + Subscriptions.removeByRoomId(room._id, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), FileUpload.removeFilesByRoomId(room._id), Messages.removeByRoomId(room._id), ReadReceipts.removeByRoomId(room._id), ]); } - await Promise.all([Subscriptions.removeByVisitorToken(token), LivechatRooms.removeByVisitorToken(token)]); + await LivechatRooms.removeByVisitorToken(token); const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray(); await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id)); @@ -1701,13 +1716,19 @@ class LivechatClass { const { _id: rid } = roomData; const { name } = guestData; - await Promise.all([ + const responses = await Promise.all([ Rooms.setFnameById(rid, name), LivechatInquiry.setNameByRoomId(rid, name), Subscriptions.updateDisplayNameByRoomId(rid, name), ]); - void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); + if (responses[1]?.modifiedCount) { + void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); + } + + if (responses[2]?.modifiedCount) { + await notifyOnSubscriptionChangedByRoomId(rid); + } } void notifyOnRoomChangedById(roomData._id); diff --git a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts index 6ef1f5567a20..f213ae4b7243 100644 --- a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts +++ b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts @@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../lib/server/lib/notifyListener'; import logger from './logger'; declare module '@rocket.chat/ddp-client' { @@ -36,7 +37,11 @@ Meteor.methods({ }); } - await Subscriptions.setAsUnreadByRoomIdAndUserId(lastMessage.rid, userId, lastMessage.ts); + const setAsUnreadResponse = await Subscriptions.setAsUnreadByRoomIdAndUserId(lastMessage.rid, userId, lastMessage.ts); + if (setAsUnreadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(lastMessage.rid, userId); + } + return; } @@ -72,7 +77,11 @@ Meteor.methods({ if (firstUnreadMessage.ts >= lastSeen) { return logger.debug('Provided message is already marked as unread'); } - logger.debug(`Updating unread message of ${originalMessage.ts} as the first unread`); - await Subscriptions.setAsUnreadByRoomIdAndUserId(originalMessage.rid, userId, originalMessage.ts); + + logger.debug(`Updating unread message of ${originalMessage.ts} as the first unread`); + const setAsUnreadResponse = await Subscriptions.setAsUnreadByRoomIdAndUserId(originalMessage.rid, userId, originalMessage.ts); + if (setAsUnreadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(originalMessage.rid, userId); + } }, }); diff --git a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts index a86ded6f24e5..ddac51b4eca9 100644 --- a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts +++ b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts @@ -4,6 +4,7 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedById } from '../../../lib/server/lib/notifyListener'; import { getUserNotificationPreference } from '../../../utils/server/getUserNotificationPreference'; const saveAudioNotificationValue = (subId: ISubscription['_id'], value: string) => @@ -132,7 +133,10 @@ Meteor.methods({ }); } - await notifications[field].updateMethod(subscription, value); + const updateResponse = await notifications[field].updateMethod(subscription, value); + if (updateResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } return true; }, @@ -144,13 +148,19 @@ Meteor.methods({ method: 'saveAudioNotificationValue', }); } + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); if (!subscription) { throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { method: 'saveAudioNotificationValue', }); } - await saveAudioNotificationValue(subscription._id, value); + + const saveAudioNotificationResponse = await saveAudioNotificationValue(subscription._id, value); + if (saveAudioNotificationResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } + return true; }, }); diff --git a/apps/meteor/app/threads/server/functions.ts b/apps/meteor/app/threads/server/functions.ts index 30daef8b8b93..194e482c54ae 100644 --- a/apps/meteor/app/threads/server/functions.ts +++ b/apps/meteor/app/threads/server/functions.ts @@ -2,51 +2,57 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { isEditedMessage } from '@rocket.chat/core-typings'; import { Messages, Subscriptions, ReadReceipts, NotificationQueue } from '@rocket.chat/models'; +import { + notifyOnSubscriptionChangedByRoomIdAndUserIds, + notifyOnSubscriptionChangedByRoomIdAndUserId, +} from '../../lib/server/lib/notifyListener'; import { getMentions, getUserIdsFromHighlights } from '../../lib/server/lib/notifyUsersOnMessage'; export async function reply({ tmid }: { tmid?: string }, message: IMessage, parentMessage: IMessage, followers: string[]) { - const { rid, ts, u } = message; if (!tmid || isEditedMessage(message)) { return false; } - const { toAll, toHere, mentionIds } = await getMentions(message); + const { rid, ts, u } = message; + + const [highlightsUids, threadFollowers, { toAll, toHere, mentionIds }] = await Promise.all([ + getUserIdsFromHighlights(rid, message), + Messages.getThreadFollowsByThreadId(tmid), + getMentions(message), + ]); const addToReplies = [ - ...new Set([ - ...followers, - ...mentionIds, - ...(Array.isArray(parentMessage.replies) && parentMessage.replies.length ? [u._id] : [parentMessage.u._id, u._id]), - ]), + ...new Set([...followers, ...mentionIds, ...(parentMessage.replies?.length ? [u._id] : [parentMessage.u._id, u._id])]), ]; - const highlightedUserIds = new Set(); - (await getUserIdsFromHighlights(rid, message)).forEach((uid) => highlightedUserIds.add(uid)); - await Messages.updateRepliesByThreadId(tmid, addToReplies, ts); - await ReadReceipts.setAsThreadById(tmid); + const threadFollowersUids = threadFollowers?.filter((userId) => userId !== u._id && !mentionIds.includes(userId)) || []; - const replies = await Messages.getThreadFollowsByThreadId(tmid); + // Notify everyone involved in the thread + const notifyOptions = toAll || toHere ? { groupMention: true } : {}; - const repliesFiltered = (replies || []).filter((userId) => userId !== u._id).filter((userId) => !mentionIds.includes(userId)); + // Notify message mentioned users and highlights + const mentionedUsers = [...new Set([...mentionIds, ...highlightsUids])]; - if (toAll || toHere) { - await Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, repliesFiltered, tmid, { - groupMention: true, - }); - } else { - await Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, repliesFiltered, tmid, {}); - } + const promises = [ + Messages.updateRepliesByThreadId(tmid, addToReplies, ts), + ReadReceipts.setAsThreadById(tmid), + Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, threadFollowersUids, tmid, notifyOptions), + ]; - const mentionedUsers = new Set([...mentionIds, ...highlightedUserIds]); - for await (const userId of mentionedUsers) { - await Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, [userId], tmid, { userMention: true }); + if (mentionedUsers.length) { + promises.push(Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, mentionedUsers, tmid, { userMention: true })); } - const highlightIds = Array.from(highlightedUserIds); - if (highlightIds.length) { - await Subscriptions.setAlertForRoomIdAndUserIds(rid, highlightIds); - await Subscriptions.setOpenForRoomIdAndUserIds(rid, highlightIds); + if (highlightsUids.length) { + promises.push( + Subscriptions.setAlertForRoomIdAndUserIds(rid, highlightsUids), + Subscriptions.setOpenForRoomIdAndUserIds(rid, highlightsUids), + ); } + + await Promise.allSettled(promises); + + void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, [...threadFollowersUids, ...mentionedUsers, ...highlightsUids]); } export async function follow({ tmid, uid }: { tmid: string; uid: string }) { @@ -62,20 +68,27 @@ export async function unfollow({ tmid, rid, uid }: { tmid: string; rid: string; return false; } - await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, uid, tmid); + const removeUnreadThreadResponse = await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, uid, tmid); + if (removeUnreadThreadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } await Messages.removeThreadFollowerByThreadId(tmid, uid); } export const readThread = async ({ userId, rid, tmid }: { userId: string; rid: string; tmid: string }) => { - const projection = { tunread: 1 }; - const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection }); + const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { tunread: 1 } }); if (!sub) { return; } + // if the thread being marked as read is the last one unread also clear the unread subscription flag const clearAlert = sub.tunread && sub.tunread?.length <= 1 && sub.tunread.includes(tmid); - await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid, clearAlert); + const removeUnreadThreadResponse = await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid, clearAlert); + if (removeUnreadThreadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, userId); + } + await NotificationQueue.clearQueueByUserId(userId); }; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts index 4e76f396617b..bc2d4fb6a3fb 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts @@ -1,6 +1,7 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; +import { notifyOnSubscriptionChangedByRoomId } from '../../../../../app/lib/server/lib/notifyListener'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; @@ -16,12 +17,16 @@ const onCloseLivechat = async (params: LivechatCloseCallbackParams) => { room: { _id: roomId }, } = params; - await Promise.all([ + const responses = await Promise.all([ LivechatRooms.unsetOnHoldByRoomId(roomId), Subscriptions.unsetOnHoldByRoomId(roomId), AutoCloseOnHoldScheduler.unscheduleRoom(roomId), ]); + if (responses[1].modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(roomId); + } + if (!settings.get('Livechat_waiting_queue')) { return params; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts index c3133387865d..f2db61ddbc9a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts @@ -5,7 +5,11 @@ import type { IOmnichannelRoom, IUser, ILivechatInquiryRecord, IOmnichannelSyste import { Logger } from '@rocket.chat/logger'; import { LivechatRooms, Subscriptions, LivechatInquiry } from '@rocket.chat/models'; -import { notifyOnLivechatInquiryChangedById, notifyOnRoomChangedById } from '../../../../../app/lib/server/lib/notifyListener'; +import { + notifyOnSubscriptionChangedByRoomId, + notifyOnLivechatInquiryChangedById, + notifyOnRoomChangedById, +} from '../../../../../app/lib/server/lib/notifyListener'; import { dispatchAgentDelegated } from '../../../../../app/livechat/server/lib/Helper'; import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; @@ -53,15 +57,21 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE throw new Error('error-unserved-rooms-cannot-be-placed-onhold'); } - await Promise.all([ + const [roomResult, subsResult] = await Promise.all([ LivechatRooms.setOnHoldByRoomId(roomId), Subscriptions.setOnHoldByRoomId(roomId), Message.saveSystemMessage('omnichannel_placed_chat_on_hold', roomId, '', onHoldBy, { comment }), ]); - await callbacks.run('livechat:afterOnHold', room); + if (roomResult.modifiedCount) { + void notifyOnRoomChangedById(roomId); + } - void notifyOnRoomChangedById(roomId); + if (subsResult.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(roomId); + } + + await callbacks.run('livechat:afterOnHold', room); } async resumeRoomOnHold( @@ -104,15 +114,21 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE clientAction, }); - await Promise.all([ + const [roomResult, subsResult] = await Promise.all([ LivechatRooms.unsetOnHoldByRoomId(roomId), Subscriptions.unsetOnHoldByRoomId(roomId), Message.saveSystemMessage('omnichannel_on_hold_chat_resumed', roomId, '', resumeBy, { comment }), ]); - await callbacks.run('livechat:afterOnHoldChatResumed', room); + if (roomResult.modifiedCount) { + void notifyOnRoomChangedById(roomId); + } - void notifyOnRoomChangedById(roomId); + if (subsResult.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(roomId); + } + + await callbacks.run('livechat:afterOnHoldChatResumed', room); } private async attemptToAssignRoomToServingAgentElseQueueIt({ diff --git a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts index 9180632768af..62623b1a4a1c 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts @@ -1,5 +1,4 @@ -import { isEditedMessage, isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { Subscriptions } from '@rocket.chat/models'; +import { isEditedMessage } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../../lib/callbacks'; import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; @@ -12,11 +11,6 @@ callbacks.add( return message; } - if (!isOmnichannelRoom(room) || !room.closedAt) { - // set subscription as read right after message was sent - await Subscriptions.setAsReadByRoomIdAndUserId(room._id, message.u._id); - } - // mark message as read as well await ReadReceipt.markMessageAsReadBySender(message, room, message.u._id); diff --git a/apps/meteor/server/database/watchCollections.ts b/apps/meteor/server/database/watchCollections.ts index 5cd56af62fd6..6dd173d5d323 100644 --- a/apps/meteor/server/database/watchCollections.ts +++ b/apps/meteor/server/database/watchCollections.ts @@ -29,7 +29,7 @@ const onlyCollections = DBWATCHER_ONLY_COLLECTIONS.split(',') .filter(Boolean); export function getWatchCollections(): string[] { - const collections = [InstanceStatus.getCollectionName(), Subscriptions.getCollectionName()]; + const collections = [InstanceStatus.getCollectionName()]; // add back to the list of collections in case db watchers are enabled if (!dbWatchersDisabled) { @@ -45,6 +45,7 @@ export function getWatchCollections(): string[] { collections.push(LoginServiceConfiguration.getCollectionName()); collections.push(EmailInbox.getCollectionName()); collections.push(IntegrationHistory.getCollectionName()); + collections.push(Subscriptions.getCollectionName()); collections.push(Settings.getCollectionName()); collections.push(LivechatDepartmentAgents.getCollectionName()); } diff --git a/apps/meteor/server/lib/readMessages.ts b/apps/meteor/server/lib/readMessages.ts index d7c8cf559288..3be43a875fac 100644 --- a/apps/meteor/server/lib/readMessages.ts +++ b/apps/meteor/server/lib/readMessages.ts @@ -1,6 +1,7 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { NotificationQueue, Subscriptions } from '@rocket.chat/models'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; import { callbacks } from '../../lib/callbacks'; export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThreads: boolean): Promise { @@ -15,7 +16,10 @@ export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThr // do not mark room as read if there are still unread threads const alert = !!(sub.alert && !readThreads && sub.tunread && sub.tunread.length > 0); - await Subscriptions.setAsReadByRoomIdAndUserId(rid, uid, readThreads, alert); + const setAsReadResponse = await Subscriptions.setAsReadByRoomIdAndUserId(rid, uid, readThreads, alert); + if (setAsReadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } await NotificationQueue.clearQueueByUserId(uid); diff --git a/apps/meteor/server/lib/resetUserE2EKey.ts b/apps/meteor/server/lib/resetUserE2EKey.ts index 8535eee9a2cd..85da6e59cf60 100644 --- a/apps/meteor/server/lib/resetUserE2EKey.ts +++ b/apps/meteor/server/lib/resetUserE2EKey.ts @@ -2,7 +2,7 @@ import { api } from '@rocket.chat/core-services'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { notifyOnUserChange } from '../../app/lib/server/lib/notifyListener'; +import { notifyOnUserChange, notifyOnSubscriptionChangedByUserId } from '../../app/lib/server/lib/notifyListener'; import * as Mailer from '../../app/mailer/server/api'; import { settings } from '../../app/settings/server'; import { i18n } from './i18n'; @@ -67,12 +67,13 @@ export async function resetUserE2EEncriptionKey(uid: string, notifyUser: boolean } // force logout the live sessions - await api.broadcast('user.forceLogout', uid); - await Users.resetE2EKey(uid); - await Subscriptions.resetUserE2EKey(uid); - await Rooms.removeUserFromE2EEQueue(uid); + const responses = await Promise.all([Users.resetE2EKey(uid), Subscriptions.resetUserE2EKey(uid), Rooms.removeUserFromE2EEQueue(uid)]); + + if (responses[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByUserId(uid); + } // Force the user to logout, so that the keys can be generated again await Users.unsetLoginTokens(uid); diff --git a/apps/meteor/server/methods/addAllUserToRoom.ts b/apps/meteor/server/methods/addAllUserToRoom.ts index c07bdc48040a..6b1b690b4bfd 100644 --- a/apps/meteor/server/methods/addAllUserToRoom.ts +++ b/apps/meteor/server/methods/addAllUserToRoom.ts @@ -6,6 +6,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; import { getDefaultSubscriptionPref } from '../../app/utils/lib/getDefaultSubscriptionPref'; import { callbacks } from '../../lib/callbacks'; @@ -58,7 +59,7 @@ Meteor.methods({ } await callbacks.run('beforeJoinRoom', user, room); const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); - await Subscriptions.createWithRoomAndUser(room, user, { + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, { ts: now, open: true, alert: true, @@ -68,6 +69,9 @@ Meteor.methods({ ...autoTranslateConfig, ...getDefaultSubscriptionPref(user), }); + if (insertedId) { + void notifyOnSubscriptionChangedById(insertedId, 'inserted'); + } await Message.saveSystemMessage('uj', rid, user.username || '', user, { ts: now }); await callbacks.run('afterJoinRoom', user, room); } diff --git a/apps/meteor/server/methods/addRoomLeader.ts b/apps/meteor/server/methods/addRoomLeader.ts index b8e09b44065a..64240bff65f0 100644 --- a/apps/meteor/server/methods/addRoomLeader.ts +++ b/apps/meteor/server/methods/addRoomLeader.ts @@ -6,6 +6,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; declare module '@rocket.chat/ddp-client' { @@ -56,7 +57,10 @@ Meteor.methods({ }); } - await Subscriptions.addRoleById(subscription._id, 'leader'); + const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'leader'); + if (addRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); diff --git a/apps/meteor/server/methods/addRoomModerator.ts b/apps/meteor/server/methods/addRoomModerator.ts index a9cc21f30e0d..da75038a3688 100644 --- a/apps/meteor/server/methods/addRoomModerator.ts +++ b/apps/meteor/server/methods/addRoomModerator.ts @@ -7,6 +7,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; import { isFederationEnabled, isFederationReady, FederationMatrixInvalidConfigurationError } from '../services/federation/utils'; @@ -71,7 +72,10 @@ Meteor.methods({ }); } - await Subscriptions.addRoleById(subscription._id, 'moderator'); + const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'moderator'); + if (addRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); if (!fromUser) { diff --git a/apps/meteor/server/methods/addRoomOwner.ts b/apps/meteor/server/methods/addRoomOwner.ts index f59267f6719a..d0a23efea024 100644 --- a/apps/meteor/server/methods/addRoomOwner.ts +++ b/apps/meteor/server/methods/addRoomOwner.ts @@ -7,6 +7,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; import { isFederationReady, isFederationEnabled, FederationMatrixInvalidConfigurationError } from '../services/federation/utils'; @@ -71,7 +72,10 @@ Meteor.methods({ }); } - await Subscriptions.addRoleById(subscription._id, 'owner'); + const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'owner'); + if (addRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); if (!fromUser) { diff --git a/apps/meteor/server/methods/hideRoom.ts b/apps/meteor/server/methods/hideRoom.ts index a53a328d549a..1fd4c6b4657e 100644 --- a/apps/meteor/server/methods/hideRoom.ts +++ b/apps/meteor/server/methods/hideRoom.ts @@ -3,6 +3,8 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -19,7 +21,13 @@ export const hideRoomMethod = async (userId: string, rid: string): Promise({ diff --git a/apps/meteor/server/methods/ignoreUser.ts b/apps/meteor/server/methods/ignoreUser.ts index 358fc3be3d8f..a8739a910b37 100644 --- a/apps/meteor/server/methods/ignoreUser.ts +++ b/apps/meteor/server/methods/ignoreUser.ts @@ -3,6 +3,8 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -23,7 +25,10 @@ Meteor.methods({ }); } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); + const [subscription, subscriptionIgnoredUser] = await Promise.all([ + Subscriptions.findOneByRoomIdAndUserId(rid, userId), + Subscriptions.findOneByRoomIdAndUserId(rid, ignoredUser), + ]); if (!subscription) { throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { @@ -31,14 +36,18 @@ Meteor.methods({ }); } - const subscriptionIgnoredUser = await Subscriptions.findOneByRoomIdAndUserId(rid, ignoredUser); - if (!subscriptionIgnoredUser) { throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { method: 'ignoreUser', }); } - return !!(await Subscriptions.ignoreUser({ _id: subscription._id, ignoredUser, ignore })); + const result = await Subscriptions.ignoreUser({ _id: subscription._id, ignoredUser, ignore }); + + if (result.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } + + return !!result; }, }); diff --git a/apps/meteor/server/methods/openRoom.ts b/apps/meteor/server/methods/openRoom.ts index b2957768f237..440de52b87fb 100644 --- a/apps/meteor/server/methods/openRoom.ts +++ b/apps/meteor/server/methods/openRoom.ts @@ -4,6 +4,8 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -23,6 +25,12 @@ Meteor.methods({ }); } - return (await Subscriptions.openByRoomIdAndUserId(rid, uid)).modifiedCount; + const openByRoomResponse = await Subscriptions.openByRoomIdAndUserId(rid, uid); + + if (openByRoomResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } + + return openByRoomResponse.modifiedCount; }, }); diff --git a/apps/meteor/server/methods/removeRoomLeader.ts b/apps/meteor/server/methods/removeRoomLeader.ts index 754d68960a4a..8a8f92d08fa0 100644 --- a/apps/meteor/server/methods/removeRoomLeader.ts +++ b/apps/meteor/server/methods/removeRoomLeader.ts @@ -6,6 +6,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; declare module '@rocket.chat/ddp-client' { @@ -56,7 +57,10 @@ Meteor.methods({ }); } - await Subscriptions.removeRoleById(subscription._id, 'leader'); + const removeRoleResponse = await Subscriptions.removeRoleById(subscription._id, 'leader'); + if (removeRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); if (!fromUser) { diff --git a/apps/meteor/server/methods/removeRoomModerator.ts b/apps/meteor/server/methods/removeRoomModerator.ts index 291cc294a5fa..bcb50076c834 100644 --- a/apps/meteor/server/methods/removeRoomModerator.ts +++ b/apps/meteor/server/methods/removeRoomModerator.ts @@ -7,6 +7,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; declare module '@rocket.chat/ddp-client' { @@ -64,7 +65,10 @@ Meteor.methods({ }); } - await Subscriptions.removeRoleById(subscription._id, 'moderator'); + const removeRoleResponse = await Subscriptions.removeRoleById(subscription._id, 'moderator'); + if (removeRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); if (!fromUser) { diff --git a/apps/meteor/server/methods/removeRoomOwner.ts b/apps/meteor/server/methods/removeRoomOwner.ts index 82ee2c37f9b8..91046655a4a6 100644 --- a/apps/meteor/server/methods/removeRoomOwner.ts +++ b/apps/meteor/server/methods/removeRoomOwner.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { getUsersInRole } from '../../app/authorization/server'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; declare module '@rocket.chat/ddp-client' { @@ -71,7 +72,10 @@ Meteor.methods({ }); } - await Subscriptions.removeRoleById(subscription._id, 'owner'); + const removeRoleResponse = await Subscriptions.removeRoleById(subscription._id, 'owner'); + if (removeRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); if (!fromUser) { diff --git a/apps/meteor/server/methods/removeUserFromRoom.ts b/apps/meteor/server/methods/removeUserFromRoom.ts index b039beb7ce64..781ffe3a2671 100644 --- a/apps/meteor/server/methods/removeUserFromRoom.ts +++ b/apps/meteor/server/methods/removeUserFromRoom.ts @@ -9,7 +9,7 @@ import { Meteor } from 'meteor/meteor'; import { canAccessRoomAsync, getUsersInRole } from '../../app/authorization/server'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { hasRoleAsync } from '../../app/authorization/server/functions/hasRole'; -import { notifyOnRoomChanged } from '../../app/lib/server/lib/notifyListener'; +import { notifyOnRoomChanged, notifyOnSubscriptionChanged } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; import { RoomMemberActions } from '../../definition/IRoomTypeConfig'; import { callbacks } from '../../lib/callbacks'; @@ -91,7 +91,10 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri await callbacks.run('beforeRemoveFromRoom', { removedUser, userWhoRemoved: fromUser }, room); - await Subscriptions.removeByRoomIdAndUserId(data.rid, removedUser._id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(data.rid, removedUser._id); + if (deletedSubscription) { + void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); + } if (['c', 'p'].includes(room.t) === true) { await removeUserFromRolesAsync(removedUser._id, ['moderator', 'owner'], data.rid); diff --git a/apps/meteor/server/methods/saveUserPreferences.ts b/apps/meteor/server/methods/saveUserPreferences.ts index f19e653f7ccc..70e3a5cb9ea7 100644 --- a/apps/meteor/server/methods/saveUserPreferences.ts +++ b/apps/meteor/server/methods/saveUserPreferences.ts @@ -1,11 +1,16 @@ -import type { ThemePreference } from '@rocket.chat/core-typings'; +import type { ISubscription, ThemePreference } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Subscriptions, Users } from '@rocket.chat/models'; import type { FontSize } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { notifyOnUserChange } from '../../app/lib/server/lib/notifyListener'; +import { + notifyOnSubscriptionChangedByAutoTranslateAndUserId, + notifyOnSubscriptionChangedByUserId, + notifyOnSubscriptionChangedByUserPreferences, + notifyOnUserChange, +} from '../../app/lib/server/lib/notifyListener'; import { settings as rcSettings } from '../../app/settings/server'; type UserPreferences = { @@ -54,6 +59,31 @@ declare module '@rocket.chat/ddp-client' { } } +async function updateNotificationPreferences( + userId: ISubscription['u']['_id'], + setting: keyof ISubscription, + newValue: string, + oldValue: string, + preferenceType: keyof ISubscription, +) { + if (newValue === oldValue) { + return; + } + + if (newValue === 'default') { + const clearNotificationResponse = await Subscriptions.clearNotificationUserPreferences(userId, setting, preferenceType); + if (clearNotificationResponse.modifiedCount) { + void notifyOnSubscriptionChangedByUserPreferences(userId, preferenceType, 'user'); + } + return; + } + + const updateNotificationResponse = await Subscriptions.updateNotificationUserPreferences(userId, newValue, setting, preferenceType); + if (updateNotificationResponse.modifiedCount) { + void notifyOnSubscriptionChangedByUserPreferences(userId, preferenceType, 'subscription'); + } +} + export const saveUserPreferences = async (settings: Partial, userId: string): Promise => { const keys = { language: Match.Optional(String), @@ -146,51 +176,41 @@ export const saveUserPreferences = async (settings: Partial, us // propagate changed notification preferences setImmediate(async () => { - if (settings.desktopNotifications && oldDesktopNotifications !== settings.desktopNotifications) { - if (settings.desktopNotifications === 'default') { - await Subscriptions.clearNotificationUserPreferences(user._id, 'desktopNotifications', 'desktopPrefOrigin'); - } else { - await Subscriptions.updateNotificationUserPreferences( - user._id, - settings.desktopNotifications, - 'desktopNotifications', - 'desktopPrefOrigin', - ); - } + const { desktopNotifications, pushNotifications, emailNotificationMode, highlights, language } = settings; + const promises = []; + + if (desktopNotifications) { + promises.push( + updateNotificationPreferences(user._id, 'desktopNotifications', desktopNotifications, oldDesktopNotifications, 'desktopPrefOrigin'), + ); } - if (settings.pushNotifications && oldMobileNotifications !== settings.pushNotifications) { - if (settings.pushNotifications === 'default') { - await Subscriptions.clearNotificationUserPreferences(user._id, 'mobilePushNotifications', 'mobilePrefOrigin'); - } else { - await Subscriptions.updateNotificationUserPreferences( - user._id, - settings.pushNotifications, - 'mobilePushNotifications', - 'mobilePrefOrigin', - ); - } + if (pushNotifications) { + promises.push( + updateNotificationPreferences(user._id, 'mobilePushNotifications', pushNotifications, oldMobileNotifications, 'mobilePrefOrigin'), + ); } - if (settings.emailNotificationMode && oldEmailNotifications !== settings.emailNotificationMode) { - if (settings.emailNotificationMode === 'default') { - await Subscriptions.clearNotificationUserPreferences(user._id, 'emailNotifications', 'emailPrefOrigin'); - } else { - await Subscriptions.updateNotificationUserPreferences( - user._id, - settings.emailNotificationMode, - 'emailNotifications', - 'emailPrefOrigin', - ); - } + if (emailNotificationMode) { + promises.push( + updateNotificationPreferences(user._id, 'emailNotifications', emailNotificationMode, oldEmailNotifications, 'emailPrefOrigin'), + ); } - if (Array.isArray(settings.highlights)) { - await Subscriptions.updateUserHighlights(user._id, settings.highlights); + await Promise.allSettled(promises); + + if (Array.isArray(highlights)) { + const response = await Subscriptions.updateUserHighlights(user._id, highlights); + if (response.modifiedCount) { + void notifyOnSubscriptionChangedByUserId(user._id); + } } - if (settings.language && oldLanguage !== settings.language && rcSettings.get('AutoTranslate_AutoEnableOnJoinRoom')) { - await Subscriptions.updateAllAutoTranslateLanguagesByUserId(user._id, settings.language); + if (language && oldLanguage !== language && rcSettings.get('AutoTranslate_AutoEnableOnJoinRoom')) { + const response = await Subscriptions.updateAllAutoTranslateLanguagesByUserId(user._id, language); + if (response.modifiedCount) { + void notifyOnSubscriptionChangedByAutoTranslateAndUserId(user._id); + } } }); }; diff --git a/apps/meteor/server/methods/toggleFavorite.ts b/apps/meteor/server/methods/toggleFavorite.ts index 36555a4566db..912b9a8f3e5c 100644 --- a/apps/meteor/server/methods/toggleFavorite.ts +++ b/apps/meteor/server/methods/toggleFavorite.ts @@ -4,6 +4,8 @@ import { Subscriptions } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -28,6 +30,12 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-subscription', 'You must be part of a room to favorite it', { method: 'toggleFavorite' }); } - return (await Subscriptions.setFavoriteByRoomIdAndUserId(rid, userId, f)).modifiedCount; + const { modifiedCount } = await Subscriptions.setFavoriteByRoomIdAndUserId(rid, userId, f); + + if (modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, userId); + } + + return modifiedCount; }, }); diff --git a/apps/meteor/server/models/dummy/BaseDummy.ts b/apps/meteor/server/models/dummy/BaseDummy.ts index c3052ede9487..049295c1a28a 100644 --- a/apps/meteor/server/models/dummy/BaseDummy.ts +++ b/apps/meteor/server/models/dummy/BaseDummy.ts @@ -53,6 +53,13 @@ export class BaseDummy< return this.collectionName; } + async findOneAndDelete(): Promise> { + return { + value: null, + ok: 1, + }; + } + async findOneAndUpdate(): Promise> { return { value: null, diff --git a/apps/meteor/server/models/raw/BaseRaw.ts b/apps/meteor/server/models/raw/BaseRaw.ts index 1a3dd1a3eb4c..fae43f2dbfb0 100644 --- a/apps/meteor/server/models/raw/BaseRaw.ts +++ b/apps/meteor/server/models/raw/BaseRaw.ts @@ -26,6 +26,7 @@ import type { InsertOneResult, DeleteResult, DeleteOptions, + FindOneAndDeleteOptions, } from 'mongodb'; import { setUpdatedAt } from './setUpdatedAt'; @@ -315,7 +316,38 @@ export abstract class BaseRaw< return this.col.deleteOne(filter); } - async deleteMany(filter: Filter, options?: DeleteOptions): Promise { + async findOneAndDelete(filter: Filter, options?: FindOneAndDeleteOptions): Promise> { + if (!this.trash) { + if (options) { + return this.col.findOneAndDelete(filter, options); + } + return this.col.findOneAndDelete(filter); + } + + const result = await this.col.findOneAndDelete(filter); + + const { value: doc } = result; + if (!doc) { + return result; + } + + const { _id, ...record } = doc; + + const trash: TDeleted = { + ...record, + _deletedAt: new Date(), + __collection__: this.name, + } as unknown as TDeleted; + + // since the operation is not atomic, we need to make sure that the record is not already deleted/inserted + await this.trash?.updateOne({ _id } as Filter, { $set: trash } as UpdateFilter, { + upsert: true, + }); + + return result; + } + + async deleteMany(filter: Filter, options?: DeleteOptions & { onTrash?: (record: ResultFields) => void }): Promise { if (!this.trash) { if (options) { return this.col.deleteMany(filter, options); @@ -341,6 +373,8 @@ export abstract class BaseRaw< await this.trash?.updateOne({ _id } as Filter, { $set: trash } as UpdateFilter, { upsert: true, }); + + void options?.onTrash?.(doc); } if (options) { diff --git a/apps/meteor/server/models/raw/Roles.ts b/apps/meteor/server/models/raw/Roles.ts index 84a5b088ea30..4e1cb09348c4 100644 --- a/apps/meteor/server/models/raw/Roles.ts +++ b/apps/meteor/server/models/raw/Roles.ts @@ -3,6 +3,7 @@ import type { IRolesModel } from '@rocket.chat/model-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import type { Collection, FindCursor, Db, Filter, FindOptions, Document } from 'mongodb'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../../app/lib/server/lib/notifyListener'; import { BaseRaw } from './BaseRaw'; export class RolesRaw extends BaseRaw implements IRolesModel { @@ -35,14 +36,15 @@ export class RolesRaw extends BaseRaw implements IRolesModel { process.env.NODE_ENV === 'development' && console.warn(`[WARN] RolesRaw.addUserRoles: role: ${roleId} not found`); continue; } - switch (role.scope) { - case 'Subscriptions': - // TODO remove dependency from other models - this logic should be inside a function/service - await Subscriptions.addRolesByUserId(userId, [role._id], scope); - break; - case 'Users': - default: - await Users.addRolesByUserId(userId, [role._id]); + + if (role.scope === 'Subscriptions' && scope) { + // TODO remove dependency from other models - this logic should be inside a function/service + const addRolesResponse = await Subscriptions.addRolesByUserId(userId, [role._id], scope); + if (addRolesResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(scope, userId); + } + } else { + await Users.addRolesByUserId(userId, [role._id]); } } return true; @@ -88,13 +90,13 @@ export class RolesRaw extends BaseRaw implements IRolesModel { continue; } - switch (role.scope) { - case 'Subscriptions': - scope && (await Subscriptions.removeRolesByUserId(userId, [roleId], scope)); - break; - case 'Users': - default: - await Users.removeRolesByUserId(userId, [roleId]); + if (role.scope === 'Subscriptions' && scope) { + const removeRolesResponse = await Subscriptions.removeRolesByUserId(userId, [roleId], scope); + if (removeRolesResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(scope, userId); + } + } else { + await Users.removeRolesByUserId(userId, [roleId]); } } return true; diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index 01440a179c7f..efb75ed3d17f 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -191,7 +191,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri readThreads = false, alert = false, options: FindOptions = {}, - ): ReturnType['update']> { + ): ReturnType['updateOne']> { const query: Filter = { rid, 'u._id': uid, @@ -327,20 +327,62 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.find(query, options || {}); } - async removeByRoomId(roomId: string): Promise { + findByRoomIdAndNotAlertOrOpenExcludingUserIds( + { + roomId, + uidsExclude, + uidsInclude, + onlyRead, + }: { + roomId: ISubscription['rid']; + uidsExclude?: ISubscription['u']['_id'][]; + uidsInclude?: ISubscription['u']['_id'][]; + onlyRead: boolean; + }, + options?: FindOptions, + ) { const query = { rid: roomId, + ...(uidsExclude?.length && { + 'u._id': { $nin: uidsExclude }, + }), + ...(onlyRead && { + $or: [...(uidsInclude?.length ? [{ 'u._id': { $in: uidsInclude } }] : []), { alert: { $ne: true } }, { open: { $ne: true } }], + }), }; - const result = (await this.deleteMany(query)).deletedCount; + return this.find(query, options || {}); + } - if (typeof result === 'number' && result > 0) { - await Rooms.incUsersCountByIds([roomId], -result); + async removeByRoomId(roomId: ISubscription['rid'], options?: { onTrash: (doc: ISubscription) => void }): Promise { + const query = { + rid: roomId, + }; + + const deleteResult = await this.deleteMany(query, options); + + if (deleteResult?.deletedCount) { + await Rooms.incUsersCountByIds([roomId], -deleteResult.deletedCount); } await Users.removeRoomByRoomId(roomId); - return result; + return deleteResult; + } + + findByRoomIdExcludingUserIds( + roomId: ISubscription['rid'], + userIds: ISubscription['u']['_id'][], + options: FindOptions = {}, + ): FindCursor { + const query = { + 'rid': roomId, + 'u._id': { + $nin: userIds, + }, + }; + + return this.find(query, options); } async findConnectedUsersExcept( @@ -532,11 +574,10 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } - async setGroupE2EKey(_id: string, key: string): Promise { + async setGroupE2EKey(_id: string, key: string): Promise { const query = { _id }; const update = { $set: { E2EKey: key } }; - await this.updateOne(query, update); - return this.findOneById(_id); + return this.updateOne(query, update); } setGroupE2ESuggestedKey(uid: string, rid: string, key: string): Promise { @@ -558,18 +599,12 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateOne({ rid }, { $unset: { onHold: 1 } }); } - findByRoomIds(roomIds: string[]): FindCursor { + findByRoomIds(roomIds: ISubscription['u']['_id'][], options?: FindOptions): FindCursor { const query = { rid: { $in: roomIds, }, }; - const options = { - projection: { - 'u._id': 1, - 'rid': 1, - }, - }; return this.find(query, options); } @@ -582,6 +617,14 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.deleteMany(query); } + findByToken(token: string, options?: FindOptions): FindCursor { + const query = { + 'v.token': token, + }; + + return this.find(query, options); + } + updateAutoTranslateById(_id: string, autoTranslate: boolean): Promise { const query = { _id, @@ -620,6 +663,19 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } + findByAutoTranslateAndUserId( + userId: ISubscription['u']['_id'], + autoTranslate: ISubscription['autoTranslate'] = true, + options?: FindOptions, + ): FindCursor { + const query = { + 'u._id': userId, + autoTranslate, + }; + + return this.find(query, options); + } + disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise { const query = { rid: roomId, @@ -1092,7 +1148,11 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return subscription?.ls; } - findByRoomIdAndUserIds(roomId: string, userIds: string[], options?: FindOptions): FindCursor { + findByRoomIdAndUserIds( + roomId: ISubscription['rid'], + userIds: ISubscription['u']['_id'][], + options?: FindOptions, + ): FindCursor { const query = { 'rid': roomId, 'u._id': { @@ -1230,6 +1290,33 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } + findByUserIdAndRoomType( + userId: ISubscription['u']['_id'], + type: ISubscription['t'], + options?: FindOptions, + ): FindCursor { + const query = { + 'u._id': userId, + 't': type, + }; + + return this.find(query, options); + } + + findByNameAndRoomType( + filter: Partial>, + options?: FindOptions, + ): FindCursor { + if (!filter.name && !filter.t) { + throw new Error('invalid filter'); + } + const query: Filter = { + ...(filter.name && { name: filter.name }), + ...(filter.t && { t: filter.t }), + }; + return this.find(query, options); + } + setFavoriteByRoomIdAndUserId(roomId: string, userId: string, favorite?: boolean): Promise { if (favorite == null) { favorite = true; @@ -1411,7 +1498,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateOne(query, update); } - setAlertForRoomIdAndUserIds(roomId: string, uids: string[]): Promise { + setAlertForRoomIdAndUserIds(roomId: ISubscription['rid'], uids: ISubscription['u']['_id'][]): Promise { const query = { 'rid': roomId, 'u._id': { $in: uids }, @@ -1423,6 +1510,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri alert: true, }, }; + return this.updateMany(query, update); } @@ -1621,6 +1709,22 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } + findByUserPreferences( + userId: string, + notificationOriginField: keyof ISubscription, + notificationOriginValue: 'user' | 'subscription', + options?: FindOptions, + ): FindCursor { + const value = notificationOriginValue === 'user' ? 'user' : { $ne: 'subscription' }; + + const query: Filter = { + 'u._id': userId, + [notificationOriginField]: value, + }; + + return this.find(query, options); + } + updateUserHighlights(userId: string, userHighlights: any): Promise { const query: Filter = { 'u._id': userId, @@ -1722,9 +1826,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri })); // @ts-expect-error - types not good :( - const result = await this.insertMany(subscriptions); - - return result; + return this.insertMany(subscriptions); } // REMOVE @@ -1746,25 +1848,25 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return result; } - async removeByRoomIdAndUserId(roomId: string, userId: string): Promise { + async removeByRoomIdAndUserId(roomId: string, userId: string): Promise { const query = { 'rid': roomId, 'u._id': userId, }; - const result = (await this.deleteMany(query)).deletedCount; + const { value: doc } = await this.findOneAndDelete(query); - if (typeof result === 'number' && result > 0) { - await Rooms.incUsersCountById(roomId, -result); + if (doc) { + await Rooms.incUsersCountById(roomId, -1); } await Users.removeRoomByUserId(userId, roomId); - return result; + return doc; } - async removeByRoomIds(rids: string[]): Promise { - const result = await this.deleteMany({ rid: { $in: rids } }); + async removeByRoomIds(rids: string[], options?: { onTrash: (doc: ISubscription) => void }): Promise { + const result = await this.deleteMany({ rid: { $in: rids } }, options); await Users.removeRoomByRoomIds(rids); @@ -1850,6 +1952,19 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } + findUnreadThreadsByRoomId( + rid: ISubscription['rid'], + tunread: ISubscription['tunread'], + options?: FindOptions, + ): FindCursor { + const query = { + rid, + tunread: { $in: tunread }, + }; + + return this.find(query, options); + } + openByRoomIdAndUserId(roomId: string, userId: string): Promise { const query = { 'rid': roomId, diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts index 91f5a6e66b43..9deabb53006e 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts @@ -7,6 +7,12 @@ import { saveRoomTopic } from '../../../../../../app/channel-settings/server'; import { addUserToRoom } from '../../../../../../app/lib/server/functions/addUserToRoom'; import { createRoom } from '../../../../../../app/lib/server/functions/createRoom'; import { removeUserFromRoom } from '../../../../../../app/lib/server/functions/removeUserFromRoom'; +import { + notifyOnSubscriptionChanged, + notifyOnSubscriptionChangedById, + notifyOnSubscriptionChangedByRoomId, + notifyOnSubscriptionChangedByRoomIdAndUserId, +} from '../../../../../../app/lib/server/lib/notifyListener'; import { settings } from '../../../../../../app/settings/server'; import { getDefaultSubscriptionPref } from '../../../../../../app/utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../../../../app/utils/server/lib/getValidRoomName'; @@ -78,9 +84,16 @@ export class RocketChatRoomAdapter { public async removeDirectMessageRoom(federatedRoom: FederatedRoom): Promise { const roomId = federatedRoom.getInternalId(); - await Rooms.removeById(roomId); - await Subscriptions.removeByRoomId(roomId); - await MatrixBridgedRoom.removeByLocalRoomId(roomId); + + await Promise.all([ + Rooms.removeById(roomId), + Subscriptions.removeByRoomId(roomId, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), + MatrixBridgedRoom.removeByLocalRoomId(roomId), + ]); } public async createFederatedRoomForDirectMessage(federatedRoom: DirectMessageFederatedRoom): Promise { @@ -160,10 +173,13 @@ export class RocketChatRoomAdapter { } const user = federatedUser.getInternalReference(); - return Subscriptions.createWithRoomAndUser(room, user, { + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, { ts: new Date(), ...getDefaultSubscriptionPref(user), }); + if (insertedId) { + void notifyOnSubscriptionChangedById(insertedId, 'inserted'); + } }) .filter(Boolean), ); @@ -182,33 +198,37 @@ export class RocketChatRoomAdapter { } public async updateRoomType(federatedRoom: FederatedRoom): Promise { - await Rooms.setRoomTypeById(federatedRoom.getInternalId(), federatedRoom.getRoomType()); - await Subscriptions.updateAllRoomTypesByRoomId(federatedRoom.getRoomType(), federatedRoom.getRoomType()); + const rid = federatedRoom.getInternalId(); + const roomType = federatedRoom.getRoomType(); + + await Rooms.setRoomTypeById(rid, roomType); + await Subscriptions.updateAllRoomTypesByRoomId(rid, roomType); + + void notifyOnSubscriptionChangedByRoomId(rid); } public async updateDisplayRoomName(federatedRoom: FederatedRoom, federatedUser: FederatedUser): Promise { - await Rooms.setFnameById(federatedRoom.getInternalId(), federatedRoom.getDisplayName()); - await Subscriptions.updateNameAndFnameByRoomId( - federatedRoom.getInternalId(), - federatedRoom.getName() || '', - federatedRoom.getDisplayName() || '', - ); + const rid = federatedRoom.getInternalId(); + const roomName = federatedRoom.getName() || ''; + const displayName = federatedRoom.getDisplayName() || ''; + const internalReference = federatedUser.getInternalReference(); - await Message.saveSystemMessage( - 'r', - federatedRoom.getInternalId(), - federatedRoom.getDisplayName() || '', - federatedUser.getInternalReference() as unknown as Required, // TODO fix type - ); + await Rooms.setFnameById(rid, displayName); + await Subscriptions.updateNameAndFnameByRoomId(rid, roomName, displayName); + await Message.saveSystemMessage('r', rid, displayName, internalReference); + + void notifyOnSubscriptionChangedByRoomId(rid); } public async updateRoomName(federatedRoom: FederatedRoom): Promise { - await Rooms.setRoomNameById(federatedRoom.getInternalId(), federatedRoom.getName()); - await Subscriptions.updateNameAndFnameByRoomId( - federatedRoom.getInternalId(), - federatedRoom.getName() || '', - federatedRoom.getDisplayName() || '', - ); + const rid = federatedRoom.getInternalId(); + const roomName = federatedRoom.getName() || ''; + const displayName = federatedRoom.getDisplayName() || ''; + + await Rooms.setRoomNameById(rid, roomName); + await Subscriptions.updateNameAndFnameByRoomId(rid, roomName, displayName); + + void notifyOnSubscriptionChangedByRoomId(rid); } public async updateRoomTopic(federatedRoom: FederatedRoom, federatedUser: FederatedUser): Promise { @@ -262,12 +282,15 @@ export class RocketChatRoomAdapter { rolesToRemove: ROCKET_CHAT_FEDERATION_ROLES[]; notifyChannel: boolean; }): Promise { - const subscription = await Subscriptions.findOneByRoomIdAndUserId(federatedRoom.getInternalId(), targetFederatedUser.getInternalId(), { - projection: { roles: 1 }, - }); + const uid = targetFederatedUser.getInternalId(); + const rid = federatedRoom.getInternalId(); + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, uid, { projection: { roles: 1 } }); + if (!subscription) { return; } + const { roles: currentRoles = [] } = subscription; const toAdd = rolesToAdd.filter((role) => !currentRoles.includes(role)); const toRemove = rolesToRemove.filter((role) => currentRoles.includes(role)); @@ -275,14 +298,19 @@ export class RocketChatRoomAdapter { _id: fromUser.getInternalId(), username: fromUser.getUsername(), }; + if (toAdd.length > 0) { - await Subscriptions.addRolesByUserId(targetFederatedUser.getInternalId(), toAdd, federatedRoom.getInternalId()); + const addRolesResponse = await Subscriptions.addRolesByUserId(uid, toAdd, rid); + if (addRolesResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } + if (notifyChannel) { await Promise.all( toAdd.map((role) => Message.saveSystemMessage( 'subscription-role-added', - federatedRoom.getInternalId(), + rid, targetFederatedUser.getInternalReference().username || '', whoDidTheChange, { role }, @@ -291,14 +319,19 @@ export class RocketChatRoomAdapter { ); } } + if (toRemove.length > 0) { - await Subscriptions.removeRolesByUserId(targetFederatedUser.getInternalId(), toRemove, federatedRoom.getInternalId()); + const removeRolesResponse = await Subscriptions.removeRolesByUserId(uid, toRemove, rid); + if (removeRolesResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } + if (notifyChannel) { await Promise.all( toRemove.map((role) => Message.saveSystemMessage( 'subscription-role-removed', - federatedRoom.getInternalId(), + rid, targetFederatedUser.getInternalReference().username || '', whoDidTheChange, { role }, @@ -307,6 +340,7 @@ export class RocketChatRoomAdapter { ); } } + if (settings.get('UI_DisplayRoles')) { this.notifyUIAboutRoomRolesChange(targetFederatedUser, federatedRoom, toAdd, toRemove); } diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index bc4211322b66..5ccf32ef779b 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -32,6 +32,7 @@ import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom'; import { checkUsernameAvailability } from '../../../app/lib/server/functions/checkUsernameAvailability'; import { getSubscribedRoomsForUserWithDetails } from '../../../app/lib/server/functions/getRoomsWithSingleOwner'; import { removeUserFromRoom } from '../../../app/lib/server/functions/removeUserFromRoom'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../../app/lib/server/lib/notifyListener'; import { settings } from '../../../app/settings/server'; export class TeamService extends ServiceClassInternal implements ITeamService { @@ -745,7 +746,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { throw new Error('invalid-team'); } - await Promise.all([ + const responses = await Promise.all([ TeamMember.updateOneByUserIdAndTeamId(member.userId, teamId, memberUpdate), Subscriptions.updateOne( { @@ -757,6 +758,10 @@ export class TeamService extends ServiceClassInternal implements ITeamService { }, ), ]); + + if (responses[1].modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(team.roomId, member.userId); + } } async removeMember(teamId: string, userId: string): Promise { diff --git a/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts b/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts index 07ee437832d2..b40b971128bc 100644 --- a/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts +++ b/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts @@ -73,7 +73,7 @@ describe('closeLivechatRoom', () => { it('should not perform any operation when a closed room with no subscriptions is provided and the caller is not subscribed to it', async () => { livechatRoomsStub.findOneById.resolves({ ...room, open: false }); - subscriptionsStub.countByRoomId.resolves(0); + subscriptionsStub.removeByRoomId.resolves({ deletedCount: 0 }); subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); hasPermissionStub.resolves(true); @@ -81,13 +81,12 @@ describe('closeLivechatRoom', () => { expect(livechatStub.closeRoom.notCalled).to.be.true; expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; - expect(subscriptionsStub.countByRoomId.calledOnceWith(room._id)).to.be.true; - expect(subscriptionsStub.removeByRoomId.notCalled).to.be.true; + expect(subscriptionsStub.removeByRoomId.calledOnceWith(room._id)).to.be.true; }); it('should remove dangling subscription when a closed room with subscriptions is provided and the caller is not subscribed to it', async () => { livechatRoomsStub.findOneById.resolves({ ...room, open: false }); - subscriptionsStub.countByRoomId.resolves(1); + subscriptionsStub.removeByRoomId.resolves({ deletedCount: 1 }); subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); hasPermissionStub.resolves(true); @@ -95,28 +94,25 @@ describe('closeLivechatRoom', () => { expect(livechatStub.closeRoom.notCalled).to.be.true; expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; - expect(subscriptionsStub.countByRoomId.calledOnceWith(room._id)).to.be.true; expect(subscriptionsStub.removeByRoomId.calledOnceWith(room._id)).to.be.true; }); it('should remove dangling subscription when a closed room is provided but the user is still subscribed to it', async () => { livechatRoomsStub.findOneById.resolves({ ...room, open: false }); subscriptionsStub.findOneByRoomIdAndUserId.resolves(subscription); - subscriptionsStub.countByRoomId.resolves(1); + subscriptionsStub.removeByRoomId.resolves({ deletedCount: 1 }); hasPermissionStub.resolves(true); await closeLivechatRoom(user, room._id, {}); expect(livechatStub.closeRoom.notCalled).to.be.true; expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; - expect(subscriptionsStub.countByRoomId.calledOnceWith(room._id)).to.be.true; expect(subscriptionsStub.removeByRoomId.calledOnceWith(room._id)).to.be.true; }); it('should not perform any operation when the caller is not subscribed to an open room and does not have the permission to close others rooms', async () => { livechatRoomsStub.findOneById.resolves(room); subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); - subscriptionsStub.countByRoomId.resolves(1); hasPermissionStub.resolves(false); await expect(closeLivechatRoom(user, room._id, {})).to.be.rejectedWith('error-not-authorized'); @@ -129,7 +125,6 @@ describe('closeLivechatRoom', () => { it('should close the room when the caller is not subscribed to it but has the permission to close others rooms', async () => { livechatRoomsStub.findOneById.resolves(room); subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); - subscriptionsStub.countByRoomId.resolves(1); hasPermissionStub.resolves(true); await closeLivechatRoom(user, room._id, {}); @@ -142,7 +137,6 @@ describe('closeLivechatRoom', () => { it('should close the room when the caller is subscribed to it and does not have the permission to close others rooms', async () => { livechatRoomsStub.findOneById.resolves(room); subscriptionsStub.findOneByRoomIdAndUserId.resolves(subscription); - subscriptionsStub.countByRoomId.resolves(1); hasPermissionStub.resolves(false); await closeLivechatRoom(user, room._id, {}); diff --git a/packages/model-typings/src/models/IBaseModel.ts b/packages/model-typings/src/models/IBaseModel.ts index 246c3ae253dd..626f91385a04 100644 --- a/packages/model-typings/src/models/IBaseModel.ts +++ b/packages/model-typings/src/models/IBaseModel.ts @@ -9,6 +9,7 @@ import type { EnhancedOmit, Filter, FindCursor, + FindOneAndDeleteOptions, FindOneAndUpdateOptions, FindOptions, InsertManyResult, @@ -53,6 +54,7 @@ export interface IBaseModel< getUpdater(): Updater; updateFromUpdater(query: Filter, updater: Updater): Promise; + findOneAndDelete(filter: Filter, options?: FindOneAndDeleteOptions): Promise>; findOneAndUpdate(query: Filter, update: UpdateFilter | T, options?: FindOneAndUpdateOptions): Promise>; findOneById(_id: T['_id'], options?: FindOptions | undefined): Promise; @@ -93,7 +95,7 @@ export interface IBaseModel< deleteOne(filter: Filter, options?: DeleteOptions & { bypassDocumentValidation?: boolean }): Promise; - deleteMany(filter: Filter, options?: DeleteOptions): Promise; + deleteMany(filter: Filter, options?: DeleteOptions & { onTrash?: (record: ResultFields) => void }): Promise; // Trash trashFind

    ( diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 91398e77ebe4..3703996898f6 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -41,7 +41,7 @@ export interface ISubscriptionsModel extends IBaseModel { readThreads?: boolean, alert?: boolean, options?: FindOptions, - ): ReturnType['update']>; + ): ReturnType['updateOne']>; removeRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid: IRoom['_id']): Promise; @@ -73,7 +73,23 @@ export interface ISubscriptionsModel extends IBaseModel { findByUserIdAndTypes(userId: string, types: ISubscription['t'][], options?: FindOptions): FindCursor; - removeByRoomId(roomId: string): Promise; + findByRoomIdAndNotAlertOrOpenExcludingUserIds( + filter: { + roomId: ISubscription['rid']; + uidsExclude?: ISubscription['u']['_id'][]; + uidsInclude?: ISubscription['u']['_id'][]; + onlyRead: boolean; + }, + options?: FindOptions, + ): FindCursor; + + removeByRoomId(roomId: ISubscription['rid'], options?: { onTrash: (doc: ISubscription) => void }): Promise; + + findByRoomIdExcludingUserIds( + roomId: ISubscription['rid'], + userIds: ISubscription['u']['_id'][], + options?: FindOptions, + ): FindCursor; findConnectedUsersExcept( userId: string, @@ -95,7 +111,7 @@ export interface ISubscriptionsModel extends IBaseModel { updateNameAndFnameByRoomId(roomId: string, name: string, fname: string): Promise; - setGroupE2EKey(_id: string, key: string): Promise; + setGroupE2EKey(_id: string, key: string): Promise; setGroupE2ESuggestedKey(uid: string, rid: string, key: string): Promise; @@ -119,9 +135,10 @@ export interface ISubscriptionsModel extends IBaseModel { updateAutoTranslateLanguageById(_id: string, autoTranslateLanguage: string): Promise; removeByVisitorToken(token: string): Promise; + findByToken(token: string, options?: FindOptions): FindCursor; updateMuteGroupMentions(_id: string, muteGroupMentions: boolean): Promise; - findByRoomIds(roomIds: string[]): FindCursor; + findByRoomIds(roomIds: ISubscription['u']['_id'][], options?: FindOptions): FindCursor; changeDepartmentByRoomId(rid: string, department: string): Promise; roleBaseQuery(userId: string, scope?: string): Filter | void; @@ -137,7 +154,24 @@ export interface ISubscriptionsModel extends IBaseModel { findByUserId(userId: string, options?: FindOptions): FindCursor; cachedFindByUserId(userId: string, options?: FindOptions): FindCursor; updateAutoTranslateById(_id: string, autoTranslate: boolean): Promise; + updateAllAutoTranslateLanguagesByUserId(userId: IUser['_id'], language: string): Promise; + findByAutoTranslateAndUserId( + userId: ISubscription['u']['_id'], + autoTranslate?: ISubscription['autoTranslate'], + options?: FindOptions, + ): FindCursor; + + findByUserIdAndRoomType( + userId: ISubscription['u']['_id'], + type: ISubscription['t'], + options?: FindOptions, + ): FindCursor; + findByNameAndRoomType( + filter: Partial>, + options?: FindOptions, + ): FindCursor; + disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise; findAlwaysNotifyDesktopUsersByRoomId(roomId: string): FindCursor; @@ -166,7 +200,11 @@ export interface ISubscriptionsModel extends IBaseModel { options?: FindOptions, ): FindCursor; findByRoomIdAndRoles(roomId: string, roles: string[], options?: FindOptions): FindCursor; - findByRoomIdAndUserIds(roomId: string, userIds: string[], options?: FindOptions): FindCursor; + findByRoomIdAndUserIds( + roomId: ISubscription['rid'], + userIds: ISubscription['u']['_id'][], + options?: FindOptions, + ): FindCursor; findByUserIdUpdatedAfter(userId: string, updatedAt: Date, options?: FindOptions): FindCursor; findByRoomIdAndUserIdsOrAllMessages(roomId: string, userIds: string[]): FindCursor; @@ -203,7 +241,7 @@ export interface ISubscriptionsModel extends IBaseModel { updateCustomFieldsByRoomId(rid: string, cfields: Record): Promise; setOpenForRoomIdAndUserIds(roomId: string, uids: string[]): Promise; - setAlertForRoomIdAndUserIds(roomId: string, uids: string[]): Promise; + setAlertForRoomIdAndUserIds(roomId: ISubscription['rid'], uids: ISubscription['u']['_id'][]): Promise; updateTypeByRoomId(roomId: string, type: ISubscription['t']): Promise; setBlockedByRoomId(rid: string, blocked: string, blocker: string): Promise; incUserMentionsAndUnreadForRoomIdAndUserIds( @@ -227,6 +265,12 @@ export interface ISubscriptionsModel extends IBaseModel { notificationField: keyof ISubscription, notificationOriginField: keyof ISubscription, ): Promise; + findByUserPreferences( + userId: string, + notificationOriginField: keyof ISubscription, + originFieldNotEqualValue: 'user' | 'subscription', + options?: FindOptions, + ): FindCursor; clearNotificationUserPreferences( userId: string, notificationField: string, @@ -239,9 +283,9 @@ export interface ISubscriptionsModel extends IBaseModel { users: { user: AtLeast; extraData: Record }[], ): Promise>; removeByRoomIdsAndUserId(rids: string[], userId: string): Promise; - removeByRoomIdAndUserId(roomId: string, userId: string): Promise; + removeByRoomIdAndUserId(roomId: string, userId: string): Promise; - removeByRoomIds(rids: string[]): Promise; + removeByRoomIds(rids: string[], options?: { onTrash: (doc: ISubscription) => void }): Promise; addUnreadThreadByRoomIdAndUserIds( rid: string, @@ -252,6 +296,12 @@ export interface ISubscriptionsModel extends IBaseModel { removeUnreadThreadByRoomIdAndUserId(rid: string, userId: string, tmid: string, clearAlert?: boolean): Promise; removeUnreadThreadsByRoomId(rid: string, tunread: string[]): Promise; + findUnreadThreadsByRoomId( + rid: ISubscription['rid'], + tunread: ISubscription['tunread'], + options?: FindOptions, + ): FindCursor; + countByRoomIdAndRoles(roomId: string, roles: string[]): Promise; countByRoomId(roomId: string): Promise; countByUserId(userId: string): Promise; From 7f881580367a83c11295a811a9561e9e6572638a Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Wed, 21 Aug 2024 23:20:31 -0300 Subject: [PATCH 48/49] fix: Users without the "Manage OAuth Apps" permission can't log in with third-party apps (#32986) --- .changeset/cool-rocks-remember.md | 6 ++ apps/meteor/app/api/server/v1/oauthapps.ts | 9 +- apps/meteor/server/models/raw/OAuthApps.ts | 18 ++-- apps/meteor/tests/end-to-end/api/oauthapps.ts | 84 +++++++++++-------- .../src/models/IOAuthAppsModel.ts | 1 + 5 files changed, 74 insertions(+), 44 deletions(-) create mode 100644 .changeset/cool-rocks-remember.md diff --git a/.changeset/cool-rocks-remember.md b/.changeset/cool-rocks-remember.md new file mode 100644 index 000000000000..97af36e94320 --- /dev/null +++ b/.changeset/cool-rocks-remember.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +Fixed login with third-party apps not working without the "Manage OAuth Apps" permission diff --git a/apps/meteor/app/api/server/v1/oauthapps.ts b/apps/meteor/app/api/server/v1/oauthapps.ts index 034a73f54104..4113b945a4db 100644 --- a/apps/meteor/app/api/server/v1/oauthapps.ts +++ b/apps/meteor/app/api/server/v1/oauthapps.ts @@ -27,11 +27,12 @@ API.v1.addRoute( { authRequired: true, validateParams: isOauthAppsGetParams }, { async get() { - if (!(await hasPermissionAsync(this.userId, 'manage-oauth-apps'))) { - return API.v1.unauthorized(); - } + const isOAuthAppsManager = await hasPermissionAsync(this.userId, 'manage-oauth-apps'); - const oauthApp = await OAuthApps.findOneAuthAppByIdOrClientId(this.queryParams); + const oauthApp = await OAuthApps.findOneAuthAppByIdOrClientId( + this.queryParams, + !isOAuthAppsManager ? { projection: { clientSecret: 0 } } : {}, + ); if (!oauthApp) { return API.v1.failure('OAuth app not found.'); diff --git a/apps/meteor/server/models/raw/OAuthApps.ts b/apps/meteor/server/models/raw/OAuthApps.ts index dc9ce4f95159..c8650a407f6e 100644 --- a/apps/meteor/server/models/raw/OAuthApps.ts +++ b/apps/meteor/server/models/raw/OAuthApps.ts @@ -13,12 +13,18 @@ export class OAuthAppsRaw extends BaseRaw implements IOAuthAppsModel return [{ key: { clientId: 1, clientSecret: 1 } }, { key: { appId: 1 } }]; } - findOneAuthAppByIdOrClientId(props: { clientId: string } | { appId: string } | { _id: string }): Promise { - return this.findOne({ - ...('_id' in props && { _id: props._id }), - ...('appId' in props && { _id: props.appId }), - ...('clientId' in props && { clientId: props.clientId }), - }); + findOneAuthAppByIdOrClientId( + props: { clientId: string } | { appId: string } | { _id: string }, + options?: FindOptions, + ): Promise { + return this.findOne( + { + ...('_id' in props && { _id: props._id }), + ...('appId' in props && { _id: props.appId }), + ...('clientId' in props && { clientId: props.clientId }), + }, + options, + ); } findOneActiveByClientId(clientId: string, options?: FindOptions): Promise { diff --git a/apps/meteor/tests/end-to-end/api/oauthapps.ts b/apps/meteor/tests/end-to-end/api/oauthapps.ts index 7bffa3297bfc..db714d1107bd 100644 --- a/apps/meteor/tests/end-to-end/api/oauthapps.ts +++ b/apps/meteor/tests/end-to-end/api/oauthapps.ts @@ -51,8 +51,11 @@ describe('[OAuthApps]', () => { }); describe('[/oauth-apps.get]', () => { - it('should return a single oauthApp by id', (done) => { - void request + before(() => updatePermission('manage-oauth-apps', ['admin'])); + after(() => updatePermission('manage-oauth-apps', ['admin'])); + + it('should return a single oauthApp by id', () => { + return request .get(api('oauth-apps.get')) .query({ appId: 'zapier' }) .set(credentials) @@ -61,11 +64,11 @@ describe('[OAuthApps]', () => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('oauthApp'); expect(res.body.oauthApp._id).to.be.equal('zapier'); - }) - .end(done); + expect(res.body.oauthApp).to.have.property('clientSecret'); + }); }); - it('should return a single oauthApp by client id', (done) => { - void request + it('should return a single oauthApp by client id', () => { + return request .get(api('oauth-apps.get')) .query({ clientId: 'zapier' }) .set(credentials) @@ -74,36 +77,49 @@ describe('[OAuthApps]', () => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('oauthApp'); expect(res.body.oauthApp._id).to.be.equal('zapier'); - }) - .end(done); + expect(res.body.oauthApp).to.have.property('clientSecret'); + }); }); - it('should return a 403 Forbidden error when the user does not have the necessary permission by client id', (done) => { - void updatePermission('manage-oauth-apps', []).then(() => { - void request - .get(api('oauth-apps.get')) - .query({ clientId: 'zapier' }) - .set(credentials) - .expect(403) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('unauthorized'); - }) - .end(done); - }); + it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by clientId', async () => { + await updatePermission('manage-oauth-apps', []); + await request + .get(api('oauth-apps.get')) + .query({ clientId: 'zapier' }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal('zapier'); + expect(res.body.oauthApp.clientId).to.be.equal('zapier'); + expect(res.body.oauthApp).to.not.have.property('clientSecret'); + }); }); - it('should return a 403 Forbidden error when the user does not have the necessary permission by app id', (done) => { - void updatePermission('manage-oauth-apps', []).then(() => { - void request - .get(api('oauth-apps.get')) - .query({ appId: 'zapier' }) - .set(credentials) - .expect(403) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('unauthorized'); - }) - .end(done); - }); + it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by appId', async () => { + await updatePermission('manage-oauth-apps', []); + await request + .get(api('oauth-apps.get')) + .query({ appId: 'zapier' }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal('zapier'); + expect(res.body.oauthApp.clientId).to.be.equal('zapier'); + expect(res.body.oauthApp).to.not.have.property('clientSecret'); + }); + }); + it('should fail returning an oauth app when an invalid id is provided (avoid NoSQL injections)', () => { + return request + .get(api('oauth-apps.get')) + .query({ _id: '{ "$ne": "" }' }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'OAuth app not found.'); + }); }); }); diff --git a/packages/model-typings/src/models/IOAuthAppsModel.ts b/packages/model-typings/src/models/IOAuthAppsModel.ts index 8f21ba16d2b7..859c972e4597 100644 --- a/packages/model-typings/src/models/IOAuthAppsModel.ts +++ b/packages/model-typings/src/models/IOAuthAppsModel.ts @@ -11,6 +11,7 @@ export interface IOAuthAppsModel extends IBaseModel { | { _id: string; }, + options?: FindOptions, ): Promise; findOneActiveByClientId(clientId: string, options?: FindOptions): Promise; From 9a69771ff02300e67ce190a4cbc715ad70f1d425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Thu, 22 Aug 2024 00:18:33 -0300 Subject: [PATCH 49/49] feat: New users page contextual bar user form (#32041) --- .../components/UserCard/UserCardInfo.tsx | 2 +- .../views/admin/rooms/RoomsTableFilters.tsx | 4 +- .../views/admin/users/AdminUserCreated.tsx | 29 ++ .../views/admin/users/AdminUserForm.tsx | 482 +++++++++--------- .../admin/users/AdminUserFormWithData.tsx | 21 +- .../AdminUserSetRandomPasswordContent.tsx | 109 ++++ .../AdminUserSetRandomPasswordRadios.tsx | 92 ++++ .../views/admin/users/AdminUsersPage.tsx | 20 +- .../admin/users/PasswordFieldSkeleton.tsx | 18 + .../admin/users/UsersTable/UsersTable.tsx | 21 +- .../users/UsersTable/UsersTableFilters.tsx | 56 +- .../CategoryFilter/CategoryDropDownList.tsx | 2 +- apps/meteor/tests/e2e/administration.spec.ts | 8 +- .../fragments/admin-flextab-users.ts | 18 +- packages/core-typings/src/IUser.ts | 4 +- packages/i18n/src/locales/en.i18n.json | 31 +- .../src/v1/users/UserCreateParamsPOST.ts | 2 +- .../MultiSelectCustom/MultiSelectCustom.tsx | 38 +- .../MultiSelectCustomAnchor.tsx | 6 +- .../MultiSelectCustomList.tsx | 24 +- .../MultiSelectCustomListWrapper.tsx | 2 +- .../PasswordVerifier/PasswordVerifier.tsx | 11 +- .../PasswordVerifier/PasswordVerifierItem.tsx | 5 +- 23 files changed, 681 insertions(+), 324 deletions(-) create mode 100644 apps/meteor/client/views/admin/users/AdminUserCreated.tsx create mode 100644 apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordContent.tsx create mode 100644 apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx create mode 100644 apps/meteor/client/views/admin/users/PasswordFieldSkeleton.tsx diff --git a/apps/meteor/client/components/UserCard/UserCardInfo.tsx b/apps/meteor/client/components/UserCard/UserCardInfo.tsx index 8e235670a3dc..2afcf6a37f2c 100644 --- a/apps/meteor/client/components/UserCard/UserCardInfo.tsx +++ b/apps/meteor/client/components/UserCard/UserCardInfo.tsx @@ -3,7 +3,7 @@ import type { ReactElement, ComponentProps } from 'react'; import React from 'react'; const UserCardInfo = (props: ComponentProps): ReactElement => ( - + ); export default UserCardInfo; diff --git a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx index d52d45415c8a..2ec5954332f7 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx @@ -94,8 +94,8 @@ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch { + const { t } = useTranslation(); + const router = useRouter(); + + return ( + <> + + + + + + + + + ); +}; + +export default AdminUserCreated; diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 69aeb4e31205..023d3df851e5 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -1,4 +1,4 @@ -import type { AvatarObject, IUser, Serialized } from '@rocket.chat/core-typings'; +import type { AvatarObject, IRole, IUser, Serialized } from '@rocket.chat/core-typings'; import { Field, FieldLabel, @@ -7,20 +7,19 @@ import { FieldHint, TextInput, TextAreaInput, - PasswordInput, MultiSelectFiltered, Box, ToggleSwitch, Icon, - Divider, FieldGroup, ContextualbarFooter, - ButtonGroup, Button, Callout, + Skeleton, } from '@rocket.chat/fuselage'; import type { SelectOption } from '@rocket.chat/fuselage'; import { useUniqueId, useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import type { UserCreateParamsPOST } from '@rocket.chat/rest-typings'; import { CustomFieldsForm } from '@rocket.chat/ui-client'; import { useAccountsCustomFields, @@ -30,8 +29,8 @@ import { useToastMessageDispatch, useTranslation, } from '@rocket.chat/ui-contexts'; -import { useQuery, useMutation } from '@tanstack/react-query'; -import React, { useCallback } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import React, { useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { validateEmail } from '../../../../lib/emailValidator'; @@ -41,24 +40,35 @@ import UserAvatarEditor from '../../../components/avatar/UserAvatarEditor'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; import { useUpdateAvatar } from '../../../hooks/useUpdateAvatar'; import { USER_STATUS_TEXT_MAX_LENGTH, BIO_TEXT_MAX_LENGTH } from '../../../lib/constants'; +import AdminUserSetRandomPasswordContent from './AdminUserSetRandomPasswordContent'; +import AdminUserSetRandomPasswordRadios from './AdminUserSetRandomPasswordRadios'; +import PasswordFieldSkeleton from './PasswordFieldSkeleton'; import { useSmtpQuery } from './hooks/useSmtpQuery'; type AdminUserFormProps = { userData?: Serialized; onReload: () => void; + context: string; + refetchUserFormData?: () => void; + roleData: { roles: IRole[] } | undefined; + roleError: unknown; }; +export type UserFormProps = Omit; + const getInitialValue = ({ data, defaultUserRoles, isSmtpEnabled, - isEditingExistingUser, + isVerificationNeeded, + isNewUserPage, }: { data?: Serialized; defaultUserRoles?: IUser['roles']; isSmtpEnabled?: boolean; - isEditingExistingUser?: boolean; -}) => ({ + isVerificationNeeded?: boolean; + isNewUserPage?: boolean; +}): UserFormProps => ({ roles: data?.roles ?? defaultUserRoles, name: data?.name ?? '', password: '', @@ -66,53 +76,59 @@ const getInitialValue = ({ bio: data?.bio ?? '', nickname: data?.nickname ?? '', email: (data?.emails?.length && data.emails[0].address) || '', - verified: (data?.emails?.length && data.emails[0].verified) || false, - setRandomPassword: false, - requirePasswordChange: data?.requirePasswordChange || false, + verified: isSmtpEnabled && isVerificationNeeded && ((data?.emails?.length && data.emails[0].verified) || false), + setRandomPassword: isNewUserPage && isSmtpEnabled, + requirePasswordChange: isNewUserPage && isSmtpEnabled && (data?.requirePasswordChange ?? true), customFields: data?.customFields ?? {}, statusText: data?.statusText ?? '', - ...(!isEditingExistingUser && { joinDefaultChannels: true }), + ...(isNewUserPage && { joinDefaultChannels: true }), sendWelcomeEmail: isSmtpEnabled, avatar: '' as AvatarObject, + passwordConfirmation: '', }); -const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { +const AdminUserForm = ({ userData, onReload, context, refetchUserFormData, roleData, roleError, ...props }: AdminUserFormProps) => { const t = useTranslation(); const router = useRouter(); const dispatchToastMessage = useToastMessageDispatch(); + const queryClient = useQueryClient(); const customFieldsMetadata = useAccountsCustomFields(); const defaultRoles = useSetting('Accounts_Registration_Users_Default_Roles') || ''; - + const isVerificationNeeded = useSetting('Accounts_EmailVerification'); const defaultUserRoles = parseCSV(defaultRoles); - const { data } = useSmtpQuery(); - const isSmtpEnabled = data?.isSMTPConfigured; - - const eventStats = useEndpointAction('POST', '/v1/statistics.telemetry'); - const updateUserAction = useEndpoint('POST', '/v1/users.update'); - const createUserAction = useEndpoint('POST', '/v1/users.create'); - const getRoles = useEndpoint('GET', '/v1/roles.list'); - const { data: roleData, error: roleError } = useQuery(['roles'], async () => getRoles()); - - const availableRoles: SelectOption[] = roleData?.roles.map(({ _id, name, description }) => [_id, description || name]) || []; - - const goToUser = useCallback((id) => router.navigate(`/admin/users/info/${id}`), [router]); - - const isEditingExistingUser = Boolean(userData?._id); + const { data, isLoading: isLoadingSmtpStatus } = useSmtpQuery(); + const isSmtpEnabled = !!data?.isSMTPConfigured; + const isNewUserPage = context === 'new'; const { control, watch, handleSubmit, - reset, formState: { errors, isDirty }, + setValue, } = useForm({ - defaultValues: getInitialValue({ data: userData, defaultUserRoles, isSmtpEnabled, isEditingExistingUser }), + values: getInitialValue({ + data: userData, + defaultUserRoles, + isSmtpEnabled, + isNewUserPage, + isVerificationNeeded: !!isVerificationNeeded, + }), mode: 'onBlur', }); - const { avatar, username, setRandomPassword } = watch(); + const { avatar, username, setRandomPassword, password } = watch(); + + const eventStats = useEndpointAction('POST', '/v1/statistics.telemetry'); + const updateUserAction = useEndpoint('POST', '/v1/users.update'); + const createUserAction = useEndpoint('POST', '/v1/users.create'); + + const availableRoles: SelectOption[] = useMemo( + () => roleData?.roles.map(({ _id, name, description }) => [_id, description || name]) || [], + [roleData], + ); const updateAvatar = useUpdateAvatar(avatar, userData?._id || ''); @@ -123,6 +139,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { await updateAvatar(); router.navigate(`/admin/users/info/${_id}`); onReload(); + refetchUserFormData?.(); }, onError: (error) => { dispatchToastMessage({ type: 'error', message: error }); @@ -131,12 +148,15 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { const handleCreateUser = useMutation({ mutationFn: createUserAction, - onSuccess: async (data) => { - dispatchToastMessage({ type: 'success', message: t('User_created_successfully!') }); + onSuccess: async ({ user: { _id } }) => { + dispatchToastMessage({ type: 'success', message: t('New_user_manually_created') }); await eventStats({ params: [{ eventName: 'updateCounter', settingsId: 'Manual_Entry_User_Count' }], }); - goToUser(data.user._id); + queryClient.invalidateQueries(['pendingUsersCount'], { + refetchType: 'all', + }); + router.navigate(`/admin/users/created/${_id}`); onReload(); }, onError: (error) => { @@ -144,12 +164,14 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { }, }); - const handleSaveUser = useMutableCallback(async (userFormPayload) => { - const { avatar, ...userFormData } = userFormPayload; - if (userData?._id) { + const handleSaveUser = useMutableCallback(async (userFormPayload: UserFormProps) => { + const { avatar, passwordConfirmation, ...userFormData } = userFormPayload; + + if (!isNewUserPage && userData?._id) { return handleUpdateUser.mutateAsync({ userId: userData?._id, data: userFormData }); } - return handleCreateUser.mutateAsync(userFormData); + + return handleCreateUser.mutateAsync({ ...userFormData, fields: '' }); }); const nameId = useUniqueId(); @@ -160,17 +182,22 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { const bioId = useUniqueId(); const nicknameId = useUniqueId(); const passwordId = useUniqueId(); - const requirePasswordChangeId = useUniqueId(); - const setRandomPasswordId = useUniqueId(); const rolesId = useUniqueId(); const joinDefaultChannelsId = useUniqueId(); const sendWelcomeEmailId = useUniqueId(); + const setRandomPasswordId = useUniqueId(); + + const [showCustomFields, setShowCustomFields] = useState(true); + + if (!context) { + return null; + } return ( <> - {isEditingExistingUser && ( + {!isNewUserPage && ( { /> )} + {isNewUserPage && {t('Manually_created_users_briefing')}} + + {t('Email')} + + (validateEmail(email) ? undefined : t('ensure_email_address_valid')), + }} + render={({ field }) => ( + + )} + /> + + {errors?.email && ( + + {errors.email.message} + + )} + {isLoadingSmtpStatus ? ( + + ) : ( + <> + + + + {t('Mark_email_as_verified')} + + + + ( + + )} + /> + + {isVerificationNeeded && !isSmtpEnabled && ( + + )} + {!isVerificationNeeded && ( + + )} + + )} + {t('Name')} @@ -226,7 +320,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { aria-describedby={`${usernameId}-error`} error={errors.username?.message} flexGrow={1} - addon={} /> )} /> @@ -238,43 +331,108 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { )} - {t('Email')} - - (validateEmail(email) ? undefined : t('error-invalid-email-address')), - }} - render={({ field }) => ( - } + {isLoadingSmtpStatus ? ( + + ) : ( + <> + + {t('Password')} + + + {!setRandomPassword && ( + )} - /> - - {errors?.email && ( - - {errors.email.message} - + )} + {t('Roles')} - {t('Verified')} - } - /> + {roleError && {roleError}} + {!roleError && ( + ( + + )} + /> + )} + {errors?.roles && {errors.roles.message}} + + {isNewUserPage && ( + + + {t('Join_default_channels')} + + ( + + )} + /> + + + + )} + + {isLoadingSmtpStatus ? ( + + ) : ( + <> + + + {t('Send_welcome_email')} + + + ( + + )} + /> + + + {!isSmtpEnabled && ( + + )} + + )} {t('StatusMessage')} @@ -291,7 +449,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { aria-invalid={errors.statusText ? 'true' : 'false'} aria-describedby={`${statusTextId}-error`} flexGrow={1} - addon={} /> )} /> @@ -332,175 +489,34 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { {t('Nickname')} - ( - } /> - )} - /> - - - - - {!setRandomPassword && ( - - {t('Password')} - - ( - } - /> - )} - /> - - {errors?.password && ( - - {errors.password.message} - - )} - - )} - - - {t('Require_password_change')} - ( - - )} - /> - - - - - {t('Set_random_password_and_send_by_email')} - ( - - )} - /> - - {!isSmtpEnabled && ( - - )} - - - {t('Roles')} - - {roleError && {roleError}} - {!roleError && ( - ( - - )} - /> - )} + } /> - {errors?.roles && {errors.roles.message}} - - {!isEditingExistingUser && ( - - - {t('Join_default_channels')} - ( - - )} - /> - - - )} - - - {t('Send_welcome_email')} - ( - - )} - /> - - {!isSmtpEnabled && ( - - )} - {Boolean(customFieldsMetadata.length) && ( + {!!customFieldsMetadata.length && ( <> - - {t('Custom_Fields')} - + + {showCustomFields && } )} - - - - + ); }; -export default UserForm; +export default AdminUserForm; diff --git a/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx b/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx index e595acc46951..db51f1401f32 100644 --- a/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx @@ -1,5 +1,4 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { isUserFederated } from '@rocket.chat/core-typings'; +import type { IRole, IUser } from '@rocket.chat/core-typings'; import { Box, Callout } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; @@ -13,9 +12,12 @@ import AdminUserForm from './AdminUserForm'; type AdminUserFormWithDataProps = { uid: IUser['_id']; onReload: () => void; + context: string; + roleData: { roles: IRole[] } | undefined; + roleError: unknown; }; -const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): ReactElement => { +const AdminUserFormWithData = ({ uid, onReload, context, roleData, roleError }: AdminUserFormWithDataProps): ReactElement => { const t = useTranslation(); const { data, isLoading, isError, refetch } = useUserInfoQuery({ userId: uid }); @@ -40,7 +42,7 @@ const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): R ); } - if (data?.user && isUserFederated(data?.user as unknown as IUser)) { + if (data?.user && !!data.user.federated) { return ( {t('Edit_Federated_User_Not_Allowed')} @@ -48,7 +50,16 @@ const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): R ); } - return ; + return ( + + ); }; export default AdminUserFormWithData; diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordContent.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordContent.tsx new file mode 100644 index 000000000000..814ab8a22776 --- /dev/null +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordContent.tsx @@ -0,0 +1,109 @@ +import { Box, FieldError, FieldLabel, FieldRow, PasswordInput, ToggleSwitch } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { PasswordVerifier } from '@rocket.chat/ui-client'; +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import type { Control, FieldErrors } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; + +import type { UserFormProps } from './AdminUserForm'; + +type AdminUserSetRandomPasswordContentProps = { + control: Control; + setRandomPassword: boolean | undefined; + isNewUserPage: boolean; + passwordId: string; + errors: FieldErrors; + password: string; +}; + +const AdminUserSetRandomPasswordContent = ({ + control, + setRandomPassword, + isNewUserPage, + passwordId, + errors, + password, +}: AdminUserSetRandomPasswordContentProps) => { + const t = useTranslation(); + + const passwordConfirmationId = useUniqueId(); + const requirePasswordChangeId = useUniqueId(); + const passwordVerifierId = useUniqueId(); + + const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation'); + const passwordPlaceholder = String(useSetting('Accounts_PasswordPlaceholder')); + const passwordConfirmationPlaceholder = String(useSetting('Accounts_ConfirmPasswordPlaceholder')); + + return ( + <> + + {t('Require_password_change')} + + ( + + )} + /> + + + + ( + + )} + /> + + {errors?.password && ( + + {errors.password.message} + + )} + {requiresPasswordConfirmation && ( + + (password === val ? true : t('Invalid_confirm_pass')), + }} + render={({ field }) => ( + + )} + /> + + )} + {errors?.passwordConfirmation && ( + + {errors.passwordConfirmation.message} + + )} + + + ); +}; + +export default AdminUserSetRandomPasswordContent; diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx new file mode 100644 index 000000000000..bc773428608a --- /dev/null +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx @@ -0,0 +1,92 @@ +import { Box, FieldHint, FieldLabel, FieldRow, RadioButton } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import type { Control, UseFormSetValue } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; + +import type { UserFormProps } from './AdminUserForm'; + +type AdminUserSetRandomPasswordProps = { + isNewUserPage: boolean | undefined; + control: Control; + isSmtpEnabled: boolean | undefined; + setRandomPasswordId: string; + setValue: UseFormSetValue; +}; + +const AdminUserSetRandomPasswordRadios = ({ + isNewUserPage, + control, + isSmtpEnabled, + setRandomPasswordId, + setValue, +}: AdminUserSetRandomPasswordProps) => { + const t = useTranslation(); + + const setPasswordManuallyId = useUniqueId(); + + const handleSetRandomPasswordChange = (onChange: (...event: any[]) => void, value: boolean) => { + setValue('requirePasswordChange', value); + + onChange(value); + }; + + return ( + <> + + + ( + handleSetRandomPasswordChange(onChange, true)} + disabled={!isSmtpEnabled} + /> + )} + /> + + + {t('Set_randomly_and_send_by_email')} + + + {!isSmtpEnabled && ( + + )} + + + ( + handleSetRandomPasswordChange(onChange, false)} + /> + )} + /> + + + {t('Set_manually')} + + + + ); +}; + +export default AdminUserSetRandomPasswordRadios; diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 4ef44122e303..56641f8959d0 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -1,5 +1,5 @@ import type { LicenseInfo } from '@rocket.chat/core-typings'; -import { Button, ButtonGroup, Callout, ContextualbarIcon, Skeleton, Tabs, TabsItem } from '@rocket.chat/fuselage'; +import { Button, ButtonGroup, Callout, ContextualbarIcon, Icon, Skeleton, Tabs, TabsItem } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import type { OptionProp } from '@rocket.chat/ui-client'; import { ExternalLink } from '@rocket.chat/ui-client'; @@ -23,6 +23,7 @@ import { useLicenseLimitsByBehavior } from '../../../hooks/useLicenseLimitsByBeh import { useShouldPreventAction } from '../../../hooks/useShouldPreventAction'; import { useCheckoutUrl } from '../subscription/hooks/useCheckoutUrl'; import AdminInviteUsers from './AdminInviteUsers'; +import AdminUserCreated from './AdminUserCreated'; import AdminUserForm from './AdminUserForm'; import AdminUserFormWithData from './AdminUserFormWithData'; import AdminUserInfoWithData from './AdminUserInfoWithData'; @@ -61,7 +62,7 @@ const AdminUsersPage = (): ReactElement => { const isCreateUserDisabled = useShouldPreventAction('activeUsers'); const getRoles = useEndpoint('GET', '/v1/roles.list'); - const { data } = useQuery(['roles'], async () => getRoles()); + const { data, error } = useQuery(['roles'], async () => getRoles()); const paginationData = usePagination(); const sortData = useSort('name'); @@ -181,14 +182,23 @@ const AdminUsersPage = (): ReactElement => { {context === 'info' && t('User_Info')} {context === 'edit' && t('Edit_User')} - {context === 'new' && t('Add_User')} + {(context === 'new' || context === 'created') && ( + <> + {t('New_user')} + + )} {context === 'invite' && t('Invite_Users')} router.navigate('/admin/users')} /> {context === 'info' && id && } - {context === 'edit' && id && } - {!isRoutePrevented && context === 'new' && } + {context === 'edit' && id && ( + + )} + {!isRoutePrevented && context === 'new' && ( + + )} + {!isRoutePrevented && context === 'created' && id && } {!isRoutePrevented && context === 'invite' && } {isRoutePrevented && } diff --git a/apps/meteor/client/views/admin/users/PasswordFieldSkeleton.tsx b/apps/meteor/client/views/admin/users/PasswordFieldSkeleton.tsx new file mode 100644 index 000000000000..b27bdabff428 --- /dev/null +++ b/apps/meteor/client/views/admin/users/PasswordFieldSkeleton.tsx @@ -0,0 +1,18 @@ +import { Box, Skeleton } from '@rocket.chat/fuselage'; +import React from 'react'; + +const PasswordFieldSkeleton = () => ( + <> + + + + + + + + + + +); + +export default PasswordFieldSkeleton; diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx index 34d71e6ab371..abdf8cb787c3 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx @@ -1,7 +1,8 @@ import type { IRole, Serialized } from '@rocket.chat/core-typings'; -import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage'; +import { Pagination } from '@rocket.chat/fuselage'; import { useEffectEvent, useBreakpoints } from '@rocket.chat/fuselage-hooks'; import type { PaginatedResult, DefaultUserInfo } from '@rocket.chat/rest-typings'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import type { ReactElement, Dispatch, SetStateAction } from 'react'; @@ -129,16 +130,16 @@ const UsersTable = ({ )} {isError && ( - - - {t('Something_went_wrong')} - - {t('Reload_page')} - - + )} - {isSuccess && data.users.length === 0 && } + {isSuccess && data.users.length === 0 && ( + + )} {isSuccess && !!data?.users && ( <> @@ -163,7 +164,7 @@ const UsersTable = ({ divider current={current} itemsPerPage={itemsPerPage} - count={data?.total || 0} + count={data.total || 0} onSetItemsPerPage={setItemsPerPage} onSetCurrent={setCurrent} {...paginationProps} diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx index e3b919ae4a02..bffbdd2f7a18 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx @@ -1,11 +1,12 @@ import type { IRole } from '@rocket.chat/core-typings'; +import { Box, Icon, Margins, TextInput } from '@rocket.chat/fuselage'; import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; import type { OptionProp } from '@rocket.chat/ui-client'; import { MultiSelectCustom } from '@rocket.chat/ui-client'; +import type { FormEvent } from 'react'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import FilterByText from '../../../../components/FilterByText'; import type { UsersFilters } from '../AdminUsersPage'; type UsersTableFiltersProps = { @@ -20,9 +21,9 @@ const UsersTableFilters = ({ roleData, setUsersFilters }: UsersTableFiltersProps const [text, setText] = useState(''); const handleSearchTextChange = useCallback( - (text) => { - setUsersFilters({ text, roles: selectedRoles }); - setText(text); + ({ target: { value } }) => { + setText(value); + setUsersFilters({ text: value, roles: selectedRoles }); }, [selectedRoles, setUsersFilters], ); @@ -58,20 +59,43 @@ const UsersTableFilters = ({ roleData, setUsersFilters }: UsersTableFiltersProps ); const breakpoints = useBreakpoints(); - const fixFiltersSize = breakpoints.includes('lg') ? { maxWidth: 'x224', minWidth: 'x224' } : null; + const isLargeScreenOrBigger = breakpoints.includes('lg'); + const fixFiltersSize = isLargeScreenOrBigger ? { maxWidth: 'x224', minWidth: 'x224' } : null; return ( - - - + ) => { + event.preventDefault(); + }} + display='flex' + flexWrap='wrap' + alignItems='center' + > + + } + onChange={handleSearchTextChange} + value={text} + flexGrow={2} + minWidth='x220' + aria-label={t('Search_Users')} + /> + + + + + ); }; diff --git a/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownList.tsx b/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownList.tsx index 68025b1a350a..b96ddddb8b7f 100644 --- a/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownList.tsx +++ b/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownList.tsx @@ -6,7 +6,7 @@ import type { CategoryDropDownListProps } from '../../definitions/CategoryDropdo const CategoryDropDownList = ({ categories, onSelected }: CategoryDropDownListProps): ReactElement => { return ( - + {categories.map((category, index) => ( {category.label && ( diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index 45fee011efc3..808c7e719faa 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -75,11 +75,12 @@ test.describe.parallel('administration', () => { test('expect create a user', async () => { await poAdmin.tabs.users.btnNewUser.click(); + await poAdmin.tabs.users.inputEmail.type(faker.internet.email()); await poAdmin.tabs.users.inputName.type(faker.person.firstName()); await poAdmin.tabs.users.inputUserName.type(faker.internet.userName()); - await poAdmin.tabs.users.inputEmail.type(faker.internet.email()); - await poAdmin.tabs.users.checkboxVerified.click(); + await poAdmin.tabs.users.inputSetManually.click(); await poAdmin.tabs.users.inputPassword.type('any_password'); + await poAdmin.tabs.users.inputConfirmPassword.type('any_password'); await expect(poAdmin.tabs.users.userRole).toBeVisible(); await poAdmin.tabs.users.btnSave.click(); }); @@ -97,8 +98,9 @@ test.describe.parallel('administration', () => { await poAdmin.tabs.users.inputName.type(faker.person.firstName()); await poAdmin.tabs.users.inputUserName.type(username); await poAdmin.tabs.users.inputEmail.type(faker.internet.email()); - await poAdmin.tabs.users.checkboxVerified.click(); + await poAdmin.tabs.users.inputSetManually.click(); await poAdmin.tabs.users.inputPassword.type('any_password'); + await poAdmin.tabs.users.inputConfirmPassword.type('any_password'); await expect(poAdmin.tabs.users.userRole).toBeVisible(); await expect(poAdmin.tabs.users.joinDefaultChannels).toBeVisible(); await poAdmin.tabs.users.btnSave.click(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts b/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts index 5b912be1fd02..0a9ccd3547c2 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts @@ -12,7 +12,7 @@ export class AdminFlextabUsers { } get btnSave(): Locator { - return this.page.locator('role=button[name="Save"]'); + return this.page.locator('role=button[name="Add user"]'); } get btnInvite(): Locator { @@ -31,12 +31,20 @@ export class AdminFlextabUsers { return this.page.locator('//label[text()="Email"]/following-sibling::span//input').first(); } + get inputSetManually(): Locator { + return this.page.locator('//label[text()="Set manually"]'); + } + get inputPassword(): Locator { - return this.page.locator('//label[text()="Password"]/following-sibling::span//input'); + return this.page.locator('input[placeholder="Password"]'); + } + + get inputConfirmPassword(): Locator { + return this.page.locator('input[placeholder="Confirm password"]'); } get checkboxVerified(): Locator { - return this.page.locator('//label[text()="Verified"]'); + return this.page.locator('//label[text()="Mark email as verified"]'); } get joinDefaultChannels(): Locator { @@ -55,4 +63,8 @@ export class AdminFlextabUsers { get setupSmtpLink(): Locator { return this.page.locator('role=link[name="Set up SMTP"]'); } + + get btnContextualbarClose(): Locator { + return this.page.locator('button[data-qa="ContextualbarActionClose"]'); + } } diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index 3c6d1c890d7b..fa411c6f7e47 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -152,9 +152,7 @@ export interface IUser extends IRocketChatRecord { private_key: string; public_key: string; }; - customFields?: { - [key: string]: any; - }; + customFields?: Record; settings?: IUserSettings; defaultRoom?: string; ldap?: boolean; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 69cc6c43fa7f..71ba23b70a31 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -121,8 +121,8 @@ "Accounts_Email_Approved_Subject": "Account approved", "Accounts_Email_Deactivated": "[name]

    Your account was deactivated.

    ", "Accounts_Email_Deactivated_Subject": "Account deactivated", - "Accounts_EmailVerification": "Only allow verified users to login", - "Accounts_EmailVerification_Description": "Make sure you have correct SMTP settings to use this feature", + "Accounts_EmailVerification": "Require email verification to login", + "Accounts_EmailVerification_Description": "Ensure SMTP is configured to enable", "Accounts_Enrollment_Email": "Enrollment Email", "Accounts_Enrollment_Email_Default": "

    Welcome to [Site_Name]

    Go to [Site_URL] and try the best open source chat solution available today!

    ", "Accounts_Enrollment_Email_Description": "You may use the following placeholders: \n - `[name]`, `[fname]`, `[lname]` for the user's full name, first name or last name, respectively. \n - `[email]` for the user's email. \n - `[Site_Name]` and `[Site_URL]` for the Application Name and URL respectively. ", @@ -308,6 +308,7 @@ "Activate": "Activate", "Activation": "Activation", "Active": "Active", + "active": "active", "Active_users": "Active users", "Activity": "Activity", "Add": "Add", @@ -319,6 +320,7 @@ "Add_files_from": "Add files from", "Add_manager": "Add manager", "Add_monitor": "Add monitor", + "Add_more_users": "Add more users", "Add_link": "Add link", "Add_Reaction": "Add reaction", "Add_Role": "Add Role", @@ -1578,6 +1580,7 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Limit by User: requests allowed", "Deactivate": "Deactivate", "Deactivated": "Deactivated", + "deactivated": "deactivated", "Decline": "Decline", "default": "default", "Default": "Default", @@ -1892,6 +1895,7 @@ "Email_Inboxes": "Email inboxes", "Email_Inbox_has_been_added": "Email Inbox has been added", "Email_Inbox_has_been_removed": "Email Inbox has been removed", + "Email_is_required": "Email is required", "Email_Notification_Mode": "Offline Email Notifications", "Email_Notification_Mode_All": "Every Mention/DM", "Email_Notification_Mode_Disabled": "Disabled", @@ -1916,6 +1920,7 @@ "Enterprise_Only": "Enterprise only", "Encrypted_field_hint": "Messages are end-to-end encrypted, search will not work and notifications may not show message content", "Email_sent": "Email sent", + "Email_verification_isnt_required": "Email verification to login is not required. To require, enable setting in Accounts > Registration", "Emoji": "Emoji", "Emoji_picker": "Emoji picker", "EmojiCustomFilesystem": "Custom Emoji Filesystem", @@ -1937,6 +1942,7 @@ "Enable_Svg_Favicon": "Enable SVG favicon", "Enable_timestamp": "Enable timestamp parsing in messages", "Enable_timestamp_description": "Enable timestamps to be parsed in messages", + "Enable_to_bypass_email_verification": "Enable to bypass email verification", "Enable_two-factor_authentication": "Enable two-factor authentication via TOTP", "Enable_two-factor_authentication_email": "Enable two-factor authentication via Email", "Enable_unlimited_apps": "Enable unlimited apps", @@ -1969,6 +1975,7 @@ "Engagement_Dashboard": "Engagement dashboard", "Enrich_your_workspace": "Enrich your workspace perspective with the engagement dashboard. Analyze practical usage statistics about your users, messages and channels. Included in Premium plans.", "Ensure_secure_workspace_access": "Ensure secure workspace access", + "ensure_email_address_valid": "Invalid email address", "Enter": "Enter", "Enter_a_custom_message": "Enter a custom message", "Enter_a_department_name": "Enter a department name", @@ -2568,6 +2575,7 @@ "Hi_username": "Hi [name]", "Hidden": "Hidden", "Hide": "Hide", + "Hide_additional_fields": "Hide additional fields", "Hide_counter": "Hide counter", "Hide_flextab": "Hide Contextual Bar by clicking outside of it", "Hide_Group_Warning": "Are you sure you want to hide the group \"%s\"?", @@ -3465,6 +3473,7 @@ "Managing_assets": "Managing assets", "Managing_integrations": "Managing integrations", "Manual_Selection": "Manual Selection", + "Manually_created_users_briefing": "Manually created users will initially be shown as pending. Once they log in for the first time, they will be shown as active.", "Manufacturing": "Manufacturing", "MapView_Enabled": "Enable Mapview", "MapView_Enabled_Description": "Enabling mapview will display a location share button on the right of the chat input field.", @@ -3473,6 +3482,7 @@ "Mark_all_as_read": "`%s` - Mark all messages (in all channels) as read", "Mark_as_read": "Mark as read", "Mark_as_unread": "Mark as unread", + "Mark_email_as_verified": "Mark email as verified", "Mark_read": "Mark Read", "Mark_unread": "Mark Unread", "Marketplace": "Marketplace", @@ -3828,6 +3838,7 @@ "New_Unit": "New Unit", "New_users": "New users", "New_user": "New user", + "New_user_manually_created": "New user manually created", "New_version_available_(s)": "New version available (%s)", "New_videocall_request": "New Video Call Request", "New_visitor_navigation": "New Navigation: {{history}}", @@ -4160,6 +4171,7 @@ "pdf_error_message": "Error generating PDF Transcript", "Peer_Password": "Peer Password", "Pending": "Pending", + "pending": "pending", "Pending_action": "Pending action", "Pending Avatars": "Pending Avatars", "Pending Files": "Pending Files", @@ -4724,6 +4736,7 @@ "Save_Mobile_Bandwidth": "Save Mobile Bandwidth", "Save_to_enable_this_action": "Save to enable this action", "Save_To_Webdav": "Save to WebDAV", + "Save_user": "Save user", "Save_your_encryption_password": "Save your encryption password", "Save_your_encryption_password_to_access": "Save your end-to-end encryption password to access", "save-all-canned-responses": "Save All Canned Responses", @@ -4827,7 +4840,7 @@ "Send_confirmation_email": "Send confirmation email", "Send_data_into_RocketChat_in_realtime": "Send data into Rocket.Chat in real-time.", "Send_email": "Send Email", - "Send_Email_SMTP_Warning": "To send this email you need to setup SMTP emailing server", + "Send_Email_SMTP_Warning": "Set up the SMTP server in email settings to enable.", "Send_invitation_email": "Send invitation email", "Send_invitation_email_error": "You haven't provided any valid email address.", "Send_invitation_email_info": "You can send multiple email invitations at once.", @@ -4887,9 +4900,11 @@ "Set_as_moderator": "Set as moderator", "Set_as_owner": "Set as owner", "Upload_app": "Upload App", + "Set_randomly_and_send_by_email": "Set randomly and send by email", "Set_random_password_and_send_by_email": "Set random password and send by email", "set-leader": "Set Leader", "set-leader_description": "Permission to set other users as leader of a channel", + "Set_manually": "Set manually", "set-moderator": "Set Moderator", "set-moderator_description": "Permission to set other users as moderator of a channel", "set-owner": "Set Owner", @@ -4916,6 +4931,7 @@ "shortcut_name": "shortcut name", "Should_be_a_URL_of_an_image": "Should be a URL of an image.", "Should_exists_a_user_with_this_username": "The user must already exist.", + "Show_additional_fields": "Show additional fields", "Show_agent_email": "Show agent email", "Show_agent_info": "Show agent information", "Show_all": "Show All", @@ -5111,7 +5127,7 @@ "Stats_Total_Uploads_Size": "Total Uploads Size", "Stats_Total_Users": "Total Users", "Status": "Status", - "StatusMessage": "Status Message", + "StatusMessage": "Status message", "StatusMessage_Change_Disabled": "Your Rocket.Chat administrator has disabled the changing of status messages", "StatusMessage_Changed_Successfully": "Status message changed successfully.", "StatusMessage_Placeholder": "What are you doing right now?", @@ -6043,6 +6059,7 @@ "You_have_a_new_message": "You have a new message", "You_have_been_muted": "You have been muted and cannot speak in this room", "You_have_been_removed_from__roomName_": "You've been removed from the room {{roomName}}", + "You_have_created_user": "You’ve created 1 user", "You_have_joined_a_new_call_with": "You have joined a new call with", "You_have_n_codes_remaining": "You have {{number}} codes remaining.", "You_have_not_verified_your_email": "You have not verified your email.", @@ -6292,6 +6309,11 @@ "Send_transcript": "Send transcript", "Undo_request": "Undo request", "No_permission": "No permission", + "Users_Table_Generic_No_users": "No %s users", + "Users_Table_no_all_users_description": "No Users found.", + "Users_Table_no_pending_users_description": "Users who are pending activation or have been manually created but haven't logged in yet appear here.", + "Users_Table_no_active_users_description": "Active users appear here.", + "Users_Table_no_deactivated_users_description": "Deactivated users appear here.", "Community_cap_description": "Community workspaces have a limit of 200 concurrent connections. If this limit is exceeded it will no longer be possible for users to see each others status. This does not affect sending and receiving of messages.", "Premium_cap_description": "Premium plans do not have a presence service limit.", "Service_status": "Service status", @@ -6366,6 +6388,7 @@ "App_will_lose_grandfathered_status": "**This {{context}} app will lose its grandfathered status.** \n \nWorkspaces on Community can have up to {{limit}} {{context}} apps enabled. Grandfathered apps count towards the limit but the limit is not applied to them.", "All_rooms": "All rooms", "All_visible": "All visible", + "all": "all", "Filter_by_room": "Filter by room type", "Filter_by_visibility": "Filter by visibility", "Theme_Appearence": "Theme Appearence", diff --git a/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts b/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts index 347498999011..49fb8b2f6912 100644 --- a/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts +++ b/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts @@ -19,7 +19,7 @@ export type UserCreateParamsPOST = { setRandomPassword?: boolean; sendWelcomeEmail?: boolean; verified?: boolean; - customFields?: object; + customFields?: Record; /* @deprecated */ fields: string; }; diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx index a90cfb1bd1c6..0855e5fc68c5 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx @@ -1,6 +1,5 @@ import { Box, Button } from '@rocket.chat/fuselage'; import { useOutsideClick, useToggle } from '@rocket.chat/fuselage-hooks'; -import type { TranslationKey } from '@rocket.chat/ui-contexts'; import type { ComponentProps, FormEvent, ReactElement, RefObject } from 'react'; import { useCallback, useRef } from 'react'; @@ -33,7 +32,6 @@ export type OptionProp = { @param selectedOptionsTitle dropdown text after clicking one or more options. For example: 'Rooms (3)' * @param selectedOptions array with clicked options. This is used in the useFilteredTypeRooms hook, to filter the Rooms' table, for example. This array joins all of the individual clicked options from all available MultiSelectCustom components in the page. It helps to create a union filter for all the selections. * @param setSelectedOptions part of an useState hook to set the previous selectedOptions - * @param customSetSelected part of an useState hook to set the individual selected checkboxes from this instance. * @param searchBarText optional text prop that creates a search bar inside the dropdown, when added. * @returns a React Component that should be used with a custom hook for filters, such as useFilteredTypeRooms.tsx. * Check out the following files, for examples: @@ -43,11 +41,11 @@ export type OptionProp = { */ type DropDownProps = { dropdownOptions: OptionProp[]; - defaultTitle: TranslationKey; - selectedOptionsTitle: TranslationKey; + defaultTitle: string; + selectedOptionsTitle: string; selectedOptions: OptionProp[]; setSelectedOptions: (roles: OptionProp[]) => void; - searchBarText?: TranslationKey; + searchBarText?: string; } & ComponentProps; export const MultiSelectCustom = ({ @@ -77,20 +75,26 @@ export const MultiSelectCustom = ({ useOutsideClick([target], onClose); - const onSelect = (item: OptionProp, e?: FormEvent): void => { - e?.stopPropagation(); - item.checked = !item.checked; + const onSelect = useCallback( + (selectedOption: OptionProp, e?: FormEvent): void => { + e?.stopPropagation(); - if (item.checked === true) { - setSelectedOptions([...new Set([...selectedOptions, item])]); - return; - } + if (selectedOption.hasOwnProperty('checked')) { + selectedOption.checked = !selectedOption.checked; - // the user has disabled this option -> remove this from the selected options list - setSelectedOptions(selectedOptions.filter((option: OptionProp) => option.id !== item.id)); - }; + if (selectedOption.checked) { + setSelectedOptions([...new Set([...selectedOptions, selectedOption])]); + return; + } - const count = dropdownOptions.filter((option) => option.checked).length; + // the user has disabled this option -> remove this from the selected options list + setSelectedOptions(selectedOptions.filter((option: OptionProp) => option.id !== selectedOption.id)); + } + }, + [selectedOptions, setSelectedOptions], + ); + + const selectedOptionsCount = dropdownOptions.filter((option) => option.hasOwnProperty('checked') && option.checked).length; return ( @@ -101,7 +105,7 @@ export const MultiSelectCustom = ({ onKeyDown={(e) => (e.code === 'Enter' || e.code === 'Space') && toggleCollapsed(!collapsed)} defaultTitle={defaultTitle} selectedOptionsTitle={selectedOptionsTitle} - selectedOptionsCount={count} + selectedOptionsCount={selectedOptionsCount} maxCount={dropdownOptions.length} {...props} /> diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx index 3a03673bc701..acd1e1eb8d6b 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx @@ -7,8 +7,8 @@ import { forwardRef } from 'react'; type MultiSelectCustomAnchorProps = { collapsed: boolean; - defaultTitle: TranslationKey; - selectedOptionsTitle: TranslationKey; + defaultTitle: string; + selectedOptionsTitle: string; selectedOptionsCount: number; maxCount: number; } & ComponentProps; @@ -37,7 +37,7 @@ const MultiSelectCustomAnchor = forwardRef - {isDirty ? `${t(selectedOptionsTitle)} (${selectedOptionsCount})` : t(defaultTitle)} + {isDirty ? `${t(selectedOptionsTitle as TranslationKey)} (${selectedOptionsCount})` : t(defaultTitle as TranslationKey)} ); diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx index 4afe036e74d2..d73d9ce6b88b 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx @@ -4,7 +4,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FormEvent } from 'react'; import { Fragment, useCallback, useState } from 'react'; -import type { OptionProp } from './MultiSelectCustom'; +import { type OptionProp } from './MultiSelectCustom'; import { useFilteredOptions } from './useFilteredOptions'; const MultiSelectCustomList = ({ @@ -14,7 +14,7 @@ const MultiSelectCustomList = ({ }: { options: OptionProp[]; onSelected: (item: OptionProp, e?: FormEvent) => void; - searchBarText?: TranslationKey; + searchBarText?: string; }) => { const t = useTranslation(); @@ -25,33 +25,33 @@ const MultiSelectCustomList = ({ const filteredOptions = useFilteredOptions(text, options); return ( - + {searchBarText && ( - + )} {filteredOptions.map((option) => ( - {option.hasOwnProperty('checked') ? ( + {!option.hasOwnProperty('checked') ? ( + + {t(option.text as TranslationKey)} + + ) : ( - ) : ( - - {t(option.text as TranslationKey)} - )} ))} diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomListWrapper.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomListWrapper.tsx index d8e2379772cc..8477492d2fa9 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomListWrapper.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomListWrapper.tsx @@ -6,7 +6,7 @@ const MultiSelectCustomListWrapper = forwardRef + {children} ); diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx index bf53360a2351..c2ed984512fa 100644 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx @@ -8,6 +8,7 @@ import { PasswordVerifierItem } from './PasswordVerifierItem'; type PasswordVerifierProps = { password: string | undefined; id?: string; + vertical?: boolean; }; type PasswordVerificationProps = { @@ -16,7 +17,7 @@ type PasswordVerificationProps = { limit?: number; }[]; -export const PasswordVerifier = ({ password, id }: PasswordVerifierProps) => { +export const PasswordVerifier = ({ password, id, vertical }: PasswordVerifierProps) => { const { t } = useTranslation(); const uniqueId = useUniqueId(); @@ -37,7 +38,13 @@ export const PasswordVerifier = ({ password, id }: PasswordVerifierProps) => { {passwordVerifications.map(({ isValid, limit, name }) => ( - + ))} diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx index 97499c0eaf73..c622fc74e6c8 100644 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx @@ -20,13 +20,14 @@ const variants: { export const PasswordVerifierItem = ({ text, isValid, + vertical, ...props -}: { text: string; isValid: boolean } & Omit, 'is'>) => { +}: { text: string; isValid: boolean; vertical: boolean } & Omit, 'is'>) => { const { icon, color } = variants[isValid ? 'success' : 'error']; return (