Sticky Dates: Support opening history calendar

This commit is contained in:
Alexander Zinchuk 2021-06-15 02:21:29 +03:00
parent b9dbcab72c
commit 36c4901e5f
10 changed files with 137 additions and 56 deletions

View File

@ -4,6 +4,7 @@ export { default as ForwardPicker } from '../components/main/ForwardPicker';
export { default as Errors } from '../components/main/Errors';
export { default as Notifications } from '../components/main/Notifications';
export { default as SafeLinkModal } from '../components/main/SafeLinkModal';
export { default as HistoryCalendar } from '../components/main/HistoryCalendar';
export { default as CalendarModal } from '../components/common/CalendarModal';
export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal';

View File

@ -0,0 +1,16 @@
import React, { FC, memo } from '../../lib/teact/teact';
import { Bundles } from '../../util/moduleLoader';
import { OwnProps } from './HistoryCalendar';
import useModuleLoader from '../../hooks/useModuleLoader';
const HistoryCalendarAsync: FC<OwnProps> = (props) => {
const { isOpen } = props;
const HistoryCalendar = useModuleLoader(Bundles.Extra, 'HistoryCalendar', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return HistoryCalendar ? <HistoryCalendar {...props} /> : undefined;
};
export default memo(HistoryCalendarAsync);

View File

@ -0,0 +1,52 @@
import React, { FC, memo, useCallback } from '../../lib/teact/teact';
import { withGlobal } from '../../lib/teact/teactn';
import { GlobalActions } from '../../global/types';
import { pick } from '../../util/iteratees';
import useLang from '../../hooks/useLang';
import CalendarModal from '../common/CalendarModal';
export type OwnProps = {
isOpen: boolean;
};
type StateProps = {
selectedAt?: number;
};
type DispatchProps = Pick<GlobalActions, 'searchMessagesByDate' | 'closeHistoryCalendar'>;
const HistoryCalendar: FC<OwnProps & StateProps & DispatchProps> = ({
isOpen, selectedAt, searchMessagesByDate, closeHistoryCalendar,
}) => {
const handleJumpToDate = useCallback((date: Date) => {
searchMessagesByDate({ timestamp: date.valueOf() / 1000 });
closeHistoryCalendar();
}, [closeHistoryCalendar, searchMessagesByDate]);
const lang = useLang();
return (
<CalendarModal
isOpen={isOpen}
selectedAt={selectedAt}
isPastMode
submitButtonLabel={lang('JumpToDate')}
onClose={closeHistoryCalendar}
onSubmit={handleJumpToDate}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
return {
selectedAt: global.historyCalendarSelectedAt,
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
'searchMessagesByDate', 'closeHistoryCalendar',
]),
)(HistoryCalendar));

View File

@ -30,6 +30,7 @@ import Notifications from './Notifications.async';
import Errors from './Errors.async';
import ForwardPicker from './ForwardPicker.async';
import SafeLinkModal from './SafeLinkModal.async';
import HistoryCalendar from './HistoryCalendar.async';
import './Main.scss';
@ -44,6 +45,7 @@ type StateProps = {
hasErrors: boolean;
audioMessage?: ApiMessage;
safeLinkModalUrl?: string;
isHistoryCalendarOpen: boolean;
};
type DispatchProps = Pick<GlobalActions, 'loadAnimatedEmojis'>;
@ -58,7 +60,6 @@ let DEBUG_isLogged = false;
const Main: FC<StateProps & DispatchProps> = ({
lastSyncTime,
loadAnimatedEmojis,
isLeftColumnShown,
isRightColumnShown,
isMediaViewerOpen,
@ -68,6 +69,8 @@ const Main: FC<StateProps & DispatchProps> = ({
hasErrors,
audioMessage,
safeLinkModalUrl,
isHistoryCalendarOpen,
loadAnimatedEmojis,
}) => {
if (DEBUG && !DEBUG_isLogged) {
DEBUG_isLogged = true;
@ -171,6 +174,7 @@ const Main: FC<StateProps & DispatchProps> = ({
<Errors isOpen={hasErrors} />
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
<SafeLinkModal url={safeLinkModalUrl} />
<HistoryCalendar isOpen={isHistoryCalendarOpen} />
</div>
);
};
@ -206,6 +210,7 @@ export default memo(withGlobal(
hasErrors: Boolean(global.errors.length),
audioMessage,
safeLinkModalUrl: global.safeLinkModalUrl,
isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt),
};
},
(setGlobal, actions): DispatchProps => pick(actions, ['loadAnimatedEmojis']),

View File

@ -186,11 +186,23 @@
body:not(.is-scrolling-messages) &.stuck {
opacity: 0;
span {
pointer-events: none;
}
}
body.animation-level-0 & {
transition: none;
}
&.interactive {
cursor: pointer;
}
span {
pointer-events: auto;
}
}
&.scrolled .sticky-date {

View File

@ -22,7 +22,8 @@ import {
selectScrollOffset,
selectThreadTopMessageId,
selectFirstMessageId,
selectScheduledMessages, selectCurrentMessageIds,
selectScheduledMessages,
selectCurrentMessageIds,
} from '../../modules/selectors';
import {
getMessageOriginalId,
@ -92,7 +93,7 @@ type StateProps = {
};
type DispatchProps = Pick<GlobalActions, (
'loadViewportMessages' | 'markMessageListRead' | 'markMessagesRead' | 'setScrollOffset'
'loadViewportMessages' | 'markMessageListRead' | 'markMessagesRead' | 'setScrollOffset' | 'openHistoryCalendar'
)>;
const BOTTOM_THRESHOLD = 100;
@ -138,6 +139,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
botDescription,
threadTopMessageId,
hasLinkedChat,
openHistoryCalendar,
}) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
@ -557,9 +559,10 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
type,
threadTopMessageId,
threadFirstMessageId,
hasLinkedChat,
Boolean(hasLinkedChat),
messageGroups ? type === 'scheduled' : false,
!messageGroups || !shouldAnimateAppearanceRef.current,
openHistoryCalendar,
)}
</MessageScroll>
) : (
@ -580,11 +583,12 @@ function renderMessages(
memoFirstUnreadIdRef: { current: number | undefined },
threadId: number,
type: MessageListType,
threadTopMessageId?: number,
threadFirstMessageId?: number,
hasLinkedChat?: boolean,
isSchedule = false,
noAppearanceAnimation = false,
threadTopMessageId: number | undefined,
threadFirstMessageId: number | undefined,
hasLinkedChat: boolean,
isSchedule: boolean,
noAppearanceAnimation: boolean,
openHistoryCalendar: Function,
) {
const unreadDivider = (
<div className={buildClassName(UNREAD_DIVIDER_CLASS, 'local-action-message')} key="unread-messages">
@ -701,7 +705,11 @@ function renderMessages(
key={dateGroup.datetime}
teactFastList
>
<div className="sticky-date" key="date-header">
<div
className={buildClassName('sticky-date', !isSchedule && 'interactive')}
key="date-header"
onClick={!isSchedule ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
>
<span dir="auto">
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (
lang('MessageScheduledUntilOnline')
@ -785,5 +793,6 @@ export default memo(withGlobal<OwnProps>(
'markMessageListRead',
'markMessagesRead',
'setScrollOffset',
'openHistoryCalendar',
]),
)(MessageList));

View File

@ -9,12 +9,10 @@ import { GlobalActions } from '../../global/types';
import { debounce } from '../../util/schedulers';
import { selectCurrentTextSearch, selectCurrentChat } from '../../modules/selectors';
import { pick } from '../../util/iteratees';
import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang';
import { getDayStartAt } from '../../util/dateFormat';
import Button from '../ui/Button';
import SearchInput from '../ui/SearchInput';
import CalendarModal from '../common/CalendarModal';
import './MobileSearch.scss';
@ -28,10 +26,11 @@ type StateProps = {
query?: string;
totalCount?: number;
foundIds?: number[];
isHistoryCalendarOpen?: boolean;
};
type DispatchProps = Pick<GlobalActions, (
'setLocalTextSearchQuery' | 'searchTextMessagesLocal' | 'closeLocalTextSearch' | 'searchMessagesByDate' |
'setLocalTextSearchQuery' | 'searchTextMessagesLocal' | 'closeLocalTextSearch' | 'openHistoryCalendar' |
'focusMessage'
)>;
@ -43,16 +42,16 @@ const MobileSearchFooter: FC<StateProps & DispatchProps> = ({
query,
totalCount,
foundIds,
isHistoryCalendarOpen,
setLocalTextSearchQuery,
searchTextMessagesLocal,
focusMessage,
closeLocalTextSearch,
searchMessagesByDate,
openHistoryCalendar,
}) => {
// eslint-disable-next-line no-null/no-null
const inputRef = useRef<HTMLInputElement>(null);
const [focusedIndex, setFocusedIndex] = useState(0);
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
// Fix for iOS keyboard
useEffect(() => {
@ -113,7 +112,7 @@ const MobileSearchFooter: FC<StateProps & DispatchProps> = ({
useLayoutEffect(() => {
const searchInput = document.querySelector<HTMLInputElement>('#MobileSearch input')!;
searchInput.blur();
}, [isCalendarOpen]);
}, [isHistoryCalendarOpen]);
const handleMessageSearchQueryChange = useCallback((newQuery: string) => {
setLocalTextSearchQuery({ query: newQuery });
@ -123,11 +122,6 @@ const MobileSearchFooter: FC<StateProps & DispatchProps> = ({
}
}, [searchTextMessagesLocal, setLocalTextSearchQuery]);
const handleJumpToDate = useCallback((date: Date) => {
searchMessagesByDate({ timestamp: date.valueOf() / 1000 });
closeCalendar();
}, [closeCalendar, searchMessagesByDate]);
const handleUp = useCallback(() => {
if (chat && foundIds) {
const newFocusIndex = focusedIndex + 1;
@ -144,8 +138,6 @@ const MobileSearchFooter: FC<StateProps & DispatchProps> = ({
}
}, [chat, focusedIndex, focusMessage, foundIds]);
const lang = useLang();
return (
<div id="MobileSearch" className={isActive ? 'active' : ''}>
<div className="header">
@ -178,7 +170,7 @@ const MobileSearchFooter: FC<StateProps & DispatchProps> = ({
round
size="smaller"
color="translucent"
onClick={openCalendar}
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
ariaLabel="Search messages by date"
>
<i className="icon-calendar" />
@ -204,13 +196,6 @@ const MobileSearchFooter: FC<StateProps & DispatchProps> = ({
<i className="icon-down" />
</Button>
</div>
<CalendarModal
isOpen={isCalendarOpen}
isPastMode
submitButtonLabel={lang('JumpToDate')}
onClose={closeCalendar}
onSubmit={handleJumpToDate}
/>
</div>
);
};
@ -230,6 +215,7 @@ export default memo(withGlobal<OwnProps>(
query,
totalCount,
foundIds,
isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt),
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
@ -237,6 +223,6 @@ export default memo(withGlobal<OwnProps>(
'searchTextMessagesLocal',
'focusMessage',
'closeLocalTextSearch',
'searchMessagesByDate',
'openHistoryCalendar',
]),
)(MobileSearchFooter));

View File

@ -19,14 +19,13 @@ import {
} from '../../modules/selectors';
import { isChatAdmin, isChatChannel, isChatPrivate } from '../../modules/helpers';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang';
import CalendarModal from '../common/CalendarModal.async';
import SearchInput from '../ui/SearchInput';
import Button from '../ui/Button';
import Transition from '../ui/Transition';
import './RightHeader.scss';
import { getDayStartAt } from '../../util/dateFormat';
type OwnProps = {
chatId?: number;
@ -52,7 +51,7 @@ type StateProps = {
type DispatchProps = Pick<GlobalActions, (
'setLocalTextSearchQuery' | 'setStickerSearchQuery' | 'setGifSearchQuery' |
'searchTextMessagesLocal' | 'toggleManagement' | 'searchMessagesByDate'
'searchTextMessagesLocal' | 'toggleManagement' | 'openHistoryCalendar'
)>;
const COLUMN_CLOSE_DELAY_MS = 300;
@ -102,13 +101,11 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
setGifSearchQuery,
searchTextMessagesLocal,
toggleManagement,
searchMessagesByDate,
openHistoryCalendar,
}) => {
// eslint-disable-next-line no-null/no-null
const backButtonRef = useRef<HTMLDivElement>(null);
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
const handleMessageSearchQueryChange = useCallback((query: string) => {
setLocalTextSearchQuery({ query });
@ -117,11 +114,6 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
}
}, [searchTextMessagesLocal, setLocalTextSearchQuery]);
const handleJumpToDate = useCallback((date: Date) => {
searchMessagesByDate({ timestamp: date.valueOf() / 1000 });
closeCalendar();
}, [closeCalendar, searchMessagesByDate]);
const handleStickerSearchQueryChange = useCallback((query: string) => {
setStickerSearchQuery({ query });
}, [setStickerSearchQuery]);
@ -205,7 +197,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
round
size="smaller"
color="translucent"
onClick={openCalendar}
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
ariaLabel="Search messages by date"
>
<i className="icon-calendar" />
@ -312,15 +304,6 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
>
{renderHeaderContent}
</Transition>
{!IS_MOBILE_SCREEN && (
<CalendarModal
isOpen={isCalendarOpen}
isPastMode
submitButtonLabel={lang('JumpToDate')}
onClose={closeCalendar}
onSubmit={handleJumpToDate}
/>
)}
</div>
);
};
@ -356,6 +339,6 @@ export default memo(withGlobal<OwnProps>(
'setGifSearchQuery',
'searchTextMessagesLocal',
'toggleManagement',
'searchMessagesByDate',
'openHistoryCalendar',
]),
)(RightHeader));

View File

@ -389,6 +389,7 @@ export type GlobalState = {
};
safeLinkModalUrl?: string;
historyCalendarSelectedAt?: number;
};
export type ActionTypes = (
@ -397,7 +398,7 @@ export type ActionTypes = (
'showNotification' | 'dismissNotification' | 'showError' | 'dismissError' |
// ui
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
'toggleSafeLinkModal' |
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' |
// auth
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' |
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'gotToAuthQrCode' | 'clearCache' |

View File

@ -199,3 +199,19 @@ addReducer('toggleSafeLinkModal', (global, actions, payload) => {
safeLinkModalUrl,
};
});
addReducer('openHistoryCalendar', (global, actions, payload) => {
const { selectedAt } = payload;
return {
...global,
historyCalendarSelectedAt: selectedAt,
};
});
addReducer('closeHistoryCalendar', (global) => {
return {
...global,
historyCalendarSelectedAt: undefined,
};
});