Browse Source

Reactions

master
Eduard Kuzmenko 2 years ago
parent
commit
caee8e33fe
  1. 2
      src/components/appMediaViewerBase.ts
  2. 4
      src/components/appSearchSuper..ts
  3. 6
      src/components/audio.ts
  4. 7
      src/components/buttonMenu.ts
  5. 282
      src/components/chat/bubbles.ts
  6. 6
      src/components/chat/chat.ts
  7. 2
      src/components/chat/commandsHelper.ts
  8. 70
      src/components/chat/contextMenu.ts
  9. 2
      src/components/chat/inlineHelper.ts
  10. 27
      src/components/chat/messageRender.ts
  11. 185
      src/components/chat/reaction.ts
  12. 198
      src/components/chat/reactions.ts
  13. 13
      src/components/chat/selection.ts
  14. 9
      src/components/horizontalMenu.ts
  15. 51
      src/components/misc.ts
  16. 6
      src/components/peerProfile.ts
  17. 2
      src/components/peerProfileAvatars.ts
  18. 2
      src/components/popups/avatar.ts
  19. 220
      src/components/popups/reactedList.ts
  20. 2
      src/components/sidebarLeft/tabs/generalSettings.ts
  21. 64
      src/components/stackedAvatars.ts
  22. 174
      src/components/wrappers.ts
  23. 17
      src/helpers/array.ts
  24. 11
      src/helpers/callbackify.ts
  25. 18
      src/helpers/callbackifyAll.ts
  26. 12
      src/helpers/getTimeFormat.ts
  27. 10
      src/lang.ts
  28. 7
      src/layer.d.ts
  29. 64
      src/lib/appManagers/appMessagesManager.ts
  30. 66
      src/lib/appManagers/appProfileManager.ts
  31. 256
      src/lib/appManagers/appReactionsManager.ts
  32. 3
      src/lib/appManagers/appStateManager.ts
  33. 7
      src/lib/mtproto/mtprotoworker.ts
  34. 2
      src/lib/mtproto/schema.ts
  35. 8
      src/lib/rootScope.ts
  36. 4
      src/lib/storages/dialogs.ts
  37. 7
      src/scripts/in/schema_additional_params.json
  38. 29
      src/scss/partials/_audio.scss
  39. 7
      src/scss/partials/_avatar.scss
  40. 178
      src/scss/partials/_button.scss
  41. 7
      src/scss/partials/_chat.scss
  42. 222
      src/scss/partials/_chatBubble.scss
  43. 4
      src/scss/partials/_chatPinned.scss
  44. 16
      src/scss/partials/_checkbox.scss
  45. 23
      src/scss/partials/_document.scss
  46. 143
      src/scss/partials/_reaction.scss
  47. 25
      src/scss/partials/_reactions.scss
  48. 13
      src/scss/partials/_rightSidebar.scss
  49. 36
      src/scss/partials/_stackedAvatars.scss
  50. 2
      src/scss/partials/pages/_pages.scss
  51. 80
      src/scss/partials/popups/_reactedList.scss
  52. 54
      src/scss/style.scss

2
src/components/appMediaViewerBase.ts

@ -1208,6 +1208,7 @@ export default class AppMediaViewerBase<
this.moveTheMover(this.content.mover, fromRight === 1); this.moveTheMover(this.content.mover, fromRight === 1);
this.setNewMover(); this.setNewMover();
} else { } else {
rootScope.isOverlayActive = true;
window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp); window.addEventListener('keyup', this.onKeyUp);
if(!IS_TOUCH_SUPPORTED) window.addEventListener('wheel', this.onWheel, {passive: false, capture: true}); if(!IS_TOUCH_SUPPORTED) window.addEventListener('wheel', this.onWheel, {passive: false, capture: true});
@ -1215,7 +1216,6 @@ export default class AppMediaViewerBase<
this.pageEl.insertBefore(this.wholeDiv, mainColumns); this.pageEl.insertBefore(this.wholeDiv, mainColumns);
void this.wholeDiv.offsetLeft; // reflow void this.wholeDiv.offsetLeft; // reflow
this.wholeDiv.classList.add('active'); this.wholeDiv.classList.add('active');
rootScope.isOverlayActive = true;
animationIntersector.checkAnimations(true); animationIntersector.checkAnimations(true);
if(!IS_MOBILE_SAFARI) { if(!IS_MOBILE_SAFARI) {

4
src/components/appSearchSuper..ts

@ -1246,14 +1246,14 @@ export default class AppSearchSuper {
return renderParticipants(participants.participants); return renderParticipants(participants.participants);
}); });
} else { } else {
promise = (appProfileManager.getChatFull(id) as Promise<ChatFull.chatFull>).then(chatFull => { promise = Promise.resolve(appProfileManager.getChatFull(id)).then(chatFull => {
if(!middleware()) { if(!middleware()) {
return; return;
} }
//console.log('anymore', chatFull); //console.log('anymore', chatFull);
this.loaded[mediaTab.inputFilter] = true; this.loaded[mediaTab.inputFilter] = true;
const participants = chatFull.participants; const participants = (chatFull as ChatFull.chatFull).participants;
if(participants._ === 'chatParticipantsForbidden') { if(participants._ === 'chatParticipantsForbidden') {
return; return;
} }

6
src/components/audio.ts

@ -608,7 +608,11 @@ export default class AudioElement extends HTMLElement {
} }
} }
this.append(downloadDiv); if(this.classList.contains('corner-download')) {
toggle.append(downloadDiv);
} else {
this.append(downloadDiv);
}
this.classList.add('downloading'); this.classList.add('downloading');

7
src/components/buttonMenu.ts

@ -7,7 +7,7 @@
import { cancelEvent } from "../helpers/dom/cancelEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent";
import { AttachClickOptions, attachClickEvent, CLICK_EVENT_NAME } from "../helpers/dom/clickEvent"; import { AttachClickOptions, attachClickEvent, CLICK_EVENT_NAME } from "../helpers/dom/clickEvent";
import ListenerSetter from "../helpers/listenerSetter"; import ListenerSetter from "../helpers/listenerSetter";
import { i18n, LangPackKey } from "../lib/langPack"; import { FormatterArguments, i18n, LangPackKey } from "../lib/langPack";
import CheckboxField from "./checkboxField"; import CheckboxField from "./checkboxField";
import { closeBtnMenu } from "./misc"; import { closeBtnMenu } from "./misc";
import { ripple } from "./ripple"; import { ripple } from "./ripple";
@ -15,6 +15,7 @@ import { ripple } from "./ripple";
export type ButtonMenuItemOptions = { export type ButtonMenuItemOptions = {
icon?: string, icon?: string,
text?: LangPackKey, text?: LangPackKey,
textArgs?: FormatterArguments,
regularText?: string, regularText?: string,
onClick: (e: MouseEvent | TouchEvent) => void | boolean, onClick: (e: MouseEvent | TouchEvent) => void | boolean,
element?: HTMLElement, element?: HTMLElement,
@ -31,12 +32,12 @@ const ButtonMenuItem = (options: ButtonMenuItemOptions) => {
const {icon, text, onClick, checkboxField, noCheckboxClickListener} = options; const {icon, text, onClick, checkboxField, noCheckboxClickListener} = options;
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'btn-menu-item' + (icon ? ' tgico-' + icon : ''); el.className = 'btn-menu-item rp-overflow' + (icon ? ' tgico-' + icon : '');
ripple(el); ripple(el);
let textElement = options.textElement; let textElement = options.textElement;
if(!textElement) { if(!textElement) {
textElement = options.textElement = text ? i18n(text) : document.createElement('span'); textElement = options.textElement = text ? i18n(text, options.textArgs) : document.createElement('span');
if(options.regularText) textElement.innerHTML = options.regularText; if(options.regularText) textElement.innerHTML = options.regularText;
} }

282
src/components/chat/bubbles.ts

@ -42,7 +42,7 @@ import LazyLoadQueue from "../lazyLoadQueue";
import ListenerSetter from "../../helpers/listenerSetter"; import ListenerSetter from "../../helpers/listenerSetter";
import PollElement from "../poll"; import PollElement from "../poll";
import AudioElement from "../audio"; import AudioElement from "../audio";
import { ChatInvite, Message, MessageEntity, MessageMedia, MessageReplyHeader, Photo, PhotoSize, ReplyMarkup, SponsoredMessage, Update, WebPage } from "../../layer"; import { ChatInvite, Message, MessageEntity, MessageMedia, MessageReplyHeader, Photo, PhotoSize, ReactionCount, ReplyMarkup, SponsoredMessage, Update, WebPage } from "../../layer";
import { NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config"; import { NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config";
import { FocusDirection } from "../../helpers/fastSmoothScroll"; import { FocusDirection } from "../../helpers/fastSmoothScroll";
import useHeavyAnimationCheck, { getHeavyAnimationPromise, dispatchHeavyAnimationEvent, interruptHeavyAnimation } from "../../hooks/useHeavyAnimationCheck"; import useHeavyAnimationCheck, { getHeavyAnimationPromise, dispatchHeavyAnimationEvent, interruptHeavyAnimation } from "../../hooks/useHeavyAnimationCheck";
@ -88,6 +88,11 @@ import { CallType } from "../../lib/calls/types";
import getVisibleRect from "../../helpers/dom/getVisibleRect"; import getVisibleRect from "../../helpers/dom/getVisibleRect";
import PopupJoinChatInvite from "../popups/joinChatInvite"; import PopupJoinChatInvite from "../popups/joinChatInvite";
import { InternalLink, INTERNAL_LINK_TYPE } from "../../lib/appManagers/internalLink"; import { InternalLink, INTERNAL_LINK_TYPE } from "../../lib/appManagers/internalLink";
import ReactionsElement from "./reactions";
import type ReactionElement from "./reaction";
import type { AppReactionsManager } from "../../lib/appManagers/appReactionsManager";
import RLottiePlayer from "../../lib/rlottie/rlottiePlayer";
import { pause } from "../../helpers/schedulers/pause";
const USE_MEDIA_TAILS = false; const USE_MEDIA_TAILS = false;
const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([ const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([
@ -198,6 +203,11 @@ export default class ChatBubbles {
private previousStickyDate: HTMLElement; private previousStickyDate: HTMLElement;
private sponsoredMessage: SponsoredMessage.sponsoredMessage; private sponsoredMessage: SponsoredMessage.sponsoredMessage;
private hoverBubble: HTMLElement;
private hoverReaction: HTMLElement;
// private reactions: Map<number, ReactionsElement>;
constructor( constructor(
private chat: Chat, private chat: Chat,
private appMessagesManager: AppMessagesManager, private appMessagesManager: AppMessagesManager,
@ -209,7 +219,8 @@ export default class ChatBubbles {
private appProfileManager: AppProfileManager, private appProfileManager: AppProfileManager,
private appDraftsManager: AppDraftsManager, private appDraftsManager: AppDraftsManager,
private appMessagesIdsManager: AppMessagesIdsManager, private appMessagesIdsManager: AppMessagesIdsManager,
private appChatsManager: AppChatsManager private appChatsManager: AppChatsManager,
private appReactionsManager: AppReactionsManager
) { ) {
//this.chat.log.error('Bubbles construction'); //this.chat.log.error('Bubbles construction');
@ -235,6 +246,8 @@ export default class ChatBubbles {
this.lazyLoadQueue = new LazyLoadQueue(); this.lazyLoadQueue = new LazyLoadQueue();
this.lazyLoadQueue.queueId = ++queueId; this.lazyLoadQueue.queueId = ++queueId;
// this.reactions = new Map();
// * events // * events
// will call when sent for update pos // will call when sent for update pos
@ -288,6 +301,13 @@ export default class ChatBubbles {
/////this.log('message_sent', bubble); /////this.log('message_sent', bubble);
const reactionsElements = Array.from(bubble.querySelectorAll('reactions-element')) as ReactionsElement[];
if(reactionsElements.length) {
reactionsElements.forEach(reactionsElement => {
reactionsElement.changeMessage(message as Message.message);
});
}
if(message.replies) { if(message.replies) {
const repliesElement = bubble.querySelector('replies-element') as RepliesElement; const repliesElement = bubble.querySelector('replies-element') as RepliesElement;
if(repliesElement) { if(repliesElement) {
@ -420,6 +440,39 @@ export default class ChatBubbles {
// }); // });
}); });
if(this.chat.type !== 'scheduled') {
this.listenerSetter.add(rootScope)('missed_reactions_element', ({message, changedResults}) => {
if(this.peerId !== message.peerId || !message.reactions || !message.reactions.results.length) {
return;
}
const bubble = this.getBubbleByMessage(message);
if(!bubble) {
return;
}
if(message.grouped_id) {
const grouped = this.getGroupedBubble(message.grouped_id);
message = grouped.message;
}
this.appendReactionsElementToBubble(bubble, message, changedResults);
});
}
/* this.listenerSetter.add(rootScope)('message_reactions', ({peerId, mid}) => {
if(this.peerId !== peerId) {
return;
}
const reactionsElement = this.reactions.get(mid);
if(!reactionsElement) {
return;
}
}); */
this.listenerSetter.add(rootScope)('album_edit', ({peerId, groupId, deletedMids}) => { this.listenerSetter.add(rootScope)('album_edit', ({peerId, groupId, deletedMids}) => {
//fastRaf(() => { // ! can't use delayed smth here, need original bubble to be edited //fastRaf(() => { // ! can't use delayed smth here, need original bubble to be edited
if(peerId !== this.peerId) return; if(peerId !== this.peerId) return;
@ -842,6 +895,110 @@ export default class ChatBubbles {
} }
} }
private onBubblesMouseMove = (e: MouseEvent) => {
const content = findUpClassName(e.target, 'bubble-content');
if(content && !this.chat.selection.isSelecting) {
const bubble = findUpClassName(content, 'bubble');
if(!this.chat.selection.canSelectBubble(bubble)) {
this.unhoverPrevious();
return;
}
let {hoverBubble, hoverReaction} = this;
if(bubble === hoverBubble) {
return;
}
this.unhoverPrevious();
hoverBubble = this.hoverBubble = bubble;
hoverReaction = this.hoverReaction;
// hoverReaction = contentWrapper.querySelector('.bubble-hover-reaction');
if(!hoverReaction) {
hoverReaction = this.hoverReaction = document.createElement('div');
hoverReaction.classList.add('bubble-hover-reaction');
const stickerWrapper = document.createElement('div');
stickerWrapper.classList.add('bubble-hover-reaction-sticker');
hoverReaction.append(stickerWrapper);
content.append(hoverReaction);
let message: Message.message = this.chat.getMessage(+bubble.dataset.mid);
message = this.appMessagesManager.getGroupsFirstMessage(message);
const middleware = this.getMiddleware(() => this.hoverReaction === hoverReaction);
Promise.all([
this.appReactionsManager.getAvailableReactionsByMessage(message),
pause(400)
]).then(([availableReactions]) => {
const availableReaction = availableReactions[0];
if(!availableReaction) {
return;
}
wrapSticker({
div: stickerWrapper,
doc: availableReaction.select_animation,
width: 18,
height: 18,
needUpscale: true,
middleware,
group: CHAT_ANIMATION_GROUP,
withThumb: false,
needFadeIn: false
}).then(player => {
assumeType<RLottiePlayer>(player);
if(!middleware()) {
return;
}
player.addEventListener('firstFrame', () => {
if(!middleware()) {
// debugger;
return;
}
hoverReaction.dataset.loaded = '1';
this.setHoverVisible(hoverReaction, true);
}, {once: true});
attachClickEvent(hoverReaction, () => {
this.appReactionsManager.sendReaction(message, availableReaction.reaction);
this.unhoverPrevious();
}, {listenerSetter: this.listenerSetter});
});
});
} else if(hoverReaction.dataset.loaded) {
this.setHoverVisible(hoverReaction, true);
}
} else {
this.unhoverPrevious();
}
};
public setReactionsHoverListeners() {
this.listenerSetter.add(rootScope)('context_menu_toggle', this.unhoverPrevious);
this.listenerSetter.add(rootScope)('overlay_toggle', this.unhoverPrevious);
this.listenerSetter.add(this.chat.selection)('toggle', this.unhoverPrevious);
this.listenerSetter.add(this.bubblesContainer)('mousemove', this.onBubblesMouseMove);
}
private setHoverVisible(hoverReaction: HTMLElement, visible: boolean) {
SetTransition(hoverReaction, 'is-visible', visible, 200, visible ? undefined : () => {
hoverReaction.remove();
}, 2);
}
private unhoverPrevious = () => {
const {hoverBubble, hoverReaction} = this;
if(hoverBubble) {
this.setHoverVisible(hoverReaction, false);
this.hoverBubble = undefined;
this.hoverReaction = undefined;
}
};
public setStickyDateManually() { public setStickyDateManually() {
const timestamps = Object.keys(this.dateMessages).map(k => +k).sort((a, b) => b - a); const timestamps = Object.keys(this.dateMessages).map(k => +k).sort((a, b) => b - a);
let lastVisible: HTMLElement; let lastVisible: HTMLElement;
@ -1083,6 +1240,18 @@ export default class ChatBubbles {
return; return;
} }
const reactionElement = findUpTag(target, 'REACTION-ELEMENT') as ReactionElement;
if(reactionElement) {
const reactionsElement = reactionElement.parentElement as ReactionsElement;
const reactionCount = reactionsElement.getReactionCount(reactionElement);
const message = reactionsElement.getMessage();
this.appReactionsManager.sendReaction(message, reactionCount.reaction);
cancelEvent(e);
return;
}
const commentsDiv: HTMLElement = findUpClassName(target, 'replies'); const commentsDiv: HTMLElement = findUpClassName(target, 'replies');
if(commentsDiv) { if(commentsDiv) {
const bubbleMid = +bubble.dataset.mid; const bubbleMid = +bubble.dataset.mid;
@ -1398,12 +1567,16 @@ export default class ChatBubbles {
return { return {
bubble: this.bubbles[mid], bubble: this.bubbles[mid],
mid: +mid, mid: +mid,
message: this.chat.getMessage(maxId) message: this.chat.getMessage(maxId) as Message.message
}; };
} }
} }
}
return null; public getBubbleByMessage(message: Message.message | Message.messageService) {
if(!(message as Message.message).grouped_id) return this.bubbles[message.mid];
const grouped = this.getGroupedBubble((message as Message.message).grouped_id);
return grouped?.bubble;
} }
public getBubbleGroupedItems(bubble: HTMLElement) { public getBubbleGroupedItems(bubble: HTMLElement) {
@ -1617,6 +1790,8 @@ export default class ChatBubbles {
if(this.emptyPlaceholderMid === mid) { if(this.emptyPlaceholderMid === mid) {
this.emptyPlaceholderMid = undefined; this.emptyPlaceholderMid = undefined;
} }
// this.reactions.delete(mid);
}); });
if(!deleted) { if(!deleted) {
@ -2015,6 +2190,8 @@ export default class ChatBubbles {
this.isTopPaddingSet = false; this.isTopPaddingSet = false;
// this.reactions.clear();
if(this.isScrollingTimeout) { if(this.isScrollingTimeout) {
clearTimeout(this.isScrollingTimeout); clearTimeout(this.isScrollingTimeout);
this.isScrollingTimeout = 0; this.isScrollingTimeout = 0;
@ -2256,6 +2433,34 @@ export default class ChatBubbles {
this.chat.dispatchEvent('setPeer', lastMsgId, !isJump); this.chat.dispatchEvent('setPeer', lastMsgId, !isJump);
const needReactionsInterval = this.appPeersManager.isChannel(peerId);
if(needReactionsInterval) {
const middleware = this.getMiddleware();
const fetchReactions = () => {
if(!middleware()) return;
const mids: number[] = [];
for(const mid in this.bubbles) {
let message: MyMessage = this.chat.getMessage(+mid);
if(message._ !== 'message') {
continue;
}
message = this.appMessagesManager.getGroupsFirstMessage(message);
mids.push(message.mid);
}
const promise = mids.length ? this.appReactionsManager.getMessagesReactions(this.peerId, mids) : Promise.resolve();
promise.then(() => {
setTimeout(fetchReactions, 10e3);
});
};
afterSetPromise.then(() => {
fetchReactions();
});
}
const needFetchInterval = this.appMessagesManager.isFetchIntervalNeeded(peerId); const needFetchInterval = this.appMessagesManager.isFetchIntervalNeeded(peerId);
const needFetchNew = savedPosition || needFetchInterval; const needFetchNew = savedPosition || needFetchInterval;
if(!needFetchNew) { if(!needFetchNew) {
@ -3094,8 +3299,8 @@ export default class ChatBubbles {
const size = bubble.classList.contains('emoji-big') ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker); const size = bubble.classList.contains('emoji-big') ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker);
this.appPhotosManager.setAttachmentSize(doc, attachmentDiv, size.width, size.height); this.appPhotosManager.setAttachmentSize(doc, attachmentDiv, size.width, size.height);
//let preloader = new ProgressivePreloader(attachmentDiv, false); //let preloader = new ProgressivePreloader(attachmentDiv, false);
bubbleContainer.style.height = attachmentDiv.style.height; bubbleContainer.style.minWidth = attachmentDiv.style.width;
bubbleContainer.style.width = attachmentDiv.style.width; bubbleContainer.style.minHeight = attachmentDiv.style.height;
//appPhotosManager.setAttachmentSize(doc, bubble); //appPhotosManager.setAttachmentSize(doc, bubble);
wrapSticker({ wrapSticker({
doc, doc,
@ -3191,7 +3396,8 @@ export default class ChatBubbles {
} }
const lastContainer = messageDiv.lastElementChild.querySelector('.document-message, .document-size, .audio'); const lastContainer = messageDiv.lastElementChild.querySelector('.document-message, .document-size, .audio');
lastContainer && lastContainer.append(timeSpan.cloneNode(true)); // lastContainer && lastContainer.append(timeSpan.cloneNode(true));
lastContainer && lastContainer.append(timeSpan);
bubble.classList.remove('is-message-empty'); bubble.classList.remove('is-message-empty');
messageDiv.classList.add((!(['photo', 'pdf'] as MyDocument['type'][]).includes(doc.type) ? doc.type || 'document' : 'document') + '-message'); messageDiv.classList.add((!(['photo', 'pdf'] as MyDocument['type'][]).includes(doc.type) ? doc.type || 'document' : 'document') + '-message');
@ -3484,6 +3690,16 @@ export default class ChatBubbles {
} }
} }
if(isMessage) {
this.appendReactionsElementToBubble(bubble, message);
}
/* if(isMessage) {
const reactionHover = document.createElement('div');
reactionHover.classList.add('bubble-reaction-hover');
contentWrapper.append(reactionHover);
} */
if(canHaveTail) { if(canHaveTail) {
bubble.classList.add('can-have-tail'); bubble.classList.add('can-have-tail');
@ -3493,6 +3709,56 @@ export default class ChatBubbles {
return bubble; return bubble;
} }
private appendReactionsElementToBubble(bubble: HTMLElement, message: Message.message, changedResults?: ReactionCount[]) {
if(this.peerId.isUser()) {
return;
}
const reactionsMessage = this.appMessagesManager.getGroupsFirstMessage(message);
if(!reactionsMessage.reactions || !reactionsMessage.reactions.results.length) {
return;
}
// message = this.appMessagesManager.getMessageWithReactions(message);
const reactionsElement = new ReactionsElement();
reactionsElement.init(reactionsMessage, 'block');
reactionsElement.render(changedResults);
if(bubble.classList.contains('is-message-empty')) {
bubble.querySelector('.bubble-content-wrapper').append(reactionsElement);
} else {
const messageDiv = bubble.querySelector('.message');
if(bubble.classList.contains('is-multiple-documents')) {
const documentContainer = messageDiv.lastElementChild as HTMLElement;
let documentMessageDiv = documentContainer.querySelector('.document-message');
let timeSpan: HTMLElement = documentMessageDiv && documentMessageDiv.querySelector('.time');
if(!timeSpan) {
timeSpan = MessageRender.setTime({
chatType: this.chat.type,
message
});
}
reactionsElement.append(timeSpan);
if(!documentMessageDiv) {
documentMessageDiv = document.createElement('div');
documentMessageDiv.classList.add('document-message');
documentContainer.querySelector('.document-wrapper').prepend(documentMessageDiv);
}
documentMessageDiv.append(reactionsElement);
} else {
const timeSpan = Array.from(bubble.querySelectorAll('.time')).pop();
reactionsElement.append(timeSpan);
messageDiv.append(reactionsElement);
}
}
}
private safeRenderMessage(message: any, reverse?: boolean, multipleRender?: boolean, bubble?: HTMLElement, updatePosition?: boolean) { private safeRenderMessage(message: any, reverse?: boolean, multipleRender?: boolean, bubble?: HTMLElement, updatePosition?: boolean) {
try { try {
return this.renderMessage(message, reverse, multipleRender, bubble, updatePosition); return this.renderMessage(message, reverse, multipleRender, bubble, updatePosition);
@ -4152,7 +4418,7 @@ export default class ChatBubbles {
this.log('inject bot description'); this.log('inject bot description');
const middleware = this.getMiddleware(); const middleware = this.getMiddleware();
return this.appProfileManager.getProfile(this.peerId.toUserId()).then(userFull => { return Promise.resolve(this.appProfileManager.getProfile(this.peerId.toUserId())).then(userFull => {
if(!middleware()) { if(!middleware()) {
return; return;
} }

6
src/components/chat/chat.ts

@ -184,7 +184,7 @@ export default class Chat extends EventListenerBase<{
// this.initPeerId = peerId; // this.initPeerId = peerId;
this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager, this.appNotificationsManager, this.appProfileManager, this.appUsersManager, this.appGroupCallsManager); this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager, this.appNotificationsManager, this.appProfileManager, this.appUsersManager, this.appGroupCallsManager);
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.appReactionsManager);
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.appReactionsManager); this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appPeersManager, this.appPollsManager, this.appDocsManager, this.appMessagesIdsManager, this.appReactionsManager);
@ -216,6 +216,10 @@ export default class Chat extends EventListenerBase<{
this.input.constructPeerHelpers(); this.input.constructPeerHelpers();
} }
if(this.type !== 'scheduled') {
this.bubbles.setReactionsHoverListeners();
}
this.container.classList.add('type-' + this.type); this.container.classList.add('type-' + this.type);
this.container.append(this.topbar.container, this.bubbles.bubblesContainer, this.input.chatInput); this.container.append(this.topbar.container, this.bubbles.bubblesContainer, this.input.chatInput);

2
src/components/chat/commandsHelper.ts

@ -37,7 +37,7 @@ export default class CommandsHelper extends AutocompletePeerHelper {
} }
const middleware = this.controller.getMiddleware(); const middleware = this.controller.getMiddleware();
this.appProfileManager.getProfileByPeerId(peerId).then(full => { Promise.resolve(this.appProfileManager.getProfileByPeerId(peerId)).then(full => {
if(!middleware()) { if(!middleware()) {
return; return;
} }

70
src/components/chat/contextMenu.ts

@ -44,6 +44,7 @@ import lottieLoader from "../../lib/rlottie/lottieLoader";
import PeerTitle from "../peerTitle"; import PeerTitle from "../peerTitle";
import StackedAvatars from "../stackedAvatars"; import StackedAvatars from "../stackedAvatars";
import { IS_APPLE } from "../../environment/userAgent"; import { IS_APPLE } from "../../environment/userAgent";
import PopupReactedList from "../popups/reactedList";
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';
@ -324,6 +325,7 @@ export default class ChatContextMenu {
private viewerPeerId: PeerId; private viewerPeerId: PeerId;
private middleware: ReturnType<typeof getMiddleware>; private middleware: ReturnType<typeof getMiddleware>;
canOpenReactedList: boolean;
constructor( constructor(
private attachTo: HTMLElement, private attachTo: HTMLElement,
@ -424,6 +426,7 @@ export default class ChatContextMenu {
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; this.viewerPeerId = undefined;
this.canOpenReactedList = undefined;
const initResult = this.init(); const initResult = this.init();
element = initResult.element; element = initResult.element;
@ -438,6 +441,7 @@ export default class ChatContextMenu {
this.peerId = undefined; this.peerId = undefined;
this.target = null; this.target = null;
this.viewerPeerId = undefined; this.viewerPeerId = undefined;
this.canOpenReactedList = undefined;
cleanup(); cleanup();
setTimeout(() => { setTimeout(() => {
@ -673,9 +677,13 @@ export default class ChatContextMenu {
this.chat.appImManager.setInnerPeer({ this.chat.appImManager.setInnerPeer({
peerId: this.viewerPeerId peerId: this.viewerPeerId
}); });
} else if(this.canOpenReactedList) {
new PopupReactedList(this.appMessagesManager, this.message as Message.message);
} else {
return false;
} }
}, },
verify: () => !this.peerId.isUser() && (!!(this.message as Message.message).reactions?.recent_reactons?.length || this.appMessagesManager.canViewMessageReadParticipants(this.message)), verify: () => !this.peerId.isUser() && (!!(this.message as Message.message).reactions?.recent_reactions?.length || this.appMessagesManager.canViewMessageReadParticipants(this.message)),
notDirect: () => true notDirect: () => true
}, { }, {
icon: 'delete danger', icon: 'delete danger',
@ -711,23 +719,33 @@ export default class ChatContextMenu {
const viewsButton = filteredButtons.find(button => !button.icon); const viewsButton = filteredButtons.find(button => !button.icon);
if(viewsButton) { if(viewsButton) {
const recentReactions = (this.message as Message.message).reactions?.recent_reactons; const reactions = (this.message as Message.message).reactions;
const recentReactions = reactions?.recent_reactions;
const isViewingReactions = !!recentReactions?.length; const isViewingReactions = !!recentReactions?.length;
const participantsCount = (this.appPeersManager.getPeer(this.peerId) as MTChat.chat).participants_count; const participantsCount = this.appMessagesManager.canViewMessageReadParticipants(this.message) ? (this.appPeersManager.getPeer(this.peerId) as MTChat.chat).participants_count : undefined;
const reactedLength = reactions ? reactions.results.reduce((acc, r) => acc + r.count, 0) : undefined;
viewsButton.element.classList.add('tgico-' + (isViewingReactions ? 'reactions' : 'checks')); viewsButton.element.classList.add('tgico-' + (isViewingReactions ? 'reactions' : 'checks'));
const i18nElem = new I18n.IntlElement({ const i18nElem = new I18n.IntlElement({
key: isViewingReactions ? 'Chat.Context.Reacted' : 'NobodyViewed', key: isViewingReactions ? (
args: isViewingReactions ? [participantsCount, participantsCount] : undefined, participantsCount === undefined ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted'
) : 'NobodyViewed',
args: isViewingReactions ? (
participantsCount === undefined ? [reactedLength] : [participantsCount, participantsCount]
) : undefined,
element: viewsButton.textElement element: viewsButton.textElement
}); });
let fakeText: HTMLElement; let fakeText: HTMLElement;
if(isViewingReactions) { if(isViewingReactions) {
fakeText = i18n( if(participantsCount === undefined) {
recentReactions.length === participantsCount ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted', fakeText = i18n('Chat.Context.ReactedFast', [reactedLength]);
[recentReactions.length, participantsCount] } else {
); fakeText = i18n(
recentReactions.length === participantsCount ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted',
[recentReactions.length, participantsCount]
);
}
} else { } else {
fakeText = i18n('Loading'); fakeText = i18n('Loading');
} }
@ -735,9 +753,10 @@ export default class ChatContextMenu {
fakeText.classList.add('btn-menu-item-text-fake'); fakeText.classList.add('btn-menu-item-text-fake');
viewsButton.element.append(fakeText); viewsButton.element.append(fakeText);
const MAX_AVATARS = 3;
const PADDING_PER_AVATAR = .875; const PADDING_PER_AVATAR = .875;
i18nElem.element.style.visibility = 'hidden'; i18nElem.element.style.visibility = 'hidden';
i18nElem.element.style.paddingRight = isViewingReactions ? PADDING_PER_AVATAR * recentReactions.length + 'rem' : '1rem'; i18nElem.element.style.paddingRight = isViewingReactions ? PADDING_PER_AVATAR * Math.min(MAX_AVATARS, recentReactions.length) + 'rem' : '1rem';
const middleware = this.middleware.get(); const middleware = this.middleware.get();
this.appMessagesManager.getMessageReactionsListAndReadParticipants(this.message as Message.message).then((result) => { this.appMessagesManager.getMessageReactionsListAndReadParticipants(this.message as Message.message).then((result) => {
if(!middleware()) { if(!middleware()) {
@ -749,22 +768,30 @@ export default class ChatContextMenu {
} }
const reactions = result.combined; const reactions = result.combined;
const reactedLength = isViewingReactions ? reactions.filter(reaction => reaction.reaction).length : reactions.length; const reactedLength = participantsCount === undefined ?
result.reactionsCount :
(
isViewingReactions ?
reactions.filter(reaction => reaction.reaction).length :
reactions.length
);
let fakeElem: HTMLElement; let fakeElem: HTMLElement;
if(reactions.length === 1) { if(reactions.length === 1) {
fakeElem = new PeerTitle({ fakeElem = new PeerTitle({
peerId: reactions[0].peerId, peerId: reactions[0].peerId,
onlyFirstName: true, onlyFirstName: true,
dialog: true dialog: false,
}).element; }).element;
this.viewerPeerId = reactions[0].peerId; if(!isViewingReactions || result.readParticipants.length <= 1) {
this.viewerPeerId = reactions[0].peerId;
}
} else if(isViewingReactions) { } else if(isViewingReactions) {
const isFull = reactedLength === reactions.length; const isFull = reactedLength === reactions.length || participantsCount === undefined;
fakeElem = i18n( fakeElem = i18n(
isFull ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted', isFull ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted',
[reactedLength, reactions.length] isFull ? [reactedLength] : [reactedLength, reactions.length]
); );
} else { } else {
if(!reactions.length) { if(!reactions.length) {
@ -775,7 +802,7 @@ export default class ChatContextMenu {
} }
if(fakeElem) { if(fakeElem) {
fakeElem.style.paddingRight = PADDING_PER_AVATAR * reactedLength + 'rem'; fakeElem.style.paddingRight = PADDING_PER_AVATAR * Math.min(MAX_AVATARS, reactedLength) + 'rem';
fakeElem.classList.add('btn-menu-item-text-fake'); fakeElem.classList.add('btn-menu-item-text-fake');
viewsButton.element.append(fakeElem); viewsButton.element.append(fakeElem);
} }
@ -784,16 +811,21 @@ export default class ChatContextMenu {
const avatars = new StackedAvatars({avatarSize: 24}); const avatars = new StackedAvatars({avatarSize: 24});
avatars.render(recentReactions ? recentReactions.map(r => r.user_id.toPeerId()) : reactions.map(reaction => reaction.peerId)); avatars.render(recentReactions ? recentReactions.map(r => r.user_id.toPeerId()) : reactions.map(reaction => reaction.peerId));
viewsButton.element.append(avatars.container); viewsButton.element.append(avatars.container);
// if(reactions.length > 1) {
// if(isViewingReactions) {
this.canOpenReactedList = true;
// }
} }
}); });
} }
let menuPadding: MenuPositionPadding; let menuPadding: MenuPositionPadding;
let reactionsMenu: ChatReactionsMenu; let reactionsMenu: ChatReactionsMenu;
if(this.message._ === 'message') { if(this.message._ === 'message' && !this.chat.selection.isSelecting && !this.message.pFlags.is_outgoing && !this.message.pFlags.is_scheduled) {
const position: 'horizontal' | 'vertical' = IS_APPLE || IS_TOUCH_SUPPORTED ? 'horizontal' : 'vertical'; const position: 'horizontal' | 'vertical' = (IS_APPLE || IS_TOUCH_SUPPORTED)/* && false */ ? 'horizontal' : 'vertical';
reactionsMenu = this.reactionsMenu = new ChatReactionsMenu(this.appReactionsManager, position, this.middleware); reactionsMenu = this.reactionsMenu = new ChatReactionsMenu(this.appReactionsManager, position, this.middleware);
reactionsMenu.init(this.message); reactionsMenu.init(this.appMessagesManager.getGroupsFirstMessage(this.message));
element.prepend(reactionsMenu.widthContainer); element.prepend(reactionsMenu.widthContainer);
const size = 42; const size = 42;

2
src/components/chat/inlineHelper.ts

@ -20,11 +20,11 @@ import AutocompleteHelperController from "./autocompleteHelperController";
import Button from "../button"; import Button from "../button";
import { attachClickEvent } from "../../helpers/dom/clickEvent"; import { attachClickEvent } from "../../helpers/dom/clickEvent";
import { MyPhoto } from "../../lib/appManagers/appPhotosManager"; import { MyPhoto } from "../../lib/appManagers/appPhotosManager";
import { readBlobAsDataURL } from "../../helpers/blob";
import assumeType from "../../helpers/assumeType"; import assumeType from "../../helpers/assumeType";
import GifsMasonry from "../gifsMasonry"; import GifsMasonry from "../gifsMasonry";
import { SuperStickerRenderer } from "../emoticonsDropdown/tabs/stickers"; import { SuperStickerRenderer } from "../emoticonsDropdown/tabs/stickers";
import mediaSizes from "../../helpers/mediaSizes"; import mediaSizes from "../../helpers/mediaSizes";
import readBlobAsDataURL from "../../helpers/blob/readBlobAsDataURL";
const ANIMATION_GROUP = 'INLINE-HELPER'; const ANIMATION_GROUP = 'INLINE-HELPER';
// const GRID_ITEMS = 5; // const GRID_ITEMS = 5;

27
src/components/chat/messageRender.ts

@ -7,12 +7,14 @@
import { formatTime, getFullDate } from "../../helpers/date"; import { formatTime, getFullDate } from "../../helpers/date";
import { formatNumber } from "../../helpers/number"; import { formatNumber } from "../../helpers/number";
import { Message } from "../../layer"; import { Message } from "../../layer";
import appMessagesManager from "../../lib/appManagers/appMessagesManager";
import { i18n, _i18n } from "../../lib/langPack"; import { i18n, _i18n } from "../../lib/langPack";
import RichTextProcessor from "../../lib/richtextprocessor"; import RichTextProcessor from "../../lib/richtextprocessor";
import { LazyLoadQueueIntersector } from "../lazyLoadQueue"; import { LazyLoadQueueIntersector } from "../lazyLoadQueue";
import PeerTitle from "../peerTitle"; import PeerTitle from "../peerTitle";
import { wrapReply } from "../wrappers"; import { wrapReply } from "../wrappers";
import Chat, { ChatType } from "./chat"; import Chat, { ChatType } from "./chat";
import ReactionsElement from "./reactions";
import RepliesElement from "./replies"; import RepliesElement from "./replies";
const NBSP = '&nbsp;'; const NBSP = '&nbsp;';
@ -39,10 +41,11 @@ export namespace MessageRender {
const date = new Date(message.date * 1000); const date = new Date(message.date * 1000);
const args: (HTMLElement | string)[] = []; const args: (HTMLElement | string)[] = [];
let editedSpan: HTMLElement, sponsoredSpan: HTMLElement; let editedSpan: HTMLElement, sponsoredSpan: HTMLElement, reactionsElement: ReactionsElement, reactionsMessage: Message.message;
const isSponsored = !!(message as Message.message).pFlags.sponsored; const isSponsored = !!(message as Message.message).pFlags.sponsored;
const isMessage = !('action' in message) && !isSponsored; const isMessage = !('action' in message) && !isSponsored;
let hasReactions: boolean;
let time: HTMLElement = isSponsored ? undefined : formatTime(date); let time: HTMLElement = isSponsored ? undefined : formatTime(date);
if(isMessage) { if(isMessage) {
@ -73,6 +76,17 @@ export namespace MessageRender {
i.classList.add('tgico-pinnedchat', 'time-icon'); i.classList.add('tgico-pinnedchat', 'time-icon');
args.unshift(i); args.unshift(i);
} }
if(message.peer_id._ === 'peerUser'/* && message.reactions?.results?.length */) {
hasReactions = true;
reactionsMessage = appMessagesManager.getGroupsFirstMessage(message);
reactionsElement = new ReactionsElement();
reactionsElement.init(reactionsMessage, 'inline', true);
reactionsElement.render();
args.unshift(reactionsElement);
}
} else if(isSponsored) { } else if(isSponsored) {
args.push(sponsoredSpan = makeSponsored()); args.push(sponsoredSpan = makeSponsored());
} }
@ -83,13 +97,13 @@ export namespace MessageRender {
let title = isSponsored ? undefined : getFullDate(date); let title = isSponsored ? undefined : getFullDate(date);
if(isMessage) { if(isMessage) {
title += (message.edit_date ? `\nEdited: ${getFullDate(new Date(message.edit_date * 1000))}` : '') title += (message.edit_date && !message.pFlags.edit_hide ? `\nEdited: ${getFullDate(new Date(message.edit_date * 1000))}` : '')
+ (message.fwd_from ? `\nOriginal: ${getFullDate(new Date(message.fwd_from.date * 1000))}` : ''); + (message.fwd_from ? `\nOriginal: ${getFullDate(new Date(message.fwd_from.date * 1000))}` : '');
} }
const timeSpan = document.createElement('span'); const timeSpan = document.createElement('span');
timeSpan.classList.add('time', 'tgico'); timeSpan.classList.add('time', 'tgico');
if(title) timeSpan.title = title; // if(title) timeSpan.title = title;
timeSpan.append(...args); timeSpan.append(...args);
const inner = document.createElement('div'); const inner = document.createElement('div');
@ -103,7 +117,12 @@ export namespace MessageRender {
if(sponsoredSpan) { if(sponsoredSpan) {
clonedArgs[clonedArgs.indexOf(sponsoredSpan)] = makeSponsored(); clonedArgs[clonedArgs.indexOf(sponsoredSpan)] = makeSponsored();
} }
clonedArgs = clonedArgs.map(a => a instanceof HTMLElement && !a.classList.contains('i18n') ? a.cloneNode(true) as HTMLElement : a); if(reactionsElement) {
const _reactionsElement = clonedArgs[clonedArgs.indexOf(reactionsElement)] = new ReactionsElement();
_reactionsElement.init(reactionsMessage, 'inline');
_reactionsElement.render();
}
clonedArgs = clonedArgs.map(a => a instanceof HTMLElement && !a.classList.contains('i18n') && !a.classList.contains('reactions') ? a.cloneNode(true) as HTMLElement : a);
if(time) { if(time) {
clonedArgs[clonedArgs.length - 1] = formatTime(date); // clone time clonedArgs[clonedArgs.length - 1] = formatTime(date); // clone time
} }

185
src/components/chat/reaction.ts

@ -0,0 +1,185 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import callbackify from "../../helpers/callbackify";
import { formatNumber } from "../../helpers/number";
import { MessageUserReaction, ReactionCount } from "../../layer";
import appReactionsManager from "../../lib/appManagers/appReactionsManager";
import RLottiePlayer from "../../lib/rlottie/rlottiePlayer";
import SetTransition from "../singleTransition";
import StackedAvatars from "../stackedAvatars";
import { wrapSticker, wrapStickerAnimation } from "../wrappers";
const CLASS_NAME = 'reaction';
const TAG_NAME = CLASS_NAME + '-element';
const REACTION_INLINE_SIZE = 14;
const REACTION_BLOCK_SIZE = 16;
export const REACTION_DISPLAY_INLINE_COUNTER_AT = 2;
export const REACTION_DISPLAY_BLOCK_COUNTER_AT = 4;
export type ReactionLayoutType = 'block' | 'inline';
export default class ReactionElement extends HTMLElement {
private type: ReactionLayoutType;
private counter: HTMLElement;
private stickerContainer: HTMLElement;
private stackedAvatars: StackedAvatars;
private canRenderAvatars: boolean;
private _reactionCount: ReactionCount;
constructor() {
super();
this.classList.add(CLASS_NAME);
}
public get reactionCount() {
return this._reactionCount;
}
public set reactionCount(reactionCount: ReactionCount) {
this._reactionCount = reactionCount;
}
public get count() {
return this.reactionCount.count;
}
public init(type: ReactionLayoutType) {
this.type = type;
this.classList.add(CLASS_NAME + '-' + type);
}
public setCanRenderAvatars(canRenderAvatars: boolean) {
this.canRenderAvatars = canRenderAvatars;
}
public render(doNotRenderSticker?: boolean) {
const hadStickerContainer = !!this.stickerContainer;
if(!hadStickerContainer) {
this.stickerContainer = document.createElement('div');
this.stickerContainer.classList.add(CLASS_NAME + '-sticker');
this.append(this.stickerContainer);
}
const reactionCount = this.reactionCount;
if(!doNotRenderSticker && !hadStickerContainer) {
const availableReaction = appReactionsManager.getReaction(reactionCount.reaction);
callbackify(availableReaction, (availableReaction) => {
const size = this.type === 'inline' ? REACTION_INLINE_SIZE : REACTION_BLOCK_SIZE;
wrapSticker({
div: this.stickerContainer,
doc: availableReaction.static_icon,
width: size,
height: size
});
});
}
}
public renderCounter() {
const reactionCount = this.reactionCount;
const displayOn = this.type === 'inline' ? REACTION_DISPLAY_INLINE_COUNTER_AT : REACTION_DISPLAY_BLOCK_COUNTER_AT;
if(reactionCount.count >= displayOn || (this.type === 'block' && !this.canRenderAvatars)) {
if(!this.counter) {
this.counter = document.createElement(this.type === 'inline' ? 'i' : 'span');
this.counter.classList.add(CLASS_NAME + '-counter');
}
const formatted = formatNumber(reactionCount.count);
if(this.counter.textContent !== formatted) {
this.counter.textContent = formatted;
}
if(!this.counter.parentElement) {
this.append(this.counter);
}
} else if(this.counter?.parentElement) {
this.counter.remove();
this.counter = undefined;
}
}
public renderAvatars(recentReactions: MessageUserReaction[]) {
if(this.type === 'inline') {
return;
}
if(this.reactionCount.count >= REACTION_DISPLAY_BLOCK_COUNTER_AT || !this.canRenderAvatars) {
if(this.stackedAvatars) {
this.stackedAvatars.container.remove();
this.stackedAvatars = undefined;
}
return;
}
if(!this.stackedAvatars) {
this.stackedAvatars = new StackedAvatars({
avatarSize: 16
});
this.append(this.stackedAvatars.container);
}
this.stackedAvatars.render(recentReactions.map(reaction => reaction.user_id.toPeerId()));
}
public setIsChosen(isChosen = !!this.reactionCount.pFlags.chosen) {
if(this.type === 'inline') return;
const wasChosen = this.classList.contains('is-chosen') && !this.classList.contains('backwards');
if(wasChosen !== isChosen) {
SetTransition(this, 'is-chosen', isChosen, this.isConnected ? 300 : 0);
}
}
public fireAroundAnimation() {
callbackify(appReactionsManager.getReaction(this.reactionCount.reaction), (availableReaction) => {
const size = (this.type === 'inline' ? REACTION_INLINE_SIZE : REACTION_BLOCK_SIZE) + 14;
const div = document.createElement('div');
div.classList.add(CLASS_NAME + '-sticker-activate');
Promise.all([
wrapSticker({
div: div,
doc: availableReaction.center_icon,
width: size,
height: size,
withThumb: false,
needUpscale: true,
play: false,
skipRatio: 1,
group: 'none',
needFadeIn: false
}) as Promise<RLottiePlayer>,
wrapStickerAnimation({
doc: availableReaction.around_animation,
size: 80,
target: this.stickerContainer,
side: 'center',
skipRatio: 1,
play: false
}).stickerPromise
]).then(([activatePlayer, aroundPlayer]) => {
activatePlayer.addEventListener('enterFrame', (frameNo) => {
if(frameNo === activatePlayer.maxFrame) {
activatePlayer.remove();
div.remove();
}
});
activatePlayer.addEventListener('firstFrame', () => {
this.stickerContainer.prepend(div);
activatePlayer.play();
aroundPlayer.play();
}, {once: true});
});
});
}
}
customElements.define(TAG_NAME, ReactionElement);

198
src/components/chat/reactions.ts

@ -0,0 +1,198 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { forEachReverse } from "../../helpers/array";
import positionElementByIndex from "../../helpers/dom/positionElementByIndex";
import { Message, ReactionCount } from "../../layer";
import appReactionsManager from "../../lib/appManagers/appReactionsManager";
import rootScope from "../../lib/rootScope";
import ReactionElement, { ReactionLayoutType, REACTION_DISPLAY_BLOCK_COUNTER_AT } from "./reaction";
const CLASS_NAME = 'reactions';
const TAG_NAME = CLASS_NAME + '-element';
const elements: Map<string, Set<ReactionsElement>> = new Map();
rootScope.addEventListener('message_reactions', ({message, changedResults}) => {
const key = message.peerId + '_' + message.mid;
const set = elements.get(key);
if(!set) {
rootScope.dispatchEvent('missed_reactions_element', {message, changedResults});
return;
}
for(const element of set) {
element.update(message, changedResults);
}
});
export default class ReactionsElement extends HTMLElement {
private message: Message.message;
private key: string;
private isPlaceholder: boolean;
private type: ReactionLayoutType;
private sorted: ReactionElement[];
private onConnectCallback: () => void;
constructor() {
super();
this.classList.add(CLASS_NAME);
this.sorted = [];
}
connectedCallback() {
let set = elements.get(this.key);
if(!set) {
elements.set(this.key, set = new Set());
}
set.add(this);
if(this.onConnectCallback && this.isConnected) {
this.onConnectCallback();
this.onConnectCallback = undefined;
}
}
disconnectedCallback() {
const set = elements.get(this.key);
set.delete(this);
if(!set.size) {
elements.delete(this.key);
}
}
public getReactionCount(reactionElement: ReactionElement) {
return this.sorted[this.sorted.indexOf(reactionElement)].reactionCount;
}
public getMessage() {
return this.message;
}
public init(message: Message.message, type: ReactionLayoutType, isPlaceholder?: boolean) {
if(this.key !== undefined) {
this.disconnectedCallback();
}
this.message = message;
this.key = this.message.peerId + '_' + this.message.mid;
this.isPlaceholder = isPlaceholder;
if(this.type !== type) {
this.type = type;
this.classList.add(CLASS_NAME + '-' + type);
}
this.connectedCallback();
}
public changeMessage(message: Message.message) {
return this.init(message, this.type, this.isPlaceholder);
}
public update(message: Message.message, changedResults?: ReactionCount[]) {
this.message = message;
this.render(changedResults);
}
public render(changedResults?: ReactionCount[]) {
const reactions = this.message.reactions;
const hasReactions = !!(reactions && reactions.results.length);
this.classList.toggle('has-no-reactions', !hasReactions);
if(!hasReactions && !this.sorted.length) return;
const availableReactionsResult = appReactionsManager.getAvailableReactions();
// callbackify(availableReactionsResult, () => {
const counts = hasReactions ? (
availableReactionsResult instanceof Promise ?
reactions.results :
reactions.results.filter(reactionCount => {
return appReactionsManager.isReactionActive(reactionCount.reaction);
})
) : [];
forEachReverse(this.sorted, (reactionElement, idx, arr) => {
const reaction = reactionElement.reactionCount.reaction;
const found = counts.some(reactionCount => reactionCount.reaction === reaction);
if(!found) {
arr.splice(idx, 1);
reactionElement.remove();
}
});
const totalReactions = counts.reduce((acc, c) => acc + c.count, 0);
const canRenderAvatars = reactions && !!reactions.pFlags.can_see_list && totalReactions < REACTION_DISPLAY_BLOCK_COUNTER_AT;
this.sorted = counts.map((reactionCount, idx) => {
const reactionElementIdx = this.sorted.findIndex(reactionElement => reactionElement.reactionCount.reaction === reactionCount.reaction);
let reactionElement = reactionElementIdx !== -1 && this.sorted[reactionElementIdx];
if(!reactionElement) {
reactionElement = new ReactionElement();
reactionElement.init(this.type);
}
positionElementByIndex(reactionElement, this, idx);
const recentReactions = reactions.recent_reactions ? reactions.recent_reactions.filter(reaction => reaction.reaction === reactionCount.reaction) : [];
reactionElement.reactionCount = {...reactionCount};
reactionElement.setCanRenderAvatars(canRenderAvatars);
reactionElement.render(this.isPlaceholder);
reactionElement.renderCounter();
reactionElement.renderAvatars(recentReactions);
reactionElement.setIsChosen();
return reactionElement;
});
// this.sorted.forEach((reactionElement, idx) => {
// /* if(this.type === 'block' && this.childElementCount !== this.sorted.length) { // because of appended time
// idx += 1;
// } */
// positionElementByIndex(reactionElement, this, idx);
// });
if(!this.isPlaceholder && changedResults?.length) {
if(this.isConnected) {
this.handleChangedResults(changedResults);
} else {
this.onConnectCallback = () => {
this.handleChangedResults(changedResults);
};
}
}
// });
// ! тут вообще не должно быть этого кода, но пока он побудет тут
if(!this.sorted.length && this.type === 'block') {
const parentElement = this.parentElement;
this.remove();
if(parentElement.classList.contains('document-message') && !parentElement.childNodes.length) {
parentElement.remove();
return;
}
const timeSpan = this.querySelector('.time');
if(timeSpan) {
parentElement.append(timeSpan);
}
}
}
private handleChangedResults(changedResults: ReactionCount[]) {
// ! temp
if(this.message.peerId !== rootScope.peerId) return;
changedResults.forEach(reactionCount => {
const reactionElement = this.sorted.find(reactionElement => reactionElement.reactionCount.reaction === reactionCount.reaction);
if(reactionElement) {
reactionElement.fireAroundAnimation();
}
});
}
}
customElements.define(TAG_NAME, ReactionsElement);

13
src/components/chat/selection.ts

@ -35,6 +35,7 @@ import { randomLong } from "../../helpers/random";
import { attachContextMenuListener } from "../misc"; import { attachContextMenuListener } from "../misc";
import { attachClickEvent, AttachClickOptions } from "../../helpers/dom/clickEvent"; import { attachClickEvent, AttachClickOptions } from "../../helpers/dom/clickEvent";
import findUpAsChild from "../../helpers/dom/findUpAsChild"; import findUpAsChild from "../../helpers/dom/findUpAsChild";
import EventListenerBase from "../../helpers/eventListenerBase";
const accumulateMapSet = (map: Map<any, Set<number>>) => { const accumulateMapSet = (map: Map<any, Set<number>>) => {
return [...map.values()].reduce((acc, v) => acc + v.size, 0); return [...map.values()].reduce((acc, v) => acc + v.size, 0);
@ -42,7 +43,9 @@ const accumulateMapSet = (map: Map<any, Set<number>>) => {
//const MIN_CLICK_MOVE = 32; // minimum bubble height //const MIN_CLICK_MOVE = 32; // minimum bubble height
class AppSelection { class AppSelection extends EventListenerBase<{
toggle: (isSelecting: boolean) => void
}> {
public selectedMids: Map<PeerId, Set<number>> = new Map(); public selectedMids: Map<PeerId, Set<number>> = new Map();
public isSelecting = false; public isSelecting = false;
@ -84,6 +87,8 @@ class AppSelection {
lookupBetweenElementsQuery: string, lookupBetweenElementsQuery: string,
isScheduled?: AppSelection['isScheduled'] isScheduled?: AppSelection['isScheduled']
}) { }) {
super(false);
safeAssign(this, options); safeAssign(this, options);
this.navigationType = 'multiselect-' + randomLong() as any; this.navigationType = 'multiselect-' + randomLong() as any;
@ -356,6 +361,8 @@ class AppSelection {
if(wasSelecting === this.isSelecting) return false; if(wasSelecting === this.isSelecting) return false;
this.dispatchEvent('toggle', this.isSelecting);
// const bubblesContainer = this.bubbles.bubblesContainer; // const bubblesContainer = this.bubbles.bubblesContainer;
//bubblesContainer.classList.toggle('is-selecting', !!size); //bubblesContainer.classList.toggle('is-selecting', !!size);
@ -666,8 +673,8 @@ export default class ChatSelection extends AppSelection {
public selectionSendNowBtn: HTMLElement; public selectionSendNowBtn: HTMLElement;
public selectionForwardBtn: HTMLElement; public selectionForwardBtn: HTMLElement;
public selectionDeleteBtn: HTMLElement; public selectionDeleteBtn: HTMLElement;
selectionLeft: HTMLDivElement; private selectionLeft: HTMLDivElement;
selectionRight: HTMLDivElement; private selectionRight: HTMLDivElement;
constructor(private chat: Chat, private bubbles: ChatBubbles, private input: ChatInput, appMessagesManager: AppMessagesManager) { constructor(private chat: Chat, private bubbles: ChatBubbles, private input: ChatInput, appMessagesManager: AppMessagesManager) {
super({ super({

9
src/components/horizontalMenu.ts

@ -12,7 +12,14 @@ import { FocusDirection } from "../helpers/fastSmoothScroll";
import findUpAsChild from "../helpers/dom/findUpAsChild"; import findUpAsChild from "../helpers/dom/findUpAsChild";
import whichChild from "../helpers/dom/whichChild"; import whichChild from "../helpers/dom/whichChild";
export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?: (id: number, tabContent: HTMLDivElement, animate: boolean) => void | boolean, onTransitionEnd?: () => void, transitionTime = 250, scrollableX?: ScrollableX) { export function horizontalMenu(
tabs: HTMLElement,
content: HTMLElement,
onClick?: (id: number, tabContent: HTMLDivElement, animate: boolean) => void | boolean,
onTransitionEnd?: () => void,
transitionTime = 250,
scrollableX?: ScrollableX
) {
const selectTab = TransitionSlider(content, tabs || content.dataset.animation === 'tabs' ? 'tabs' : 'navigation', transitionTime, onTransitionEnd); const selectTab = TransitionSlider(content, tabs || content.dataset.animation === 'tabs' ? 'tabs' : 'navigation', transitionTime, onTransitionEnd);
if(tabs) { if(tabs) {

51
src/components/misc.ts

@ -179,36 +179,62 @@ export function openBtnMenu(menuElement: HTMLElement, onClose?: () => void) {
rootScope.dispatchEvent('context_menu_toggle', true); rootScope.dispatchEvent('context_menu_toggle', true);
} }
export type MenuPositionPadding = {
top?: number,
right?: number,
bottom?: number,
left?: number
};
const PADDING_TOP = 8; const PADDING_TOP = 8;
const PADDING_BOTTOM = PADDING_TOP;
const PADDING_LEFT = 8; const PADDING_LEFT = 8;
export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLElement, side?: 'left' | 'right' | 'center') { const PADDING_RIGHT = PADDING_LEFT;
export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLElement, side?: 'left' | 'right' | 'center', additionalPadding?: MenuPositionPadding) {
//let {clientX, clientY} = e; //let {clientX, clientY} = e;
// * side mean the OPEN side // * side mean the OPEN side
let {scrollWidth: menuWidth, scrollHeight: menuHeight} = elem; const getScrollWidthFromElement = (Array.from(elem.children) as HTMLElement[]).find(element => element.classList.contains('btn-menu-item') && !element.classList.contains('hide')) || elem;
let {scrollWidth: menuWidth} = getScrollWidthFromElement;
let {scrollHeight: menuHeight} = elem;
//let {innerWidth: windowWidth, innerHeight: windowHeight} = window; //let {innerWidth: windowWidth, innerHeight: windowHeight} = window;
const rect = document.body.getBoundingClientRect(); const rect = document.body.getBoundingClientRect();
const windowWidth = rect.width; const windowWidth = rect.width;
const windowHeight = rect.height; const windowHeight = rect.height;
let paddingTop = PADDING_TOP, paddingRight = PADDING_RIGHT, paddingBottom = PADDING_BOTTOM, paddingLeft = PADDING_LEFT;
if(additionalPadding) {
if(additionalPadding.top) paddingTop += additionalPadding.top;
if(additionalPadding.right) paddingRight += additionalPadding.right;
if(additionalPadding.bottom) paddingBottom += additionalPadding.bottom;
if(additionalPadding.left) paddingLeft += additionalPadding.left;
}
side = mediaSizes.isMobile ? 'right' : 'left'; side = mediaSizes.isMobile ? 'right' : 'left';
let verticalSide: 'top' /* | 'bottom' */ | 'center' = 'top'; let verticalSide: 'top' /* | 'bottom' */ | 'center' = 'top';
const maxTop = windowHeight - menuHeight - paddingBottom;
const maxLeft = windowWidth - menuWidth - paddingRight;
const minTop = paddingTop;
const minLeft = paddingLeft;
const getSides = () => { const getSides = () => {
return { return {
x: { x: {
left: pageX, left: pageX,
right: pageX - menuWidth right: Math.min(maxLeft, pageX - menuWidth)
}, },
intermediateX: side === 'right' ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT, intermediateX: side === 'right' ? minLeft : maxLeft,
//intermediateX: clientX < windowWidth / 2 ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT, //intermediateX: clientX < windowWidth / 2 ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT,
y: { y: {
top: pageY, top: pageY,
bottom: pageY - menuHeight bottom: pageY - menuHeight
}, },
//intermediateY: verticalSide === 'top' ? PADDING_TOP : windowHeight - menuHeight - PADDING_TOP, //intermediateY: verticalSide === 'top' ? paddingTop : windowHeight - menuHeight - paddingTop,
intermediateY: pageY < windowHeight / 2 ? PADDING_TOP : windowHeight - menuHeight - PADDING_TOP, // intermediateY: pageY < (windowHeight / 2) ? paddingTop : windowHeight - menuHeight - paddingBottom,
intermediateY: maxTop,
}; };
}; };
@ -216,12 +242,12 @@ export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLEleme
const possibleSides = { const possibleSides = {
x: { x: {
left: sides.x.left + menuWidth + PADDING_LEFT <= windowWidth, left: (sides.x.left + menuWidth + paddingRight) <= windowWidth,
right: sides.x.right >= PADDING_LEFT right: sides.x.right >= paddingLeft
}, },
y: { y: {
top: sides.y.top + menuHeight + PADDING_TOP <= windowHeight, top: (sides.y.top + menuHeight + paddingBottom) <= windowHeight,
bottom: sides.y.bottom - PADDING_TOP >= PADDING_TOP bottom: (sides.y.bottom - paddingBottom) >= paddingBottom
} }
}; };
@ -277,6 +303,11 @@ export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLEleme
(verticalSide === 'center' ? verticalSide : 'bottom') + (verticalSide === 'center' ? verticalSide : 'bottom') +
'-' + '-' +
(side === 'center' ? side : (side === 'left' ? 'right' : 'left'))); (side === 'center' ? side : (side === 'left' ? 'right' : 'left')));
return {
width: menuWidth,
height: menuHeight
};
} }
let _cancelContextMenuOpening = false, _cancelContextMenuOpeningTimeout = 0; let _cancelContextMenuOpening = false, _cancelContextMenuOpeningTimeout = 0;

6
src/components/peerProfile.ts

@ -93,7 +93,7 @@ export default class PeerProfile {
return; return;
} }
appProfileManager.getProfileByPeerId(this.peerId).then(full => { Promise.resolve(appProfileManager.getProfileByPeerId(this.peerId)).then(full => {
copyTextToClipboard(full.about); copyTextToClipboard(full.about);
toast(I18n.format('BioCopied', true)); toast(I18n.format('BioCopied', true));
}); });
@ -324,7 +324,7 @@ export default class PeerProfile {
let promise: Promise<boolean>; let promise: Promise<boolean>;
if(peerId.isUser()) { if(peerId.isUser()) {
promise = appProfileManager.getProfile(peerId, override).then(userFull => { promise = Promise.resolve(appProfileManager.getProfile(peerId, override)).then(userFull => {
if(this.peerId !== peerId || this.threadId !== threadId) { if(this.peerId !== peerId || this.threadId !== threadId) {
//this.log.warn('peer changed'); //this.log.warn('peer changed');
return false; return false;
@ -338,7 +338,7 @@ export default class PeerProfile {
return true; return true;
}); });
} else { } else {
promise = appProfileManager.getChatFull(peerId.toChatId(), override).then((chatFull) => { promise = Promise.resolve(appProfileManager.getChatFull(peerId.toChatId(), override)).then((chatFull) => {
if(this.peerId !== peerId || this.threadId !== threadId) { if(this.peerId !== peerId || this.threadId !== threadId) {
//this.log.warn('peer changed'); //this.log.warn('peer changed');
return false; return false;

2
src/components/peerProfileAvatars.ts

@ -245,7 +245,7 @@ export default class PeerProfileAvatars {
} else { } else {
const promises: [Promise<ChatFull>, ReturnType<AppMessagesManager['getSearch']>] = [] as any; const promises: [Promise<ChatFull>, ReturnType<AppMessagesManager['getSearch']>] = [] as any;
if(!listLoader.current) { if(!listLoader.current) {
promises.push(appProfileManager.getChatFull(peerId.toChatId())); promises.push(Promise.resolve(appProfileManager.getChatFull(peerId.toChatId())));
} }
promises.push(appMessagesManager.getSearch({ promises.push(appMessagesManager.getSearch({

2
src/components/popups/avatar.ts

@ -8,8 +8,8 @@ import appDownloadManager from "../../lib/appManagers/appDownloadManager";
import resizeableImage from "../../lib/cropper"; import resizeableImage from "../../lib/cropper";
import PopupElement from "."; import PopupElement from ".";
import { _i18n } from "../../lib/langPack"; import { _i18n } from "../../lib/langPack";
import { readBlobAsDataURL } from "../../helpers/blob";
import { attachClickEvent } from "../../helpers/dom/clickEvent"; import { attachClickEvent } from "../../helpers/dom/clickEvent";
import readBlobAsDataURL from "../../helpers/blob/readBlobAsDataURL";
export default class PopupAvatar extends PopupElement { export default class PopupAvatar extends PopupElement {
private cropContainer: HTMLElement; private cropContainer: HTMLElement;

220
src/components/popups/reactedList.ts

@ -0,0 +1,220 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import PopupElement from ".";
import { Message } from "../../layer";
import { generateDelimiter, SettingSection } from "../sidebarLeft";
import ReactionsElement from "../chat/reactions";
import { horizontalMenu } from "../horizontalMenu";
import Scrollable from "../scrollable";
import ScrollableLoader from "../../helpers/scrollableLoader";
import appDialogsManager from "../../lib/appManagers/appDialogsManager";
import replaceContent from "../../helpers/dom/replaceContent";
import appUsersManager from "../../lib/appManagers/appUsersManager";
import appReactionsManager from "../../lib/appManagers/appReactionsManager";
import { wrapSticker } from "../wrappers";
import ReactionElement from "../chat/reaction";
export default class PopupReactedList extends PopupElement {
constructor(
private appMessagesManager: AppMessagesManager,
private message: Message.message
) {
super('popup-reacted-list', /* [{
langKey: 'Close',
isCancel: true
}] */null, {closable: true, overlayClosable: true, body: true});
this.init();
}
private async init() {
const message = this.appMessagesManager.getGroupsFirstMessage(this.message);
const canViewReadParticipants = this.appMessagesManager.canViewMessageReadParticipants(message);
// this.body.append(generateDelimiter());
const reactionsElement = new ReactionsElement();
const newMessage: Message.message = {
...message,
mid: 0,
id: 0,
reactions: {
_: 'messageReactions',
results: [],
...message.reactions,
pFlags: {},
recent_reactions: []
}
};
newMessage.reactions.results = newMessage.reactions.results.map(reactionCount => {
return {
...reactionCount,
pFlags: {}
};
});
reactionsElement.init(newMessage, 'block');
reactionsElement.render();
reactionsElement.classList.add('no-stripe');
reactionsElement.classList.remove('has-no-reactions');
reactionsElement.append(this.btnClose);
this.header.append(reactionsElement);
const tabsContainer = document.createElement('div');
tabsContainer.classList.add('tabs-container');
tabsContainer.dataset.animation = 'tabs';
const loaders: Map<HTMLElement, ScrollableLoader> = new Map();
let hasAllReactions = false;
if(newMessage.reactions.results.length) {
const reaction = this.createFakeReaction('reactions', newMessage.reactions.results.reduce((acc, r) => acc + r.count, 0));
reactionsElement.prepend(reaction);
newMessage.reactions.results.unshift(reaction.reactionCount);
hasAllReactions = true;
}
let hasReadParticipants = false;
if(canViewReadParticipants) {
try {
const readUserIds = await this.appMessagesManager.getMessageReadParticipants(message.peerId, message.mid);
if(!readUserIds.length) {
throw '';
}
const reaction = this.createFakeReaction('checks', readUserIds.length);
reactionsElement.prepend(reaction);
newMessage.reactions.results.unshift(reaction.reactionCount);
hasReadParticipants = true;
} catch(err) {
}
}
newMessage.reactions.results.forEach(reactionCount => {
const scrollable = new Scrollable(undefined);
scrollable.container.classList.add('tabs-tab');
const section = new SettingSection({
noShadow: true,
noDelimiter: true
});
const chatlist = appDialogsManager.createChatList({
dialogSize: 72
});
appDialogsManager.setListClickListener(chatlist, () => {
this.hide();
}, undefined, false, true);
section.content.append(chatlist);
scrollable.container.append(section.container);
const skipReadParticipants = reactionCount.reaction !== 'checks';
const skipReactionsList = reactionCount.reaction === 'checks';
if(['checks', 'reactions'].includes(reactionCount.reaction)) {
reactionCount.reaction = undefined;
}
let nextOffset: string;
const loader = new ScrollableLoader({
scrollable,
getPromise: async() => {
const result = await this.appMessagesManager.getMessageReactionsListAndReadParticipants(message, undefined, reactionCount.reaction, nextOffset, skipReadParticipants, skipReactionsList);
nextOffset = result.nextOffset;
result.combined.forEach(({peerId, reaction}) => {
const {dom} = appDialogsManager.addDialogNew({
dialog: peerId,
autonomous: true,
container: chatlist,
avatarSize: 54,
rippleEnabled: false,
meAsSaved: false,
drawStatus: false
});
if(reaction) {
const stickerContainer = document.createElement('div');
stickerContainer.classList.add('reacted-list-reaction-icon');
const availableReaction = appReactionsManager.getReactionCached(reaction);
wrapSticker({
doc: availableReaction.static_icon,
div: stickerContainer,
width: 24,
height: 24
});
dom.listEl.append(stickerContainer);
}
replaceContent(dom.lastMessageSpan, appUsersManager.getUserStatusString(peerId.toUserId()));
});
return !nextOffset;
}
});
loaders.set(scrollable.container, loader);
tabsContainer.append(scrollable.container);
});
this.body.append(tabsContainer);
const selectTab = horizontalMenu(reactionsElement, tabsContainer, (id, tabContent) => {
if(id === (reactionsElement.childElementCount - 1)) {
return false;
}
const reaction = reactionsElement.children[id] as ReactionElement;
const prevId = selectTab.prevId();
if(prevId !== -1) {
(reactionsElement.children[prevId] as ReactionElement).setIsChosen(false);
}
reaction.setIsChosen(true);
const loader = loaders.get(tabContent);
loader.load();
});
// selectTab(hasAllReactions && hasReadParticipants ? 1 : 0, false);
selectTab(0, false);
this.show();
}
private createFakeReaction(icon: string, count: number) {
const reaction = new ReactionElement();
reaction.init('block');
reaction.reactionCount = {
_: 'reactionCount',
count: count,
reaction: icon
};
reaction.setCanRenderAvatars(false);
reaction.renderCounter();
const allReactionsSticker = document.createElement('div');
allReactionsSticker.classList.add('reaction-counter', 'reaction-sticker-icon', 'tgico-' + icon);
reaction.prepend(allReactionsSticker);
return reaction;
}
}

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

@ -296,7 +296,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
}); });
const renderQuickReaction = () => { const renderQuickReaction = () => {
appReactionsManager.getQuickReaction().then(reaction => { Promise.resolve(appReactionsManager.getQuickReaction()).then(reaction => {
wrapStickerToRow({ wrapStickerToRow({
row: reactionsRow, row: reactionsRow,
doc: reaction.static_icon, doc: reaction.static_icon,

64
src/components/stackedAvatars.ts

@ -0,0 +1,64 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import AvatarElement from "./avatar";
import type { LazyLoadQueueIntersector } from "./lazyLoadQueue";
const CLASS_NAME = 'stacked-avatars';
const AVATAR_CLASS_NAME = CLASS_NAME + '-avatar';
const AVATAR_CONTAINER_CLASS_NAME = AVATAR_CLASS_NAME + '-container';
export default class StackedAvatars {
public container: HTMLElement;
private lazyLoadQueue: LazyLoadQueueIntersector;
private avatarSize: number;
constructor(options: {
lazyLoadQueue?: LazyLoadQueueIntersector,
avatarSize: number
}) {
this.lazyLoadQueue = options.lazyLoadQueue;
this.avatarSize = options.avatarSize;
this.container = document.createElement('div');
this.container.classList.add(CLASS_NAME);
this.container.style.setProperty('--avatar-size', options.avatarSize + 'px');
}
public render(peerIds: PeerId[], loadPromises?: Promise<any>[]) {
const children = this.container.children;
peerIds.slice().reverse().forEach((peerId, idx) => {
let avatarContainer = children[idx] as HTMLElement;
if(!avatarContainer) {
avatarContainer = document.createElement('div');
avatarContainer.classList.add(AVATAR_CONTAINER_CLASS_NAME);
}
let avatarElem = avatarContainer.firstElementChild as AvatarElement;
if(!avatarElem) {
avatarElem = new AvatarElement();
avatarElem.setAttribute('dialog', '0');
avatarElem.classList.add('avatar-' + this.avatarSize, AVATAR_CLASS_NAME);
avatarElem.lazyLoadQueue = this.lazyLoadQueue;
avatarElem.loadPromises = loadPromises;
}
avatarElem.setAttribute('peer', '' + peerId);
if(!avatarElem.parentNode) {
avatarContainer.append(avatarElem);
}
if(!avatarContainer.parentNode) {
this.container.append(avatarContainer);
}
});
// if were 3 and became 2
(Array.from(children) as HTMLElement[]).slice(peerIds.length).forEach(el => el.remove());
}
}

174
src/components/wrappers.ts

@ -1118,6 +1118,102 @@ export function renderImageWithFadeIn(container: HTMLElement,
// }); // });
// } // }
export function wrapStickerAnimation({
size,
doc,
middleware,
target,
side,
skipRatio,
play
}: {
size: number,
doc: MyDocument,
middleware?: () => boolean,
target: HTMLElement,
side: 'left' | 'center' | 'right',
skipRatio?: number,
play: boolean
}) {
const animationDiv = document.createElement('div');
animationDiv.classList.add('emoji-animation');
// const size = 280;
animationDiv.style.width = size + 'px';
animationDiv.style.height = size + 'px';
const stickerPromise = wrapSticker({
div: animationDiv,
doc,
middleware,
withThumb: false,
needFadeIn: false,
loop: false,
width: size,
height: size,
play,
group: 'none',
skipRatio
}).then(animation => {
assumeType<RLottiePlayer>(animation);
animation.addEventListener('enterFrame', (frameNo) => {
if(frameNo === animation.maxFrame) {
animation.remove();
animationDiv.remove();
appImManager.chat.bubbles.scrollable.container.removeEventListener('scroll', onScroll);
}
});
if(IS_VIBRATE_SUPPORTED) {
animation.addEventListener('firstFrame', () => {
navigator.vibrate(100);
}, {once: true});
}
return animation;
});
const generateRandomSigned = (max: number) => {
const r = Math.random() * max * 2;
return r > max ? -r % max : r;
};
const randomOffsetX = generateRandomSigned(16);
const randomOffsetY = generateRandomSigned(4);
const stableOffsetX = size / 8 * (side === 'right' ? 1 : -1);
const setPosition = () => {
if(!isInDOM(target)) {
return;
}
const rect = target.getBoundingClientRect();
/* const boxWidth = Math.max(rect.width, rect.height);
const boxHeight = Math.max(rect.width, rect.height);
const x = rect.left + ((boxWidth - size) / 2);
const y = rect.top + ((boxHeight - size) / 2); */
const rectX = side === 'right' ? rect.right : rect.left;
const addOffsetX = side === 'center' ? (rect.width - size) / 2 : (side === 'right' ? -size : 0) + stableOffsetX + randomOffsetX;
const x = rectX + addOffsetX;
// const y = rect.bottom - size + size / 4;
const y = rect.top + ((rect.height - size) / 2) + (side === 'center' ? 0 : randomOffsetY);
// animationDiv.style.transform = `translate(${x}px, ${y}px)`;
animationDiv.style.top = y + 'px';
animationDiv.style.left = x + 'px';
};
const onScroll = throttleWithRaf(setPosition);
appImManager.chat.bubbles.scrollable.container.addEventListener('scroll', onScroll);
setPosition();
appImManager.emojiAnimationContainer.append(animationDiv);
return {animationDiv, stickerPromise};
}
export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn, needUpscale, skipRatio}: { 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,
@ -1384,80 +1480,18 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
return; return;
} }
const animationDiv = document.createElement('div'); const bubble = findUpClassName(div, 'bubble');
animationDiv.classList.add('emoji-animation'); const isOut = bubble.classList.contains('is-out');
const size = 280;
animationDiv.style.width = size + 'px';
animationDiv.style.height = size + 'px';
wrapSticker({ const {animationDiv} = wrapStickerAnimation({
div: animationDiv,
doc, doc,
middleware, middleware,
withThumb: false, side: isOut ? 'right' : 'left',
needFadeIn: false, size: 280,
loop: false, target: div,
width: size, play: true
height: size,
play: true,
group: 'none'
}).then(animation => {
assumeType<RLottiePlayer>(animation);
animation.addEventListener('enterFrame', (frameNo) => {
if(frameNo === animation.maxFrame) {
animation.remove();
animationDiv.remove();
appImManager.chat.bubbles.scrollable.container.removeEventListener('scroll', onScroll);
}
});
if(IS_VIBRATE_SUPPORTED) {
animation.addEventListener('firstFrame', () => {
navigator.vibrate(100);
}, {once: true});
}
}); });
const generateRandomSigned = (max: number) => {
const r = Math.random() * max * 2;
return r > max ? -r % max : r;
};
const bubble = findUpClassName(div, 'bubble');
const isOut = bubble.classList.contains('is-out');
const randomOffsetX = generateRandomSigned(16);
const randomOffsetY = generateRandomSigned(4);
const stableOffsetX = size / 8 * (isOut ? 1 : -1);
const setPosition = () => {
if(!isInDOM(div)) {
return;
}
const rect = div.getBoundingClientRect();
/* const boxWidth = Math.max(rect.width, rect.height);
const boxHeight = Math.max(rect.width, rect.height);
const x = rect.left + ((boxWidth - size) / 2);
const y = rect.top + ((boxHeight - size) / 2); */
const rectX = isOut ? rect.right : rect.left;
const addOffsetX = (isOut ? -size : 0) + stableOffsetX + randomOffsetX;
const x = rectX + addOffsetX;
// const y = rect.bottom - size + size / 4;
const y = rect.top + ((rect.height - size) / 2) + randomOffsetY;
// animationDiv.style.transform = `translate(${x}px, ${y}px)`;
animationDiv.style.top = y + 'px';
animationDiv.style.left = x + 'px';
};
const onScroll = throttleWithRaf(setPosition);
appImManager.chat.bubbles.scrollable.container.addEventListener('scroll', onScroll);
setPosition();
if(bubble) { if(bubble) {
if(isOut) { if(isOut) {
animationDiv.classList.add('is-out'); animationDiv.classList.add('is-out');
@ -1466,8 +1500,6 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
} }
} }
appImManager.emojiAnimationContainer.append(animationDiv);
if(!sendInteractionThrottled) { if(!sendInteractionThrottled) {
sendInteractionThrottled = throttle(() => { sendInteractionThrottled = throttle(() => {
const length = data.a.length; const length = data.a.length;

17
src/helpers/array.ts

@ -48,16 +48,17 @@ export function insertInDescendSortedArray<T extends {[smth in K]?: number}, K e
if(pos === undefined) { if(pos === undefined) {
pos = array.indexOf(element); pos = array.indexOf(element);
if(pos !== -1) { }
const prev = array[pos - 1];
const next = array[pos + 1];
if((!prev || prev[property] >= sortProperty) && (!next || next[property] <= sortProperty)) {
// console.warn('same pos', pos, sortProperty, prev, next);
return pos;
}
array.splice(pos, 1); if(pos !== -1) {
const prev = array[pos - 1];
const next = array[pos + 1];
if((!prev || prev[property] >= sortProperty) && (!next || next[property] <= sortProperty)) {
// console.warn('same pos', pos, sortProperty, prev, next);
return pos;
} }
array.splice(pos, 1);
} }
const len = array.length; const len = array.length;

11
src/helpers/callbackify.ts

@ -1,6 +1,15 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {Awaited} from '../types'; import {Awaited} from '../types';
export default function callbackify<T extends Awaited<any>, R extends any>(smth: T, callback: (result: Awaited<T>) => R): PromiseLike<R> | R { export default function callbackify<T extends Awaited<any>, R>(
smth: T,
callback: (result: Awaited<T>) => R
): PromiseLike<R> | R {
if(smth instanceof Promise) { if(smth instanceof Promise) {
return smth.then(callback); return smth.then(callback);
} else { } else {

18
src/helpers/callbackifyAll.ts

@ -0,0 +1,18 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {Awaited} from '../types';
export default function callbackifyAll<T extends readonly unknown[] | [], R extends any>(
values: T,
callback: (result: { -readonly [P in keyof T]: Awaited<T[P]> }) => R
): PromiseLike<R> | R {
if(values.some(value => value instanceof Promise)) {
return Promise.all(values).then(callback as any);
} else {
return callback(values as any);
}
}

12
src/helpers/getTimeFormat.ts

@ -0,0 +1,12 @@
export default function getTimeFormat(): 'h12' | 'h23' {
// try {
// const resolvedOptions = Intl.DateTimeFormat(navigator.language, {hour: 'numeric'}).resolvedOptions();
// if('hourCycle' in resolvedOptions) {
// return (resolvedOptions as any).hourCycle === 'h12' ? 'h12' : 'h23';
// } else {
// return resolvedOptions.hour12 ? 'h12' : 'h23';
// }
// } catch(err) {
return new Date().toLocaleString().match(/\s(AM|PM)/) ? 'h12' : 'h23';
// }
}

10
src/lang.ts

@ -646,6 +646,12 @@ const lang = {
"EnableReactionsChannelInfo": "Allow subscribers to react to channel posts.", "EnableReactionsChannelInfo": "Allow subscribers to react to channel posts.",
"EnableReactionsGroupInfo": "Allow members to react to group messages.", "EnableReactionsGroupInfo": "Allow members to react to group messages.",
"AvailableReactions": "Available reactions", "AvailableReactions": "Available reactions",
"NobodyViewed": "Nobody viewed",
"MessageSeen": {
"one_value": "Seen",
"other_value": "%1$d Seen"
},
// "Close": "Close",
// * macos // * macos
"AccountSettings.Filters": "Chat Folders", "AccountSettings.Filters": "Chat Folders",
@ -710,6 +716,10 @@ const lang = {
}, },
"Chat.CopySelectedText": "Copy Selected Text", "Chat.CopySelectedText": "Copy Selected Text",
"Chat.Confirm.Unpin": "Would you like to unpin this message?", "Chat.Confirm.Unpin": "Would you like to unpin this message?",
"Chat.Context.Reacted": "%1$@/%2$@ Reacted",
"Chat.Context.ReactedFast": {
"other_value": "%d Reacted"
},
"Chat.Date.ScheduledFor": "Scheduled for %@", "Chat.Date.ScheduledFor": "Scheduled for %@",
"Chat.Date.ScheduledForToday": "Scheduled for today", "Chat.Date.ScheduledForToday": "Scheduled for today",
"Chat.DropTitle": "Drop files here to send them", "Chat.DropTitle": "Drop files here to send them",

7
src/layer.d.ts vendored

@ -2640,7 +2640,10 @@ export namespace Update {
_: 'updateMessageReactions', _: 'updateMessageReactions',
peer: Peer, peer: Peer,
msg_id: number, msg_id: number,
reactions: MessageReactions reactions: MessageReactions,
pts?: number,
pts_count?: number,
local?: boolean
}; };
export type updateNewDiscussionMessage = { export type updateNewDiscussionMessage = {
@ -9308,7 +9311,7 @@ export namespace MessageReactions {
can_see_list?: true, can_see_list?: true,
}>, }>,
results: Array<ReactionCount>, results: Array<ReactionCount>,
recent_reactons?: Array<MessageUserReaction> recent_reactions?: Array<MessageUserReaction>
}; };
} }

64
src/lib/appManagers/appMessagesManager.ts

@ -17,7 +17,7 @@ import { createPosterForVideo } from "../../helpers/files";
import { copy, deepEqual, 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, MessageUserReaction } 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, ReactionCount } 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";
@ -2380,6 +2380,20 @@ export class AppMessagesManager {
return {message, entities, totalEntities}; return {message, entities, totalEntities};
} }
public getGroupsFirstMessage(message: Message.message): Message.message {
if(!message.grouped_id) return message;
const storage = this.groupedMessagesStorage[message.grouped_id];
let minMid = Number.MAX_SAFE_INTEGER;
for(const [mid, message] of storage) {
if(message.mid < minMid) {
minMid = message.mid;
}
}
return storage.get(minMid);
}
public getMidsByAlbum(grouped_id: string) { public getMidsByAlbum(grouped_id: string) {
return getObjectKeysAndSort(this.groupedMessagesStorage[grouped_id], 'asc'); return getObjectKeysAndSort(this.groupedMessagesStorage[grouped_id], 'asc');
//return Object.keys(this.groupedMessagesStorage[grouped_id]).map(id => +id).sort((a, b) => a - b); //return Object.keys(this.groupedMessagesStorage[grouped_id]).map(id => +id).sort((a, b) => a - b);
@ -2390,10 +2404,10 @@ export class AppMessagesManager {
else return [message.mid]; else return [message.mid];
} }
public filterMessages(message: any, verify: (message: MyMessage) => boolean) { public filterMessages(message: MyMessage, verify: (message: MyMessage) => boolean) {
const out: MyMessage[] = []; const out: MyMessage[] = [];
if(message.grouped_id) { if((message as Message.message).grouped_id) {
const storage = this.groupedMessagesStorage[message.grouped_id]; const storage = this.groupedMessagesStorage[(message as Message.message).grouped_id];
for(const [mid, message] of storage) { for(const [mid, message] of storage) {
if(verify(message)) { if(verify(message)) {
out.push(message); out.push(message);
@ -3481,7 +3495,7 @@ export class AppMessagesManager {
} }
if(!message.pFlags.out || ( if(!message.pFlags.out || (
message.peerId.isUser() && message.peer_id._ !== 'peerChannel' &&
message.date < (tsNow(true) - rootScope.config.edit_time_limit) && message.date < (tsNow(true) - rootScope.config.edit_time_limit) &&
(message as Message.message).media?._ !== 'messageMediaPoll' (message as Message.message).media?._ !== 'messageMediaPoll'
) )
@ -3938,7 +3952,7 @@ export class AppMessagesManager {
appUsersManager.saveApiUsers(result.users); appUsersManager.saveApiUsers(result.users);
this.saveMessages(result.messages); this.saveMessages(result.messages);
const message = this.filterMessages(result.messages[0], message => !!(message as Message.message).replies)[0] as Message.message; const message = this.filterMessages(result.messages[0] as Message.message, message => !!(message as Message.message).replies)[0] as Message.message;
const threadKey = message.peerId + '_' + message.mid; const threadKey = message.peerId + '_' + message.mid;
this.generateThreadServiceStartMessage(message); this.generateThreadServiceStartMessage(message);
@ -4581,11 +4595,11 @@ export class AppMessagesManager {
return; return;
} }
const recentReactions = reactions.recent_reactons; const recentReactions = reactions?.recent_reactions;
if(recentReactions) { if(recentReactions?.length) {
const recentReaction = recentReactions[recentReactions.length - 1]; const recentReaction = recentReactions[recentReactions.length - 1];
const previousReactions = message.reactions; const previousReactions = message.reactions;
const previousRecentReactions = previousReactions?.recent_reactons; const previousRecentReactions = previousReactions?.recent_reactions;
if( if(
recentReaction.user_id !== rootScope.myId.toUserId() && ( recentReaction.user_id !== rootScope.myId.toUserId() && (
!previousRecentReactions || !previousRecentReactions ||
@ -4605,11 +4619,30 @@ export class AppMessagesManager {
} }
} }
const results = reactions?.results ?? [];
const previousResults = message.reactions?.results ?? [];
const changedResults = results.filter(reactionCount => {
const previousReactionCount = previousResults.find(_reactionCount => _reactionCount.reaction === reactionCount.reaction);
return (
message.pFlags.out && (
!previousReactionCount ||
reactionCount.count > previousReactionCount.count
)
) || (
reactionCount.pFlags.chosen && (
!previousReactionCount ||
!previousReactionCount.pFlags.chosen
)
);
});
message.reactions = reactions; message.reactions = reactions;
rootScope.dispatchEvent('message_reactions', message); rootScope.dispatchEvent('message_reactions', {message, changedResults});
this.setDialogToStateIfMessageIsTop(message); if(!update.local) {
this.setDialogToStateIfMessageIsTop(message);
}
}; };
private onUpdateDialogUnreadMark = (update: Update.updateDialogUnreadMark) => { private onUpdateDialogUnreadMark = (update: Update.updateDialogUnreadMark) => {
@ -5329,7 +5362,9 @@ export class AppMessagesManager {
message: Message.message, message: Message.message,
limit?: number, limit?: number,
reaction?: string, reaction?: string,
offset?: string offset?: string,
skipReadParticipants?: boolean,
skipReactionsList?: boolean
) { ) {
const emptyMessageReactionsList = { const emptyMessageReactionsList = {
reactions: [] as MessageUserReaction[], reactions: [] as MessageUserReaction[],
@ -5345,9 +5380,9 @@ export class AppMessagesManager {
} }
return Promise.all([ return Promise.all([
canViewMessageReadParticipants ? this.getMessageReadParticipants(message.peerId, message.mid).catch(() => [] as UserId[]) : [] as UserId[], canViewMessageReadParticipants && !reaction && !skipReadParticipants ? 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 message.reactions?.recent_reactions?.length && !skipReactionsList ? appReactionsManager.getMessageReactionsList(message.peerId, message.mid, limit, reaction, offset).catch(err => emptyMessageReactionsList) : emptyMessageReactionsList
]).then(([userIds, messageReactionsList]) => { ]).then(([userIds, messageReactionsList]) => {
const readParticipantsPeerIds = userIds.map(userId => userId.toPeerId()); const readParticipantsPeerIds = userIds.map(userId => userId.toPeerId());
@ -5363,6 +5398,7 @@ export class AppMessagesManager {
return { return {
reactions: messageReactionsList.reactions, reactions: messageReactionsList.reactions,
reactionsCount: messageReactionsList.count,
readParticipants: readParticipantsPeerIds, readParticipants: readParticipantsPeerIds,
combined: combined, combined: combined,
nextOffset: messageReactionsList.next_offset nextOffset: messageReactionsList.next_offset

66
src/lib/appManagers/appProfileManager.ts

@ -31,8 +31,8 @@ export type UserTyping = Partial<{userId: UserId, action: SendMessageAction, tim
export class AppProfileManager { export class AppProfileManager {
//private botInfos: any = {}; //private botInfos: any = {};
public usersFull: {[id: UserId]: UserFull.userFull} = {}; private usersFull: {[id: UserId]: UserFull.userFull} = {};
public chatsFull: {[id: ChatId]: ChatFull} = {}; private chatsFull: {[id: ChatId]: ChatFull} = {};
private typingsInPeer: {[peerId: PeerId]: UserTyping[]}; private typingsInPeer: {[peerId: PeerId]: UserTyping[]};
constructor() { constructor() {
@ -110,7 +110,7 @@ export class AppProfileManager {
const {photo} = chat as Chat.chat; const {photo} = chat as Chat.chat;
if(photo) { if(photo) {
const hasChatPhoto = photo._ !== 'chatPhotoEmpty'; const hasChatPhoto = photo._ !== 'chatPhotoEmpty';
const hasFullChatPhoto = fullChat.chat_photo?._ !== 'photoEmpty'; const hasFullChatPhoto = !!(fullChat.chat_photo && fullChat.chat_photo._ !== 'photoEmpty'); // chat_photo can be missing
if(hasChatPhoto !== hasFullChatPhoto || (photo as ChatPhoto.chatPhoto).photo_id !== fullChat.chat_photo?.id) { if(hasChatPhoto !== hasFullChatPhoto || (photo as ChatPhoto.chatPhoto).photo_id !== fullChat.chat_photo?.id) {
updated = true; updated = true;
} }
@ -158,9 +158,9 @@ export class AppProfileManager {
}; };
} */ } */
public getProfile(id: UserId, override?: true): Promise<UserFull> { public getProfile(id: UserId, override?: true) {
if(this.usersFull[id] && !override) { if(this.usersFull[id] && !override) {
return Promise.resolve(this.usersFull[id]); return this.usersFull[id];
} }
return apiManager.invokeApiSingleProcess({ return apiManager.invokeApiSingleProcess({
@ -201,7 +201,7 @@ export class AppProfileManager {
}); });
} }
public getProfileByPeerId(peerId: PeerId, override?: true): Promise<ChatFull.chatFull | ChatFull.channelFull | UserFull.userFull> { public getProfileByPeerId(peerId: PeerId, override?: true) {
if(appPeersManager.isAnyChat(peerId)) return this.getChatFull(peerId.toChatId(), override); if(appPeersManager.isAnyChat(peerId)) return this.getChatFull(peerId.toChatId(), override);
else return this.getProfile(peerId.toUserId(), override); else return this.getProfile(peerId.toUserId(), override);
} }
@ -218,16 +218,15 @@ export class AppProfileManager {
return peerId.isUser() ? this.getCachedFullUser(peerId.toUserId()) : this.getCachedFullChat(peerId.toChatId()); return peerId.isUser() ? this.getCachedFullUser(peerId.toUserId()) : this.getCachedFullChat(peerId.toChatId());
} }
public getFullPhoto(peerId: PeerId) { public async getFullPhoto(peerId: PeerId) {
return this.getProfileByPeerId(peerId).then(profile => { const profile = await this.getProfileByPeerId(peerId);
switch(profile._) { switch(profile._) {
case 'userFull': case 'userFull':
return profile.profile_photo; return profile.profile_photo;
case 'channelFull': case 'channelFull':
case 'chatFull': case 'chatFull':
return profile.chat_photo; return profile.chat_photo;
} }
});
} }
/* public getPeerBots(peerId: PeerId) { /* public getPeerBots(peerId: PeerId) {
@ -254,7 +253,7 @@ export class AppProfileManager {
}); });
} */ } */
public getChatFull(id: ChatId, override?: true): Promise<ChatFull.chatFull | ChatFull.channelFull> { public getChatFull(id: ChatId, override?: true) {
if(appChatsManager.isChannel(id)) { if(appChatsManager.isChannel(id)) {
return this.getChannelFull(id, override); return this.getChannelFull(id, override);
} }
@ -264,7 +263,7 @@ export class AppProfileManager {
const chat = appChatsManager.getChat(id); const chat = appChatsManager.getChat(id);
if(chat.version === (fullChat.participants as ChatParticipants.chatParticipants).version || if(chat.version === (fullChat.participants as ChatParticipants.chatParticipants).version ||
chat.pFlags.left) { chat.pFlags.left) {
return Promise.resolve(fullChat); return fullChat as ChatFull;
} }
} }
@ -296,23 +295,22 @@ export class AppProfileManager {
}); });
} }
public getChatInviteLink(id: ChatId, force?: boolean) { public async getChatInviteLink(id: ChatId, force?: boolean) {
return this.getChatFull(id).then((chatFull) => { const chatFull = await this.getChatFull(id);
if(!force && if(!force &&
chatFull.exported_invite && chatFull.exported_invite &&
chatFull.exported_invite._ == 'chatInviteExported') { chatFull.exported_invite._ == 'chatInviteExported') {
return chatFull.exported_invite.link; return chatFull.exported_invite.link;
} }
return apiManager.invokeApi('messages.exportChatInvite', { return apiManager.invokeApi('messages.exportChatInvite', {
peer: appPeersManager.getInputPeerById(id.toPeerId(true)) peer: appPeersManager.getInputPeerById(id.toPeerId(true))
}).then((exportedInvite) => { }).then((exportedInvite) => {
if(this.chatsFull[id] !== undefined) { if(this.chatsFull[id] !== undefined) {
this.chatsFull[id].exported_invite = exportedInvite; this.chatsFull[id].exported_invite = exportedInvite;
} }
return (exportedInvite as ExportedChatInvite.chatInviteExported).link; return (exportedInvite as ExportedChatInvite.chatInviteExported).link;
});
}); });
} }
@ -661,7 +659,7 @@ export class AppProfileManager {
// let's load user here // let's load user here
if(update._ === 'updateChatUserTyping') { if(update._ === 'updateChatUserTyping') {
if(update.chat_id && appChatsManager.hasChat(update.chat_id) && !appChatsManager.isChannel(update.chat_id)) { if(update.chat_id && appChatsManager.hasChat(update.chat_id) && !appChatsManager.isChannel(update.chat_id)) {
appProfileManager.getChatFull(update.chat_id).then(() => { Promise.resolve(this.getChatFull(update.chat_id)).then(() => {
if(typing.timeout !== undefined && appUsersManager.hasUser(fromId)) { if(typing.timeout !== undefined && appUsersManager.hasUser(fromId)) {
rootScope.dispatchEvent('peer_typings', {peerId, typings}); rootScope.dispatchEvent('peer_typings', {peerId, typings});
} }

256
src/lib/appManagers/appReactionsManager.ts

@ -7,11 +7,18 @@
import { MOUNT_CLASS_TO } from "../../config/debug"; import { MOUNT_CLASS_TO } from "../../config/debug";
import assumeType from "../../helpers/assumeType"; import assumeType from "../../helpers/assumeType";
import callbackify from "../../helpers/callbackify"; import callbackify from "../../helpers/callbackify";
import { AvailableReaction, MessagesAvailableReactions } from "../../layer"; import callbackifyAll from "../../helpers/callbackifyAll";
import { copy } from "../../helpers/object";
import { AvailableReaction, Message, MessagesAvailableReactions, MessageUserReaction, Update, Updates } from "../../layer";
import apiManager from "../mtproto/mtprotoworker"; import apiManager from "../mtproto/mtprotoworker";
import { ReferenceContext } from "../mtproto/referenceDatabase"; import { ReferenceContext } from "../mtproto/referenceDatabase";
import rootScope from "../rootScope"; import rootScope from "../rootScope";
import apiUpdatesManager from "./apiUpdatesManager";
import appDocsManager from "./appDocsManager"; import appDocsManager from "./appDocsManager";
import appMessagesIdsManager from "./appMessagesIdsManager";
import appPeersManager from "./appPeersManager";
import appProfileManager from "./appProfileManager";
import appUsersManager from "./appUsersManager";
const SAVE_DOC_KEYS = [ const SAVE_DOC_KEYS = [
'static_icon' as const, 'static_icon' as const,
@ -23,18 +30,36 @@ const SAVE_DOC_KEYS = [
'center_icon' as const 'center_icon' as const
]; ];
const REFERENCE_CONTEXXT: ReferenceContext = { const REFERENCE_CONTEXT: ReferenceContext = {
type: 'reactions' type: 'reactions'
}; };
export class AppReactionsManager { export class AppReactionsManager {
private availableReactions: AvailableReaction[]; private availableReactions: AvailableReaction[];
private sendReactionPromises: Map<string, Promise<any>>;
private lastSendingTimes: Map<string, number>;
constructor() { constructor() {
rootScope.addEventListener('language_change', () => { rootScope.addEventListener('language_change', () => {
this.availableReactions = undefined; this.availableReactions = undefined;
this.getAvailableReactions(); this.getAvailableReactions();
}); });
this.sendReactionPromises = new Map();
this.lastSendingTimes = new Map();
setTimeout(() => {
Promise.resolve(this.getAvailableReactions()).then(async(availableReactions) => {
for(const availableReaction of availableReactions) {
await Promise.all([
availableReaction.around_animation && appDocsManager.downloadDoc(availableReaction.around_animation),
availableReaction.static_icon && appDocsManager.downloadDoc(availableReaction.static_icon),
availableReaction.appear_animation && appDocsManager.downloadDoc(availableReaction.appear_animation),
availableReaction.center_icon && appDocsManager.downloadDoc(availableReaction.center_icon)
]);
}
});
}, 7.5e3);
} }
public getAvailableReactions() { public getAvailableReactions() {
@ -51,7 +76,7 @@ export class AppReactionsManager {
continue; continue;
} }
reaction[key] = appDocsManager.saveDoc(reaction[key], REFERENCE_CONTEXXT); reaction[key] = appDocsManager.saveDoc(reaction[key], REFERENCE_CONTEXT);
} }
} }
@ -69,16 +94,60 @@ export class AppReactionsManager {
}); });
} }
public getAvailableReactionsForPeer(peerId: PeerId) {
const activeAvailableReactions = this.getActiveAvailableReactions();
if(peerId.isUser()) {
return this.unshiftQuickReaction(activeAvailableReactions);
}
const chatFull = appProfileManager.getChatFull(peerId.toChatId());
return callbackifyAll([activeAvailableReactions, chatFull, this.getQuickReaction()], ([activeAvailableReactions, chatFull, quickReaction]) => {
const chatAvailableReactions = chatFull.available_reactions ?? [];
const filteredChatAvailableReactions = chatAvailableReactions.map(reaction => {
return activeAvailableReactions.find(availableReaction => availableReaction.reaction === reaction);
}).filter(Boolean);
return this.unshiftQuickReactionInner(filteredChatAvailableReactions, quickReaction);
});
}
private unshiftQuickReactionInner(availableReactions: AvailableReaction.availableReaction[], quickReaction: AvailableReaction.availableReaction) {
const availableReaction = availableReactions.findAndSplice(availableReaction => availableReaction.reaction === quickReaction.reaction);
if(availableReaction) {
availableReactions.unshift(availableReaction);
}
return availableReactions;
}
private unshiftQuickReaction(
availableReactions: AvailableReaction.availableReaction[] | PromiseLike<AvailableReaction.availableReaction[]>,
quickReaction: ReturnType<AppReactionsManager['getQuickReaction']> = this.getQuickReaction()
) {
return callbackifyAll([
availableReactions,
quickReaction
], ([availableReactions, quickReaction]) => {
return this.unshiftQuickReactionInner(availableReactions, quickReaction);
});
}
public getAvailableReactionsByMessage(message: Message.message) {
const peerId = (message.fwd_from?.channel_post && appPeersManager.isMegagroup(message.peerId) && message.fwdFromId) || message.peerId;
return this.getAvailableReactionsForPeer(peerId);
}
public isReactionActive(reaction: string) { public isReactionActive(reaction: string) {
if(!this.availableReactions) return false; if(!this.availableReactions) return false;
return !!this.availableReactions.find(availableReaction => availableReaction.reaction === reaction); return !!this.availableReactions.find(availableReaction => availableReaction.reaction === reaction);
} }
public getQuickReaction() { public getQuickReaction() {
return Promise.all([ return callbackifyAll([
apiManager.getAppConfig(), apiManager.getAppConfig(),
this.getAvailableReactions() this.getAvailableReactions()
]).then(([appConfig, availableReactions]) => { ], ([appConfig, availableReactions]) => {
return availableReactions.find(reaction => reaction.reaction === appConfig.reactions_default); return availableReactions.find(reaction => reaction.reaction === appConfig.reactions_default);
}); });
} }
@ -93,7 +162,7 @@ export class AppReactionsManager {
}); });
} }
/* public getMessagesReactions(peerId: PeerId, mids: number[]) { public getMessagesReactions(peerId: PeerId, mids: number[]) {
return apiManager.invokeApiSingleProcess({ return apiManager.invokeApiSingleProcess({
method: 'messages.getMessagesReactions', method: 'messages.getMessagesReactions',
params: { params: {
@ -107,7 +176,24 @@ export class AppReactionsManager {
// return update.reactions; // return update.reactions;
} }
}); });
} */ }
public getMessageReactionsList(peerId: PeerId, mid: number, limit: number, reaction?: string, offset?: string) {
return apiManager.invokeApiSingleProcess({
method: 'messages.getMessageReactionsList',
params: {
peer: appPeersManager.getInputPeerById(peerId),
id: appMessagesIdsManager.getServerMessageId(mid),
limit,
reaction,
offset
},
processResult: (messageReactionsList) => {
appUsersManager.saveApiUsers(messageReactionsList.users);
return messageReactionsList;
}
});
}
public setDefaultReaction(reaction: string) { public setDefaultReaction(reaction: string) {
return apiManager.invokeApi('messages.setDefaultReaction', {reaction}).then(value => { return apiManager.invokeApi('messages.setDefaultReaction', {reaction}).then(value => {
@ -125,6 +211,162 @@ export class AppReactionsManager {
return value; return value;
}); });
} }
public sendReaction(message: Message.message, reaction?: string, onlyLocal?: boolean) {
const lastSendingTimeKey = message.peerId + '_' + message.mid;
const lastSendingTime = this.lastSendingTimes.get(lastSendingTimeKey);
if(lastSendingTime) {
return;
} else {
this.lastSendingTimes.set(lastSendingTimeKey, Date.now());
setTimeout(() => {
this.lastSendingTimes.delete(lastSendingTimeKey);
}, 333);
}
const {peerId, mid} = message;
const myUserId = rootScope.myId.toUserId();
let reactions = onlyLocal ? message.reactions : copy(message.reactions);
let chosenReactionIdx = reactions ? reactions.results.findIndex((reactionCount) => reactionCount.pFlags.chosen) : -1;
let chosenReaction = chosenReactionIdx !== -1 && reactions.results[chosenReactionIdx];
if(chosenReaction) { // clear current reaction
--chosenReaction.count;
delete chosenReaction.pFlags.chosen;
if(reaction === chosenReaction.reaction) {
reaction = undefined;
}
if(!chosenReaction.count) {
reactions.results.splice(chosenReactionIdx, 1);
}/* else {
insertInDescendSortedArray(reactions.results, chosenReaction, 'count', chosenReactionIdx);
} */
if(reactions.recent_reactions) {
reactions.recent_reactions.findAndSplice((recentReaction) => recentReaction.user_id === myUserId);
}
if(!reactions.results.length) {
reactions = undefined;
}
}
if(reaction) {
if(!reactions) {
reactions/* = message.reactions */ = {
_: 'messageReactions',
results: [],
pFlags: {}
};
if(!appPeersManager.isBroadcast(message.peerId)) {
reactions.pFlags.can_see_list = true;
}
}
let reactionCountIdx = reactions.results.findIndex((reactionCount) => reactionCount.reaction === reaction);
let reactionCount = reactionCountIdx !== -1 && reactions.results[reactionCountIdx];
if(!reactionCount) {
reactionCount = {
_: 'reactionCount',
count: 0,
reaction,
pFlags: {}
};
reactionCountIdx = reactions.results.push(reactionCount) - 1;
}
++reactionCount.count;
reactionCount.pFlags.chosen = true;
if(!reactions.recent_reactions && reactions.pFlags.can_see_list) {
reactions.recent_reactions = [];
}
if(reactions.recent_reactions) {
const userReaction: MessageUserReaction = {
_: 'messageUserReaction',
reaction,
user_id: myUserId
};
if(!appPeersManager.isMegagroup(peerId)) {
reactions.recent_reactions.push(userReaction);
reactions.recent_reactions = reactions.recent_reactions.slice(-3);
} else {
reactions.recent_reactions.unshift(userReaction);
reactions.recent_reactions = reactions.recent_reactions.slice(0, 3);
}
}
// insertInDescendSortedArray(reactions.results, reactionCount, 'count', reactionCountIdx);
}
const availableReactions = this.availableReactions;
if(reactions && availableReactions?.length) {
const indexes: Map<string, number> = new Map();
availableReactions.forEach((availableReaction, idx) => {
indexes.set(availableReaction.reaction, idx);
});
reactions.results.sort((a, b) => {
return (b.count - a.count) || (indexes.get(a.reaction) - indexes.get(b.reaction));
});
}
if(onlyLocal) {
message.reactions = reactions;
rootScope.dispatchEvent('message_reactions', {message, changedResults: []});
return Promise.resolve();
}
apiUpdatesManager.processLocalUpdate({
_: 'updateMessageReactions',
peer: message.peer_id,
msg_id: message.id,
reactions: reactions,
local: true
});
const promiseKey = [peerId, mid].join('-');
const msgId = appMessagesIdsManager.getServerMessageId(mid);
const promise = apiManager.invokeApi('messages.sendReaction', {
peer: appPeersManager.getInputPeerById(peerId),
msg_id: msgId,
reaction
}).then((updates) => {
assumeType<Updates.updates>(updates);
const editMessageUpdateIdx = updates.updates.findIndex(update => update._ === 'updateEditMessage' || update._ === 'updateEditChannelMessage');
if(editMessageUpdateIdx !== -1) {
const editMessageUpdate = updates.updates[editMessageUpdateIdx] as Update.updateEditMessage | Update.updateEditChannelMessage;
updates.updates[editMessageUpdateIdx] = {
_: 'updateMessageReactions',
msg_id: msgId,
peer: appPeersManager.getOutputPeer(peerId),
reactions: (editMessageUpdate.message as Message.message).reactions,
pts: editMessageUpdate.pts,
pts_count: editMessageUpdate.pts_count
};
}
apiUpdatesManager.processUpdateMessage(updates);
}).catch(err => {
if(err.type === 'REACTION_INVALID' && this.sendReactionPromises.get(promiseKey) === promise) {
this.sendReaction(message, chosenReaction?.reaction, true);
}
}).finally(() => {
if(this.sendReactionPromises.get(promiseKey) === promise) {
this.sendReactionPromises.delete(promiseKey);
}
});
this.sendReactionPromises.set(promiseKey, promise);
return promise;
}
} }
const appReactionsManager = new AppReactionsManager(); const appReactionsManager = new AppReactionsManager();

3
src/lib/appManagers/appStateManager.ts

@ -24,6 +24,7 @@ import DATABASE_STATE from '../../config/databases/state';
import sessionStorage from '../sessionStorage'; import sessionStorage from '../sessionStorage';
import { nextRandomUint } from '../../helpers/random'; import { nextRandomUint } from '../../helpers/random';
import compareVersion from '../../helpers/compareVersion'; import compareVersion from '../../helpers/compareVersion';
import getTimeFormat from '../../helpers/getTimeFormat';
const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day
// const REFRESH_EVERY = 1e3; // const REFRESH_EVERY = 1e3;
@ -166,7 +167,7 @@ export const STATE_INIT: State = {
notifications: { notifications: {
sound: false sound: false
}, },
timeFormat: new Date().toLocaleString().match(/\s(AM|PM)/) ? 'h12' : 'h23' timeFormat: getTimeFormat()
}, },
keepSigned: true, keepSigned: true,
chatContextMenuHintWasShown: false, chatContextMenuHintWasShown: false,

7
src/lib/mtproto/mtprotoworker.ts

@ -696,11 +696,12 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
}); });
} }
public getAppConfig(overwrite?: boolean): Promise<MTAppConfig> { public getAppConfig(overwrite?: boolean) {
if(rootScope.appConfig && !overwrite) return rootScope.appConfig;
if(this.getAppConfigPromise && !overwrite) return this.getAppConfigPromise; if(this.getAppConfigPromise && !overwrite) return this.getAppConfigPromise;
const promise = this.getAppConfigPromise = this.invokeApi('help.getAppConfig').then(config => { const promise: Promise<MTAppConfig> = this.getAppConfigPromise = this.invokeApi('help.getAppConfig').then(config => {
if(this.getAppConfigPromise !== promise) { if(this.getAppConfigPromise !== promise) {
return; return this.getAppConfigPromise;
} }
rootScope.appConfig = config; rootScope.appConfig = config;

2
src/lib/mtproto/schema.ts

File diff suppressed because one or more lines are too long

8
src/lib/rootScope.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import type { Message, StickerSet, Update, NotifyPeer, PeerNotifySettings, ConstructorDeclMap, Config, PollResults, Poll, WebPage, GroupCall, GroupCallParticipant, PhoneCall, MethodDeclMap, MessageReactions } from "../layer"; import type { Message, StickerSet, Update, NotifyPeer, PeerNotifySettings, ConstructorDeclMap, Config, PollResults, Poll, WebPage, GroupCall, GroupCallParticipant, PhoneCall, MethodDeclMap, MessageReactions, ReactionCount } from "../layer";
import type { MyDocument } from "./appManagers/appDocsManager"; import type { MyDocument } from "./appManagers/appDocsManager";
import type { AppMessagesManager, Dialog, MessagesStorage, MyMessage } from "./appManagers/appMessagesManager"; import type { AppMessagesManager, Dialog, MessagesStorage, MyMessage } from "./appManagers/appMessagesManager";
import type { MyDialogFilter } from "./storages/filters"; import type { MyDialogFilter } from "./storages/filters";
@ -76,7 +76,7 @@ export type BroadcastEvents = {
'message_edit': {storage: MessagesStorage, peerId: PeerId, mid: number}, 'message_edit': {storage: MessagesStorage, peerId: PeerId, mid: number},
'message_views': {peerId: PeerId, mid: number, views: number}, 'message_views': {peerId: PeerId, mid: number, views: number},
'message_sent': {storage: MessagesStorage, tempId: number, tempMessage: any, mid: number, message: MyMessage}, 'message_sent': {storage: MessagesStorage, tempId: number, tempMessage: any, mid: number, message: MyMessage},
'message_reactions': Message.message, 'message_reactions': {message: Message.message, changedResults: ReactionCount[]},
'messages_pending': void, 'messages_pending': void,
'messages_read': void, 'messages_read': void,
'messages_downloaded': {peerId: PeerId, mids: number[]}, 'messages_downloaded': {peerId: PeerId, mids: number[]},
@ -158,7 +158,9 @@ export type BroadcastEvents = {
'call_instance': {hasCurrent: boolean, instance: any/* CallInstance */}, 'call_instance': {hasCurrent: boolean, instance: any/* CallInstance */},
'quick_reaction': string 'quick_reaction': string,
'missed_reactions_element': {message: Message.message, changedResults: ReactionCount[]}
}; };
export class RootScope extends EventListenerBase<{ export class RootScope extends EventListenerBase<{

4
src/lib/storages/dialogs.ts

@ -391,7 +391,7 @@ export default class DialogsStorage {
} }
if(newDialogIndex) { if(newDialogIndex) {
insertInDescendSortedArray(dialogs, dialog, indexKey, wasIndex); insertInDescendSortedArray(dialogs, dialog, indexKey, -1);
} }
} }
@ -618,7 +618,7 @@ export default class DialogsStorage {
this.prepareFolderUnreadCountModifyingByDialog(folder_id, dialog, true); this.prepareFolderUnreadCountModifyingByDialog(folder_id, dialog, true);
} }
/* const newPos = */insertInDescendSortedArray(dialogs, dialog, 'index', pos); /* const newPos = */insertInDescendSortedArray(dialogs, dialog, 'index', -1);
/* if(pos !== -1 && pos !== newPos) { /* if(pos !== -1 && pos !== newPos) {
rootScope.dispatchEvent('dialog_order', {dialog, pos: newPos}); rootScope.dispatchEvent('dialog_order', {dialog, pos: newPos});
} */ } */

7
src/scripts/in/schema_additional_params.json

@ -357,4 +357,11 @@
{"name": "around_animation", "type": "Document.document"}, {"name": "around_animation", "type": "Document.document"},
{"name": "center_icon", "type": "Document.document"} {"name": "center_icon", "type": "Document.document"}
] ]
}, {
"predicate": "updateMessageReactions",
"params": [
{"name": "pts", "type": "number"},
{"name": "pts_count", "type": "number"},
{"name": "local", "type": "boolean"}
]
}] }]

29
src/scss/partials/_audio.scss

@ -5,19 +5,12 @@
*/ */
.audio { .audio {
position: relative; // position: relative;
padding-left: 67px;
overflow: visible!important; overflow: visible!important;
height: 3.375rem;
user-select: none;
/* @include respond-to(handhelds) {
padding-left: 45px;
} */
&-toggle, &-toggle,
&-download { &-download {
overflow: hidden; // overflow: hidden;
border-radius: 50%; border-radius: 50%;
background-color: var(--primary-color); background-color: var(--primary-color);
align-items: center; align-items: center;
@ -25,12 +18,15 @@
&.corner-download { &.corner-download {
.audio-download { .audio-download {
// top: 0;
width: 1.375rem; width: 1.375rem;
height: 1.375rem; height: 1.375rem;
margin: 2rem 2rem 0; // margin: 2rem 2rem 0;
margin: 0 !important;
top: 57.5%;
left: 57.5%;
background: none; background: none;
display: flex !important; display: flex !important;
top: 0;
} }
.preloader-container { .preloader-container {
@ -416,8 +412,9 @@
// //} // //}
// &.audio-48 { // &.audio-48 {
height: 3rem; --icon-size: 3rem;
padding-left: calc(3rem + .5625rem); --icon-margin: .5625rem;
height: var(--icon-size);
.audio-details { .audio-details {
margin-top: 3px; margin-top: 3px;
@ -428,12 +425,6 @@
margin-bottom: -2px; margin-bottom: -2px;
} }
&-ico,
&-download {
width: 3rem;
height: 3rem;
}
.part { .part {
height: 112px !important; height: 112px !important;
width: 112px !important; width: 112px !important;

7
src/scss/partials/_avatar.scss

@ -151,6 +151,8 @@ avatar-element {
font-size: 56px; font-size: 56px;
} }
} */ } */
// * can get multiplier by diving 54 / X
&.avatar-120 { &.avatar-120 {
--size: 120px; --size: 120px;
--multiplier: .45; --multiplier: .45;
@ -206,6 +208,11 @@ avatar-element {
--multiplier: 1.8; --multiplier: 1.8;
} }
&.avatar-24 {
--size: 24px;
--multiplier: 2.25;
}
&.avatar-18 { &.avatar-18 {
--size: 18px; --size: 18px;
--multiplier: 3; --multiplier: 3;

178
src/scss/partials/_button.scss

@ -103,11 +103,11 @@
opacity: 0; opacity: 0;
transform: scale(.8); transform: scale(.8);
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: 1rem;
&, &/* ,
&-reactions { &-reactions */ {
box-shadow: 0px 2px 8px 1px rgba(0, 0, 0, .24); box-shadow: var(--menu-box-shadow);
} }
body.animation-level-0 & { body.animation-level-0 & {
@ -172,17 +172,21 @@
} }
&-item { &-item {
--padding-left: 1rem;
--padding-right: 2.5rem;
--icon-margin: 1.5rem;
--icon-size: 1.5rem;
display: flex; display: flex;
position: relative; position: relative;
padding: 0 40px 0 1rem; padding: 0 var(--padding-right) 0 var(--padding-left);
height: 56px; height: 3rem;
cursor: pointer !important; cursor: pointer !important;
pointer-events: all !important; pointer-events: all !important;
color: var(--primary-text-color); color: var(--primary-text-color);
text-transform: none; text-transform: none;
white-space: nowrap; white-space: nowrap;
overflow: hidden; // overflow: hidden;
text-overflow: ellipsis; // text-overflow: ellipsis;
align-items: center; align-items: center;
text-align: left; text-align: left;
line-height: var(--line-height); line-height: var(--line-height);
@ -195,17 +199,46 @@
&:before { &:before {
color: var(--secondary-text-color); color: var(--secondary-text-color);
font-size: 1.5rem; font-size: var(--icon-size);
margin-right: 2rem; margin-right: var(--icon-margin);
position: relative;
} }
@include respond-to(handhelds) { @include respond-to(handhelds) {
padding: 0 30px 0 16px; --padding-right: 1.875rem;
height: 50px;
} }
&-text { &-text {
position: relative;
flex: 1 1 auto; flex: 1 1 auto;
&,
&-fake {
pointer-events: none;
}
&-fake {
--margin-left: calc(var(--icon-size) + var(--icon-margin));
position: absolute;
margin-left: var(--margin-left);
max-width: calc(100% - var(--margin-left) - var(--padding-left) - var(--padding-right));
@include text-overflow();
}
}
.stacked-avatars {
--margin-right: -.6875rem;
flex: 0 0 auto;
right: 1rem;
// margin-right: -1.5rem;
// margin-left: 1rem;
position: absolute;
pointer-events: none;
/* @include respond-to(handhelds) {
margin-right: -.875rem;
} */
} }
} }
@ -264,16 +297,97 @@
hr { hr {
padding: 0; padding: 0;
margin: .5rem 0; margin: .5rem 0;
display: block !important;
} }
&-reactions { &-reactions {
--height: 2.5rem; --inner-shadow-degree: 90deg;
height: var(--height); max-width: 100%;
border-radius: 1.25rem; max-height: 100%;
height: inherit;
border-radius: var(--height);
background-color: var(--surface-color); background-color: var(--surface-color);
position: absolute; position: absolute;
top: calc((var(--height) + .5rem) * -1); opacity: 0;
max-width: 100%; transform: scale(.8);
// filter: drop-shadow(0 .125rem .5rem rgba(0, 0, 0, .24));
&-container {
--height: 2.625rem;
--bubble-side-offset: -2.25rem;
--other-side-offset: -1.5rem;
--width: calc(100% + (var(--bubble-side-offset) + var(--other-side-offset)) * -1);
position: absolute;
top: calc((var(--height) + .5rem) * -1);
width: var(--width);
max-width: var(--width);
left: var(--other-side-offset);
// left: var(--bubble-side-offset);
display: flex;
justify-content: flex-end;
height: var(--height);
&-vertical {
top: var(--other-side-offset);
left: calc((var(--height) + .5rem) * -1);
width: var(--height);
height: var(--width);
max-width: var(--height);
max-height: var(--width);
flex-direction: column;
.btn-menu-reactions {
--inner-shadow-degree: 180deg;
width: inherit;
height: auto;
display: flex;
flex-direction: column;
}
.btn-menu-reactions-reaction {
--padding-vertical: var(--padding-base);
--padding-horizontal: 0rem;
}
.btn-menu-reactions-bubble-big {
right: calc(var(--size) / -2);
bottom: var(--offset);
}
}
}
@include animation-level(2) {
transition: opacity var(--transition-standard-in), transform var(--transition-standard-in);
}
&.is-visible {
opacity: 1;
transform: scale(1);
}
&-bubble {
position: absolute;
background-color: inherit;
border-radius: 50%;
z-index: -1;
/* &-small {
width: .5rem;
height: .5rem;
right: .5rem;
bottom: -1.25rem;
} */
&-big {
--size: 1rem;
--offset: calc(var(--height) / 2);
width: var(--size);
height: var(--size);
right: var(--offset);
// left: var(--offset);
bottom: calc(var(--size) / -2);
}
}
&:after { &:after {
position: absolute; position: absolute;
@ -284,22 +398,36 @@
content: " "; content: " ";
pointer-events: none; pointer-events: none;
border-radius: inherit; border-radius: inherit;
background: linear-gradient(90deg, var(--surface-color) 0%, transparent 1rem, transparent calc(100% - 1rem), var(--surface-color) 100%); background: linear-gradient(var(--inner-shadow-degree), var(--surface-color) 0%, transparent 1rem, transparent calc(100% - 1rem), var(--surface-color) 100%);
} }
.scrollable-x { .scrollable {
$padding: .25rem;
position: relative; position: relative;
display: flex; display: flex;
align-items: center;
padding: 0 .25rem;
border-radius: inherit; border-radius: inherit;
&-x {
align-items: center;
padding: 0 #{$padding};
}
&-y {
align-items: center;
padding: #{$padding} 0;
flex-direction: column;
}
} }
&-reaction { &-reaction {
width: 2rem; --size: 1.75rem;
height: 1.5rem; --padding-base: .25rem;
--padding-vertical: 0rem;
--padding-horizontal: var(--padding-base);
width: calc(var(--size) + var(--padding-horizontal) * 2);
height: calc(var(--size) + var(--padding-vertical) * 2);
flex: 0 0 auto; flex: 0 0 auto;
padding: 0 .25rem; padding: var(--padding-vertical) var(--padding-horizontal);
cursor: pointer; cursor: pointer;
&-scale { &-scale {
@ -421,7 +549,7 @@
//width: auto; //width: auto;
//text-transform: capitalize; //text-transform: capitalize;
font-weight: normal; font-weight: normal;
line-height: 1.3125; // * it centers the text line-height: var(--line-height); // * it centers the text
@include respond-to(handhelds) { @include respond-to(handhelds) {
height: 3rem; height: 3rem;

7
src/scss/partials/_chat.scss

@ -727,6 +727,11 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
&.type-chat .bubbles.is-chat-input-hidden .bubbles-date-group:last-of-type .bubble:last-of-type { &.type-chat .bubbles.is-chat-input-hidden .bubbles-date-group:last-of-type .bubble:last-of-type {
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
.contextmenu {
box-shadow: none !important;
filter: drop-shadow(0 .125rem .5rem var(--menu-box-shadow-color));
}
} }
.chat-input-wrapper { .chat-input-wrapper {
@ -1142,7 +1147,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
&-field { &-field {
--size: 1.5rem; --size: 1.5rem;
order: 0; order: 0;
margin: 0 2rem 0 0; margin: 0 var(--icon-margin) 0 0;
} }
&-box { &-box {

222
src/scss/partials/_chatBubble.scss

@ -296,12 +296,53 @@ $bubble-beside-button-width: 38px;
} }
} }
&-hover-reaction {
--size: 1.875rem;
--offset: calc(var(--size) * -.75);
position: absolute;
right: var(--offset);
bottom: -.125rem;
width: var(--size);
height: 1.625rem;
border-radius: var(--size);
z-index: 2;
background-color: var(--surface-color);
cursor: pointer;
opacity: 0;
transform: scale(.8);
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--menu-box-shadow);
@include animation-level(2) {
transition: opacity var(--btn-corner-transition), transform var(--btn-corner-transition);
}
&.is-visible {
&:not(.backwards) {
transform: scale(1);
opacity: 1;
}
}
&-sticker {
width: 1.125rem;
height: 1.125rem;
position: relative;
}
}
/* &.with-beside-button &-content { /* &.with-beside-button &-content {
@include respond-to(handhelds) { @include respond-to(handhelds) {
max-width: calc(100% - var(--message-handhelds-margin)) !important; max-width: calc(100% - var(--message-handhelds-margin)) !important;
} }
} */ } */
/* &.with-beside-button &-content {
margin-right: calc(var(--message-beside-button-margin) * -1);
} */
&.service { &.service {
//padding: 1rem 0; //padding: 1rem 0;
//padding: $bubble-margin 0; //padding: $bubble-margin 0;
@ -529,16 +570,22 @@ $bubble-beside-button-width: 38px;
} }
.chat:not(.no-forwards) & { .chat:not(.no-forwards) & {
cursor: text; .attachment {
user-select: text; cursor: text;
user-select: text;
}
}
.message {
margin-top: -1.125rem;
} }
} }
&.sticker .bubble-content { /* &.sticker .bubble-content {
max-width: 140px !important; max-width: 140px !important;
max-height: 140px !important; max-height: 140px !important;
user-select: none !important; user-select: none !important;
} } */
} }
&.emoji-1x .attachment { &.emoji-1x .attachment {
@ -595,16 +642,31 @@ $bubble-beside-button-width: 38px;
} */ } */
} }
&.sticker,
&.emoji-big:not(.sticker) {
.bubble-content {
align-self: flex-start;
}
.message {
position: relative !important;
// align-self: flex-start;
margin-left: auto;
right: 0 !important;
}
}
&.sticker { &.sticker {
.attachment { .attachment {
position: absolute;
border-radius: 0; border-radius: 0;
z-index: 1; z-index: 1;
} }
.bubble-content { /* .bubble-content {
max-width: 200px !important; max-width: 200px !important;
max-height: 200px !important; max-height: 200px !important;
} } */
} }
&.round { &.round {
@ -910,11 +972,15 @@ $bubble-beside-button-width: 38px;
.web { .web {
// margin: .125rem 0; // margin: .125rem 0;
margin: .125rem 0 -.5625rem; margin: .125rem 0 0;
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
line-height: var(--line-height); line-height: var(--line-height);
& + .time {
display: block;
}
.preview { .preview {
max-width: unquote('min(420px, 100%)'); max-width: unquote('min(420px, 100%)');
max-height: unquote('min(340px, 100%)'); max-height: unquote('min(340px, 100%)');
@ -1141,7 +1207,7 @@ $bubble-beside-button-width: 38px;
padding: 0 .5rem .375rem .625rem; padding: 0 .5rem .375rem .625rem;
max-width: 100%; max-width: 100%;
color: var(--primary-text-color); color: var(--primary-text-color);
line-height: 1.3125; // 21 / 16 line-height: var(--line-height); // 21 / 16
word-break: break-word; word-break: break-word;
white-space: pre-wrap; // * fix spaces on line begin white-space: pre-wrap; // * fix spaces on line begin
position: relative; position: relative;
@ -1154,11 +1220,7 @@ $bubble-beside-button-width: 38px;
@include respond-to(handhelds) { @include respond-to(handhelds) {
.document, .document,
.audio { .audio {
&-ico, --icon-size: 2.25rem;
&-download {
height: 2.25rem;
width: 2.25rem;
}
} }
} }
@ -1176,7 +1238,7 @@ $bubble-beside-button-width: 38px;
@include respond-to(handhelds) { @include respond-to(handhelds) {
height: 2.375rem; height: 2.375rem;
padding-left: calc(2.375rem + .5625rem); --icon-margin: .6875rem;
.audio-details { .audio-details {
margin-top: 2px; margin-top: 2px;
@ -1187,9 +1249,9 @@ $bubble-beside-button-width: 38px;
margin-top: -1px; margin-top: -1px;
} }
&.corner-download .audio-download { /* &.corner-download .audio-download {
margin: 1.375rem 1.375rem 0; margin: 1.375rem 1.375rem 0;
} } */
} }
} }
@ -1260,11 +1322,11 @@ $bubble-beside-button-width: 38px;
max-width: 325px !important; max-width: 325px !important;
.document { .document {
padding-left: 66px; --icon-margin: .75rem;
height: 58px; height: 58px;
@include respond-to(handhelds) { @include respond-to(handhelds) {
padding-left: 44px; //было 44 --icon-margin: .5rem;
height: 44px; height: 44px;
.document-size { .document-size {
@ -1272,7 +1334,7 @@ $bubble-beside-button-width: 38px;
} }
&:not(.document-with-thumb) .document-ico { &:not(.document-with-thumb) .document-ico {
padding: 1.125rem 0px 0px 0px; padding: 1.125rem 0 0 0;
} }
} }
@ -1343,14 +1405,26 @@ $bubble-beside-button-width: 38px;
} }
} }
.document-container .time.tgico { /* .document,
position: relative !important; .audio {
height: 0px !important; .time.tgico {
visibility: hidden !important; position: relative !important;
float: none; height: 0px !important;
.inner {
visibility: hidden !important; visibility: hidden !important;
float: none;
.inner {
visibility: hidden !important;
}
}
} */
.document-message {
& + .document,
& + .audio {
.time {
display: none !important;
}
} }
} }
@ -1427,15 +1501,19 @@ $bubble-beside-button-width: 38px;
} }
.bubble-select-checkbox { .bubble-select-checkbox {
left: 2rem; --margin-top: .25rem;
top: 2rem; --margin-left: .125rem;
left: auto;
top: auto;
background: #fff; background: #fff;
border-radius: 50%; border-radius: 50%;
margin-left: calc(var(--padding-left) * -1 + var(--icon-size) - var(--size) + var(--margin-left));
margin-top: calc(var(--icon-size) - var(--size) + var(--margin-top));
@include respond-to(handhelds) { @include respond-to(handhelds) {
--size: 1.125rem; --size: 1.125rem;
left: 20px; // left: 20px;
top: 25px; // top: 25px;
} }
&:before { &:before {
@ -1533,7 +1611,7 @@ $bubble-beside-button-width: 38px;
} }
.message { .message {
&.document-message, // &.document-message,
&.audio-message, &.audio-message,
&.voice-message, &.voice-message,
&.poll-message, &.poll-message,
@ -1598,6 +1676,16 @@ $bubble-beside-button-width: 38px;
bottom: 0; bottom: 0;
} }
} }
&:not(.emoji-big) {
.reactions-block {
max-width: fit-content;
}
}
.reaction-block {
--chosen-background-color: var(--primary-color);
}
} }
&.with-reply-markup { &.with-reply-markup {
@ -1633,8 +1721,11 @@ $bubble-beside-button-width: 38px;
} }
&-icon { &-icon {
margin-left: 2px;
pointer-events: none; pointer-events: none;
&:not(:first-child) {
margin-left: 2px;
}
} }
i.edited { i.edited {
@ -1950,6 +2041,30 @@ $bubble-beside-button-width: 38px;
margin-left: 0; margin-left: 0;
} }
} }
.message {
.reaction {
--background-color: var(--light-filled-message-primary-color);
&:not(.is-chosen),
&.is-chosen.backwards {
--counter-color: var(--message-primary-color);
.stacked-avatars {
--border-color: var(--background-color);
}
}
}
.reactions-block {
.time {
position: unset !important;
right: auto !important;
bottom: auto !important;
order: 100;
}
}
}
} }
// * fix scroll with only 1 bubble // * fix scroll with only 1 bubble
@ -1967,6 +2082,9 @@ $bubble-beside-button-width: 38px;
transform-origin: center; transform-origin: center;
opacity: 1; opacity: 1;
display: flex;
flex-direction: column;
@include animation-level(2) { @include animation-level(2) {
transition: transform var(--transition-standard-out), opacity var(--transition-standard-out); transition: transform var(--transition-standard-out), opacity var(--transition-standard-out);
} }
@ -2190,8 +2308,12 @@ $bubble-beside-button-width: 38px;
--light-message-background-color: var(--light-message-out-background-color); --light-message-background-color: var(--light-message-out-background-color);
--dark-message-background-color: var(--dark-message-out-background-color); --dark-message-background-color: var(--dark-message-out-background-color);
--link-color: var(--message-out-link-color); --link-color: var(--message-out-link-color);
--message-primary-color: var(--message-out-primary-color);
--light-filled-message-primary-color: var(--light-filled-message-out-primary-color);
.bubble-content { .bubble-content {
margin-left: auto;
&, &,
.poll-footer-button { .poll-footer-button {
border-radius: 12px 6px 6px 12px; border-radius: 12px 6px 6px 12px;
@ -2420,6 +2542,30 @@ $bubble-beside-button-width: 38px;
} }
} }
&.is-message-empty {
.reactions-block {
justify-content: flex-end;
}
.reaction-block {
margin-right: .25rem;
&:last-child {
margin-right: 0;
}
&.is-chosen {
--chosen-background-color: var(--surface-color);
}
}
&:not(.emoji-big) {
.reactions-block {
margin-left: auto;
}
}
}
.contact-number, .contact-number,
.document-size, .document-size,
.bubble-call-subtitle { .bubble-call-subtitle {
@ -2524,6 +2670,18 @@ $bubble-beside-button-width: 38px;
background-color: var(--message-background-color); background-color: var(--message-background-color);
} }
} }
.bubble-hover-reaction {
right: auto;
left: var(--offset);
}
/* &.sticker {
.message {
margin-right: 0;
margin-left: auto;
}
} */
} }
.reply-markup { .reply-markup {

4
src/scss/partials/_chatPinned.scss

@ -612,11 +612,11 @@
&-ico { &-ico {
&:before { &:before {
content: $tgico-largeplay; content: $tgico-play;
} }
&.flip-icon:before { &.flip-icon:before {
content: $tgico-largepause; content: $tgico-pause;
} }
} }

16
src/scss/partials/_checkbox.scss

@ -171,7 +171,7 @@
position: relative; position: relative;
text-align: left; text-align: left;
margin: 1.25rem 0; margin: 1.25rem 0;
line-height: 1.3125; // omg it centers the text line-height: var(--line-height); // omg it centers the text
cursor: pointer; cursor: pointer;
&.hidden-widget { &.hidden-widget {
@ -247,6 +247,20 @@
} */ } */
} }
&.radio-field-right {
.radio-field-main {
&:before {
left: auto;
right: 0;
}
&:after {
left: auto;
right: .3125rem;
}
}
}
/* &-with-subtitle { /* &-with-subtitle {
.radio-field-main { .radio-field-main {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;

23
src/scss/partials/_document.scss

@ -7,7 +7,6 @@
.document { .document {
--background-color: #{var(--primary-color)}; --background-color: #{var(--primary-color)};
$border-radius: .375rem; $border-radius: .375rem;
padding-left: 4.25rem;
height: 70px; height: 70px;
.media-photo { .media-photo {
@ -157,19 +156,24 @@
.document, .document,
.audio { .audio {
--icon-size: 3.375rem;
--icon-margin: .875rem;
--padding-left: calc(var(--icon-size) + var(--icon-margin));
padding-left: var(--padding-left);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
position: relative; // position: relative;
user-select: none; user-select: none;
&-ico, &-ico,
&-download { &-download {
position: absolute; position: absolute;
left: 0; // left: 0;
width: 3.375rem; margin-left: calc(var(--padding-left) * -1);
height: 3.375rem; width: var(--icon-size);
height: var(--icon-size);
color: #fff; color: #fff;
} }
@ -197,6 +201,15 @@
&:not(.corner-download) .preloader-container:not(.preloader-streamable) { &:not(.corner-download) .preloader-container:not(.preloader-streamable) {
transform: scale(1) !important; transform: scale(1) !important;
} }
.checkbox-field-round {
--margin-top: .25rem;
--margin-left: .125rem;
margin-left: calc(var(--padding-left) * -1 + var(--icon-size) - var(--size) + var(--margin-left));
margin-top: calc(var(--icon-size) - var(--size) + var(--margin-top));
top: auto;
left: auto;
}
} }
.audio { .audio {

143
src/scss/partials/_reaction.scss

@ -0,0 +1,143 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
/* @keyframes reaction-activate {
0% {
opacity: 1;
transform: scale(1.75);
}
95% {
opacity: 1;
}
to {
opacity: 0;
transform: scale(1.25);
}
} */
.reaction {
display: flex;
align-items: center;
&-sticker {
position: relative;
width: var(--reaction-size);
height: var(--reaction-size);
&-activate {
--offset: -.4375rem;
// --offset: 0;
position: absolute;
top: var(--offset);
right: var(--offset);
bottom: var(--offset);
left: var(--offset);
// animation: reaction-activate 2s linear forwards;
& + .media-sticker {
opacity: 0;
}
}
}
&-inline {
--reaction-size: .875rem;
min-width: var(--reaction-size);
min-height: var(--reaction-size);
}
&-inline &-counter {
font-size: inherit !important;
order: -1;
margin-right: .0625rem !important;
}
&-block {
--additional-height: .625rem;
--margin: .25rem;
// --reaction-size: 1.0625rem;
--reaction-size: 1rem;
--background-color: var(--message-highlightning-color);
--chosen-background-color: var(--message-primary-color);
--counter-color: #fff;
--reaction-total-size: calc(var(--reaction-size) + var(--additional-height));
height: var(--reaction-total-size);
border-radius: var(--reaction-total-size);
// padding: 0 .375rem 0 .625rem;
padding: 0 .5rem;
background-color: var(--background-color);
cursor: pointer;
position: relative;
margin-top: var(--margin);
margin-right: var(--margin);
color: var(--counter-color);
&:last-child {
margin-right: 0;
}
&:before {
content: " ";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: var(--chosen-background-color);
// visibility: hidden;
border-radius: inherit;
// transform: scale3d(.2, .2, .2);
transform: scale3d(0, 0, 0);
opacity: 0;
}
&.is-chosen {
&:not(.backwards) {
&:before {
transform: scale3d(1, 1, 1);
opacity: 1;
// visibility: visible;
}
.stacked-avatars {
--border-color: var(--chosen-background-color);
}
}
&.animating {
&:before {
transition: transform var(--transition-standard-in), opacity var(--transition-standard-in);
// transition: transform var(--transition-standard-in);
}
.reaction-counter {
transition: color var(--transition-standard-in);
// transition: color 1s linear;
}
.stacked-avatars-avatar-container {
transition: border-color var(--transition-standard-in);
}
}
}
.stacked-avatars {
--border-color: transparent;
margin-left: .25rem;
// margin-right: .0625rem;
}
}
&-block &-counter {
font-size: .875rem !important;
font-weight: 500;
margin: 0 .125rem 0 .375rem;
line-height: 1;
position: relative;
}
}

25
src/scss/partials/_reactions.scss

@ -0,0 +1,25 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.reactions {
&-block {
display: flex;
flex-wrap: wrap;
user-select: none;
&.has-no-reactions {
display: unset;
}
}
&-inline {
display: inline-flex;
&:not(:empty) {
margin-right: .125rem;
}
}
}

13
src/scss/partials/_rightSidebar.scss

@ -387,7 +387,7 @@
} }
} }
.document, /* .document,
.audio { .audio {
.checkbox-field { .checkbox-field {
top: 50%; top: 50%;
@ -396,7 +396,7 @@
margin-top: 1rem; margin-top: 1rem;
transform: translateY(-50%); transform: translateY(-50%);
} }
} } */
&-content-media &-month { &-content-media &-month {
&-items { &-items {
@ -421,15 +421,12 @@
} }
.document { .document {
padding-left: 60px; --icon-size: 3rem;
// padding-right: 1rem; --icon-margin: .75rem;
//height: 54px;
height: calc(48px + 1.5rem); height: calc(48px + 1.5rem);
&-ico, &-ico,
&-download { &-download {
width: 48px;
height: 48px;
border-radius: 5px !important; border-radius: 5px !important;
} }
@ -867,7 +864,7 @@
.profile-name { .profile-name {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1.3125; line-height: var(--line-height);
} }
} }

36
src/scss/partials/_stackedAvatars.scss

@ -0,0 +1,36 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.stacked-avatars {
--border-color: var(--surface-color);
--border-size: 1px;
--margin-right: -.3125rem;
--avatar-size: 1rem;
--avatar-total-size: calc(var(--avatar-size) + var(--border-size) * 2);
display: flex;
flex-direction: row-reverse;
&-avatar {
width: var(--avatar-size);
height: var(--avatar-size);
z-index: 0; // * fix border blinking
&-container {
width: var(--avatar-total-size);
height: var(--avatar-total-size);
border: var(--border-size) solid var(--border-color);
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
position: relative;
&:not(:first-child) {
margin-right: var(--margin-right);
}
}
}
}

2
src/scss/partials/pages/_pages.scss

@ -209,7 +209,7 @@
.qr-description { .qr-description {
max-width: 480px; max-width: 480px;
margin: 1rem auto; margin: 1rem auto;
line-height: 1.3125; line-height: var(--line-height);
text-align: left; text-align: left;
li { li {

80
src/scss/partials/popups/_reactedList.scss

@ -0,0 +1,80 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.popup-reacted-list {
$parent: ".popup";
#{$parent} {
&-container {
width: 25rem;
height: 600px;
max-height: 600px;
padding: 0;
}
&-header {
min-height: 3.5625rem;
margin: 0;
padding: .25rem .75rem .75rem;
border-bottom: 1px solid var(--border-color);
}
&-close {
margin-top: .375rem;
margin-right: .5rem;
height: 40px;
order: -1;
}
}
.reaction {
--additional-height: .75rem;
--reaction-size: 1.5rem;
--margin: .5rem;
// --background-color: var(--secondary-color);
--background-color: var(--light-filled-primary-color);
--counter-color: var(--primary-color);
&.is-chosen {
&:not(.backwards) {
// --counter-color: var(--primary-text-color);
--counter-color: #fff;
}
}
&-sticker-icon {
font-size: 1.25rem !important;
margin: 0;
display: flex;
align-items: center;
}
}
.sidebar-left-section {
margin-bottom: 0 !important;
}
/* .gradient-delimiter {
flex: 0 0 auto;
} */
.tabs-container {
flex: 1 1 auto;
overflow: hidden;
}
.tabs-tab {
background-color: var(--surface-color);
}
.reacted-list-reaction-icon {
width: 1.5rem;
height: 1.5rem;
margin: 0;
top: 50%;
transform: translateY(-50%);
}
}

54
src/scss/style.scss

@ -32,6 +32,10 @@ $chat-input-inner-padding-handhelds: .25rem;
@return rgba($color, $hover-alpha); @return rgba($color, $hover-alpha);
} }
@function rgba-to-rgb($rgba, $background: #fff) {
@return mix(rgb(red($rgba), green($rgba), blue($rgba)), $background, alpha($rgba) * 100%);
}
/* @mixin safari-overflow() { /* @mixin safari-overflow() {
html.is-safari & { html.is-safari & {
-webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); -webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%);
@ -80,6 +84,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--esg-sticker-size: 80px; --esg-sticker-size: 80px;
--disabled-opacity: .3; --disabled-opacity: .3;
--round-video-size: 280px; --round-video-size: 280px;
--menu-box-shadow: 0px 2px 8px 1px var(--menu-box-shadow-color);
--topbar-floating-scaleX: 1; --topbar-floating-scaleX: 1;
--topbar-call-height: 3rem; --topbar-call-height: 3rem;
@ -132,15 +137,25 @@ $chat-input-inner-padding-handhelds: .25rem;
} }
} }
@mixin splitColor($property, $color, $light: true, $dark: true) { @mixin splitColor($property, $color, $light: true, $dark: true, $light-rgb: false, $dark-rgb: false) {
--#{$property}: #{$color}; --#{$property}: #{$color};
$lightened: hover-color($color);
@if $light != false { @if $light != false {
--light-#{$property}: #{hover-color($color)}; --light-#{$property}: #{$lightened};
}
@if $light-rgb != false {
--light-filled-#{$property}: #{rgba-to-rgb($lightened, $light-rgb)};
} }
$darkened: darken($color, $hover-alpha * 100);
@if $dark != false { @if $dark != false {
--dark-#{$property}: #{darken($color, $hover-alpha * 100)}; --dark-#{$property}: #{$darkened};
}
@if $dark-rgb != false {
--dark-filled-#{$property}: #{rgba-to-rgb($darkened, $dark-rgb)};
} }
} }
@ -154,11 +169,12 @@ $chat-input-inner-padding-handhelds: .25rem;
--surface-color: #fff; --surface-color: #fff;
--scrollbar-color: rgba(0, 0, 0, .2); --scrollbar-color: rgba(0, 0, 0, .2);
--section-box-shadow-color: rgba(0, 0, 0, .06); --section-box-shadow-color: rgba(0, 0, 0, .06);
--menu-box-shadow-color: rgba(0, 0, 0, .24);
--input-search-background-color: #fff; --input-search-background-color: #fff;
--input-search-border-color: #dfe1e5; --input-search-border-color: #dfe1e5;
@include splitColor(primary-color, #3390ec, true, true); @include splitColor(primary-color, #3390ec, true, true, #fff);
--primary-text-color: #000; --primary-text-color: #000;
--secondary-color: #c4c9cc; --secondary-color: #c4c9cc;
@ -181,11 +197,14 @@ $chat-input-inner-padding-handhelds: .25rem;
--message-background-color: var(--surface-color); --message-background-color: var(--surface-color);
--message-checkbox-color: #61c642; --message-checkbox-color: #61c642;
--message-checkbox-border-color: #fff; --message-checkbox-border-color: #fff;
--message-primary-color: var(--primary-color);
--light-filled-message-primary-color: var(--light-filled-primary-color);
--message-secondary-color: var(--secondary-color); --message-secondary-color: var(--secondary-color);
@include splitColor(message-out-background-color, #eeffde, true, true); $message-out-background-color: #eeffde;
@include splitColor(message-out-background-color, $message-out-background-color, true, true);
--message-out-link-color: var(--link-color); --message-out-link-color: var(--link-color);
--message-out-primary-color: #4fae4e; @include splitColor(message-out-primary-color, #4fae4e, false, false, $message-out-background-color);
--message-out-status-color: var(--message-out-primary-color); --message-out-status-color: var(--message-out-primary-color);
--message-out-audio-play-button-color: #fff; --message-out-audio-play-button-color: #fff;
@ -226,7 +245,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--input-search-background-color: #181818; --input-search-background-color: #181818;
--input-search-border-color: #2f2f2f; --input-search-border-color: #2f2f2f;
@include splitColor(primary-color, #8774E1, true, true); @include splitColor(primary-color, #8774E1, true, true, #212121);
--primary-text-color: #fff; --primary-text-color: #fff;
--secondary-color: #707579; --secondary-color: #707579;
@ -251,10 +270,11 @@ $chat-input-inner-padding-handhelds: .25rem;
--message-checkbox-border-color: #fff; --message-checkbox-border-color: #fff;
--message-secondary-color: var(--secondary-color); --message-secondary-color: var(--secondary-color);
$message-out-background-color: #8774E1;
//@include splitColor(message-out-background-color, #ae582d, true, true); //@include splitColor(message-out-background-color, #ae582d, true, true);
@include splitColor(message-out-background-color, #8774E1, true, true); @include splitColor(message-out-background-color, $message-out-background-color, true, true);
--message-out-link-color: #fff; --message-out-link-color: #fff;
--message-out-primary-color: #fff; @include splitColor(message-out-primary-color, #fff, false, false, $message-out-background-color);
--message-out-status-color: rgba(255, 255, 255, .6); --message-out-status-color: rgba(255, 255, 255, .6);
--message-out-audio-play-button-color: var(--message-out-background-color); --message-out-audio-play-button-color: var(--message-out-background-color);
// * Night theme end // * Night theme end
@ -306,6 +326,9 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/peopleNearby"; @import "partials/peopleNearby";
@import "partials/spoiler"; @import "partials/spoiler";
@import "partials/emojiAnimation"; @import "partials/emojiAnimation";
@import "partials/reactions";
@import "partials/reaction";
@import "partials/stackedAvatars";
@import "partials/popups/popup"; @import "partials/popups/popup";
@import "partials/popups/editAvatar"; @import "partials/popups/editAvatar";
@ -321,6 +344,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/popups/groupCall"; @import "partials/popups/groupCall";
@import "partials/popups/call"; @import "partials/popups/call";
@import "partials/popups/sponsored"; @import "partials/popups/sponsored";
@import "partials/popups/reactedList";
@import "partials/pages/pages"; @import "partials/pages/pages";
@import "partials/pages/authCode"; @import "partials/pages/authCode";
@ -1543,6 +1567,18 @@ hr {
} }
} }
.quick-reaction-title {
display: flex;
align-items: center;
}
.quick-reaction-sticker {
width: 32px !important;
height: 32px !important;
position: relative !important;
margin: 0 .5rem 0 0 !important;
}
.verified-icon { .verified-icon {
flex: 0 0 auto; flex: 0 0 auto;
width: 1.25rem; width: 1.25rem;

Loading…
Cancel
Save