diff --git a/src/components/chat/messageRender.ts b/src/components/chat/messageRender.ts index 020ed1dd..67bb9dd8 100644 --- a/src/components/chat/messageRender.ts +++ b/src/components/chat/messageRender.ts @@ -4,9 +4,10 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { getFullDate } from "../../helpers/date"; +import { formatTime, getFullDate } from "../../helpers/date"; import { formatNumber } from "../../helpers/number"; -import { i18n } from "../../lib/langPack"; +import { Message } from "../../layer"; +import { i18n, _i18n } from "../../lib/langPack"; import RichTextProcessor from "../../lib/richtextprocessor"; import { LazyLoadQueueIntersector } from "../lazyLoadQueue"; import PeerTitle from "../peerTitle"; @@ -19,26 +20,47 @@ export namespace MessageRender { }; */ - export const setTime = (chat: Chat, message: any, bubble: HTMLElement, bubbleContainer: HTMLElement, messageDiv: HTMLElement) => { + export const setTime = (chat: Chat, message: Message.message, bubble: HTMLElement, bubbleContainer: HTMLElement, messageDiv: HTMLElement) => { const date = new Date(message.date * 1000); - let time = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2); + const args: (HTMLElement | string)[] = []; + let time = formatTime(date); if(message.views) { const postAuthor = message.post_author || message.fwd_from?.post_author; bubble.classList.add('channel-post'); - time = '' + formatNumber(message.views, 1) + ' ' + (postAuthor ? RichTextProcessor.wrapEmojiText(postAuthor) + ', ' : '') + time; + + const postViewsSpan = document.createElement('span'); + postViewsSpan.classList.add('post-views'); + postViewsSpan.innerText = formatNumber(message.views, 1); + + const channelViews = document.createElement('i'); + channelViews.classList.add('tgico-channelviews', 'time-icon'); + + args.push(postViewsSpan, ' ', channelViews); + if(postAuthor) { + args.push(RichTextProcessor.wrapEmojiText(postAuthor), ', '); + } } if(message.edit_date && chat.type !== 'scheduled' && !message.pFlags.edit_hide) { bubble.classList.add('is-edited'); - time = 'edited ' + time; + + const edited = document.createElement('i'); + edited.classList.add('edited'); + _i18n(edited, 'EditedMessage'); + args.unshift(edited); } if(chat.type !== 'pinned' && message.pFlags.pinned) { bubble.classList.add('is-pinned'); - time = '' + time; + + const i = document.createElement('i'); + i.classList.add('tgico-pinnedchat', 'time-icon'); + args.unshift(i); } + + args.push(time); const title = getFullDate(date) + (message.edit_date ? `\nEdited: ${getFullDate(new Date(message.edit_date * 1000))}` : '') @@ -47,7 +69,17 @@ export namespace MessageRender { const timeSpan = document.createElement('span'); timeSpan.classList.add('time', 'tgico'); timeSpan.title = title; - timeSpan.innerHTML = `${time}
${time}
`; + timeSpan.append(...args); + + const inner = document.createElement('div'); + inner.classList.add('inner', 'tgico'); + inner.title = title; + + const clonedArgs = args.slice(0, -1).map(a => a instanceof HTMLElement ? a.cloneNode(true) : a); + clonedArgs.push(formatTime(date)); // clone time + inner.append(...clonedArgs); + + timeSpan.append(inner); messageDiv.append(timeSpan); @@ -57,7 +89,7 @@ export namespace MessageRender { export const renderReplies = ({bubble, bubbleContainer, message, messageDiv, loadPromises, lazyLoadQueue}: { bubble: HTMLElement, bubbleContainer: HTMLElement, - message: any, + message: Message.message, messageDiv: HTMLElement, loadPromises?: Promise[], lazyLoadQueue?: LazyLoadQueueIntersector @@ -77,7 +109,7 @@ export namespace MessageRender { chat: Chat, bubble: HTMLElement, bubbleContainer?: HTMLElement, - message: any + message: Message.message }) => { const isReplacing = !bubbleContainer; if(isReplacing) { diff --git a/src/components/sidebarLeft/tabs/generalSettings.ts b/src/components/sidebarLeft/tabs/generalSettings.ts index 0249db11..b7aec737 100644 --- a/src/components/sidebarLeft/tabs/generalSettings.ts +++ b/src/components/sidebarLeft/tabs/generalSettings.ts @@ -4,13 +4,12 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { SliderSuperTab } from "../../slider" import { generateSection } from ".."; import RangeSelector from "../../rangeSelector"; import Button from "../../button"; import CheckboxField from "../../checkboxField"; import RadioField from "../../radioField"; -import appStateManager from "../../../lib/appManagers/appStateManager"; +import appStateManager, { State } from "../../../lib/appManagers/appStateManager"; import rootScope from "../../../lib/rootScope"; import { IS_APPLE } from "../../../environment/userAgent"; import Row from "../../row"; @@ -24,6 +23,8 @@ import RichTextProcessor from "../../../lib/richtextprocessor"; import { wrapStickerSetThumb } from "../../wrappers"; import LazyLoadQueue from "../../lazyLoadQueue"; import PopupStickers from "../../popups/stickers"; +import eachMinute from "../../../helpers/eachMinute"; +import { SliderSuperTabEventable } from "../../sliderTab"; export class RangeSettingSelector { public container: HTMLDivElement; @@ -70,7 +71,7 @@ export class RangeSettingSelector { } } -export default class AppGeneralSettingsTab extends SliderSuperTab { +export default class AppGeneralSettingsTab extends SliderSuperTabEventable { init() { this.container.classList.add('general-settings-container'); this.setTitle('General'); @@ -106,21 +107,24 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { const form = document.createElement('form'); + const name = 'send-shortcut'; + const stateKey = 'settings.sendShortcut'; + const enterRow = new Row({ radioField: new RadioField({ langKey: 'General.SendShortcut.Enter', - name: 'send-shortcut', + name, value: 'enter', - stateKey: 'settings.sendShortcut' + stateKey }), subtitleLangKey: 'General.SendShortcut.NewLine.ShiftEnter' }); const ctrlEnterRow = new Row({ radioField: new RadioField({ - name: 'send-shortcut', + name, value: 'ctrlEnter', - stateKey: 'settings.sendShortcut' + stateKey }), subtitleLangKey: 'General.SendShortcut.NewLine.Enter' }); @@ -130,6 +134,51 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { container.append(form); } + { + const container = section('General.TimeFormat'); + + const form = document.createElement('form'); + + const name = 'time-format'; + const stateKey = 'settings.timeFormat'; + + const formats: [State['settings']['timeFormat'], LangPackKey][] = [ + ['h12', 'General.TimeFormat.h12'], + ['h23', 'General.TimeFormat.h23'] + ]; + + const rows = formats.map(([format, langPackKey]) => { + const row = new Row({ + radioField: new RadioField({ + langKey: langPackKey, + name, + value: format, + stateKey + }) + }); + + return row; + }); + + const cancel = eachMinute(() => { + const date = new Date(); + + formats.forEach(([format], idx) => { + const str = date.toLocaleTimeString("en-us-u-hc-" + format, { + hour: '2-digit', + minute: '2-digit' + }); + + rows[idx].subtitle.textContent = str; + }); + }); + + this.eventListener.addEventListener('destroy', cancel); + + form.append(...rows.map(row => row.container)); + container.append(form); + } + { const container = section('AutoDownloadMedia'); //container.classList.add('sidebar-left-section-disabled'); diff --git a/src/config/app.ts b/src/config/app.ts index 90f1d76d..3d860576 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -19,7 +19,7 @@ const App = { version: process.env.VERSION, versionFull: process.env.VERSION_FULL, build: +process.env.BUILD, - langPackVersion: '0.3.5', + langPackVersion: '0.3.6', langPack: 'macos', langPackCode: 'en', domains: [MAIN_DOMAIN] as string[], diff --git a/src/helpers/eachMinute.ts b/src/helpers/eachMinute.ts new file mode 100644 index 00000000..84f5449c --- /dev/null +++ b/src/helpers/eachMinute.ts @@ -0,0 +1,31 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import ctx from "../environment/ctx"; +import noop from "./noop"; + +// It's better to use timeout instead of interval, because interval can be corrupted +export default function eachMinute(callback: () => any, runFirst = true) { + const cancel = () => { + clearTimeout(timeout); + }; + + // replace callback to run noop and restore after + const _callback = callback; + if(!runFirst) { + callback = noop; + } + + let timeout: number; + (function run() { + callback(); + timeout = ctx.setTimeout(run, (60 - new Date().getSeconds()) * 1000); + })(); + + callback = _callback; + + return cancel; +} diff --git a/src/index.ts b/src/index.ts index 404754e9..d548778d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -230,6 +230,8 @@ console.timeEnd('get storage1'); */ //console.log('got auth:', auth); //console.timeEnd('get storage'); + I18n.default.setTimeFormat(state.settings.timeFormat); + rootScope.default.setThemeListener(); if(langPack.appVersion !== App.langPackVersion) { diff --git a/src/lang.ts b/src/lang.ts index 88e85dc9..14519c40 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -68,6 +68,9 @@ const lang = { "General.SendShortcut.NewLine.ShiftEnter": "New line by Shift + Enter", "General.SendShortcut.NewLine.Enter": "New line by Enter", "General.AutoplayMedia": "Auto-Play Media", + "General.TimeFormat": "Time Format", + "General.TimeFormat.h12": "12-hour", + "General.TimeFormat.h23": "24-hour", "ChatBackground.UploadWallpaper": "Upload Wallpaper", "ChatBackground.Blur": "Blur Wallpaper Image", "Notifications.Sound": "Notification Sound", @@ -592,6 +595,7 @@ const lang = { "one_value": "%1$d online", "other_value": "%1$d online" }, + "EditedMessage": "edited", // * macos "AccountSettings.Filters": "Chat Folders", diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index f750207a..168c37c9 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -42,7 +42,7 @@ import { MOUNT_CLASS_TO } from '../../config/debug'; import appNavigationController from '../../components/appNavigationController'; import appNotificationsManager from './appNotificationsManager'; import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search'; -import { i18n, join, LangPackKey } from '../langPack'; +import I18n, { i18n, join, LangPackKey } from '../langPack'; import { ChatInvite, Dialog, SendMessageAction } from '../../layer'; import { hslaStringToHex } from '../../helpers/color'; import { copy, getObjectKeysAndSort } from '../../helpers/object'; @@ -843,6 +843,8 @@ export class AppImManager { for(const chat of this.chats) { chat.setAutoDownloadMedia(); } + + I18n.setTimeFormat(rootScope.settings.timeFormat); }; // * не могу использовать тут TransitionSlider, так как мне нужен отрисованный блок рядом diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index 71fb5015..892c4841 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -97,6 +97,7 @@ export type State = { sound: boolean }, nightTheme?: boolean, // ! DEPRECATED + timeFormat: 'h12' | 'h23' }, keepSigned: boolean, chatContextMenuHintWasShown: boolean, @@ -162,7 +163,8 @@ export const STATE_INIT: State = { theme: 'system', notifications: { sound: false - } + }, + timeFormat: new Date().toLocaleString().match(/\s(AM|PM)/) ? 'h12' : 'h23' }, keepSigned: true, chatContextMenuHintWasShown: false, diff --git a/src/lib/langPack.ts b/src/lib/langPack.ts index d1447f9d..02d57855 100644 --- a/src/lib/langPack.ts +++ b/src/lib/langPack.ts @@ -9,6 +9,7 @@ import { safeAssign } from "../helpers/object"; import { capitalizeFirstLetter } from "../helpers/string"; import type lang from "../lang"; import type langSign from "../langSign"; +import type { State } from "./appManagers/appStateManager"; import { HelpCountriesList, HelpCountry, LangPackDifference, LangPackString } from "../layer"; import apiManager from "./mtproto/mtprotoworker"; import stateStorage from "./stateStorage"; @@ -74,6 +75,7 @@ namespace I18n { export let lastRequestedLangCode: string; export let lastAppliedLangCode: string; export let requestedServerLanguage = false; + export let timeFormat: State['settings']['timeFormat']; export function getCacheLangPack(): Promise { if(cacheLangPackPromise) return cacheLangPackPromise; return cacheLangPackPromise = Promise.all([ @@ -99,6 +101,22 @@ namespace I18n { }); } + export function setTimeFormat(format: State['settings']['timeFormat']) { + const haveToUpdate = !!timeFormat && timeFormat !== format; + timeFormat = format; + + if(haveToUpdate) { + const elements = Array.from(document.querySelectorAll(`.i18n`)) as HTMLElement[]; + elements.forEach(element => { + const instance = weakMap.get(element); + + if(instance instanceof IntlDateElement) { + instance.update(); + } + }); + } + } + export function loadLocalLangPack() { const defaultCode = App.langPackCode; lastRequestedLangCode = defaultCode; @@ -422,7 +440,7 @@ namespace I18n { //var options = { month: 'long', day: 'numeric' }; // * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle#adding_an_hour_cycle_via_the_locale_string - const dateTimeFormat = new Intl.DateTimeFormat(lastRequestedLangCode + '-u-hc-h23', this.options); + const dateTimeFormat = new Intl.DateTimeFormat(lastRequestedLangCode + '-u-hc-' + timeFormat, this.options); (this.element as any)[this.property] = capitalizeFirstLetter(dateTimeFormat.format(this.date)); }