Browse Source

Temp

master
Eduard Kuzmenko 3 years ago
parent
commit
7c73101ef4
  1. 8
      src/components/inputField.ts
  2. 7
      src/components/popups/confirmAction.ts
  3. 15
      src/components/popups/index.ts
  4. 7
      src/components/popups/pickUser.ts
  5. 65
      src/components/privacySection.ts
  6. 7
      src/components/radioField.ts
  7. 9
      src/components/sidebarLeft/index.ts
  8. 23
      src/components/sidebarLeft/tabs/activeSessions.ts
  9. 2
      src/components/sidebarLeft/tabs/archivedTab.ts
  10. 4
      src/components/sidebarLeft/tabs/blockedUsers.ts
  11. 2
      src/components/sidebarLeft/tabs/generalSettings.ts
  12. 17
      src/components/sidebarLeft/tabs/includedChats.ts
  13. 6
      src/components/sidebarLeft/tabs/notifications.ts
  14. 9
      src/components/sidebarLeft/tabs/privacy/addToGroups.ts
  15. 15
      src/components/sidebarLeft/tabs/privacy/calls.ts
  16. 9
      src/components/sidebarLeft/tabs/privacy/forwardMessages.ts
  17. 9
      src/components/sidebarLeft/tabs/privacy/lastSeen.ts
  18. 9
      src/components/sidebarLeft/tabs/privacy/profilePhoto.ts
  19. 19
      src/components/sidebarLeft/tabs/privacyAndSecurity.ts
  20. 61
      src/lang.ts
  21. 2
      src/scss/partials/_chatlist.scss
  22. 1
      src/scss/partials/popups/_peer.scss
  23. 381
      src/vendor/dateFormat.ts

8
src/components/inputField.ts

@ -163,10 +163,12 @@ class InputField { @@ -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 { @@ -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);
}
}

7
src/components/popups/confirmAction.ts

@ -1,17 +1,18 @@ @@ -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);
}

15
src/components/popups/index.ts

@ -3,10 +3,13 @@ import { blurActiveElement, findUpClassName } from "../../helpers/dom"; @@ -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 { @@ -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[]) => { @@ -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
});
}

7
src/components/popups/pickUser.ts

@ -21,16 +21,17 @@ export default class PopupPickUser extends PopupElement { @@ -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: () => {

65
src/components/privacySection.ts

@ -2,6 +2,7 @@ import { randomLong } from "../helpers/random"; @@ -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"; @@ -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<PrivacyType, Row>;
public radioSection: SettingSection;
public exceptions: Map<keyof PrivacySection['peerIds'], {
title: string,
titleLangKey: LangPackKey,
key: keyof PrivacySection['peerIds'],
row: Row,
icon: string,
subtitle: string,
subtitleLangKey: LangPackKey,
clickable: true
}>;
public peerIds: {
@ -28,32 +30,32 @@ export default class PrivacySection { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
}
}
}

7
src/components/radioField.ts

@ -8,7 +8,8 @@ export default class RadioField { @@ -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 { @@ -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 { @@ -49,6 +50,8 @@ export default class RadioField {
}
main.append(caption); */
} else if(options.langKey) {
_i18n(main, options.langKey);
}
label.append(input, main);

9
src/components/sidebarLeft/index.ts

@ -437,7 +437,7 @@ export class SettingSection { @@ -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 { @@ -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 { @@ -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;

23
src/components/sidebarLeft/tabs/activeSessions.ts

@ -11,6 +11,7 @@ import PopupConfirmAction from "../../popups/confirmAction"; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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();
};

2
src/components/sidebarLeft/tabs/archivedTab.ts

@ -8,7 +8,7 @@ export default class AppArchivedTab extends SliderSuperTab { @@ -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);

4
src/components/sidebarLeft/tabs/blockedUsers.ts

@ -15,11 +15,11 @@ export default class AppBlockedUsersTab extends SliderSuperTab { @@ -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);

2
src/components/sidebarLeft/tabs/generalSettings.ts

@ -91,7 +91,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { @@ -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'

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

@ -96,7 +96,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { @@ -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 { @@ -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); */
});
};

6
src/components/sidebarLeft/tabs/notifications.ts

@ -79,19 +79,19 @@ export default class AppNotificationsTab extends SliderSuperTabEventable { @@ -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'
});

9
src/components/sidebarLeft/tabs/privacy/addToGroups.ts

@ -1,18 +1,19 @@ @@ -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
});
}

15
src/components/sidebarLeft/tabs/privacy/calls.ts

@ -1,29 +1,30 @@ @@ -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
});
}

9
src/components/sidebarLeft/tabs/privacy/forwardMessages.ts

@ -1,18 +1,19 @@ @@ -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
});
}

9
src/components/sidebarLeft/tabs/privacy/lastSeen.ts

@ -1,18 +1,19 @@ @@ -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.<br>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
});
}

9
src/components/sidebarLeft/tabs/privacy/profilePhoto.ts

@ -1,19 +1,20 @@ @@ -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]
});

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

@ -19,7 +19,7 @@ import AppBlockedUsersTab from "./blockedUsers"; @@ -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 { @@ -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 { @@ -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 { @@ -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(', ')})`);
}
});
};

61
src/lang.ts

@ -39,11 +39,6 @@ const lang = { @@ -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 = { @@ -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 = { @@ -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 = { @@ -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;

2
src/scss/partials/_chatlist.scss

@ -152,7 +152,7 @@ ul.chatlist { @@ -152,7 +152,7 @@ ul.chatlist {
}
span {
display: inline-block;
//display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

1
src/scss/partials/popups/_peer.scss

@ -17,7 +17,6 @@ @@ -17,7 +17,6 @@
font-size: 1.25rem;
font-weight: 500;
margin-bottom: .125rem;
text-transform: capitalize;
&:not(:first-child) {
padding-left: .75rem;

381
src/vendor/dateFormat.ts vendored

@ -0,0 +1,381 @@ @@ -0,0 +1,381 @@
/*
* Date Format 1.2.3
* (c) 2007-2009 Steven Levithan <stevenlevithan.com>
* MIT license
*
* Includes enhancements by Scott Trenda <scott.trenda.net>
* and Kris Kowal <cixar.com/~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();
}; */
Loading…
Cancel
Save