Vertical reactions menu
Support 'Seen By' Notification about new reaction
This commit is contained in:
parent
c70369e4e9
commit
3b4184f34e
@ -13,7 +13,7 @@ import type { AppReactionsManager } from "../../lib/appManagers/appReactionsMana
|
|||||||
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";
|
||||||
import { attachContextMenuListener, openBtnMenu, positionMenu } from "../misc";
|
import { attachContextMenuListener, MenuPositionPadding, openBtnMenu, positionMenu } from "../misc";
|
||||||
import PopupDeleteMessages from "../popups/deleteMessages";
|
import PopupDeleteMessages from "../popups/deleteMessages";
|
||||||
import PopupForward from "../popups/forward";
|
import PopupForward from "../popups/forward";
|
||||||
import PopupPinMessage from "../popups/unpinMessage";
|
import PopupPinMessage from "../popups/unpinMessage";
|
||||||
@ -29,7 +29,7 @@ import { Message, Poll, Chat as MTChat, MessageMedia, AvailableReaction } from "
|
|||||||
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 Scrollable, { ScrollableBase, ScrollableX } from "../scrollable";
|
||||||
import { wrapSticker } from "../wrappers";
|
import { wrapSticker } from "../wrappers";
|
||||||
import RLottiePlayer from "../../lib/rlottie/rlottiePlayer";
|
import RLottiePlayer from "../../lib/rlottie/rlottiePlayer";
|
||||||
import getVisibleRect from "../../helpers/dom/getVisibleRect";
|
import getVisibleRect from "../../helpers/dom/getVisibleRect";
|
||||||
@ -37,11 +37,18 @@ import ListenerSetter from "../../helpers/listenerSetter";
|
|||||||
import animationIntersector from "../animationIntersector";
|
import animationIntersector from "../animationIntersector";
|
||||||
import { getMiddleware } from "../../helpers/middleware";
|
import { getMiddleware } from "../../helpers/middleware";
|
||||||
import noop from "../../helpers/noop";
|
import noop from "../../helpers/noop";
|
||||||
|
import callbackify from "../../helpers/callbackify";
|
||||||
|
import rootScope from "../../lib/rootScope";
|
||||||
|
import { fastRaf } from "../../helpers/schedulers";
|
||||||
|
import lottieLoader from "../../lib/rlottie/lottieLoader";
|
||||||
|
import PeerTitle from "../peerTitle";
|
||||||
|
import StackedAvatars from "../stackedAvatars";
|
||||||
|
import { IS_APPLE } from "../../environment/userAgent";
|
||||||
|
|
||||||
const REACTIONS_CLASS_NAME = 'btn-menu-reactions';
|
const REACTIONS_CLASS_NAME = 'btn-menu-reactions';
|
||||||
const REACTION_CLASS_NAME = REACTIONS_CLASS_NAME + '-reaction';
|
const REACTION_CLASS_NAME = REACTIONS_CLASS_NAME + '-reaction';
|
||||||
|
|
||||||
const REACTION_SIZE = 24;
|
const REACTION_SIZE = 28;
|
||||||
const PADDING = 4;
|
const PADDING = 4;
|
||||||
const REACTION_CONTAINER_SIZE = REACTION_SIZE + PADDING * 2;
|
const REACTION_CONTAINER_SIZE = REACTION_SIZE + PADDING * 2;
|
||||||
|
|
||||||
@ -49,26 +56,43 @@ type ChatReactionsMenuPlayers = {
|
|||||||
select?: RLottiePlayer,
|
select?: RLottiePlayer,
|
||||||
appear?: RLottiePlayer,
|
appear?: RLottiePlayer,
|
||||||
selectWrapper: HTMLElement,
|
selectWrapper: HTMLElement,
|
||||||
appearWrapper: HTMLElement
|
appearWrapper: HTMLElement,
|
||||||
|
reaction: string
|
||||||
};
|
};
|
||||||
export class ChatReactionsMenu {
|
export class ChatReactionsMenu {
|
||||||
public container: HTMLElement;
|
public widthContainer: HTMLElement;
|
||||||
|
private container: HTMLElement;
|
||||||
private reactionsMap: Map<HTMLElement, ChatReactionsMenuPlayers>;
|
private reactionsMap: Map<HTMLElement, ChatReactionsMenuPlayers>;
|
||||||
private scrollable: ScrollableX;
|
private scrollable: ScrollableBase;
|
||||||
private animationGroup: string;
|
private animationGroup: string;
|
||||||
private middleware: ReturnType<typeof getMiddleware>;
|
private middleware: ReturnType<typeof getMiddleware>;
|
||||||
|
private message: Message.message;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private appReactionsManager: AppReactionsManager
|
private appReactionsManager: AppReactionsManager,
|
||||||
|
private type: 'horizontal' | 'vertical',
|
||||||
|
middleware: ChatReactionsMenu['middleware']
|
||||||
) {
|
) {
|
||||||
|
const widthContainer = this.widthContainer = document.createElement('div');
|
||||||
|
widthContainer.classList.add(REACTIONS_CLASS_NAME + '-container');
|
||||||
|
widthContainer.classList.add(REACTIONS_CLASS_NAME + '-container-' + type);
|
||||||
|
|
||||||
const reactionsContainer = this.container = document.createElement('div');
|
const reactionsContainer = this.container = document.createElement('div');
|
||||||
reactionsContainer.classList.add(REACTIONS_CLASS_NAME);
|
reactionsContainer.classList.add(REACTIONS_CLASS_NAME);
|
||||||
|
|
||||||
const reactionsScrollable = this.scrollable = new ScrollableX(undefined);
|
const reactionsScrollable = this.scrollable = type === 'vertical' ? new Scrollable(undefined) : new ScrollableX(undefined);
|
||||||
reactionsContainer.append(reactionsScrollable.container);
|
reactionsContainer.append(reactionsScrollable.container);
|
||||||
reactionsScrollable.onAdditionalScroll = this.onScroll;
|
reactionsScrollable.onAdditionalScroll = this.onScroll;
|
||||||
reactionsScrollable.setListeners();
|
reactionsScrollable.setListeners();
|
||||||
|
|
||||||
|
reactionsScrollable.container.classList.add('no-scrollbar');
|
||||||
|
|
||||||
|
['big'].forEach(type => {
|
||||||
|
const bubble = document.createElement('div');
|
||||||
|
bubble.classList.add(REACTIONS_CLASS_NAME + '-bubble', REACTIONS_CLASS_NAME + '-bubble-' + type);
|
||||||
|
reactionsContainer.append(bubble);
|
||||||
|
});
|
||||||
|
|
||||||
this.reactionsMap = new Map();
|
this.reactionsMap = new Map();
|
||||||
this.animationGroup = 'CHAT-MENU-REACTIONS-' + Date.now();
|
this.animationGroup = 'CHAT-MENU-REACTIONS-' + Date.now();
|
||||||
animationIntersector.setOverrideIdleGroup(this.animationGroup, true);
|
animationIntersector.setOverrideIdleGroup(this.animationGroup, true);
|
||||||
@ -77,13 +101,42 @@ export class ChatReactionsMenu {
|
|||||||
reactionsContainer.addEventListener('mousemove', this.onMouseMove);
|
reactionsContainer.addEventListener('mousemove', this.onMouseMove);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.middleware = getMiddleware();
|
attachClickEvent(reactionsContainer, (e) => {
|
||||||
|
const reactionDiv = findUpClassName(e.target, REACTION_CLASS_NAME);
|
||||||
|
if(!reactionDiv) return;
|
||||||
|
|
||||||
|
const players = this.reactionsMap.get(reactionDiv);
|
||||||
|
if(!players) return;
|
||||||
|
|
||||||
|
this.appReactionsManager.sendReaction(this.message, players.reaction);
|
||||||
|
});
|
||||||
|
|
||||||
|
widthContainer.append(reactionsContainer);
|
||||||
|
|
||||||
|
this.middleware = middleware ?? getMiddleware();
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(message: Message.message) {
|
||||||
|
this.message = message;
|
||||||
|
|
||||||
const middleware = this.middleware.get();
|
const middleware = this.middleware.get();
|
||||||
appReactionsManager.getAvailableReactions().then(reactions => {
|
// const result = Promise.resolve(this.appReactionsManager.getAvailableReactionsForPeer(message.peerId)).then((res) => pause(1000).then(() => res));
|
||||||
if(!middleware()) return;
|
const result = this.appReactionsManager.getAvailableReactionsByMessage(message);
|
||||||
|
callbackify(result, (reactions) => {
|
||||||
|
if(!middleware() || !reactions.length) return;
|
||||||
reactions.forEach(reaction => {
|
reactions.forEach(reaction => {
|
||||||
this.renderReaction(reaction);
|
this.renderReaction(reaction);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setVisible = () => {
|
||||||
|
this.container.classList.add('is-visible');
|
||||||
|
};
|
||||||
|
|
||||||
|
if(result instanceof Promise) {
|
||||||
|
fastRaf(setVisible);
|
||||||
|
} else {
|
||||||
|
setVisible();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,21 +162,26 @@ export class ChatReactionsMenu {
|
|||||||
scaleContainer.classList.add(REACTION_CLASS_NAME + '-scale');
|
scaleContainer.classList.add(REACTION_CLASS_NAME + '-scale');
|
||||||
|
|
||||||
const appearWrapper = document.createElement('div');
|
const appearWrapper = document.createElement('div');
|
||||||
const selectWrapper = document.createElement('div');
|
let selectWrapper: HTMLElement;;
|
||||||
appearWrapper.classList.add(REACTION_CLASS_NAME + '-appear');
|
appearWrapper.classList.add(REACTION_CLASS_NAME + '-appear');
|
||||||
selectWrapper.classList.add(REACTION_CLASS_NAME + '-select', 'hide');
|
|
||||||
|
|
||||||
const hoverScale = IS_TOUCH_SUPPORTED ? 1 : 1.25;
|
if(rootScope.settings.animationsEnabled) {
|
||||||
const size = REACTION_SIZE * hoverScale;
|
selectWrapper = document.createElement('div');
|
||||||
|
selectWrapper.classList.add(REACTION_CLASS_NAME + '-select', 'hide');
|
||||||
|
}
|
||||||
|
|
||||||
const players: ChatReactionsMenuPlayers = {
|
const players: ChatReactionsMenuPlayers = {
|
||||||
selectWrapper,
|
selectWrapper,
|
||||||
appearWrapper
|
appearWrapper,
|
||||||
|
reaction: reaction.reaction
|
||||||
};
|
};
|
||||||
this.reactionsMap.set(reactionDiv, players);
|
this.reactionsMap.set(reactionDiv, players);
|
||||||
|
|
||||||
const middleware = this.middleware.get();
|
const middleware = this.middleware.get();
|
||||||
|
|
||||||
|
const hoverScale = IS_TOUCH_SUPPORTED ? 1 : 1.25;
|
||||||
|
const size = REACTION_SIZE * hoverScale;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
@ -133,54 +191,66 @@ export class ChatReactionsMenu {
|
|||||||
group: this.animationGroup,
|
group: this.animationGroup,
|
||||||
middleware
|
middleware
|
||||||
};
|
};
|
||||||
|
|
||||||
let isFirst = true;
|
|
||||||
wrapSticker({
|
|
||||||
doc: reaction.appear_animation,
|
|
||||||
div: appearWrapper,
|
|
||||||
play: true,
|
|
||||||
...options
|
|
||||||
}).then(player => {
|
|
||||||
assumeType<RLottiePlayer>(player);
|
|
||||||
|
|
||||||
players.appear = player;
|
if(!rootScope.settings.animationsEnabled) {
|
||||||
|
delete options.needFadeIn;
|
||||||
|
delete options.withThumb;
|
||||||
|
|
||||||
player.addEventListener('enterFrame', (frameNo) => {
|
wrapSticker({
|
||||||
if(player.maxFrame === frameNo) {
|
doc: reaction.static_icon,
|
||||||
selectLoadPromise.then((selectPlayer) => {
|
div: appearWrapper,
|
||||||
assumeType<RLottiePlayer>(selectPlayer);
|
...options
|
||||||
appearWrapper.classList.add('hide');
|
|
||||||
selectWrapper.classList.remove('hide');
|
|
||||||
|
|
||||||
if(isFirst) {
|
|
||||||
players.select = selectPlayer;
|
|
||||||
isFirst = false;
|
|
||||||
}
|
|
||||||
}, noop);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, noop);
|
} else {
|
||||||
|
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
|
||||||
|
}).then(player => {
|
||||||
|
assumeType<RLottiePlayer>(player);
|
||||||
|
|
||||||
const selectLoadPromise = wrapSticker({
|
return lottieLoader.waitForFirstFrame(player);
|
||||||
doc: reaction.select_animation,
|
}).catch(noop);
|
||||||
div: selectWrapper,
|
}
|
||||||
...options
|
|
||||||
}).catch(noop);
|
|
||||||
|
|
||||||
scaleContainer.append(appearWrapper, selectWrapper);
|
scaleContainer.append(appearWrapper);
|
||||||
|
selectWrapper && scaleContainer.append(selectWrapper);
|
||||||
reactionDiv.append(scaleContainer);
|
reactionDiv.append(scaleContainer);
|
||||||
this.scrollable.append(reactionDiv);
|
this.scrollable.append(reactionDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onScrollProcessItem(div: HTMLElement, players: ChatReactionsMenuPlayers) {
|
private onScrollProcessItem(div: HTMLElement, players: ChatReactionsMenuPlayers) {
|
||||||
if(!players.appear) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scaleContainer = div.firstElementChild as HTMLElement;
|
const scaleContainer = div.firstElementChild as HTMLElement;
|
||||||
const visibleRect = getVisibleRect(div, this.scrollable.container);
|
const visibleRect = getVisibleRect(div, this.scrollable.container);
|
||||||
if(!visibleRect) {
|
if(!visibleRect) {
|
||||||
if(!players.appearWrapper.classList.contains('hide')) {
|
if(!players.appearWrapper.classList.contains('hide') || !players.appear) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,6 +322,9 @@ export default class ChatContextMenu {
|
|||||||
private reactionsMenu: ChatReactionsMenu;
|
private reactionsMenu: ChatReactionsMenu;
|
||||||
private listenerSetter: ListenerSetter;
|
private listenerSetter: ListenerSetter;
|
||||||
|
|
||||||
|
private viewerPeerId: PeerId;
|
||||||
|
private middleware: ReturnType<typeof getMiddleware>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private attachTo: HTMLElement,
|
private attachTo: HTMLElement,
|
||||||
private chat: Chat,
|
private chat: Chat,
|
||||||
@ -263,6 +336,7 @@ export default class ChatContextMenu {
|
|||||||
private appReactionsManager: AppReactionsManager
|
private appReactionsManager: AppReactionsManager
|
||||||
) {
|
) {
|
||||||
this.listenerSetter = new ListenerSetter();
|
this.listenerSetter = new ListenerSetter();
|
||||||
|
this.middleware = getMiddleware();
|
||||||
|
|
||||||
if(IS_TOUCH_SUPPORTED/* && false */) {
|
if(IS_TOUCH_SUPPORTED/* && false */) {
|
||||||
attachClickEvent(attachTo, (e) => {
|
attachClickEvent(attachTo, (e) => {
|
||||||
@ -349,19 +423,21 @@ export default class ChatContextMenu {
|
|||||||
this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid);
|
this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid);
|
||||||
this.message = this.chat.getMessage(this.mid);
|
this.message = this.chat.getMessage(this.mid);
|
||||||
this.noForwards = !isSponsored && !this.appMessagesManager.canForward(this.message);
|
this.noForwards = !isSponsored && !this.appMessagesManager.canForward(this.message);
|
||||||
|
this.viewerPeerId = undefined;
|
||||||
|
|
||||||
const initResult = this.init();
|
const initResult = this.init();
|
||||||
element = initResult.element;
|
element = initResult.element;
|
||||||
const {cleanup, destroy} = initResult;
|
const {cleanup, destroy, menuPadding} = initResult;
|
||||||
|
|
||||||
const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right';
|
const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right';
|
||||||
//bubble.parentElement.append(element);
|
//bubble.parentElement.append(element);
|
||||||
//appImManager.log('contextmenu', e, bubble, side);
|
//appImManager.log('contextmenu', e, bubble, side);
|
||||||
positionMenu((e as TouchEvent).touches ? (e as TouchEvent).touches[0] : e as MouseEvent, element, side);
|
positionMenu((e as TouchEvent).touches ? (e as TouchEvent).touches[0] : e as MouseEvent, element, side, menuPadding);
|
||||||
openBtnMenu(element, () => {
|
openBtnMenu(element, () => {
|
||||||
this.mid = 0;
|
this.mid = 0;
|
||||||
this.peerId = undefined;
|
this.peerId = undefined;
|
||||||
this.target = null;
|
this.target = null;
|
||||||
|
this.viewerPeerId = undefined;
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -373,6 +449,7 @@ export default class ChatContextMenu {
|
|||||||
public cleanup() {
|
public cleanup() {
|
||||||
this.listenerSetter.removeAll();
|
this.listenerSetter.removeAll();
|
||||||
this.reactionsMenu && this.reactionsMenu.cleanup();
|
this.reactionsMenu && this.reactionsMenu.cleanup();
|
||||||
|
this.middleware.clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
@ -590,6 +667,16 @@ export default class ChatContextMenu {
|
|||||||
verify: () => this.isSelected,
|
verify: () => this.isSelected,
|
||||||
notDirect: () => true,
|
notDirect: () => true,
|
||||||
withSelection: true
|
withSelection: true
|
||||||
|
}, {
|
||||||
|
onClick: () => {
|
||||||
|
if(this.viewerPeerId) {
|
||||||
|
this.chat.appImManager.setInnerPeer({
|
||||||
|
peerId: this.viewerPeerId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verify: () => !this.peerId.isUser() && (!!(this.message as Message.message).reactions?.recent_reactons?.length || this.appMessagesManager.canViewMessageReadParticipants(this.message)),
|
||||||
|
notDirect: () => true
|
||||||
}, {
|
}, {
|
||||||
icon: 'delete danger',
|
icon: 'delete danger',
|
||||||
text: 'Delete',
|
text: 'Delete',
|
||||||
@ -622,8 +709,110 @@ export default class ChatContextMenu {
|
|||||||
element.id = 'bubble-contextmenu';
|
element.id = 'bubble-contextmenu';
|
||||||
element.classList.add('contextmenu');
|
element.classList.add('contextmenu');
|
||||||
|
|
||||||
const reactionsMenu = this.reactionsMenu = new ChatReactionsMenu(this.appReactionsManager);
|
const viewsButton = filteredButtons.find(button => !button.icon);
|
||||||
element.prepend(reactionsMenu.container);
|
if(viewsButton) {
|
||||||
|
const recentReactions = (this.message as Message.message).reactions?.recent_reactons;
|
||||||
|
const isViewingReactions = !!recentReactions?.length;
|
||||||
|
const participantsCount = (this.appPeersManager.getPeer(this.peerId) as MTChat.chat).participants_count;
|
||||||
|
|
||||||
|
viewsButton.element.classList.add('tgico-' + (isViewingReactions ? 'reactions' : 'checks'));
|
||||||
|
const i18nElem = new I18n.IntlElement({
|
||||||
|
key: isViewingReactions ? 'Chat.Context.Reacted' : 'NobodyViewed',
|
||||||
|
args: isViewingReactions ? [participantsCount, participantsCount] : undefined,
|
||||||
|
element: viewsButton.textElement
|
||||||
|
});
|
||||||
|
|
||||||
|
let fakeText: HTMLElement;
|
||||||
|
if(isViewingReactions) {
|
||||||
|
fakeText = i18n(
|
||||||
|
recentReactions.length === participantsCount ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted',
|
||||||
|
[recentReactions.length, participantsCount]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
fakeText = i18n('Loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeText.classList.add('btn-menu-item-text-fake');
|
||||||
|
viewsButton.element.append(fakeText);
|
||||||
|
|
||||||
|
const PADDING_PER_AVATAR = .875;
|
||||||
|
i18nElem.element.style.visibility = 'hidden';
|
||||||
|
i18nElem.element.style.paddingRight = isViewingReactions ? PADDING_PER_AVATAR * recentReactions.length + 'rem' : '1rem';
|
||||||
|
const middleware = this.middleware.get();
|
||||||
|
this.appMessagesManager.getMessageReactionsListAndReadParticipants(this.message as Message.message).then((result) => {
|
||||||
|
if(!middleware()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(fakeText) {
|
||||||
|
fakeText.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const reactions = result.combined;
|
||||||
|
const reactedLength = isViewingReactions ? reactions.filter(reaction => reaction.reaction).length : reactions.length;
|
||||||
|
|
||||||
|
let fakeElem: HTMLElement;
|
||||||
|
if(reactions.length === 1) {
|
||||||
|
fakeElem = new PeerTitle({
|
||||||
|
peerId: reactions[0].peerId,
|
||||||
|
onlyFirstName: true,
|
||||||
|
dialog: true
|
||||||
|
}).element;
|
||||||
|
|
||||||
|
this.viewerPeerId = reactions[0].peerId;
|
||||||
|
} else if(isViewingReactions) {
|
||||||
|
const isFull = reactedLength === reactions.length;
|
||||||
|
fakeElem = i18n(
|
||||||
|
isFull ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted',
|
||||||
|
[reactedLength, reactions.length]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if(!reactions.length) {
|
||||||
|
i18nElem.element.style.visibility = '';
|
||||||
|
} else {
|
||||||
|
fakeElem = i18n('MessageSeen', [reactions.length]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(fakeElem) {
|
||||||
|
fakeElem.style.paddingRight = PADDING_PER_AVATAR * reactedLength + 'rem';
|
||||||
|
fakeElem.classList.add('btn-menu-item-text-fake');
|
||||||
|
viewsButton.element.append(fakeElem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(reactions.length) {
|
||||||
|
const avatars = new StackedAvatars({avatarSize: 24});
|
||||||
|
avatars.render(recentReactions ? recentReactions.map(r => r.user_id.toPeerId()) : reactions.map(reaction => reaction.peerId));
|
||||||
|
viewsButton.element.append(avatars.container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let menuPadding: MenuPositionPadding;
|
||||||
|
let reactionsMenu: ChatReactionsMenu;
|
||||||
|
if(this.message._ === 'message') {
|
||||||
|
const position: 'horizontal' | 'vertical' = IS_APPLE || IS_TOUCH_SUPPORTED ? 'horizontal' : 'vertical';
|
||||||
|
reactionsMenu = this.reactionsMenu = new ChatReactionsMenu(this.appReactionsManager, position, this.middleware);
|
||||||
|
reactionsMenu.init(this.message);
|
||||||
|
element.prepend(reactionsMenu.widthContainer);
|
||||||
|
|
||||||
|
const size = 42;
|
||||||
|
const margin = 8;
|
||||||
|
const totalSize = size + margin;
|
||||||
|
if(position === 'vertical') {
|
||||||
|
menuPadding = {
|
||||||
|
top: 24,
|
||||||
|
// bottom: 36, // positionMenu will detect it itself somehow
|
||||||
|
left: totalSize
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
menuPadding = {
|
||||||
|
top: totalSize,
|
||||||
|
right: 36,
|
||||||
|
left: 24
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.chat.container.append(element);
|
this.chat.container.append(element);
|
||||||
|
|
||||||
@ -631,11 +820,12 @@ export default class ChatContextMenu {
|
|||||||
element,
|
element,
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
reactionsMenu.cleanup();
|
reactionsMenu && reactionsMenu.cleanup();
|
||||||
},
|
},
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
element.remove();
|
element.remove();
|
||||||
}
|
},
|
||||||
|
menuPadding
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,10 +14,10 @@ import ProgressivePreloader from "../../components/preloader";
|
|||||||
import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise";
|
import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise";
|
||||||
import { formatDateAccordingToTodayNew, formatTime, tsNow } from "../../helpers/date";
|
import { formatDateAccordingToTodayNew, formatTime, tsNow } from "../../helpers/date";
|
||||||
import { createPosterForVideo } from "../../helpers/files";
|
import { createPosterForVideo } from "../../helpers/files";
|
||||||
import { copy, getObjectKeysAndSort } from "../../helpers/object";
|
import { copy, deepEqual, getObjectKeysAndSort } from "../../helpers/object";
|
||||||
import { randomLong } from "../../helpers/random";
|
import { randomLong } from "../../helpers/random";
|
||||||
import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string";
|
import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string";
|
||||||
import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason, MessagesGetDialogs, InputChannel, InputDialogPeer } from "../../layer";
|
import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason, MessagesGetDialogs, InputChannel, InputDialogPeer, MessageUserReaction } from "../../layer";
|
||||||
import { InvokeApiOptions } from "../../types";
|
import { InvokeApiOptions } from "../../types";
|
||||||
import I18n, { FormatterArguments, i18n, join, langPack, LangPackKey, UNSUPPORTED_LANG_PACK_KEY, _i18n } from "../langPack";
|
import I18n, { FormatterArguments, i18n, join, langPack, LangPackKey, UNSUPPORTED_LANG_PACK_KEY, _i18n } from "../langPack";
|
||||||
import { logger, LogTypes } from "../logger";
|
import { logger, LogTypes } from "../logger";
|
||||||
@ -63,6 +63,7 @@ import IMAGE_MIME_TYPES_SUPPORTED from "../../environment/imageMimeTypesSupport"
|
|||||||
import VIDEO_MIME_TYPES_SUPPORTED from "../../environment/videoMimeTypesSupport";
|
import VIDEO_MIME_TYPES_SUPPORTED from "../../environment/videoMimeTypesSupport";
|
||||||
import './appGroupCallsManager';
|
import './appGroupCallsManager';
|
||||||
import appGroupCallsManager from "./appGroupCallsManager";
|
import appGroupCallsManager from "./appGroupCallsManager";
|
||||||
|
import appReactionsManager from "./appReactionsManager";
|
||||||
|
|
||||||
//console.trace('include');
|
//console.trace('include');
|
||||||
// TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках
|
// TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках
|
||||||
@ -223,6 +224,8 @@ export class AppMessagesManager {
|
|||||||
updateEditMessage: this.onUpdateEditMessage,
|
updateEditMessage: this.onUpdateEditMessage,
|
||||||
updateEditChannelMessage: this.onUpdateEditMessage,
|
updateEditChannelMessage: this.onUpdateEditMessage,
|
||||||
|
|
||||||
|
updateMessageReactions: this.onUpdateMessageReactions,
|
||||||
|
|
||||||
updateReadChannelDiscussionInbox: this.onUpdateReadHistory,
|
updateReadChannelDiscussionInbox: this.onUpdateReadHistory,
|
||||||
updateReadChannelDiscussionOutbox: this.onUpdateReadHistory,
|
updateReadChannelDiscussionOutbox: this.onUpdateReadHistory,
|
||||||
updateReadHistoryInbox: this.onUpdateReadHistory,
|
updateReadHistoryInbox: this.onUpdateReadHistory,
|
||||||
@ -1518,7 +1521,7 @@ export class AppMessagesManager {
|
|||||||
private generateReplies(peerId: PeerId) {
|
private generateReplies(peerId: PeerId) {
|
||||||
let replies: MessageReplies.messageReplies;
|
let replies: MessageReplies.messageReplies;
|
||||||
if(appPeersManager.isBroadcast(peerId)) {
|
if(appPeersManager.isBroadcast(peerId)) {
|
||||||
const channelFull = appProfileManager.chatsFull[peerId.toChatId()] as ChatFull.channelFull;
|
const channelFull = appProfileManager.getCachedFullChat(peerId.toChatId()) as ChatFull.channelFull;
|
||||||
if(channelFull?.linked_chat_id) {
|
if(channelFull?.linked_chat_id) {
|
||||||
replies = {
|
replies = {
|
||||||
_: 'messageReplies',
|
_: 'messageReplies',
|
||||||
@ -4314,6 +4317,18 @@ export class AppMessagesManager {
|
|||||||
return this.historiesStorage[peerId] ?? (this.historiesStorage[peerId] = {count: null, history: new SlicedArray()});
|
return this.historiesStorage[peerId] ?? (this.historiesStorage[peerId] = {count: null, history: new SlicedArray()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getNotifyPeerSettings(peerId: PeerId) {
|
||||||
|
return Promise.all([
|
||||||
|
appNotificationsManager.getNotifyPeerTypeSettings(),
|
||||||
|
appNotificationsManager.getNotifySettings(appPeersManager.getInputNotifyPeerById(peerId, true))
|
||||||
|
]).then(([_, peerTypeNotifySettings]) => {
|
||||||
|
return {
|
||||||
|
muted: appNotificationsManager.isPeerLocalMuted(peerId, true),
|
||||||
|
peerTypeNotifySettings
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private handleNotifications = () => {
|
private handleNotifications = () => {
|
||||||
window.clearTimeout(this.notificationsHandlePromise);
|
window.clearTimeout(this.notificationsHandlePromise);
|
||||||
this.notificationsHandlePromise = 0;
|
this.notificationsHandlePromise = 0;
|
||||||
@ -4328,13 +4343,9 @@ export class AppMessagesManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notifyPeerToHandle = this.notificationsToHandle[peerId];
|
const notifyPeerToHandle = this.notificationsToHandle[peerId];
|
||||||
|
this.getNotifyPeerSettings(peerId).then(({muted, peerTypeNotifySettings}) => {
|
||||||
Promise.all([
|
|
||||||
appNotificationsManager.getNotifyPeerTypeSettings(),
|
|
||||||
appNotificationsManager.getNotifySettings(appPeersManager.getInputNotifyPeerById(peerId, true))
|
|
||||||
]).then(([_, peerTypeNotifySettings]) => {
|
|
||||||
const topMessage = notifyPeerToHandle.topMessage;
|
const topMessage = notifyPeerToHandle.topMessage;
|
||||||
if(appNotificationsManager.isPeerLocalMuted(peerId, true) || !topMessage.pFlags.unread) {
|
if(muted || !topMessage.pFlags.unread) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4560,6 +4571,47 @@ export class AppMessagesManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onUpdateMessageReactions = (update: Update.updateMessageReactions) => {
|
||||||
|
const {peer, msg_id, reactions} = update;
|
||||||
|
const mid = appMessagesIdsManager.generateMessageId(msg_id);
|
||||||
|
const peerId = appPeersManager.getPeerId(peer);
|
||||||
|
const message: MyMessage = this.getMessageByPeer(peerId, mid);
|
||||||
|
|
||||||
|
if(message._ !== 'message') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentReactions = reactions.recent_reactons;
|
||||||
|
if(recentReactions) {
|
||||||
|
const recentReaction = recentReactions[recentReactions.length - 1];
|
||||||
|
const previousReactions = message.reactions;
|
||||||
|
const previousRecentReactions = previousReactions?.recent_reactons;
|
||||||
|
if(
|
||||||
|
recentReaction.user_id !== rootScope.myId.toUserId() && (
|
||||||
|
!previousRecentReactions ||
|
||||||
|
previousRecentReactions.length <= recentReactions.length
|
||||||
|
) && (
|
||||||
|
!previousRecentReactions ||
|
||||||
|
!deepEqual(recentReaction, previousRecentReactions[previousRecentReactions.length - 1])
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.getNotifyPeerSettings(peerId).then(({muted, peerTypeNotifySettings}) => {
|
||||||
|
if(muted || !peerTypeNotifySettings.show_previews) return;
|
||||||
|
this.notifyAboutMessage(message, {
|
||||||
|
userReaction: recentReaction,
|
||||||
|
peerTypeNotifySettings
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message.reactions = reactions;
|
||||||
|
|
||||||
|
rootScope.dispatchEvent('message_reactions', message);
|
||||||
|
|
||||||
|
this.setDialogToStateIfMessageIsTop(message);
|
||||||
|
};
|
||||||
|
|
||||||
private onUpdateDialogUnreadMark = (update: Update.updateDialogUnreadMark) => {
|
private onUpdateDialogUnreadMark = (update: Update.updateDialogUnreadMark) => {
|
||||||
//this.log('updateDialogUnreadMark', update);
|
//this.log('updateDialogUnreadMark', update);
|
||||||
const peerId = appPeersManager.getPeerId((update.peer as DialogPeer.dialogPeer).peer);
|
const peerId = appPeersManager.getPeerId((update.peer as DialogPeer.dialogPeer).peer);
|
||||||
@ -4614,6 +4666,20 @@ export class AppMessagesManager {
|
|||||||
rootScope.dispatchEvent('dialog_flush', {peerId});
|
rootScope.dispatchEvent('dialog_flush', {peerId});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// no sense in dispatching message_edit since only reactions have changed
|
||||||
|
if(oldMessage?._ === 'message' && !deepEqual(oldMessage.reactions, (newMessage as Message.message).reactions)) {
|
||||||
|
const newReactions = (newMessage as Message.message).reactions;
|
||||||
|
(newMessage as Message.message).reactions = oldMessage.reactions;
|
||||||
|
apiUpdatesManager.processLocalUpdate({
|
||||||
|
_: 'updateMessageReactions',
|
||||||
|
peer: appPeersManager.getOutputPeer(peerId),
|
||||||
|
msg_id: message.id,
|
||||||
|
reactions: newReactions
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
rootScope.dispatchEvent('message_edit', {
|
rootScope.dispatchEvent('message_edit', {
|
||||||
storage,
|
storage,
|
||||||
peerId,
|
peerId,
|
||||||
@ -5259,6 +5325,75 @@ export class AppMessagesManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getMessageReactionsListAndReadParticipants(
|
||||||
|
message: Message.message,
|
||||||
|
limit?: number,
|
||||||
|
reaction?: string,
|
||||||
|
offset?: string
|
||||||
|
) {
|
||||||
|
const emptyMessageReactionsList = {
|
||||||
|
reactions: [] as MessageUserReaction[],
|
||||||
|
count: 0,
|
||||||
|
next_offset: undefined as string
|
||||||
|
};
|
||||||
|
|
||||||
|
const canViewMessageReadParticipants = this.canViewMessageReadParticipants(message);
|
||||||
|
if(canViewMessageReadParticipants && limit === undefined) {
|
||||||
|
limit = 100;
|
||||||
|
} else if(limit === undefined) {
|
||||||
|
limit = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
canViewMessageReadParticipants ? this.getMessageReadParticipants(message.peerId, message.mid).catch(() => [] as UserId[]) : [] as UserId[],
|
||||||
|
|
||||||
|
message.reactions?.recent_reactons?.length ? appReactionsManager.getMessageReactionsList(message.peerId, message.mid, limit, reaction, offset).catch(err => emptyMessageReactionsList) : emptyMessageReactionsList
|
||||||
|
]).then(([userIds, messageReactionsList]) => {
|
||||||
|
const readParticipantsPeerIds = userIds.map(userId => userId.toPeerId());
|
||||||
|
|
||||||
|
const filteredReadParticipants = readParticipantsPeerIds.slice();
|
||||||
|
forEachReverse(filteredReadParticipants, (peerId, idx, arr) => {
|
||||||
|
if(messageReactionsList.reactions.some(reaction => reaction.user_id.toPeerId() === peerId)) {
|
||||||
|
arr.splice(idx, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let combined: {peerId: PeerId, reaction?: string}[] = messageReactionsList.reactions.map(reaction => ({peerId: reaction.user_id.toPeerId(), reaction: reaction.reaction}));
|
||||||
|
combined = combined.concat(filteredReadParticipants.map(readPeerId => ({peerId: readPeerId})));
|
||||||
|
|
||||||
|
return {
|
||||||
|
reactions: messageReactionsList.reactions,
|
||||||
|
readParticipants: readParticipantsPeerIds,
|
||||||
|
combined: combined,
|
||||||
|
nextOffset: messageReactionsList.next_offset
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMessageReadParticipants(peerId: PeerId, mid: number): Promise<UserId[]> {
|
||||||
|
return apiManager.invokeApiSingle('messages.getMessageReadParticipants', {
|
||||||
|
peer: appPeersManager.getInputPeerById(peerId),
|
||||||
|
msg_id: appMessagesIdsManager.getServerMessageId(mid)
|
||||||
|
}).then(userIds => { // ! convert long to number
|
||||||
|
return userIds.map(userId => userId.toUserId());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public canViewMessageReadParticipants(message: Message) {
|
||||||
|
if(
|
||||||
|
message._ !== 'message' ||
|
||||||
|
message.pFlags.is_outgoing ||
|
||||||
|
!message.pFlags.out ||
|
||||||
|
!appPeersManager.isAnyGroup(message.peerId)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chat: Chat.chat | Chat.channel = appChatsManager.getChat(message.peerId.toChatId());
|
||||||
|
return chat.participants_count < rootScope.appConfig.chat_read_mark_size_threshold &&
|
||||||
|
(tsNow(true) - message.date) < rootScope.appConfig.chat_read_mark_expire_period;
|
||||||
|
}
|
||||||
|
|
||||||
public incrementMessageViews(peerId: PeerId, mids: number[]) {
|
public incrementMessageViews(peerId: PeerId, mids: number[]) {
|
||||||
if(!mids.length) {
|
if(!mids.length) {
|
||||||
return;
|
return;
|
||||||
@ -5291,9 +5426,11 @@ export class AppMessagesManager {
|
|||||||
|
|
||||||
private notifyAboutMessage(message: MyMessage, options: Partial<{
|
private notifyAboutMessage(message: MyMessage, options: Partial<{
|
||||||
fwdCount: number,
|
fwdCount: number,
|
||||||
|
userReaction: MessageUserReaction,
|
||||||
peerTypeNotifySettings: PeerNotifySettings
|
peerTypeNotifySettings: PeerNotifySettings
|
||||||
}> = {}) {
|
}> = {}) {
|
||||||
const peerId = this.getMessagePeer(message);
|
const peerId = this.getMessagePeer(message);
|
||||||
|
const isAnyChat = peerId.isAnyChat();
|
||||||
const notification: NotifyOptions = {};
|
const notification: NotifyOptions = {};
|
||||||
const peerString = appPeersManager.getPeerString(peerId);
|
const peerString = appPeersManager.getPeerString(peerId);
|
||||||
let notificationMessage: string;
|
let notificationMessage: string;
|
||||||
@ -5303,13 +5440,27 @@ export class AppMessagesManager {
|
|||||||
notificationMessage = I18n.format('Notifications.Forwarded', true, [options.fwdCount]);
|
notificationMessage = I18n.format('Notifications.Forwarded', true, [options.fwdCount]);
|
||||||
} else {
|
} else {
|
||||||
notificationMessage = this.wrapMessageForReply(message, undefined, undefined, true);
|
notificationMessage = this.wrapMessageForReply(message, undefined, undefined, true);
|
||||||
|
|
||||||
|
if(options.userReaction) {
|
||||||
|
const langPackKey: LangPackKey = /* isAnyChat ? 'Notification.Group.Reacted' : */'Notification.Contact.Reacted';
|
||||||
|
const args: FormatterArguments = [
|
||||||
|
options.userReaction.reaction,
|
||||||
|
notificationMessage
|
||||||
|
];
|
||||||
|
|
||||||
|
/* if(isAnyChat) {
|
||||||
|
args.unshift(appPeersManager.getPeerTitle(message.fromId, true));
|
||||||
|
} */
|
||||||
|
|
||||||
|
notificationMessage = I18n.format(langPackKey, true, args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
notificationMessage = I18n.format('Notifications.New', true);
|
notificationMessage = I18n.format('Notifications.New', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
notification.title = appPeersManager.getPeerTitle(peerId, true);
|
notification.title = appPeersManager.getPeerTitle(peerId, true);
|
||||||
if(peerId.isAnyChat() && message.fromId !== message.peerId) {
|
if(isAnyChat && message.fromId !== message.peerId) {
|
||||||
notification.title = appPeersManager.getPeerTitle(message.fromId, true) +
|
notification.title = appPeersManager.getPeerTitle(message.fromId, true) +
|
||||||
' @ ' +
|
' @ ' +
|
||||||
notification.title;
|
notification.title;
|
||||||
@ -5329,7 +5480,7 @@ export class AppMessagesManager {
|
|||||||
const peerPhoto = appPeersManager.getPeerPhoto(peerId);
|
const peerPhoto = appPeersManager.getPeerPhoto(peerId);
|
||||||
if(peerPhoto) {
|
if(peerPhoto) {
|
||||||
appAvatarsManager.loadAvatar(peerId, peerPhoto, 'photo_small').loadPromise.then(url => {
|
appAvatarsManager.loadAvatar(peerId, peerPhoto, 'photo_small').loadPromise.then(url => {
|
||||||
if(message.pFlags.unread) {
|
if(message.pFlags.unread || options.userReaction) {
|
||||||
notification.image = url;
|
notification.image = url;
|
||||||
appNotificationsManager.notify(notification);
|
appNotificationsManager.notify(notification);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user