Unread badge in chat

New arrow
Follow by reply history
Moved from LocalStorage to CacheStorage
Multiple files
Pinned messages inner chat
Pinned message index fix
This commit is contained in:
Eduard Kuzmenko 2020-12-08 21:48:44 +02:00
parent e7f483b573
commit 2580c4e720
49 changed files with 1774 additions and 890 deletions

View File

@ -7,7 +7,6 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager";
import { formatPhoneNumber } from "./misc"; import { formatPhoneNumber } from "./misc";
import appChatsManager from "../lib/appManagers/appChatsManager"; import appChatsManager from "../lib/appManagers/appChatsManager";
import SearchInput from "./searchInput"; import SearchInput from "./searchInput";
import { Peer } from "../layer";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import { escapeRegExp } from "../helpers/string"; import { escapeRegExp } from "../helpers/string";
import searchIndexManager from "../lib/searchIndexManager"; import searchIndexManager from "../lib/searchIndexManager";
@ -237,7 +236,7 @@ export default class AppSearch {
}); });
} }
return this.searchPromise = appMessagesManager.getSearch(this.peerID, query, null, maxID, 20, this.offsetRate).then(res => { return this.searchPromise = appMessagesManager.getSearch(this.peerID, query, {_: 'inputMessagesFilterEmpty'}, maxID, 20, this.offsetRate).then(res => {
this.searchPromise = null; this.searchPromise = null;
if(this.searchInput.value != query) { if(this.searchInput.value != query) {

View File

@ -320,7 +320,7 @@ export default class AppSelectPeers {
if(this.multiSelect) { if(this.multiSelect) {
const selected = this.selected.has(peerID); const selected = this.selected.has(peerID);
dom.containerEl.insertAdjacentHTML('afterbegin', `<div class="checkbox"><label><input type="checkbox" ${selected ? 'checked' : ''}><span></span></label></div>`); dom.containerEl.insertAdjacentHTML('afterbegin', `<div class="checkbox"><label class="checkbox-field"><input type="checkbox" ${selected ? 'checked' : ''}><span></span></label></div>`);
if(selected) dom.listEl.classList.add('active'); if(selected) dom.listEl.classList.add('active');
} }

View File

@ -3,6 +3,7 @@ import appProfileManager from "../lib/appManagers/appProfileManager";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import { cancelEvent } from "../helpers/dom"; import { cancelEvent } from "../helpers/dom";
import AppMediaViewer, { AppMediaViewerAvatar } from "./appMediaViewer"; import AppMediaViewer, { AppMediaViewerAvatar } from "./appMediaViewer";
import { Photo } from "../layer";
rootScope.on('avatar_update', (e) => { rootScope.on('avatar_update', (e) => {
let peerID = e.detail; let peerID = e.detail;
@ -66,6 +67,8 @@ export default class AvatarElement extends HTMLElement {
_: 'messageMediaPhoto', _: 'messageMediaPhoto',
photo: photo photo: photo
}, },
peerID,
date: (photo as Photo.photo).date,
fromID: peerID fromID: peerID
}; };

View File

@ -1,8 +1,9 @@
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import { generatePathData } from "../helpers/dom"; import { generatePathData } from "../helpers/dom";
type BubbleGroup = {timestamp: number, fromID: number, mid: number, group: HTMLDivElement[]};
export default class BubbleGroups { export default class BubbleGroups {
bubblesByGroups: Array<{timestamp: number, fromID: number, mid: number, group: HTMLDivElement[]}> = []; // map to group bubblesByGroups: Array<BubbleGroup> = []; // map to group
groups: Array<HTMLDivElement[]> = []; groups: Array<HTMLDivElement[]> = [];
//updateRAFs: Map<HTMLDivElement[], number> = new Map(); //updateRAFs: Map<HTMLDivElement[], number> = new Map();
newGroupDiff = 120; newGroupDiff = 120;
@ -62,8 +63,9 @@ export default class BubbleGroups {
setClipIfNeeded(bubble: HTMLDivElement, remove = false) { setClipIfNeeded(bubble: HTMLDivElement, remove = false) {
//console.log('setClipIfNeeded', bubble, remove); //console.log('setClipIfNeeded', bubble, remove);
if(bubble.classList.contains('is-message-empty')/* && !bubble.classList.contains('is-reply') */ const className = bubble.className;
&& (bubble.classList.contains('photo') || bubble.classList.contains('video'))) { if(className.includes('is-message-empty')/* && !className.includes('is-reply') */
&& (className.includes('photo') || className.includes('video'))) {
let container = bubble.querySelector('.bubble__media-container') as SVGSVGElement; let container = bubble.querySelector('.bubble__media-container') as SVGSVGElement;
//console.log('setClipIfNeeded', bubble, remove, container); //console.log('setClipIfNeeded', bubble, remove, container);
if(!container) return; if(!container) return;
@ -78,21 +80,21 @@ export default class BubbleGroups {
let path = container.firstElementChild.firstElementChild.lastElementChild as SVGPathElement; let path = container.firstElementChild.firstElementChild.lastElementChild as SVGPathElement;
let width = +object.getAttributeNS(null, 'width'); let width = +object.getAttributeNS(null, 'width');
let height = +object.getAttributeNS(null, 'height'); let height = +object.getAttributeNS(null, 'height');
let isOut = bubble.classList.contains('is-out'); let isOut = className.includes('is-out');
let isReply = bubble.classList.contains('is-reply'); let isReply = className.includes('is-reply');
let d = ''; let d = '';
//console.log('setClipIfNeeded', object, width, height, isOut); //console.log('setClipIfNeeded', object, width, height, isOut);
let tr: number, tl: number; let tr: number, tl: number;
if(bubble.classList.contains('forwarded') || isReply) { if(className.includes('forwarded') || isReply) {
tr = tl = 0; tr = tl = 0;
} else if(isOut) { } else if(isOut) {
tr = bubble.classList.contains('is-group-first') ? 12 : 6; tr = className.includes('is-group-first') ? 12 : 6;
tl = 12; tl = 12;
} else { } else {
tr = 12; tr = 12;
tl = bubble.classList.contains('is-group-first') ? 12 : 6; tl = className.includes('is-group-first') ? 12 : 6;
} }
if(isOut) { if(isOut) {

View File

@ -14,7 +14,7 @@ export default class ChatAudio extends PinnedContainer {
private toggleEl: HTMLElement; private toggleEl: HTMLElement;
constructor(protected topbar: ChatTopbar, protected chat: Chat, protected appMessagesManager: AppMessagesManager, protected appPeersManager: AppPeersManager) { constructor(protected topbar: ChatTopbar, protected chat: Chat, protected appMessagesManager: AppMessagesManager, protected appPeersManager: AppPeersManager) {
super(topbar, chat, 'audio', new DivAndCaption('pinned-audio', (title: string, subtitle: string) => { super(topbar, chat, topbar.listenerSetter, 'audio', new DivAndCaption('pinned-audio', (title: string, subtitle: string) => {
this.divAndCaption.title.innerHTML = title; this.divAndCaption.title.innerHTML = title;
this.divAndCaption.subtitle.innerHTML = subtitle; this.divAndCaption.subtitle.innerHTML = subtitle;
}), () => { }), () => {

View File

@ -1,4 +1,4 @@
import { AppImManager, CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager";
import type { AppMessagesManager, Dialog, HistoryResult } from "../../lib/appManagers/appMessagesManager"; import type { AppMessagesManager, Dialog, HistoryResult } from "../../lib/appManagers/appMessagesManager";
import type { AppSidebarRight } from "../sidebarRight"; import type { AppSidebarRight } from "../sidebarRight";
import type { AppStickersManager } from "../../lib/appManagers/appStickersManager"; import type { AppStickersManager } from "../../lib/appManagers/appStickersManager";
@ -7,22 +7,19 @@ import type { AppInlineBotsManager } from "../../lib/appManagers/AppInlineBotsMa
import type { AppPhotosManager } from "../../lib/appManagers/appPhotosManager"; import type { AppPhotosManager } from "../../lib/appManagers/appPhotosManager";
import type { AppDocsManager } from "../../lib/appManagers/appDocsManager"; import type { AppDocsManager } from "../../lib/appManagers/appDocsManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import { findUpClassName, cancelEvent, findUpTag, CLICK_EVENT_NAME, whichChild } from "../../helpers/dom"; import { findUpClassName, cancelEvent, findUpTag, CLICK_EVENT_NAME, whichChild, getElementByPoint } from "../../helpers/dom";
import { getObjectKeysAndSort } from "../../helpers/object"; import { getObjectKeysAndSort } from "../../helpers/object";
import { isTouchSupported } from "../../helpers/touchSupport"; import { isTouchSupported } from "../../helpers/touchSupport";
import { logger, LogLevels } from "../../lib/logger"; import { logger } from "../../lib/logger";
import rootScope from "../../lib/rootScope"; import rootScope from "../../lib/rootScope";
import AppMediaViewer from "../appMediaViewer"; import AppMediaViewer from "../appMediaViewer";
import BubbleGroups from "../bubbleGroups"; import BubbleGroups from "../bubbleGroups";
import Button from "../button";
import PopupDatePicker from "../popupDatepicker"; import PopupDatePicker from "../popupDatepicker";
import PopupForward from "../popupForward"; import PopupForward from "../popupForward";
import PopupStickers from "../popupStickers"; import PopupStickers from "../popupStickers";
import ProgressivePreloader from "../preloader"; import ProgressivePreloader from "../preloader";
import Scrollable from "../scrollable"; import Scrollable from "../scrollable";
import StickyIntersector from "../stickyIntersector"; import StickyIntersector from "../stickyIntersector";
import ChatContextMenu from "./contextMenu";
import ChatSelection from "./selection";
import animationIntersector from "../animationIntersector"; import animationIntersector from "../animationIntersector";
import { months } from "../../helpers/date"; import { months } from "../../helpers/date";
import RichTextProcessor from "../../lib/richtextprocessor"; import RichTextProcessor from "../../lib/richtextprocessor";
@ -50,8 +47,6 @@ let TEST_SCROLL = TEST_SCROLL_TIMES;
export default class ChatBubbles { export default class ChatBubbles {
bubblesContainer: HTMLDivElement; bubblesContainer: HTMLDivElement;
chatInner: HTMLDivElement; chatInner: HTMLDivElement;
goDownBtn: HTMLButtonElement;
scrollable: Scrollable; scrollable: Scrollable;
scroll: HTMLElement; scroll: HTMLElement;
@ -59,6 +54,7 @@ export default class ChatBubbles {
private getHistoryBottomPromise: Promise<boolean>; private getHistoryBottomPromise: Promise<boolean>;
public peerID = 0; public peerID = 0;
//public messagesCount: number = -1;
public unreadOut = new Set<number>(); public unreadOut = new Set<number>();
public needUpdate: {replyMid: number, mid: number}[] = []; // if need wrapSingleMessage public needUpdate: {replyMid: number, mid: number}[] = []; // if need wrapSingleMessage
@ -104,6 +100,8 @@ export default class ChatBubbles {
public listenerSetter: ListenerSetter; public listenerSetter: ListenerSetter;
public replyFollowHistory: number[] = [];
constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appSidebarRight: AppSidebarRight, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager) { constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appSidebarRight: AppSidebarRight, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager) {
this.chat.log.error('Bubbles construction'); this.chat.log.error('Bubbles construction');
@ -115,9 +113,7 @@ export default class ChatBubbles {
this.chatInner = document.createElement('div'); this.chatInner = document.createElement('div');
this.chatInner.classList.add('bubbles-inner'); this.chatInner.classList.add('bubbles-inner');
this.goDownBtn = Button('bubbles-go-down btn-corner z-depth-1 hide', {icon: 'down'}); this.bubblesContainer.append(this.chatInner);
this.bubblesContainer.append(this.chatInner, this.goDownBtn);
this.setScroll(); this.setScroll();
@ -209,7 +205,7 @@ export default class ChatBubbles {
// set new mids to album items for mediaViewer // set new mids to album items for mediaViewer
if(message.grouped_id) { if(message.grouped_id) {
const items = bubble.querySelectorAll('.album-item'); const items = bubble.querySelectorAll('.grouped-item');
const groupIDs = getObjectKeysAndSort(appMessagesManager.groupedMessagesStorage[message.grouped_id]); const groupIDs = getObjectKeysAndSort(appMessagesManager.groupedMessagesStorage[message.grouped_id]);
(Array.from(items) as HTMLElement[]).forEach((item, idx) => { (Array.from(items) as HTMLElement[]).forEach((item, idx) => {
item.dataset.mid = '' + groupIDs[idx]; item.dataset.mid = '' + groupIDs[idx];
@ -315,10 +311,25 @@ export default class ChatBubbles {
const info = e.detail; const info = e.detail;
const dialog = appMessagesManager.getDialogByPeerID(info.peerID)[0]; const dialog = appMessagesManager.getDialogByPeerID(info.peerID)[0];
if(dialog) { if(dialog?.peerID == this.peerID) {
if(dialog.peerID == this.peerID) { this.chat.input.setUnreadCount();
this.updateUnreadByDialog(dialog); this.updateUnreadByDialog(dialog);
} }
});
this.listenerSetter.add(rootScope, 'dialogs_multiupdate', (e) => {
const dialogs = e.detail;
if(dialogs[this.peerID]) {
this.chat.input.setUnreadCount();
}
});
this.listenerSetter.add(rootScope, 'dialog_notify_settings', (e) => {
const peerID = e.detail;
if(this.peerID == peerID) {
this.chat.input.setUnreadCount();
} }
}); });
@ -362,7 +373,7 @@ export default class ChatBubbles {
} }
//this.chatSelection.toggleByBubble(bubble); //this.chatSelection.toggleByBubble(bubble);
this.chat.selection.toggleByBubble(findUpClassName(target, 'album-item') || bubble); this.chat.selection.toggleByBubble(findUpClassName(target, 'grouped-item') || bubble);
return; return;
} }
@ -494,6 +505,7 @@ export default class ChatBubbles {
} catch(err) {} } catch(err) {}
if(isReplyClick && bubble.classList.contains('is-reply')/* || bubble.classList.contains('forwarded') */) { if(isReplyClick && bubble.classList.contains('is-reply')/* || bubble.classList.contains('forwarded') */) {
this.replyFollowHistory.push(+bubble.dataset.mid);
let originalMessageID = +bubble.getAttribute('data-original-mid'); let originalMessageID = +bubble.getAttribute('data-original-mid');
this.chat.setPeer(this.peerID, originalMessageID); this.chat.setPeer(this.peerID, originalMessageID);
} }
@ -508,18 +520,6 @@ export default class ChatBubbles {
//console.log('chatInner click', e); //console.log('chatInner click', e);
}, {capture: true, passive: false}); }, {capture: true, passive: false});
this.listenerSetter.add(this.goDownBtn, CLICK_EVENT_NAME, (e) => {
cancelEvent(e);
const dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0];
if(dialog) {
this.chat.setPeer(this.peerID/* , dialog.top_message */);
} else {
this.log('will scroll down 3');
this.scroll.scrollTop = this.scroll.scrollHeight;
}
});
this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => { this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => {
for(const timestamp in this.dateMessages) { for(const timestamp in this.dateMessages) {
const dateMessage = this.dateMessages[timestamp]; const dateMessage = this.dateMessages[timestamp];
@ -574,7 +574,43 @@ export default class ChatBubbles {
}); });
} }
public getAlbumBubble(groupID: string) { public onGoDownClick() {
if(this.replyFollowHistory.length) {
this.replyFollowHistory.forEachReverse((mid, idx) => {
const bubble = this.bubbles[mid];
let bad = true;
if(bubble) {
const rect = bubble.getBoundingClientRect();
bad = (this.appPhotosManager.windowH / 2) > rect.top;
} else {
const message = this.appMessagesManager.getMessage(mid);
if(!message.deleted) {
bad = false;
}
}
if(bad) {
this.replyFollowHistory.splice(idx, 1);
}
});
this.replyFollowHistory.sort((a, b) => b - a);
const mid = this.replyFollowHistory.pop();
this.chat.setPeer(this.peerID, mid);
} else {
const dialog = this.appMessagesManager.getDialogByPeerID(this.peerID)[0];
if(dialog) {
this.chat.setPeer(this.peerID/* , dialog.top_message */);
} else {
this.log('will scroll down 3');
this.scroll.scrollTop = this.scroll.scrollHeight;
}
}
}
public getGroupedBubble(groupID: string) {
const group = this.appMessagesManager.groupedMessagesStorage[groupID]; const group = this.appMessagesManager.groupedMessagesStorage[groupID];
for(const mid in group) { for(const mid in group) {
if(this.bubbles[mid]) { if(this.bubbles[mid]) {
@ -588,18 +624,22 @@ export default class ChatBubbles {
return null; return null;
} }
public getBubbleAlbumItems(bubble: HTMLElement) { public getBubbleGroupedItems(bubble: HTMLElement) {
return Array.from(bubble.querySelectorAll('.album-item')) as HTMLElement[]; return Array.from(bubble.querySelectorAll('.grouped-item')) as HTMLElement[];
} }
public getMountedBubble(mid: number) { public getMountedBubble(mid: number) {
const message = this.appMessagesManager.getMessage(mid); const message = this.appMessagesManager.getMessage(mid);
const bubble = this.bubbles[mid]; if(message.grouped_id) {
if(!bubble && message.grouped_id) { const a = this.getGroupedBubble(message.grouped_id);
const a = this.getAlbumBubble(message.grouped_id); if(a) {
if(a) return a; a.bubble = a.bubble.querySelector(`.document-container[data-mid="${mid}"]`) || a.bubble;
return a;
} }
}
const bubble = this.bubbles[mid];
if(!bubble) return; if(!bubble) return;
return {bubble, message}; return {bubble, message};
@ -634,7 +674,7 @@ export default class ChatBubbles {
let dialog = this.appMessagesManager.getDialogByPeerID(this.peerID)[0]; let dialog = this.appMessagesManager.getDialogByPeerID(this.peerID)[0];
// if scroll down after search // if scroll down after search
if(!top && (!dialog || history.indexOf(dialog.top_message) === -1)) { if(!top && (!dialog || history.indexOf(dialog.top_message) === -1)/* && this.chat.type == 'chat' */) {
this.log('Will load more (down) history by maxID:', history[history.length - 1], history); this.log('Will load more (down) history by maxID:', history[history.length - 1], history);
/* false && */this.getHistory(history[history.length - 1], false, true, undefined, justLoad); /* false && */this.getHistory(history[history.length - 1], false, true, undefined, justLoad);
} }
@ -666,11 +706,13 @@ export default class ChatBubbles {
this.scrolledDown = false; this.scrolledDown = false;
} }
if(this.chat.topbar.pinnedMessage) {
this.chat.topbar.pinnedMessage.setCorrectIndex(this.scrollable.lastScrollDirection); this.chat.topbar.pinnedMessage.setCorrectIndex(this.scrollable.lastScrollDirection);
}
}; };
public setScroll() { public setScroll() {
this.scrollable = new Scrollable(this.bubblesContainer/* .firstElementChild */ as HTMLElement, 'IM', 300); this.scrollable = new Scrollable(this.bubblesContainer/* .firstElementChild */ as HTMLElement, 'IM', /* 10300 */300);
/* const getScrollOffset = () => { /* const getScrollOffset = () => {
//return Math.round(Math.max(300, appPhotosManager.windowH / 1.5)); //return Math.round(Math.max(300, appPhotosManager.windowH / 1.5));
@ -684,8 +726,6 @@ export default class ChatBubbles {
this.scrollable = new Scrollable(this.bubblesContainer, 'y', 'IM', this.chatInner, getScrollOffset()); */ this.scrollable = new Scrollable(this.bubblesContainer, 'y', 'IM', this.chatInner, getScrollOffset()); */
this.scroll = this.scrollable.container; this.scroll = this.scrollable.container;
this.bubblesContainer/* .firstElementChild */.append(this.goDownBtn);
this.scrollable.onAdditionalScroll = this.onScroll; this.scrollable.onAdditionalScroll = this.onScroll;
this.scrollable.onScrolledTop = () => this.loadMoreHistory(true); this.scrollable.onScrolledTop = () => this.loadMoreHistory(true);
this.scrollable.onScrolledBottom = () => this.loadMoreHistory(false); this.scrollable.onScrolledBottom = () => this.loadMoreHistory(false);
@ -923,7 +963,6 @@ export default class ChatBubbles {
//console.time('appImManager setPeer pre promise'); //console.time('appImManager setPeer pre promise');
////console.time('appImManager: pre render start'); ////console.time('appImManager: pre render start');
if(peerID == 0) { if(peerID == 0) {
this.goDownBtn.classList.add('hide');
this.cleanup(true); this.cleanup(true);
this.peerID = 0; this.peerID = 0;
return null; return null;
@ -932,14 +971,9 @@ export default class ChatBubbles {
const samePeer = this.peerID == peerID; const samePeer = this.peerID == peerID;
const dialog = this.appMessagesManager.getDialogByPeerID(peerID)[0] || null; const dialog = this.appMessagesManager.getDialogByPeerID(peerID)[0] || null;
let topMessage = lastMsgID <= 0 ? lastMsgID : dialog?.top_message ?? 0; // убрать + 1 после создания базы референсов let topMessage = lastMsgID <= 0 ? lastMsgID : dialog?.top_message ?? 0;
const isTarget = lastMsgID !== undefined; const isTarget = lastMsgID !== undefined;
// @ts-ignore
/* if(topMessage && dialog && dialog.top_message == topMessage && dialog.refetchTopMessage) {
// @ts-ignore
dialog.refetchTopMessage = false;
topMessage += 1;
} */
if(!isTarget && dialog) { if(!isTarget && dialog) {
if(dialog.unread_count && !samePeer) { if(dialog.unread_count && !samePeer) {
lastMsgID = dialog.read_inbox_max_id; lastMsgID = dialog.read_inbox_max_id;
@ -949,26 +983,30 @@ export default class ChatBubbles {
} }
} }
const isJump = lastMsgID != topMessage;
if(samePeer) { if(samePeer) {
const mounted = this.getMountedBubble(lastMsgID); const mounted = this.getMountedBubble(lastMsgID);
if(mounted) { if(mounted) {
if(isTarget) { if(isTarget) {
this.scrollable.scrollIntoView(mounted.bubble); this.scrollable.scrollIntoView(mounted.bubble);
this.highlightBubble(mounted.bubble); this.highlightBubble(mounted.bubble);
} else if(dialog && lastMsgID == topMessage) { this.chat.setListenerResult('setPeer', lastMsgID, false);
} else if(dialog && !isJump) {
//this.log('will scroll down', this.scroll.scrollTop, this.scroll.scrollHeight); //this.log('will scroll down', this.scroll.scrollTop, this.scroll.scrollHeight);
this.scroll.scrollTop = this.scroll.scrollHeight; this.scroll.scrollTop = this.scroll.scrollHeight;
this.chat.setListenerResult('setPeer', lastMsgID, true);
} }
return null; return null;
} }
} else { } else {
this.peerID = peerID; this.peerID = peerID;
this.replyFollowHistory.length = 0;
} }
this.log('setPeer peerID:', this.peerID, dialog, lastMsgID, topMessage); this.log('setPeer peerID:', this.peerID, dialog, lastMsgID, topMessage);
const isJump = lastMsgID != topMessage;
// add last message, bc in getHistory will load < max_id // add last message, bc in getHistory will load < max_id
const additionMsgID = isJump ? 0 : topMessage; const additionMsgID = isJump ? 0 : topMessage;
@ -978,7 +1016,21 @@ export default class ChatBubbles {
//////appSidebarRight.toggleSidebar(true); //////appSidebarRight.toggleSidebar(true);
const maxBubbleID = samePeer && Math.max(...Object.keys(this.bubbles).map(mid => +mid)); let maxBubbleID = 0;
if(samePeer) {
let el = getElementByPoint(this.chat.bubbles.scrollable.container, 'bottom');
//this.chat.log('[PM]: setCorrectIndex: get last element perf:', performance.now() - perf, el);
if(el) {
el = findUpClassName(el, 'bubble');
if(el) { // TODO: а что делать, если id будет -1, -2, -3?
maxBubbleID = +el.dataset.mid;
}
}
if(maxBubbleID <= 0) {
maxBubbleID = Math.max(...Object.keys(this.bubbles).map(mid => +mid));
}
}
const oldChatInner = this.chatInner; const oldChatInner = this.chatInner;
this.cleanup(); this.cleanup();
@ -999,6 +1051,10 @@ export default class ChatBubbles {
this.scrollable.container.innerHTML = ''; this.scrollable.container.innerHTML = '';
//oldChatInner.remove(); //oldChatInner.remove();
if(!samePeer) {
this.chat.finishPeerChange(isTarget, isJump, lastMsgID);
}
this.preloader.attach(this.bubblesContainer); this.preloader.attach(this.bubblesContainer);
} }
@ -1009,6 +1065,10 @@ export default class ChatBubbles {
////this.log('setPeer removing preloader'); ////this.log('setPeer removing preloader');
if(cached) { if(cached) {
if(!samePeer) {
this.chat.finishPeerChange(isTarget, isJump, lastMsgID); // * костыль
}
this.scrollable.container.innerHTML = ''; this.scrollable.container.innerHTML = '';
//oldChatInner.remove(); //oldChatInner.remove();
} else { } else {
@ -1024,7 +1084,7 @@ export default class ChatBubbles {
this.lazyLoadQueue.unlock(); this.lazyLoadQueue.unlock();
//if(dialog && lastMsgID && lastMsgID != topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) { //if(dialog && lastMsgID && lastMsgID != topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) {
if(dialog && (isTarget || (lastMsgID != topMessage)) && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) { if(dialog && (isTarget || isJump)) {
if(this.scrollable.scrollLocked) { if(this.scrollable.scrollLocked) {
clearTimeout(this.scrollable.scrollLocked); clearTimeout(this.scrollable.scrollLocked);
this.scrollable.scrollLocked = 0; this.scrollable.scrollLocked = 0;
@ -1034,9 +1094,12 @@ export default class ChatBubbles {
const forwardingUnread = dialog.read_inbox_max_id == lastMsgID && !isTarget; const forwardingUnread = dialog.read_inbox_max_id == lastMsgID && !isTarget;
if(!fromUp && (samePeer || forwardingUnread)) { if(!fromUp && (samePeer || forwardingUnread)) {
this.scrollable.scrollTop = this.scrollable.scrollHeight; this.scrollable.scrollTop = this.scrollable.scrollHeight;
} else if(fromUp/* && (samePeer || forwardingUnread) */) {
this.scrollable.scrollTop = 0;
} }
let bubble: HTMLElement = forwardingUnread ? (this.firstUnreadBubble || this.bubbles[lastMsgID]) : this.bubbles[lastMsgID]; const mountedByLastMsgID = this.getMountedBubble(lastMsgID);
let bubble: HTMLElement = (forwardingUnread && this.firstUnreadBubble) || mountedByLastMsgID?.bubble;
if(!bubble?.parentElement) { if(!bubble?.parentElement) {
bubble = this.findNextMountedBubbleByMsgID(lastMsgID); bubble = this.findNextMountedBubbleByMsgID(lastMsgID);
} }
@ -1049,6 +1112,8 @@ export default class ChatBubbles {
this.scrollable.scrollTop = this.scrollable.scrollHeight; this.scrollable.scrollTop = this.scrollable.scrollHeight;
} }
this.chat.setListenerResult('setPeer', lastMsgID, !isJump);
// warning // warning
if(!lastMsgID || this.bubbles[topMessage] || lastMsgID == topMessage) { if(!lastMsgID || this.bubbles[topMessage] || lastMsgID == topMessage) {
this.scrolledAllDown = true; this.scrolledAllDown = true;
@ -1084,7 +1149,6 @@ export default class ChatBubbles {
const isAnyGroup = this.appPeersManager.isAnyGroup(peerID); const isAnyGroup = this.appPeersManager.isAnyGroup(peerID);
const isChannel = this.appPeersManager.isChannel(peerID); const isChannel = this.appPeersManager.isChannel(peerID);
const isBroadcast = this.appPeersManager.isBroadcast(peerID);
const canWrite = this.appMessagesManager.canWriteToPeer(peerID); const canWrite = this.appMessagesManager.canWriteToPeer(peerID);
@ -1093,11 +1157,6 @@ export default class ChatBubbles {
this.chatInner.classList.toggle('is-chat', isAnyGroup || peerID == rootScope.myID); this.chatInner.classList.toggle('is-chat', isAnyGroup || peerID == rootScope.myID);
this.chatInner.classList.toggle('is-channel', isChannel); this.chatInner.classList.toggle('is-channel', isChannel);
this.goDownBtn.classList.toggle('is-broadcast', isBroadcast);
window.requestAnimationFrame(() => {
this.goDownBtn.classList.remove('hide');
});
} }
public renderMessagesQueue(message: any, bubble: HTMLDivElement, reverse: boolean) { public renderMessagesQueue(message: any, bubble: HTMLDivElement, reverse: boolean) {
@ -1205,8 +1264,9 @@ export default class ChatBubbles {
public renderMessage(message: any, reverse = false, multipleRender = false, bubble: HTMLDivElement = null, updatePosition = true) { public renderMessage(message: any, reverse = false, multipleRender = false, bubble: HTMLDivElement = null, updatePosition = true) {
this.log.debug('message to render:', message); this.log.debug('message to render:', message);
//return; //return;
const albumMustBeRenderedFull = this.chat.type == 'chat';
if(message.deleted) return; if(message.deleted) return;
else if(message.grouped_id) { // will render only last album's message else if(message.grouped_id && albumMustBeRenderedFull) { // will render only last album's message
const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id];
const maxID = Math.max(...Object.keys(storage).map(i => +i)); const maxID = Math.max(...Object.keys(storage).map(i => +i));
if(message.mid < maxID) { if(message.mid < maxID) {
@ -1215,7 +1275,7 @@ export default class ChatBubbles {
} }
const peerID = this.peerID; const peerID = this.peerID;
const our = message.fromID == rootScope.myID; const our = message.fromID == rootScope.myID; // * can't use 'message.pFlags.out' here because this check will be used to define side of message (left-right)
const messageDiv = document.createElement('div'); const messageDiv = document.createElement('div');
messageDiv.classList.add('message'); messageDiv.classList.add('message');
@ -1233,7 +1293,7 @@ export default class ChatBubbles {
bubble.classList.add('bubble'); bubble.classList.add('bubble');
bubble.appendChild(bubbleContainer); bubble.appendChild(bubbleContainer);
if(!our) { if(!our && !message.pFlags.out) {
//this.log('not our message', message, message.pFlags.unread); //this.log('not our message', message, message.pFlags.unread);
if(message.pFlags.unread) { if(message.pFlags.unread) {
this.unreadedObserver.observe(bubble); this.unreadedObserver.observe(bubble);
@ -1300,7 +1360,9 @@ export default class ChatBubbles {
let messageMedia = message.media; let messageMedia = message.media;
let messageMessage: string, totalEntities: any[]; let messageMessage: string, totalEntities: any[];
if(message.grouped_id) { if(messageMedia?.document && !messageMedia.document.type) {
// * just filter this case
} else if(message.grouped_id && albumMustBeRenderedFull) {
const t = this.appMessagesManager.getAlbumText(message.grouped_id); const t = this.appMessagesManager.getAlbumText(message.grouped_id);
messageMessage = t.message; messageMessage = t.message;
totalEntities = t.totalEntities; totalEntities = t.totalEntities;
@ -1436,6 +1498,7 @@ export default class ChatBubbles {
} }
const isOut = our && (!message.fwd_from || this.peerID != rootScope.myID); const isOut = our && (!message.fwd_from || this.peerID != rootScope.myID);
let nameContainer = bubbleContainer;
// media // media
if(messageMedia/* && messageMedia._ == 'messageMediaPhoto' */) { if(messageMedia/* && messageMedia._ == 'messageMediaPhoto' */) {
@ -1457,7 +1520,7 @@ export default class ChatBubbles {
case 'album': { case 'album': {
this.log('will wrap pending album'); this.log('will wrap pending album');
bubble.classList.add('hide-name', 'photo', 'is-album'); bubble.classList.add('hide-name', 'photo', 'is-album', 'is-grouped');
wrapAlbum({ wrapAlbum({
groupID: '' + message.id, groupID: '' + message.id,
attachmentDiv, attachmentDiv,
@ -1548,8 +1611,8 @@ export default class ChatBubbles {
const tailSupported = !isAndroid; const tailSupported = !isAndroid;
const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id];
if(message.grouped_id && Object.keys(storage).length != 1) { if(message.grouped_id && Object.keys(storage).length != 1 && albumMustBeRenderedFull) {
bubble.classList.add('is-album'); bubble.classList.add('is-album', 'is-grouped');
wrapAlbum({ wrapAlbum({
groupID: message.grouped_id, groupID: message.grouped_id,
attachmentDiv, attachmentDiv,
@ -1713,8 +1776,8 @@ export default class ChatBubbles {
bubble.classList.add('hide-name', doc.type == 'round' ? 'round' : 'video'); bubble.classList.add('hide-name', doc.type == 'round' ? 'round' : 'video');
const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id];
if(message.grouped_id && Object.keys(storage).length != 1) { if(message.grouped_id && Object.keys(storage).length != 1 && albumMustBeRenderedFull) {
bubble.classList.add('is-album'); bubble.classList.add('is-album', 'is-grouped');
wrapAlbum({ wrapAlbum({
groupID: message.grouped_id, groupID: message.grouped_id,
@ -1742,10 +1805,55 @@ export default class ChatBubbles {
break; break;
} else { } else {
const docDiv = wrapDocument(doc, false, false, message.mid); //const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id];
//const isFullAlbum = storage && Object.keys(storage).length != 1;
const mids = albumMustBeRenderedFull ? this.appMessagesManager.getMidsByMid(message.mid) : [message.mid];
mids.forEach((mid, idx) => {
const message = this.appMessagesManager.getMessage(mid);
const doc = message.media.document;
const div = wrapDocument(doc, false, false, mid);
const container = document.createElement('div');
container.classList.add('document-container');
container.dataset.mid = '' + mid;
const wrapper = document.createElement('div');
wrapper.classList.add('document-wrapper');
if(message.message) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('document-message');
const richText = RichTextProcessor.wrapRichText(message.message, {
entities: message.totalEntities
});
messageDiv.innerHTML = richText;
wrapper.append(messageDiv);
}
if(mids.length > 1) {
const selection = document.createElement('div');
selection.classList.add('document-selection');
container.append(selection);
container.classList.add('grouped-item');
if(idx === 0) {
nameContainer = wrapper;
}
}
wrapper.append(div);
container.append(wrapper);
messageDiv.append(container);
});
if(mids.length > 1) {
bubble.classList.add('is-multiple-documents', 'is-grouped');
}
bubble.classList.remove('is-message-empty'); bubble.classList.remove('is-message-empty');
messageDiv.append(docDiv);
messageDiv.classList.add((doc.type != 'photo' ? doc.type || 'document' : 'document') + '-message'); messageDiv.classList.add((doc.type != 'photo' ? doc.type || 'document' : 'document') + '-message');
processingWebPage = true; processingWebPage = true;
@ -1819,6 +1927,8 @@ export default class ChatBubbles {
} */ } */
} }
let savedFrom = '';
if((this.peerID < 0 && !our) || message.fwd_from || message.reply_to_mid) { // chat if((this.peerID < 0 && !our) || message.fwd_from || message.reply_to_mid) { // chat
let title = this.appPeersManager.getPeerTitle(message.fwdFromID || message.fromID); let title = this.appPeersManager.getPeerTitle(message.fwdFromID || message.fromID);
@ -1840,11 +1950,7 @@ export default class ChatBubbles {
} }
if(message.savedFrom) { if(message.savedFrom) {
let goto = document.createElement('div'); savedFrom = message.savedFrom;
goto.classList.add('bubble-beside-button', 'goto-original', 'tgico-next');
bubbleContainer.append(goto);
bubble.dataset.savedFrom = message.savedFrom;
bubble.classList.add('with-beside-button');
} }
if(!bubble.classList.contains('sticker')) { if(!bubble.classList.contains('sticker')) {
@ -1861,7 +1967,7 @@ export default class ChatBubbles {
nameDiv.innerHTML = 'Forwarded from ' + title; nameDiv.innerHTML = 'Forwarded from ' + title;
} }
bubbleContainer.append(nameDiv); nameContainer.append(nameDiv);
} }
} else { } else {
if(message.reply_to_mid) { if(message.reply_to_mid) {
@ -1895,7 +2001,7 @@ export default class ChatBubbles {
nameDiv.innerHTML = title; nameDiv.innerHTML = title;
nameDiv.style.color = this.appPeersManager.getPeerColorByID(message.fromID, false); nameDiv.style.color = this.appPeersManager.getPeerColorByID(message.fromID, false);
nameDiv.dataset.peerID = message.fromID; nameDiv.dataset.peerID = message.fromID;
bubbleContainer.append(nameDiv); nameContainer.append(nameDiv);
} else /* if(!message.reply_to_mid) */ { } else /* if(!message.reply_to_mid) */ {
bubble.classList.add('hide-name'); bubble.classList.add('hide-name');
} }
@ -1921,6 +2027,18 @@ export default class ChatBubbles {
bubble.classList.add('hide-name'); bubble.classList.add('hide-name');
} }
if(this.chat.type == 'pinned') {
savedFrom = `${this.chat.peerID}_${message.mid}`;
}
if(savedFrom) {
const goto = document.createElement('div');
goto.classList.add('bubble-beside-button', 'goto-original', 'tgico-next');
bubbleContainer.append(goto);
bubble.dataset.savedFrom = savedFrom;
bubble.classList.add('with-beside-button');
}
bubble.classList.add(isOut ? 'is-out' : 'is-in'); bubble.classList.add(isOut ? 'is-out' : 'is-in');
if(updatePosition) { if(updatePosition) {
this.bubbleGroups.addBubble(bubble, message, reverse); this.bubbleGroups.addBubble(bubble, message, reverse);
@ -1977,13 +2095,14 @@ export default class ChatBubbles {
const method = (reverse ? history.shift : history.pop).bind(history); const method = (reverse ? history.shift : history.pop).bind(history);
//const padding = 99999; //const padding = 10000;
const realLength = this.scrollable.container.childElementCount; const realLength = this.scrollable.container.childElementCount;
let previousScrollHeightMinusTop: number/* , previousScrollHeight: number */; let previousScrollHeightMinusTop: number/* , previousScrollHeight: number */;
if(realLength > 0 && (reverse || isSafari)) { // for safari need set when scrolling bottom too if(realLength > 0 && (reverse || isSafari)) { // for safari need set when scrolling bottom too
this.messagesQueueOnRender = () => { this.messagesQueueOnRender = () => {
const {scrollTop, scrollHeight} = this.scrollable; const {scrollTop, scrollHeight} = this.scrollable;
//previousScrollHeight = scrollHeight;
//previousScrollHeight = scrollHeight + padding; //previousScrollHeight = scrollHeight + padding;
previousScrollHeightMinusTop = reverse ? scrollHeight - scrollTop : scrollTop; previousScrollHeightMinusTop = reverse ? scrollHeight - scrollTop : scrollTop;
@ -2011,6 +2130,10 @@ export default class ChatBubbles {
/* const scrollHeight = this.scrollable.scrollHeight; /* const scrollHeight = this.scrollable.scrollHeight;
const addedHeight = scrollHeight - previousScrollHeight; const addedHeight = scrollHeight - previousScrollHeight;
this.chatInner.style.paddingTop = (10000 - addedHeight) + 'px'; */
/* const scrollHeight = this.scrollable.scrollHeight;
const addedHeight = scrollHeight - previousScrollHeight;
this.chatInner.style.paddingTop = (padding - addedHeight) + 'px'; this.chatInner.style.paddingTop = (padding - addedHeight) + 'px';
//const newScrollTop = reverse ? scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; //const newScrollTop = reverse ? scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop;
@ -2054,6 +2177,26 @@ export default class ChatBubbles {
}); });
}; };
public requestHistory(maxID: number, loadCount: number, backLimit: number) {
//const middleware = this.getMiddleware();
if(this.chat.type == 'chat') {
return this.appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit);
} else if(this.chat.type == 'pinned') {
const promise = this.appMessagesManager.getSearch(this.peerID, '', {_: 'inputMessagesFilterPinned'}, maxID, loadCount, 0, backLimit);
/* if(maxID) {
promise.then(result => {
if(!middleware()) return;
this.messagesCount = result.count;
this.chat.topbar.setTitle();
});
} */
return promise;
}
}
/** /**
* Load and render history * Load and render history
* @param maxID max message id * @param maxID max message id
@ -2099,7 +2242,7 @@ export default class ChatBubbles {
} }
let additionMsgIDs: number[]; let additionMsgIDs: number[];
if(additionMsgID) { if(additionMsgID && !isBackLimit) {
const historyStorage = this.appMessagesManager.historiesStorage[peerID]; const historyStorage = this.appMessagesManager.historiesStorage[peerID];
if(historyStorage && historyStorage.history.length < loadCount) { if(historyStorage && historyStorage.history.length < loadCount) {
additionMsgIDs = historyStorage.history.slice(); additionMsgIDs = historyStorage.history.slice();
@ -2118,7 +2261,7 @@ export default class ChatBubbles {
/* const result = additionMsgID ? /* const result = additionMsgID ?
{history: [additionMsgID]} : {history: [additionMsgID]} :
appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit); */ appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit); */
let result: ReturnType<AppMessagesManager['getHistory']> | {history: number[]} = this.appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit); let result: ReturnType<AppMessagesManager['getHistory']> | {history: number[]} = this.requestHistory(maxID, loadCount, backLimit) as any;
let resultPromise: Promise<any>; let resultPromise: Promise<any>;
//const isFirstMessageRender = !!additionMsgID && result instanceof Promise && !appMessagesManager.getMessage(additionMsgID).grouped_id; //const isFirstMessageRender = !!additionMsgID && result instanceof Promise && !appMessagesManager.getMessage(additionMsgID).grouped_id;
@ -2251,10 +2394,19 @@ export default class ChatBubbles {
// preload more // preload more
//if(!isFirstMessageRender) { //if(!isFirstMessageRender) {
if(this.chat.type == 'chat') {
const storage = this.appMessagesManager.historiesStorage[peerID];
const isMaxIDInHistory = storage.history.indexOf(maxID) !== -1;
if(isMaxIDInHistory) { // * otherwise it is a search or jump
setTimeout(() => { setTimeout(() => {
if(reverse) {
this.loadMoreHistory(true, true); this.loadMoreHistory(true, true);
} else {
this.loadMoreHistory(false, true); this.loadMoreHistory(false, true);
}
}, 0); }, 0);
}
}
//} //}
}); });

View File

@ -10,6 +10,7 @@ import type { AppProfileManager } from "../../lib/appManagers/appProfileManager"
import type { AppStickersManager } from "../../lib/appManagers/appStickersManager"; import type { AppStickersManager } from "../../lib/appManagers/appStickersManager";
import type { AppUsersManager } from "../../lib/appManagers/appUsersManager"; import type { AppUsersManager } from "../../lib/appManagers/appUsersManager";
import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager"; import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager";
import EventListenerBase from "../../helpers/eventListenerBase";
import { logger, LogLevels } from "../../lib/logger"; import { logger, LogLevels } from "../../lib/logger";
import rootScope from "../../lib/rootScope"; import rootScope from "../../lib/rootScope";
import appSidebarRight, { AppSidebarRight } from "../sidebarRight"; import appSidebarRight, { AppSidebarRight } from "../sidebarRight";
@ -19,7 +20,11 @@ import ChatInput from "./input";
import ChatSelection from "./selection"; import ChatSelection from "./selection";
import ChatTopbar from "./topbar"; import ChatTopbar from "./topbar";
export default class Chat { export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion';
export default class Chat extends EventListenerBase<{
setPeer: (mid: number, isTopMessage: boolean) => void
}> {
public container: HTMLElement; public container: HTMLElement;
public backgroundEl: HTMLElement; public backgroundEl: HTMLElement;
@ -35,7 +40,11 @@ export default class Chat {
public log: ReturnType<typeof logger>; public log: ReturnType<typeof logger>;
public type: ChatType = 'chat';
constructor(public appImManager: AppImManager, private appChatsManager: AppChatsManager, private appDocsManager: AppDocsManager, private appInlineBotsManager: AppInlineBotsManager, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appPhotosManager: AppPhotosManager, private appProfileManager: AppProfileManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appWebPagesManager: AppWebPagesManager, private appSidebarRight: AppSidebarRight, private appPollsManager: AppPollsManager) { constructor(public appImManager: AppImManager, private appChatsManager: AppChatsManager, private appDocsManager: AppDocsManager, private appInlineBotsManager: AppInlineBotsManager, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appPhotosManager: AppPhotosManager, private appProfileManager: AppProfileManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appWebPagesManager: AppWebPagesManager, private appSidebarRight: AppSidebarRight, private appPollsManager: AppPollsManager) {
super();
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.classList.add('chat'); this.container.classList.add('chat');
@ -52,12 +61,25 @@ export default class Chat {
} }
private init() { private init() {
this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager, this.appUsersManager, this.appProfileManager); this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager);
this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appSidebarRight, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appDocsManager, this.appPeersManager, this.appChatsManager); this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appSidebarRight, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appDocsManager, this.appPeersManager, this.appChatsManager);
this.input = new ChatInput(this, this.appMessagesManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager); this.input = new ChatInput(this, this.appMessagesManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager);
this.selection = new ChatSelection(this.bubbles, this.input, this.appMessagesManager); this.selection = new ChatSelection(this.bubbles, this.input, this.appMessagesManager);
this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appChatsManager, this.appPeersManager, this.appPollsManager); this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appChatsManager, this.appPeersManager, this.appPollsManager);
if(this.type == 'chat') {
this.topbar.constructPeerHelpers();
}
this.topbar.construct();
this.input.construct();
if(this.type == 'chat') { // * гений в деле, разный порядок из-за разной последовательности действий
this.input.constructPeerHelpers();
} else if(this.type == 'pinned') {
this.input.constructPinnedHelpers();
}
this.container.append(this.topbar.container, this.bubbles.bubblesContainer, this.input.chatInput); this.container.append(this.topbar.container, this.bubbles.bubblesContainer, this.input.chatInput);
} }
@ -126,24 +148,11 @@ export default class Chat {
return; return;
} }
const {cached, promise} = result; const {promise} = result;
// clear
if(!cached) {
if(!samePeer) {
this.finishPeerChange();
}
}
//console.timeEnd('appImManager setPeer pre promise'); //console.timeEnd('appImManager setPeer pre promise');
this.setPeerPromise = promise.then(() => { this.setPeerPromise = promise.finally(() => {
if(cached) {
if(!samePeer) {
this.finishPeerChange();
}
}
}).finally(() => {
if(this.peerID == peerID) { if(this.peerID == peerID) {
this.setPeerPromise = null; this.setPeerPromise = null;
} }
@ -155,18 +164,19 @@ export default class Chat {
return this.setPeerPromise; return this.setPeerPromise;
} }
public finishPeerChange() { public finishPeerChange(isTarget: boolean, isJump: boolean, lastMsgID: number) {
if(this.peerChanged) return; if(this.peerChanged) return;
let peerID = this.peerID; let peerID = this.peerID;
this.peerChanged = true; this.peerChanged = true;
this.topbar.setPeer(peerID); this.topbar.setPeer(peerID);
this.topbar.finishPeerChange(isTarget, isJump, lastMsgID);
this.bubbles.finishPeerChange(); this.bubbles.finishPeerChange();
this.input.finishPeerChange(); this.input.finishPeerChange();
appSidebarRight.sharedMediaTab.fillProfileElements(); appSidebarRight.sharedMediaTab.fillProfileElements();
rootScope.broadcast('peer_changed', this.peerID); rootScope.broadcast('peer_changed', peerID);
} }
} }

View File

@ -4,7 +4,6 @@ import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type { AppPollsManager, Poll } from "../../lib/appManagers/appPollsManager"; import type { AppPollsManager, Poll } from "../../lib/appManagers/appPollsManager";
import type Chat from "./chat"; import type Chat from "./chat";
import { isTouchSupported } from "../../helpers/touchSupport"; import { isTouchSupported } from "../../helpers/touchSupport";
import rootScope from "../../lib/rootScope";
import { attachClickEvent, cancelEvent, cancelSelection, findUpClassName } from "../../helpers/dom"; import { attachClickEvent, cancelEvent, cancelSelection, findUpClassName } from "../../helpers/dom";
import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu";
import { attachContextMenuListener, openBtnMenu, positionMenu } from "../misc"; import { attachContextMenuListener, openBtnMenu, positionMenu } from "../misc";
@ -18,7 +17,7 @@ export default class ChatContextMenu {
private element: HTMLElement; private element: HTMLElement;
private target: HTMLElement; private target: HTMLElement;
private isTargetAnAlbumItem: boolean; private isTargetAGroupedItem: boolean;
public peerID: number; public peerID: number;
public msgID: number; public msgID: number;
@ -63,10 +62,10 @@ export default class ChatContextMenu {
//this.msgID = msgID; //this.msgID = msgID;
this.target = e.target as HTMLElement; this.target = e.target as HTMLElement;
const albumItem = findUpClassName(this.target, 'album-item'); const groupedItem = findUpClassName(this.target, 'grouped-item');
this.isTargetAnAlbumItem = !!albumItem; this.isTargetAGroupedItem = !!groupedItem;
if(albumItem) { if(groupedItem) {
this.msgID = +albumItem.dataset.mid; this.msgID = +groupedItem.dataset.mid;
} else { } else {
this.msgID = mid; this.msgID = mid;
} }
@ -125,7 +124,7 @@ export default class ChatContextMenu {
cancelSelection(); cancelSelection();
//cancelEvent(e as any); //cancelEvent(e as any);
const bubble = findUpClassName(e.target, 'album-item') || findUpClassName(e.target, 'bubble'); const bubble = findUpClassName(e.target, 'grouped-item') || findUpClassName(e.target, 'bubble');
if(bubble) { if(bubble) {
chat.selection.toggleByBubble(bubble); chat.selection.toggleByBubble(bubble);
} }
@ -138,13 +137,13 @@ export default class ChatContextMenu {
icon: 'reply', icon: 'reply',
text: 'Reply', text: 'Reply',
onClick: this.onReplyClick, onClick: this.onReplyClick,
verify: () => (this.peerID > 0 || this.appChatsManager.hasRights(-this.peerID, 'send')) && this.msgID > 0/* , verify: () => (this.peerID > 0 || this.appChatsManager.hasRights(-this.peerID, 'send')) && this.msgID > 0 && !!this.chat.input.messageInput/* ,
cancelEvent: true */ cancelEvent: true */
}, { }, {
icon: 'edit', icon: 'edit',
text: 'Edit', text: 'Edit',
onClick: this.onEditClick, onClick: this.onEditClick,
verify: () => this.appMessagesManager.canEditMessage(this.msgID, 'text') verify: () => this.appMessagesManager.canEditMessage(this.msgID, 'text') && !!this.chat.input.messageInput
}, { }, {
icon: 'copy', icon: 'copy',
text: 'Copy', text: 'Copy',
@ -163,15 +162,16 @@ export default class ChatContextMenu {
onClick: this.onPinClick, onClick: this.onPinClick,
verify: () => { verify: () => {
const message = this.appMessagesManager.getMessage(this.msgID); const message = this.appMessagesManager.getMessage(this.msgID);
// for new layer return this.msgID > 0 && message._ != 'messageService' && !message.pFlags.pinned && this.appPeersManager.canPinMessage(this.peerID);
// return this.msgID > 0 && message._ != 'messageService' && appImManager.pinnedMsgID != this.msgID && (this.peerID > 0 || appChatsManager.hasRights(-this.peerID, 'pin'));
return this.msgID > 0 && message._ != 'messageService' && /* appImManager.pinnedMsgID != this.msgID && */ (this.peerID == rootScope.myID || (this.peerID < 0 && this.appChatsManager.hasRights(-this.peerID, 'pin')));
} }
}, { }, {
icon: 'unpin', icon: 'unpin',
text: 'Unpin', text: 'Unpin',
onClick: this.onUnpinClick, onClick: this.onUnpinClick,
verify: () => /* appImManager.pinnedMsgID == this.msgID && */ this.appPeersManager.canPinMessage(this.peerID) verify: () => {
const message = this.appMessagesManager.getMessage(this.msgID);
return message.pFlags.pinned && this.appPeersManager.canPinMessage(this.peerID);
}
}, { }, {
icon: 'revote', icon: 'revote',
text: 'Revote', text: 'Revote',
@ -284,12 +284,12 @@ export default class ChatContextMenu {
if(this.chat.selection.isSelecting) { if(this.chat.selection.isSelecting) {
this.chat.selection.selectionForwardBtn.click(); this.chat.selection.selectionForwardBtn.click();
} else { } else {
new PopupForward(this.isTargetAnAlbumItem ? [this.msgID] : this.appMessagesManager.getMidsByMid(this.msgID)); new PopupForward(this.isTargetAGroupedItem ? [this.msgID] : this.appMessagesManager.getMidsByMid(this.msgID));
} }
}; };
private onSelectClick = () => { private onSelectClick = () => {
this.chat.selection.toggleByBubble(findUpClassName(this.target, 'album-item') || findUpClassName(this.target, 'bubble')); this.chat.selection.toggleByBubble(findUpClassName(this.target, 'grouped-item') || findUpClassName(this.target, 'bubble'));
}; };
private onClearSelectionClick = () => { private onClearSelectionClick = () => {
@ -300,7 +300,7 @@ export default class ChatContextMenu {
if(this.chat.selection.isSelecting) { if(this.chat.selection.isSelecting) {
this.chat.selection.selectionDeleteBtn.click(); this.chat.selection.selectionDeleteBtn.click();
} else { } else {
new PopupDeleteMessages(this.isTargetAnAlbumItem ? [this.msgID] : this.appMessagesManager.getMidsByMid(this.msgID)); new PopupDeleteMessages(this.isTargetAGroupedItem ? [this.msgID] : this.appMessagesManager.getMidsByMid(this.msgID));
} }
}; };
} }

View File

@ -22,12 +22,12 @@ import { toast } from "../toast";
import { wrapReply } from "../wrappers"; import { wrapReply } from "../wrappers";
import InputField from '../inputField'; import InputField from '../inputField';
import { MessageEntity } from '../../layer'; import { MessageEntity } from '../../layer';
import MarkupTooltip from './markupTooltip';
import StickersHelper from './stickersHelper'; import StickersHelper from './stickersHelper';
import ButtonIcon from '../buttonIcon'; import ButtonIcon from '../buttonIcon';
import DivAndCaption from '../divAndCaption'; import DivAndCaption from '../divAndCaption';
import ButtonMenuToggle from '../buttonMenuToggle'; import ButtonMenuToggle from '../buttonMenuToggle';
import ListenerSetter from '../../helpers/listenerSetter'; import ListenerSetter from '../../helpers/listenerSetter';
import Button from '../button';
const RECORD_MIN_TIME = 500; const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -94,11 +94,19 @@ export default class ChatInput {
public stickersHelper: StickersHelper; public stickersHelper: StickersHelper;
public listenerSetter: ListenerSetter; public listenerSetter: ListenerSetter;
public pinnedControlBtn: HTMLButtonElement;
public goDownBtn: HTMLButtonElement;
public goDownUnreadBadge: HTMLElement;
constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appDocsManager: AppDocsManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appWebPagesManager: AppWebPagesManager, private appImManager: AppImManager) { constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appDocsManager: AppDocsManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appWebPagesManager: AppWebPagesManager, private appImManager: AppImManager) {
this.listenerSetter = new ListenerSetter(); this.listenerSetter = new ListenerSetter();
}
public construct() {
this.chatInput = document.createElement('div'); this.chatInput = document.createElement('div');
this.chatInput.classList.add('chat-input'); this.chatInput.classList.add('chat-input');
this.chatInput.style.display = 'none';
this.inputContainer = document.createElement('div'); this.inputContainer = document.createElement('div');
this.inputContainer.classList.add('chat-input-container'); this.inputContainer.classList.add('chat-input-container');
@ -106,6 +114,24 @@ export default class ChatInput {
this.rowsWrapper = document.createElement('div'); this.rowsWrapper = document.createElement('div');
this.rowsWrapper.classList.add('rows-wrapper'); this.rowsWrapper.classList.add('rows-wrapper');
this.inputContainer.append(this.rowsWrapper);
this.chatInput.append(this.inputContainer);
this.goDownBtn = Button('bubbles-go-down btn-corner btn-circle z-depth-1 hide', {icon: 'arrow-down'});
this.goDownUnreadBadge = document.createElement('span');
this.goDownUnreadBadge.classList.add('badge', 'badge-24', 'badge-green');
this.goDownBtn.append(this.goDownUnreadBadge);
this.chatInput.append(this.goDownBtn);
this.listenerSetter.add(this.goDownBtn, CLICK_EVENT_NAME, (e) => {
cancelEvent(e);
this.chat.bubbles.onGoDownClick();
});
// * constructor end
}
public constructPeerHelpers() {
this.replyElements.container = document.createElement('div'); this.replyElements.container = document.createElement('div');
this.replyElements.container.classList.add('reply-wrapper'); this.replyElements.container.classList.add('reply-wrapper');
@ -137,7 +163,7 @@ export default class ChatInput {
this.willAttachType = 'media'; this.willAttachType = 'media';
this.fileInput.click(); this.fileInput.click();
}, },
verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media') verify: (peerID: number) => peerID > 0 || this.appChatsManager.hasRights(peerID, 'send', 'send_media')
}, { }, {
icon: 'document', icon: 'document',
text: 'Document', text: 'Document',
@ -147,14 +173,14 @@ export default class ChatInput {
this.willAttachType = 'document'; this.willAttachType = 'document';
this.fileInput.click(); this.fileInput.click();
}, },
verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media') verify: (peerID: number) => peerID > 0 || this.appChatsManager.hasRights(peerID, 'send', 'send_media')
}, { }, {
icon: 'poll', icon: 'poll',
text: 'Poll', text: 'Poll',
onClick: () => { onClick: () => {
new PopupCreatePoll(this.chat.peerID).show(); new PopupCreatePoll(this.chat.peerID).show();
}, },
verify: (peerID: number) => peerID < 0 && appChatsManager.hasRights(peerID, 'send', 'send_polls') verify: (peerID: number) => peerID < 0 && this.appChatsManager.hasRights(peerID, 'send', 'send_polls')
}]; }];
this.attachMenu = ButtonMenuToggle({noRipple: true, listenerSetter: this.listenerSetter}, 'top-left', this.attachMenuButtons); this.attachMenu = ButtonMenuToggle({noRipple: true, listenerSetter: this.listenerSetter}, 'top-left', this.attachMenuButtons);
@ -189,12 +215,8 @@ export default class ChatInput {
this.btnSendContainer.append(this.recordRippleEl, this.btnSend); this.btnSendContainer.append(this.recordRippleEl, this.btnSend);
this.inputContainer.append(this.rowsWrapper, this.btnCancelRecord, this.btnSendContainer); this.inputContainer.append(this.btnCancelRecord, this.btnSendContainer);
this.chatInput.append(this.inputContainer);
// * constructor end
const toggleClass = isTouchSupported ? 'flip-icon' : 'active';
emoticonsDropdown.attachButtonListener(this.btnToggleEmoticons); emoticonsDropdown.attachButtonListener(this.btnToggleEmoticons);
emoticonsDropdown.events.onOpen.push(this.onEmoticonsOpen); emoticonsDropdown.events.onOpen.push(this.onEmoticonsOpen);
emoticonsDropdown.events.onClose.push(this.onEmoticonsClose); emoticonsDropdown.events.onClose.push(this.onEmoticonsClose);
@ -272,7 +294,7 @@ export default class ChatInput {
let peerID = this.chat.peerID; let peerID = this.chat.peerID;
// тут objectURL ставится уже с audio/wav // тут objectURL ставится уже с audio/wav
appMessagesManager.sendFile(peerID, dataBlob, { this.appMessagesManager.sendFile(peerID, dataBlob, {
isVoiceMessage: true, isVoiceMessage: true,
isMedia: true, isMedia: true,
duration, duration,
@ -290,6 +312,34 @@ export default class ChatInput {
this.listenerSetter.add(this.replyElements.container, CLICK_EVENT_NAME, this.onHelperClick); this.listenerSetter.add(this.replyElements.container, CLICK_EVENT_NAME, this.onHelperClick);
} }
public constructPinnedHelpers() {
const container = document.createElement('div');
container.classList.add('pinned-container');
this.pinnedControlBtn = Button('btn-primary btn-transparent pinned-container-button', {icon: 'unpin'});
container.append(this.pinnedControlBtn);
this.listenerSetter.add(this.pinnedControlBtn, 'click', () => {
const peerID = this.chat.peerID;
let promise: Promise<any>;
if(this.appPeersManager.canPinMessage(peerID)) {
promise = this.appMessagesManager.unpinAllMessages(peerID);
} else {
promise = this.appMessagesManager.hidePinnedMessages(peerID);
}
promise.then(() => {
this.chat.appImManager.setPeer(0); // * close tab
});
});
this.rowsWrapper.append(container);
this.chatInput.classList.add('type-pinned');
this.rowsWrapper.classList.add('is-centered');
}
private onEmoticonsOpen = () => { private onEmoticonsOpen = () => {
const toggleClass = isTouchSupported ? 'flip-icon' : 'active'; const toggleClass = isTouchSupported ? 'flip-icon' : 'active';
this.btnToggleEmoticons.classList.toggle(toggleClass, true); this.btnToggleEmoticons.classList.toggle(toggleClass, true);
@ -300,6 +350,13 @@ export default class ChatInput {
this.btnToggleEmoticons.classList.toggle(toggleClass, false); this.btnToggleEmoticons.classList.toggle(toggleClass, false);
}; };
public setUnreadCount() {
const dialog = this.appMessagesManager.getDialogByPeerID(this.chat.peerID)[0];
const count = dialog?.unread_count;
this.goDownUnreadBadge.innerText = '' + (count || '');
this.goDownUnreadBadge.classList.toggle('badge-gray', this.appMessagesManager.isPeerMuted(this.chat.peerID));
}
public destroy() { public destroy() {
this.chat.log.error('Input destroying'); this.chat.log.error('Input destroying');
@ -312,25 +369,43 @@ export default class ChatInput {
public cleanup() { public cleanup() {
if(!this.chat.peerID) { if(!this.chat.peerID) {
this.chatInput.style.display = 'none'; this.chatInput.style.display = 'none';
this.goDownBtn.classList.add('hide');
} }
cancelSelection(); cancelSelection();
if(this.messageInput) {
this.clearInput(); this.clearInput();
this.clearHelper(); this.clearHelper();
} }
}
public finishPeerChange() { public finishPeerChange() {
const peerID = this.chat.peerID; const peerID = this.chat.peerID;
this.chatInput.style.display = '';
const isBroadcast = this.appPeersManager.isBroadcast(peerID);
this.goDownBtn.classList.toggle('is-broadcast', isBroadcast);
this.goDownBtn.classList.remove('hide');
if(this.goDownUnreadBadge) {
this.setUnreadCount();
}
if(this.messageInput) {
const canWrite = this.appMessagesManager.canWriteToPeer(peerID);
this.chatInput.classList.add('no-transition');
this.chatInput.classList.toggle('is-hidden', !canWrite);
void this.chatInput.offsetLeft; // reflow
this.chatInput.classList.remove('no-transition');
const visible = this.attachMenuButtons.filter(button => { const visible = this.attachMenuButtons.filter(button => {
const good = button.verify(peerID); const good = button.verify(peerID);
button.element.classList.toggle('hide', !good); button.element.classList.toggle('hide', !good);
return good; return good;
}); });
const canWrite = this.appMessagesManager.canWriteToPeer(peerID);
this.chatInput.style.display = '';
this.chatInput.classList.toggle('is-hidden', !canWrite);
if(!canWrite) { if(!canWrite) {
this.messageInput.removeAttribute('contenteditable'); this.messageInput.removeAttribute('contenteditable');
} else { } else {
@ -339,6 +414,9 @@ export default class ChatInput {
this.attachMenu.toggleAttribute('disabled', !visible.length); this.attachMenu.toggleAttribute('disabled', !visible.length);
this.updateSendBtn(); this.updateSendBtn();
} else if(this.pinnedControlBtn) {
this.pinnedControlBtn.append(this.appPeersManager.canPinMessage(this.chat.peerID) ? 'Unpin all messages' : 'Don\'t show pinned messages');
}
} }
private attachMessageInputField() { private attachMessageInputField() {

View File

@ -1,5 +1,6 @@
import { getFullDate } from "../../helpers/date"; import { getFullDate } from "../../helpers/date";
import { formatNumber } from "../../helpers/number"; import { formatNumber } from "../../helpers/number";
import appImManager from "../../lib/appManagers/appImManager";
import RichTextProcessor from "../../lib/richtextprocessor"; import RichTextProcessor from "../../lib/richtextprocessor";
type Message = any; type Message = any;
@ -39,7 +40,7 @@ export namespace MessageRender {
time = '<i class="edited">edited</i> ' + time; time = '<i class="edited">edited</i> ' + time;
} }
if(message.pFlags.pinned) { if(appImManager.chat.type != 'pinned' && message.pFlags.pinned) {
bubble.classList.add('is-pinned'); bubble.classList.add('is-pinned');
time = '<i class="tgico tgico-pinnedchat"></i>' + time; time = '<i class="tgico tgico-pinnedchat"></i>' + time;
} }

View File

@ -4,6 +4,7 @@ import mediaSizes from "../../helpers/mediaSizes";
import { cancelEvent } from "../../helpers/dom"; import { cancelEvent } from "../../helpers/dom";
import DivAndCaption from "../divAndCaption"; import DivAndCaption from "../divAndCaption";
import { ripple } from "../ripple"; import { ripple } from "../ripple";
import ListenerSetter from "../../helpers/listenerSetter";
const classNames: string[] = []; const classNames: string[] = [];
const CLASSNAME_BASE = 'pinned-container'; const CLASSNAME_BASE = 'pinned-container';
@ -13,7 +14,7 @@ export default class PinnedContainer {
private close: HTMLElement; private close: HTMLElement;
protected wrapper: HTMLElement; protected wrapper: HTMLElement;
constructor(protected topbar: ChatTopbar, protected chat: Chat, protected className: string, public divAndCaption: DivAndCaption<(title: string, subtitle: string, message?: any) => void>, onClose?: () => void | Promise<boolean>) { constructor(protected topbar: ChatTopbar, protected chat: Chat, public listenerSetter: ListenerSetter, protected className: string, public divAndCaption: DivAndCaption<(title: string, subtitle: string, message?: any) => void>, onClose?: () => void | Promise<boolean>) {
/* const prev = this.divAndCaption.fill; /* const prev = this.divAndCaption.fill;
this.divAndCaption.fill = (mid, title, subtitle) => { this.divAndCaption.fill = (mid, title, subtitle) => {
this.divAndCaption.container.dataset.mid = '' + mid; this.divAndCaption.container.dataset.mid = '' + mid;
@ -39,7 +40,7 @@ export default class PinnedContainer {
divAndCaption.container.append(this.close, this.wrapper); divAndCaption.container.append(this.close, this.wrapper);
this.topbar.listenerSetter.add(this.close, 'click', (e) => { this.listenerSetter.add(this.close, 'click', (e) => {
cancelEvent(e); cancelEvent(e);
((onClose ? onClose() : null) || Promise.resolve(true)).then(needClose => { ((onClose ? onClose() : null) || Promise.resolve(true)).then(needClose => {

View File

@ -1,4 +1,3 @@
import type { AppImManager } from "../../lib/appManagers/appImManager";
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type ChatTopbar from "./topbar"; import type ChatTopbar from "./topbar";
@ -8,8 +7,10 @@ import PinnedContainer from "./pinnedContainer";
import PinnedMessageBorder from "./pinnedMessageBorder"; import PinnedMessageBorder from "./pinnedMessageBorder";
import ReplyContainer, { wrapReplyDivAndCaption } from "./replyContainer"; import ReplyContainer, { wrapReplyDivAndCaption } from "./replyContainer";
import rootScope from "../../lib/rootScope"; import rootScope from "../../lib/rootScope";
import { findUpClassName } from "../../helpers/dom"; import { cancelEvent, findUpClassName, getElementByPoint, handleScrollSideEvent } from "../../helpers/dom";
import Chat from "./chat"; import Chat from "./chat";
import ListenerSetter from "../../helpers/listenerSetter";
import ButtonIcon from "../buttonIcon";
class AnimatedSuper { class AnimatedSuper {
static DURATION = 200; static DURATION = 200;
@ -191,28 +192,54 @@ class AnimatedCounter {
} }
export default class ChatPinnedMessage { export default class ChatPinnedMessage {
public static LOAD_COUNT = 50;
public static LOAD_OFFSET = 5;
public pinnedMessageContainer: PinnedContainer; public pinnedMessageContainer: PinnedContainer;
public pinnedMessageBorder: PinnedMessageBorder; public pinnedMessageBorder: PinnedMessageBorder;
public pinnedIndex = 0;
public pinnedMaxMid = 0;
public pinnedMid = 0;
public pinnedIndex = -1;
public wasPinnedIndex = 0; public wasPinnedIndex = 0;
public locked = false; public locked = false;
public waitForScrollBottom = false; public waitForScrollBottom = false;
public count = 0;
public mids: number[] = [];
public offsetIndex = 0;
public loading = false;
public loadedBottom = false;
public loadedTop = false;
public animatedSubtitle: AnimatedSuper; public animatedSubtitle: AnimatedSuper;
public animatedMedia: AnimatedSuper; public animatedMedia: AnimatedSuper;
public animatedCounter: AnimatedCounter; public animatedCounter: AnimatedCounter;
public listenerSetter: ListenerSetter;
public scrollDownListenerSetter: ListenerSetter = null;
public hidden = false;
public getCurrentIndexPromise: Promise<any> = null;
public btnOpen: HTMLButtonElement;
constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) { constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) {
this.pinnedMessageContainer = new PinnedContainer(topbar, chat, 'message', new ReplyContainer('pinned-message'), () => { this.listenerSetter = new ListenerSetter();
this.pinnedMessageContainer = new PinnedContainer(topbar, chat, this.listenerSetter, 'message', new ReplyContainer('pinned-message'), () => {
if(appPeersManager.canPinMessage(this.topbar.peerID)) { if(appPeersManager.canPinMessage(this.topbar.peerID)) {
new PopupPinMessage(this.topbar.peerID, 0); new PopupPinMessage(this.topbar.peerID, this.pinnedMid, true);
return Promise.resolve(false); return Promise.resolve(false);
} else {
return this.appMessagesManager.hidePinnedMessages(this.topbar.peerID).then(() => true);
} }
}); });
this.pinnedMessageBorder = new PinnedMessageBorder(); this.pinnedMessageBorder = new PinnedMessageBorder();
this.pinnedMessageContainer.divAndCaption.border.replaceWith(this.pinnedMessageBorder.render(1, 0)); this.pinnedMessageContainer.divAndCaption.border.replaceWith(this.pinnedMessageBorder.render(1, 0));
this.topbar.btnJoin.parentElement.insertBefore(this.pinnedMessageContainer.divAndCaption.container, this.topbar.btnJoin);
this.animatedSubtitle = new AnimatedSuper(); this.animatedSubtitle = new AnimatedSuper();
this.pinnedMessageContainer.divAndCaption.subtitle.append(this.animatedSubtitle.container); this.pinnedMessageContainer.divAndCaption.subtitle.append(this.animatedSubtitle.container);
@ -225,90 +252,261 @@ export default class ChatPinnedMessage {
this.pinnedMessageContainer.divAndCaption.title.innerHTML = 'Pinned Message '; this.pinnedMessageContainer.divAndCaption.title.innerHTML = 'Pinned Message ';
this.pinnedMessageContainer.divAndCaption.title.append(this.animatedCounter.container); this.pinnedMessageContainer.divAndCaption.title.append(this.animatedCounter.container);
this.topbar.listenerSetter.add(rootScope, 'peer_pinned_messages', (e) => { this.btnOpen = ButtonIcon('pinlist pinned-container-close pinned-message-pinlist', {noRipple: true});
this.pinnedMessageContainer.divAndCaption.container.prepend(this.btnOpen);
this.listenerSetter.add(this.btnOpen, 'click', (e) => {
cancelEvent(e);
this.topbar.openPinned(true);
});
this.listenerSetter.add(rootScope, 'peer_pinned_messages', (e) => {
const peerID = e.detail; const peerID = e.detail;
if(peerID == this.topbar.peerID) { if(peerID == this.topbar.peerID) {
this.setPinnedMessage(); //this.wasPinnedIndex = 0;
//setTimeout(() => {
if(this.hidden) {
this.pinnedMessageContainer.toggle(this.hidden = false);
}
this.loadedTop = this.loadedBottom = false;
this.pinnedIndex = -1;
this.pinnedMid = 0;
this.count = 0;
this.mids = [];
this.offsetIndex = 0;
this.pinnedMaxMid = 0;
this.setCorrectIndex(0);
//}, 300);
}
});
this.listenerSetter.add(rootScope, 'peer_pinned_hidden', (e) => {
const {peerID, maxID} = e.detail;
if(peerID == this.topbar.peerID) {
this.pinnedMessageContainer.toggle(this.hidden = true);
} }
}); });
} }
public destroy() {
this.pinnedMessageContainer.divAndCaption.container.remove();
this.listenerSetter.removeAll();
this.unsetScrollDownListener(false);
}
public setCorrectIndex(lastScrollDirection?: number) { public setCorrectIndex(lastScrollDirection?: number) {
if(this.locked || this.chat.setPeerPromise) { if(this.locked || this.hidden/* || this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise */) {
return;
}/* else if(this.waitForScrollBottom) {
if(lastScrollDirection === 1) {
this.waitForScrollBottom = false;
} else {
return; return;
} }
} */
///const perf = performance.now(); if((this.loadedBottom || this.loadedTop) && !this.count) {
const rect = this.chat.bubbles.scrollable.container.getBoundingClientRect(); return;
const x = Math.ceil(rect.left + ((rect.right - rect.left) / 2) + 1); }
const y = Math.floor(rect.top + rect.height - 1);
let el: HTMLElement = document.elementFromPoint(x, y) as any; //const perf = performance.now();
//this.appImManager.log('[PM]: setCorrectIndex: get last element perf:', performance.now() - perf, el, x, y); let el = getElementByPoint(this.chat.bubbles.scrollable.container, 'bottom');
//this.chat.log('[PM]: setCorrectIndex: get last element perf:', performance.now() - perf, el);
if(!el) return; if(!el) return;
el = findUpClassName(el, 'bubble'); el = findUpClassName(el, 'bubble');
if(!el) return; if(!el) return;
if(el && el.dataset.mid !== undefined) { const mid = el.dataset.mid;
const mid = +el.dataset.mid; if(el && mid !== undefined) {
this.appMessagesManager.getPinnedMessages(this.topbar.peerID).then(mids => { this.chat.log('[PM]: setCorrectIndex will test mid:', mid);
let currentIndex = mids.findIndex(_mid => _mid <= mid); this.testMid(+mid, lastScrollDirection);
if(currentIndex === -1) { }
currentIndex = mids.length ? mids.length - 1 : 0;
} }
//this.appImManager.log('pinned currentIndex', currentIndex); public testMid(mid: number, lastScrollDirection?: number) {
//if(lastScrollDirection !== undefined) return;
if(this.hidden) return;
this.chat.log('[PM]: testMid', mid);
let currentIndex: number = this.mids.findIndex(_mid => _mid <= mid);
if(currentIndex !== -1 && !this.isNeededMore(currentIndex)) {
currentIndex += this.offsetIndex;
} else if(this.loadedTop && mid < this.mids[this.mids.length - 1]) {
//currentIndex = 0;
currentIndex = this.mids.length - 1 + this.offsetIndex;
} else {
if(!this.getCurrentIndexPromise) {
this.getCurrentIndexPromise = this.getCurrentIndex(mid, lastScrollDirection !== undefined);
}
return;
}
//const idx = Math.max(0, this.mids.indexOf(mid));
/* if(currentIndex == this.count) {
currentIndex = 0;
} */
this.chat.log('[PM]: testMid: pinned currentIndex', currentIndex, mid);
const changed = this.pinnedIndex != currentIndex; const changed = this.pinnedIndex != currentIndex;
if(changed) { if(changed) {
if(this.waitForScrollBottom) { if(this.waitForScrollBottom && lastScrollDirection !== undefined) {
if(lastScrollDirection === 1) { // если проскроллил вниз - разблокировать if(this.pinnedIndex === 0 || this.pinnedIndex > currentIndex) { // если не скроллил вниз и пытается поставить нижний пиннед - выйти
this.waitForScrollBottom = false;
} else if(this.pinnedIndex > currentIndex) { // если не скроллил вниз и пытается поставить нижний пиннед - выйти
return; return;
} }
} }
this.pinnedIndex = currentIndex; this.pinnedIndex = currentIndex;
this.pinnedMid = this.mids.find(_mid => _mid <= mid) || this.mids[this.mids.length - 1];
this.setPinnedMessage(); this.setPinnedMessage();
} }
}
private isNeededMore(currentIndex: number) {
return (this.count > ChatPinnedMessage.LOAD_COUNT &&
(
(!this.loadedBottom && currentIndex <= ChatPinnedMessage.LOAD_OFFSET) ||
(!this.loadedTop && (this.count - 1 - currentIndex) <= ChatPinnedMessage.LOAD_OFFSET)
)
);
}
private async getCurrentIndex(mid: number, correctAfter = true) {
if(this.loading) return;
this.loading = true;
try {
let gotRest = false;
const promises = [
this.appMessagesManager.getSearch(this.topbar.peerID, '', {_: 'inputMessagesFilterPinned'}, mid, ChatPinnedMessage.LOAD_COUNT, 0, ChatPinnedMessage.LOAD_COUNT)
.then(r => {
gotRest = true;
return r;
})
];
if(!this.pinnedMaxMid) {
const promise = this.appMessagesManager.getPinnedMessage(this.topbar.peerID).then(p => {
if(!p.maxID) return;
this.pinnedMaxMid = p.maxID;
if(!gotRest && correctAfter) {
this.mids = [this.pinnedMaxMid];
this.count = p.count;
this.pinnedIndex = 0;
this.pinnedMid = this.mids[0];
this.setPinnedMessage();
//this.pinnedMessageContainer.toggle(false);
}
}); });
promises.push(promise as any);
}
const result = (await Promise.all(promises))[0];
let backLimited = result.history.findIndex(_mid => _mid <= mid);
if(backLimited === -1) {
backLimited = result.history.length;
}/* else {
backLimited -= 1;
} */
this.offsetIndex = result.offset_id_offset ? result.offset_id_offset - backLimited : 0;
this.mids = result.history.slice();
this.count = result.count;
if(!this.count) {
this.pinnedMessageContainer.toggle(true);
}
this.loadedTop = (this.offsetIndex + this.mids.length) == this.count;
this.loadedBottom = !this.offsetIndex;
this.chat.log('[PM]: getCurrentIndex result:', mid, result, backLimited, this.offsetIndex, this.loadedTop, this.loadedBottom);
} catch(err) {
this.chat.log.error('[PM]: getCurrentIndex error', err);
}
this.loading = false;
if(this.locked) {
this.testMid(mid);
} else if(correctAfter) {
this.setCorrectIndex(0);
}
this.getCurrentIndexPromise = null;
//return result.offset_id_offset || 0;
}
public setScrollDownListener() {
this.waitForScrollBottom = true;
if(!this.scrollDownListenerSetter) {
this.scrollDownListenerSetter = new ListenerSetter();
handleScrollSideEvent(this.chat.bubbles.scrollable.container, 'bottom', () => {
this.unsetScrollDownListener();
}, this.scrollDownListenerSetter);
}
}
public unsetScrollDownListener(refreshPosition = true) {
this.waitForScrollBottom = false;
if(this.scrollDownListenerSetter) {
this.scrollDownListenerSetter.removeAll();
this.scrollDownListenerSetter = null;
}
if(refreshPosition) {
this.setCorrectIndex(0);
}
}
public async handleFollowingPinnedMessage() {
this.locked = true;
this.chat.log('[PM]: handleFollowingPinnedMessage');
try {
this.setScrollDownListener();
const setPeerPromise = this.chat.setPeerPromise;
if(setPeerPromise instanceof Promise) {
await setPeerPromise;
}
await this.chat.bubbles.scrollable.scrollLockedPromise;
if(this.getCurrentIndexPromise) {
await this.getCurrentIndexPromise;
}
this.chat.log('[PM]: handleFollowingPinnedMessage: unlock');
this.locked = false;
/* // подождём, пока скролл остановится
setTimeout(() => {
this.chat.log('[PM]: handleFollowingPinnedMessage: unlock');
this.locked = false;
}, 50); */
} catch(err) {
this.chat.log.error('[PM]: handleFollowingPinnedMessage error:', err);
this.locked = false;
this.waitForScrollBottom = false;
this.setCorrectIndex(0);
} }
} }
public async followPinnedMessage(mid: number) { public async followPinnedMessage(mid: number) {
const message = this.appMessagesManager.getMessage(mid); const message = this.appMessagesManager.getMessage(mid);
if(message && !message.deleted) { if(message && !message.deleted) {
this.locked = true; this.chat.setPeer(this.topbar.peerID, mid);
(this.chat.setPeerPromise || Promise.resolve()).then(() => { // * debounce fast clicker
try { this.handleFollowingPinnedMessage();
const mids = await this.appMessagesManager.getPinnedMessages(message.peerID); this.testMid(this.pinnedIndex >= (this.count - 1) ? this.pinnedMaxMid : mid - 1);
const index = mids.indexOf(mid); });
this.pinnedIndex = index >= (mids.length - 1) ? 0 : index + 1;
this.setPinnedMessage();
const setPeerPromise = this.chat.setPeer(message.peerID, mid);
if(setPeerPromise instanceof Promise) {
await setPeerPromise;
}
await this.chat.bubbles.scrollable.scrollLockedPromise;
} catch(err) {
this.chat.log.error('[PM]: followPinnedMessage error:', err);
}
// подождём, пока скролл остановится
setTimeout(() => {
this.locked = false;
this.waitForScrollBottom = true;
}, 50);
} }
} }
@ -320,24 +518,24 @@ export default class ChatPinnedMessage {
public setPinnedMessage() { public setPinnedMessage() {
/////this.log('setting pinned message', message); /////this.log('setting pinned message', message);
//return; //return;
const promise: Promise<any> = this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise || Promise.resolve(); /* const promise: Promise<any> = this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise || Promise.resolve();
Promise.all([ Promise.all([
this.appMessagesManager.getPinnedMessages(this.topbar.peerID),
promise promise
]).then(([mids]) => { ]).then(() => { */
//const mids = results[0]; //const mids = results[0];
if(mids.length) { const count = this.count;
const pinnedIndex = this.pinnedIndex >= mids.length ? mids.length - 1 : this.pinnedIndex; if(count) {
const message = this.appMessagesManager.getMessage(mids[pinnedIndex]); const pinnedIndex = this.pinnedIndex;
const message = this.appMessagesManager.getMessage(this.pinnedMid);
//this.animatedCounter.prepareNumber(mids.length); //this.animatedCounter.prepareNumber(count);
//setTimeout(() => { //setTimeout(() => {
const isLast = pinnedIndex === 0; const isLast = pinnedIndex === 0;
this.animatedCounter.container.classList.toggle('is-last', isLast); this.animatedCounter.container.classList.toggle('is-last', isLast);
//SetTransition(this.animatedCounter.container, 'is-last', isLast, AnimatedSuper.DURATION); //SetTransition(this.animatedCounter.container, 'is-last', isLast, AnimatedSuper.DURATION);
if(!isLast) { if(!isLast) {
this.animatedCounter.setCount(mids.length - pinnedIndex); this.animatedCounter.setCount(count - pinnedIndex);
} }
//}, 100); //}, 100);
@ -372,13 +570,15 @@ export default class ChatPinnedMessage {
} }
//} //}
this.pinnedMessageBorder.render(mids.length, mids.length - pinnedIndex - 1); this.pinnedMessageBorder.render(count, count - pinnedIndex - 1);
this.wasPinnedIndex = pinnedIndex; this.wasPinnedIndex = pinnedIndex;
this.pinnedMessageContainer.divAndCaption.container.dataset.mid = '' + message.mid; this.pinnedMessageContainer.divAndCaption.container.dataset.mid = '' + message.mid;
} else { } else {
this.pinnedMessageContainer.toggle(true); this.pinnedMessageContainer.toggle(true);
this.wasPinnedIndex = 0; this.wasPinnedIndex = 0;
} }
});
this.pinnedMessageContainer.divAndCaption.container.classList.toggle('is-many', this.count > 1);
//});
} }
} }

View File

@ -28,9 +28,9 @@ export default class ChatSelection {
private listenerSetter: ListenerSetter; private listenerSetter: ListenerSetter;
constructor(private chatBubbles: ChatBubbles, private chatInput: ChatInput, private appMessagesManager: AppMessagesManager) { constructor(private bubbles: ChatBubbles, private input: ChatInput, private appMessagesManager: AppMessagesManager) {
const bubblesContainer = chatBubbles.bubblesContainer; const bubblesContainer = bubbles.bubblesContainer;
this.listenerSetter = chatBubbles.listenerSetter; this.listenerSetter = bubbles.listenerSetter;
if(isTouchSupported) { if(isTouchSupported) {
this.listenerSetter.add(bubblesContainer, 'touchend', (e) => { this.listenerSetter.add(bubblesContainer, 'touchend', (e) => {
@ -49,6 +49,7 @@ export default class ChatSelection {
|| ( || (
!this.selectedMids.size !this.selectedMids.size
&& !(e.target as HTMLElement).classList.contains('bubble') && !(e.target as HTMLElement).classList.contains('bubble')
&& !(e.target as HTMLElement).classList.contains('document-selection')
&& bubble && bubble
) )
) { ) {
@ -86,7 +87,7 @@ export default class ChatSelection {
/* if(foundTargets.has(e.target as HTMLElement)) return; /* if(foundTargets.has(e.target as HTMLElement)) return;
foundTargets.set(e.target as HTMLElement, true); */ foundTargets.set(e.target as HTMLElement, true); */
const bubble = findUpClassName(e.target, 'bubble'); const bubble = findUpClassName(e.target, 'grouped-item') || findUpClassName(e.target, 'bubble');
if(!bubble) { if(!bubble) {
//console.error('found no bubble', e); //console.error('found no bubble', e);
return; return;
@ -96,7 +97,7 @@ export default class ChatSelection {
if(!mid) return; if(!mid) return;
// * cancel selecting if selecting message text // * cancel selecting if selecting message text
if(e.target != bubble && selecting === undefined && !this.selectedMids.size) { if(e.target != bubble && !(e.target as HTMLElement).classList.contains('document-selection') && selecting === undefined && !this.selectedMids.size) {
this.listenerSetter.removeManual(bubblesContainer, 'mousemove', onMouseMove); this.listenerSetter.removeManual(bubblesContainer, 'mousemove', onMouseMove);
this.listenerSetter.removeManual(document, 'mouseup', onMouseUp, documentListenerOptions); this.listenerSetter.removeManual(document, 'mouseup', onMouseUp, documentListenerOptions);
return; return;
@ -115,7 +116,7 @@ export default class ChatSelection {
if(!this.selectedMids.size) { if(!this.selectedMids.size) {
if(seen.size == 2) { if(seen.size == 2) {
[...seen].forEach(mid => { [...seen].forEach(mid => {
const mounted = this.chatBubbles.getMountedBubble(mid); const mounted = this.bubbles.getMountedBubble(mid);
if(mounted) { if(mounted) {
this.toggleByBubble(mounted.bubble); this.toggleByBubble(mounted.bubble);
} }
@ -151,7 +152,7 @@ export default class ChatSelection {
public toggleBubbleCheckbox(bubble: HTMLElement, show: boolean) { public toggleBubbleCheckbox(bubble: HTMLElement, show: boolean) {
const hasCheckbox = !!this.getCheckboxInputFromBubble(bubble); const hasCheckbox = !!this.getCheckboxInputFromBubble(bubble);
const isAlbum = bubble.classList.contains('is-album'); const isGrouped = bubble.classList.contains('is-grouped');
if(show) { if(show) {
if(hasCheckbox) return; if(hasCheckbox) return;
@ -160,23 +161,44 @@ export default class ChatSelection {
// * if it is a render of new message // * if it is a render of new message
const mid = +bubble.dataset.mid; const mid = +bubble.dataset.mid;
if(this.selectedMids.has(mid) && (!isAlbum || this.isAlbumMidsSelected(mid))) { if(this.selectedMids.has(mid) && (!isGrouped || this.isGroupedMidsSelected(mid))) {
checkboxField.input.checked = true; checkboxField.input.checked = true;
bubble.classList.add('is-selected'); bubble.classList.add('is-selected');
} }
if(bubble.classList.contains('document-container')) {
bubble.querySelector('.document, audio-element').append(checkboxField.label);
} else {
bubble.prepend(checkboxField.label); bubble.prepend(checkboxField.label);
}
} else if(hasCheckbox) { } else if(hasCheckbox) {
bubble.firstElementChild.remove(); this.getCheckboxInputFromBubble(bubble).parentElement.remove();
} }
if(isAlbum) { if(isGrouped) {
this.chatBubbles.getBubbleAlbumItems(bubble).forEach(item => this.toggleBubbleCheckbox(item, show)); this.bubbles.getBubbleGroupedItems(bubble).forEach(item => this.toggleBubbleCheckbox(item, show));
} }
} }
public getCheckboxInputFromBubble(bubble: HTMLElement) { public getCheckboxInputFromBubble(bubble: HTMLElement): HTMLInputElement {
return bubble.firstElementChild.tagName == 'LABEL' && bubble.firstElementChild.firstElementChild as HTMLInputElement; /* let perf = performance.now();
let checkbox = bubble.firstElementChild.tagName == 'LABEL' && bubble.firstElementChild.firstElementChild as HTMLInputElement;
console.log('getCheckboxInputFromBubble firstElementChild time:', performance.now() - perf);
perf = performance.now();
checkbox = bubble.querySelector('label input');
console.log('getCheckboxInputFromBubble querySelector time:', performance.now() - perf); */
/* let perf = performance.now();
let contains = bubble.classList.contains('document-container');
console.log('getCheckboxInputFromBubble classList time:', performance.now() - perf);
perf = performance.now();
contains = bubble.className.includes('document-container');
console.log('getCheckboxInputFromBubble className time:', performance.now() - perf); */
return bubble.classList.contains('document-container') ?
bubble.querySelector('label input') :
bubble.firstElementChild.tagName == 'LABEL' && bubble.firstElementChild.firstElementChild as HTMLInputElement;
} }
public updateForwardContainer(forceSelection = false) { public updateForwardContainer(forceSelection = false) {
@ -213,7 +235,7 @@ export default class ChatSelection {
if(wasSelecting == this.isSelecting) return; if(wasSelecting == this.isSelecting) return;
const bubblesContainer = this.chatBubbles.bubblesContainer; const bubblesContainer = this.bubbles.bubblesContainer;
//bubblesContainer.classList.toggle('is-selecting', !!this.selectedMids.size); //bubblesContainer.classList.toggle('is-selecting', !!this.selectedMids.size);
/* if(bubblesContainer.classList.contains('is-chat-input-hidden')) { /* if(bubblesContainer.classList.contains('is-chat-input-hidden')) {
@ -234,7 +256,9 @@ export default class ChatSelection {
blurActiveElement(); // * for mobile keyboards blurActiveElement(); // * for mobile keyboards
SetTransition(bubblesContainer, 'is-selecting', !!this.selectedMids.size || forceSelection, 200, () => { const forwards = !!this.selectedMids.size || forceSelection;
SetTransition(this.input.rowsWrapper, 'is-centering', forwards, 200);
SetTransition(bubblesContainer, 'is-selecting', forwards, 200, () => {
if(!this.isSelecting) { if(!this.isSelecting) {
this.selectionContainer.remove(); this.selectionContainer.remove();
this.selectionContainer = this.selectionForwardBtn = this.selectionDeleteBtn = null; this.selectionContainer = this.selectionForwardBtn = this.selectionDeleteBtn = null;
@ -242,7 +266,7 @@ export default class ChatSelection {
} }
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
this.chatBubbles.onScroll(); this.bubbles.onScroll();
}); });
}); });
@ -277,13 +301,13 @@ export default class ChatSelection {
this.selectionContainer.append(btnCancel, this.selectionCountEl, this.selectionForwardBtn, this.selectionDeleteBtn); this.selectionContainer.append(btnCancel, this.selectionCountEl, this.selectionForwardBtn, this.selectionDeleteBtn);
this.chatInput.rowsWrapper.append(this.selectionContainer); this.input.rowsWrapper.append(this.selectionContainer);
} }
} }
if(toggleCheckboxes) { if(toggleCheckboxes) {
for(const mid in this.chatBubbles.bubbles) { for(const mid in this.bubbles.bubbles) {
const bubble = this.chatBubbles.bubbles[mid]; const bubble = this.bubbles.bubbles[mid];
this.toggleBubbleCheckbox(bubble, this.isSelecting); this.toggleBubbleCheckbox(bubble, this.isSelecting);
} }
} }
@ -295,9 +319,10 @@ export default class ChatSelection {
public cancelSelection = () => { public cancelSelection = () => {
for(const mid of this.selectedMids) { for(const mid of this.selectedMids) {
const mounted = this.chatBubbles.getMountedBubble(mid); const mounted = this.bubbles.getMountedBubble(mid);
if(mounted) { if(mounted) {
this.toggleByBubble(mounted.message.grouped_id ? mounted.bubble.querySelector(`.album-item[data-mid="${mid}"]`) : mounted.bubble); //this.toggleByBubble(mounted.message.grouped_id ? mounted.bubble.querySelector(`.grouped-item[data-mid="${mid}"]`) : mounted.bubble);
this.toggleByBubble(mounted.bubble);
} }
/* const bubble = this.appImManager.bubbles[mid]; /* const bubble = this.appImManager.bubbles[mid];
if(bubble) { if(bubble) {
@ -325,12 +350,12 @@ export default class ChatSelection {
SetTransition(bubble, 'is-selected', isSelected, 200); SetTransition(bubble, 'is-selected', isSelected, 200);
} }
public isAlbumBubbleSelected(bubble: HTMLElement) { public isGroupedBubbleSelected(bubble: HTMLElement) {
const albumCheckboxInput = this.getCheckboxInputFromBubble(bubble); const groupedCheckboxInput = this.getCheckboxInputFromBubble(bubble);
return albumCheckboxInput?.checked; return groupedCheckboxInput?.checked;
} }
public isAlbumMidsSelected(mid: number) { public isGroupedMidsSelected(mid: number) {
const mids = this.appMessagesManager.getMidsByMid(mid); const mids = this.appMessagesManager.getMidsByMid(mid);
const selectedMids = mids.filter(mid => this.selectedMids.has(mid)); const selectedMids = mids.filter(mid => this.selectedMids.has(mid));
return mids.length == selectedMids.length; return mids.length == selectedMids.length;
@ -339,14 +364,14 @@ export default class ChatSelection {
public toggleByBubble = (bubble: HTMLElement) => { public toggleByBubble = (bubble: HTMLElement) => {
const mid = +bubble.dataset.mid; const mid = +bubble.dataset.mid;
const isAlbum = bubble.classList.contains('is-album'); const isGrouped = bubble.classList.contains('is-grouped');
if(isAlbum) { if(isGrouped) {
if(!this.isAlbumBubbleSelected(bubble)) { if(!this.isGroupedBubbleSelected(bubble)) {
const mids = this.appMessagesManager.getMidsByMid(mid); const mids = this.appMessagesManager.getMidsByMid(mid);
mids.forEach(mid => this.selectedMids.delete(mid)); mids.forEach(mid => this.selectedMids.delete(mid));
} }
this.chatBubbles.getBubbleAlbumItems(bubble).forEach(this.toggleByBubble); this.bubbles.getBubbleGroupedItems(bubble).forEach(this.toggleByBubble);
return; return;
} }
@ -376,15 +401,15 @@ export default class ChatSelection {
this.selectedMids.add(mid); this.selectedMids.add(mid);
} }
const isAlbumItem = bubble.classList.contains('album-item'); const isGroupedItem = bubble.classList.contains('grouped-item');
if(isAlbumItem) { if(isGroupedItem) {
const albumContainer = findUpClassName(bubble, 'bubble'); const groupContainer = findUpClassName(bubble, 'bubble');
const isAlbumSelected = this.isAlbumBubbleSelected(albumContainer); const isGroupedSelected = this.isGroupedBubbleSelected(groupContainer);
const isAlbumMidsSelected = this.isAlbumMidsSelected(mid); const isGroupedMidsSelected = this.isGroupedMidsSelected(mid);
const willChange = isAlbumMidsSelected || isAlbumSelected; const willChange = isGroupedMidsSelected || isGroupedSelected;
if(willChange) { if(willChange) {
this.updateBubbleSelection(albumContainer, isAlbumMidsSelected); this.updateBubbleSelection(groupContainer, isGroupedMidsSelected);
} }
} }

View File

@ -1,8 +1,6 @@
import type { AppChatsManager, Channel } from "../../lib/appManagers/appChatsManager"; import type { AppChatsManager, Channel } from "../../lib/appManagers/appChatsManager";
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type { AppProfileManager } from "../../lib/appManagers/appProfileManager";
import type { AppUsersManager } from "../../lib/appManagers/appUsersManager";
import type { AppSidebarRight } from "../sidebarRight"; import type { AppSidebarRight } from "../sidebarRight";
import type Chat from "./chat"; import type Chat from "./chat";
import { findUpClassName, cancelEvent, attachClickEvent } from "../../helpers/dom"; import { findUpClassName, cancelEvent, attachClickEvent } from "../../helpers/dom";
@ -18,6 +16,7 @@ import ChatPinnedMessage from "./pinnedMessage";
import ChatSearch from "./search"; import ChatSearch from "./search";
import { ButtonMenuItemOptions } from "../buttonMenu"; import { ButtonMenuItemOptions } from "../buttonMenu";
import ListenerSetter from "../../helpers/listenerSetter"; import ListenerSetter from "../../helpers/listenerSetter";
import appStateManager from "../../lib/appManagers/appStateManager";
export default class ChatTopbar { export default class ChatTopbar {
container: HTMLDivElement; container: HTMLDivElement;
@ -38,14 +37,19 @@ export default class ChatTopbar {
private setUtilsRAF: number; private setUtilsRAF: number;
public peerID: number; public peerID: number;
public wasPeerID: number;
private setPeerStatusInterval: number; private setPeerStatusInterval: number;
public listenerSetter: ListenerSetter; public listenerSetter: ListenerSetter;
constructor(private chat: Chat, private appSidebarRight: AppSidebarRight, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager, private appUsersManager: AppUsersManager, private appProfileManager: AppProfileManager) { public menuButtons: (ButtonMenuItemOptions & {verify: () => boolean})[] = [];
this.chat.log.error('Topbar construction');
constructor(private chat: Chat, private appSidebarRight: AppSidebarRight, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager) {
this.listenerSetter = new ListenerSetter(); this.listenerSetter = new ListenerSetter();
}
public construct() {
this.chat.log.error('Topbar construction');
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.classList.add('sidebar-header', 'topbar'); this.container.classList.add('sidebar-header', 'topbar');
@ -59,10 +63,6 @@ export default class ChatTopbar {
const person = document.createElement('div'); const person = document.createElement('div');
person.classList.add('person'); person.classList.add('person');
this.avatarElement = new AvatarElement();
this.avatarElement.setAttribute('dialog', '1');
this.avatarElement.setAttribute('clickable', '');
const content = document.createElement('div'); const content = document.createElement('div');
content.classList.add('content'); content.classList.add('content');
@ -77,13 +77,16 @@ export default class ChatTopbar {
const bottom = document.createElement('div'); const bottom = document.createElement('div');
bottom.classList.add('bottom'); bottom.classList.add('bottom');
this.subtitle = document.createElement('div'); if(this.subtitle) {
this.subtitle.classList.add('info');
bottom.append(this.subtitle); bottom.append(this.subtitle);
}
content.append(top, bottom); content.append(top, bottom);
person.append(this.avatarElement, content); if(this.avatarElement) {
person.append(this.avatarElement);
}
person.append(content);
this.chatInfo.append(person); this.chatInfo.append(person);
// * chat utils section // * chat utils section
@ -92,25 +95,77 @@ export default class ChatTopbar {
this.chatAudio = new ChatAudio(this, this.chat, this.appMessagesManager, this.appPeersManager); this.chatAudio = new ChatAudio(this, this.chat, this.appMessagesManager, this.appPeersManager);
if(this.menuButtons.length) {
this.btnMore = ButtonMenuToggle({listenerSetter: this.listenerSetter}, 'bottom-left', this.menuButtons, () => {
this.menuButtons.forEach(button => {
button.element.classList.toggle('hide', !button.verify());
});
});
}
this.chatUtils.append(...[this.chatAudio ? this.chatAudio.divAndCaption.container : null, this.pinnedMessage ? this.pinnedMessage.pinnedMessageContainer.divAndCaption.container : null, this.btnJoin, this.btnPinned, this.btnMute, this.btnSearch, this.btnMore].filter(Boolean));
this.container.append(this.btnBack, this.chatInfo, this.chatUtils);
// * construction end
// * fix topbar overflow section
this.listenerSetter.add(window, 'resize', this.onResize);
mediaSizes.addListener('changeScreen', this.onChangeScreen);
this.listenerSetter.add(this.container, 'click', (e) => {
const pinned: HTMLElement = findUpClassName(e.target, 'pinned-container');
if(pinned) {
cancelEvent(e);
const mid = +pinned.dataset.mid;
if(pinned.classList.contains('pinned-message')) {
//if(!this.pinnedMessage.locked) {
this.pinnedMessage.followPinnedMessage(mid);
//}
} else {
const message = this.appMessagesManager.getMessage(mid);
this.chat.setPeer(message.peerID, mid);
}
} else {
this.appSidebarRight.toggleSidebar(true);
}
});
this.listenerSetter.add(this.btnBack, 'click', (e) => {
cancelEvent(e);
this.chat.appImManager.setPeer(0);
});
}
public constructPeerHelpers() {
this.avatarElement = new AvatarElement();
this.avatarElement.setAttribute('dialog', '1');
this.avatarElement.setAttribute('clickable', '');
this.subtitle = document.createElement('div');
this.subtitle.classList.add('info');
this.pinnedMessage = new ChatPinnedMessage(this, this.chat, this.appMessagesManager, this.appPeersManager);
this.btnJoin = Button('btn-primary chat-join hide'); this.btnJoin = Button('btn-primary chat-join hide');
this.btnJoin.append('SUBSCRIBE'); this.btnJoin.append('SUBSCRIBE');
this.btnPinned = ButtonIcon('pinlist'); this.menuButtons = [{
this.btnMute = ButtonIcon('mute');
this.btnSearch = ButtonIcon('search');
const menuButtons: (ButtonMenuItemOptions & {verify: () => boolean})[] = [{
icon: 'search', icon: 'search',
text: 'Search', text: 'Search',
onClick: () => { onClick: () => {
new ChatSearch(this, this.chat); new ChatSearch(this, this.chat);
}, },
verify: () => mediaSizes.isMobile verify: () => mediaSizes.isMobile
}, { }, /* {
icon: 'pinlist', icon: 'pinlist',
text: 'Pinned Messages', text: 'Pinned Messages',
onClick: () => {}, onClick: () => this.openPinned(false),
verify: () => mediaSizes.isMobile verify: () => mediaSizes.isMobile
}, { }, */ {
icon: 'mute', icon: 'mute',
text: 'Mute', text: 'Mute',
onClick: () => { onClick: () => {
@ -144,53 +199,20 @@ export default class ChatTopbar {
onClick: () => {}, onClick: () => {},
verify: () => true verify: () => true
}]; }];
//menuButtons.forEach(b => b.options = {listenerSetter: this.listenerSetter});
this.btnMore = ButtonMenuToggle({listenerSetter: this.listenerSetter}, 'bottom-left', menuButtons, () => {
menuButtons.forEach(button => {
button.element.classList.toggle('hide', !button.verify());
});
});
this.chatUtils.append(this.chatAudio.divAndCaption.container, this.btnJoin, this.btnPinned, this.btnMute, this.btnSearch, this.btnMore); this.btnPinned = ButtonIcon('pinlist');
this.btnMute = ButtonIcon('mute');
this.btnSearch = ButtonIcon('search');
this.container.append(this.btnBack, this.chatInfo, this.chatUtils); this.listenerSetter.add(this.btnPinned, 'click', (e) => {
// * construction end
// * fix topbar overflow section
this.listenerSetter.add(window, 'resize', this.onResize);
mediaSizes.addListener('changeScreen', this.onChangeScreen);
this.pinnedMessage = new ChatPinnedMessage(this, this.chat, this.appMessagesManager, this.appPeersManager);
this.listenerSetter.add(this.container, 'click', (e) => {
const pinned: HTMLElement = findUpClassName(e.target, 'pinned-container');
if(pinned) {
cancelEvent(e); cancelEvent(e);
this.openPinned(true);
const mid = +pinned.dataset.mid;
if(pinned.classList.contains('pinned-message')) {
this.pinnedMessage.followPinnedMessage(mid);
} else {
const message = this.appMessagesManager.getMessage(mid);
this.chat.setPeer(message.peerID, mid);
}
} else {
this.appSidebarRight.toggleSidebar(true);
}
});
this.listenerSetter.add(this.btnBack, 'click', (e) => {
cancelEvent(e);
this.chat.appImManager.setPeer(0);
}); });
this.listenerSetter.add(this.btnSearch, 'click', (e) => { this.listenerSetter.add(this.btnSearch, 'click', (e) => {
cancelEvent(e); cancelEvent(e);
if(this.peerID) { if(this.peerID) {
appSidebarRight.searchTab.open(this.peerID); this.appSidebarRight.searchTab.open(this.peerID);
} }
}); });
@ -199,12 +221,11 @@ export default class ChatTopbar {
this.appMessagesManager.mutePeer(this.peerID); this.appMessagesManager.mutePeer(this.peerID);
}); });
//this.listenerSetter.add(this.btnJoin, 'mousedown', (e) => {
attachClickEvent(this.btnJoin, (e) => { attachClickEvent(this.btnJoin, (e) => {
cancelEvent(e); cancelEvent(e);
this.btnJoin.setAttribute('disabled', 'true'); this.btnJoin.setAttribute('disabled', 'true');
appChatsManager.joinChannel(-this.peerID).finally(() => { this.appChatsManager.joinChannel(-this.peerID).finally(() => {
this.btnJoin.removeAttribute('disabled'); this.btnJoin.removeAttribute('disabled');
}); });
//}); //});
@ -213,7 +234,7 @@ export default class ChatTopbar {
this.listenerSetter.add(rootScope, 'chat_update', (e) => { this.listenerSetter.add(rootScope, 'chat_update', (e) => {
const peerID: number = e.detail; const peerID: number = e.detail;
if(this.peerID == -peerID) { if(this.peerID == -peerID) {
const chat = appChatsManager.getChat(peerID) as Channel/* | Chat */; const chat = this.appChatsManager.getChat(peerID) as Channel/* | Chat */;
this.btnJoin.classList.toggle('hide', !(chat as Channel)?.pFlags?.left); this.btnJoin.classList.toggle('hide', !(chat as Channel)?.pFlags?.left);
this.setUtilsWidth(); this.setUtilsWidth();
@ -244,7 +265,23 @@ export default class ChatTopbar {
} }
}); });
this.chat.addListener('setPeer', (mid, isTopMessage) => {
if(isTopMessage) {
this.pinnedMessage.unsetScrollDownListener();
this.pinnedMessage.testMid(mid, 0); // * because slider will not let get bubble by document.elementFromPoint
} else if(!this.pinnedMessage.locked) {
this.pinnedMessage.handleFollowingPinnedMessage();
this.pinnedMessage.testMid(mid);
}
});
this.setPeerStatusInterval = window.setInterval(this.setPeerStatus, 60e3); this.setPeerStatusInterval = window.setInterval(this.setPeerStatus, 60e3);
return this;
}
public openPinned(byCurrent: boolean) {
this.chat.appImManager.setInnerPeer(this.peerID, byCurrent ? +this.pinnedMessage.pinnedMessageContainer.divAndCaption.container.dataset.mid : 0, 'pinned');
} }
private onResize = () => { private onResize = () => {
@ -252,8 +289,8 @@ export default class ChatTopbar {
}; };
private onChangeScreen = (from: ScreenSize, to: ScreenSize) => { private onChangeScreen = (from: ScreenSize, to: ScreenSize) => {
this.chatAudio.divAndCaption.container.classList.toggle('is-floating', to == ScreenSize.mobile); this.chatAudio && this.chatAudio.divAndCaption.container.classList.toggle('is-floating', to == ScreenSize.mobile);
this.pinnedMessage.onChangeScreen(from, to); this.pinnedMessage && this.pinnedMessage.onChangeScreen(from, to);
this.setUtilsWidth(true); this.setUtilsWidth(true);
}; };
@ -264,47 +301,85 @@ export default class ChatTopbar {
mediaSizes.removeListener('changeScreen', this.onChangeScreen); mediaSizes.removeListener('changeScreen', this.onChangeScreen);
window.clearInterval(this.setPeerStatusInterval); window.clearInterval(this.setPeerStatusInterval);
if(this.pinnedMessage) {
this.pinnedMessage.destroy(); // * возможно это можно не делать
}
delete this.chatAudio; delete this.chatAudio;
delete this.pinnedMessage; delete this.pinnedMessage;
} }
public setPeer(peerID: number) { public setPeer(peerID: number) {
this.wasPeerID = this.peerID;
this.peerID = peerID; this.peerID = peerID;
this.container.style.display = peerID ? '' : 'none';
}
public finishPeerChange(isTarget: boolean, isJump: boolean, lastMsgID: number) {
const peerID = this.peerID;
if(this.avatarElement) {
this.avatarElement.setAttribute('peer', '' + peerID); this.avatarElement.setAttribute('peer', '' + peerID);
this.avatarElement.update(); this.avatarElement.update();
}
this.container.classList.remove('is-pinned-shown'); this.container.classList.remove('is-pinned-shown');
this.container.style.display = peerID ? '' : 'none';
const isBroadcast = this.appPeersManager.isBroadcast(peerID); const isBroadcast = this.appPeersManager.isBroadcast(peerID);
this.btnMute.classList.toggle('hide', !isBroadcast); this.btnMute && this.btnMute.classList.toggle('hide', !isBroadcast);
this.btnJoin.classList.toggle('hide', !this.appChatsManager.getChat(-peerID)?.pFlags?.left); this.btnJoin && this.btnJoin.classList.toggle('hide', !this.appChatsManager.getChat(-peerID)?.pFlags?.left);
this.setUtilsWidth(); this.setUtilsWidth();
const middleware = this.chat.bubbles.getMiddleware();
if(this.pinnedMessage) { // * replace with new one
if(this.wasPeerID) { // * change
const newPinnedMessage = new ChatPinnedMessage(this, this.chat, this.appMessagesManager, this.appPeersManager);
this.pinnedMessage.pinnedMessageContainer.divAndCaption.container.replaceWith(newPinnedMessage.pinnedMessageContainer.divAndCaption.container);
this.pinnedMessage.destroy();
this.pinnedMessage = newPinnedMessage;
}
appStateManager.getState().then((state) => {
if(!middleware()) return;
this.pinnedMessage.hidden = !!state.hiddenPinnedMessages[peerID];
if(!isTarget) {
this.pinnedMessage.setCorrectIndex(0);
}
});
}
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
this.pinnedMessage.pinnedIndex/* = this.pinnedMessage.wasPinnedIndex */ = 0; this.setTitle();
//this.pinnedMessage.setCorrectIndex();
this.pinnedMessage.setPinnedMessage();
/* noTransition.forEach(el => {
el.classList.remove('no-transition-all');
}); */
/* if(needToChangeInputDisplay) {
this.chatInput.style.display = '';
} */
let title = '';
if(peerID == rootScope.myID) title = 'Saved Messages';
else title = this.appPeersManager.getPeerTitle(peerID);
this.title.innerHTML = title;
this.setPeerStatus(true); this.setPeerStatus(true);
this.setMutedState(); this.setMutedState();
}); });
} }
public setTitle(count?: number) {
let title = '';
if(this.chat.type == 'pinned') {
title = count === -1 ? 'Pinned Messages' : (count === 1 ? 'Pinned Message' : (count + ' Pinned Messages'));
if(count === undefined) {
this.appMessagesManager.getSearchCounters(this.peerID, [{_: 'inputMessagesFilterPinned'}]).then(result => {
this.setTitle(result[0].count);
});
}
} else {
if(this.peerID == rootScope.myID) title = 'Saved Messages';
else title = this.appPeersManager.getPeerTitle(this.peerID);
}
this.title.innerHTML = title;
}
public setMutedState() { public setMutedState() {
if(!this.btnMute) return;
const peerID = this.peerID; const peerID = this.peerID;
let muted = this.appMessagesManager.isPeerMuted(peerID); let muted = this.appMessagesManager.isPeerMuted(peerID);
if(this.appPeersManager.isBroadcast(peerID)) { // not human if(this.appPeersManager.isBroadcast(peerID)) { // not human
@ -350,6 +425,8 @@ export default class ChatTopbar {
}; };
public setPeerStatus = (needClear = false) => { public setPeerStatus = (needClear = false) => {
if(!this.subtitle) return;
const peerID = this.peerID; const peerID = this.peerID;
if(needClear) { if(needClear) {
this.subtitle.innerHTML = ''; this.subtitle.innerHTML = '';

View File

@ -8,6 +8,7 @@ const CheckboxField = (text: string, name: string, round = false) => {
const span = document.createElement('span'); const span = document.createElement('span');
span.classList.add('checkbox-caption'); span.classList.add('checkbox-caption');
if(round) span.classList.add('tgico-check');
if(text) { if(text) {
span.innerText = text; span.innerText = text;
} }

View File

@ -6,7 +6,11 @@ export default class PopupPinMessage {
constructor(peerID: number, mid: number, unpin?: true) { constructor(peerID: number, mid: number, unpin?: true) {
let title: string, description: string, buttons: PopupButton[] = []; let title: string, description: string, buttons: PopupButton[] = [];
const callback = () => appMessagesManager.updatePinnedMessage(peerID, mid, unpin); const callback = () => {
setTimeout(() => { // * костыль, потому что document.elementFromPoint вернёт popup-peer пока он будет закрываться
appMessagesManager.updatePinnedMessage(peerID, mid, unpin);
}, 300);
};
if(unpin) { if(unpin) {
title = `Unpin Message?`; title = `Unpin Message?`;
description = 'Would you like to unpin this message?'; description = 'Would you like to unpin this message?';

View File

@ -70,7 +70,7 @@ export class ScrollableBase {
} }
protected setListeners() { protected setListeners() {
window.addEventListener('resize', this.onScroll); window.addEventListener('resize', this.onScroll, {passive: true});
this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true}); this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true});
} }
@ -160,7 +160,7 @@ export default class Scrollable extends ScrollableBase {
this.lastScrollDirection = this.lastScrollTop == scrollTop ? 0 : (this.lastScrollTop < scrollTop ? 1 : -1); // * 1 - bottom, -1 - top this.lastScrollDirection = this.lastScrollTop == scrollTop ? 0 : (this.lastScrollTop < scrollTop ? 1 : -1); // * 1 - bottom, -1 - top
this.lastScrollTop = scrollTop; this.lastScrollTop = scrollTop;
if(this.onAdditionalScroll) { if(this.onAdditionalScroll && this.lastScrollDirection !== 0) {
this.onAdditionalScroll(); this.onAdditionalScroll();
} }

View File

@ -91,7 +91,7 @@ export default class AppIncludedChatsTab implements SliderTab {
} }
checkbox(selected?: boolean) { checkbox(selected?: boolean) {
return `<div class="checkbox"><label><input type="checkbox" ${selected ? 'checked' : ''}><span></span></label></div>`; return `<div class="checkbox"><label class="checkbox-field"><input type="checkbox" ${selected ? 'checked' : ''}><span></span></label></div>`;
} }
renderResults = async(peerIDs: number[]) => { renderResults = async(peerIDs: number[]) => {

View File

@ -15,7 +15,7 @@ import LazyLoadQueue from "../../lazyLoadQueue";
import { putPreloader, renderImageFromUrl } from "../../misc"; import { putPreloader, renderImageFromUrl } from "../../misc";
import Scrollable from "../../scrollable"; import Scrollable from "../../scrollable";
import { SliderTab } from "../../slider"; import { SliderTab } from "../../slider";
import { wrapAudio, wrapDocument } from "../../wrappers"; import { wrapDocument } from "../../wrappers";
const testScroll = false; const testScroll = false;
@ -101,6 +101,7 @@ export default class AppSharedMediaTab implements SliderTab {
private log = logger('SM'/* , LogLevels.error */); private log = logger('SM'/* , LogLevels.error */);
setPeerStatusInterval: number; setPeerStatusInterval: number;
cleaned: boolean;
public init() { public init() {
this.container = document.getElementById('shared-media-container'); this.container = document.getElementById('shared-media-container');
@ -804,6 +805,7 @@ export default class AppSharedMediaTab implements SliderTab {
}); });
this.sharedMediaType = 'inputMessagesFilterPhotoVideo'; this.sharedMediaType = 'inputMessagesFilterPhotoVideo';
this.cleaned = true;
} }
public cleanupHTML() { public cleanupHTML() {
@ -861,6 +863,8 @@ export default class AppSharedMediaTab implements SliderTab {
} }
public setPeer(peerID: number) { public setPeer(peerID: number) {
if(this.peerID == peerID) return;
if(this.init) { if(this.init) {
this.init(); this.init();
this.init = null; this.init = null;
@ -871,7 +875,10 @@ export default class AppSharedMediaTab implements SliderTab {
} }
public fillProfileElements() { public fillProfileElements() {
let peerID = this.peerID = appImManager.chat.peerID; if(!this.cleaned) return;
this.cleaned = false;
const peerID = this.peerID;
this.cleanupHTML(); this.cleanupHTML();
@ -906,7 +913,7 @@ export default class AppSharedMediaTab implements SliderTab {
setText(user.rPhone, this.profileElements.phone); setText(user.rPhone, this.profileElements.phone);
} }
appProfileManager.getProfile(peerID, true).then(userFull => { appProfileManager.getProfile(peerID).then(userFull => {
if(this.peerID != peerID) { if(this.peerID != peerID) {
this.log.warn('peer changed'); this.log.warn('peer changed');
return; return;

View File

@ -24,6 +24,7 @@ import { renderImageFromUrl } from './misc';
import PollElement from './poll'; import PollElement from './poll';
import ProgressivePreloader from './preloader'; import ProgressivePreloader from './preloader';
import './middleEllipsis'; import './middleEllipsis';
import { nextRandomInt } from '../helpers/random';
const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB
@ -432,7 +433,7 @@ function wrapMediaWithTail(photo: MyPhoto | MyDocument, message: {mid: number, m
svg.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height); svg.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height);
svg.setAttributeNS(null, 'preserveAspectRatio', 'none'); svg.setAttributeNS(null, 'preserveAspectRatio', 'none');
const clipID = 'clip' + message.mid; const clipID = 'clip' + message.mid + '_' + nextRandomInt(9999);
svg.dataset.clipID = clipID; svg.dataset.clipID = clipID;
const defs = document.createElementNS("http://www.w3.org/2000/svg", 'defs'); const defs = document.createElementNS("http://www.w3.org/2000/svg", 'defs');
@ -803,7 +804,7 @@ export function prepareAlbum(options: {
container.append(div); container.append(div);
} }
div.classList.add('album-item'); div.classList.add('album-item', 'grouped-item');
div.style.width = (geometry.width / width * 100) + '%'; div.style.width = (geometry.width / width * 100) + '%';
div.style.height = (geometry.height / height * 100) + '%'; div.style.height = (geometry.height / height * 100) + '%';

View File

@ -543,3 +543,50 @@ export const isSelectionSingle = (input: Element = document.activeElement) => {
return single; return single;
}; };
export const handleScrollSideEvent = (elem: HTMLElement, side: 'top' | 'bottom', callback: () => void, listenerSetter: ListenerSetter) => {
if(isTouchSupported) {
let lastY: number;
const options = {passive: true};
listenerSetter.add(elem, 'touchstart', (e) => {
if(e.touches.length > 1) {
onTouchEnd();
return;
}
lastY = e.touches[0].clientY;
listenerSetter.add(elem, 'touchmove', onTouchMove, options);
listenerSetter.add(elem, 'touchend', onTouchEnd, options);
}, options);
const onTouchMove = (e: TouchEvent) => {
const clientY = e.touches[0].clientY;
const isDown = clientY < lastY;
if(side == 'bottom' && isDown) callback();
else if(side == 'top' && !isDown) callback();
lastY = clientY;
//alert('isDown: ' + !!isDown);
};
const onTouchEnd = () => {
listenerSetter.removeManual(elem, 'touchmove', onTouchMove, options);
listenerSetter.removeManual(elem, 'touchend', onTouchEnd, options);
};
} else {
listenerSetter.add(elem, 'wheel', (e) => {
const isDown = e.deltaY > 0;
//this.log('wheel', e, isDown);
if(side == 'bottom' && isDown) callback();
else if(side == 'top' && !isDown) callback();
}, {passive: true});
}
};
export const getElementByPoint = (container: HTMLElement, verticalSide: 'top' | 'bottom'): HTMLElement => {
const rect = container.getBoundingClientRect();
const x = Math.ceil(rect.left + ((rect.right - rect.left) / 2) + 1);
const y = verticalSide == 'bottom' ? Math.floor(rect.top + rect.height - 1) : Math.ceil(rect.top + 1);
return document.elementFromPoint(x, y) as any;
};

View File

@ -30,7 +30,8 @@ export default class EventListenerBase<Listeners extends {[name: string]: Functi
} }
} }
protected setListenerResult(name: keyof Listeners, ...args: ArgumentTypes<Listeners[typeof name]>) { // * must be protected, but who cares
public setListenerResult(name: keyof Listeners, ...args: ArgumentTypes<Listeners[typeof name]>) {
if(this.reuseResults) { if(this.reuseResults) {
this.listenerResults[name] = args; this.listenerResults[name] = args;
} }

View File

@ -157,7 +157,7 @@
<div class="folders-tabs-scrollable hide"> <div class="folders-tabs-scrollable hide">
<nav class="menu-horizontal" id="folders-tabs"> <nav class="menu-horizontal" id="folders-tabs">
<ul> <ul>
<li class="rp"><span>All<span class="unread-count"></span><i></i></span></li> <li class="rp"><span>All<span class="badge badge-20 badge-blue"></span><i></i></span></li>
</ul> </ul>
</nav> </nav>
</div> </div>
@ -402,7 +402,7 @@
<p class="profile-row-label">Phone</p> <p class="profile-row-label">Phone</p>
</div> </div>
<div class="profile-row profile-row-notifications"> <div class="profile-row profile-row-notifications">
<label> <label class="checkbox-field">
<input type="checkbox" id="profile-notifications" checked="checked"> <input type="checkbox" id="profile-notifications" checked="checked">
<span>Notifications</span> <span>Notifications</span>
</label> </label>

View File

@ -203,7 +203,7 @@ export class AppDialogsManager {
constructor() { constructor() {
this.chatsPreloader = putPreloader(null, true); this.chatsPreloader = putPreloader(null, true);
this.allUnreadCount = this.folders.menu.querySelector('.unread-count'); this.allUnreadCount = this.folders.menu.querySelector('.badge');
this.folders.menuScrollContainer = this.folders.menu.parentElement; this.folders.menuScrollContainer = this.folders.menu.parentElement;
@ -218,7 +218,7 @@ export class AppDialogsManager {
this.scroll.setVirtualContainer(this.chatList); this.scroll.setVirtualContainer(this.chatList);
//this.scroll.attachSentinels(); //this.scroll.attachSentinels();
if(isTouchSupported && isSafari) { /* if(isTouchSupported && isSafari) {
let allowUp: boolean, allowDown: boolean, slideBeginY: number; let allowUp: boolean, allowDown: boolean, slideBeginY: number;
const container = this.scroll.container; const container = this.scroll.container;
container.addEventListener('touchstart', (event) => { container.addEventListener('touchstart', (event) => {
@ -238,7 +238,7 @@ export class AppDialogsManager {
event.preventDefault(); event.preventDefault();
} }
}); });
} } */
this.setListClickListener(this.chatList, null, true); this.setListClickListener(this.chatList, null, true);
@ -578,7 +578,7 @@ export class AppDialogsManager {
const titleSpan = document.createElement('span'); const titleSpan = document.createElement('span');
titleSpan.innerHTML = RichTextProcessor.wrapEmojiText(filter.title); titleSpan.innerHTML = RichTextProcessor.wrapEmojiText(filter.title);
const unreadSpan = document.createElement('span'); const unreadSpan = document.createElement('span');
unreadSpan.classList.add('unread-count'); unreadSpan.classList.add('badge', 'badge-20', 'badge-blue');
const i = document.createElement('i'); const i = document.createElement('i');
span.append(titleSpan, unreadSpan, i); span.append(titleSpan, unreadSpan, i);
li.append(span); li.append(span);

View File

@ -5,8 +5,8 @@ import type { DownloadOptions } from "../mtproto/apiFileManager";
import { InputFile } from "../../layer"; import { InputFile } from "../../layer";
import referenceDatabase, {ReferenceBytes} from "../mtproto/referenceDatabase"; import referenceDatabase, {ReferenceBytes} from "../mtproto/referenceDatabase";
import type { ApiError } from "../mtproto/apiManager"; import type { ApiError } from "../mtproto/apiManager";
import cacheStorage from "../cacheStorage";
import { getFileNameByLocation } from "../../helpers/fileName"; import { getFileNameByLocation } from "../../helpers/fileName";
import CacheStorageController from "../cacheStorage";
export type ResponseMethodBlob = 'blob'; export type ResponseMethodBlob = 'blob';
export type ResponseMethodJson = 'json'; export type ResponseMethodJson = 'json';
@ -23,6 +23,7 @@ export type Progress = {done: number, fileName: string, total: number, offset: n
export type ProgressCallback = (details: Progress) => void; export type ProgressCallback = (details: Progress) => void;
export class AppDownloadManager { export class AppDownloadManager {
private cacheStorage = new CacheStorageController('cachedFiles');
private downloads: {[fileName: string]: Download} = {}; private downloads: {[fileName: string]: Download} = {};
private progress: {[fileName: string]: Progress} = {}; private progress: {[fileName: string]: Progress} = {};
private progressCallbacks: {[fileName: string]: Array<ProgressCallback>} = {}; private progressCallbacks: {[fileName: string]: Array<ProgressCallback>} = {};
@ -102,7 +103,7 @@ export class AppDownloadManager {
const tryDownload = (): Promise<unknown> => { const tryDownload = (): Promise<unknown> => {
if(!apiManager.worker) { if(!apiManager.worker) {
return cacheStorage.getFile(fileName).then((blob) => { return this.cacheStorage.getFile(fileName).then((blob) => {
if(blob.size < options.size) throw 'wrong size'; if(blob.size < options.size) throw 'wrong size';
else deferred.resolve(blob); else deferred.resolve(blob);
}).catch(() => { }).catch(() => {

View File

@ -10,7 +10,7 @@ import { MOUNT_CLASS_TO } from '../mtproto/mtproto_config';
import rootScope from '../rootScope'; import rootScope from '../rootScope';
import apiUpdatesManager from './apiUpdatesManager'; import apiUpdatesManager from './apiUpdatesManager';
import appUsersManager from "./appUsersManager"; import appUsersManager from "./appUsersManager";
import Chat from '../../components/chat/chat'; import Chat, { ChatType } from '../../components/chat/chat';
import appChatsManager from './appChatsManager'; import appChatsManager from './appChatsManager';
import appDocsManager from './appDocsManager'; import appDocsManager from './appDocsManager';
import appInlineBotsManager from './AppInlineBotsManager'; import appInlineBotsManager from './AppInlineBotsManager';
@ -139,11 +139,12 @@ export class AppImManager {
//if(target.tagName == 'INPUT') return; //if(target.tagName == 'INPUT') return;
this.log('onkeydown', e, document.activeElement); //this.log('onkeydown', e, document.activeElement);
const chat = this.chat; const chat = this.chat;
if(e.key == 'Escape') { if(e.key == 'Escape') {
let cancel = true;
if(this.markupTooltip?.container?.classList.contains('is-visible')) { if(this.markupTooltip?.container?.classList.contains('is-visible')) {
this.markupTooltip.hide(); this.markupTooltip.hide();
} else if(chat.selection.isSelecting) { } else if(chat.selection.isSelecting) {
@ -152,11 +153,12 @@ export class AppImManager {
chat.input.replyElements.cancelBtn.click(); chat.input.replyElements.cancelBtn.click();
} else if(chat.peerID != 0) { // hide current dialog } else if(chat.peerID != 0) { // hide current dialog
this.setPeer(0); this.setPeer(0);
cancelEvent(e); } else {
cancel = false;
} }
// * cancel event for safari, because if application is in fullscreen, browser will try to exit fullscreen // * cancel event for safari, because if application is in fullscreen, browser will try to exit fullscreen
if(chat.peerID) { if(cancel) {
cancelEvent(e); cancelEvent(e);
} }
} else if(e.key == 'Meta' || e.key == 'Control') { } else if(e.key == 'Meta' || e.key == 'Control') {
@ -183,12 +185,13 @@ export class AppImManager {
if(goodMid) { if(goodMid) {
chat.input.initMessageEditing(goodMid); chat.input.initMessageEditing(goodMid);
cancelEvent(e); // * prevent from scrolling
} }
} }
} }
} }
if(e.target != chat.input.messageInput && target.tagName != 'INPUT' && !target.hasAttribute('contenteditable')) { if(chat.input.messageInput && e.target != chat.input.messageInput && target.tagName != 'INPUT' && !target.hasAttribute('contenteditable')) {
chat.input.messageInput.focus(); chat.input.messageInput.focus();
placeCaretAtEnd(chat.input.messageInput); placeCaretAtEnd(chat.input.messageInput);
} }
@ -336,9 +339,9 @@ export class AppImManager {
this.selectTab(1); this.selectTab(1);
} }
public setInnerPeer(peerID: number, lastMsgID?: number) { public setInnerPeer(peerID: number, lastMsgID?: number, type: ChatType = 'chat') {
// * prevent opening already opened peer // * prevent opening already opened peer
const existingIndex = this.chats.findIndex(chat => chat.peerID == peerID); const existingIndex = this.chats.findIndex(chat => chat.peerID == peerID && chat.type == type);
if(existingIndex !== -1) { if(existingIndex !== -1) {
this.spliceChats(existingIndex + 1); this.spliceChats(existingIndex + 1);
return this.setPeer(peerID, lastMsgID); return this.setPeer(peerID, lastMsgID);
@ -346,6 +349,10 @@ export class AppImManager {
this.createNewChat(); this.createNewChat();
if(type) {
this.chat.type = type;
}
this.chatsSelectTab(this.chat.container); this.chatsSelectTab(this.chat.container);
return this.setPeer(peerID, lastMsgID); return this.setPeer(peerID, lastMsgID);

View File

@ -1,8 +1,7 @@
import ProgressivePreloader from "../../components/preloader"; import ProgressivePreloader from "../../components/preloader";
import { listMergeSorted } from "../../helpers/array";
import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise";
import { tsNow } from "../../helpers/date"; import { tsNow } from "../../helpers/date";
import { copy, defineNotNumerableProperties, deepEqual, safeReplaceObject, getObjectKeysAndSort } from "../../helpers/object"; import { copy, defineNotNumerableProperties, safeReplaceObject, getObjectKeysAndSort } from "../../helpers/object";
import { randomLong } from "../../helpers/random"; import { randomLong } from "../../helpers/random";
import { splitStringByLength, limitSymbols } from "../../helpers/string"; import { splitStringByLength, limitSymbols } from "../../helpers/string";
import { Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMessage, InputNotifyPeer, InputPeerNotifySettings, Message, MessageAction, MessageEntity, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PhotoSize, SendMessageAction, Update } from "../../layer"; import { Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMessage, InputNotifyPeer, InputPeerNotifySettings, Message, MessageAction, MessageEntity, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PhotoSize, SendMessageAction, Update } from "../../layer";
@ -75,6 +74,11 @@ type MyInputMessagesFilter = 'inputMessagesFilterEmpty'
| 'inputMessagesFilterChatPhotos' | 'inputMessagesFilterChatPhotos'
| 'inputMessagesFilterPinned'; | 'inputMessagesFilterPinned';
export type PinnedStorage = Partial<{
promise: Promise<PinnedStorage>,
count: number,
maxID: number
}>;
export class AppMessagesManager { export class AppMessagesManager {
public messagesStorage: {[mid: string]: any} = {}; public messagesStorage: {[mid: string]: any} = {};
public messagesStorageByPeerID: {[peerID: string]: AppMessagesManager['messagesStorage']} = {}; public messagesStorageByPeerID: {[peerID: string]: AppMessagesManager['messagesStorage']} = {};
@ -82,9 +86,15 @@ export class AppMessagesManager {
public historiesStorage: { public historiesStorage: {
[peerID: string]: HistoryStorage [peerID: string]: HistoryStorage
} = {}; } = {};
public searchesStorage: {
// * mids - descend sorted [peerID: string]: Partial<{
public pinnedMessagesStorage: {[peerID: string]: Partial<{promise: Promise<number[]>, mids: number[]}>} = {}; [inputFilter in MyInputMessagesFilter]: {
count?: number,
history: number[]
}
}>
} = {};
public pinnedMessages: {[peerID: string]: PinnedStorage} = {};
public pendingByRandomID: {[randomID: string]: [number, number]} = {}; public pendingByRandomID: {[randomID: string]: [number, number]} = {};
public pendingByMessageID: any = {}; public pendingByMessageID: any = {};
@ -101,9 +111,6 @@ export class AppMessagesManager {
} }
} = {}; } = {};
public lastSearchFilter: any = {};
public lastSearchResults: any = [];
public needSingleMessages: number[] = []; public needSingleMessages: number[] = [];
private fetchSingleMessagesPromise: Promise<void> = null; private fetchSingleMessagesPromise: Promise<void> = null;
@ -293,8 +300,6 @@ export class AppMessagesManager {
}); });
} }
public getInputEntities(entities: any) { public getInputEntities(entities: any) {
var sendEntites = copy(entities); var sendEntites = copy(entities);
sendEntites.forEach((entity: any) => { sendEntites.forEach((entity: any) => {
@ -1801,47 +1806,33 @@ export class AppMessagesManager {
}); });
} }
/* public savePinnedMessage(peerID: number, mid: number) { public hidePinnedMessages(peerID: number) {
if(!mid) { return Promise.all([
delete this.pinnedMessagesStorage[peerID]; appStateManager.getState(),
} else { this.getPinnedMessage(peerID)
this.pinnedMessagesStorage[peerID] = mid; ])
.then(([state, pinned]) => {
if(!this.messagesStorage.hasOwnProperty(mid)) { state.hiddenPinnedMessages[peerID] = pinned.maxID;
this.wrapSingleMessage(mid).then(() => { rootScope.broadcast('peer_pinned_hidden', {peerID, maxID: pinned.maxID});
rootScope.broadcast('peer_pinned_message', peerID);
}); });
return;
}
} }
rootScope.broadcast('peer_pinned_message', peerID); public getPinnedMessage(peerID: number) {
} */ const p = this.pinnedMessages[peerID] ?? (this.pinnedMessages[peerID] = {});
if(p.promise) return p.promise;
else if(p.maxID) return Promise.resolve(p);
public getPinnedMessagesStorage(peerID: number) { return p.promise = this.getSearch(peerID, '', {_: 'inputMessagesFilterPinned'}, 0, 1).then(result => {
return this.pinnedMessagesStorage[peerID] ?? (this.pinnedMessagesStorage[peerID] = {}); p.count = result.count;
} p.maxID = result.history[0];
return p;
public getPinnedMessages(peerID: number) {
const storage = this.getPinnedMessagesStorage(peerID);
if(storage.mids) {
return Promise.resolve(storage.mids);
} else if(storage.promise) {
return storage.promise;
}
return storage.promise = new Promise<number[]>((resolve, reject) => {
this.getSearch(peerID, '', {_: 'inputMessagesFilterPinned'}, 0, 50).then(result => {
resolve(storage.mids = result.history);
}, reject);
}).finally(() => { }).finally(() => {
storage.promise = null; delete p.promise;
}); });
} }
public updatePinnedMessage(peerID: number, mid: number, unpin?: true, silent?: true, oneSide?: true) { public updatePinnedMessage(peerID: number, mid: number, unpin?: true, silent?: true, oneSide?: true) {
apiManager.invokeApi('messages.updatePinnedMessage', { return apiManager.invokeApi('messages.updatePinnedMessage', {
peer: appPeersManager.getInputPeerByID(peerID), peer: appPeersManager.getInputPeerByID(peerID),
unpin, unpin,
silent, silent,
@ -1853,6 +1844,38 @@ export class AppMessagesManager {
}); });
} }
public unpinAllMessages(peerID: number): Promise<boolean> {
return apiManager.invokeApi('messages.unpinAllMessages', {
peer: appPeersManager.getInputPeerByID(peerID)
}).then(affectedHistory => {
apiUpdatesManager.processUpdateMessage({
_: 'updateShort',
update: {
_: 'updatePts',
pts: affectedHistory.pts,
pts_count: affectedHistory.pts_count
}
});
if(!affectedHistory.offset) {
const storage = this.messagesStorageByPeerID[peerID];
for(const mid in storage) {
const message = storage[mid];
if(message.pFlags.pinned) {
delete message.pFlags.pinned;
}
}
rootScope.broadcast('peer_pinned_messages', peerID);
delete this.pinnedMessages[peerID];
return true;
}
return this.unpinAllMessages(peerID);
});
}
public getAlbumText(grouped_id: string) { public getAlbumText(grouped_id: string) {
const group = appMessagesManager.groupedMessagesStorage[grouped_id]; const group = appMessagesManager.groupedMessagesStorage[grouped_id];
let foundMessages = 0, message: string, totalEntities: MessageEntity[]; let foundMessages = 0, message: string, totalEntities: MessageEntity[];
@ -1884,9 +1907,7 @@ export class AppMessagesManager {
else return [mid]; else return [mid];
} }
public saveMessages(messages: any[], options: { public saveMessages(messages: any[]) {
isEdited?: boolean
} = {}) {
let albums: Set<string>; let albums: Set<string>;
messages.forEach((message) => { messages.forEach((message) => {
if(message.pFlags === undefined) { if(message.pFlags === undefined) {
@ -2669,36 +2690,59 @@ export class AppMessagesManager {
return false; return false;
} }
public getSearchStorage(peerID: number, inputFilter: MyInputMessagesFilter) {
if(!this.searchesStorage[peerID]) this.searchesStorage[peerID] = {};
if(!this.searchesStorage[peerID][inputFilter]) this.searchesStorage[peerID][inputFilter] = {history: []};
return this.searchesStorage[peerID][inputFilter];
}
public getSearchCounters(peerID: number, filters: MessagesFilter[]) {
return apiManager.invokeApi('messages.getSearchCounters', {
peer: appPeersManager.getInputPeerByID(peerID),
filters
});
}
public getSearch(peerID = 0, query: string = '', inputFilter: { public getSearch(peerID = 0, query: string = '', inputFilter: {
_?: MyInputMessagesFilter _: MyInputMessagesFilter
} = {_: 'inputMessagesFilterEmpty'}, maxID: number, limit = 20, offsetRate = 0, backLimit = 0): Promise<{ } = {_: 'inputMessagesFilterEmpty'}, maxID: number, limit = 20, offsetRate = 0, backLimit = 0): Promise<{
count: number, count: number,
next_rate: number, next_rate: number,
offset_id_offset: number,
history: number[] history: number[]
}> { }> {
//peerID = peerID ? parseInt(peerID) : 0;
const foundMsgs: number[] = []; const foundMsgs: number[] = [];
const useSearchCache = !query;
const newSearchFilter = {peer: peerID, filter: inputFilter};
const sameSearchCache = useSearchCache && deepEqual(this.lastSearchFilter, newSearchFilter);
if(useSearchCache && !sameSearchCache) { //this.log('search', maxID);
// this.log.warn(dT(), 'new search filter', lastSearchFilter, newSearchFilter)
this.lastSearchFilter = newSearchFilter; if(backLimit) {
this.lastSearchResults = []; limit += backLimit;
} }
//this.log(dT(), 'search', useSearchCache, sameSearchCache, this.lastSearchResults, maxID); //const beta = inputFilter._ == 'inputMessagesFilterPinned' && !backLimit;
const beta = false;
if(peerID && !maxID && !query) { let storage: {
const historyStorage = this.historiesStorage[peerID]; count?: number;
history: number[];
};
// * костыль для limit 1, если нужно и получить сообщение, и узнать количество сообщений
if(peerID && !backLimit && !maxID && !query && limit !== 1/* && inputFilter._ !== 'inputMessagesFilterPinned' */) {
storage = beta ?
this.getSearchStorage(peerID, inputFilter._) :
this.historiesStorage[peerID];
let filtering = true; let filtering = true;
if(historyStorage !== undefined && historyStorage.history.length) { const history = maxID ? storage.history.slice(storage.history.indexOf(maxID) + 1) : storage.history;
var neededContents: {
if(storage !== undefined && history.length) {
const neededContents: {
[messageMediaType: string]: boolean [messageMediaType: string]: boolean
} = {}, } = {},
neededDocTypes: string[] = [], excludeDocTypes: string[] = []; neededDocTypes: string[] = [],
excludeDocTypes: string[] = []/* ,
neededFlags: string[] = [] */;
switch(inputFilter._) { switch(inputFilter._) {
case 'inputMessagesFilterPhotos': case 'inputMessagesFilterPhotos':
@ -2749,6 +2793,10 @@ export class AppMessagesManager {
neededContents['avatar'] = true; neededContents['avatar'] = true;
break; break;
/* case 'inputMessagesFilterPinned':
neededFlags.push('pinned');
break; */
/* case 'inputMessagesFilterMyMentions': /* case 'inputMessagesFilterMyMentions':
neededContents['mentioned'] = true; neededContents['mentioned'] = true;
break; */ break; */
@ -2764,8 +2812,8 @@ export class AppMessagesManager {
} }
if(filtering) { if(filtering) {
for(let i = 0, length = historyStorage.history.length; i < length; i++) { for(let i = 0, length = history.length; i < length; i++) {
const message = this.messagesStorage[historyStorage.history[i]]; const message = this.messagesStorage[history[i]];
//|| (neededContents['mentioned'] && message.totalEntities.find((e: any) => e._ == 'messageEntityMention')); //|| (neededContents['mentioned'] && message.totalEntities.find((e: any) => e._ == 'messageEntityMention'));
@ -2786,7 +2834,9 @@ export class AppMessagesManager {
} }
} else if(neededContents['avatar'] && message.action && ['messageActionChannelEditPhoto', 'messageActionChatEditPhoto'].includes(message.action._)) { } else if(neededContents['avatar'] && message.action && ['messageActionChannelEditPhoto', 'messageActionChatEditPhoto'].includes(message.action._)) {
found = true; found = true;
} }/* else if(neededFlags.find(flag => message.pFlags[flag])) {
found = true;
} */
if(found) { if(found) {
foundMsgs.push(message.mid); foundMsgs.push(message.mid);
@ -2797,37 +2847,27 @@ export class AppMessagesManager {
} }
} }
} }
// this.log.warn(dT(), 'before append', foundMsgs)
if(filtering && foundMsgs.length < limit && this.lastSearchResults.length && sameSearchCache) {
let minID = foundMsgs.length ? foundMsgs[foundMsgs.length - 1] : false;
for(let i = 0; i < this.lastSearchResults.length; i++) {
if(minID === false || this.lastSearchResults[i] < minID) {
foundMsgs.push(this.lastSearchResults[i]);
if(foundMsgs.length >= limit) {
break;
}
}
}
}
// this.log.warn(dT(), 'after append', foundMsgs)
} }
if(foundMsgs.length) { if(foundMsgs.length) {
if(foundMsgs.length < limit) { if(foundMsgs.length < limit && (beta ? storage.count !== storage.history.length : true)) {
maxID = foundMsgs[foundMsgs.length - 1]; maxID = foundMsgs[foundMsgs.length - 1];
limit = limit - foundMsgs.length; limit = limit - foundMsgs.length;
} else { } else {
if(useSearchCache) {
this.lastSearchResults = listMergeSorted(this.lastSearchResults, foundMsgs)
}
return Promise.resolve({ return Promise.resolve({
count: 0, count: beta ? storage.count : 0,
next_rate: 0, next_rate: 0,
offset_id_offset: 0,
history: foundMsgs history: foundMsgs
}); });
} }
} else if(beta && storage?.count) {
return Promise.resolve({
count: storage.count,
next_rate: 0,
offset_id_offset: 0,
history: []
});
} }
let apiPromise: Promise<any>; let apiPromise: Promise<any>;
@ -2835,7 +2875,7 @@ export class AppMessagesManager {
apiPromise = apiManager.invokeApi('messages.search', { apiPromise = apiManager.invokeApi('messages.search', {
peer: appPeersManager.getInputPeerByID(peerID), peer: appPeersManager.getInputPeerByID(peerID),
q: query || '', q: query || '',
filter: (inputFilter || {_: 'inputMessagesFilterEmpty'}) as any as MessagesFilter, filter: inputFilter as any as MessagesFilter,
min_date: 0, min_date: 0,
max_date: 0, max_date: 0,
limit, limit,
@ -2862,7 +2902,7 @@ export class AppMessagesManager {
apiPromise = apiManager.invokeApi('messages.searchGlobal', { apiPromise = apiManager.invokeApi('messages.searchGlobal', {
q: query, q: query,
filter: (inputFilter || {_: 'inputMessagesFilterEmpty'}) as any as MessagesFilter, filter: inputFilter as any as MessagesFilter,
min_date: 0, min_date: 0,
max_date: 0, max_date: 0,
offset_rate: offsetRate, offset_rate: offsetRate,
@ -2880,6 +2920,14 @@ export class AppMessagesManager {
appChatsManager.saveApiChats(searchResult.chats); appChatsManager.saveApiChats(searchResult.chats);
this.saveMessages(searchResult.messages); this.saveMessages(searchResult.messages);
if(beta && storage && (!maxID || storage.history[storage.history.length - 1] == maxID)) {
const storage = this.getSearchStorage(peerID, inputFilter._);
const add = (searchResult.messages.map((m: any) => m.mid) as number[]).filter(mid => storage.history.indexOf(mid) === -1);
storage.history.push(...add);
storage.history.sort((a, b) => b - a);
storage.count = searchResult.count;
}
this.log('messages.search result:', inputFilter, searchResult); this.log('messages.search result:', inputFilter, searchResult);
const foundCount: number = searchResult.count || (foundMsgs.length + searchResult.messages.length); const foundCount: number = searchResult.count || (foundMsgs.length + searchResult.messages.length);
@ -2892,17 +2940,13 @@ export class AppMessagesManager {
this.migrateChecks(peerID, -chat.migrated_to.channel_id); this.migrateChecks(peerID, -chat.migrated_to.channel_id);
} }
} }
foundMsgs.push(message.mid); foundMsgs.push(message.mid);
}); });
if(useSearchCache &&
(!maxID || sameSearchCache && this.lastSearchResults.indexOf(maxID) >= 0)) {
this.lastSearchResults = listMergeSorted(this.lastSearchResults, foundMsgs);
}
// this.log(dT(), 'after API', foundMsgs, lastSearchResults)
return { return {
count: foundCount, count: foundCount,
offset_id_offset: searchResult.offset_id_offset,
next_rate: searchResult.next_rate, next_rate: searchResult.next_rate,
history: foundMsgs history: foundMsgs
}; };
@ -3825,11 +3869,16 @@ export class AppMessagesManager {
const channelID = update._ == 'updatePinnedChannelMessages' ? update.channel_id : undefined; const channelID = update._ == 'updatePinnedChannelMessages' ? update.channel_id : undefined;
const peerID = channelID ? -channelID : appPeersManager.getPeerID((update as Update.updatePinnedMessages).peer); const peerID = channelID ? -channelID : appPeersManager.getPeerID((update as Update.updatePinnedMessages).peer);
const storage = this.getPinnedMessagesStorage(peerID); /* const storage = this.getSearchStorage(peerID, 'inputMessagesFilterPinned');
if(!storage.mids) { if(storage.count !== storage.history.length) {
break; if(storage.count !== undefined) {
delete this.searchesStorage[peerID]['inputMessagesFilterPinned'];
} }
rootScope.broadcast('peer_pinned_messages', peerID);
break;
} */
const messages = channelID ? update.messages.map(messageID => appMessagesIDsManager.getFullMessageID(messageID, channelID)) : update.messages; const messages = channelID ? update.messages.map(messageID => appMessagesIDsManager.getFullMessageID(messageID, channelID)) : update.messages;
const missingMessages = messages.filter(mid => !this.messagesStorage[mid]); const missingMessages = messages.filter(mid => !this.messagesStorage[mid]);
@ -3838,22 +3887,31 @@ export class AppMessagesManager {
const werePinned = update.pFlags?.pinned; const werePinned = update.pFlags?.pinned;
if(werePinned) { if(werePinned) {
for(const mid of messages) { for(const mid of messages) {
storage.mids.push(mid); //storage.history.push(mid);
const message = this.messagesStorage[mid]; const message = this.messagesStorage[mid];
message.pFlags.pinned = true; message.pFlags.pinned = true;
} }
storage.mids.sort((a, b) => b - a); /* if(this.pinnedMessages[peerID]?.maxID) {
const maxMid = Math.max(...messages);
this.pinnedMessages
} */
//storage.history.sort((a, b) => b - a);
} else { } else {
for(const mid of messages) { for(const mid of messages) {
storage.mids.findAndSplice(_mid => _mid == mid); //storage.history.findAndSplice(_mid => _mid == mid);
const message = this.messagesStorage[mid]; const message = this.messagesStorage[mid];
delete message.pFlags.pinned; delete message.pFlags.pinned;
} }
} }
delete this.pinnedMessages[peerID];
appStateManager.getState().then(state => {
delete state.hiddenPinnedMessages[peerID];
rootScope.broadcast('peer_pinned_messages', peerID); rootScope.broadcast('peer_pinned_messages', peerID);
}); });
});
break; break;
} }

View File

@ -29,7 +29,7 @@ export class AppPeersManager {
} */ } */
public canPinMessage(peerID: number) { public canPinMessage(peerID: number) {
return peerID == rootScope.myID || (peerID < 0 && appChatsManager.hasRights(-peerID, 'pin')); return peerID > 0 || appChatsManager.hasRights(-peerID, 'pin');
} }
public getPeerPhoto(peerID: number) { public getPeerPhoto(peerID: number) {

View File

@ -37,7 +37,8 @@ type State = Partial<{
channelLocals: AppMessagesIDsManager['channelLocals'], channelLocals: AppMessagesIDsManager['channelLocals'],
channelsByLocals: AppMessagesIDsManager['channelsByLocals'], channelsByLocals: AppMessagesIDsManager['channelsByLocals'],
channelCurLocal: AppMessagesIDsManager['channelCurLocal'], channelCurLocal: AppMessagesIDsManager['channelCurLocal'],
} },
hiddenPinnedMessages: {[peerID: string]: number}
}>; }>;
const REFRESH_KEYS = ['dialogs', 'allDialogsLoaded', 'messages', 'contactsList', 'stateCreatedTime', const REFRESH_KEYS = ['dialogs', 'allDialogsLoaded', 'messages', 'contactsList', 'stateCreatedTime',
@ -65,7 +66,7 @@ export class AppStateManager extends EventListenerBase<{
if(state) { if(state) {
if(state.version != STATE_VERSION) { if(state.version != STATE_VERSION) {
state = {}; state = {};
} else if((state.stateCreatedTime || 0) + REFRESH_EVERY < time) { } else if(((state.stateCreatedTime || 0) + REFRESH_EVERY) < time && false) {
this.log('will refresh state', state.stateCreatedTime, time); this.log('will refresh state', state.stateCreatedTime, time);
REFRESH_KEYS.forEach(key => { REFRESH_KEYS.forEach(key => {
delete state[key]; delete state[key];
@ -77,6 +78,7 @@ export class AppStateManager extends EventListenerBase<{
this.state = state || {}; this.state = state || {};
this.state.chats = state.chats || {}; this.state.chats = state.chats || {};
this.state.users = state.users || {}; this.state.users = state.users || {};
this.state.hiddenPinnedMessages = this.state.hiddenPinnedMessages || {};
this.state.version = STATE_VERSION; this.state.version = STATE_VERSION;
// ??= doesn't compiles // ??= doesn't compiles
@ -97,7 +99,7 @@ export class AppStateManager extends EventListenerBase<{
} }
//console.timeEnd('load state'); //console.timeEnd('load state');
resolve(state); resolve(this.state);
}).catch(resolve).finally(() => { }).catch(resolve).finally(() => {
setInterval(() => this.saveState(), 10000); setInterval(() => this.saveState(), 10000);
}); });

View File

@ -1,15 +1,15 @@
import { blobConstruct } from '../helpers/blob'; import { blobConstruct } from '../helpers/blob';
import FileManager from './filemanager'; import FileManager from './filemanager';
import { MOUNT_CLASS_TO } from './mtproto/mtproto_config'; //import { MOUNT_CLASS_TO } from './mtproto/mtproto_config';
//import { logger } from './polyfill'; //import { logger } from './polyfill';
class CacheStorageController { export default class CacheStorageController {
public dbName = 'cachedFiles'; //public dbName = 'cachedFiles';
public openDbPromise: Promise<Cache>; public openDbPromise: Promise<Cache>;
//private log: ReturnType<typeof logger> = logger('CS'); //private log: ReturnType<typeof logger> = logger('CS');
constructor() { constructor(public dbName: string) {
this.openDatabase(); this.openDatabase();
} }
@ -21,9 +21,19 @@ class CacheStorageController {
return this.openDbPromise = caches.open(this.dbName); return this.openDbPromise = caches.open(this.dbName);
} }
public deleteFile(fileName: string) { public delete(entryName: string) {
return this.timeoutOperation(async(cache) => { return this.timeoutOperation(async(cache) => {
const deleted = await cache.delete('/' + fileName); const deleted = await cache.delete('/' + entryName);
});
}
public deleteAll() {
return caches.delete(this.dbName);
}
public save(entryName: string, response: Response) {
return this.timeoutOperation((cache) => {
return cache.put('/' + entryName, response);
}); });
} }
@ -33,18 +43,16 @@ class CacheStorageController {
blob = blobConstruct(blob) as Blob; blob = blobConstruct(blob) as Blob;
} }
return this.timeoutOperation(async(cache) => { return this.save(fileName, new Response(blob)).then(() => {
await cache.put('/' + fileName, new Response(blob));
return blob as Blob; return blob as Blob;
}); });
} }
public getBlobSize(blob: any) { /* public getBlobSize(blob: any) {
return blob.size || blob.byteLength || blob.length; return blob.size || blob.byteLength || blob.length;
} } */
public getFile(fileName: string) { public getFile(fileName: string, method: 'blob' | 'json' | 'text' = 'blob'): Promise<any> {
//return Promise.reject(); //return Promise.reject();
// const str = `get fileName: ${fileName}`; // const str = `get fileName: ${fileName}`;
@ -54,10 +62,10 @@ class CacheStorageController {
if(!response || !cache) { if(!response || !cache) {
//console.warn('getFile:', response, fileName); //console.warn('getFile:', response, fileName);
throw 'No response???'; throw 'NO_ENTRY_FOUND';
} }
const promise = response.blob(); const promise = response[method]();
// promise.then(() => { // promise.then(() => {
// console.timeEnd(str); // console.timeEnd(str);
// }); // });
@ -101,6 +109,6 @@ class CacheStorageController {
} }
} }
const cacheStorage = new CacheStorageController(); //const cacheStorage = new CacheStorageController();
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.cacheStorage = cacheStorage); //MOUNT_CLASS_TO && (MOUNT_CLASS_TO.cacheStorage = cacheStorage);
export default cacheStorage; //export default cacheStorage;

View File

@ -13,7 +13,7 @@ function dT() {
} }
export function logger(prefix: string, level = LogLevels.log | LogLevels.warn | LogLevels.error) { export function logger(prefix: string, level = LogLevels.log | LogLevels.warn | LogLevels.error) {
if(process.env.NODE_ENV != 'development' || true) { if(process.env.NODE_ENV != 'development'/* || true */) {
level = LogLevels.error; level = LogLevels.error;
} }

View File

@ -3,7 +3,7 @@ import { notifyAll, notifySomeone } from "../../helpers/context";
import { getFileNameByLocation } from "../../helpers/fileName"; import { getFileNameByLocation } from "../../helpers/fileName";
import { nextRandomInt } from "../../helpers/random"; import { nextRandomInt } from "../../helpers/random";
import { FileLocation, InputFile, InputFileLocation, UploadFile } from "../../layer"; import { FileLocation, InputFile, InputFileLocation, UploadFile } from "../../layer";
import cacheStorage from "../cacheStorage"; import CacheStorageController from "../cacheStorage";
import cryptoWorker from "../crypto/cryptoworker"; import cryptoWorker from "../crypto/cryptoworker";
import FileManager from "../filemanager"; import FileManager from "../filemanager";
import { logger, LogLevels } from "../logger"; import { logger, LogLevels } from "../logger";
@ -33,6 +33,8 @@ type MyUploadFile = UploadFile.uploadFile;
const MAX_FILE_SAVE_SIZE = 20e6; const MAX_FILE_SAVE_SIZE = 20e6;
export class ApiFileManager { export class ApiFileManager {
public cacheStorage = new CacheStorageController('cachedFiles');
public cachedDownloadPromises: { public cachedDownloadPromises: {
[fileName: string]: CancellablePromise<Blob> [fileName: string]: CancellablePromise<Blob>
} = {}; } = {};
@ -112,7 +114,7 @@ export class ApiFileManager {
} }
public getFileStorage() { public getFileStorage() {
return cacheStorage; return this.cacheStorage;
} }
public cancelDownload(fileName: string) { public cancelDownload(fileName: string) {
@ -409,7 +411,7 @@ export class ApiFileManager {
public deleteFile(fileName: string) { public deleteFile(fileName: string) {
//this.log('will delete file:', fileName); //this.log('will delete file:', fileName);
delete this.cachedDownloadPromises[fileName]; delete this.cachedDownloadPromises[fileName];
return this.getFileStorage().deleteFile(fileName); return this.getFileStorage().delete(fileName);
} }
public uploadFile({file, fileName}: {file: Blob | File, fileName: string}) { public uploadFile({file, fileName}: {file: Blob | File, fileName: string}) {

View File

@ -112,7 +112,7 @@ export class ApiManager {
} }
// WebPushApiManager.forceUnsubscribe(); // WARNING // WebPushApiManager.forceUnsubscribe(); // WARNING
let storageResult = await AppStorage.get<string[]|boolean[]>(storageKeys); let storageResult = await AppStorage.get<string[]|boolean[]>(...storageKeys);
let logoutPromises = []; let logoutPromises = [];
for(let i = 0; i < storageResult.length; i++) { for(let i = 0; i < storageResult.length; i++) {
@ -127,7 +127,10 @@ export class ApiManager {
}).finally(() => { }).finally(() => {
this.baseDcID = 0; this.baseDcID = 0;
//this.telegramMeNotify(false); //this.telegramMeNotify(false);
AppStorage.clear(); const promise = AppStorage.clear();
promise.finally(() => {
self.postMessage({type: 'reload'});
});
})/* .then(() => { })/* .then(() => {
location.pathname = '/'; location.pathname = '/';
}) */; }) */;
@ -176,7 +179,7 @@ export class ApiManager {
const akID = 'dc' + dcID + '_auth_keyID'; const akID = 'dc' + dcID + '_auth_keyID';
const ss = 'dc' + dcID + '_server_salt'; const ss = 'dc' + dcID + '_server_salt';
return this.gettingNetworkers[getKey] = AppStorage.get<string[]/* |boolean[] */>([ak, akID, ss]) return this.gettingNetworkers[getKey] = AppStorage.get<string[]>(ak, akID, ss)
.then(async([authKeyHex, authKeyIDHex, serverSaltHex]) => { .then(async([authKeyHex, authKeyIDHex, serverSaltHex]) => {
let networker: MTPNetworker; let networker: MTPNetworker;
if(authKeyHex && authKeyHex.length == 512) { if(authKeyHex && authKeyHex.length == 512) {

View File

@ -2,7 +2,6 @@
import '../polyfill'; import '../polyfill';
import apiManager from "./apiManager"; import apiManager from "./apiManager";
import AppStorage from '../storage';
import cryptoWorker from "../crypto/cryptoworker"; import cryptoWorker from "../crypto/cryptoworker";
import networkerFactory from "./networkerFactory"; import networkerFactory from "./networkerFactory";
import apiFileManager from './apiFileManager'; import apiFileManager from './apiFileManager';
@ -74,10 +73,7 @@ ctx.addEventListener('message', async(e) => {
//debugger; //debugger;
if(task.useLs) { if(task.type == 'convertWebp') {
AppStorage.finishTask(task.taskID, task.args);
return;
} else if(task.type == 'convertWebp') {
const {fileName, bytes} = task.payload; const {fileName, bytes} = task.payload;
const deferred = apiFileManager.webpConvertPromises[fileName]; const deferred = apiFileManager.webpConvertPromises[fileName];
if(deferred) { if(deferred) {

View File

@ -132,17 +132,14 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
return; return;
} }
if(task.useLs) { if(task.update) {
// @ts-ignore
AppStorage[task.task](...task.args).then(res => {
this.postMessage({useLs: true, taskID: task.taskID, args: res});
});
} else if(task.update) {
if(this.updatesProcessor) { if(this.updatesProcessor) {
this.updatesProcessor(task.update.obj, task.update.bool); this.updatesProcessor(task.update.obj, task.update.bool);
} }
} else if(task.progress) { } else if(task.progress) {
rootScope.broadcast('download_progress', task.progress); rootScope.broadcast('download_progress', task.progress);
} else if(task.type == 'reload') {
location.reload();
} else if(task.type == 'connectionStatusChange') { } else if(task.type == 'connectionStatusChange') {
rootScope.broadcast('connection_status_change', task.payload); rootScope.broadcast('connection_status_change', task.payload);
} else if(task.type == 'convertWebp') { } else if(task.type == 'convertWebp') {
@ -255,15 +252,17 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
return this.performTaskWorker('invokeApi', method, params, options).then((result: any) => { return this.performTaskWorker('invokeApi', method, params, options).then((result: any) => {
if(result._.includes('NotModified')) { if(result._.includes('NotModified')) {
//this.log.warn('NotModified saved!', method, queryJSON); this.log.warn('NotModified saved!', method, queryJSON);
return cached.result; return cached.result;
} }
if(result.hash) { if(result.hash/* || result.messages */) {
const hash = result.hash/* || this.computeHash(result.messages) */;
if(!this.hashes[method]) this.hashes[method] = {}; if(!this.hashes[method]) this.hashes[method] = {};
this.hashes[method][queryJSON] = { this.hashes[method][queryJSON] = {
hash: result.hash, hash,
result: result result
}; };
} }
@ -271,6 +270,10 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
}); });
} }
/* private computeHash(smth: any[]) {
return smth.reduce((hash, v) => (((hash * 0x4F25) & 0x7FFFFFFF) + v.id) & 0x7FFFFFFF, 0);
} */
public setBaseDcID(dcID: number) { public setBaseDcID(dcID: number) {
return this.performTaskWorker('setBaseDcID', dcID); return this.performTaskWorker('setBaseDcID', dcID);
} }

View File

@ -12,6 +12,7 @@ type BroadcastEvents = {
'user_auth': UserAuth, 'user_auth': UserAuth,
'peer_changed': number, 'peer_changed': number,
'peer_pinned_messages': number, 'peer_pinned_messages': number,
'peer_pinned_hidden': {peerID: number, maxID: number},
'peer_typings': {peerID: number, typings: UserTyping[]}, 'peer_typings': {peerID: number, typings: UserTyping[]},
'filter_delete': MyDialogFilter, 'filter_delete': MyDialogFilter,
@ -90,9 +91,9 @@ class RootScope {
} }
public broadcast = <T extends keyof BroadcastEvents>(name: T, detail?: BroadcastEvents[T]) => { public broadcast = <T extends keyof BroadcastEvents>(name: T, detail?: BroadcastEvents[T]) => {
/* if(name != 'user_update') { if(name != 'user_update') {
console.debug(dT(), 'Broadcasting ' + name + ' event, with args:', detail); console.debug('Broadcasting ' + name + ' event, with args:', detail);
} */ }
const myCustomEvent = new CustomEvent(name, {detail}); const myCustomEvent = new CustomEvent(name, {detail});
document.dispatchEvent(myCustomEvent); document.dispatchEvent(myCustomEvent);

View File

@ -1,68 +1,73 @@
import { Modes } from './mtproto/mtproto_config'; import CacheStorageController from './cacheStorage';
import { notifySomeone, isWorker } from '../helpers/context'; import { MOUNT_CLASS_TO } from './mtproto/mtproto_config';
//import { stringify } from '../helpers/json'; //import { stringify } from '../helpers/json';
class ConfigStorage { class AppStorage {
public keyPrefix = ''; //private log = (...args: any[]) => console.log('[SW LS]', ...args);
public noPrefix = false; private log = (...args: any[]) => {};
private cache: {[key: string]: any} = {};
private useLs = true;
storageGetPrefix() { private cacheStorage = new CacheStorageController('session');
if(this.noPrefix) {
//public noPrefix = false;
private cache: {[key: string]: any} = {};
private useCS = true;
constructor() {
}
public storageGetPrefix() {
/* if(this.noPrefix) {
this.noPrefix = false; this.noPrefix = false;
return ''; return '';
} */
return '';
//return this.keyPrefix;
} }
return this.keyPrefix; public async get<T>(...keys: string[]): Promise<T> {
} const single = keys.length === 1;
const result: any[] = [];
get(keys: string | string[], callback: any) { const prefix = this.storageGetPrefix();
var single = false;
if(!Array.isArray(keys)) {
keys = Array.prototype.slice.call(arguments);
callback = keys.pop();
single = keys.length == 1;
}
var result = [];
var allFound = true;
var prefix = this.storageGetPrefix();
for(let key of keys) { for(let key of keys) {
key = prefix + key; key = prefix + key;
if(this.cache.hasOwnProperty(key)) { if(this.cache.hasOwnProperty(key)) {
result.push(this.cache[key]); result.push(this.cache[key]);
} else if(this.useLs) { } else if(this.useCS) {
let value: any; let value: any;
try { try {
value = localStorage.getItem(key); value = await this.cacheStorage.getFile(key, 'text');
value = JSON.parse(value);
} catch(e) { } catch(e) {
this.useLs = false; if(e !== 'NO_ENTRY_FOUND') {
this.useCS = false;
value = undefined;
console.error('[AS]: get error:', e, key, value);
}
} }
// const str = `[get] ${keys.join(', ')}`; // const str = `[get] ${keys.join(', ')}`;
// console.time(str); // console.time(str);
try { try {
value = (value === undefined || value === null) ? false : JSON.parse(value); value = (value === undefined || value === null) ? false : value;
} catch(e) { } catch(e) {
value = false; value = false;
} }
//console.timeEnd(str); //console.timeEnd(str);
result.push(this.cache[key] = value); result.push(this.cache[key] = value);
} else { } else {
allFound = false; throw 'something went wrong';
} }
} }
if(allFound) { return single ? result[0] : result;
return callback(single ? result[0] : result);
}
} }
set(obj: any, callback: any) { public async set(obj: any) {
var keyValues: any = {}; let keyValues: any = {};
var prefix = this.storageGetPrefix(), let prefix = this.storageGetPrefix(),
key, value; key, value;
//console.log('storageSetValue', obj, callback, arguments); //console.log('storageSetValue', obj, callback, arguments);
@ -82,140 +87,48 @@ class ConfigStorage {
value = stringify(value); value = stringify(value);
console.log('LocalStorage set: stringify time by own stringify:', performance.now() - perf); */ console.log('LocalStorage set: stringify time by own stringify:', performance.now() - perf); */
if(this.useLs) { if(this.useCS) {
try { try {
//console.log('setItem', key, value); //console.log('setItem', key, value);
localStorage.setItem(key, value); await this.cacheStorage.save(key, new Response(value, {headers: {'Content-Type': 'application/json'}}));
} catch(e) { } catch(e) {
this.useLs = false; //this.useCS = false;
console.error('[AS]: set error:', e, value);
} }
} else { } else {
keyValues[key] = value; keyValues[key] = value;
} }
} }
} }
if(this.useLs) {
if(callback) {
callback();
} }
return; public async remove(...keys: any[]) {
}
}
remove(keys: any, callback: any) {
if(!Array.isArray(keys)) { if(!Array.isArray(keys)) {
keys = Array.prototype.slice.call(arguments) keys = Array.prototype.slice.call(arguments);
if(typeof keys[keys.length - 1] === 'function') {
callback = keys.pop();
}
} }
var prefix = this.storageGetPrefix(), let prefix = this.storageGetPrefix(),
i, key; i, key;
for(i = 0; i < keys.length; i++) { for(i = 0; i < keys.length; i++) {
key = keys[i] = prefix + keys[i]; key = keys[i] = prefix + keys[i];
delete this.cache[key]; delete this.cache[key];
if(this.useLs) { if(this.useCS) {
try { try {
localStorage.removeItem(key); await this.cacheStorage.delete(key);
} catch(e) { } catch(e) {
this.useLs = false; this.useCS = false;
console.error('[AS]: remove error:', e);
} }
} }
} }
if(callback) {
callback();
}
}
clear() {
localStorage.clear();
location.reload();
}
}
class AppStorage {
private taskID = 0;
private tasks: {[taskID: number]: (result: any) => void} = {};
//private log = (...args: any[]) => console.log('[SW LS]', ...args);
private log = (...args: any[]) => {};
private configStorage: ConfigStorage;
constructor() {
if(Modes.test) {
this.setPrefix('t_');
}
if(!isWorker) {
this.configStorage = new ConfigStorage();
}
}
public setPrefix(newPrefix: string) {
if(this.configStorage) {
this.configStorage.keyPrefix = newPrefix;
}
}
public noPrefix() {
if(this.configStorage) {
this.configStorage.noPrefix = true;
}
}
public finishTask(taskID: number, result: any) {
this.log('finishTask:', taskID, result, Object.keys(this.tasks));
if(!this.tasks.hasOwnProperty(taskID)) {
this.log('no such task:', taskID, result);
return;
}
this.tasks[taskID](result);
delete this.tasks[taskID];
}
private proxy<T>(methodName: 'set' | 'get' | 'remove' | 'clear', ..._args: any[]) {
return new Promise<T>((resolve, reject) => {
if(isWorker) {
const taskID = this.taskID++;
this.tasks[taskID] = resolve;
const task = {useLs: true, task: methodName, taskID, args: _args};
notifySomeone(task);
} else {
let args = Array.prototype.slice.call(_args);
args.push((result: T) => {
resolve(result);
});
this.configStorage[methodName].apply(this.configStorage, args as any);
}
});
}
public get<T>(...args: any[]) {
return this.proxy<T>('get', ...args);
}
public set<T>(...args: any[]) {
//console.trace(...args);
return this.proxy<T>('set', ...args);
}
public remove<T>(...args: any[]) {
return this.proxy<T>('remove', ...args);
} }
public clear() { public clear() {
return this.proxy('clear'); return this.cacheStorage.deleteAll();
} }
} }
export default new AppStorage(); const appStorage = new AppStorage();
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appStorage = appStorage);
export default appStorage;

View File

@ -38,7 +38,7 @@
bottom: 20px; bottom: 20px;
right: 20px; right: 20px;
//transition: .2s ease; //transition: .2s ease;
transition: var(--btn-corner-transition) !important; transition: transform var(--btn-corner-transition) !important;
transform: translate3d(0, var(--translateY), 0); transform: translate3d(0, var(--translateY), 0);
z-index: 3; z-index: 3;
user-select: none; user-select: none;
@ -49,11 +49,11 @@
border: none; border: none;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 !important;
&.is-visible { &.is-visible {
--translateY: 0; --translateY: 0;

View File

@ -1,8 +1,4 @@
$btn-send-margin: .5625rem; $btn-send-margin: .5625rem;
$chat-input-size: 3.375rem;
$chat-input-handhelds-size: 2.875rem;
$chat-padding: 1rem;
$chat-padding-handhelds: .5rem;
$chat-helper-size: 39px; $chat-helper-size: 39px;
/* #bubble-contextmenu > div { /* #bubble-contextmenu > div {
@ -41,7 +37,7 @@ $chat-helper-size: 39px;
//transition: transform var(--layer-transition); //transition: transform var(--layer-transition);
body.is-right-column-shown & { body.is-right-column-shown & {
transform: translate3d(calc(var(--right-column-width) / -2), var(--translateY), 0); transform: translate3d(calc(var(--right-column-width) / -2), var(--translateY), 0) !important;
} }
body.animation-level-0 & { body.animation-level-0 & {
@ -51,8 +47,8 @@ $chat-helper-size: 39px;
&.is-hidden { &.is-hidden {
--translateY: 100%; --translateY: 100%;
transform: translate3d(0, var(--translateY), 0); transform: translate3d(0, var(--translateY), 0) !important;
position: absolute; position: absolute !important;
bottom: 0; bottom: 0;
.bubbles.is-selecting:not(.backwards) ~ & { .bubbles.is-selecting:not(.backwards) ~ & {
@ -155,13 +151,13 @@ $chat-helper-size: 39px;
.btn-send-container { .btn-send-container {
position: absolute; position: absolute;
right: var(--padding-horizontal); right: var(--padding-horizontal);
z-index: 2;
bottom: 0; bottom: 0;
padding-bottom: inherit; padding-bottom: inherit;
} }
.btn-send { .btn-send {
color: #9e9e9e; color: #9e9e9e;
z-index: 3;
> .tgico { > .tgico {
position: absolute; position: absolute;
@ -215,7 +211,7 @@ $chat-helper-size: 39px;
position: absolute; position: absolute;
top: -94px; top: -94px;
left: -94px; left: -94px;
transition: transform .03s, visibility .1s; transition: transform .03s ease-in-out, visibility .1s;
visibility: hidden; visibility: hidden;
@include respond-to(handhelds) { @include respond-to(handhelds) {
@ -251,11 +247,7 @@ $chat-helper-size: 39px;
} }
.rows-wrapper { .rows-wrapper {
width: calc(100% - #{$chat-input-size * 2 + $btn-send-margin * 2}); width: calc(100% - (var(--chat-input-size) * 2 + #{$btn-send-margin * 2}));
@include respond-to(handhelds) {
width: calc(100% - #{$chat-input-handhelds-size * 2 + $btn-send-margin * 2});
}
} }
.attach-file { .attach-file {
@ -280,7 +272,7 @@ $chat-helper-size: 39px;
} }
.bubbles.is-selecting ~ & { .bubbles.is-selecting ~ & {
.new-message-wrapper { .new-message-wrapper, .pinned-container {
html:not(.is-safari) & { html:not(.is-safari) & {
transition: .1s opacity; transition: .1s opacity;
} }
@ -295,26 +287,10 @@ $chat-helper-size: 39px;
transition: .2s transform; transition: .2s transform;
} }
} }
.rows-wrapper {
html:not(.is-safari) & {
transition: width .2s, border-bottom-right-radius .1s, transform .2s;
&:after {
transition: transform .1s;
}
}
html.is-safari & {
transition: none;
}
//will-change: transform;
}
} }
.bubbles.is-selecting:not(.backwards) ~ & { .bubbles.is-selecting:not(.backwards) ~ & {
.new-message-wrapper { .new-message-wrapper, .pinned-container {
opacity: 0; opacity: 0;
} }
@ -329,39 +305,9 @@ $chat-helper-size: 39px;
} }
.rows-wrapper { .rows-wrapper {
max-height: $chat-input-size; max-height: var(--chat-input-size);
border-bottom-right-radius: 12px;
transform-origin: left;
width: 28.75rem; width: 28.75rem;
transform: translate3d(25%, 0, 0); //max-width: 28.75rem;
//transform: translate3d(#{calc(28.75rem / 4)}, 0, 0);
/* html.is-safari & {
max-width: 28.75rem;
} */
/* transform: translateX(-50%);
left: 50%;
position: absolute; */
// left sidebar (420px) + 728px chat max width
@media only screen and (min-width: $floating-left-sidebar + 1) and (max-width: $large-screen / 4 + $messages-container-width) {
//transform: translateX(calc((100vw - 420px - 100%) / 2 - #{$chat-padding}));
transform: translate3d(calc((100vw - min(100vw / 2.5, #{$large-screen / 4}) - 100%) / 2 - #{$chat-padding}), 0, 0);
}
@media only screen and (max-width: 728px) {
//transform: translateX(calc((100vw - 420px - 100%) / 2 - #{$chat-padding}));
transform: translate3d(calc((100vw - 100%) / 2 - #{$chat-padding}), 0, 0);
}
@include respond-to(handhelds) {
transform: translate3d(calc((100vw - 100%) / 2 - #{$chat-padding-handhelds}), 0, 0);
}
&:after {
transform: scaleX(-1) translateX(#{.5625rem * 2});
}
} }
.reply-wrapper { .reply-wrapper {
@ -374,14 +320,11 @@ $chat-helper-size: 39px;
} }
} }
.bubbles.is-selecting.backwards ~ & { .bubbles.is-selecting.backwards ~ & {
.new-message-wrapper { .new-message-wrapper, .pinned-container {
html:not(.is-safari) & { html:not(.is-safari) & {
transition-delay: .1s; transition-delay: .1s;
} }
} }
.selection-container { .selection-container {
@ -488,20 +431,78 @@ $chat-helper-size: 39px;
display: flex; display: flex;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
width: calc(100% - #{$chat-input-size + $btn-send-margin}); width: calc(100% - (var(--chat-input-size) + #{$btn-send-margin}));
max-width: calc(100% - #{$chat-input-size + $btn-send-margin}); max-width: calc(100% - (var(--chat-input-size) + #{$btn-send-margin}));
justify-content: center; justify-content: center;
background-color: #fff; background-color: #fff;
border-radius: 12px; border-radius: 12px;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
box-shadow: 0 1px 2px 0 rgba(16, 35, 47, .07); box-shadow: 0 1px 2px 0 rgba(16, 35, 47, .07);
min-height: $chat-input-size; min-height: var(--chat-input-size);
max-height: 30rem; max-height: 30rem;
flex: 0 0 auto; flex: 0 0 auto;
position: relative; position: relative;
z-index: 3; z-index: 3;
transition: width .1s; transition: width .1s;
.chat-input.type-pinned & {
width: 17.125rem;
}
&.is-centering {
html:not(.is-safari) & {
transition: width .2s, border-bottom-right-radius .1s, transform .2s;
&:after {
transition: transform .1s;
}
}
html.is-safari & {
transition: none;
}
//will-change: transform;
}
&.is-centering:not(.backwards), &.is-centered {
border-bottom-right-radius: 12px;
transform-origin: left;
transform: translate3d(25%, 0, 0);
//transform: translate3d(calc(((var(--messages-container-width) - var(--chat-input-padding) * 2) - 100%) / 2), 0, 0);
//transform: translate3d(#{calc(28.75rem / 4)}, 0, 0);
/* html.is-safari & {
max-width: 28.75rem;
} */
/* transform: translateX(-50%);
left: 50%;
position: absolute; */
// left sidebar (420px) + 728px chat max width
@media only screen and (min-width: $floating-left-sidebar + 1) and (max-width: $large-screen / 4 + $messages-container-width) {
//transform: translateX(calc((100vw - 420px - 100%) / 2 - #{$chat-padding}));
transform: translate3d(calc((100vw - min(100vw / 2.5, #{$large-screen / 4}) - 100%) / 2 - #{$chat-padding}), 0, 0) !important;
}
@media only screen and (max-width: 728px) {
//transform: translateX(calc((100vw - 420px - 100%) / 2 - #{$chat-padding}));
transform: translate3d(calc((100vw - 100%) / 2 - #{$chat-padding}), 0, 0) !important;
}
@include respond-to(handhelds) {
transform: translate3d(calc((100vw - 100%) / 2 - #{$chat-padding-handhelds}), 0, 0) !important;
}
&:after {
transform: scaleX(-1) translateX(#{.5625rem * 2});
}
}
&.is-centered, &.is-centered.is-centering:not(.backwards) {
transform: translate3d(calc(((var(--messages-container-width) - var(--chat-input-padding) * 2) - 100%) / 2), 0, 0);
}
// ! Need due to reply transform under the container // ! Need due to reply transform under the container
&:before { &:before {
position: absolute; position: absolute;
@ -524,9 +525,6 @@ $chat-helper-size: 39px;
@include respond-to(handhelds) { @include respond-to(handhelds) {
--padding-vertical: .5px; --padding-vertical: .5px;
--padding-horizontal: .5rem; --padding-horizontal: .5rem;
width: calc(100% - #{$chat-input-handhelds-size + $btn-send-margin});
max-width: calc(100% - #{$chat-input-handhelds-size + $btn-send-margin});
min-height: $chat-input-handhelds-size;
} }
@media only screen and (max-width: 420px) { @media only screen and (max-width: 420px) {
@ -702,6 +700,15 @@ $chat-helper-size: 39px;
} }
} }
.pinned-container {
width: 17.125rem;
&-button {
height: 2.5rem;
padding: 0;
}
}
.btn-icon { .btn-icon {
display: block; display: block;
flex: 0 0 auto; flex: 0 0 auto;
@ -836,12 +843,12 @@ $chat-helper-size: 39px;
&:not(.scrolled-down):not(.search-results-active) { &:not(.scrolled-down):not(.search-results-active) {
//> .bubbles-transform-helper { //> .bubbles-transform-helper {
// ! these lines will blur messages if chat input helper is active // ! these lines will blur messages if chat input helper is active
> .scrollable { //> .scrollable {
-webkit-mask-image: -webkit-linear-gradient(bottom, transparent, #000 20px); -webkit-mask-image: -webkit-linear-gradient(bottom, transparent, #000 28px);
mask-image: linear-gradient(0deg, transparent 0, #000 20px); mask-image: linear-gradient(0deg, transparent 0, #000 28px);
} //}
> .bubbles-go-down { & + .chat-input .bubbles-go-down {
cursor: pointer; cursor: pointer;
--translateY: 0; --translateY: 0;
opacity: 1; opacity: 1;
@ -884,6 +891,8 @@ $chat-helper-size: 39px;
padding: 0 1rem; padding: 0 1rem;
max-width: var(--messages-container-width); max-width: var(--messages-container-width);
//padding-top: 10000px;
transition: transform var(--layer-transition); transition: transform var(--layer-transition);
transform: translateY(0); transform: translateY(0);
/* transition: margin-top var(--layer-transition); /* transition: margin-top var(--layer-transition);
@ -903,7 +912,10 @@ $chat-helper-size: 39px;
} }
&.is-chat { &.is-chat {
.is-in .bubble__container { .is-in {
//margin-left: 45px;
.bubble__container {
margin-left: 45px; margin-left: 45px;
//margin-left: 3rem; #DO JS3 //margin-left: 3rem; #DO JS3
@ -912,6 +924,7 @@ $chat-helper-size: 39px;
} }
} }
} }
}
&.is-channel:not(.is-chat) { &.is-channel:not(.is-chat) {
.bubble { .bubble {
@ -949,42 +962,35 @@ $chat-helper-size: 39px;
position: absolute; position: absolute;
background-color: #fff; background-color: #fff;
border-radius: 50%; border-radius: 50%;
width: 3.25rem;
height: 3.25rem;
color: $placeholder-color; color: $placeholder-color;
font-size: 30px; font-size: 1.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
right: 17.5px; right: var(--chat-input-padding);
bottom: 17.5px; top: calc((var(--chat-input-size) * -1) - 6px);
cursor: default; cursor: default;
opacity: 0; opacity: 0;
z-index: 2; z-index: 2;
transition: var(--btn-corner-transition), opacity .2s !important; transition: transform var(--layer-transition), opacity var(--layer-transition) !important;
overflow: hidden; overflow: visible;
--translateY: calc(var(--chat-input-size) + 10px);
@include respond-to(handhelds) { //--translateY: calc(100% + 10px);
right: .5rem;
width: 2.875rem;
height: 2.875rem;
}
&:before {
margin-left: .75px;
}
@include respond-to(medium-screens) {
transition: transform var(--layer-transition), opacity .2s !important;
body.is-right-column-shown & {
transform: translate3d(calc(var(--right-column-width) * -.5), var(--translateY), 0);
}
}
/* &.is-broadcast { /* &.is-broadcast {
--translateY: 99px !important; --translateY: 99px !important;
} */ } */
.badge {
position: absolute;
top: -.25rem;
right: -.25rem;
@include respond-to(handhelds) {
top: -.75rem;
right: .1875rem;
}
}
} }
.popup { .popup {

View File

@ -146,6 +146,12 @@ $bubble-margin: .25rem;
display: block; display: block;
} }
&.is-multiple-documents {
&:before, &:after {
display: none;
}
}
&.is-date { &.is-date {
position: sticky; position: sticky;
top: $bubble-margin; top: $bubble-margin;
@ -188,46 +194,26 @@ $bubble-margin: .25rem;
} }
&-select-checkbox { &-select-checkbox {
z-index: 2; z-index: 3;
position: absolute; position: absolute;
left: 0; left: 0;
//bottom: .75rem; // * by avatar
bottom: 5px; // * by middle of one-line message
/* left: 0;
top: 50%;
transform: translateY(-50%); */
display: flex; display: flex;
[type="checkbox"] { [type="checkbox"] {
/* &:not(:checked) + .checkbox-caption:after {
} */
&:checked + .checkbox-caption { &:checked + .checkbox-caption {
&:after { &:after {
background-color: #61c642; background-color: #61c642;
} }
} }
} }
.checkbox-caption {
padding: 0;
width: 25px;
&:before {
top: 7px !important;
left: 3px !important;
width: 6px !important;
height: 11px !important;
} }
&:after { & > &-select-checkbox {
box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, .4); //bottom: .75rem; // * by avatar
border: 2px solid #fff !important; bottom: 5px; // * by middle of one-line message
width: 22px; /* left: 0;
height: 22px; top: 50%;
} transform: translateY(-50%); */
}
} }
.bubbles.is-selecting &:not(.is-album) { .bubbles.is-selecting &:not(.is-album) {
@ -267,12 +253,11 @@ $bubble-margin: .25rem;
> .user-avatar { > .user-avatar {
position: absolute; position: absolute;
left: -45px; margin-left: -45px;
//left: -3rem; # DO JS3 //left: -3rem; # DO JS3
width: 40px; width: 40px;
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
bottom: 0;
font-size: 1rem; font-size: 1rem;
cursor: pointer; cursor: pointer;
@ -346,12 +331,12 @@ $bubble-margin: .25rem;
&.is-group-last { &.is-group-last {
padding-bottom: $bubble-margin; padding-bottom: $bubble-margin;
.bubble-select-checkbox { > .bubble-select-checkbox {
bottom: 8px; bottom: 8px;
} }
.bubbles-inner.is-chat &.is-in { .bubbles-inner.is-chat &.is-in {
.bubble-select-checkbox { > .bubble-select-checkbox {
bottom: 7px; bottom: 7px;
} }
} }
@ -359,7 +344,7 @@ $bubble-margin: .25rem;
&:not(.forwarded) { &:not(.forwarded) {
&:not(.is-group-first) { &:not(.is-group-first) {
.bubble__container > .name { .bubble__container > .name, .document-wrapper > .name {
display: none; display: none;
} }
@ -635,7 +620,7 @@ $bubble-margin: .25rem;
} }
.album-item { .album-item {
background-color: lighten($color-blue, 35%); background-color: rgba(0, 0, 0, .06);
max-width: 100%; max-width: 100%;
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
@ -656,33 +641,33 @@ $bubble-margin: .25rem;
.bubble-select-checkbox { .bubble-select-checkbox {
bottom: auto !important; bottom: auto !important;
left: auto; left: auto;
right: .25rem; right: .5rem;
top: .25rem; top: .5rem;
} }
&.is-selected { &.is-selected {
border-radius: 0; //border-radius: 0;
.album-item-media { .album-item-media {
transform: scale(1); transform: scale(1);
} }
&.animating { &.animating {
transition: border-radius var(--layer-transition); //transition: border-radius var(--layer-transition);
.album-item-media { .album-item-media {
transition: transform var(--layer-transition), border-radius var(--layer-transition); transition: transform var(--layer-transition)/* , border-radius var(--layer-transition) */;
} }
} }
&:not(.backwards) { &:not(.backwards) {
.album-item-media { .album-item-media {
transform: scale(.925); transform: scale(.883333);
} }
&, .album-item-media { /* &, .album-item-media {
border-radius: .5rem; border-radius: .5rem;
} } */
} }
} }
} }
@ -867,8 +852,8 @@ $bubble-margin: .25rem;
.message { .message {
font-size: 16px; font-size: 16px;
//padding: 0 .6rem .2675rem .6rem; //padding: 0 9px .2675rem 9px;
padding: 0 .6rem 6px .6rem; padding: 0 9px 6px 9px;
/* overflow: hidden; /* overflow: hidden;
text-overflow: ellipsis; */ text-overflow: ellipsis; */
max-width: 100%; max-width: 100%;
@ -1050,6 +1035,178 @@ $bubble-margin: .25rem;
} }
} }
.document-wrapper {
display: flex;
flex-direction: column-reverse;
.document-message {
margin-top: .25rem;
}
}
&.is-multiple-documents {
/* .bubble__container {
position: unset;
} */
.message {
padding: 0 !important;
border-radius: inherit;
}
.document-container {
position: relative;
border-radius: inherit;
.document-selection {
position: absolute;
top: 0;
bottom: 0;
z-index: -1;
width: 200vw;
left: -75vw;
}
&.is-selected {
.document-selection {
background-color: rgba(77, 142, 80, .4);
animation: fade-in-opacity .2s linear forwards;
}
&.backwards {
.document-selection, .document-wrapper:before {
animation: fade-in-backwards-opacity .2s linear forwards;
}
}
.document-wrapper {
&:before {
content: " ";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, .06);
animation: fade-in-opacity .2s linear forwards;
border-radius: inherit;
}
}
}
.bubble-select-checkbox {
left: 2rem;
top: 2rem;
width: 1.5rem;
height: 1.5rem;
background: #fff;
border-radius: 50%;
&:before {
content: " ";
position: absolute;
width: 1.25rem;
height: 1.25rem;
left: .125rem;
top: .125rem;
border: 2px solid #c4c9cc;
border-radius: inherit;
}
/* [type="checkbox"]:not(:checked) + .checkbox-caption {
&:after {
width: 1.25rem;
height: 1.25rem;
left: .125rem;
top: .125rem;
border-color: #c4c9cc;
}
} */
.checkbox-caption {
&:after {
box-shadow: none;
}
}
}
&:first-of-type {
.document-selection {
top: -2px; // * padding inner + half padding outer
}
.document-wrapper {
padding-top: .5rem;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
}
&:last-of-type {
.document-selection {
bottom: -2px;
}
.document-wrapper {
padding-bottom: .5rem;
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
}
}
}
/* &.is-group-first .document-container {
&:first-of-type {
.document-selection {
top: -4px;
}
}
} */
&.is-group-last .document-container {
&:last-of-type {
.document-selection {
bottom: -6px;
}
}
}
.document {
height: 54px !important;
}
.document-wrapper {
background-color: #fff;
padding: .25rem .5rem;
> .name {
padding: 0 0 .25rem 0;
margin-top: -.25rem;
}
}
// * if have name
/* .bubbles-inner.is-chat &.is-in.is-group-first {
.document-container:first-of-type {
.document-selection {
top: -30px;
}
.document-wrapper {
border-top-left-radius: 0;
border-top-right-radius: 0;
&:before {
top: -26px;
border-top-left-radius: $border-radius-big;
border-top-right-radius: $border-radius-big;
}
}
}
} */
}
.message { .message {
&.document-message, &.audio-message, &.voice-message, &.poll-message, &.contact-message { &.document-message, &.audio-message, &.voice-message, &.poll-message, &.contact-message {
.time { .time {
@ -1102,6 +1259,7 @@ $bubble-margin: .25rem;
vertical-align: middle; vertical-align: middle;
pointer-events: none; // do not show title pointer-events: none; // do not show title
display: inline-flex; display: inline-flex;
z-index: 1;
/* display: inline-flex; /* display: inline-flex;
align-items: center; */ align-items: center; */
@ -1183,10 +1341,10 @@ $bubble-margin: .25rem;
user-select: none; user-select: none;
} }
&__container > .name { &__container > .name, .document-wrapper > .name {
/* padding: .2675rem .6rem 0 .6rem; */ /* padding: .2675rem 9px 0 9px; */
/* padding: .32rem .6rem 0 .6rem; */ /* padding: .32rem 9px 0 9px; */
padding: 5px .6rem 0 .6rem; padding: 5px 9px 0 9px;
font-weight: 500 !important; font-weight: 500 !important;
/* padding-bottom: 4px; */ /* padding-bottom: 4px; */
color: $color-blue; color: $color-blue;
@ -1235,13 +1393,14 @@ $bubble-margin: .25rem;
&:not(.sticker):not(.emoji-big):not(.round).is-group-last .bubble__container:after { &:not(.sticker):not(.emoji-big):not(.round).is-group-last .bubble__container:after {
position: absolute; position: absolute;
bottom: 0; //bottom: 0;
width: 11px; width: 11px;
height: 20px; height: 20px;
background-repeat: no-repeat no-repeat; background-repeat: no-repeat no-repeat;
content: ''; content: '';
background-size: 11px 20px; background-size: 11px 20px;
background-position-y: 1px; background-position-y: 1px;
z-index: -2;
} }
&.photo, &.video { &.photo, &.video {
@ -1264,6 +1423,7 @@ $bubble-margin: .25rem;
&__media-container { &__media-container {
cursor: pointer; cursor: pointer;
border-radius: inherit;
} }
audio-element, poll-element { audio-element, poll-element {
@ -1335,7 +1495,7 @@ $bubble-margin: .25rem;
} }
.bubble__container:after { .bubble__container:after {
left: -8.4px; margin-left: -8.4px;
background-image: url('assets/img/msg-tail-left.svg'); background-image: url('assets/img/msg-tail-left.svg');
} }
} }
@ -1536,10 +1696,6 @@ $bubble-margin: .25rem;
color: $darkgreen; color: $darkgreen;
} }
.album-item {
background-color: darken(#eeffde, 10%) !important;
}
.time { .time {
padding-right: 4px; padding-right: 4px;
margin-left: -4px; margin-left: -4px;
@ -1697,6 +1853,34 @@ $bubble-margin: .25rem;
right: auto; right: auto;
left: -46px; left: -46px;
//transform: scaleX(-1); //transform: scaleX(-1);
&.goto-original {
transform: rotate(180deg);
}
}
&.is-multiple-documents {
.document-container {
.bubble-select-checkbox {
background-color: #eeffde;
&:before {
border-color: #9ed695;
}
.checkbox-caption:after {
border-color: #eeffde;
}
}
/* &:after {
left: -50vw;
} */
}
.document-wrapper {
background-color: #eeffde;
}
} }
} }

View File

@ -336,6 +336,30 @@
} */ } */
} }
} }
&.hide ~ .tgico-pinlist, &:not(.is-many) ~ .tgico-pinlist {
display: none;
}
&.is-many {
&:not(.is-floating) {
.pinned-message-pinlist {
display: none;
}
}
&.is-floating {
.pinned-message-close {
display: none;
}
}
}
&:not(.is-many) {
.pinned-message-pinlist {
display: none;
}
}
} }
.pinned-audio { .pinned-audio {

View File

@ -12,28 +12,44 @@
} }
.checkbox-field-round { .checkbox-field-round {
display: block; .checkbox-caption {
text-align: left; min-width: 1.5rem;
min-height: 1.5rem;
[type="checkbox"] {
&:checked + .checkbox-caption {
&:before { &:before {
top: 5px; color: #fff;
left: 0px; font-size: 16px;
line-height: 24px;
margin-left: 4px;
transition: opacity .2s ease-in-out;
opacity: 0;
position: absolute;
font-weight: bold;
z-index: 1;
} }
&:after { &:after {
background-color: #4EA4F6; content: '';
border: none; position: absolute;
} border-radius: 50%;
height: 1.5rem;
width: 1.5rem;
box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, .4);
border: 2px solid #fff;
//left: 0;
background-color: transparent;
transition: background-color .2s ease-in-out;
} }
} }
.checkbox-caption:after { [type="checkbox"]:checked + .checkbox-caption {
border-radius: 50%; &:before {
height: 20px; opacity: 1;
width: 20px; }
border-color: #dadbdc;
&:after {
background-color: $button-primary-background;
}
} }
} }
@ -122,7 +138,7 @@
position: absolute; position: absolute;
} }
[type="checkbox"] { .checkbox-field [type="checkbox"] {
& + span { & + span {
position: relative; position: relative;
padding-left: 3.5rem; padding-left: 3.5rem;

View File

@ -86,7 +86,7 @@
.tgico-mention:before { .tgico-mention:before {
content: "\e910"; content: "\e910";
} }
.tgico-down:before { .tgico-arrow-down:before {
content: "\e911"; content: "\e911";
} }
.tgico-pinlist:before { .tgico-pinlist:before {
@ -260,7 +260,7 @@
.tgico-eye1:before { .tgico-eye1:before {
content: "\e94a"; content: "\e94a";
} }
.tgico-FullScreen:before { .tgico-fullscreen:before {
content: "\e94b"; content: "\e94b";
} }
.tgico-smallscreen:before { .tgico-smallscreen:before {

View File

@ -30,7 +30,7 @@ $tgico-comments: "\e90d";
$tgico-previous: "\e90e"; $tgico-previous: "\e90e";
$tgico-next: "\e90f"; $tgico-next: "\e90f";
$tgico-mention: "\e910"; $tgico-mention: "\e910";
$tgico-down: "\e911"; $tgico-arrow-down: "\e911";
$tgico-pinlist: "\e912"; $tgico-pinlist: "\e912";
$tgico-replace: "\e913"; $tgico-replace: "\e913";
$tgico-schedule: "\e914"; $tgico-schedule: "\e914";

View File

@ -86,21 +86,8 @@
} }
} }
.unread-count { .badge {
margin-left: 5px; margin-left: 5px;
background: #50a2e9;
height: 1.25rem;
border-radius: .75rem;
font-weight: 500;
color: white;
line-height: 1.25rem;
min-width: 1.25rem;
font-size: .9rem; // ! this will fix vertical center
padding: 0 5.75px;
&:empty {
display: none;
}
} }
&:not(.hide) + .scrollable { &:not(.hide) + .scrollable {
@ -654,6 +641,11 @@
} }
} }
} }
.checkbox-field {
margin: 0;
padding: 0;
}
} }
.folder-category-button { .folder-category-button {

View File

@ -84,6 +84,11 @@
position: relative; position: relative;
width: 100%; width: 100%;
.checkbox-field {
margin: 0;
padding: 0;
}
[type="checkbox"] + span { [type="checkbox"] + span {
padding-left: 54px; padding-left: 54px;
margin-left: -54px; margin-left: -54px;

View File

@ -3,7 +3,7 @@
user-select: none; user-select: none;
} }
.rp-overflow, .btn-menu-toggle.rp, .menu-horizontal li.rp/* , html.is-safari .c-ripple */ { .rp-overflow, .btn-menu-toggle.rp, .menu-horizontal li.rp, .btn-corner.rp/* , html.is-safari .c-ripple */ {
.c-ripple { .c-ripple {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -164,4 +164,9 @@
margin-top: 11px; margin-top: 11px;
padding-left: 11px; padding-left: 11px;
} }
.checkbox-field {
margin: 0;
padding: 0;
}
} }

View File

@ -26,6 +26,11 @@ $large-screen: 1680px;
$floating-left-sidebar: 925px; $floating-left-sidebar: 925px;
$messages-container-width: 728px; $messages-container-width: 728px;
$chat-input-size: 3.375rem;
$chat-input-handhelds-size: 2.875rem;
$chat-padding: 1rem;
$chat-padding-handhelds: .5rem;
@mixin respond-to($media) { @mixin respond-to($media) {
@if $media == handhelds { @if $media == handhelds {
@media only screen and (max-width: $small-screen) { @content; } @media only screen and (max-width: $small-screen) { @content; }
@ -81,7 +86,7 @@ $messages-container-width: 728px;
--layer-transition: .2s ease-in-out; --layer-transition: .2s ease-in-out;
//--layer-transition: .3s cubic-bezier(.33, 1, .68, 1); //--layer-transition: .3s cubic-bezier(.33, 1, .68, 1);
//--layer-transition: none; //--layer-transition: none;
--btn-corner-transition: transform .2s cubic-bezier(.34, 1.56, .64, 1); --btn-corner-transition: .2s cubic-bezier(.34, 1.56, .64, 1);
--message-handhelds-margin: 5.5625rem; --message-handhelds-margin: 5.5625rem;
--message-beside-button-margin: 2.875rem; --message-beside-button-margin: 2.875rem;
--message-time-background: rgba(0, 0, 0, .35); --message-time-background: rgba(0, 0, 0, .35);
@ -91,10 +96,16 @@ $messages-container-width: 728px;
@include respond-to(handhelds) { @include respond-to(handhelds) {
--right-column-width: 100vw; --right-column-width: 100vw;
--esg-sticker-size: 68px; --esg-sticker-size: 68px;
--chat-input-size: #{$chat-input-handhelds-size};
--chat-input-padding: #{$chat-padding-handhelds};
} }
@include respond-to(not-handhelds) { @include respond-to(not-handhelds) {
--right-column-width: calc(#{$large-screen} / 4); --right-column-width: calc(#{$large-screen} / 4);
--chat-input-size: #{$chat-input-size};
--chat-input-padding: #{$chat-padding};
} }
@include respond-to(only-medium-screens) { @include respond-to(only-medium-screens) {
@ -1048,3 +1059,41 @@ middle-ellipsis-element {
height: 100%; height: 100%;
} }
} }
.badge {
border-radius: .75rem;
font-weight: 500;
color: white;
font-size: .9rem; // ! this will fix vertical center
transition: background-color .2s ease-in-out;
&:empty {
display: none;
}
&-20 {
height: 1.25rem;
min-width: 1.25rem;
line-height: 1.25rem !important;
padding: 0 5.75px;
}
&-24 {
height: 1.5rem;
min-width: 1.5rem;
line-height: 1.5rem !important;
padding: 0 7.75px;
}
&-green {
background-color: $color-green;
}
&-blue {
background-color: $color-blue;
}
&-gray {
background-color: #c5c9cc;
}
}