Calendar: Fix re-scheduling to the past; Composer: Add schedule icon (#1653)

This commit is contained in:
Alexander Zinchuk 2022-01-21 17:29:45 +01:00
parent 417745a998
commit 3a282b8086
3 changed files with 90 additions and 43 deletions

View File

@ -3,7 +3,7 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName'; 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 useLang, { LangFn } from '../../hooks/useLang';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
@ -14,9 +14,11 @@ import Button from '../ui/Button';
import './CalendarModal.scss'; import './CalendarModal.scss';
const MAX_SAFE_DATE = 2147483647 * 1000; // API has int for dates const MAX_SAFE_DATE = 2147483647 * 1000; // API has int for dates
const MIN_SAFE_DATE = 0;
export type OwnProps = { export type OwnProps = {
selectedAt?: number; selectedAt?: number;
minAt?: number;
maxAt?: number; maxAt?: number;
isFutureMode?: boolean; isFutureMode?: boolean;
isPastMode?: boolean; isPastMode?: boolean;
@ -41,6 +43,7 @@ const WEEKDAY_LETTERS = [
const CalendarModal: FC<OwnProps> = ({ const CalendarModal: FC<OwnProps> = ({
selectedAt, selectedAt,
minAt,
maxAt, maxAt,
isFutureMode, isFutureMode,
isPastMode, isPastMode,
@ -54,20 +57,29 @@ const CalendarModal: FC<OwnProps> = ({
}) => { }) => {
const lang = useLang(); const lang = useLang();
const now = new Date(); 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 prevIsOpen = usePrevious(isOpen);
const [isTimeInputFocused, markTimeInputAsFocused, unmarkTimeInputAsFocused] = useFlag(false); const [isTimeInputFocused, markTimeInputAsFocused, unmarkTimeInputAsFocused] = useFlag(false);
const [selectedDate, setSelectedDate] = useState<Date>(defaultSelectedDate); const [selectedDate, setSelectedDate] = useState<Date>(passedSelectedDate);
const [currentMonthAndYear, setCurrentMonthAndYear] = useState<Date>( const [currentMonthAndYear, setCurrentMonthAndYear] = useState<Date>(
new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1), new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1),
); );
const [selectedHours, setSelectedHours] = useState<string>( const [selectedHours, setSelectedHours] = useState<string>(
formatInputTime(defaultSelectedDate.getHours()), formatInputTime(passedSelectedDate.getHours()),
); );
const [selectedMinutes, setSelectedMinutes] = useState<string>( const [selectedMinutes, setSelectedMinutes] = useState<string>(
formatInputTime(defaultSelectedDate.getMinutes()), formatInputTime(passedSelectedDate.getMinutes()),
); );
const selectedDay = formatDay(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate()); const selectedDay = formatDay(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate());
@ -76,22 +88,39 @@ const CalendarModal: FC<OwnProps> = ({
useEffect(() => { useEffect(() => {
if (!prevIsOpen && isOpen) { if (!prevIsOpen && isOpen) {
setSelectedDate(defaultSelectedDate); setSelectedDate(passedSelectedDate);
setCurrentMonthAndYear(new Date(defaultSelectedDate.getFullYear(), defaultSelectedDate.getMonth(), 1)); setCurrentMonthAndYear(new Date(passedSelectedDate.getFullYear(), passedSelectedDate.getMonth(), 1));
if (withTimePicker) { if (withTimePicker) {
setSelectedHours(defaultSelectedDate.getHours().toString()); setSelectedHours(formatInputTime(passedSelectedDate.getHours()));
setSelectedMinutes(defaultSelectedDate.getMinutes().toString()); setSelectedMinutes(formatInputTime(passedSelectedDate.getMinutes()));
} }
} }
}, [defaultSelectedDate, isOpen, prevIsOpen, withTimePicker]); }, [passedSelectedDate, isOpen, prevIsOpen, withTimePicker]);
useEffect(() => { useEffect(() => {
if (isFutureMode && !isTimeInputFocused && selectedDate.getTime() < defaultSelectedDate.getTime()) { if (isFutureMode && !isTimeInputFocused && selectedDate.getTime() < minDate.getTime()) {
setSelectedDate(defaultSelectedDate); setSelectedDate(minDate);
setSelectedHours(formatInputTime(defaultSelectedDate.getHours())); setSelectedHours(formatInputTime(minDate.getHours()));
setSelectedMinutes(formatInputTime(defaultSelectedDate.getMinutes())); 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()) const shouldDisableNextMonth = (isPastMode && currentYear >= now.getFullYear() && currentMonth >= now.getMonth())
|| (maxDate && currentYear >= maxDate.getFullYear() && currentMonth >= maxDate.getMonth()); || (maxDate && currentYear >= maxDate.getFullYear() && currentMonth >= maxDate.getMonth());
@ -261,7 +290,7 @@ const CalendarModal: FC<OwnProps> = ({
className={buildClassName( className={buildClassName(
'day-button', 'day-button',
isDisabledDay( isDisabledDay(
currentYear, currentMonth, gridDate, isFutureMode ? now : undefined, isPastMode ? now : maxDate, currentYear, currentMonth, gridDate, minDate, maxDate,
) )
? 'disabled' ? 'disabled'
: `${gridDate ? 'clickable' : ''}`, : `${gridDate ? 'clickable' : ''}`,
@ -328,9 +357,9 @@ function buildCalendarGrid(year: number, month: number) {
} }
function isDisabledDay(year: number, month: number, day: number, minDate?: Date, maxDate?: Date) { function isDisabledDay(year: number, month: number, day: number, minDate?: Date, maxDate?: Date) {
const selectedDay = new Date(year, month, day, 0, 0, 0, 0); const selectedDay = new Date(year, month, day);
const fixedMinDate = minDate && new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate(), 0, 0, 0, 0); const fixedMinDate = minDate && getDayStart(minDate);
const fixedMaxDate = maxDate && new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 0, 0, 0, 0); const fixedMaxDate = maxDate && getDayStart(maxDate);
if (fixedMaxDate && selectedDay > fixedMaxDate) { if (fixedMaxDate && selectedDay > fixedMaxDate) {
return true; return true;

View File

@ -59,6 +59,7 @@
} }
.icon-send, .icon-send,
.icon-schedule,
.icon-microphone-alt, .icon-microphone-alt,
.icon-check { .icon-check {
position: absolute; position: absolute;
@ -66,6 +67,7 @@
&:not(:active):not(:focus):not(:hover) { &:not(:active):not(:focus):not(:hover) {
.icon-send, .icon-send,
.icon-schedule,
.icon-check { .icon-check {
color: var(--color-primary); color: var(--color-primary);
} }
@ -92,7 +94,20 @@
} }
.icon-microphone-alt, .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; animation: hide-icon .4s forwards ease-out;
} }
} }
@ -103,20 +118,22 @@
} }
.icon-send, .icon-send,
.icon-check { .icon-check,
.icon-schedule {
animation: hide-icon .4s forwards ease-out; animation: hide-icon .4s forwards ease-out;
} }
} }
&.edit { &.edit {
.icon-send,
.icon-microphone-alt {
animation: hide-icon .4s forwards ease-out;
}
.icon-check { .icon-check {
animation: grow-icon .4s ease-out; animation: grow-icon .4s ease-out;
} }
.icon-send,
.icon-microphone-alt,
.icon-schedule {
animation: hide-icon .4s forwards ease-out;
}
} }
&.not-ready > i { &.not-ready > i {
@ -124,7 +141,7 @@
} }
body.animation-level-0 &, body.animation-level-1 & { 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; animation-duration: 0ms !important;
} }
} }

View File

@ -154,6 +154,7 @@ enum MainButtonState {
Send = 'send', Send = 'send',
Record = 'record', Record = 'record',
Edit = 'edit', Edit = 'edit',
Schedule = 'schedule',
} }
const VOICE_RECORDING_FILENAME = 'wonderful-voice-message.ogg'; const VOICE_RECORDING_FILENAME = 'wonderful-voice-message.ogg';
@ -312,10 +313,9 @@ const Composer: FC<OwnProps & StateProps> = ({
} }
}, [activeVoiceRecording, sendMessageAction]); }, [activeVoiceRecording, sendMessageAction]);
const mainButtonState = editingMessage const mainButtonState = editingMessage ? MainButtonState.Edit
? MainButtonState.Edit : (!IS_VOICE_RECORDING_SUPPORTED || activeVoiceRecording || (html && !attachments.length) || isForwarding)
: !IS_VOICE_RECORDING_SUPPORTED || activeVoiceRecording || (html && !attachments.length) || isForwarding ? (shouldSchedule ? MainButtonState.Schedule : MainButtonState.Send)
? MainButtonState.Send
: MainButtonState.Record; : MainButtonState.Record;
const canShowCustomSendMenu = !shouldSchedule; const canShowCustomSendMenu = !shouldSchedule;
@ -769,14 +769,7 @@ const Composer: FC<OwnProps & StateProps> = ({
const mainButtonHandler = useCallback(() => { const mainButtonHandler = useCallback(() => {
switch (mainButtonState) { switch (mainButtonState) {
case MainButtonState.Send: case MainButtonState.Send:
if (shouldSchedule) { handleSend();
if (activeVoiceRecording) {
pauseRecordingVoice();
}
openCalendar();
} else {
void handleSend();
}
break; break;
case MainButtonState.Record: case MainButtonState.Record:
void startRecordingVoice(); void startRecordingVoice();
@ -784,12 +777,18 @@ const Composer: FC<OwnProps & StateProps> = ({
case MainButtonState.Edit: case MainButtonState.Edit:
handleEditComplete(); handleEditComplete();
break; break;
case MainButtonState.Schedule:
if (activeVoiceRecording) {
pauseRecordingVoice();
}
openCalendar();
break;
default: default:
break; break;
} }
}, [ }, [
mainButtonState, shouldSchedule, startRecordingVoice, handleEditComplete, mainButtonState, handleSend, startRecordingVoice, handleEditComplete,
activeVoiceRecording, openCalendar, pauseRecordingVoice, handleSend, activeVoiceRecording, openCalendar, pauseRecordingVoice,
]); ]);
const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record
@ -832,7 +831,8 @@ const Composer: FC<OwnProps & StateProps> = ({
const onSend = mainButtonState === MainButtonState.Edit const onSend = mainButtonState === MainButtonState.Edit
? handleEditComplete ? handleEditComplete
: (shouldSchedule ? openCalendar : handleSend); : mainButtonState === MainButtonState.Schedule ? openCalendar
: handleSend;
return ( return (
<div className={className}> <div className={className}>
@ -1103,6 +1103,7 @@ const Composer: FC<OwnProps & StateProps> = ({
} }
> >
<i className="icon-send" /> <i className="icon-send" />
<i className="icon-schedule" />
<i className="icon-microphone-alt" /> <i className="icon-microphone-alt" />
<i className="icon-check" /> <i className="icon-check" />
</Button> </Button>