diff --git a/src/components/common/CalendarModal.tsx b/src/components/common/CalendarModal.tsx index a1da175e3..82cf4e19b 100644 --- a/src/components/common/CalendarModal.tsx +++ b/src/components/common/CalendarModal.tsx @@ -3,7 +3,7 @@ import React, { } from '../../lib/teact/teact'; import buildClassName from '../../util/buildClassName'; -import { formatTime, formatDateToString } from '../../util/dateFormat'; +import { formatTime, formatDateToString, getDayStart } from '../../util/dateFormat'; import useLang, { LangFn } from '../../hooks/useLang'; import usePrevious from '../../hooks/usePrevious'; import useFlag from '../../hooks/useFlag'; @@ -14,9 +14,11 @@ import Button from '../ui/Button'; import './CalendarModal.scss'; const MAX_SAFE_DATE = 2147483647 * 1000; // API has int for dates +const MIN_SAFE_DATE = 0; export type OwnProps = { selectedAt?: number; + minAt?: number; maxAt?: number; isFutureMode?: boolean; isPastMode?: boolean; @@ -41,6 +43,7 @@ const WEEKDAY_LETTERS = [ const CalendarModal: FC = ({ selectedAt, + minAt, maxAt, isFutureMode, isPastMode, @@ -54,20 +57,29 @@ const CalendarModal: FC = ({ }) => { const lang = useLang(); const now = new Date(); - const defaultSelectedDate = useMemo(() => (selectedAt ? new Date(selectedAt) : new Date()), [selectedAt]); - const maxDate = new Date(Math.min(maxAt || MAX_SAFE_DATE, MAX_SAFE_DATE)); + + const minDate = useMemo(() => { + if (isFutureMode && !minAt) return new Date(); + return new Date(Math.max(minAt || MIN_SAFE_DATE, MIN_SAFE_DATE)); + }, [isFutureMode, minAt]); + const maxDate = useMemo(() => { + if (isPastMode && !maxAt) return new Date(); + return new Date(Math.min(maxAt || MAX_SAFE_DATE, MAX_SAFE_DATE)); + }, [isPastMode, maxAt]); + + const passedSelectedDate = useMemo(() => (selectedAt ? new Date(selectedAt) : new Date()), [selectedAt]); const prevIsOpen = usePrevious(isOpen); const [isTimeInputFocused, markTimeInputAsFocused, unmarkTimeInputAsFocused] = useFlag(false); - const [selectedDate, setSelectedDate] = useState(defaultSelectedDate); + const [selectedDate, setSelectedDate] = useState(passedSelectedDate); const [currentMonthAndYear, setCurrentMonthAndYear] = useState( new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1), ); const [selectedHours, setSelectedHours] = useState( - formatInputTime(defaultSelectedDate.getHours()), + formatInputTime(passedSelectedDate.getHours()), ); const [selectedMinutes, setSelectedMinutes] = useState( - formatInputTime(defaultSelectedDate.getMinutes()), + formatInputTime(passedSelectedDate.getMinutes()), ); const selectedDay = formatDay(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate()); @@ -76,22 +88,39 @@ const CalendarModal: FC = ({ useEffect(() => { if (!prevIsOpen && isOpen) { - setSelectedDate(defaultSelectedDate); - setCurrentMonthAndYear(new Date(defaultSelectedDate.getFullYear(), defaultSelectedDate.getMonth(), 1)); + setSelectedDate(passedSelectedDate); + setCurrentMonthAndYear(new Date(passedSelectedDate.getFullYear(), passedSelectedDate.getMonth(), 1)); if (withTimePicker) { - setSelectedHours(defaultSelectedDate.getHours().toString()); - setSelectedMinutes(defaultSelectedDate.getMinutes().toString()); + setSelectedHours(formatInputTime(passedSelectedDate.getHours())); + setSelectedMinutes(formatInputTime(passedSelectedDate.getMinutes())); } } - }, [defaultSelectedDate, isOpen, prevIsOpen, withTimePicker]); + }, [passedSelectedDate, isOpen, prevIsOpen, withTimePicker]); useEffect(() => { - if (isFutureMode && !isTimeInputFocused && selectedDate.getTime() < defaultSelectedDate.getTime()) { - setSelectedDate(defaultSelectedDate); - setSelectedHours(formatInputTime(defaultSelectedDate.getHours())); - setSelectedMinutes(formatInputTime(defaultSelectedDate.getMinutes())); + if (isFutureMode && !isTimeInputFocused && selectedDate.getTime() < minDate.getTime()) { + setSelectedDate(minDate); + setSelectedHours(formatInputTime(minDate.getHours())); + setSelectedMinutes(formatInputTime(minDate.getMinutes())); } - }, [defaultSelectedDate, isTimeInputFocused, isFutureMode, selectedDate]); + }, [isFutureMode, isTimeInputFocused, minDate, selectedDate]); + + useEffect(() => { + if (isPastMode && !isTimeInputFocused && selectedDate.getTime() > maxDate.getTime()) { + setSelectedDate(maxDate); + setSelectedHours(formatInputTime(maxDate.getHours())); + setSelectedMinutes(formatInputTime(maxDate.getMinutes())); + } + }, [isFutureMode, isPastMode, isTimeInputFocused, maxDate, minDate, selectedDate]); + + useEffect(() => { + if (selectedAt) { + const newSelectedDate = new Date(selectedAt); + setSelectedDate(newSelectedDate); + setSelectedHours(formatInputTime(newSelectedDate.getHours())); + setSelectedMinutes(formatInputTime(newSelectedDate.getMinutes())); + } + }, [selectedAt]); const shouldDisableNextMonth = (isPastMode && currentYear >= now.getFullYear() && currentMonth >= now.getMonth()) || (maxDate && currentYear >= maxDate.getFullYear() && currentMonth >= maxDate.getMonth()); @@ -261,7 +290,7 @@ const CalendarModal: FC = ({ className={buildClassName( 'day-button', isDisabledDay( - currentYear, currentMonth, gridDate, isFutureMode ? now : undefined, isPastMode ? now : maxDate, + currentYear, currentMonth, gridDate, minDate, maxDate, ) ? 'disabled' : `${gridDate ? 'clickable' : ''}`, @@ -328,9 +357,9 @@ function buildCalendarGrid(year: number, month: number) { } function isDisabledDay(year: number, month: number, day: number, minDate?: Date, maxDate?: Date) { - const selectedDay = new Date(year, month, day, 0, 0, 0, 0); - const fixedMinDate = minDate && new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate(), 0, 0, 0, 0); - const fixedMaxDate = maxDate && new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 0, 0, 0, 0); + const selectedDay = new Date(year, month, day); + const fixedMinDate = minDate && getDayStart(minDate); + const fixedMaxDate = maxDate && getDayStart(maxDate); if (fixedMaxDate && selectedDay > fixedMaxDate) { return true; diff --git a/src/components/middle/composer/Composer.scss b/src/components/middle/composer/Composer.scss index 17a545e81..bfc551b04 100644 --- a/src/components/middle/composer/Composer.scss +++ b/src/components/middle/composer/Composer.scss @@ -59,6 +59,7 @@ } .icon-send, + .icon-schedule, .icon-microphone-alt, .icon-check { position: absolute; @@ -66,6 +67,7 @@ &:not(:active):not(:focus):not(:hover) { .icon-send, + .icon-schedule, .icon-check { color: var(--color-primary); } @@ -92,7 +94,20 @@ } .icon-microphone-alt, - .icon-check { + .icon-check, + .icon-schedule { + animation: hide-icon .4s forwards ease-out; + } + } + + &.schedule { + .icon-schedule { + animation: grow-icon .4s ease-out; + } + + .icon-microphone-alt, + .icon-check, + .icon-send { animation: hide-icon .4s forwards ease-out; } } @@ -103,20 +118,22 @@ } .icon-send, - .icon-check { + .icon-check, + .icon-schedule { animation: hide-icon .4s forwards ease-out; } } &.edit { - .icon-send, - .icon-microphone-alt { - animation: hide-icon .4s forwards ease-out; - } - .icon-check { animation: grow-icon .4s ease-out; } + + .icon-send, + .icon-microphone-alt, + .icon-schedule { + animation: hide-icon .4s forwards ease-out; + } } &.not-ready > i { @@ -124,7 +141,7 @@ } body.animation-level-0 &, body.animation-level-1 & { - .icon-send, .icon-microphone-alt, .icon-check { + .icon-send, .icon-microphone-alt, .icon-check, .icon-schedule { animation-duration: 0ms !important; } } diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index 05d508a35..d55c6ba54 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -154,6 +154,7 @@ enum MainButtonState { Send = 'send', Record = 'record', Edit = 'edit', + Schedule = 'schedule', } const VOICE_RECORDING_FILENAME = 'wonderful-voice-message.ogg'; @@ -312,10 +313,9 @@ const Composer: FC = ({ } }, [activeVoiceRecording, sendMessageAction]); - const mainButtonState = editingMessage - ? MainButtonState.Edit - : !IS_VOICE_RECORDING_SUPPORTED || activeVoiceRecording || (html && !attachments.length) || isForwarding - ? MainButtonState.Send + const mainButtonState = editingMessage ? MainButtonState.Edit + : (!IS_VOICE_RECORDING_SUPPORTED || activeVoiceRecording || (html && !attachments.length) || isForwarding) + ? (shouldSchedule ? MainButtonState.Schedule : MainButtonState.Send) : MainButtonState.Record; const canShowCustomSendMenu = !shouldSchedule; @@ -769,14 +769,7 @@ const Composer: FC = ({ const mainButtonHandler = useCallback(() => { switch (mainButtonState) { case MainButtonState.Send: - if (shouldSchedule) { - if (activeVoiceRecording) { - pauseRecordingVoice(); - } - openCalendar(); - } else { - void handleSend(); - } + handleSend(); break; case MainButtonState.Record: void startRecordingVoice(); @@ -784,12 +777,18 @@ const Composer: FC = ({ case MainButtonState.Edit: handleEditComplete(); break; + case MainButtonState.Schedule: + if (activeVoiceRecording) { + pauseRecordingVoice(); + } + openCalendar(); + break; default: break; } }, [ - mainButtonState, shouldSchedule, startRecordingVoice, handleEditComplete, - activeVoiceRecording, openCalendar, pauseRecordingVoice, handleSend, + mainButtonState, handleSend, startRecordingVoice, handleEditComplete, + activeVoiceRecording, openCalendar, pauseRecordingVoice, ]); const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record @@ -832,7 +831,8 @@ const Composer: FC = ({ const onSend = mainButtonState === MainButtonState.Edit ? handleEditComplete - : (shouldSchedule ? openCalendar : handleSend); + : mainButtonState === MainButtonState.Schedule ? openCalendar + : handleSend; return (
@@ -1103,6 +1103,7 @@ const Composer: FC = ({ } > +