From 33685bb13c716bf959c2ec6a726315ea0e96efe2 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Thu, 9 Mar 2023 16:06:12 +0400 Subject: [PATCH] Chat ranks --- src/components/chat/bubbles.ts | 89 ++++++++++++++++++++++-- src/lang.ts | 2 +- src/lib/appManagers/appProfileManager.ts | 16 +++-- src/lib/mtproto/api_methods.ts | 32 ++++++--- src/scss/partials/_chatBubble.scss | 11 +++ 5 files changed, 127 insertions(+), 23 deletions(-) diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index be4e6111..8161f847 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -29,7 +29,7 @@ import LazyLoadQueue from '../lazyLoadQueue'; import ListenerSetter from '../../helpers/listenerSetter'; import PollElement from '../poll'; import AudioElement from '../audio'; -import {Chat as MTChat, ChatInvite, Document, Message, MessageEntity, MessageMedia, MessageReplyHeader, Photo, PhotoSize, ReactionCount, ReplyMarkup, SponsoredMessage, Update, UrlAuthResult, User, WebPage} from '../../layer'; +import {ChannelParticipant, Chat as MTChat, ChatInvite, ChatParticipant, Document, Message, MessageEntity, MessageMedia, MessageReplyHeader, Photo, PhotoSize, ReactionCount, ReplyMarkup, SponsoredMessage, Update, UrlAuthResult, User, WebPage} from '../../layer'; import {BOT_START_PARAM, NULL_PEER_ID, REPLIES_PEER_ID} from '../../lib/mtproto/mtproto_config'; import {FocusDirection, ScrollStartCallbackDimensions} from '../../helpers/fastSmoothScroll'; import useHeavyAnimationCheck, {getHeavyAnimationPromise, dispatchHeavyAnimationEvent, interruptHeavyAnimation} from '../../hooks/useHeavyAnimationCheck'; @@ -127,9 +127,6 @@ import wrapUrl from '../../lib/richTextProcessor/wrapUrl'; import getMessageThreadId from '../../lib/appManagers/utils/messages/getMessageThreadId'; import wrapTopicNameButton from '../wrappers/topicNameButton'; import wrapMediaSpoiler, {onMediaSpoilerClick, toggleMediaSpoiler} from '../wrappers/mediaSpoiler'; -import confirmationPopup from '../confirmationPopup'; -import wrapPeerTitle from '../wrappers/peerTitle'; -import {PopupPeerCheckboxOptions} from '../popups/peer'; import toggleDisability from '../../helpers/dom/toggleDisability'; import {copyTextToClipboard} from '../../helpers/clipboard'; import liteMode from '../../helpers/liteMode'; @@ -137,6 +134,7 @@ import getMediaDurationFromMessage from '../../lib/appManagers/utils/messages/ge import wrapLocalSticker from '../wrappers/localSticker'; import {LottieAssetName} from '../../lib/rlottie/lottieLoader'; import clamp from '../../helpers/number/clamp'; +import getParticipantRank from '../../lib/appManagers/utils/chats/getParticipantRank'; export const USER_REACTIONS_INLINE = false; const USE_MEDIA_TAILS = false; @@ -302,6 +300,9 @@ export default class ChatBubbles { private batchProcessor: BatchProcessor>>; + private ranks: Map>; + private processRanks: Set<() => void>; + private canShowRanks: boolean; // private reactions: Map; constructor( @@ -3146,6 +3147,45 @@ export default class ChatBubbles { this.preloader.attach(this.container); } + if(!samePeer) { + this.ranks = undefined; + this.processRanks = undefined; + this.canShowRanks = false; + + if(this.chat.isChannel) { + this.canShowRanks = true; + const promise = this.managers.acknowledged.appProfileManager.getParticipants(this.peerId.toChatId(), {_: 'channelParticipantsAdmins'}, 100); + const ackedResult = await m(promise); + const setRanksPromise = ackedResult.result.then((channelParticipants) => { + const participants = channelParticipants.participants as (ChatParticipant.chatParticipantAdmin | ChannelParticipant.channelParticipantAdmin)[]; + this.ranks = new Map(); + participants.forEach((participant) => { + const rank = getParticipantRank(participant); + this.ranks.set(participant.user_id.toPeerId(), rank); + }); + }); + + if(ackedResult.cached) { + try { + await setRanksPromise; + } catch(err) { + this.ranks = new Map(); + this.log.error('ranks error', err); + } + } else { + const processRanks = this.processRanks = new Set(); + setRanksPromise.then(() => { + if(this.processRanks !== processRanks) { + return; + } + + processRanks.forEach((callback) => callback()); + this.processRanks = undefined; + }); + } + } + } + /* this.ladderDeferred && this.ladderDeferred.resolve(); this.ladderDeferred = deferredPromise(); */ @@ -5093,14 +5133,16 @@ export default class ChatBubbles { const isForward = fwdFromId || fwdFrom; if(isHidden) { - // /////this.log('message to render hidden', message); title = document.createElement('span'); setInnerHTML(title, wrapEmojiText(fwdFrom.from_name)); title.classList.add('peer-title'); - // title = fwdFrom.from_name; bubble.classList.add('hidden-profile'); } else { - title = new PeerTitle({peerId: fwdFromId || message.fromId, withPremiumIcon: !isForward, wrapOptions}).element; + title = new PeerTitle({ + peerId: fwdFromId || message.fromId, + withPremiumIcon: !isForward, + wrapOptions + }).element; } let replyContainer: HTMLElement; @@ -5218,6 +5260,30 @@ export default class ChatBubbles { replyContainer.classList.add('floating-part'); } + if(title && !isHidden && !fwdFromId && this.canShowRanks) { + const processRank = () => { + const rank = this.ranks.get(message.fromId); + if(!rank) { + return; + } + + (title as HTMLElement).after(this.createBubbleNameRank(rank)); + }; + + if(this.ranks) { + processRank(); + } else { + const processRanks = this.processRanks; + processRanks.add(processRank); + + middleware.onDestroy(() => { + processRanks.delete(processRank); + }); + } + } else if(isForwardFromChannel) { + (title as HTMLElement).after(this.createBubbleNameRank(0)); + } + if(topicNameButtonContainer && isStandaloneMedia) { if(!attachmentDiv) { this.log.error('no attachment div?', bubble, message); @@ -5340,6 +5406,15 @@ export default class ChatBubbles { } } + private createBubbleNameRank(rank: ReturnType | 0) { + const span = document.createElement('span'); + span.classList.add('bubble-name-rank'); + span.append(typeof(rank) === 'number' ? + i18n(!rank ? 'Chat.ChannelBadge' : (rank === 1 ? 'Chat.OwnerBadge' : 'ChatAdmin')) : + rank); + return span; + } + private prepareToSaveScroll(reverse?: boolean) { const isMounted = !!this.chatInner.parentElement; if(!isMounted) { diff --git a/src/lang.ts b/src/lang.ts index 613b17b9..3cfe71a2 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -1104,7 +1104,7 @@ const lang = { 'one_value': '%d Comment', 'other_value': '%d Comments' }, - 'Chat.TopicBadge': 'topic creator', + // 'Chat.TopicBadge': 'topic creator', 'ChatTitle.ReportMessages': 'Report Messages', 'Chat.Send.WithoutSound': 'Send Without Sound', 'Chat.Send.SetReminder': 'Set a Reminder', diff --git a/src/lib/appManagers/appProfileManager.ts b/src/lib/appManagers/appProfileManager.ts index 62f46349..7dc4660c 100644 --- a/src/lib/appManagers/appProfileManager.ts +++ b/src/lib/appManagers/appProfileManager.ts @@ -300,7 +300,7 @@ export class AppProfileManager extends AppManager { return this.getChannelParticipants(id, filter, limit, offset); } - return Promise.resolve(this.getChatFull(id)).then((chatFull) => { + return callbackify(this.getChatFull(id), (chatFull) => { const chatParticipants = (chatFull as ChatFull.chatFull).participants; if(chatParticipants._ !== 'chatParticipants') { throw makeError('CHAT_PRIVATE'); @@ -330,7 +330,7 @@ export class AppProfileManager extends AppManager { return this.getChannelParticipant(id, peerId); } - return this.getParticipants(id).then((chatParticipants) => { + return Promise.resolve(this.getParticipants(id)).then((chatParticipants) => { assumeType(chatParticipants); const found = chatParticipants.participants.find((chatParticipant) => { if(getParticipantPeerId(chatParticipant) === peerId) { @@ -360,17 +360,19 @@ export class AppProfileManager extends AppManager { !(chat as Chat.channel).pFlags.creator && !(chat as Chat.channel).admin_rights )) { - return Promise.reject(); + throw makeError('PEER_ID_INVALID'); } } - return this.apiManager.invokeApiCacheable('channels.getParticipants', { + const result = this.apiManager.invokeApiCacheable('channels.getParticipants', { channel: this.appChatsManager.getChannelInput(id), filter, offset, limit, hash: '0' - }, {cacheSeconds: 60}).then((result) => { + }, {cacheSeconds: 60, syncIfHasResult: true}); + + return callbackify(result, (result) => { this.appUsersManager.saveApiUsers((result as ChannelsChannelParticipants.channelsChannelParticipants).users); return result as ChannelsChannelParticipants.channelsChannelParticipants; }); @@ -498,11 +500,11 @@ export class AppProfileManager extends AppManager { let promise: Promise; if(this.appChatsManager.isChannel(chatId)) { - promise = this.getChannelParticipants(chatId, { + promise = Promise.resolve(this.getChannelParticipants(chatId, { _: 'channelParticipantsMentions', q: query, top_msg_id: getServerMessageId(threadId) - }, 50, 0).then((cP) => { + }, 50, 0)).then((cP) => { return cP.participants.map((p) => getParticipantPeerId(p)); }); } else if(chatId) { diff --git a/src/lib/mtproto/api_methods.ts b/src/lib/mtproto/api_methods.ts index de155a02..6f2afebb 100644 --- a/src/lib/mtproto/api_methods.ts +++ b/src/lib/mtproto/api_methods.ts @@ -41,6 +41,7 @@ export default abstract class ApiManagerMethods extends AppManager { timestamp: number, promise: Promise, fulfilled: boolean, + result?: any, timeout?: number, params: any } @@ -159,7 +160,7 @@ export default abstract class ApiManagerMethods extends AppManager { const {method, processResult, processError, params, options} = o; const cache = this.apiPromisesSingleProcess; const cacheKey = options.cacheKey || JSON.stringify(params); - const map = cache[method] ?? (cache[method] = new Map()); + const map = cache[method] ??= new Map(); const oldPromise = map.get(cacheKey); if(oldPromise) { return oldPromise; @@ -201,16 +202,23 @@ export default abstract class ApiManagerMethods extends AppManager { return p; } - public invokeApiCacheable(method: T, params: MethodDeclMap[T]['req'] = {} as any, options: InvokeApiOptions & Partial<{cacheSeconds: number, override: boolean}> = {}): Promise { - const cache = this.apiPromisesCacheable[method] ?? (this.apiPromisesCacheable[method] = {}); + public invokeApiCacheable< + T extends keyof MethodDeclMap, + O extends InvokeApiOptions & Partial<{cacheSeconds: number, override: boolean, syncIfHasResult: boolean}> + >( + method: T, + params: MethodDeclMap[T]['req'] = {} as any, + options: O = {} as any + ): O['syncIfHasResult'] extends true ? MethodDeclMap[T]['res'] | Promise : Promise { + const cache = this.apiPromisesCacheable[method] ??= {}; const queryJSON = JSON.stringify(params); - const item = cache[queryJSON]; + let item = cache[queryJSON]; if(item && (!options.override || !item.fulfilled)) { - return item.promise; + return options.syncIfHasResult && item.hasOwnProperty('result') ? item.result : item.promise; } if(options.override) { - if(item && item.timeout) { + if(item?.timeout) { clearTimeout(item.timeout); delete item.timeout; } @@ -221,14 +229,22 @@ export default abstract class ApiManagerMethods extends AppManager { let timeout: number; if(options.cacheSeconds) { timeout = ctx.setTimeout(() => { - delete cache[queryJSON]; + if(cache[queryJSON] === item) { + delete cache[queryJSON]; + } }, options.cacheSeconds * 1000); delete options.cacheSeconds; } const promise = this.invokeApi(method, params, options); - cache[queryJSON] = { + const onResult = (result: any) => { + item.result = result; + }; + + promise.then(onResult, onResult); + + item = cache[queryJSON] = { timestamp: Date.now(), fulfilled: false, timeout, diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 65717229..92192622 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -2115,6 +2115,9 @@ $bubble-border-radius-big: 12px; // order: 1; //width: max-content; //white-space: nowrap; + display: flex; + justify-content: space-between; + align-items: center; .badge-fake { display: inline-block; @@ -2123,6 +2126,14 @@ $bubble-border-radius-big: 12px; } } + &-name-rank { + color: var(--message-time-color); + font-weight: var(--font-weight-normal); + font-size: var(--messages-time-text-size); + margin-inline-start: .125rem; + @include text-overflow(); + } + /* &:not(.is-group-first) .bubble-content > .name .name { display: none; } */