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 appChatsManager from "../lib/appManagers/appChatsManager";
import SearchInput from "./searchInput";
import { Peer } from "../layer";
import rootScope from "../lib/rootScope";
import { escapeRegExp } from "../helpers/string";
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;
if(this.searchInput.value != query) {

View File

@ -320,7 +320,7 @@ export default class AppSelectPeers {
if(this.multiSelect) {
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');
}

View File

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

View File

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

View File

@ -14,7 +14,7 @@ export default class ChatAudio extends PinnedContainer {
private toggleEl: HTMLElement;
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.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 { AppSidebarRight } from "../sidebarRight";
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 { AppDocsManager } from "../../lib/appManagers/appDocsManager";
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 { isTouchSupported } from "../../helpers/touchSupport";
import { logger, LogLevels } from "../../lib/logger";
import { logger } from "../../lib/logger";
import rootScope from "../../lib/rootScope";
import AppMediaViewer from "../appMediaViewer";
import BubbleGroups from "../bubbleGroups";
import Button from "../button";
import PopupDatePicker from "../popupDatepicker";
import PopupForward from "../popupForward";
import PopupStickers from "../popupStickers";
import ProgressivePreloader from "../preloader";
import Scrollable from "../scrollable";
import StickyIntersector from "../stickyIntersector";
import ChatContextMenu from "./contextMenu";
import ChatSelection from "./selection";
import animationIntersector from "../animationIntersector";
import { months } from "../../helpers/date";
import RichTextProcessor from "../../lib/richtextprocessor";
@ -50,8 +47,6 @@ let TEST_SCROLL = TEST_SCROLL_TIMES;
export default class ChatBubbles {
bubblesContainer: HTMLDivElement;
chatInner: HTMLDivElement;
goDownBtn: HTMLButtonElement;
scrollable: Scrollable;
scroll: HTMLElement;
@ -59,6 +54,7 @@ export default class ChatBubbles {
private getHistoryBottomPromise: Promise<boolean>;
public peerID = 0;
//public messagesCount: number = -1;
public unreadOut = new Set<number>();
public needUpdate: {replyMid: number, mid: number}[] = []; // if need wrapSingleMessage
@ -104,6 +100,8 @@ export default class ChatBubbles {
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) {
this.chat.log.error('Bubbles construction');
@ -115,9 +113,7 @@ export default class ChatBubbles {
this.chatInner = document.createElement('div');
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.goDownBtn);
this.bubblesContainer.append(this.chatInner);
this.setScroll();
@ -209,7 +205,7 @@ export default class ChatBubbles {
// set new mids to album items for mediaViewer
if(message.grouped_id) {
const items = bubble.querySelectorAll('.album-item');
const items = bubble.querySelectorAll('.grouped-item');
const groupIDs = getObjectKeysAndSort(appMessagesManager.groupedMessagesStorage[message.grouped_id]);
(Array.from(items) as HTMLElement[]).forEach((item, idx) => {
item.dataset.mid = '' + groupIDs[idx];
@ -315,10 +311,25 @@ export default class ChatBubbles {
const info = e.detail;
const dialog = appMessagesManager.getDialogByPeerID(info.peerID)[0];
if(dialog) {
if(dialog.peerID == this.peerID) {
this.updateUnreadByDialog(dialog);
}
if(dialog?.peerID == this.peerID) {
this.chat.input.setUnreadCount();
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.chat.selection.toggleByBubble(findUpClassName(target, 'album-item') || bubble);
this.chat.selection.toggleByBubble(findUpClassName(target, 'grouped-item') || bubble);
return;
}
@ -494,6 +505,7 @@ export default class ChatBubbles {
} catch(err) {}
if(isReplyClick && bubble.classList.contains('is-reply')/* || bubble.classList.contains('forwarded') */) {
this.replyFollowHistory.push(+bubble.dataset.mid);
let originalMessageID = +bubble.getAttribute('data-original-mid');
this.chat.setPeer(this.peerID, originalMessageID);
}
@ -507,18 +519,6 @@ export default class ChatBubbles {
//console.log('chatInner click', e);
}, {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) => {
for(const timestamp in this.dateMessages) {
@ -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];
for(const mid in group) {
if(this.bubbles[mid]) {
@ -588,18 +624,22 @@ export default class ChatBubbles {
return null;
}
public getBubbleAlbumItems(bubble: HTMLElement) {
return Array.from(bubble.querySelectorAll('.album-item')) as HTMLElement[];
public getBubbleGroupedItems(bubble: HTMLElement) {
return Array.from(bubble.querySelectorAll('.grouped-item')) as HTMLElement[];
}
public getMountedBubble(mid: number) {
const message = this.appMessagesManager.getMessage(mid);
const bubble = this.bubbles[mid];
if(!bubble && message.grouped_id) {
const a = this.getAlbumBubble(message.grouped_id);
if(a) return a;
if(message.grouped_id) {
const a = this.getGroupedBubble(message.grouped_id);
if(a) {
a.bubble = a.bubble.querySelector(`.document-container[data-mid="${mid}"]`) || a.bubble;
return a;
}
}
const bubble = this.bubbles[mid];
if(!bubble) return;
return {bubble, message};
@ -634,7 +674,7 @@ export default class ChatBubbles {
let dialog = this.appMessagesManager.getDialogByPeerID(this.peerID)[0];
// 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);
/* false && */this.getHistory(history[history.length - 1], false, true, undefined, justLoad);
}
@ -666,11 +706,13 @@ export default class ChatBubbles {
this.scrolledDown = false;
}
this.chat.topbar.pinnedMessage.setCorrectIndex(this.scrollable.lastScrollDirection);
if(this.chat.topbar.pinnedMessage) {
this.chat.topbar.pinnedMessage.setCorrectIndex(this.scrollable.lastScrollDirection);
}
};
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 = () => {
//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.scroll = this.scrollable.container;
this.bubblesContainer/* .firstElementChild */.append(this.goDownBtn);
this.scrollable.onAdditionalScroll = this.onScroll;
this.scrollable.onScrolledTop = () => this.loadMoreHistory(true);
this.scrollable.onScrolledBottom = () => this.loadMoreHistory(false);
@ -923,7 +963,6 @@ export default class ChatBubbles {
//console.time('appImManager setPeer pre promise');
////console.time('appImManager: pre render start');
if(peerID == 0) {
this.goDownBtn.classList.add('hide');
this.cleanup(true);
this.peerID = 0;
return null;
@ -932,14 +971,9 @@ export default class ChatBubbles {
const samePeer = this.peerID == peerID;
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;
// @ts-ignore
/* if(topMessage && dialog && dialog.top_message == topMessage && dialog.refetchTopMessage) {
// @ts-ignore
dialog.refetchTopMessage = false;
topMessage += 1;
} */
if(!isTarget && dialog) {
if(dialog.unread_count && !samePeer) {
lastMsgID = dialog.read_inbox_max_id;
@ -948,6 +982,8 @@ export default class ChatBubbles {
//lastMsgID = topMessage;
}
}
const isJump = lastMsgID != topMessage;
if(samePeer) {
const mounted = this.getMountedBubble(lastMsgID);
@ -955,20 +991,22 @@ export default class ChatBubbles {
if(isTarget) {
this.scrollable.scrollIntoView(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.scroll.scrollTop = this.scroll.scrollHeight;
this.chat.setListenerResult('setPeer', lastMsgID, true);
}
return null;
}
} else {
this.peerID = peerID;
this.replyFollowHistory.length = 0;
}
this.log('setPeer peerID:', this.peerID, dialog, lastMsgID, topMessage);
const isJump = lastMsgID != topMessage;
// add last message, bc in getHistory will load < max_id
const additionMsgID = isJump ? 0 : topMessage;
@ -978,7 +1016,21 @@ export default class ChatBubbles {
//////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;
this.cleanup();
@ -999,6 +1051,10 @@ export default class ChatBubbles {
this.scrollable.container.innerHTML = '';
//oldChatInner.remove();
if(!samePeer) {
this.chat.finishPeerChange(isTarget, isJump, lastMsgID);
}
this.preloader.attach(this.bubblesContainer);
}
@ -1009,6 +1065,10 @@ export default class ChatBubbles {
////this.log('setPeer removing preloader');
if(cached) {
if(!samePeer) {
this.chat.finishPeerChange(isTarget, isJump, lastMsgID); // * костыль
}
this.scrollable.container.innerHTML = '';
//oldChatInner.remove();
} else {
@ -1024,7 +1084,7 @@ export default class ChatBubbles {
this.lazyLoadQueue.unlock();
//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) {
clearTimeout(this.scrollable.scrollLocked);
this.scrollable.scrollLocked = 0;
@ -1034,9 +1094,12 @@ export default class ChatBubbles {
const forwardingUnread = dialog.read_inbox_max_id == lastMsgID && !isTarget;
if(!fromUp && (samePeer || forwardingUnread)) {
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) {
bubble = this.findNextMountedBubbleByMsgID(lastMsgID);
}
@ -1049,6 +1112,8 @@ export default class ChatBubbles {
this.scrollable.scrollTop = this.scrollable.scrollHeight;
}
this.chat.setListenerResult('setPeer', lastMsgID, !isJump);
// warning
if(!lastMsgID || this.bubbles[topMessage] || lastMsgID == topMessage) {
this.scrolledAllDown = true;
@ -1084,7 +1149,6 @@ export default class ChatBubbles {
const isAnyGroup = this.appPeersManager.isAnyGroup(peerID);
const isChannel = this.appPeersManager.isChannel(peerID);
const isBroadcast = this.appPeersManager.isBroadcast(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-channel', isChannel);
this.goDownBtn.classList.toggle('is-broadcast', isBroadcast);
window.requestAnimationFrame(() => {
this.goDownBtn.classList.remove('hide');
});
}
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) {
this.log.debug('message to render:', message);
//return;
const albumMustBeRenderedFull = this.chat.type == 'chat';
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 maxID = Math.max(...Object.keys(storage).map(i => +i));
if(message.mid < maxID) {
@ -1215,7 +1275,7 @@ export default class ChatBubbles {
}
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');
messageDiv.classList.add('message');
@ -1233,7 +1293,7 @@ export default class ChatBubbles {
bubble.classList.add('bubble');
bubble.appendChild(bubbleContainer);
if(!our) {
if(!our && !message.pFlags.out) {
//this.log('not our message', message, message.pFlags.unread);
if(message.pFlags.unread) {
this.unreadedObserver.observe(bubble);
@ -1300,7 +1360,9 @@ export default class ChatBubbles {
let messageMedia = message.media;
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);
messageMessage = t.message;
totalEntities = t.totalEntities;
@ -1436,6 +1498,7 @@ export default class ChatBubbles {
}
const isOut = our && (!message.fwd_from || this.peerID != rootScope.myID);
let nameContainer = bubbleContainer;
// media
if(messageMedia/* && messageMedia._ == 'messageMediaPhoto' */) {
@ -1457,7 +1520,7 @@ export default class ChatBubbles {
case '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({
groupID: '' + message.id,
attachmentDiv,
@ -1548,8 +1611,8 @@ export default class ChatBubbles {
const tailSupported = !isAndroid;
const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id];
if(message.grouped_id && Object.keys(storage).length != 1) {
bubble.classList.add('is-album');
if(message.grouped_id && Object.keys(storage).length != 1 && albumMustBeRenderedFull) {
bubble.classList.add('is-album', 'is-grouped');
wrapAlbum({
groupID: message.grouped_id,
attachmentDiv,
@ -1713,8 +1776,8 @@ export default class ChatBubbles {
bubble.classList.add('hide-name', doc.type == 'round' ? 'round' : 'video');
const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id];
if(message.grouped_id && Object.keys(storage).length != 1) {
bubble.classList.add('is-album');
if(message.grouped_id && Object.keys(storage).length != 1 && albumMustBeRenderedFull) {
bubble.classList.add('is-album', 'is-grouped');
wrapAlbum({
groupID: message.grouped_id,
@ -1742,10 +1805,55 @@ export default class ChatBubbles {
break;
} 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');
messageDiv.append(docDiv);
messageDiv.classList.add((doc.type != 'photo' ? doc.type || 'document' : 'document') + '-message');
processingWebPage = true;
@ -1818,6 +1926,8 @@ export default class ChatBubbles {
}
} */
}
let savedFrom = '';
if((this.peerID < 0 && !our) || message.fwd_from || message.reply_to_mid) { // chat
let title = this.appPeersManager.getPeerTitle(message.fwdFromID || message.fromID);
@ -1840,11 +1950,7 @@ export default class ChatBubbles {
}
if(message.savedFrom) {
let goto = document.createElement('div');
goto.classList.add('bubble-beside-button', 'goto-original', 'tgico-next');
bubbleContainer.append(goto);
bubble.dataset.savedFrom = message.savedFrom;
bubble.classList.add('with-beside-button');
savedFrom = message.savedFrom;
}
if(!bubble.classList.contains('sticker')) {
@ -1861,7 +1967,7 @@ export default class ChatBubbles {
nameDiv.innerHTML = 'Forwarded from ' + title;
}
bubbleContainer.append(nameDiv);
nameContainer.append(nameDiv);
}
} else {
if(message.reply_to_mid) {
@ -1895,7 +2001,7 @@ export default class ChatBubbles {
nameDiv.innerHTML = title;
nameDiv.style.color = this.appPeersManager.getPeerColorByID(message.fromID, false);
nameDiv.dataset.peerID = message.fromID;
bubbleContainer.append(nameDiv);
nameContainer.append(nameDiv);
} else /* if(!message.reply_to_mid) */ {
bubble.classList.add('hide-name');
}
@ -1920,6 +2026,18 @@ export default class ChatBubbles {
} else {
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');
if(updatePosition) {
@ -1977,13 +2095,14 @@ export default class ChatBubbles {
const method = (reverse ? history.shift : history.pop).bind(history);
//const padding = 99999;
//const padding = 10000;
const realLength = this.scrollable.container.childElementCount;
let previousScrollHeightMinusTop: number/* , previousScrollHeight: number */;
if(realLength > 0 && (reverse || isSafari)) { // for safari need set when scrolling bottom too
this.messagesQueueOnRender = () => {
const {scrollTop, scrollHeight} = this.scrollable;
//previousScrollHeight = scrollHeight;
//previousScrollHeight = scrollHeight + padding;
previousScrollHeightMinusTop = reverse ? scrollHeight - scrollTop : scrollTop;
@ -2011,6 +2130,10 @@ export default class ChatBubbles {
/* const scrollHeight = this.scrollable.scrollHeight;
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';
//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
* @param maxID max message id
@ -2099,7 +2242,7 @@ export default class ChatBubbles {
}
let additionMsgIDs: number[];
if(additionMsgID) {
if(additionMsgID && !isBackLimit) {
const historyStorage = this.appMessagesManager.historiesStorage[peerID];
if(historyStorage && historyStorage.history.length < loadCount) {
additionMsgIDs = historyStorage.history.slice();
@ -2118,7 +2261,7 @@ export default class ChatBubbles {
/* const result = additionMsgID ?
{history: [additionMsgID]} :
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>;
//const isFirstMessageRender = !!additionMsgID && result instanceof Promise && !appMessagesManager.getMessage(additionMsgID).grouped_id;
@ -2251,10 +2394,19 @@ export default class ChatBubbles {
// preload more
//if(!isFirstMessageRender) {
setTimeout(() => {
this.loadMoreHistory(true, true);
this.loadMoreHistory(false, true);
}, 0);
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(() => {
if(reverse) {
this.loadMoreHistory(true, true);
} else {
this.loadMoreHistory(false, true);
}
}, 0);
}
}
//}
});

View File

@ -10,6 +10,7 @@ import type { AppProfileManager } from "../../lib/appManagers/appProfileManager"
import type { AppStickersManager } from "../../lib/appManagers/appStickersManager";
import type { AppUsersManager } from "../../lib/appManagers/appUsersManager";
import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager";
import EventListenerBase from "../../helpers/eventListenerBase";
import { logger, LogLevels } from "../../lib/logger";
import rootScope from "../../lib/rootScope";
import appSidebarRight, { AppSidebarRight } from "../sidebarRight";
@ -19,7 +20,11 @@ import ChatInput from "./input";
import ChatSelection from "./selection";
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 backgroundEl: HTMLElement;
@ -33,9 +38,13 @@ export default class Chat {
public setPeerPromise: Promise<void>;
public peerChanged: boolean;
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) {
super();
this.container = document.createElement('div');
this.container.classList.add('chat');
@ -52,12 +61,25 @@ export default class Chat {
}
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.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.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);
}
@ -126,24 +148,11 @@ export default class Chat {
return;
}
const {cached, promise} = result;
const {promise} = result;
// clear
if(!cached) {
if(!samePeer) {
this.finishPeerChange();
}
}
//console.timeEnd('appImManager setPeer pre promise');
this.setPeerPromise = promise.then(() => {
if(cached) {
if(!samePeer) {
this.finishPeerChange();
}
}
}).finally(() => {
this.setPeerPromise = promise.finally(() => {
if(this.peerID == peerID) {
this.setPeerPromise = null;
}
@ -155,18 +164,19 @@ export default class Chat {
return this.setPeerPromise;
}
public finishPeerChange() {
public finishPeerChange(isTarget: boolean, isJump: boolean, lastMsgID: number) {
if(this.peerChanged) return;
let peerID = this.peerID;
this.peerChanged = true;
this.topbar.setPeer(peerID);
this.topbar.finishPeerChange(isTarget, isJump, lastMsgID);
this.bubbles.finishPeerChange();
this.input.finishPeerChange();
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 Chat from "./chat";
import { isTouchSupported } from "../../helpers/touchSupport";
import rootScope from "../../lib/rootScope";
import { attachClickEvent, cancelEvent, cancelSelection, findUpClassName } from "../../helpers/dom";
import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu";
import { attachContextMenuListener, openBtnMenu, positionMenu } from "../misc";
@ -18,7 +17,7 @@ export default class ChatContextMenu {
private element: HTMLElement;
private target: HTMLElement;
private isTargetAnAlbumItem: boolean;
private isTargetAGroupedItem: boolean;
public peerID: number;
public msgID: number;
@ -63,10 +62,10 @@ export default class ChatContextMenu {
//this.msgID = msgID;
this.target = e.target as HTMLElement;
const albumItem = findUpClassName(this.target, 'album-item');
this.isTargetAnAlbumItem = !!albumItem;
if(albumItem) {
this.msgID = +albumItem.dataset.mid;
const groupedItem = findUpClassName(this.target, 'grouped-item');
this.isTargetAGroupedItem = !!groupedItem;
if(groupedItem) {
this.msgID = +groupedItem.dataset.mid;
} else {
this.msgID = mid;
}
@ -125,7 +124,7 @@ export default class ChatContextMenu {
cancelSelection();
//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) {
chat.selection.toggleByBubble(bubble);
}
@ -138,13 +137,13 @@ export default class ChatContextMenu {
icon: 'reply',
text: 'Reply',
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 */
}, {
icon: 'edit',
text: 'Edit',
onClick: this.onEditClick,
verify: () => this.appMessagesManager.canEditMessage(this.msgID, 'text')
verify: () => this.appMessagesManager.canEditMessage(this.msgID, 'text') && !!this.chat.input.messageInput
}, {
icon: 'copy',
text: 'Copy',
@ -163,15 +162,16 @@ export default class ChatContextMenu {
onClick: this.onPinClick,
verify: () => {
const message = this.appMessagesManager.getMessage(this.msgID);
// for new layer
// 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')));
return this.msgID > 0 && message._ != 'messageService' && !message.pFlags.pinned && this.appPeersManager.canPinMessage(this.peerID);
}
}, {
icon: 'unpin',
text: 'Unpin',
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',
text: 'Revote',
@ -284,12 +284,12 @@ export default class ChatContextMenu {
if(this.chat.selection.isSelecting) {
this.chat.selection.selectionForwardBtn.click();
} 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 = () => {
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 = () => {
@ -300,7 +300,7 @@ export default class ChatContextMenu {
if(this.chat.selection.isSelecting) {
this.chat.selection.selectionDeleteBtn.click();
} 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 InputField from '../inputField';
import { MessageEntity } from '../../layer';
import MarkupTooltip from './markupTooltip';
import StickersHelper from './stickersHelper';
import ButtonIcon from '../buttonIcon';
import DivAndCaption from '../divAndCaption';
import ButtonMenuToggle from '../buttonMenuToggle';
import ListenerSetter from '../../helpers/listenerSetter';
import Button from '../button';
const RECORD_MIN_TIME = 500;
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 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) {
this.listenerSetter = new ListenerSetter();
}
public construct() {
this.chatInput = document.createElement('div');
this.chatInput.classList.add('chat-input');
this.chatInput.style.display = 'none';
this.inputContainer = document.createElement('div');
this.inputContainer.classList.add('chat-input-container');
@ -106,6 +114,24 @@ export default class ChatInput {
this.rowsWrapper = document.createElement('div');
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.classList.add('reply-wrapper');
@ -137,7 +163,7 @@ export default class ChatInput {
this.willAttachType = 'media';
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',
text: 'Document',
@ -147,14 +173,14 @@ export default class ChatInput {
this.willAttachType = 'document';
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',
text: 'Poll',
onClick: () => {
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);
@ -189,12 +215,8 @@ export default class ChatInput {
this.btnSendContainer.append(this.recordRippleEl, this.btnSend);
this.inputContainer.append(this.rowsWrapper, this.btnCancelRecord, this.btnSendContainer);
this.chatInput.append(this.inputContainer);
this.inputContainer.append(this.btnCancelRecord, this.btnSendContainer);
// * constructor end
const toggleClass = isTouchSupported ? 'flip-icon' : 'active';
emoticonsDropdown.attachButtonListener(this.btnToggleEmoticons);
emoticonsDropdown.events.onOpen.push(this.onEmoticonsOpen);
emoticonsDropdown.events.onClose.push(this.onEmoticonsClose);
@ -272,7 +294,7 @@ export default class ChatInput {
let peerID = this.chat.peerID;
// тут objectURL ставится уже с audio/wav
appMessagesManager.sendFile(peerID, dataBlob, {
this.appMessagesManager.sendFile(peerID, dataBlob, {
isVoiceMessage: true,
isMedia: true,
duration,
@ -290,6 +312,34 @@ export default class ChatInput {
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 = () => {
const toggleClass = isTouchSupported ? 'flip-icon' : 'active';
this.btnToggleEmoticons.classList.toggle(toggleClass, true);
@ -300,6 +350,13 @@ export default class ChatInput {
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() {
this.chat.log.error('Input destroying');
@ -312,33 +369,54 @@ export default class ChatInput {
public cleanup() {
if(!this.chat.peerID) {
this.chatInput.style.display = 'none';
this.goDownBtn.classList.add('hide');
}
cancelSelection();
this.clearInput();
this.clearHelper();
if(this.messageInput) {
this.clearInput();
this.clearHelper();
}
}
public finishPeerChange() {
const peerID = this.chat.peerID;
const visible = this.attachMenuButtons.filter(button => {
const good = button.verify(peerID);
button.element.classList.toggle('hide', !good);
return good;
});
const canWrite = this.appMessagesManager.canWriteToPeer(peerID);
this.chatInput.style.display = '';
this.chatInput.classList.toggle('is-hidden', !canWrite);
if(!canWrite) {
this.messageInput.removeAttribute('contenteditable');
} else {
this.messageInput.setAttribute('contenteditable', 'true');
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 good = button.verify(peerID);
button.element.classList.toggle('hide', !good);
return good;
});
if(!canWrite) {
this.messageInput.removeAttribute('contenteditable');
} else {
this.messageInput.setAttribute('contenteditable', 'true');
}
this.attachMenu.toggleAttribute('disabled', !visible.length);
this.updateSendBtn();
} else if(this.pinnedControlBtn) {
this.pinnedControlBtn.append(this.appPeersManager.canPinMessage(this.chat.peerID) ? 'Unpin all messages' : 'Don\'t show pinned messages');
}
this.attachMenu.toggleAttribute('disabled', !visible.length);
this.updateSendBtn();
}
private attachMessageInputField() {

View File

@ -1,5 +1,6 @@
import { getFullDate } from "../../helpers/date";
import { formatNumber } from "../../helpers/number";
import appImManager from "../../lib/appManagers/appImManager";
import RichTextProcessor from "../../lib/richtextprocessor";
type Message = any;
@ -39,7 +40,7 @@ export namespace MessageRender {
time = '<i class="edited">edited</i> ' + time;
}
if(message.pFlags.pinned) {
if(appImManager.chat.type != 'pinned' && message.pFlags.pinned) {
bubble.classList.add('is-pinned');
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 DivAndCaption from "../divAndCaption";
import { ripple } from "../ripple";
import ListenerSetter from "../../helpers/listenerSetter";
const classNames: string[] = [];
const CLASSNAME_BASE = 'pinned-container';
@ -13,7 +14,7 @@ export default class PinnedContainer {
private close: 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;
this.divAndCaption.fill = (mid, title, subtitle) => {
this.divAndCaption.container.dataset.mid = '' + mid;
@ -39,7 +40,7 @@ export default class PinnedContainer {
divAndCaption.container.append(this.close, this.wrapper);
this.topbar.listenerSetter.add(this.close, 'click', (e) => {
this.listenerSetter.add(this.close, 'click', (e) => {
cancelEvent(e);
((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 { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type ChatTopbar from "./topbar";
@ -8,8 +7,10 @@ import PinnedContainer from "./pinnedContainer";
import PinnedMessageBorder from "./pinnedMessageBorder";
import ReplyContainer, { wrapReplyDivAndCaption } from "./replyContainer";
import rootScope from "../../lib/rootScope";
import { findUpClassName } from "../../helpers/dom";
import { cancelEvent, findUpClassName, getElementByPoint, handleScrollSideEvent } from "../../helpers/dom";
import Chat from "./chat";
import ListenerSetter from "../../helpers/listenerSetter";
import ButtonIcon from "../buttonIcon";
class AnimatedSuper {
static DURATION = 200;
@ -191,28 +192,54 @@ class AnimatedCounter {
}
export default class ChatPinnedMessage {
public static LOAD_COUNT = 50;
public static LOAD_OFFSET = 5;
public pinnedMessageContainer: PinnedContainer;
public pinnedMessageBorder: PinnedMessageBorder;
public pinnedIndex = 0;
public pinnedMaxMid = 0;
public pinnedMid = 0;
public pinnedIndex = -1;
public wasPinnedIndex = 0;
public locked = 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 animatedMedia: AnimatedSuper;
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) {
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)) {
new PopupPinMessage(this.topbar.peerID, 0);
new PopupPinMessage(this.topbar.peerID, this.pinnedMid, true);
return Promise.resolve(false);
} else {
return this.appMessagesManager.hidePinnedMessages(this.topbar.peerID).then(() => true);
}
});
this.pinnedMessageBorder = new PinnedMessageBorder();
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.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.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;
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 setCorrectIndex(lastScrollDirection?: number) {
if(this.locked || this.chat.setPeerPromise) {
return;
}/* else if(this.waitForScrollBottom) {
if(lastScrollDirection === 1) {
this.waitForScrollBottom = false;
} else {
return;
}
} */
public destroy() {
this.pinnedMessageContainer.divAndCaption.container.remove();
this.listenerSetter.removeAll();
this.unsetScrollDownListener(false);
}
///const perf = performance.now();
const rect = this.chat.bubbles.scrollable.container.getBoundingClientRect();
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;
//this.appImManager.log('[PM]: setCorrectIndex: get last element perf:', performance.now() - perf, el, x, y);
public setCorrectIndex(lastScrollDirection?: number) {
if(this.locked || this.hidden/* || this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise */) {
return;
}
if((this.loadedBottom || this.loadedTop) && !this.count) {
return;
}
//const perf = performance.now();
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;
el = findUpClassName(el, 'bubble');
if(!el) return;
if(el && el.dataset.mid !== undefined) {
const mid = +el.dataset.mid;
this.appMessagesManager.getPinnedMessages(this.topbar.peerID).then(mids => {
let currentIndex = mids.findIndex(_mid => _mid <= mid);
if(currentIndex === -1) {
currentIndex = mids.length ? mids.length - 1 : 0;
}
const mid = el.dataset.mid;
if(el && mid !== undefined) {
this.chat.log('[PM]: setCorrectIndex will test mid:', mid);
this.testMid(+mid, lastScrollDirection);
}
}
//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;
if(changed) {
if(this.waitForScrollBottom && lastScrollDirection !== undefined) {
if(this.pinnedIndex === 0 || this.pinnedIndex > currentIndex) { // если не скроллил вниз и пытается поставить нижний пиннед - выйти
return;
}
}
this.pinnedIndex = currentIndex;
this.pinnedMid = this.mids.find(_mid => _mid <= mid) || this.mids[this.mids.length - 1];
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;
})
];
const changed = this.pinnedIndex != currentIndex;
if(changed) {
if(this.waitForScrollBottom) {
if(lastScrollDirection === 1) { // если проскроллил вниз - разблокировать
this.waitForScrollBottom = false;
} else if(this.pinnedIndex > currentIndex) { // если не скроллил вниз и пытается поставить нижний пиннед - выйти
return;
}
}
if(!this.pinnedMaxMid) {
const promise = this.appMessagesManager.getPinnedMessage(this.topbar.peerID).then(p => {
if(!p.maxID) return;
this.pinnedMaxMid = p.maxID;
this.pinnedIndex = currentIndex;
this.setPinnedMessage();
}
});
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) {
const message = this.appMessagesManager.getMessage(mid);
if(message && !message.deleted) {
this.locked = true;
try {
const mids = await this.appMessagesManager.getPinnedMessages(message.peerID);
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);
this.chat.setPeer(this.topbar.peerID, mid);
(this.chat.setPeerPromise || Promise.resolve()).then(() => { // * debounce fast clicker
this.handleFollowingPinnedMessage();
this.testMid(this.pinnedIndex >= (this.count - 1) ? this.pinnedMaxMid : mid - 1);
});
}
}
@ -320,24 +518,24 @@ export default class ChatPinnedMessage {
public setPinnedMessage() {
/////this.log('setting pinned message', message);
//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([
this.appMessagesManager.getPinnedMessages(this.topbar.peerID),
promise
]).then(([mids]) => {
]).then(() => { */
//const mids = results[0];
if(mids.length) {
const pinnedIndex = this.pinnedIndex >= mids.length ? mids.length - 1 : this.pinnedIndex;
const message = this.appMessagesManager.getMessage(mids[pinnedIndex]);
const count = this.count;
if(count) {
const pinnedIndex = this.pinnedIndex;
const message = this.appMessagesManager.getMessage(this.pinnedMid);
//this.animatedCounter.prepareNumber(mids.length);
//this.animatedCounter.prepareNumber(count);
//setTimeout(() => {
const isLast = pinnedIndex === 0;
this.animatedCounter.container.classList.toggle('is-last', isLast);
//SetTransition(this.animatedCounter.container, 'is-last', isLast, AnimatedSuper.DURATION);
if(!isLast) {
this.animatedCounter.setCount(mids.length - pinnedIndex);
this.animatedCounter.setCount(count - pinnedIndex);
}
//}, 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.pinnedMessageContainer.divAndCaption.container.dataset.mid = '' + message.mid;
} else {
this.pinnedMessageContainer.toggle(true);
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;
constructor(private chatBubbles: ChatBubbles, private chatInput: ChatInput, private appMessagesManager: AppMessagesManager) {
const bubblesContainer = chatBubbles.bubblesContainer;
this.listenerSetter = chatBubbles.listenerSetter;
constructor(private bubbles: ChatBubbles, private input: ChatInput, private appMessagesManager: AppMessagesManager) {
const bubblesContainer = bubbles.bubblesContainer;
this.listenerSetter = bubbles.listenerSetter;
if(isTouchSupported) {
this.listenerSetter.add(bubblesContainer, 'touchend', (e) => {
@ -49,6 +49,7 @@ export default class ChatSelection {
|| (
!this.selectedMids.size
&& !(e.target as HTMLElement).classList.contains('bubble')
&& !(e.target as HTMLElement).classList.contains('document-selection')
&& bubble
)
) {
@ -86,7 +87,7 @@ export default class ChatSelection {
/* if(foundTargets.has(e.target as HTMLElement)) return;
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) {
//console.error('found no bubble', e);
return;
@ -96,7 +97,7 @@ export default class ChatSelection {
if(!mid) return;
// * 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(document, 'mouseup', onMouseUp, documentListenerOptions);
return;
@ -115,7 +116,7 @@ export default class ChatSelection {
if(!this.selectedMids.size) {
if(seen.size == 2) {
[...seen].forEach(mid => {
const mounted = this.chatBubbles.getMountedBubble(mid);
const mounted = this.bubbles.getMountedBubble(mid);
if(mounted) {
this.toggleByBubble(mounted.bubble);
}
@ -151,7 +152,7 @@ export default class ChatSelection {
public toggleBubbleCheckbox(bubble: HTMLElement, show: boolean) {
const hasCheckbox = !!this.getCheckboxInputFromBubble(bubble);
const isAlbum = bubble.classList.contains('is-album');
const isGrouped = bubble.classList.contains('is-grouped');
if(show) {
if(hasCheckbox) return;
@ -160,23 +161,44 @@ export default class ChatSelection {
// * if it is a render of new message
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;
bubble.classList.add('is-selected');
}
bubble.prepend(checkboxField.label);
if(bubble.classList.contains('document-container')) {
bubble.querySelector('.document, audio-element').append(checkboxField.label);
} else {
bubble.prepend(checkboxField.label);
}
} else if(hasCheckbox) {
bubble.firstElementChild.remove();
this.getCheckboxInputFromBubble(bubble).parentElement.remove();
}
if(isAlbum) {
this.chatBubbles.getBubbleAlbumItems(bubble).forEach(item => this.toggleBubbleCheckbox(item, show));
if(isGrouped) {
this.bubbles.getBubbleGroupedItems(bubble).forEach(item => this.toggleBubbleCheckbox(item, show));
}
}
public getCheckboxInputFromBubble(bubble: HTMLElement) {
return bubble.firstElementChild.tagName == 'LABEL' && bubble.firstElementChild.firstElementChild as HTMLInputElement;
public getCheckboxInputFromBubble(bubble: HTMLElement): 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) {
@ -213,7 +235,7 @@ export default class ChatSelection {
if(wasSelecting == this.isSelecting) return;
const bubblesContainer = this.chatBubbles.bubblesContainer;
const bubblesContainer = this.bubbles.bubblesContainer;
//bubblesContainer.classList.toggle('is-selecting', !!this.selectedMids.size);
/* if(bubblesContainer.classList.contains('is-chat-input-hidden')) {
@ -234,7 +256,9 @@ export default class ChatSelection {
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) {
this.selectionContainer.remove();
this.selectionContainer = this.selectionForwardBtn = this.selectionDeleteBtn = null;
@ -242,7 +266,7 @@ export default class ChatSelection {
}
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.chatInput.rowsWrapper.append(this.selectionContainer);
this.input.rowsWrapper.append(this.selectionContainer);
}
}
if(toggleCheckboxes) {
for(const mid in this.chatBubbles.bubbles) {
const bubble = this.chatBubbles.bubbles[mid];
for(const mid in this.bubbles.bubbles) {
const bubble = this.bubbles.bubbles[mid];
this.toggleBubbleCheckbox(bubble, this.isSelecting);
}
}
@ -295,9 +319,10 @@ export default class ChatSelection {
public cancelSelection = () => {
for(const mid of this.selectedMids) {
const mounted = this.chatBubbles.getMountedBubble(mid);
const mounted = this.bubbles.getMountedBubble(mid);
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];
if(bubble) {
@ -325,12 +350,12 @@ export default class ChatSelection {
SetTransition(bubble, 'is-selected', isSelected, 200);
}
public isAlbumBubbleSelected(bubble: HTMLElement) {
const albumCheckboxInput = this.getCheckboxInputFromBubble(bubble);
return albumCheckboxInput?.checked;
public isGroupedBubbleSelected(bubble: HTMLElement) {
const groupedCheckboxInput = this.getCheckboxInputFromBubble(bubble);
return groupedCheckboxInput?.checked;
}
public isAlbumMidsSelected(mid: number) {
public isGroupedMidsSelected(mid: number) {
const mids = this.appMessagesManager.getMidsByMid(mid);
const selectedMids = mids.filter(mid => this.selectedMids.has(mid));
return mids.length == selectedMids.length;
@ -339,14 +364,14 @@ export default class ChatSelection {
public toggleByBubble = (bubble: HTMLElement) => {
const mid = +bubble.dataset.mid;
const isAlbum = bubble.classList.contains('is-album');
if(isAlbum) {
if(!this.isAlbumBubbleSelected(bubble)) {
const isGrouped = bubble.classList.contains('is-grouped');
if(isGrouped) {
if(!this.isGroupedBubbleSelected(bubble)) {
const mids = this.appMessagesManager.getMidsByMid(mid);
mids.forEach(mid => this.selectedMids.delete(mid));
}
this.chatBubbles.getBubbleAlbumItems(bubble).forEach(this.toggleByBubble);
this.bubbles.getBubbleGroupedItems(bubble).forEach(this.toggleByBubble);
return;
}
@ -376,15 +401,15 @@ export default class ChatSelection {
this.selectedMids.add(mid);
}
const isAlbumItem = bubble.classList.contains('album-item');
if(isAlbumItem) {
const albumContainer = findUpClassName(bubble, 'bubble');
const isAlbumSelected = this.isAlbumBubbleSelected(albumContainer);
const isAlbumMidsSelected = this.isAlbumMidsSelected(mid);
const isGroupedItem = bubble.classList.contains('grouped-item');
if(isGroupedItem) {
const groupContainer = findUpClassName(bubble, 'bubble');
const isGroupedSelected = this.isGroupedBubbleSelected(groupContainer);
const isGroupedMidsSelected = this.isGroupedMidsSelected(mid);
const willChange = isAlbumMidsSelected || isAlbumSelected;
const willChange = isGroupedMidsSelected || isGroupedSelected;
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 { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
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 Chat from "./chat";
import { findUpClassName, cancelEvent, attachClickEvent } from "../../helpers/dom";
@ -18,6 +16,7 @@ import ChatPinnedMessage from "./pinnedMessage";
import ChatSearch from "./search";
import { ButtonMenuItemOptions } from "../buttonMenu";
import ListenerSetter from "../../helpers/listenerSetter";
import appStateManager from "../../lib/appManagers/appStateManager";
export default class ChatTopbar {
container: HTMLDivElement;
@ -38,15 +37,20 @@ export default class ChatTopbar {
private setUtilsRAF: number;
public peerID: number;
public wasPeerID: number;
private setPeerStatusInterval: number;
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})[] = [];
constructor(private chat: Chat, private appSidebarRight: AppSidebarRight, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager) {
this.listenerSetter = new ListenerSetter();
}
public construct() {
this.chat.log.error('Topbar construction');
this.listenerSetter = new ListenerSetter();
this.container = document.createElement('div');
this.container.classList.add('sidebar-header', 'topbar');
@ -59,10 +63,6 @@ export default class ChatTopbar {
const person = document.createElement('div');
person.classList.add('person');
this.avatarElement = new AvatarElement();
this.avatarElement.setAttribute('dialog', '1');
this.avatarElement.setAttribute('clickable', '');
const content = document.createElement('div');
content.classList.add('content');
@ -77,13 +77,16 @@ export default class ChatTopbar {
const bottom = document.createElement('div');
bottom.classList.add('bottom');
this.subtitle = document.createElement('div');
this.subtitle.classList.add('info');
bottom.append(this.subtitle);
if(this.subtitle) {
bottom.append(this.subtitle);
}
content.append(top, bottom);
person.append(this.avatarElement, content);
if(this.avatarElement) {
person.append(this.avatarElement);
}
person.append(content);
this.chatInfo.append(person);
// * chat utils section
@ -92,25 +95,77 @@ export default class ChatTopbar {
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.append('SUBSCRIBE');
this.btnPinned = ButtonIcon('pinlist');
this.btnMute = ButtonIcon('mute');
this.btnSearch = ButtonIcon('search');
const menuButtons: (ButtonMenuItemOptions & {verify: () => boolean})[] = [{
this.menuButtons = [{
icon: 'search',
text: 'Search',
onClick: () => {
new ChatSearch(this, this.chat);
},
verify: () => mediaSizes.isMobile
}, {
}, /* {
icon: 'pinlist',
text: 'Pinned Messages',
onClick: () => {},
onClick: () => this.openPinned(false),
verify: () => mediaSizes.isMobile
}, {
}, */ {
icon: 'mute',
text: 'Mute',
onClick: () => {
@ -144,53 +199,20 @@ export default class ChatTopbar {
onClick: () => {},
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);
// * 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);
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) => {
this.listenerSetter.add(this.btnPinned, 'click', (e) => {
cancelEvent(e);
this.chat.appImManager.setPeer(0);
this.openPinned(true);
});
this.listenerSetter.add(this.btnSearch, 'click', (e) => {
cancelEvent(e);
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.listenerSetter.add(this.btnJoin, 'mousedown', (e) => {
attachClickEvent(this.btnJoin, (e) => {
cancelEvent(e);
this.btnJoin.setAttribute('disabled', 'true');
appChatsManager.joinChannel(-this.peerID).finally(() => {
this.appChatsManager.joinChannel(-this.peerID).finally(() => {
this.btnJoin.removeAttribute('disabled');
});
//});
@ -213,7 +234,7 @@ export default class ChatTopbar {
this.listenerSetter.add(rootScope, 'chat_update', (e) => {
const peerID: number = e.detail;
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.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);
return this;
}
public openPinned(byCurrent: boolean) {
this.chat.appImManager.setInnerPeer(this.peerID, byCurrent ? +this.pinnedMessage.pinnedMessageContainer.divAndCaption.container.dataset.mid : 0, 'pinned');
}
private onResize = () => {
@ -252,8 +289,8 @@ export default class ChatTopbar {
};
private onChangeScreen = (from: ScreenSize, to: ScreenSize) => {
this.chatAudio.divAndCaption.container.classList.toggle('is-floating', to == ScreenSize.mobile);
this.pinnedMessage.onChangeScreen(from, to);
this.chatAudio && this.chatAudio.divAndCaption.container.classList.toggle('is-floating', to == ScreenSize.mobile);
this.pinnedMessage && this.pinnedMessage.onChangeScreen(from, to);
this.setUtilsWidth(true);
};
@ -263,48 +300,86 @@ export default class ChatTopbar {
this.listenerSetter.removeAll();
mediaSizes.removeListener('changeScreen', this.onChangeScreen);
window.clearInterval(this.setPeerStatusInterval);
if(this.pinnedMessage) {
this.pinnedMessage.destroy(); // * возможно это можно не делать
}
delete this.chatAudio;
delete this.pinnedMessage;
}
public setPeer(peerID: number) {
this.wasPeerID = this.peerID;
this.peerID = peerID;
this.avatarElement.setAttribute('peer', '' + peerID);
this.avatarElement.update();
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.update();
}
this.container.classList.remove('is-pinned-shown');
this.container.style.display = peerID ? '' : 'none';
const isBroadcast = this.appPeersManager.isBroadcast(peerID);
this.btnMute.classList.toggle('hide', !isBroadcast);
this.btnJoin.classList.toggle('hide', !this.appChatsManager.getChat(-peerID)?.pFlags?.left);
this.btnMute && this.btnMute.classList.toggle('hide', !isBroadcast);
this.btnJoin && this.btnJoin.classList.toggle('hide', !this.appChatsManager.getChat(-peerID)?.pFlags?.left);
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(() => {
this.pinnedMessage.pinnedIndex/* = this.pinnedMessage.wasPinnedIndex */ = 0;
//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.setTitle();
this.setPeerStatus(true);
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() {
if(!this.btnMute) return;
const peerID = this.peerID;
let muted = this.appMessagesManager.isPeerMuted(peerID);
if(this.appPeersManager.isBroadcast(peerID)) { // not human
@ -350,6 +425,8 @@ export default class ChatTopbar {
};
public setPeerStatus = (needClear = false) => {
if(!this.subtitle) return;
const peerID = this.peerID;
if(needClear) {
this.subtitle.innerHTML = '';

View File

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

View File

@ -6,7 +6,11 @@ export default class PopupPinMessage {
constructor(peerID: number, mid: number, unpin?: true) {
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) {
title = `Unpin Message?`;
description = 'Would you like to unpin this message?';

View File

@ -70,7 +70,7 @@ export class ScrollableBase {
}
protected setListeners() {
window.addEventListener('resize', this.onScroll);
window.addEventListener('resize', this.onScroll, {passive: 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.lastScrollTop = scrollTop;
if(this.onAdditionalScroll) {
if(this.onAdditionalScroll && this.lastScrollDirection !== 0) {
this.onAdditionalScroll();
}

View File

@ -91,7 +91,7 @@ export default class AppIncludedChatsTab implements SliderTab {
}
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[]) => {

View File

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

View File

@ -24,6 +24,7 @@ import { renderImageFromUrl } from './misc';
import PollElement from './poll';
import ProgressivePreloader from './preloader';
import './middleEllipsis';
import { nextRandomInt } from '../helpers/random';
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, 'preserveAspectRatio', 'none');
const clipID = 'clip' + message.mid;
const clipID = 'clip' + message.mid + '_' + nextRandomInt(9999);
svg.dataset.clipID = clipID;
const defs = document.createElementNS("http://www.w3.org/2000/svg", 'defs');
@ -803,7 +804,7 @@ export function prepareAlbum(options: {
container.append(div);
}
div.classList.add('album-item');
div.classList.add('album-item', 'grouped-item');
div.style.width = (geometry.width / width * 100) + '%';
div.style.height = (geometry.height / height * 100) + '%';

View File

@ -543,3 +543,50 @@ export const isSelectionSingle = (input: Element = document.activeElement) => {
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) {
this.listenerResults[name] = args;
}

View File

@ -157,7 +157,7 @@
<div class="folders-tabs-scrollable hide">
<nav class="menu-horizontal" id="folders-tabs">
<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>
</nav>
</div>
@ -402,7 +402,7 @@
<p class="profile-row-label">Phone</p>
</div>
<div class="profile-row profile-row-notifications">
<label>
<label class="checkbox-field">
<input type="checkbox" id="profile-notifications" checked="checked">
<span>Notifications</span>
</label>

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import ProgressivePreloader from "../../components/preloader";
import { listMergeSorted } from "../../helpers/array";
import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise";
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 { 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";
@ -75,6 +74,11 @@ type MyInputMessagesFilter = 'inputMessagesFilterEmpty'
| 'inputMessagesFilterChatPhotos'
| 'inputMessagesFilterPinned';
export type PinnedStorage = Partial<{
promise: Promise<PinnedStorage>,
count: number,
maxID: number
}>;
export class AppMessagesManager {
public messagesStorage: {[mid: string]: any} = {};
public messagesStorageByPeerID: {[peerID: string]: AppMessagesManager['messagesStorage']} = {};
@ -82,9 +86,15 @@ export class AppMessagesManager {
public historiesStorage: {
[peerID: string]: HistoryStorage
} = {};
// * mids - descend sorted
public pinnedMessagesStorage: {[peerID: string]: Partial<{promise: Promise<number[]>, mids: number[]}>} = {};
public searchesStorage: {
[peerID: string]: Partial<{
[inputFilter in MyInputMessagesFilter]: {
count?: number,
history: number[]
}
}>
} = {};
public pinnedMessages: {[peerID: string]: PinnedStorage} = {};
public pendingByRandomID: {[randomID: string]: [number, number]} = {};
public pendingByMessageID: any = {};
@ -101,9 +111,6 @@ export class AppMessagesManager {
}
} = {};
public lastSearchFilter: any = {};
public lastSearchResults: any = [];
public needSingleMessages: number[] = [];
private fetchSingleMessagesPromise: Promise<void> = null;
@ -293,8 +300,6 @@ export class AppMessagesManager {
});
}
public getInputEntities(entities: any) {
var sendEntites = copy(entities);
sendEntites.forEach((entity: any) => {
@ -1801,47 +1806,33 @@ export class AppMessagesManager {
});
}
/* public savePinnedMessage(peerID: number, mid: number) {
if(!mid) {
delete this.pinnedMessagesStorage[peerID];
} else {
this.pinnedMessagesStorage[peerID] = mid;
if(!this.messagesStorage.hasOwnProperty(mid)) {
this.wrapSingleMessage(mid).then(() => {
rootScope.broadcast('peer_pinned_message', peerID);
});
return;
}
}
rootScope.broadcast('peer_pinned_message', peerID);
} */
public getPinnedMessagesStorage(peerID: number) {
return this.pinnedMessagesStorage[peerID] ?? (this.pinnedMessagesStorage[peerID] = {});
}
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(() => {
storage.promise = null;
public hidePinnedMessages(peerID: number) {
return Promise.all([
appStateManager.getState(),
this.getPinnedMessage(peerID)
])
.then(([state, pinned]) => {
state.hiddenPinnedMessages[peerID] = pinned.maxID;
rootScope.broadcast('peer_pinned_hidden', {peerID, maxID: pinned.maxID});
});
}
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);
return p.promise = this.getSearch(peerID, '', {_: 'inputMessagesFilterPinned'}, 0, 1).then(result => {
p.count = result.count;
p.maxID = result.history[0];
return p;
}).finally(() => {
delete p.promise;
});
}
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),
unpin,
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) {
const group = appMessagesManager.groupedMessagesStorage[grouped_id];
let foundMessages = 0, message: string, totalEntities: MessageEntity[];
@ -1884,9 +1907,7 @@ export class AppMessagesManager {
else return [mid];
}
public saveMessages(messages: any[], options: {
isEdited?: boolean
} = {}) {
public saveMessages(messages: any[]) {
let albums: Set<string>;
messages.forEach((message) => {
if(message.pFlags === undefined) {
@ -2669,36 +2690,59 @@ export class AppMessagesManager {
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: {
_?: MyInputMessagesFilter
_: MyInputMessagesFilter
} = {_: 'inputMessagesFilterEmpty'}, maxID: number, limit = 20, offsetRate = 0, backLimit = 0): Promise<{
count: number,
next_rate: number,
offset_id_offset: number,
history: number[]
}> {
//peerID = peerID ? parseInt(peerID) : 0;
const foundMsgs: number[] = [];
const useSearchCache = !query;
const newSearchFilter = {peer: peerID, filter: inputFilter};
const sameSearchCache = useSearchCache && deepEqual(this.lastSearchFilter, newSearchFilter);
if(useSearchCache && !sameSearchCache) {
// this.log.warn(dT(), 'new search filter', lastSearchFilter, newSearchFilter)
this.lastSearchFilter = newSearchFilter;
this.lastSearchResults = [];
//this.log('search', maxID);
if(backLimit) {
limit += backLimit;
}
//this.log(dT(), 'search', useSearchCache, sameSearchCache, this.lastSearchResults, maxID);
//const beta = inputFilter._ == 'inputMessagesFilterPinned' && !backLimit;
const beta = false;
if(peerID && !maxID && !query) {
const historyStorage = this.historiesStorage[peerID];
let storage: {
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;
if(historyStorage !== undefined && historyStorage.history.length) {
var neededContents: {
const history = maxID ? storage.history.slice(storage.history.indexOf(maxID) + 1) : storage.history;
if(storage !== undefined && history.length) {
const neededContents: {
[messageMediaType: string]: boolean
} = {},
neededDocTypes: string[] = [], excludeDocTypes: string[] = [];
neededDocTypes: string[] = [],
excludeDocTypes: string[] = []/* ,
neededFlags: string[] = [] */;
switch(inputFilter._) {
case 'inputMessagesFilterPhotos':
@ -2749,6 +2793,10 @@ export class AppMessagesManager {
neededContents['avatar'] = true;
break;
/* case 'inputMessagesFilterPinned':
neededFlags.push('pinned');
break; */
/* case 'inputMessagesFilterMyMentions':
neededContents['mentioned'] = true;
break; */
@ -2764,8 +2812,8 @@ export class AppMessagesManager {
}
if(filtering) {
for(let i = 0, length = historyStorage.history.length; i < length; i++) {
const message = this.messagesStorage[historyStorage.history[i]];
for(let i = 0, length = history.length; i < length; i++) {
const message = this.messagesStorage[history[i]];
//|| (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._)) {
found = true;
}
}/* else if(neededFlags.find(flag => message.pFlags[flag])) {
found = true;
} */
if(found) {
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 < limit) {
if(foundMsgs.length < limit && (beta ? storage.count !== storage.history.length : true)) {
maxID = foundMsgs[foundMsgs.length - 1];
limit = limit - foundMsgs.length;
} else {
if(useSearchCache) {
this.lastSearchResults = listMergeSorted(this.lastSearchResults, foundMsgs)
}
return Promise.resolve({
count: 0,
count: beta ? storage.count : 0,
next_rate: 0,
offset_id_offset: 0,
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>;
@ -2835,7 +2875,7 @@ export class AppMessagesManager {
apiPromise = apiManager.invokeApi('messages.search', {
peer: appPeersManager.getInputPeerByID(peerID),
q: query || '',
filter: (inputFilter || {_: 'inputMessagesFilterEmpty'}) as any as MessagesFilter,
filter: inputFilter as any as MessagesFilter,
min_date: 0,
max_date: 0,
limit,
@ -2862,7 +2902,7 @@ export class AppMessagesManager {
apiPromise = apiManager.invokeApi('messages.searchGlobal', {
q: query,
filter: (inputFilter || {_: 'inputMessagesFilterEmpty'}) as any as MessagesFilter,
filter: inputFilter as any as MessagesFilter,
min_date: 0,
max_date: 0,
offset_rate: offsetRate,
@ -2880,6 +2920,14 @@ export class AppMessagesManager {
appChatsManager.saveApiChats(searchResult.chats);
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);
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);
}
}
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 {
count: foundCount,
offset_id_offset: searchResult.offset_id_offset,
next_rate: searchResult.next_rate,
history: foundMsgs
};
@ -3825,10 +3869,15 @@ export class AppMessagesManager {
const channelID = update._ == 'updatePinnedChannelMessages' ? update.channel_id : undefined;
const peerID = channelID ? -channelID : appPeersManager.getPeerID((update as Update.updatePinnedMessages).peer);
const storage = this.getPinnedMessagesStorage(peerID);
if(!storage.mids) {
/* const storage = this.getSearchStorage(peerID, 'inputMessagesFilterPinned');
if(storage.count !== storage.history.length) {
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;
@ -3838,21 +3887,30 @@ export class AppMessagesManager {
const werePinned = update.pFlags?.pinned;
if(werePinned) {
for(const mid of messages) {
storage.mids.push(mid);
//storage.history.push(mid);
const message = this.messagesStorage[mid];
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 {
for(const mid of messages) {
storage.mids.findAndSplice(_mid => _mid == mid);
//storage.history.findAndSplice(_mid => _mid == mid);
const message = this.messagesStorage[mid];
delete message.pFlags.pinned;
}
}
rootScope.broadcast('peer_pinned_messages', peerID);
delete this.pinnedMessages[peerID];
appStateManager.getState().then(state => {
delete state.hiddenPinnedMessages[peerID];
rootScope.broadcast('peer_pinned_messages', peerID);
});
});
break;

View File

@ -29,7 +29,7 @@ export class AppPeersManager {
} */
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) {

View File

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

View File

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

View File

@ -13,7 +13,7 @@ function dT() {
}
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;
}

View File

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

View File

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

View File

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

View File

@ -132,17 +132,14 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
return;
}
if(task.useLs) {
// @ts-ignore
AppStorage[task.task](...task.args).then(res => {
this.postMessage({useLs: true, taskID: task.taskID, args: res});
});
} else if(task.update) {
if(task.update) {
if(this.updatesProcessor) {
this.updatesProcessor(task.update.obj, task.update.bool);
}
} else if(task.progress) {
rootScope.broadcast('download_progress', task.progress);
} else if(task.type == 'reload') {
location.reload();
} else if(task.type == 'connectionStatusChange') {
rootScope.broadcast('connection_status_change', task.payload);
} else if(task.type == 'convertWebp') {
@ -255,15 +252,17 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
return this.performTaskWorker('invokeApi', method, params, options).then((result: any) => {
if(result._.includes('NotModified')) {
//this.log.warn('NotModified saved!', method, queryJSON);
this.log.warn('NotModified saved!', method, queryJSON);
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] = {};
this.hashes[method][queryJSON] = {
hash: result.hash,
result: result
hash,
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) {
return this.performTaskWorker('setBaseDcID', dcID);
}

View File

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

View File

@ -1,68 +1,73 @@
import { Modes } from './mtproto/mtproto_config';
import { notifySomeone, isWorker } from '../helpers/context';
import CacheStorageController from './cacheStorage';
import { MOUNT_CLASS_TO } from './mtproto/mtproto_config';
//import { stringify } from '../helpers/json';
class ConfigStorage {
public keyPrefix = '';
public noPrefix = false;
class AppStorage {
//private log = (...args: any[]) => console.log('[SW LS]', ...args);
private log = (...args: any[]) => {};
private cacheStorage = new CacheStorageController('session');
//public noPrefix = false;
private cache: {[key: string]: any} = {};
private useLs = true;
private useCS = true;
storageGetPrefix() {
if(this.noPrefix) {
this.noPrefix = false;
return '';
}
constructor() {
return this.keyPrefix;
}
get(keys: string | string[], callback: any) {
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();
public storageGetPrefix() {
/* if(this.noPrefix) {
this.noPrefix = false;
return '';
} */
return '';
//return this.keyPrefix;
}
public async get<T>(...keys: string[]): Promise<T> {
const single = keys.length === 1;
const result: any[] = [];
const prefix = this.storageGetPrefix();
for(let key of keys) {
key = prefix + key;
if(this.cache.hasOwnProperty(key)) {
result.push(this.cache[key]);
} else if(this.useLs) {
} else if(this.useCS) {
let value: any;
try {
value = localStorage.getItem(key);
value = await this.cacheStorage.getFile(key, 'text');
value = JSON.parse(value);
} 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(', ')}`;
// console.time(str);
try {
value = (value === undefined || value === null) ? false : JSON.parse(value);
value = (value === undefined || value === null) ? false : value;
} catch(e) {
value = false;
}
//console.timeEnd(str);
result.push(this.cache[key] = value);
} else {
allFound = false;
throw 'something went wrong';
}
}
if(allFound) {
return callback(single ? result[0] : result);
}
return single ? result[0] : result;
}
set(obj: any, callback: any) {
var keyValues: any = {};
var prefix = this.storageGetPrefix(),
public async set(obj: any) {
let keyValues: any = {};
let prefix = this.storageGetPrefix(),
key, value;
//console.log('storageSetValue', obj, callback, arguments);
@ -82,140 +87,48 @@ class ConfigStorage {
value = stringify(value);
console.log('LocalStorage set: stringify time by own stringify:', performance.now() - perf); */
if(this.useLs) {
if(this.useCS) {
try {
//console.log('setItem', key, value);
localStorage.setItem(key, value);
} catch (e) {
this.useLs = false;
await this.cacheStorage.save(key, new Response(value, {headers: {'Content-Type': 'application/json'}}));
} catch(e) {
//this.useCS = false;
console.error('[AS]: set error:', e, value);
}
} else {
keyValues[key] = value;
}
}
}
if(this.useLs) {
if(callback) {
callback();
}
return;
}
}
remove(keys: any, callback: any) {
public async remove(...keys: any[]) {
if(!Array.isArray(keys)) {
keys = Array.prototype.slice.call(arguments)
if(typeof keys[keys.length - 1] === 'function') {
callback = keys.pop();
}
keys = Array.prototype.slice.call(arguments);
}
var prefix = this.storageGetPrefix(),
let prefix = this.storageGetPrefix(),
i, key;
for(i = 0; i < keys.length; i++) {
key = keys[i] = prefix + keys[i];
delete this.cache[key];
if(this.useLs) {
if(this.useCS) {
try {
localStorage.removeItem(key);
await this.cacheStorage.delete(key);
} 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() {
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;
right: 20px;
//transition: .2s ease;
transition: var(--btn-corner-transition) !important;
transition: transform var(--btn-corner-transition) !important;
transform: translate3d(0, var(--translateY), 0);
z-index: 3;
user-select: none;
@ -49,12 +49,12 @@
border: none;
outline: none;
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
padding: 0 !important;
&.is-visible {
--translateY: 0;
}

View File

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

View File

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

View File

@ -12,28 +12,44 @@
}
.checkbox-field-round {
display: block;
text-align: left;
[type="checkbox"] {
&:checked + .checkbox-caption {
&:before {
top: 5px;
left: 0px;
}
.checkbox-caption {
min-width: 1.5rem;
min-height: 1.5rem;
&:after {
background-color: #4EA4F6;
border: none;
}
&:before {
color: #fff;
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 {
content: '';
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 {
border-radius: 50%;
height: 20px;
width: 20px;
border-color: #dadbdc;
[type="checkbox"]:checked + .checkbox-caption {
&:before {
opacity: 1;
}
&:after {
background-color: $button-primary-background;
}
}
}
@ -122,7 +138,7 @@
position: absolute;
}
[type="checkbox"] {
.checkbox-field [type="checkbox"] {
& + span {
position: relative;
padding-left: 3.5rem;

View File

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

View File

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

View File

@ -86,21 +86,8 @@
}
}
.unread-count {
.badge {
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 {
@ -654,6 +641,11 @@
}
}
}
.checkbox-field {
margin: 0;
padding: 0;
}
}
.folder-category-button {

View File

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

View File

@ -3,7 +3,7 @@
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 {
width: 100%;
height: 100%;

View File

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

View File

@ -26,6 +26,11 @@ $large-screen: 1680px;
$floating-left-sidebar: 925px;
$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) {
@if $media == handhelds {
@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: .3s cubic-bezier(.33, 1, .68, 1);
//--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-beside-button-margin: 2.875rem;
--message-time-background: rgba(0, 0, 0, .35);
@ -91,10 +96,16 @@ $messages-container-width: 728px;
@include respond-to(handhelds) {
--right-column-width: 100vw;
--esg-sticker-size: 68px;
--chat-input-size: #{$chat-input-handhelds-size};
--chat-input-padding: #{$chat-padding-handhelds};
}
@include respond-to(not-handhelds) {
--right-column-width: calc(#{$large-screen} / 4);
--chat-input-size: #{$chat-input-size};
--chat-input-padding: #{$chat-padding};
}
@include respond-to(only-medium-screens) {
@ -1048,3 +1059,41 @@ middle-ellipsis-element {
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;
}
}