diff --git a/src/components/MessageComposer/MessageComposerUI.tsx b/src/components/MessageComposer/MessageComposerUI.tsx
index f70a909060..ecb03aad86 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 && }
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/ScheduledMessageGroupHeader.tsx b/src/components/ScheduledMessages/ScheduledMessageGroupHeader.tsx
new file mode 100644
index 0000000000..9159a2f0bf
--- /dev/null
+++ b/src/components/ScheduledMessages/ScheduledMessageGroupHeader.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import { useTranslationContext } from '../../context';
+import { ScheduledMessageGroupType } from './constants';
+import type { ScheduledMessageGroupHeaderProps } from './types';
+
+export const ScheduledMessageGroupHeader = ({
+ group,
+}: ScheduledMessageGroupHeaderProps) => {
+ 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 0000000000..5d1275f22c
--- /dev/null
+++ b/src/components/ScheduledMessages/ScheduledMessageItem.tsx
@@ -0,0 +1,62 @@
+import clsx from 'clsx';
+import React, { useMemo } from 'react';
+
+import { useChatContext, useTranslationContext } from '../../context';
+import { ChannelAvatar } from '../Avatar';
+import { useChannelPreviewInfo } from '../ChannelListItem/hooks/useChannelPreviewInfo';
+import { SummarizedMessagePreview } from '../SummarizedMessagePreview';
+import { ScheduledMessageItemStatus } from './ScheduledMessageItemStatus';
+import type { ScheduledMessageItemProps } from './types';
+import { isFailedScheduledMessage, toStoredScheduledMessagePreview } from './utils';
+
+export const ScheduledMessageItem = ({ scheduledMessage }: ScheduledMessageItemProps) => {
+ const { t } = useTranslationContext();
+ const { client } = useChatContext();
+
+ const { cid, scheduled_at } = scheduledMessage;
+ const channel = client.activeChannels[cid];
+ const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({
+ channel,
+ });
+
+ const previewMessage = useMemo(
+ () => toStoredScheduledMessagePreview(scheduledMessage),
+ [scheduledMessage],
+ );
+
+ const isFailed = isFailedScheduledMessage(scheduledMessage);
+ const sendTime = t('timestamp/ScheduledMessageSendTime', {
+ timestamp: new Date(scheduled_at),
+ });
+
+ return (
+
+
+
+
+
+ {displayTitle && (
+
+ {displayTitle}
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/components/ScheduledMessages/ScheduledMessageItemStatus.tsx b/src/components/ScheduledMessages/ScheduledMessageItemStatus.tsx
new file mode 100644
index 0000000000..dd3cbb68c1
--- /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/ScheduledMessageList.tsx b/src/components/ScheduledMessages/ScheduledMessageList.tsx
new file mode 100644
index 0000000000..4610fa6488
--- /dev/null
+++ b/src/components/ScheduledMessages/ScheduledMessageList.tsx
@@ -0,0 +1,78 @@
+import React from 'react';
+
+import { useComponentContext, useTranslationContext } from '../../context';
+import { Button } from '../Button';
+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,
+ searchQuery,
+}: ScheduledMessageListProps) => {
+ const { t } = useTranslationContext();
+ const { ScheduledMessageItem = DefaultScheduledMessageItem } = useComponentContext();
+
+ const { groups, hasScheduledMessages } = useScheduledMessageGroups({
+ cid,
+ parentId,
+ searchQuery,
+ });
+ const { canLoadMore, isLoading, loadMore } = useScheduledMessagesPagination();
+ const { hasLoadFailed, isFirstLoad, reload } = useScheduledMessagesLoadState();
+
+ if (!groups.length) {
+ if (hasScheduledMessages) return
;
+
+ if (isFirstLoad) return
;
+
+ if (hasLoadFailed) return
;
+
+ return
;
+ }
+
+ 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 0000000000..53d6a0497a
--- /dev/null
+++ b/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx
@@ -0,0 +1,53 @@
+import React, { useCallback, useState } from 'react';
+import type { ChangeEvent } from 'react';
+
+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';
+
+export const ScheduledMessageListDialog = ({
+ cid,
+ close,
+ 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');
+
+ return (
+
+
+
+ {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
new file mode 100644
index 0000000000..60093e56ab
--- /dev/null
+++ b/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx
@@ -0,0 +1,40 @@
+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/ScheduledMessagesBanner.tsx b/src/components/ScheduledMessages/ScheduledMessagesBanner.tsx
new file mode 100644
index 0000000000..afa44f4787
--- /dev/null
+++ b/src/components/ScheduledMessages/ScheduledMessagesBanner.tsx
@@ -0,0 +1,77 @@
+import React, { useRef } from 'react';
+
+import {
+ useChannelStateContext,
+ useComponentContext,
+ useComponentContextIcons,
+ useTranslationContext,
+} from '../../context';
+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 { 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 0000000000..7f5bc6a632
--- /dev/null
+++ b/src/components/ScheduledMessages/ScheduledMessagesEmptyList.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+import { useComponentContextIcons, useTranslationContext } from '../../context';
+import { ScheduledMessagesEmptyState } from './ScheduledMessagesEmptyState';
+
+export const ScheduledMessagesEmptyList = () => {
+ const { t } = useTranslationContext();
+ const { IconClock } = useComponentContextIcons();
+
+ return (
+
+ );
+};
diff --git a/src/components/ScheduledMessages/ScheduledMessagesEmptyState.tsx b/src/components/ScheduledMessages/ScheduledMessagesEmptyState.tsx
new file mode 100644
index 0000000000..2110487d24
--- /dev/null
+++ b/src/components/ScheduledMessages/ScheduledMessagesEmptyState.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+import type { ScheduledMessagesEmptyStateProps } from './types';
+
+export const ScheduledMessagesEmptyState = ({
+ children,
+ description,
+ Icon,
+ title,
+}: ScheduledMessagesEmptyStateProps) => (
+
+
+
{title}
+ {description && (
+
+ {description}
+
+ )}
+ {children}
+
+);
diff --git a/src/components/ScheduledMessages/ScheduledMessagesLoadError.tsx b/src/components/ScheduledMessages/ScheduledMessagesLoadError.tsx
new file mode 100644
index 0000000000..1838252106
--- /dev/null
+++ b/src/components/ScheduledMessages/ScheduledMessagesLoadError.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+
+import { useComponentContextIcons, useTranslationContext } from '../../context';
+import { Button } from '../Button';
+import { ScheduledMessagesEmptyState } from './ScheduledMessagesEmptyState';
+import type { ScheduledMessagesLoadErrorProps } from './types';
+
+export const ScheduledMessagesLoadError = ({
+ onRetry,
+}: ScheduledMessagesLoadErrorProps) => {
+ const { t } = useTranslationContext();
+ const { IconExclamationMark } = useComponentContextIcons();
+
+ return (
+
+
+
+ );
+};
diff --git a/src/components/ScheduledMessages/ScheduledMessagesNoResults.tsx b/src/components/ScheduledMessages/ScheduledMessagesNoResults.tsx
new file mode 100644
index 0000000000..1af9f159f0
--- /dev/null
+++ b/src/components/ScheduledMessages/ScheduledMessagesNoResults.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+import { useComponentContextIcons, useTranslationContext } from '../../context';
+import { ScheduledMessagesEmptyState } from './ScheduledMessagesEmptyState';
+
+export const ScheduledMessagesNoResults = () => {
+ const { t } = useTranslationContext();
+ const { IconSearch } = useComponentContextIcons();
+
+ return ;
+};
diff --git a/src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx b/src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx
new file mode 100644
index 0000000000..a98e659189
--- /dev/null
+++ b/src/components/ScheduledMessages/__tests__/ScheduledMessageList.test.tsx
@@ -0,0 +1,307 @@
+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[];
+ searchQuery?: string;
+};
+
+const renderList = async ({
+ components = {},
+ scheduledMessages = [],
+ searchQuery,
+}: 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(
+ '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 () => {
+ const { client } = await renderList();
+
+ act(() => client.scheduledMessages.state.partialNext({ isLoading: true }));
+
+ expect(screen.queryByText('No scheduled messages')).not.toBeInTheDocument();
+ expect(
+ screen.getByRole('status', { name: 'Loading scheduled messages' }),
+ ).toBeInTheDocument();
+ });
+
+ it('shows the empty state while a later reload runs', async () => {
+ const { client } = await renderList();
+
+ act(() =>
+ client.scheduledMessages.state.partialNext({ isLoaded: true, isLoading: true }),
+ );
+
+ expect(screen.getByText('No scheduled messages')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('scheduled-message-list-loading'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('shows the load error and reloads on retry', async () => {
+ const { client } = await renderList();
+ const load = vi.spyOn(client.scheduledMessages, 'load').mockResolvedValue({
+ duration: '',
+ scheduled_messages: [],
+ });
+
+ act(() =>
+ client.scheduledMessages.state.partialNext({
+ lastQueryError: new Error('offline'),
+ }),
+ );
+ fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
+
+ expect(screen.getByText("Couldn't load scheduled messages")).toBeInTheDocument();
+ expect(load).toHaveBeenCalledTimes(1);
+ });
+
+ it('keeps showing stored messages when a reload fails', async () => {
+ const { client } = await renderList({
+ scheduledMessages: [
+ generateScheduledMessage({ cid, message: { text: 'Launch moved to Monday' } }),
+ ],
+ });
+
+ act(() =>
+ client.scheduledMessages.state.partialNext({
+ lastQueryError: new Error('offline'),
+ }),
+ );
+
+ expect(screen.getByText('Launch moved to Monday')).toBeInTheDocument();
+ expect(
+ screen.queryByText("Couldn't load scheduled messages"),
+ ).not.toBeInTheDocument();
+ });
+
+ it('shows only the messages matching the search query', async () => {
+ await renderList({
+ scheduledMessages: [
+ generateScheduledMessage({ cid, message: { text: 'Launch moved to Monday' } }),
+ generateScheduledMessage({ cid, message: { text: 'Lunch at noon' } }),
+ ],
+ searchQuery: 'launch',
+ });
+
+ expect(screen.getByText('Launch moved to Monday')).toBeInTheDocument();
+ expect(screen.queryByText('Lunch at noon')).not.toBeInTheDocument();
+ });
+
+ it('shows no results when the search matches nothing', async () => {
+ await renderList({
+ scheduledMessages: [
+ generateScheduledMessage({ cid, message: { text: 'Launch moved to Monday' } }),
+ ],
+ searchQuery: 'friday',
+ });
+
+ expect(screen.getByText('No messages found')).toBeInTheDocument();
+ expect(screen.queryByText('No scheduled messages')).not.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 send time and preview 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('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 () => {
+ 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.each([
+ ['loading', { isLoading: true }],
+ ['load error', { lastQueryError: new Error('offline') }],
+ ])('has no accessibility violations in the %s state', async (_, state) => {
+ const { client, container } = await renderList();
+
+ act(() => client.scheduledMessages.state.partialNext(state));
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+
+ 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 0000000000..53285fd3e9
--- /dev/null
+++ b/src/components/ScheduledMessages/__tests__/ScheduledMessagesBanner.test.tsx
@@ -0,0 +1,247 @@
+import React from 'react';
+import type { PropsWithChildren } from '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';
+
+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 all', async () => {
+ await renderComposer({
+ scheduledMessages: [
+ generateScheduledMessage({ cid, message: { text: 'Launch moved to Monday' } }),
+ ],
+ });
+
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'View all' })));
+
+ const dialog = await screen.findByTestId('scheduled-message-list-dialog');
+
+ expect(dialog).toHaveTextContent('Scheduled messages');
+ 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 all' })));
+
+ 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/constants.ts b/src/components/ScheduledMessages/constants.ts
index 0823441a74..8ef597f5bc 100644
--- a/src/components/ScheduledMessages/constants.ts
+++ b/src/components/ScheduledMessages/constants.ts
@@ -38,3 +38,15 @@ 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';
+
+export const SCHEDULED_MESSAGE_LIST_LOADING_ROW_COUNT = 6;
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 0000000000..24077864a4
--- /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 f04801deb4..03756714fd 100644
--- a/src/components/ScheduledMessages/hooks/index.ts
+++ b/src/components/ScheduledMessages/hooks/index.ts
@@ -1,8 +1,11 @@
export * from './useCanScheduleMessage';
export * from './useIsConnectionOnline';
export * from './useScheduledMessageCount';
+export * from './useScheduledMessageGroups';
export * from './useScheduledMessagePreview';
export * from './useScheduledMessages';
+export * from './useScheduledMessagesLoadState';
+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 0000000000..b9409457b2
--- /dev/null
+++ b/src/components/ScheduledMessages/hooks/useScheduledMessageGroups.ts
@@ -0,0 +1,29 @@
+import { useMemo } from 'react';
+
+import type { ScheduledMessageGroups, UseScheduledMessageGroupsParams } from '../types';
+import { filterScheduledMessagesByText, groupScheduledMessages } from '../utils';
+import { useScheduledMessages } from './useScheduledMessages';
+
+export const useScheduledMessageGroups = ({
+ cid,
+ parentId,
+ searchQuery,
+}: UseScheduledMessageGroupsParams) => {
+ const { isLoaded, scheduledMessages } = useScheduledMessages({ cid, parentId });
+
+ const groups = useMemo(
+ () =>
+ groupScheduledMessages(
+ filterScheduledMessagesByText(scheduledMessages, searchQuery),
+ ),
+ [scheduledMessages, searchQuery],
+ );
+
+ const scheduledMessageGroups: ScheduledMessageGroups = {
+ groups,
+ hasScheduledMessages: scheduledMessages.length > 0,
+ isLoaded,
+ };
+
+ return scheduledMessageGroups;
+};
diff --git a/src/components/ScheduledMessages/hooks/useScheduledMessagesLoadState.ts b/src/components/ScheduledMessages/hooks/useScheduledMessagesLoadState.ts
new file mode 100644
index 0000000000..d6a29a274b
--- /dev/null
+++ b/src/components/ScheduledMessages/hooks/useScheduledMessagesLoadState.ts
@@ -0,0 +1,39 @@
+import { useCallback } from 'react';
+import type { ScheduledMessageManagerState } from 'stream-chat';
+
+import { useChatContext } from '../../../context';
+import { useStateStore } from '../../../store';
+import type { ScheduledMessagesLoadState } from '../types';
+
+const loadStateSelector = ({
+ isLoaded,
+ isLoading,
+ lastQueryError,
+}: ScheduledMessageManagerState) => ({
+ hasLoadFailed: !isLoaded && !isLoading && !!lastQueryError,
+ isFirstLoad: !isLoaded && isLoading,
+});
+
+export const useScheduledMessagesLoadState = () => {
+ const { client } = useChatContext('useScheduledMessagesLoadState');
+ const { scheduledMessages: manager } = client;
+
+ const { hasLoadFailed, isFirstLoad } = useStateStore(manager.state, loadStateSelector);
+
+ const reload = useCallback(() => {
+ manager.load().catch((error: Error) =>
+ client.logger('warn', 'scheduledMessages: reload failed', {
+ error,
+ tags: ['scheduled_messages'],
+ }),
+ );
+ }, [client, manager]);
+
+ const scheduledMessagesLoadState: ScheduledMessagesLoadState = {
+ hasLoadFailed,
+ isFirstLoad,
+ reload,
+ };
+
+ return scheduledMessagesLoadState;
+};
diff --git a/src/components/ScheduledMessages/hooks/useScheduledMessagesPagination.ts b/src/components/ScheduledMessages/hooks/useScheduledMessagesPagination.ts
new file mode 100644
index 0000000000..d76dda4d93
--- /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;
+};
diff --git a/src/components/ScheduledMessages/index.ts b/src/components/ScheduledMessages/index.ts
index c92f1e6606..2f88fa541b 100644
--- a/src/components/ScheduledMessages/index.ts
+++ b/src/components/ScheduledMessages/index.ts
@@ -1,9 +1,21 @@
export {
SCHEDULE_MESSAGE_CAPABILITY,
+ ScheduledMessageGroupType,
ScheduleMessageNoticeSeverity,
ScheduleTimeError,
} from './constants';
export * from './hooks';
+export * from './ScheduledMessageGroupHeader';
+export * from './ScheduledMessageItem';
+export * from './ScheduledMessageItemStatus';
+export * from './ScheduledMessageList';
+export * from './ScheduledMessageListDialog';
+export * from './ScheduledMessageListLoading';
+export * from './ScheduledMessagesBanner';
+export * from './ScheduledMessagesEmptyList';
+export * from './ScheduledMessagesEmptyState';
+export * from './ScheduledMessagesLoadError';
+export * from './ScheduledMessagesNoResults';
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 0000000000..059b3f378c
--- /dev/null
+++ b/src/components/ScheduledMessages/styling/ScheduledMessageList.scss
@@ -0,0 +1,246 @@
+@use '../../../styling/utils';
+
+.str-chat {
+ --str-chat__scheduled-message-list-dialog-max-width: 520px;
+
+ .str-chat__scheduled-message-list-dialog {
+ 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-group__item,
+ .str-chat__scheduled-message-list-loading__row {
+ margin-inline: calc(-1 * var(--str-chat__spacing-xl));
+ }
+ }
+ }
+
+ .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;
+
+ .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;
+
+ .str-chat__scheduled-message-group__header {
+ 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 {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ .str-chat__scheduled-message-group__item {
+ 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__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-metadata-default);
+ }
+
+ .str-chat__scheduled-message-item__status--failed {
+ color: var(--str-chat__accent-error);
+ font: var(--str-chat__font-caption-default);
+ }
+
+ .str-chat__scheduled-message-item__status-icon {
+ flex-shrink: 0;
+ height: var(--str-chat__icon-size-sm);
+ width: var(--str-chat__icon-size-sm);
+ }
+ }
+
+ .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);
+ }
+
+ .str-chat__scheduled-messages-empty-list__action {
+ margin-block-start: var(--str-chat__spacing-sm);
+ }
+ }
+
+ .str-chat__scheduled-message-list-loading {
+ display: flex;
+ flex-direction: column;
+
+ .str-chat__scheduled-message-list-loading__row {
+ display: flex;
+ flex-direction: column;
+ gap: var(--str-chat__spacing-xs);
+ 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);
+ border-radius: 999px;
+ background-image: linear-gradient(
+ 90deg,
+ var(--str-chat__skeleton-loading-base) 0%,
+ var(--str-chat__skeleton-loading-highlight) 50%,
+ var(--str-chat__skeleton-loading-base) 100%
+ );
+ background-repeat: no-repeat;
+ background-size: 200% 100%;
+ 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: 40%;
+ }
+
+ .str-chat__scheduled-message-list-loading__subtitle {
+ width: 80%;
+ }
+ }
+}
+
+@keyframes str-chat__scheduled-message-list-loading-shimmer {
+ from {
+ background-position: 200% 0;
+ }
+
+ to {
+ background-position: -200% 0;
+ }
+}
+
+@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/styling/ScheduledMessagesBanner.scss b/src/components/ScheduledMessages/styling/ScheduledMessagesBanner.scss
new file mode 100644
index 0000000000..03e4edd8db
--- /dev/null
+++ b/src/components/ScheduledMessages/styling/ScheduledMessagesBanner.scss
@@ -0,0 +1,43 @@
+@use '../../../styling/utils';
+
+.str-chat {
+ .str-chat__scheduled-messages-banner {
+ display: flex;
+ align-items: center;
+ gap: var(--str-chat__spacing-sm);
+ width: 100%;
+ 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-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;
+ }
+
+ .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;
+ }
+ }
+ }
+}
diff --git a/src/components/ScheduledMessages/styling/index.scss b/src/components/ScheduledMessages/styling/index.scss
index 594c108d64..20164baf1c 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/components/ScheduledMessages/types.ts b/src/components/ScheduledMessages/types.ts
index 9216dd34d1..9f15710a85 100644
--- a/src/components/ScheduledMessages/types.ts
+++ b/src/components/ScheduledMessages/types.ts
@@ -1,7 +1,13 @@
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 { IconComponent } from '../Icons';
+
+import type {
+ ScheduledMessageGroupType,
+ ScheduleMessageNoticeSeverity,
+ ScheduleTimeError,
+} from './constants';
export type UseScheduledMessagesParams = {
cid?: string;
@@ -26,11 +32,40 @@ 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 UseScheduledMessageGroupsParams = UseScheduledMessagesParams & {
+ searchQuery?: string;
+};
+
+export type ScheduledMessageGroups = {
+ groups: ScheduledMessageGroup[];
+ hasScheduledMessages: boolean;
+ isLoaded: boolean;
+};
+
+export type ScheduledMessagesLoadState = {
+ hasLoadFailed: boolean;
+ isFirstLoad: boolean;
+ reload: () => void;
+};
+
+export type ScheduledMessagesPagination = {
+ canLoadMore: boolean;
+ isLoading: boolean;
+ loadMore: () => void;
+};
+
export type ScheduleTimeForm = {
bounds: ScheduleTimeBounds;
dateValue: string;
@@ -72,3 +107,39 @@ export type ScheduleMessageDialogNoticeProps = {
children: ReactNode;
severity: ScheduleMessageNoticeSeverity;
};
+
+export type ScheduledMessageListProps = {
+ cid: string;
+ parentId?: string;
+ searchQuery?: string;
+};
+
+export type ScheduledMessagesEmptyStateProps = {
+ Icon: IconComponent;
+ title: string;
+ children?: ReactNode;
+ description?: string;
+};
+
+export type ScheduledMessagesLoadErrorProps = {
+ onRetry: () => void;
+};
+
+export type ScheduledMessageListDialogProps = Omit<
+ ScheduledMessageListProps,
+ 'searchQuery'
+> & {
+ close: () => void;
+};
+
+export type ScheduledMessageGroupHeaderProps = {
+ group: ScheduledMessageGroup;
+};
+
+export type ScheduledMessageItemProps = {
+ scheduledMessage: ScheduledMessageResponse;
+};
+
+export type ScheduledMessageItemStatusProps = {
+ 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 0000000000..e9c7ae63a5
--- /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 0000000000..2a27d0e4af
--- /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 4234cddd13..16dbecd665 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/__tests__/scheduledMessageSearch.test.ts b/src/components/ScheduledMessages/utils/__tests__/scheduledMessageSearch.test.ts
new file mode 100644
index 0000000000..2a5e8a6050
--- /dev/null
+++ b/src/components/ScheduledMessages/utils/__tests__/scheduledMessageSearch.test.ts
@@ -0,0 +1,29 @@
+import { filterScheduledMessagesByText } from '../scheduledMessageSearch';
+import { generateScheduledMessage } from '../../../../mock-builders';
+
+const launch = generateScheduledMessage({ message: { text: 'Launch moved to Monday' } });
+const lunch = generateScheduledMessage({ message: { text: 'Lunch at noon' } });
+const attachmentOnly = generateScheduledMessage({ message: { text: '' } });
+
+const scheduledMessages = [launch, lunch, attachmentOnly];
+
+describe('filterScheduledMessagesByText', () => {
+ it('returns every message for an empty or blank query', () => {
+ expect(filterScheduledMessagesByText(scheduledMessages)).toBe(scheduledMessages);
+ expect(filterScheduledMessagesByText(scheduledMessages, ' ')).toBe(
+ scheduledMessages,
+ );
+ });
+
+ it('matches the message text case-insensitively', () => {
+ expect(filterScheduledMessagesByText(scheduledMessages, 'MONDAY')).toEqual([launch]);
+ });
+
+ it('ignores surrounding whitespace in the query', () => {
+ expect(filterScheduledMessagesByText(scheduledMessages, ' lunch ')).toEqual([lunch]);
+ });
+
+ it('returns no messages when nothing matches', () => {
+ expect(filterScheduledMessagesByText(scheduledMessages, 'friday')).toEqual([]);
+ });
+});
diff --git a/src/components/ScheduledMessages/utils/channelContainer.ts b/src/components/ScheduledMessages/utils/channelContainer.ts
new file mode 100644
index 0000000000..48dd86097e
--- /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 88f041ca3d..e2e470df36 100644
--- a/src/components/ScheduledMessages/utils/index.ts
+++ b/src/components/ScheduledMessages/utils/index.ts
@@ -1,7 +1,11 @@
+export * from './channelContainer';
export * from './dateTimeInputValues';
export * from './nativePicker';
export * from './schedulePresets';
export * from './scheduledMessageErrors';
+export * from './scheduledMessageFailureReason';
+export * from './scheduledMessageGroups';
export * from './scheduledMessagePreview';
+export * from './scheduledMessageSearch';
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 0000000000..d03e5b371d
--- /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 0000000000..531fdd3af4
--- /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 09fc3c900c..9c87153d19 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;
+};
diff --git a/src/components/ScheduledMessages/utils/scheduledMessageSearch.ts b/src/components/ScheduledMessages/utils/scheduledMessageSearch.ts
new file mode 100644
index 0000000000..e411530608
--- /dev/null
+++ b/src/components/ScheduledMessages/utils/scheduledMessageSearch.ts
@@ -0,0 +1,20 @@
+import type { ScheduledMessageResponse } from 'stream-chat';
+
+const normalizeSearchText = (text: string) => text.trim().toLocaleLowerCase();
+
+export const filterScheduledMessagesByText = (
+ scheduledMessages: ScheduledMessageResponse[],
+ searchQuery = '',
+) => {
+ const normalizedQuery = normalizeSearchText(searchQuery);
+
+ if (!normalizedQuery) return scheduledMessages;
+
+ const matchingScheduledMessages = scheduledMessages.filter(({ message }) => {
+ const { text = '' } = message;
+
+ return normalizeSearchText(text).includes(normalizedQuery);
+ });
+
+ return matchingScheduledMessages;
+};
diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx
index 68f5601cf6..0d9b0ee535 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) */
diff --git a/src/i18n/de.json b/src/i18n/de.json
index 199b82a9cc..8f47a8f5c9 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",
@@ -264,6 +269,7 @@
"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",
@@ -279,6 +285,7 @@
"Contact name": "Kontaktname",
"Cooking up something good": "Ich koche etwas Feines zusammen",
"Copy Message": "Nachricht kopieren",
+ "Couldn't load scheduled messages": "Geplante Nachrichten konnten nicht geladen werden",
"Couldn't schedule message. Try again": "Nachricht konnte nicht geplant werden. Versuche es erneut",
"Create": "Erstellen",
"Create a question, add options, and configure poll settings": "Erstelle eine Frage, füge Optionen hinzu und konfiguriere die Umfrageeinstellungen",
@@ -349,6 +356,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",
@@ -460,6 +468,7 @@
"Live until {{ timestamp }}": "Live bis {{ timestamp }}",
"Load more": "Mehr laden",
"Loading chart...": "Diagramm wird geladen...",
+ "Loading scheduled messages": "Geplante Nachrichten werden geladen",
"Local upload attachment missing local id": "Lokaler Upload-Anhang hat keine lokale ID",
"Location": "Standort",
"Location sharing ended": "Standortfreigabe beendet",
@@ -509,8 +518,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",
@@ -560,6 +571,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",
@@ -592,7 +604,10 @@
"Saved for later": "Für später gespeichert",
"Schedule": "Planen",
"Schedule message": "Nachricht planen",
+ "Scheduled": "Scheduled",
+ "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",
@@ -610,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",
@@ -632,6 +648,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 +658,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",
@@ -676,6 +697,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: ddd, D. MMM YYYY) }}",
"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",
@@ -685,6 +708,7 @@
"Translated": "Übersetzt",
"Translated from {{ language }}": "Übersetzung aus {{ language }}",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "Erneut versuchen",
"Type a number from 2 to 10": "Geben Sie eine Zahl von 2 bis 10 ein",
"Unarchive": "Archivierung aufheben",
"unban-command-args": "[@Benutzername]",
@@ -741,6 +765,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 5bfab15994..1f4bd92884 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",
@@ -264,6 +269,7 @@
"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",
@@ -279,6 +285,7 @@
"Contact name": "Contact name",
"Cooking up something good": "Cooking up something good",
"Copy Message": "Copy Message",
+ "Couldn't load scheduled messages": "Couldn't load scheduled messages",
"Couldn't schedule message. Try again": "Couldn't schedule message. Try again",
"Create": "Create",
"Create a question, add options, and configure poll settings": "Create a question, add options, and configure poll settings",
@@ -349,6 +356,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}}",
@@ -460,6 +468,7 @@
"Live until {{ timestamp }}": "Live until {{ timestamp }}",
"Load more": "Load more",
"Loading chart...": "Loading chart...",
+ "Loading scheduled messages": "Loading scheduled messages",
"Local upload attachment missing local id": "Local upload attachment missing local id",
"Location": "Location",
"Location sharing ended": "Location sharing ended",
@@ -509,8 +518,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",
@@ -560,6 +571,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",
@@ -592,7 +604,10 @@
"Saved for later": "Saved for later",
"Schedule": "Schedule",
"Schedule message": "Schedule message",
+ "Scheduled": "Scheduled",
+ "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",
@@ -610,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",
@@ -632,6 +648,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 +658,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",
@@ -676,6 +697,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: ddd, MMM D, YYYY) }}",
"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",
@@ -685,6 +708,7 @@
"Translated": "Translated",
"Translated from {{ language }}": "Translated from {{ language }}",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "Try again",
"Type a number from 2 to 10": "Type a number from 2 to 10",
"Unarchive": "Unarchive",
"unban-command-args": "[@username]",
@@ -741,6 +765,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 5d9e7cb480..00d35487be 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",
@@ -281,6 +288,7 @@
"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",
@@ -296,6 +304,7 @@
"Contact name": "Nombre del contacto",
"Cooking up something good": "Cocinando algo bueno",
"Copy Message": "Copiar mensaje",
+ "Couldn't load scheduled messages": "No se pudieron cargar los mensajes programados",
"Couldn't schedule message. Try again": "No se pudo programar el mensaje. Inténtalo de nuevo",
"Create": "Crear",
"Create a question, add options, and configure poll settings": "Crea una pregunta, añade opciones y configura los ajustes de la encuesta",
@@ -366,6 +375,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}}",
@@ -480,6 +490,7 @@
"Live until {{ timestamp }}": "En vivo hasta {{ timestamp }}",
"Load more": "Cargar más",
"Loading chart...": "Cargando gráfico...",
+ "Loading scheduled messages": "Cargando mensajes programados",
"Local upload attachment missing local id": "El adjunto de subida local no tiene id local",
"Location": "Ubicación",
"Location sharing ended": "Compartir ubicación terminado",
@@ -529,8 +540,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",
@@ -580,6 +593,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",
@@ -615,7 +629,10 @@
"Saved for later": "Guardado para más tarde",
"Schedule": "Programar",
"Schedule message": "Programar mensaje",
+ "Scheduled": "Programado",
+ "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",
@@ -635,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",
@@ -657,6 +675,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 +685,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",
@@ -702,6 +725,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: ddd, D MMM YYYY) }}",
"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",
@@ -712,6 +737,7 @@
"Translated": "Traducido",
"Translated from {{ language }}": "Traducido de {{ language }}",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "Intentar de nuevo",
"Type a number from 2 to 10": "Escribe un número del 2 al 10",
"Unarchive": "Desarchivar",
"unban-command-args": "[@usuario]",
@@ -771,6 +797,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 9686309672..4bba46cb21 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",
@@ -281,6 +288,7 @@
"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",
@@ -296,6 +304,7 @@
"Contact name": "Nom du contact",
"Cooking up something good": "Je mijote quelque chose de bon",
"Copy Message": "Copier le message",
+ "Couldn't load scheduled messages": "Impossible de charger les messages programmés",
"Couldn't schedule message. Try again": "Impossible de programmer le message. Réessayez",
"Create": "Créer",
"Create a question, add options, and configure poll settings": "Créez une question, ajoutez des options et configurez les paramètres du sondage",
@@ -366,6 +375,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}}",
@@ -480,6 +490,7 @@
"Live until {{ timestamp }}": "En direct jusqu'à {{ timestamp }}",
"Load more": "Charger plus",
"Loading chart...": "Chargement du graphique...",
+ "Loading scheduled messages": "Chargement des messages programmés",
"Local upload attachment missing local id": "Pièce jointe locale sans identifiant local",
"Location": "Emplacement",
"Location sharing ended": "Partage d'emplacement terminé",
@@ -529,8 +540,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",
@@ -580,6 +593,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",
@@ -615,7 +629,10 @@
"Saved for later": "Enregistré pour plus tard",
"Schedule": "Programmer",
"Schedule message": "Programmer le message",
+ "Scheduled": "Programmé",
+ "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",
@@ -635,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",
@@ -657,6 +675,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 +685,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é",
@@ -702,6 +725,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: ddd D MMM YYYY) }}",
"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",
@@ -712,6 +737,7 @@
"Translated": "Traduit",
"Translated from {{ language }}": "Traduit du {{ language }}",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "Réessayer",
"Type a number from 2 to 10": "Tapez un nombre de 2 à 10",
"Unarchive": "Désarchiver",
"unban-command-args": "[@nomdutilisateur]",
@@ -771,6 +797,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 73e2a67f80..eb1d37107a 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": "पिन किए गए संदेश देखें",
@@ -264,6 +269,7 @@
"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": "इमोजी पिकर बंद करें",
@@ -279,6 +285,7 @@
"Contact name": "संपर्क का नाम",
"Cooking up something good": "कुछ बढ़िया पका रहा हूँ",
"Copy Message": "संदेश कॉपी करें",
+ "Couldn't load scheduled messages": "शेड्यूल किए गए संदेश लोड नहीं हो सके",
"Couldn't schedule message. Try again": "संदेश शेड्यूल नहीं हो सका। फिर से कोशिश करें",
"Create": "बनाएँ",
"Create a question, add options, and configure poll settings": "एक प्रश्न बनाएं, विकल्प जोड़ें और पोल सेटिंग्स कॉन्फ़िगर करें",
@@ -350,6 +357,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}} के कारण नहीं बन सका",
@@ -461,6 +469,7 @@
"Live until {{ timestamp }}": "{{ timestamp }} तक लाइव",
"Load more": "और लोड करें",
"Loading chart...": "चार्ट लोड हो रहा है...",
+ "Loading scheduled messages": "शेड्यूल किए गए संदेश लोड हो रहे हैं",
"Local upload attachment missing local id": "लोकल अपलोड अटैचमेंट में लोकल आईडी नहीं है",
"Location": "स्थान",
"Location sharing ended": "स्थान साझा करना समाप्त",
@@ -510,8 +519,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": "ऑफलाइन",
@@ -561,6 +572,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": "अनुस्मारक सेट किया गया",
@@ -593,7 +605,10 @@
"Saved for later": "बाद के लिए सहेजा गया",
"Schedule": "शेड्यूल करें",
"Schedule message": "संदेश शेड्यूल करें",
+ "Scheduled": "शेड्यूल किया गया",
+ "Scheduled messages": "शेड्यूल किए गए संदेश",
"Scheduled messages are turned off": "शेड्यूल किए गए संदेश बंद हैं",
+ "Scheduled replies": "शेड्यूल किए गए जवाब",
"Search": "खोज",
"Search GIFs": "GIF खोजें",
"search-results-header-filter-source-button-label--channels": "चैनल्स",
@@ -611,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": "वैसे भी भेजें",
@@ -633,6 +649,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 +659,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": "यह संदेश शेड्यूल नहीं किया जा सकता",
@@ -677,6 +698,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: ddd, D MMM YYYY) }}",
"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": "रिकॉर्डिंग शुरू करने के लिए, अपने ब्राउज़र में कैमरा तक पहुँच दें",
@@ -686,6 +709,7 @@
"Translated": "अनुवादित",
"Translated from {{ language }}": "{{ language }} से अनुवादित",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "फिर से कोशिश करें",
"Type a number from 2 to 10": "2 से 10 तक का एक नंबर टाइप करें",
"Unarchive": "अनआर्काइव",
"unban-command-args": "[@उपयोगकर्तनाम]",
@@ -742,6 +766,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 07655992b6..406588779e 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",
@@ -281,6 +288,7 @@
"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",
@@ -296,6 +304,7 @@
"Contact name": "Nome del contatto",
"Cooking up something good": "Sto cucinando qualcosa di buono",
"Copy Message": "Copia messaggio",
+ "Couldn't load scheduled messages": "Impossibile caricare i messaggi programmati",
"Couldn't schedule message. Try again": "Impossibile programmare il messaggio. Riprova",
"Create": "Crea",
"Create a question, add options, and configure poll settings": "Crea una domanda, aggiungi opzioni e configura le impostazioni del sondaggio",
@@ -366,6 +375,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}}",
@@ -480,6 +490,7 @@
"Live until {{ timestamp }}": "Live fino a {{ timestamp }}",
"Load more": "Carica di più",
"Loading chart...": "Caricamento del grafico...",
+ "Loading scheduled messages": "Caricamento dei messaggi programmati",
"Local upload attachment missing local id": "Allegato di caricamento locale senza id locale",
"Location": "Posizione",
"Location sharing ended": "Condivisione posizione terminata",
@@ -529,8 +540,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",
@@ -580,6 +593,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",
@@ -615,7 +629,10 @@
"Saved for later": "Salvato per dopo",
"Schedule": "Programma",
"Schedule message": "Programma messaggio",
+ "Scheduled": "Programmato",
+ "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",
@@ -635,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",
@@ -657,6 +675,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 +685,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",
@@ -702,6 +725,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: ddd D MMM YYYY) }}",
"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",
@@ -712,6 +737,7 @@
"Translated": "Tradotto",
"Translated from {{ language }}": "Tradotto da {{ language }}",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "Riprova",
"Type a number from 2 to 10": "Digita un numero da 2 a 10",
"Unarchive": "Ripristina",
"unban-command-args": "[@nomeutente]",
@@ -771,6 +797,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 d4183c0ab8..b79f6be3f6 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": "ピン留めメッセージを表示",
@@ -255,6 +258,7 @@
"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": "絵文字ピッカーを閉める",
@@ -270,6 +274,7 @@
"Contact name": "連絡先名",
"Cooking up something good": "いいものを作っています",
"Copy Message": "メッセージをコピー",
+ "Couldn't load scheduled messages": "予約メッセージを読み込めませんでした",
"Couldn't schedule message. Try again": "メッセージを予約できませんでした。もう一度お試しください",
"Create": "作成",
"Create a question, add options, and configure poll settings": "質問を作成し、選択肢を追加して投票設定を構成",
@@ -340,6 +345,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}} のため投票の作成に失敗しました",
@@ -448,6 +454,7 @@
"Live until {{ timestamp }}": "{{ timestamp }}までライブ",
"Load more": "もっと読み込む",
"Loading chart...": "チャートを読み込み中...",
+ "Loading scheduled messages": "予約メッセージを読み込んでいます",
"Local upload attachment missing local id": "ローカルアップロード添付にローカルIDがありません",
"Location": "位置情報",
"Location sharing ended": "位置情報の共有が終了しました",
@@ -497,8 +504,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": "オフライン",
@@ -548,6 +557,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": "リマインダーを設定しました",
@@ -578,7 +588,10 @@
"Saved for later": "後で保存済み",
"Schedule": "予約",
"Schedule message": "メッセージを予約",
+ "Scheduled": "予約済み",
+ "Scheduled messages": "予約メッセージ",
"Scheduled messages are turned off": "予約メッセージはオフになっています",
+ "Scheduled replies": "予約返信",
"Search": "探す",
"Search GIFs": "GIFを検索",
"search-results-header-filter-source-button-label--channels": "チャンネル",
@@ -596,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": "とにかく送信する",
@@ -618,6 +632,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 +642,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": "このメッセージは予約できません",
@@ -661,6 +680,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: YYYY[年]M[月]D[日] ddd) }}",
"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": "録音を開始するには、ブラウザーでカメラへのアクセスを許可してください",
@@ -669,6 +690,7 @@
"Translated": "翻訳済み",
"Translated from {{ language }}": "{{ language }}から翻訳",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "再試行",
"Type a number from 2 to 10": "2から10までの数字を入力してください",
"Unarchive": "アーカイブ解除",
"unban-command-args": "[@ユーザ名]",
@@ -723,6 +745,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 ec49dd78df..fab159b0e0 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": "고정된 메시지 보기",
@@ -255,6 +258,7 @@
"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": "이모티콘 선택기 닫기",
@@ -270,6 +274,7 @@
"Contact name": "연락처 이름",
"Cooking up something good": "맛있는 답변을 요리하는 중",
"Copy Message": "메시지 복사",
+ "Couldn't load scheduled messages": "예약된 메시지를 불러올 수 없습니다",
"Couldn't schedule message. Try again": "메시지를 예약하지 못했습니다. 다시 시도하세요",
"Create": "생성",
"Create a question, add options, and configure poll settings": "질문을 만들고 옵션을 추가한 뒤 투표 설정 구성",
@@ -340,6 +345,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}} 때문에 투표를 생성하지 못했습니다",
@@ -448,6 +454,7 @@
"Live until {{ timestamp }}": "{{ timestamp }}까지 라이브",
"Load more": "더 불러오기",
"Loading chart...": "차트 불러오는 중...",
+ "Loading scheduled messages": "예약된 메시지를 불러오는 중",
"Local upload attachment missing local id": "로컬 업로드 첨부에 로컬 ID가 없습니다",
"Location": "위치",
"Location sharing ended": "위치 공유가 종료되었습니다",
@@ -497,8 +504,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": "오프라인",
@@ -548,6 +557,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": "알림 설정됨",
@@ -578,7 +588,10 @@
"Saved for later": "나중에 저장됨",
"Schedule": "예약",
"Schedule message": "메시지 예약",
+ "Scheduled": "예약됨",
+ "Scheduled messages": "예약된 메시지",
"Scheduled messages are turned off": "예약 메시지가 꺼져 있습니다",
+ "Scheduled replies": "예약된 답장",
"Search": "찾다",
"Search GIFs": "GIF 검색",
"search-results-header-filter-source-button-label--channels": "채널",
@@ -596,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": "어쨌든 보내기",
@@ -618,6 +632,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 +642,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": "이 메시지는 예약할 수 없습니다",
@@ -661,6 +680,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: YYYY[년] M[월] D[일] ddd) }}",
"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": "브라우저에서 카메라 액세스를 허용하여 녹음을 시작합니다",
@@ -669,6 +690,7 @@
"Translated": "번역됨",
"Translated from {{ language }}": "{{ language }}(으)로 번역됨",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "다시 시도",
"Type a number from 2 to 10": "2에서 10 사이의 숫자를 입력하세요",
"Unarchive": "아카이브 해제",
"unban-command-args": "[@사용자이름]",
@@ -723,6 +745,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 479c427102..0116f5f6a0 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",
@@ -264,6 +269,7 @@
"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",
@@ -279,6 +285,7 @@
"Contact name": "Contactnaam",
"Cooking up something good": "Ik kook iets lekkers",
"Copy Message": "Bericht kopiëren",
+ "Couldn't load scheduled messages": "Geplande berichten konden niet worden geladen",
"Couldn't schedule message. Try again": "Bericht kon niet worden ingepland. Probeer het opnieuw",
"Create": "Maak",
"Create a question, add options, and configure poll settings": "Maak een vraag, voeg opties toe en stel de pollinstellingen in",
@@ -349,6 +356,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}}",
@@ -460,6 +468,7 @@
"Live until {{ timestamp }}": "Live tot {{ timestamp }}",
"Load more": "Meer laden",
"Loading chart...": "Grafiek laden...",
+ "Loading scheduled messages": "Geplande berichten laden",
"Local upload attachment missing local id": "Lokale uploadbijlage mist lokale id",
"Location": "Locatie",
"Location sharing ended": "Locatie delen beëindigd",
@@ -509,8 +518,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",
@@ -560,6 +571,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",
@@ -592,7 +604,10 @@
"Saved for later": "Bewaard voor later",
"Schedule": "Inplannen",
"Schedule message": "Bericht inplannen",
+ "Scheduled": "Ingepland",
+ "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",
@@ -610,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",
@@ -634,6 +650,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 +660,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",
@@ -678,6 +699,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: ddd D MMM YYYY) }}",
"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",
@@ -687,6 +710,7 @@
"Translated": "Vertaald",
"Translated from {{ language }}": "Vertaald uit {{ language }}",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "Opnieuw proberen",
"Type a number from 2 to 10": "Typ een getal van 2 tot 10",
"Unarchive": "Uit archief halen",
"unban-command-args": "[@gebruikersnaam]",
@@ -743,6 +767,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 4ac67eb05d..64646bd25d 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",
@@ -281,6 +288,7 @@
"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",
@@ -296,6 +304,7 @@
"Contact name": "Nome do contato",
"Cooking up something good": "Cozinhando algo bom",
"Copy Message": "Copiar mensagem",
+ "Couldn't load scheduled messages": "Não foi possível carregar as mensagens agendadas",
"Couldn't schedule message. Try again": "Não foi possível agendar a mensagem. Tente novamente",
"Create": "Criar",
"Create a question, add options, and configure poll settings": "Crie uma pergunta, adicione opções e configure as definições da enquete",
@@ -366,6 +375,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}}",
@@ -480,6 +490,7 @@
"Live until {{ timestamp }}": "Ao vivo até {{ timestamp }}",
"Load more": "Carregar mais",
"Loading chart...": "Carregando gráfico...",
+ "Loading scheduled messages": "Carregando mensagens agendadas",
"Local upload attachment missing local id": "Anexo de envio local sem id local",
"Location": "Localização",
"Location sharing ended": "Compartilhamento de localização encerrado",
@@ -529,8 +540,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",
@@ -580,6 +593,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",
@@ -615,7 +629,10 @@
"Saved for later": "Salvo para depois",
"Schedule": "Agendar",
"Schedule message": "Agendar mensagem",
+ "Scheduled": "Agendada",
+ "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",
@@ -635,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",
@@ -657,6 +675,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 +685,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",
@@ -702,6 +725,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: ddd, D [de] MMM [de] YYYY) }}",
"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",
@@ -712,6 +737,7 @@
"Translated": "Traduzido",
"Translated from {{ language }}": "Traduzido de {{ language }}",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "Tentar novamente",
"Type a number from 2 to 10": "Digite um número de 2 a 10",
"Unarchive": "Desarquivar",
"unban-command-args": "[@nomedeusuário]",
@@ -771,6 +797,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 071fe612a1..cf56753739 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": "Просмотреть закрепленные сообщения",
@@ -299,6 +308,7 @@
"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": "Закрыть окно выбора смайлов",
@@ -314,6 +324,7 @@
"Contact name": "Имя контакта",
"Cooking up something good": "Готовлю что-нибудь вкусное",
"Copy Message": "Копировать сообщение",
+ "Couldn't load scheduled messages": "Не удалось загрузить запланированные сообщения",
"Couldn't schedule message. Try again": "Не удалось запланировать сообщение. Попробуйте ещё раз",
"Create": "Создать",
"Create a question, add options, and configure poll settings": "Создайте вопрос, добавьте варианты и настройте параметры опроса",
@@ -384,6 +395,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}}",
@@ -504,6 +516,7 @@
"Live until {{ timestamp }}": "В прямом эфире до {{ timestamp }}",
"Load more": "Загрузить больше",
"Loading chart...": "Загрузка графика...",
+ "Loading scheduled messages": "Загрузка запланированных сообщений",
"Local upload attachment missing local id": "У локального вложения нет локального id",
"Location": "Местоположение",
"Location sharing ended": "Обмен местоположением завершен",
@@ -553,8 +566,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": "Не в сети",
@@ -604,6 +619,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": "Напоминание установлено",
@@ -642,7 +658,10 @@
"Saved for later": "Сохранено на потом",
"Schedule": "Запланировать",
"Schedule message": "Запланировать сообщение",
+ "Scheduled": "Запланировано",
+ "Scheduled messages": "Запланированные сообщения",
"Scheduled messages are turned off": "Запланированные сообщения отключены",
+ "Scheduled replies": "Запланированные ответы",
"Search": "Поиск",
"Search GIFs": "Поиск GIF",
"search-results-header-filter-source-button-label--channels": "каналы",
@@ -664,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": "Мне всё равно, отправить",
@@ -686,6 +706,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 +716,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": "Это сообщение нельзя запланировать",
@@ -732,6 +757,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: ddd, D MMM YYYY) }}",
"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": "Для начала записи разрешите доступ к камере в вашем браузере",
@@ -743,6 +770,7 @@
"Translated": "Переведено",
"Translated from {{ language }}": "Переведено с {{ language }}",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "Повторить",
"Type a number from 2 to 10": "Введите число от 2 до 10",
"Unarchive": "Удалить из архива",
"unban-command-args": "[@имяпользователя]",
@@ -805,6 +833,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 6806c353af..0011c0bf0e 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",
@@ -264,6 +269,7 @@
"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",
@@ -279,6 +285,7 @@
"Contact name": "Kişi adı",
"Cooking up something good": "Güzel bir şeyler pişiriyorum",
"Copy Message": "Mesajı kopyala",
+ "Couldn't load scheduled messages": "Planlanmış mesajlar yüklenemedi",
"Couldn't schedule message. Try again": "Mesaj zamanlanamadı. Tekrar deneyin",
"Create": "Oluştur",
"Create a question, add options, and configure poll settings": "Bir soru oluşturun, seçenekler ekleyin ve anket ayarlarını yapılandırın",
@@ -349,6 +356,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ı",
@@ -460,6 +468,7 @@
"Live until {{ timestamp }}": "{{ timestamp }}'e kadar canlı",
"Load more": "Daha fazla yükle",
"Loading chart...": "Grafik yükleniyor...",
+ "Loading scheduled messages": "Planlanmış mesajlar yükleniyor",
"Local upload attachment missing local id": "Yerel yükleme ekinde yerel kimlik eksik",
"Location": "Konum",
"Location sharing ended": "Konum paylaşımı sona erdi",
@@ -509,8 +518,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ışı",
@@ -560,6 +571,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ı",
@@ -592,7 +604,10 @@
"Saved for later": "Daha sonra kaydedildi",
"Schedule": "Zamanla",
"Schedule message": "Mesajı zamanla",
+ "Scheduled": "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",
@@ -610,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",
@@ -632,6 +648,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 +658,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",
@@ -676,6 +697,8 @@
"timestamp/ScheduleDate": "{{ timestamp | timestampFormatter(format: D MMM YYYY ddd) }}",
"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",
@@ -685,6 +708,7 @@
"Translated": "Çevrildi",
"Translated from {{ language }}": "{{ language }} dilinden çevrildi",
"translationBuilderTopic/notification": "{{value, notification}}",
+ "Try again": "Tekrar dene",
"Type a number from 2 to 10": "2 ile 10 arasında bir sayı yazın",
"Unarchive": "Arşivden çıkar",
"unban-command-args": "[@kullanıcıadı]",
@@ -741,6 +765,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",
diff --git a/src/plugins/ChannelDetail/ChannelDetail.tsx b/src/plugins/ChannelDetail/ChannelDetail.tsx
index 860319a1cd..9a6f83df62 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();
+
+ const scheduledMessageCount = useScheduledMessageCount({ cid: channel.cid });
+
+ const [searchQuery, setSearchQuery] = useState('');
+
+ return (
+
+
+
+ {scheduledMessageCount > 0 && (
+
+ )}
+
+
+
+ );
+};
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 0000000000..e9807b7172
--- /dev/null
+++ b/src/plugins/ChannelDetail/Views/ScheduledMessagesView/__tests__/ScheduledMessagesView.test.tsx
@@ -0,0 +1,115 @@
+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();
+ expect(screen.queryByRole('searchbox', { name: 'Search' })).not.toBeInTheDocument();
+ });
+
+ it('filters the list by the search field', async () => {
+ await renderView([
+ generateScheduledMessage({ cid, message: { text: 'Launch moved to Monday' } }),
+ generateScheduledMessage({ cid, message: { text: 'Lunch at noon' } }),
+ ]);
+
+ fireEvent.change(screen.getByRole('searchbox', { name: 'Search' }), {
+ target: { value: 'lunch' },
+ });
+
+ expect(screen.getByText('Lunch at noon')).toBeInTheDocument();
+ expect(screen.queryByText('Launch moved to Monday')).not.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 0000000000..81ab402742
--- /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 8d1a469d0f..078f7cc8c2 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 0000000000..c650ed919b
--- /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 db73bf5d10..490bc536e6 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;