diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index eac63887..85daf28a 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -44,8 +44,8 @@ export default class AppSelectPeers { private appendTo: HTMLElement; private onChange: (length: number) => void; private peerType: PeerType[] = ['dialogs']; - private renderResultsFunc?: (peerIds: number[]) => void; - private chatRightsAction?: ChatRights; + private renderResultsFunc: (peerIds: number[]) => void; + private chatRightsAction: ChatRights; private multiSelect = true; private rippleEnabled = true; diff --git a/src/components/popups/forward.ts b/src/components/popups/forward.ts index bc018f41..dbc4f261 100644 --- a/src/components/popups/forward.ts +++ b/src/components/popups/forward.ts @@ -1,48 +1,24 @@ -import { isTouchSupported } from "../../helpers/touchSupport"; import appImManager from "../../lib/appManagers/appImManager"; -import AppSelectPeers from "../appSelectPeers"; -import PopupElement from "."; +import PopupPickUser from "./pickUser"; -export default class PopupForward extends PopupElement { - private selector: AppSelectPeers; - //private scrollable: Scrollable; - +export default class PopupForward extends PopupPickUser { constructor(fromPeerId: number, mids: number[], onSelect?: () => Promise | void, onClose?: () => void) { - super('popup-forward', null, {closable: true, overlayClosable: true, body: true}); - - if(onClose) this.onClose = onClose; - - this.selector = new AppSelectPeers({ - appendTo: this.body, - onChange: async() => { - const peerId = this.selector.getSelected()[0]; - this.btnClose.click(); - - this.selector = null; - - await (onSelect ? onSelect() || Promise.resolve() : Promise.resolve()); + super({ + peerTypes: ['dialogs', 'contacts'], + onSelect: async(peerId) => { + if(onSelect) { + const res = onSelect(); + if(res instanceof Promise) { + await res; + } + } appImManager.setInnerPeer(peerId); appImManager.chat.input.initMessagesForward(fromPeerId, mids.slice()); - }, - peerType: ['dialogs', 'contacts'], - onFirstRender: () => { - this.show(); - this.selector.checkForTriggers(); // ! due to zero height before mounting - - if(!isTouchSupported) { - this.selector.input.focus(); - } - }, - chatRightsAction: 'send', - multiSelect: false, - rippleEnabled: false + }, + onClose, + placeholder: 'Forward to...', + chatRightsAction: 'send' }); - - //this.scrollable = new Scrollable(this.body); - - this.selector.input.placeholder = 'Forward to...'; - this.title.append(this.selector.input); } - -} \ No newline at end of file +} diff --git a/src/components/popups/pickUser.ts b/src/components/popups/pickUser.ts new file mode 100644 index 00000000..80f40759 --- /dev/null +++ b/src/components/popups/pickUser.ts @@ -0,0 +1,53 @@ +import { isTouchSupported } from "../../helpers/touchSupport"; +import AppSelectPeers from "../appSelectPeers"; +import PopupElement from "."; + +export default class PopupPickUser extends PopupElement { + protected selector: AppSelectPeers; + + constructor(options: { + peerTypes: AppSelectPeers['peerType'], + onSelect?: (peerId: number) => Promise | void, + onClose?: () => void, + placeholder: string, + chatRightsAction?: AppSelectPeers['chatRightsAction'] + }) { + super('popup-forward', null, {closable: true, overlayClosable: true, body: true}); + + if(options.onClose) this.onClose = options.onClose; + + this.selector = new AppSelectPeers({ + 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; + } + } + }, + peerType: options.peerTypes, + onFirstRender: () => { + this.show(); + this.selector.checkForTriggers(); // ! due to zero height before mounting + + if(!isTouchSupported) { + this.selector.input.focus(); + } + }, + chatRightsAction: options.chatRightsAction, + multiSelect: false, + rippleEnabled: false + }); + + //this.scrollable = new Scrollable(this.body); + + this.selector.input.placeholder = options.placeholder; + this.title.append(this.selector.input); + } +} diff --git a/src/components/sidebarLeft/tabs/blockedUsers.ts b/src/components/sidebarLeft/tabs/blockedUsers.ts new file mode 100644 index 00000000..cc953e40 --- /dev/null +++ b/src/components/sidebarLeft/tabs/blockedUsers.ts @@ -0,0 +1,115 @@ +import { SliderSuperTab } from "../../slider"; +import { SettingSection } from ".."; +import { attachContextMenuListener, openBtnMenu, positionMenu } from "../../misc"; +import { attachClickEvent, findUpTag } from "../../../helpers/dom"; +import ButtonMenu from "../../buttonMenu"; +import appDialogsManager from "../../../lib/appManagers/appDialogsManager"; +import appUsersManager from "../../../lib/appManagers/appUsersManager"; +import Button from "../../button"; +import PopupPickUser from "../../popups/pickUser"; +import rootScope from "../../../lib/rootScope"; + +export default class AppBlockedUsersTab extends SliderSuperTab { + public peerIds: number[]; + private menuElement: HTMLElement; + + protected init() { + this.container.classList.add('blocked-users-container'); + this.title.innerText = 'Blocked Users'; + + { + const section = new SettingSection({ + caption: 'Blocked users will not be able to contact you and will not see your Last Seen time.' + }); + + this.scrollable.append(section.container); + } + + const btnAdd = Button('btn-circle btn-corner tgico-add is-visible'); + this.content.append(btnAdd); + + attachClickEvent(btnAdd, (e) => { + new PopupPickUser({ + peerTypes: ['contacts'], + placeholder: 'Block user...', + onSelect: (peerId) => { + //console.log('block', peerId); + appUsersManager.toggleBlock(peerId, true); + }, + }); + }, {listenerSetter: this.listenerSetter}); + + const list = document.createElement('ul'); + this.scrollable.container.classList.add('chatlist-container'); + this.scrollable.append(list); + + const add = (peerId: number, append: boolean) => { + const {dom} = appDialogsManager.addDialogNew({ + dialog: peerId, + container: list, + drawStatus: false, + rippleEnabled: true, + avatarSize: 48, + append + }); + + const user = appUsersManager.getUser(peerId); + dom.lastMessageSpan.innerHTML = user.pFlags.bot ? ('@' + user.username) : user.rPhone || (user.username ? '@' + user.username : appUsersManager.getUserStatusString(peerId)); + }; + + for(const peerId of this.peerIds) { + add(peerId, true); + } + + let target: HTMLElement; + const onUnblock = () => { + const peerId = +target.dataset.peerId; + appUsersManager.toggleBlock(peerId, false); + }; + + const element = this.menuElement = ButtonMenu([{ + icon: 'unlock', + text: 'Unblock', + onClick: onUnblock, + options: {listenerSetter: this.listenerSetter} + }]); + element.id = 'blocked-users-contextmenu'; + element.classList.add('contextmenu'); + + document.getElementById('page-chats').append(element); + + attachContextMenuListener(this.scrollable.container, (e) => { + target = findUpTag(e.target, 'LI'); + if(!target) { + return; + } + + if(e instanceof MouseEvent) e.preventDefault(); + // smth + if(e instanceof MouseEvent) e.cancelBubble = true; + + positionMenu(e, element); + openBtnMenu(element); + }, this.listenerSetter); + + this.listenerSetter.add(rootScope, 'peer_block', (update) => { + const {peerId, blocked} = update; + if(blocked) { + add(peerId, false); + } else { + const li = list.querySelector(`[data-peer-id="${peerId}"]`); + if(li) { + li.remove(); + } + } + }); + } + + onCloseAfterTimeout() { + if(this.menuElement) { + this.menuElement.remove(); + } + + return super.onCloseAfterTimeout(); + } +} diff --git a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts index 688b8d47..6c704891 100644 --- a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts +++ b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts @@ -15,6 +15,8 @@ import AppPrivacyAddToGroupsTab from "./privacy/addToGroups"; import AppPrivacyCallsTab from "./privacy/calls"; import AppActiveSessionsTab from "./activeSessions"; import apiManager from "../../../lib/mtproto/mtprotoworker"; +import AppBlockedUsersTab from "./blockedUsers"; +import appUsersManager from "../../../lib/appManagers/appUsersManager"; export default class AppPrivacyAndSecurityTab extends SliderSuperTab { protected init() { @@ -26,12 +28,18 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab { { const section = new SettingSection({noDelimiter: true}); + let blockedPeerIds: number[]; const blockedUsersRow = new Row({ icon: 'deleteuser', title: 'Blocked Users', - subtitle: '6 users', - clickable: true + subtitle: 'Loading...', + clickable: () => { + const tab = new AppBlockedUsersTab(this.slider); + tab.peerIds = blockedPeerIds; + tab.open(); + } }); + blockedUsersRow.freezed = true; let passwordState: AccountPassword; const twoFactorRowOptions = { @@ -75,6 +83,12 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab { section.content.append(blockedUsersRow.container, twoFactorRow.container, activeSessionRow.container); this.scrollable.append(section.container); + appUsersManager.getBlocked().then(res => { + blockedUsersRow.freezed = false; + blockedUsersRow.subtitle.innerText = res.count + ' ' + (res.count !== 1 ? 'users' : 'user'); + blockedPeerIds = res.peerIds; + }); + passwordManager.getState().then(state => { passwordState = state; twoFactorRow.subtitle.innerText = state.pFlags.has_password ? 'On' : 'Off'; @@ -87,7 +101,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab { apiManager.invokeApi('account.getAuthorizations').then(auths => { activeSessionRow.freezed = false; authorizations = auths.authorizations; - activeSessionRow.subtitle.innerText = authorizations.length + ' ' + (authorizations.length > 1 ? 'devices' : 'device'); + activeSessionRow.subtitle.innerText = authorizations.length + ' ' + (authorizations.length !== 1 ? 'devices' : 'device'); console.log('auths', auths); }); } diff --git a/src/components/sliderTab.ts b/src/components/sliderTab.ts index 91c8e9e8..ca787534 100644 --- a/src/components/sliderTab.ts +++ b/src/components/sliderTab.ts @@ -1,4 +1,5 @@ import EventListenerBase from "../helpers/eventListenerBase"; +import ListenerSetter from "../helpers/listenerSetter"; import ButtonIcon from "./buttonIcon"; import Scrollable from "./scrollable"; import SidebarSlider from "./slider"; @@ -26,6 +27,7 @@ export default class SliderSuperTab implements SliderTab { public slider: SidebarSlider; public destroyable: boolean; + public listenerSetter: ListenerSetter; constructor(slider: SidebarSlider, destroyable?: boolean) { this._constructor(slider, destroyable); @@ -56,6 +58,8 @@ export default class SliderSuperTab implements SliderTab { this.container.append(this.header, this.content); this.slider.addTab(this); + + this.listenerSetter = new ListenerSetter(); } public close() { @@ -83,6 +87,10 @@ export default class SliderSuperTab implements SliderTab { this.slider.tabs.delete(this); this.container.remove(); } + + if(this.listenerSetter) { + this.listenerSetter.removeAll(); + } } } diff --git a/src/helpers/eventListenerBase.ts b/src/helpers/eventListenerBase.ts index ff4344b4..b52612bd 100644 --- a/src/helpers/eventListenerBase.ts +++ b/src/helpers/eventListenerBase.ts @@ -1,5 +1,48 @@ +//import { MOUNT_CLASS_TO } from "../config/debug"; import type { ArgumentTypes, SuperReturnType } from "../types"; +// class EventSystem { +// wm: WeakMap>> = new WeakMap(); + +// add(target: any, event: any, listener: any) { +// let listeners = this.wm.get(target); +// if (listeners === undefined) { +// listeners = {}; +// } +// let listenersForEvent = listeners[event]; +// if (listenersForEvent === undefined) { +// listenersForEvent = new Set(); +// } +// listenersForEvent.add(listener); +// listeners[event] = listenersForEvent; +// //target.addEventListener(event, listener); +// this.wm.set(target, listeners); +// }; + +// remove(target: any, event: any, listener: any) { +// let listeners = this.wm.get(target); +// if (!listeners) return; +// let listenersForEvent = listeners[event]; +// if (!listenersForEvent) return; +// listenersForEvent.delete(listener); +// }; + +// /* fire(target, event) { +// let listeners = this.wm.get(target); +// if (!listeners) return; +// let listenersForEvent = listeners[event]; +// if (!listenersForEvent) return; +// for (let handler of handlers) { +// setTimeout(handler, 0, event, target); // we use a setTimeout here because we want event triggering to be asynchronous. +// } +// }; */ +// } + +// console.log = () => {}; + +// const e = new EventSystem(); +// MOUNT_CLASS_TO && (MOUNT_CLASS_TO.e = e); + /** * Better not to remove listeners during setting * Should add listener callback only once @@ -34,12 +77,14 @@ export default class EventListenerBase l.callback === callback); } + //e.remove(this, name, callback); } // * must be protected, but who cares @@ -49,12 +94,16 @@ export default class EventListenerBase> = []; + + /* let a = e.wm.get(this)[name]; + if(!a) return arr; + const listeners = [...a]; */ const listeners = this.listeners[name]; if(listeners) { // ! this one will guarantee execution even if delete another listener during setting const left = listeners.slice(); - left.forEach(listener => { - const index = listeners.findIndex(l => l.callback === listener.callback); + left.forEach((listener: any) => { + const index = listeners.findIndex((l: any) => l.callback === listener.callback); if(index === -1) { return; } diff --git a/src/helpers/listenerSetter.ts b/src/helpers/listenerSetter.ts index 70e22f11..b10df2ff 100644 --- a/src/helpers/listenerSetter.ts +++ b/src/helpers/listenerSetter.ts @@ -6,23 +6,23 @@ export type ListenerCallback = (...args: any[]) => any; export default class ListenerSetter { private listeners: Set = new Set(); - public add = (element: ListenerElement, event: ListenerEvent, callback: ListenerCallback, options?: ListenerOptions) => { + public add(element: ListenerElement, event: ListenerEvent, callback: ListenerCallback, options?: ListenerOptions) { const listener = {element, event, callback, options}; this.addManual(listener); return listener; - }; + } - public addManual = (listener: Listener) => { + public addManual(listener: Listener) { listener.element.addEventListener(listener.event, listener.callback, listener.options); this.listeners.add(listener); - }; + } - public remove = (listener: Listener) => { + public remove(listener: Listener) { listener.element.removeEventListener(listener.event, listener.callback, listener.options); this.listeners.delete(listener); - }; + } - public removeManual = (element: ListenerElement, event: ListenerEvent, callback: ListenerCallback, options?: ListenerOptions) => { + public removeManual(element: ListenerElement, event: ListenerEvent, callback: ListenerCallback, options?: ListenerOptions) { let listener: Listener; for(const _listener of this.listeners) { if(_listener.element === element && _listener.event === event && _listener.callback === callback && _listener.options === options) { @@ -34,11 +34,11 @@ export default class ListenerSetter { if(listener) { this.remove(listener); } - }; + } - public removeAll = () => { + public removeAll() { this.listeners.forEach(listener => { this.remove(listener); }); - }; -} \ No newline at end of file + } +} diff --git a/src/lib/appManagers/appPeersManager.ts b/src/lib/appManagers/appPeersManager.ts index ec0cdfc5..ae970338 100644 --- a/src/lib/appManagers/appPeersManager.ts +++ b/src/lib/appManagers/appPeersManager.ts @@ -1,6 +1,6 @@ import { MOUNT_CLASS_TO } from "../../config/debug"; import { isObject } from "../../helpers/object"; -import { DialogPeer, InputDialogPeer, InputPeer, Peer } from "../../layer"; +import { DialogPeer, InputDialogPeer, InputPeer, Peer, Update } from "../../layer"; import { RichTextProcessor } from "../richtextprocessor"; import rootScope from "../rootScope"; import appChatsManager from "./appChatsManager"; @@ -24,6 +24,18 @@ const DialogColorsMap = [0, 7, 4, 1, 6, 3, 5]; export type PeerType = 'channel' | 'chat' | 'megagroup' | 'group' | 'saved'; export class AppPeersManager { + constructor() { + rootScope.on('apiUpdate', (e) => { + const update = e as Update; + //console.log('on apiUpdate', update); + switch(update._) { + case 'updatePeerBlocked': { + rootScope.broadcast('peer_block', {peerId: this.getPeerId(update.peer_id), blocked: update.blocked}); + break; + } + } + }); + } /* public savePeerInstance(peerId: number, instance: any) { if(peerId < 0) appChatsManager.saveApiChat(instance); else appUsersManager.saveApiUser(instance); @@ -113,13 +125,13 @@ export class AppPeersManager { : appChatsManager.getChat(-peerId) } - public getPeerId(peerString: any/* Peer | number | string */): number { - if(typeof(peerString) === 'number') return peerString; - else if(isObject(peerString)) return peerString.user_id ? peerString.user_id : -(peerString.channel_id || peerString.chat_id); - else if(!peerString) return 0; + public getPeerId(peerId: any/* Peer | number | string */): number { + if(typeof(peerId) === 'number') return peerId; + else if(isObject(peerId)) return peerId.user_id ? peerId.user_id : -(peerId.channel_id || peerId.chat_id); + else if(!peerId) return 0; - const isUser = peerString.charAt(0) === 'u'; - const peerParams = peerString.substr(1).split('_'); + const isUser = peerId.charAt(0) === 'u'; + const peerParams = peerId.substr(1).split('_'); return isUser ? peerParams[0] : -peerParams[0] || 0; } diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index b51c4513..2c90a20e 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -83,18 +83,7 @@ export class AppUsersManager { break; } - - /* // @ts-ignore - case 'updateUserBlocked': { - const id = (update as any).user_id; - const blocked: boolean = (update as any).blocked; - const user = this.getUser(id); - if(user) { - } - break; - } */ - /* case 'updateContactLink': this.onContactUpdated(update.user_id, update.my_link._ === 'contactLinkContact'); break; */ @@ -225,6 +214,25 @@ export class AppUsersManager { }); } + public toggleBlock(peerId: number, block: boolean) { + return apiManager.invokeApi(block ? 'contacts.block' : 'contacts.unblock', { + id: appPeersManager.getInputPeerById(peerId) + }).then(value => { + if(value) { + apiUpdatesManager.processUpdateMessage({ + _: 'updateShort', + update: { + _: 'updatePeerBlocked', + peer_id: appPeersManager.getOutputPeer(peerId), + blocked: block + } as Update.updatePeerBlocked + }); + } + + return value; + }); + } + public testSelfSearch(query: string) { const user = this.getSelf(); const index = searchIndexManager.createIndex(); @@ -633,6 +641,18 @@ export class AppUsersManager { }); } + public getBlocked(offset = 0, limit = 0) { + return apiManager.invokeApi('contacts.getBlocked', {offset, limit}).then(contactsBlocked => { + this.saveApiUsers(contactsBlocked.users); + appChatsManager.saveApiChats(contactsBlocked.chats); + const count = contactsBlocked._ === 'contacts.blocked' ? contactsBlocked.users.length + contactsBlocked.chats.length : contactsBlocked.count; + + const peerIds = contactsBlocked.users.map(u => u.id).concat(contactsBlocked.chats.map(c => -c.id)); + + return {count, peerIds}; + }); + } + /* public searchContacts(query: string, limit = 20) { return Promise.all([ this.getContacts(query), diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 67ce7753..a0b46dc5 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -20,6 +20,7 @@ type BroadcastEvents = { 'peer_pinned_messages': {peerId: number, mids?: number[], pinned?: boolean, unpinAll?: true}, 'peer_pinned_hidden': {peerId: number, maxId: number}, 'peer_typings': {peerId: number, typings: UserTyping[]}, + 'peer_block': {peerId: number, blocked: boolean}, 'filter_delete': MyDialogFilter, 'filter_update': MyDialogFilter, @@ -125,11 +126,11 @@ class RootScope extends EventListenerBase { } public broadcast = (name: T, detail?: BroadcastEvents[T]) => { - /* if(DEBUG) { + //if(DEBUG) { if(name !== 'user_update') { console.debug('Broadcasting ' + name + ' event, with args:', detail); } - } */ + //} this.setListenerResult(name, detail); }; diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index 4da13a4a..212e2d23 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -1030,6 +1030,12 @@ } } +.blocked-users-container { + .sidebar-left-section-caption { + font-size: 1rem; + } +} + .range-setting-selector { padding: 1rem .875rem;