diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index b9cf489a..537f464c 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -184,12 +184,13 @@ export class AppSelectPeers { let title = appPeersManager.getPeerTitle(peerID, false, true); - let avatarDiv = document.createElement('div'); - avatarDiv.classList.add('user-avatar', 'tgico'); - appProfileManager.putPhoto(avatarDiv, peerID); + let avatarEl = document.createElement('avatar-element'); + avatarEl.classList.add('selector-user-avatar', 'tgico'); + avatarEl.setAttribute('dialog', '1'); + avatarEl.setAttribute('peer', '' + peerID); div.innerHTML = title; - div.insertAdjacentElement('afterbegin', avatarDiv); + div.insertAdjacentElement('afterbegin', avatarEl); this.selectedContainer.insertBefore(div, this.input); this.selectedScrollable.scrollTop = this.selectedScrollable.scrollHeight; diff --git a/src/components/avatar.ts b/src/components/avatar.ts new file mode 100644 index 00000000..5c4db71d --- /dev/null +++ b/src/components/avatar.ts @@ -0,0 +1,62 @@ +import appProfileManager from "../lib/appManagers/appProfileManager"; +import { $rootScope } from "../lib/utils"; + +$rootScope.$on('avatar_update', (e: CustomEvent) => { + let peerID = e.detail; + + appProfileManager.removeFromAvatarsCache(peerID); + (Array.from(document.querySelectorAll('avatar-element[peer="' + peerID + '"]')) as AvatarElement[]).forEach(elem => { + console.log('updating avatar:', elem); + elem.update(); + }); +}); + +export default class AvatarElement extends HTMLElement { + private peerID: number; + private isDialog = false; + + constructor() { + super(); + // элемент создан + } + + connectedCallback() { + // браузер вызывает этот метод при добавлении элемента в документ + // (может вызываться много раз, если элемент многократно добавляется/удаляется) + + this.isDialog = !!this.getAttribute('dialog'); + } + + disconnectedCallback() { + // браузер вызывает этот метод при удалении элемента из документа + // (может вызываться много раз, если элемент многократно добавляется/удаляется) + } + + static get observedAttributes(): string[] { + return ['peer', 'dialog'/* массив имён атрибутов для отслеживания их изменений */]; + } + + attributeChangedCallback(name: string, oldValue: string, newValue: string) { + // вызывается при изменении одного из перечисленных выше атрибутов + if(name == 'peer') { + this.peerID = +newValue; + } else if(name == 'dialog') { + this.isDialog = !!newValue; + } + + this.update(); + } + + public update() { + appProfileManager.putPhoto(this, this.peerID, this.isDialog); + } + + adoptedCallback() { + // вызывается, когда элемент перемещается в новый документ + // (происходит в document.adoptNode, используется очень редко) + } + + // у элемента могут быть ещё другие методы и свойства +} + +customElements.define("avatar-element", AvatarElement); \ No newline at end of file diff --git a/src/components/misc.ts b/src/components/misc.ts index 2e4ef7a5..5fabc9af 100644 --- a/src/components/misc.ts +++ b/src/components/misc.ts @@ -329,33 +329,59 @@ let onMouseMove = (e: MouseEvent) => { let diffY = clientY >= rect.bottom ? clientY - rect.bottom : rect.top - clientY; if(diffX >= 100 || diffY >= 100) { - openedMenu.classList.remove('active'); - openedMenu.parentElement.classList.remove('menu-open'); + closeBtnMenu(); //openedMenu.parentElement.click(); } //console.log('mousemove', diffX, diffY); }; -let openedMenu: HTMLDivElement = null; -export function openBtnMenu(menuElement: HTMLDivElement) { +let onClick = (e: MouseEvent) => { + e.preventDefault(); + closeBtnMenu(); +}; + +let closeBtnMenu = () => { if(openedMenu) { openedMenu.classList.remove('active'); openedMenu.parentElement.classList.remove('menu-open'); + openedMenu = null; } + if(openedMenuOnClose) { + openedMenuOnClose(); + openedMenuOnClose = null; + } + + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('click', onClick); + window.removeEventListener('contextmenu', onClick); +}; + +let openedMenu: HTMLDivElement = null, openedMenuOnClose: () => void = null; +export function openBtnMenu(menuElement: HTMLDivElement, onClose?: () => void) { + closeBtnMenu(); + openedMenu = menuElement; openedMenu.classList.add('active'); openedMenu.parentElement.classList.add('menu-open'); - window.addEventListener('click', () => { - if(openedMenu) { - openedMenu.parentElement.classList.remove('menu-open'); - openedMenu.classList.remove('active'); - openedMenu = null; - } - - window.removeEventListener('mousemove', onMouseMove); - }, {once: true}); + openedMenuOnClose = onClose; window.addEventListener('mousemove', onMouseMove); + window.addEventListener('click', onClick, {once: true}); + window.addEventListener('contextmenu', onClick, {once: true}); +} + +export function positionMenu(e: MouseEvent, elem: HTMLElement, side: 'left' | 'right' = 'left') { + elem.classList.remove('bottom-left', 'bottom-right'); + elem.classList.add(side == 'left' ? 'bottom-right' : 'bottom-left'); + + let {clientX, clientY} = e; + + elem.style.left = (side == 'right' ? clientX - elem.scrollWidth : clientX) + 'px'; + if((clientY + elem.scrollHeight) > window.innerHeight) { + elem.style.top = (window.innerHeight - elem.scrollHeight) + 'px'; + } else { + elem.style.top = clientY + 'px'; + } } diff --git a/src/components/popupAvatar.ts b/src/components/popupAvatar.ts index d550c8f7..70b159f2 100644 --- a/src/components/popupAvatar.ts +++ b/src/components/popupAvatar.ts @@ -58,12 +58,7 @@ export class PopupAvatar { this.canvas.toBlob(blob => { this.blob = blob; // save blob to send after reg - - // darken - let ctx = this.canvas.getContext('2d'); - ctx.fillStyle = "rgba(0, 0, 0, 0.3)"; - ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - + this.darkenCanvas(); this.resolve(); }, 'image/jpeg', 1); }); @@ -92,6 +87,12 @@ export class PopupAvatar { this.input.click(); } + + public darkenCanvas() { + let ctx = this.canvas.getContext('2d'); + ctx.fillStyle = "rgba(0, 0, 0, 0.3)"; + ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } } export default new PopupAvatar(); diff --git a/src/components/scrollable_new.ts b/src/components/scrollable_new.ts index 3ea5f75f..f40249c8 100644 --- a/src/components/scrollable_new.ts +++ b/src/components/scrollable_new.ts @@ -68,7 +68,7 @@ export default class Scrollable { private lastBottomID = 0; private lastScrollDirection = 0; // true = bottom - private scrollLocked = 0; + public scrollLocked = 0; private setVisible(element: HTMLElement) { if(this.visible.has(element)) return; @@ -367,8 +367,8 @@ export default class Scrollable { return !!element.parentElement; } - public scrollIntoView(element: HTMLElement) { - if(element.parentElement) { + public scrollIntoView(element: HTMLElement, smooth = true, fromUp = false) { + if(element.parentElement && !this.scrollLocked) { let scrollTop = this.scrollTop; let offsetTop = element.offsetTop; let clientHeight = this.container.clientHeight; @@ -383,12 +383,30 @@ export default class Scrollable { offsetTop -= diff; //} + if(element.dataset.timeout) { + clearTimeout(+element.dataset.timeout); + element.classList.remove('is-selected'); + void element.offsetWidth; // reflow + } + element.classList.add('is-selected'); + element.dataset.timeout = '' + setTimeout(() => { + element.classList.remove('is-selected'); + delete element.dataset.timeout; + }, 2000); + + if(scrollTop == Math.floor(offsetTop)) { + return; + } + if(this.scrollLocked) clearTimeout(this.scrollLocked); this.scrollLocked = setTimeout(() => { this.scrollLocked = 0; this.onScroll(); }, 468); - this.container.scrollTo({behavior: 'smooth', top: offsetTop}); + if(fromUp) { + this.container.scrollTo({behavior: 'auto', top: 0}); + } + this.container.scrollTo({behavior: smooth ? 'smooth' : 'auto', top: offsetTop}); //element.scrollIntoView({behavior: 'smooth', block: 'center'}); } } diff --git a/src/lib/appManagers/appChatsManager.ts b/src/lib/appManagers/appChatsManager.ts index 48f05cec..9c201a91 100644 --- a/src/lib/appManagers/appChatsManager.ts +++ b/src/lib/appManagers/appChatsManager.ts @@ -1,7 +1,6 @@ import { $rootScope, isObject, SearchIndexManager, safeReplaceObject, copy, numberWithCommas } from "../utils"; import { RichTextProcessor } from "../richtextprocessor"; import appUsersManager from "./appUsersManager"; -import appPeersManager from "./appPeersManager"; import apiManager from '../mtproto/mtprotoworker'; import apiUpdatesManager from "./apiUpdatesManager"; @@ -68,40 +67,46 @@ export class AppChatsManager { apiChat.rTitle = apiChat.title || 'chat_title_deleted'; apiChat.rTitle = RichTextProcessor.wrapRichText(apiChat.title, {noLinks: true, noLinebreaks: true}) || 'chat_title_deleted'; - var result = this.chats[apiChat.id]; - var titleWords = SearchIndexManager.cleanSearchText(apiChat.title || '').split(' '); - var firstWord = titleWords.shift(); - var lastWord = titleWords.pop(); - apiChat.initials = firstWord.charAt(0) + (lastWord ? lastWord.charAt(0) : firstWord.charAt(1)); + let oldChat = this.chats[apiChat.id]; - apiChat.num = (Math.abs(apiChat.id >> 1) % 8) + 1; + let titleWords = SearchIndexManager.cleanSearchText(apiChat.title || '', false).split(' '); + let firstWord = titleWords.shift(); + let lastWord = titleWords.pop(); + apiChat.initials = firstWord.charAt(0) + (lastWord ? lastWord.charAt(0) : firstWord.charAt(1)); if(apiChat.pFlags === undefined) { apiChat.pFlags = {}; } if(apiChat.pFlags.min) { - if(result !== undefined) { + if(oldChat !== undefined) { return; } } if(apiChat._ == 'channel' && apiChat.participants_count === undefined && - result !== undefined && - result.participants_count) { - apiChat.participants_count = result.participants_count; + oldChat !== undefined && + oldChat.participants_count) { + apiChat.participants_count = oldChat.participants_count; } if(apiChat.username) { - var searchUsername = SearchIndexManager.cleanUsername(apiChat.username); + let searchUsername = SearchIndexManager.cleanUsername(apiChat.username); this.usernames[searchUsername] = apiChat.id; } - if(result === undefined) { - result = this.chats[apiChat.id] = apiChat; + let changedPhoto = false; + if(oldChat === undefined) { + oldChat = this.chats[apiChat.id] = apiChat; } else { - safeReplaceObject(result, apiChat); + let oldPhoto = oldChat.photo && oldChat.photo.photo_small; + let newPhoto = apiChat.photo && apiChat.photo.photo_small; + if(JSON.stringify(oldPhoto) !== JSON.stringify(newPhoto)) { + changedPhoto = true; + } + + safeReplaceObject(oldChat, apiChat); $rootScope.$broadcast('chat_update', apiChat.id); } @@ -109,6 +114,10 @@ export class AppChatsManager { safeReplaceObject(this.cachedPhotoLocations[apiChat.id], apiChat && apiChat.photo ? apiChat.photo : {empty: true}); } + + if(changedPhoto) { + $rootScope.$broadcast('avatar_update', -apiChat.id); + } } public getChat(id: number) { @@ -116,10 +125,11 @@ export class AppChatsManager { return this.chats[id] || {id: id, deleted: true, access_hash: this.channelAccess[id]}; } - public hasRights(id: number, action: any) { + public hasRights(id: number, action: 'send' | 'edit_title' | 'edit_photo' | 'invite') { if(!(id in this.chats)) { return false; } + var chat = this.getChat(id); if(chat._ == 'chatForbidden' || chat._ == 'channelForbidden' || @@ -127,22 +137,25 @@ export class AppChatsManager { chat.pFlags.left) { return false; } + if(chat.pFlags.creator) { return true; } switch(action) { - case 'send': + case 'send': { if(chat._ == 'channel' && - !chat.pFlags.megagroup && - !chat.pFlags.editor) { + !chat.pFlags.megagroup && + !chat.pFlags.editor) { return false; } break; + } + case 'edit_title': case 'edit_photo': - case 'invite': + case 'invite': { if(chat._ == 'channel') { if(chat.pFlags.megagroup) { if(!chat.pFlags.editor && @@ -159,7 +172,9 @@ export class AppChatsManager { } } break; + } } + return true; } diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 5758745e..198af71e 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -1,18 +1,18 @@ -import { langPack, findUpClassName, $rootScope, escapeRegExp, whichChild } from "../utils"; +import { findUpClassName, $rootScope, escapeRegExp, whichChild, findUpTag } from "../utils"; import appImManager, { AppImManager } from "./appImManager"; import appPeersManager from './appPeersManager'; import appMessagesManager, { AppMessagesManager, Dialog } from "./appMessagesManager"; import appUsersManager, { User } from "./appUsersManager"; import { RichTextProcessor } from "../richtextprocessor"; -import { ripple, putPreloader } from "../../components/misc"; +import { ripple, putPreloader, positionMenu, openBtnMenu } from "../../components/misc"; //import Scrollable from "../../components/scrollable"; import Scrollable from "../../components/scrollable_new"; -import appProfileManager from "./appProfileManager"; import { logger } from "../polyfill"; import appChatsManager from "./appChatsManager"; +import AvatarElement from "../../components/avatar"; type DialogDom = { - avatarDiv: HTMLDivElement, + avatarEl: AvatarElement, captionDiv: HTMLDivElement, titleSpan: HTMLSpanElement, statusSpan: HTMLSpanElement, @@ -25,6 +25,346 @@ type DialogDom = { let testScroll = false; +class PopupElement { + protected element = document.createElement('div'); + protected container = document.createElement('div'); + protected header = document.createElement('div'); + protected title = document.createElement('div'); + + constructor(className: string) { + this.element.classList.add('popup'); + this.element.className = 'popup' + (className ? ' ' + className : ''); + this.container.classList.add('popup-container', 'z-depth-1'); + + this.header.classList.add('popup-header'); + this.title.classList.add('popup-title'); + + this.header.append(this.title); + this.container.append(this.header); + this.element.append(this.container); + } + + public show() { + document.body.append(this.element); + void this.element.offsetWidth; // reflow + this.element.classList.add('active'); + } + + public destroy() { + this.element.classList.remove('active'); + setTimeout(() => { + this.element.remove(); + }, 1000); + } +} + +type PopupPeerButton = { + text: string, + callback?: () => void, + isDanger?: true, + isCancel?: true +}; + +class PopupPeer extends PopupElement { + constructor(private className: string, options: Partial<{ + peerID: number, + title: string, + description: string, + buttons: Array + }> = {}) { + super('popup-peer' + (className ? ' ' + className : '')); + + let avatarEl = new AvatarElement(); + avatarEl.setAttribute('dialog', '1'); + avatarEl.setAttribute('peer', '' + options.peerID); + avatarEl.classList.add('peer-avatar'); + + this.title.innerText = options.title || ''; + this.header.prepend(avatarEl); + + let p = document.createElement('p'); + p.classList.add('popup-description'); + p.innerHTML = options.description; + + let buttonsDiv = document.createElement('div'); + buttonsDiv.classList.add('popup-buttons'); + + let buttons = options.buttons.map(b => { + let button = document.createElement('button'); + ripple(button); + button.className = 'btn' + (b.isDanger ? ' danger' : ''); + button.innerHTML = b.text; + + if(b.callback) { + button.addEventListener('click', () => { + b.callback(); + this.destroy(); + }); + } else if(b.isCancel) { + button.addEventListener('click', () => { + this.destroy(); + }); + } + + return button; + }); + + buttonsDiv.append(...buttons); + + this.container.append(p, buttonsDiv); + } +} + +class DialogsContextMenu { + private element = document.getElementById('dialogs-contextmenu') as HTMLDivElement; + private buttons: { + archive: HTMLButtonElement, + pin: HTMLButtonElement, + mute: HTMLButtonElement, + unread: HTMLButtonElement, + delete: HTMLButtonElement, + //clear: HTMLButtonElement, + } = {} as any; + private selectedID: number; + private peerType: 'channel' | 'chat' | 'megagroup' | 'group' | 'saved'; + + constructor(private attachTo: HTMLElement[]) { + (Array.from(this.element.querySelectorAll('.btn-menu-item')) as HTMLElement[]).forEach(el => { + let name = el.className.match(/ menu-(.+?) /)[1]; + // @ts-ignore + this.buttons[name] = el; + }); + + const onContextMenu = (e: MouseEvent) => { + let li: HTMLDivElement = null; + + try { + li = findUpTag(e.target, 'LI'); + } catch(e) {} + + if(!li) return; + + e.preventDefault(); + + if(this.element.classList.contains('active')) { + /* this.element.classList.remove('active'); + this.element.parentElement.classList.remove('menu-open'); */ + return false; + } + + e.cancelBubble = true; + + this.selectedID = +li.getAttribute('data-peerID'); + const dialog = appMessagesManager.getDialogByPeerID(this.selectedID)[0]; + const notOurDialog = dialog.peerID != $rootScope.myID; + + // archive button + if(notOurDialog) { + const button = this.buttons.archive; + let text = ''; + if(dialog.folder_id == 1) { + text = 'Unarchive chat'; + button.classList.remove('tgico-archive'); + } else { + text = 'Archive chat'; + button.classList.add('tgico-archive'); + } + button.innerText = text; + this.buttons.archive.style.display = ''; + } else { + this.buttons.archive.style.display = 'none'; + } + + // pin button + { + const button = this.buttons.pin; + let text = ''; + if(dialog.pFlags?.pinned) { + text = 'Unpin from top'; + button.classList.remove('tgico-pin'); + } else { + text = 'Pin to top'; + button.classList.add('tgico-pin'); + } + button.innerText = text; + } + + // mute button + if(notOurDialog) { + const button = this.buttons.mute; + let text = ''; + if(dialog.notify_settings && dialog.notify_settings.mute_until > (Date.now() / 1000 | 0)) { + text = 'Enable notifications'; + button.classList.remove('tgico-mute'); + } else { + text = 'Disable notifications'; + button.classList.add('tgico-mute'); + } + button.innerText = text; + this.buttons.mute.style.display = ''; + } else { + this.buttons.mute.style.display = 'none'; + } + + // unread button + { + const button = this.buttons.unread; + let text = ''; + if(dialog.pFlags?.unread_mark) { + text = 'Mark as read'; + button.classList.add('tgico-message'); + } else { + text = 'Mark as unread'; + button.classList.remove('tgico-message'); + } + button.innerText = text; + } + + /* // clear history button + if(appPeersManager.isChannel(this.selectedID)) { + this.buttons.clear.style.display = 'none'; + } else { + this.buttons.clear.style.display = ''; + } */ + + // delete button + let deleteButtonText = ''; + if(appPeersManager.isMegagroup(this.selectedID)) { + deleteButtonText = 'Leave group'; + this.peerType = 'megagroup'; + } else if(appPeersManager.isChannel(this.selectedID)) { + deleteButtonText = 'Leave channel'; + this.peerType = 'channel'; + } else if(this.selectedID < 0) { + deleteButtonText = 'Delete and leave'; + this.peerType = 'group'; + } else { + deleteButtonText = 'Delete chat'; + this.peerType = this.selectedID == $rootScope.myID ? 'saved' : 'chat'; + } + this.buttons.delete.innerText = deleteButtonText; + + li.classList.add('menu-open'); + positionMenu(e, this.element); + openBtnMenu(this.element, () => { + li.classList.remove('menu-open'); + }); + }; + + this.attachTo.forEach(el => { + el.addEventListener('contextmenu', onContextMenu); + }); + + this.buttons.archive.addEventListener('click', () => { + let dialog = appMessagesManager.getDialogByPeerID(this.selectedID)[0]; + if(dialog) { + appMessagesManager.editPeerFolders([dialog.peerID], +!dialog.folder_id); + } + }); + + this.buttons.pin.addEventListener('click', () => { + appMessagesManager.toggleDialogPin(this.selectedID); + }); + + this.buttons.mute.addEventListener('click', () => { + appImManager.mutePeer(this.selectedID); + }); + + this.buttons.unread.addEventListener('click', () => { + appMessagesManager.markDialogUnread(this.selectedID); + }); + + this.buttons.delete.addEventListener('click', () => { + let firstName = appPeersManager.getPeerTitle(this.selectedID, false, true); + + let callback = (justClear: boolean) => { + appMessagesManager.flushHistory(this.selectedID, justClear); + }; + + let title: string, description: string, buttons: PopupPeerButton[]; + switch(this.peerType) { + case 'channel': { + title = 'Leave Channel?'; + description = `Are you sure you want to leave this channel?`; + buttons = [{ + text: 'LEAVE ' + firstName, + isDanger: true, + callback: () => callback(true) + }]; + + break; + } + + case 'megagroup': { + title = 'Leave Group?'; + description = `Are you sure you want to leave this group?`; + buttons = [{ + text: 'LEAVE ' + firstName, + isDanger: true, + callback: () => callback(true) + }]; + + break; + } + + case 'chat': { + title = 'Delete Chat?'; + description = `Are you sure you want to delete chat with ${firstName}?`; + buttons = [{ + text: 'DELETE FOR ME AND ' + firstName, + isDanger: true, + callback: () => callback(false) + }, { + text: 'DELETE JUST FOR ME', + isDanger: true, + callback: () => callback(true) + }]; + + break; + } + + case 'saved': { + title = 'Delete Saved Messages?'; + description = `Are you sure you want to delete all your saved messages?`; + buttons = [{ + text: 'DELETE SAVED MESSAGES', + isDanger: true, + callback: () => callback(false) + }]; + + break; + } + + case 'group': { + title = 'Delete and leave Group?'; + description = `Are you sure you want to delete all message history and leave ${firstName}?`; + buttons = [{ + text: 'DELETE AND LEAVE ' + firstName, + isDanger: true, + callback: () => callback(true) + }]; + + break; + } + } + + buttons.push({ + text: 'CANCEL', + isCancel: true + }); + + let popup = new PopupPeer('popup-delete-chat', { + peerID: this.selectedID, + title: title, + description: description, + buttons: buttons + }); + + popup.show(); + }); + } +} + export class AppDialogsManager { public chatList = document.getElementById('dialogs') as HTMLUListElement; public chatListArchived = document.getElementById('dialogs-archived') as HTMLUListElement; @@ -59,6 +399,8 @@ export class AppDialogsManager { private log = logger('DIALOGS'); + private contextMenu = new DialogsContextMenu([this.chatList, this.chatListArchived]); + constructor() { this.chatsPreloader = putPreloader(null, true); //this.chatsContainer.append(this.chatsPreloader); @@ -127,9 +469,9 @@ export class AppDialogsManager { if(dom) { if(online) { - dom.avatarDiv.classList.add('is-online'); + dom.avatarEl.classList.add('is-online'); } else { - dom.avatarDiv.classList.remove('is-online'); + dom.avatarEl.classList.remove('is-online'); } } } @@ -143,12 +485,19 @@ export class AppDialogsManager { let dialog: any = e.detail; this.setLastMessage(dialog); - this.setUnreadMessages(dialog); this.setDialogPosition(dialog); this.setPinnedDelimiter(); }); + $rootScope.$on('dialog_flush', (e: CustomEvent) => { + let peerID: number = e.detail.peerID; + let dialog = appMessagesManager.getDialogByPeerID(peerID)[0]; + if(dialog) { + this.setLastMessage(dialog); + } + }); + $rootScope.$on('dialogs_multiupdate', (e: CustomEvent) => { let dialogs = e.detail; @@ -162,7 +511,6 @@ export class AppDialogsManager { } this.setLastMessage(dialog); - this.setUnreadMessages(dialog); this.setDialogPosition(dialog); } @@ -309,7 +657,7 @@ export class AppDialogsManager { this.lastActiveListElement = elem; } - result = appImManager.setPeer(peerID, lastMsgID, false, true); + result = appImManager.setPeer(peerID, lastMsgID, true); if(result instanceof Promise) { this.lastGoodClickID = this.lastClickID; @@ -350,6 +698,9 @@ export class AppDialogsManager { let dom = this.getDialogDom(dialog.peerID); let prevPos = whichChild(dom.listEl); + let wrongFolder = (dialog.folder_id == 1 && this.chatList == dom.listEl.parentElement) || (dialog.folder_id == 0 && this.chatListArchived == dom.listEl.parentElement); + if(wrongFolder) prevPos = 0xFFFF; + if(prevPos == pos) { return; } else if(prevPos < pos) { // was higher @@ -408,13 +759,17 @@ export class AppDialogsManager { } ///////console.log('setlastMessage:', lastMessage); - - if(lastMessage._ == 'messageEmpty') return; - if(!dom) { dom = this.getDialogDom(dialog.peerID); } + if(lastMessage._ == 'messageEmpty') { + dom.lastMessageSpan.innerHTML = ''; + dom.lastTimeSpan.innerHTML = ''; + dom.listEl.removeAttribute('data-mid'); + return; + } + let peer = dialog.peer; let peerID = dialog.peerID; //let peerID = appMessagesManager.getMessagePeer(lastMessage); @@ -422,11 +777,12 @@ export class AppDialogsManager { //console.log('setting last message:', lastMessage); /* if(!dom.lastMessageSpan.classList.contains('user-typing')) */ { - /* let messageText = lastMessage.message; - let messageWrapped = ''; - if(messageText) { + + if(highlightWord && lastMessage.message) { + let lastMessageText = appMessagesManager.getRichReplyText(lastMessage, ''); + + let messageText = lastMessage.message; let entities = RichTextProcessor.parseEntities(messageText.replace(/\n/g, ' '), {noLinebreakers: true}); - if(highlightWord) { let regExp = new RegExp(escapeRegExp(highlightWord), 'gi'); let match: any; @@ -436,21 +792,23 @@ export class AppDialogsManager { entities.push({_: 'messageEntityHighlight', length: highlightWord.length, offset: match.index}); found = true; } - + if(found) { entities.sort((a: any, b: any) => a.offset - b.offset); } - } - - messageWrapped = RichTextProcessor.wrapRichText(messageText, { + + let messageWrapped = RichTextProcessor.wrapRichText(messageText, { noLinebreakers: true, entities: entities, noTextFormat: true }); - } */ - //dom.lastMessageSpan.innerHTML = lastMessageText + messageWrapped; - dom.lastMessageSpan.innerHTML = lastMessage.rReply; + dom.lastMessageSpan.innerHTML = lastMessageText + messageWrapped; + } else if(!lastMessage.deleted) { + dom.lastMessageSpan.innerHTML = lastMessage.rReply; + } else { + dom.lastMessageSpan.innerHTML = ''; + } /* if(lastMessage.from_id == auth.id) { // You: */ if(peer._ != 'peerUser' && peerID != -lastMessage.from_id) { @@ -473,25 +831,27 @@ export class AppDialogsManager { } } - let timeStr = ''; - let timestamp = lastMessage.date; - let now = Date.now() / 1000; - let time = new Date(lastMessage.date * 1000); - - if((now - timestamp) < 86400) { // if < 1 day - timeStr = ('0' + time.getHours()).slice(-2) + - ':' + ('0' + time.getMinutes()).slice(-2); - } else if((now - timestamp) < (86400 * 7)) { // week - let date = new Date(timestamp * 1000); - timeStr = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date.getDay()]; - } else { - let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - - timeStr = months[time.getMonth()] + - ' ' + ('0' + time.getDate()).slice(-2); - } - - dom.lastTimeSpan.innerHTML = timeStr; + if(!lastMessage.deleted) { + let timeStr = ''; + let timestamp = lastMessage.date; + let now = Date.now() / 1000; + let time = new Date(lastMessage.date * 1000); + + if((now - timestamp) < 86400) { // if < 1 day + timeStr = ('0' + time.getHours()).slice(-2) + + ':' + ('0' + time.getMinutes()).slice(-2); + } else if((now - timestamp) < (86400 * 7)) { // week + let date = new Date(timestamp * 1000); + timeStr = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date.getDay()]; + } else { + let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + timeStr = months[time.getMonth()] + + ' ' + ('0' + time.getDate()).slice(-2); + } + + dom.lastTimeSpan.innerHTML = timeStr; + } else dom.lastTimeSpan.innerHTML = ''; dom.listEl.setAttribute('data-mid', lastMessage.mid); @@ -504,7 +864,7 @@ export class AppDialogsManager { let dom = this.getDialogDom(dialog.peerID); let lastMessage = appMessagesManager.getMessage(dialog.top_message); - if(lastMessage._ != 'messageEmpty' && + if(lastMessage._ != 'messageEmpty' && !lastMessage.deleted && lastMessage.from_id == $rootScope.myID && lastMessage.peerID != $rootScope.myID && dialog.read_outbox_max_id) { // maybe comment, 06.20.2020 let outgoing = (lastMessage.pFlags && lastMessage.pFlags.unread) @@ -523,12 +883,12 @@ export class AppDialogsManager { dom.unreadMessagesSpan.innerText = ''; dom.unreadMessagesSpan.classList.remove('tgico-pinnedchat'); - if(dialog.unread_count) { - dom.unreadMessagesSpan.innerText = '' + dialog.unread_count; + if(dialog.unread_count || dialog.pFlags.unread_mark) { + dom.unreadMessagesSpan.innerText = '' + (dialog.unread_count || ' '); //dom.unreadMessagesSpan.classList.remove('tgico-pinnedchat'); dom.unreadMessagesSpan.classList.add(new Date(dialog.notify_settings.mute_until * 1000) > new Date() ? 'unread-muted' : 'unread'); - } else if(dialog.pFlags.pinned) { + } else if(dialog.pFlags.pinned && dialog.folder_id == 0) { dom.unreadMessagesSpan.classList.remove('unread', 'unread-muted'); dom.unreadMessagesSpan.classList.add('tgico-pinnedchat'); } @@ -575,8 +935,10 @@ export class AppDialogsManager { let title = appPeersManager.getPeerTitle(peerID, false, onlyFirstName); - let avatarDiv = document.createElement('div'); - avatarDiv.classList.add('user-avatar'); + let avatarEl = new AvatarElement(); + avatarEl.setAttribute('dialog', '1'); + avatarEl.setAttribute('peer', '' + peerID); + avatarEl.classList.add('dialog-avatar'); if(drawStatus && peerID != $rootScope.myID && dialog.peer) { let peer = dialog.peer; @@ -587,7 +949,7 @@ export class AppDialogsManager { //console.log('found user', user); if(user.status && user.status._ == 'userStatusOnline') { - avatarDiv.classList.add('is-online'); + avatarEl.classList.add('is-online'); } break; @@ -618,9 +980,6 @@ export class AppDialogsManager { title = onlyFirstName ? 'Saved' : 'Saved Messages'; } - //console.log('trying to load photo for:', title); - appProfileManager.putPhoto(avatarDiv, dialog.peerID, true); - titleSpan.innerHTML = title; //p.classList.add('') @@ -632,7 +991,7 @@ export class AppDialogsManager { let paddingDiv = document.createElement('div'); paddingDiv.classList.add('rp'); - paddingDiv.append(avatarDiv, captionDiv); + paddingDiv.append(avatarEl, captionDiv); if(rippleEnabled) { ripple(paddingDiv, (id) => { @@ -677,7 +1036,7 @@ export class AppDialogsManager { captionDiv.append(titleP, messageP); let dom: DialogDom = { - avatarDiv, + avatarEl, captionDiv, titleSpan, statusSpan, diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 7108a9b2..1e7304f6 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -19,7 +19,7 @@ import appMessagesIDsManager from "./appMessagesIDsManager"; import apiUpdatesManager from './apiUpdatesManager'; import { wrapDocument, wrapPhoto, wrapVideo, wrapSticker, wrapReply, wrapAlbum, wrapPoll } from '../../components/wrappers'; import ProgressivePreloader from '../../components/preloader'; -import { openBtnMenu, formatPhoneNumber } from '../../components/misc'; +import { openBtnMenu, formatPhoneNumber, positionMenu } from '../../components/misc'; import { ChatInput } from '../../components/chatInput'; //import Scrollable from '../../components/scrollable'; import Scrollable from '../../components/scrollable_new'; @@ -27,6 +27,8 @@ import BubbleGroups from '../../components/bubbleGroups'; import LazyLoadQueue from '../../components/lazyLoadQueue'; import appDocsManager from './appDocsManager'; import appForward from '../../components/appForward'; +import appStickersManager from './appStickersManager'; +import AvatarElement from '../../components/avatar'; console.log('appImManager included!'); @@ -38,7 +40,7 @@ export class AppImManager { public pageEl = document.querySelector('.page-chats') as HTMLDivElement; public btnMute = this.pageEl.querySelector('.tool-mute') as HTMLButtonElement; public btnMenuMute = this.pageEl.querySelector('.menu-mute') as HTMLButtonElement; - public avatarEl = document.getElementById('im-avatar') as HTMLDivElement; + public avatarEl = document.getElementById('im-avatar') as AvatarElement; public titleEl = document.getElementById('im-title') as HTMLDivElement; public subtitleEl = document.getElementById('im-subtitle') as HTMLDivElement; public bubblesContainer = document.getElementById('bubbles') as HTMLDivElement; @@ -71,8 +73,6 @@ export class AppImManager { private pinnedMessageContainer = this.pageEl.querySelector('.pinned-message') as HTMLDivElement; private pinnedMessageContent = this.pinnedMessageContainer.querySelector('.pinned-message-subtitle') as HTMLDivElement; - private firstTopMsgID = 0; - public lazyLoadQueue = new LazyLoadQueue(); public scroll: HTMLDivElement = null; @@ -189,6 +189,13 @@ export class AppImManager { this.deleteMessagesByIDs(Object.keys(detail.msgs).map(s => +s)); }); + + $rootScope.$on('dialog_flush', (e: CustomEvent) => { + let peerID: number = e.detail.peerID; + if(this.peerID == peerID) { + this.deleteMessagesByIDs(Object.keys(this.bubbles).map(m => +m)); + } + }); // Calls when message successfully sent and we have an ID $rootScope.$on('message_sent', (e: CustomEvent) => { @@ -346,7 +353,7 @@ export class AppImManager { return; } - if((target.tagName == 'IMG' && !target.classList.contains('emoji') && !target.parentElement.classList.contains('user-avatar')) + if((target.tagName == 'IMG' && !target.classList.contains('emoji') && target.parentElement.tagName != "AVATAR-ELEMENT") || target.tagName == 'image' || target.classList.contains('album-item') || (target.tagName == 'VIDEO' && !bubble.classList.contains('round'))) { @@ -391,16 +398,16 @@ export class AppImManager { if(['IMG', 'DIV'].indexOf(target.tagName) === -1) target = findUpTag(target, 'DIV'); - if(target.tagName == 'DIV') { + if(target.tagName == 'DIV' || target.tagName == "AVATAR-ELEMENT") { if(target.classList.contains('forward')) { let savedFrom = bubble.dataset.savedFrom; let splitted = savedFrom.split('_'); let peerID = +splitted[0]; let msgID = +splitted[1]; ////this.log('savedFrom', peerID, msgID); - this.setPeer(peerID, msgID, true); + this.setPeer(peerID, msgID/* , true */); return; - } else if(target.classList.contains('user-avatar') || target.classList.contains('name')) { + } else if(target.tagName == "AVATAR-ELEMENT" || target.classList.contains('name')) { let peerID = +target.dataset.peerID; if(!isNaN(peerID)) { @@ -420,7 +427,7 @@ export class AppImManager { let originalMessageID = +bubble.getAttribute('data-original-mid'); this.setPeer(this.peerID, originalMessageID); } - } else if(target.tagName == 'IMG' && target.parentElement.classList.contains('user-avatar')) { + } else if(target.tagName == 'IMG' && target.parentElement.tagName == "AVATAR-ELEMENT") { let peerID = +target.parentElement.dataset.peerID; if(!isNaN(peerID)) { @@ -447,8 +454,8 @@ export class AppImManager { this.setPeer(this.peerID, mid); }); - this.btnMenuMute.addEventListener('click', () => this.mutePeer()); - this.btnMute.addEventListener('click', () => this.mutePeer()); + this.btnMenuMute.addEventListener('click', () => this.mutePeer(this.peerID)); + this.btnMute.addEventListener('click', () => this.mutePeer(this.peerID)); let onKeyDown = (e: KeyboardEvent) => { let target = e.target as HTMLElement; @@ -520,19 +527,7 @@ export class AppImManager { this.contextMenuEdit.style.display = side == 'right' ? '' : 'none'; - this.contextMenu.classList.remove('bottom-left', 'bottom-right'); - this.contextMenu.classList.add(side == 'left' ? 'bottom-right' : 'bottom-left'); - - let {clientX, clientY} = e; - - this.contextMenu.style.left = (side == 'right' ? clientX - this.contextMenu.scrollWidth : clientX) + 'px'; - if((clientY + this.contextMenu.scrollHeight) > window.innerHeight) { - this.contextMenu.style.top = (window.innerHeight - this.contextMenu.scrollHeight) + 'px'; - } else { - this.contextMenu.style.top = clientY + 'px'; - } - - //this.contextMenu.classList.add('active'); + positionMenu(e, this.contextMenu, side as any); openBtnMenu(this.contextMenu); /////this.log('contextmenu', e, bubble, msgID, side); @@ -793,7 +788,7 @@ export class AppImManager { this.isScrollingTimeout = 0; }, 300); - if(this.scroll.scrollHeight - (this.scroll.scrollTop + this.scroll.offsetHeight) == 0/* <= 5 */) { + if(this.scroll.scrollHeight - Math.round(this.scroll.scrollTop + this.scroll.offsetHeight) <= 1/* <= 5 */) { this.scroll.parentElement.classList.add('scrolled-down'); this.scrolledDown = true; } else if(this.scroll.parentElement.classList.contains('scrolled-down')) { @@ -865,14 +860,16 @@ export class AppImManager { appMessagesManager.wrapSingleMessage(chatInfo.pinned_msg_id); } - let participants_count = chatInfo.participants_count || (chatInfo.participants && chatInfo.participants.participants.length); - let subtitle = numberWithCommas(participants_count) + ' ' + (isChannel ? 'subscribers' : 'members'); + let participants_count = chatInfo.participants_count || (chatInfo.participants && chatInfo.participants.participants && chatInfo.participants.participants.length); + if(participants_count) { + let subtitle = numberWithCommas(participants_count) + ' ' + (isChannel ? 'subscribers' : 'members'); - if(onlines > 1) { - subtitle += ', ' + numberWithCommas(onlines) + ' online'; + if(onlines > 1) { + subtitle += ', ' + numberWithCommas(onlines) + ' online'; + } + + this.subtitleEl.innerText = appSidebarRight.profileElements.subtitle.innerText = subtitle; } - - this.subtitleEl.innerText = appSidebarRight.profileElements.subtitle.innerText = subtitle; }); } else if(!appUsersManager.isBot(this.peerID)) { // user let user = appUsersManager.getUser(this.peerID); @@ -914,10 +911,6 @@ export class AppImManager { this.scrolledAllDown = false; this.muted = false; - /* for(let i in this.bubbles) { - let bubble = this.bubbles[i]; - bubble.remove(); - } */ this.bubbles = {}; this.dateMessages = {}; this.bubbleGroups.cleanup(); @@ -949,7 +942,7 @@ export class AppImManager { ////console.timeEnd('appImManager cleanup'); } - public setPeer(peerID: number, lastMsgID = 0, forwarding = false, fromClick = false) { + public setPeer(peerID: number, lastMsgID = 0, fromClick = false) { console.time('appImManager setPeer'); console.time('appImManager setPeer pre promise'); ////console.time('appImManager: pre render start'); @@ -994,6 +987,8 @@ export class AppImManager { appSidebarRight.searchCloseBtn.click(); } + const maxBubbleID = Math.max(...Object.keys(this.bubbles).map(mid => +mid)); + // clear this.cleanup(); @@ -1008,17 +1003,20 @@ export class AppImManager { let dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0] || null; //////this.log('setPeer peerID:', this.peerID, dialog, lastMsgID); - appProfileManager.putPhoto(this.avatarEl, this.peerID); - appProfileManager.putPhoto(appSidebarRight.profileElements.avatar, this.peerID); - this.firstTopMsgID = dialog ? dialog.top_message : 0; - + this.avatarEl.setAttribute('peer', '' + this.peerID); + + const isChannel = appPeersManager.isChannel(peerID); + const hasRights = isChannel && appChatsManager.hasRights(-peerID, 'send'); + if(hasRights) this.chatInner.classList.add('has-rights'); + else this.chatInner.classList.remove('has-rights'); + this.chatInput.style.display = !isChannel || hasRights ? '' : 'none'; + this.chatInner.style.visibility = 'hidden'; - this.chatInput.style.display = appPeersManager.isChannel(peerID) && !appPeersManager.isMegagroup(peerID) ? 'none' : ''; this.topbar.style.display = ''; if(appPeersManager.isAnyGroup(peerID)) this.chatInner.classList.add('is-chat'); else this.chatInner.classList.remove('is-chat'); - if(appPeersManager.isChannel(peerID)) this.chatInner.classList.add('is-channel'); + if(isChannel) this.chatInner.classList.add('is-channel'); else this.chatInner.classList.remove('is-channel'); this.pinnedMessageContainer.style.display = 'none'; window.requestAnimationFrame(() => { @@ -1052,10 +1050,10 @@ export class AppImManager { this.setPeerStatus(true); }); + const isJump = lastMsgID != dialog?.top_message; + // add last message, bc in getHistory will load < max_id - let additionMsgID = 0; - if(lastMsgID && !forwarding) additionMsgID = lastMsgID; - else if(dialog && dialog.top_message) additionMsgID = dialog.top_message; + const additionMsgID = isJump ? 0 : dialog.top_message; /* this.setPeerPromise = null; this.preloader.detach(); @@ -1066,21 +1064,32 @@ export class AppImManager { console.timeEnd('appImManager setPeer pre promise'); this.preloader.attach(this.bubblesContainer); return this.setPeerPromise = Promise.all([ - this.getHistory(forwarding ? lastMsgID + 1 : lastMsgID, true, false, additionMsgID).then(() => { + this.getHistory(lastMsgID, true, isJump, additionMsgID).then(() => { ////this.log('setPeer removing preloader'); if(lastMsgID) { if(!dialog || lastMsgID != dialog.top_message) { let bubble = this.bubbles[lastMsgID]; - if(bubble) this.bubbles[lastMsgID].scrollIntoView(); - else this.log.warn('no bubble by lastMsgID:', lastMsgID); + let fromUp = maxBubbleID && maxBubbleID < lastMsgID; + if(bubble) { + if(this.scrollable.scrollLocked) { + clearTimeout(this.scrollable.scrollLocked); + this.scrollable.scrollLocked = 0; + } + + this.scrollable.scrollIntoView(bubble, samePeer, fromUp); + } else this.log.warn('no bubble by lastMsgID:', lastMsgID); } else { this.log('will scroll down 2'); this.scroll.scrollTop = this.scroll.scrollHeight; } } + if(!lastMsgID || (dialog && dialog.top_message == lastMsgID)) { + this.scrolledAllDown = true; + } + /* this.onScroll(); this.scrollable.onScroll();*/ @@ -1146,17 +1155,8 @@ export class AppImManager { } } - public deleteMessagesByIDs(msgIDs: number[], forever = true) { + public deleteMessagesByIDs(msgIDs: number[]) { msgIDs.forEach(id => { - if(this.firstTopMsgID == id && forever) { - let dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0]; - - if(dialog) { - ///////this.log('setting firstTopMsgID after delete:', id, dialog.top_message, dialog); - this.firstTopMsgID = dialog.top_message; - } - } - if(!(id in this.bubbles)) return; let bubble = this.bubbles[id]; @@ -1173,7 +1173,7 @@ export class AppImManager { } public renderNewMessagesByIDs(msgIDs: number[]) { - if(!this.bubbles[this.firstTopMsgID] && Object.keys(this.bubbles).length) { // seems search active + if(!this.scrolledAllDown) { // seems search active or sliced //////this.log('seems search is active, skipping render:', msgIDs); return; } @@ -1385,20 +1385,32 @@ export class AppImManager { entities: totalEntities }); + let messageMedia = message.media; + if(totalEntities) { let emojiEntities = totalEntities.filter((e: any) => e._ == 'messageEntityEmoji'); let strLength = messageMessage.length; let emojiStrLength = emojiEntities.reduce((acc: number, curr: any) => acc + curr.length, 0); if(emojiStrLength == strLength && emojiEntities.length <= 3) { - let attachmentDiv = document.createElement('div'); - attachmentDiv.classList.add('attachment'); - - attachmentDiv.innerHTML = richText; - - bubble.classList.add('is-message-empty', 'emoji-' + emojiEntities.length + 'x', 'emoji-big'); - - bubbleContainer.append(attachmentDiv); + let sticker = appStickersManager.getAnimatedEmojiSticker(messageMessage); + if(emojiEntities.length == 1 && !messageMedia && sticker) { + messageMedia = { + _: 'messageMediaDocument', + document: sticker + }; + } else { + let attachmentDiv = document.createElement('div'); + attachmentDiv.classList.add('attachment'); + + attachmentDiv.innerHTML = richText; + + bubble.classList.add('emoji-' + emojiEntities.length + 'x'); + + bubbleContainer.append(attachmentDiv); + } + + bubble.classList.add('is-message-empty', 'emoji-big'); } else { messageDiv.innerHTML = richText; } @@ -1430,7 +1442,7 @@ export class AppImManager { } // media - if(message.media/* && message.media._ == 'messageMediaPhoto' */) { + if(messageMedia/* && messageMedia._ == 'messageMediaPhoto' */) { let attachmentDiv = document.createElement('div'); attachmentDiv.classList.add('attachment'); @@ -1440,9 +1452,9 @@ export class AppImManager { let processingWebPage = false; - switch(message.media._) { + switch(messageMedia._) { case 'messageMediaPending': { - let pending = message.media; + let pending = messageMedia; let preloader = pending.preloader as ProgressivePreloader; switch(pending.type) { @@ -1513,7 +1525,7 @@ export class AppImManager { } case 'messageMediaPhoto': { - let photo = message.media.photo; + let photo = messageMedia.photo; ////////this.log('messageMediaPhoto', photo); bubble.classList.add('hide-name', 'photo'); @@ -1541,7 +1553,7 @@ export class AppImManager { case 'messageMediaWebPage': { processingWebPage = true; - let webpage = message.media.webpage; + let webpage = messageMedia.webpage; ////////this.log('messageMediaWebPage', webpage); if(webpage._ == 'webPageEmpty') { break; @@ -1630,7 +1642,7 @@ export class AppImManager { } case 'messageMediaDocument': { - let doc = message.media.document; + let doc = messageMedia.document; //this.log('messageMediaDocument', doc, bubble); @@ -1722,7 +1734,7 @@ export class AppImManager { let contactDiv = document.createElement('div'); contactDiv.classList.add('contact'); - contactDiv.dataset.peerID = '' + message.media.user_id; + contactDiv.dataset.peerID = '' + messageMedia.user_id; messageDiv.classList.add('contact-message'); processingWebPage = true; @@ -1737,10 +1749,11 @@ export class AppImManager {
${message.media.phone_number ? '+' + formatPhoneNumber(message.media.phone_number).formatted : 'Unknown phone number'}
`; - let avatarDiv = document.createElement('div'); - avatarDiv.classList.add('contact-avatar', 'user-avatar'); - contactDiv.prepend(avatarDiv); - appProfileManager.putPhoto(avatarDiv, message.media.user_id); + let avatarElem = new AvatarElement(); + avatarElem.setAttribute('peer', '' + message.media.user_id); + avatarElem.classList.add('contact-avatar'); + + contactDiv.prepend(avatarElem); bubble.classList.remove('is-message-empty'); messageDiv.append(contactDiv); @@ -1841,21 +1854,20 @@ export class AppImManager { } if(!our && this.peerID < 0 && (!appPeersManager.isChannel(this.peerID) || appPeersManager.isMegagroup(this.peerID))) { - let avatarDiv = document.createElement('div'); - avatarDiv.classList.add('user-avatar'); + let avatarElem = new AvatarElement(); + avatarElem.classList.add('user-avatar'); + avatarElem.setAttribute('peer', '' + (message.fromID || 0)); /////////this.log('exec loadDialogPhoto', message); - if(message.fromID) { // if no - user hidden + /* if(message.fromID) { // if no - user hidden appProfileManager.putPhoto(avatarDiv, message.fromID); } else if(!title && message.fwd_from && message.fwd_from.from_name) { title = message.fwd_from.from_name; - appProfileManager.putPhoto(avatarDiv, 0, false, title); - } - - avatarDiv.dataset.peerID = message.fromID; - - bubbleContainer.append(avatarDiv); + appProfileManager.putPhoto(avatarDiv, 0, false); + } */ + + bubbleContainer.append(avatarElem); } } else { bubble.classList.add('hide-name'); @@ -2097,8 +2109,11 @@ export class AppImManager { let backLimit = 0; if(isBackLimit) { backLimit = loadCount; - loadCount = 0; - maxID += 1; + + if(!reverse) { // if not jump + loadCount = 0; + maxID += 1; + } } let result = appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit); @@ -2170,7 +2185,7 @@ export class AppImManager { this.log('getHistory: will slice ids:', ids, reverse); - this.deleteMessagesByIDs(ids, false); + this.deleteMessagesByIDs(ids); /* ids.forEach(id => { this.bubbles[id].remove(); delete this.bubbles[id]; @@ -2222,8 +2237,8 @@ export class AppImManager { this.btnMenuMute.appendChild(rp); } - public mutePeer() { - let inputPeer = appPeersManager.getInputPeerByID(this.peerID); + public mutePeer(peerID: number) { + let inputPeer = appPeersManager.getInputPeerByID(peerID); let inputNotifyPeer = { _: 'inputNotifyPeer', peer: inputPeer @@ -2234,10 +2249,16 @@ export class AppImManager { flags: 0, mute_until: 0 }; + + let dialog = appMessagesManager.getDialogByPeerID(peerID)[0]; + let muted = true; + if(dialog && dialog.notify_settings) { + muted = dialog.notify_settings.mute_until > (Date.now() / 1000 | 0); + } - if(!this.muted) { + if(!muted) { settings.flags |= 1 << 2; - settings.mute_until = 2147483646; + settings.mute_until = 2147483647; } else { settings.flags |= 2; } @@ -2245,7 +2266,7 @@ export class AppImManager { apiManager.invokeApi('account.updateNotifySettings', { peer: inputNotifyPeer, settings: settings - }).then(res => { + }).then(bool => { this.handleUpdate({_: 'updateNotifySettings', peer: inputNotifyPeer, notify_settings: settings}); }); diff --git a/src/lib/appManagers/appMediaViewer.ts b/src/lib/appManagers/appMediaViewer.ts index 4de47e47..52e5d62d 100644 --- a/src/lib/appManagers/appMediaViewer.ts +++ b/src/lib/appManagers/appMediaViewer.ts @@ -8,13 +8,13 @@ import { findUpClassName, $rootScope, generatePathData, fillPropertyValue } from import appDocsManager from "./appDocsManager"; import VideoPlayer from "../mediaPlayer"; import { renderImageFromUrl } from "../../components/misc"; -import appProfileManager from "./appProfileManager"; +import AvatarElement from "../../components/avatar"; export class AppMediaViewer { private overlaysDiv = document.querySelector('.overlays') as HTMLDivElement; private mediaViewerDiv = this.overlaysDiv.firstElementChild as HTMLDivElement; private author = { - avatarEl: this.overlaysDiv.querySelector('.user-avatar') as HTMLDivElement, + avatarEl: this.overlaysDiv.querySelector('.media-viewer-userpic') as AvatarElement, nameEl: this.overlaysDiv.querySelector('.media-viewer-name') as HTMLDivElement, date: this.overlaysDiv.querySelector('.media-viewer-date') as HTMLDivElement }; @@ -590,7 +590,7 @@ export class AppMediaViewer { this.content.caption.innerHTML = ''; } - appProfileManager.putPhoto(this.author.avatarEl, message.fromID); + this.author.avatarEl.setAttribute('peer', '' + message.fromID); // ok set @@ -629,6 +629,10 @@ export class AppMediaViewer { source = video.firstElementChild as HTMLSourceElement; } + if(media.type == 'gif') { + video.autoplay = true; + } + video.dataset.ckin = 'default'; video.dataset.overlay = '1'; @@ -661,9 +665,11 @@ export class AppMediaViewer { video.append(source); } - let player = new VideoPlayer(video, true); + if(media.type != 'gif') { + let player = new VideoPlayer(video, true); + } }); - } else { + } else if(media.type != 'gif') { let player = new VideoPlayer(video, true); } diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index b5932851..db0e2d58 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -56,7 +56,8 @@ export type Dialog = { peerID: number, pinnedIndex: number, pFlags: Partial<{ - pinned: boolean + pinned: true, + unread_mark: true }>, pts: number } @@ -1253,8 +1254,8 @@ export class AppMessagesManager { this.allDialogsLoaded[folderID] = true; } - if(hasPrepend && !this.newDialogsHandlePromise) { - this.newDialogsHandlePromise = window.setTimeout(this.handleNewDialogs.bind(this), 0); + if(hasPrepend) { + this.scheduleHandleNewDialogs(); } else { $rootScope.$broadcast('dialogs_multiupdate', {}); } @@ -1344,7 +1345,7 @@ export class AppMessagesManager { topDate = savedDraft.date; } - if(dialog.pFlags.pinned) { + if(dialog.pFlags.pinned && dialog.folder_id == 0) { topDate = this.generateDialogPinnedDate(dialog); //console.log('topDate', peerID, topDate); } @@ -1354,15 +1355,15 @@ export class AppMessagesManager { public pushDialogToStorage(dialog: Dialog, offsetDate?: number) { let dialogs = this.dialogsStorage.dialogs[dialog.folder_id] ?? (this.dialogsStorage.dialogs[dialog.folder_id] = []); - let pos = this.getDialogByPeerID(dialog.peerID)[1]; - if(pos !== undefined) { + let pos = dialogs.findIndex(d => d.peerID == dialog.peerID); + if(pos !== -1) { dialogs.splice(pos, 1); } if(offsetDate && !dialog.pFlags.pinned && (!this.dialogsOffsetDate[dialog.folder_id] || offsetDate < this.dialogsOffsetDate[dialog.folder_id])) { - if(pos !== undefined) { + if(pos !== -1) { // So the dialog jumped to the last position return false; } @@ -1426,6 +1427,81 @@ export class AppMessagesManager { }).then(this.applyConversations.bind(this)); } + private doFlushHistory(inputPeer: any, justClear: boolean): Promise { + let flags = 0; + if(justClear) { + flags |= 1; + } + + return apiManager.invokeApi('messages.deleteHistory', { + flags: flags, + peer: inputPeer, + max_id: 0 + }).then((affectedHistory) => { + apiUpdatesManager.processUpdateMessage({ + _: 'updateShort', + update: { + _: 'updatePts', + pts: affectedHistory.pts, + pts_count: affectedHistory.pts_count + } + }); + + if(!affectedHistory.offset) { + return true; + } + + return this.doFlushHistory(inputPeer, justClear); + }) + } + + public async flushHistory(peerID: number, justClear: boolean) { + if(AppPeersManager.isChannel(peerID)) { + let promise = this.getHistory(peerID, 0, 1); + + let historyResult = promise instanceof Promise ? await promise : promise; + + let channelID = -peerID; + let maxID = appMessagesIDsManager.getMessageLocalID(historyResult.history[0] || 0); + return apiManager.invokeApi('channels.deleteHistory', { + channel: appChatsManager.getChannelInput(channelID), + max_id: maxID + }).then(() => { + apiUpdatesManager.processUpdateMessage({ + _: 'updateShort', + update: { + _: 'updateChannelAvailableMessages', + channel_id: channelID, + available_min_id: maxID + } + }); + + return true; + }); + } + + return this.doFlushHistory(AppPeersManager.getInputPeerByID(peerID), justClear).then(() => { + delete this.historiesStorage[peerID]; + for(let mid in this.messagesStorage) { + let message = this.messagesStorage[mid]; + if(message.peerID == peerID) { + delete this.messagesStorage[mid]; + } + } + + if(justClear) { + $rootScope.$broadcast('dialog_flush', {peerID: peerID}); + } else { + let foundDialog = this.getDialogByPeerID(peerID); + if(foundDialog[0]) { + this.dialogsStorage.dialogs[foundDialog[0].folder_id].splice(foundDialog[1], 1); + } + + $rootScope.$broadcast('dialog_drop', {peerID: peerID}); + } + }); + } + public saveMessages(apiMessages: any[], options: { isNew?: boolean, isEdited?: boolean @@ -1651,7 +1727,7 @@ export class AppMessagesManager { }); } - public getRichReplyText(message: any) { + public getRichReplyText(message: any, text: string = message.message) { let messageText = ''; if(message.media) { @@ -1739,7 +1815,6 @@ export class AppMessagesManager { messageText = '' + langPack[_] + suffix + ''; } - let text = message.message; let messageWrapped = ''; if(text) { let entities = RichTextProcessor.parseEntities(text.replace(/\n/g, ' '), {noLinebreakers: true}); @@ -1754,6 +1829,69 @@ export class AppMessagesManager { return messageText + messageWrapped; } + public editPeerFolders(peerIDs: number[], folderID: number) { + apiManager.invokeApi('folders.editPeerFolders', { + folder_peers: peerIDs.map(peerID => { + return { + _: 'inputFolderPeer', + peer: AppPeersManager.getInputPeerByID(peerID), + folder_id: folderID + }; + }) + }).then(updates => { + console.log('editPeerFolders updates:', updates); + apiUpdatesManager.processUpdateMessage(updates); // WARNING! возможно тут нужно добавлять channelID, и вызывать апдейт для каждого канала отдельно + }); + } + + public toggleDialogPin(peerID: number) { + let dialog = this.getDialogByPeerID(peerID)[0]; + if(!dialog) return Promise.reject(); + + let peer = { + _: 'inputDialogPeer', + peer: AppPeersManager.getInputPeerByID(peerID) + }; + + let flags = dialog.pFlags?.pinned ? 0 : 1; + return apiManager.invokeApi('messages.toggleDialogPin', { + flags, + peer + }).then(bool => { + this.handleUpdate({ + _: 'updateDialogPinned', + peer: peer, + pFlags: { + pinned: flags + } + }); + }); + } + + public markDialogUnread(peerID: number) { + let dialog = this.getDialogByPeerID(peerID)[0]; + if(!dialog) return Promise.reject(); + + let peer = { + _: 'inputDialogPeer', + peer: AppPeersManager.getInputPeerByID(peerID) + }; + + let flags = dialog.pFlags?.unread_mark ? 0 : 1; + return apiManager.invokeApi('messages.markDialogUnread', { + flags, + peer + }).then(bool => { + this.handleUpdate({ + _: 'updateDialogUnreadMark', + peer: peer, + pFlags: { + unread: flags + } + }); + }); + } + public migrateChecks(migrateFrom: number, migrateTo: number) { if(!this.migratedFromTo[migrateFrom] && !this.migratedToFrom[migrateTo] && @@ -1931,10 +2069,10 @@ export class AppMessagesManager { dialog.read_inbox_max_id = appMessagesIDsManager.getFullMessageID(dialog.read_inbox_max_id, channelID); dialog.read_outbox_max_id = appMessagesIDsManager.getFullMessageID(dialog.read_outbox_max_id, channelID); - this.generateIndexForDialog(dialog); + if(!dialog.hasOwnProperty('folder_id')) dialog.folder_id = 0; dialog.peerID = peerID; - if(!dialog.folder_id) dialog.folder_id = 0; + this.generateIndexForDialog(dialog); this.pushDialogToStorage(dialog, offsetDate); // Because we saved message without dialog present @@ -2283,7 +2421,7 @@ export class AppMessagesManager { this.newDialogsHandlePromise = 0; let newMaxSeenID = 0; - Object.keys(this.newDialogsToHandle).forEach((peerID) => { + for(let peerID in this.newDialogsToHandle) { let dialog = this.newDialogsToHandle[peerID]; if('reload' in dialog) { this.reloadConversation(+peerID); @@ -2294,7 +2432,7 @@ export class AppMessagesManager { newMaxSeenID = Math.max(newMaxSeenID, dialog.top_message || 0); } } - }); + } console.log('after order:', this.dialogsStorage.dialogs[0].map(d => d.peerID)); @@ -2306,6 +2444,12 @@ export class AppMessagesManager { this.newDialogsToHandle = {}; } + public scheduleHandleNewDialogs() { + if(!this.newDialogsHandlePromise) { + this.newDialogsHandlePromise = window.setTimeout(this.handleNewDialogs.bind(this), 0); + } + } + public readHistory(peerID: number, maxID = 0, minID = 0): Promise { // console.trace('start read') var isChannel = AppPeersManager.isChannel(peerID); @@ -2486,9 +2630,7 @@ export class AppMessagesManager { if(!foundDialog.length) { this.newDialogsToHandle[peerID] = {reload: true} - if(!this.newDialogsHandlePromise) { - this.newDialogsHandlePromise = window.setTimeout(this.handleNewDialogs.bind(this), 0); - } + this.scheduleHandleNewDialogs(); if(this.newUpdatesAfterReloadToHandle[peerID] === undefined) { this.newUpdatesAfterReloadToHandle[peerID] = []; } @@ -2572,22 +2714,66 @@ export class AppMessagesManager { } this.newDialogsToHandle[peerID] = dialog; - if(!this.newDialogsHandlePromise) { - this.newDialogsHandlePromise = window.setTimeout(this.handleNewDialogs.bind(this), 0); + this.scheduleHandleNewDialogs(); + + break; + } + + case 'updateDialogUnreadMark': { + console.log('updateDialogUnreadMark', update); + let peerID = AppPeersManager.getPeerID(update.peer.peer); + let foundDialog = this.getDialogByPeerID(peerID); + + if(!foundDialog.length) { + this.newDialogsToHandle[peerID] = {reload: true}; + this.scheduleHandleNewDialogs(); + } else { + let dialog = foundDialog[0]; + + if(!update.pFlags.unread) { + delete dialog.pFlags.unread_mark; + } else { + dialog.pFlags.unread_mark = true; + } + + $rootScope.$broadcast('dialogs_multiupdate', {peerID: dialog}); } break; } + case 'updateFolderPeers': { + console.log('updateFolderPeers', update); + let peers = update.folder_peers; + + this.scheduleHandleNewDialogs(); + peers.forEach((folderPeer: any) => { + let {folder_id, peer} = folderPeer; + + let peerID = AppPeersManager.getPeerID(peer); + let foundDialog = this.getDialogByPeerID(peerID); + if(!foundDialog.length) { + this.newDialogsToHandle[peerID] = {reload: true}; + } else { + let dialog = foundDialog[0]; + this.newDialogsToHandle[peerID] = dialog; + + this.dialogsStorage.dialogs[dialog.folder_id].splice(foundDialog[1], 1); + dialog.folder_id = folder_id; + + this.generateIndexForDialog(dialog); + this.pushDialogToStorage(dialog); // need for simultaneously updatePinnedDialogs + } + }); + break; + } + case 'updateDialogPinned': { console.log('updateDialogPinned', update); let peerID = AppPeersManager.getPeerID(update.peer.peer); let foundDialog = this.getDialogByPeerID(peerID); - if(!this.newDialogsHandlePromise) { - this.newDialogsHandlePromise = window.setTimeout(this.handleNewDialogs.bind(this), 0); - } - + this.scheduleHandleNewDialogs(); if(!foundDialog.length) { this.newDialogsToHandle[peerID] = {reload: true}; break; @@ -2597,6 +2783,7 @@ export class AppMessagesManager { if(!update.pFlags.pinned) { delete dialog.pFlags.pinned; + delete dialog.pinnedIndex; } else { // means set dialog.pFlags.pinned = true; } @@ -2623,9 +2810,7 @@ export class AppMessagesManager { let peerID = dialog.peerID; if(dialog.pFlags.pinned && !newPinned[peerID]) { this.newDialogsToHandle[peerID] = {reload: true}; - if(!this.newDialogsHandlePromise) { - this.newDialogsHandlePromise = window.setTimeout(this.handleNewDialogs.bind(this), 0); - } + this.scheduleHandleNewDialogs(); } }); }); @@ -2665,8 +2850,8 @@ export class AppMessagesManager { } }); - if(willHandle && !this.newDialogsHandlePromise) { - this.newDialogsHandlePromise = window.setTimeout(this.handleNewDialogs.bind(this), 0); + if(willHandle) { + this.scheduleHandleNewDialogs(); } break; @@ -3119,7 +3304,7 @@ export class AppMessagesManager { if(!offsetNotFound && ( historyStorage.count !== null && historyStorage.history.length == historyStorage.count || - historyStorage.history.length >= offset + (limit || 1) + historyStorage.history.length >= offset + limit )) { if(backLimit) { backLimit = Math.min(offset, backLimit); @@ -3134,7 +3319,7 @@ export class AppMessagesManager { history = historyStorage.pending.slice().concat(history); } - return this.wrapHistoryResult(peerID, { + return this.wrapHistoryResult({ count: historyStorage.count, history: history, unreadOffset: unreadOffset, @@ -3166,7 +3351,7 @@ export class AppMessagesManager { history = historyStorage.pending.slice().concat(history); } - return this.wrapHistoryResult(peerID, { + return this.wrapHistoryResult({ count: historyStorage.count, history: history, unreadOffset: unreadOffset, @@ -3190,7 +3375,7 @@ export class AppMessagesManager { history = historyStorage.pending.slice().concat(history); } - return this.wrapHistoryResult(peerID, { + return this.wrapHistoryResult({ count: historyStorage.count, history: history, unreadOffset: unreadOffset, @@ -3262,7 +3447,7 @@ export class AppMessagesManager { }); } - public wrapHistoryResult(peerID: number, result: HistoryResult) { + public wrapHistoryResult(result: HistoryResult) { var unreadOffset = result.unreadOffset; if(unreadOffset) { var i; @@ -3276,7 +3461,6 @@ export class AppMessagesManager { } } return result; - //return Promise.resolve(result); } public requestHistory(peerID: number, maxID: number, limit: number, offset = 0): Promise { diff --git a/src/lib/appManagers/appSidebarLeft.ts b/src/lib/appManagers/appSidebarLeft.ts index 436ab903..c60baf9f 100644 --- a/src/lib/appManagers/appSidebarLeft.ts +++ b/src/lib/appManagers/appSidebarLeft.ts @@ -13,6 +13,10 @@ import { appPeersManager } from "../services"; import popupAvatar from "../../components/popupAvatar"; import appChatsManager from "./appChatsManager"; import { AppSelectPeers } from "../../components/appSelectPeers"; +import AvatarElement from "../../components/avatar"; +import appProfileManager from "./appProfileManager"; + +AvatarElement; const SLIDERITEMSIDS = { archived: 1, @@ -20,6 +24,8 @@ const SLIDERITEMSIDS = { newChannel: 3, addMembers: 4, newGroup: 5, + settings: 6, + editProfile: 7, }; interface SliderTab { @@ -317,6 +323,215 @@ class AppContactsTab implements SliderTab { } } +class AppSettingsTab implements SliderTab { + private container = document.querySelector('.settings-container') as HTMLDivElement; + private avatarElem = this.container.querySelector('.profile-avatar') as AvatarElement; + private nameDiv = this.container.querySelector('.profile-name') as HTMLDivElement; + private phoneDiv = this.container.querySelector('.profile-subtitle') as HTMLDivElement; + + private logOutBtn = this.container.querySelector('.menu-logout') as HTMLButtonElement; + + private buttons: { + edit: HTMLButtonElement, + general: HTMLButtonElement, + notifications: HTMLButtonElement, + privacy: HTMLButtonElement, + language: HTMLButtonElement + } = {} as any; + + constructor() { + (Array.from(this.container.querySelector('.profile-buttons').children) as HTMLButtonElement[]).forEach(el => { + let name = el.className.match(/ menu-(.+?) /)[1]; + // @ts-ignore + this.buttons[name] = el; + }); + + $rootScope.$on('user_auth', (e: CustomEvent) => { + this.fillElements(); + }); + + this.logOutBtn.addEventListener('click', (e) => { + apiManager.logOut(); + }); + + this.buttons.edit.addEventListener('click', () => { + appSidebarLeft.selectTab(SLIDERITEMSIDS.editProfile); + appSidebarLeft.editProfileTab.fillElements(); + }); + } + + public fillElements() { + let user = appUsersManager.getSelf(); + this.avatarElem.setAttribute('peer', '' + user.id); + + this.nameDiv.innerHTML = user.rFullName || ''; + this.phoneDiv.innerHTML = user.rPhone || ''; + } + + onClose() { + + } +} + +class AppEditProfileTab implements SliderTab { + private container = document.querySelector('.edit-profile-container') as HTMLDivElement; + private scrollWrapper = this.container.querySelector('.scroll-wrapper') as HTMLDivElement; + private nextBtn = this.container.querySelector('.btn-corner') as HTMLButtonElement; + private canvas = this.container.querySelector('.avatar-edit-canvas') as HTMLCanvasElement; + private uploadAvatar: () => Promise = null; + + private firstNameInput = this.container.querySelector('.firstname') as HTMLInputElement; + private lastNameInput = this.container.querySelector('.lastname') as HTMLInputElement; + private bioInput = this.container.querySelector('.bio') as HTMLInputElement; + private userNameInput = this.container.querySelector('.username') as HTMLInputElement; + + private avatarElem = document.createElement('avatar-element'); + + private originalValues = { + firstName: '', + lastName: '', + userName: '', + bio: '' + }; + + constructor() { + this.container.querySelector('.avatar-edit').addEventListener('click', () => { + popupAvatar.open(this.canvas, (_upload) => { + this.uploadAvatar = _upload; + this.handleChange(); + this.avatarElem.remove(); + }); + }); + + this.avatarElem.classList.add('avatar-placeholder'); + + let userNameLabel = this.userNameInput.nextElementSibling as HTMLLabelElement; + + this.firstNameInput.addEventListener('input', () => this.handleChange()); + this.lastNameInput.addEventListener('input', () => this.handleChange()); + this.bioInput.addEventListener('input', () => this.handleChange()); + this.userNameInput.addEventListener('input', () => { + this.handleChange(); + let value = this.userNameInput.value; + + console.log('userNameInput:', value); + if(value == this.originalValues.userName) { + this.userNameInput.classList.remove('valid', 'error'); + userNameLabel.innerText = 'Username (optional)'; + return; + } else if(value.length < 5 || value.length > 32 || !/^[a-zA-Z0-9_]+$/.test(value)) { // does not check the last underscore + this.userNameInput.classList.add('error'); + this.userNameInput.classList.remove('valid'); + userNameLabel.innerText = 'Username is invalid'; + } else { + this.userNameInput.classList.remove('error'); + /* */ + } + + apiManager.invokeApi('account.checkUsername', { + username: value + }).then(available => { + if(this.userNameInput.value != value) return; + + if(available) { + this.userNameInput.classList.add('valid'); + this.userNameInput.classList.remove('error'); + userNameLabel.innerText = 'Username is available'; + } else { + this.userNameInput.classList.add('error'); + this.userNameInput.classList.remove('valid'); + userNameLabel.innerText = 'Username is already taken'; + } + }, (err) => { + if(this.userNameInput.value != value) return; + + switch(err.type) { + case 'USERNAME_INVALID': { + this.userNameInput.classList.add('error'); + this.userNameInput.classList.remove('valid'); + userNameLabel.innerText = 'Username is invalid'; + break; + } + } + }); + }); + + this.nextBtn.addEventListener('click', () => { + this.nextBtn.disabled = true; + + let promises: Promise[] = []; + + + promises.push(appProfileManager.updateProfile(this.firstNameInput.value, this.lastNameInput.value, this.bioInput.value).then(() => { + appSidebarLeft.selectTab(0); + }, (err) => { + console.error('updateProfile error:', err); + })); + + if(this.uploadAvatar) { + promises.push(this.uploadAvatar().then(inputFile => { + appProfileManager.uploadProfilePhoto(inputFile); + })); + } + + if(this.userNameInput.value != this.originalValues.userName && this.userNameInput.classList.contains('valid')) { + promises.push(appProfileManager.updateUsername(this.userNameInput.value)); + } + + Promise.race(promises).then(() => { + this.nextBtn.disabled = false; + }, () => { + this.nextBtn.disabled = false; + }); + }); + + let scrollable = new Scrollable(this.scrollWrapper as HTMLElement, 'y'); + } + + public fillElements() { + let user = appUsersManager.getSelf(); + this.firstNameInput.value = this.originalValues.firstName = user.first_name ?? ''; + this.lastNameInput.value = this.originalValues.lastName = user.last_name ?? ''; + this.userNameInput.value = this.originalValues.userName = user.username ?? ''; + + this.userNameInput.classList.remove('valid', 'error'); + this.userNameInput.nextElementSibling.innerHTML = 'Username (optional)'; + + appProfileManager.getProfile(user.id).then(userFull => { + if(userFull.rAbout) { + this.bioInput.value = this.originalValues.bio = userFull.rAbout; + } + }); + + this.avatarElem.setAttribute('peer', '' + $rootScope.myID); + if(!this.avatarElem.parentElement) { + this.canvas.parentElement.append(this.avatarElem); + } + + this.uploadAvatar = null; + } + + private isChanged() { + return !!this.uploadAvatar + || this.firstNameInput.value != this.originalValues.firstName + || this.lastNameInput.value != this.originalValues.lastName + || this.userNameInput.value != this.originalValues.userName + || this.bioInput.value != this.originalValues.bio; + } + + private handleChange() { + if(this.isChanged()) { + this.nextBtn.classList.add('is-visible'); + } else { + this.nextBtn.classList.remove('is-visible'); + } + } + + onCloseAfterTimeout() { + this.nextBtn.classList.remove('is-visible'); + } +} + class AppSidebarLeft { private sidebarEl = document.getElementById('column-left') as HTMLDivElement; private toolsBtn = this.sidebarEl.querySelector('.sidebar-tools-button') as HTMLButtonElement; @@ -329,7 +544,7 @@ class AppSidebarLeft { private contactsBtn = this.menuEl.querySelector('.menu-contacts'); private archivedBtn = this.menuEl.querySelector('.menu-archive'); private savedBtn = this.menuEl.querySelector('.menu-saved'); - private logOutBtn = this.menuEl.querySelector('.menu-logout'); + private settingsBtn = this.menuEl.querySelector('.menu-settings'); public archivedCount = this.archivedBtn.querySelector('.archived-count') as HTMLSpanElement; private newBtnMenu = this.sidebarEl.querySelector('#new-menu'); @@ -343,12 +558,16 @@ class AppSidebarLeft { public addMembersTab = new AppAddMembersTab(); public contactsTab = new AppContactsTab(); public newGroupTab = new AppNewGroupTab(); + public settingsTab = new AppSettingsTab(); + public editProfileTab = new AppEditProfileTab(); private tabs: {[id: number]: SliderTab} = { [SLIDERITEMSIDS.newChannel]: this.newChannelTab, [SLIDERITEMSIDS.contacts]: this.contactsTab, [SLIDERITEMSIDS.addMembers]: this.addMembersTab, [SLIDERITEMSIDS.newGroup]: this.newGroupTab, + [SLIDERITEMSIDS.settings]: this.settingsTab, + [SLIDERITEMSIDS.editProfile]: this.editProfileTab, }; //private log = logger('SL'); @@ -388,8 +607,9 @@ class AppSidebarLeft { this.contactsTab.openContacts(); }); - this.logOutBtn.addEventListener('click', (e) => { - apiManager.logOut(); + this.settingsBtn.addEventListener('click', () => { + this.settingsTab.fillElements(); + this.selectTab(SLIDERITEMSIDS.settings); }); this.searchInput.addEventListener('focus', (e) => { @@ -399,13 +619,13 @@ class AppSidebarLeft { void this.searchContainer.offsetWidth; // reflow this.searchContainer.classList.add('active'); - false && this.searchInput.addEventListener('blur', (e) => { + /* this.searchInput.addEventListener('blur', (e) => { if(!this.searchInput.value) { this.toolsBtn.classList.add('active'); this.backBtn.classList.remove('active'); this.backBtn.click(); } - }, {once: true}); + }, {once: true}); */ }); this.backBtn.addEventListener('click', (e) => { @@ -446,7 +666,7 @@ class AppSidebarLeft { console.log('sidebar-close-button click:', this.historyTabIDs); let closingID = this.historyTabIDs.pop(); // pop current this.onCloseTab(closingID); - this._selectTab(this.historyTabIDs.pop() || 0); + this._selectTab(this.historyTabIDs[this.historyTabIDs.length - 1] || 0); }; Array.from(this.sidebarEl.querySelectorAll('.sidebar-close-button') as any as HTMLElement[]).forEach(el => { el.addEventListener('click', onCloseBtnClick); diff --git a/src/lib/appManagers/appSidebarRight.ts b/src/lib/appManagers/appSidebarRight.ts index 46584cde..2e92cccf 100644 --- a/src/lib/appManagers/appSidebarRight.ts +++ b/src/lib/appManagers/appSidebarRight.ts @@ -14,6 +14,7 @@ import appMediaViewer from "./appMediaViewer"; import LazyLoadQueue from "../../components/lazyLoadQueue"; import { wrapDocument, wrapAudio } from "../../components/wrappers"; import AppSearch, { SearchGroup } from "../../components/appSearch"; +import AvatarElement from "../../components/avatar"; const testScroll = false; @@ -23,7 +24,7 @@ class AppSidebarRight { public profileContentEl = this.sidebarEl.querySelector('.profile-content') as HTMLDivElement; public contentContainer = this.sidebarEl.querySelector('.content-container') as HTMLDivElement; public profileElements = { - avatar: this.profileContentEl.querySelector('.profile-avatar') as HTMLDivElement, + avatar: this.profileContentEl.querySelector('.profile-avatar') as AvatarElement, name: this.profileContentEl.querySelector('.profile-name') as HTMLDivElement, subtitle: this.profileContentEl.querySelector('.profile-subtitle') as HTMLDivElement, bio: this.profileContentEl.querySelector('.profile-row-bio') as HTMLDivElement, @@ -162,7 +163,7 @@ class AppSidebarRight { this.profileElements.notificationsCheckbox.addEventListener('change', () => { //let checked = this.profileElements.notificationsCheckbox.checked; - appImManager.mutePeer(); + appImManager.mutePeer(this.peerID); }); window.addEventListener('resize', this.onSidebarScroll.bind(this)); @@ -576,6 +577,8 @@ class AppSidebarRight { //this.log('fillProfileElements'); this.contentContainer.classList.remove('loaded'); + + this.profileElements.avatar.setAttribute('peer', '' + peerID); window.requestAnimationFrame(() => { this.profileContentEl.parentElement.scrollTop = 0; @@ -660,7 +663,7 @@ class AppSidebarRight { if(peerID > 0) { let user = appUsersManager.getUser(peerID); if(user.phone && peerID != appImManager.myID) { - setText('+' + formatPhoneNumber(user.phone).formatted, this.profileElements.phone); + setText(user.rPhone, this.profileElements.phone); } appProfileManager.getProfile(peerID, true).then(userFull => { diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index 256dc187..2e5825bf 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -43,7 +43,7 @@ export type MTStickerSetFull = { documents: MTDocument[] }; -class appStickersManager { +class AppStickersManager { private documents: { [fileID: string]: MTDocument } = {}; @@ -69,8 +69,12 @@ class appStickersManager { this.stickerSets = sets; } + + if(!this.stickerSets['emoji']) { + this.getStickerSet({id: 'emoji', access_hash: ''}); + } }); - } + } public saveSticker(doc: MTDocument) { if(this.documents[doc.id]) return this.documents[doc.id]; @@ -99,7 +103,9 @@ class appStickersManager { if(this.stickerSets[set.id]) return this.stickerSets[set.id]; let promise = apiManager.invokeApi('messages.getStickerSet', { - stickerset: { + stickerset: set.id == 'emoji' ? { + _: 'inputStickerSetAnimatedEmoji' + } : { _: 'inputStickerSetID', id: set.id, access_hash: set.access_hash @@ -114,19 +120,23 @@ class appStickersManager { documents: MTDocument[] } = res as any; - this.saveStickerSet(stickerSet); + this.saveStickerSet(stickerSet, set.id); return stickerSet; } + + public getAnimatedEmojiSticker(emoji: string) { + let stickerSet = this.stickerSets.emoji; + + return stickerSet.documents.find(doc => doc.attributes.find(attribute => attribute.alt == emoji)); + } public async saveStickerSet(res: { _: "messages.stickerSet", set: MTStickerSet, packs: any[], documents: MTDocument[] - }) { - let id = res.set.id; - + }, id: string) { //console.log('stickers save set', res); this.stickerSets[id] = { @@ -171,4 +181,6 @@ class appStickersManager { } } -export default new appStickersManager(); +const appStickersManager = new AppStickersManager(); +(window as any).appStickersManager = appStickersManager; +export default appStickersManager; diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index d62ff268..977aa4b4 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -4,6 +4,7 @@ import appChatsManager from "./appChatsManager"; //import apiManager from '../mtproto/apiManager'; import apiManager from '../mtproto/mtprotoworker'; import serverTimeManager from "../mtproto/serverTimeManager"; +import { formatPhoneNumber } from "../../components/misc"; export type User = { _: 'user', @@ -28,6 +29,7 @@ export type User = { pFlags: Partial<{verified: boolean, support: boolean, self: boolean, bot: boolean, min: number, deleted: boolean}>, rFirstName?: string, rFullName?: string, + rPhone?: string, sortName?: string, sortStatus?: number, }; @@ -97,6 +99,7 @@ export class AppUsersManager { } $rootScope.$broadcast('user_update', userID); + $rootScope.$broadcast('avatar_update', userID); } else console.warn('No user by id:', userID); break @@ -204,11 +207,9 @@ export class AppUsersManager { } if(apiUser.phone) { - //apiUser.rPhone = $filter('phoneNumber')(apiUser.phone); // warning + apiUser.rPhone = '+' + formatPhoneNumber(apiUser.phone).formatted; } - apiUser.num = (Math.abs(userID) % 8) + 1; - if(apiUser.first_name) { apiUser.rFirstName = RichTextProcessor.wrapRichText(apiUser.first_name, {noLinks: true, noLinebreaks: true}) apiUser.rFullName = apiUser.last_name ? RichTextProcessor.wrapRichText(apiUser.first_name + ' ' + (apiUser.last_name || ''), {noLinks: true, noLinebreaks: true}) : apiUser.rFirstName; @@ -222,8 +223,7 @@ export class AppUsersManager { this.usernames[searchUsername] = userID; } - //apiUser.sortName = apiUser.pFlags.deleted ? '' : SearchIndexManager.cleanSearchText(apiUser.first_name + ' ' + (apiUser.last_name || '')); - apiUser.sortName = apiUser.pFlags.deleted ? '' : apiUser.first_name + ' ' + (apiUser.last_name || ''); + apiUser.sortName = apiUser.pFlags.deleted ? '' : SearchIndexManager.cleanSearchText(apiUser.first_name + ' ' + (apiUser.last_name || ''), false); var nameWords = apiUser.sortName.split(' '); var firstWord = nameWords.shift(); @@ -290,7 +290,7 @@ export class AppUsersManager { return id; } - return this.users[id] || {id: id, pFlags: {deleted: true}, num: 1, access_hash: this.userAccess[id]} as User; + return this.users[id] || {id: id, pFlags: {deleted: true}, access_hash: this.userAccess[id]} as User; } public getSelf() { diff --git a/src/lib/utils.js b/src/lib/utils.js index 37d6025b..4d9cbc73 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -6,30 +6,31 @@ */ var _logTimer = Date.now(); export function dT () { - return '[' + ((Date.now() - _logTimer) / 1000).toFixed(3) + ']' + return '[' + ((Date.now() - _logTimer) / 1000).toFixed(3) + ']'; } -export function checkClick (e, noprevent) { - if (e.which == 1 && (e.ctrlKey || e.metaKey) || e.which == 2) { - return true +export function checkClick(e, noprevent) { + if(e.which == 1 && (e.ctrlKey || e.metaKey) || e.which == 2) { + return true; } - if (!noprevent) { - e.preventDefault() + if(!noprevent) { + e.preventDefault(); } - return false + return false; } -export function isInDOM (element, parentNode) { - if (!element) { - return false +export function isInDOM(element, parentNode) { + if(!element) { + return false; } - parentNode = parentNode || document.body - if (element == parentNode) { - return true + + parentNode = parentNode || document.body; + if(element == parentNode) { + return true; } - return isInDOM(element.parentNode, parentNode) + return isInDOM(element.parentNode, parentNode); } export function checkDragEvent(e) { @@ -48,17 +49,17 @@ export function checkDragEvent(e) { } export function cancelEvent (event) { - event = event || window.event - if (event) { - event = event.originalEvent || event + event = event || window.event; + if(event) { + event = event.originalEvent || event; - if (event.stopPropagation) event.stopPropagation() - if (event.preventDefault) event.preventDefault() - event.returnValue = false - event.cancelBubble = true + if (event.stopPropagation) event.stopPropagation(); + if (event.preventDefault) event.preventDefault(); + event.returnValue = false; + event.cancelBubble = true; } - return false + return false; } export function setFieldSelection (field, from, to) { @@ -370,7 +371,8 @@ export const langPack = { "messageActionChatLeave": "left the group", "messageActionChatDeleteUser": "removed user", "messageActionChatJoinedByLink": "joined the group", - "messageActionPinMessage": "pinned message", + "messageActionPinMessage": "pinned message", + "messageActionContactSignUp": "joined Telegram", "messageActionChannelCreate": "Channel created", "messageActionChannelEditTitle": "Channel renamed", "messageActionChannelEditPhoto": "Channel photo updated", @@ -475,23 +477,6 @@ export function isScrolledIntoView(el) { return isVisible; } -/* export function isScrolledIntoView(el) { - var rect = el.getBoundingClientRect(), top = rect.top, height = rect.height, - el = el.parentNode - // Check if bottom of the element is off the page - if (rect.bottom < 0) return false - // Check its within the document viewport - if (top > document.documentElement.clientHeight) return false - do { - rect = el.getBoundingClientRect() - if (top <= rect.bottom === false) return false - // Check if the element is out of view due to a container scrolling - if ((top + height) <= rect.top) return false - el = el.parentNode - } while (el != document.body) - return true -}; */ - export function whichChild(elem/* : Node */) { let i = 0; // @ts-ignore @@ -529,30 +514,6 @@ export function copy(obj) { return clonedObj; } -/* export function ripple(elem) { - elem.addEventListener('mousedown', function(e) { - let rect = this.getBoundingClientRect(); - - const startTime = Date.now(); - const animationTime = 350; - - let X = e.clientX - rect.left; - let Y = e.clientY - rect.top; - let rippleDiv = document.createElement("div"); - rippleDiv.classList.add("ripple"); - rippleDiv.setAttribute("style", "top:" + Y + "px; left:" + X + "px;"); - this.appendChild(rippleDiv); - - elem.addEventListener('mouseup', () => { - let elapsed = Date.now() - startTime; - - setTimeout(() => { - rippleDiv.parentElement.removeChild(rippleDiv); - }, elapsed < animationTime ? animationTime - elapsed : 0); - }, {once: true}); - }); -}; */ - export function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; @@ -803,94 +764,97 @@ function versionCompare (ver1, ver2) { } } - function cleanSearchText (text) { - var hasTag = text.charAt(0) == '%' - text = text.replace(badCharsRe, ' ').replace(trimRe, '') - text = text.replace(/[^A-Za-z0-9]/g, function (ch) { - var latinizeCh = Config.LatinizeMap[ch] - return latinizeCh !== undefined ? latinizeCh : ch - }) - text = text.toLowerCase() - if (hasTag) { - text = '%' + text + function cleanSearchText(text, latinize = true) { + var hasTag = text.charAt(0) == '%'; + text = text.replace(badCharsRe, ' ').replace(trimRe, ''); + if(latinize) { + text = text.replace(/[^A-Za-z0-9]/g, (ch) => { + var latinizeCh = Config.LatinizeMap[ch]; + return latinizeCh !== undefined ? latinizeCh : ch; + }); + } + + text = text.toLowerCase(); + if(hasTag) { + text = '%' + text; } - return text + return text; } - function cleanUsername (username) { - return username && username.toLowerCase() || '' + function cleanUsername(username) { + return username && username.toLowerCase() || ''; } - function indexObject (id, searchText, searchIndex) { - if (searchIndex.fullTexts[id] !== undefined) { - return false + function indexObject(id, searchText, searchIndex) { + if(searchIndex.fullTexts[id] !== undefined) { + return false; } - searchText = cleanSearchText(searchText) + searchText = cleanSearchText(searchText); - if (!searchText.length) { - return false + if(!searchText.length) { + return false; } - var shortIndexes = searchIndex.shortIndexes + var shortIndexes = searchIndex.shortIndexes; - searchIndex.fullTexts[id] = searchText + searchIndex.fullTexts[id] = searchText; - searchText.split(' ').forEach(function(searchWord) { + searchText.split(' ').forEach((searchWord) => { var len = Math.min(searchWord.length, 3), - wordPart, i - for (i = 1; i <= len; i++) { - wordPart = searchWord.substr(0, i) - if (shortIndexes[wordPart] === undefined) { - shortIndexes[wordPart] = [id] + wordPart, i; + for(i = 1; i <= len; i++) { + wordPart = searchWord.substr(0, i); + if(shortIndexes[wordPart] === undefined) { + shortIndexes[wordPart] = [id]; } else { - shortIndexes[wordPart].push(id) + shortIndexes[wordPart].push(id); } } - }) + }); } - function search (query, searchIndex) { - var shortIndexes = searchIndex.shortIndexes - var fullTexts = searchIndex.fullTexts + function search(query, searchIndex) { + var shortIndexes = searchIndex.shortIndexes; + var fullTexts = searchIndex.fullTexts; - query = cleanSearchText(query) + query = cleanSearchText(query); - var queryWords = query.split(' ') + var queryWords = query.split(' '); var foundObjs = false, - newFoundObjs, i - var j, searchText - var found - - for (i = 0; i < queryWords.length; i++) { - newFoundObjs = shortIndexes[queryWords[i].substr(0, 3)] - if (!newFoundObjs) { - foundObjs = [] - break + newFoundObjs, i; + var j, searchText; + var found; + + for(i = 0; i < queryWords.length; i++) { + newFoundObjs = shortIndexes[queryWords[i].substr(0, 3)]; + if(!newFoundObjs) { + foundObjs = []; + break; } - if (foundObjs === false || foundObjs.length > newFoundObjs.length) { - foundObjs = newFoundObjs + if(foundObjs === false || foundObjs.length > newFoundObjs.length) { + foundObjs = newFoundObjs; } } - newFoundObjs = {} + newFoundObjs = {}; - for (j = 0; j < foundObjs.length; j++) { - found = true - searchText = fullTexts[foundObjs[j]] - for (i = 0; i < queryWords.length; i++) { - if (searchText.indexOf(queryWords[i]) == -1) { - found = false - break + for(j = 0; j < foundObjs.length; j++) { + found = true; + searchText = fullTexts[foundObjs[j]]; + for(i = 0; i < queryWords.length; i++) { + if(searchText.indexOf(queryWords[i]) == -1) { + found = false; + break; } } - if (found) { - newFoundObjs[foundObjs[j]] = true + if(found) { + newFoundObjs[foundObjs[j]] = true; } } - return newFoundObjs + return newFoundObjs; } let SearchIndexManager = { diff --git a/src/pages/pageSignUp.ts b/src/pages/pageSignUp.ts index 95ed7334..394e4e46 100644 --- a/src/pages/pageSignUp.ts +++ b/src/pages/pageSignUp.ts @@ -4,6 +4,7 @@ import pageIm from './pageIm'; import apiManager from '../lib/mtproto/mtprotoworker'; import Page from './page'; import popupAvatar from '../components/popupAvatar'; +import appProfileManager from '../lib/appManagers/appProfileManager'; let authCode: { 'phone_number': string, @@ -45,12 +46,7 @@ let onFirstMount = () => { uploadAvatar().then((inputFile: any) => { console.log('uploaded smthn', inputFile); - apiManager.invokeApi('photos.uploadProfilePhoto', { - file: inputFile - }).then((updateResult) => { - console.log('updated photo!'); - resolve(); - }, reject); + appProfileManager.uploadProfilePhoto(inputFile).then(resolve, reject); }, reject); }); diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 4fbe79db..51a5889c 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -1,5 +1,5 @@ $chat-max-width: 696px; -$time-background: rgba(0, 0, 0, 0.35); +$time-background: rgba(0, 0, 0, .35); #bubble-contextmenu > div { padding: 0 84px 0 16px; @@ -13,7 +13,7 @@ $time-background: rgba(0, 0, 0, 0.35); -webkit-user-select: none; display: flex; align-items: center; - box-shadow: 0 1px 2px 0 rgba(16, 35, 47, 0.07); + box-shadow: 0 1px 2px 0 rgba(16, 35, 47, .07); padding: .5rem 15px; flex: 0 0 auto; /* Forces side columns to stay same width */ min-height: 61px; @@ -51,13 +51,7 @@ $time-background: rgba(0, 0, 0, 0.35); &:hover { background-color: transparent; } - - .user-avatar { - width: 44px; - height: 44px; - line-height: 44px; - } - + .bottom { font-size: 14px; line-height: 18px; @@ -68,6 +62,12 @@ $time-background: rgba(0, 0, 0, 0.35); } } } + + #im-avatar { + width: 44px; + height: 44px; + line-height: 44px; + } } #chat-input { @@ -522,11 +522,13 @@ $time-background: rgba(0, 0, 0, 0.35); } &.is-channel:not(.is-chat) { - padding-bottom: 55px; - .bubble__container { max-width: 100%; } + + &:not(.has-rights) { + padding-bottom: 55px; + } } &:not(.is-channel), &.is-chat { @@ -565,12 +567,6 @@ $time-background: rgba(0, 0, 0, 0.35); } } -#bubble-contextmenu { - position: fixed; - right: auto; - bottom: auto; -} - .popup { &.popup-delete-message { .popup-header { @@ -588,7 +584,7 @@ $time-background: rgba(0, 0, 0, 0.35); background: none; outline: none; border: none; - padding: .5rem .5rem; + padding: .5rem; text-transform: uppercase; transition: .2s; border-radius: $border-radius; diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 43f7e299..296ccac4 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -1,3 +1,17 @@ +@keyframes bubbleSelected { + 0% { + opacity: 0; + } + + 25% { + opacity: 1; + } + + to { + opacity: 0; + } +} + .bubble { padding-top: 5px; /* display: grid; @@ -5,16 +19,23 @@ grid-row-gap: 0px; */ max-width: $chat-max-width; margin: 0 auto; + position: relative; &.is-selected { &:before { position: absolute; - width: 100%; + width: 200%; height: 100%; - background-color: #7ca09f; + background-color: rgba(0, 132, 255, .3); content: " "; display: block; - left: 0; + left: -50%; + top: 0; + animation: bubbleSelected 2s linear; + } + + &:not(.is-group-last):before { + height: calc(100% + 5px); } } @@ -187,7 +208,7 @@ -webkit-user-select: none; } - .attachment { + &:not(.sticker) .attachment { padding-top: .5rem; padding-bottom: 1.5rem; max-width: fit-content!important; @@ -198,6 +219,11 @@ width: auto; } } + + &.sticker .bubble__container { + max-width: 140px; + max-height: 140px; + } } &.emoji-1x .attachment { @@ -233,7 +259,7 @@ } } - &.sticker, &.round { + &.sticker, &.round, &.emoji-big { .bubble__container { cursor: pointer; background: none!important; @@ -245,14 +271,18 @@ img { object-fit: contain; } - + &.is-message-empty .message { + background-color: rgba(0, 0, 0, .23); + } + + /* &.is-message-empty .message { display: none; } &.is-message-empty:hover .message { display: block; - } + } */ } &.sticker { @@ -756,8 +786,16 @@ width: 5rem; } - &.is-edited .time { - width: 90px; + &.is-edited { + .time { + width: 78px !important; + } + + &.emoji-big, &.sticker { + .time { + width: 81px !important; + } + } } &:not(.forwarded).hide-name, &.emoji-big { @@ -821,7 +859,7 @@ bottom: 0; width: 11px; height: 20px; - background-repeat: no-repeat repeat; + background-repeat: no-repeat no-repeat; content: ''; background-size: 11px 20px; background-position-y: 1px; @@ -1171,7 +1209,7 @@ poll-element { margin-top: -1px; display: block; - min-width: 240px; + min-width: 280px; .poll { &-title { diff --git a/src/scss/partials/_chatlist.scss b/src/scss/partials/_chatlist.scss index a8448777..3d7c0fbb 100644 --- a/src/scss/partials/_chatlist.scss +++ b/src/scss/partials/_chatlist.scss @@ -5,6 +5,7 @@ position: relative; width: 100%; margin-left: 22px; + margin-right: 4px; input { background-color: rgba(112, 117, 121, .08); @@ -86,8 +87,10 @@ } } - li.active > .rp { - background: rgba(112, 117, 121, 0.08); + li.active, li.menu-open { + > .rp { + background: rgba(112, 117, 121, 0.08); + } } .pinned-delimiter { @@ -139,7 +142,7 @@ user-select: none; } - .user-avatar { + .dialog-avatar { flex: 0 0 auto; } @@ -157,6 +160,8 @@ } .user-title { + max-width: 82%; + img.emoji { vertical-align: top; margin-top: 4px; @@ -185,6 +190,7 @@ } .user-last-message { + max-width: 86%; img.emoji { width: 20px; height: 20px; @@ -200,8 +206,6 @@ } .user-title, .user-last-message { - max-width: 86%; - i { font-style: normal; color: $color-blue; @@ -285,7 +289,7 @@ // use together like class="chats-container contacts-container" .contacts-container, .search-group-contacts { - .user-avatar { + .dialog-avatar { width: 48px; height: 48px; } diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index 567a3d18..dde9e44e 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -101,7 +101,7 @@ margin: 0; } - .user-avatar { + .dialog-avatar { width: 54px; height: 54px; } @@ -155,7 +155,7 @@ } } -.new-channel-container, .new-group-container { +.new-channel-container, .new-group-container, .edit-profile-container { .sidebar-content { flex-direction: column; } @@ -178,9 +178,76 @@ } .caption { - font-size: 14px; + font-size: 0.875rem; margin-top: 14px; margin-left: 23px; color: #707579; } } + +.new-group-members { + padding: 1.5rem 0 0.4375rem; + + .search-group__name { + text-transform: capitalize; + } +} + +.settings-container { + .profile { + &-button { + display: flex; + padding: 1.125rem 0.625rem; + height: 3.5rem; + line-height: 1.4; + border-radius: 0.625rem; + margin: 0px 0.5rem 0px 0.4375rem; + + &:hover { + background: rgba(112, 117, 121, 0.08); + cursor: pointer; + } + + &:before { + font-size: 24px; + color: #707579; + margin-left: 0.375rem; + margin-top: -0.0625rem; + } + + p { + padding-left: 2rem; + user-select: none; + } + } + + &-buttons { + margin-top: .9375rem; + width: 100%; + } + } +} + +.edit-profile-container { + .caption { + margin-top: 1.063rem; + margin-left: 1.438rem; + line-height: 1.2; + padding-bottom: 1.438rem; + } + + .sidebar-left-h2 { + color: #707579; + padding: 0 1.438rem; + padding-bottom: 1.5rem; + font-weight: 500; + } + + hr { + margin-bottom: 1.5rem; + } + + .scroll-wrapper { + width: 100%; + } +} \ No newline at end of file diff --git a/src/scss/partials/_mediaViewer.scss b/src/scss/partials/_mediaViewer.scss index ec842727..3172afae 100644 --- a/src/scss/partials/_mediaViewer.scss +++ b/src/scss/partials/_mediaViewer.scss @@ -25,22 +25,22 @@ &:hover { color: #fff; } + } - .user-avatar { - width: 44px; - height: 44px; - position: absolute; - top: 8px; - left: 20px; - } - - .media-viewer-name { - font-weight: 500; - } + &-userpic { + width: 44px; + height: 44px; + position: absolute; + top: 8px; + left: 20px; + } - .media-viewer-date { - font-size: 15px; - } + &-name { + font-weight: 500; + } + + &-date { + font-size: 15px; } &-buttons { diff --git a/src/scss/partials/_rightSIdebar.scss b/src/scss/partials/_rightSIdebar.scss index 5ab4d0ab..52d94f44 100644 --- a/src/scss/partials/_rightSIdebar.scss +++ b/src/scss/partials/_rightSIdebar.scss @@ -150,7 +150,7 @@ } } - &-avatar.user-avatar { + &-avatar { width: 120px; height: 120px; margin: 1px auto 21px; diff --git a/src/scss/partials/_selector.scss b/src/scss/partials/_selector.scss index 4ce4869f..13f5d055 100644 --- a/src/scss/partials/_selector.scss +++ b/src/scss/partials/_selector.scss @@ -55,8 +55,9 @@ background-color: #fae2e3; cursor: pointer; - .user-avatar:after { + .selector-user-avatar:after { opacity: 1; + transform: scaleX(-1) rotate(-90deg); } } @@ -69,7 +70,7 @@ animation-direction: reverse; } - .user-avatar { + &-avatar { height: 32px !important; width: 32px !important; float: left; @@ -87,10 +88,10 @@ width: 100%; z-index: 2; font-size: 23px; - line-height: 32px; + line-height: 32px !important; opacity: 0; - transition: .2s opacity; - transform: scaleX(-1); + transition: .2s opacity, .2s transform; + transform: scaleX(-1) rotate(0deg); } } } @@ -101,7 +102,7 @@ } ul { - .user-avatar { + .dialog-avatar { height: 48px; width: 48px; } @@ -133,10 +134,6 @@ } hr { - width: 100%; - height: 1px; - border: none; - background-color: #DADCE0; margin: 0 0 8px; } diff --git a/src/scss/partials/_sidebar.scss b/src/scss/partials/_sidebar.scss index 1ced7c6a..785c2400 100644 --- a/src/scss/partials/_sidebar.scss +++ b/src/scss/partials/_sidebar.scss @@ -14,7 +14,7 @@ display: flex; align-items: center; justify-content: space-between; - padding: 7.5px 20px 7.5px 15px; + padding: 7.5px 16px; min-height: 60px; &__title { diff --git a/src/scss/partials/popups/_peer.scss b/src/scss/partials/popups/_peer.scss new file mode 100644 index 00000000..392d611f --- /dev/null +++ b/src/scss/partials/popups/_peer.scss @@ -0,0 +1,48 @@ +.popup-peer { + $parent: ".popup"; + + #{$parent} { + &-header { + display: flex; + margin-bottom: 0.4375rem; + align-items: center; + padding: 0.125rem 0.25rem; + } + + &-container { + padding: 1rem 1.5rem 0.75rem 1rem; + } + + &-title { + padding-left: 0.75rem; + font-size: 1.25rem; + font-weight: 500; + margin-bottom: 0.125rem; + } + + &-description { + padding: 0 0.25rem; + margin-top: 0; + margin-bottom: 1.625rem; + min-width: 15rem; + max-width: fit-content; + } + + &-buttons { + margin-right: -0.75rem; + + .btn { + font-weight: 500; + + & + .btn { + margin-top: 0.625rem; + } + } + } + } + + .peer-avatar { + height: 2rem; + width: 2rem; + } +} \ No newline at end of file diff --git a/src/scss/style.scss b/src/scss/style.scss index 229425d2..9c1ffca6 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -38,6 +38,7 @@ $large-screen: 1680px; @import "partials/popups/popup"; @import "partials/popups/editAvatar"; @import "partials/popups/mediaAttacher"; +@import "partials/popups/peer"; html, body { height: 100%; @@ -259,6 +260,12 @@ input { } } +#bubble-contextmenu, #dialogs-contextmenu { + position: fixed; + right: auto; + bottom: auto; +} + @keyframes fadeIn { 0% { opacity: 0; @@ -269,7 +276,14 @@ input { } } -.user-avatar { +hr { + width: 100%; + border: none; + border-bottom: 1px solid #DADCE0; + margin: 0 0 8px; +} + +avatar-element { color: #fff; width: 54px; height: 54px; @@ -689,6 +703,15 @@ input { z-index: 2; color: #fff; } + + .avatar-placeholder { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + filter: brightness(0.7); + } } .page-signUp { @@ -775,12 +798,23 @@ input { &.error { border-color: $color-error; - transition: .2s border-width; - + & + label { color: $color-error!important; } } + + &.valid { + border-color: #26962F; + + & + label { + color: #26962F !important; + } + } + + /* &.error, &.valid { + transition: .2s border-width; + } */ &:focus ~ .arrow-down { margin-top: -4px; @@ -894,7 +928,7 @@ input { } } -.input-wrapper > * ~ * { +.input-wrapper > * + * { margin-top: 1.5rem; } diff --git a/tsconfig.json b/tsconfig.json index 398a47e3..117bddac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -66,6 +66,7 @@ "public", "coverage", "./src/lib/ckin.js", + "./src/lib/crypto/crypto.worker.js", "src/lib/config.ts", "./src/lib/StackBlur.js", "./src/lib/*.js",