Reactions menu
This commit is contained in:
parent
edf5553692
commit
980755bd38
@ -8,7 +8,7 @@ import rootScope from "../lib/rootScope";
|
||||
import { IS_SAFARI } from "../environment/userAgent";
|
||||
import { MOUNT_CLASS_TO } from "../config/debug";
|
||||
import isInDOM from "../helpers/dom/isInDOM";
|
||||
import { indexOfAndSplice } from "../helpers/array";
|
||||
import { forEachReverse, indexOfAndSplice } from "../helpers/array";
|
||||
import RLottiePlayer from "../lib/rlottie/rlottiePlayer";
|
||||
|
||||
export interface AnimationItem {
|
||||
@ -18,9 +18,10 @@ export interface AnimationItem {
|
||||
};
|
||||
|
||||
export class AnimationIntersector {
|
||||
public observer: IntersectionObserver;
|
||||
private observer: IntersectionObserver;
|
||||
private visible: Set<AnimationItem> = new Set();
|
||||
|
||||
private overrideIdleGroups: Set<string>;
|
||||
private byGroups: {[group: string]: AnimationItem[]} = {};
|
||||
private lockedGroups: {[group: string]: true} = {};
|
||||
private onlyOnePlayableGroup: string = '';
|
||||
@ -30,7 +31,7 @@ export class AnimationIntersector {
|
||||
|
||||
constructor() {
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
if(rootScope.idle.isIDLE) return;
|
||||
// if(rootScope.idle.isIDLE) return;
|
||||
|
||||
for(const entry of entries) {
|
||||
const target = entry.target;
|
||||
@ -61,6 +62,8 @@ export class AnimationIntersector {
|
||||
}
|
||||
});
|
||||
|
||||
this.overrideIdleGroups = new Set();
|
||||
|
||||
rootScope.addEventListener('media_play', ({doc}) => {
|
||||
if(doc.type === 'round') {
|
||||
this.videosLocked = true;
|
||||
@ -76,6 +79,11 @@ export class AnimationIntersector {
|
||||
});
|
||||
}
|
||||
|
||||
public setOverrideIdleGroup(group: string, override: boolean) {
|
||||
if(override) this.overrideIdleGroups.add(group);
|
||||
else this.overrideIdleGroups.delete(group);
|
||||
}
|
||||
|
||||
public getAnimations(element: HTMLElement) {
|
||||
const found: AnimationItem[] = [];
|
||||
for(const group in this.byGroups) {
|
||||
@ -101,8 +109,12 @@ export class AnimationIntersector {
|
||||
}, 1e3);
|
||||
}
|
||||
|
||||
for(const group in this.byGroups) {
|
||||
indexOfAndSplice(this.byGroups[group], player);
|
||||
const group = this.byGroups[player.group];
|
||||
if(group) {
|
||||
indexOfAndSplice(group, player);
|
||||
if(!group.length) {
|
||||
delete this.byGroups[player.group];
|
||||
}
|
||||
}
|
||||
|
||||
this.observer.unobserve(el);
|
||||
@ -127,20 +139,19 @@ export class AnimationIntersector {
|
||||
}
|
||||
|
||||
public checkAnimations(blurred?: boolean, group?: string, destroy = false) {
|
||||
if(rootScope.idle.isIDLE) return;
|
||||
|
||||
const groups = group /* && false */ ? [group] : Object.keys(this.byGroups);
|
||||
// if(rootScope.idle.isIDLE) return;
|
||||
|
||||
if(group && !this.byGroups[group]) {
|
||||
if(group !== undefined && !this.byGroups[group]) {
|
||||
//console.warn('no animation group:', group);
|
||||
this.byGroups[group] = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const groups = group !== undefined /* && false */ ? [group] : Object.keys(this.byGroups);
|
||||
|
||||
for(const group of groups) {
|
||||
const animations = this.byGroups[group];
|
||||
|
||||
animations.forEach(player => {
|
||||
forEachReverse(animations, (player) => {
|
||||
this.checkAnimation(player, blurred, destroy);
|
||||
});
|
||||
}
|
||||
@ -162,7 +173,8 @@ export class AnimationIntersector {
|
||||
} else if(animation.paused &&
|
||||
this.visible.has(player) &&
|
||||
animation.autoplay &&
|
||||
(!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup === group)
|
||||
(!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup === group) &&
|
||||
(!rootScope.idle.isIDLE || this.overrideIdleGroups.has(player.group))
|
||||
) {
|
||||
//console.warn('play animation:', animation);
|
||||
animation.play();
|
||||
@ -211,4 +223,4 @@ const animationIntersector = new AnimationIntersector();
|
||||
if(MOUNT_CLASS_TO) {
|
||||
MOUNT_CLASS_TO.animationIntersector = animationIntersector;
|
||||
}
|
||||
export default animationIntersector;
|
||||
export default animationIntersector;
|
||||
|
@ -2196,7 +2196,7 @@ export default class ChatBubbles {
|
||||
// this.ladderDeferred.resolve();
|
||||
|
||||
scrollable.lastScrollDirection = 0;
|
||||
scrollable.lastScrollTop = 0;
|
||||
scrollable.lastScrollPosition = 0;
|
||||
replaceContent(scrollable.container, chatInner);
|
||||
|
||||
animationIntersector.unlockGroup(CHAT_ANIMATION_GROUP);
|
||||
@ -3633,7 +3633,7 @@ export default class ChatBubbles {
|
||||
//this.scrollable.scrollTop = this.scrollable.scrollHeight;
|
||||
//isTouchSupported && isApple && (this.scrollable.container.style.overflow = '');
|
||||
|
||||
this.scrollable.lastScrollTop = newScrollTop;
|
||||
this.scrollable.lastScrollPosition = newScrollTop;
|
||||
this.scrollable.lastScrollDirection = 0;
|
||||
|
||||
if(IS_SAFARI/* && !isAppleMobile */) { // * fix blinking and jumping
|
||||
|
@ -23,6 +23,7 @@ import type { AppEmojiManager } from "../../lib/appManagers/appEmojiManager";
|
||||
import type { ServerTimeManager } from "../../lib/mtproto/serverTimeManager";
|
||||
import type { AppMessagesIdsManager } from "../../lib/appManagers/appMessagesIdsManager";
|
||||
import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager";
|
||||
import type { AppReactionsManager } from "../../lib/appManagers/appReactionsManager";
|
||||
import type { State } from "../../lib/appManagers/appStateManager";
|
||||
import type stateStorage from '../../lib/stateStorage';
|
||||
import EventListenerBase from "../../helpers/eventListenerBase";
|
||||
@ -92,7 +93,8 @@ export default class Chat extends EventListenerBase<{
|
||||
public appNotificationsManager: AppNotificationsManager,
|
||||
public appEmojiManager: AppEmojiManager,
|
||||
public appMessagesIdsManager: AppMessagesIdsManager,
|
||||
public appGroupCallsManager: AppGroupCallsManager
|
||||
public appGroupCallsManager: AppGroupCallsManager,
|
||||
public appReactionsManager: AppReactionsManager
|
||||
) {
|
||||
super();
|
||||
|
||||
@ -185,7 +187,7 @@ export default class Chat extends EventListenerBase<{
|
||||
this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appPeersManager, this.appProfileManager, this.appDraftsManager, this.appMessagesIdsManager, this.appChatsManager);
|
||||
this.input = new ChatInput(this, this.appMessagesManager, this.appMessagesIdsManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager, this.appDraftsManager, this.serverTimeManager, this.appNotificationsManager, this.appEmojiManager, this.appUsersManager, this.appInlineBotsManager);
|
||||
this.selection = new ChatSelection(this, this.bubbles, this.input, this.appMessagesManager);
|
||||
this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appPeersManager, this.appPollsManager, this.appDocsManager, this.appMessagesIdsManager);
|
||||
this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appPeersManager, this.appPollsManager, this.appDocsManager, this.appMessagesIdsManager, this.appReactionsManager);
|
||||
|
||||
if(this.type === 'chat') {
|
||||
this.topbar.constructUtils();
|
||||
@ -240,6 +242,7 @@ export default class Chat extends EventListenerBase<{
|
||||
this.topbar.destroy();
|
||||
this.bubbles.destroy();
|
||||
this.input.destroy();
|
||||
this.contextMenu && this.contextMenu.destroy();
|
||||
|
||||
delete this.topbar;
|
||||
delete this.bubbles;
|
||||
|
@ -9,6 +9,7 @@ import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
|
||||
import type { AppPollsManager } from "../../lib/appManagers/appPollsManager";
|
||||
import type { AppDocsManager, MyDocument } from "../../lib/appManagers/appDocsManager";
|
||||
import type { AppMessagesIdsManager } from "../../lib/appManagers/appMessagesIdsManager";
|
||||
import type { AppReactionsManager } from "../../lib/appManagers/appReactionsManager";
|
||||
import type Chat from "./chat";
|
||||
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
|
||||
import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu";
|
||||
@ -24,10 +25,211 @@ import findUpClassName from "../../helpers/dom/findUpClassName";
|
||||
import { cancelEvent } from "../../helpers/dom/cancelEvent";
|
||||
import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
|
||||
import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty";
|
||||
import { Message, Poll, Chat as MTChat, MessageMedia } from "../../layer";
|
||||
import { Message, Poll, Chat as MTChat, MessageMedia, AvailableReaction } from "../../layer";
|
||||
import PopupReportMessages from "../popups/reportMessages";
|
||||
import assumeType from "../../helpers/assumeType";
|
||||
import PopupSponsored from "../popups/sponsored";
|
||||
import { ScrollableX } from "../scrollable";
|
||||
import { wrapSticker } from "../wrappers";
|
||||
import RLottiePlayer from "../../lib/rlottie/rlottiePlayer";
|
||||
import getVisibleRect from "../../helpers/dom/getVisibleRect";
|
||||
import ListenerSetter from "../../helpers/listenerSetter";
|
||||
import animationIntersector from "../animationIntersector";
|
||||
import { getMiddleware } from "../../helpers/middleware";
|
||||
import noop from "../../helpers/noop";
|
||||
|
||||
const REACTIONS_CLASS_NAME = 'btn-menu-reactions';
|
||||
const REACTION_CLASS_NAME = REACTIONS_CLASS_NAME + '-reaction';
|
||||
|
||||
const REACTION_SIZE = 24;
|
||||
const PADDING = 4;
|
||||
const REACTION_CONTAINER_SIZE = REACTION_SIZE + PADDING * 2;
|
||||
|
||||
type ChatReactionsMenuPlayers = {
|
||||
select?: RLottiePlayer,
|
||||
appear?: RLottiePlayer,
|
||||
selectWrapper: HTMLElement,
|
||||
appearWrapper: HTMLElement
|
||||
};
|
||||
export class ChatReactionsMenu {
|
||||
public container: HTMLElement;
|
||||
private reactionsMap: Map<HTMLElement, ChatReactionsMenuPlayers>;
|
||||
private scrollable: ScrollableX;
|
||||
private animationGroup: string;
|
||||
private middleware: ReturnType<typeof getMiddleware>;
|
||||
|
||||
constructor(
|
||||
private appReactionsManager: AppReactionsManager
|
||||
) {
|
||||
const reactionsContainer = this.container = document.createElement('div');
|
||||
reactionsContainer.classList.add(REACTIONS_CLASS_NAME);
|
||||
|
||||
const reactionsScrollable = this.scrollable = new ScrollableX(undefined);
|
||||
reactionsContainer.append(reactionsScrollable.container);
|
||||
reactionsScrollable.onAdditionalScroll = this.onScroll;
|
||||
reactionsScrollable.setListeners();
|
||||
|
||||
this.reactionsMap = new Map();
|
||||
this.animationGroup = 'CHAT-MENU-REACTIONS-' + Date.now();
|
||||
animationIntersector.setOverrideIdleGroup(this.animationGroup, true);
|
||||
|
||||
if(!IS_TOUCH_SUPPORTED) {
|
||||
reactionsContainer.addEventListener('mousemove', this.onMouseMove);
|
||||
}
|
||||
|
||||
this.middleware = getMiddleware();
|
||||
const middleware = this.middleware.get();
|
||||
appReactionsManager.getAvailableReactions().then(reactions => {
|
||||
if(!middleware()) return;
|
||||
reactions.forEach(reaction => {
|
||||
this.renderReaction(reaction);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
this.middleware.clean();
|
||||
this.scrollable.removeListeners();
|
||||
this.reactionsMap.clear();
|
||||
animationIntersector.setOverrideIdleGroup(this.animationGroup, false);
|
||||
animationIntersector.checkAnimations(true, this.animationGroup, true);
|
||||
}
|
||||
|
||||
private onScroll = () => {
|
||||
this.reactionsMap.forEach((players, div) => {
|
||||
this.onScrollProcessItem(div, players);
|
||||
});
|
||||
};
|
||||
|
||||
private renderReaction(reaction: AvailableReaction) {
|
||||
const reactionDiv = document.createElement('div');
|
||||
reactionDiv.classList.add(REACTION_CLASS_NAME);
|
||||
|
||||
const scaleContainer = document.createElement('div');
|
||||
scaleContainer.classList.add(REACTION_CLASS_NAME + '-scale');
|
||||
|
||||
const appearWrapper = document.createElement('div');
|
||||
const selectWrapper = document.createElement('div');
|
||||
appearWrapper.classList.add(REACTION_CLASS_NAME + '-appear');
|
||||
selectWrapper.classList.add(REACTION_CLASS_NAME + '-select', 'hide');
|
||||
|
||||
const hoverScale = IS_TOUCH_SUPPORTED ? 1 : 1.25;
|
||||
const size = REACTION_SIZE * hoverScale;
|
||||
|
||||
const players: ChatReactionsMenuPlayers = {
|
||||
selectWrapper,
|
||||
appearWrapper
|
||||
};
|
||||
this.reactionsMap.set(reactionDiv, players);
|
||||
|
||||
const middleware = this.middleware.get();
|
||||
|
||||
const options = {
|
||||
width: size,
|
||||
height: size,
|
||||
skipRatio: 1,
|
||||
needFadeIn: false,
|
||||
withThumb: false,
|
||||
group: this.animationGroup,
|
||||
middleware
|
||||
};
|
||||
|
||||
let isFirst = true;
|
||||
wrapSticker({
|
||||
doc: reaction.appear_animation,
|
||||
div: appearWrapper,
|
||||
play: true,
|
||||
...options
|
||||
}).then(player => {
|
||||
assumeType<RLottiePlayer>(player);
|
||||
|
||||
players.appear = player;
|
||||
|
||||
player.addEventListener('enterFrame', (frameNo) => {
|
||||
if(player.maxFrame === frameNo) {
|
||||
selectLoadPromise.then((selectPlayer) => {
|
||||
assumeType<RLottiePlayer>(selectPlayer);
|
||||
appearWrapper.classList.add('hide');
|
||||
selectWrapper.classList.remove('hide');
|
||||
|
||||
if(isFirst) {
|
||||
players.select = selectPlayer;
|
||||
isFirst = false;
|
||||
}
|
||||
}, noop);
|
||||
}
|
||||
});
|
||||
}, noop);
|
||||
|
||||
const selectLoadPromise = wrapSticker({
|
||||
doc: reaction.select_animation,
|
||||
div: selectWrapper,
|
||||
...options
|
||||
}).catch(noop);
|
||||
|
||||
scaleContainer.append(appearWrapper, selectWrapper);
|
||||
reactionDiv.append(scaleContainer);
|
||||
this.scrollable.append(reactionDiv);
|
||||
}
|
||||
|
||||
private onScrollProcessItem(div: HTMLElement, players: ChatReactionsMenuPlayers) {
|
||||
if(!players.appear) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scaleContainer = div.firstElementChild as HTMLElement;
|
||||
const visibleRect = getVisibleRect(div, this.scrollable.container);
|
||||
if(!visibleRect) {
|
||||
if(!players.appearWrapper.classList.contains('hide')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(players.select) {
|
||||
players.select.stop();
|
||||
}
|
||||
|
||||
players.appear.stop();
|
||||
players.appear.autoplay = true;
|
||||
players.appearWrapper.classList.remove('hide');
|
||||
players.selectWrapper.classList.add('hide');
|
||||
scaleContainer.style.transform = '';
|
||||
} else if(visibleRect.overflow.left || visibleRect.overflow.right) {
|
||||
const diff = Math.abs(visibleRect.rect.left - visibleRect.rect.right);
|
||||
const scale = Math.min(diff ** 2 / REACTION_CONTAINER_SIZE ** 2, 1);
|
||||
|
||||
scaleContainer.style.transform = `scale(${scale})`;
|
||||
} else {
|
||||
scaleContainer.style.transform = '';
|
||||
}
|
||||
}
|
||||
|
||||
private onMouseMove = (e: MouseEvent) => {
|
||||
const reactionDiv = findUpClassName(e.target, REACTION_CLASS_NAME);
|
||||
if(!reactionDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
const players = this.reactionsMap.get(reactionDiv);
|
||||
if(!players) {
|
||||
return;
|
||||
}
|
||||
|
||||
// do not play select animation when appearing
|
||||
if(!players.appear?.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
const player = players.select;
|
||||
if(!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(player.paused) {
|
||||
player.autoplay = true;
|
||||
player.restart();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default class ChatContextMenu {
|
||||
private buttons: (ButtonMenuItemOptions & {verify: () => boolean, notDirect?: () => boolean, withSelection?: true, isSponsored?: true})[];
|
||||
@ -40,116 +242,27 @@ export default class ChatContextMenu {
|
||||
private isTextSelected: boolean;
|
||||
private isAnchorTarget: boolean;
|
||||
private isUsernameTarget: boolean;
|
||||
private isSponsored: boolean;
|
||||
private isOverBubble: boolean;
|
||||
private peerId: PeerId;
|
||||
private mid: number;
|
||||
private message: Message.message | Message.messageService;
|
||||
private noForwards: boolean;
|
||||
|
||||
constructor(private attachTo: HTMLElement,
|
||||
private reactionsMenu: ChatReactionsMenu;
|
||||
private listenerSetter: ListenerSetter;
|
||||
|
||||
constructor(
|
||||
private attachTo: HTMLElement,
|
||||
private chat: Chat,
|
||||
private appMessagesManager: AppMessagesManager,
|
||||
private appPeersManager: AppPeersManager,
|
||||
private appPollsManager: AppPollsManager,
|
||||
private appDocsManager: AppDocsManager,
|
||||
private appMessagesIdsManager: AppMessagesIdsManager
|
||||
private appMessagesIdsManager: AppMessagesIdsManager,
|
||||
private appReactionsManager: AppReactionsManager
|
||||
) {
|
||||
const onContextMenu = (e: MouseEvent | Touch | TouchEvent) => {
|
||||
if(this.init) {
|
||||
this.init();
|
||||
this.init = null;
|
||||
}
|
||||
|
||||
let bubble: HTMLElement, contentWrapper: HTMLElement;
|
||||
|
||||
try {
|
||||
contentWrapper = findUpClassName(e.target, 'bubble-content-wrapper');
|
||||
bubble = contentWrapper ? contentWrapper.parentElement : findUpClassName(e.target, 'bubble');
|
||||
} catch(e) {}
|
||||
|
||||
// ! context menu click by date bubble (there is no pointer-events)
|
||||
if(!bubble || bubble.classList.contains('bubble-first')) return;
|
||||
|
||||
if(e instanceof MouseEvent || e.hasOwnProperty('preventDefault')) (e as any).preventDefault();
|
||||
if(this.element.classList.contains('active')) {
|
||||
return false;
|
||||
}
|
||||
if(e instanceof MouseEvent || e.hasOwnProperty('cancelBubble')) (e as any).cancelBubble = true;
|
||||
|
||||
let mid = +bubble.dataset.mid;
|
||||
if(!mid) return;
|
||||
|
||||
const isSponsored = mid < 0;
|
||||
this.isSelectable = this.chat.selection.canSelectBubble(bubble);
|
||||
this.peerId = this.chat.peerId;
|
||||
//this.msgID = msgID;
|
||||
this.target = e.target as HTMLElement;
|
||||
this.isTextSelected = !isSelectionEmpty();
|
||||
this.isAnchorTarget = this.target.tagName === 'A' && (
|
||||
(this.target as HTMLAnchorElement).target === '_blank' ||
|
||||
this.target.classList.contains('anchor-url')
|
||||
);
|
||||
this.isUsernameTarget = this.target.tagName === 'A' && this.target.classList.contains('mention');
|
||||
|
||||
// * если открыть контекстное меню для альбома не по бабблу, и последний элемент не выбран, чтобы показать остальные пункты
|
||||
if(chat.selection.isSelecting && !contentWrapper) {
|
||||
if(isSponsored) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mids = this.chat.getMidsByMid(mid);
|
||||
if(mids.length > 1) {
|
||||
const selectedMid = this.chat.selection.isMidSelected(this.peerId, mid) ?
|
||||
mid :
|
||||
mids.find(mid => this.chat.selection.isMidSelected(this.peerId, mid));
|
||||
if(selectedMid) {
|
||||
mid = selectedMid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const groupedItem = findUpClassName(this.target, 'grouped-item');
|
||||
this.isTargetAGroupedItem = !!groupedItem;
|
||||
if(groupedItem) {
|
||||
this.mid = +groupedItem.dataset.mid;
|
||||
} else {
|
||||
this.mid = mid;
|
||||
}
|
||||
|
||||
this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid);
|
||||
this.message = this.chat.getMessage(this.mid);
|
||||
if(isSponsored) {
|
||||
this.buttons.forEach(button => {
|
||||
button.element.classList.toggle('hide', !button.isSponsored);
|
||||
});
|
||||
} else {
|
||||
this.noForwards = !this.appMessagesManager.canForward(this.message);
|
||||
|
||||
this.buttons.forEach(button => {
|
||||
let good: boolean;
|
||||
|
||||
//if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) {
|
||||
if(chat.selection.isSelecting && !button.withSelection) {
|
||||
good = false;
|
||||
} else {
|
||||
good = contentWrapper || IS_TOUCH_SUPPORTED || true ?
|
||||
button.verify() :
|
||||
button.notDirect && button.verify() && button.notDirect();
|
||||
}
|
||||
|
||||
button.element.classList.toggle('hide', !good);
|
||||
});
|
||||
}
|
||||
|
||||
const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right';
|
||||
//bubble.parentElement.append(this.element);
|
||||
//appImManager.log('contextmenu', e, bubble, side);
|
||||
positionMenu((e as TouchEvent).touches ? (e as TouchEvent).touches[0] : e as MouseEvent, this.element, side);
|
||||
openBtnMenu(this.element, () => {
|
||||
this.mid = 0;
|
||||
this.peerId = undefined;
|
||||
this.target = null;
|
||||
});
|
||||
};
|
||||
this.listenerSetter = new ListenerSetter();
|
||||
|
||||
if(IS_TOUCH_SUPPORTED/* && false */) {
|
||||
attachClickEvent(attachTo, (e) => {
|
||||
@ -167,13 +280,129 @@ export default class ChatContextMenu {
|
||||
cancelEvent(e);
|
||||
//onContextMenu((e as TouchEvent).changedTouches[0]);
|
||||
// onContextMenu((e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0] : e as MouseEvent);
|
||||
onContextMenu(e);
|
||||
this.onContextMenu(e);
|
||||
}
|
||||
}, {listenerSetter: this.chat.bubbles.listenerSetter});
|
||||
} else attachContextMenuListener(attachTo, onContextMenu, this.chat.bubbles.listenerSetter);
|
||||
} else attachContextMenuListener(attachTo, this.onContextMenu, this.chat.bubbles.listenerSetter);
|
||||
}
|
||||
|
||||
private init() {
|
||||
private onContextMenu = (e: MouseEvent | Touch | TouchEvent) => {
|
||||
let bubble: HTMLElement, contentWrapper: HTMLElement;
|
||||
|
||||
try {
|
||||
contentWrapper = findUpClassName(e.target, 'bubble-content-wrapper');
|
||||
bubble = contentWrapper ? contentWrapper.parentElement : findUpClassName(e.target, 'bubble');
|
||||
} catch(e) {}
|
||||
|
||||
// ! context menu click by date bubble (there is no pointer-events)
|
||||
if(!bubble || bubble.classList.contains('bubble-first')) return;
|
||||
|
||||
let element = this.element;
|
||||
if(e instanceof MouseEvent || e.hasOwnProperty('preventDefault')) (e as any).preventDefault();
|
||||
if(element && element.classList.contains('active')) {
|
||||
return false;
|
||||
}
|
||||
if(e instanceof MouseEvent || e.hasOwnProperty('cancelBubble')) (e as any).cancelBubble = true;
|
||||
|
||||
let mid = +bubble.dataset.mid;
|
||||
if(!mid) return;
|
||||
|
||||
const isSponsored = this.isSponsored = mid < 0;
|
||||
this.isSelectable = this.chat.selection.canSelectBubble(bubble);
|
||||
this.peerId = this.chat.peerId;
|
||||
//this.msgID = msgID;
|
||||
this.target = e.target as HTMLElement;
|
||||
this.isTextSelected = !isSelectionEmpty();
|
||||
this.isAnchorTarget = this.target.tagName === 'A' && (
|
||||
(this.target as HTMLAnchorElement).target === '_blank' ||
|
||||
this.target.classList.contains('anchor-url')
|
||||
);
|
||||
this.isUsernameTarget = this.target.tagName === 'A' && this.target.classList.contains('mention');
|
||||
|
||||
// * если открыть контекстное меню для альбома не по бабблу, и последний элемент не выбран, чтобы показать остальные пункты
|
||||
if(this.chat.selection.isSelecting && !contentWrapper) {
|
||||
if(isSponsored) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mids = this.chat.getMidsByMid(mid);
|
||||
if(mids.length > 1) {
|
||||
const selectedMid = this.chat.selection.isMidSelected(this.peerId, mid) ?
|
||||
mid :
|
||||
mids.find(mid => this.chat.selection.isMidSelected(this.peerId, mid));
|
||||
if(selectedMid) {
|
||||
mid = selectedMid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isOverBubble = !!contentWrapper;
|
||||
|
||||
const groupedItem = findUpClassName(this.target, 'grouped-item');
|
||||
this.isTargetAGroupedItem = !!groupedItem;
|
||||
if(groupedItem) {
|
||||
this.mid = +groupedItem.dataset.mid;
|
||||
} else {
|
||||
this.mid = mid;
|
||||
}
|
||||
|
||||
this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid);
|
||||
this.message = this.chat.getMessage(this.mid);
|
||||
this.noForwards = !isSponsored && !this.appMessagesManager.canForward(this.message);
|
||||
|
||||
const initResult = this.init();
|
||||
element = initResult.element;
|
||||
const {cleanup, destroy} = initResult;
|
||||
|
||||
const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right';
|
||||
//bubble.parentElement.append(element);
|
||||
//appImManager.log('contextmenu', e, bubble, side);
|
||||
positionMenu((e as TouchEvent).touches ? (e as TouchEvent).touches[0] : e as MouseEvent, element, side);
|
||||
openBtnMenu(element, () => {
|
||||
this.mid = 0;
|
||||
this.peerId = undefined;
|
||||
this.target = null;
|
||||
cleanup();
|
||||
|
||||
setTimeout(() => {
|
||||
destroy();
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
public cleanup() {
|
||||
this.listenerSetter.removeAll();
|
||||
this.reactionsMenu && this.reactionsMenu.cleanup();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
private filterButtons(buttons: ChatContextMenu['buttons']) {
|
||||
if(this.isSponsored) {
|
||||
return buttons.filter(button => {
|
||||
return button.isSponsored;
|
||||
});
|
||||
} else {
|
||||
return buttons.filter(button => {
|
||||
let good: boolean;
|
||||
|
||||
//if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) {
|
||||
if(this.chat.selection.isSelecting && !button.withSelection) {
|
||||
good = false;
|
||||
} else {
|
||||
good = this.isOverBubble || IS_TOUCH_SUPPORTED || true ?
|
||||
button.verify() :
|
||||
button.notDirect && button.verify() && button.notDirect();
|
||||
}
|
||||
|
||||
return good;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private setButtons() {
|
||||
this.buttons = [{
|
||||
icon: 'send2',
|
||||
text: 'MessageScheduleSend',
|
||||
@ -382,11 +611,32 @@ export default class ChatContextMenu {
|
||||
verify: () => false,
|
||||
isSponsored: true
|
||||
}];
|
||||
}
|
||||
|
||||
this.element = ButtonMenu(this.buttons, this.chat.bubbles.listenerSetter);
|
||||
this.element.id = 'bubble-contextmenu';
|
||||
this.element.classList.add('contextmenu');
|
||||
this.chat.container.append(this.element);
|
||||
private init() {
|
||||
this.cleanup();
|
||||
this.setButtons();
|
||||
|
||||
const filteredButtons = this.filterButtons(this.buttons);
|
||||
const element = this.element = ButtonMenu(filteredButtons, this.listenerSetter);
|
||||
element.id = 'bubble-contextmenu';
|
||||
element.classList.add('contextmenu');
|
||||
|
||||
const reactionsMenu = this.reactionsMenu = new ChatReactionsMenu(this.appReactionsManager);
|
||||
element.prepend(reactionsMenu.container);
|
||||
|
||||
this.chat.container.append(element);
|
||||
|
||||
return {
|
||||
element,
|
||||
cleanup: () => {
|
||||
this.cleanup();
|
||||
reactionsMenu.cleanup();
|
||||
},
|
||||
destroy: () => {
|
||||
element.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private onSendScheduledClick = () => {
|
||||
|
@ -55,12 +55,25 @@ const scrollsIntersector = new IntersectionObserver(entries => {
|
||||
export class ScrollableBase {
|
||||
protected log: ReturnType<typeof logger>;
|
||||
|
||||
public splitUp: HTMLElement;
|
||||
public onScrollMeasure: number = 0;
|
||||
protected onScroll: () => void;
|
||||
|
||||
public lastScrollPosition: number = 0;
|
||||
public lastScrollDirection: number = 0;
|
||||
|
||||
public onAdditionalScroll: () => void;
|
||||
public onScrolledTop: () => void;
|
||||
public onScrolledBottom: () => void;
|
||||
|
||||
public isHeavyAnimationInProgress = false;
|
||||
protected needCheckAfterAnimation = false;
|
||||
|
||||
public checkForTriggers?: () => void;
|
||||
|
||||
public scrollProperty: 'scrollTop' | 'scrollLeft';
|
||||
|
||||
private removeHeavyAnimationListener: () => void;
|
||||
|
||||
constructor(public el: HTMLElement, logPrefix = '', public container: HTMLElement = document.createElement('div')) {
|
||||
this.container.classList.add('scrollable');
|
||||
|
||||
@ -74,11 +87,15 @@ export class ScrollableBase {
|
||||
//this.onScroll();
|
||||
}
|
||||
|
||||
protected setListeners() {
|
||||
public setListeners() {
|
||||
if(this.removeHeavyAnimationListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.onScroll, {passive: true});
|
||||
this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true});
|
||||
|
||||
useHeavyAnimationCheck(() => {
|
||||
this.removeHeavyAnimationListener = useHeavyAnimationCheck(() => {
|
||||
this.isHeavyAnimationInProgress = true;
|
||||
|
||||
if(this.onScrollMeasure) {
|
||||
@ -95,6 +112,17 @@ export class ScrollableBase {
|
||||
});
|
||||
}
|
||||
|
||||
public removeListeners() {
|
||||
if(!this.removeHeavyAnimationListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', this.onScroll);
|
||||
this.container.removeEventListener('scroll', this.onScroll, {capture: true});
|
||||
|
||||
this.removeHeavyAnimationListener();
|
||||
}
|
||||
|
||||
public append(element: HTMLElement) {
|
||||
this.container.append(element);
|
||||
}
|
||||
@ -106,42 +134,6 @@ export class ScrollableBase {
|
||||
container: this.container
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type SliceSides = 'top' | 'bottom';
|
||||
export type SliceSidesContainer = {[k in SliceSides]: boolean};
|
||||
|
||||
export default class Scrollable extends ScrollableBase {
|
||||
public splitUp: HTMLElement;
|
||||
public padding: HTMLElement;
|
||||
|
||||
public onAdditionalScroll: () => void;
|
||||
public onScrolledTop: () => void;
|
||||
public onScrolledBottom: () => void;
|
||||
|
||||
public lastScrollTop: number = 0;
|
||||
public lastScrollDirection: number = 0;
|
||||
|
||||
public loadedAll: SliceSidesContainer = {top: true, bottom: false};
|
||||
|
||||
constructor(el: HTMLElement, logPrefix = '', public onScrollOffset = 300, withPaddingContainer?: boolean) {
|
||||
super(el, logPrefix);
|
||||
|
||||
/* if(withPaddingContainer) {
|
||||
this.padding = document.createElement('div');
|
||||
this.padding.classList.add('scrollable-padding');
|
||||
Array.from(this.container.children).forEach(c => this.padding.append(c));
|
||||
this.container.append(this.padding);
|
||||
} */
|
||||
|
||||
this.container.classList.add('scrollable-y');
|
||||
this.setListeners();
|
||||
}
|
||||
|
||||
public setVirtualContainer(el?: HTMLElement) {
|
||||
this.splitUp = el;
|
||||
this.log('setVirtualContainer:', el, this);
|
||||
}
|
||||
|
||||
public onScroll = () => {
|
||||
//if(this.debug) {
|
||||
@ -165,9 +157,9 @@ export default class Scrollable extends ScrollableBase {
|
||||
this.onScrollMeasure = window.requestAnimationFrame(() => {
|
||||
this.onScrollMeasure = 0;
|
||||
|
||||
const scrollTop = this.container.scrollTop;
|
||||
this.lastScrollDirection = this.lastScrollTop === scrollTop ? 0 : (this.lastScrollTop < scrollTop ? 1 : -1); // * 1 - bottom, -1 - top
|
||||
this.lastScrollTop = scrollTop;
|
||||
const scrollPosition = this.container[this.scrollProperty];
|
||||
this.lastScrollDirection = this.lastScrollPosition === scrollPosition ? 0 : (this.lastScrollPosition < scrollPosition ? 1 : -1); // * 1 - bottom, -1 - top
|
||||
this.lastScrollPosition = scrollPosition;
|
||||
|
||||
if(this.onAdditionalScroll && this.lastScrollDirection !== 0) {
|
||||
this.onAdditionalScroll();
|
||||
@ -178,6 +170,35 @@ export default class Scrollable extends ScrollableBase {
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export type SliceSides = 'top' | 'bottom';
|
||||
export type SliceSidesContainer = {[k in SliceSides]: boolean};
|
||||
|
||||
export default class Scrollable extends ScrollableBase {
|
||||
public padding: HTMLElement;
|
||||
|
||||
public loadedAll: SliceSidesContainer = {top: true, bottom: false};
|
||||
|
||||
constructor(el: HTMLElement, logPrefix = '', public onScrollOffset = 300, withPaddingContainer?: boolean) {
|
||||
super(el, logPrefix);
|
||||
|
||||
/* if(withPaddingContainer) {
|
||||
this.padding = document.createElement('div');
|
||||
this.padding.classList.add('scrollable-padding');
|
||||
Array.from(this.container.children).forEach(c => this.padding.append(c));
|
||||
this.container.append(this.padding);
|
||||
} */
|
||||
|
||||
this.container.classList.add('scrollable-y');
|
||||
this.setListeners();
|
||||
this.scrollProperty = 'scrollTop';
|
||||
}
|
||||
|
||||
public setVirtualContainer(el?: HTMLElement) {
|
||||
this.splitUp = el;
|
||||
this.log('setVirtualContainer:', el, this);
|
||||
}
|
||||
|
||||
public checkForTriggers = () => {
|
||||
if((!this.onScrolledTop && !this.onScrolledBottom)) return;
|
||||
@ -194,7 +215,7 @@ export default class Scrollable extends ScrollableBase {
|
||||
|
||||
const clientHeight = this.container.clientHeight;
|
||||
const maxScrollTop = scrollHeight - clientHeight;
|
||||
const scrollTop = this.lastScrollTop;
|
||||
const scrollTop = this.lastScrollPosition;
|
||||
|
||||
//this.log('checkForTriggers:', scrollTop, maxScrollTop);
|
||||
|
||||
@ -253,5 +274,7 @@ export class ScrollableX extends ScrollableBase {
|
||||
|
||||
this.container.addEventListener('wheel', scrollHorizontally, {passive: false});
|
||||
}
|
||||
|
||||
this.scrollProperty = 'scrollLeft';
|
||||
}
|
||||
}
|
||||
|
@ -20,12 +20,13 @@ import appStickersManager from "../../../lib/appManagers/appStickersManager";
|
||||
import assumeType from "../../../helpers/assumeType";
|
||||
import { MessagesAllStickers, StickerSet } from "../../../layer";
|
||||
import RichTextProcessor from "../../../lib/richtextprocessor";
|
||||
import { wrapStickerSetThumb } from "../../wrappers";
|
||||
import { wrapSticker, wrapStickerSetThumb } from "../../wrappers";
|
||||
import LazyLoadQueue from "../../lazyLoadQueue";
|
||||
import PopupStickers from "../../popups/stickers";
|
||||
import eachMinute from "../../../helpers/eachMinute";
|
||||
import { SliderSuperTabEventable } from "../../sliderTab";
|
||||
import IS_GEOLOCATION_SUPPORTED from "../../../environment/geolocationSupport";
|
||||
import appReactionsManager from "../../../lib/appManagers/appReactionsManager";
|
||||
|
||||
export class RangeSettingSelector {
|
||||
public container: HTMLDivElement;
|
||||
@ -285,6 +286,28 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
|
||||
{
|
||||
const container = section('Telegram.InstalledStickerPacksController');
|
||||
|
||||
const reactionsRow = new Row({
|
||||
titleLangKey: 'Reactions',
|
||||
havePadding: true,
|
||||
clickable: () => {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const quickReactionMediaDiv = document.createElement('div');
|
||||
quickReactionMediaDiv.classList.add('row-media', 'row-media-small');
|
||||
|
||||
appReactionsManager.getQuickReaction().then(reaction => {
|
||||
wrapSticker({
|
||||
div: quickReactionMediaDiv,
|
||||
doc: reaction.static_icon,
|
||||
width: 32,
|
||||
height: 32
|
||||
});
|
||||
});
|
||||
|
||||
reactionsRow.container.append(quickReactionMediaDiv);
|
||||
|
||||
const suggestCheckboxField = new CheckboxField({
|
||||
text: 'Stickers.SuggestStickers',
|
||||
name: 'suggest',
|
||||
@ -356,7 +379,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
|
||||
}
|
||||
});
|
||||
|
||||
container.append(suggestCheckboxField.label, loopCheckboxField.label);
|
||||
container.append(reactionsRow.container, suggestCheckboxField.label, loopCheckboxField.label);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1118,7 +1118,7 @@ export function renderImageWithFadeIn(container: HTMLElement,
|
||||
// });
|
||||
// }
|
||||
|
||||
export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn, needUpscale}: {
|
||||
export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn, needUpscale, skipRatio}: {
|
||||
doc: MyDocument,
|
||||
div: HTMLElement,
|
||||
middleware?: () => boolean,
|
||||
@ -1133,7 +1133,8 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
|
||||
loop?: boolean,
|
||||
loadPromises?: Promise<any>[],
|
||||
needFadeIn?: boolean,
|
||||
needUpscale?: boolean
|
||||
needUpscale?: boolean,
|
||||
skipRatio?: number
|
||||
}): Promise<RLottiePlayer | void> {
|
||||
const stickerType = doc.sticker;
|
||||
|
||||
@ -1280,7 +1281,9 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
|
||||
.then(async(json) => {
|
||||
//console.timeEnd('download sticker' + doc.id);
|
||||
//console.log('loaded sticker:', doc, div/* , blob */);
|
||||
if(middleware && !middleware()) return;
|
||||
if(middleware && !middleware()) {
|
||||
throw new Error('wrapSticker 2 middleware');
|
||||
}
|
||||
|
||||
let animation = await LottieLoader.loadAnimationWorker({
|
||||
container: div,
|
||||
@ -1290,14 +1293,17 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
|
||||
width,
|
||||
height,
|
||||
name: 'doc' + doc.id,
|
||||
needUpscale
|
||||
}, group, toneIndex);
|
||||
needUpscale,
|
||||
skipRatio
|
||||
}, group, toneIndex, middleware);
|
||||
|
||||
//const deferred = deferredPromise<void>();
|
||||
|
||||
animation.addEventListener('firstFrame', () => {
|
||||
const element = div.firstElementChild;
|
||||
needFadeIn = (needFadeIn || !element || element.tagName === 'svg') && rootScope.settings.animationsEnabled;
|
||||
if(needFadeIn !== false) {
|
||||
needFadeIn = (needFadeIn || !element || element.tagName === 'svg') && rootScope.settings.animationsEnabled;
|
||||
}
|
||||
|
||||
const cb = () => {
|
||||
if(element && element !== animation.canvas) {
|
||||
@ -1325,7 +1331,9 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
|
||||
});
|
||||
}
|
||||
|
||||
appDocsManager.saveLottiePreview(doc, animation.canvas, toneIndex);
|
||||
if(withThumb !== false) {
|
||||
appDocsManager.saveLottiePreview(doc, animation.canvas, toneIndex);
|
||||
}
|
||||
|
||||
//deferred.resolve();
|
||||
}, {once: true});
|
||||
@ -1511,7 +1519,9 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
|
||||
} else if(stickerType === 1) {
|
||||
const image = new Image();
|
||||
const thumbImage = div.firstElementChild !== image && div.firstElementChild;
|
||||
needFadeIn = (needFadeIn || !downloaded || thumbImage) && rootScope.settings.animationsEnabled;
|
||||
if(needFadeIn !== false) {
|
||||
needFadeIn = (needFadeIn || !downloaded || thumbImage) && rootScope.settings.animationsEnabled;
|
||||
}
|
||||
|
||||
image.classList.add('media-sticker');
|
||||
|
||||
|
@ -13,9 +13,13 @@ export function readBlobAs(blob: Blob, method: 'readAsText'): Promise<string>;
|
||||
export function readBlobAs(blob: Blob, method: 'readAsDataURL'): Promise<string>;
|
||||
export function readBlobAs(blob: Blob, method: 'readAsArrayBuffer'): Promise<ArrayBuffer>;
|
||||
export function readBlobAs(blob: Blob, method: 'readAsArrayBuffer' | 'readAsText' | 'readAsDataURL'): Promise<any> {
|
||||
// const perf = performance.now();
|
||||
return new Promise<any>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('loadend', (e) => resolve(e.target.result));
|
||||
reader.addEventListener('loadend', (e) => {
|
||||
// console.log('readBlobAs time:', method, performance.now() - perf);
|
||||
resolve(e.target.result);
|
||||
});
|
||||
reader[method](blob);
|
||||
});
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ export default function getVisibleRect(element: HTMLElement, overflowElement: HT
|
||||
const rect = element.getBoundingClientRect();
|
||||
const overflowRect = overflowElement.getBoundingClientRect();
|
||||
|
||||
let {top: overflowTop, bottom: overflowBottom} = overflowRect;
|
||||
let {top: overflowTop, right: overflowRight, bottom: overflowBottom, left: overflowLeft} = overflowRect;
|
||||
|
||||
// * respect sticky headers
|
||||
if(lookForSticky) {
|
||||
@ -21,8 +21,8 @@ export default function getVisibleRect(element: HTMLElement, overflowElement: HT
|
||||
|
||||
if(rect.top >= overflowBottom
|
||||
|| rect.bottom <= overflowTop
|
||||
|| rect.right <= overflowRect.left
|
||||
|| rect.left >= overflowRect.right) {
|
||||
|| rect.right <= overflowLeft
|
||||
|| rect.left >= overflowRight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -43,9 +43,9 @@ export default function getVisibleRect(element: HTMLElement, overflowElement: HT
|
||||
return {
|
||||
rect: {
|
||||
top: rect.top < overflowTop && overflowTop !== 0 ? (overflow.top = true, ++overflow.vertical, overflowTop) : rect.top,
|
||||
right: 0,
|
||||
right: rect.right > overflowRight && overflowRight !== windowWidth ? (overflow.right = true, ++overflow.horizontal, overflowRight) : rect.right,
|
||||
bottom: rect.bottom > overflowBottom && overflowBottom !== windowHeight ? (overflow.bottom = true, ++overflow.vertical, overflowBottom) : rect.bottom,
|
||||
left: 0
|
||||
left: rect.left < overflowLeft && overflowLeft !== 0 ? (overflow.left = true, ++overflow.horizontal, overflowLeft) : rect.left
|
||||
},
|
||||
overflow
|
||||
};
|
||||
|
@ -640,6 +640,8 @@ const lang = {
|
||||
"RequestToJoinGroupApproved": "Your request to join the group was approved",
|
||||
"RequestToJoinChannelApproved": "Your request to join the channel was approved",
|
||||
"Update": "UPDATE",
|
||||
"Reactions": "Reactions",
|
||||
"DoubleTapSetting": "Quick Reaction",
|
||||
|
||||
// * macos
|
||||
"AccountSettings.Filters": "Chat Folders",
|
||||
|
18
src/layer.d.ts
vendored
18
src/layer.d.ts
vendored
@ -866,7 +866,6 @@ export namespace Message {
|
||||
replies?: MessageReplies,
|
||||
edit_date?: number,
|
||||
post_author?: string,
|
||||
grouped_id?: string,
|
||||
reactions?: MessageReactions,
|
||||
restriction_reason?: Array<RestrictionReason>,
|
||||
ttl_period?: number,
|
||||
@ -875,6 +874,7 @@ export namespace Message {
|
||||
peerId?: PeerId,
|
||||
fromId?: PeerId,
|
||||
fwdFromId?: PeerId,
|
||||
grouped_id?: string,
|
||||
random_id?: string,
|
||||
rReply?: string,
|
||||
viaBotId?: PeerId,
|
||||
@ -3224,10 +3224,10 @@ export namespace Document {
|
||||
date: number,
|
||||
mime_type: string,
|
||||
size: number,
|
||||
thumbs?: Array<PhotoSize.photoSize | PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize | PhotoSize.photoPathSize>,
|
||||
video_thumbs?: Array<VideoSize>,
|
||||
dc_id: number,
|
||||
attributes: Array<DocumentAttribute>,
|
||||
thumbs?: Array<PhotoSize.photoSize | PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize | PhotoSize.photoPathSize>,
|
||||
type?: 'gif' | 'sticker' | 'audio' | 'voice' | 'video' | 'round' | 'photo' | 'pdf',
|
||||
h?: number,
|
||||
w?: number,
|
||||
@ -9355,13 +9355,13 @@ export namespace AvailableReaction {
|
||||
}>,
|
||||
reaction: string,
|
||||
title: string,
|
||||
static_icon: Document,
|
||||
appear_animation: Document,
|
||||
select_animation: Document,
|
||||
activate_animation: Document,
|
||||
effect_animation: Document,
|
||||
around_animation?: Document,
|
||||
center_icon?: Document
|
||||
static_icon: Document.document,
|
||||
appear_animation: Document.document,
|
||||
select_animation: Document.document,
|
||||
activate_animation: Document.document,
|
||||
effect_animation: Document.document,
|
||||
around_animation: Document.document,
|
||||
center_icon: Document.document
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -83,6 +83,7 @@ import { Modify, SendMessageEmojiInteractionData } from '../../types';
|
||||
import htmlToSpan from '../../helpers/dom/htmlToSpan';
|
||||
import getVisibleRect from '../../helpers/dom/getVisibleRect';
|
||||
import { simulateClickEvent } from '../../helpers/dom/clickEvent';
|
||||
import appReactionsManager from './appReactionsManager';
|
||||
|
||||
//console.log('appImManager included33!');
|
||||
|
||||
@ -1427,7 +1428,8 @@ export class AppImManager {
|
||||
appNotificationsManager,
|
||||
appEmojiManager,
|
||||
appMessagesIdsManager,
|
||||
appGroupCallsManager
|
||||
appGroupCallsManager,
|
||||
appReactionsManager
|
||||
);
|
||||
|
||||
if(this.chats.length) {
|
||||
|
77
src/lib/appManagers/appReactionsManager.ts
Normal file
77
src/lib/appManagers/appReactionsManager.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import { MOUNT_CLASS_TO } from "../../config/debug";
|
||||
import assumeType from "../../helpers/assumeType";
|
||||
import { AvailableReaction, MessagesAvailableReactions } from "../../layer";
|
||||
import apiManager from "../mtproto/mtprotoworker";
|
||||
import { ReferenceContext } from "../mtproto/referenceDatabase";
|
||||
import rootScope from "../rootScope";
|
||||
import appDocsManager from "./appDocsManager";
|
||||
|
||||
const SAVE_DOC_KEYS = [
|
||||
'static_icon' as const,
|
||||
'appear_animation' as const,
|
||||
'select_animation' as const,
|
||||
'activate_animation' as const,
|
||||
'effect_animation' as const,
|
||||
'around_animation' as const,
|
||||
'center_icon' as const
|
||||
];
|
||||
|
||||
const REFERENCE_CONTEXXT: ReferenceContext = {
|
||||
type: 'reactions'
|
||||
};
|
||||
|
||||
export class AppReactionsManager {
|
||||
private availableReactions: AvailableReaction[];
|
||||
|
||||
constructor() {
|
||||
rootScope.addEventListener('language_change', () => {
|
||||
this.availableReactions = undefined;
|
||||
this.getAvailableReactions();
|
||||
});
|
||||
}
|
||||
|
||||
public getAvailableReactions() {
|
||||
if(this.availableReactions) return Promise.resolve(this.availableReactions);
|
||||
return apiManager.invokeApiSingleProcess({
|
||||
method: 'messages.getAvailableReactions',
|
||||
processResult: (messagesAvailableReactions) => {
|
||||
assumeType<MessagesAvailableReactions.messagesAvailableReactions>(messagesAvailableReactions);
|
||||
|
||||
const availableReactions = this.availableReactions = messagesAvailableReactions.reactions;
|
||||
for(const reaction of availableReactions) {
|
||||
for(const key of SAVE_DOC_KEYS) {
|
||||
if(!reaction[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
reaction[key] = appDocsManager.saveDoc(reaction[key], REFERENCE_CONTEXXT);
|
||||
}
|
||||
}
|
||||
|
||||
return availableReactions;
|
||||
},
|
||||
params: {
|
||||
hash: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getQuickReaction() {
|
||||
return Promise.all([
|
||||
apiManager.getAppConfig(),
|
||||
this.getAvailableReactions()
|
||||
]).then(([appConfig, availableReactions]) => {
|
||||
return availableReactions.find(reaction => reaction.reaction === appConfig.reactions_default);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const appReactionsManager = new AppReactionsManager();
|
||||
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appReactionsManager = appReactionsManager);
|
||||
export default appReactionsManager;
|
@ -15,7 +15,7 @@ import apiManager from "./mtprotoworker";
|
||||
import assumeType from "../../helpers/assumeType";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export type ReferenceContext = ReferenceContext.referenceContextProfilePhoto | ReferenceContext.referenceContextMessage | ReferenceContext.referenceContextEmojiesSounds;
|
||||
export type ReferenceContext = ReferenceContext.referenceContextProfilePhoto | ReferenceContext.referenceContextMessage | ReferenceContext.referenceContextEmojiesSounds | ReferenceContext.referenceContextReactions;
|
||||
export namespace ReferenceContext {
|
||||
export type referenceContextProfilePhoto = {
|
||||
type: 'profilePhoto',
|
||||
@ -31,6 +31,10 @@ export namespace ReferenceContext {
|
||||
export type referenceContextEmojiesSounds = {
|
||||
type: 'emojiesSounds'
|
||||
};
|
||||
|
||||
export type referenceContextReactions = {
|
||||
type: 'reactions'
|
||||
};
|
||||
}
|
||||
|
||||
export type ReferenceBytes = Photo.photo['file_reference'];
|
||||
|
@ -130,7 +130,7 @@ export class LottieLoader {
|
||||
]).then(() => player);
|
||||
}
|
||||
|
||||
public async loadAnimationWorker(params: RLottieOptions, group = params.group || '', toneIndex = -1): Promise<RLottiePlayer> {
|
||||
public async loadAnimationWorker(params: RLottieOptions, group = params.group || '', toneIndex = -1, middleware?: () => boolean): Promise<RLottiePlayer> {
|
||||
if(!this.isWebAssemblySupported) {
|
||||
return this.loadPromise as any;
|
||||
}
|
||||
@ -150,6 +150,10 @@ export class LottieLoader {
|
||||
await this.loadLottieWorkers();
|
||||
}
|
||||
|
||||
if(middleware && !middleware()) {
|
||||
throw new Error('middleware');
|
||||
}
|
||||
|
||||
if(!params.width || !params.height) {
|
||||
params.width = parseInt(params.container.style.width);
|
||||
params.height = parseInt(params.container.style.height);
|
||||
|
@ -342,6 +342,7 @@ export default class RLottiePlayer extends EventListenerBase<{
|
||||
this.pause();
|
||||
this.sendQuery('destroy');
|
||||
if(this.cacheName) cache.releaseCache(this.cacheName);
|
||||
this.cleanup();
|
||||
//this.removed = true;
|
||||
}
|
||||
|
||||
|
@ -12,23 +12,37 @@ const replace = require(__dirname + '/in/schema_replace_types.json');
|
||||
const mtproto = schema.API;
|
||||
|
||||
for(const constructor of additional) {
|
||||
constructor.params.forEach(param => {
|
||||
const additionalParams = constructor.params || (constructor.params = []);
|
||||
additionalParams.forEach(param => {
|
||||
param.type = 'flags.-1?' + param.type;
|
||||
});
|
||||
|
||||
if(constructor.properties) {
|
||||
additionalParams.push(...constructor.properties);
|
||||
}
|
||||
|
||||
if(constructor.type) {
|
||||
mtproto.constructors.push(constructor);
|
||||
}
|
||||
|
||||
const realConstructor = constructor.type ? constructor : mtproto.constructors.find(c => c.predicate == constructor.predicate);
|
||||
|
||||
if(!constructor.type) {
|
||||
for(let i = realConstructor.params.length - 1; i >= 0; --i) {
|
||||
const param = realConstructor.params[i];
|
||||
if(additionalParams.find(newParam => newParam.name === param.name)) {
|
||||
realConstructor.params.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* constructor.params.forEach(param => {
|
||||
const index = realConstructor.params.findIndex(_param => _param.predicate == param.predicate);
|
||||
if(index !== -1) {
|
||||
realConstructor.params.splice(index, 1);
|
||||
}
|
||||
}); */
|
||||
realConstructor.params.splice(realConstructor.params.length, 0, ...constructor.params);
|
||||
realConstructor.params.splice(realConstructor.params.length, 0, ...additionalParams);
|
||||
}
|
||||
|
||||
['Vector t', 'Bool', 'True', 'Null'].forEach(key => {
|
||||
@ -116,7 +130,7 @@ const processParamType = (type, parseBooleanFlags, overrideTypes) => {
|
||||
default:
|
||||
//console.log('no such type', type);
|
||||
//throw new Error('no such type: ' + type);
|
||||
return isAdditional ? type : camelizeName(type, true);
|
||||
return isAdditional || type[0] === type[0].toUpperCase() ? type : camelizeName(type, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -346,4 +346,15 @@
|
||||
{"name": "action", "type": "MessageAction.messageActionPhoneCall"}
|
||||
],
|
||||
"type": "MessageMedia"
|
||||
}, {
|
||||
"predicate": "availableReaction",
|
||||
"properties": [
|
||||
{"name": "static_icon", "type": "Document.document"},
|
||||
{"name": "appear_animation", "type": "Document.document"},
|
||||
{"name": "select_animation", "type": "Document.document"},
|
||||
{"name": "activate_animation", "type": "Document.document"},
|
||||
{"name": "effect_animation", "type": "Document.document"},
|
||||
{"name": "around_animation", "type": "Document.document"},
|
||||
{"name": "center_icon", "type": "Document.document"}
|
||||
]
|
||||
}]
|
@ -96,7 +96,6 @@
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
background-color: var(--surface-color);
|
||||
box-shadow: 0px 2px 8px 1px rgba(0, 0, 0, .24);
|
||||
z-index: 3;
|
||||
top: 100%;
|
||||
padding: .5rem 0;
|
||||
@ -106,6 +105,11 @@
|
||||
transition: opacity var(--btn-menu-transition), transform var(--btn-menu-transition), visibility var(--btn-menu-transition);
|
||||
font-size: 16px;
|
||||
|
||||
&,
|
||||
&-reactions {
|
||||
box-shadow: 0px 2px 8px 1px rgba(0, 0, 0, .24);
|
||||
}
|
||||
|
||||
body.animation-level-0 & {
|
||||
transition: none;
|
||||
}
|
||||
@ -261,6 +265,79 @@
|
||||
padding: 0;
|
||||
margin: .5rem 0;
|
||||
}
|
||||
|
||||
&-reactions {
|
||||
--height: 2.5rem;
|
||||
height: var(--height);
|
||||
border-radius: 1.25rem;
|
||||
background-color: var(--surface-color);
|
||||
position: absolute;
|
||||
top: calc((var(--height) + .5rem) * -1);
|
||||
max-width: 100%;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
content: " ";
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, var(--surface-color) 0%, transparent 1rem, transparent calc(100% - 1rem), var(--surface-color) 100%);
|
||||
}
|
||||
|
||||
.scrollable-x {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 .25rem;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
&-reaction {
|
||||
width: 2rem;
|
||||
height: 1.5rem;
|
||||
flex: 0 0 auto;
|
||||
padding: 0 .25rem;
|
||||
cursor: pointer;
|
||||
|
||||
&-scale {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: scale(1);
|
||||
|
||||
@include animation-level(2) {
|
||||
transition: transform .1s linear;
|
||||
}
|
||||
}
|
||||
|
||||
&-select {
|
||||
html.no-touch & {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
html.no-touch body.animation-level-2 & {
|
||||
transition: transform var(--transition-standard-in);
|
||||
}
|
||||
|
||||
@include hover() {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
}
|
||||
|
||||
.media-sticker-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0; */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
|
@ -114,6 +114,12 @@
|
||||
position: absolute !important;
|
||||
margin: 0 !important;
|
||||
left: .5rem;
|
||||
|
||||
&-small {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
left: .75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-open {
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit d10c87ef1aec54cdd2e506fcd980ea848e60eedc
|
||||
Subproject commit 2c4d08587b77a388d4beb8bc018dcb56ebd8a589
|
Loading…
x
Reference in New Issue
Block a user