From 96e7ef6b81cf8a957b0e0ef3d1eca0b689c5a488 Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Fri, 30 Oct 2026 13:08:37 +0100 Subject: [PATCH 01/12] refactor(ScheduledMessages): share the channel container lookup Move the channel container lookup used to portal the schedule dialog into the ScheduledMessages utilities so other scheduled message dialogs can reuse it. --- src/components/ScheduledMessages/ScheduleMessageButton.tsx | 4 +--- src/components/ScheduledMessages/utils/channelContainer.ts | 3 +++ src/components/ScheduledMessages/utils/index.ts | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 src/components/ScheduledMessages/utils/channelContainer.ts diff --git a/src/components/ScheduledMessages/ScheduleMessageButton.tsx b/src/components/ScheduledMessages/ScheduleMessageButton.tsx index 5dfe1c4e1..fcba3210a 100644 --- a/src/components/ScheduledMessages/ScheduleMessageButton.tsx +++ b/src/components/ScheduledMessages/ScheduleMessageButton.tsx @@ -7,7 +7,6 @@ import { useTranslationContext, } from '../../context'; import { Button } from '../Button'; -import { CHANNEL_CONTAINER_ID } from '../Channel/constants'; import { ContextMenu, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog'; import { useMessageComposerHasSendableData } from '../MessageComposer/hooks'; import { useMessageComposerController } from '../MessageComposer/hooks/useMessageComposerController'; @@ -22,8 +21,7 @@ import { import { ScheduleMessageDialog as DefaultScheduleMessageDialog } from './ScheduleMessageDialog'; import { ScheduleMessageMenu } from './ScheduleMessageMenu'; import { ScheduleMessageMenuHeader } from './ScheduleMessageMenuHeader'; - -const getChannelContainer = () => document.getElementById(CHANNEL_CONTAINER_ID); +import { getChannelContainer } from './utils'; const getMenuDialogId = (threadId?: string | null) => threadId ? `${SCHEDULE_MESSAGE_MENU_ID}-thread` : SCHEDULE_MESSAGE_MENU_ID; diff --git a/src/components/ScheduledMessages/utils/channelContainer.ts b/src/components/ScheduledMessages/utils/channelContainer.ts new file mode 100644 index 000000000..48dd86097 --- /dev/null +++ b/src/components/ScheduledMessages/utils/channelContainer.ts @@ -0,0 +1,3 @@ +import { CHANNEL_CONTAINER_ID } from '../../Channel/constants'; + +export const getChannelContainer = () => document.getElementById(CHANNEL_CONTAINER_ID); diff --git a/src/components/ScheduledMessages/utils/index.ts b/src/components/ScheduledMessages/utils/index.ts index 88f041ca3..a64168ddb 100644 --- a/src/components/ScheduledMessages/utils/index.ts +++ b/src/components/ScheduledMessages/utils/index.ts @@ -1,3 +1,4 @@ +export * from './channelContainer'; export * from './dateTimeInputValues'; export * from './nativePicker'; export * from './schedulePresets'; From 4903a92ddd5aa988c21060a8e6a61ada184e1475 Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Fri, 30 Oct 2026 13:08:50 +0100 Subject: [PATCH 02/12] feat(ScheduledMessages): add grouping, preview and failure copy utilities Group scheduled messages into a failed group followed by one group per local day, build list previews from stored scheduled messages, and map each failure_reason to its user-facing copy. Add the list, item, banner and group types used by the scheduled message list. --- src/components/ScheduledMessages/constants.ts | 10 +++ src/components/ScheduledMessages/types.ts | 49 ++++++++++- .../scheduledMessageFailureReason.test.ts | 44 ++++++++++ .../__tests__/scheduledMessageGroups.test.ts | 85 +++++++++++++++++++ .../__tests__/scheduledMessageHelpers.test.ts | 34 +++++++- .../ScheduledMessages/utils/index.ts | 2 + .../utils/scheduledMessageFailureReason.ts | 32 +++++++ .../utils/scheduledMessageGroups.ts | 61 +++++++++++++ .../utils/scheduledMessagePreview.ts | 16 +++- 9 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 src/components/ScheduledMessages/utils/__tests__/scheduledMessageFailureReason.test.ts create mode 100644 src/components/ScheduledMessages/utils/__tests__/scheduledMessageGroups.test.ts create mode 100644 src/components/ScheduledMessages/utils/scheduledMessageFailureReason.ts create mode 100644 src/components/ScheduledMessages/utils/scheduledMessageGroups.ts diff --git a/src/components/ScheduledMessages/constants.ts b/src/components/ScheduledMessages/constants.ts index 0823441a7..632da9f2e 100644 --- a/src/components/ScheduledMessages/constants.ts +++ b/src/components/ScheduledMessages/constants.ts @@ -38,3 +38,13 @@ export const ScheduledMessageNotificationType = { export type ScheduledMessageNotificationType = (typeof ScheduledMessageNotificationType)[keyof typeof ScheduledMessageNotificationType]; + +export const ScheduledMessageGroupType = { + Day: 'day', + Failed: 'failed', +} as const; + +export type ScheduledMessageGroupType = + (typeof ScheduledMessageGroupType)[keyof typeof ScheduledMessageGroupType]; + +export const FAILED_SCHEDULED_MESSAGE_GROUP_ID = 'failed'; diff --git a/src/components/ScheduledMessages/types.ts b/src/components/ScheduledMessages/types.ts index 9216dd34d..b8f63b1fd 100644 --- a/src/components/ScheduledMessages/types.ts +++ b/src/components/ScheduledMessages/types.ts @@ -1,7 +1,11 @@ import type { ChangeEventHandler, ReactNode } from 'react'; -import type { LocalAttachment, ScheduledMessageResponse } from 'stream-chat'; +import type { Attachment, ScheduledMessageResponse } from 'stream-chat'; -import type { ScheduleMessageNoticeSeverity, ScheduleTimeError } from './constants'; +import type { + ScheduledMessageGroupType, + ScheduleMessageNoticeSeverity, + ScheduleTimeError, +} from './constants'; export type UseScheduledMessagesParams = { cid?: string; @@ -26,11 +30,29 @@ export type ScheduleTimeBounds = { }; export type ScheduledMessagePreviewSource = { - attachments: LocalAttachment[]; + attachments: Attachment[]; id: string; text: string; }; +export type ScheduledMessageGroup = { + id: string; + scheduledMessages: ScheduledMessageResponse[]; + type: ScheduledMessageGroupType; + day?: Date; +}; + +export type ScheduledMessageGroups = { + groups: ScheduledMessageGroup[]; + isLoaded: boolean; +}; + +export type ScheduledMessagesPagination = { + canLoadMore: boolean; + isLoading: boolean; + loadMore: () => void; +}; + export type ScheduleTimeForm = { bounds: ScheduleTimeBounds; dateValue: string; @@ -72,3 +94,24 @@ export type ScheduleMessageDialogNoticeProps = { children: ReactNode; severity: ScheduleMessageNoticeSeverity; }; + +export type ScheduledMessageListProps = { + cid: string; + parentId?: string; +}; + +export type ScheduledMessageListDialogProps = ScheduledMessageListProps & { + close: () => void; +}; + +export type ScheduledMessageGroupHeaderProps = { + group: ScheduledMessageGroup; +}; + +export type ScheduledMessageItemProps = { + scheduledMessage: ScheduledMessageResponse; +}; + +export type ScheduledMessageStatusBadgeProps = { + scheduledMessage: ScheduledMessageResponse; +}; diff --git a/src/components/ScheduledMessages/utils/__tests__/scheduledMessageFailureReason.test.ts b/src/components/ScheduledMessages/utils/__tests__/scheduledMessageFailureReason.test.ts new file mode 100644 index 000000000..e9c7ae63a --- /dev/null +++ b/src/components/ScheduledMessages/utils/__tests__/scheduledMessageFailureReason.test.ts @@ -0,0 +1,44 @@ +import type { TFunction } from 'i18next'; +import { ScheduledMessageFailureReason } from 'stream-chat'; + +import { translateScheduledMessageFailureReason } from '../scheduledMessageFailureReason'; + +const t = ((key: string) => key) as TFunction; + +describe('translateScheduledMessageFailureReason', () => { + it.each([ + [ScheduledMessageFailureReason.Banned, "You can't send messages in this channel"], + [ScheduledMessageFailureReason.NoPermission, "You're no longer allowed to post here"], + [ScheduledMessageFailureReason.ChannelFrozen, 'This channel is frozen'], + [ScheduledMessageFailureReason.ChannelDeleted, 'This channel was deleted'], + [ScheduledMessageFailureReason.ModerationBlocked, 'Blocked by moderation'], + [ScheduledMessageFailureReason.HookRejected, 'Rejected by the app'], + [ScheduledMessageFailureReason.Expired, 'Not sent: the service was delayed'], + [ + ScheduledMessageFailureReason.InvalidMessage, + "The message no longer meets the app's limits. Edit it", + ], + [ + ScheduledMessageFailureReason.PendingNotSupported, + 'This channel now holds messages for approval. Schedule it again', + ], + [ScheduledMessageFailureReason.InternalError, 'Something went wrong. Try again'], + ])('translates %s', (failureReason, expected) => { + expect(translateScheduledMessageFailureReason(t, failureReason)).toBe(expected); + }); + + it('shows no copy when the user was deleted', () => { + expect( + translateScheduledMessageFailureReason( + t, + ScheduledMessageFailureReason.UserDeleted, + ), + ).toBeUndefined(); + }); + + it('falls back to the generic copy without a reason', () => { + expect(translateScheduledMessageFailureReason(t)).toBe( + 'Something went wrong. Try again', + ); + }); +}); diff --git a/src/components/ScheduledMessages/utils/__tests__/scheduledMessageGroups.test.ts b/src/components/ScheduledMessages/utils/__tests__/scheduledMessageGroups.test.ts new file mode 100644 index 000000000..2a27d0e4a --- /dev/null +++ b/src/components/ScheduledMessages/utils/__tests__/scheduledMessageGroups.test.ts @@ -0,0 +1,85 @@ +import { ScheduledMessageStatus } from 'stream-chat'; + +import { ScheduledMessageGroupType } from '../../constants'; +import { + groupScheduledMessages, + isFailedScheduledMessage, +} from '../scheduledMessageGroups'; +import { generateScheduledMessage } from '../../../../mock-builders'; + +const atLocalTime = (day: number, hour: number) => + new Date(2026, 9, day, hour, 0).toISOString(); + +const scheduledFor = ( + id: string, + scheduledAt: string, + status: ScheduledMessageStatus = ScheduledMessageStatus.Scheduled, +) => generateScheduledMessage({ id, scheduled_at: scheduledAt, status }); + +describe('isFailedScheduledMessage', () => { + it.each([ + [ScheduledMessageStatus.Failed, true], + [ScheduledMessageStatus.Scheduled, false], + [ScheduledMessageStatus.Sending, false], + ])('treats the %s status as failed: %s', (status, expected) => { + expect(isFailedScheduledMessage(scheduledFor('id', atLocalTime(29, 9), status))).toBe( + expected, + ); + }); +}); + +describe('groupScheduledMessages', () => { + it('returns no groups for an empty list', () => { + expect(groupScheduledMessages([])).toEqual([]); + }); + + it('puts failed messages in one group before the upcoming days', () => { + const failed = scheduledFor( + 'failed', + atLocalTime(30, 9), + ScheduledMessageStatus.Failed, + ); + const upcoming = scheduledFor('upcoming', atLocalTime(29, 9)); + + const groups = groupScheduledMessages([upcoming, failed]); + + expect(groups.map(({ id, type }) => [id, type])).toEqual([ + ['failed', ScheduledMessageGroupType.Failed], + ['2026-10-29', ScheduledMessageGroupType.Day], + ]); + expect(groups[0].scheduledMessages).toEqual([failed]); + expect(groups[0].day).toBeUndefined(); + }); + + it('groups upcoming messages by local day and keeps their order', () => { + const morning = scheduledFor('morning', atLocalTime(29, 9)); + const afternoon = scheduledFor('afternoon', atLocalTime(29, 13)); + const nextDay = scheduledFor('next-day', atLocalTime(30, 9)); + + const groups = groupScheduledMessages([morning, afternoon, nextDay]); + + expect(groups.map(({ id }) => id)).toEqual(['2026-10-29', '2026-10-30']); + expect(groups[0].scheduledMessages).toEqual([morning, afternoon]); + expect(groups[1].scheduledMessages).toEqual([nextDay]); + expect(groups[0].day).toEqual(new Date(morning.scheduled_at)); + }); + + it('keeps sending messages in their day group', () => { + const sending = scheduledFor( + 'sending', + atLocalTime(29, 9), + ScheduledMessageStatus.Sending, + ); + + const [group] = groupScheduledMessages([sending]); + + expect(group.type).toBe(ScheduledMessageGroupType.Day); + expect(group.scheduledMessages).toEqual([sending]); + }); + + it('leaves out the failed group when nothing failed', () => { + const groups = groupScheduledMessages([scheduledFor('upcoming', atLocalTime(29, 9))]); + + expect(groups.map(({ type }) => type)).toEqual([ScheduledMessageGroupType.Day]); + }); +}); diff --git a/src/components/ScheduledMessages/utils/__tests__/scheduledMessageHelpers.test.ts b/src/components/ScheduledMessages/utils/__tests__/scheduledMessageHelpers.test.ts index 4234cddd1..16dbecd66 100644 --- a/src/components/ScheduledMessages/utils/__tests__/scheduledMessageHelpers.test.ts +++ b/src/components/ScheduledMessages/utils/__tests__/scheduledMessageHelpers.test.ts @@ -3,7 +3,11 @@ import { ErrorFromResponse, ScheduledMessageErrorReason } from 'stream-chat'; import type { LocalAttachment } from 'stream-chat'; import { isScheduledMessageError } from '../scheduledMessageErrors'; -import { toScheduledMessagePreview } from '../scheduledMessagePreview'; +import { + toScheduledMessagePreview, + toStoredScheduledMessagePreview, +} from '../scheduledMessagePreview'; +import { generateScheduledMessage } from '../../../../mock-builders'; const createApiError = (code: number) => new ErrorFromResponse('request failed', { @@ -53,3 +57,31 @@ describe('toScheduledMessagePreview', () => { }); }); }); + +describe('toStoredScheduledMessagePreview', () => { + it('builds a pending local message dated at the scheduled time', () => { + const scheduledAt = new Date(2026, 9, 29, 9, 0); + const attachments = [{ type: 'image' }]; + const scheduledMessage = generateScheduledMessage({ + id: 'scheduled-id', + message: { attachments, text: 'Launch moved to Monday' }, + scheduled_at: scheduledAt.toISOString(), + }); + + const previewMessage = toStoredScheduledMessagePreview(scheduledMessage); + + expect(previewMessage).toMatchObject({ + attachments, + created_at: scheduledAt, + id: 'scheduled-id', + status: 'pending', + text: 'Launch moved to Monday', + }); + }); + + it('uses an empty attachment list when the message has none', () => { + const scheduledMessage = generateScheduledMessage({ message: { text: 'Hi' } }); + + expect(toStoredScheduledMessagePreview(scheduledMessage).attachments).toEqual([]); + }); +}); diff --git a/src/components/ScheduledMessages/utils/index.ts b/src/components/ScheduledMessages/utils/index.ts index a64168ddb..60ca2de55 100644 --- a/src/components/ScheduledMessages/utils/index.ts +++ b/src/components/ScheduledMessages/utils/index.ts @@ -3,6 +3,8 @@ export * from './dateTimeInputValues'; export * from './nativePicker'; export * from './schedulePresets'; export * from './scheduledMessageErrors'; +export * from './scheduledMessageFailureReason'; +export * from './scheduledMessageGroups'; export * from './scheduledMessagePreview'; export * from './scheduleTimeBounds'; export * from './timeZone'; diff --git a/src/components/ScheduledMessages/utils/scheduledMessageFailureReason.ts b/src/components/ScheduledMessages/utils/scheduledMessageFailureReason.ts new file mode 100644 index 000000000..d03e5b371 --- /dev/null +++ b/src/components/ScheduledMessages/utils/scheduledMessageFailureReason.ts @@ -0,0 +1,32 @@ +import type { TFunction } from 'i18next'; +import { ScheduledMessageFailureReason } from 'stream-chat'; + +export const translateScheduledMessageFailureReason = ( + t: TFunction, + failureReason?: ScheduledMessageFailureReason, +) => { + switch (failureReason) { + case ScheduledMessageFailureReason.Banned: + return t("You can't send messages in this channel"); + case ScheduledMessageFailureReason.NoPermission: + return t("You're no longer allowed to post here"); + case ScheduledMessageFailureReason.ChannelFrozen: + return t('This channel is frozen'); + case ScheduledMessageFailureReason.ChannelDeleted: + return t('This channel was deleted'); + case ScheduledMessageFailureReason.UserDeleted: + return undefined; + case ScheduledMessageFailureReason.ModerationBlocked: + return t('Blocked by moderation'); + case ScheduledMessageFailureReason.HookRejected: + return t('Rejected by the app'); + case ScheduledMessageFailureReason.Expired: + return t('Not sent: the service was delayed'); + case ScheduledMessageFailureReason.InvalidMessage: + return t("The message no longer meets the app's limits. Edit it"); + case ScheduledMessageFailureReason.PendingNotSupported: + return t('This channel now holds messages for approval. Schedule it again'); + default: + return t('Something went wrong. Try again'); + } +}; diff --git a/src/components/ScheduledMessages/utils/scheduledMessageGroups.ts b/src/components/ScheduledMessages/utils/scheduledMessageGroups.ts new file mode 100644 index 000000000..531fdd3af --- /dev/null +++ b/src/components/ScheduledMessages/utils/scheduledMessageGroups.ts @@ -0,0 +1,61 @@ +import { ScheduledMessageStatus } from 'stream-chat'; +import type { ScheduledMessageResponse } from 'stream-chat'; + +import { + FAILED_SCHEDULED_MESSAGE_GROUP_ID, + ScheduledMessageGroupType, +} from '../constants'; +import type { ScheduledMessageGroup } from '../types'; +import { toDateInputValue } from './dateTimeInputValues'; + +export const isFailedScheduledMessage = ({ status }: ScheduledMessageResponse) => + status === ScheduledMessageStatus.Failed; + +const createFailedGroup = ( + scheduledMessages: ScheduledMessageResponse[], +): ScheduledMessageGroup => ({ + id: FAILED_SCHEDULED_MESSAGE_GROUP_ID, + scheduledMessages, + type: ScheduledMessageGroupType.Failed, +}); + +const createDayGroup = (id: string, day: Date): ScheduledMessageGroup => ({ + day, + id, + scheduledMessages: [], + type: ScheduledMessageGroupType.Day, +}); + +export const groupScheduledMessagesByDay = ( + scheduledMessages: ScheduledMessageResponse[], +) => { + const dayGroups = new Map(); + + scheduledMessages.forEach((scheduledMessage) => { + const day = new Date(scheduledMessage.scheduled_at); + const dayId = toDateInputValue(day); + + const dayGroup = dayGroups.get(dayId) ?? createDayGroup(dayId, day); + + dayGroup.scheduledMessages.push(scheduledMessage); + dayGroups.set(dayId, dayGroup); + }); + + const groupsByDay = Array.from(dayGroups.values()); + + return groupsByDay; +}; + +export const groupScheduledMessages = (scheduledMessages: ScheduledMessageResponse[]) => { + const failedMessages = scheduledMessages.filter(isFailedScheduledMessage); + const upcomingMessages = scheduledMessages.filter( + (scheduledMessage) => !isFailedScheduledMessage(scheduledMessage), + ); + + const failedGroups = failedMessages.length ? [createFailedGroup(failedMessages)] : []; + const dayGroups = groupScheduledMessagesByDay(upcomingMessages); + + const groups = [...failedGroups, ...dayGroups]; + + return groups; +}; diff --git a/src/components/ScheduledMessages/utils/scheduledMessagePreview.ts b/src/components/ScheduledMessages/utils/scheduledMessagePreview.ts index 09fc3c900..9c87153d1 100644 --- a/src/components/ScheduledMessages/utils/scheduledMessagePreview.ts +++ b/src/components/ScheduledMessages/utils/scheduledMessagePreview.ts @@ -1,4 +1,4 @@ -import type { LocalMessageBase } from 'stream-chat'; +import type { LocalMessageBase, ScheduledMessageResponse } from 'stream-chat'; import type { ScheduledMessagePreviewSource } from '../types'; @@ -20,3 +20,17 @@ export const toScheduledMessagePreview = ( return previewMessage; }; + +export const toStoredScheduledMessagePreview = ( + scheduledMessage: ScheduledMessageResponse, +): LocalMessageBase => { + const { id, message, scheduled_at } = scheduledMessage; + const { attachments = [], text } = message; + + const previewMessage = toScheduledMessagePreview( + { attachments, id, text }, + new Date(scheduled_at), + ); + + return previewMessage; +}; From 04fd81c5d2161b327f4d3c19c1463f9077e9016e Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Fri, 30 Oct 2026 13:08:55 +0100 Subject: [PATCH 03/12] feat(ScheduledMessages): add hooks for the scheduled message list useScheduledMessageGroups groups the scoped scheduled messages for a channel or thread, and useScheduledMessagesPagination exposes whether another page exists and loads it through the manager. --- .../scheduledMessageListHooks.test.tsx | 170 ++++++++++++++++++ .../ScheduledMessages/hooks/index.ts | 2 + .../hooks/useScheduledMessageGroups.ts | 21 +++ .../hooks/useScheduledMessagesPagination.ts | 35 ++++ 4 files changed, 228 insertions(+) create mode 100644 src/components/ScheduledMessages/hooks/__tests__/scheduledMessageListHooks.test.tsx create mode 100644 src/components/ScheduledMessages/hooks/useScheduledMessageGroups.ts create mode 100644 src/components/ScheduledMessages/hooks/useScheduledMessagesPagination.ts diff --git a/src/components/ScheduledMessages/hooks/__tests__/scheduledMessageListHooks.test.tsx b/src/components/ScheduledMessages/hooks/__tests__/scheduledMessageListHooks.test.tsx new file mode 100644 index 000000000..24077864a --- /dev/null +++ b/src/components/ScheduledMessages/hooks/__tests__/scheduledMessageListHooks.test.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import type { PropsWithChildren } from 'react'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import { ScheduledMessageEventType, ScheduledMessageStatus } from 'stream-chat'; +import type { StreamChat } from 'stream-chat'; + +import { useScheduledMessageGroups } from '../useScheduledMessageGroups'; +import { useScheduledMessagesPagination } from '../useScheduledMessagesPagination'; +import { ChatProvider } from '../../../../context'; +import { + dispatchScheduledMessageEvent, + generateScheduledMessage, + getTestClientWithUser, + mockChatContext, +} from '../../../../mock-builders'; + +const cid = 'messaging:general'; +const parentId = 'parent-1'; + +const createWrapper = (client: StreamChat) => { + const ChatWrapper = ({ children }: PropsWithChildren) => ( + {children} + ); + + return ChatWrapper; +}; + +const atLocalTime = (day: number, hour: number) => + new Date(2026, 9, day, hour, 0).toISOString(); + +describe('useScheduledMessageGroups', () => { + it('groups the channel messages with failed ones first', async () => { + const client = await getTestClientWithUser({ id: 'user-id' }); + const failed = generateScheduledMessage({ + cid, + scheduled_at: atLocalTime(30, 9), + status: ScheduledMessageStatus.Failed, + }); + const upcoming = generateScheduledMessage({ cid, scheduled_at: atLocalTime(29, 9) }); + const threadReply = generateScheduledMessage({ + cid, + message: { parent_id: parentId, text: 'reply' }, + }); + [failed, upcoming, threadReply].forEach(client.scheduledMessages.upsertToState); + + const { result } = renderHook(() => useScheduledMessageGroups({ cid }), { + wrapper: createWrapper(client), + }); + + expect( + result.current.groups.map(({ scheduledMessages }) => scheduledMessages), + ).toEqual([[failed], [upcoming]]); + }); + + it('groups only the replies of a thread', async () => { + const client = await getTestClientWithUser({ id: 'user-id' }); + const threadReply = generateScheduledMessage({ + cid, + message: { parent_id: parentId, text: 'reply' }, + }); + [generateScheduledMessage({ cid }), threadReply].forEach( + client.scheduledMessages.upsertToState, + ); + + const { result } = renderHook(() => useScheduledMessageGroups({ cid, parentId }), { + wrapper: createWrapper(client), + }); + + expect(result.current.groups).toHaveLength(1); + expect(result.current.groups[0].scheduledMessages).toEqual([threadReply]); + }); + + it('regroups when a scheduled message event arrives', async () => { + const client = await getTestClientWithUser({ id: 'user-id' }); + client.scheduledMessages.registerSubscriptions(); + const scheduledMessage = generateScheduledMessage({ cid }); + + const { result } = renderHook(() => useScheduledMessageGroups({ cid }), { + wrapper: createWrapper(client), + }); + + expect(result.current.groups).toEqual([]); + + act(() => + dispatchScheduledMessageEvent({ + client, + scheduledMessage, + type: ScheduledMessageEventType.Created, + }), + ); + + await waitFor(() => expect(result.current.groups).toHaveLength(1)); + + client.scheduledMessages.unregisterSubscriptions(); + }); + + it('keeps the same groups when another channel changes', async () => { + const client = await getTestClientWithUser({ id: 'user-id' }); + client.scheduledMessages.upsertToState(generateScheduledMessage({ cid })); + + const { result } = renderHook(() => useScheduledMessageGroups({ cid }), { + wrapper: createWrapper(client), + }); + const initialGroups = result.current.groups; + + act(() => + client.scheduledMessages.upsertToState( + generateScheduledMessage({ cid: 'messaging:random' }), + ), + ); + + expect(result.current.groups).toBe(initialGroups); + }); +}); + +describe('useScheduledMessagesPagination', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('can load more only while the store has a next cursor', async () => { + const client = await getTestClientWithUser({ id: 'user-id' }); + + const { result } = renderHook(() => useScheduledMessagesPagination(), { + wrapper: createWrapper(client), + }); + + expect(result.current.canLoadMore).toBe(false); + + act(() => client.scheduledMessages.state.partialNext({ next: 'cursor' })); + + expect(result.current.canLoadMore).toBe(true); + }); + + it('loads the next page through the manager', async () => { + const client = await getTestClientWithUser({ id: 'user-id' }); + const loadMore = vi + .spyOn(client.scheduledMessages, 'loadMore') + .mockResolvedValue(undefined); + + const { result } = renderHook(() => useScheduledMessagesPagination(), { + wrapper: createWrapper(client), + }); + + act(() => result.current.loadMore()); + + expect(loadMore).toHaveBeenCalledTimes(1); + }); + + it('logs a failed page load instead of throwing', async () => { + const client = await getTestClientWithUser({ id: 'user-id' }); + const error = new Error('Network Error'); + vi.spyOn(client.scheduledMessages, 'loadMore').mockRejectedValue(error); + const logger = vi.spyOn(client, 'logger').mockImplementation(() => undefined); + + const { result } = renderHook(() => useScheduledMessagesPagination(), { + wrapper: createWrapper(client), + }); + + act(() => result.current.loadMore()); + + await waitFor(() => + expect(logger).toHaveBeenCalledWith( + 'warn', + 'scheduledMessages: load more failed', + expect.objectContaining({ error }), + ), + ); + }); +}); diff --git a/src/components/ScheduledMessages/hooks/index.ts b/src/components/ScheduledMessages/hooks/index.ts index f04801deb..318ec7845 100644 --- a/src/components/ScheduledMessages/hooks/index.ts +++ b/src/components/ScheduledMessages/hooks/index.ts @@ -1,8 +1,10 @@ export * from './useCanScheduleMessage'; export * from './useIsConnectionOnline'; export * from './useScheduledMessageCount'; +export * from './useScheduledMessageGroups'; export * from './useScheduledMessagePreview'; export * from './useScheduledMessages'; +export * from './useScheduledMessagesPagination'; export * from './useScheduleMessage'; export * from './useScheduleMessageDialog'; export * from './useScheduleMessageSubmit'; diff --git a/src/components/ScheduledMessages/hooks/useScheduledMessageGroups.ts b/src/components/ScheduledMessages/hooks/useScheduledMessageGroups.ts new file mode 100644 index 000000000..0ad41a035 --- /dev/null +++ b/src/components/ScheduledMessages/hooks/useScheduledMessageGroups.ts @@ -0,0 +1,21 @@ +import { useMemo } from 'react'; + +import type { ScheduledMessageGroups, UseScheduledMessagesParams } from '../types'; +import { groupScheduledMessages } from '../utils'; +import { useScheduledMessages } from './useScheduledMessages'; + +export const useScheduledMessageGroups = ({ + cid, + parentId, +}: UseScheduledMessagesParams) => { + const { isLoaded, scheduledMessages } = useScheduledMessages({ cid, parentId }); + + const groups = useMemo( + () => groupScheduledMessages(scheduledMessages), + [scheduledMessages], + ); + + const scheduledMessageGroups: ScheduledMessageGroups = { groups, isLoaded }; + + return scheduledMessageGroups; +}; diff --git a/src/components/ScheduledMessages/hooks/useScheduledMessagesPagination.ts b/src/components/ScheduledMessages/hooks/useScheduledMessagesPagination.ts new file mode 100644 index 000000000..d76dda4d9 --- /dev/null +++ b/src/components/ScheduledMessages/hooks/useScheduledMessagesPagination.ts @@ -0,0 +1,35 @@ +import { useCallback } from 'react'; +import type { ScheduledMessageManagerState } from 'stream-chat'; + +import { useChatContext } from '../../../context'; +import { useStateStore } from '../../../store'; +import type { ScheduledMessagesPagination } from '../types'; + +const paginationSelector = ({ isLoading, next }: ScheduledMessageManagerState) => ({ + canLoadMore: !!next, + isLoading, +}); + +export const useScheduledMessagesPagination = () => { + const { client } = useChatContext('useScheduledMessagesPagination'); + const { scheduledMessages: manager } = client; + + const { canLoadMore, isLoading } = useStateStore(manager.state, paginationSelector); + + const loadMore = useCallback(() => { + manager.loadMore().catch((error: Error) => + client.logger('warn', 'scheduledMessages: load more failed', { + error, + tags: ['scheduled_messages'], + }), + ); + }, [client, manager]); + + const scheduledMessagesPagination: ScheduledMessagesPagination = { + canLoadMore, + isLoading, + loadMore, + }; + + return scheduledMessagesPagination; +}; From 859175f8d4665df04869d99a7878dec9e9bf8334 Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Fri, 30 Oct 2026 13:09:11 +0100 Subject: [PATCH 04/12] feat(ScheduledMessages): show scheduled messages above the composer Render a banner with the number of scheduled messages (or scheduled thread replies) above the composer. View opens a list with failed messages first and the rest grouped by day; each row shows the preview, scheduled time, status and failure reason. Customise it through the ScheduledMessagesBanner and ScheduledMessageItem component slots. --- .../MessageComposer/MessageComposerUI.tsx | 3 + .../ScheduledMessageGroupHeader.tsx | 20 ++ .../ScheduledMessageItem.tsx | 58 +++++ .../ScheduledMessageList.tsx | 57 +++++ .../ScheduledMessageListDialog.tsx | 28 +++ .../ScheduledMessageStatusBadge.tsx | 40 ++++ .../ScheduledMessagesBanner.tsx | 81 +++++++ .../ScheduledMessagesEmptyList.tsx | 20 ++ .../__tests__/ScheduledMessageList.test.tsx | 213 +++++++++++++++++ .../ScheduledMessagesBanner.test.tsx | 221 ++++++++++++++++++ src/components/ScheduledMessages/index.ts | 8 + .../styling/ScheduledMessageList.scss | 96 ++++++++ .../styling/ScheduledMessagesBanner.scss | 31 +++ .../ScheduledMessages/styling/index.scss | 2 + src/context/ComponentContext.tsx | 3 + 15 files changed, 881 insertions(+) create mode 100644 src/components/ScheduledMessages/ScheduledMessageGroupHeader.tsx create mode 100644 src/components/ScheduledMessages/ScheduledMessageItem.tsx create mode 100644 src/components/ScheduledMessages/ScheduledMessageList.tsx create mode 100644 src/components/ScheduledMessages/ScheduledMessageListDialog.tsx create mode 100644 src/components/ScheduledMessages/ScheduledMessageStatusBadge.tsx create mode 100644 src/components/ScheduledMessages/ScheduledMessagesBanner.tsx create mode 100644 src/components/ScheduledMessages/ScheduledMessagesEmptyList.tsx create mode 100644 src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx create mode 100644 src/components/ScheduledMessages/__tests__/ScheduledMessagesBanner.test.tsx create mode 100644 src/components/ScheduledMessages/styling/ScheduledMessageList.scss create mode 100644 src/components/ScheduledMessages/styling/ScheduledMessagesBanner.scss diff --git a/src/components/MessageComposer/MessageComposerUI.tsx b/src/components/MessageComposer/MessageComposerUI.tsx index f70a90906..ecb03aad8 100644 --- a/src/components/MessageComposer/MessageComposerUI.tsx +++ b/src/components/MessageComposer/MessageComposerUI.tsx @@ -36,6 +36,7 @@ import { } from 'stream-chat'; import { CommandChip as DefaultCommandChip } from './CommandChip'; import { GeolocationPreview } from './AttachmentPreviewList/GeolocationPreview'; +import { ScheduledMessagesBanner as DefaultScheduledMessagesBanner } from '../ScheduledMessages/ScheduledMessagesBanner'; const messageComposerStateSelector = ({ editedMessage, @@ -151,6 +152,7 @@ export const MessageComposerUI = () => { AttachmentSelector = message ? SimpleAttachmentSelector : DefaultAttachmentSelector, AudioRecorder = DefaultAudioRecorder, CommandChip = DefaultCommandChip, + ScheduledMessagesBanner = DefaultScheduledMessagesBanner, SendToChannelCheckbox = DefaultSendToChannelCheckbox, TextareaComposer = DefaultTextareaComposer, } = useComponentContext(); @@ -160,6 +162,7 @@ export const MessageComposerUI = () => { className='str-chat__message-composer-container' component='div' > + {!message && ScheduledMessagesBanner && }
{ + const { t } = useTranslationContext(); + + const { day, type } = group; + + const title = + type === ScheduledMessageGroupType.Failed + ? t('Failed') + : t('timestamp/ScheduledMessageGroup', { timestamp: day }); + + return

{title}

; +}; diff --git a/src/components/ScheduledMessages/ScheduledMessageItem.tsx b/src/components/ScheduledMessages/ScheduledMessageItem.tsx new file mode 100644 index 000000000..e18b61872 --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageItem.tsx @@ -0,0 +1,58 @@ +import clsx from 'clsx'; +import React, { useMemo } from 'react'; + +import { useTranslationContext } from '../../context'; +import { ListItemLayout } from '../ListItemLayout'; +import { SummarizedMessagePreview } from '../SummarizedMessagePreview'; +import { ScheduledMessageStatusBadge } from './ScheduledMessageStatusBadge'; +import type { ScheduledMessageItemProps } from './types'; +import { + isFailedScheduledMessage, + toStoredScheduledMessagePreview, + translateScheduledMessageFailureReason, +} from './utils'; + +export const ScheduledMessageItem = ({ scheduledMessage }: ScheduledMessageItemProps) => { + const { t } = useTranslationContext(); + + const { failure_reason, scheduled_at } = scheduledMessage; + + const previewMessage = useMemo( + () => toStoredScheduledMessagePreview(scheduledMessage), + [scheduledMessage], + ); + + const StatusBadge = useMemo( + () => + function ScheduledMessageItemStatusBadge() { + return ; + }, + [scheduledMessage], + ); + + const isFailed = isFailedScheduledMessage(scheduledMessage); + const failureMessage = isFailed + ? translateScheduledMessageFailureReason(t, failure_reason) + : undefined; + const scheduledTime = t('timestamp/ScheduledMessage', { + timestamp: new Date(scheduled_at), + }); + + return ( +
+ {scheduledTime}} + subtitleClassName='str-chat__scheduled-message-item__time' + title={} + TrailingSlot={StatusBadge} + /> +
+ ); +}; diff --git a/src/components/ScheduledMessages/ScheduledMessageList.tsx b/src/components/ScheduledMessages/ScheduledMessageList.tsx new file mode 100644 index 000000000..bb53c8202 --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageList.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { useComponentContext, useTranslationContext } from '../../context'; +import { Button } from '../Button'; +import { LoadingIndicator } from '../Loading'; +import { useScheduledMessageGroups, useScheduledMessagesPagination } from './hooks'; +import { ScheduledMessageGroupHeader } from './ScheduledMessageGroupHeader'; +import { ScheduledMessageItem as DefaultScheduledMessageItem } from './ScheduledMessageItem'; +import { ScheduledMessagesEmptyList } from './ScheduledMessagesEmptyList'; +import type { ScheduledMessageListProps } from './types'; + +export const ScheduledMessageList = ({ cid, parentId }: ScheduledMessageListProps) => { + const { t } = useTranslationContext(); + const { ScheduledMessageItem = DefaultScheduledMessageItem } = useComponentContext(); + + const { groups } = useScheduledMessageGroups({ cid, parentId }); + const { canLoadMore, isLoading, loadMore } = useScheduledMessagesPagination(); + + if (!groups.length) { + return isLoading ? : ; + } + + return ( +
+ {groups.map((group) => ( +
+ +
    + {group.scheduledMessages.map((scheduledMessage) => ( +
  • + +
  • + ))} +
+
+ ))} + {canLoadMore && ( + + )} +
+ ); +}; diff --git a/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx b/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx new file mode 100644 index 000000000..ad94d6b2d --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import { useTranslationContext } from '../../context'; +import { Prompt } from '../Dialog'; +import { ScheduledMessageList } from './ScheduledMessageList'; +import type { ScheduledMessageListDialogProps } from './types'; + +export const ScheduledMessageListDialog = ({ + cid, + close, + parentId, +}: ScheduledMessageListDialogProps) => { + const { t } = useTranslationContext(); + + const title = parentId ? t('Scheduled replies') : t('Scheduled messages'); + + return ( + + + + + + + ); +}; diff --git a/src/components/ScheduledMessages/ScheduledMessageStatusBadge.tsx b/src/components/ScheduledMessages/ScheduledMessageStatusBadge.tsx new file mode 100644 index 000000000..90f4bd245 --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageStatusBadge.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import type { TFunction } from 'i18next'; +import { ScheduledMessageStatus } from 'stream-chat'; + +import { useTranslationContext } from '../../context'; +import { Badge } from '../Badge'; +import type { BadgeVariant } from '../Badge'; +import type { ScheduledMessageStatusBadgeProps } from './types'; + +const translateScheduledMessageStatus = ( + t: TFunction, + status: ScheduledMessageStatus, +) => { + if (status === ScheduledMessageStatus.Failed) return t('Failed'); + + if (status === ScheduledMessageStatus.Sending) return t('Sending...'); + + return t('Scheduled'); +}; + +const getStatusBadgeVariant = (status: ScheduledMessageStatus): BadgeVariant => + status === ScheduledMessageStatus.Failed ? 'error' : 'neutral'; + +export const ScheduledMessageStatusBadge = ({ + scheduledMessage, +}: ScheduledMessageStatusBadgeProps) => { + const { t } = useTranslationContext(); + + const { status } = scheduledMessage; + + return ( + + {translateScheduledMessageStatus(t, status)} + + ); +}; diff --git a/src/components/ScheduledMessages/ScheduledMessagesBanner.tsx b/src/components/ScheduledMessages/ScheduledMessagesBanner.tsx new file mode 100644 index 000000000..22bd85095 --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessagesBanner.tsx @@ -0,0 +1,81 @@ +import React, { useRef } from 'react'; + +import { + useChannelStateContext, + useComponentContext, + useComponentContextIcons, + useTranslationContext, +} from '../../context'; +import { Button } from '../Button'; +import { useMessageComposerController } from '../MessageComposer/hooks/useMessageComposerController'; +import { GlobalModal } from '../Modal'; +import { Portal } from '../Portal/Portal'; +import { SCHEDULE_MESSAGE_CAPABILITY } from './constants'; +import { useScheduledMessageCount, useScheduleMessageDialog } from './hooks'; +import { ScheduledMessageListDialog } from './ScheduledMessageListDialog'; +import { getChannelContainer } from './utils'; + +export const ScheduledMessagesBanner = () => { + const { t } = useTranslationContext(); + const { IconChevronRight, IconClock } = useComponentContextIcons(); + const { Modal = GlobalModal } = useComponentContext(); + const { channelCapabilities } = useChannelStateContext('ScheduledMessagesBanner'); + + const { channel, threadId } = useMessageComposerController(); + const { cid } = channel; + const parentId = threadId ?? undefined; + const scheduledMessageCount = useScheduledMessageCount({ cid, parentId }); + + const viewButtonRef = useRef(null); + + const { closeDialog, isDialogOpen, openDialog } = + useScheduleMessageDialog(viewButtonRef); + + const hasCapability = !!channelCapabilities[SCHEDULE_MESSAGE_CAPABILITY]; + const isBannerVisible = hasCapability && scheduledMessageCount > 0; + const label = parentId + ? t('{{ count }} scheduled replies', { count: scheduledMessageCount }) + : t('{{ count }} scheduled messages', { count: scheduledMessageCount }); + + if (!isBannerVisible && !isDialogOpen) return null; + + return ( + <> + {isBannerVisible && ( +
+ + {label} + +
+ )} + + + {isDialogOpen && ( + + )} + + + + ); +}; diff --git a/src/components/ScheduledMessages/ScheduledMessagesEmptyList.tsx b/src/components/ScheduledMessages/ScheduledMessagesEmptyList.tsx new file mode 100644 index 000000000..0883c4e30 --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessagesEmptyList.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import { useComponentContextIcons, useTranslationContext } from '../../context'; + +export const ScheduledMessagesEmptyList = () => { + const { t } = useTranslationContext(); + const { IconClock } = useComponentContextIcons(); + + return ( +
+ +

+ {t('No scheduled messages')} +

+

+ {t('Messages you schedule appear here')} +

+
+ ); +}; diff --git a/src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx b/src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx new file mode 100644 index 000000000..48038b588 --- /dev/null +++ b/src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx @@ -0,0 +1,213 @@ +import React from 'react'; +import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react'; +import { ScheduledMessageFailureReason, ScheduledMessageStatus } from 'stream-chat'; +import type { ScheduledMessageResponse, StreamChat } from 'stream-chat'; + +import { axe } from '../../../../axe-helper'; +import { ChatProvider, TranslationProvider } from '../../../context'; +import { WithComponents } from '../../../context/WithComponents'; +import { Streami18n } from '../../../i18n'; +import { + generateScheduledMessage, + getTestClientWithUser, + mockChatContext, +} from '../../../mock-builders'; +import { ScheduledMessageList } from '../ScheduledMessageList'; +import type { ScheduledMessageItemProps } from '../types'; + +const cid = 'messaging:general'; + +const WEDNESDAY_MORNING = new Date(2026, 9, 28, 10, 30); + +const atLocalTime = (day: number, hour: number) => + new Date(2026, 9, day, hour, 0).toISOString(); + +type RenderListOptions = { + components?: Parameters[0]['overrides']; + scheduledMessages?: ScheduledMessageResponse[]; +}; + +const renderList = async ({ + components = {}, + scheduledMessages = [], +}: RenderListOptions = {}) => { + const client = await getTestClientWithUser({ id: 'user-id' }); + scheduledMessages.forEach(client.scheduledMessages.upsertToState); + + const translators = await new Streami18n({ language: 'en' }).getTranslators(); + + const view = render( + + + + + + + , + ); + + return { ...view, client }; +}; + +const getGroupHeaders = () => + screen.getAllByRole('heading', { level: 3 }).map(({ textContent }) => textContent); + +const setNextCursor = (client: StreamChat) => + act(() => client.scheduledMessages.state.partialNext({ next: 'cursor' })); + +describe('ScheduledMessageList', () => { + beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true, toFake: ['Date'] }); + vi.setSystemTime(WEDNESDAY_MORNING); + }); + + afterEach(() => { + cleanup(); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('shows the empty state without scheduled messages', async () => { + await renderList(); + + expect(screen.getByText('No scheduled messages')).toBeInTheDocument(); + expect(screen.getByText('Messages you schedule appear here')).toBeInTheDocument(); + }); + + it('shows a loading indicator while the first page loads', async () => { + const { client } = await renderList(); + + act(() => client.scheduledMessages.state.partialNext({ isLoading: true })); + + expect(screen.queryByText('No scheduled messages')).not.toBeInTheDocument(); + expect(document.querySelector('.str-chat__loading-indicator')).toBeInTheDocument(); + }); + + it('lists failed messages first, then one group per day', async () => { + await renderList({ + scheduledMessages: [ + generateScheduledMessage({ cid, scheduled_at: atLocalTime(29, 9) }), + generateScheduledMessage({ cid, scheduled_at: atLocalTime(30, 13) }), + generateScheduledMessage({ + cid, + scheduled_at: atLocalTime(29, 13), + status: ScheduledMessageStatus.Failed, + }), + ], + }); + + expect(getGroupHeaders()).toEqual(['Failed', 'Tomorrow', 'Friday']); + }); + + it('shows the preview, scheduled time and status of each message', async () => { + await renderList({ + scheduledMessages: [ + generateScheduledMessage({ + cid, + message: { text: 'Launch moved to Monday' }, + scheduled_at: atLocalTime(29, 9), + }), + ], + }); + + const item = screen.getByTestId('scheduled-message-item'); + + expect(within(item).getByText('Launch moved to Monday')).toBeInTheDocument(); + expect(within(item).getByText('Tomorrow at 09:00')).toBeInTheDocument(); + expect(within(item).getByText('Scheduled')).toBeInTheDocument(); + }); + + it('summarises attachments when the message has no text', async () => { + await renderList({ + scheduledMessages: [ + generateScheduledMessage({ + cid, + message: { + attachments: [{ image_url: 'https://example.com/a.png', type: 'image' }], + text: '', + }, + }), + ], + }); + + expect(screen.getByText('Image')).toBeInTheDocument(); + }); + + it('marks a message that is being sent', async () => { + await renderList({ + scheduledMessages: [ + generateScheduledMessage({ cid, status: ScheduledMessageStatus.Sending }), + ], + }); + + expect(screen.getByText('Sending...')).toBeInTheDocument(); + }); + + it('explains why a message failed', async () => { + await renderList({ + scheduledMessages: [ + generateScheduledMessage({ + cid, + failure_reason: ScheduledMessageFailureReason.ChannelFrozen, + status: ScheduledMessageStatus.Failed, + }), + ], + }); + + const item = screen.getByTestId('scheduled-message-item'); + + expect(item).toHaveClass('str-chat__scheduled-message-item--failed'); + expect(within(item).getByText('This channel is frozen')).toBeInTheDocument(); + }); + + it('loads more only when the store has another page', async () => { + const { client } = await renderList({ + scheduledMessages: [generateScheduledMessage({ cid })], + }); + const loadMore = vi + .spyOn(client.scheduledMessages, 'loadMore') + .mockResolvedValue(undefined); + + expect(screen.queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument(); + + setNextCursor(client); + fireEvent.click(screen.getByRole('button', { name: 'Load more' })); + + expect(loadMore).toHaveBeenCalledTimes(1); + }); + + it('disables load more while a page is loading', async () => { + const { client } = await renderList({ + scheduledMessages: [generateScheduledMessage({ cid })], + }); + + setNextCursor(client); + act(() => client.scheduledMessages.state.partialNext({ isLoading: true })); + + expect(screen.getByRole('button', { name: 'Load more' })).toBeDisabled(); + }); + + it('renders items through the ScheduledMessageItem slot', async () => { + const CustomItem = ({ scheduledMessage }: ScheduledMessageItemProps) => ( +
{scheduledMessage.id}
+ ); + + await renderList({ + components: { ScheduledMessageItem: CustomItem }, + scheduledMessages: [generateScheduledMessage({ cid, id: 'scheduled-id' })], + }); + + expect(screen.getByTestId('custom-item')).toHaveTextContent('scheduled-id'); + }); + + it('has no accessibility violations', async () => { + const { container } = await renderList({ + scheduledMessages: [ + generateScheduledMessage({ cid }), + generateScheduledMessage({ cid, status: ScheduledMessageStatus.Failed }), + ], + }); + + expect(await axe(container)).toHaveNoViolations(); + }); +}); diff --git a/src/components/ScheduledMessages/__tests__/ScheduledMessagesBanner.test.tsx b/src/components/ScheduledMessages/__tests__/ScheduledMessagesBanner.test.tsx new file mode 100644 index 000000000..0d40b2886 --- /dev/null +++ b/src/components/ScheduledMessages/__tests__/ScheduledMessagesBanner.test.tsx @@ -0,0 +1,221 @@ +import React from 'react'; +import type { PropsWithChildren } from 'react'; +import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; +import { formatMessage, ScheduledMessageEventType, SearchController } from 'stream-chat'; +import type { LocalMessage, ScheduledMessageResponse } from 'stream-chat'; + +import { axe } from '../../../../axe-helper'; +import { Channel } from '../../Channel'; +import { DialogManagerProvider, TranslationProvider } from '../../../context'; +import { type ChatContextValue, ChatProvider } from '../../../context/ChatContext'; +import { WithComponents } from '../../../context/WithComponents'; +import { Streami18n } from '../../../i18n'; +import { MessageComposer } from '../../MessageComposer'; +import { LegacyThreadContext } from '../../Thread/LegacyThreadContext'; +import { + dispatchScheduledMessageEvent, + generateChannel, + generateMessage, + generateScheduledMessage, + generateUser, + initClientWithChannels, +} from '../../../mock-builders'; + +const user = generateUser({ id: 'user-id' }); + +const cid = 'messaging:general'; +const parentId = 'parent-1'; + +const channelDataWith = (ownCapabilities: string[]) => + generateChannel({ + channel: { id: 'general', own_capabilities: ownCapabilities, type: 'messaging' }, + }); + +const threadReply = () => + generateScheduledMessage({ cid, message: { parent_id: parentId, text: 'reply' } }); + +type RenderComposerOptions = { + canSchedule?: boolean; + components?: Parameters[0]['overrides']; + inThread?: boolean; + scheduledMessages?: ScheduledMessageResponse[]; +}; + +const renderComposer = async ({ + canSchedule = true, + components = {}, + inThread = false, + scheduledMessages = [], +}: RenderComposerOptions = {}) => { + const ownCapabilities = canSchedule + ? ['send-message', 'schedule-message'] + : ['send-message']; + const { + channels: [channel], + client, + } = await initClientWithChannels({ + channelsData: [channelDataWith(ownCapabilities)], + customUser: user, + }); + scheduledMessages.forEach(client.scheduledMessages.upsertToState); + client.scheduledMessages.registerSubscriptions(); + + const translators = await new Streami18n({ language: 'en' }).getTranslators(); + const legacyThread: LocalMessage | undefined = inThread + ? formatMessage(generateMessage({ cid, id: parentId })) + : undefined; + + const ThreadScope = ({ children }: PropsWithChildren) => ( + + {children} + + ); + + await act(() => { + render( + + ({ + channel, + channelsQueryState: { queryInProgress: 'uninitialized' }, + client, + getAppSettings: vi.fn(), + latestMessageDatesByChannels: {}, + mutes: [], + searchController: new SearchController(), + })} + > + + + + + + + + + + + , + ); + }); + + return { channel, client }; +}; + +const queryBanner = () => screen.queryByTestId('scheduled-messages-banner'); + +describe('ScheduledMessagesBanner', () => { + afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + }); + + it('stays hidden without scheduled messages', async () => { + await renderComposer(); + + expect(queryBanner()).not.toBeInTheDocument(); + }); + + it('counts the scheduled messages of the channel', async () => { + await renderComposer({ + scheduledMessages: [ + generateScheduledMessage({ cid }), + generateScheduledMessage({ cid }), + threadReply(), + ], + }); + + expect(queryBanner()).toHaveTextContent('2 scheduled messages'); + }); + + it('uses the singular for one scheduled message', async () => { + await renderComposer({ scheduledMessages: [generateScheduledMessage({ cid })] }); + + expect(queryBanner()).toHaveTextContent('1 scheduled message'); + expect(queryBanner()).not.toHaveTextContent('1 scheduled messages'); + }); + + it('counts only the replies of the thread in a thread composer', async () => { + await renderComposer({ + inThread: true, + scheduledMessages: [generateScheduledMessage({ cid }), threadReply()], + }); + + expect(queryBanner()).toHaveTextContent('1 scheduled reply'); + }); + + it('stays hidden without the schedule-message capability', async () => { + await renderComposer({ + canSchedule: false, + scheduledMessages: [generateScheduledMessage({ cid })], + }); + + expect(queryBanner()).not.toBeInTheDocument(); + }); + + it('updates when scheduled message events arrive', async () => { + const { client } = await renderComposer(); + const scheduledMessage = generateScheduledMessage({ cid }); + + act(() => + dispatchScheduledMessageEvent({ + client, + scheduledMessage, + type: ScheduledMessageEventType.Created, + }), + ); + + await waitFor(() => expect(queryBanner()).toHaveTextContent('1 scheduled message')); + + act(() => + dispatchScheduledMessageEvent({ + client, + scheduledMessage: { ...scheduledMessage, status: 'sent' }, + type: ScheduledMessageEventType.Sent, + }), + ); + + await waitFor(() => expect(queryBanner()).not.toBeInTheDocument()); + }); + + it('opens the scheduled message list from View', async () => { + await renderComposer({ + scheduledMessages: [ + generateScheduledMessage({ cid, message: { text: 'Launch moved to Monday' } }), + ], + }); + + await act(() => fireEvent.click(screen.getByRole('button', { name: 'View' }))); + + const dialog = await screen.findByTestId('scheduled-message-list-dialog'); + + expect(dialog).toHaveTextContent('Scheduled messages'); + expect(dialog).toHaveTextContent('Launch moved to Monday'); + }); + + it('titles the list as replies in a thread', async () => { + await renderComposer({ inThread: true, scheduledMessages: [threadReply()] }); + + await act(() => fireEvent.click(screen.getByRole('button', { name: 'View' }))); + + expect(await screen.findByTestId('scheduled-message-list-dialog')).toHaveTextContent( + 'Scheduled replies', + ); + }); + + it('can be hidden through the ScheduledMessagesBanner slot', async () => { + await renderComposer({ + components: { ScheduledMessagesBanner: null }, + scheduledMessages: [generateScheduledMessage({ cid })], + }); + + expect(queryBanner()).not.toBeInTheDocument(); + }); + + it('has no accessibility violations', async () => { + await renderComposer({ scheduledMessages: [generateScheduledMessage({ cid })] }); + + expect(await axe(queryBanner() as HTMLElement)).toHaveNoViolations(); + }); +}); diff --git a/src/components/ScheduledMessages/index.ts b/src/components/ScheduledMessages/index.ts index c92f1e660..8cafdc2e0 100644 --- a/src/components/ScheduledMessages/index.ts +++ b/src/components/ScheduledMessages/index.ts @@ -1,9 +1,17 @@ export { SCHEDULE_MESSAGE_CAPABILITY, + ScheduledMessageGroupType, ScheduleMessageNoticeSeverity, ScheduleTimeError, } from './constants'; export * from './hooks'; +export * from './ScheduledMessageGroupHeader'; +export * from './ScheduledMessageItem'; +export * from './ScheduledMessageList'; +export * from './ScheduledMessageListDialog'; +export * from './ScheduledMessagesBanner'; +export * from './ScheduledMessagesEmptyList'; +export * from './ScheduledMessageStatusBadge'; export * from './ScheduleMessageButton'; export * from './ScheduleMessageDialog'; export * from './ScheduleMessageDialogNotice'; diff --git a/src/components/ScheduledMessages/styling/ScheduledMessageList.scss b/src/components/ScheduledMessages/styling/ScheduledMessageList.scss new file mode 100644 index 000000000..a07e1f5c5 --- /dev/null +++ b/src/components/ScheduledMessages/styling/ScheduledMessageList.scss @@ -0,0 +1,96 @@ +.str-chat { + --str-chat__scheduled-message-list-dialog-max-width: 504px; + + .str-chat__scheduled-message-list-dialog { + width: 100%; + max-width: var(--str-chat__scheduled-message-list-dialog-max-width); + + .str-chat__scheduled-message-list-dialog__body { + overflow-y: auto; + } + } + + .str-chat__scheduled-message-list { + display: flex; + flex-direction: column; + gap: var(--str-chat__spacing-md); + + .str-chat__scheduled-message-list__load-more { + align-self: center; + } + } + + .str-chat__scheduled-message-group { + display: flex; + flex-direction: column; + gap: var(--str-chat__spacing-xxs); + + .str-chat__scheduled-message-group__header { + margin: 0; + padding-inline: var(--str-chat__spacing-sm); + color: var(--str-chat__text-secondary); + font: var(--str-chat__font-caption-emphasis); + } + + .str-chat__scheduled-message-group__items { + display: flex; + flex-direction: column; + margin: 0; + padding: 0; + list-style: none; + } + } + + .str-chat__scheduled-message-item { + .str-chat__summarized-message-preview { + min-width: 0; + } + + .str-chat__scheduled-message-item__time { + color: var(--str-chat__text-secondary); + font: var(--str-chat__font-caption-default); + } + + .str-chat__scheduled-message-item__failure-reason { + color: var(--str-chat__accent-error); + font: var(--str-chat__font-caption-default); + } + + .str-chat__scheduled-message-status-badge { + flex-shrink: 0; + } + } + + .str-chat__scheduled-messages-empty-list { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--str-chat__spacing-xxs); + padding-block: var(--str-chat__spacing-3xl); + padding-inline: var(--str-chat__spacing-md); + text-align: center; + + .str-chat__scheduled-messages-empty-list__icon { + width: 32px; + height: 32px; + margin-block-end: var(--str-chat__spacing-xs); + color: var(--str-chat__text-tertiary); + } + + .str-chat__scheduled-messages-empty-list__title, + .str-chat__scheduled-messages-empty-list__description { + margin: 0; + } + + .str-chat__scheduled-messages-empty-list__title { + color: var(--str-chat__text-primary); + font: var(--str-chat__font-caption-emphasis); + } + + .str-chat__scheduled-messages-empty-list__description { + color: var(--str-chat__text-secondary); + font: var(--str-chat__font-caption-default); + } + } +} diff --git a/src/components/ScheduledMessages/styling/ScheduledMessagesBanner.scss b/src/components/ScheduledMessages/styling/ScheduledMessagesBanner.scss new file mode 100644 index 000000000..778e4ea4f --- /dev/null +++ b/src/components/ScheduledMessages/styling/ScheduledMessagesBanner.scss @@ -0,0 +1,31 @@ +.str-chat { + .str-chat__scheduled-messages-banner { + display: flex; + align-items: center; + gap: var(--str-chat__spacing-xs); + width: 100%; + padding-inline: var(--str-chat__spacing-sm); + border-radius: var(--str-chat__radius-md); + background-color: var(--str-chat__background-core-surface-subtle); + color: var(--str-chat__text-secondary); + font: var(--str-chat__font-caption-default); + + .str-chat__scheduled-messages-banner__icon { + flex-shrink: 0; + height: var(--str-chat__icon-size-sm); + width: var(--str-chat__icon-size-sm); + } + + .str-chat__scheduled-messages-banner__label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .str-chat__scheduled-messages-banner__view-button { + flex-shrink: 0; + } + } +} diff --git a/src/components/ScheduledMessages/styling/index.scss b/src/components/ScheduledMessages/styling/index.scss index 594c108d6..20164baf1 100644 --- a/src/components/ScheduledMessages/styling/index.scss +++ b/src/components/ScheduledMessages/styling/index.scss @@ -1,2 +1,4 @@ +@use 'ScheduledMessageList'; +@use 'ScheduledMessagesBanner'; @use 'ScheduleMessageButton'; @use 'ScheduleMessageDialog'; diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 68f5601cf..0d9b0ee53 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -48,6 +48,7 @@ import { type ReactionSelectorProps, type RecordingPermissionDeniedNotificationProps, type ReminderNotificationProps, + type ScheduledMessageItemProps, type ScheduleMessageDialogProps, type ScrollToLatestMessageButtonProps, type SearchResultsPresearchProps, @@ -271,6 +272,8 @@ export type ComponentContextValue = { SearchSourceResultsHeader?: React.ComponentType; /** Custom component to display the search source results UI during the search query execution, defaults to and accepts same props as: [SearchSourceResultsLoadingIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Search/SearchResults/SearchSourceResultsLoadingIndicator.tsx) */ SearchSourceResultsLoadingIndicator?: React.ComponentType; + ScheduledMessageItem?: React.ComponentType; + ScheduledMessagesBanner?: React.ComponentType | null; /** Custom UI component rendered next to the send button that opens the schedule menu, defaults to: [ScheduleMessageButton](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ScheduledMessages/ScheduleMessageButton.tsx). Pass `null` to hide it. */ ScheduleMessageButton?: React.ComponentType | null; /** Custom UI component to render the custom-time dialog for scheduling a message, defaults to and accepts same props as: [ScheduleMessageDialog](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ScheduledMessages/ScheduleMessageDialog.tsx) */ From a85f390163d4ac0ceea641e624c97cfb6de4d840 Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Fri, 30 Oct 2026 13:09:16 +0100 Subject: [PATCH 05/12] feat(i18n): translate the scheduled message list and banner Add the banner counts, list titles, statuses, empty state, failure reasons and the day group timestamp format in all locales. --- src/i18n/de.json | 22 ++++++++++++++++++++++ src/i18n/en.json | 22 ++++++++++++++++++++++ src/i18n/es.json | 24 ++++++++++++++++++++++++ src/i18n/fr.json | 24 ++++++++++++++++++++++++ src/i18n/hi.json | 22 ++++++++++++++++++++++ src/i18n/it.json | 24 ++++++++++++++++++++++++ src/i18n/ja.json | 20 ++++++++++++++++++++ src/i18n/ko.json | 20 ++++++++++++++++++++ src/i18n/nl.json | 22 ++++++++++++++++++++++ src/i18n/pt.json | 24 ++++++++++++++++++++++++ src/i18n/ru.json | 26 ++++++++++++++++++++++++++ src/i18n/tr.json | 22 ++++++++++++++++++++++ 12 files changed, 272 insertions(+) diff --git a/src/i18n/de.json b/src/i18n/de.json index c74097c44..b840353e0 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -14,6 +14,10 @@ "{{ count }} photos_other": "{{ count }} Fotos", "{{ count }} reactions_one": "{{ count }} Reaktion", "{{ count }} reactions_other": "{{ count }} Reaktionen", + "{{ count }} scheduled messages_one": "{{ count }} scheduled messages", + "{{ count }} scheduled messages_other": "{{ count }} scheduled messages", + "{{ count }} scheduled replies_one": "{{ count }} scheduled replies", + "{{ count }} scheduled replies_other": "{{ count }} scheduled replies", "{{ count }} videos_one": "{{ count }} Video", "{{ count }} videos_other": "{{ count }} Videos", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} und {{ secondUser }}", @@ -243,6 +247,7 @@ "ban-command-description": "Einen Benutzer verbannen", "Block user": "Benutzer blockieren", "Block User": "Benutzer blockieren", + "Blocked by moderation": "Blocked by moderation", "Brewing up an answer": "Ich braue eine Antwort zusammen", "Browse channel members": "Kanalmitglieder durchsuchen", "Browse pinned messages": "Angeheftete Nachrichten durchsuchen", @@ -349,6 +354,7 @@ "Error uploading image": "Fehler beim Hochladen des Bildes", "Error: {{ errorMessage }}": "Fehler: {{ errorMessage }}", "Exit command {{ command }}": "Befehl beenden {{ command }}", + "Failed": "Failed", "Failed to block user": "Benutzer konnte nicht blockiert werden", "Failed to create the poll": "Fehler beim Erstellen der Umfrage", "Failed to create the poll due to {{reason}}": "Die Umfrage konnte aufgrund von {{reason}} nicht erstellt werden", @@ -485,6 +491,8 @@ "Message unpinned": "Nachricht nicht mehr angeheftet", "Message was blocked by moderation policies": "Nachricht wurde durch moderationsrichtlinien blockiert", "Messages have been marked unread.": "Nachrichten wurden als ungelesen markiert.", + "Messages waiting to be sent": "Messages waiting to be sent", + "Messages you schedule appear here": "Messages you schedule appear here", "Missing permissions to upload the attachment": "Fehlende Berechtigungen zum Hochladen des Anhangs", "Moderator": "Moderator", "Multiple votes": "Mehrfachstimmen", @@ -508,8 +516,10 @@ "No photos or videos": "Keine Fotos oder Videos", "No pinned messages": "Keine angehefteten Nachrichten", "No results found": "Keine Ergebnisse gefunden", + "No scheduled messages": "No scheduled messages", "No user found": "Kein Benutzer gefunden", "Nobody will be able to vote in this poll anymore.": "Niemand kann mehr in dieser Umfrage abstimmen.", + "Not sent: the service was delayed": "Not sent: the service was delayed", "Nothing yet...": "Noch nichts...", "Notify all {{ role }} members": "Alle Mitglieder mit Rolle {{ role }} benachrichtigen", "Offline": "Offline", @@ -559,6 +569,7 @@ "Reading the digital tea leaves": "Ich lese die digitalen Teeblätter", "Ready": "Bereit", "Recording format is not supported and cannot be reproduced": "Aufnahmeformat wird nicht unterstützt und kann nicht wiedergegeben werden", + "Rejected by the app": "Rejected by the app", "Remind me": "Erinnern", "Remind Me": "Erinnern", "Reminder set": "Erinnerung gesetzt", @@ -591,8 +602,11 @@ "Saved for later": "Für später gespeichert", "Schedule": "Planen", "Schedule message": "Nachricht planen", + "Scheduled": "Scheduled", "Scheduled for {{ time }}": "Geplant für {{ time }}", + "Scheduled messages": "Scheduled messages", "Scheduled messages are turned off": "Geplante Nachrichten sind deaktiviert", + "Scheduled replies": "Scheduled replies", "Search": "Suche", "Search GIFs": "GIFs suchen", "search-results-header-filter-source-button-label--channels": "Kanäle", @@ -632,6 +646,7 @@ "Slow Mode ON": "Langsamer Modus EIN", "Slow mode, wait {{ seconds }}s...": "Langsamer Modus, warte {{ seconds }}s...", "Some of the files will not be accepted": "Einige der Dateien werden nicht akzeptiert", + "Something went wrong. Try again": "Something went wrong. Try again", "Speaking": "Spricht", "Start typing to search": "Tippen Sie, um zu suchen", "Stop sharing": "Teilen beenden", @@ -641,9 +656,13 @@ "Summoning my inner genius": "Ich beschwöre mein inneres Genie", "Tap to remove": "Tippen zum Entfernen", "Tap to remove: {{ reactionName }}": "Tippen zum Entfernen: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "The message no longer meets the app's limits. Edit it", "Thinking": "Denkt nach", "Thinking really hard": "Ich denke angestrengt nach", "Thinking...": "Denken...", + "This channel is frozen": "This channel is frozen", + "This channel now holds messages for approval. Schedule it again": "This channel now holds messages for approval. Schedule it again", + "This channel was deleted": "This channel was deleted", "this content could not be displayed": "Dieser Inhalt konnte nicht angezeigt werden", "This field cannot be empty or contain only spaces": "Dieses Feld darf nicht leer sein oder nur Leerzeichen enthalten", "This message can't be scheduled": "Diese Nachricht kann nicht geplant werden", @@ -674,6 +693,7 @@ "timestamp/relativeYesterday": "Gestern", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Heute] [um] HH:mm\", \"nextDay\": \"[Morgen] [um] HH:mm\", \"lastDay\": \"[Gestern] [um] HH:mm\", \"nextWeek\": \"dddd [um] HH:mm\", \"lastWeek\": \"[letzten] dddd [um] HH:mm\", \"sameElse\": \"ddd, D MMM [um] HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Heute] [um] HH:mm\", \"nextDay\": \"[Morgen] [um] HH:mm\", \"lastDay\": \"[Gestern] [um] HH:mm\", \"nextWeek\": \"dddd [um] HH:mm\", \"lastWeek\": \"[letzten] dddd [um] HH:mm\", \"sameElse\": \"ddd, D MMM [um] HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Heute]\", \"nextDay\": \"[Morgen]\", \"lastDay\": \"[Gestern]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Letzte] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Um mit der Aufnahme zu beginnen, erlauben Sie den Zugriff auf die Kamera in Ihrem Browser", "To start recording, allow the microphone access in your browser": "Um mit der Aufnahme zu beginnen, erlauben Sie den Zugriff auf das Mikrofon in Ihrem Browser", @@ -738,6 +758,8 @@ "Working my magic": "Ich lasse meine Magie wirken", "You": "Du", "You can't schedule messages here": "Du kannst hier keine Nachrichten planen", + "You can't send messages in this channel": "You can't send messages in this channel", + "You're no longer allowed to post here": "You're no longer allowed to post here", "You're offline. Reconnect to schedule this message.": "Du bist offline. Stelle die Verbindung wieder her, um diese Nachricht zu planen.", "You've reached the limit of scheduled messages": "Du hast das Limit für geplante Nachrichten erreicht", "You've reached the limit of scheduled messages in this channel": "Du hast das Limit für geplante Nachrichten in diesem Kanal erreicht", diff --git a/src/i18n/en.json b/src/i18n/en.json index 3c0d87b85..d7a950ff3 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -14,6 +14,10 @@ "{{ count }} photos_other": "{{ count }} photos", "{{ count }} reactions_one": "{{ count }} reaction", "{{ count }} reactions_other": "{{ count }} reactions", + "{{ count }} scheduled messages_one": "{{ count }} scheduled message", + "{{ count }} scheduled messages_other": "{{ count }} scheduled messages", + "{{ count }} scheduled replies_one": "{{ count }} scheduled reply", + "{{ count }} scheduled replies_other": "{{ count }} scheduled replies", "{{ count }} videos_one": "{{ count }} video", "{{ count }} videos_other": "{{ count }} videos", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} and {{ secondUser }}", @@ -243,6 +247,7 @@ "ban-command-description": "Ban a user", "Block user": "Block user", "Block User": "Block User", + "Blocked by moderation": "Blocked by moderation", "Brewing up an answer": "Brewing up an answer", "Browse channel members": "Browse channel members", "Browse pinned messages": "Browse pinned messages", @@ -349,6 +354,7 @@ "Error uploading image": "Error uploading image", "Error: {{ errorMessage }}": "Error: {{ errorMessage }}", "Exit command {{ command }}": "Exit command {{ command }}", + "Failed": "Failed", "Failed to block user": "Failed to block user", "Failed to create the poll": "Failed to create the poll", "Failed to create the poll due to {{reason}}": "Failed to create the poll due to {{reason}}", @@ -485,6 +491,8 @@ "Message unpinned": "Message unpinned", "Message was blocked by moderation policies": "Message was blocked by moderation policies", "Messages have been marked unread.": "Messages have been marked unread.", + "Messages waiting to be sent": "Messages waiting to be sent", + "Messages you schedule appear here": "Messages you schedule appear here", "Missing permissions to upload the attachment": "Missing permissions to upload the attachment", "Moderator": "Moderator", "Multiple votes": "Multiple Votes", @@ -508,8 +516,10 @@ "No photos or videos": "No photos or videos", "No pinned messages": "No pinned messages", "No results found": "No results found", + "No scheduled messages": "No scheduled messages", "No user found": "No user found", "Nobody will be able to vote in this poll anymore.": "Nobody will be able to vote in this poll anymore.", + "Not sent: the service was delayed": "Not sent: the service was delayed", "Nothing yet...": "Nothing yet...", "Notify all {{ role }} members": "Notify all {{ role }} members", "Offline": "Offline", @@ -559,6 +569,7 @@ "Reading the digital tea leaves": "Reading the digital tea leaves", "Ready": "Ready", "Recording format is not supported and cannot be reproduced": "Recording format is not supported and cannot be reproduced", + "Rejected by the app": "Rejected by the app", "Remind me": "Remind me", "Remind Me": "Remind Me", "Reminder set": "Reminder set", @@ -591,8 +602,11 @@ "Saved for later": "Saved for later", "Schedule": "Schedule", "Schedule message": "Schedule message", + "Scheduled": "Scheduled", "Scheduled for {{ time }}": "Scheduled for {{ time }}", + "Scheduled messages": "Scheduled messages", "Scheduled messages are turned off": "Scheduled messages are turned off", + "Scheduled replies": "Scheduled replies", "Search": "Search", "Search GIFs": "Search GIFs", "search-results-header-filter-source-button-label--channels": "channels", @@ -632,6 +646,7 @@ "Slow Mode ON": "Slow Mode ON", "Slow mode, wait {{ seconds }}s...": "Slow mode, wait {{ seconds }}s...", "Some of the files will not be accepted": "Some of the files will not be accepted", + "Something went wrong. Try again": "Something went wrong. Try again", "Speaking": "Speaking", "Start typing to search": "Start typing to search", "Stop sharing": "Stop sharing", @@ -641,9 +656,13 @@ "Summoning my inner genius": "Summoning my inner genius", "Tap to remove": "Tap to remove", "Tap to remove: {{ reactionName }}": "Tap to remove: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "The message no longer meets the app's limits. Edit it", "Thinking": "Thinking", "Thinking really hard": "Thinking really hard", "Thinking...": "Thinking...", + "This channel is frozen": "This channel is frozen", + "This channel now holds messages for approval. Schedule it again": "This channel now holds messages for approval. Schedule it again", + "This channel was deleted": "This channel was deleted", "this content could not be displayed": "this content could not be displayed", "This field cannot be empty or contain only spaces": "This field cannot be empty or contain only spaces", "This message can't be scheduled": "This message can't be scheduled", @@ -674,6 +693,7 @@ "timestamp/relativeYesterday": "Yesterday", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Today] [at] HH:mm\", \"nextDay\": \"[Tomorrow] [at] HH:mm\", \"lastDay\": \"[Yesterday] [at] HH:mm\", \"nextWeek\": \"dddd [at] HH:mm\", \"lastWeek\": \"[Last] dddd [at] HH:mm\", \"sameElse\": \"ddd, D MMM [at] HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Today] [at] HH:mm\", \"nextDay\": \"[Tomorrow] [at] HH:mm\", \"lastDay\": \"[Yesterday] [at] HH:mm\", \"nextWeek\": \"dddd [at] HH:mm\", \"lastWeek\": \"[Last] dddd [at] HH:mm\", \"sameElse\": \"ddd, D MMM [at] HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Today]\", \"nextDay\": \"[Tomorrow]\", \"lastDay\": \"[Yesterday]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Last] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "To start recording, allow the camera access in your browser", "To start recording, allow the microphone access in your browser": "To start recording, allow the microphone access in your browser", @@ -738,6 +758,8 @@ "Working my magic": "Working my magic", "You": "You", "You can't schedule messages here": "You can't schedule messages here", + "You can't send messages in this channel": "You can't send messages in this channel", + "You're no longer allowed to post here": "You're no longer allowed to post here", "You're offline. Reconnect to schedule this message.": "You're offline. Reconnect to schedule this message.", "You've reached the limit of scheduled messages": "You've reached the limit of scheduled messages", "You've reached the limit of scheduled messages in this channel": "You've reached the limit of scheduled messages in this channel", diff --git a/src/i18n/es.json b/src/i18n/es.json index c2478ba6c..d736a97f3 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -19,6 +19,12 @@ "{{ count }} reactions_one": "{{ count }} reacción", "{{ count }} reactions_many": "{{ count }} reacciones", "{{ count }} reactions_other": "{{ count }} reacciones", + "{{ count }} scheduled messages_one": "{{ count }} mensaje programado", + "{{ count }} scheduled messages_many": "{{ count }} mensajes programados", + "{{ count }} scheduled messages_other": "{{ count }} mensajes programados", + "{{ count }} scheduled replies_one": "{{ count }} respuesta programada", + "{{ count }} scheduled replies_many": "{{ count }} respuestas programadas", + "{{ count }} scheduled replies_other": "{{ count }} respuestas programadas", "{{ count }} videos_one": "{{ count }} vídeo", "{{ count }} videos_many": "{{ count }} vídeos", "{{ count }} videos_other": "{{ count }} vídeos", @@ -260,6 +266,7 @@ "ban-command-description": "Prohibir a un usuario", "Block user": "Bloquear usuario", "Block User": "Bloquear usuario", + "Blocked by moderation": "Bloqueado por la moderación", "Brewing up an answer": "Preparando una respuesta", "Browse channel members": "Explorar miembros del canal", "Browse pinned messages": "Explorar mensajes fijados", @@ -366,6 +373,7 @@ "Error uploading image": "Error al subir la imagen", "Error: {{ errorMessage }}": "Error: {{ errorMessage }}", "Exit command {{ command }}": "Salir del comando {{ command }}", + "Failed": "Fallido", "Failed to block user": "No se pudo bloquear al usuario", "Failed to create the poll": "Error al crear la encuesta", "Failed to create the poll due to {{reason}}": "No se pudo crear la encuesta debido a {{reason}}", @@ -505,6 +513,8 @@ "Message unpinned": "Mensaje desanclado", "Message was blocked by moderation policies": "El mensaje fue bloqueado por las políticas de moderación", "Messages have been marked unread.": "Los mensajes han sido marcados como no leídos.", + "Messages waiting to be sent": "Mensajes pendientes de envío", + "Messages you schedule appear here": "Los mensajes que programes aparecerán aquí", "Missing permissions to upload the attachment": "Faltan permisos para subir el archivo adjunto", "Moderator": "Moderador", "Multiple votes": "Votos múltiples", @@ -528,8 +538,10 @@ "No photos or videos": "No hay fotos ni videos", "No pinned messages": "No hay mensajes fijados", "No results found": "No se han encontrado resultados", + "No scheduled messages": "No hay mensajes programados", "No user found": "No se encontró ningún usuario", "Nobody will be able to vote in this poll anymore.": "Nadie podrá votar en esta encuesta.", + "Not sent: the service was delayed": "No enviado: el servicio se retrasó", "Nothing yet...": "Nada aún...", "Notify all {{ role }} members": "Notificar a todos los miembros con rol {{ role }}", "Offline": "Desconectado", @@ -579,6 +591,7 @@ "Reading the digital tea leaves": "Leyendo las hojas de té digitales", "Ready": "Listo", "Recording format is not supported and cannot be reproduced": "El formato de grabación no es compatible y no se puede reproducir", + "Rejected by the app": "Rechazado por la aplicación", "Remind me": "Recordarme", "Remind Me": "Recordarme", "Reminder set": "Recordatorio establecido", @@ -614,8 +627,11 @@ "Saved for later": "Guardado para más tarde", "Schedule": "Programar", "Schedule message": "Programar mensaje", + "Scheduled": "Programado", "Scheduled for {{ time }}": "Programado para {{ time }}", + "Scheduled messages": "Mensajes programados", "Scheduled messages are turned off": "Los mensajes programados están desactivados", + "Scheduled replies": "Respuestas programadas", "Search": "Buscar", "Search GIFs": "Buscar GIFs", "search-results-header-filter-source-button-label--channels": "canales", @@ -657,6 +673,7 @@ "Slow Mode ON": "Modo lento activado", "Slow mode, wait {{ seconds }}s...": "Modo lento, espera {{ seconds }} s...", "Some of the files will not be accepted": "Algunos archivos no serán aceptados", + "Something went wrong. Try again": "Algo salió mal. Inténtalo de nuevo", "Speaking": "Hablando", "Start typing to search": "Empieza a escribir para buscar", "Stop sharing": "Dejar de compartir", @@ -666,9 +683,13 @@ "Summoning my inner genius": "Invocando a mi genio interior", "Tap to remove": "Toca para quitar", "Tap to remove: {{ reactionName }}": "Toca para quitar: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "El mensaje ya no cumple los límites de la aplicación. Edítalo", "Thinking": "Pensando", "Thinking really hard": "Pensando muy fuerte", "Thinking...": "Pensando...", + "This channel is frozen": "Este canal está congelado", + "This channel now holds messages for approval. Schedule it again": "Este canal ahora retiene los mensajes para su aprobación. Vuelve a programarlo", + "This channel was deleted": "Este canal se eliminó", "this content could not be displayed": "Este contenido no se pudo mostrar", "This field cannot be empty or contain only spaces": "Este campo no puede estar vacío o contener solo espacios", "This message can't be scheduled": "Este mensaje no se puede programar", @@ -700,6 +721,7 @@ "timestamp/relativeYesterday": "Ayer", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoy] [a las] HH:mm\", \"nextDay\": \"[Mañana] [a las] HH:mm\", \"lastDay\": \"[Ayer] [a las] HH:mm\", \"nextWeek\": \"dddd [a las] HH:mm\", \"lastWeek\": \"[Último] dddd [a las] HH:mm\", \"sameElse\": \"ddd, D MMM [a las] HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoy] [a las] HH:mm\", \"nextDay\": \"[Mañana] [a las] HH:mm\", \"lastDay\": \"[Ayer] [a las] HH:mm\", \"nextWeek\": \"dddd [a las] HH:mm\", \"lastWeek\": \"[Último] dddd [a las] HH:mm\", \"sameElse\": \"ddd, D MMM [a las] HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoy]\", \"nextDay\": \"[Mañana]\", \"lastDay\": \"[Ayer]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Último] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Para comenzar a grabar, permita el acceso a la cámara en su navegador", "To start recording, allow the microphone access in your browser": "Para comenzar a grabar, permita el acceso al micrófono en su navegador", @@ -768,6 +790,8 @@ "Working my magic": "Haciendo mi magia", "You": "Tú", "You can't schedule messages here": "No puedes programar mensajes aquí", + "You can't send messages in this channel": "No puedes enviar mensajes en este canal", + "You're no longer allowed to post here": "Ya no tienes permiso para publicar aquí", "You're offline. Reconnect to schedule this message.": "Estás sin conexión. Vuelve a conectarte para programar este mensaje.", "You've reached the limit of scheduled messages": "Has alcanzado el límite de mensajes programados", "You've reached the limit of scheduled messages in this channel": "Has alcanzado el límite de mensajes programados en este canal", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index c77dfb953..5097109f8 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -19,6 +19,12 @@ "{{ count }} reactions_one": "{{ count }} réaction", "{{ count }} reactions_many": "{{ count }} réactions", "{{ count }} reactions_other": "{{ count }} réactions", + "{{ count }} scheduled messages_one": "{{ count }} message programmé", + "{{ count }} scheduled messages_many": "{{ count }} messages programmés", + "{{ count }} scheduled messages_other": "{{ count }} messages programmés", + "{{ count }} scheduled replies_one": "{{ count }} réponse programmée", + "{{ count }} scheduled replies_many": "{{ count }} réponses programmées", + "{{ count }} scheduled replies_other": "{{ count }} réponses programmées", "{{ count }} videos_one": "{{ count }} vidéo", "{{ count }} videos_many": "{{ count }} vidéos", "{{ count }} videos_other": "{{ count }} vidéos", @@ -260,6 +266,7 @@ "ban-command-description": "Bannir un utilisateur", "Block user": "Bloquer l'utilisateur", "Block User": "Bloquer l'utilisateur", + "Blocked by moderation": "Bloqué par la modération", "Brewing up an answer": "Je concocte une réponse", "Browse channel members": "Parcourir les membres du canal", "Browse pinned messages": "Parcourir les messages épinglés", @@ -366,6 +373,7 @@ "Error uploading image": "Erreur lors de l'envoi de l'image", "Error: {{ errorMessage }}": "Erreur : {{ errorMessage }}", "Exit command {{ command }}": "Quitter la commande {{ command }}", + "Failed": "Échec", "Failed to block user": "Impossible de bloquer l'utilisateur", "Failed to create the poll": "Échec de la création du sondage", "Failed to create the poll due to {{reason}}": "Échec de la création du sondage en raison de {{reason}}", @@ -505,6 +513,8 @@ "Message unpinned": "Message désépinglé", "Message was blocked by moderation policies": "Le message a été bloqué par les politiques de modération", "Messages have been marked unread.": "Les messages ont été marqués comme non lus.", + "Messages waiting to be sent": "Messages en attente d'envoi", + "Messages you schedule appear here": "Les messages que vous programmez apparaissent ici", "Missing permissions to upload the attachment": "Autorisations manquantes pour télécharger la pièce jointe", "Moderator": "Modérateur", "Multiple votes": "Votes multiples", @@ -528,8 +538,10 @@ "No photos or videos": "Aucune photo ni vidéo", "No pinned messages": "Aucun message épinglé", "No results found": "Aucun résultat trouvé", + "No scheduled messages": "Aucun message programmé", "No user found": "Aucun utilisateur trouvé", "Nobody will be able to vote in this poll anymore.": "Personne ne pourra plus voter dans ce sondage.", + "Not sent: the service was delayed": "Non envoyé : le service a pris du retard", "Nothing yet...": "Rien pour l'instant...", "Notify all {{ role }} members": "Notifier tous les membres ayant le rôle {{ role }}", "Offline": "Hors ligne", @@ -579,6 +591,7 @@ "Reading the digital tea leaves": "Je lis dans le marc de café numérique", "Ready": "Prêt", "Recording format is not supported and cannot be reproduced": "Le format d'enregistrement n'est pas pris en charge et ne peut pas être reproduit", + "Rejected by the app": "Refusé par l'application", "Remind me": "Me rappeler", "Remind Me": "Me rappeler", "Reminder set": "Rappel défini", @@ -614,8 +627,11 @@ "Saved for later": "Enregistré pour plus tard", "Schedule": "Programmer", "Schedule message": "Programmer le message", + "Scheduled": "Programmé", "Scheduled for {{ time }}": "Programmé pour {{ time }}", + "Scheduled messages": "Messages programmés", "Scheduled messages are turned off": "Les messages programmés sont désactivés", + "Scheduled replies": "Réponses programmées", "Search": "Rechercher", "Search GIFs": "Rechercher des GIFs", "search-results-header-filter-source-button-label--channels": "canaux", @@ -657,6 +673,7 @@ "Slow Mode ON": "Mode lent activé", "Slow mode, wait {{ seconds }}s...": "Mode lent, attendez {{ seconds }} s...", "Some of the files will not be accepted": "Certains fichiers ne seront pas acceptés", + "Something went wrong. Try again": "Une erreur s'est produite. Réessayez", "Speaking": "Parle", "Start typing to search": "Commencez à taper pour rechercher", "Stop sharing": "Arrêter de partager", @@ -666,9 +683,13 @@ "Summoning my inner genius": "J'invoque mon génie intérieur", "Tap to remove": "Appuyez pour retirer", "Tap to remove: {{ reactionName }}": "Appuyez pour retirer: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "Le message ne respecte plus les limites de l'application. Modifiez-le", "Thinking": "Réfléchit", "Thinking really hard": "Je réfléchis très fort", "Thinking...": "Réflexion...", + "This channel is frozen": "Ce canal est gelé", + "This channel now holds messages for approval. Schedule it again": "Ce canal retient désormais les messages pour approbation. Programmez-le à nouveau", + "This channel was deleted": "Ce canal a été supprimé", "this content could not be displayed": "ce contenu n'a pas pu être affiché", "This field cannot be empty or contain only spaces": "Ce champ ne peut pas être vide ou contenir uniquement des espaces", "This message can't be scheduled": "Ce message ne peut pas être programmé", @@ -700,6 +721,7 @@ "timestamp/relativeYesterday": "Hier", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Aujourd'hui] [à] HH:mm\", \"nextDay\": \"[Demain] [à] HH:mm\", \"lastDay\": \"[Hier] [à] HH:mm\", \"nextWeek\": \"dddd [à] HH:mm\", \"lastWeek\": \"dddd [dernier à] HH:mm\", \"sameElse\": \"ddd, D MMM [à] HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Aujourd'hui] [à] HH:mm\", \"nextDay\": \"[Demain] [à] HH:mm\", \"lastDay\": \"[Hier] [à] HH:mm\", \"nextWeek\": \"dddd [à] HH:mm\", \"lastWeek\": \"dddd [dernier à] HH:mm\", \"sameElse\": \"ddd, D MMM [à] HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Aujourd'hui]\", \"nextDay\": \"[Demain]\", \"lastDay\": \"[Hier]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Dernier] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Pour commencer l'enregistrement, autorisez l'accès à la caméra dans votre navigateur", "To start recording, allow the microphone access in your browser": "Pour commencer l'enregistrement, autorisez l'accès au microphone dans votre navigateur", @@ -768,6 +790,8 @@ "Working my magic": "Je fais ma magie", "You": "Vous", "You can't schedule messages here": "Vous ne pouvez pas programmer de messages ici", + "You can't send messages in this channel": "Vous ne pouvez pas envoyer de messages dans ce canal", + "You're no longer allowed to post here": "Vous n'êtes plus autorisé à publier ici", "You're offline. Reconnect to schedule this message.": "Vous êtes hors ligne. Reconnectez-vous pour programmer ce message.", "You've reached the limit of scheduled messages": "Vous avez atteint la limite de messages programmés", "You've reached the limit of scheduled messages in this channel": "Vous avez atteint la limite de messages programmés dans ce canal", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index 54c727e1b..b63252c5a 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -14,6 +14,10 @@ "{{ count }} photos_other": "{{ count }} फ़ोटो", "{{ count }} reactions_one": "{{ count }} प्रतिक्रिया", "{{ count }} reactions_other": "{{ count }} प्रतिक्रियाएं", + "{{ count }} scheduled messages_one": "{{ count }} शेड्यूल किया गया संदेश", + "{{ count }} scheduled messages_other": "{{ count }} शेड्यूल किए गए संदेश", + "{{ count }} scheduled replies_one": "{{ count }} शेड्यूल किया गया जवाब", + "{{ count }} scheduled replies_other": "{{ count }} शेड्यूल किए गए जवाब", "{{ count }} videos_one": "{{ count }} वीडियो", "{{ count }} videos_other": "{{ count }} वीडियो", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} और {{ secondUser }}", @@ -243,6 +247,7 @@ "ban-command-description": "एक उपयोगकर्ता को प्रतिषेधित करें", "Block user": "उपयोगकर्ता को ब्लॉक करें", "Block User": "उपयोगकर्ता को ब्लॉक करें", + "Blocked by moderation": "मॉडरेशन द्वारा ब्लॉक किया गया", "Brewing up an answer": "जवाब तैयार कर रहा हूँ", "Browse channel members": "चैनल सदस्य देखें", "Browse pinned messages": "पिन किए गए संदेश देखें", @@ -350,6 +355,7 @@ "Error uploading image": "छवि अपलोड करने में त्रुटि", "Error: {{ errorMessage }}": "फेल: {{ errorMessage }}", "Exit command {{ command }}": "कमांड से बाहर निकलें {{ command }}", + "Failed": "विफल", "Failed to block user": "उपयोगकर्ता को ब्लॉक करने में विफल", "Failed to create the poll": "मतदान बनाने में विफल", "Failed to create the poll due to {{reason}}": "मतदान {{reason}} के कारण नहीं बन सका", @@ -486,6 +492,8 @@ "Message unpinned": "संदेश अनपिन किया गया", "Message was blocked by moderation policies": "संदेश को मॉडरेशन नीतियों द्वारा ब्लॉक कर दिया गया है", "Messages have been marked unread.": "संदेशों को अपठित चिह्नित किया गया है।", + "Messages waiting to be sent": "भेजे जाने की प्रतीक्षा में संदेश", + "Messages you schedule appear here": "आपके द्वारा शेड्यूल किए गए संदेश यहाँ दिखाई देते हैं", "Missing permissions to upload the attachment": "अटैचमेंट अपलोड करने के लिए अनुमतियां गायब", "Moderator": "मॉडरेटर", "Multiple votes": "कई वोट", @@ -509,8 +517,10 @@ "No photos or videos": "कोई फ़ोटो या वीडियो नहीं", "No pinned messages": "कोई पिन किया गया संदेश नहीं", "No results found": "कोई परिणाम नहीं मिला", + "No scheduled messages": "कोई शेड्यूल किया गया संदेश नहीं", "No user found": "कोई उपयोगकर्ता नहीं मिला", "Nobody will be able to vote in this poll anymore.": "अब कोई भी इस मतदान में मतदान नहीं कर सकेगा।", + "Not sent: the service was delayed": "नहीं भेजा गया: सेवा में देरी हुई", "Nothing yet...": "कोई मैसेज नहीं है", "Notify all {{ role }} members": "{{ role }} भूमिका वाले सभी सदस्यों को सूचित करें", "Offline": "ऑफलाइन", @@ -560,6 +570,7 @@ "Reading the digital tea leaves": "डिजिटल चाय की पत्तियाँ पढ़ रहा हूँ", "Ready": "तैयार", "Recording format is not supported and cannot be reproduced": "रेकॉर्डिंग फ़ॉर्मेट समर्थित नहीं है और पुनः उत्पन्न नहीं किया जा सकता", + "Rejected by the app": "ऐप द्वारा अस्वीकार किया गया", "Remind me": "मुझे याद दिलाएं", "Remind Me": "मुझे याद दिलाएं", "Reminder set": "अनुस्मारक सेट किया गया", @@ -592,8 +603,11 @@ "Saved for later": "बाद के लिए सहेजा गया", "Schedule": "शेड्यूल करें", "Schedule message": "संदेश शेड्यूल करें", + "Scheduled": "शेड्यूल किया गया", "Scheduled for {{ time }}": "{{ time }} के लिए शेड्यूल किया गया", + "Scheduled messages": "शेड्यूल किए गए संदेश", "Scheduled messages are turned off": "शेड्यूल किए गए संदेश बंद हैं", + "Scheduled replies": "शेड्यूल किए गए जवाब", "Search": "खोज", "Search GIFs": "GIF खोजें", "search-results-header-filter-source-button-label--channels": "चैनल्स", @@ -633,6 +647,7 @@ "Slow Mode ON": "स्लो मोड ऑन", "Slow mode, wait {{ seconds }}s...": "स्लो मोड, {{ seconds }} सेकंड प्रतीक्षा करें...", "Some of the files will not be accepted": "कुछ फ़ाइलें स्वीकार नहीं की जाएंगी", + "Something went wrong. Try again": "कुछ गलत हो गया। फिर से कोशिश करें", "Speaking": "बोल रहा है", "Start typing to search": "खोजने के लिए टाइप करना शुरू करें", "Stop sharing": "साझा करना बंद करें", @@ -642,9 +657,13 @@ "Summoning my inner genius": "अपने अंदर की प्रतिभा को बुला रहा हूँ", "Tap to remove": "हटाने के लिए टैप करें", "Tap to remove: {{ reactionName }}": "हटाने के लिए टैप करें: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "संदेश अब ऐप की सीमाओं को पूरा नहीं करता। इसे संपादित करें", "Thinking": "सोच रहा है", "Thinking really hard": "बहुत गहराई से सोच रहा हूँ", "Thinking...": "सोच रहा है...", + "This channel is frozen": "यह चैनल फ़्रीज़ है", + "This channel now holds messages for approval. Schedule it again": "यह चैनल अब संदेशों को स्वीकृति के लिए रोकता है। इसे फिर से शेड्यूल करें", + "This channel was deleted": "यह चैनल हटा दिया गया", "this content could not be displayed": "यह कॉन्टेंट लोड नहीं हो पाया", "This field cannot be empty or contain only spaces": "यह फ़ील्ड खाली नहीं हो सकता या केवल रिक्त स्थान नहीं रख सकता", "This message can't be scheduled": "यह संदेश शेड्यूल नहीं किया जा सकता", @@ -675,6 +694,7 @@ "timestamp/relativeYesterday": "बीता कल", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[आज] HH:mm\", \"nextDay\": \"[कल] HH:mm\", \"lastDay\": \"[बीता कल] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"[पिछले] dddd, HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[आज] HH:mm\", \"nextDay\": \"[कल] HH:mm\", \"lastDay\": \"[बीता कल] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"[पिछले] dddd, HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[आज]\", \"nextDay\": \"[कल]\", \"lastDay\": \"[बीता कल]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[पिछला] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "रिकॉर्डिंग शुरू करने के लिए, अपने ब्राउज़र में कैमरा तक पहुँच दें", "To start recording, allow the microphone access in your browser": "रिकॉर्डिंग शुरू करने के लिए, अपने ब्राउज़र में माइक्रोफ़ोन तक पहुँच दें", @@ -739,6 +759,8 @@ "Working my magic": "अपना जादू चला रहा हूँ", "You": "आप", "You can't schedule messages here": "आप यहां संदेश शेड्यूल नहीं कर सकते", + "You can't send messages in this channel": "आप इस चैनल में संदेश नहीं भेज सकते", + "You're no longer allowed to post here": "अब आपको यहाँ पोस्ट करने की अनुमति नहीं है", "You're offline. Reconnect to schedule this message.": "आप ऑफ़लाइन हैं। इस संदेश को शेड्यूल करने के लिए फिर से कनेक्ट करें।", "You've reached the limit of scheduled messages": "आप शेड्यूल किए गए संदेशों की सीमा तक पहुंच गए हैं", "You've reached the limit of scheduled messages in this channel": "आप इस चैनल में शेड्यूल किए गए संदेशों की सीमा तक पहुंच गए हैं", diff --git a/src/i18n/it.json b/src/i18n/it.json index c9c7d94e4..46f6b0b3c 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -19,6 +19,12 @@ "{{ count }} reactions_one": "{{ count }} reazione", "{{ count }} reactions_many": "{{ count }} reazioni", "{{ count }} reactions_other": "{{ count }} reazioni", + "{{ count }} scheduled messages_one": "{{ count }} messaggio programmato", + "{{ count }} scheduled messages_many": "{{ count }} messaggi programmati", + "{{ count }} scheduled messages_other": "{{ count }} messaggi programmati", + "{{ count }} scheduled replies_one": "{{ count }} risposta programmata", + "{{ count }} scheduled replies_many": "{{ count }} risposte programmate", + "{{ count }} scheduled replies_other": "{{ count }} risposte programmate", "{{ count }} videos_one": "{{ count }} video", "{{ count }} videos_many": "{{ count }} video", "{{ count }} videos_other": "{{ count }} video", @@ -260,6 +266,7 @@ "ban-command-description": "Vietare un utente", "Block user": "Blocca utente", "Block User": "Blocca utente", + "Blocked by moderation": "Bloccato dalla moderazione", "Brewing up an answer": "Sto preparando una risposta", "Browse channel members": "Sfoglia membri del canale", "Browse pinned messages": "Sfoglia messaggi fissati", @@ -366,6 +373,7 @@ "Error uploading image": "Errore durante il caricamento dell'immagine", "Error: {{ errorMessage }}": "Errore: {{ errorMessage }}", "Exit command {{ command }}": "Esci dal comando {{ command }}", + "Failed": "Non riuscito", "Failed to block user": "Impossibile bloccare l'utente", "Failed to create the poll": "Impossibile creare il sondaggio", "Failed to create the poll due to {{reason}}": "Impossibile creare il sondaggio a causa di {{reason}}", @@ -505,6 +513,8 @@ "Message unpinned": "Messaggio rimosso dai fissati", "Message was blocked by moderation policies": "Il messaggio è stato bloccato dalle politiche di moderazione", "Messages have been marked unread.": "I messaggi sono stati contrassegnati come non letti.", + "Messages waiting to be sent": "Messaggi in attesa di invio", + "Messages you schedule appear here": "I messaggi che programmi appaiono qui", "Missing permissions to upload the attachment": "Autorizzazioni mancanti per caricare l'allegato", "Moderator": "Moderatore", "Multiple votes": "Voti multipli", @@ -528,8 +538,10 @@ "No photos or videos": "Nessuna foto o video", "No pinned messages": "Nessun messaggio fissato", "No results found": "Nessun risultato trovato", + "No scheduled messages": "Nessun messaggio programmato", "No user found": "Nessun utente trovato", "Nobody will be able to vote in this poll anymore.": "Nessuno potrà più votare in questo sondaggio.", + "Not sent: the service was delayed": "Non inviato: il servizio ha subito un ritardo", "Nothing yet...": "Ancora niente...", "Notify all {{ role }} members": "Notifica tutti i membri con ruolo {{ role }}", "Offline": "Offline", @@ -579,6 +591,7 @@ "Reading the digital tea leaves": "Leggo i fondi di tè digitali", "Ready": "Pronto", "Recording format is not supported and cannot be reproduced": "Il formato di registrazione non è supportato e non può essere riprodotto", + "Rejected by the app": "Rifiutato dall'app", "Remind me": "Promemoria", "Remind Me": "Ricordami", "Reminder set": "Promemoria impostato", @@ -614,8 +627,11 @@ "Saved for later": "Salvato per dopo", "Schedule": "Programma", "Schedule message": "Programma messaggio", + "Scheduled": "Programmato", "Scheduled for {{ time }}": "Programmato per {{ time }}", + "Scheduled messages": "Messaggi programmati", "Scheduled messages are turned off": "I messaggi programmati sono disattivati", + "Scheduled replies": "Risposte programmate", "Search": "Cerca", "Search GIFs": "Cerca GIF", "search-results-header-filter-source-button-label--channels": "canali", @@ -657,6 +673,7 @@ "Slow Mode ON": "Modalità lenta attivata", "Slow mode, wait {{ seconds }}s...": "Modalità lenta, attendi {{ seconds }} s...", "Some of the files will not be accepted": "Alcuni dei file non saranno accettati", + "Something went wrong. Try again": "Si è verificato un errore. Riprova", "Speaking": "Sta parlando", "Start typing to search": "Inizia a digitare per cercare", "Stop sharing": "Ferma condivisione", @@ -666,9 +683,13 @@ "Summoning my inner genius": "Evoco il mio genio interiore", "Tap to remove": "Tocca per rimuovere", "Tap to remove: {{ reactionName }}": "Tocca per rimuovere: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "Il messaggio non rispetta più i limiti dell'app. Modificalo", "Thinking": "Sta pensando", "Thinking really hard": "Sto pensando intensamente", "Thinking...": "Pensando...", + "This channel is frozen": "Questo canale è congelato", + "This channel now holds messages for approval. Schedule it again": "Questo canale ora trattiene i messaggi per l'approvazione. Programmalo di nuovo", + "This channel was deleted": "Questo canale è stato eliminato", "this content could not be displayed": "questo contenuto non può essere mostrato", "This field cannot be empty or contain only spaces": "Questo campo non può essere vuoto o contenere solo spazi", "This message can't be scheduled": "Questo messaggio non può essere programmato", @@ -700,6 +721,7 @@ "timestamp/relativeYesterday": "Ieri", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Oggi] [alle] HH:mm\", \"nextDay\": \"[Domani] [alle] HH:mm\", \"lastDay\": \"[Ieri] [alle] HH:mm\", \"nextWeek\": \"dddd [alle] HH:mm\", \"lastWeek\": \"[lo scorso] dddd [alle] HH:mm\", \"sameElse\": \"ddd, D MMM [alle] HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Oggi] [alle] HH:mm\", \"nextDay\": \"[Domani] [alle] HH:mm\", \"lastDay\": \"[Ieri] [alle] HH:mm\", \"nextWeek\": \"dddd [alle] HH:mm\", \"lastWeek\": \"[lo scorso] dddd [alle] HH:mm\", \"sameElse\": \"ddd, D MMM [alle] HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Oggi]\", \"nextDay\": \"[Domani]\", \"lastDay\": \"[Ieri]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Scorsa] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Per iniziare a registrare, consenti l'accesso alla fotocamera nel tuo browser", "To start recording, allow the microphone access in your browser": "Per iniziare a registrare, consenti l'accesso al microfono nel tuo browser", @@ -768,6 +790,8 @@ "Working my magic": "Faccio la mia magia", "You": "Tu", "You can't schedule messages here": "Non puoi programmare messaggi qui", + "You can't send messages in this channel": "Non puoi inviare messaggi in questo canale", + "You're no longer allowed to post here": "Non sei più autorizzato a pubblicare qui", "You're offline. Reconnect to schedule this message.": "Sei offline. Riconnettiti per programmare questo messaggio.", "You've reached the limit of scheduled messages": "Hai raggiunto il limite di messaggi programmati", "You've reached the limit of scheduled messages in this channel": "Hai raggiunto il limite di messaggi programmati in questo canale", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 709864484..1e5f3348e 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -11,6 +11,8 @@ "{{ count }} photos_one": "{{ count }} 写真", "{{ count }} photos_other": "{{ count }} 写真", "{{ count }} reactions_other": "{{ count }}件のリアクション", + "{{ count }} scheduled messages_other": "{{ count }} 件の予約メッセージ", + "{{ count }} scheduled replies_other": "{{ count }} 件の予約返信", "{{ count }} videos_one": "{{ count }} 動画", "{{ count }} videos_other": "{{ count }} 動画", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} と {{ secondUser }}", @@ -234,6 +236,7 @@ "ban-command-description": "ユーザーを禁止する", "Block user": "ユーザーをブロック", "Block User": "ユーザーをブロック", + "Blocked by moderation": "モデレーションによりブロックされました", "Brewing up an answer": "回答を準備しています", "Browse channel members": "チャンネルメンバーを表示", "Browse pinned messages": "ピン留めメッセージを表示", @@ -340,6 +343,7 @@ "Error uploading image": "画像をアップロードのエラーが発生しました", "Error: {{ errorMessage }}": "エラー: {{ errorMessage }}", "Exit command {{ command }}": "コマンドを終了 {{ command }}", + "Failed": "失敗", "Failed to block user": "ユーザーのブロックに失敗しました", "Failed to create the poll": "投票の作成に失敗しました", "Failed to create the poll due to {{reason}}": "{{reason}} のため投票の作成に失敗しました", @@ -473,6 +477,8 @@ "Message unpinned": "メッセージのピン留めを解除しました", "Message was blocked by moderation policies": "メッセージはモデレーションポリシーによってブロックされました", "Messages have been marked unread.": "メッセージは未読としてマークされました。", + "Messages waiting to be sent": "送信待ちのメッセージ", + "Messages you schedule appear here": "予約したメッセージはここに表示されます", "Missing permissions to upload the attachment": "添付ファイルをアップロードするための許可がありません", "Moderator": "モデレーター", "Multiple votes": "複数投票", @@ -496,8 +502,10 @@ "No photos or videos": "写真や動画はありません", "No pinned messages": "ピン留めメッセージはありません", "No results found": "結果が見つかりません", + "No scheduled messages": "予約メッセージはありません", "No user found": "ユーザーが見つかりません", "Nobody will be able to vote in this poll anymore.": "この投票では、誰も投票できなくなります。", + "Not sent: the service was delayed": "未送信: サービスが遅延しました", "Nothing yet...": "まだ何もありません...", "Notify all {{ role }} members": "{{ role }} メンバー全員に通知", "Offline": "オフライン", @@ -547,6 +555,7 @@ "Reading the digital tea leaves": "デジタルの茶葉を読み解いています", "Ready": "準備完了", "Recording format is not supported and cannot be reproduced": "録音形式はサポートされておらず、再生できません", + "Rejected by the app": "アプリにより拒否されました", "Remind me": "リマインド", "Remind Me": "リマインダー", "Reminder set": "リマインダーを設定しました", @@ -577,8 +586,11 @@ "Saved for later": "後で保存済み", "Schedule": "予約", "Schedule message": "メッセージを予約", + "Scheduled": "予約済み", "Scheduled for {{ time }}": "{{ time }}に予約済み", + "Scheduled messages": "予約メッセージ", "Scheduled messages are turned off": "予約メッセージはオフになっています", + "Scheduled replies": "予約返信", "Search": "探す", "Search GIFs": "GIFを検索", "search-results-header-filter-source-button-label--channels": "チャンネル", @@ -618,6 +630,7 @@ "Slow Mode ON": "スローモードオン", "Slow mode, wait {{ seconds }}s...": "スローモード、{{ seconds }}秒お待ちください...", "Some of the files will not be accepted": "一部のファイルは受け付けられません", + "Something went wrong. Try again": "問題が発生しました。もう一度お試しください", "Speaking": "話しています", "Start typing to search": "検索するには入力を開始してください", "Stop sharing": "共有を停止", @@ -627,9 +640,13 @@ "Summoning my inner genius": "内なる天才を呼び覚ましています", "Tap to remove": "タップして削除", "Tap to remove: {{ reactionName }}": "タップして削除: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "メッセージがアプリの制限を満たさなくなりました。編集してください", "Thinking": "考え中", "Thinking really hard": "一生懸命考えています", "Thinking...": "考え中...", + "This channel is frozen": "このチャンネルは凍結されています", + "This channel now holds messages for approval. Schedule it again": "このチャンネルではメッセージが承認待ちになります。もう一度予約してください", + "This channel was deleted": "このチャンネルは削除されました", "this content could not be displayed": "このコンテンツは表示できませんでした", "This field cannot be empty or contain only spaces": "このフィールドは空にすることはできません。また、空白文字のみを含むこともできません", "This message can't be scheduled": "このメッセージは予約できません", @@ -659,6 +676,7 @@ "timestamp/relativeYesterday": "昨日", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[今日] HH:mm\", \"nextDay\": \"[明日] HH:mm\", \"lastDay\": \"[昨日] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"dddd HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[今日] HH:mm\", \"nextDay\": \"[明日] HH:mm\", \"lastDay\": \"[昨日] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"dddd HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[今日]\", \"nextDay\": \"[明日]\", \"lastDay\": \"[昨日]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[先週の] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "録音を開始するには、ブラウザーでカメラへのアクセスを許可してください", "To start recording, allow the microphone access in your browser": "録音を開始するには、ブラウザーでマイクロフォンへのアクセスを許可してください", @@ -720,6 +738,8 @@ "Working my magic": "魔法をかけています", "You": "あなた", "You can't schedule messages here": "ここではメッセージを予約できません", + "You can't send messages in this channel": "このチャンネルではメッセージを送信できません", + "You're no longer allowed to post here": "ここに投稿する権限がなくなりました", "You're offline. Reconnect to schedule this message.": "オフラインです。このメッセージを予約するには再接続してください。", "You've reached the limit of scheduled messages": "予約メッセージの上限に達しました", "You've reached the limit of scheduled messages in this channel": "このチャンネルの予約メッセージの上限に達しました", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 4517d2688..49f0ed7f6 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -11,6 +11,8 @@ "{{ count }} photos_one": "{{ count }}개 사진", "{{ count }} photos_other": "{{ count }}개 사진", "{{ count }} reactions_other": "{{ count }}개 반응", + "{{ count }} scheduled messages_other": "예약된 메시지 {{ count }}개", + "{{ count }} scheduled replies_other": "예약된 답장 {{ count }}개", "{{ count }} videos_one": "{{ count }}개 동영상", "{{ count }} videos_other": "{{ count }}개 동영상", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} 그리고 {{ secondUser }}", @@ -234,6 +236,7 @@ "ban-command-description": "사용자를 차단", "Block user": "사용자 차단", "Block User": "사용자 차단", + "Blocked by moderation": "검토에 의해 차단됨", "Brewing up an answer": "답변을 만들고 있어요", "Browse channel members": "채널 멤버 보기", "Browse pinned messages": "고정된 메시지 보기", @@ -340,6 +343,7 @@ "Error uploading image": "이미지를 업로드하는 동안 오류가 발생했습니다.", "Error: {{ errorMessage }}": "오류: {{ errorMessage }}", "Exit command {{ command }}": "명령 종료 {{ command }}", + "Failed": "실패", "Failed to block user": "사용자 차단에 실패했습니다", "Failed to create the poll": "투표 생성 실패", "Failed to create the poll due to {{reason}}": "{{reason}} 때문에 투표를 생성하지 못했습니다", @@ -473,6 +477,8 @@ "Message unpinned": "메시지 고정이 해제됨", "Message was blocked by moderation policies": "메시지가 관리 정책에 의해 차단되었습니다.", "Messages have been marked unread.": "메시지가 읽지 않음으로 표시되었습니다.", + "Messages waiting to be sent": "전송 대기 중인 메시지", + "Messages you schedule appear here": "예약한 메시지가 여기에 표시됩니다", "Missing permissions to upload the attachment": "첨부 파일을 업로드하려면 권한이 필요합니다", "Moderator": "운영자", "Multiple votes": "복수 투표", @@ -496,8 +502,10 @@ "No photos or videos": "사진 또는 동영상이 없습니다", "No pinned messages": "고정된 메시지가 없습니다", "No results found": "검색 결과가 없습니다", + "No scheduled messages": "예약된 메시지가 없습니다", "No user found": "사용자를 찾을 수 없습니다", "Nobody will be able to vote in this poll anymore.": "이 투표에 더 이상 아무도 투표할 수 없습니다.", + "Not sent: the service was delayed": "전송되지 않음: 서비스가 지연되었습니다", "Nothing yet...": "아직 아무것도...", "Notify all {{ role }} members": "{{ role }} 역할의 모든 멤버에게 알림", "Offline": "오프라인", @@ -547,6 +555,7 @@ "Reading the digital tea leaves": "디지털 찻잎을 읽는 중", "Ready": "준비됨", "Recording format is not supported and cannot be reproduced": "녹음 형식이 지원되지 않으므로 재생할 수 없습니다", + "Rejected by the app": "앱에서 거부됨", "Remind me": "알림", "Remind Me": "알림 설정", "Reminder set": "알림 설정됨", @@ -577,8 +586,11 @@ "Saved for later": "나중에 저장됨", "Schedule": "예약", "Schedule message": "메시지 예약", + "Scheduled": "예약됨", "Scheduled for {{ time }}": "{{ time }}에 예약됨", + "Scheduled messages": "예약된 메시지", "Scheduled messages are turned off": "예약 메시지가 꺼져 있습니다", + "Scheduled replies": "예약된 답장", "Search": "찾다", "Search GIFs": "GIF 검색", "search-results-header-filter-source-button-label--channels": "채널", @@ -618,6 +630,7 @@ "Slow Mode ON": "슬로우 모드 켜짐", "Slow mode, wait {{ seconds }}s...": "슬로우 모드, {{ seconds }}초 기다려 주세요...", "Some of the files will not be accepted": "일부 파일은 허용되지 않을 수 있습니다", + "Something went wrong. Try again": "문제가 발생했습니다. 다시 시도하세요", "Speaking": "말하는 중", "Start typing to search": "검색하려면 입력을 시작하세요", "Stop sharing": "공유 중지", @@ -627,9 +640,13 @@ "Summoning my inner genius": "내면의 천재를 불러내는 중", "Tap to remove": "제거하려면 탭하세요", "Tap to remove: {{ reactionName }}": "제거하려면 탭하세요: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "메시지가 더 이상 앱의 제한을 충족하지 않습니다. 수정하세요", "Thinking": "생각 중", "Thinking really hard": "열심히 생각하는 중", "Thinking...": "생각 중...", + "This channel is frozen": "이 채널은 동결되었습니다", + "This channel now holds messages for approval. Schedule it again": "이 채널은 이제 메시지를 승인 대기 상태로 둡니다. 다시 예약하세요", + "This channel was deleted": "이 채널은 삭제되었습니다", "this content could not be displayed": "이 콘텐츠를 표시할 수 없습니다", "This field cannot be empty or contain only spaces": "이 필드는 비워둘 수 없으며 공백만 포함할 수도 없습니다", "This message can't be scheduled": "이 메시지는 예약할 수 없습니다", @@ -659,6 +676,7 @@ "timestamp/relativeYesterday": "어제", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[오늘] HH:mm\", \"nextDay\": \"[내일] HH:mm\", \"lastDay\": \"[어제] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"[지난] dddd HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[오늘] HH:mm\", \"nextDay\": \"[내일] HH:mm\", \"lastDay\": \"[어제] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"[지난] dddd HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[오늘]\", \"nextDay\": \"[내일]\", \"lastDay\": \"[어제]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[지난] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "브라우저에서 카메라 액세스를 허용하여 녹음을 시작합니다", "To start recording, allow the microphone access in your browser": "브라우저에서 마이크로폰 액세스를 허용하여 녹음을 시작합니다", @@ -720,6 +738,8 @@ "Working my magic": "마법을 부리는 중", "You": "당신", "You can't schedule messages here": "여기에서는 메시지를 예약할 수 없습니다", + "You can't send messages in this channel": "이 채널에서는 메시지를 보낼 수 없습니다", + "You're no longer allowed to post here": "더 이상 여기에 게시할 수 없습니다", "You're offline. Reconnect to schedule this message.": "오프라인 상태입니다. 이 메시지를 예약하려면 다시 연결하세요.", "You've reached the limit of scheduled messages": "예약 메시지 한도에 도달했습니다", "You've reached the limit of scheduled messages in this channel": "이 채널의 예약 메시지 한도에 도달했습니다", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index a6f0ff393..9f2a95a19 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -14,6 +14,10 @@ "{{ count }} photos_other": "{{ count }} foto's", "{{ count }} reactions_one": "{{ count }} reactie", "{{ count }} reactions_other": "{{ count }} reacties", + "{{ count }} scheduled messages_one": "{{ count }} ingepland bericht", + "{{ count }} scheduled messages_other": "{{ count }} ingeplande berichten", + "{{ count }} scheduled replies_one": "{{ count }} ingepland antwoord", + "{{ count }} scheduled replies_other": "{{ count }} ingeplande antwoorden", "{{ count }} videos_one": "{{ count }} video", "{{ count }} videos_other": "{{ count }} video's", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} en {{ secondUser }}", @@ -243,6 +247,7 @@ "ban-command-description": "Een gebruiker verbannen", "Block user": "Gebruiker blokkeren", "Block User": "Gebruiker blokkeren", + "Blocked by moderation": "Geblokkeerd door moderatie", "Brewing up an answer": "Ik brouw een antwoord", "Browse channel members": "Kanaalleden bekijken", "Browse pinned messages": "Vastgemaakte berichten bekijken", @@ -349,6 +354,7 @@ "Error uploading image": "Fout bij uploaden afbeelding", "Error: {{ errorMessage }}": "Fout: {{ errorMessage }}", "Exit command {{ command }}": "Opdracht verlaten {{ command }}", + "Failed": "Mislukt", "Failed to block user": "Gebruiker blokkeren mislukt", "Failed to create the poll": "Fout bij het maken van de peiling", "Failed to create the poll due to {{reason}}": "Peiling kon niet worden aangemaakt vanwege {{reason}}", @@ -485,6 +491,8 @@ "Message unpinned": "Bericht losgemaakt", "Message was blocked by moderation policies": "Bericht is geblokkeerd door moderatiebeleid", "Messages have been marked unread.": "Berichten zijn gemarkeerd als ongelezen.", + "Messages waiting to be sent": "Berichten die wachten op verzending", + "Messages you schedule appear here": "Berichten die je inplant verschijnen hier", "Missing permissions to upload the attachment": "Missende toestemmingen om de bijlage te uploaden", "Moderator": "Moderator", "Multiple votes": "Meerdere stemmen", @@ -508,8 +516,10 @@ "No photos or videos": "Geen foto's of video's", "No pinned messages": "Geen vastgemaakte berichten", "No results found": "Geen resultaten gevonden", + "No scheduled messages": "Geen ingeplande berichten", "No user found": "Geen gebruiker gevonden", "Nobody will be able to vote in this poll anymore.": "Niemand kan meer stemmen in deze peiling.", + "Not sent: the service was delayed": "Niet verzonden: de dienst had vertraging", "Nothing yet...": "Nog niets ...", "Notify all {{ role }} members": "Alle leden met rol {{ role }} informeren", "Offline": "Offline", @@ -559,6 +569,7 @@ "Reading the digital tea leaves": "Ik lees de digitale theeblaadjes", "Ready": "Klaar", "Recording format is not supported and cannot be reproduced": "Opnameformaat wordt niet ondersteund en kan niet worden gereproduceerd", + "Rejected by the app": "Geweigerd door de app", "Remind me": "Herinner me", "Remind Me": "Herinner mij", "Reminder set": "Herinnering ingesteld", @@ -591,8 +602,11 @@ "Saved for later": "Bewaard voor later", "Schedule": "Inplannen", "Schedule message": "Bericht inplannen", + "Scheduled": "Ingepland", "Scheduled for {{ time }}": "Ingepland voor {{ time }}", + "Scheduled messages": "Ingeplande berichten", "Scheduled messages are turned off": "Ingeplande berichten zijn uitgeschakeld", + "Scheduled replies": "Ingeplande antwoorden", "Search": "Zoeken", "Search GIFs": "GIF's zoeken", "search-results-header-filter-source-button-label--channels": "kanalen", @@ -634,6 +648,7 @@ "Slow wait, wait {{ seconds }}s": "Langzame modus, wacht {{ seconds }}s", "Slow wait, wait {{ seconds }}s...": "Langzame modus, wacht {{ seconds }}s...", "Some of the files will not be accepted": "Sommige bestanden zullen niet worden geaccepteerd", + "Something went wrong. Try again": "Er is iets misgegaan. Probeer het opnieuw", "Speaking": "Spreekt", "Start typing to search": "Begin met typen om te zoeken", "Stop sharing": "Delen stoppen", @@ -643,9 +658,13 @@ "Summoning my inner genius": "Ik roep mijn innerlijke genie op", "Tap to remove": "Tik om te verwijderen", "Tap to remove: {{ reactionName }}": "Tik om te verwijderen: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "Het bericht voldoet niet meer aan de limieten van de app. Bewerk het", "Thinking": "Denkt na", "Thinking really hard": "Ik denk heel diep na", "Thinking...": "Denken...", + "This channel is frozen": "Dit kanaal is bevroren", + "This channel now holds messages for approval. Schedule it again": "Dit kanaal houdt berichten nu vast voor goedkeuring. Plan het opnieuw in", + "This channel was deleted": "Dit kanaal is verwijderd", "this content could not be displayed": "Deze inhoud kan niet weergegeven worden", "This field cannot be empty or contain only spaces": "Dit veld mag niet leeg zijn of alleen spaties bevatten", "This message can't be scheduled": "Dit bericht kan niet worden ingepland", @@ -676,6 +695,7 @@ "timestamp/relativeYesterday": "Gisteren", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Vandaag] [om] HH:mm\", \"nextDay\": \"[Morgen] [om] HH:mm\", \"lastDay\": \"[Gisteren] [om] HH:mm\", \"nextWeek\": \"dddd [om] HH:mm\", \"lastWeek\": \"[afgelopen] dddd [om] HH:mm\", \"sameElse\": \"ddd, D MMM [om] HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Vandaag] [om] HH:mm\", \"nextDay\": \"[Morgen] [om] HH:mm\", \"lastDay\": \"[Gisteren] [om] HH:mm\", \"nextWeek\": \"dddd [om] HH:mm\", \"lastWeek\": \"[afgelopen] dddd [om] HH:mm\", \"sameElse\": \"ddd, D MMM [om] HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Vandaag]\", \"nextDay\": \"[Morgen]\", \"lastDay\": \"[Gisteren]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Laatste] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Om te beginnen met opnemen, sta toegang tot de camera toe in uw browser", "To start recording, allow the microphone access in your browser": "Om te beginnen met opnemen, sta toegang tot de microfoon toe in uw browser", @@ -740,6 +760,8 @@ "Working my magic": "Ik doe mijn toverkunsten", "You": "Jij", "You can't schedule messages here": "Je kunt hier geen berichten inplannen", + "You can't send messages in this channel": "Je kunt geen berichten sturen in dit kanaal", + "You're no longer allowed to post here": "Je mag hier niet meer posten", "You're offline. Reconnect to schedule this message.": "Je bent offline. Maak opnieuw verbinding om dit bericht in te plannen.", "You've reached the limit of scheduled messages": "Je hebt de limiet voor ingeplande berichten bereikt", "You've reached the limit of scheduled messages in this channel": "Je hebt de limiet voor ingeplande berichten in dit kanaal bereikt", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 1bea0ce3a..cb43fdcf3 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -19,6 +19,12 @@ "{{ count }} reactions_one": "{{ count }} reação", "{{ count }} reactions_many": "{{ count }} reações", "{{ count }} reactions_other": "{{ count }} reações", + "{{ count }} scheduled messages_one": "{{ count }} mensagem agendada", + "{{ count }} scheduled messages_many": "{{ count }} mensagens agendadas", + "{{ count }} scheduled messages_other": "{{ count }} mensagens agendadas", + "{{ count }} scheduled replies_one": "{{ count }} resposta agendada", + "{{ count }} scheduled replies_many": "{{ count }} respostas agendadas", + "{{ count }} scheduled replies_other": "{{ count }} respostas agendadas", "{{ count }} videos_one": "{{ count }} vídeo", "{{ count }} videos_many": "{{ count }} vídeos", "{{ count }} videos_other": "{{ count }} vídeos", @@ -260,6 +266,7 @@ "ban-command-description": "Banir um usuário", "Block user": "Bloquear usuário", "Block User": "Bloquear usuário", + "Blocked by moderation": "Bloqueada pela moderação", "Brewing up an answer": "Preparando uma resposta", "Browse channel members": "Ver membros do canal", "Browse pinned messages": "Ver mensagens fixadas", @@ -366,6 +373,7 @@ "Error uploading image": "Erro ao carregar a imagem", "Error: {{ errorMessage }}": "Erro: {{ errorMessage }}", "Exit command {{ command }}": "Sair do comando {{ command }}", + "Failed": "Falhou", "Failed to block user": "Falha ao bloquear o usuário", "Failed to create the poll": "Falha ao criar a pesquisa", "Failed to create the poll due to {{reason}}": "Falha ao criar a enquete devido a {{reason}}", @@ -505,6 +513,8 @@ "Message unpinned": "Mensagem desafixada", "Message was blocked by moderation policies": "A mensagem foi bloqueada pelas políticas de moderação", "Messages have been marked unread.": "Mensagens foram marcadas como não lidas.", + "Messages waiting to be sent": "Mensagens aguardando envio", + "Messages you schedule appear here": "As mensagens que você agendar aparecem aqui", "Missing permissions to upload the attachment": "Faltando permissões para enviar o anexo", "Moderator": "Moderador", "Multiple votes": "Votos múltiplos", @@ -528,8 +538,10 @@ "No photos or videos": "Nenhuma foto ou vídeo", "No pinned messages": "Nenhuma mensagem fixada", "No results found": "Nenhum resultado encontrado", + "No scheduled messages": "Nenhuma mensagem agendada", "No user found": "Nenhum usuário encontrado", "Nobody will be able to vote in this poll anymore.": "Ninguém mais poderá votar nesta pesquisa.", + "Not sent: the service was delayed": "Não enviada: o serviço atrasou", "Nothing yet...": "Nada ainda...", "Notify all {{ role }} members": "Notificar todos os membros com a função {{ role }}", "Offline": "Offline", @@ -579,6 +591,7 @@ "Reading the digital tea leaves": "Lendo as folhas de chá digitais", "Ready": "Pronto", "Recording format is not supported and cannot be reproduced": "Formato de gravação não é suportado e não pode ser reproduzido", + "Rejected by the app": "Rejeitada pelo app", "Remind me": "Lembrar-me", "Remind Me": "Lembrar-me", "Reminder set": "Lembrete definido", @@ -614,8 +627,11 @@ "Saved for later": "Salvo para depois", "Schedule": "Agendar", "Schedule message": "Agendar mensagem", + "Scheduled": "Agendada", "Scheduled for {{ time }}": "Agendada para {{ time }}", + "Scheduled messages": "Mensagens agendadas", "Scheduled messages are turned off": "As mensagens agendadas estão desativadas", + "Scheduled replies": "Respostas agendadas", "Search": "Buscar", "Search GIFs": "Pesquisar GIFs", "search-results-header-filter-source-button-label--channels": "canais", @@ -657,6 +673,7 @@ "Slow Mode ON": "Modo lento LIGADO", "Slow mode, wait {{ seconds }}s...": "Modo lento, aguarde {{ seconds }} s...", "Some of the files will not be accepted": "Alguns arquivos não serão aceitos", + "Something went wrong. Try again": "Algo deu errado. Tente novamente", "Speaking": "Falando", "Start typing to search": "Comece a digitar para pesquisar", "Stop sharing": "Parar de compartilhar", @@ -666,9 +683,13 @@ "Summoning my inner genius": "Invocando meu gênio interior", "Tap to remove": "Toque para remover", "Tap to remove: {{ reactionName }}": "Toque para remover: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "A mensagem não atende mais aos limites do app. Edite-a", "Thinking": "Pensando", "Thinking really hard": "Pensando bem forte", "Thinking...": "Pensando...", + "This channel is frozen": "Este canal está congelado", + "This channel now holds messages for approval. Schedule it again": "Este canal agora retém mensagens para aprovação. Agende-a novamente", + "This channel was deleted": "Este canal foi excluído", "this content could not be displayed": "este conteúdo não pôde ser exibido", "This field cannot be empty or contain only spaces": "Este campo não pode estar vazio ou conter apenas espaços", "This message can't be scheduled": "Esta mensagem não pode ser agendada", @@ -700,6 +721,7 @@ "timestamp/relativeYesterday": "Ontem", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoje] [às] HH:mm\", \"nextDay\": \"[Amanhã] [às] HH:mm\", \"lastDay\": \"[Ontem] [às] HH:mm\", \"nextWeek\": \"dddd [às] HH:mm\", \"lastWeek\": \"dddd [passada às] HH:mm\", \"sameElse\": \"ddd, D MMM [às] HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoje] [às] HH:mm\", \"nextDay\": \"[Amanhã] [às] HH:mm\", \"lastDay\": \"[Ontem] [às] HH:mm\", \"nextWeek\": \"dddd [às] HH:mm\", \"lastWeek\": \"dddd [passada às] HH:mm\", \"sameElse\": \"ddd, D MMM [às] HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoje]\", \"nextDay\": \"[Amanhã]\", \"lastDay\": \"[Ontem]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Último] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Para começar a gravar, permita o acesso à câmera no seu navegador", "To start recording, allow the microphone access in your browser": "Para começar a gravar, permita o acesso ao microfone no seu navegador", @@ -768,6 +790,8 @@ "Working my magic": "Fazendo minha mágica", "You": "Você", "You can't schedule messages here": "Você não pode agendar mensagens aqui", + "You can't send messages in this channel": "Você não pode enviar mensagens neste canal", + "You're no longer allowed to post here": "Você não tem mais permissão para postar aqui", "You're offline. Reconnect to schedule this message.": "Você está offline. Reconecte-se para agendar esta mensagem.", "You've reached the limit of scheduled messages": "Você atingiu o limite de mensagens agendadas", "You've reached the limit of scheduled messages in this channel": "Você atingiu o limite de mensagens agendadas neste canal", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 448c87dd6..0ed145eda 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -25,6 +25,14 @@ "{{ count }} reactions_few": "{{ count }} реакции", "{{ count }} reactions_many": "{{ count }} реакций", "{{ count }} reactions_other": "{{ count }} реакций", + "{{ count }} scheduled messages_one": "{{ count }} запланированное сообщение", + "{{ count }} scheduled messages_few": "{{ count }} запланированных сообщения", + "{{ count }} scheduled messages_many": "{{ count }} запланированных сообщений", + "{{ count }} scheduled messages_other": "{{ count }} запланированного сообщения", + "{{ count }} scheduled replies_one": "{{ count }} запланированный ответ", + "{{ count }} scheduled replies_few": "{{ count }} запланированных ответа", + "{{ count }} scheduled replies_many": "{{ count }} запланированных ответов", + "{{ count }} scheduled replies_other": "{{ count }} запланированного ответа", "{{ count }} videos_one": "{{ count }} видео", "{{ count }} videos_few": "{{ count }} видео", "{{ count }} videos_many": "{{ count }} видео", @@ -278,6 +286,7 @@ "ban-command-description": "Заблокировать пользователя", "Block user": "Заблокировать пользователя", "Block User": "Заблокировать пользователя", + "Blocked by moderation": "Заблокировано модерацией", "Brewing up an answer": "Готовлю ответ", "Browse channel members": "Просмотреть участников канала", "Browse pinned messages": "Просмотреть закрепленные сообщения", @@ -384,6 +393,7 @@ "Error uploading image": "Ошибка загрузки изображения", "Error: {{ errorMessage }}": "Ошибка: {{ errorMessage }}", "Exit command {{ command }}": "Выйти из команды {{ command }}", + "Failed": "Ошибка", "Failed to block user": "Не удалось заблокировать пользователя", "Failed to create the poll": "Не удалось создать опрос", "Failed to create the poll due to {{reason}}": "Не удалось создать опрос из-за {{reason}}", @@ -529,6 +539,8 @@ "Message unpinned": "Сообщение откреплено", "Message was blocked by moderation policies": "Сообщение было заблокировано модерацией", "Messages have been marked unread.": "Сообщения были отмечены как непрочитанные.", + "Messages waiting to be sent": "Сообщения, ожидающие отправки", + "Messages you schedule appear here": "Здесь появятся запланированные вами сообщения", "Missing permissions to upload the attachment": "Отсутствуют разрешения для загрузки вложения", "Moderator": "Модератор", "Multiple votes": "Несколько голосов", @@ -552,8 +564,10 @@ "No photos or videos": "Нет фото или видео", "No pinned messages": "Нет закрепленных сообщений", "No results found": "Результаты не найдены", + "No scheduled messages": "Нет запланированных сообщений", "No user found": "Пользователь не найден", "Nobody will be able to vote in this poll anymore.": "Никто больше не сможет голосовать в этом опросе.", + "Not sent: the service was delayed": "Не отправлено: сервис задержался", "Nothing yet...": "Пока ничего нет...", "Notify all {{ role }} members": "Уведомить всех участников с ролью {{ role }}", "Offline": "Не в сети", @@ -603,6 +617,7 @@ "Reading the digital tea leaves": "Гадаю на цифровой чайной заварке", "Ready": "Готов", "Recording format is not supported and cannot be reproduced": "Формат записи не поддерживается и не может быть воспроизведен", + "Rejected by the app": "Отклонено приложением", "Remind me": "Напомнить мне", "Remind Me": "Напомнить мне", "Reminder set": "Напоминание установлено", @@ -641,8 +656,11 @@ "Saved for later": "Сохранено на потом", "Schedule": "Запланировать", "Schedule message": "Запланировать сообщение", + "Scheduled": "Запланировано", "Scheduled for {{ time }}": "Запланировано на {{ time }}", + "Scheduled messages": "Запланированные сообщения", "Scheduled messages are turned off": "Запланированные сообщения отключены", + "Scheduled replies": "Запланированные ответы", "Search": "Поиск", "Search GIFs": "Поиск GIF", "search-results-header-filter-source-button-label--channels": "каналы", @@ -686,6 +704,7 @@ "Slow Mode ON": "Медленный режим включен", "Slow mode, wait {{ seconds }}s...": "Медленный режим: подождите {{ seconds }} с...", "Some of the files will not be accepted": "Некоторые файлы не будут приняты", + "Something went wrong. Try again": "Что-то пошло не так. Попробуйте ещё раз", "Speaking": "Говорит", "Start typing to search": "Начните вводить для поиска", "Stop sharing": "Прекратить делиться", @@ -695,9 +714,13 @@ "Summoning my inner genius": "Пробуждаю внутреннего гения", "Tap to remove": "Нажмите, чтобы удалить", "Tap to remove: {{ reactionName }}": "Нажмите, чтобы удалить: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "Сообщение больше не соответствует ограничениям приложения. Отредактируйте его", "Thinking": "Думает", "Thinking really hard": "Усердно думаю", "Thinking...": "Думаю...", + "This channel is frozen": "Этот канал заморожен", + "This channel now holds messages for approval. Schedule it again": "Теперь этот канал отправляет сообщения на одобрение. Запланируйте его снова", + "This channel was deleted": "Этот канал был удалён", "this content could not be displayed": "Этот контент не может быть отображен в данный момент", "This field cannot be empty or contain only spaces": "Это поле не может быть пустым или содержать только пробелы", "This message can't be scheduled": "Это сообщение нельзя запланировать", @@ -730,6 +753,7 @@ "timestamp/relativeYesterday": "Вчера", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Сегодня] [в] HH:mm\", \"nextDay\": \"[Завтра] [в] HH:mm\", \"lastDay\": \"[Вчера] [в] HH:mm\", \"nextWeek\": \"dddd [в] HH:mm\", \"lastWeek\": \"dddd [в] HH:mm\", \"sameElse\": \"ddd, D MMM [в] HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Сегодня] [в] HH:mm\", \"nextDay\": \"[Завтра] [в] HH:mm\", \"lastDay\": \"[Вчера] [в] HH:mm\", \"nextWeek\": \"dddd [в] HH:mm\", \"lastWeek\": \"dddd [в] HH:mm\", \"sameElse\": \"ddd, D MMM [в] HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Сегодня]\", \"nextDay\": \"[Завтра]\", \"lastDay\": \"[Вчера]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[В прошлый] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Для начала записи разрешите доступ к камере в вашем браузере", "To start recording, allow the microphone access in your browser": "Для начала записи разрешите доступ к микрофону в вашем браузере", @@ -802,6 +826,8 @@ "Working my magic": "Колдую", "You": "Вы", "You can't schedule messages here": "Здесь нельзя планировать сообщения", + "You can't send messages in this channel": "Вы не можете отправлять сообщения в этот канал", + "You're no longer allowed to post here": "Вам больше нельзя публиковать здесь", "You're offline. Reconnect to schedule this message.": "Вы не в сети. Подключитесь снова, чтобы запланировать это сообщение.", "You've reached the limit of scheduled messages": "Вы достигли лимита запланированных сообщений", "You've reached the limit of scheduled messages in this channel": "Вы достигли лимита запланированных сообщений в этом канале", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index e2a3f0c58..a39edcb4c 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -14,6 +14,10 @@ "{{ count }} photos_other": "{{ count }} fotoğraf", "{{ count }} reactions_one": "{{ count }} tepki", "{{ count }} reactions_other": "{{ count }} tepki", + "{{ count }} scheduled messages_one": "{{ count }} zamanlanmış mesaj", + "{{ count }} scheduled messages_other": "{{ count }} zamanlanmış mesaj", + "{{ count }} scheduled replies_one": "{{ count }} zamanlanmış yanıt", + "{{ count }} scheduled replies_other": "{{ count }} zamanlanmış yanıt", "{{ count }} videos_one": "{{ count }} video", "{{ count }} videos_other": "{{ count }} video", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} ve {{ secondUser }}", @@ -243,6 +247,7 @@ "ban-command-description": "Bir kullanıcıyı yasakla", "Block user": "Kullanıcıyı engelle", "Block User": "Kullanıcıyı engelle", + "Blocked by moderation": "Moderasyon tarafından engellendi", "Brewing up an answer": "Bir cevap hazırlıyorum", "Browse channel members": "Kanal üyelerine göz at", "Browse pinned messages": "Sabitlenmiş mesajlara göz at", @@ -349,6 +354,7 @@ "Error uploading image": "Resmi yüklerken hata", "Error: {{ errorMessage }}": "Hata: {{ errorMessage }}", "Exit command {{ command }}": "Komuttan çık {{ command }}", + "Failed": "Başarısız", "Failed to block user": "Kullanıcı engellenemedi", "Failed to create the poll": "Anket oluşturulurken hata oluştu", "Failed to create the poll due to {{reason}}": "{{reason}} nedeniyle anket oluşturulamadı", @@ -485,6 +491,8 @@ "Message unpinned": "Mesaj sabitlemesi kaldırıldı", "Message was blocked by moderation policies": "Mesaj moderasyon politikaları tarafından engellendi", "Messages have been marked unread.": "Mesajlar okunmamış olarak işaretlendi.", + "Messages waiting to be sent": "Gönderilmeyi bekleyen mesajlar", + "Messages you schedule appear here": "Zamanladığınız mesajlar burada görünür", "Missing permissions to upload the attachment": "Ek yüklemek için izinler eksik", "Moderator": "Moderatör", "Multiple votes": "Çoklu oy", @@ -508,8 +516,10 @@ "No photos or videos": "Fotoğraf veya video yok", "No pinned messages": "Sabitlenmiş mesaj yok", "No results found": "Sonuç bulunamadı", + "No scheduled messages": "Zamanlanmış mesaj yok", "No user found": "Kullanıcı bulunamadı", "Nobody will be able to vote in this poll anymore.": "Artık bu ankette kimse oy kullanamayacak.", + "Not sent: the service was delayed": "Gönderilmedi: hizmet gecikti", "Nothing yet...": "Şimdilik hiçbir şey...", "Notify all {{ role }} members": "{{ role }} rolündeki tüm üyelere bildir", "Offline": "Çevrimdışı", @@ -559,6 +569,7 @@ "Reading the digital tea leaves": "Dijital çay yapraklarını okuyorum", "Ready": "Hazır", "Recording format is not supported and cannot be reproduced": "Kayıt formatı desteklenmiyor ve çoğaltılamıyor", + "Rejected by the app": "Uygulama tarafından reddedildi", "Remind me": "Bana hatırlat", "Remind Me": "Hatırlat", "Reminder set": "Hatırlatıcı ayarlandı", @@ -591,8 +602,11 @@ "Saved for later": "Daha sonra kaydedildi", "Schedule": "Zamanla", "Schedule message": "Mesajı zamanla", + "Scheduled": "Zamanlandı", "Scheduled for {{ time }}": "{{ time }} için zamanlandı", + "Scheduled messages": "Zamanlanmış mesajlar", "Scheduled messages are turned off": "Zamanlanmış mesajlar kapalı", + "Scheduled replies": "Zamanlanmış yanıtlar", "Search": "Arama", "Search GIFs": "GIF ara", "search-results-header-filter-source-button-label--channels": "kanallar", @@ -632,6 +646,7 @@ "Slow Mode ON": "Yavaş Mod Açık", "Slow mode, wait {{ seconds }}s...": "Yavaş mod, {{ seconds }} sn bekleyin...", "Some of the files will not be accepted": "Bazı dosyalar kabul edilmeyecek", + "Something went wrong. Try again": "Bir şeyler ters gitti. Tekrar deneyin", "Speaking": "Konuşuyor", "Start typing to search": "Aramak için yazmaya başlayın", "Stop sharing": "Paylaşımı durdur", @@ -641,9 +656,13 @@ "Summoning my inner genius": "İçimdeki dahiyi uyandırıyorum", "Tap to remove": "Kaldırmak için dokunun", "Tap to remove: {{ reactionName }}": "Kaldırmak için dokunun: {{ reactionName }}", + "The message no longer meets the app's limits. Edit it": "Mesaj artık uygulamanın sınırlarını karşılamıyor. Düzenleyin", "Thinking": "Düşünüyor", "Thinking really hard": "Çok derin düşünüyorum", "Thinking...": "Düşünüyor...", + "This channel is frozen": "Bu kanal donduruldu", + "This channel now holds messages for approval. Schedule it again": "Bu kanal artık mesajları onay için bekletiyor. Yeniden zamanlayın", + "This channel was deleted": "Bu kanal silindi", "this content could not be displayed": "bu içerik gösterilemiyor", "This field cannot be empty or contain only spaces": "Bu alan boş olamaz veya sadece boşluk içeremez", "This message can't be scheduled": "Bu mesaj zamanlanamaz", @@ -674,6 +693,7 @@ "timestamp/relativeYesterday": "Dün", "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Bugün] [saat] HH:mm\", \"nextDay\": \"[Yarın] [saat] HH:mm\", \"lastDay\": \"[Dün] [saat] HH:mm\", \"nextWeek\": \"dddd [saat] HH:mm\", \"lastWeek\": \"[Geçen] dddd [saat] HH:mm\", \"sameElse\": \"ddd, D MMM [saat] HH:mm\" }) }}", "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Bugün] [saat] HH:mm\", \"nextDay\": \"[Yarın] [saat] HH:mm\", \"lastDay\": \"[Dün] [saat] HH:mm\", \"nextWeek\": \"dddd [saat] HH:mm\", \"lastWeek\": \"[Geçen] dddd [saat] HH:mm\", \"sameElse\": \"ddd, D MMM [saat] HH:mm\" }) }}", + "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Bugün]\", \"nextDay\": \"[Yarın]\", \"lastDay\": \"[Dün]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Geçen] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Kayıt yapmaya başlamak için tarayıcınızda kameraya erişime izin verin", "To start recording, allow the microphone access in your browser": "Kayıt yapmaya başlamak için tarayıcınızda mikrofona erişime izin verin", @@ -738,6 +758,8 @@ "Working my magic": "Büyümü yapıyorum", "You": "Sen", "You can't schedule messages here": "Burada mesaj zamanlayamazsınız", + "You can't send messages in this channel": "Bu kanalda mesaj gönderemezsiniz", + "You're no longer allowed to post here": "Artık burada paylaşım yapmanıza izin verilmiyor", "You're offline. Reconnect to schedule this message.": "Çevrimdışısınız. Bu mesajı zamanlamak için yeniden bağlanın.", "You've reached the limit of scheduled messages": "Zamanlanmış mesaj sınırına ulaştınız", "You've reached the limit of scheduled messages in this channel": "Bu kanaldaki zamanlanmış mesaj sınırına ulaştınız", From 537a4b3f09138152d26202b61bdb7d31db27a309 Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Fri, 30 Oct 2026 13:09:21 +0100 Subject: [PATCH 06/12] feat(ChannelDetail): add a scheduled messages section List the channel's scheduled messages in a new ChannelDetail section placed after the pinned messages. --- src/plugins/ChannelDetail/ChannelDetail.tsx | 19 ++++ .../ScheduledMessagesView.tsx | 26 +++++ .../__tests__/ScheduledMessagesView.test.tsx | 100 ++++++++++++++++++ .../Views/ScheduledMessagesView/index.ts | 1 + src/plugins/ChannelDetail/index.ts | 1 + .../styling/ScheduledMessagesView.scss | 14 +++ src/plugins/ChannelDetail/styling/index.scss | 1 + 7 files changed, 162 insertions(+) create mode 100644 src/plugins/ChannelDetail/Views/ScheduledMessagesView/ScheduledMessagesView.tsx create mode 100644 src/plugins/ChannelDetail/Views/ScheduledMessagesView/__tests__/ScheduledMessagesView.test.tsx create mode 100644 src/plugins/ChannelDetail/Views/ScheduledMessagesView/index.ts create mode 100644 src/plugins/ChannelDetail/styling/ScheduledMessagesView.scss diff --git a/src/plugins/ChannelDetail/ChannelDetail.tsx b/src/plugins/ChannelDetail/ChannelDetail.tsx index 860319a1c..c151b4747 100644 --- a/src/plugins/ChannelDetail/ChannelDetail.tsx +++ b/src/plugins/ChannelDetail/ChannelDetail.tsx @@ -17,6 +17,7 @@ import { ChannelManagementView } from './Views/ChannelManagementView'; import { ChannelMediaView } from './Views/ChannelMediaView'; import { ChannelMembersView } from './Views/ChannelMembersView'; import { PinnedMessagesView } from './Views/PinnedMessagesView'; +import { ScheduledMessagesView } from './Views/ScheduledMessagesView'; import { Prompt } from '../../components/Dialog'; import { useComponentContextIcons } from '../../context'; @@ -35,6 +36,11 @@ const PinnedMessagesNavButtonIcon = () => { return ; }; +const ScheduledMessagesNavButtonIcon = () => { + const { IconClock } = useComponentContextIcons(); + return ; +}; + const ChannelMediaNavButtonIcon = () => { const { IconImage } = useComponentContextIcons(); return ; @@ -69,6 +75,14 @@ export const PinnedMessagesNavButton = (props: SectionNavigatorNavButtonProps) = /> ); +export const ScheduledMessagesNavButton = (props: SectionNavigatorNavButtonProps) => ( + +); + export const ChannelMediaNavButton = (props: SectionNavigatorNavButtonProps) => ( { + const { t } = useTranslationContext(); + const { close } = useModalContext(); + const { channel } = useChannelDetailContext(); + + return ( +
+ + + + +
+ ); +}; diff --git a/src/plugins/ChannelDetail/Views/ScheduledMessagesView/__tests__/ScheduledMessagesView.test.tsx b/src/plugins/ChannelDetail/Views/ScheduledMessagesView/__tests__/ScheduledMessagesView.test.tsx new file mode 100644 index 000000000..fbf451f8a --- /dev/null +++ b/src/plugins/ChannelDetail/Views/ScheduledMessagesView/__tests__/ScheduledMessagesView.test.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import type { ScheduledMessageResponse } from 'stream-chat'; + +import { axe } from '../../../../../../axe-helper'; +import { + ChatProvider, + ModalContextProvider, + TranslationProvider, +} from '../../../../../context'; +import { Streami18n } from '../../../../../i18n'; +import { + generateChannel, + generateScheduledMessage, + generateUser, + initClientWithChannels, + mockChatContext, +} from '../../../../../mock-builders'; +import { defaultChannelDetailSections } from '../../../ChannelDetail'; +import { ChannelDetailProvider } from '../../../ChannelDetailContext'; +import { ScheduledMessagesView } from '../ScheduledMessagesView'; + +const cid = 'messaging:general'; + +const renderView = async (scheduledMessages: ScheduledMessageResponse[] = []) => { + const { + channels: [channel], + client, + } = await initClientWithChannels({ + channelsData: [generateChannel({ channel: { id: 'general', type: 'messaging' } })], + customUser: generateUser({ id: 'user-id' }), + }); + scheduledMessages.forEach(client.scheduledMessages.upsertToState); + + const close = vi.fn(); + const translators = await new Streami18n({ language: 'en' }).getTranslators(); + + const view = render( + + + + + + + + + , + ); + + return { ...view, close }; +}; + +describe('ScheduledMessagesView', () => { + afterEach(() => { + cleanup(); + }); + + it('is one of the default channel detail sections', () => { + const sectionIds = defaultChannelDetailSections.map(({ id }) => id); + + expect(sectionIds).toContain('scheduled-messages'); + expect(sectionIds.indexOf('scheduled-messages')).toBe( + sectionIds.indexOf('pinned-messages') + 1, + ); + }); + + it('lists the scheduled messages of the channel', async () => { + await renderView([ + generateScheduledMessage({ cid, message: { text: 'Launch moved to Monday' } }), + generateScheduledMessage({ + cid: 'messaging:random', + message: { text: 'Elsewhere' }, + }), + ]); + + expect(screen.getByText('Scheduled messages')).toBeInTheDocument(); + expect(screen.getByText('Launch moved to Monday')).toBeInTheDocument(); + expect(screen.queryByText('Elsewhere')).not.toBeInTheDocument(); + }); + + it('shows the empty state when nothing is scheduled', async () => { + await renderView(); + + expect(screen.getByText('No scheduled messages')).toBeInTheDocument(); + }); + + it('closes the channel detail modal from the header', async () => { + const { close } = await renderView(); + + fireEvent.click(screen.getByRole('button', { name: 'Close' })); + + expect(close).toHaveBeenCalledTimes(1); + }); + + it('has no accessibility violations', async () => { + const { container } = await renderView([generateScheduledMessage({ cid })]); + + expect(await axe(container)).toHaveNoViolations(); + }); +}); diff --git a/src/plugins/ChannelDetail/Views/ScheduledMessagesView/index.ts b/src/plugins/ChannelDetail/Views/ScheduledMessagesView/index.ts new file mode 100644 index 000000000..81ab40274 --- /dev/null +++ b/src/plugins/ChannelDetail/Views/ScheduledMessagesView/index.ts @@ -0,0 +1 @@ +export * from './ScheduledMessagesView'; diff --git a/src/plugins/ChannelDetail/index.ts b/src/plugins/ChannelDetail/index.ts index 8d1a469d0..078f7cc8c 100644 --- a/src/plugins/ChannelDetail/index.ts +++ b/src/plugins/ChannelDetail/index.ts @@ -13,3 +13,4 @@ export * from './Views/ChannelMediaView'; export * from './Views/ChannelMemberDetailView'; export * from './Views/ChannelMembersView'; export * from './Views/PinnedMessagesView'; +export * from './Views/ScheduledMessagesView'; diff --git a/src/plugins/ChannelDetail/styling/ScheduledMessagesView.scss b/src/plugins/ChannelDetail/styling/ScheduledMessagesView.scss new file mode 100644 index 000000000..c650ed919 --- /dev/null +++ b/src/plugins/ChannelDetail/styling/ScheduledMessagesView.scss @@ -0,0 +1,14 @@ +.str-chat__channel-detail__scheduled-messages-view { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.str-chat__channel-detail__scheduled-messages-view__body { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow-y: auto; +} diff --git a/src/plugins/ChannelDetail/styling/index.scss b/src/plugins/ChannelDetail/styling/index.scss index db73bf5d1..490bc536e 100644 --- a/src/plugins/ChannelDetail/styling/index.scss +++ b/src/plugins/ChannelDetail/styling/index.scss @@ -7,5 +7,6 @@ @use 'ChannelMembersView'; @use 'ChannelMembersViewListFooter'; @use 'PinnedMessagesView'; +@use 'ScheduledMessagesView'; @use '../SectionNavigator/styling' as SectionNavigator; @use '../VirtualizedList/styling' as VirtualizedList; From 9d9b6c1b2548219c9e92d533d5dbaf58989c978e Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Mon, 2 Nov 2026 14:02:43 +0100 Subject: [PATCH 07/12] feat(ScheduledMessages): add search, loading and error states to the list Follow the web designs for the scheduled message list: - Channel Detail's scheduled section gets a search field (shown when the channel has scheduled messages) that filters the list by message text - the first load shows skeleton rows instead of a spinner - a failed first load shows an error state with Try again, which reloads the store; stored messages stay listed when a later reload fails - a search without matches shows "No messages found" - the empty, no-results and error states share one layout New strings are translated in all 12 locales. Refs: REACT-1388 --- .../ScheduledMessageList.tsx | 31 +++++- .../ScheduledMessageListLoading.tsx | 34 +++++++ .../ScheduledMessagesEmptyList.tsx | 15 ++- .../ScheduledMessagesEmptyState.tsx | 21 ++++ .../ScheduledMessagesLoadError.tsx | 31 ++++++ .../ScheduledMessagesNoResults.tsx | 11 +++ .../__tests__/ScheduledMessageList.test.tsx | 96 ++++++++++++++++++- src/components/ScheduledMessages/constants.ts | 2 + .../ScheduledMessages/hooks/index.ts | 1 + .../hooks/useScheduledMessageGroups.ts | 20 ++-- .../hooks/useScheduledMessagesLoadState.ts | 39 ++++++++ src/components/ScheduledMessages/index.ts | 4 + .../styling/ScheduledMessageList.scss | 58 +++++++++++ src/components/ScheduledMessages/types.ts | 25 +++++ .../__tests__/scheduledMessageSearch.test.ts | 29 ++++++ .../ScheduledMessages/utils/index.ts | 1 + .../utils/scheduledMessageSearch.ts | 20 ++++ src/i18n/de.json | 6 +- src/i18n/en.json | 6 +- src/i18n/es.json | 6 +- src/i18n/fr.json | 6 +- src/i18n/hi.json | 6 +- src/i18n/it.json | 6 +- src/i18n/ja.json | 6 +- src/i18n/ko.json | 6 +- src/i18n/nl.json | 6 +- src/i18n/pt.json | 6 +- src/i18n/ru.json | 6 +- src/i18n/tr.json | 6 +- .../ScheduledMessagesView.tsx | 17 +++- .../__tests__/ScheduledMessagesView.test.tsx | 15 +++ 31 files changed, 504 insertions(+), 38 deletions(-) create mode 100644 src/components/ScheduledMessages/ScheduledMessageListLoading.tsx create mode 100644 src/components/ScheduledMessages/ScheduledMessagesEmptyState.tsx create mode 100644 src/components/ScheduledMessages/ScheduledMessagesLoadError.tsx create mode 100644 src/components/ScheduledMessages/ScheduledMessagesNoResults.tsx create mode 100644 src/components/ScheduledMessages/hooks/useScheduledMessagesLoadState.ts create mode 100644 src/components/ScheduledMessages/utils/__tests__/scheduledMessageSearch.test.ts create mode 100644 src/components/ScheduledMessages/utils/scheduledMessageSearch.ts diff --git a/src/components/ScheduledMessages/ScheduledMessageList.tsx b/src/components/ScheduledMessages/ScheduledMessageList.tsx index bb53c8202..4610fa648 100644 --- a/src/components/ScheduledMessages/ScheduledMessageList.tsx +++ b/src/components/ScheduledMessages/ScheduledMessageList.tsx @@ -2,22 +2,43 @@ import React from 'react'; import { useComponentContext, useTranslationContext } from '../../context'; import { Button } from '../Button'; -import { LoadingIndicator } from '../Loading'; -import { useScheduledMessageGroups, useScheduledMessagesPagination } from './hooks'; +import { + useScheduledMessageGroups, + useScheduledMessagesLoadState, + useScheduledMessagesPagination, +} from './hooks'; import { ScheduledMessageGroupHeader } from './ScheduledMessageGroupHeader'; import { ScheduledMessageItem as DefaultScheduledMessageItem } from './ScheduledMessageItem'; +import { ScheduledMessageListLoading } from './ScheduledMessageListLoading'; import { ScheduledMessagesEmptyList } from './ScheduledMessagesEmptyList'; +import { ScheduledMessagesLoadError } from './ScheduledMessagesLoadError'; +import { ScheduledMessagesNoResults } from './ScheduledMessagesNoResults'; import type { ScheduledMessageListProps } from './types'; -export const ScheduledMessageList = ({ cid, parentId }: ScheduledMessageListProps) => { +export const ScheduledMessageList = ({ + cid, + parentId, + searchQuery, +}: ScheduledMessageListProps) => { const { t } = useTranslationContext(); const { ScheduledMessageItem = DefaultScheduledMessageItem } = useComponentContext(); - const { groups } = useScheduledMessageGroups({ cid, parentId }); + const { groups, hasScheduledMessages } = useScheduledMessageGroups({ + cid, + parentId, + searchQuery, + }); const { canLoadMore, isLoading, loadMore } = useScheduledMessagesPagination(); + const { hasLoadFailed, isFirstLoad, reload } = useScheduledMessagesLoadState(); if (!groups.length) { - return isLoading ? : ; + if (hasScheduledMessages) return ; + + if (isFirstLoad) return ; + + if (hasLoadFailed) return ; + + return ; } return ( diff --git a/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx b/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx new file mode 100644 index 000000000..edd041036 --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import { useTranslationContext } from '../../context'; +import { SCHEDULED_MESSAGE_LIST_LOADING_ROW_COUNT } from './constants'; + +const LOADING_ROW_KEYS = Array.from( + { length: SCHEDULED_MESSAGE_LIST_LOADING_ROW_COUNT }, + (_, index) => `loading-row-${index}`, +); + +export const ScheduledMessageListLoading = () => { + const { t } = useTranslationContext(); + + return ( +
+ {LOADING_ROW_KEYS.map((key) => ( + )} diff --git a/src/components/ScheduledMessages/__tests__/ScheduledMessagesBanner.test.tsx b/src/components/ScheduledMessages/__tests__/ScheduledMessagesBanner.test.tsx index 0d40b2886..53285fd3e 100644 --- a/src/components/ScheduledMessages/__tests__/ScheduledMessagesBanner.test.tsx +++ b/src/components/ScheduledMessages/__tests__/ScheduledMessagesBanner.test.tsx @@ -1,6 +1,14 @@ import React from 'react'; import type { PropsWithChildren } from 'react'; -import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { + act, + cleanup, + fireEvent, + render, + screen, + waitFor, + within, +} from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { formatMessage, ScheduledMessageEventType, SearchController } from 'stream-chat'; import type { LocalMessage, ScheduledMessageResponse } from 'stream-chat'; @@ -179,14 +187,14 @@ describe('ScheduledMessagesBanner', () => { await waitFor(() => expect(queryBanner()).not.toBeInTheDocument()); }); - it('opens the scheduled message list from View', async () => { + it('opens the scheduled message list from View all', async () => { await renderComposer({ scheduledMessages: [ generateScheduledMessage({ cid, message: { text: 'Launch moved to Monday' } }), ], }); - await act(() => fireEvent.click(screen.getByRole('button', { name: 'View' }))); + await act(() => fireEvent.click(screen.getByRole('button', { name: 'View all' }))); const dialog = await screen.findByTestId('scheduled-message-list-dialog'); @@ -194,10 +202,28 @@ describe('ScheduledMessagesBanner', () => { expect(dialog).toHaveTextContent('Launch moved to Monday'); }); + it('filters the scheduled message list with the search field', async () => { + await renderComposer({ + scheduledMessages: [ + generateScheduledMessage({ cid, message: { text: 'Launch moved to Monday' } }), + generateScheduledMessage({ cid, message: { text: 'Lunch at noon' } }), + ], + }); + + await act(() => fireEvent.click(screen.getByRole('button', { name: 'View all' }))); + const dialog = await screen.findByTestId('scheduled-message-list-dialog'); + fireEvent.change(within(dialog).getByRole('searchbox', { name: 'Search' }), { + target: { value: 'launch' }, + }); + + expect(dialog).toHaveTextContent('Launch moved to Monday'); + expect(dialog).not.toHaveTextContent('Lunch at noon'); + }); + it('titles the list as replies in a thread', async () => { await renderComposer({ inThread: true, scheduledMessages: [threadReply()] }); - await act(() => fireEvent.click(screen.getByRole('button', { name: 'View' }))); + await act(() => fireEvent.click(screen.getByRole('button', { name: 'View all' }))); expect(await screen.findByTestId('scheduled-message-list-dialog')).toHaveTextContent( 'Scheduled replies', diff --git a/src/components/ScheduledMessages/styling/ScheduledMessagesBanner.scss b/src/components/ScheduledMessages/styling/ScheduledMessagesBanner.scss index 778e4ea4f..03e4edd8d 100644 --- a/src/components/ScheduledMessages/styling/ScheduledMessagesBanner.scss +++ b/src/components/ScheduledMessages/styling/ScheduledMessagesBanner.scss @@ -1,31 +1,43 @@ +@use '../../../styling/utils'; + .str-chat { .str-chat__scheduled-messages-banner { display: flex; align-items: center; - gap: var(--str-chat__spacing-xs); + gap: var(--str-chat__spacing-sm); width: 100%; - padding-inline: var(--str-chat__spacing-sm); - border-radius: var(--str-chat__radius-md); - background-color: var(--str-chat__background-core-surface-subtle); - color: var(--str-chat__text-secondary); - font: var(--str-chat__font-caption-default); + min-height: var(--str-chat__button-visual-height-md); + box-sizing: border-box; + padding: var(--str-chat__spacing-xs) var(--str-chat__spacing-md); + border-radius: var(--str-chat__radius-max); + background-color: var(--str-chat__background-core-elevation-2); + box-shadow: var(--str-chat__box-shadow-3); + color: var(--str-chat__text-primary); + font: var(--str-chat__font-metadata-emphasis); .str-chat__scheduled-messages-banner__icon { flex-shrink: 0; - height: var(--str-chat__icon-size-sm); - width: var(--str-chat__icon-size-sm); + height: var(--str-chat__icon-size-md); + width: var(--str-chat__icon-size-md); + color: var(--str-chat__accent-primary); } .str-chat__scheduled-messages-banner__label { + @include utils.ellipsis-text(); flex: 1; min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } .str-chat__scheduled-messages-banner__view-button { + @include utils.button-reset; flex-shrink: 0; + cursor: pointer; + color: var(--str-chat__text-link); + font: var(--str-chat__font-metadata-default); + + &:focus-visible { + @include utils.focusable; + } } } } From d21cc906484b3010614c8e893cd059699a46c412 Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Tue, 3 Nov 2026 15:03:55 +0100 Subject: [PATCH 09/12] fix(ScheduledMessages): match the scheduled message list to the design Each row now shows "Send Thu, Oct 22 at 09:00" above the channel avatar, the channel name and the message preview, with a "Sending..." or failure reason line instead of a status badge. Day groups keep their titles for screen readers only, rows are divided edge to edge, the loading skeleton mirrors the rows, the empty state explains how to schedule, and the list dialog gets the search field. --- .../ScheduledMessageItem.tsx | 62 +++++---- .../ScheduledMessageItemStatus.tsx | 35 +++++ .../ScheduledMessageListDialog.tsx | 31 ++++- .../ScheduledMessageListLoading.tsx | 10 +- .../ScheduledMessageStatusBadge.tsx | 40 ------ .../ScheduledMessagesEmptyList.tsx | 4 +- .../ScheduledMessagesLoadError.tsx | 1 - .../__tests__/ScheduledMessageList.test.tsx | 12 +- src/components/ScheduledMessages/index.ts | 2 +- .../styling/ScheduledMessageList.scss | 125 +++++++++++++++--- src/components/ScheduledMessages/types.ts | 7 +- 11 files changed, 228 insertions(+), 101 deletions(-) create mode 100644 src/components/ScheduledMessages/ScheduledMessageItemStatus.tsx delete mode 100644 src/components/ScheduledMessages/ScheduledMessageStatusBadge.tsx diff --git a/src/components/ScheduledMessages/ScheduledMessageItem.tsx b/src/components/ScheduledMessages/ScheduledMessageItem.tsx index e18b61872..5d1275f22 100644 --- a/src/components/ScheduledMessages/ScheduledMessageItem.tsx +++ b/src/components/ScheduledMessages/ScheduledMessageItem.tsx @@ -1,40 +1,31 @@ import clsx from 'clsx'; import React, { useMemo } from 'react'; -import { useTranslationContext } from '../../context'; -import { ListItemLayout } from '../ListItemLayout'; +import { useChatContext, useTranslationContext } from '../../context'; +import { ChannelAvatar } from '../Avatar'; +import { useChannelPreviewInfo } from '../ChannelListItem/hooks/useChannelPreviewInfo'; import { SummarizedMessagePreview } from '../SummarizedMessagePreview'; -import { ScheduledMessageStatusBadge } from './ScheduledMessageStatusBadge'; +import { ScheduledMessageItemStatus } from './ScheduledMessageItemStatus'; import type { ScheduledMessageItemProps } from './types'; -import { - isFailedScheduledMessage, - toStoredScheduledMessagePreview, - translateScheduledMessageFailureReason, -} from './utils'; +import { isFailedScheduledMessage, toStoredScheduledMessagePreview } from './utils'; export const ScheduledMessageItem = ({ scheduledMessage }: ScheduledMessageItemProps) => { const { t } = useTranslationContext(); + const { client } = useChatContext(); - const { failure_reason, scheduled_at } = scheduledMessage; + const { cid, scheduled_at } = scheduledMessage; + const channel = client.activeChannels[cid]; + const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({ + channel, + }); const previewMessage = useMemo( () => toStoredScheduledMessagePreview(scheduledMessage), [scheduledMessage], ); - const StatusBadge = useMemo( - () => - function ScheduledMessageItemStatusBadge() { - return ; - }, - [scheduledMessage], - ); - const isFailed = isFailedScheduledMessage(scheduledMessage); - const failureMessage = isFailed - ? translateScheduledMessageFailureReason(t, failure_reason) - : undefined; - const scheduledTime = t('timestamp/ScheduledMessage', { + const sendTime = t('timestamp/ScheduledMessageSendTime', { timestamp: new Date(scheduled_at), }); @@ -45,14 +36,27 @@ export const ScheduledMessageItem = ({ scheduledMessage }: ScheduledMessageItemP })} data-testid='scheduled-message-item' > - {scheduledTime}} - subtitleClassName='str-chat__scheduled-message-item__time' - title={} - TrailingSlot={StatusBadge} - /> + +
+
); }; diff --git a/src/components/ScheduledMessages/ScheduledMessageItemStatus.tsx b/src/components/ScheduledMessages/ScheduledMessageItemStatus.tsx new file mode 100644 index 000000000..dd3cbb68c --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageItemStatus.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { ScheduledMessageStatus } from 'stream-chat'; + +import { useComponentContextIcons, useTranslationContext } from '../../context'; +import type { ScheduledMessageItemStatusProps } from './types'; +import { translateScheduledMessageFailureReason } from './utils'; + +export const ScheduledMessageItemStatus = ({ + scheduledMessage, +}: ScheduledMessageItemStatusProps) => { + const { t } = useTranslationContext(); + const { IconExclamationTriangleFill, IconLoading } = useComponentContextIcons(); + + const { failure_reason, status } = scheduledMessage; + + if (status === ScheduledMessageStatus.Failed) { + return ( + + + {translateScheduledMessageFailureReason(t, failure_reason)} + + ); + } + + if (status === ScheduledMessageStatus.Sending) { + return ( + + + {t('Sending...')} + + ); + } + + return null; +}; diff --git a/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx b/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx index ad94d6b2d..53d6a0497 100644 --- a/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx +++ b/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx @@ -1,7 +1,10 @@ -import React from 'react'; +import React, { useCallback, useState } from 'react'; +import type { ChangeEvent } from 'react'; -import { useTranslationContext } from '../../context'; +import { useComponentContextIcons, useTranslationContext } from '../../context'; import { Prompt } from '../Dialog'; +import { TextInput } from '../Form'; +import { useScheduledMessageCount } from './hooks'; import { ScheduledMessageList } from './ScheduledMessageList'; import type { ScheduledMessageListDialogProps } from './types'; @@ -11,6 +14,16 @@ export const ScheduledMessageListDialog = ({ parentId, }: ScheduledMessageListDialogProps) => { const { t } = useTranslationContext(); + const { IconSearch } = useComponentContextIcons(); + + const scheduledMessageCount = useScheduledMessageCount({ cid, parentId }); + + const [searchQuery, setSearchQuery] = useState(''); + + const updateSearchQuery = useCallback( + ({ target }: ChangeEvent) => setSearchQuery(target.value), + [], + ); const title = parentId ? t('Scheduled replies') : t('Scheduled messages'); @@ -21,7 +34,19 @@ export const ScheduledMessageListDialog = ({ > - + {scheduledMessageCount > 0 && ( + } + onChange={updateSearchQuery} + placeholder={t('Search')} + type='search' + value={searchQuery} + /> + )} + ); diff --git a/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx b/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx index edd041036..60093e56a 100644 --- a/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx +++ b/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx @@ -25,8 +25,14 @@ export const ScheduledMessageListLoading = () => { className='str-chat__scheduled-message-list-loading__row' key={key} > -
-
+
+
+
+
+
+
+
+
))}
diff --git a/src/components/ScheduledMessages/ScheduledMessageStatusBadge.tsx b/src/components/ScheduledMessages/ScheduledMessageStatusBadge.tsx deleted file mode 100644 index 90f4bd245..000000000 --- a/src/components/ScheduledMessages/ScheduledMessageStatusBadge.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import type { TFunction } from 'i18next'; -import { ScheduledMessageStatus } from 'stream-chat'; - -import { useTranslationContext } from '../../context'; -import { Badge } from '../Badge'; -import type { BadgeVariant } from '../Badge'; -import type { ScheduledMessageStatusBadgeProps } from './types'; - -const translateScheduledMessageStatus = ( - t: TFunction, - status: ScheduledMessageStatus, -) => { - if (status === ScheduledMessageStatus.Failed) return t('Failed'); - - if (status === ScheduledMessageStatus.Sending) return t('Sending...'); - - return t('Scheduled'); -}; - -const getStatusBadgeVariant = (status: ScheduledMessageStatus): BadgeVariant => - status === ScheduledMessageStatus.Failed ? 'error' : 'neutral'; - -export const ScheduledMessageStatusBadge = ({ - scheduledMessage, -}: ScheduledMessageStatusBadgeProps) => { - const { t } = useTranslationContext(); - - const { status } = scheduledMessage; - - return ( - - {translateScheduledMessageStatus(t, status)} - - ); -}; diff --git a/src/components/ScheduledMessages/ScheduledMessagesEmptyList.tsx b/src/components/ScheduledMessages/ScheduledMessagesEmptyList.tsx index bb45985d3..7f5bc6a63 100644 --- a/src/components/ScheduledMessages/ScheduledMessagesEmptyList.tsx +++ b/src/components/ScheduledMessages/ScheduledMessagesEmptyList.tsx @@ -9,7 +9,9 @@ export const ScheduledMessagesEmptyList = () => { return ( diff --git a/src/components/ScheduledMessages/ScheduledMessagesLoadError.tsx b/src/components/ScheduledMessages/ScheduledMessagesLoadError.tsx index 57d4c325f..183825210 100644 --- a/src/components/ScheduledMessages/ScheduledMessagesLoadError.tsx +++ b/src/components/ScheduledMessages/ScheduledMessagesLoadError.tsx @@ -13,7 +13,6 @@ export const ScheduledMessagesLoadError = ({ return ( diff --git a/src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx b/src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx index 34577124c..a98e65918 100644 --- a/src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx +++ b/src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx @@ -73,7 +73,11 @@ describe('ScheduledMessageList', () => { await renderList(); expect(screen.getByText('No scheduled messages')).toBeInTheDocument(); - expect(screen.getByText('Messages you schedule appear here')).toBeInTheDocument(); + expect( + screen.getByText( + 'Click the arrow next to the send button in a channel to schedule a message.', + ), + ).toBeInTheDocument(); }); it('shows skeleton rows while the first page loads', async () => { @@ -178,7 +182,7 @@ describe('ScheduledMessageList', () => { expect(getGroupHeaders()).toEqual(['Failed', 'Tomorrow', 'Friday']); }); - it('shows the preview, scheduled time and status of each message', async () => { + it('shows the send time and preview of each message', async () => { await renderList({ scheduledMessages: [ generateScheduledMessage({ @@ -192,8 +196,8 @@ describe('ScheduledMessageList', () => { const item = screen.getByTestId('scheduled-message-item'); expect(within(item).getByText('Launch moved to Monday')).toBeInTheDocument(); - expect(within(item).getByText('Tomorrow at 09:00')).toBeInTheDocument(); - expect(within(item).getByText('Scheduled')).toBeInTheDocument(); + expect(within(item).getByText('Send Thu, Oct 29 at 09:00')).toBeInTheDocument(); + expect(within(item).queryByText('Scheduled')).not.toBeInTheDocument(); }); it('summarises attachments when the message has no text', async () => { diff --git a/src/components/ScheduledMessages/index.ts b/src/components/ScheduledMessages/index.ts index 9995687ae..2f88fa541 100644 --- a/src/components/ScheduledMessages/index.ts +++ b/src/components/ScheduledMessages/index.ts @@ -7,6 +7,7 @@ export { export * from './hooks'; export * from './ScheduledMessageGroupHeader'; export * from './ScheduledMessageItem'; +export * from './ScheduledMessageItemStatus'; export * from './ScheduledMessageList'; export * from './ScheduledMessageListDialog'; export * from './ScheduledMessageListLoading'; @@ -15,7 +16,6 @@ export * from './ScheduledMessagesEmptyList'; export * from './ScheduledMessagesEmptyState'; export * from './ScheduledMessagesLoadError'; export * from './ScheduledMessagesNoResults'; -export * from './ScheduledMessageStatusBadge'; export * from './ScheduleMessageButton'; export * from './ScheduleMessageDialog'; export * from './ScheduleMessageDialogNotice'; diff --git a/src/components/ScheduledMessages/styling/ScheduledMessageList.scss b/src/components/ScheduledMessages/styling/ScheduledMessageList.scss index 82c7bfacf..9661e407d 100644 --- a/src/components/ScheduledMessages/styling/ScheduledMessageList.scss +++ b/src/components/ScheduledMessages/styling/ScheduledMessageList.scss @@ -1,35 +1,45 @@ +@use '../../../styling/utils'; + .str-chat { - --str-chat__scheduled-message-list-dialog-max-width: 504px; + --str-chat__scheduled-message-list-dialog-max-width: 520px; .str-chat__scheduled-message-list-dialog { - width: 100%; - max-width: var(--str-chat__scheduled-message-list-dialog-max-width); + width: var(--str-chat__scheduled-message-list-dialog-max-width); + max-width: 100%; .str-chat__scheduled-message-list-dialog__body { overflow-y: auto; } } + .str-chat__scheduled-message-list-dialog__search { + margin-block-end: var(--str-chat__spacing-xs); + } + .str-chat__scheduled-message-list { display: flex; flex-direction: column; - gap: var(--str-chat__spacing-md); .str-chat__scheduled-message-list__load-more { align-self: center; + margin-block: var(--str-chat__spacing-sm); } } .str-chat__scheduled-message-group { display: flex; flex-direction: column; - gap: var(--str-chat__spacing-xxs); .str-chat__scheduled-message-group__header { - margin: 0; - padding-inline: var(--str-chat__spacing-sm); - color: var(--str-chat__text-secondary); - font: var(--str-chat__font-caption-emphasis); + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } .str-chat__scheduled-message-group__items { @@ -39,25 +49,74 @@ padding: 0; list-style: none; } + + .str-chat__scheduled-message-group__item { + margin-inline: calc(-1 * var(--str-chat__spacing-xl)); + padding: var(--str-chat__spacing-md) var(--str-chat__spacing-xl); + border-block-end: 1px solid var(--str-chat__border-core-subtle); + } + } + + .str-chat__scheduled-message-group:last-of-type + .str-chat__scheduled-message-group__item:last-child { + border-block-end: none; } .str-chat__scheduled-message-item { + display: flex; + flex-direction: column; + gap: var(--str-chat__spacing-xs); + min-width: 0; + + .str-chat__scheduled-message-item__time { + color: var(--str-chat__text-tertiary); + font: var(--str-chat__font-caption-default); + } + + .str-chat__scheduled-message-item__body { + display: flex; + align-items: flex-start; + gap: var(--str-chat__spacing-sm); + min-width: 0; + } + + .str-chat__scheduled-message-item__content { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + color: var(--str-chat__text-primary); + font: var(--str-chat__font-body-default); + } + + .str-chat__scheduled-message-item__channel-name { + @include utils.ellipsis-text(); + color: var(--str-chat__text-secondary); + font: var(--str-chat__font-caption-emphasis); + } + .str-chat__summarized-message-preview { min-width: 0; } - .str-chat__scheduled-message-item__time { + .str-chat__scheduled-message-item__status { + display: flex; + align-items: center; + gap: var(--str-chat__spacing-xxs); + margin-block-start: var(--str-chat__spacing-xxs); color: var(--str-chat__text-secondary); - font: var(--str-chat__font-caption-default); + font: var(--str-chat__font-metadata-default); } - .str-chat__scheduled-message-item__failure-reason { + .str-chat__scheduled-message-item__status--failed { color: var(--str-chat__accent-error); font: var(--str-chat__font-caption-default); } - .str-chat__scheduled-message-status-badge { + .str-chat__scheduled-message-item__status-icon { flex-shrink: 0; + height: var(--str-chat__icon-size-sm); + width: var(--str-chat__icon-size-sm); } } @@ -105,12 +164,36 @@ .str-chat__scheduled-message-list-loading__row { display: flex; flex-direction: column; - justify-content: center; gap: var(--str-chat__spacing-xs); - min-height: 64px; - padding-inline: var(--str-chat__spacing-sm); + margin-inline: calc(-1 * var(--str-chat__spacing-xl)); + padding: var(--str-chat__spacing-md) var(--str-chat__spacing-xl); + border-block-end: 1px solid var(--str-chat__border-core-subtle); } + .str-chat__scheduled-message-list-loading__body { + display: flex; + align-items: center; + gap: var(--str-chat__spacing-sm); + } + + .str-chat__scheduled-message-list-loading__lines { + display: flex; + flex: 1; + flex-direction: column; + gap: var(--str-chat__spacing-xs); + } + + .str-chat__scheduled-message-list-loading__avatar { + flex-shrink: 0; + width: 40px; + } + + .str-chat__scheduled-message-list-loading__time { + width: 30%; + } + + .str-chat__scheduled-message-list-loading__time, + .str-chat__scheduled-message-list-loading__avatar, .str-chat__scheduled-message-list-loading__title, .str-chat__scheduled-message-list-loading__subtitle { height: var(--str-chat__size-12); @@ -126,12 +209,16 @@ animation: str-chat__scheduled-message-list-loading-shimmer 1.2s linear infinite; } + .str-chat__scheduled-message-list-loading__avatar { + height: 40px; + } + .str-chat__scheduled-message-list-loading__title { - width: 70%; + width: 40%; } .str-chat__scheduled-message-list-loading__subtitle { - width: 40%; + width: 80%; } } } @@ -147,6 +234,8 @@ } @media (prefers-reduced-motion: reduce) { + .str-chat .str-chat__scheduled-message-list-loading__time, + .str-chat .str-chat__scheduled-message-list-loading__avatar, .str-chat .str-chat__scheduled-message-list-loading__title, .str-chat .str-chat__scheduled-message-list-loading__subtitle { animation: none; diff --git a/src/components/ScheduledMessages/types.ts b/src/components/ScheduledMessages/types.ts index 7f0afb6f0..9f15710a8 100644 --- a/src/components/ScheduledMessages/types.ts +++ b/src/components/ScheduledMessages/types.ts @@ -125,7 +125,10 @@ export type ScheduledMessagesLoadErrorProps = { onRetry: () => void; }; -export type ScheduledMessageListDialogProps = ScheduledMessageListProps & { +export type ScheduledMessageListDialogProps = Omit< + ScheduledMessageListProps, + 'searchQuery' +> & { close: () => void; }; @@ -137,6 +140,6 @@ export type ScheduledMessageItemProps = { scheduledMessage: ScheduledMessageResponse; }; -export type ScheduledMessageStatusBadgeProps = { +export type ScheduledMessageItemStatusProps = { scheduledMessage: ScheduledMessageResponse; }; From 857f09318d1328d05bfd92bb9631ab509e6d5907 Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Tue, 3 Nov 2026 15:04:00 +0100 Subject: [PATCH 10/12] fix(ChannelDetail): label the scheduled section as in the design Name the navigation item "Scheduled" and drop the section description. --- src/plugins/ChannelDetail/ChannelDetail.tsx | 2 +- .../Views/ScheduledMessagesView/ScheduledMessagesView.tsx | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/plugins/ChannelDetail/ChannelDetail.tsx b/src/plugins/ChannelDetail/ChannelDetail.tsx index c151b4747..9a6f83df6 100644 --- a/src/plugins/ChannelDetail/ChannelDetail.tsx +++ b/src/plugins/ChannelDetail/ChannelDetail.tsx @@ -79,7 +79,7 @@ export const ScheduledMessagesNavButton = (props: SectionNavigatorNavButtonProps ); diff --git a/src/plugins/ChannelDetail/Views/ScheduledMessagesView/ScheduledMessagesView.tsx b/src/plugins/ChannelDetail/Views/ScheduledMessagesView/ScheduledMessagesView.tsx index 1195e2db9..1f6909e70 100644 --- a/src/plugins/ChannelDetail/Views/ScheduledMessagesView/ScheduledMessagesView.tsx +++ b/src/plugins/ChannelDetail/Views/ScheduledMessagesView/ScheduledMessagesView.tsx @@ -21,11 +21,7 @@ export const ScheduledMessagesView = () => { return (
- + {scheduledMessageCount > 0 && ( From 20552375661ba5e1434905ac4b90b187dce09ffc Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Tue, 3 Nov 2026 15:04:04 +0100 Subject: [PATCH 11/12] feat(i18n): translate the scheduled message list rows --- src/i18n/de.json | 6 +++--- src/i18n/en.json | 6 +++--- src/i18n/es.json | 6 +++--- src/i18n/fr.json | 6 +++--- src/i18n/hi.json | 6 +++--- src/i18n/it.json | 6 +++--- src/i18n/ja.json | 6 +++--- src/i18n/ko.json | 6 +++--- src/i18n/nl.json | 6 +++--- src/i18n/pt.json | 6 +++--- src/i18n/ru.json | 6 +++--- src/i18n/tr.json | 6 +++--- 12 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/i18n/de.json b/src/i18n/de.json index f56b9fb2f..8f47a8f5c 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -266,10 +266,10 @@ "Channels": "Kanäle", "Chat deleted": "Chat deleted", "Chats": "Chats", - "Check your connection and try again": "Überprüfe deine Verbindung und versuche es erneut", "Choose a later time": "Wähle eine spätere Uhrzeit", "Choose an earlier time": "Wähle eine frühere Uhrzeit", "Choose between 2 to 10 options": "Wähle zwischen 2 und 10 Optionen", + "Click the arrow next to the send button in a channel to schedule a message.": "Klicke auf den Pfeil neben der Senden-Schaltfläche in einem Kanal, um eine Nachricht zu planen.", "Close": "Schließen", "Close dialog": "Dialog schließen", "Close emoji picker": "Emoji-Auswahl schließen", @@ -495,8 +495,6 @@ "Message unpinned": "Nachricht nicht mehr angeheftet", "Message was blocked by moderation policies": "Nachricht wurde durch moderationsrichtlinien blockiert", "Messages have been marked unread.": "Nachrichten wurden als ungelesen markiert.", - "Messages waiting to be sent": "Messages waiting to be sent", - "Messages you schedule appear here": "Messages you schedule appear here", "Missing permissions to upload the attachment": "Fehlende Berechtigungen zum Hochladen des Anhangs", "Moderator": "Moderator", "Multiple votes": "Mehrfachstimmen", @@ -627,6 +625,7 @@ "Select up to {{count}}_other": "Bis zu {{count}} auswählen", "Select your current location and optionally enable live location sharing": "Wähle deinen aktuellen Standort und aktiviere optional das Teilen des Live-Standorts", "Send": "Senden", + "Send {{ time }}": "Senden {{ time }}", "Send a message": "Nachricht senden", "Send a message to start the conversation": "Senden Sie eine Nachricht, um die Unterhaltung zu beginnen", "Send Anyway": "Trotzdem senden", @@ -699,6 +698,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Heute] [um] HH:mm\", \"nextDay\": \"[Morgen] [um] HH:mm\", \"lastDay\": \"[Gestern] [um] HH:mm\", \"nextWeek\": \"dddd [um] HH:mm\", \"lastWeek\": \"[letzten] dddd [um] HH:mm\", \"sameElse\": \"ddd, D MMM [um] HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM [um] HH:mm\", \"lastDay\": \"ddd, D MMM [um] HH:mm\", \"nextWeek\": \"ddd, D MMM [um] HH:mm\", \"lastWeek\": \"ddd, D MMM [um] HH:mm\", \"sameElse\": \"ddd, D MMM [um] HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Heute]\", \"nextDay\": \"[Morgen]\", \"lastDay\": \"[Gestern]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Letzte] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM [um] HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Heute] [um] HH:mm\", \"nextDay\": \"[Morgen] [um] HH:mm\", \"lastDay\": \"[Gestern] [um] HH:mm\", \"nextWeek\": \"dddd [um] HH:mm\", \"lastWeek\": \"[letzten] dddd [um] HH:mm\", \"sameElse\": \"dddd [um] HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Um mit der Aufnahme zu beginnen, erlauben Sie den Zugriff auf die Kamera in Ihrem Browser", diff --git a/src/i18n/en.json b/src/i18n/en.json index dd5d39b55..1f4bd9288 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -266,10 +266,10 @@ "Channels": "Channels", "Chat deleted": "Chat deleted", "Chats": "Chats", - "Check your connection and try again": "Check your connection and try again", "Choose a later time": "Choose a later time", "Choose an earlier time": "Choose an earlier time", "Choose between 2 to 10 options": "Choose Between 2 to 10 Options", + "Click the arrow next to the send button in a channel to schedule a message.": "Click the arrow next to the send button in a channel to schedule a message.", "Close": "Close", "Close dialog": "Close dialog", "Close emoji picker": "Close emoji picker", @@ -495,8 +495,6 @@ "Message unpinned": "Message unpinned", "Message was blocked by moderation policies": "Message was blocked by moderation policies", "Messages have been marked unread.": "Messages have been marked unread.", - "Messages waiting to be sent": "Messages waiting to be sent", - "Messages you schedule appear here": "Messages you schedule appear here", "Missing permissions to upload the attachment": "Missing permissions to upload the attachment", "Moderator": "Moderator", "Multiple votes": "Multiple Votes", @@ -627,6 +625,7 @@ "Select up to {{count}}_other": "Select up to {{count}}", "Select your current location and optionally enable live location sharing": "Select your current location and optionally enable live location sharing", "Send": "Send", + "Send {{ time }}": "Send {{ time }}", "Send a message": "Send a message", "Send a message to start the conversation": "Send a message to start the conversation", "Send Anyway": "Send Anyway", @@ -699,6 +698,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Today] [at] HH:mm\", \"nextDay\": \"[Tomorrow] [at] HH:mm\", \"lastDay\": \"[Yesterday] [at] HH:mm\", \"nextWeek\": \"dddd [at] HH:mm\", \"lastWeek\": \"[Last] dddd [at] HH:mm\", \"sameElse\": \"ddd, D MMM [at] HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, MMM D [at] HH:mm\", \"lastDay\": \"ddd, MMM D [at] HH:mm\", \"nextWeek\": \"ddd, MMM D [at] HH:mm\", \"lastWeek\": \"ddd, MMM D [at] HH:mm\", \"sameElse\": \"ddd, MMM D [at] HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Today]\", \"nextDay\": \"[Tomorrow]\", \"lastDay\": \"[Yesterday]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Last] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, MMM D [at] HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Today] [at] HH:mm\", \"nextDay\": \"[Tomorrow] [at] HH:mm\", \"lastDay\": \"[Yesterday] [at] HH:mm\", \"nextWeek\": \"dddd [at] HH:mm\", \"lastWeek\": \"[Last] dddd [at] HH:mm\", \"sameElse\": \"dddd [at] HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "To start recording, allow the camera access in your browser", diff --git a/src/i18n/es.json b/src/i18n/es.json index 15fcfdb9d..00d35487b 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -285,10 +285,10 @@ "Channels": "Canales", "Chat deleted": "Chat deleted", "Chats": "Chats", - "Check your connection and try again": "Comprueba tu conexión e inténtalo de nuevo", "Choose a later time": "Elige una hora posterior", "Choose an earlier time": "Elige una hora anterior", "Choose between 2 to 10 options": "Elige entre 2 y 10 opciones", + "Click the arrow next to the send button in a channel to schedule a message.": "Haz clic en la flecha junto al botón de enviar en un canal para programar un mensaje.", "Close": "Cerrar", "Close dialog": "Cerrar diálogo", "Close emoji picker": "Cerrar el selector de emojis", @@ -517,8 +517,6 @@ "Message unpinned": "Mensaje desanclado", "Message was blocked by moderation policies": "El mensaje fue bloqueado por las políticas de moderación", "Messages have been marked unread.": "Los mensajes han sido marcados como no leídos.", - "Messages waiting to be sent": "Mensajes pendientes de envío", - "Messages you schedule appear here": "Los mensajes que programes aparecerán aquí", "Missing permissions to upload the attachment": "Faltan permisos para subir el archivo adjunto", "Moderator": "Moderador", "Multiple votes": "Votos múltiples", @@ -654,6 +652,7 @@ "Select up to {{count}}_other": "Selecciona hasta {{count}}", "Select your current location and optionally enable live location sharing": "Selecciona tu ubicación actual y, opcionalmente, habilita el uso compartido de ubicación en vivo", "Send": "Enviar", + "Send {{ time }}": "Enviar el {{ time }}", "Send a message": "Envía un mensaje", "Send a message to start the conversation": "Envía un mensaje para iniciar la conversación", "Send Anyway": "Enviar de todos modos", @@ -727,6 +726,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoy] [a las] HH:mm\", \"nextDay\": \"[Mañana] [a las] HH:mm\", \"lastDay\": \"[Ayer] [a las] HH:mm\", \"nextWeek\": \"dddd [a las] HH:mm\", \"lastWeek\": \"[Último] dddd [a las] HH:mm\", \"sameElse\": \"ddd, D MMM [a las] HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM [a las] HH:mm\", \"lastDay\": \"ddd, D MMM [a las] HH:mm\", \"nextWeek\": \"ddd, D MMM [a las] HH:mm\", \"lastWeek\": \"ddd, D MMM [a las] HH:mm\", \"sameElse\": \"ddd, D MMM [a las] HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoy]\", \"nextDay\": \"[Mañana]\", \"lastDay\": \"[Ayer]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Último] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM [a las] HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoy] [a las] HH:mm\", \"nextDay\": \"[Mañana] [a las] HH:mm\", \"lastDay\": \"[Ayer] [a las] HH:mm\", \"nextWeek\": \"dddd [a las] HH:mm\", \"lastWeek\": \"[Último] dddd [a las] HH:mm\", \"sameElse\": \"dddd [a las] HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Para comenzar a grabar, permita el acceso a la cámara en su navegador", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index bcfe80039..4bba46cb2 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -285,10 +285,10 @@ "Channels": "Canaux", "Chat deleted": "Chat deleted", "Chats": "Discussions", - "Check your connection and try again": "Vérifiez votre connexion et réessayez", "Choose a later time": "Choisissez une heure plus tardive", "Choose an earlier time": "Choisissez une heure plus proche", "Choose between 2 to 10 options": "Choisir entre 2 et 10 options", + "Click the arrow next to the send button in a channel to schedule a message.": "Cliquez sur la flèche à côté du bouton d'envoi dans un canal pour programmer un message.", "Close": "Fermer", "Close dialog": "Fermer la boîte de dialogue", "Close emoji picker": "Fermer le sélecteur d'émojis", @@ -517,8 +517,6 @@ "Message unpinned": "Message désépinglé", "Message was blocked by moderation policies": "Le message a été bloqué par les politiques de modération", "Messages have been marked unread.": "Les messages ont été marqués comme non lus.", - "Messages waiting to be sent": "Messages en attente d'envoi", - "Messages you schedule appear here": "Les messages que vous programmez apparaissent ici", "Missing permissions to upload the attachment": "Autorisations manquantes pour télécharger la pièce jointe", "Moderator": "Modérateur", "Multiple votes": "Votes multiples", @@ -654,6 +652,7 @@ "Select up to {{count}}_other": "Sélectionner jusqu'à {{count}}", "Select your current location and optionally enable live location sharing": "Sélectionnez votre position actuelle et activez éventuellement le partage de position en direct", "Send": "Envoyer", + "Send {{ time }}": "Envoi le {{ time }}", "Send a message": "Envoyez un message", "Send a message to start the conversation": "Envoyez un message pour commencer la conversation", "Send Anyway": "Envoyer quand même", @@ -727,6 +726,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Aujourd'hui] [à] HH:mm\", \"nextDay\": \"[Demain] [à] HH:mm\", \"lastDay\": \"[Hier] [à] HH:mm\", \"nextWeek\": \"dddd [à] HH:mm\", \"lastWeek\": \"dddd [dernier à] HH:mm\", \"sameElse\": \"ddd, D MMM [à] HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM [à] HH:mm\", \"lastDay\": \"ddd, D MMM [à] HH:mm\", \"nextWeek\": \"ddd, D MMM [à] HH:mm\", \"lastWeek\": \"ddd, D MMM [à] HH:mm\", \"sameElse\": \"ddd, D MMM [à] HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Aujourd'hui]\", \"nextDay\": \"[Demain]\", \"lastDay\": \"[Hier]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Dernier] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM [à] HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Aujourd'hui] [à] HH:mm\", \"nextDay\": \"[Demain] [à] HH:mm\", \"lastDay\": \"[Hier] [à] HH:mm\", \"nextWeek\": \"dddd [à] HH:mm\", \"lastWeek\": \"dddd [dernier à] HH:mm\", \"sameElse\": \"dddd [à] HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Pour commencer l'enregistrement, autorisez l'accès à la caméra dans votre navigateur", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index ac7db45b5..eb1d37107 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -266,10 +266,10 @@ "Channels": "चैनल", "Chat deleted": "Chat deleted", "Chats": "चैट", - "Check your connection and try again": "अपना कनेक्शन जांचें और फिर से कोशिश करें", "Choose a later time": "बाद का समय चुनें", "Choose an earlier time": "पहले का समय चुनें", "Choose between 2 to 10 options": "2 से 10 विकल्प चुनें", + "Click the arrow next to the send button in a channel to schedule a message.": "संदेश शेड्यूल करने के लिए किसी चैनल में भेजें बटन के पास वाले तीर पर क्लिक करें।", "Close": "बंद करे", "Close dialog": "डायलॉग बंद करें", "Close emoji picker": "इमोजी पिकर बंद करें", @@ -496,8 +496,6 @@ "Message unpinned": "संदेश अनपिन किया गया", "Message was blocked by moderation policies": "संदेश को मॉडरेशन नीतियों द्वारा ब्लॉक कर दिया गया है", "Messages have been marked unread.": "संदेशों को अपठित चिह्नित किया गया है।", - "Messages waiting to be sent": "भेजे जाने की प्रतीक्षा में संदेश", - "Messages you schedule appear here": "आपके द्वारा शेड्यूल किए गए संदेश यहाँ दिखाई देते हैं", "Missing permissions to upload the attachment": "अटैचमेंट अपलोड करने के लिए अनुमतियां गायब", "Moderator": "मॉडरेटर", "Multiple votes": "कई वोट", @@ -628,6 +626,7 @@ "Select up to {{count}}_other": "अधिकतम {{count}} तक चुनें", "Select your current location and optionally enable live location sharing": "अपना वर्तमान स्थान चुनें और वैकल्पिक रूप से लाइव लोकेशन शेयरिंग सक्षम करें", "Send": "भेजे", + "Send {{ time }}": "{{ time }} को भेजें", "Send a message": "संदेश भेजें", "Send a message to start the conversation": "बातचीत शुरू करने के लिए संदेश भेजें", "Send Anyway": "वैसे भी भेजें", @@ -700,6 +699,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[आज] HH:mm\", \"nextDay\": \"[कल] HH:mm\", \"lastDay\": \"[बीता कल] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"[पिछले] dddd, HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM HH:mm\", \"lastDay\": \"ddd, D MMM HH:mm\", \"nextWeek\": \"ddd, D MMM HH:mm\", \"lastWeek\": \"ddd, D MMM HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[आज]\", \"nextDay\": \"[कल]\", \"lastDay\": \"[बीता कल]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[पिछला] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[आज] HH:mm\", \"nextDay\": \"[कल] HH:mm\", \"lastDay\": \"[बीता कल] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"[पिछले] dddd, HH:mm\", \"sameElse\": \"dddd HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "रिकॉर्डिंग शुरू करने के लिए, अपने ब्राउज़र में कैमरा तक पहुँच दें", diff --git a/src/i18n/it.json b/src/i18n/it.json index 5676f4e83..406588779 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -285,10 +285,10 @@ "Channels": "Canali", "Chat deleted": "Chat deleted", "Chats": "Chat", - "Check your connection and try again": "Controlla la connessione e riprova", "Choose a later time": "Scegli un orario successivo", "Choose an earlier time": "Scegli un orario precedente", "Choose between 2 to 10 options": "Scegli tra 2 e 10 opzioni", + "Click the arrow next to the send button in a channel to schedule a message.": "Fai clic sulla freccia accanto al pulsante di invio in un canale per programmare un messaggio.", "Close": "Chiudi", "Close dialog": "Chiudi finestra di dialogo", "Close emoji picker": "Chiudi il selettore di emoji", @@ -517,8 +517,6 @@ "Message unpinned": "Messaggio rimosso dai fissati", "Message was blocked by moderation policies": "Il messaggio è stato bloccato dalle politiche di moderazione", "Messages have been marked unread.": "I messaggi sono stati contrassegnati come non letti.", - "Messages waiting to be sent": "Messaggi in attesa di invio", - "Messages you schedule appear here": "I messaggi che programmi appaiono qui", "Missing permissions to upload the attachment": "Autorizzazioni mancanti per caricare l'allegato", "Moderator": "Moderatore", "Multiple votes": "Voti multipli", @@ -654,6 +652,7 @@ "Select up to {{count}}_other": "Seleziona fino a {{count}}", "Select your current location and optionally enable live location sharing": "Seleziona la tua posizione attuale e abilita facoltativamente la condivisione della posizione in tempo reale", "Send": "Invia", + "Send {{ time }}": "Invio {{ time }}", "Send a message": "Invia un messaggio", "Send a message to start the conversation": "Invia un messaggio per iniziare la conversazione", "Send Anyway": "Invia comunque", @@ -727,6 +726,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Oggi] [alle] HH:mm\", \"nextDay\": \"[Domani] [alle] HH:mm\", \"lastDay\": \"[Ieri] [alle] HH:mm\", \"nextWeek\": \"dddd [alle] HH:mm\", \"lastWeek\": \"[lo scorso] dddd [alle] HH:mm\", \"sameElse\": \"ddd, D MMM [alle] HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM [alle] HH:mm\", \"lastDay\": \"ddd, D MMM [alle] HH:mm\", \"nextWeek\": \"ddd, D MMM [alle] HH:mm\", \"lastWeek\": \"ddd, D MMM [alle] HH:mm\", \"sameElse\": \"ddd, D MMM [alle] HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Oggi]\", \"nextDay\": \"[Domani]\", \"lastDay\": \"[Ieri]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Scorsa] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM [alle] HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Oggi] [alle] HH:mm\", \"nextDay\": \"[Domani] [alle] HH:mm\", \"lastDay\": \"[Ieri] [alle] HH:mm\", \"nextWeek\": \"dddd [alle] HH:mm\", \"lastWeek\": \"[lo scorso] dddd [alle] HH:mm\", \"sameElse\": \"dddd [alle] HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Per iniziare a registrare, consenti l'accesso alla fotocamera nel tuo browser", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 2e32e010a..b79f6be3f 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -255,10 +255,10 @@ "Channels": "チャンネル", "Chat deleted": "Chat deleted", "Chats": "チャット", - "Check your connection and try again": "接続を確認して、もう一度お試しください", "Choose a later time": "もっと後の時刻を選択してください", "Choose an earlier time": "もっと前の時刻を選択してください", "Choose between 2 to 10 options": "2〜10の選択肢から選ぶ", + "Click the arrow next to the send button in a channel to schedule a message.": "チャンネルの送信ボタンの横にある矢印をクリックして、メッセージを予約します。", "Close": "閉める", "Close dialog": "ダイアログを閉じる", "Close emoji picker": "絵文字ピッカーを閉める", @@ -481,8 +481,6 @@ "Message unpinned": "メッセージのピン留めを解除しました", "Message was blocked by moderation policies": "メッセージはモデレーションポリシーによってブロックされました", "Messages have been marked unread.": "メッセージは未読としてマークされました。", - "Messages waiting to be sent": "送信待ちのメッセージ", - "Messages you schedule appear here": "予約したメッセージはここに表示されます", "Missing permissions to upload the attachment": "添付ファイルをアップロードするための許可がありません", "Moderator": "モデレーター", "Multiple votes": "複数投票", @@ -611,6 +609,7 @@ "Select up to {{count}}_other": "最大{{count}}まで選択", "Select your current location and optionally enable live location sharing": "現在地を選択し、必要に応じてライブ位置情報共有を有効化", "Send": "送信", + "Send {{ time }}": "{{ time }}に送信", "Send a message": "メッセージを送る", "Send a message to start the conversation": "メッセージを送って会話を始めましょう", "Send Anyway": "とにかく送信する", @@ -682,6 +681,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[今日] HH:mm\", \"nextDay\": \"[明日] HH:mm\", \"lastDay\": \"[昨日] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"dddd HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM HH:mm\", \"lastDay\": \"ddd, D MMM HH:mm\", \"nextWeek\": \"ddd, D MMM HH:mm\", \"lastWeek\": \"ddd, D MMM HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[今日]\", \"nextDay\": \"[明日]\", \"lastDay\": \"[昨日]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[先週の] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[今日] HH:mm\", \"nextDay\": \"[明日] HH:mm\", \"lastDay\": \"[昨日] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"dddd HH:mm\", \"sameElse\": \"dddd HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "録音を開始するには、ブラウザーでカメラへのアクセスを許可してください", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 73d37b2b8..fab159b0e 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -255,10 +255,10 @@ "Channels": "채널", "Chat deleted": "Chat deleted", "Chats": "채팅", - "Check your connection and try again": "연결을 확인하고 다시 시도하세요", "Choose a later time": "더 늦은 시간을 선택하세요", "Choose an earlier time": "더 이른 시간을 선택하세요", "Choose between 2 to 10 options": "2~10개의 선택지 중에서 선택", + "Click the arrow next to the send button in a channel to schedule a message.": "채널에서 보내기 버튼 옆의 화살표를 클릭해 메시지를 예약하세요.", "Close": "닫기", "Close dialog": "대화 상자 닫기", "Close emoji picker": "이모티콘 선택기 닫기", @@ -481,8 +481,6 @@ "Message unpinned": "메시지 고정이 해제됨", "Message was blocked by moderation policies": "메시지가 관리 정책에 의해 차단되었습니다.", "Messages have been marked unread.": "메시지가 읽지 않음으로 표시되었습니다.", - "Messages waiting to be sent": "전송 대기 중인 메시지", - "Messages you schedule appear here": "예약한 메시지가 여기에 표시됩니다", "Missing permissions to upload the attachment": "첨부 파일을 업로드하려면 권한이 필요합니다", "Moderator": "운영자", "Multiple votes": "복수 투표", @@ -611,6 +609,7 @@ "Select up to {{count}}_other": "{{count}}개까지 선택", "Select your current location and optionally enable live location sharing": "현재 위치를 선택하고 필요 시 실시간 위치 공유를 활성화", "Send": "보내다", + "Send {{ time }}": "{{ time }}에 전송", "Send a message": "메시지 보내기", "Send a message to start the conversation": "대화를 시작하려면 메시지를 보내세요", "Send Anyway": "어쨌든 보내기", @@ -682,6 +681,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[오늘] HH:mm\", \"nextDay\": \"[내일] HH:mm\", \"lastDay\": \"[어제] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"[지난] dddd HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM HH:mm\", \"lastDay\": \"ddd, D MMM HH:mm\", \"nextWeek\": \"ddd, D MMM HH:mm\", \"lastWeek\": \"ddd, D MMM HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[오늘]\", \"nextDay\": \"[내일]\", \"lastDay\": \"[어제]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[지난] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[오늘] HH:mm\", \"nextDay\": \"[내일] HH:mm\", \"lastDay\": \"[어제] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"[지난] dddd HH:mm\", \"sameElse\": \"dddd HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "브라우저에서 카메라 액세스를 허용하여 녹음을 시작합니다", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 9306d7db6..0116f5f6a 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -266,10 +266,10 @@ "Channels": "Kanalen", "Chat deleted": "Chat deleted", "Chats": "Chats", - "Check your connection and try again": "Controleer je verbinding en probeer het opnieuw", "Choose a later time": "Kies een later tijdstip", "Choose an earlier time": "Kies een eerder tijdstip", "Choose between 2 to 10 options": "Kies tussen 2 en 10 opties", + "Click the arrow next to the send button in a channel to schedule a message.": "Klik op de pijl naast de verzendknop in een kanaal om een bericht te plannen.", "Close": "Sluit", "Close dialog": "Dialoog sluiten", "Close emoji picker": "Sluit de emoji-kiezer", @@ -495,8 +495,6 @@ "Message unpinned": "Bericht losgemaakt", "Message was blocked by moderation policies": "Bericht is geblokkeerd door moderatiebeleid", "Messages have been marked unread.": "Berichten zijn gemarkeerd als ongelezen.", - "Messages waiting to be sent": "Berichten die wachten op verzending", - "Messages you schedule appear here": "Berichten die je inplant verschijnen hier", "Missing permissions to upload the attachment": "Missende toestemmingen om de bijlage te uploaden", "Moderator": "Moderator", "Multiple votes": "Meerdere stemmen", @@ -627,6 +625,7 @@ "Select up to {{count}}_other": "Selecteer tot {{count}}", "Select your current location and optionally enable live location sharing": "Selecteer je huidige locatie en schakel eventueel live locatiedeling in", "Send": "Verstuur", + "Send {{ time }}": "Verzenden op {{ time }}", "Send a message": "Stuur een bericht", "Send a message to start the conversation": "Stuur een bericht om het gesprek te beginnen", "Send Anyway": "Toch versturen", @@ -701,6 +700,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Vandaag] [om] HH:mm\", \"nextDay\": \"[Morgen] [om] HH:mm\", \"lastDay\": \"[Gisteren] [om] HH:mm\", \"nextWeek\": \"dddd [om] HH:mm\", \"lastWeek\": \"[afgelopen] dddd [om] HH:mm\", \"sameElse\": \"ddd, D MMM [om] HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM [om] HH:mm\", \"lastDay\": \"ddd, D MMM [om] HH:mm\", \"nextWeek\": \"ddd, D MMM [om] HH:mm\", \"lastWeek\": \"ddd, D MMM [om] HH:mm\", \"sameElse\": \"ddd, D MMM [om] HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Vandaag]\", \"nextDay\": \"[Morgen]\", \"lastDay\": \"[Gisteren]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Laatste] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM [om] HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Vandaag] [om] HH:mm\", \"nextDay\": \"[Morgen] [om] HH:mm\", \"lastDay\": \"[Gisteren] [om] HH:mm\", \"nextWeek\": \"dddd [om] HH:mm\", \"lastWeek\": \"[afgelopen] dddd [om] HH:mm\", \"sameElse\": \"dddd [om] HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Om te beginnen met opnemen, sta toegang tot de camera toe in uw browser", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index c88c60fb1..64646bd25 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -285,10 +285,10 @@ "Channels": "Canais", "Chat deleted": "Chat deleted", "Chats": "Conversas", - "Check your connection and try again": "Verifique sua conexão e tente novamente", "Choose a later time": "Escolha um horário posterior", "Choose an earlier time": "Escolha um horário anterior", "Choose between 2 to 10 options": "Escolha entre 2 a 10 opções", + "Click the arrow next to the send button in a channel to schedule a message.": "Clique na seta ao lado do botão de enviar em um canal para agendar uma mensagem.", "Close": "Fechar", "Close dialog": "Fechar diálogo", "Close emoji picker": "Fechar seletor de emoji", @@ -517,8 +517,6 @@ "Message unpinned": "Mensagem desafixada", "Message was blocked by moderation policies": "A mensagem foi bloqueada pelas políticas de moderação", "Messages have been marked unread.": "Mensagens foram marcadas como não lidas.", - "Messages waiting to be sent": "Mensagens aguardando envio", - "Messages you schedule appear here": "As mensagens que você agendar aparecem aqui", "Missing permissions to upload the attachment": "Faltando permissões para enviar o anexo", "Moderator": "Moderador", "Multiple votes": "Votos múltiplos", @@ -654,6 +652,7 @@ "Select up to {{count}}_other": "Selecionar até {{count}}", "Select your current location and optionally enable live location sharing": "Selecione sua localização atual e, opcionalmente, ative o compartilhamento de localização ao vivo", "Send": "Enviar", + "Send {{ time }}": "Enviar em {{ time }}", "Send a message": "Envie uma mensagem", "Send a message to start the conversation": "Envie uma mensagem para iniciar a conversa", "Send Anyway": "Enviar de qualquer forma", @@ -727,6 +726,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoje] [às] HH:mm\", \"nextDay\": \"[Amanhã] [às] HH:mm\", \"lastDay\": \"[Ontem] [às] HH:mm\", \"nextWeek\": \"dddd [às] HH:mm\", \"lastWeek\": \"dddd [passada às] HH:mm\", \"sameElse\": \"ddd, D MMM [às] HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM [às] HH:mm\", \"lastDay\": \"ddd, D MMM [às] HH:mm\", \"nextWeek\": \"ddd, D MMM [às] HH:mm\", \"lastWeek\": \"ddd, D MMM [às] HH:mm\", \"sameElse\": \"ddd, D MMM [às] HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoje]\", \"nextDay\": \"[Amanhã]\", \"lastDay\": \"[Ontem]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Último] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM [às] HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoje] [às] HH:mm\", \"nextDay\": \"[Amanhã] [às] HH:mm\", \"lastDay\": \"[Ontem] [às] HH:mm\", \"nextWeek\": \"dddd [às] HH:mm\", \"lastWeek\": \"dddd [passada às] HH:mm\", \"sameElse\": \"dddd [às] HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Para começar a gravar, permita o acesso à câmera no seu navegador", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 58b6d8c94..cf5675373 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -305,10 +305,10 @@ "Channels": "Каналы", "Chat deleted": "Chat deleted", "Chats": "Чаты", - "Check your connection and try again": "Проверьте подключение и повторите попытку", "Choose a later time": "Выберите более позднее время", "Choose an earlier time": "Выберите более раннее время", "Choose between 2 to 10 options": "Выберите от 2 до 10 вариантов", + "Click the arrow next to the send button in a channel to schedule a message.": "Нажмите стрелку рядом с кнопкой отправки в канале, чтобы запланировать сообщение.", "Close": "Закрыть", "Close dialog": "Закрыть диалог", "Close emoji picker": "Закрыть окно выбора смайлов", @@ -543,8 +543,6 @@ "Message unpinned": "Сообщение откреплено", "Message was blocked by moderation policies": "Сообщение было заблокировано модерацией", "Messages have been marked unread.": "Сообщения были отмечены как непрочитанные.", - "Messages waiting to be sent": "Сообщения, ожидающие отправки", - "Messages you schedule appear here": "Здесь появятся запланированные вами сообщения", "Missing permissions to upload the attachment": "Отсутствуют разрешения для загрузки вложения", "Moderator": "Модератор", "Multiple votes": "Несколько голосов", @@ -685,6 +683,7 @@ "Select up to {{count}}_other": "Выберите до {{count}}", "Select your current location and optionally enable live location sharing": "Выберите ваше текущее местоположение и при необходимости включите передачу геопозиции в реальном времени", "Send": "Отправить", + "Send {{ time }}": "Отправить {{ time }}", "Send a message": "Отправьте сообщение", "Send a message to start the conversation": "Отправьте сообщение, чтобы начать разговор", "Send Anyway": "Мне всё равно, отправить", @@ -759,6 +758,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Сегодня] [в] HH:mm\", \"nextDay\": \"[Завтра] [в] HH:mm\", \"lastDay\": \"[Вчера] [в] HH:mm\", \"nextWeek\": \"dddd [в] HH:mm\", \"lastWeek\": \"dddd [в] HH:mm\", \"sameElse\": \"ddd, D MMM [в] HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM [в] HH:mm\", \"lastDay\": \"ddd, D MMM [в] HH:mm\", \"nextWeek\": \"ddd, D MMM [в] HH:mm\", \"lastWeek\": \"ddd, D MMM [в] HH:mm\", \"sameElse\": \"ddd, D MMM [в] HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Сегодня]\", \"nextDay\": \"[Завтра]\", \"lastDay\": \"[Вчера]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[В прошлый] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM [в] HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Сегодня] [в] HH:mm\", \"nextDay\": \"[Завтра] [в] HH:mm\", \"lastDay\": \"[Вчера] [в] HH:mm\", \"nextWeek\": \"dddd [в] HH:mm\", \"lastWeek\": \"dddd [в] HH:mm\", \"sameElse\": \"dddd [в] HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Для начала записи разрешите доступ к камере в вашем браузере", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index d01ae10e1..0011c0bf0 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -266,10 +266,10 @@ "Channels": "Kanallar", "Chat deleted": "Chat deleted", "Chats": "Sohbetler", - "Check your connection and try again": "Bağlantınızı kontrol edip tekrar deneyin", "Choose a later time": "Daha geç bir saat seçin", "Choose an earlier time": "Daha erken bir saat seçin", "Choose between 2 to 10 options": "2 ile 10 seçenek arasından seçin", + "Click the arrow next to the send button in a channel to schedule a message.": "Mesaj planlamak için bir kanalda gönder düğmesinin yanındaki oka tıklayın.", "Close": "Kapat", "Close dialog": "İletişim kutusunu kapat", "Close emoji picker": "Emoji seçiciyi kapat", @@ -495,8 +495,6 @@ "Message unpinned": "Mesaj sabitlemesi kaldırıldı", "Message was blocked by moderation policies": "Mesaj moderasyon politikaları tarafından engellendi", "Messages have been marked unread.": "Mesajlar okunmamış olarak işaretlendi.", - "Messages waiting to be sent": "Gönderilmeyi bekleyen mesajlar", - "Messages you schedule appear here": "Zamanladığınız mesajlar burada görünür", "Missing permissions to upload the attachment": "Ek yüklemek için izinler eksik", "Moderator": "Moderatör", "Multiple votes": "Çoklu oy", @@ -627,6 +625,7 @@ "Select up to {{count}}_other": "En fazla {{count}}'yi seçin", "Select your current location and optionally enable live location sharing": "Mevcut konumunuzu seçin ve isteğe bağlı olarak canlı konum paylaşımını etkinleştirin", "Send": "Gönder", + "Send {{ time }}": "{{ time }} gönder", "Send a message": "Bir mesaj gönderin", "Send a message to start the conversation": "Sohbete başlamak için bir mesaj gönderin", "Send Anyway": "Yine de gönder", @@ -699,6 +698,7 @@ "timestamp/ScheduledMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Bugün] [saat] HH:mm\", \"nextDay\": \"[Yarın] [saat] HH:mm\", \"lastDay\": \"[Dün] [saat] HH:mm\", \"nextWeek\": \"dddd [saat] HH:mm\", \"lastWeek\": \"[Geçen] dddd [saat] HH:mm\", \"sameElse\": \"ddd, D MMM [saat] HH:mm\" }) }}", "timestamp/ScheduledMessageConfirmation": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"HH:mm\", \"nextDay\": \"ddd, D MMM [saat] HH:mm\", \"lastDay\": \"ddd, D MMM [saat] HH:mm\", \"nextWeek\": \"ddd, D MMM [saat] HH:mm\", \"lastWeek\": \"ddd, D MMM [saat] HH:mm\", \"sameElse\": \"ddd, D MMM [saat] HH:mm\" }) }}", "timestamp/ScheduledMessageGroup": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Bugün]\", \"nextDay\": \"[Yarın]\", \"lastDay\": \"[Dün]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Geçen] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", + "timestamp/ScheduledMessageSendTime": "{{ timestamp | timestampFormatter(format: ddd, D MMM [saat] HH:mm) }}", "timestamp/SchedulePreset": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Bugün] [saat] HH:mm\", \"nextDay\": \"[Yarın] [saat] HH:mm\", \"lastDay\": \"[Dün] [saat] HH:mm\", \"nextWeek\": \"dddd [saat] HH:mm\", \"lastWeek\": \"[Geçen] dddd [saat] HH:mm\", \"sameElse\": \"dddd [saat] HH:mm\" }) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "To start recording, allow the camera access in your browser": "Kayıt yapmaya başlamak için tarayıcınızda kameraya erişime izin verin", From cf035b842ed2feb0d6f5ab8b36dc46403537f4a8 Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Tue, 3 Nov 2026 15:14:34 +0100 Subject: [PATCH 12/12] fix(ScheduledMessages): align the scheduled message rows in the channel detail The rows pulled themselves out by the prompt body padding everywhere, so in the channel detail section they overflowed the panel and their content sat off the title and search field. Inset the rows by default and only extend them to the edges inside the list dialog. --- .../ScheduledMessages/styling/ScheduledMessageList.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/ScheduledMessages/styling/ScheduledMessageList.scss b/src/components/ScheduledMessages/styling/ScheduledMessageList.scss index 9661e407d..059b3f378 100644 --- a/src/components/ScheduledMessages/styling/ScheduledMessageList.scss +++ b/src/components/ScheduledMessages/styling/ScheduledMessageList.scss @@ -9,6 +9,11 @@ .str-chat__scheduled-message-list-dialog__body { overflow-y: auto; + + .str-chat__scheduled-message-group__item, + .str-chat__scheduled-message-list-loading__row { + margin-inline: calc(-1 * var(--str-chat__spacing-xl)); + } } } @@ -51,7 +56,6 @@ } .str-chat__scheduled-message-group__item { - margin-inline: calc(-1 * var(--str-chat__spacing-xl)); padding: var(--str-chat__spacing-md) var(--str-chat__spacing-xl); border-block-end: 1px solid var(--str-chat__border-core-subtle); } @@ -165,7 +169,6 @@ display: flex; flex-direction: column; gap: var(--str-chat__spacing-xs); - margin-inline: calc(-1 * var(--str-chat__spacing-xl)); padding: var(--str-chat__spacing-md) var(--str-chat__spacing-xl); border-block-end: 1px solid var(--str-chat__border-core-subtle); }