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));
}