From 6028e0b4cebbfd9166270ee35c9082b3eca1a3bc Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 5 Nov 2021 21:56:39 +0300 Subject: [PATCH] Settings / General: Support time formats (#1524) --- src/components/common/Audio.tsx | 8 +++-- src/components/common/CalendarModal.tsx | 4 +-- src/components/common/PrivateChatInfo.tsx | 2 +- src/components/common/ProfileInfo.tsx | 9 +++-- src/components/common/WebLink.tsx | 4 ++- src/components/left/main/Chat.tsx | 5 ++- .../left/settings/SettingsGeneral.tsx | 36 +++++++++++++++++-- src/components/main/HistoryCalendar.tsx | 4 +-- src/components/mediaViewer/SenderInfo.tsx | 12 +++++-- src/components/middle/composer/Composer.tsx | 4 +-- src/components/middle/message/MessageMeta.tsx | 2 +- src/components/right/Profile.scss | 5 +++ src/components/right/Profile.tsx | 2 +- src/components/ui/RadioGroup.tsx | 2 +- src/global/initial.ts | 1 + src/modules/helpers/chats.ts | 6 ++-- src/modules/helpers/users.ts | 4 +-- src/types/index.ts | 3 ++ src/util/dateFormat.ts | 27 +++++++++----- src/util/langProvider.ts | 26 ++++++++++++-- 20 files changed, 128 insertions(+), 38 deletions(-) diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index 4007845d1..4bcb704ec 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -365,8 +365,8 @@ const Audio: FC = ({ )} {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 && ( <> - {formatMediaDateTime(lang, date * 1000, true)} + + {formatMediaDateTime(lang, date * 1000, true)} + )} diff --git a/src/components/common/CalendarModal.tsx b/src/components/common/CalendarModal.tsx index e76b2aa9d..cefda9602 100644 --- a/src/components/common/CalendarModal.tsx +++ b/src/components/common/CalendarModal.tsx @@ -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); diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index 878032f58..9c6527f72 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -55,9 +55,9 @@ const PrivateChatInfo: FC = ({ isSavedMessages, areMessagesLoaded, lastSyncTime, + serverTimeOffset, loadFullUser, openMediaViewer, - serverTimeOffset, }) => { const { id: userId } = user || {}; const fullName = getUserFullName(user); diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 1d512e6bd..ea0e31cdc 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -47,9 +47,9 @@ const ProfileInfo: FC = ({ isSavedMessages, connectionState, animationLevel, + serverTimeOffset, loadFullUser, openMediaViewer, - serverTimeOffset, }) => { const { id: userId } = user || {}; const { id: chatId } = chat || {}; @@ -232,7 +232,12 @@ export default memo(withGlobal( 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']), diff --git a/src/components/common/WebLink.tsx b/src/components/common/WebLink.tsx index e7a8ceaee..0b88dde13 100644 --- a/src/components/common/WebLink.tsx +++ b/src/components/common/WebLink.tsx @@ -23,7 +23,9 @@ type OwnProps = { onMessageClick: (messageId: number, chatId: number) => void; }; -const WebLink: FC = ({ message, senderTitle, onMessageClick }) => { +const WebLink: FC = ({ + message, senderTitle, onMessageClick, +}) => { const lang = useLang(); let linkData: ApiWebPage | undefined = getMessageWebPage(message); diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 51348f0eb..890d3d196 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -292,7 +292,10 @@ const Chat: FC = ({ {chat.isVerified && } {isMuted && } {chat.lastMessage && ( - + )}
diff --git a/src/components/left/settings/SettingsGeneral.tsx b/src/components/left/settings/SettingsGeneral.tsx index 4af383ecd..8e41630be 100644 --- a/src/components/left/settings/SettingsGeneral.tsx +++ b/src/components/left/settings/SettingsGeneral.tsx @@ -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 & { stickerSetIds?: string[]; stickerSetsById?: Record; @@ -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 = ({ isActive, onScreenSelect, @@ -71,6 +81,7 @@ const SettingsGeneral: FC = ({ shouldAutoPlayVideos, shouldSuggestStickers, shouldLoopStickers, + timeFormat, setSettingOption, loadStickerSets, loadAddedStickers, @@ -121,6 +132,12 @@ const SettingsGeneral: FC = ({ 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 = ({
+
+

+ Time Format +

+ +
+

Animation Level @@ -274,6 +303,7 @@ export default memo(withGlobal( 'shouldLoopStickers', 'isSensitiveEnabled', 'canChangeSensitive', + 'timeFormat', ]), stickerSetIds: global.stickers.added.setIds, stickerSetsById: global.stickers.setsById, diff --git a/src/components/main/HistoryCalendar.tsx b/src/components/main/HistoryCalendar.tsx index 1c44b58d9..1927453ae 100644 --- a/src/components/main/HistoryCalendar.tsx +++ b/src/components/main/HistoryCalendar.tsx @@ -42,9 +42,7 @@ const HistoryCalendar: FC = ({ export default memo(withGlobal( (global): StateProps => { - return { - selectedAt: global.historyCalendarSelectedAt, - }; + return { selectedAt: global.historyCalendarSelectedAt }; }, (setGlobal, actions): DispatchProps => pick(actions, [ 'searchMessagesByDate', 'closeHistoryCalendar', diff --git a/src/components/mediaViewer/SenderInfo.tsx b/src/components/mediaViewer/SenderInfo.tsx index f9ce24209..0db8ac8fd 100644 --- a/src/components/mediaViewer/SenderInfo.tsx +++ b/src/components/mediaViewer/SenderInfo.tsx @@ -34,7 +34,13 @@ type StateProps = { type DispatchProps = Pick; const SenderInfo: FC = ({ - 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 = ({ {senderTitle && renderText(senderTitle)}

- {isAvatar ? lang('lng_mediaview_profile_photo') : formatMediaDateTime(lang, message!.date * 1000, true)} + {isAvatar + ? lang('lng_mediaview_profile_photo') + : formatMediaDateTime(lang, message!.date * 1000, true)}
diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index f0879bf0a..27db8bd40 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -1054,7 +1054,7 @@ export default memo(withGlobal( 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( 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, diff --git a/src/components/middle/message/MessageMeta.tsx b/src/components/middle/message/MessageMeta.tsx index f8375cdc7..352ba842e 100644 --- a/src/components/middle/message/MessageMeta.tsx +++ b/src/components/middle/message/MessageMeta.tsx @@ -38,7 +38,7 @@ const MessageMeta: FC = ({ )} {message.isEdited && `${lang('EditedMessage')} `} - {formatTime(message.date * 1000)} + {formatTime(message.date * 1000, lang)} {outgoingStatus && ( diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss index 99ca6ce22..739b795a0 100644 --- a/src/components/right/Profile.scss +++ b/src/components/right/Profile.scss @@ -58,6 +58,11 @@ .Tab { padding: 1rem .75rem; + + span { + white-space: nowrap; + } + i { bottom: -1rem; } diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index 74fa97449..bd00dc9b5 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -126,6 +126,7 @@ const Profile: FC = ({ isRestricted, lastSyncTime, activeDownloadIds, + serverTimeOffset, setLocalMediaSearchType, loadMoreMembers, searchMediaMessagesLocal, @@ -135,7 +136,6 @@ const Profile: FC = ({ focusMessage, loadProfilePhotos, setNewChatMembersDialogState, - serverTimeOffset, }) => { // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); diff --git a/src/components/ui/RadioGroup.tsx b/src/components/ui/RadioGroup.tsx index 49754bc10..4a2136ec8 100644 --- a/src/components/ui/RadioGroup.tsx +++ b/src/components/ui/RadioGroup.tsx @@ -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; diff --git a/src/global/initial.ts b/src/global/initial.ts index b20885ca0..75d51468b 100644 --- a/src/global/initial.ts +++ b/src/global/initial.ts @@ -145,6 +145,7 @@ export const INITIAL_STATE: GlobalState = { shouldSuggestStickers: true, shouldLoopStickers: true, language: 'en', + timeFormat: '24h', }, themes: { light: { diff --git a/src/modules/helpers/chats.ts b/src/modules/helpers/chats.ts index 997b08393..6deaed6ca 100644 --- a/src/modules/helpers/chats.ts +++ b/src/modules/helpers/chats.ts @@ -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'); diff --git a/src/modules/helpers/users.ts b/src/modules/helpers/users.ts index 0d315a6fa..e5ef111b2 100644 --- a/src/modules/helpers/users.ts +++ b/src/modules/helpers/users.ts @@ -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)); diff --git a/src/types/index.ts b/src/types/index.ts index 687006141..c80e08944 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -48,6 +48,8 @@ export type LangCode = ( | 'tr' | 'uk' | 'uz' ); +export type TimeFormat = '24h' | '12h'; + export interface ISettings extends NotifySettings, Record { theme: ThemeKey; shouldUseSystemTheme: boolean; @@ -67,6 +69,7 @@ export interface ISettings extends NotifySettings, Record { language: LangCode; isSensitiveEnabled?: boolean; canChangeSensitive?: boolean; + timeFormat: TimeFormat; } export interface ApiPrivacySettings { diff --git a/src/util/dateFormat.ts b/src/util/dateFormat.ts index a9e2259be..0ee868dc2 100644 --- a/src/util/dateFormat.ts +++ b/src/util/dateFormat.ts @@ -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) { diff --git a/src/util/langProvider.ts b/src/util/langProvider.ts index 30572cad0..4941b9dd0 100644 --- a/src/util/langProvider.ts +++ b/src/util/langProvider.ts @@ -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();