diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 7b1a7efa..5377669f 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -10,6 +10,7 @@ import Scrollable from "./scrollable"; import { FocusDirection } from "../helpers/fastSmoothScroll"; import CheckboxField from "./checkboxField"; import appProfileManager from "../lib/appManagers/appProfileManager"; +import { safeAssign } from "../helpers/object"; type PeerType = 'contacts' | 'dialogs' | 'channelParticipants'; @@ -69,7 +70,7 @@ export default class AppSelectPeers { rippleEnabled?: boolean, avatarSize?: AppSelectPeers['avatarSize'], }) { - Object.assign(this, options); + safeAssign(this, options); this.container.classList.add('selector'); @@ -440,7 +441,7 @@ export default class AppSelectPeers { }); } - public add(peerId: any, title?: string, scroll = true) { + public add(peerId: any, title?: string | HTMLElement, scroll = true) { //console.trace('add'); this.selected.add(peerId); @@ -467,7 +468,12 @@ export default class AppSelectPeers { } if(title) { - div.innerHTML = title; + if(typeof(title) === 'string') { + div.innerHTML = title; + } else { + div.innerHTML = ''; + div.append(title); + } } div.insertAdjacentElement('afterbegin', avatarEl); diff --git a/src/components/editPeer.ts b/src/components/editPeer.ts index 9d008f4c..882ff66e 100644 --- a/src/components/editPeer.ts +++ b/src/components/editPeer.ts @@ -4,6 +4,7 @@ import AvatarElement from "./avatar"; import InputField from "./inputField"; import ListenerSetter from "../helpers/listenerSetter"; import Button from "./button"; +import { safeAssign } from "../helpers/object"; export default class EditPeer { public nextBtn: HTMLButtonElement; @@ -23,7 +24,7 @@ export default class EditPeer { listenerSetter: ListenerSetter, doNotEditAvatar?: boolean, }) { - Object.assign(this, options); + safeAssign(this, options); this.nextBtn = Button('btn-circle btn-corner tgico-check'); diff --git a/src/components/inputField.ts b/src/components/inputField.ts index fceef259..dc454d37 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -1,7 +1,7 @@ import { getRichValue, isInputEmpty } from "../helpers/dom"; import { debounce } from "../helpers/schedulers"; import { checkRTL } from "../helpers/string"; -import { i18n_, LangPackKey } from "../lib/langPack"; +import { i18n, LangPackKey } from "../lib/langPack"; import RichTextProcessor from "../lib/richtextprocessor"; let init = () => { @@ -143,7 +143,7 @@ class InputField { if(label) { this.label = document.createElement('label'); - i18n_({element: this.label, key: label}); + this.label.append(i18n(label)); this.container.append(this.label); } @@ -234,9 +234,10 @@ class InputField { } } - public setState(state: InputState, label?: string) { + public setState(state: InputState, label?: LangPackKey) { if(label) { - this.label.innerHTML = label; + this.label.innerHTML = ''; + this.label.append(i18n(label)); } this.input.classList.toggle('error', !!(state & InputState.Error)); diff --git a/src/components/preloader.ts b/src/components/preloader.ts index f7a0a01e..b17fa185 100644 --- a/src/components/preloader.ts +++ b/src/components/preloader.ts @@ -2,6 +2,7 @@ import { isInDOM, cancelEvent, attachClickEvent } from "../helpers/dom"; import { CancellablePromise } from "../helpers/cancellablePromise"; import SetTransition from "./singleTransition"; import { fastRaf } from "../helpers/schedulers"; +import { safeAssign } from "../helpers/object"; const TRANSITION_TIME = 200; @@ -34,7 +35,7 @@ export default class ProgressivePreloader { attachMethod: ProgressivePreloader['attachMethod'] }>) { if(options) { - Object.assign(this, options); + safeAssign(this, options); } } diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 2df87c85..a7d79d75 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -24,6 +24,8 @@ import AppContactsTab from "./tabs/contacts"; import AppArchivedTab from "./tabs/archivedTab"; import AppAddMembersTab from "./tabs/addMembers"; import { i18n_, LangPackKey } from "../../lib/langPack"; +import ButtonMenuToggle from "../buttonMenuToggle"; +import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; export const LEFT_COLUMN_ACTIVE_CLASSNAME = 'is-left-column-shown'; @@ -34,14 +36,6 @@ export class AppSidebarLeft extends SidebarSlider { private inputSearch: InputSearch; private menuEl: HTMLElement; - private buttons: { - newGroup: HTMLButtonElement, - contacts: HTMLButtonElement, - archived: HTMLButtonElement, - saved: HTMLButtonElement, - settings: HTMLButtonElement, - help: HTMLButtonElement - } = {} as any; public archivedCount: HTMLSpanElement; private newBtnMenu: HTMLElement; @@ -68,58 +62,87 @@ export class AppSidebarLeft extends SidebarSlider { const sidebarHeader = this.sidebarEl.querySelector('.item-main .sidebar-header'); sidebarHeader.append(this.inputSearch.container); + const onNewGroupClick = () => { + new AppAddMembersTab(this).open({ + peerId: 0, + type: 'chat', + skippable: false, + takeOut: (peerIds) => { + new AppNewGroupTab(this).open(peerIds); + }, + title: 'Add Members', + placeholder: 'Add People...' + }); + }; + + const onContactsClick = () => { + new AppContactsTab(this).open(); + }; + this.toolsBtn = this.sidebarEl.querySelector('.sidebar-tools-button') as HTMLButtonElement; this.backBtn = this.sidebarEl.querySelector('.sidebar-back-button') as HTMLButtonElement; + const btnArchive: ButtonMenuItemOptions = { + icon: 'archive', + text: 'Archived', + onClick: () => { + new AppArchivedTab(this).open(); + } + }; + + const btnMenu = ButtonMenu([{ + icon: 'newgroup', + text: 'New Group', + onClick: onNewGroupClick + }, { + icon: 'user', + text: 'Contacts', + onClick: onContactsClick + }, btnArchive, { + icon: 'savedmessages', + text: 'Saved', + onClick: () => { + setTimeout(() => { // menu doesn't close if no timeout (lol) + appImManager.setPeer(appImManager.myId); + }, 0); + } + }, { + icon: 'settings', + text: 'Settings', + onClick: () => { + new AppSettingsTab(this).open(); + } + }, { + icon: 'help btn-disabled', + text: 'Help', + onClick: () => { + + } + }]); + + btnMenu.classList.add('bottom-right'); + + this.toolsBtn.append(btnMenu); + this.menuEl = this.toolsBtn.querySelector('.btn-menu'); this.newBtnMenu = this.sidebarEl.querySelector('#new-menu'); this.inputSearch.input.addEventListener('focus', () => this.initSearch(), {once: true}); - parseMenuButtonsTo(this.buttons, this.menuEl.children); parseMenuButtonsTo(this.newButtons, this.newBtnMenu.firstElementChild.children); - this.archivedCount = this.buttons.archived.querySelector('.archived-count') as HTMLSpanElement; - - attachClickEvent(this.buttons.saved, (e) => { - ///////this.log('savedbtn click'); - setTimeout(() => { // menu doesn't close if no timeout (lol) - appImManager.setPeer(appImManager.myId); - }, 0); - }); - - attachClickEvent(this.buttons.archived, (e) => { - new AppArchivedTab(this).open(); - }); + this.archivedCount = document.createElement('span'); + this.archivedCount.className = 'archived-count badge badge-24 badge-gray'; - [this.newButtons.privateChat, this.buttons.contacts].forEach(btn => { - attachClickEvent(btn, (e) => { - new AppContactsTab(this).open(); - }); - }); + btnArchive.element.append(this.archivedCount); - attachClickEvent(this.buttons.settings, (e) => { - new AppSettingsTab(this).open(); - }); + attachClickEvent(this.newButtons.privateChat, onContactsClick); attachClickEvent(this.newButtons.channel, (e) => { new AppNewChannelTab(this).open(); }); - [this.newButtons.group, this.buttons.newGroup].forEach(btn => { - attachClickEvent(btn, (e) => { - new AppAddMembersTab(this).open({ - peerId: 0, - type: 'chat', - skippable: false, - takeOut: (peerIds) => { - new AppNewGroupTab(this).open(peerIds); - }, - title: 'Add Members', - placeholder: 'Add People...' - }); - }); - }); + attachClickEvent(this.newButtons.group, onNewGroupClick); rootScope.on('dialogs_archived_unread', (e) => { this.archivedCount.innerText = '' + formatNumber(e.count, 1); diff --git a/src/components/sidebarLeft/tabs/editFolder.ts b/src/components/sidebarLeft/tabs/editFolder.ts index 1ec37b4b..2115c7c0 100644 --- a/src/components/sidebarLeft/tabs/editFolder.ts +++ b/src/components/sidebarLeft/tabs/editFolder.ts @@ -13,6 +13,7 @@ import ButtonMenuToggle from "../../buttonMenuToggle"; import { ButtonMenuItemOptions } from "../../buttonMenu"; import Button from "../../button"; import AppIncludedChatsTab from "./includedChats"; +import { i18n, i18n_, LangPackKey } from "../../../lib/langPack"; const MAX_FOLDER_NAME_LENGTH = 12; @@ -38,7 +39,8 @@ export default class AppEditFolderTab extends SliderSuperTab { this.container.classList.add('edit-folder-container'); this.caption = document.createElement('div'); this.caption.classList.add('caption'); - this.caption.innerHTML = `Choose chats and types of chats that will
appear and never appear in this folder.`; + this.caption.append(i18n(`Choose chats and types of chats that will +appear and never appear in this folder.`)); this.stickerContainer = document.createElement('div'); this.stickerContainer.classList.add('sticker-container'); @@ -72,13 +74,13 @@ export default class AppEditFolderTab extends SliderSuperTab { inputWrapper.append(this.nameInputField.container); - const generateList = (className: string, h2Text: string, buttons: {icon: string, name?: string, withRipple?: true, text: string}[], to: any) => { + const generateList = (className: string, h2Text: LangPackKey, buttons: {icon: string, name?: string, withRipple?: true, text: string}[], to: any) => { const container = document.createElement('div'); container.classList.add('folder-list', className); const h2 = document.createElement('div'); h2.classList.add('sidebar-left-h2'); - h2.innerHTML = h2Text; + i18n_({element: h2, key: h2Text}); const categories = document.createElement('div'); categories.classList.add('folder-categories'); @@ -102,46 +104,46 @@ export default class AppEditFolderTab extends SliderSuperTab { return container; }; - this.include_peers = generateList('folder-list-included', 'Included chats', [{ + this.include_peers = generateList('folder-list-included', 'ChatList.Filter.Include.Header', [{ icon: 'add primary', - text: 'Add Chats', + text: 'ChatList.Filter.Include.AddChat', withRipple: true }, { - text: 'Contacts', + text: 'ChatList.Filter.Contacts', icon: 'newprivate', name: 'contacts' }, { - text: 'Non-Contacts', + text: 'ChatList.Filter.NonContacts', icon: 'noncontacts', name: 'non_contacts' }, { - text: 'Groups', + text: 'ChatList.Filter.Groups', icon: 'group', name: 'groups' }, { - text: 'Channels', + text: 'ChatList.Filter.Channels', icon: 'channel', name: 'broadcasts' }, { - text: 'Bots', + text: 'ChatList.Filter.Bots', icon: 'bots', name: 'bots' }], this.flags); - this.exclude_peers = generateList('folder-list-excluded', 'Excluded chats', [{ + this.exclude_peers = generateList('folder-list-excluded', 'ChatList.Filter.Exclude.Header', [{ icon: 'minus primary', - text: 'Remove Chats', + text: 'ChatList.Filter.Exclude.AddChat', withRipple: true }, { - text: 'Muted', + text: 'ChatList.Filter.MutedChats', icon: 'mute', name: 'exclude_muted' }, { - text: 'Archived', + text: 'ChatList.Filter.Archive', icon: 'archive', name: 'exclude_archived' }, { - text: 'Read', + text: 'ChatList.Filter.ReadChats', icon: 'readchats', name: 'exclude_read' }], this.flags); @@ -225,7 +227,7 @@ export default class AppEditFolderTab extends SliderSuperTab { private onCreateOpen() { this.caption.style.display = ''; - this.title.innerText = 'New Folder'; + this.setTitle('New Folder'); this.menuBtn.classList.add('hide'); this.confirmBtn.classList.remove('hide'); this.nameInputField.value = ''; @@ -238,7 +240,7 @@ export default class AppEditFolderTab extends SliderSuperTab { private onEditOpen() { this.caption.style.display = 'none'; - this.title.innerText = this.type === 'create' ? 'New Folder' : 'Edit Folder'; + this.setTitle(this.type === 'create' ? 'New Folder' : 'Edit Folder'); if(this.type === 'edit') { this.menuBtn.classList.remove('hide'); diff --git a/src/components/sidebarLeft/tabs/editProfile.ts b/src/components/sidebarLeft/tabs/editProfile.ts index d1a7b98b..02633838 100644 --- a/src/components/sidebarLeft/tabs/editProfile.ts +++ b/src/components/sidebarLeft/tabs/editProfile.ts @@ -5,7 +5,7 @@ import { SliderSuperTab } from "../../slider"; import { attachClickEvent } from "../../../helpers/dom"; import EditPeer from "../../editPeer"; import { UsernameInputField } from "../../usernameInputField"; -import { i18n_ } from "../../../lib/langPack"; +import { i18n, i18n_ } from "../../../lib/langPack"; // TODO: аватарка не поменяется в этой вкладке после изменения почему-то (если поставить в другом клиенте, и потом тут проверить, для этого ещё вышел в чатлист) @@ -93,8 +93,21 @@ export default class AppEditProfileTab extends SliderSuperTab { const caption = document.createElement('div'); caption.classList.add('caption'); - caption.innerHTML = `You can choose a username on Telegram. If you do, other people will be able to find you by this username and contact you without knowing your phone number.

You can use a-z, 0-9 and underscores. Minimum length is 5 characters.

This link opens a chat with you: -
`; + caption.append(i18n('UsernameSettings.ChangeDescription')); + caption.append(document.createElement('br'), document.createElement('br')); + + const profileUrlContainer = this.profileUrlContainer = document.createElement('div'); + profileUrlContainer.classList.add('profile-url-container'); + profileUrlContainer.append(i18n('This link opens a chat with you:')); + + const profileUrlAnchor = this.profileUrlAnchor = document.createElement('a'); + profileUrlAnchor.classList.add('profile-url'); + profileUrlAnchor.href = '#'; + profileUrlAnchor.target = '_blank'; + + profileUrlContainer.append(profileUrlAnchor); + + caption.append(profileUrlContainer); this.profileUrlContainer = caption.querySelector('.profile-url-container'); this.profileUrlAnchor = this.profileUrlContainer.lastElementChild as HTMLAnchorElement; diff --git a/src/components/sidebarLeft/tabs/includedChats.ts b/src/components/sidebarLeft/tabs/includedChats.ts index b7eb226c..6f82cfeb 100644 --- a/src/components/sidebarLeft/tabs/includedChats.ts +++ b/src/components/sidebarLeft/tabs/includedChats.ts @@ -10,6 +10,7 @@ import ButtonIcon from "../../buttonIcon"; import CheckboxField from "../../checkboxField"; import Button from "../../button"; import AppEditFolderTab from "./editFolder"; +import { i18n, LangPackKey, _i18n } from "../../../lib/langPack"; export default class AppIncludedChatsTab extends SliderSuperTab { private editFolderTab: AppEditFolderTab; @@ -123,7 +124,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { dom.containerEl.append(this.checkbox(selected)); if(selected) dom.listEl.classList.add('active'); - let subtitle = ''; + let subtitle: LangPackKey; if(peerId > 0) { if(peerId === rootScope.myId) { @@ -137,7 +138,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { subtitle = appPeersManager.isBroadcast(peerId) ? 'Channel' : 'Group'; } - dom.lastMessageSpan.innerHTML = subtitle; + _i18n(dom.lastMessageSpan, subtitle); }); }; @@ -148,14 +149,14 @@ export default class AppIncludedChatsTab extends SliderSuperTab { } this.confirmBtn.style.display = this.type === 'excluded' ? '' : 'none'; - this.title.innerText = this.type === 'included' ? 'Included Chats' : 'Excluded Chats'; + this.setTitle(this.type === 'included' ? 'Included Chats' : 'Excluded Chats'); const filter = this.filter; const fragment = document.createDocumentFragment(); const dd = document.createElement('div'); dd.classList.add('sidebar-left-h2'); - dd.innerText = 'Chat types'; + _i18n(dd, 'ChatList.Add.TopSeparator'); const categories = document.createElement('div'); categories.classList.add('folder-categories'); @@ -163,17 +164,17 @@ export default class AppIncludedChatsTab extends SliderSuperTab { let details: {[flag: string]: {ico: string, text: string}}; if(this.type === 'excluded') { details = { - exclude_muted: {ico: 'mute', text: 'Muted'}, - exclude_archived: {ico: 'archive', text: 'Archived'}, - exclude_read: {ico: 'readchats', text: 'Read'} + exclude_muted: {ico: 'mute', text: 'ChatList.Filter.MutedChats'}, + exclude_archived: {ico: 'archive', text: 'ChatList.Filter.Archive'}, + exclude_read: {ico: 'readchats', text: 'ChatList.Filter.ReadChats'} }; } else { details = { - contacts: {ico: 'newprivate', text: 'Contacts'}, - non_contacts: {ico: 'noncontacts', text: 'Non-Contacts'}, - groups: {ico: 'group', text: 'Groups'}, - broadcasts: {ico: 'newchannel', text: 'Channels'}, - bots: {ico: 'bots', text: 'Bots'} + contacts: {ico: 'newprivate', text: 'ChatList.Filter.Contacts'}, + non_contacts: {ico: 'noncontacts', text: 'ChatList.Filter.NonContacts'}, + groups: {ico: 'group', text: 'ChatList.Filter.Groups'}, + broadcasts: {ico: 'newchannel', text: 'ChatList.Filter.Channels'}, + bots: {ico: 'bots', text: 'ChatList.Filter.Bots'} }; } @@ -191,7 +192,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { const d = document.createElement('div'); d.classList.add('sidebar-left-h2'); - d.innerText = 'Chats'; + _i18n(d, 'ChatList.Add.BottomSeparator'); fragment.append(dd, categories, hr, d); @@ -210,7 +211,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { const _add = this.selector.add.bind(this.selector); this.selector.add = (peerId, title, scroll) => { - const div = _add(peerId, details[peerId]?.text, scroll); + const div = _add(peerId, details[peerId] ? i18n(details[peerId].text) : undefined, scroll); if(details[peerId]) { div.querySelector('avatar-element').classList.add('tgico-' + details[peerId].ico); } @@ -256,4 +257,4 @@ export default class AppIncludedChatsTab extends SliderSuperTab { return super.open(); } -} \ No newline at end of file +} diff --git a/src/components/sidebarLeft/tabs/language.ts b/src/components/sidebarLeft/tabs/language.ts new file mode 100644 index 00000000..d687649c --- /dev/null +++ b/src/components/sidebarLeft/tabs/language.ts @@ -0,0 +1,107 @@ +import { SettingSection } from ".."; +import { randomLong } from "../../../helpers/random"; +import I18n from "../../../lib/langPack"; +import RadioField from "../../radioField"; +import Row, { RadioFormFromRows } from "../../row"; +import { SliderSuperTab } from "../../slider" + +export default class AppLanguageTab extends SliderSuperTab { + protected init() { + this.container.classList.add('language-container'); + this.setTitle('Telegram.LanguageViewController'); + + const section = new SettingSection({}); + + const radioRows: Map = new Map(); + + let r = [{ + code: 'en', + text: 'English', + subtitle: 'English' + }, { + code: 'be', + text: 'Belarusian', + subtitle: 'Беларуская' + }, { + code: 'ca', + text: 'Catalan', + subtitle: 'Català' + }, { + code: 'nl', + text: 'Dutch', + subtitle: 'Nederlands' + }, { + code: 'fr', + text: 'French', + subtitle: 'Français' + }, { + code: 'de', + text: 'German', + subtitle: 'Deutsch' + }, { + code: 'it', + text: 'Italian', + subtitle: 'Italiano' + }, { + code: 'ms', + text: 'Malay', + subtitle: 'Bahasa Melayu' + }, { + code: 'pl', + text: 'Polish', + subtitle: 'Polski' + }, { + code: 'pt', + text: 'Portuguese (Brazil)', + subtitle: 'Português (Brasil)' + }, { + code: 'ru', + text: 'Russian', + subtitle: 'Русский' + }, { + code: 'es', + text: 'Spanish', + subtitle: 'Español' + }, { + code: 'tr', + text: 'Turkish', + subtitle: 'Türkçe' + }, { + code: 'uk', + text: 'Ukrainian', + subtitle: 'Українська' + }]; + + const random = randomLong(); + r.forEach(({code, text, subtitle}) => { + const row = new Row({ + radioField: new RadioField({ + text, + name: random, + value: code + }), + subtitle + }); + + radioRows.set(code, row); + }); + + const form = RadioFormFromRows([...radioRows.values()], (value) => { + I18n.getLangPack(value); + }); + + I18n.getCacheLangPack().then(langPack => { + const row = radioRows.get(langPack.lang_code); + if(!row) { + console.error('no row', row, langPack); + return; + } + + row.radioField.setValueSilently(true); + }); + + section.content.append(form); + + this.scrollable.append(section.container); + } +} diff --git a/src/components/sidebarLeft/tabs/settings.ts b/src/components/sidebarLeft/tabs/settings.ts index 221d32de..5abf1de6 100644 --- a/src/components/sidebarLeft/tabs/settings.ts +++ b/src/components/sidebarLeft/tabs/settings.ts @@ -10,6 +10,7 @@ import AppEditProfileTab from "./editProfile"; import AppChatFoldersTab from "./chatFolders"; import AppNotificationsTab from "./notifications"; import PeerTitle from "../../peerTitle"; +import AppLanguageTab from "./language"; //import AppMediaViewer from "../../appMediaViewerNew"; export default class AppSettingsTab extends SliderSuperTab { @@ -28,7 +29,7 @@ export default class AppSettingsTab extends SliderSuperTab { init() { this.container.classList.add('settings-container'); - this.title.innerText = 'Settings'; + this.setTitle('Settings'); const btnMenu = ButtonMenuToggle({}, 'bottom-left', [{ icon: 'logout', @@ -130,6 +131,10 @@ export default class AppSettingsTab extends SliderSuperTab { this.buttons.privacy.addEventListener('click', () => { new AppPrivacyAndSecurityTab(this.slider).open(); }); + + this.buttons.language.addEventListener('click', () => { + new AppLanguageTab(this.slider).open(); + }); } public fillElements() { diff --git a/src/components/slider.ts b/src/components/slider.ts index 31855a69..c84939fa 100644 --- a/src/components/slider.ts +++ b/src/components/slider.ts @@ -3,6 +3,7 @@ import { horizontalMenu } from "./horizontalMenu"; import { TransitionSlider } from "./transition"; import appNavigationController, { NavigationItem } from "./appNavigationController"; import SliderSuperTab, { SliderSuperTabConstructable, SliderTab } from "./sliderTab"; +import { safeAssign } from "../helpers/object"; const TRANSITION_TIME = 250; @@ -24,7 +25,7 @@ export default class SidebarSlider { canHideFirst?: SidebarSlider['canHideFirst'], navigationType: SidebarSlider['navigationType'] }) { - Object.assign(this, options); + safeAssign(this, options); if(!this.tabs) { this.tabs = new Map(); diff --git a/src/components/usernameInputField.ts b/src/components/usernameInputField.ts index a2d0e8bc..c1fb8e73 100644 --- a/src/components/usernameInputField.ts +++ b/src/components/usernameInputField.ts @@ -1,6 +1,7 @@ import ListenerSetter from "../helpers/listenerSetter"; import { debounce } from "../helpers/schedulers"; import appChatsManager from "../lib/appManagers/appChatsManager"; +import { LangPackKey } from "../lib/langPack"; import apiManager from "../lib/mtproto/mtprotoworker"; import RichTextProcessor from "../lib/richtextprocessor"; import InputField, { InputFieldOptions, InputState } from "./inputField"; @@ -12,9 +13,9 @@ export class UsernameInputField extends InputField { peerId: number, listenerSetter: ListenerSetter, onChange?: () => void, - invalidText: string, - takenText: string, - availableText: string, + invalidText: LangPackKey, + takenText: LangPackKey, + availableText: LangPackKey, head?: string }; diff --git a/src/helpers/listLoader.ts b/src/helpers/listLoader.ts index b938ea32..8196ef71 100644 --- a/src/helpers/listLoader.ts +++ b/src/helpers/listLoader.ts @@ -1,4 +1,5 @@ import Scrollable from "../components/scrollable"; +import { safeAssign } from "./object"; export default class ScrollableLoader { public loading = false; @@ -11,7 +12,7 @@ export default class ScrollableLoader { scrollable: ScrollableLoader['scrollable'], getPromise: ScrollableLoader['getPromise'] }) { - Object.assign(this, options); + safeAssign(this, options); options.scrollable.onScrolledBottom = () => { this.load(); diff --git a/src/helpers/object.ts b/src/helpers/object.ts index f8396428..14e4d812 100644 --- a/src/helpers/object.ts +++ b/src/helpers/object.ts @@ -120,3 +120,13 @@ export function validateInitObject(initObject: any, currentObject: any) { } } } + +export function safeAssign(object: any, fromObject: any) { + if(!fromObject) return; + + for(let i in fromObject) { + if(fromObject[i] !== undefined) { + object[i] = fromObject[i]; + } + } +} diff --git a/src/index.hbs b/src/index.hbs index 3f75e662..9f2a406c 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -99,16 +99,7 @@ diff --git a/src/lib/idb.ts b/src/lib/idb.ts index c73d5c2a..e09b7e74 100644 --- a/src/lib/idb.ts +++ b/src/lib/idb.ts @@ -1,5 +1,6 @@ import Database from '../config/database'; import { blobConstruct } from '../helpers/blob'; +import { safeAssign } from '../helpers/object'; import { logger } from './logger'; /** @@ -36,7 +37,7 @@ export default class IDBStorage { public storeName: string; constructor(options: IDBOptions) { - Object.assign(this, options); + safeAssign(this, options); this.openDatabase(true); } diff --git a/src/lib/langPack.ts b/src/lib/langPack.ts index 717df687..45e565bc 100644 --- a/src/lib/langPack.ts +++ b/src/lib/langPack.ts @@ -1,6 +1,8 @@ import { MOUNT_CLASS_TO } from "../config/debug"; -import { LangPackString } from "../layer"; +import { safeAssign } from "../helpers/object"; +import { LangPackDifference, LangPackString } from "../layer"; import apiManager from "./mtproto/mtprotoworker"; +import sessionStorage from "./sessionStorage"; export const langPack: {[actionType: string]: string} = { "messageActionChatCreate": "created the group", @@ -41,9 +43,15 @@ namespace Strings { export type AccountSettings = 'AccountSettings.Filters' | 'AccountSettings.Notifications' | 'AccountSettings.PrivacyAndSecurity' | 'AccountSettings.Language' | 'AccountSettings.Bio'; - export type Telegram = 'Telegram.GeneralSettingsViewController' | 'Telegram.NotificationSettingsViewController'; + export type Telegram = 'Telegram.GeneralSettingsViewController' | 'Telegram.NotificationSettingsViewController' | 'Telegram.LanguageViewController'; - export type ChatFilters = 'ChatList.Filter.Header' | 'ChatList.Filter.NewTitle' | 'ChatList.Filter.List.Header' | 'ChatList.Filter.Recommended.Header' | 'ChatList.Filter.Recommended.Add' | 'ChatList.Filter.List.Title'; + export type ChatList = ChatListFilter; + export type ChatListAdd = 'ChatList.Add.TopSeparator' | 'ChatList.Add.BottomSeparator'; + export type ChatListFilterIncluded = 'ChatList.Filter.Include.Header' | 'ChatList.Filter.Include.AddChat'; + export type ChatListFilterExcluded = 'ChatList.Filter.Exclude.Header' | 'ChatList.Filter.Exclude.AddChat'; + export type ChatListFilterList = 'ChatList.Filter.List.Header' | 'ChatList.Filter.List.Title'; + export type ChatListFilterRecommended = 'ChatList.Filter.Recommended.Header' | 'ChatList.Filter.Recommended.Add'; + export type ChatListFilter = ChatListAdd | ChatListFilterIncluded | ChatListFilterExcluded | ChatListFilterList | ChatListFilterRecommended | 'ChatList.Filter.Header' | 'ChatList.Filter.NewTitle' | 'ChatList.Filter.NonContacts' | 'ChatList.Filter.Contacts' | 'ChatList.Filter.Groups' | 'ChatList.Filter.Channels' | 'ChatList.Filter.Bots'; export type AutoDownloadSettings = 'AutoDownloadSettings.TypePrivateChats' | 'AutoDownloadSettings.TypeChannels'; @@ -51,37 +59,68 @@ namespace Strings { export type Suggest = 'Suggest.Localization.Other'; - export type LangPackKey = string | AccountSettings | EditAccount | Telegram | ChatFilters | LoginRegister | Bio | AutoDownloadSettings | DataAndStorage | Suggest; + export type UsernameSettings = 'UsernameSettings.ChangeDescription'; + + export type LangPackKey = string | AccountSettings | EditAccount | Telegram | ChatList | LoginRegister | Bio | AutoDownloadSettings | DataAndStorage | Suggest | UsernameSettings; } export type LangPackKey = Strings.LangPackKey; namespace I18n { - let strings: Partial<{[key in LangPackKey]: LangPackString}> = {}; + export const strings: Map = new Map(); + + let lastRequestedLangCode: string; + export function getCacheLangPack(): Promise { + return sessionStorage.get('langPack').then((langPack: LangPackDifference) => { + if(!langPack) { + return getLangPack('en'); + } + + if(!lastRequestedLangCode) { + lastRequestedLangCode = langPack.lang_code; + } + + applyLangPack(langPack); + return langPack; + }); + } export function getLangPack(langCode: string) { + lastRequestedLangCode = langCode; return apiManager.invokeApi('langpack.getLangPack', { lang_code: langCode, lang_pack: 'macos' }).then(langPack => { - strings = {}; - for(const string of langPack.strings) { - strings[string.key as LangPackKey] = string; - } + return sessionStorage.set({langPack}).then(() => { + applyLangPack(langPack); + return langPack; + }); + }); + } + + export function applyLangPack(langPack: LangPackDifference) { + if(langPack.lang_code !== lastRequestedLangCode) { + return; + } - const elements = Array.from(document.querySelectorAll(`.i18n`)) as HTMLElement[]; - elements.forEach(element => { - const instance = weakMap.get(element); + strings.clear(); - if(instance) { - instance.update(); - } - }); + for(const string of langPack.strings) { + strings.set(string.key as LangPackKey, string); + } + + const elements = Array.from(document.querySelectorAll(`.i18n`)) as HTMLElement[]; + elements.forEach(element => { + const instance = weakMap.get(element); + + if(instance) { + instance.update(); + } }); } export function getString(key: LangPackKey, args?: any[]) { - const str = strings[key]; + const str = strings.get(key); let out = ''; if(str) { @@ -103,7 +142,7 @@ namespace I18n { export type IntlElementOptions = { element?: HTMLElement, - property?: 'innerHTML' | 'placeholder' + property?: 'innerText' | 'innerHTML' | 'placeholder' key: LangPackKey, args?: any[] }; @@ -111,7 +150,7 @@ namespace I18n { public element: IntlElementOptions['element']; public key: IntlElementOptions['key']; public args: IntlElementOptions['args']; - public property: IntlElementOptions['property'] = 'innerHTML'; + public property: IntlElementOptions['property'] = 'innerText'; constructor(options: IntlElementOptions) { this.element = options.element || document.createElement('span'); @@ -122,9 +161,7 @@ namespace I18n { } public update(options?: IntlElementOptions) { - if(options) { - Object.assign(this, options); - } + safeAssign(this, options); (this.element as any)[this.property] = getString(this.key, this.args); } @@ -137,6 +174,10 @@ namespace I18n { export function i18n_(options: IntlElementOptions) { return new IntlElement(options).element; } + + export function _i18n(element: HTMLElement, key: LangPackKey, args?: any[], property?: IntlElementOptions['property']) { + return new IntlElement({element, key, args, property}).element; + } } export {I18n}; @@ -148,4 +189,7 @@ export {i18n}; const i18n_ = I18n.i18n_; export {i18n_}; +const _i18n = I18n._i18n; +export {_i18n}; + MOUNT_CLASS_TO && (MOUNT_CLASS_TO.I18n = I18n); diff --git a/src/lib/sessionStorage.ts b/src/lib/sessionStorage.ts index d5517773..b992aaae 100644 --- a/src/lib/sessionStorage.ts +++ b/src/lib/sessionStorage.ts @@ -1,4 +1,5 @@ import { MOUNT_CLASS_TO } from '../config/debug'; +import { LangPackDifference } from '../layer'; import type { State } from './appManagers/appStateManager'; import AppStorage from './storage'; @@ -19,6 +20,7 @@ const sessionStorage = new AppStorage<{ top: number } }, + langPack: LangPackDifference } & State>({ storeName: 'session' }); diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index c5723f3e..69629b3d 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -569,7 +569,7 @@ align-items: center; margin: 15px auto 1rem; border-radius: 30px; - padding: 0 12px; + padding: 0 24px 0 12px; display: flex; } @@ -581,8 +581,9 @@ .row { .btn-primary { height: 30px; + padding: 0 12px; font-size: 15px; - width: 52px; + width: auto; transition: width 0.2s; margin: 0; position: absolute;