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:
parent
e7f483b573
commit
2580c4e720
@ -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) {
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}), () => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
//}
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
@ -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);
|
||||
//});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 = '';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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?';
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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[]) => {
|
||||
|
@ -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;
|
||||
|
@ -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) + '%';
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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(() => {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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}) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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";
|
||||
|
@ -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 {
|
||||
|
@ -84,6 +84,11 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.checkbox-field {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[type="checkbox"] + span {
|
||||
padding-left: 54px;
|
||||
margin-left: -54px;
|
||||
|
@ -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%;
|
||||
|
@ -164,4 +164,9 @@
|
||||
margin-top: 11px;
|
||||
padding-left: 11px;
|
||||
}
|
||||
|
||||
.checkbox-field {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user