Message: Display formatted dates (#6795)
This commit is contained in:
parent
bc7468265d
commit
b98f790308
@ -286,6 +286,21 @@ export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMess
|
||||
};
|
||||
}
|
||||
|
||||
if (entity instanceof GramJs.MessageEntityFormattedDate) {
|
||||
return {
|
||||
type: ApiMessageEntityTypes.FormattedDate,
|
||||
offset,
|
||||
length,
|
||||
date: entity.date,
|
||||
relative: entity.relative,
|
||||
shortTime: entity.shortTime,
|
||||
longTime: entity.longTime,
|
||||
shortDate: entity.shortDate,
|
||||
longDate: entity.longDate,
|
||||
dayOfWeek: entity.dayOfWeek,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: type as `${ApiMessageEntityDefault['type']}`,
|
||||
offset,
|
||||
|
||||
@ -420,6 +420,18 @@ export function buildMtpMessageEntity(entity: ApiMessageEntity): GramJs.TypeMess
|
||||
return new GramJs.MessageEntitySpoiler({ offset, length });
|
||||
case ApiMessageEntityTypes.CustomEmoji:
|
||||
return new GramJs.MessageEntityCustomEmoji({ offset, length, documentId: BigInt(entity.documentId) });
|
||||
case ApiMessageEntityTypes.FormattedDate:
|
||||
return new GramJs.MessageEntityFormattedDate({
|
||||
offset,
|
||||
length,
|
||||
date: entity.date,
|
||||
relative: entity.relative,
|
||||
shortTime: entity.shortTime,
|
||||
longTime: entity.longTime,
|
||||
shortDate: entity.shortDate,
|
||||
longDate: entity.longDate,
|
||||
dayOfWeek: entity.dayOfWeek,
|
||||
});
|
||||
default:
|
||||
return new GramJs.MessageEntityUnknown({ offset, length });
|
||||
}
|
||||
|
||||
@ -509,7 +509,8 @@ export type ApiMessageEntityDefault = {
|
||||
type: Exclude<
|
||||
`${ApiMessageEntityTypes}`,
|
||||
`${ApiMessageEntityTypes.Pre}` | `${ApiMessageEntityTypes.TextUrl}` | `${ApiMessageEntityTypes.MentionName}` |
|
||||
`${ApiMessageEntityTypes.Blockquote}` | `${ApiMessageEntityTypes.CustomEmoji}` | `${ApiMessageEntityTypes.Timestamp}`
|
||||
`${ApiMessageEntityTypes.Blockquote}` | `${ApiMessageEntityTypes.CustomEmoji}` |
|
||||
`${ApiMessageEntityTypes.Timestamp}` | `${ApiMessageEntityTypes.FormattedDate}`
|
||||
>;
|
||||
offset: number;
|
||||
length: number;
|
||||
@ -550,6 +551,19 @@ export type ApiMessageEntityCustomEmoji = {
|
||||
documentId: string;
|
||||
};
|
||||
|
||||
export type ApiMessageEntityFormattedDate = {
|
||||
type: ApiMessageEntityTypes.FormattedDate;
|
||||
offset: number;
|
||||
length: number;
|
||||
date: number;
|
||||
relative?: true;
|
||||
shortTime?: true;
|
||||
longTime?: true;
|
||||
shortDate?: true;
|
||||
longDate?: true;
|
||||
dayOfWeek?: true;
|
||||
};
|
||||
|
||||
// Local entities
|
||||
export type ApiMessageEntityTimestamp = {
|
||||
type: ApiMessageEntityTypes.Timestamp;
|
||||
@ -559,7 +573,8 @@ export type ApiMessageEntityTimestamp = {
|
||||
};
|
||||
|
||||
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
|
||||
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp;
|
||||
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp |
|
||||
ApiMessageEntityFormattedDate;
|
||||
|
||||
export enum ApiMessageEntityTypes {
|
||||
Bold = 'MessageEntityBold',
|
||||
@ -582,6 +597,7 @@ export enum ApiMessageEntityTypes {
|
||||
CustomEmoji = 'MessageEntityCustomEmoji',
|
||||
Timestamp = 'MessageEntityTimestamp',
|
||||
QuoteFocus = 'MessageEntityQuoteFocus',
|
||||
FormattedDate = 'MessageEntityFormattedDate',
|
||||
Unknown = 'MessageEntityUnknown',
|
||||
}
|
||||
|
||||
|
||||
@ -2772,5 +2772,8 @@
|
||||
"RankEditTextOwn" = "Share your role, title or how you're known in this group. Your tag is visible to all members.";
|
||||
"RankEditText" = "Add a short tag next to {user}'s name.";
|
||||
"MenuAddCaption" = "Add Caption";
|
||||
"MenuCopyDate" = "Copy Date";
|
||||
"DateCopiedToast" = "Date copied to clipboard";
|
||||
"ReminderSetToast" = "You set up a reminder in **Saved Messages**";
|
||||
"NoForwardsRequestReject" = "Reject";
|
||||
"NoForwardsRequestAccept" = "Accept";
|
||||
|
||||
@ -1265,7 +1265,6 @@ const Composer = ({
|
||||
effectId,
|
||||
webPageMediaSize: attachmentSettings.webPageMediaSize,
|
||||
webPageUrl: hasWebPagePreview ? webPagePreview.url : undefined,
|
||||
isForwarding,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
177
src/components/common/FormattedDate.tsx
Normal file
177
src/components/common/FormattedDate.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import { type TeactNode, useMemo, useRef, useState } from '@teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import { type ApiMessageEntityFormattedDate, ApiMessageEntityTypes } from '../../api/types';
|
||||
|
||||
import { copyTextToClipboard } from '../../util/clipboard';
|
||||
import { formatDateTime, secondsToDate } from '../../util/localization/dateFormat';
|
||||
import { getServerTime } from '../../util/serverTime';
|
||||
|
||||
import useInterval from '../../hooks/schedulers/useInterval';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useSchedule from '../../hooks/useSchedule';
|
||||
|
||||
import Menu from '../ui/Menu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
|
||||
type OwnProps = {
|
||||
children?: TeactNode;
|
||||
entity: ApiMessageEntityFormattedDate;
|
||||
asPreview?: boolean;
|
||||
chatId?: string;
|
||||
messageId?: number;
|
||||
};
|
||||
|
||||
const FormattedDate = ({
|
||||
children,
|
||||
chatId,
|
||||
messageId,
|
||||
entity,
|
||||
asPreview,
|
||||
}: OwnProps) => {
|
||||
const { showNotification, openForwardMenu, forwardToSavedMessages } = getActions();
|
||||
const [cacheBreaker, setCacheBreaker] = useState(0);
|
||||
|
||||
const ref = useRef<HTMLAnchorElement>();
|
||||
const menuRef = useRef<HTMLDivElement>();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const [requestCalendar, calendar] = useSchedule(undefined, undefined, entity.date);
|
||||
|
||||
useInterval(
|
||||
() => setCacheBreaker((prev) => prev + 1),
|
||||
getUpdateInterval(Math.abs(entity.date - getServerTime())),
|
||||
);
|
||||
|
||||
const canSetReminder = Boolean(chatId && messageId);
|
||||
|
||||
const { formattedDate, canonicalDate } = useMemo(() => {
|
||||
void cacheBreaker;
|
||||
|
||||
const { type, offset, length, date, ...formatOptions } = entity;
|
||||
const canonical = formatDateTime(lang, secondsToDate(date), {
|
||||
date: 'long',
|
||||
includeYear: true,
|
||||
includeDay: true,
|
||||
time: 'long',
|
||||
});
|
||||
|
||||
if (Object.values(formatOptions).every((value) => value === undefined)) {
|
||||
return { formattedDate: undefined, canonicalDate: canonical };
|
||||
}
|
||||
|
||||
const { relative, shortTime, longTime, shortDate, longDate, dayOfWeek } = formatOptions;
|
||||
|
||||
const formatted = formatDateTime(lang, secondsToDate(date), {
|
||||
relative: relative ? 'auto' : undefined,
|
||||
time: shortTime ? 'short' : longTime ? 'long' : undefined,
|
||||
date: shortDate ? 'short' : longDate ? 'long' : undefined,
|
||||
weekday: dayOfWeek ? 'long' : undefined,
|
||||
});
|
||||
|
||||
return { formattedDate: formatted, canonicalDate: canonical };
|
||||
}, [lang, entity, cacheBreaker]);
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
contextMenuAnchor,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, asPreview);
|
||||
|
||||
const getTriggerElement = useLastCallback(() => ref.current);
|
||||
const getRootElement = useLastCallback(() => document.body);
|
||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
const handleCopy = useLastCallback(() => {
|
||||
copyTextToClipboard(canonicalDate);
|
||||
showNotification({ message: lang('DateCopiedToast') });
|
||||
});
|
||||
|
||||
const handleSetReminder = useLastCallback(() => {
|
||||
if (!chatId || !messageId) return;
|
||||
requestCalendar((scheduledAt) => {
|
||||
openForwardMenu({ fromChatId: chatId, messageIds: [messageId] });
|
||||
forwardToSavedMessages({ scheduledAt });
|
||||
showNotification({
|
||||
message: {
|
||||
key: 'ReminderSetToast',
|
||||
options: {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (asPreview) {
|
||||
return formattedDate ?? children;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
onClick={handleContextMenu}
|
||||
onContextMenu={handleContextMenu}
|
||||
className="text-entity-link"
|
||||
dir="auto"
|
||||
data-entity-type={ApiMessageEntityTypes.FormattedDate}
|
||||
data-unix={entity.date}
|
||||
data-format={formatToString(entity)}
|
||||
title={canonicalDate}
|
||||
>
|
||||
{formattedDate ?? children}
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
withPortal
|
||||
autoClose
|
||||
>
|
||||
<MenuItem icon="copy" onClick={handleCopy}>{lang('MenuCopyDate')}</MenuItem>
|
||||
{canSetReminder && (
|
||||
<MenuItem icon="unmute" onClick={handleSetReminder}>{lang('SetReminder')}</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
{calendar}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormattedDate;
|
||||
|
||||
function getUpdateInterval(diffInSeconds: number) {
|
||||
if (diffInSeconds < 60) {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
if (diffInSeconds < 60 * 60) {
|
||||
return 60000;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatToString(entity: ApiMessageEntityFormattedDate) {
|
||||
const { relative, shortTime, longTime, shortDate, longDate, dayOfWeek } = entity;
|
||||
return [
|
||||
relative && 'r',
|
||||
dayOfWeek && 'w',
|
||||
shortDate && 'd',
|
||||
longDate && 'D',
|
||||
shortTime && 't',
|
||||
longTime && 'T',
|
||||
].filter(Boolean).join('');
|
||||
}
|
||||
@ -16,6 +16,7 @@ import MentionLink from '../../middle/message/MentionLink';
|
||||
import Blockquote from '../Blockquote';
|
||||
import CodeBlock from '../code/CodeBlock';
|
||||
import CustomEmoji from '../CustomEmoji';
|
||||
import FormattedDate from '../FormattedDate';
|
||||
import SafeLink from '../SafeLink';
|
||||
import Spoiler from '../spoiler/Spoiler';
|
||||
|
||||
@ -511,6 +512,11 @@ function processEntity({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (entity.type === ApiMessageEntityTypes.FormattedDate && entity.date) { // Old entities can have missing fields
|
||||
return <FormattedDate entity={entity} asPreview>{text}</FormattedDate>;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@ -668,6 +674,16 @@ function processEntity({
|
||||
return (
|
||||
<span className="matching-text-highlight is-quote">{renderNestedMessagePart()}</span>
|
||||
);
|
||||
case ApiMessageEntityTypes.FormattedDate:
|
||||
return (
|
||||
<FormattedDate
|
||||
entity={entity}
|
||||
chatId={chatId}
|
||||
messageId={messageId}
|
||||
>
|
||||
{renderNestedMessagePart()}
|
||||
</FormattedDate>
|
||||
);
|
||||
default:
|
||||
return renderNestedMessagePart();
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
: 'Conversation.ForwardTooltip.SavedMessages.One',
|
||||
);
|
||||
|
||||
forwardToSavedMessages();
|
||||
forwardToSavedMessages({});
|
||||
showNotification({ message });
|
||||
} else {
|
||||
const chatId = recipientId;
|
||||
|
||||
@ -369,7 +369,7 @@ const Main = ({
|
||||
|
||||
loadCountryList({ langCode: lang.code });
|
||||
}
|
||||
}, [lang, isMasterTab]);
|
||||
}, [lang.code, isMasterTab]);
|
||||
|
||||
// Re-fetch cached saved emoji for `localDb`
|
||||
useEffect(() => {
|
||||
|
||||
@ -121,6 +121,7 @@ const ListItem = ({
|
||||
if (ref) {
|
||||
containerRef = ref;
|
||||
}
|
||||
const menuRef = useRef<HTMLDivElement>();
|
||||
const [isTouched, markIsTouched, unmarkIsTouched] = useFlag();
|
||||
|
||||
const {
|
||||
@ -131,10 +132,7 @@ const ListItem = ({
|
||||
|
||||
const getTriggerElement = useLastCallback(() => containerRef.current);
|
||||
const getRootElement = useLastCallback(() => containerRef.current!.closest('.custom-scroll'));
|
||||
const getMenuElement = useLastCallback(() => {
|
||||
return (withPortalForMenu ? document.querySelector('#portals') : containerRef.current)!
|
||||
.querySelector('.ListItem-context-menu .bubble');
|
||||
});
|
||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: withPortalForMenu }));
|
||||
|
||||
const handleClickEvent = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
@ -271,6 +269,7 @@ const ListItem = ({
|
||||
</ButtonElementTag>
|
||||
{contextActions && contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
|
||||
@ -2583,7 +2583,7 @@ addActionHandler('setForwardChatOrTopic', async (global, actions, payload): Prom
|
||||
});
|
||||
|
||||
addActionHandler('forwardToSavedMessages', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const { scheduledAt, tabId = getCurrentTabId() } = payload || {};
|
||||
global = updateTabState(global, {
|
||||
forwardMessages: {
|
||||
...selectTabState(global, tabId).forwardMessages,
|
||||
@ -2593,7 +2593,7 @@ addActionHandler('forwardToSavedMessages', (global, actions, payload): ActionRet
|
||||
setGlobal(global);
|
||||
|
||||
actions.exitMessageSelectMode({ tabId });
|
||||
actions.forwardMessages({ isSilent: true, tabId });
|
||||
actions.forwardMessages({ isSilent: true, scheduledAt, tabId });
|
||||
});
|
||||
|
||||
addActionHandler('forwardStory', (global, actions, payload): ActionReturnType => {
|
||||
|
||||
@ -2023,7 +2023,9 @@ export interface ActionPayloads {
|
||||
} & WithTabId;
|
||||
exitForwardMode: WithTabId | undefined;
|
||||
changeRecipient: WithTabId | undefined;
|
||||
forwardToSavedMessages: WithTabId | undefined;
|
||||
forwardToSavedMessages: {
|
||||
scheduledAt?: number;
|
||||
} & WithTabId;
|
||||
forwardStory: {
|
||||
toChatId: string;
|
||||
} & WithTabId;
|
||||
|
||||
@ -210,7 +210,7 @@ function createClasses(classesType: 'constructor' | 'request', params: Generatio
|
||||
const flagGroupSuffix = arg.flagGroup > 1 ? arg.flagGroup : '';
|
||||
const flagValue = args[`flags${flagGroupSuffix}`] & (1 << arg.flagIndex);
|
||||
if (arg.type === 'true') {
|
||||
args[argName] = Boolean(flagValue);
|
||||
args[argName] = flagValue ? true : undefined;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@ -777,7 +777,6 @@ export type SendMessageParams = {
|
||||
messagePriceInStars?: number;
|
||||
localMessage?: ApiMessage;
|
||||
forwardedLocalMessagesSlice?: ForwardedLocalMessagesSlice;
|
||||
isForwarding?: boolean;
|
||||
forwardParams?: ForwardMessagesParams;
|
||||
isStoryReply?: boolean;
|
||||
suggestedMedia?: MediaContent;
|
||||
|
||||
3
src/types/language.d.ts
vendored
3
src/types/language.d.ts
vendored
@ -2036,6 +2036,9 @@ export interface LangPair {
|
||||
'RankEditSave': undefined;
|
||||
'RankEditTextOwn': undefined;
|
||||
'MenuAddCaption': undefined;
|
||||
'MenuCopyDate': undefined;
|
||||
'DateCopiedToast': undefined;
|
||||
'ReminderSetToast': undefined;
|
||||
'NoForwardsRequestReject': undefined;
|
||||
'NoForwardsRequestAccept': undefined;
|
||||
}
|
||||
|
||||
@ -239,6 +239,10 @@ function getHourCycle(timeFormat: TimeFormat) {
|
||||
return timeFormat === '12h' ? 'h12' : 'h23';
|
||||
}
|
||||
|
||||
export function secondsToDate(seconds: number) {
|
||||
return new Date(seconds * 1000);
|
||||
}
|
||||
|
||||
function serializeRecord(record: object) {
|
||||
return Object.entries(record as Record<string, unknown>)
|
||||
.filter(([, value]) => value !== undefined)
|
||||
|
||||
@ -356,7 +356,7 @@ export function setTimeFormat(timeFormat: TimeFormat) {
|
||||
|
||||
currentTimeFormat = timeFormat;
|
||||
resetDateFormatCache();
|
||||
translationFn.timeFormat = currentTimeFormat;
|
||||
translationFn = createTranslationFn();
|
||||
scheduleCallbacks();
|
||||
}
|
||||
|
||||
|
||||
@ -198,13 +198,21 @@ function getEntityDataFromNode(
|
||||
}
|
||||
|
||||
if (type === ApiMessageEntityTypes.CustomEmoji) {
|
||||
const nodeElement = node as HTMLElement;
|
||||
const documentId = nodeElement.dataset.documentId || nodeElement.getAttribute('emoji-id');
|
||||
if (!documentId) {
|
||||
return {
|
||||
index,
|
||||
entity: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
index,
|
||||
entity: {
|
||||
type,
|
||||
offset,
|
||||
length,
|
||||
documentId: (node as HTMLImageElement).dataset.documentId!,
|
||||
documentId,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -229,6 +237,28 @@ function getEntityDataFromNode(
|
||||
};
|
||||
}
|
||||
|
||||
if (type === ApiMessageEntityTypes.FormattedDate) {
|
||||
const date = Number((node as HTMLElement).dataset.unix);
|
||||
if (Number.isNaN(date)) {
|
||||
return {
|
||||
index,
|
||||
entity: undefined,
|
||||
};
|
||||
}
|
||||
const format = (node as HTMLElement).dataset.format;
|
||||
const relative = format?.includes('r') || undefined;
|
||||
const dayOfWeek = format?.includes('w') || undefined;
|
||||
const shortDate = format?.includes('d') || undefined;
|
||||
const longDate = format?.includes('D') || undefined;
|
||||
const shortTime = format?.includes('t') || undefined;
|
||||
const longTime = format?.includes('T') || undefined;
|
||||
|
||||
return {
|
||||
index,
|
||||
entity: { type, offset, length, date, relative, dayOfWeek, shortDate, longDate, shortTime, longTime },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
entity: {
|
||||
@ -279,5 +309,13 @@ function getEntityTypeFromNode(node: ChildNode): ApiMessageEntityTypes | undefin
|
||||
}
|
||||
}
|
||||
|
||||
if (node.nodeName === 'TG-TIME') {
|
||||
return ApiMessageEntityTypes.FormattedDate;
|
||||
}
|
||||
|
||||
if (node.nodeName === 'TG-EMOJI') {
|
||||
return ApiMessageEntityTypes.CustomEmoji;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user