Chat ranks

This commit is contained in:
Eduard Kuzmenko 2023-03-09 16:06:12 +04:00
parent 253dcf55b5
commit 33685bb13c
5 changed files with 127 additions and 23 deletions

View File

@ -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<Awaited<ReturnType<ChatBubbles['safeRenderMessage']>>>;
private ranks: Map<PeerId, ReturnType<typeof getParticipantRank>>;
private processRanks: Set<() => void>;
private canShowRanks: boolean;
// private reactions: Map<number, ReactionsElement>;
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<void>(); */
@ -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<typeof getParticipantRank> | 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) {

View File

@ -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',

View File

@ -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.chatParticipants>(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<PeerId[]>;
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) {

View File

@ -41,6 +41,7 @@ export default abstract class ApiManagerMethods extends AppManager {
timestamp: number,
promise: Promise<any>,
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<T extends keyof MethodDeclMap>(method: T, params: MethodDeclMap[T]['req'] = {} as any, options: InvokeApiOptions & Partial<{cacheSeconds: number, override: boolean}> = {}): Promise<MethodDeclMap[T]['res']> {
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<MethodDeclMap[T]['res']> : Promise<MethodDeclMap[T]['res']> {
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,

View File

@ -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;
} */