Browse Source

Reactions menu

master
Eduard Kuzmenko 3 years ago
parent
commit
980755bd38
  1. 38
      src/components/animationIntersector.ts
  2. 4
      src/components/chat/bubbles.ts
  3. 7
      src/components/chat/chat.ts
  4. 464
      src/components/chat/contextMenu.ts
  5. 109
      src/components/scrollable.ts
  6. 27
      src/components/sidebarLeft/tabs/generalSettings.ts
  7. 26
      src/components/wrappers.ts
  8. 6
      src/helpers/blob.ts
  9. 10
      src/helpers/dom/getVisibleRect.ts
  10. 2
      src/lang.ts
  11. 18
      src/layer.d.ts
  12. 4
      src/lib/appManagers/appImManager.ts
  13. 77
      src/lib/appManagers/appReactionsManager.ts
  14. 6
      src/lib/mtproto/referenceDatabase.ts
  15. 6
      src/lib/rlottie/lottieLoader.ts
  16. 1
      src/lib/rlottie/rlottiePlayer.ts
  17. 20
      src/scripts/generate_mtproto_types.js
  18. 11
      src/scripts/in/schema_additional_params.json
  19. 79
      src/scss/partials/_button.scss
  20. 6
      src/scss/partials/_row.scss
  21. 2
      tweb-design

38
src/components/animationIntersector.ts

@ -8,7 +8,7 @@ import rootScope from "../lib/rootScope";
import { IS_SAFARI } from "../environment/userAgent"; import { IS_SAFARI } from "../environment/userAgent";
import { MOUNT_CLASS_TO } from "../config/debug"; import { MOUNT_CLASS_TO } from "../config/debug";
import isInDOM from "../helpers/dom/isInDOM"; import isInDOM from "../helpers/dom/isInDOM";
import { indexOfAndSplice } from "../helpers/array"; import { forEachReverse, indexOfAndSplice } from "../helpers/array";
import RLottiePlayer from "../lib/rlottie/rlottiePlayer"; import RLottiePlayer from "../lib/rlottie/rlottiePlayer";
export interface AnimationItem { export interface AnimationItem {
@ -18,9 +18,10 @@ export interface AnimationItem {
}; };
export class AnimationIntersector { export class AnimationIntersector {
public observer: IntersectionObserver; private observer: IntersectionObserver;
private visible: Set<AnimationItem> = new Set(); private visible: Set<AnimationItem> = new Set();
private overrideIdleGroups: Set<string>;
private byGroups: {[group: string]: AnimationItem[]} = {}; private byGroups: {[group: string]: AnimationItem[]} = {};
private lockedGroups: {[group: string]: true} = {}; private lockedGroups: {[group: string]: true} = {};
private onlyOnePlayableGroup: string = ''; private onlyOnePlayableGroup: string = '';
@ -30,7 +31,7 @@ export class AnimationIntersector {
constructor() { constructor() {
this.observer = new IntersectionObserver((entries) => { this.observer = new IntersectionObserver((entries) => {
if(rootScope.idle.isIDLE) return; // if(rootScope.idle.isIDLE) return;
for(const entry of entries) { for(const entry of entries) {
const target = entry.target; const target = entry.target;
@ -61,6 +62,8 @@ export class AnimationIntersector {
} }
}); });
this.overrideIdleGroups = new Set();
rootScope.addEventListener('media_play', ({doc}) => { rootScope.addEventListener('media_play', ({doc}) => {
if(doc.type === 'round') { if(doc.type === 'round') {
this.videosLocked = true; 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) { public getAnimations(element: HTMLElement) {
const found: AnimationItem[] = []; const found: AnimationItem[] = [];
for(const group in this.byGroups) { for(const group in this.byGroups) {
@ -101,8 +109,12 @@ export class AnimationIntersector {
}, 1e3); }, 1e3);
} }
for(const group in this.byGroups) { const group = this.byGroups[player.group];
indexOfAndSplice(this.byGroups[group], player); if(group) {
indexOfAndSplice(group, player);
if(!group.length) {
delete this.byGroups[player.group];
}
} }
this.observer.unobserve(el); this.observer.unobserve(el);
@ -127,20 +139,19 @@ export class AnimationIntersector {
} }
public checkAnimations(blurred?: boolean, group?: string, destroy = false) { public checkAnimations(blurred?: boolean, group?: string, destroy = false) {
if(rootScope.idle.isIDLE) return; // if(rootScope.idle.isIDLE) return;
const groups = group /* && false */ ? [group] : Object.keys(this.byGroups);
if(group && !this.byGroups[group]) { if(group !== undefined && !this.byGroups[group]) {
//console.warn('no animation group:', group); //console.warn('no animation group:', group);
this.byGroups[group] = [];
return; return;
} }
const groups = group !== undefined /* && false */ ? [group] : Object.keys(this.byGroups);
for(const group of groups) { for(const group of groups) {
const animations = this.byGroups[group]; const animations = this.byGroups[group];
animations.forEach(player => { forEachReverse(animations, (player) => {
this.checkAnimation(player, blurred, destroy); this.checkAnimation(player, blurred, destroy);
}); });
} }
@ -162,7 +173,8 @@ export class AnimationIntersector {
} else if(animation.paused && } else if(animation.paused &&
this.visible.has(player) && this.visible.has(player) &&
animation.autoplay && 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); //console.warn('play animation:', animation);
animation.play(); animation.play();
@ -211,4 +223,4 @@ const animationIntersector = new AnimationIntersector();
if(MOUNT_CLASS_TO) { if(MOUNT_CLASS_TO) {
MOUNT_CLASS_TO.animationIntersector = animationIntersector; MOUNT_CLASS_TO.animationIntersector = animationIntersector;
} }
export default animationIntersector; export default animationIntersector;

4
src/components/chat/bubbles.ts

@ -2196,7 +2196,7 @@ export default class ChatBubbles {
// this.ladderDeferred.resolve(); // this.ladderDeferred.resolve();
scrollable.lastScrollDirection = 0; scrollable.lastScrollDirection = 0;
scrollable.lastScrollTop = 0; scrollable.lastScrollPosition = 0;
replaceContent(scrollable.container, chatInner); replaceContent(scrollable.container, chatInner);
animationIntersector.unlockGroup(CHAT_ANIMATION_GROUP); animationIntersector.unlockGroup(CHAT_ANIMATION_GROUP);
@ -3633,7 +3633,7 @@ export default class ChatBubbles {
//this.scrollable.scrollTop = this.scrollable.scrollHeight; //this.scrollable.scrollTop = this.scrollable.scrollHeight;
//isTouchSupported && isApple && (this.scrollable.container.style.overflow = ''); //isTouchSupported && isApple && (this.scrollable.container.style.overflow = '');
this.scrollable.lastScrollTop = newScrollTop; this.scrollable.lastScrollPosition = newScrollTop;
this.scrollable.lastScrollDirection = 0; this.scrollable.lastScrollDirection = 0;
if(IS_SAFARI/* && !isAppleMobile */) { // * fix blinking and jumping if(IS_SAFARI/* && !isAppleMobile */) { // * fix blinking and jumping

7
src/components/chat/chat.ts

@ -23,6 +23,7 @@ import type { AppEmojiManager } from "../../lib/appManagers/appEmojiManager";
import type { ServerTimeManager } from "../../lib/mtproto/serverTimeManager"; import type { ServerTimeManager } from "../../lib/mtproto/serverTimeManager";
import type { AppMessagesIdsManager } from "../../lib/appManagers/appMessagesIdsManager"; import type { AppMessagesIdsManager } from "../../lib/appManagers/appMessagesIdsManager";
import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager"; import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager";
import type { AppReactionsManager } from "../../lib/appManagers/appReactionsManager";
import type { State } from "../../lib/appManagers/appStateManager"; import type { State } from "../../lib/appManagers/appStateManager";
import type stateStorage from '../../lib/stateStorage'; import type stateStorage from '../../lib/stateStorage';
import EventListenerBase from "../../helpers/eventListenerBase"; import EventListenerBase from "../../helpers/eventListenerBase";
@ -92,7 +93,8 @@ export default class Chat extends EventListenerBase<{
public appNotificationsManager: AppNotificationsManager, public appNotificationsManager: AppNotificationsManager,
public appEmojiManager: AppEmojiManager, public appEmojiManager: AppEmojiManager,
public appMessagesIdsManager: AppMessagesIdsManager, public appMessagesIdsManager: AppMessagesIdsManager,
public appGroupCallsManager: AppGroupCallsManager public appGroupCallsManager: AppGroupCallsManager,
public appReactionsManager: AppReactionsManager
) { ) {
super(); 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.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.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.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') { if(this.type === 'chat') {
this.topbar.constructUtils(); this.topbar.constructUtils();
@ -240,6 +242,7 @@ export default class Chat extends EventListenerBase<{
this.topbar.destroy(); this.topbar.destroy();
this.bubbles.destroy(); this.bubbles.destroy();
this.input.destroy(); this.input.destroy();
this.contextMenu && this.contextMenu.destroy();
delete this.topbar; delete this.topbar;
delete this.bubbles; delete this.bubbles;

464
src/components/chat/contextMenu.ts

@ -9,6 +9,7 @@ import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type { AppPollsManager } from "../../lib/appManagers/appPollsManager"; import type { AppPollsManager } from "../../lib/appManagers/appPollsManager";
import type { AppDocsManager, MyDocument } from "../../lib/appManagers/appDocsManager"; import type { AppDocsManager, MyDocument } from "../../lib/appManagers/appDocsManager";
import type { AppMessagesIdsManager } from "../../lib/appManagers/appMessagesIdsManager"; import type { AppMessagesIdsManager } from "../../lib/appManagers/appMessagesIdsManager";
import type { AppReactionsManager } from "../../lib/appManagers/appReactionsManager";
import type Chat from "./chat"; import type Chat from "./chat";
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport"; import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu";
@ -24,10 +25,211 @@ import findUpClassName from "../../helpers/dom/findUpClassName";
import { cancelEvent } from "../../helpers/dom/cancelEvent"; import { cancelEvent } from "../../helpers/dom/cancelEvent";
import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent"; import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty"; 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 PopupReportMessages from "../popups/reportMessages";
import assumeType from "../../helpers/assumeType"; import assumeType from "../../helpers/assumeType";
import PopupSponsored from "../popups/sponsored"; 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 { export default class ChatContextMenu {
private buttons: (ButtonMenuItemOptions & {verify: () => boolean, notDirect?: () => boolean, withSelection?: true, isSponsored?: true})[]; private buttons: (ButtonMenuItemOptions & {verify: () => boolean, notDirect?: () => boolean, withSelection?: true, isSponsored?: true})[];
@ -40,116 +242,27 @@ export default class ChatContextMenu {
private isTextSelected: boolean; private isTextSelected: boolean;
private isAnchorTarget: boolean; private isAnchorTarget: boolean;
private isUsernameTarget: boolean; private isUsernameTarget: boolean;
private isSponsored: boolean;
private isOverBubble: boolean;
private peerId: PeerId; private peerId: PeerId;
private mid: number; private mid: number;
private message: Message.message | Message.messageService; private message: Message.message | Message.messageService;
private noForwards: boolean; private noForwards: boolean;
constructor(private attachTo: HTMLElement, private reactionsMenu: ChatReactionsMenu;
private listenerSetter: ListenerSetter;
constructor(
private attachTo: HTMLElement,
private chat: Chat, private chat: Chat,
private appMessagesManager: AppMessagesManager, private appMessagesManager: AppMessagesManager,
private appPeersManager: AppPeersManager, private appPeersManager: AppPeersManager,
private appPollsManager: AppPollsManager, private appPollsManager: AppPollsManager,
private appDocsManager: AppDocsManager, private appDocsManager: AppDocsManager,
private appMessagesIdsManager: AppMessagesIdsManager private appMessagesIdsManager: AppMessagesIdsManager,
private appReactionsManager: AppReactionsManager
) { ) {
const onContextMenu = (e: MouseEvent | Touch | TouchEvent) => { this.listenerSetter = new ListenerSetter();
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;
});
};
if(IS_TOUCH_SUPPORTED/* && false */) { if(IS_TOUCH_SUPPORTED/* && false */) {
attachClickEvent(attachTo, (e) => { attachClickEvent(attachTo, (e) => {
@ -167,13 +280,129 @@ export default class ChatContextMenu {
cancelEvent(e); cancelEvent(e);
//onContextMenu((e as TouchEvent).changedTouches[0]); //onContextMenu((e as TouchEvent).changedTouches[0]);
// onContextMenu((e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0] : e as MouseEvent); // onContextMenu((e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0] : e as MouseEvent);
onContextMenu(e); this.onContextMenu(e);
} }
}, {listenerSetter: this.chat.bubbles.listenerSetter}); }, {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 = [{ this.buttons = [{
icon: 'send2', icon: 'send2',
text: 'MessageScheduleSend', text: 'MessageScheduleSend',
@ -382,11 +611,32 @@ export default class ChatContextMenu {
verify: () => false, verify: () => false,
isSponsored: true isSponsored: true
}]; }];
}
this.element = ButtonMenu(this.buttons, this.chat.bubbles.listenerSetter); private init() {
this.element.id = 'bubble-contextmenu'; this.cleanup();
this.element.classList.add('contextmenu'); this.setButtons();
this.chat.container.append(this.element);
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 = () => { private onSendScheduledClick = () => {

109
src/components/scrollable.ts

@ -55,12 +55,25 @@ const scrollsIntersector = new IntersectionObserver(entries => {
export class ScrollableBase { export class ScrollableBase {
protected log: ReturnType<typeof logger>; protected log: ReturnType<typeof logger>;
public splitUp: HTMLElement;
public onScrollMeasure: number = 0; 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; public isHeavyAnimationInProgress = false;
protected needCheckAfterAnimation = 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')) { constructor(public el: HTMLElement, logPrefix = '', public container: HTMLElement = document.createElement('div')) {
this.container.classList.add('scrollable'); this.container.classList.add('scrollable');
@ -74,11 +87,15 @@ export class ScrollableBase {
//this.onScroll(); //this.onScroll();
} }
protected setListeners() { public setListeners() {
if(this.removeHeavyAnimationListener) {
return;
}
window.addEventListener('resize', this.onScroll, {passive: true}); window.addEventListener('resize', this.onScroll, {passive: true});
this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true}); this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true});
useHeavyAnimationCheck(() => { this.removeHeavyAnimationListener = useHeavyAnimationCheck(() => {
this.isHeavyAnimationInProgress = true; this.isHeavyAnimationInProgress = true;
if(this.onScrollMeasure) { 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) { public append(element: HTMLElement) {
this.container.append(element); this.container.append(element);
} }
@ -106,42 +134,6 @@ export class ScrollableBase {
container: this.container 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 = () => { public onScroll = () => {
//if(this.debug) { //if(this.debug) {
@ -165,9 +157,9 @@ export default class Scrollable extends ScrollableBase {
this.onScrollMeasure = window.requestAnimationFrame(() => { this.onScrollMeasure = window.requestAnimationFrame(() => {
this.onScrollMeasure = 0; this.onScrollMeasure = 0;
const scrollTop = this.container.scrollTop; const scrollPosition = this.container[this.scrollProperty];
this.lastScrollDirection = this.lastScrollTop === scrollTop ? 0 : (this.lastScrollTop < scrollTop ? 1 : -1); // * 1 - bottom, -1 - top this.lastScrollDirection = this.lastScrollPosition === scrollPosition ? 0 : (this.lastScrollPosition < scrollPosition ? 1 : -1); // * 1 - bottom, -1 - top
this.lastScrollTop = scrollTop; this.lastScrollPosition = scrollPosition;
if(this.onAdditionalScroll && this.lastScrollDirection !== 0) { if(this.onAdditionalScroll && this.lastScrollDirection !== 0) {
this.onAdditionalScroll(); 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 = () => { public checkForTriggers = () => {
if((!this.onScrolledTop && !this.onScrolledBottom)) return; if((!this.onScrolledTop && !this.onScrolledBottom)) return;
@ -194,7 +215,7 @@ export default class Scrollable extends ScrollableBase {
const clientHeight = this.container.clientHeight; const clientHeight = this.container.clientHeight;
const maxScrollTop = scrollHeight - clientHeight; const maxScrollTop = scrollHeight - clientHeight;
const scrollTop = this.lastScrollTop; const scrollTop = this.lastScrollPosition;
//this.log('checkForTriggers:', scrollTop, maxScrollTop); //this.log('checkForTriggers:', scrollTop, maxScrollTop);
@ -253,5 +274,7 @@ export class ScrollableX extends ScrollableBase {
this.container.addEventListener('wheel', scrollHorizontally, {passive: false}); this.container.addEventListener('wheel', scrollHorizontally, {passive: false});
} }
this.scrollProperty = 'scrollLeft';
} }
} }

27
src/components/sidebarLeft/tabs/generalSettings.ts

@ -20,12 +20,13 @@ import appStickersManager from "../../../lib/appManagers/appStickersManager";
import assumeType from "../../../helpers/assumeType"; import assumeType from "../../../helpers/assumeType";
import { MessagesAllStickers, StickerSet } from "../../../layer"; import { MessagesAllStickers, StickerSet } from "../../../layer";
import RichTextProcessor from "../../../lib/richtextprocessor"; import RichTextProcessor from "../../../lib/richtextprocessor";
import { wrapStickerSetThumb } from "../../wrappers"; import { wrapSticker, wrapStickerSetThumb } from "../../wrappers";
import LazyLoadQueue from "../../lazyLoadQueue"; import LazyLoadQueue from "../../lazyLoadQueue";
import PopupStickers from "../../popups/stickers"; import PopupStickers from "../../popups/stickers";
import eachMinute from "../../../helpers/eachMinute"; import eachMinute from "../../../helpers/eachMinute";
import { SliderSuperTabEventable } from "../../sliderTab"; import { SliderSuperTabEventable } from "../../sliderTab";
import IS_GEOLOCATION_SUPPORTED from "../../../environment/geolocationSupport"; import IS_GEOLOCATION_SUPPORTED from "../../../environment/geolocationSupport";
import appReactionsManager from "../../../lib/appManagers/appReactionsManager";
export class RangeSettingSelector { export class RangeSettingSelector {
public container: HTMLDivElement; public container: HTMLDivElement;
@ -285,6 +286,28 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
{ {
const container = section('Telegram.InstalledStickerPacksController'); 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({ const suggestCheckboxField = new CheckboxField({
text: 'Stickers.SuggestStickers', text: 'Stickers.SuggestStickers',
name: 'suggest', 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);
} }
} }

26
src/components/wrappers.ts

@ -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, doc: MyDocument,
div: HTMLElement, div: HTMLElement,
middleware?: () => boolean, middleware?: () => boolean,
@ -1133,7 +1133,8 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
loop?: boolean, loop?: boolean,
loadPromises?: Promise<any>[], loadPromises?: Promise<any>[],
needFadeIn?: boolean, needFadeIn?: boolean,
needUpscale?: boolean needUpscale?: boolean,
skipRatio?: number
}): Promise<RLottiePlayer | void> { }): Promise<RLottiePlayer | void> {
const stickerType = doc.sticker; const stickerType = doc.sticker;
@ -1280,7 +1281,9 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
.then(async(json) => { .then(async(json) => {
//console.timeEnd('download sticker' + doc.id); //console.timeEnd('download sticker' + doc.id);
//console.log('loaded sticker:', doc, div/* , blob */); //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({ let animation = await LottieLoader.loadAnimationWorker({
container: div, container: div,
@ -1290,14 +1293,17 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
width, width,
height, height,
name: 'doc' + doc.id, name: 'doc' + doc.id,
needUpscale needUpscale,
}, group, toneIndex); skipRatio
}, group, toneIndex, middleware);
//const deferred = deferredPromise<void>(); //const deferred = deferredPromise<void>();
animation.addEventListener('firstFrame', () => { animation.addEventListener('firstFrame', () => {
const element = div.firstElementChild; 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 = () => { const cb = () => {
if(element && element !== animation.canvas) { 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(); //deferred.resolve();
}, {once: true}); }, {once: true});
@ -1511,7 +1519,9 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
} else if(stickerType === 1) { } else if(stickerType === 1) {
const image = new Image(); const image = new Image();
const thumbImage = div.firstElementChild !== image && div.firstElementChild; 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'); image.classList.add('media-sticker');

6
src/helpers/blob.ts

@ -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: 'readAsDataURL'): Promise<string>;
export function readBlobAs(blob: Blob, method: 'readAsArrayBuffer'): Promise<ArrayBuffer>; export function readBlobAs(blob: Blob, method: 'readAsArrayBuffer'): Promise<ArrayBuffer>;
export function readBlobAs(blob: Blob, method: 'readAsArrayBuffer' | 'readAsText' | 'readAsDataURL'): Promise<any> { export function readBlobAs(blob: Blob, method: 'readAsArrayBuffer' | 'readAsText' | 'readAsDataURL'): Promise<any> {
// const perf = performance.now();
return new Promise<any>((resolve) => { return new Promise<any>((resolve) => {
const reader = new FileReader(); 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); reader[method](blob);
}); });
} }

10
src/helpers/dom/getVisibleRect.ts

@ -8,7 +8,7 @@ export default function getVisibleRect(element: HTMLElement, overflowElement: HT
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
const overflowRect = overflowElement.getBoundingClientRect(); const overflowRect = overflowElement.getBoundingClientRect();
let {top: overflowTop, bottom: overflowBottom} = overflowRect; let {top: overflowTop, right: overflowRight, bottom: overflowBottom, left: overflowLeft} = overflowRect;
// * respect sticky headers // * respect sticky headers
if(lookForSticky) { if(lookForSticky) {
@ -21,8 +21,8 @@ export default function getVisibleRect(element: HTMLElement, overflowElement: HT
if(rect.top >= overflowBottom if(rect.top >= overflowBottom
|| rect.bottom <= overflowTop || rect.bottom <= overflowTop
|| rect.right <= overflowRect.left || rect.right <= overflowLeft
|| rect.left >= overflowRect.right) { || rect.left >= overflowRight) {
return null; return null;
} }
@ -43,9 +43,9 @@ export default function getVisibleRect(element: HTMLElement, overflowElement: HT
return { return {
rect: { rect: {
top: rect.top < overflowTop && overflowTop !== 0 ? (overflow.top = true, ++overflow.vertical, overflowTop) : rect.top, 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, 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 overflow
}; };

2
src/lang.ts

@ -640,6 +640,8 @@ const lang = {
"RequestToJoinGroupApproved": "Your request to join the group was approved", "RequestToJoinGroupApproved": "Your request to join the group was approved",
"RequestToJoinChannelApproved": "Your request to join the channel was approved", "RequestToJoinChannelApproved": "Your request to join the channel was approved",
"Update": "UPDATE", "Update": "UPDATE",
"Reactions": "Reactions",
"DoubleTapSetting": "Quick Reaction",
// * macos // * macos
"AccountSettings.Filters": "Chat Folders", "AccountSettings.Filters": "Chat Folders",

18
src/layer.d.ts vendored

@ -866,7 +866,6 @@ export namespace Message {
replies?: MessageReplies, replies?: MessageReplies,
edit_date?: number, edit_date?: number,
post_author?: string, post_author?: string,
grouped_id?: string,
reactions?: MessageReactions, reactions?: MessageReactions,
restriction_reason?: Array<RestrictionReason>, restriction_reason?: Array<RestrictionReason>,
ttl_period?: number, ttl_period?: number,
@ -875,6 +874,7 @@ export namespace Message {
peerId?: PeerId, peerId?: PeerId,
fromId?: PeerId, fromId?: PeerId,
fwdFromId?: PeerId, fwdFromId?: PeerId,
grouped_id?: string,
random_id?: string, random_id?: string,
rReply?: string, rReply?: string,
viaBotId?: PeerId, viaBotId?: PeerId,
@ -3224,10 +3224,10 @@ export namespace Document {
date: number, date: number,
mime_type: string, mime_type: string,
size: number, size: number,
thumbs?: Array<PhotoSize.photoSize | PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize | PhotoSize.photoPathSize>,
video_thumbs?: Array<VideoSize>, video_thumbs?: Array<VideoSize>,
dc_id: number, dc_id: number,
attributes: Array<DocumentAttribute>, attributes: Array<DocumentAttribute>,
thumbs?: Array<PhotoSize.photoSize | PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize | PhotoSize.photoPathSize>,
type?: 'gif' | 'sticker' | 'audio' | 'voice' | 'video' | 'round' | 'photo' | 'pdf', type?: 'gif' | 'sticker' | 'audio' | 'voice' | 'video' | 'round' | 'photo' | 'pdf',
h?: number, h?: number,
w?: number, w?: number,
@ -9355,13 +9355,13 @@ export namespace AvailableReaction {
}>, }>,
reaction: string, reaction: string,
title: string, title: string,
static_icon: Document, static_icon: Document.document,
appear_animation: Document, appear_animation: Document.document,
select_animation: Document, select_animation: Document.document,
activate_animation: Document, activate_animation: Document.document,
effect_animation: Document, effect_animation: Document.document,
around_animation?: Document, around_animation: Document.document,
center_icon?: Document center_icon: Document.document
}; };
} }

4
src/lib/appManagers/appImManager.ts

@ -83,6 +83,7 @@ import { Modify, SendMessageEmojiInteractionData } from '../../types';
import htmlToSpan from '../../helpers/dom/htmlToSpan'; import htmlToSpan from '../../helpers/dom/htmlToSpan';
import getVisibleRect from '../../helpers/dom/getVisibleRect'; import getVisibleRect from '../../helpers/dom/getVisibleRect';
import { simulateClickEvent } from '../../helpers/dom/clickEvent'; import { simulateClickEvent } from '../../helpers/dom/clickEvent';
import appReactionsManager from './appReactionsManager';
//console.log('appImManager included33!'); //console.log('appImManager included33!');
@ -1427,7 +1428,8 @@ export class AppImManager {
appNotificationsManager, appNotificationsManager,
appEmojiManager, appEmojiManager,
appMessagesIdsManager, appMessagesIdsManager,
appGroupCallsManager appGroupCallsManager,
appReactionsManager
); );
if(this.chats.length) { if(this.chats.length) {

77
src/lib/appManagers/appReactionsManager.ts

@ -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;

6
src/lib/mtproto/referenceDatabase.ts

@ -15,7 +15,7 @@ import apiManager from "./mtprotoworker";
import assumeType from "../../helpers/assumeType"; import assumeType from "../../helpers/assumeType";
import { logger } from "../logger"; 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 namespace ReferenceContext {
export type referenceContextProfilePhoto = { export type referenceContextProfilePhoto = {
type: 'profilePhoto', type: 'profilePhoto',
@ -31,6 +31,10 @@ export namespace ReferenceContext {
export type referenceContextEmojiesSounds = { export type referenceContextEmojiesSounds = {
type: 'emojiesSounds' type: 'emojiesSounds'
}; };
export type referenceContextReactions = {
type: 'reactions'
};
} }
export type ReferenceBytes = Photo.photo['file_reference']; export type ReferenceBytes = Photo.photo['file_reference'];

6
src/lib/rlottie/lottieLoader.ts

@ -130,7 +130,7 @@ export class LottieLoader {
]).then(() => player); ]).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) { if(!this.isWebAssemblySupported) {
return this.loadPromise as any; return this.loadPromise as any;
} }
@ -150,6 +150,10 @@ export class LottieLoader {
await this.loadLottieWorkers(); await this.loadLottieWorkers();
} }
if(middleware && !middleware()) {
throw new Error('middleware');
}
if(!params.width || !params.height) { if(!params.width || !params.height) {
params.width = parseInt(params.container.style.width); params.width = parseInt(params.container.style.width);
params.height = parseInt(params.container.style.height); params.height = parseInt(params.container.style.height);

1
src/lib/rlottie/rlottiePlayer.ts

@ -342,6 +342,7 @@ export default class RLottiePlayer extends EventListenerBase<{
this.pause(); this.pause();
this.sendQuery('destroy'); this.sendQuery('destroy');
if(this.cacheName) cache.releaseCache(this.cacheName); if(this.cacheName) cache.releaseCache(this.cacheName);
this.cleanup();
//this.removed = true; //this.removed = true;
} }

20
src/scripts/generate_mtproto_types.js

@ -12,23 +12,37 @@ const replace = require(__dirname + '/in/schema_replace_types.json');
const mtproto = schema.API; const mtproto = schema.API;
for(const constructor of additional) { for(const constructor of additional) {
constructor.params.forEach(param => { const additionalParams = constructor.params || (constructor.params = []);
additionalParams.forEach(param => {
param.type = 'flags.-1?' + param.type; param.type = 'flags.-1?' + param.type;
}); });
if(constructor.properties) {
additionalParams.push(...constructor.properties);
}
if(constructor.type) { if(constructor.type) {
mtproto.constructors.push(constructor); mtproto.constructors.push(constructor);
} }
const realConstructor = constructor.type ? constructor : mtproto.constructors.find(c => c.predicate == constructor.predicate); 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 => { /* constructor.params.forEach(param => {
const index = realConstructor.params.findIndex(_param => _param.predicate == param.predicate); const index = realConstructor.params.findIndex(_param => _param.predicate == param.predicate);
if(index !== -1) { if(index !== -1) {
realConstructor.params.splice(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 => { ['Vector t', 'Bool', 'True', 'Null'].forEach(key => {
@ -116,7 +130,7 @@ const processParamType = (type, parseBooleanFlags, overrideTypes) => {
default: default:
//console.log('no such type', type); //console.log('no such type', type);
//throw new Error('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);
} }
}; };

11
src/scripts/in/schema_additional_params.json

@ -346,4 +346,15 @@
{"name": "action", "type": "MessageAction.messageActionPhoneCall"} {"name": "action", "type": "MessageAction.messageActionPhoneCall"}
], ],
"type": "MessageMedia" "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"}
]
}] }]

79
src/scss/partials/_button.scss

@ -96,7 +96,6 @@
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
background-color: var(--surface-color); background-color: var(--surface-color);
box-shadow: 0px 2px 8px 1px rgba(0, 0, 0, .24);
z-index: 3; z-index: 3;
top: 100%; top: 100%;
padding: .5rem 0; padding: .5rem 0;
@ -106,6 +105,11 @@
transition: opacity var(--btn-menu-transition), transform var(--btn-menu-transition), visibility var(--btn-menu-transition); transition: opacity var(--btn-menu-transition), transform var(--btn-menu-transition), visibility var(--btn-menu-transition);
font-size: 16px; font-size: 16px;
&,
&-reactions {
box-shadow: 0px 2px 8px 1px rgba(0, 0, 0, .24);
}
body.animation-level-0 & { body.animation-level-0 & {
transition: none; transition: none;
} }
@ -261,6 +265,79 @@
padding: 0; padding: 0;
margin: .5rem 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 { .btn-primary {

6
src/scss/partials/_row.scss

@ -114,6 +114,12 @@
position: absolute !important; position: absolute !important;
margin: 0 !important; margin: 0 !important;
left: .5rem; left: .5rem;
&-small {
width: 32px !important;
height: 32px !important;
left: .75rem !important;
}
} }
&.menu-open { &.menu-open {

2
tweb-design

@ -1 +1 @@
Subproject commit d10c87ef1aec54cdd2e506fcd980ea848e60eedc Subproject commit 2c4d08587b77a388d4beb8bc018dcb56ebd8a589
Loading…
Cancel
Save