Scheduled Messages: Support repeated mode (#6503)
This commit is contained in:
parent
d81fe3ec96
commit
9d03d14af7
@ -47,7 +47,7 @@ import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnl
|
||||
import { addTimestampEntities } from '../../../util/dates/timestamp';
|
||||
import { omitUndefined, pick } from '../../../util/iteratees';
|
||||
import { toJSNumber } from '../../../util/numbers';
|
||||
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { interpolateArray } from '../../../util/waveform';
|
||||
import {
|
||||
buildApiCurrencyAmount,
|
||||
@ -254,6 +254,7 @@ export function buildApiMessageWithChatId(
|
||||
viewsCount: mtpMessage.views,
|
||||
forwardsCount: mtpMessage.forwards,
|
||||
isScheduled,
|
||||
scheduleRepeatPeriod: mtpMessage.scheduleRepeatPeriod,
|
||||
isFromScheduled: mtpMessage.fromScheduled,
|
||||
isSilent: mtpMessage.silent,
|
||||
isPinned: mtpMessage.pinned,
|
||||
@ -442,6 +443,7 @@ export function buildLocalMessage(
|
||||
contact?: ApiContact,
|
||||
groupedId?: string,
|
||||
scheduledAt?: number,
|
||||
scheduleRepeatPeriod?: number,
|
||||
sendAs?: ApiPeer,
|
||||
story?: ApiStory | ApiStorySkipped,
|
||||
isInvertedMedia?: true,
|
||||
@ -475,7 +477,7 @@ export function buildLocalMessage(
|
||||
pollId: localPoll?.id,
|
||||
todo: localTodo,
|
||||
}),
|
||||
date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(),
|
||||
date: scheduledAt || getServerTime(),
|
||||
isOutgoing: !isChannel,
|
||||
senderId: chat.type !== 'chatTypePrivate' ? (sendAs?.id || currentUserId) : undefined,
|
||||
replyInfo: resultReplyInfo,
|
||||
@ -485,6 +487,7 @@ export function buildLocalMessage(
|
||||
...(media && (media.photo || media.video) && { isInAlbum: true }),
|
||||
}),
|
||||
...(scheduledAt && { isScheduled: true }),
|
||||
scheduleRepeatPeriod,
|
||||
isForwardingAllowed: true,
|
||||
isInvertedMedia,
|
||||
effectId,
|
||||
@ -506,6 +509,7 @@ export function buildLocalForwardedMessage({
|
||||
toThreadId,
|
||||
message,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
noAuthors,
|
||||
noCaptions,
|
||||
isCurrentUserPremium,
|
||||
@ -516,6 +520,7 @@ export function buildLocalForwardedMessage({
|
||||
toThreadId?: number;
|
||||
message: ApiMessage;
|
||||
scheduledAt?: number;
|
||||
scheduleRepeatPeriod?: number;
|
||||
noAuthors?: boolean;
|
||||
noCaptions?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
@ -565,7 +570,8 @@ export function buildLocalForwardedMessage({
|
||||
id: localId,
|
||||
chatId: toChat.id,
|
||||
content: updatedContent,
|
||||
date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(),
|
||||
date: scheduledAt || getServerTime(),
|
||||
scheduleRepeatPeriod,
|
||||
isOutgoing: !asIncomingInChatWithSelf && toChat.type !== 'chatTypeChannel',
|
||||
senderId: toChat.type !== 'chatTypePrivate' ? (sendAs?.id || currentUserId) : undefined,
|
||||
sendingState: 'messageSendingStatePending',
|
||||
|
||||
@ -300,7 +300,8 @@ export function sendMessageLocal(
|
||||
) {
|
||||
const {
|
||||
chat, lastMessageId, text, entities, replyInfo, suggestedPostInfo, attachment, sticker, story, gif, poll, todo,
|
||||
contact, scheduledAt, groupedId, sendAs, wasDrafted, isInvertedMedia, effectId, isPending, messagePriceInStars,
|
||||
contact, scheduledAt, scheduleRepeatPeriod, groupedId, sendAs, wasDrafted, isInvertedMedia, effectId, isPending,
|
||||
messagePriceInStars,
|
||||
} = params;
|
||||
|
||||
if (!chat) return undefined;
|
||||
@ -323,6 +324,7 @@ export function sendMessageLocal(
|
||||
contact,
|
||||
groupedId,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
sendAs,
|
||||
story,
|
||||
isInvertedMedia,
|
||||
@ -352,7 +354,7 @@ export function sendApiMessage(
|
||||
chat, text, entities, replyInfo, suggestedPostInfo, suggestedMedia,
|
||||
attachment, sticker, story, gif, poll, todo, contact,
|
||||
|
||||
isSilent, scheduledAt, groupedId, noWebPage, sendAs, shouldUpdateStickerSetOrder,
|
||||
isSilent, scheduledAt, scheduleRepeatPeriod, groupedId, noWebPage, sendAs, shouldUpdateStickerSetOrder,
|
||||
isInvertedMedia, effectId, webPageMediaSize, webPageUrl, messagePriceInStars,
|
||||
} = params;
|
||||
|
||||
@ -384,6 +386,7 @@ export function sendApiMessage(
|
||||
groupedId,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
messagePriceInStars,
|
||||
}, randomId, localMessage, onProgress);
|
||||
}
|
||||
@ -484,6 +487,7 @@ export function sendApiMessage(
|
||||
replyTo: replyInfo && buildInputReplyTo(replyInfo),
|
||||
silent: isSilent || undefined,
|
||||
scheduleDate: scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
sendAs: sendAs && buildInputPeer(sendAs.id, sendAs.accessHash),
|
||||
updateStickersetsOrder: shouldUpdateStickerSetOrder || undefined,
|
||||
invertMedia: isInvertedMedia || undefined,
|
||||
@ -556,6 +560,7 @@ function sendGroupedMedia(
|
||||
groupedId,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
sendAs,
|
||||
messagePriceInStars,
|
||||
}: {
|
||||
@ -568,6 +573,7 @@ function sendGroupedMedia(
|
||||
groupedId: string;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
scheduleRepeatPeriod?: number;
|
||||
sendAs?: ApiPeer;
|
||||
messagePriceInStars?: number;
|
||||
},
|
||||
@ -645,6 +651,7 @@ function sendGroupedMedia(
|
||||
replyTo: replyInfo && buildInputReplyTo(replyInfo),
|
||||
...(isSilent && { silent: isSilent }),
|
||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||
...(scheduleRepeatPeriod && { scheduleRepeatPeriod }),
|
||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||
...(messagePriceInStars && { allowPaidStars: BigInt(messagePriceInStars * count) }),
|
||||
...(suggestedPostInfo && { suggestedPost: buildInputSuggestedPost(suggestedPostInfo) }),
|
||||
@ -756,6 +763,7 @@ export async function editMessage({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
id: message.id,
|
||||
...(isScheduled && { scheduleDate: message.date }),
|
||||
...(message.scheduleRepeatPeriod && { scheduleRepeatPeriod: message.scheduleRepeatPeriod }),
|
||||
...(noWebPage && { noWebpage: noWebPage }),
|
||||
...(isInvertedMedia && { invertMedia: isInvertedMedia }),
|
||||
}), { shouldThrow: true });
|
||||
@ -893,15 +901,18 @@ export async function rescheduleMessage({
|
||||
chat,
|
||||
message,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
message: ApiMessage;
|
||||
scheduledAt: number;
|
||||
scheduleRepeatPeriod?: number;
|
||||
}) {
|
||||
await invokeRequest(new GramJs.messages.EditMessage({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
id: message.id,
|
||||
scheduleDate: scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -1842,7 +1853,7 @@ export async function fetchExtendedMedia({
|
||||
export function forwardMessagesLocal(params: ForwardMessagesParams) {
|
||||
const {
|
||||
toChat, toThreadId, messages,
|
||||
scheduledAt, sendAs, noAuthors, noCaptions,
|
||||
scheduledAt, scheduleRepeatPeriod, sendAs, noAuthors, noCaptions,
|
||||
isCurrentUserPremium, wasDrafted, lastMessageId,
|
||||
} = params;
|
||||
|
||||
@ -1855,6 +1866,7 @@ export function forwardMessagesLocal(params: ForwardMessagesParams) {
|
||||
toThreadId: Number(toThreadId),
|
||||
message,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
noAuthors,
|
||||
noCaptions,
|
||||
isCurrentUserPremium,
|
||||
@ -1877,7 +1889,7 @@ export function forwardMessagesLocal(params: ForwardMessagesParams) {
|
||||
export async function forwardApiMessages(params: ForwardMessagesParams) {
|
||||
const {
|
||||
fromChat, toChat, toThreadId, isSilent,
|
||||
scheduledAt, sendAs, withMyScore, noAuthors, noCaptions,
|
||||
scheduledAt, scheduleRepeatPeriod, sendAs, withMyScore, noAuthors, noCaptions,
|
||||
forwardedLocalMessagesSlice, messagePriceInStars,
|
||||
} = params;
|
||||
|
||||
@ -1902,6 +1914,7 @@ export async function forwardApiMessages(params: ForwardMessagesParams) {
|
||||
dropMediaCaptions: noCaptions || undefined,
|
||||
...(toThreadId && { topMsgId: Number(toThreadId) }),
|
||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||
...(scheduleRepeatPeriod && { scheduleRepeatPeriod }),
|
||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||
...(priceInStars && { allowPaidStars: BigInt(priceInStars) }),
|
||||
}), {
|
||||
|
||||
@ -648,6 +648,7 @@ export interface ApiMessage {
|
||||
viaBusinessBotId?: string;
|
||||
postAuthorTitle?: string;
|
||||
isScheduled?: boolean;
|
||||
scheduleRepeatPeriod?: number;
|
||||
shouldHideKeyboardButtons?: boolean;
|
||||
isHideKeyboardSelective?: boolean;
|
||||
isFromScheduled?: boolean;
|
||||
|
||||
@ -1167,6 +1167,16 @@
|
||||
"SecretChat" = "Secret Chat";
|
||||
"ChannelPermissionDeniedSendMessagesUntil" = "The admins of this group have restricted you from messaging until {date}.";
|
||||
"FormatDateAtTime" = "{date} at {time}";
|
||||
"MessageRepeatPeriodEveryMinutes_one" = "every minute";
|
||||
"MessageRepeatPeriodEveryMinutes_other" = "every {count} minutes";
|
||||
"MessageRepeatPeriodDaily" = "daily";
|
||||
"MessageRepeatPeriodWeekly" = "weekly";
|
||||
"MessageRepeatPeriodBiweekly" = "biweekly";
|
||||
"MessageRepeatPeriodMonthly" = "monthly";
|
||||
"MessageRepeatPeriodEveryMonths_one" = "every month";
|
||||
"MessageRepeatPeriodEveryMonths_other" = "every {count} months";
|
||||
"MessageRepeatPeriodYearly" = "yearly";
|
||||
"MessageScheduledRepeatPremium" = "Subscribe to **Telegram Premium** to schedule repeating messages.";
|
||||
"StickerPackRemoveStickerCount_one" = "Remove {count} sticker";
|
||||
"StickerPackRemoveStickerCount_other" = "Remove {count} stickers";
|
||||
"StickerPackAddStickerCount_one" = "Add {count} sticker";
|
||||
@ -1225,6 +1235,17 @@
|
||||
"Archive" = "Archive";
|
||||
"WaitingForNetwork" = "Waiting for network...";
|
||||
"ScheduleSendWhenOnline" = "Send When Online";
|
||||
"ScheduleRepeat" = "Repeat: {value}";
|
||||
"ScheduleRepeatNever" = "Never";
|
||||
"ScheduleRepeatEveryMinutes_one" = "Every Minute";
|
||||
"ScheduleRepeatEveryMinutes_other" = "Every {count} Minutes";
|
||||
"ScheduleRepeatDaily" = "Daily";
|
||||
"ScheduleRepeatWeekly" = "Weekly";
|
||||
"ScheduleRepeatBiweekly" = "Biweekly";
|
||||
"ScheduleRepeatMonthly" = "Monthly";
|
||||
"ScheduleRepeatEveryMonths_one" = "Every {count} Month";
|
||||
"ScheduleRepeatEveryMonths_other" = "Every {count} Months";
|
||||
"ScheduleRepeatYearly" = "Yearly";
|
||||
"VoipIncoming" = "Incoming call";
|
||||
"Years_one" = "{count} year";
|
||||
"Years_other" = "{count} years";
|
||||
|
||||
@ -6,6 +6,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.repeat-mode {
|
||||
position: relative;
|
||||
|
||||
.repeat-mode-button {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.drop-down-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.expanded-icon {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.repeated-mode-transition-slide {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.timepicker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -1,20 +1,29 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type React from '../../lib/teact/teact';
|
||||
import {
|
||||
memo, useCallback, useEffect, useMemo, useState,
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import type { RepeatedMessageMode } from '../../util/scheduledMessages';
|
||||
|
||||
import { MAX_INT_32 } from '../../config';
|
||||
import { selectIsCurrentUserPremium } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatDateToString, formatTime, getDayStart } from '../../util/dates/dateFormat';
|
||||
import { ALL_REPEAT_MODES, getScheduleRepeatModeText, TEST_SERVER_ONLY_MODES } from '../../util/scheduledMessages';
|
||||
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import Menu from '../ui/Menu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
import './CalendarModal.scss';
|
||||
@ -30,15 +39,22 @@ export type OwnProps = {
|
||||
isPastMode?: boolean;
|
||||
isOpen: boolean;
|
||||
withTimePicker?: boolean;
|
||||
withRepeatMode?: boolean;
|
||||
initialRepeatMode?: RepeatedMessageMode;
|
||||
submitButtonLabel?: string;
|
||||
secondButtonLabel?: string;
|
||||
description?: string;
|
||||
onClose: () => void;
|
||||
onSubmit: (date: Date) => void;
|
||||
onSubmit: (date: Date, repeatMode?: RepeatedMessageMode) => void;
|
||||
onDateChange?: (date: Date) => void;
|
||||
onSecondButtonClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
isTestServer?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const WEEKDAY_LETTERS = [
|
||||
'lng_weekday1',
|
||||
'lng_weekday2',
|
||||
@ -49,7 +65,7 @@ const WEEKDAY_LETTERS = [
|
||||
'lng_weekday7',
|
||||
];
|
||||
|
||||
const CalendarModal: FC<OwnProps> = ({
|
||||
const CalendarModal: FC<OwnProps & StateProps> = ({
|
||||
selectedAt,
|
||||
minAt,
|
||||
maxAt,
|
||||
@ -57,17 +73,39 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
isPastMode,
|
||||
isOpen,
|
||||
withTimePicker,
|
||||
withRepeatMode,
|
||||
initialRepeatMode,
|
||||
submitButtonLabel,
|
||||
secondButtonLabel,
|
||||
description,
|
||||
isTestServer,
|
||||
isCurrentUserPremium,
|
||||
onClose,
|
||||
onSubmit,
|
||||
onDateChange,
|
||||
onSecondButtonClick,
|
||||
}) => {
|
||||
const lang = useOldLang();
|
||||
const { showNotification } = getActions();
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>();
|
||||
const dialogRef = useRef<HTMLDivElement>();
|
||||
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
const now = new Date();
|
||||
|
||||
const {
|
||||
isContextMenuOpen: isRepeatMenuOpen,
|
||||
contextMenuAnchor: repeatMenuAnchor,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(menuRef);
|
||||
|
||||
const getRootElement = useLastCallback(() => dialogRef.current);
|
||||
const getMenuElement = useLastCallback(() => menuRef.current!.querySelector('.bubble'));
|
||||
const getTriggerElement = useLastCallback(() => dialogRef.current!.querySelector('.repeat-mode-button'));
|
||||
|
||||
const minDate = useMemo(() => {
|
||||
if (isFutureMode && !minAt) return new Date();
|
||||
return new Date(Math.max(minAt || MIN_SAFE_DATE, MIN_SAFE_DATE));
|
||||
@ -91,6 +129,9 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
const [selectedMinutes, setSelectedMinutes] = useState<string>(() => (
|
||||
formatInputTime(passedSelectedDate.getMinutes())
|
||||
));
|
||||
const [repeatedMessageMode, setRepeatedMessageMode] = useState<RepeatedMessageMode>(
|
||||
initialRepeatMode || 'never',
|
||||
);
|
||||
|
||||
const selectedDay = formatDay(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate());
|
||||
const currentYear = currentMonthAndYear.getFullYear();
|
||||
@ -99,6 +140,16 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
const isDisabled = (isFutureMode && selectedDate.getTime() < minDate.getTime())
|
||||
|| (isPastMode && selectedDate.getTime() > maxDate.getTime());
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setRepeatedMessageMode('never');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
setRepeatedMessageMode(initialRepeatMode || 'never');
|
||||
}, [initialRepeatMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!prevIsOpen && isOpen) {
|
||||
setSelectedDate(passedSelectedDate);
|
||||
@ -144,8 +195,23 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
), [currentMonth, currentYear]);
|
||||
|
||||
const submitLabel = useMemo(() => {
|
||||
return submitButtonLabel || formatSubmitLabel(lang, selectedDate);
|
||||
}, [lang, selectedDate, submitButtonLabel]);
|
||||
return submitButtonLabel || formatSubmitLabel(oldLang, selectedDate);
|
||||
}, [oldLang, selectedDate, submitButtonLabel]);
|
||||
|
||||
const handleRepeatModeClick = useLastCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!isCurrentUserPremium) {
|
||||
showNotification({
|
||||
message: lang('MessageScheduledRepeatPremium'),
|
||||
action: {
|
||||
action: 'openPremiumModal',
|
||||
payload: { },
|
||||
},
|
||||
actionText: lang('PremiumMore'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
handleContextMenu(e);
|
||||
});
|
||||
|
||||
function handlePrevMonth() {
|
||||
setCurrentMonthAndYear((d) => {
|
||||
@ -178,14 +244,15 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const repeatMode = withRepeatMode && repeatedMessageMode !== 'never' ? repeatedMessageMode : undefined;
|
||||
if (isFutureMode && selectedDate < minDate) {
|
||||
onSubmit(minDate);
|
||||
onSubmit(minDate, repeatMode);
|
||||
} else if (isPastMode && selectedDate > maxDate) {
|
||||
onSubmit(maxDate);
|
||||
onSubmit(maxDate, repeatMode);
|
||||
} else {
|
||||
onSubmit(selectedDate);
|
||||
onSubmit(selectedDate, repeatMode);
|
||||
}
|
||||
}, [isFutureMode, isPastMode, minDate, maxDate, onSubmit, selectedDate]);
|
||||
}, [isFutureMode, isPastMode, minDate, maxDate, onSubmit, selectedDate, withRepeatMode, repeatedMessageMode]);
|
||||
|
||||
const handleChangeHours = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/[^\d]+/g, '');
|
||||
@ -227,6 +294,18 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
e.target.value = minutesStr;
|
||||
}, [selectedDate, onDateChange]);
|
||||
|
||||
const renderRepeatMenuItem = useCallback((mode: RepeatedMessageMode) => {
|
||||
return (
|
||||
<MenuItem key={mode} onClick={() => setRepeatedMessageMode(mode)}>
|
||||
{getScheduleRepeatModeText(mode, lang)}
|
||||
</MenuItem>
|
||||
);
|
||||
}, [lang]);
|
||||
|
||||
const availableRepeatModes = useMemo(() => {
|
||||
return ALL_REPEAT_MODES.filter((mode) => isTestServer || !TEST_SERVER_ONLY_MODES.has(mode));
|
||||
}, [isTestServer]);
|
||||
|
||||
function renderTimePicker() {
|
||||
return (
|
||||
<div className="timepicker">
|
||||
@ -251,12 +330,46 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderRepeatMode() {
|
||||
const dropDownIconClass = buildClassName('drop-down-icon', !isRepeatMenuOpen && 'expanded-icon');
|
||||
|
||||
return (
|
||||
<div className="repeat-mode" ref={menuRef}>
|
||||
<Button
|
||||
className="repeat-mode-button"
|
||||
onClick={handleRepeatModeClick}
|
||||
noForcedUpperCase
|
||||
isText
|
||||
iconName={isCurrentUserPremium ? 'down' : 'lock-badge'}
|
||||
iconClassName={isCurrentUserPremium ? dropDownIconClass : undefined}
|
||||
iconAlignment="end"
|
||||
>
|
||||
{lang('ScheduleRepeat', { value: getScheduleRepeatModeText(repeatedMessageMode, lang) })}
|
||||
</Button>
|
||||
<Menu
|
||||
isOpen={isRepeatMenuOpen}
|
||||
className="with-menu-transitions"
|
||||
anchor={repeatMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
autoClose
|
||||
>
|
||||
{availableRepeatModes.map(renderRepeatMenuItem)}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className="CalendarModal"
|
||||
onEnter={handleSubmit}
|
||||
dialogRef={dialogRef}
|
||||
>
|
||||
<div className="container">
|
||||
<div className="month-selector">
|
||||
@ -269,7 +382,7 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
/>
|
||||
|
||||
<h4>
|
||||
{lang(`lng_month${currentMonth + 1}`)}
|
||||
{oldLang(`lng_month${currentMonth + 1}`)}
|
||||
{' '}
|
||||
{currentYear}
|
||||
</h4>
|
||||
@ -298,7 +411,7 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
<div className="calendar-grid">
|
||||
{WEEKDAY_LETTERS.map((day) => (
|
||||
<div className="day-button faded weekday">
|
||||
<span>{lang(day)}</span>
|
||||
<span>{oldLang(day)}</span>
|
||||
</div>
|
||||
))}
|
||||
{prevMonthGrid.map((gridDate) => (
|
||||
@ -332,6 +445,7 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
</div>
|
||||
|
||||
{withTimePicker && renderTimePicker()}
|
||||
{withRepeatMode && isOpen && renderRepeatMode()}
|
||||
|
||||
<div className="footer">
|
||||
{description && (
|
||||
@ -422,4 +536,13 @@ function formatSubmitLabel(lang: OldLangFn, date: Date) {
|
||||
return lang('Conversation.ScheduleMessage.SendOn', [day, formatTime(lang, date)]);
|
||||
}
|
||||
|
||||
export default memo(CalendarModal);
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): Complete<StateProps> => {
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
return {
|
||||
isTestServer: global.config?.isTestServer,
|
||||
isCurrentUserPremium,
|
||||
};
|
||||
},
|
||||
)(CalendarModal));
|
||||
|
||||
@ -1079,6 +1079,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
sendGrouped = attachmentSettings.shouldSendGrouped,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
isInvertedMedia,
|
||||
}: {
|
||||
attachments: ApiAttachment[];
|
||||
@ -1086,6 +1087,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
sendGrouped?: boolean;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
scheduleRepeatPeriod?: number;
|
||||
isInvertedMedia?: true;
|
||||
}) => {
|
||||
if (!currentMessageList && !storyId) {
|
||||
@ -1110,6 +1112,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
text,
|
||||
entities,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetOrder,
|
||||
attachments: prepareAttachmentsToSend(attachmentsToSend, sendCompressed),
|
||||
@ -1159,6 +1162,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
isSilent?: boolean,
|
||||
scheduledAt?: number,
|
||||
isInvertedMedia?: true,
|
||||
scheduleRepeatPeriod?: number,
|
||||
) => {
|
||||
if (canSendAttachments(attachments)) {
|
||||
sendAttachments({
|
||||
@ -1167,13 +1171,19 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
sendGrouped,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
isInvertedMedia,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleSendCore = useLastCallback(
|
||||
(currentAttachments: ApiAttachment[], isSilent = false, scheduledAt?: number) => {
|
||||
(
|
||||
currentAttachments: ApiAttachment[],
|
||||
isSilent = false,
|
||||
scheduledAt?: number,
|
||||
scheduleRepeatPeriod?: number,
|
||||
) => {
|
||||
const { text, entities } = parseHtmlAsFormattedText(getHtml());
|
||||
|
||||
if (currentAttachments.length) {
|
||||
@ -1181,6 +1191,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
sendAttachments({
|
||||
attachments: currentAttachments,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
isSilent,
|
||||
});
|
||||
}
|
||||
@ -1209,6 +1220,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
text,
|
||||
entities,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetOrder,
|
||||
isInvertedMedia,
|
||||
@ -1235,7 +1247,11 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
},
|
||||
);
|
||||
|
||||
const handleSend = useLastCallback(async (isSilent = false, scheduledAt?: number) => {
|
||||
const handleSend = useLastCallback(async (
|
||||
isSilent = false,
|
||||
scheduledAt?: number,
|
||||
scheduleRepeatPeriod?: number,
|
||||
) => {
|
||||
if (!currentMessageList && !storyId) {
|
||||
return;
|
||||
}
|
||||
@ -1257,11 +1273,15 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
handleSendCore(currentAttachments, isSilent, scheduledAt);
|
||||
handleSendCore(currentAttachments, isSilent, scheduledAt, scheduleRepeatPeriod);
|
||||
});
|
||||
|
||||
const handleSendWithConfirmation = useLastCallback((isSilent = false, scheduledAt?: number) => {
|
||||
handleActionWithPaymentConfirmation(handleSend, isSilent, scheduledAt);
|
||||
const handleSendWithConfirmation = useLastCallback((
|
||||
isSilent = false,
|
||||
scheduledAt?: number,
|
||||
scheduleRepeatPeriod?: number,
|
||||
) => {
|
||||
handleActionWithPaymentConfirmation(handleSend, isSilent, scheduledAt, scheduleRepeatPeriod);
|
||||
});
|
||||
|
||||
const handleTodoListCreate = useLastCallback(() => {
|
||||
@ -1302,7 +1322,11 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleMessageSchedule = useLastCallback((
|
||||
args: ScheduledMessageArgs, scheduledAt: number, messageList: MessageList, effectId?: string,
|
||||
args: ScheduledMessageArgs,
|
||||
scheduledAt: number,
|
||||
scheduleRepeatPeriod: number | undefined,
|
||||
messageList: MessageList,
|
||||
effectId?: string,
|
||||
) => {
|
||||
if (args && 'queryId' in args) {
|
||||
const { id, queryId, isSilent } = args;
|
||||
@ -1320,15 +1344,17 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
const { isSilent, ...restArgs } = args || {};
|
||||
|
||||
if (!args || Object.keys(restArgs).length === 0) {
|
||||
void handleSend(Boolean(isSilent), scheduledAt);
|
||||
void handleSend(Boolean(isSilent), scheduledAt, scheduleRepeatPeriod);
|
||||
} else if (args.sendCompressed !== undefined || args.sendGrouped !== undefined) {
|
||||
const { sendCompressed = false, sendGrouped = false, isInvertedMedia } = args;
|
||||
void handleSendAttachments(sendCompressed, sendGrouped, isSilent, scheduledAt, isInvertedMedia);
|
||||
void handleSendAttachments(sendCompressed, sendGrouped, isSilent, scheduledAt, isInvertedMedia,
|
||||
scheduleRepeatPeriod);
|
||||
} else {
|
||||
sendMessage({
|
||||
...args,
|
||||
messageList,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
effectId,
|
||||
});
|
||||
}
|
||||
@ -1336,8 +1362,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useEffectWithPrevDeps(([prevContentToBeScheduled]) => {
|
||||
if (currentMessageList && contentToBeScheduled && contentToBeScheduled !== prevContentToBeScheduled) {
|
||||
requestCalendar((scheduledAt) => {
|
||||
handleMessageSchedule(contentToBeScheduled, scheduledAt, currentMessageList);
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
handleMessageSchedule(contentToBeScheduled, scheduledAt, scheduleRepeatPeriod, currentMessageList, undefined);
|
||||
});
|
||||
}
|
||||
}, [contentToBeScheduled, currentMessageList, handleMessageSchedule, requestCalendar]);
|
||||
@ -1393,9 +1419,15 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (isInScheduledList || isScheduleRequested) {
|
||||
forceShowSymbolMenu();
|
||||
requestCalendar((scheduledAt) => {
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
cancelForceShowSymbolMenu();
|
||||
handleActionWithPaymentConfirmation(handleMessageSchedule, { gif, isSilent }, scheduledAt, currentMessageList!);
|
||||
handleActionWithPaymentConfirmation(
|
||||
handleMessageSchedule,
|
||||
{ gif, isSilent },
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
currentMessageList!,
|
||||
);
|
||||
requestMeasure(() => {
|
||||
resetComposer(true);
|
||||
});
|
||||
@ -1430,10 +1462,14 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (isInScheduledList || isScheduleRequested) {
|
||||
forceShowSymbolMenu();
|
||||
requestCalendar((scheduledAt) => {
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
cancelForceShowSymbolMenu();
|
||||
handleActionWithPaymentConfirmation(
|
||||
handleMessageSchedule, { sticker, isSilent }, scheduledAt, currentMessageList!,
|
||||
handleMessageSchedule,
|
||||
{ sticker, isSilent },
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
currentMessageList!,
|
||||
);
|
||||
requestMeasure(() => {
|
||||
resetComposer(shouldPreserveInput);
|
||||
@ -1467,7 +1503,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
isSilent = isSilent || isSilentPosting;
|
||||
|
||||
if (isInScheduledList || isScheduleRequested) {
|
||||
requestCalendar((scheduledAt) => {
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
handleActionWithPaymentConfirmation(
|
||||
handleMessageSchedule,
|
||||
{
|
||||
@ -1476,6 +1512,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
isSilent,
|
||||
},
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
currentMessageList!,
|
||||
);
|
||||
});
|
||||
@ -1516,11 +1553,12 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
if (isInScheduledList) {
|
||||
requestCalendar((scheduledAt) => {
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
handleActionWithPaymentConfirmation(
|
||||
handleMessageSchedule,
|
||||
{ poll },
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
currentMessageList,
|
||||
);
|
||||
});
|
||||
@ -1540,11 +1578,12 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
if (isInScheduledList) {
|
||||
requestCalendar((scheduledAt) => {
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
handleActionWithPaymentConfirmation(
|
||||
handleMessageSchedule,
|
||||
{ todo },
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
currentMessageList,
|
||||
);
|
||||
});
|
||||
@ -1558,8 +1597,13 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const sendSilent = useLastCallback((additionalArgs?: ScheduledMessageArgs) => {
|
||||
if (isInScheduledList) {
|
||||
requestCalendar((scheduledAt) => {
|
||||
handleMessageSchedule({ ...additionalArgs, isSilent: true }, scheduledAt, currentMessageList!);
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
handleMessageSchedule(
|
||||
{ ...additionalArgs, isSilent: true },
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
currentMessageList!,
|
||||
);
|
||||
});
|
||||
} else if (additionalArgs && ('sendCompressed' in additionalArgs || 'sendGrouped' in additionalArgs)) {
|
||||
const { sendCompressed = false, sendGrouped = false, isInvertedMedia } = additionalArgs;
|
||||
@ -1779,8 +1823,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
if (!currentMessageList) {
|
||||
return;
|
||||
}
|
||||
requestCalendar((scheduledAt) => {
|
||||
handleMessageSchedule({}, scheduledAt, currentMessageList, effect?.id);
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
handleMessageSchedule({}, scheduledAt, scheduleRepeatPeriod, currentMessageList, effect?.id);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
@ -1870,8 +1914,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleSendScheduled = useLastCallback(() => {
|
||||
requestCalendar((scheduledAt) => {
|
||||
handleMessageSchedule({}, scheduledAt, currentMessageList!);
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
handleMessageSchedule({}, scheduledAt, scheduleRepeatPeriod, currentMessageList!, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1881,18 +1925,20 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleSendWhenOnline = useLastCallback(() => {
|
||||
handleActionWithPaymentConfirmation(
|
||||
handleMessageSchedule, {}, SCHEDULED_WHEN_ONLINE, currentMessageList!, effect?.id,
|
||||
handleMessageSchedule, {}, SCHEDULED_WHEN_ONLINE, undefined, currentMessageList!, effect?.id,
|
||||
);
|
||||
});
|
||||
|
||||
const handleSendScheduledAttachments = useLastCallback(
|
||||
(sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true) => {
|
||||
requestCalendar((scheduledAt) => {
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
handleActionWithPaymentConfirmation(
|
||||
handleMessageSchedule,
|
||||
{ sendCompressed, sendGrouped, isInvertedMedia },
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
currentMessageList!,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
@ -125,9 +125,9 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
if (shouldSchedule || isScheduleRequested) {
|
||||
requestCalendar((scheduledAt) => {
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
sendMessage({
|
||||
messageList: currentMessageList, sticker, isSilent, scheduledAt,
|
||||
messageList: currentMessageList, sticker, isSilent, scheduledAt, scheduleRepeatPeriod,
|
||||
});
|
||||
onClose();
|
||||
});
|
||||
|
||||
@ -287,7 +287,12 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
||||
const [selectionQuoteOffset, setSelectionQuoteOffset] = useState(UNQUOTABLE_OFFSET);
|
||||
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline, onClose, message.date);
|
||||
const [requestCalendar, calendar] = useSchedule(
|
||||
canScheduleUntilOnline,
|
||||
onClose,
|
||||
message.date,
|
||||
message.scheduleRepeatPeriod,
|
||||
);
|
||||
|
||||
// `undefined` indicates that emoji are present and loading
|
||||
const hasCustomEmoji = customEmojiSetsInfo === undefined || Boolean(customEmojiSetsInfo.length);
|
||||
@ -535,11 +540,15 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
const handleRescheduleMessage = useLastCallback((scheduledAt: number) => {
|
||||
const handleRescheduleMessage = useLastCallback((
|
||||
scheduledAt: number,
|
||||
scheduleRepeatPeriod?: number,
|
||||
) => {
|
||||
rescheduleMessage({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
});
|
||||
onClose();
|
||||
});
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatDateTimeToString, formatPastTimeShort, formatTime } from '../../../util/dates/dateFormat';
|
||||
import { formatStarsAsIcon } from '../../../util/localization/format';
|
||||
import { getRepeatPeriodText } from '../../../util/scheduledMessages';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
@ -83,6 +84,10 @@ const MessageMeta: FC<OwnProps> = ({
|
||||
onOpenThread();
|
||||
}
|
||||
|
||||
const repeatPeriodText = useMemo(() => {
|
||||
return getRepeatPeriodText(message.scheduleRepeatPeriod, lang);
|
||||
}, [message.scheduleRepeatPeriod, lang]);
|
||||
|
||||
const dateTitle = useMemo(() => {
|
||||
if (!isActivated) return undefined;
|
||||
const createDateTime = formatDateTimeToString(message.date * 1000, oldLang.code, undefined, oldLang.timeFormat);
|
||||
@ -134,12 +139,16 @@ const MessageMeta: FC<OwnProps> = ({
|
||||
|
||||
const date = useMemo(() => {
|
||||
const time = formatTime(oldLang, message.date * 1000);
|
||||
if (!withFullDate) {
|
||||
return time;
|
||||
const baseDate = !withFullDate
|
||||
? time
|
||||
: formatPastTimeShort(oldLang, (message.forwardInfo?.date || message.date) * 1000, true);
|
||||
|
||||
if (repeatPeriodText) {
|
||||
return lang('FormatDateAtTime', { date: repeatPeriodText, time: baseDate });
|
||||
}
|
||||
|
||||
return formatPastTimeShort(oldLang, (message.forwardInfo?.date || message.date) * 1000, true);
|
||||
}, [oldLang, message.date, message.forwardInfo?.date, withFullDate]);
|
||||
return baseDate;
|
||||
}, [oldLang, message.date, message.forwardInfo?.date, withFullDate, repeatPeriodText, lang]);
|
||||
|
||||
const fullClassName = buildClassName(
|
||||
'MessageMeta',
|
||||
|
||||
@ -87,11 +87,12 @@ const GifSearch: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
if (shouldSchedule) {
|
||||
requestCalendar((scheduledAt) => {
|
||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||
sendMessage({
|
||||
messageList: currentMessageList,
|
||||
gif,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
isSilent,
|
||||
});
|
||||
});
|
||||
|
||||
@ -1545,7 +1545,7 @@ addActionHandler('sendScheduledMessages', (global, actions, payload): ActionRetu
|
||||
|
||||
addActionHandler('rescheduleMessage', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, messageId, scheduledAt,
|
||||
chatId, messageId, scheduledAt, scheduleRepeatPeriod,
|
||||
} = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
@ -1558,6 +1558,7 @@ addActionHandler('rescheduleMessage', (global, actions, payload): ActionReturnTy
|
||||
chat,
|
||||
message,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
});
|
||||
});
|
||||
|
||||
@ -1610,19 +1611,19 @@ addActionHandler('loadCustomEmojis', async (global, actions, payload): Promise<v
|
||||
|
||||
addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
isSilent, scheduledAt, tabId = getCurrentTabId(),
|
||||
isSilent, scheduledAt, scheduleRepeatPeriod, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const { toChatId } = selectTabState(global, tabId).forwardMessages;
|
||||
const toChat = toChatId ? selectChat(global, toChatId) : undefined;
|
||||
if (!toChat) return;
|
||||
executeForwardMessages(global, { chat: toChat, isSilent, scheduledAt }, tabId);
|
||||
executeForwardMessages(global, { chat: toChat, isSilent, scheduledAt, scheduleRepeatPeriod }, tabId);
|
||||
});
|
||||
|
||||
async function executeForwardMessages(global: GlobalState, sendParams: SendMessageParams, tabId: number) {
|
||||
const {
|
||||
fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions, toThreadId = MAIN_THREAD_ID,
|
||||
} = selectTabState(global, tabId).forwardMessages;
|
||||
const { messagePriceInStars, isSilent, scheduledAt } = sendParams;
|
||||
const { messagePriceInStars, isSilent, scheduledAt, scheduleRepeatPeriod } = sendParams;
|
||||
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
const isToMainThread = toThreadId === MAIN_THREAD_ID;
|
||||
@ -1659,6 +1660,7 @@ async function executeForwardMessages(global: GlobalState, sendParams: SendMessa
|
||||
messages: slice,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
sendAs,
|
||||
withMyScore,
|
||||
noAuthors,
|
||||
@ -1696,6 +1698,7 @@ async function executeForwardMessages(global: GlobalState, sendParams: SendMessa
|
||||
sticker,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
scheduleRepeatPeriod,
|
||||
sendAs,
|
||||
lastMessageId,
|
||||
};
|
||||
|
||||
@ -461,6 +461,7 @@ export interface ActionPayloads {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
scheduledAt: number;
|
||||
scheduleRepeatPeriod?: number;
|
||||
};
|
||||
deleteScheduledMessages: { messageIds: number[] } & WithTabId;
|
||||
// Message
|
||||
@ -1934,6 +1935,7 @@ export interface ActionPayloads {
|
||||
forwardMessages: {
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
scheduleRepeatPeriod?: number;
|
||||
} & WithTabId;
|
||||
setForwardNoAuthors: {
|
||||
noAuthors: boolean;
|
||||
|
||||
@ -1,33 +1,39 @@
|
||||
import { useState } from '../lib/teact/teact';
|
||||
|
||||
import type { RepeatedMessageMode } from '../util/scheduledMessages';
|
||||
|
||||
import { SCHEDULED_WHEN_ONLINE } from '../config';
|
||||
import { getDayStartAt } from '../util/dates/dateFormat';
|
||||
import { getRepeatModeFromSeconds, getRepeatPeriodSeconds } from '../util/scheduledMessages';
|
||||
import { getServerTimeOffset } from '../util/serverTime';
|
||||
import useLastCallback from './useLastCallback';
|
||||
import useOldLang from './useOldLang';
|
||||
|
||||
import CalendarModal from '../components/common/CalendarModal.async';
|
||||
|
||||
type OnScheduledCallback = (scheduledAt: number) => void;
|
||||
type OnScheduledCallback = (scheduledAt: number, repeatPeriod?: number) => void;
|
||||
|
||||
const useSchedule = (
|
||||
canScheduleUntilOnline?: boolean,
|
||||
onCancel?: () => void,
|
||||
openAt?: number,
|
||||
initialRepeatPeriod?: number,
|
||||
) => {
|
||||
const lang = useOldLang();
|
||||
const [onScheduled, setOnScheduled] = useState<OnScheduledCallback | undefined>();
|
||||
|
||||
const handleMessageSchedule = useLastCallback((date: Date, isWhenOnline = false) => {
|
||||
const handleMessageSchedule = useLastCallback((date: Date, repeatMode?: RepeatedMessageMode) => {
|
||||
// Scheduled time can not be less than 10 seconds in future
|
||||
const isWhenOnline = date.getTime() === SCHEDULED_WHEN_ONLINE * 1000;
|
||||
const scheduledAt = Math.round(Math.max(date.getTime(), Date.now() + 60 * 1000) / 1000)
|
||||
+ (isWhenOnline ? 0 : getServerTimeOffset());
|
||||
onScheduled?.(scheduledAt);
|
||||
const repeatPeriod = getRepeatPeriodSeconds(repeatMode);
|
||||
onScheduled?.(scheduledAt, repeatPeriod);
|
||||
setOnScheduled(undefined);
|
||||
});
|
||||
|
||||
const handleMessageScheduleUntilOnline = useLastCallback(() => {
|
||||
handleMessageSchedule(new Date(SCHEDULED_WHEN_ONLINE * 1000), true);
|
||||
handleMessageSchedule(new Date(SCHEDULED_WHEN_ONLINE * 1000));
|
||||
});
|
||||
|
||||
const handleCloseCalendar = useLastCallback(() => {
|
||||
@ -46,10 +52,14 @@ const useSchedule = (
|
||||
const scheduledMaxDate = new Date();
|
||||
scheduledMaxDate.setFullYear(scheduledMaxDate.getFullYear() + 1);
|
||||
|
||||
const initialRepeatMode = getRepeatModeFromSeconds(initialRepeatPeriod);
|
||||
|
||||
const calendar = (
|
||||
<CalendarModal
|
||||
isOpen={Boolean(onScheduled)}
|
||||
withTimePicker
|
||||
withRepeatMode
|
||||
initialRepeatMode={initialRepeatMode}
|
||||
selectedAt={scheduledDefaultDate.getTime()}
|
||||
maxAt={getDayStartAt(scheduledMaxDate)}
|
||||
isFutureMode
|
||||
|
||||
@ -723,6 +723,7 @@ export type SendMessageParams = {
|
||||
contact?: ApiContact;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
scheduleRepeatPeriod?: number;
|
||||
groupedId?: string;
|
||||
noWebPage?: boolean;
|
||||
sendAs?: ApiPeer;
|
||||
@ -758,6 +759,7 @@ export type ForwardMessagesParams = {
|
||||
messages: ApiMessage[];
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
scheduleRepeatPeriod?: number;
|
||||
sendAs?: ApiPeer;
|
||||
withMyScore?: boolean;
|
||||
noAuthors?: boolean;
|
||||
|
||||
27
src/types/language.d.ts
vendored
27
src/types/language.d.ts
vendored
@ -1006,6 +1006,12 @@ export interface LangPair {
|
||||
'WeekdayYesterday': undefined;
|
||||
'User': undefined;
|
||||
'SecretChat': undefined;
|
||||
'MessageRepeatPeriodDaily': undefined;
|
||||
'MessageRepeatPeriodWeekly': undefined;
|
||||
'MessageRepeatPeriodBiweekly': undefined;
|
||||
'MessageRepeatPeriodMonthly': undefined;
|
||||
'MessageRepeatPeriodYearly': undefined;
|
||||
'MessageScheduledRepeatPremium': undefined;
|
||||
'ChatListFilterErrorEmpty': undefined;
|
||||
'ChatListFilterErrorTitleEmpty': undefined;
|
||||
'FilterMuted': undefined;
|
||||
@ -1052,6 +1058,12 @@ export interface LangPair {
|
||||
'Archive': undefined;
|
||||
'WaitingForNetwork': undefined;
|
||||
'ScheduleSendWhenOnline': undefined;
|
||||
'ScheduleRepeatNever': undefined;
|
||||
'ScheduleRepeatDaily': undefined;
|
||||
'ScheduleRepeatWeekly': undefined;
|
||||
'ScheduleRepeatBiweekly': undefined;
|
||||
'ScheduleRepeatMonthly': undefined;
|
||||
'ScheduleRepeatYearly': undefined;
|
||||
'VoipIncoming': undefined;
|
||||
'LiveLocationUpdatedJustNow': undefined;
|
||||
'RightNow': undefined;
|
||||
@ -2057,6 +2069,9 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'count': V;
|
||||
'total': V;
|
||||
};
|
||||
'ScheduleRepeat': {
|
||||
'value': V;
|
||||
};
|
||||
'MessageTimerShortHours': {
|
||||
'count': V;
|
||||
};
|
||||
@ -3182,12 +3197,24 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
|
||||
'LastSeenHoursAgo': {
|
||||
'count': V;
|
||||
};
|
||||
'MessageRepeatPeriodEveryMinutes': {
|
||||
'count': V;
|
||||
};
|
||||
'MessageRepeatPeriodEveryMonths': {
|
||||
'count': V;
|
||||
};
|
||||
'StickerPackRemoveStickerCount': {
|
||||
'count': V;
|
||||
};
|
||||
'StickerPackAddStickerCount': {
|
||||
'count': V;
|
||||
};
|
||||
'ScheduleRepeatEveryMinutes': {
|
||||
'count': V;
|
||||
};
|
||||
'ScheduleRepeatEveryMonths': {
|
||||
'count': V;
|
||||
};
|
||||
'Years': {
|
||||
'count': V;
|
||||
};
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
export const MINUTE = 60;
|
||||
export const HOUR = 3600;
|
||||
export const DAY = 86400;
|
||||
export const WEEK = 7 * DAY;
|
||||
export const MONTH = 30 * DAY;
|
||||
|
||||
export function getMinutes(seconds: number, roundDown?: boolean) {
|
||||
const roundFunc = roundDown ? Math.floor : Math.ceil;
|
||||
|
||||
100
src/util/scheduledMessages.ts
Normal file
100
src/util/scheduledMessages.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import type { LangFn } from './localization';
|
||||
|
||||
import { DAY, MINUTE, MONTH, WEEK } from './dates/units';
|
||||
|
||||
export type RepeatedMessageMode = 'never' | 'everyminute' | 'every5minutes' | 'daily' | 'weekly'
|
||||
| 'biweekly' | 'monthly' | 'every3months' | 'every6months' | 'yearly';
|
||||
|
||||
type ActualRepeatedMessageMode = Exclude<RepeatedMessageMode, 'never'>;
|
||||
|
||||
const MODE_TO_SECONDS: Record<ActualRepeatedMessageMode, number> = {
|
||||
everyminute: MINUTE,
|
||||
every5minutes: 5 * MINUTE,
|
||||
daily: DAY,
|
||||
weekly: WEEK,
|
||||
biweekly: 2 * WEEK,
|
||||
monthly: MONTH,
|
||||
every3months: 91 * DAY,
|
||||
every6months: 182 * DAY,
|
||||
yearly: 365 * DAY,
|
||||
};
|
||||
|
||||
export const ALL_REPEAT_MODES: RepeatedMessageMode[] = [
|
||||
'never',
|
||||
...Object.keys(MODE_TO_SECONDS) as ActualRepeatedMessageMode[],
|
||||
];
|
||||
|
||||
export const TEST_SERVER_ONLY_MODES = new Set<RepeatedMessageMode>([
|
||||
'everyminute',
|
||||
'every5minutes',
|
||||
]);
|
||||
|
||||
export function getRepeatPeriodSeconds(mode?: RepeatedMessageMode) {
|
||||
if (!mode || mode === 'never') return undefined;
|
||||
|
||||
return MODE_TO_SECONDS[mode];
|
||||
}
|
||||
|
||||
export function getRepeatModeFromSeconds(seconds?: number) {
|
||||
if (!seconds) return undefined;
|
||||
|
||||
return (Object.keys(MODE_TO_SECONDS) as ActualRepeatedMessageMode[])
|
||||
.find((mode) => MODE_TO_SECONDS[mode] === seconds);
|
||||
}
|
||||
|
||||
export function getRepeatPeriodText(seconds: number | undefined, lang: LangFn) {
|
||||
if (!seconds) return undefined;
|
||||
|
||||
const mode = getRepeatModeFromSeconds(seconds);
|
||||
if (!mode) return undefined;
|
||||
|
||||
switch (mode) {
|
||||
case 'everyminute':
|
||||
return lang('MessageRepeatPeriodEveryMinutes', { count: 1 }, { pluralValue: 1 });
|
||||
case 'every5minutes':
|
||||
return lang('MessageRepeatPeriodEveryMinutes', { count: 5 }, { pluralValue: 5 });
|
||||
case 'daily':
|
||||
return lang('MessageRepeatPeriodDaily');
|
||||
case 'weekly':
|
||||
return lang('MessageRepeatPeriodWeekly');
|
||||
case 'biweekly':
|
||||
return lang('MessageRepeatPeriodBiweekly');
|
||||
case 'monthly':
|
||||
return lang('MessageRepeatPeriodMonthly');
|
||||
case 'every3months':
|
||||
return lang('MessageRepeatPeriodEveryMonths', { count: 3 }, { pluralValue: 3 });
|
||||
case 'every6months':
|
||||
return lang('MessageRepeatPeriodEveryMonths', { count: 6 }, { pluralValue: 6 });
|
||||
case 'yearly':
|
||||
return lang('MessageRepeatPeriodYearly');
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getScheduleRepeatModeText(mode: RepeatedMessageMode, lang: LangFn) {
|
||||
switch (mode) {
|
||||
case 'never':
|
||||
return lang('ScheduleRepeatNever');
|
||||
case 'everyminute':
|
||||
return lang('ScheduleRepeatEveryMinutes', { count: 1 }, { pluralValue: 1 });
|
||||
case 'every5minutes':
|
||||
return lang('ScheduleRepeatEveryMinutes', { count: 5 }, { pluralValue: 5 });
|
||||
case 'daily':
|
||||
return lang('ScheduleRepeatDaily');
|
||||
case 'weekly':
|
||||
return lang('ScheduleRepeatWeekly');
|
||||
case 'biweekly':
|
||||
return lang('ScheduleRepeatBiweekly');
|
||||
case 'monthly':
|
||||
return lang('ScheduleRepeatMonthly');
|
||||
case 'every3months':
|
||||
return lang('ScheduleRepeatEveryMonths', { count: 3 }, { pluralValue: 3 });
|
||||
case 'every6months':
|
||||
return lang('ScheduleRepeatEveryMonths', { count: 6 }, { pluralValue: 6 });
|
||||
case 'yearly':
|
||||
return lang('ScheduleRepeatYearly');
|
||||
default:
|
||||
return lang('ScheduleRepeatNever');
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user