Settings / General: Support time formats (#1524)
This commit is contained in:
parent
f8640762af
commit
6028e0b4ce
@ -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">•</span>
|
||||
<Link className="date" onClick={handleDateClick}>{formatMediaDateTime(lang, date * 1000, true)}</Link>
|
||||
<Link className="date" onClick={handleDateClick}>
|
||||
{formatMediaDateTime(lang, date * 1000, true)}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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']),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -58,6 +58,11 @@
|
||||
|
||||
.Tab {
|
||||
padding: 1rem .75rem;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
i {
|
||||
bottom: -1rem;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -145,6 +145,7 @@ export const INITIAL_STATE: GlobalState = {
|
||||
shouldSuggestStickers: true,
|
||||
shouldLoopStickers: true,
|
||||
language: 'en',
|
||||
timeFormat: '24h',
|
||||
},
|
||||
themes: {
|
||||
light: {
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user