diff --git a/src/components/inputField.ts b/src/components/inputField.ts index dc454d37..d264d2d8 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -163,10 +163,12 @@ class InputField { //this.onLengthChange && this.onLengthChange(inputLength, isError); if(isError || diff <= showLengthOn) { - labelEl.innerText = label + ` (${maxLength - inputLength})`; + labelEl.innerHTML = ''; + labelEl.append(i18n(label), ` (${maxLength - inputLength})`); if(!showingLength) showingLength = true; } else if((wasError && !isError) || showingLength) { - labelEl.innerText = label; + labelEl.innerHTML = ''; + labelEl.append(i18n(label)); showingLength = false; } }; @@ -244,7 +246,7 @@ class InputField { this.input.classList.toggle('valid', !!(state & InputState.Valid)); } - public setError(label?: string) { + public setError(label?: LangPackKey) { this.setState(InputState.Error, label); } } diff --git a/src/components/popups/confirmAction.ts b/src/components/popups/confirmAction.ts index 2e142dec..5c645132 100644 --- a/src/components/popups/confirmAction.ts +++ b/src/components/popups/confirmAction.ts @@ -1,17 +1,18 @@ import PopupElement, { addCancelButton, PopupButton, PopupOptions } from "."; +import { LangPackKey, _i18n } from "../../lib/langPack"; export default class PopupConfirmAction extends PopupElement { - constructor(className: string, buttons: PopupButton[], options: PopupOptions & Partial<{title: string, text: string}> = {}) { + constructor(className: string, buttons: PopupButton[], options: PopupOptions & {title: LangPackKey, text: LangPackKey}) { super('popup-peer popup-confirm-action ' + className, addCancelButton(buttons), { overlayClosable: true, ...options }); - this.title.innerHTML = options.title || 'Warning'; + _i18n(this.title, options.title); const p = document.createElement('p'); p.classList.add('popup-description'); - p.innerHTML = options.text; + _i18n(p, options.text); this.container.insertBefore(p, this.header.nextElementSibling); } diff --git a/src/components/popups/index.ts b/src/components/popups/index.ts index 1577706e..ba7705d1 100644 --- a/src/components/popups/index.ts +++ b/src/components/popups/index.ts @@ -3,10 +3,13 @@ import { blurActiveElement, findUpClassName } from "../../helpers/dom"; import { ripple } from "../ripple"; import animationIntersector from "../animationIntersector"; import appNavigationController, { NavigationItem } from "../appNavigationController"; +import { i18n, LangPackKey } from "../../lib/langPack"; export type PopupButton = { - text: string, + text?: string, callback?: () => void, + langKey?: LangPackKey, + langArgs?: any[], isDanger?: true, isCancel?: true }; @@ -89,7 +92,13 @@ export default class PopupElement { const buttonsElements = buttons.map(b => { const button = document.createElement('button'); button.className = 'btn' + (b.isDanger ? ' danger' : ' primary'); - button.innerHTML = b.text; + + if(b.text) { + button.innerHTML = b.text; + } else { + button.append(i18n(b.langKey, b.langArgs)); + } + ripple(button); if(b.callback) { @@ -157,7 +166,7 @@ export const addCancelButton = (buttons: PopupButton[]) => { const button = buttons.find(b => b.isCancel); if(!button) { buttons.push({ - text: 'CANCEL', + langKey: 'Cancel', isCancel: true }); } diff --git a/src/components/popups/pickUser.ts b/src/components/popups/pickUser.ts index e7edad2e..d35c2a9b 100644 --- a/src/components/popups/pickUser.ts +++ b/src/components/popups/pickUser.ts @@ -21,16 +21,17 @@ export default class PopupPickUser extends PopupElement { appendTo: this.body, onChange: async() => { const peerId = this.selector.getSelected()[0]; - this.btnClose.click(); - + this.selector = null; - + if(options.onSelect) { const res = options.onSelect(peerId); if(res instanceof Promise) { await res; } } + + this.hide(); }, peerType: options.peerTypes, onFirstRender: () => { diff --git a/src/components/privacySection.ts b/src/components/privacySection.ts index a04e924f..d3a76780 100644 --- a/src/components/privacySection.ts +++ b/src/components/privacySection.ts @@ -2,6 +2,7 @@ import { randomLong } from "../helpers/random"; import { InputPrivacyKey, InputPrivacyRule } from "../layer"; import appPrivacyManager, { PrivacyType } from "../lib/appManagers/appPrivacyManager"; import appUsersManager from "../lib/appManagers/appUsersManager"; +import { i18n, join, LangPackKey, _i18n } from "../lib/langPack"; import RadioField from "./radioField"; import Row, { RadioFormFromRows } from "./row"; import Scrollable from "./scrollable"; @@ -9,15 +10,16 @@ import { SettingSection, generateSection } from "./sidebarLeft"; import AppAddMembersTab from "./sidebarLeft/tabs/addMembers"; import { SliderSuperTabEventable } from "./sliderTab"; +type PrivacySectionStr = LangPackKey | ''; export default class PrivacySection { public radioRows: Map; public radioSection: SettingSection; public exceptions: Map; public peerIds: { @@ -28,32 +30,32 @@ export default class PrivacySection { constructor(public options: { tab: SliderSuperTabEventable, - title: string, + title: LangPackKey, inputKey: InputPrivacyKey['_'], - captions?: [string, string, string], + captions?: [PrivacySectionStr, PrivacySectionStr, PrivacySectionStr], appendTo?: Scrollable, noExceptions?: boolean, onRadioChange?: (value: number) => any, skipTypes?: PrivacyType[], - exceptionTexts?: [string, string] + exceptionTexts?: [LangPackKey, LangPackKey] }) { if(options.captions) { options.captions.reverse(); } - this.radioSection = new SettingSection({name: options.title, caption: ' '}); + this.radioSection = new SettingSection({name: options.title, caption: true}); this.radioRows = new Map(); - let r = [{ + let r: Array<{type: PrivacyType, langKey: LangPackKey}> = [{ type: PrivacyType.Everybody, - text: 'Everybody' + langKey: 'PrivacySettingsController.Everbody' }, { type: PrivacyType.Contacts, - text: 'My Contacts' + langKey: 'PrivacySettingsController.MyContacts' }, { type: PrivacyType.Nobody, - text: 'Nobody' + langKey: 'PrivacySettingsController.Nobody' }]; if(options.skipTypes) { @@ -61,10 +63,10 @@ export default class PrivacySection { } const random = randomLong(); - r.forEach(({type, text}) => { + r.forEach(({type, langKey}) => { const row = new Row({ radioField: new RadioField({ - text, + langKey, name: random, value: '' + type }) @@ -81,26 +83,26 @@ export default class PrivacySection { } if(!options.noExceptions) { - const container = generateSection(options.appendTo, 'Exceptions', 'You can add users or entire groups as exceptions that will override settings above.'); + const container = generateSection(options.appendTo, 'PrivacyExceptions', 'PrivacySettingsController.PeerInfo'); this.exceptions = new Map([[ 'disallow', { - title: options.exceptionTexts[0], + titleLangKey: options.exceptionTexts[0], key: 'disallow', row: null, icon: 'deleteuser', - subtitle: 'Add Users', + subtitleLangKey: 'PrivacySettingsController.AddUsers', clickable: true } ], [ 'allow', { - title: options.exceptionTexts[1], + titleLangKey: options.exceptionTexts[1], key: 'allow', row: null, icon: 'adduser', - subtitle: 'Add Users', + subtitleLangKey: 'PrivacySettingsController.AddUsers', clickable: true } ]]); @@ -114,12 +116,13 @@ export default class PrivacySection { new AppAddMembersTab(options.tab.slider).open({ type: 'privacy', skippable: true, - title: exception.title, + title: exception.titleLangKey, placeholder: 'Add Users or Groups...', takeOut: (newPeerIds) => { _peerIds.length = 0; _peerIds.push(...newPeerIds); - exception.row.subtitle.innerHTML = this.generateStr(this.splitPeersByType(newPeerIds)); + exception.row.subtitle.innerHTML = ''; + exception.row.subtitle.append(...this.generateStr(this.splitPeersByType(newPeerIds))); }, selectedPeerIds: _peerIds }); @@ -146,7 +149,9 @@ export default class PrivacySection { arr.push(...from.users); arr.push(...from.chats.map(id => -id)); this.peerIds[k] = arr; - this.exceptions.get(k).row.subtitle.innerHTML = this.generateStr(from); + const s = this.exceptions.get(k).row.subtitle; + s.innerHTML = ''; + s.append(...this.generateStr(from)); }); } @@ -204,7 +209,11 @@ export default class PrivacySection { const caption = this.options.captions[this.type]; const captionElement = this.radioSection.caption; - captionElement.innerHTML = caption; + if(!caption) { + captionElement.innerHTML = ''; + } else { + _i18n(captionElement, caption); + } captionElement.classList.toggle('hide', !caption); if(this.exceptions) { @@ -230,14 +239,14 @@ export default class PrivacySection { return peers; } - private generateStr(peers: {users: number[], chats: number[]}) { + private generateStr(peers: {users: number[], chats: number[]}): HTMLElement[] { if(!peers.users.length && !peers.chats.length) { - return 'Add Users'; + return [i18n('PrivacySettingsController.AddUsers')]; } - return [ - peers.users.length ? peers.users.length + ' ' + (peers.users.length === 1 ? 'user' : 'users') : '', - peers.chats.length ? peers.chats.length + ' ' + (peers.chats.length === 1 ? 'chat' : 'chats') : '' - ].filter(Boolean).join(', '); + return join([ + peers.users.length ? i18n('Users', [peers.users.length]) : null, + peers.chats.length ? i18n('Chats', [peers.chats.length]) : null + ].filter(Boolean), false); } -} \ No newline at end of file +} diff --git a/src/components/radioField.ts b/src/components/radioField.ts index 12fbd6ab..0390bf4a 100644 --- a/src/components/radioField.ts +++ b/src/components/radioField.ts @@ -8,7 +8,8 @@ export default class RadioField { public main: HTMLElement; constructor(options: { - text?: LangPackKey, + text?: string, + langKey?: LangPackKey, name: string, value?: string, stateKey?: string @@ -38,7 +39,7 @@ export default class RadioField { main.classList.add('radio-field-main'); if(options.text) { - _i18n(main, options.text); + main.innerHTML = options.text; /* const caption = document.createElement('div'); caption.classList.add('radio-field-main-caption'); caption.innerHTML = text; @@ -49,6 +50,8 @@ export default class RadioField { } main.append(caption); */ + } else if(options.langKey) { + _i18n(main, options.langKey); } label.append(input, main); diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index c10a27de..a211dea9 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -437,7 +437,7 @@ export class SettingSection { constructor(options: { name?: LangPackKey, - caption?: string, + caption?: LangPackKey | true, noDelimiter?: boolean }) { this.container = document.createElement('div'); @@ -462,7 +462,10 @@ export class SettingSection { if(options.caption) { this.caption = this.generateContentElement(); this.caption.classList.add('sidebar-left-section-caption'); - this.caption.innerHTML = options.caption; + + if(options.caption !== true) { + i18n_({element: this.caption, key: options.caption}); + } } } @@ -474,7 +477,7 @@ export class SettingSection { } } -export const generateSection = (appendTo: Scrollable, name?: LangPackKey, caption?: string) => { +export const generateSection = (appendTo: Scrollable, name?: LangPackKey, caption?: LangPackKey) => { const section = new SettingSection({name, caption}); appendTo.append(section.container); return section.content; diff --git a/src/components/sidebarLeft/tabs/activeSessions.ts b/src/components/sidebarLeft/tabs/activeSessions.ts index 96ea240c..3364b329 100644 --- a/src/components/sidebarLeft/tabs/activeSessions.ts +++ b/src/components/sidebarLeft/tabs/activeSessions.ts @@ -11,6 +11,7 @@ import PopupConfirmAction from "../../popups/confirmAction"; import apiManager from "../../../lib/mtproto/mtprotoworker"; import { toast } from "../../toast"; import AppPrivacyAndSecurityTab from "./privacyAndSecurity"; +import I18n from "../../../lib/langPack"; export default class AppActiveSessionsTab extends SliderSuperTab { public privacyTab: AppPrivacyAndSecurityTab; @@ -19,7 +20,7 @@ export default class AppActiveSessionsTab extends SliderSuperTab { protected init() { this.container.classList.add('active-sessions-container'); - this.title.innerText = 'Active Sessions'; + this.setTitle('SessionsTitle'); const Session = (auth: Authorization.authorization) => { const row = new Row({ @@ -44,7 +45,7 @@ export default class AppActiveSessionsTab extends SliderSuperTab { { const section = new SettingSection({ - name: 'Current Session' + name: 'CurrentSession' }); const auth = authorizations.findAndSplice(auth => auth.pFlags.current); @@ -53,10 +54,10 @@ export default class AppActiveSessionsTab extends SliderSuperTab { section.content.append(session.container); if(authorizations.length) { - const btnTerminate = Button('btn-primary btn-transparent danger', {icon: 'stop', text: 'Terminate all other sessions'}); + const btnTerminate = Button('btn-primary btn-transparent danger', {icon: 'stop', text: 'TerminateAllSessions'}); attachClickEvent(btnTerminate, (e) => { new PopupConfirmAction('revoke-session', [{ - text: 'TERMINATE', + langKey: 'Terminate', isDanger: true, callback: () => { const toggle = toggleDisability([btnTerminate], true); @@ -70,8 +71,8 @@ export default class AppActiveSessionsTab extends SliderSuperTab { }); } }], { - title: 'Terminate All Other Sessions', - text: 'Are you sure you want to terminate all other sessions?' + title: 'AreYouSureSessionsTitle', + text: 'AreYouSureSessions' }).show(); }); @@ -86,7 +87,7 @@ export default class AppActiveSessionsTab extends SliderSuperTab { } const otherSection = new SettingSection({ - name: 'Other Sessions' + name: 'OtherSessions' }); authorizations.forEach(auth => { @@ -97,7 +98,7 @@ export default class AppActiveSessionsTab extends SliderSuperTab { const onError = (err: any) => { if(err.type === 'FRESH_RESET_AUTHORISATION_FORBIDDEN') { - toast('For security reasons, you can\'t terminate older sessions from a device that you\'ve just connected. Please use an earlier connection or wait for a few hours.'); + toast(I18n.getString('RecentSessions.Error.FreshReset')); } }; @@ -106,7 +107,7 @@ export default class AppActiveSessionsTab extends SliderSuperTab { const hash = target.dataset.hash; new PopupConfirmAction('revoke-session', [{ - text: 'TERMINATE', + langKey: 'Terminate', isDanger: true, callback: () => { apiManager.invokeApi('account.resetAuthorization', {hash}) @@ -118,8 +119,8 @@ export default class AppActiveSessionsTab extends SliderSuperTab { }, onError); } }], { - title: 'Terminate Session', - text: 'Do you want to terminate this session?' + title: 'AreYouSureSessionTitle', + text: 'TerminateSessionText' }).show(); }; diff --git a/src/components/sidebarLeft/tabs/archivedTab.ts b/src/components/sidebarLeft/tabs/archivedTab.ts index c0049c8f..8202d7eb 100644 --- a/src/components/sidebarLeft/tabs/archivedTab.ts +++ b/src/components/sidebarLeft/tabs/archivedTab.ts @@ -8,7 +8,7 @@ export default class AppArchivedTab extends SliderSuperTab { init() { this.container.id = 'chats-archived-container'; - this.title.innerHTML = 'Archived Chats'; + this.setTitle('ArchivedChats'); //this.scrollable = new Scrollable(this.container, 'CLA', 500); this.scrollable.append(appDialogsManager.chatListArchived); diff --git a/src/components/sidebarLeft/tabs/blockedUsers.ts b/src/components/sidebarLeft/tabs/blockedUsers.ts index 9ad1e630..e818f29a 100644 --- a/src/components/sidebarLeft/tabs/blockedUsers.ts +++ b/src/components/sidebarLeft/tabs/blockedUsers.ts @@ -15,11 +15,11 @@ export default class AppBlockedUsersTab extends SliderSuperTab { protected init() { this.container.classList.add('blocked-users-container'); - this.title.innerText = 'Blocked Users'; + this.setTitle('BlockedUsers'); { const section = new SettingSection({ - caption: 'Blocked users will not be able to contact you and will not see your Last Seen time.' + caption: 'BlockedUsersInfo' }); this.scrollable.append(section.container); diff --git a/src/components/sidebarLeft/tabs/generalSettings.ts b/src/components/sidebarLeft/tabs/generalSettings.ts index c4891dba..2d47cb3f 100644 --- a/src/components/sidebarLeft/tabs/generalSettings.ts +++ b/src/components/sidebarLeft/tabs/generalSettings.ts @@ -91,7 +91,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { const enterRow = new Row({ radioField: new RadioField({ - text: 'General.SendShortcut.Enter', + langKey: 'General.SendShortcut.Enter', name: 'send-shortcut', value: 'enter', stateKey: 'settings.sendShortcut' diff --git a/src/components/sidebarLeft/tabs/includedChats.ts b/src/components/sidebarLeft/tabs/includedChats.ts index 588c68eb..2adaae7a 100644 --- a/src/components/sidebarLeft/tabs/includedChats.ts +++ b/src/components/sidebarLeft/tabs/includedChats.ts @@ -96,7 +96,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { }); this.dialogsByFilters = new Map(); - appMessagesManager.filtersStorage.getDialogFilters().then(filters => { + return appMessagesManager.filtersStorage.getDialogFilters().then(filters => { for(const filter of filters) { this.dialogsByFilters.set(filter, new Set(appMessagesManager.dialogsStorage.getFolder(filter.id).map(d => d.peerId))); } @@ -146,21 +146,6 @@ export default class AppIncludedChatsTab extends SliderSuperTab { joined.forEach(el => { dom.lastMessageSpan.append(el); }); - /* let subtitle: LangPackKey; - - if(peerId > 0) { - if(peerId === rootScope.myId) { - subtitle = 'Chat with yourself'; - } else if(appUsersManager.isBot(peerId)) { - subtitle = 'Bot'; - } else { - subtitle = appUsersManager.contactsList.has(peerId) ? 'Contact' : 'Non-Contact'; - } - } else { - subtitle = appPeersManager.isBroadcast(peerId) ? 'Channel' : 'Group'; - } - - _i18n(dom.lastMessageSpan, subtitle); */ }); }; diff --git a/src/components/sidebarLeft/tabs/notifications.ts b/src/components/sidebarLeft/tabs/notifications.ts index c8c5bbfa..a4470a00 100644 --- a/src/components/sidebarLeft/tabs/notifications.ts +++ b/src/components/sidebarLeft/tabs/notifications.ts @@ -79,19 +79,19 @@ export default class AppNotificationsTab extends SliderSuperTabEventable { }; NotifySection({ - name: 'AutoDownloadSettings.TypePrivateChats', + name: 'NotificationsPrivateChats', typeText: 'NotificationsForPrivateChats', inputKey: 'inputNotifyUsers' }); NotifySection({ - name: 'AutoDownloadSettings.TypeGroupChats', + name: 'NotificationsGroups', typeText: 'NotificationsForGroups', inputKey: 'inputNotifyChats' }); NotifySection({ - name: 'AutoDownloadSettings.TypeChannels', + name: 'NotificationsChannels', typeText: 'NotificationsForChannels', inputKey: 'inputNotifyBroadcasts' }); diff --git a/src/components/sidebarLeft/tabs/privacy/addToGroups.ts b/src/components/sidebarLeft/tabs/privacy/addToGroups.ts index ae89cc29..881af408 100644 --- a/src/components/sidebarLeft/tabs/privacy/addToGroups.ts +++ b/src/components/sidebarLeft/tabs/privacy/addToGroups.ts @@ -1,18 +1,19 @@ import { SliderSuperTabEventable } from "../../../sliderTab"; import PrivacySection from "../../../privacySection"; +import { LangPackKey } from "../../../../lib/langPack"; export default class AppPrivacyAddToGroupsTab extends SliderSuperTabEventable { protected init() { this.container.classList.add('privacy-tab', 'privacy-add-to-groups'); - this.title.innerHTML = 'Groups and Channels'; + this.setTitle('PrivacySettings.Groups'); - const caption = 'You can restrict who can add you to groups and channels with granular precision.'; + const caption: LangPackKey = 'PrivacySettingsController.GroupDescription'; new PrivacySection({ tab: this, - title: 'Who can add me to group chats?', + title: 'WhoCanAddMe', inputKey: 'inputPrivacyKeyChatInvite', captions: [caption, caption, caption], - exceptionTexts: ['Never Allow', 'Always Allow'], + exceptionTexts: ['PrivacySettingsController.NeverAllow', 'PrivacySettingsController.AlwaysAllow'], appendTo: this.scrollable }); } diff --git a/src/components/sidebarLeft/tabs/privacy/calls.ts b/src/components/sidebarLeft/tabs/privacy/calls.ts index f6b387a8..723bfa6e 100644 --- a/src/components/sidebarLeft/tabs/privacy/calls.ts +++ b/src/components/sidebarLeft/tabs/privacy/calls.ts @@ -1,29 +1,30 @@ import { SliderSuperTabEventable } from "../../../sliderTab"; import PrivacySection from "../../../privacySection"; +import { LangPackKey } from "../../../../lib/langPack"; export default class AppPrivacyCallsTab extends SliderSuperTabEventable { protected init() { this.container.classList.add('privacy-tab', 'privacy-calls'); - this.title.innerHTML = 'Calls'; + this.setTitle('PrivacySettings.VoiceCalls'); - const caption = 'You can restrict who can call you with granular precision.'; + const caption: LangPackKey = 'PrivacySettingsController.PhoneCallDescription'; new PrivacySection({ tab: this, - title: 'Who can call me?', + title: 'WhoCanCallMe', inputKey: 'inputPrivacyKeyPhoneCall', captions: [caption, caption, caption], - exceptionTexts: ['Never Allow', 'Always Allow'], + exceptionTexts: ['PrivacySettingsController.NeverAllow', 'PrivacySettingsController.AlwaysAllow'], appendTo: this.scrollable }); { - const caption = 'Disabling peer-to-peer will relay all calls through Telegram servers to avoid revealing your IP address, but will slightly decrease audio quality.'; + const caption: LangPackKey = 'PrivacySettingsController.P2p.Desc'; new PrivacySection({ tab: this, - title: 'Peer to peer?', + title: 'PrivacyP2PHeader', inputKey: 'inputPrivacyKeyPhoneP2P', captions: [caption, caption, caption], - exceptionTexts: ['Never Allow', 'Always Allow'], + exceptionTexts: ['PrivacySettingsController.NeverAllow', 'PrivacySettingsController.AlwaysAllow'], appendTo: this.scrollable }); } diff --git a/src/components/sidebarLeft/tabs/privacy/forwardMessages.ts b/src/components/sidebarLeft/tabs/privacy/forwardMessages.ts index d974daa7..2558cccb 100644 --- a/src/components/sidebarLeft/tabs/privacy/forwardMessages.ts +++ b/src/components/sidebarLeft/tabs/privacy/forwardMessages.ts @@ -1,18 +1,19 @@ import { SliderSuperTabEventable } from "../../../sliderTab"; import PrivacySection from "../../../privacySection"; +import { LangPackKey } from "../../../../lib/langPack"; export default class AppPrivacyForwardMessagesTab extends SliderSuperTabEventable { protected init() { this.container.classList.add('privacy-tab', 'privacy-forward-messages'); - this.title.innerHTML = 'Forward Messages'; + this.setTitle('PrivacySettings.Forwards'); - const caption = 'You can restrict who can add a link to your account when forwarding your messages.'; + const caption: LangPackKey = 'PrivacySettingsController.Forwards.CustomHelp'; new PrivacySection({ tab: this, - title: 'Who can add a link to my account when forwarding my messages?', + title: 'PrivacyForwardsTitle', inputKey: 'inputPrivacyKeyForwards', captions: [caption, caption, caption], - exceptionTexts: ['Never Allow', 'Always Allow'], + exceptionTexts: ['PrivacySettingsController.NeverAllow', 'PrivacySettingsController.AlwaysAllow'], appendTo: this.scrollable }); } diff --git a/src/components/sidebarLeft/tabs/privacy/lastSeen.ts b/src/components/sidebarLeft/tabs/privacy/lastSeen.ts index e5a600ca..52eb60fb 100644 --- a/src/components/sidebarLeft/tabs/privacy/lastSeen.ts +++ b/src/components/sidebarLeft/tabs/privacy/lastSeen.ts @@ -1,18 +1,19 @@ import { SliderSuperTabEventable } from "../../../sliderTab"; import PrivacySection from "../../../privacySection"; +import { LangPackKey } from "../../../../lib/langPack"; export default class AppPrivacyLastSeenTab extends SliderSuperTabEventable { protected init() { this.container.classList.add('privacy-tab', 'privacy-last-seen'); - this.title.innerHTML = 'Last Seen & Online'; + this.setTitle('PrivacyLastSeen'); - const caption = 'You won\'t see Last Seen and online statuses for people with whom you don\'t share yours.
Approximate last seen will be shown instead (recently, within a week, within a month).'; + const caption: LangPackKey = 'PrivacySettingsController.LastSeenDescription'; new PrivacySection({ tab: this, - title: 'Who can see your Last Seen time?', + title: 'LastSeenTitle', inputKey: 'inputPrivacyKeyStatusTimestamp', captions: [caption, caption, caption], - exceptionTexts: ['Never Share With', 'Always Share With'], + exceptionTexts: ['PrivacySettingsController.NeverShare', 'PrivacySettingsController.AlwaysShare'], appendTo: this.scrollable }); } diff --git a/src/components/sidebarLeft/tabs/privacy/profilePhoto.ts b/src/components/sidebarLeft/tabs/privacy/profilePhoto.ts index 1df97680..0f9cbf1d 100644 --- a/src/components/sidebarLeft/tabs/privacy/profilePhoto.ts +++ b/src/components/sidebarLeft/tabs/privacy/profilePhoto.ts @@ -1,19 +1,20 @@ import { SliderSuperTabEventable } from "../../../sliderTab"; import PrivacySection from "../../../privacySection"; import { PrivacyType } from "../../../../lib/appManagers/appPrivacyManager"; +import { LangPackKey } from "../../../../lib/langPack"; export default class AppPrivacyProfilePhotoTab extends SliderSuperTabEventable { protected init() { this.container.classList.add('privacy-tab', 'privacy-profile-photo'); - this.title.innerHTML = 'Profile Photo'; + this.setTitle('PrivacyProfilePhoto'); - const caption = 'You can restrict who can see your profile photo with granular precision.'; + const caption: LangPackKey = 'PrivacySettingsController.ProfilePhoto.CustomHelp'; new PrivacySection({ tab: this, - title: 'Who can see your profile photo?', + title: 'PrivacyProfilePhotoTitle', inputKey: 'inputPrivacyKeyChatInvite', captions: [caption, caption, caption], - exceptionTexts: ['Never Share With', 'Always Share With'], + exceptionTexts: ['PrivacySettingsController.NeverShare', 'PrivacySettingsController.AlwaysShare'], appendTo: this.scrollable, skipTypes: [PrivacyType.Nobody] }); diff --git a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts index 0ebedc14..db6eeaa3 100644 --- a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts +++ b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts @@ -19,7 +19,7 @@ import AppBlockedUsersTab from "./blockedUsers"; import appUsersManager from "../../../lib/appManagers/appUsersManager"; import rootScope from "../../../lib/rootScope"; import { convertKeyToInputKey } from "../../../helpers/string"; -import { LangPackKey, _i18n } from "../../../lib/langPack"; +import { i18n, LangPackKey, _i18n } from "../../../lib/langPack"; export default class AppPrivacyAndSecurityTab extends SliderSuperTab { private activeSessionsRow: Row; @@ -97,9 +97,9 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab { blockedCount = count; if(count) { - _i18n(blockedUsersRow.subtitle, 'Privacy.BlockedUsers', [count]); + _i18n(blockedUsersRow.subtitle, 'PrivacySettingsController.UserCount', [count]); } else { - _i18n(blockedUsersRow.subtitle, 'Privacy.BlockedUsers.None'); + _i18n(blockedUsersRow.subtitle, 'BlockedEmpty'); } }; @@ -124,7 +124,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab { passwordManager.getState().then(state => { passwordState = state; - twoFactorRow.subtitle.innerText = state.pFlags.has_password ? 'On' : 'Off'; + _i18n(twoFactorRow.subtitle, state.pFlags.has_password ? 'PrivacyAndSecurity.Item.On' : 'PrivacyAndSecurity.Item.Off'); twoFactorRow.freezed = false; //console.log('password state', state); @@ -198,11 +198,16 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab { appPrivacyManager.getPrivacy(key).then(rules => { const details = appPrivacyManager.getPrivacyRulesDetails(rules); - const type = details.type === PrivacyType.Everybody ? 'Everybody' : (details.type === PrivacyType.Contacts ? 'My Contacts' : 'Nobody'); + const langKey = details.type === PrivacyType.Everybody ? 'PrivacySettingsController.Everbody' : (details.type === PrivacyType.Contacts ? 'PrivacySettingsController.MyContacts' : 'PrivacySettingsController.Nobody'); const disallowLength = details.disallowPeers.users.length + details.disallowPeers.chats.length; const allowLength = details.allowPeers.users.length + details.allowPeers.chats.length; - const str = type + (disallowLength || allowLength ? ` (${[-disallowLength, allowLength ? '+' + allowLength : 0].filter(Boolean).join(', ')})` : ''); - row.subtitle.innerHTML = str; + + row.subtitle.innerHTML = ''; + const s = i18n(langKey); + row.subtitle.append(s); + if(disallowLength || allowLength) { + row.subtitle.append(` (${[-disallowLength, allowLength ? '+' + allowLength : 0].filter(Boolean).join(', ')})`); + } }); }; diff --git a/src/lang.ts b/src/lang.ts index 9047323d..e2733f83 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -39,11 +39,6 @@ const lang = { one_value: '%1$d device', other_value: '%1$d devices' }, - "Privacy.BlockedUsers": { - one_value: '%1$d user', - other_value: '%1$d users', - }, - "Privacy.BlockedUsers.None": 'None', // * android FilterAlwaysShow: 'Include Chats', @@ -68,6 +63,10 @@ const lang = { one_value: '%1$d group', other_value: '%1$d groups' }, + Users: { + one_value: "%1$d user", + other_value: "%1$d users" + }, UsernameHelpLink: "This link opens a chat with you:\n%1$s", NewGroup: "New Group", Contacts: "Contacts", @@ -88,20 +87,44 @@ const lang = { NotificationsForGroups: 'Notifications for groups', NotificationsForPrivateChats: 'Notifications for private chats', NotificationsForChannels: 'Notifications for channels', + NotificationsPrivateChats: "Private Chats", + NotificationsGroups: "Groups", + NotificationsChannels: "Channels", NotificationsOther: 'Other', ContactJoined: 'Contact joined Telegram', Loading: "Loading...", + Unblock: "Unblock", BlockedUsers: "Blocked Users", + BlockedUsersInfo: 'Blocked users will not be able to contact you and will not see your Last Seen time.', + BlockedEmpty: "None", TwoStepVerification: "Two-Step Verification", + PrivacyExceptions: "Exceptions", + PrivacyLastSeen: "Last Seen & Online", PrivacySettings: "Privacy and Security", PrivacyTitle: "Privacy", + PrivacyPhone: "Phone Number", PrivacyPhoneTitle: "Who can see my phone number?", + PrivacyPhoneTitle2: "Who can find me by my number?", + PrivacyPhoneInfo: "Users who have your number saved in their contacts will also see it on Telegram.", + PrivacyPhoneInfo3: "Users who add your number to their contacts will see it on Telegram only if they are your contacts.", + PrivacyProfilePhoto: "Profile Photos", PrivacyProfilePhotoTitle: "Who can see my profile photos & videos?", + PrivacyP2PHeader: "Peer-to-Peer", PrivacyForwardsTitle: "Who can add a link to my account when forwarding my messages?", LastSeenTitle: "Who can see your Last Seen time?", SessionsTitle: "Active Sessions", + CurrentSession: "This device", + TerminateAllSessions: "Terminate All Other Sessions", + TerminateSessionText: "Are you sure you want to terminate this session?", + OtherSessions: "Active sessions", + AreYouSureSessionTitle: "Terminate session", + AreYouSureSessionsTitle: "Terminate sessions", + AreYouSureSessions: "Are you sure you want to terminate all other sessions?", + Terminate: "Terminate", WhoCanCallMe: "Who can call me?", WhoCanAddMe: "Who can add me to group chats?", + ArchivedChats: "Archived Chats", + Cancel: "Cancel", // * macos "ChatList.Filter.Header": "Create folders for different groups of chats and quickly switch between them.", @@ -132,9 +155,31 @@ const lang = { "Telegram.NotificationSettingsViewController": "Notifications", "Stickers.SuggestStickers": "Suggest Stickers by Emoji", "InstalledStickers.LoopAnimated": "Loop Animated Stickers", - "AutoDownloadSettings.TypePrivateChats": "Private Chats", - "AutoDownloadSettings.TypeGroupChats": "Groups", - "AutoDownloadSettings.TypeChannels": "Channels", + "PrivacyAndSecurity.Item.On": "On", + "PrivacyAndSecurity.Item.Off": "Off", + "PrivacySettings.VoiceCalls": "Calls", + "PrivacySettings.Forwards": "Forwarded Messages", + "PrivacySettings.Groups": "Groups and Channels", + "PrivacySettingsController.AddUsers": "Add Users", + "PrivacySettingsController.GroupDescription": "You can restrict who can add you to groups and channels with granular precision.", + "PrivacySettingsController.Forwards.CustomHelp": "You can restrict who can add a link to your account when forwarding your messages.", + "PrivacySettingsController.P2p.Desc": "Disabling peer-to-peer will relay all calls through Telegram servers to avoid revealing your IP address, but may slightly decrease audio and video quality.", + "PrivacySettingsController.PhoneCallDescription": "You can restrict who can call you with granular precision.", + "PrivacySettingsController.ProfilePhoto.CustomHelp": "You can restrict who can see your profile photo with granular precision.", + "PrivacySettingsController.LastSeenDescription": "You won't see Last Seen and Online statuses for people with whom you don't share yours. Approximate last seen will be shown instead (recently, within a week, within a month).", + "PrivacySettingsController.PeerInfo": "You can add users or entire groups as exceptions that will override the settings above.", + "PrivacySettingsController.Everbody": "Everybody", + "PrivacySettingsController.MyContacts": "My Contacts", + "PrivacySettingsController.Nobody": "Nobody", + "PrivacySettingsController.NeverShare": "Never Share With", + "PrivacySettingsController.AlwaysShare": "Always Share With", + "PrivacySettingsController.NeverAllow": "Never Allow", + "PrivacySettingsController.AlwaysAllow": "Always Allow", + "PrivacySettingsController.UserCount": { + one_value: '%d user', + other_value: '%d users' + }, + "RecentSessions.Error.FreshReset": "For security reasons, you can't terminate older sessions from a device that you've just connected. Please use an earlier connection or wait for a few hours.", }; export default lang; diff --git a/src/scss/partials/_chatlist.scss b/src/scss/partials/_chatlist.scss index bca84739..6ac1919a 100644 --- a/src/scss/partials/_chatlist.scss +++ b/src/scss/partials/_chatlist.scss @@ -152,7 +152,7 @@ ul.chatlist { } span { - display: inline-block; + //display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/scss/partials/popups/_peer.scss b/src/scss/partials/popups/_peer.scss index 17a275ec..38d087de 100644 --- a/src/scss/partials/popups/_peer.scss +++ b/src/scss/partials/popups/_peer.scss @@ -17,7 +17,6 @@ font-size: 1.25rem; font-weight: 500; margin-bottom: .125rem; - text-transform: capitalize; &:not(:first-child) { padding-left: .75rem; diff --git a/src/vendor/dateFormat.ts b/src/vendor/dateFormat.ts new file mode 100644 index 00000000..30e74e8e --- /dev/null +++ b/src/vendor/dateFormat.ts @@ -0,0 +1,381 @@ +/* + * Date Format 1.2.3 + * (c) 2007-2009 Steven Levithan + * MIT license + * + * Includes enhancements by Scott Trenda + * and Kris Kowal + * + * Accepts a date, a mask, or a date and a mask. + * Returns a formatted version of the given date. + * The date defaults to the current date/time. + * The mask defaults to dateFormat.masks.default. + */ + +const dateFormat = (() => { + const token = /d{1,4}|D{3,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|W{1,2}|[LlopSZN]|"[^"]*"|'[^']*'/g; + const timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g; + const timezoneClip = /[^-+\dA-Z]/g; + + // Regexes and supporting functions are cached through closure + function f(date?: number | Date, mask?: string, utc?: boolean, gmt?: boolean): string { + // You can't provide utc if you skip other args (use the 'UTC:' mask prefix) + /* if( + arguments.length === 1 && + kindOf(date) === "string" && + !/\d/.test(date) + ) { + mask = date; + date = undefined; + } */ + + date = date || date === 0 ? date : new Date(); + + if(!(date instanceof Date)) { + date = new Date(date); + } + + /* if(isNaN(date)) { + throw TypeError("Invalid date"); + } */ + + /* mask = String( + dateFormat.masks[mask] || mask || dateFormat.masks["default"] + ); */ + + // Allow setting the utc/gmt argument via the mask + const maskSlice = mask.slice(0, 4); + if(maskSlice === "UTC:" || maskSlice === "GMT:") { + mask = mask.slice(4); + utc = true; + if(maskSlice === "GMT:") { + gmt = true; + } + } + + const _ = () => (utc ? "getUTC" : "get"); + const d = (): number => (date as any)[_() + "Date"](); + const D = (): number => (date as any)[_() + "Day"](); + const m = (): number => (date as any)[_() + "Month"](); + const y = (): number => (date as any)[_() + "FullYear"](); + const H = (): number => (date as any)[_() + "Hours"](); + const M = (): number => (date as any)[_() + "Minutes"](); + const s = (): number => (date as any)[_() + "Seconds"](); + const L = (): number => (date as any)[_() + "Milliseconds"](); + const o = (): number => (utc ? 0 : (date as Date).getTimezoneOffset()); + const W = (): number => getWeek(date as Date); + const N = (): number => getDayOfWeek(date as Date); + + const flags = { + d: () => d(), + dd: () => pad(d()), + ddd: () => dateFormat.i18n.dayNames[D()], + DDD: () => getDayName({ + y: y(), + m: m(), + d: d(), + _: _(), + dayName: dateFormat.i18n.dayNames[D()], + short: true + }), + dddd: () => dateFormat.i18n.dayNames[D() + 7], + DDDD: () => getDayName({ + y: y(), + m: m(), + d: d(), + _: _(), + dayName: dateFormat.i18n.dayNames[D() + 7] + }), + m: () => m() + 1, + mm: () => pad(m() + 1), + mmm: () => dateFormat.i18n.monthNames[m()], + mmmm: () => dateFormat.i18n.monthNames[m() + 12], + yy: () => String(y()).slice(2), + yyyy: () => pad(y(), 4), + h: () => H() % 12 || 12, + hh: () => pad(H() % 12 || 12), + H: () => H(), + HH: () => pad(H()), + M: () => M(), + MM: () => pad(M()), + s: () => s(), + ss: () => pad(s()), + l: () => pad(L(), 3), + L: () => pad(Math.floor(L() / 10)), + t: () => + H() < 12 ? + dateFormat.i18n.timeNames[0] : dateFormat.i18n.timeNames[1], + tt: () => + H() < 12 ? + dateFormat.i18n.timeNames[2] : dateFormat.i18n.timeNames[3], + T: () => + H() < 12 ? + dateFormat.i18n.timeNames[4] : dateFormat.i18n.timeNames[5], + TT: () => + H() < 12 ? + dateFormat.i18n.timeNames[6] : dateFormat.i18n.timeNames[7], + Z: () => + gmt ? + "GMT" : utc ? + "UTC" : (String(date).match(timezone) || [""]) + .pop() + .replace(timezoneClip, "") + .replace(/GMT\+0000/g, "UTC"), + o: () => + (o() > 0 ? "-" : "+") + + pad(Math.floor(Math.abs(o()) / 60) * 100 + (Math.abs(o()) % 60), 4), + p: () => + (o() > 0 ? "-" : "+") + + pad(Math.floor(Math.abs(o()) / 60), 2) + + ":" + + pad(Math.floor(Math.abs(o()) % 60), 2), + S: () => ["th", "st", "nd", "rd"][ + // @ts-ignore + d() % 10 > 3 ? 0 : (((d() % 100) - (d() % 10) != 10) * d()) % 10 + ], + W: () => W(), + WW: () => pad(W()), + N: () => N(), + }; + + return mask.replace(token, (match) => { + if(match in flags) { + // @ts-ignore + return flags[match](); + } + return match.slice(1, match.length - 1); + }); + }; + + // Internationalization strings + f.i18n = { + dayNames: [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ], + monthNames: [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ], + timeNames: ["a", "p", "am", "pm", "A", "P", "AM", "PM"], + }; + + f.setLocale = function(code: string) { + const date = new Date(); + + { + const dateTimeFormat = new Intl.DateTimeFormat(code, {month: 'long'}); + const dateTimeFormatShort = new Intl.DateTimeFormat(code, {month: 'short'}); + for(let i = 0; i < 12; ++i) { + date.setMonth(i); + f.i18n.monthNames[i] = dateTimeFormatShort.format(date); + f.i18n.monthNames[i + 12] = dateTimeFormat.format(date); + } + } + + { + const day = date.getDay(); + if(day !== 0) { + date.setDate(date.getDate() - day); + } + + const dateTimeFormat = new Intl.DateTimeFormat(code, {weekday: 'long'}); + const dateTimeFormatShort = new Intl.DateTimeFormat(code, {weekday: 'short'}); + for(let i = 0; i < 7; ++i) { + date.setDate(date.getDate() + 1); + f.i18n.dayNames[i] = dateTimeFormatShort.format(date); + f.i18n.dayNames[i + 7] = dateTimeFormat.format(date); + } + } + }; + + return f; +})(); + +export default dateFormat; + +(window as any).dateFormat = dateFormat; + +/* dateFormat.masks = { + default: "ddd mmm dd yyyy HH:MM:ss", + shortDate: "m/d/yy", + paddedShortDate: "mm/dd/yyyy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:sso", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'", + expiresHeaderFormat: "ddd, dd mmm yyyy HH:MM:ss Z", +}; */ + +const pad = (val: number | string, len = 2) => { + val = String(val); + while(val.length < len) { + val = "0" + val; + } + return val; +}; + +/** + * Get day name + * Yesterday, Today, Tomorrow if the date lies within, else fallback to Monday - Sunday + * @param {Object} + * @return {String} + */ +const getDayName = ({ + y, + m, + d, + _, + dayName, + short = false +}: { + y: number, + m: number, + d: number, + _: any, + dayName: any, + short?: boolean +}) => { + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate((yesterday as any)[_ + 'Date']() - 1); + const tomorrow = new Date(); + tomorrow.setDate((tomorrow as any)[_ + 'Date']() + 1); + const today_d = (): number => (today as any)[_ + 'Date'](); + const today_m = (): number => (today as any)[_ + 'Month'](); + const today_y = (): number => (today as any)[_ + 'FullYear'](); + const yesterday_d = (): number => (yesterday as any)[_ + 'Date'](); + const yesterday_m = (): number => (yesterday as any)[_ + 'Month'](); + const yesterday_y = (): number => (yesterday as any)[_ + 'FullYear'](); + const tomorrow_d = (): number => (tomorrow as any)[_ + 'Date'](); + const tomorrow_m = (): number => (tomorrow as any)[_ + 'Month'](); + const tomorrow_y = (): number => (tomorrow as any)[_ + 'FullYear'](); + + if(today_y() === y && today_m() === m && today_d() === d) { + return short ? 'Tdy' : 'Today'; + } else if(yesterday_y() === y && yesterday_m() === m && yesterday_d() === d) { + return short ? 'Ysd' : 'Yesterday'; + } else if(tomorrow_y() === y && tomorrow_m() === m && tomorrow_d() === d) { + return short ? 'Tmw' : 'Tomorrow'; + } + return dayName; +}; + +/** + * Get the ISO 8601 week number + * Based on comments from + * http://techblog.procurios.nl/k/n618/news/view/33796/14863/Calculate-ISO-8601-week-and-year-in-javascript.html + * + * @param {Object} `date` + * @return {Number} + */ +const getWeek = (date: Date) => { + // Remove time components of date + const targetThursday = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate() + ); + + // Change date to Thursday same week + targetThursday.setDate( + targetThursday.getDate() - ((targetThursday.getDay() + 6) % 7) + 3 + ); + + // Take January 4th as it is always in week 1 (see ISO 8601) + const firstThursday = new Date(targetThursday.getFullYear(), 0, 4); + + // Change date to Thursday same week + firstThursday.setDate( + firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7) + 3 + ); + + // Check if daylight-saving-time-switch occurred and correct for it + const ds = + targetThursday.getTimezoneOffset() - firstThursday.getTimezoneOffset(); + targetThursday.setHours(targetThursday.getHours() - ds); + + // Number of weeks between target Thursday and first Thursday + const weekDiff = (targetThursday.getTime() - firstThursday.getTime()) / (86400000 * 7); + return 1 + Math.floor(weekDiff); +}; + +/** + * Get ISO-8601 numeric representation of the day of the week + * 1 (for Monday) through 7 (for Sunday) + * + * @param {Object} `date` + * @return {Number} + */ +const getDayOfWeek = (date: Date) => { + let dow = date.getDay(); + if(dow === 0) { + dow = 7; + } + return dow; +}; + +/** + * kind-of shortcut + * @param {*} val + * @return {String} + */ +/* const kindOf = (val: any) => { + if(val === null) { + return "null"; + } + + if(val === undefined) { + return "undefined"; + } + + if(typeof val !== "object") { + return typeof val; + } + + if(Array.isArray(val)) { + return "array"; + } + + return {}.toString.call(val).slice(8, -1).toLowerCase(); +}; */