Browse Source

Reactions

master
Eduard Kuzmenko 3 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. 29
      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. 19
      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. 66
      src/lib/appManagers/appMessagesManager.ts
  30. 66
      src/lib/appManagers/appProfileManager.ts
  31. 258
      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< @@ -1208,6 +1208,7 @@ export default class AppMediaViewerBase<
this.moveTheMover(this.content.mover, fromRight === 1);
this.setNewMover();
} else {
rootScope.isOverlayActive = true;
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
if(!IS_TOUCH_SUPPORTED) window.addEventListener('wheel', this.onWheel, {passive: false, capture: true});
@ -1215,7 +1216,6 @@ export default class AppMediaViewerBase< @@ -1215,7 +1216,6 @@ export default class AppMediaViewerBase<
this.pageEl.insertBefore(this.wholeDiv, mainColumns);
void this.wholeDiv.offsetLeft; // reflow
this.wholeDiv.classList.add('active');
rootScope.isOverlayActive = true;
animationIntersector.checkAnimations(true);
if(!IS_MOBILE_SAFARI) {

4
src/components/appSearchSuper..ts

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

6
src/components/audio.ts

@ -608,7 +608,11 @@ export default class AudioElement extends HTMLElement { @@ -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');

7
src/components/buttonMenu.ts

@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
import { cancelEvent } from "../helpers/dom/cancelEvent";
import { AttachClickOptions, attachClickEvent, CLICK_EVENT_NAME } from "../helpers/dom/clickEvent";
import ListenerSetter from "../helpers/listenerSetter";
import { i18n, LangPackKey } from "../lib/langPack";
import { FormatterArguments, i18n, LangPackKey } from "../lib/langPack";
import CheckboxField from "./checkboxField";
import { closeBtnMenu } from "./misc";
import { ripple } from "./ripple";
@ -15,6 +15,7 @@ import { ripple } from "./ripple"; @@ -15,6 +15,7 @@ import { ripple } from "./ripple";
export type ButtonMenuItemOptions = {
icon?: string,
text?: LangPackKey,
textArgs?: FormatterArguments,
regularText?: string,
onClick: (e: MouseEvent | TouchEvent) => void | boolean,
element?: HTMLElement,
@ -31,12 +32,12 @@ const ButtonMenuItem = (options: ButtonMenuItemOptions) => { @@ -31,12 +32,12 @@ const ButtonMenuItem = (options: ButtonMenuItemOptions) => {
const {icon, text, onClick, checkboxField, noCheckboxClickListener} = options;
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);
let textElement = options.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;
}

282
src/components/chat/bubbles.ts

@ -42,7 +42,7 @@ import LazyLoadQueue from "../lazyLoadQueue"; @@ -42,7 +42,7 @@ import LazyLoadQueue from "../lazyLoadQueue";
import ListenerSetter from "../../helpers/listenerSetter";
import PollElement from "../poll";
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 { FocusDirection } from "../../helpers/fastSmoothScroll";
import useHeavyAnimationCheck, { getHeavyAnimationPromise, dispatchHeavyAnimationEvent, interruptHeavyAnimation } from "../../hooks/useHeavyAnimationCheck";
@ -88,6 +88,11 @@ import { CallType } from "../../lib/calls/types"; @@ -88,6 +88,11 @@ import { CallType } from "../../lib/calls/types";
import getVisibleRect from "../../helpers/dom/getVisibleRect";
import PopupJoinChatInvite from "../popups/joinChatInvite";
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 IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([
@ -197,6 +202,11 @@ export default class ChatBubbles { @@ -197,6 +202,11 @@ export default class ChatBubbles {
private previousStickyDate: HTMLElement;
private sponsoredMessage: SponsoredMessage.sponsoredMessage;
private hoverBubble: HTMLElement;
private hoverReaction: HTMLElement;
// private reactions: Map<number, ReactionsElement>;
constructor(
private chat: Chat,
@ -209,7 +219,8 @@ export default class ChatBubbles { @@ -209,7 +219,8 @@ export default class ChatBubbles {
private appProfileManager: AppProfileManager,
private appDraftsManager: AppDraftsManager,
private appMessagesIdsManager: AppMessagesIdsManager,
private appChatsManager: AppChatsManager
private appChatsManager: AppChatsManager,
private appReactionsManager: AppReactionsManager
) {
//this.chat.log.error('Bubbles construction');
@ -235,6 +246,8 @@ export default class ChatBubbles { @@ -235,6 +246,8 @@ export default class ChatBubbles {
this.lazyLoadQueue = new LazyLoadQueue();
this.lazyLoadQueue.queueId = ++queueId;
// this.reactions = new Map();
// * events
// will call when sent for update pos
@ -288,6 +301,13 @@ export default class ChatBubbles { @@ -288,6 +301,13 @@ export default class ChatBubbles {
/////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) {
const repliesElement = bubble.querySelector('replies-element') as RepliesElement;
if(repliesElement) {
@ -420,6 +440,39 @@ export default class ChatBubbles { @@ -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}) => {
//fastRaf(() => { // ! can't use delayed smth here, need original bubble to be edited
if(peerId !== this.peerId) return;
@ -842,6 +895,110 @@ export default class ChatBubbles { @@ -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() {
const timestamps = Object.keys(this.dateMessages).map(k => +k).sort((a, b) => b - a);
let lastVisible: HTMLElement;
@ -1083,6 +1240,18 @@ export default class ChatBubbles { @@ -1083,6 +1240,18 @@ export default class ChatBubbles {
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');
if(commentsDiv) {
const bubbleMid = +bubble.dataset.mid;
@ -1398,12 +1567,16 @@ export default class ChatBubbles { @@ -1398,12 +1567,16 @@ export default class ChatBubbles {
return {
bubble: this.bubbles[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) {
@ -1617,6 +1790,8 @@ export default class ChatBubbles { @@ -1617,6 +1790,8 @@ export default class ChatBubbles {
if(this.emptyPlaceholderMid === mid) {
this.emptyPlaceholderMid = undefined;
}
// this.reactions.delete(mid);
});
if(!deleted) {
@ -2015,6 +2190,8 @@ export default class ChatBubbles { @@ -2015,6 +2190,8 @@ export default class ChatBubbles {
this.isTopPaddingSet = false;
// this.reactions.clear();
if(this.isScrollingTimeout) {
clearTimeout(this.isScrollingTimeout);
this.isScrollingTimeout = 0;
@ -2256,6 +2433,34 @@ export default class ChatBubbles { @@ -2256,6 +2433,34 @@ export default class ChatBubbles {
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 needFetchNew = savedPosition || needFetchInterval;
if(!needFetchNew) {
@ -3094,8 +3299,8 @@ export default class ChatBubbles { @@ -3094,8 +3299,8 @@ export default class ChatBubbles {
const size = bubble.classList.contains('emoji-big') ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker);
this.appPhotosManager.setAttachmentSize(doc, attachmentDiv, size.width, size.height);
//let preloader = new ProgressivePreloader(attachmentDiv, false);
bubbleContainer.style.height = attachmentDiv.style.height;
bubbleContainer.style.width = attachmentDiv.style.width;
bubbleContainer.style.minWidth = attachmentDiv.style.width;
bubbleContainer.style.minHeight = attachmentDiv.style.height;
//appPhotosManager.setAttachmentSize(doc, bubble);
wrapSticker({
doc,
@ -3191,7 +3396,8 @@ export default class ChatBubbles { @@ -3191,7 +3396,8 @@ export default class ChatBubbles {
}
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');
messageDiv.classList.add((!(['photo', 'pdf'] as MyDocument['type'][]).includes(doc.type) ? doc.type || 'document' : 'document') + '-message');
@ -3484,6 +3690,16 @@ export default class ChatBubbles { @@ -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) {
bubble.classList.add('can-have-tail');
@ -3493,6 +3709,56 @@ export default class ChatBubbles { @@ -3493,6 +3709,56 @@ export default class ChatBubbles {
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) {
try {
return this.renderMessage(message, reverse, multipleRender, bubble, updatePosition);
@ -4152,7 +4418,7 @@ export default class ChatBubbles { @@ -4152,7 +4418,7 @@ export default class ChatBubbles {
this.log('inject bot description');
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()) {
return;
}

6
src/components/chat/chat.ts

@ -184,7 +184,7 @@ export default class Chat extends EventListenerBase<{ @@ -184,7 +184,7 @@ export default class Chat extends EventListenerBase<{
// this.initPeerId = peerId;
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.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);
@ -216,6 +216,10 @@ export default class Chat extends EventListenerBase<{ @@ -216,6 +216,10 @@ export default class Chat extends EventListenerBase<{
this.input.constructPeerHelpers();
}
if(this.type !== 'scheduled') {
this.bubbles.setReactionsHoverListeners();
}
this.container.classList.add('type-' + this.type);
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 { @@ -37,7 +37,7 @@ export default class CommandsHelper extends AutocompletePeerHelper {
}
const middleware = this.controller.getMiddleware();
this.appProfileManager.getProfileByPeerId(peerId).then(full => {
Promise.resolve(this.appProfileManager.getProfileByPeerId(peerId)).then(full => {
if(!middleware()) {
return;
}

70
src/components/chat/contextMenu.ts

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

2
src/components/chat/inlineHelper.ts

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

29
src/components/chat/messageRender.ts

@ -7,12 +7,14 @@ @@ -7,12 +7,14 @@
import { formatTime, getFullDate } from "../../helpers/date";
import { formatNumber } from "../../helpers/number";
import { Message } from "../../layer";
import appMessagesManager from "../../lib/appManagers/appMessagesManager";
import { i18n, _i18n } from "../../lib/langPack";
import RichTextProcessor from "../../lib/richtextprocessor";
import { LazyLoadQueueIntersector } from "../lazyLoadQueue";
import PeerTitle from "../peerTitle";
import { wrapReply } from "../wrappers";
import Chat, { ChatType } from "./chat";
import ReactionsElement from "./reactions";
import RepliesElement from "./replies";
const NBSP = '&nbsp;';
@ -39,10 +41,11 @@ export namespace MessageRender { @@ -39,10 +41,11 @@ export namespace MessageRender {
const date = new Date(message.date * 1000);
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 isMessage = !('action' in message) && !isSponsored;
let hasReactions: boolean;
let time: HTMLElement = isSponsored ? undefined : formatTime(date);
if(isMessage) {
@ -63,7 +66,7 @@ export namespace MessageRender { @@ -63,7 +66,7 @@ export namespace MessageRender {
args.push(span);
}
}
if(message.edit_date && chatType !== 'scheduled' && !message.pFlags.edit_hide) {
args.unshift(editedSpan = makeEdited());
}
@ -73,6 +76,17 @@ export namespace MessageRender { @@ -73,6 +76,17 @@ export namespace MessageRender {
i.classList.add('tgico-pinnedchat', 'time-icon');
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) {
args.push(sponsoredSpan = makeSponsored());
}
@ -83,13 +97,13 @@ export namespace MessageRender { @@ -83,13 +97,13 @@ export namespace MessageRender {
let title = isSponsored ? undefined : getFullDate(date);
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))}` : '');
}
const timeSpan = document.createElement('span');
timeSpan.classList.add('time', 'tgico');
if(title) timeSpan.title = title;
// if(title) timeSpan.title = title;
timeSpan.append(...args);
const inner = document.createElement('div');
@ -103,7 +117,12 @@ export namespace MessageRender { @@ -103,7 +117,12 @@ export namespace MessageRender {
if(sponsoredSpan) {
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) {
clonedArgs[clonedArgs.length - 1] = formatTime(date); // clone time
}

185
src/components/chat/reaction.ts

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

9
src/components/horizontalMenu.ts

@ -12,7 +12,14 @@ import { FocusDirection } from "../helpers/fastSmoothScroll"; @@ -12,7 +12,14 @@ import { FocusDirection } from "../helpers/fastSmoothScroll";
import findUpAsChild from "../helpers/dom/findUpAsChild";
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);
if(tabs) {

51
src/components/misc.ts

@ -179,36 +179,62 @@ export function openBtnMenu(menuElement: HTMLElement, onClose?: () => void) { @@ -179,36 +179,62 @@ export function openBtnMenu(menuElement: HTMLElement, onClose?: () => void) {
rootScope.dispatchEvent('context_menu_toggle', true);
}
export type MenuPositionPadding = {
top?: number,
right?: number,
bottom?: number,
left?: number
};
const PADDING_TOP = 8;
const PADDING_BOTTOM = PADDING_TOP;
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;
// * 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;
const rect = document.body.getBoundingClientRect();
const windowWidth = rect.width;
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';
let verticalSide: 'top' /* | 'bottom' */ | 'center' = 'top';
const maxTop = windowHeight - menuHeight - paddingBottom;
const maxLeft = windowWidth - menuWidth - paddingRight;
const minTop = paddingTop;
const minLeft = paddingLeft;
const getSides = () => {
return {
x: {
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,
y: {
top: pageY,
bottom: pageY - menuHeight
},
//intermediateY: verticalSide === 'top' ? PADDING_TOP : windowHeight - menuHeight - PADDING_TOP,
intermediateY: pageY < windowHeight / 2 ? PADDING_TOP : windowHeight - menuHeight - PADDING_TOP,
//intermediateY: verticalSide === 'top' ? paddingTop : windowHeight - menuHeight - paddingTop,
// intermediateY: pageY < (windowHeight / 2) ? paddingTop : windowHeight - menuHeight - paddingBottom,
intermediateY: maxTop,
};
};
@ -216,12 +242,12 @@ export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLEleme @@ -216,12 +242,12 @@ export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLEleme
const possibleSides = {
x: {
left: sides.x.left + menuWidth + PADDING_LEFT <= windowWidth,
right: sides.x.right >= PADDING_LEFT
left: (sides.x.left + menuWidth + paddingRight) <= windowWidth,
right: sides.x.right >= paddingLeft
},
y: {
top: sides.y.top + menuHeight + PADDING_TOP <= windowHeight,
bottom: sides.y.bottom - PADDING_TOP >= PADDING_TOP
top: (sides.y.top + menuHeight + paddingBottom) <= windowHeight,
bottom: (sides.y.bottom - paddingBottom) >= paddingBottom
}
};
@ -277,6 +303,11 @@ export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLEleme @@ -277,6 +303,11 @@ export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLEleme
(verticalSide === 'center' ? verticalSide : 'bottom') +
'-' +
(side === 'center' ? side : (side === 'left' ? 'right' : 'left')));
return {
width: menuWidth,
height: menuHeight
};
}
let _cancelContextMenuOpening = false, _cancelContextMenuOpeningTimeout = 0;

6
src/components/peerProfile.ts

@ -93,7 +93,7 @@ export default class PeerProfile { @@ -93,7 +93,7 @@ export default class PeerProfile {
return;
}
appProfileManager.getProfileByPeerId(this.peerId).then(full => {
Promise.resolve(appProfileManager.getProfileByPeerId(this.peerId)).then(full => {
copyTextToClipboard(full.about);
toast(I18n.format('BioCopied', true));
});
@ -324,7 +324,7 @@ export default class PeerProfile { @@ -324,7 +324,7 @@ export default class PeerProfile {
let promise: Promise<boolean>;
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) {
//this.log.warn('peer changed');
return false;
@ -338,7 +338,7 @@ export default class PeerProfile { @@ -338,7 +338,7 @@ export default class PeerProfile {
return true;
});
} 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) {
//this.log.warn('peer changed');
return false;

2
src/components/peerProfileAvatars.ts

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

2
src/components/popups/avatar.ts

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

220
src/components/popups/reactedList.ts

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

64
src/components/stackedAvatars.ts

@ -0,0 +1,64 @@ @@ -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, @@ -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}: {
doc: MyDocument,
div: HTMLElement,
@ -1384,80 +1480,18 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -1384,80 +1480,18 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
return;
}
const animationDiv = document.createElement('div');
animationDiv.classList.add('emoji-animation');
const size = 280;
animationDiv.style.width = size + 'px';
animationDiv.style.height = size + 'px';
const bubble = findUpClassName(div, 'bubble');
const isOut = bubble.classList.contains('is-out');
wrapSticker({
div: animationDiv,
const {animationDiv} = wrapStickerAnimation({
doc,
middleware,
withThumb: false,
needFadeIn: false,
loop: false,
width: size,
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});
}
side: isOut ? 'right' : 'left',
size: 280,
target: div,
play: 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(isOut) {
animationDiv.classList.add('is-out');
@ -1466,8 +1500,6 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -1466,8 +1500,6 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
}
}
appImManager.emojiAnimationContainer.append(animationDiv);
if(!sendInteractionThrottled) {
sendInteractionThrottled = throttle(() => {
const length = data.a.length;

19
src/helpers/array.ts

@ -48,16 +48,17 @@ export function insertInDescendSortedArray<T extends {[smth in K]?: number}, K e @@ -48,16 +48,17 @@ export function insertInDescendSortedArray<T extends {[smth in K]?: number}, K e
if(pos === undefined) {
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;

11
src/helpers/callbackify.ts

@ -1,6 +1,15 @@ @@ -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';
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) {
return smth.then(callback);
} else {

18
src/helpers/callbackifyAll.ts

@ -0,0 +1,18 @@ @@ -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 @@ @@ -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 = { @@ -646,6 +646,12 @@ const lang = {
"EnableReactionsChannelInfo": "Allow subscribers to react to channel posts.",
"EnableReactionsGroupInfo": "Allow members to react to group messages.",
"AvailableReactions": "Available reactions",
"NobodyViewed": "Nobody viewed",
"MessageSeen": {
"one_value": "Seen",
"other_value": "%1$d Seen"
},
// "Close": "Close",
// * macos
"AccountSettings.Filters": "Chat Folders",
@ -710,6 +716,10 @@ const lang = { @@ -710,6 +716,10 @@ const lang = {
},
"Chat.CopySelectedText": "Copy Selected Text",
"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.ScheduledForToday": "Scheduled for today",
"Chat.DropTitle": "Drop files here to send them",

7
src/layer.d.ts vendored

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

66
src/lib/appManagers/appMessagesManager.ts

@ -17,7 +17,7 @@ import { createPosterForVideo } from "../../helpers/files"; @@ -17,7 +17,7 @@ import { createPosterForVideo } from "../../helpers/files";
import { copy, deepEqual, getObjectKeysAndSort } from "../../helpers/object";
import { randomLong } from "../../helpers/random";
import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string";
import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason, MessagesGetDialogs, InputChannel, InputDialogPeer, 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 I18n, { FormatterArguments, i18n, join, langPack, LangPackKey, UNSUPPORTED_LANG_PACK_KEY, _i18n } from "../langPack";
import { logger, LogTypes } from "../logger";
@ -2368,7 +2368,7 @@ export class AppMessagesManager { @@ -2368,7 +2368,7 @@ export class AppMessagesManager {
message = m.message;
totalEntities = m.totalEntities;
entities = m.entities;
}
}
}
if(foundMessages > 1) {
@ -2380,6 +2380,20 @@ export class AppMessagesManager { @@ -2380,6 +2380,20 @@ export class AppMessagesManager {
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) {
return getObjectKeysAndSort(this.groupedMessagesStorage[grouped_id], 'asc');
//return Object.keys(this.groupedMessagesStorage[grouped_id]).map(id => +id).sort((a, b) => a - b);
@ -2390,10 +2404,10 @@ export class AppMessagesManager { @@ -2390,10 +2404,10 @@ export class AppMessagesManager {
else return [message.mid];
}
public filterMessages(message: any, verify: (message: MyMessage) => boolean) {
public filterMessages(message: MyMessage, verify: (message: MyMessage) => boolean) {
const out: MyMessage[] = [];
if(message.grouped_id) {
const storage = this.groupedMessagesStorage[message.grouped_id];
if((message as Message.message).grouped_id) {
const storage = this.groupedMessagesStorage[(message as Message.message).grouped_id];
for(const [mid, message] of storage) {
if(verify(message)) {
out.push(message);
@ -3481,7 +3495,7 @@ export class AppMessagesManager { @@ -3481,7 +3495,7 @@ export class AppMessagesManager {
}
if(!message.pFlags.out || (
message.peerId.isUser() &&
message.peer_id._ !== 'peerChannel' &&
message.date < (tsNow(true) - rootScope.config.edit_time_limit) &&
(message as Message.message).media?._ !== 'messageMediaPoll'
)
@ -3938,7 +3952,7 @@ export class AppMessagesManager { @@ -3938,7 +3952,7 @@ export class AppMessagesManager {
appUsersManager.saveApiUsers(result.users);
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;
this.generateThreadServiceStartMessage(message);
@ -4581,11 +4595,11 @@ export class AppMessagesManager { @@ -4581,11 +4595,11 @@ export class AppMessagesManager {
return;
}
const recentReactions = reactions.recent_reactons;
if(recentReactions) {
const recentReactions = reactions?.recent_reactions;
if(recentReactions?.length) {
const recentReaction = recentReactions[recentReactions.length - 1];
const previousReactions = message.reactions;
const previousRecentReactions = previousReactions?.recent_reactons;
const previousRecentReactions = previousReactions?.recent_reactions;
if(
recentReaction.user_id !== rootScope.myId.toUserId() && (
!previousRecentReactions ||
@ -4605,11 +4619,30 @@ export class AppMessagesManager { @@ -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;
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) => {
@ -5329,7 +5362,9 @@ export class AppMessagesManager { @@ -5329,7 +5362,9 @@ export class AppMessagesManager {
message: Message.message,
limit?: number,
reaction?: string,
offset?: string
offset?: string,
skipReadParticipants?: boolean,
skipReactionsList?: boolean
) {
const emptyMessageReactionsList = {
reactions: [] as MessageUserReaction[],
@ -5345,9 +5380,9 @@ export class AppMessagesManager { @@ -5345,9 +5380,9 @@ export class AppMessagesManager {
}
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]) => {
const readParticipantsPeerIds = userIds.map(userId => userId.toPeerId());
@ -5363,6 +5398,7 @@ export class AppMessagesManager { @@ -5363,6 +5398,7 @@ export class AppMessagesManager {
return {
reactions: messageReactionsList.reactions,
reactionsCount: messageReactionsList.count,
readParticipants: readParticipantsPeerIds,
combined: combined,
nextOffset: messageReactionsList.next_offset

66
src/lib/appManagers/appProfileManager.ts

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

258
src/lib/appManagers/appReactionsManager.ts

@ -7,11 +7,18 @@ @@ -7,11 +7,18 @@
import { MOUNT_CLASS_TO } from "../../config/debug";
import assumeType from "../../helpers/assumeType";
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 { ReferenceContext } from "../mtproto/referenceDatabase";
import rootScope from "../rootScope";
import apiUpdatesManager from "./apiUpdatesManager";
import appDocsManager from "./appDocsManager";
import appMessagesIdsManager from "./appMessagesIdsManager";
import appPeersManager from "./appPeersManager";
import appProfileManager from "./appProfileManager";
import appUsersManager from "./appUsersManager";
const SAVE_DOC_KEYS = [
'static_icon' as const,
@ -23,18 +30,36 @@ const SAVE_DOC_KEYS = [ @@ -23,18 +30,36 @@ const SAVE_DOC_KEYS = [
'center_icon' as const
];
const REFERENCE_CONTEXXT: ReferenceContext = {
const REFERENCE_CONTEXT: ReferenceContext = {
type: 'reactions'
};
export class AppReactionsManager {
private availableReactions: AvailableReaction[];
private sendReactionPromises: Map<string, Promise<any>>;
private lastSendingTimes: Map<string, number>;
constructor() {
rootScope.addEventListener('language_change', () => {
this.availableReactions = undefined;
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() {
@ -51,7 +76,7 @@ export class AppReactionsManager { @@ -51,7 +76,7 @@ export class AppReactionsManager {
continue;
}
reaction[key] = appDocsManager.saveDoc(reaction[key], REFERENCE_CONTEXXT);
reaction[key] = appDocsManager.saveDoc(reaction[key], REFERENCE_CONTEXT);
}
}
@ -69,16 +94,60 @@ export class AppReactionsManager { @@ -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) {
if(!this.availableReactions) return false;
return !!this.availableReactions.find(availableReaction => availableReaction.reaction === reaction);
}
public getQuickReaction() {
return Promise.all([
apiManager.getAppConfig(),
return callbackifyAll([
apiManager.getAppConfig(),
this.getAvailableReactions()
]).then(([appConfig, availableReactions]) => {
], ([appConfig, availableReactions]) => {
return availableReactions.find(reaction => reaction.reaction === appConfig.reactions_default);
});
}
@ -93,7 +162,7 @@ export class AppReactionsManager { @@ -93,7 +162,7 @@ export class AppReactionsManager {
});
}
/* public getMessagesReactions(peerId: PeerId, mids: number[]) {
public getMessagesReactions(peerId: PeerId, mids: number[]) {
return apiManager.invokeApiSingleProcess({
method: 'messages.getMessagesReactions',
params: {
@ -107,7 +176,24 @@ export class AppReactionsManager { @@ -107,7 +176,24 @@ export class AppReactionsManager {
// 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) {
return apiManager.invokeApi('messages.setDefaultReaction', {reaction}).then(value => {
@ -125,6 +211,162 @@ export class AppReactionsManager { @@ -125,6 +211,162 @@ export class AppReactionsManager {
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();

3
src/lib/appManagers/appStateManager.ts

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

7
src/lib/mtproto/mtprotoworker.ts

@ -696,11 +696,12 @@ export class ApiManagerProxy extends CryptoWorkerMethods { @@ -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;
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) {
return;
return this.getAppConfigPromise;
}
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 @@ @@ -4,7 +4,7 @@
* 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 { AppMessagesManager, Dialog, MessagesStorage, MyMessage } from "./appManagers/appMessagesManager";
import type { MyDialogFilter } from "./storages/filters";
@ -76,7 +76,7 @@ export type BroadcastEvents = { @@ -76,7 +76,7 @@ export type BroadcastEvents = {
'message_edit': {storage: MessagesStorage, peerId: PeerId, mid: number},
'message_views': {peerId: PeerId, mid: number, views: number},
'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_read': void,
'messages_downloaded': {peerId: PeerId, mids: number[]},
@ -158,7 +158,9 @@ export type BroadcastEvents = { @@ -158,7 +158,9 @@ export type BroadcastEvents = {
'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<{

4
src/lib/storages/dialogs.ts

@ -391,7 +391,7 @@ export default class DialogsStorage { @@ -391,7 +391,7 @@ export default class DialogsStorage {
}
if(newDialogIndex) {
insertInDescendSortedArray(dialogs, dialog, indexKey, wasIndex);
insertInDescendSortedArray(dialogs, dialog, indexKey, -1);
}
}
@ -618,7 +618,7 @@ export default class DialogsStorage { @@ -618,7 +618,7 @@ export default class DialogsStorage {
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) {
rootScope.dispatchEvent('dialog_order', {dialog, pos: newPos});
} */

7
src/scripts/in/schema_additional_params.json

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

7
src/scss/partials/_avatar.scss

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

178
src/scss/partials/_button.scss

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

7
src/scss/partials/_chat.scss

@ -727,6 +727,11 @@ $background-transition-total-time: #{$input-transition-time - $background-transi @@ -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 {
margin-bottom: 1.25rem;
}
.contextmenu {
box-shadow: none !important;
filter: drop-shadow(0 .125rem .5rem var(--menu-box-shadow-color));
}
}
.chat-input-wrapper {
@ -1142,7 +1147,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi @@ -1142,7 +1147,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
&-field {
--size: 1.5rem;
order: 0;
margin: 0 2rem 0 0;
margin: 0 var(--icon-margin) 0 0;
}
&-box {

222
src/scss/partials/_chatBubble.scss

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

4
src/scss/partials/_chatPinned.scss

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

16
src/scss/partials/_checkbox.scss

@ -171,7 +171,7 @@ @@ -171,7 +171,7 @@
position: relative;
text-align: left;
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;
&.hidden-widget {
@ -247,6 +247,20 @@ @@ -247,6 +247,20 @@
} */
}
&.radio-field-right {
.radio-field-main {
&:before {
left: auto;
right: 0;
}
&:after {
left: auto;
right: .3125rem;
}
}
}
/* &-with-subtitle {
.radio-field-main {
margin-bottom: 1.5rem;

23
src/scss/partials/_document.scss

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

143
src/scss/partials/_reaction.scss

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

36
src/scss/partials/_stackedAvatars.scss

@ -0,0 +1,36 @@ @@ -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 @@ @@ -209,7 +209,7 @@
.qr-description {
max-width: 480px;
margin: 1rem auto;
line-height: 1.3125;
line-height: var(--line-height);
text-align: left;
li {

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

@ -0,0 +1,80 @@ @@ -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; @@ -32,6 +32,10 @@ $chat-input-inner-padding-handhelds: .25rem;
@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() {
html.is-safari & {
-webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%);
@ -80,6 +84,7 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -80,6 +84,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--esg-sticker-size: 80px;
--disabled-opacity: .3;
--round-video-size: 280px;
--menu-box-shadow: 0px 2px 8px 1px var(--menu-box-shadow-color);
--topbar-floating-scaleX: 1;
--topbar-call-height: 3rem;
@ -132,15 +137,25 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -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};
$lightened: hover-color($color);
@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 {
--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; @@ -154,11 +169,12 @@ $chat-input-inner-padding-handhelds: .25rem;
--surface-color: #fff;
--scrollbar-color: rgba(0, 0, 0, .2);
--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-border-color: #dfe1e5;
@include splitColor(primary-color, #3390ec, true, true);
@include splitColor(primary-color, #3390ec, true, true, #fff);
--primary-text-color: #000;
--secondary-color: #c4c9cc;
@ -181,11 +197,14 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -181,11 +197,14 @@ $chat-input-inner-padding-handhelds: .25rem;
--message-background-color: var(--surface-color);
--message-checkbox-color: #61c642;
--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);
@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-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-audio-play-button-color: #fff;
@ -226,7 +245,7 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -226,7 +245,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--input-search-background-color: #181818;
--input-search-border-color: #2f2f2f;
@include splitColor(primary-color, #8774E1, true, true);
@include splitColor(primary-color, #8774E1, true, true, #212121);
--primary-text-color: #fff;
--secondary-color: #707579;
@ -251,10 +270,11 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -251,10 +270,11 @@ $chat-input-inner-padding-handhelds: .25rem;
--message-checkbox-border-color: #fff;
--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, #8774E1, true, true);
@include splitColor(message-out-background-color, $message-out-background-color, true, true);
--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-audio-play-button-color: var(--message-out-background-color);
// * Night theme end
@ -306,6 +326,9 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -306,6 +326,9 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/peopleNearby";
@import "partials/spoiler";
@import "partials/emojiAnimation";
@import "partials/reactions";
@import "partials/reaction";
@import "partials/stackedAvatars";
@import "partials/popups/popup";
@import "partials/popups/editAvatar";
@ -321,6 +344,7 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -321,6 +344,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/popups/groupCall";
@import "partials/popups/call";
@import "partials/popups/sponsored";
@import "partials/popups/reactedList";
@import "partials/pages/pages";
@import "partials/pages/authCode";
@ -1543,6 +1567,18 @@ hr { @@ -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 {
flex: 0 0 auto;
width: 1.25rem;

Loading…
Cancel
Save