Browse Source

Vertical reactions menu

Support 'Seen By'
Notification about new reaction
master
Eduard Kuzmenko 3 years ago
parent
commit
3b4184f34e
  1. 310
      src/components/chat/contextMenu.ts
  2. 173
      src/lib/appManagers/appMessagesManager.ts

310
src/components/chat/contextMenu.ts

@ -13,7 +13,7 @@ import type { AppReactionsManager } from "../../lib/appManagers/appReactionsMana @@ -13,7 +13,7 @@ import type { AppReactionsManager } from "../../lib/appManagers/appReactionsMana
import type Chat from "./chat";
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu";
import { attachContextMenuListener, openBtnMenu, positionMenu } from "../misc";
import { attachContextMenuListener, MenuPositionPadding, openBtnMenu, positionMenu } from "../misc";
import PopupDeleteMessages from "../popups/deleteMessages";
import PopupForward from "../popups/forward";
import PopupPinMessage from "../popups/unpinMessage";
@ -29,7 +29,7 @@ import { Message, Poll, Chat as MTChat, MessageMedia, AvailableReaction } from " @@ -29,7 +29,7 @@ import { Message, Poll, Chat as MTChat, MessageMedia, AvailableReaction } from "
import PopupReportMessages from "../popups/reportMessages";
import assumeType from "../../helpers/assumeType";
import PopupSponsored from "../popups/sponsored";
import { ScrollableX } from "../scrollable";
import Scrollable, { ScrollableBase, ScrollableX } from "../scrollable";
import { wrapSticker } from "../wrappers";
import RLottiePlayer from "../../lib/rlottie/rlottiePlayer";
import getVisibleRect from "../../helpers/dom/getVisibleRect";
@ -37,11 +37,18 @@ import ListenerSetter from "../../helpers/listenerSetter"; @@ -37,11 +37,18 @@ import ListenerSetter from "../../helpers/listenerSetter";
import animationIntersector from "../animationIntersector";
import { getMiddleware } from "../../helpers/middleware";
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 REACTION_CLASS_NAME = REACTIONS_CLASS_NAME + '-reaction';
const REACTION_SIZE = 24;
const REACTION_SIZE = 28;
const PADDING = 4;
const REACTION_CONTAINER_SIZE = REACTION_SIZE + PADDING * 2;
@ -49,26 +56,43 @@ type ChatReactionsMenuPlayers = { @@ -49,26 +56,43 @@ type ChatReactionsMenuPlayers = {
select?: RLottiePlayer,
appear?: RLottiePlayer,
selectWrapper: HTMLElement,
appearWrapper: HTMLElement
appearWrapper: HTMLElement,
reaction: string
};
export class ChatReactionsMenu {
public container: HTMLElement;
public widthContainer: HTMLElement;
private container: HTMLElement;
private reactionsMap: Map<HTMLElement, ChatReactionsMenuPlayers>;
private scrollable: ScrollableX;
private scrollable: ScrollableBase;
private animationGroup: string;
private middleware: ReturnType<typeof getMiddleware>;
private message: Message.message;
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');
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);
reactionsScrollable.onAdditionalScroll = this.onScroll;
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.animationGroup = 'CHAT-MENU-REACTIONS-' + Date.now();
animationIntersector.setOverrideIdleGroup(this.animationGroup, true);
@ -77,13 +101,42 @@ export class ChatReactionsMenu { @@ -77,13 +101,42 @@ export class ChatReactionsMenu {
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();
appReactionsManager.getAvailableReactions().then(reactions => {
if(!middleware()) return;
// const result = Promise.resolve(this.appReactionsManager.getAvailableReactionsForPeer(message.peerId)).then((res) => pause(1000).then(() => res));
const result = this.appReactionsManager.getAvailableReactionsByMessage(message);
callbackify(result, (reactions) => {
if(!middleware() || !reactions.length) return;
reactions.forEach(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 { @@ -109,21 +162,26 @@ export class ChatReactionsMenu {
scaleContainer.classList.add(REACTION_CLASS_NAME + '-scale');
const appearWrapper = document.createElement('div');
const selectWrapper = document.createElement('div');
let selectWrapper: HTMLElement;;
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;
if(rootScope.settings.animationsEnabled) {
selectWrapper = document.createElement('div');
selectWrapper.classList.add(REACTION_CLASS_NAME + '-select', 'hide');
}
const players: ChatReactionsMenuPlayers = {
selectWrapper,
appearWrapper
appearWrapper,
reaction: reaction.reaction
};
this.reactionsMap.set(reactionDiv, players);
const middleware = this.middleware.get();
const hoverScale = IS_TOUCH_SUPPORTED ? 1 : 1.25;
const size = REACTION_SIZE * hoverScale;
const options = {
width: size,
height: size,
@ -133,54 +191,66 @@ export class ChatReactionsMenu { @@ -133,54 +191,66 @@ export class ChatReactionsMenu {
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);
if(!rootScope.settings.animationsEnabled) {
delete options.needFadeIn;
delete options.withThumb;
wrapSticker({
doc: reaction.static_icon,
div: appearWrapper,
...options
});
} 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);
return lottieLoader.waitForFirstFrame(player);
}).catch(noop);
}
scaleContainer.append(appearWrapper, selectWrapper);
scaleContainer.append(appearWrapper);
selectWrapper && scaleContainer.append(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')) {
if(!players.appearWrapper.classList.contains('hide') || !players.appear) {
return;
}
@ -252,6 +322,9 @@ export default class ChatContextMenu { @@ -252,6 +322,9 @@ export default class ChatContextMenu {
private reactionsMenu: ChatReactionsMenu;
private listenerSetter: ListenerSetter;
private viewerPeerId: PeerId;
private middleware: ReturnType<typeof getMiddleware>;
constructor(
private attachTo: HTMLElement,
private chat: Chat,
@ -263,6 +336,7 @@ export default class ChatContextMenu { @@ -263,6 +336,7 @@ export default class ChatContextMenu {
private appReactionsManager: AppReactionsManager
) {
this.listenerSetter = new ListenerSetter();
this.middleware = getMiddleware();
if(IS_TOUCH_SUPPORTED/* && false */) {
attachClickEvent(attachTo, (e) => {
@ -349,19 +423,21 @@ export default class ChatContextMenu { @@ -349,19 +423,21 @@ export default class ChatContextMenu {
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);
this.viewerPeerId = undefined;
const initResult = this.init();
element = initResult.element;
const {cleanup, destroy} = initResult;
const {cleanup, destroy, menuPadding} = 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);
positionMenu((e as TouchEvent).touches ? (e as TouchEvent).touches[0] : e as MouseEvent, element, side, menuPadding);
openBtnMenu(element, () => {
this.mid = 0;
this.peerId = undefined;
this.target = null;
this.viewerPeerId = undefined;
cleanup();
setTimeout(() => {
@ -373,6 +449,7 @@ export default class ChatContextMenu { @@ -373,6 +449,7 @@ export default class ChatContextMenu {
public cleanup() {
this.listenerSetter.removeAll();
this.reactionsMenu && this.reactionsMenu.cleanup();
this.middleware.clean();
}
public destroy() {
@ -590,6 +667,16 @@ export default class ChatContextMenu { @@ -590,6 +667,16 @@ export default class ChatContextMenu {
verify: () => this.isSelected,
notDirect: () => 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',
text: 'Delete',
@ -622,8 +709,110 @@ export default class ChatContextMenu { @@ -622,8 +709,110 @@ export default class ChatContextMenu {
element.id = 'bubble-contextmenu';
element.classList.add('contextmenu');
const reactionsMenu = this.reactionsMenu = new ChatReactionsMenu(this.appReactionsManager);
element.prepend(reactionsMenu.container);
const viewsButton = filteredButtons.find(button => !button.icon);
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);
@ -631,11 +820,12 @@ export default class ChatContextMenu { @@ -631,11 +820,12 @@ export default class ChatContextMenu {
element,
cleanup: () => {
this.cleanup();
reactionsMenu.cleanup();
reactionsMenu && reactionsMenu.cleanup();
},
destroy: () => {
element.remove();
}
},
menuPadding
};
}

173
src/lib/appManagers/appMessagesManager.ts

@ -14,10 +14,10 @@ import ProgressivePreloader from "../../components/preloader"; @@ -14,10 +14,10 @@ import ProgressivePreloader from "../../components/preloader";
import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise";
import { formatDateAccordingToTodayNew, formatTime, tsNow } from "../../helpers/date";
import { createPosterForVideo } from "../../helpers/files";
import { copy, getObjectKeysAndSort } from "../../helpers/object";
import { copy, deepEqual, getObjectKeysAndSort } from "../../helpers/object";
import { randomLong } from "../../helpers/random";
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 I18n, { FormatterArguments, i18n, join, langPack, LangPackKey, UNSUPPORTED_LANG_PACK_KEY, _i18n } from "../langPack";
import { logger, LogTypes } from "../logger";
@ -63,6 +63,7 @@ import IMAGE_MIME_TYPES_SUPPORTED from "../../environment/imageMimeTypesSupport" @@ -63,6 +63,7 @@ import IMAGE_MIME_TYPES_SUPPORTED from "../../environment/imageMimeTypesSupport"
import VIDEO_MIME_TYPES_SUPPORTED from "../../environment/videoMimeTypesSupport";
import './appGroupCallsManager';
import appGroupCallsManager from "./appGroupCallsManager";
import appReactionsManager from "./appReactionsManager";
//console.trace('include');
// TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках
@ -223,6 +224,8 @@ export class AppMessagesManager { @@ -223,6 +224,8 @@ export class AppMessagesManager {
updateEditMessage: this.onUpdateEditMessage,
updateEditChannelMessage: this.onUpdateEditMessage,
updateMessageReactions: this.onUpdateMessageReactions,
updateReadChannelDiscussionInbox: this.onUpdateReadHistory,
updateReadChannelDiscussionOutbox: this.onUpdateReadHistory,
updateReadHistoryInbox: this.onUpdateReadHistory,
@ -1518,7 +1521,7 @@ export class AppMessagesManager { @@ -1518,7 +1521,7 @@ export class AppMessagesManager {
private generateReplies(peerId: PeerId) {
let replies: MessageReplies.messageReplies;
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) {
replies = {
_: 'messageReplies',
@ -4314,6 +4317,18 @@ export class AppMessagesManager { @@ -4314,6 +4317,18 @@ export class AppMessagesManager {
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 = () => {
window.clearTimeout(this.notificationsHandlePromise);
this.notificationsHandlePromise = 0;
@ -4328,13 +4343,9 @@ export class AppMessagesManager { @@ -4328,13 +4343,9 @@ export class AppMessagesManager {
}
const notifyPeerToHandle = this.notificationsToHandle[peerId];
Promise.all([
appNotificationsManager.getNotifyPeerTypeSettings(),
appNotificationsManager.getNotifySettings(appPeersManager.getInputNotifyPeerById(peerId, true))
]).then(([_, peerTypeNotifySettings]) => {
this.getNotifyPeerSettings(peerId).then(({muted, peerTypeNotifySettings}) => {
const topMessage = notifyPeerToHandle.topMessage;
if(appNotificationsManager.isPeerLocalMuted(peerId, true) || !topMessage.pFlags.unread) {
if(muted || !topMessage.pFlags.unread) {
return;
}
@ -4560,6 +4571,47 @@ export class AppMessagesManager { @@ -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) => {
//this.log('updateDialogUnreadMark', update);
const peerId = appPeersManager.getPeerId((update.peer as DialogPeer.dialogPeer).peer);
@ -4614,6 +4666,20 @@ export class AppMessagesManager { @@ -4614,6 +4666,20 @@ export class AppMessagesManager {
rootScope.dispatchEvent('dialog_flush', {peerId});
}
} 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', {
storage,
peerId,
@ -5259,6 +5325,75 @@ export class AppMessagesManager { @@ -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[]) {
if(!mids.length) {
return;
@ -5291,9 +5426,11 @@ export class AppMessagesManager { @@ -5291,9 +5426,11 @@ export class AppMessagesManager {
private notifyAboutMessage(message: MyMessage, options: Partial<{
fwdCount: number,
userReaction: MessageUserReaction,
peerTypeNotifySettings: PeerNotifySettings
}> = {}) {
const peerId = this.getMessagePeer(message);
const isAnyChat = peerId.isAnyChat();
const notification: NotifyOptions = {};
const peerString = appPeersManager.getPeerString(peerId);
let notificationMessage: string;
@ -5303,13 +5440,27 @@ export class AppMessagesManager { @@ -5303,13 +5440,27 @@ export class AppMessagesManager {
notificationMessage = I18n.format('Notifications.Forwarded', true, [options.fwdCount]);
} else {
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 {
notificationMessage = I18n.format('Notifications.New', 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;
@ -5329,7 +5480,7 @@ export class AppMessagesManager { @@ -5329,7 +5480,7 @@ export class AppMessagesManager {
const peerPhoto = appPeersManager.getPeerPhoto(peerId);
if(peerPhoto) {
appAvatarsManager.loadAvatar(peerId, peerPhoto, 'photo_small').loadPromise.then(url => {
if(message.pFlags.unread) {
if(message.pFlags.unread || options.userReaction) {
notification.image = url;
appNotificationsManager.notify(notification);
}

Loading…
Cancel
Save