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';
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<OwnProps> = ({
selectedAt,
minAt,
maxAt,
isFutureMode,
isPastMode,
@ -54,20 +57,29 @@ const CalendarModal: FC<OwnProps> = ({
}) => {
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<Date>(defaultSelectedDate);
const [selectedDate, setSelectedDate] = useState<Date>(passedSelectedDate);
const [currentMonthAndYear, setCurrentMonthAndYear] = useState<Date>(
new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1),
);
const [selectedHours, setSelectedHours] = useState<string>(
formatInputTime(defaultSelectedDate.getHours()),
formatInputTime(passedSelectedDate.getHours()),
);
const [selectedMinutes, setSelectedMinutes] = useState<string>(
formatInputTime(defaultSelectedDate.getMinutes()),
formatInputTime(passedSelectedDate.getMinutes()),
);
const selectedDay = formatDay(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate());
@ -76,22 +88,39 @@ const CalendarModal: FC<OwnProps> = ({
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<OwnProps> = ({
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;

View File

@ -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;
}
}

View File

@ -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<OwnProps & StateProps> = ({
}
}, [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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
const onSend = mainButtonState === MainButtonState.Edit
? handleEditComplete
: (shouldSchedule ? openCalendar : handleSend);
: mainButtonState === MainButtonState.Schedule ? openCalendar
: handleSend;
return (
<div className={className}>
@ -1103,6 +1103,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}
>
<i className="icon-send" />
<i className="icon-schedule" />
<i className="icon-microphone-alt" />
<i className="icon-check" />
</Button>