Settings / General: Support time formats (#1524)

This commit is contained in:
Alexander Zinchuk 2021-11-05 21:56:39 +03:00
parent f8640762af
commit 6028e0b4ce
20 changed files with 128 additions and 38 deletions

View File

@ -365,8 +365,8 @@ const Audio: FC<OwnProps> = ({
)}
{origin === AudioOrigin.Search && renderWithTitle()}
{origin !== AudioOrigin.Search && audio && renderAudio(
lang, audio, duration, isPlaying, playProgress, bufferedProgress, seekerRef, (isDownloading || isUploading),
date, transferProgress, onDateClick ? handleDateClick : undefined,
lang, audio, duration, isPlaying, playProgress, bufferedProgress, seekerRef,
(isDownloading || isUploading), date, transferProgress, onDateClick ? handleDateClick : undefined,
)}
{origin === AudioOrigin.SharedMedia && (voice || video) && renderWithTitle()}
{origin === AudioOrigin.Inline && voice && renderVoice(voice, renderedWaveform, playProgress, isMediaUnread)}
@ -417,7 +417,9 @@ function renderAudio(
{date && (
<>
<span className="bullet">&bull;</span>
<Link className="date" onClick={handleDateClick}>{formatMediaDateTime(lang, date * 1000, true)}</Link>
<Link className="date" onClick={handleDateClick}>
{formatMediaDateTime(lang, date * 1000, true)}
</Link>
</>
)}
</div>

View File

@ -348,10 +348,10 @@ function formatSubmitLabel(lang: LangFn, date: Date) {
const today = formatDateToString(new Date(), lang.code);
if (day === today) {
return lang('Conversation.ScheduleMessage.SendToday', formatTime(date));
return lang('Conversation.ScheduleMessage.SendToday', formatTime(date, lang));
}
return lang('Conversation.ScheduleMessage.SendOn', [day, formatTime(date)]);
return lang('Conversation.ScheduleMessage.SendOn', [day, formatTime(date, lang)]);
}
export default memo(CalendarModal);

View File

@ -55,9 +55,9 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
isSavedMessages,
areMessagesLoaded,
lastSyncTime,
serverTimeOffset,
loadFullUser,
openMediaViewer,
serverTimeOffset,
}) => {
const { id: userId } = user || {};
const fullName = getUserFullName(user);

View File

@ -47,9 +47,9 @@ const ProfileInfo: FC<OwnProps & StateProps & DispatchProps> = ({
isSavedMessages,
connectionState,
animationLevel,
serverTimeOffset,
loadFullUser,
openMediaViewer,
serverTimeOffset,
}) => {
const { id: userId } = user || {};
const { id: chatId } = chat || {};
@ -232,7 +232,12 @@ export default memo(withGlobal<OwnProps>(
const { animationLevel } = global.settings.byKey;
return {
connectionState, user, chat, isSavedMessages, animationLevel, serverTimeOffset,
connectionState,
user,
chat,
isSavedMessages,
animationLevel,
serverTimeOffset,
};
},
(setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']),

View File

@ -23,7 +23,9 @@ type OwnProps = {
onMessageClick: (messageId: number, chatId: number) => void;
};
const WebLink: FC<OwnProps> = ({ message, senderTitle, onMessageClick }) => {
const WebLink: FC<OwnProps> = ({
message, senderTitle, onMessageClick,
}) => {
const lang = useLang();
let linkData: ApiWebPage | undefined = getMessageWebPage(message);

View File

@ -292,7 +292,10 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
{chat.isVerified && <VerifiedIcon />}
{isMuted && <i className="icon-muted-chat" />}
{chat.lastMessage && (
<LastMessageMeta message={chat.lastMessage} outgoingStatus={lastMessageOutgoingStatus} />
<LastMessageMeta
message={chat.lastMessage}
outgoingStatus={lastMessageOutgoingStatus}
/>
)}
</div>
<div className="subtitle">

View File

@ -4,11 +4,12 @@ import React, {
import { withGlobal } from '../../../lib/teact/teactn';
import { GlobalActions } from '../../../global/types';
import { SettingsScreens, ISettings } from '../../../types';
import { SettingsScreens, ISettings, TimeFormat } from '../../../types';
import { ApiSticker, ApiStickerSet } from '../../../api/types';
import { IS_IOS, IS_MAC_OS, IS_TOUCH_ENV } from '../../../util/environment';
import { pick } from '../../../util/iteratees';
import { setTimeFormat } from '../../../util/langProvider';
import useLang from '../../../hooks/useLang';
import useFlag from '../../../hooks/useFlag';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
@ -17,7 +18,7 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
import ListItem from '../../ui/ListItem';
import RangeSlider from '../../ui/RangeSlider';
import Checkbox from '../../ui/Checkbox';
import RadioGroup from '../../ui/RadioGroup';
import RadioGroup, { IRadioOption } from '../../ui/RadioGroup';
import SettingsStickerSet from './SettingsStickerSet';
import StickerSetModal from '../../common/StickerSetModal.async';
@ -38,7 +39,8 @@ type StateProps = Pick<ISettings, (
'shouldAutoPlayGifs' |
'shouldAutoPlayVideos' |
'shouldSuggestStickers' |
'shouldLoopStickers'
'shouldLoopStickers' |
'timeFormat'
)> & {
stickerSetIds?: string[];
stickerSetsById?: Record<string, ApiStickerSet>;
@ -54,6 +56,14 @@ const ANIMATION_LEVEL_OPTIONS = [
'Lots of Stuff',
];
const TIME_FORMAT_OPTIONS: IRadioOption[] = [{
label: '12-hour',
value: '12h',
}, {
label: '24-hour',
value: '24h',
}];
const SettingsGeneral: FC<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
@ -71,6 +81,7 @@ const SettingsGeneral: FC<OwnProps & StateProps & DispatchProps> = ({
shouldAutoPlayVideos,
shouldSuggestStickers,
shouldLoopStickers,
timeFormat,
setSettingOption,
loadStickerSets,
loadAddedStickers,
@ -121,6 +132,12 @@ const SettingsGeneral: FC<OwnProps & StateProps & DispatchProps> = ({
setSettingOption({ messageTextSize: newSize });
}, [setSettingOption]);
const handleTimeFormatChange = useCallback((value: string) => {
setTimeFormat(value as TimeFormat, () => {
setSettingOption({ timeFormat: value });
});
}, [setSettingOption]);
const handleStickerSetClick = useCallback((value: ApiSticker) => {
setSticker(value);
openModal();
@ -153,6 +170,18 @@ const SettingsGeneral: FC<OwnProps & StateProps & DispatchProps> = ({
</ListItem>
</div>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
Time Format
</h4>
<RadioGroup
name="timeformat"
options={TIME_FORMAT_OPTIONS}
selected={timeFormat}
onChange={handleTimeFormatChange}
/>
</div>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
Animation Level
@ -274,6 +303,7 @@ export default memo(withGlobal<OwnProps>(
'shouldLoopStickers',
'isSensitiveEnabled',
'canChangeSensitive',
'timeFormat',
]),
stickerSetIds: global.stickers.added.setIds,
stickerSetsById: global.stickers.setsById,

View File

@ -42,9 +42,7 @@ const HistoryCalendar: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
return {
selectedAt: global.historyCalendarSelectedAt,
};
return { selectedAt: global.historyCalendarSelectedAt };
},
(setGlobal, actions): DispatchProps => pick(actions, [
'searchMessagesByDate', 'closeHistoryCalendar',

View File

@ -34,7 +34,13 @@ type StateProps = {
type DispatchProps = Pick<GlobalActions, 'closeMediaViewer' | 'focusMessage'>;
const SenderInfo: FC<OwnProps & StateProps & DispatchProps> = ({
chatId, messageId, sender, isAvatar, message, closeMediaViewer, focusMessage,
chatId,
messageId,
sender,
isAvatar,
message,
closeMediaViewer,
focusMessage,
}) => {
const handleFocusMessage = useCallback(() => {
closeMediaViewer();
@ -62,7 +68,9 @@ const SenderInfo: FC<OwnProps & StateProps & DispatchProps> = ({
{senderTitle && renderText(senderTitle)}
</div>
<div className="date" dir="auto">
{isAvatar ? lang('lng_mediaview_profile_photo') : formatMediaDateTime(lang, message!.date * 1000, true)}
{isAvatar
? lang('lng_mediaview_profile_photo')
: formatMediaDateTime(lang, message!.date * 1000, true)}
</div>
</div>
</div>

View File

@ -1054,7 +1054,7 @@ export default memo(withGlobal<OwnProps>(
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId);
const scheduledIds = selectScheduledIds(global, chatId);
const { language } = global.settings.byKey;
const { language, shouldSuggestStickers } = global.settings.byKey;
const baseEmojiKeywords = global.emojiKeywords[BASE_EMOJI_KEYWORD_LANG];
const emojiKeywords = language !== BASE_EMOJI_KEYWORD_LANG ? global.emojiKeywords[language] : undefined;
const botKeyboardMessageId = messageWithActualBotKeyboard ? messageWithActualBotKeyboard.id : undefined;
@ -1090,7 +1090,7 @@ export default memo(withGlobal<OwnProps>(
usersById: global.users.byId,
lastSyncTime: global.lastSyncTime,
contentToBeScheduled: global.messages.contentToBeScheduled,
shouldSuggestStickers: global.settings.byKey.shouldSuggestStickers,
shouldSuggestStickers,
recentEmojis: global.recentEmojis,
baseEmojiKeywords: baseEmojiKeywords?.keywords,
emojiKeywords: emojiKeywords?.keywords,

View File

@ -38,7 +38,7 @@ const MessageMeta: FC<OwnProps> = ({
)}
<span className="message-time">
{message.isEdited && `${lang('EditedMessage')} `}
{formatTime(message.date * 1000)}
{formatTime(message.date * 1000, lang)}
</span>
{outgoingStatus && (
<MessageOutgoingStatus status={outgoingStatus} />

View File

@ -58,6 +58,11 @@
.Tab {
padding: 1rem .75rem;
span {
white-space: nowrap;
}
i {
bottom: -1rem;
}

View File

@ -126,6 +126,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
isRestricted,
lastSyncTime,
activeDownloadIds,
serverTimeOffset,
setLocalMediaSearchType,
loadMoreMembers,
searchMediaMessagesLocal,
@ -135,7 +136,6 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
focusMessage,
loadProfilePhotos,
setNewChatMembersDialogState,
serverTimeOffset,
}) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);

View File

@ -3,7 +3,7 @@ import React, { FC, useCallback, memo } from '../../lib/teact/teact';
import Radio from './Radio';
type IRadioOption = {
export type IRadioOption = {
label: string;
subLabel?: string;
value: string;

View File

@ -145,6 +145,7 @@ export const INITIAL_STATE: GlobalState = {
shouldSuggestStickers: true,
shouldLoopStickers: true,
language: 'en',
timeFormat: '24h',
},
themes: {
light: {

View File

@ -185,7 +185,9 @@ export function getAllowedAttachmentOptions(chat?: ApiChat, isChatWithBot = fals
}
export function getMessageSendingRestrictionReason(
lang: LangFn, currentUserBannedRights?: ApiChatBannedRights, defaultBannedRights?: ApiChatBannedRights,
lang: LangFn,
currentUserBannedRights?: ApiChatBannedRights,
defaultBannedRights?: ApiChatBannedRights,
) {
if (currentUserBannedRights?.sendMessages) {
const { untilDate } = currentUserBannedRights;
@ -194,7 +196,7 @@ export function getMessageSendingRestrictionReason(
'Channel.Persmission.Denied.SendMessages.Until',
lang(
'formatDateAtTime',
[formatDateToString(new Date(untilDate * 1000), lang.code), formatTime(untilDate * 1000)],
[formatDateToString(new Date(untilDate * 1000), lang.code), formatTime(untilDate * 1000, lang)],
),
)
: lang('Channel.Persmission.Denied.SendMessages.Forever');

View File

@ -128,7 +128,7 @@ export function getUserStatus(lang: LangFn, user: ApiUser, serverTimeOffset: num
}
// other
return lang('LastSeen.TodayAt', formatTime(wasOnlineDate));
return lang('LastSeen.TodayAt', formatTime(wasOnlineDate, lang));
}
// yesterday
@ -137,7 +137,7 @@ export function getUserStatus(lang: LangFn, user: ApiUser, serverTimeOffset: num
yesterday.setHours(0, 0, 0, 0);
const serverYesterday = new Date(yesterday.getTime() + serverTimeOffset * 1000);
if (wasOnlineDate > serverYesterday) {
return lang('LastSeen.YesterdayAt', formatTime(wasOnlineDate));
return lang('LastSeen.YesterdayAt', formatTime(wasOnlineDate, lang));
}
return lang('LastSeen.AtDate', formatFullDate(lang, wasOnlineDate));

View File

@ -48,6 +48,8 @@ export type LangCode = (
| 'tr' | 'uk' | 'uz'
);
export type TimeFormat = '24h' | '12h';
export interface ISettings extends NotifySettings, Record<string, any> {
theme: ThemeKey;
shouldUseSystemTheme: boolean;
@ -67,6 +69,7 @@ export interface ISettings extends NotifySettings, Record<string, any> {
language: LangCode;
isSensitiveEnabled?: boolean;
canChangeSensitive?: boolean;
timeFormat: TimeFormat;
}
export interface ApiPrivacySettings {

View File

@ -10,7 +10,7 @@ const MONTHS_FULL_LOWERCASE = MONTHS_FULL.map((month) => month.toLowerCase());
const MIN_SEARCH_YEAR = 2015;
const MAX_DAY_IN_MONTH = 31;
const MAX_MONTH_IN_YEAR = 12;
export const MILISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
export const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
export function getDayStart(datetime: number | Date) {
const date = new Date(datetime);
@ -31,12 +31,17 @@ function toIsoString(date: Date) {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
}
export function formatTime(datetime: number | Date) {
export function formatTime(datetime: number | Date, lang: LangFn) {
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const timeFormat = lang.timeFormat || '24h';
return `${hours}:${minutes}`;
const time = date.toLocaleTimeString(lang.code, {
hour12: timeFormat === '12h',
hour: timeFormat === '12h' ? 'numeric' : '2-digit',
minute: '2-digit',
});
return timeFormat === '12h' ? time.replace(/^0:/, '12:') : time;
}
export function formatPastTimeShort(lang: LangFn, datetime: number | Date) {
@ -44,7 +49,7 @@ export function formatPastTimeShort(lang: LangFn, datetime: number | Date) {
const today = getDayStart(new Date());
if (date >= today) {
return formatTime(date);
return formatTime(date, lang);
}
const weekAgo = new Date(today);
@ -132,10 +137,14 @@ function formatDate(lang: LangFn, date: Date, format: string) {
.replace('yyyy', String(date.getFullYear()));
}
export function formatMediaDateTime(lang: LangFn, datetime: number | Date, isUpperFirst?: boolean) {
export function formatMediaDateTime(
lang: LangFn,
datetime: number | Date,
isUpperFirst?: boolean,
) {
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
return `${formatHumanDate(lang, date, true, undefined, isUpperFirst)}, ${formatTime(date)}`;
return `${formatHumanDate(lang, date, true, undefined, isUpperFirst)}, ${formatTime(date, lang)}`;
}
export function formatMediaDuration(duration: number, maxValue?: number) {
@ -231,7 +240,7 @@ export function parseDateString(query = ''): string | undefined {
}
export function timestampPlusDay(timestamp: number) {
return timestamp + MILISECONDS_IN_DAY / 1000;
return timestamp + MILLISECONDS_IN_DAY / 1000;
}
function lowerFirst(str: string) {

View File

@ -1,7 +1,7 @@
import { getGlobal } from '../lib/teact/teactn';
import { ApiLangPack, ApiLangString } from '../api/types';
import { LangCode } from '../types';
import { LangCode, TimeFormat } from '../types';
import {
DEFAULT_LANG_CODE, DEFAULT_LANG_PACK, LANG_CACHE_NAME, LANG_PACKS,
@ -16,6 +16,7 @@ interface LangFn {
isRtl?: boolean;
code?: LangCode;
timeFormat?: TimeFormat;
}
const SUBSTITUTION_REGEX = /%\d?\$?[sdf@]/g;
@ -57,6 +58,7 @@ const {
export { addCallback, removeCallback };
let currentLangCode: string | undefined;
let currentTimeFormat: TimeFormat | undefined;
export const getTranslation: LangFn = (key: string, value?: any, format?: 'i') => {
if (value !== undefined) {
@ -124,10 +126,30 @@ export async function setLanguage(langCode: LangCode, callback?: NoneToVoidFunct
langPack = newLangPack;
document.documentElement.lang = langCode;
const { languages } = getGlobal().settings.byKey;
const { languages, timeFormat } = getGlobal().settings.byKey;
const langInfo = languages?.find((l) => l.langCode === langCode);
getTranslation.isRtl = Boolean(langInfo?.rtl);
getTranslation.code = langCode;
getTranslation.timeFormat = timeFormat;
if (callback) {
callback();
}
runCallbacks();
}
export function setTimeFormat(timeFormat: TimeFormat, callback?: NoneToVoidFunction) {
if (timeFormat && timeFormat === currentTimeFormat) {
if (callback) {
callback();
}
return;
}
currentTimeFormat = timeFormat;
getTranslation.timeFormat = timeFormat;
if (callback) {
callback();