diff --git a/src/components/chat/topbar.ts b/src/components/chat/topbar.ts index c01376b6..aa023d1a 100644 --- a/src/components/chat/topbar.ts +++ b/src/components/chat/topbar.ts @@ -35,13 +35,14 @@ import blurActiveElement from "../../helpers/dom/blurActiveElement"; import { cancelEvent } from "../../helpers/dom/cancelEvent"; import { attachClickEvent } from "../../helpers/dom/clickEvent"; import findUpTag from "../../helpers/dom/findUpTag"; -import { toast } from "../toast"; +import { toast, toastNew } from "../toast"; import replaceContent from "../../helpers/dom/replaceContent"; import { ChatFull } from "../../layer"; import PopupPickUser from "../popups/pickUser"; import PopupPeer from "../popups/peer"; import generateVerifiedIcon from "../generateVerifiedIcon"; import { fastRaf } from "../../helpers/schedulers"; +import AppEditContactTab from "../sidebarRight/tabs/editContact"; export default class ChatTopbar { public container: HTMLDivElement; @@ -273,6 +274,15 @@ export default class ChatTopbar { this.chat.selection.cancelSelection(); }, verify: () => this.chat.selection.isSelecting + }, { + icon: 'adduser', + text: 'AddContact', + onClick: () => { + const tab = new AppEditContactTab(this.appSidebarRight); + tab.peerId = this.peerId; + tab.open(); + }, + verify: () => this.peerId > 0 && !this.appUsersManager.isContact(this.peerId) }, { icon: 'forward', text: 'ShareContact', @@ -312,6 +322,46 @@ export default class ChatTopbar { }); }, verify: () => rootScope.myId !== this.peerId && this.peerId > 0 && this.appUsersManager.isContact(this.peerId) + }, { + icon: 'lock', + text: 'BlockUser', + onClick: () => { + new PopupPeer('', { + peerId: this.peerId, + titleLangKey: 'BlockUser', + descriptionLangKey: 'AreYouSureBlockContact2', + descriptionLangArgs: [new PeerTitle({peerId: this.peerId}).element], + buttons: [{ + langKey: 'BlockUser', + isDanger: true, + callback: () => { + this.appUsersManager.toggleBlock(this.peerId, true).then(value => { + if(value) { + toastNew({langPackKey: 'UserBlocked'}); + } + }); + } + }] + }).show(); + }, + verify: () => { + const userFull = this.appProfileManager.usersFull[this.peerId]; + return this.peerId > 0 && userFull && !userFull.pFlags?.blocked; + } + }, { + icon: 'lockoff', + text: 'Unblock', + onClick: () => { + this.appUsersManager.toggleBlock(this.peerId, false).then(value => { + if(value) { + toastNew({langPackKey: 'UserUnblocked'}); + } + }); + }, + verify: () => { + const userFull = this.appProfileManager.usersFull[this.peerId]; + return this.peerId > 0 && !!userFull?.pFlags?.blocked; + } }, { icon: 'delete danger', text: 'Delete', diff --git a/src/components/inputField.ts b/src/components/inputField.ts index 031af343..a0c89105 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -8,6 +8,7 @@ import simulateEvent from "../helpers/dom/dispatchEvent"; import findUpAttribute from "../helpers/dom/findUpAttribute"; import getRichValue from "../helpers/dom/getRichValue"; import isInputEmpty from "../helpers/dom/isInputEmpty"; +import selectElementContents from "../helpers/dom/selectElementContents"; import { i18n, LangPackKey, _i18n } from "../lib/langPack"; import RichTextProcessor from "../lib/richtextprocessor"; @@ -215,8 +216,14 @@ class InputField { } public select() { - if((this.input as HTMLInputElement).value) { // * avoid selecting whole empty field on iOS devices + if(!this.value) { // * avoid selecting whole empty field on iOS devices + return; + } + + if(this.options.plainText) { (this.input as HTMLInputElement).select(); // * select text + } else { + selectElementContents(this.input); } } @@ -278,9 +285,7 @@ class InputField { (!this.required || !isInputEmpty(this.input)); } - public setOriginalValue(value: InputField['originalValue'] = '', silent = false) { - this.originalValue = value; - + public setDraftValue(value = '', silent = false) { if(!this.options.plainText) { value = RichTextProcessor.wrapDraftText(value); } @@ -292,6 +297,11 @@ class InputField { } } + public setOriginalValue(value: InputField['originalValue'] = '', silent = false) { + this.originalValue = value; + this.setDraftValue(value, silent); + } + public setState(state: InputState, label?: LangPackKey) { if(label) { this.label.textContent = ''; diff --git a/src/components/sidebarRight/tabs/editContact.ts b/src/components/sidebarRight/tabs/editContact.ts index 666f6cd6..46b4598c 100644 --- a/src/components/sidebarRight/tabs/editContact.ts +++ b/src/components/sidebarRight/tabs/editContact.ts @@ -31,7 +31,8 @@ export default class AppEditContactTab extends SliderSuperTab { protected init() { this.container.classList.add('edit-peer-container', 'edit-contact-container'); - this.setTitle(this.peerId ? 'Edit' : 'AddContactTitle'); + const isNew = !appUsersManager.isContact(this.peerId); + this.setTitle(isNew ? 'AddContactTitle' : 'Edit'); { const section = new SettingSection({noDelimiter: true}); @@ -41,12 +42,13 @@ export default class AppEditContactTab extends SliderSuperTab { inputWrapper.classList.add('input-wrapper'); this.nameInputField = new InputField({ - label: 'EditProfile.FirstNameLabel', + label: 'FirstName', name: 'contact-name', - maxLength: 70 + maxLength: 70, + required: true }); this.lastNameInputField = new InputField({ - label: 'Login.Register.LastName.Placeholder', + label: 'LastName', name: 'contact-lastname', maxLength: 70 }); @@ -54,8 +56,13 @@ export default class AppEditContactTab extends SliderSuperTab { if(this.peerId) { const user = appUsersManager.getUser(this.peerId); - this.nameInputField.setOriginalValue(user.first_name); - this.lastNameInputField.setOriginalValue(user.last_name); + if(isNew) { + this.nameInputField.setDraftValue(user.first_name); + this.lastNameInputField.setDraftValue(user.last_name); + } else { + this.nameInputField.setOriginalValue(user.first_name); + this.lastNameInputField.setOriginalValue(user.last_name); + } } inputWrapper.append(this.nameInputField.container, this.lastNameInputField.container); @@ -97,13 +104,6 @@ export default class AppEditContactTab extends SliderSuperTab { } }); - const notificationsRow = new Row({ - checkboxField: notificationsCheckboxField - }); - - const enabled = !appNotificationsManager.isPeerLocalMuted(this.peerId, false); - notificationsCheckboxField.checked = enabled; - const profileNameDiv = document.createElement('div'); profileNameDiv.classList.add('profile-name'); profileNameDiv.append(new PeerTitle({ @@ -115,7 +115,30 @@ export default class AppEditContactTab extends SliderSuperTab { profileSubtitleDiv.classList.add('profile-subtitle'); profileSubtitleDiv.append(i18n('EditContact.OriginalName')); - section.content.append(div, profileNameDiv, profileSubtitleDiv, inputWrapper, notificationsRow.container); + section.content.append(div, profileNameDiv, profileSubtitleDiv, inputWrapper); + + if(!isNew) { + const notificationsRow = new Row({ + checkboxField: notificationsCheckboxField + }); + + const enabled = !appNotificationsManager.isPeerLocalMuted(this.peerId, false); + notificationsCheckboxField.checked = enabled; + + section.content.append(notificationsRow.container); + } else { + const user = appUsersManager.getUser(this.peerId); + + const phoneRow = new Row({ + icon: 'phone', + titleLangKey: user.phone ? undefined : 'MobileHidden', + title: user.phone ? appUsersManager.formatUserPhone(user.phone) : undefined, + subtitleLangKey: user.phone ? 'Phone' : 'MobileHiddenExceptionInfo', + subtitleLangArgs: user.phone ? undefined : [new PeerTitle({peerId: this.peerId}).element] + }); + + section.content.append(phoneRow.container); + } } else { section.content.append(inputWrapper); } @@ -133,7 +156,7 @@ export default class AppEditContactTab extends SliderSuperTab { }, {listenerSetter: this.listenerSetter}); } - if(this.peerId) { + if(!isNew) { const section = new SettingSection({ }); diff --git a/src/helpers/dom/selectElementContents.ts b/src/helpers/dom/selectElementContents.ts new file mode 100644 index 00000000..eff62b4e --- /dev/null +++ b/src/helpers/dom/selectElementContents.ts @@ -0,0 +1,8 @@ +// https://stackoverflow.com/a/6150060 +export default function selectElementContents(el: HTMLElement) { + const range = document.createRange(); + range.selectNodeContents(el); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); +} diff --git a/src/lang.ts b/src/lang.ts index 3f091e98..ea814e22 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -575,6 +575,15 @@ const lang = { "DiscardVoiceMessageTitle": "Discard Voice Message", "DiscardVoiceMessageDescription": "Are you sure you want to stop recording and discard your voice message?", "DiscardVoiceMessageAction": "Discard", + "AddContact": "Add to contacts", + "BlockUser": "Block user", + "MobileHidden": "Mobile hidden", + "MobileHiddenExceptionInfo": "Phone number will be visible once %1$s adds you as a contact.", + "FirstName": "First name (required)", + "LastName": "Last name (optional)", + "AreYouSureBlockContact2": "Are you sure you want to block **%1$s**?", + "UserBlocked": "User blocked", + "UserUnblocked": "User unblocked", // * macos "AccountSettings.Filters": "Chat Folders", diff --git a/src/lib/appManagers/appPeersManager.ts b/src/lib/appManagers/appPeersManager.ts index e5688f8c..1cfeefa4 100644 --- a/src/lib/appManagers/appPeersManager.ts +++ b/src/lib/appManagers/appPeersManager.ts @@ -37,13 +37,6 @@ const DialogColorsMap = [0, 7, 4, 1, 6, 3, 5]; export type PeerType = 'channel' | 'chat' | 'megagroup' | 'group' | 'saved'; export class AppPeersManager { - constructor() { - rootScope.addMultipleEventsListeners({ - updatePeerBlocked: (update) => { - rootScope.dispatchEvent('peer_block', {peerId: this.getPeerId(update.peer_id), blocked: update.blocked}); - } - }); - } /* public savePeerInstance(peerId: number, instance: any) { if(peerId < 0) appChatsManager.saveApiChat(instance); else appUsersManager.saveApiUser(instance); diff --git a/src/lib/appManagers/appProfileManager.ts b/src/lib/appManagers/appProfileManager.ts index 8c5601ae..3ec4243b 100644 --- a/src/lib/appManagers/appProfileManager.ts +++ b/src/lib/appManagers/appProfileManager.ts @@ -31,7 +31,7 @@ export type UserTyping = Partial<{userId: number, action: SendMessageAction, tim export class AppProfileManager { //private botInfos: any = {}; - private usersFull: {[id: string]: UserFull.userFull} = {}; + public usersFull: {[id: string]: UserFull.userFull} = {}; public chatsFull: {[id: string]: ChatFull} = {}; private fullPromises: {[peerId: string]: Promise} = {}; @@ -94,7 +94,9 @@ export class AppProfileManager { updateUserTyping: this.onUpdateUserTyping, updateChatUserTyping: this.onUpdateUserTyping, - updateChannelUserTyping: this.onUpdateUserTyping + updateChannelUserTyping: this.onUpdateUserTyping, + + updatePeerBlocked: this.onUpdatePeerBlocked }); rootScope.addEventListener('chat_update', (chatId) => { @@ -639,6 +641,19 @@ export class AppProfileManager { } }; + private onUpdatePeerBlocked = (update: Update.updatePeerBlocked) => { + const peerId = appPeersManager.getPeerId(update.peer_id); + if(peerId > 0) { + const userFull = this.usersFull[peerId]; + if(userFull) { + if(update.blocked) userFull.pFlags.blocked = true; + else delete userFull.pFlags.blocked; + } + } + + rootScope.dispatchEvent('peer_block', {peerId, blocked: update.blocked}); + }; + public getPeerTypings(peerId: number) { return this.typingsInPeer[peerId]; } diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index 86afb1db..681b8060 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -31,8 +31,6 @@ import appChatsManager from "./appChatsManager"; import appPeersManager from "./appPeersManager"; import appStateManager from "./appStateManager"; -// TODO: updateUserBlocked - export type User = MTUser.user; export type TopPeerType = 'correspondents' | 'bots_inline'; export type MyTopPeer = {id: number, rating: number}; diff --git a/src/scss/partials/_scrollable.scss b/src/scss/partials/_scrollable.scss index 820eb068..9859e43f 100644 --- a/src/scss/partials/_scrollable.scss +++ b/src/scss/partials/_scrollable.scss @@ -80,14 +80,14 @@ html:not(.is-safari):not(.is-ios) { &.scrollable-x { overflow-x: auto; - scrollbar-width: none; + scrollbar-width: thin; // Firefox only -ms-overflow-style: none; } &.scrollable-y { overflow-y: auto; overflow-y: overlay; - scrollbar-width: none; + scrollbar-width: thin; // Firefox only -ms-overflow-style: none; /* html.is-safari & {