Browse Source

Language tab

Save language
master
Eduard Kuzmenko 3 years ago
parent
commit
585eb78ea2
  1. 12
      src/components/appSelectPeers.ts
  2. 3
      src/components/editPeer.ts
  3. 9
      src/components/inputField.ts
  4. 3
      src/components/preloader.ts
  5. 109
      src/components/sidebarLeft/index.ts
  6. 36
      src/components/sidebarLeft/tabs/editFolder.ts
  7. 19
      src/components/sidebarLeft/tabs/editProfile.ts
  8. 31
      src/components/sidebarLeft/tabs/includedChats.ts
  9. 107
      src/components/sidebarLeft/tabs/language.ts
  10. 7
      src/components/sidebarLeft/tabs/settings.ts
  11. 3
      src/components/slider.ts
  12. 7
      src/components/usernameInputField.ts
  13. 3
      src/helpers/listLoader.ts
  14. 10
      src/helpers/object.ts
  15. 11
      src/index.hbs
  16. 3
      src/lib/idb.ts
  17. 88
      src/lib/langPack.ts
  18. 2
      src/lib/sessionStorage.ts
  19. 5
      src/scss/partials/_leftSidebar.scss

12
src/components/appSelectPeers.ts

@ -10,6 +10,7 @@ import Scrollable from "./scrollable";
import { FocusDirection } from "../helpers/fastSmoothScroll"; import { FocusDirection } from "../helpers/fastSmoothScroll";
import CheckboxField from "./checkboxField"; import CheckboxField from "./checkboxField";
import appProfileManager from "../lib/appManagers/appProfileManager"; import appProfileManager from "../lib/appManagers/appProfileManager";
import { safeAssign } from "../helpers/object";
type PeerType = 'contacts' | 'dialogs' | 'channelParticipants'; type PeerType = 'contacts' | 'dialogs' | 'channelParticipants';
@ -69,7 +70,7 @@ export default class AppSelectPeers {
rippleEnabled?: boolean, rippleEnabled?: boolean,
avatarSize?: AppSelectPeers['avatarSize'], avatarSize?: AppSelectPeers['avatarSize'],
}) { }) {
Object.assign(this, options); safeAssign(this, options);
this.container.classList.add('selector'); 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'); //console.trace('add');
this.selected.add(peerId); this.selected.add(peerId);
@ -467,7 +468,12 @@ export default class AppSelectPeers {
} }
if(title) { if(title) {
div.innerHTML = title; if(typeof(title) === 'string') {
div.innerHTML = title;
} else {
div.innerHTML = '';
div.append(title);
}
} }
div.insertAdjacentElement('afterbegin', avatarEl); div.insertAdjacentElement('afterbegin', avatarEl);

3
src/components/editPeer.ts

@ -4,6 +4,7 @@ import AvatarElement from "./avatar";
import InputField from "./inputField"; import InputField from "./inputField";
import ListenerSetter from "../helpers/listenerSetter"; import ListenerSetter from "../helpers/listenerSetter";
import Button from "./button"; import Button from "./button";
import { safeAssign } from "../helpers/object";
export default class EditPeer { export default class EditPeer {
public nextBtn: HTMLButtonElement; public nextBtn: HTMLButtonElement;
@ -23,7 +24,7 @@ export default class EditPeer {
listenerSetter: ListenerSetter, listenerSetter: ListenerSetter,
doNotEditAvatar?: boolean, doNotEditAvatar?: boolean,
}) { }) {
Object.assign(this, options); safeAssign(this, options);
this.nextBtn = Button('btn-circle btn-corner tgico-check'); this.nextBtn = Button('btn-circle btn-corner tgico-check');

9
src/components/inputField.ts

@ -1,7 +1,7 @@
import { getRichValue, isInputEmpty } from "../helpers/dom"; import { getRichValue, isInputEmpty } from "../helpers/dom";
import { debounce } from "../helpers/schedulers"; import { debounce } from "../helpers/schedulers";
import { checkRTL } from "../helpers/string"; import { checkRTL } from "../helpers/string";
import { i18n_, LangPackKey } from "../lib/langPack"; import { i18n, LangPackKey } from "../lib/langPack";
import RichTextProcessor from "../lib/richtextprocessor"; import RichTextProcessor from "../lib/richtextprocessor";
let init = () => { let init = () => {
@ -143,7 +143,7 @@ class InputField {
if(label) { if(label) {
this.label = document.createElement('label'); this.label = document.createElement('label');
i18n_({element: this.label, key: label}); this.label.append(i18n(label));
this.container.append(this.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) { if(label) {
this.label.innerHTML = label; this.label.innerHTML = '';
this.label.append(i18n(label));
} }
this.input.classList.toggle('error', !!(state & InputState.Error)); this.input.classList.toggle('error', !!(state & InputState.Error));

3
src/components/preloader.ts

@ -2,6 +2,7 @@ import { isInDOM, cancelEvent, attachClickEvent } from "../helpers/dom";
import { CancellablePromise } from "../helpers/cancellablePromise"; import { CancellablePromise } from "../helpers/cancellablePromise";
import SetTransition from "./singleTransition"; import SetTransition from "./singleTransition";
import { fastRaf } from "../helpers/schedulers"; import { fastRaf } from "../helpers/schedulers";
import { safeAssign } from "../helpers/object";
const TRANSITION_TIME = 200; const TRANSITION_TIME = 200;
@ -34,7 +35,7 @@ export default class ProgressivePreloader {
attachMethod: ProgressivePreloader['attachMethod'] attachMethod: ProgressivePreloader['attachMethod']
}>) { }>) {
if(options) { if(options) {
Object.assign(this, options); safeAssign(this, options);
} }
} }

109
src/components/sidebarLeft/index.ts

@ -24,6 +24,8 @@ import AppContactsTab from "./tabs/contacts";
import AppArchivedTab from "./tabs/archivedTab"; import AppArchivedTab from "./tabs/archivedTab";
import AppAddMembersTab from "./tabs/addMembers"; import AppAddMembersTab from "./tabs/addMembers";
import { i18n_, LangPackKey } from "../../lib/langPack"; 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'; export const LEFT_COLUMN_ACTIVE_CLASSNAME = 'is-left-column-shown';
@ -34,14 +36,6 @@ export class AppSidebarLeft extends SidebarSlider {
private inputSearch: InputSearch; private inputSearch: InputSearch;
private menuEl: HTMLElement; private menuEl: HTMLElement;
private buttons: {
newGroup: HTMLButtonElement,
contacts: HTMLButtonElement,
archived: HTMLButtonElement,
saved: HTMLButtonElement,
settings: HTMLButtonElement,
help: HTMLButtonElement
} = {} as any;
public archivedCount: HTMLSpanElement; public archivedCount: HTMLSpanElement;
private newBtnMenu: HTMLElement; private newBtnMenu: HTMLElement;
@ -68,58 +62,87 @@ export class AppSidebarLeft extends SidebarSlider {
const sidebarHeader = this.sidebarEl.querySelector('.item-main .sidebar-header'); const sidebarHeader = this.sidebarEl.querySelector('.item-main .sidebar-header');
sidebarHeader.append(this.inputSearch.container); 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.toolsBtn = this.sidebarEl.querySelector('.sidebar-tools-button') as HTMLButtonElement;
this.backBtn = this.sidebarEl.querySelector('.sidebar-back-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.menuEl = this.toolsBtn.querySelector('.btn-menu');
this.newBtnMenu = this.sidebarEl.querySelector('#new-menu'); this.newBtnMenu = this.sidebarEl.querySelector('#new-menu');
this.inputSearch.input.addEventListener('focus', () => this.initSearch(), {once: true}); this.inputSearch.input.addEventListener('focus', () => this.initSearch(), {once: true});
parseMenuButtonsTo(this.buttons, this.menuEl.children);
parseMenuButtonsTo(this.newButtons, this.newBtnMenu.firstElementChild.children); parseMenuButtonsTo(this.newButtons, this.newBtnMenu.firstElementChild.children);
this.archivedCount = this.buttons.archived.querySelector('.archived-count') as HTMLSpanElement; this.archivedCount = document.createElement('span');
this.archivedCount.className = 'archived-count badge badge-24 badge-gray';
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.newButtons.privateChat, this.buttons.contacts].forEach(btn => { btnArchive.element.append(this.archivedCount);
attachClickEvent(btn, (e) => {
new AppContactsTab(this).open();
});
});
attachClickEvent(this.buttons.settings, (e) => { attachClickEvent(this.newButtons.privateChat, onContactsClick);
new AppSettingsTab(this).open();
});
attachClickEvent(this.newButtons.channel, (e) => { attachClickEvent(this.newButtons.channel, (e) => {
new AppNewChannelTab(this).open(); new AppNewChannelTab(this).open();
}); });
[this.newButtons.group, this.buttons.newGroup].forEach(btn => { attachClickEvent(this.newButtons.group, onNewGroupClick);
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...'
});
});
});
rootScope.on('dialogs_archived_unread', (e) => { rootScope.on('dialogs_archived_unread', (e) => {
this.archivedCount.innerText = '' + formatNumber(e.count, 1); this.archivedCount.innerText = '' + formatNumber(e.count, 1);

36
src/components/sidebarLeft/tabs/editFolder.ts

@ -13,6 +13,7 @@ import ButtonMenuToggle from "../../buttonMenuToggle";
import { ButtonMenuItemOptions } from "../../buttonMenu"; import { ButtonMenuItemOptions } from "../../buttonMenu";
import Button from "../../button"; import Button from "../../button";
import AppIncludedChatsTab from "./includedChats"; import AppIncludedChatsTab from "./includedChats";
import { i18n, i18n_, LangPackKey } from "../../../lib/langPack";
const MAX_FOLDER_NAME_LENGTH = 12; const MAX_FOLDER_NAME_LENGTH = 12;
@ -38,7 +39,8 @@ export default class AppEditFolderTab extends SliderSuperTab {
this.container.classList.add('edit-folder-container'); this.container.classList.add('edit-folder-container');
this.caption = document.createElement('div'); this.caption = document.createElement('div');
this.caption.classList.add('caption'); this.caption.classList.add('caption');
this.caption.innerHTML = `Choose chats and types of chats that will<br>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 = document.createElement('div');
this.stickerContainer.classList.add('sticker-container'); this.stickerContainer.classList.add('sticker-container');
@ -72,13 +74,13 @@ export default class AppEditFolderTab extends SliderSuperTab {
inputWrapper.append(this.nameInputField.container); 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'); const container = document.createElement('div');
container.classList.add('folder-list', className); container.classList.add('folder-list', className);
const h2 = document.createElement('div'); const h2 = document.createElement('div');
h2.classList.add('sidebar-left-h2'); h2.classList.add('sidebar-left-h2');
h2.innerHTML = h2Text; i18n_({element: h2, key: h2Text});
const categories = document.createElement('div'); const categories = document.createElement('div');
categories.classList.add('folder-categories'); categories.classList.add('folder-categories');
@ -102,46 +104,46 @@ export default class AppEditFolderTab extends SliderSuperTab {
return container; 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', icon: 'add primary',
text: 'Add Chats', text: 'ChatList.Filter.Include.AddChat',
withRipple: true withRipple: true
}, { }, {
text: 'Contacts', text: 'ChatList.Filter.Contacts',
icon: 'newprivate', icon: 'newprivate',
name: 'contacts' name: 'contacts'
}, { }, {
text: 'Non-Contacts', text: 'ChatList.Filter.NonContacts',
icon: 'noncontacts', icon: 'noncontacts',
name: 'non_contacts' name: 'non_contacts'
}, { }, {
text: 'Groups', text: 'ChatList.Filter.Groups',
icon: 'group', icon: 'group',
name: 'groups' name: 'groups'
}, { }, {
text: 'Channels', text: 'ChatList.Filter.Channels',
icon: 'channel', icon: 'channel',
name: 'broadcasts' name: 'broadcasts'
}, { }, {
text: 'Bots', text: 'ChatList.Filter.Bots',
icon: 'bots', icon: 'bots',
name: 'bots' name: 'bots'
}], this.flags); }], 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', icon: 'minus primary',
text: 'Remove Chats', text: 'ChatList.Filter.Exclude.AddChat',
withRipple: true withRipple: true
}, { }, {
text: 'Muted', text: 'ChatList.Filter.MutedChats',
icon: 'mute', icon: 'mute',
name: 'exclude_muted' name: 'exclude_muted'
}, { }, {
text: 'Archived', text: 'ChatList.Filter.Archive',
icon: 'archive', icon: 'archive',
name: 'exclude_archived' name: 'exclude_archived'
}, { }, {
text: 'Read', text: 'ChatList.Filter.ReadChats',
icon: 'readchats', icon: 'readchats',
name: 'exclude_read' name: 'exclude_read'
}], this.flags); }], this.flags);
@ -225,7 +227,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
private onCreateOpen() { private onCreateOpen() {
this.caption.style.display = ''; this.caption.style.display = '';
this.title.innerText = 'New Folder'; this.setTitle('New Folder');
this.menuBtn.classList.add('hide'); this.menuBtn.classList.add('hide');
this.confirmBtn.classList.remove('hide'); this.confirmBtn.classList.remove('hide');
this.nameInputField.value = ''; this.nameInputField.value = '';
@ -238,7 +240,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
private onEditOpen() { private onEditOpen() {
this.caption.style.display = 'none'; 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') { if(this.type === 'edit') {
this.menuBtn.classList.remove('hide'); this.menuBtn.classList.remove('hide');

19
src/components/sidebarLeft/tabs/editProfile.ts

@ -5,7 +5,7 @@ import { SliderSuperTab } from "../../slider";
import { attachClickEvent } from "../../../helpers/dom"; import { attachClickEvent } from "../../../helpers/dom";
import EditPeer from "../../editPeer"; import EditPeer from "../../editPeer";
import { UsernameInputField } from "../../usernameInputField"; import { UsernameInputField } from "../../usernameInputField";
import { i18n_ } from "../../../lib/langPack"; import { i18n, i18n_ } from "../../../lib/langPack";
// TODO: аватарка не поменяется в этой вкладке после изменения почему-то (если поставить в другом клиенте, и потом тут проверить, для этого ещё вышел в чатлист) // TODO: аватарка не поменяется в этой вкладке после изменения почему-то (если поставить в другом клиенте, и потом тут проверить, для этого ещё вышел в чатлист)
@ -93,8 +93,21 @@ export default class AppEditProfileTab extends SliderSuperTab {
const caption = document.createElement('div'); const caption = document.createElement('div');
caption.classList.add('caption'); 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.<br><br>You can use a-z, 0-9 and underscores. Minimum length is 5 characters.<br><br><div class="profile-url-container">This link opens a chat with you: caption.append(i18n('UsernameSettings.ChangeDescription'));
<br><a class="profile-url" href="#" target="_blank"></a></div>`; 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.profileUrlContainer = caption.querySelector('.profile-url-container');
this.profileUrlAnchor = this.profileUrlContainer.lastElementChild as HTMLAnchorElement; this.profileUrlAnchor = this.profileUrlContainer.lastElementChild as HTMLAnchorElement;

31
src/components/sidebarLeft/tabs/includedChats.ts

@ -10,6 +10,7 @@ import ButtonIcon from "../../buttonIcon";
import CheckboxField from "../../checkboxField"; import CheckboxField from "../../checkboxField";
import Button from "../../button"; import Button from "../../button";
import AppEditFolderTab from "./editFolder"; import AppEditFolderTab from "./editFolder";
import { i18n, LangPackKey, _i18n } from "../../../lib/langPack";
export default class AppIncludedChatsTab extends SliderSuperTab { export default class AppIncludedChatsTab extends SliderSuperTab {
private editFolderTab: AppEditFolderTab; private editFolderTab: AppEditFolderTab;
@ -123,7 +124,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
dom.containerEl.append(this.checkbox(selected)); dom.containerEl.append(this.checkbox(selected));
if(selected) dom.listEl.classList.add('active'); if(selected) dom.listEl.classList.add('active');
let subtitle = ''; let subtitle: LangPackKey;
if(peerId > 0) { if(peerId > 0) {
if(peerId === rootScope.myId) { if(peerId === rootScope.myId) {
@ -137,7 +138,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
subtitle = appPeersManager.isBroadcast(peerId) ? 'Channel' : 'Group'; 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.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 filter = this.filter;
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
const dd = document.createElement('div'); const dd = document.createElement('div');
dd.classList.add('sidebar-left-h2'); dd.classList.add('sidebar-left-h2');
dd.innerText = 'Chat types'; _i18n(dd, 'ChatList.Add.TopSeparator');
const categories = document.createElement('div'); const categories = document.createElement('div');
categories.classList.add('folder-categories'); categories.classList.add('folder-categories');
@ -163,17 +164,17 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
let details: {[flag: string]: {ico: string, text: string}}; let details: {[flag: string]: {ico: string, text: string}};
if(this.type === 'excluded') { if(this.type === 'excluded') {
details = { details = {
exclude_muted: {ico: 'mute', text: 'Muted'}, exclude_muted: {ico: 'mute', text: 'ChatList.Filter.MutedChats'},
exclude_archived: {ico: 'archive', text: 'Archived'}, exclude_archived: {ico: 'archive', text: 'ChatList.Filter.Archive'},
exclude_read: {ico: 'readchats', text: 'Read'} exclude_read: {ico: 'readchats', text: 'ChatList.Filter.ReadChats'}
}; };
} else { } else {
details = { details = {
contacts: {ico: 'newprivate', text: 'Contacts'}, contacts: {ico: 'newprivate', text: 'ChatList.Filter.Contacts'},
non_contacts: {ico: 'noncontacts', text: 'Non-Contacts'}, non_contacts: {ico: 'noncontacts', text: 'ChatList.Filter.NonContacts'},
groups: {ico: 'group', text: 'Groups'}, groups: {ico: 'group', text: 'ChatList.Filter.Groups'},
broadcasts: {ico: 'newchannel', text: 'Channels'}, broadcasts: {ico: 'newchannel', text: 'ChatList.Filter.Channels'},
bots: {ico: 'bots', text: 'Bots'} bots: {ico: 'bots', text: 'ChatList.Filter.Bots'}
}; };
} }
@ -191,7 +192,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
const d = document.createElement('div'); const d = document.createElement('div');
d.classList.add('sidebar-left-h2'); d.classList.add('sidebar-left-h2');
d.innerText = 'Chats'; _i18n(d, 'ChatList.Add.BottomSeparator');
fragment.append(dd, categories, hr, d); fragment.append(dd, categories, hr, d);
@ -210,7 +211,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
const _add = this.selector.add.bind(this.selector); const _add = this.selector.add.bind(this.selector);
this.selector.add = (peerId, title, scroll) => { 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]) { if(details[peerId]) {
div.querySelector('avatar-element').classList.add('tgico-' + details[peerId].ico); div.querySelector('avatar-element').classList.add('tgico-' + details[peerId].ico);
} }
@ -256,4 +257,4 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
return super.open(); return super.open();
} }
} }

107
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<string, Row> = 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);
}
}

7
src/components/sidebarLeft/tabs/settings.ts

@ -10,6 +10,7 @@ import AppEditProfileTab from "./editProfile";
import AppChatFoldersTab from "./chatFolders"; import AppChatFoldersTab from "./chatFolders";
import AppNotificationsTab from "./notifications"; import AppNotificationsTab from "./notifications";
import PeerTitle from "../../peerTitle"; import PeerTitle from "../../peerTitle";
import AppLanguageTab from "./language";
//import AppMediaViewer from "../../appMediaViewerNew"; //import AppMediaViewer from "../../appMediaViewerNew";
export default class AppSettingsTab extends SliderSuperTab { export default class AppSettingsTab extends SliderSuperTab {
@ -28,7 +29,7 @@ export default class AppSettingsTab extends SliderSuperTab {
init() { init() {
this.container.classList.add('settings-container'); this.container.classList.add('settings-container');
this.title.innerText = 'Settings'; this.setTitle('Settings');
const btnMenu = ButtonMenuToggle({}, 'bottom-left', [{ const btnMenu = ButtonMenuToggle({}, 'bottom-left', [{
icon: 'logout', icon: 'logout',
@ -130,6 +131,10 @@ export default class AppSettingsTab extends SliderSuperTab {
this.buttons.privacy.addEventListener('click', () => { this.buttons.privacy.addEventListener('click', () => {
new AppPrivacyAndSecurityTab(this.slider).open(); new AppPrivacyAndSecurityTab(this.slider).open();
}); });
this.buttons.language.addEventListener('click', () => {
new AppLanguageTab(this.slider).open();
});
} }
public fillElements() { public fillElements() {

3
src/components/slider.ts

@ -3,6 +3,7 @@ import { horizontalMenu } from "./horizontalMenu";
import { TransitionSlider } from "./transition"; import { TransitionSlider } from "./transition";
import appNavigationController, { NavigationItem } from "./appNavigationController"; import appNavigationController, { NavigationItem } from "./appNavigationController";
import SliderSuperTab, { SliderSuperTabConstructable, SliderTab } from "./sliderTab"; import SliderSuperTab, { SliderSuperTabConstructable, SliderTab } from "./sliderTab";
import { safeAssign } from "../helpers/object";
const TRANSITION_TIME = 250; const TRANSITION_TIME = 250;
@ -24,7 +25,7 @@ export default class SidebarSlider {
canHideFirst?: SidebarSlider['canHideFirst'], canHideFirst?: SidebarSlider['canHideFirst'],
navigationType: SidebarSlider['navigationType'] navigationType: SidebarSlider['navigationType']
}) { }) {
Object.assign(this, options); safeAssign(this, options);
if(!this.tabs) { if(!this.tabs) {
this.tabs = new Map(); this.tabs = new Map();

7
src/components/usernameInputField.ts

@ -1,6 +1,7 @@
import ListenerSetter from "../helpers/listenerSetter"; import ListenerSetter from "../helpers/listenerSetter";
import { debounce } from "../helpers/schedulers"; import { debounce } from "../helpers/schedulers";
import appChatsManager from "../lib/appManagers/appChatsManager"; import appChatsManager from "../lib/appManagers/appChatsManager";
import { LangPackKey } from "../lib/langPack";
import apiManager from "../lib/mtproto/mtprotoworker"; import apiManager from "../lib/mtproto/mtprotoworker";
import RichTextProcessor from "../lib/richtextprocessor"; import RichTextProcessor from "../lib/richtextprocessor";
import InputField, { InputFieldOptions, InputState } from "./inputField"; import InputField, { InputFieldOptions, InputState } from "./inputField";
@ -12,9 +13,9 @@ export class UsernameInputField extends InputField {
peerId: number, peerId: number,
listenerSetter: ListenerSetter, listenerSetter: ListenerSetter,
onChange?: () => void, onChange?: () => void,
invalidText: string, invalidText: LangPackKey,
takenText: string, takenText: LangPackKey,
availableText: string, availableText: LangPackKey,
head?: string head?: string
}; };

3
src/helpers/listLoader.ts

@ -1,4 +1,5 @@
import Scrollable from "../components/scrollable"; import Scrollable from "../components/scrollable";
import { safeAssign } from "./object";
export default class ScrollableLoader { export default class ScrollableLoader {
public loading = false; public loading = false;
@ -11,7 +12,7 @@ export default class ScrollableLoader {
scrollable: ScrollableLoader['scrollable'], scrollable: ScrollableLoader['scrollable'],
getPromise: ScrollableLoader['getPromise'] getPromise: ScrollableLoader['getPromise']
}) { }) {
Object.assign(this, options); safeAssign(this, options);
options.scrollable.onScrolledBottom = () => { options.scrollable.onScrolledBottom = () => {
this.load(); this.load();

10
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];
}
}
}

11
src/index.hbs

@ -99,16 +99,7 @@
<div class="sidebar-header"> <div class="sidebar-header">
<div class="sidebar-header__btn-container"> <div class="sidebar-header__btn-container">
<div class="animated-menu-icon"></div> <div class="animated-menu-icon"></div>
<div class="btn-icon btn-menu-toggle rp sidebar-tools-button is-visible"> <div class="btn-icon btn-menu-toggle rp sidebar-tools-button is-visible"></div>
<div class="btn-menu bottom-right">
<div class="btn-menu-item menu-newGroup tgico-newgroup rp">New Group</div>
<div class="btn-menu-item menu-contacts tgico-user rp">Contacts</div>
<div class="btn-menu-item menu-archived tgico-archive rp">Archived <span class="archived-count badge badge-24 badge-gray"></span></div>
<div class="btn-menu-item menu-saved tgico-savedmessages rp">Saved</div>
<div class="btn-menu-item menu-settings tgico-settings rp">Settings</div>
<div class="btn-menu-item menu-help tgico-help rp btn-disabled">Help</div>
</div>
</div>
<div class="btn-icon rp sidebar-back-button"></div> <div class="btn-icon rp sidebar-back-button"></div>
</div> </div>
</div> </div>

3
src/lib/idb.ts

@ -1,5 +1,6 @@
import Database from '../config/database'; import Database from '../config/database';
import { blobConstruct } from '../helpers/blob'; import { blobConstruct } from '../helpers/blob';
import { safeAssign } from '../helpers/object';
import { logger } from './logger'; import { logger } from './logger';
/** /**
@ -36,7 +37,7 @@ export default class IDBStorage {
public storeName: string; public storeName: string;
constructor(options: IDBOptions) { constructor(options: IDBOptions) {
Object.assign(this, options); safeAssign(this, options);
this.openDatabase(true); this.openDatabase(true);
} }

88
src/lib/langPack.ts

@ -1,6 +1,8 @@
import { MOUNT_CLASS_TO } from "../config/debug"; 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 apiManager from "./mtproto/mtprotoworker";
import sessionStorage from "./sessionStorage";
export const langPack: {[actionType: string]: string} = { export const langPack: {[actionType: string]: string} = {
"messageActionChatCreate": "created the group", "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 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'; export type AutoDownloadSettings = 'AutoDownloadSettings.TypePrivateChats' | 'AutoDownloadSettings.TypeChannels';
@ -51,37 +59,68 @@ namespace Strings {
export type Suggest = 'Suggest.Localization.Other'; 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; export type LangPackKey = Strings.LangPackKey;
namespace I18n { namespace I18n {
let strings: Partial<{[key in LangPackKey]: LangPackString}> = {}; export const strings: Map<LangPackKey, LangPackString> = new Map();
let lastRequestedLangCode: string;
export function getCacheLangPack(): Promise<LangPackDifference> {
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) { export function getLangPack(langCode: string) {
lastRequestedLangCode = langCode;
return apiManager.invokeApi('langpack.getLangPack', { return apiManager.invokeApi('langpack.getLangPack', {
lang_code: langCode, lang_code: langCode,
lang_pack: 'macos' lang_pack: 'macos'
}).then(langPack => { }).then(langPack => {
strings = {}; return sessionStorage.set({langPack}).then(() => {
for(const string of langPack.strings) { applyLangPack(langPack);
strings[string.key as LangPackKey] = string; return langPack;
} });
});
}
export function applyLangPack(langPack: LangPackDifference) {
if(langPack.lang_code !== lastRequestedLangCode) {
return;
}
const elements = Array.from(document.querySelectorAll(`.i18n`)) as HTMLElement[]; strings.clear();
elements.forEach(element => {
const instance = weakMap.get(element);
if(instance) { for(const string of langPack.strings) {
instance.update(); 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[]) { export function getString(key: LangPackKey, args?: any[]) {
const str = strings[key]; const str = strings.get(key);
let out = ''; let out = '';
if(str) { if(str) {
@ -103,7 +142,7 @@ namespace I18n {
export type IntlElementOptions = { export type IntlElementOptions = {
element?: HTMLElement, element?: HTMLElement,
property?: 'innerHTML' | 'placeholder' property?: 'innerText' | 'innerHTML' | 'placeholder'
key: LangPackKey, key: LangPackKey,
args?: any[] args?: any[]
}; };
@ -111,7 +150,7 @@ namespace I18n {
public element: IntlElementOptions['element']; public element: IntlElementOptions['element'];
public key: IntlElementOptions['key']; public key: IntlElementOptions['key'];
public args: IntlElementOptions['args']; public args: IntlElementOptions['args'];
public property: IntlElementOptions['property'] = 'innerHTML'; public property: IntlElementOptions['property'] = 'innerText';
constructor(options: IntlElementOptions) { constructor(options: IntlElementOptions) {
this.element = options.element || document.createElement('span'); this.element = options.element || document.createElement('span');
@ -122,9 +161,7 @@ namespace I18n {
} }
public update(options?: IntlElementOptions) { public update(options?: IntlElementOptions) {
if(options) { safeAssign(this, options);
Object.assign(this, options);
}
(this.element as any)[this.property] = getString(this.key, this.args); (this.element as any)[this.property] = getString(this.key, this.args);
} }
@ -137,6 +174,10 @@ namespace I18n {
export function i18n_(options: IntlElementOptions) { export function i18n_(options: IntlElementOptions) {
return new IntlElement(options).element; 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}; export {I18n};
@ -148,4 +189,7 @@ export {i18n};
const i18n_ = I18n.i18n_; const i18n_ = I18n.i18n_;
export {i18n_}; export {i18n_};
const _i18n = I18n._i18n;
export {_i18n};
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.I18n = I18n); MOUNT_CLASS_TO && (MOUNT_CLASS_TO.I18n = I18n);

2
src/lib/sessionStorage.ts

@ -1,4 +1,5 @@
import { MOUNT_CLASS_TO } from '../config/debug'; import { MOUNT_CLASS_TO } from '../config/debug';
import { LangPackDifference } from '../layer';
import type { State } from './appManagers/appStateManager'; import type { State } from './appManagers/appStateManager';
import AppStorage from './storage'; import AppStorage from './storage';
@ -19,6 +20,7 @@ const sessionStorage = new AppStorage<{
top: number top: number
} }
}, },
langPack: LangPackDifference
} & State>({ } & State>({
storeName: 'session' storeName: 'session'
}); });

5
src/scss/partials/_leftSidebar.scss

@ -569,7 +569,7 @@
align-items: center; align-items: center;
margin: 15px auto 1rem; margin: 15px auto 1rem;
border-radius: 30px; border-radius: 30px;
padding: 0 12px; padding: 0 24px 0 12px;
display: flex; display: flex;
} }
@ -581,8 +581,9 @@
.row { .row {
.btn-primary { .btn-primary {
height: 30px; height: 30px;
padding: 0 12px;
font-size: 15px; font-size: 15px;
width: 52px; width: auto;
transition: width 0.2s; transition: width 0.2s;
margin: 0; margin: 0;
position: absolute; position: absolute;

Loading…
Cancel
Save