Message: Show admin and owner marks (#2156)

This commit is contained in:
Alexander Zinchuk 2022-11-27 19:16:54 +01:00
parent 365a021dda
commit 5c510a91a4
20 changed files with 139 additions and 64 deletions

View File

@ -274,10 +274,14 @@ export function buildChatMember(
return { return {
userId, userId,
inviterId: 'inviterId' in member ? buildApiPeerId(member.inviterId as BigInt.BigInteger, 'user') : undefined, inviterId: 'inviterId' in member && member.inviterId
? buildApiPeerId(member.inviterId as BigInt.BigInteger, 'user')
: undefined,
joinedDate: 'date' in member ? member.date : undefined, joinedDate: 'date' in member ? member.date : undefined,
kickedByUserId: 'kickedBy' in member ? buildApiPeerId(member.kickedBy, 'user') : undefined, kickedByUserId: 'kickedBy' in member && member.kickedBy ? buildApiPeerId(member.kickedBy, 'user') : undefined,
promotedByUserId: 'promotedBy' in member ? buildApiPeerId(member.promotedBy, 'user') : undefined, promotedByUserId: 'promotedBy' in member && member.promotedBy
? buildApiPeerId(member.promotedBy, 'user')
: undefined,
bannedRights: 'bannedRights' in member ? omitVirtualClassFields(member.bannedRights) : undefined, bannedRights: 'bannedRights' in member ? omitVirtualClassFields(member.bannedRights) : undefined,
adminRights: 'adminRights' in member ? omitVirtualClassFields(member.adminRights) : undefined, adminRights: 'adminRights' in member ? omitVirtualClassFields(member.adminRights) : undefined,
customTitle: 'rank' in member ? member.rank : undefined, customTitle: 'rank' in member ? member.rank : undefined,

View File

@ -210,7 +210,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
...(shouldHideKeyboardButtons && { shouldHideKeyboardButtons }), ...(shouldHideKeyboardButtons && { shouldHideKeyboardButtons }),
...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }), ...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }),
...(replies?.comments && { threadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }), ...(replies?.comments && { threadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }),
...(postAuthor && { adminTitle: postAuthor }), ...(postAuthor && { postAuthorTitle: postAuthor }),
isProtected, isProtected,
isForwardingAllowed, isForwardingAllowed,
}; };
@ -399,7 +399,7 @@ function buildApiMessageForwardInfo(fwdFrom: GramJs.MessageFwdHeader, isChatWith
fromMessageId: fwdFrom.savedFromMsgId || fwdFrom.channelPost, fromMessageId: fwdFrom.savedFromMsgId || fwdFrom.channelPost,
senderUserId: fromId, senderUserId: fromId,
hiddenUserName: fwdFrom.fromName, hiddenUserName: fwdFrom.fromName,
adminTitle: fwdFrom.postAuthor, postAuthorTitle: fwdFrom.postAuthor,
}; };
} }

View File

@ -385,7 +385,7 @@ async function getFullChatInfo(chatId: string): Promise<FullChatData | undefined
...(chatPhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(chatPhoto) }), ...(chatPhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(chatPhoto) }),
about, about,
members, members,
adminMembers, adminMembersById: adminMembers ? buildCollectionByKey(adminMembers, 'userId') : undefined,
canViewMembers: true, canViewMembers: true,
botCommands, botCommands,
...(exportedInvite instanceof GramJs.ChatInviteExported && { ...(exportedInvite instanceof GramJs.ChatInviteExported && {
@ -463,7 +463,7 @@ async function getFullChannelInfo(
canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'kicked') canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'kicked')
) || {}; ) || {};
const { members: adminMembers, users: adminUsers, userStatusesById: adminStatusesById } = ( const { members: adminMembers, users: adminUsers, userStatusesById: adminStatusesById } = (
canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'admin') canViewParticipants && await fetchMembers(id, accessHash, 'admin')
) || {}; ) || {};
const botCommands = botInfo ? buildApiChatBotCommands(botInfo) : undefined; const botCommands = botInfo ? buildApiChatBotCommands(botInfo) : undefined;
@ -506,7 +506,7 @@ async function getFullChannelInfo(
isPreHistoryHidden: hiddenPrehistory, isPreHistoryHidden: hiddenPrehistory,
members, members,
kickedMembers, kickedMembers,
adminMembers, adminMembersById: adminMembers ? buildCollectionByKey(adminMembers, 'userId') : undefined,
groupCallId: call ? String(call.id) : undefined, groupCallId: call ? String(call.id) : undefined,
linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'chat') : undefined, linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'chat') : undefined,
botCommands, botCommands,

View File

@ -86,7 +86,7 @@ export interface ApiChatFullInfo {
onlineCount?: number; onlineCount?: number;
members?: ApiChatMember[]; members?: ApiChatMember[];
kickedMembers?: ApiChatMember[]; kickedMembers?: ApiChatMember[];
adminMembers?: ApiChatMember[]; adminMembersById?: Record<string, ApiChatMember>;
canViewMembers?: boolean; canViewMembers?: boolean;
isPreHistoryHidden?: boolean; isPreHistoryHidden?: boolean;
inviteLink?: string; inviteLink?: string;

View File

@ -293,7 +293,7 @@ export interface ApiMessageForwardInfo {
senderUserId?: string; senderUserId?: string;
fromMessageId?: number; fromMessageId?: number;
hiddenUserName?: string; hiddenUserName?: string;
adminTitle?: string; postAuthorTitle?: string;
} }
export type ApiMessageEntityDefault = { export type ApiMessageEntityDefault = {
@ -408,7 +408,7 @@ export interface ApiMessage {
isKeyboardSingleUse?: boolean; isKeyboardSingleUse?: boolean;
viaBotId?: string; viaBotId?: string;
threadInfo?: ApiThreadInfo; threadInfo?: ApiThreadInfo;
adminTitle?: string; postAuthorTitle?: string;
isScheduled?: boolean; isScheduled?: boolean;
shouldHideKeyboardButtons?: boolean; shouldHideKeyboardButtons?: boolean;
isFromScheduled?: boolean; isFromScheduled?: boolean;

View File

@ -2,7 +2,9 @@ import React, { useEffect, useCallback, memo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import type { ApiUser, ApiTypingStatus, ApiUserStatus } from '../../api/types'; import type {
ApiUser, ApiTypingStatus, ApiUserStatus, ApiChatMember,
} from '../../api/types';
import type { GlobalState } from '../../global/types'; import type { GlobalState } from '../../global/types';
import type { AnimationLevel } from '../../types'; import type { AnimationLevel } from '../../types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
@ -34,6 +36,7 @@ type OwnProps = {
emojiStatusSize?: number; emojiStatusSize?: number;
noStatusOrTyping?: boolean; noStatusOrTyping?: boolean;
noRtl?: boolean; noRtl?: boolean;
adminMember?: ApiChatMember;
}; };
type StateProps = type StateProps =
@ -67,6 +70,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
animationLevel, animationLevel,
lastSyncTime, lastSyncTime,
serverTimeOffset, serverTimeOffset,
adminMember,
}) => { }) => {
const { const {
loadFullUser, loadFullUser,
@ -131,6 +135,10 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
); );
} }
const customTitle = adminMember
? adminMember.customTitle || lang(adminMember.isOwner ? 'GroupInfo.LabelOwner' : 'GroupInfo.LabelAdmin')
: undefined;
return ( return (
<div className="ChatInfo" dir={!noRtl && lang.isRtl ? 'rtl' : undefined}> <div className="ChatInfo" dir={!noRtl && lang.isRtl ? 'rtl' : undefined}>
<Avatar <Avatar
@ -143,12 +151,15 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
animationLevel={animationLevel} animationLevel={animationLevel}
/> />
<div className="info"> <div className="info">
<FullNameTitle <div className="info-name-title">
peer={user} <FullNameTitle
withEmojiStatus peer={user}
emojiStatusSize={emojiStatusSize} withEmojiStatus
isSavedMessages={isSavedMessages} emojiStatusSize={emojiStatusSize}
/> isSavedMessages={isSavedMessages}
/>
{customTitle && <span className="custom-title">{customTitle}</span>}
</div>
{(status || (!isSavedMessages && !noStatusOrTyping)) && renderStatusOrTyping()} {(status || (!isSavedMessages && !noStatusOrTyping)) && renderStatusOrTyping()}
</div> </div>
</div> </div>

View File

@ -45,7 +45,7 @@ import {
selectTheme, selectTheme,
} from '../../global/selectors'; } from '../../global/selectors';
import { import {
getCanPostInChat, getMessageSendingRestrictionReason, isChatChannel, isChatSuperGroup, isUserId, getCanPostInChat, getMessageSendingRestrictionReason, isChatChannel, isChatGroup, isChatSuperGroup, isUserId,
} from '../../global/helpers'; } from '../../global/helpers';
import captureEscKeyListener from '../../util/captureEscKeyListener'; import captureEscKeyListener from '../../util/captureEscKeyListener';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -108,6 +108,7 @@ type StateProps = {
canSubscribe?: boolean; canSubscribe?: boolean;
canStartBot?: boolean; canStartBot?: boolean;
canRestartBot?: boolean; canRestartBot?: boolean;
shouldLoadFullChat?: boolean;
activeEmojiInteractions?: ActiveEmojiInteraction[]; activeEmojiInteractions?: ActiveEmojiInteraction[];
shouldJoinToSend?: boolean; shouldJoinToSend?: boolean;
shouldSendJoinRequest?: boolean; shouldSendJoinRequest?: boolean;
@ -154,6 +155,7 @@ const MiddleColumn: FC<StateProps> = ({
activeEmojiInteractions, activeEmojiInteractions,
shouldJoinToSend, shouldJoinToSend,
shouldSendJoinRequest, shouldSendJoinRequest,
shouldLoadFullChat,
lastSyncTime, lastSyncTime,
}) => { }) => {
const { const {
@ -168,6 +170,7 @@ const MiddleColumn: FC<StateProps> = ({
sendBotCommand, sendBotCommand,
restartBot, restartBot,
showNotification, showNotification,
loadFullChat,
} = getActions(); } = getActions();
const { width: windowWidth } = useWindowSize(); const { width: windowWidth } = useWindowSize();
@ -264,6 +267,12 @@ const MiddleColumn: FC<StateProps> = ({
} }
}, [chatId, isPrivate, areChatSettingsLoaded, lastSyncTime, loadChatSettings]); }, [chatId, isPrivate, areChatSettingsLoaded, lastSyncTime, loadChatSettings]);
useEffect(() => {
if (chatId && shouldLoadFullChat && isReady) {
loadFullChat({ chatId });
}
}, [shouldLoadFullChat, chatId, isReady, loadFullChat]);
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => { const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
if (IS_TOUCH_ENV) { if (IS_TOUCH_ENV) {
return; return;
@ -616,6 +625,7 @@ export default memo(withGlobal(
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest); const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id)); const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
const canStartBot = !canRestartBot && isBotNotStarted; const canStartBot = !canRestartBot && isBotNotStarted;
const shouldLoadFullChat = Boolean(chat && isChatGroup(chat) && !chat.fullInfo && lastSyncTime);
return { return {
...state, ...state,
@ -645,6 +655,7 @@ export default memo(withGlobal(
canRestartBot, canRestartBot,
shouldJoinToSend, shouldJoinToSend,
shouldSendJoinRequest, shouldSendJoinRequest,
shouldLoadFullChat,
}; };
}, },
)(MiddleColumn)); )(MiddleColumn));

View File

@ -280,7 +280,6 @@ const Composer: FC<OwnProps & StateProps> = ({
addRecentEmoji, addRecentEmoji,
sendInlineBotResult, sendInlineBotResult,
loadSendAs, loadSendAs,
loadFullChat,
resetOpenChatWithDraft, resetOpenChatWithDraft,
callAttachBot, callAttachBot,
openLimitReachedModal, openLimitReachedModal,
@ -334,12 +333,6 @@ const Composer: FC<OwnProps & StateProps> = ({
} }
}, [chat, chatId, isReady, lastSyncTime, loadSendAs, sendAsPeerIds]); }, [chat, chatId, isReady, lastSyncTime, loadSendAs, sendAsPeerIds]);
useEffect(() => {
if (chatId && chat && lastSyncTime && !chat.fullInfo && isReady && isChatSuperGroup(chat)) {
loadFullChat({ chatId });
}
}, [chat, chatId, isReady, lastSyncTime, loadFullChat]);
const shouldAnimateSendAsButtonRef = useRef(false); const shouldAnimateSendAsButtonRef = useRef(false);
useOnChange(([prevChatId, prevSendAsPeerIds]) => { useOnChange(([prevChatId, prevSendAsPeerIds]) => {
// We only animate send-as button if `sendAsPeerIds` was missing when opening the chat // We only animate send-as button if `sendAsPeerIds` was missing when opening the chat

View File

@ -17,6 +17,7 @@ import type {
ApiChat, ApiChat,
ApiThreadInfo, ApiThreadInfo,
ApiAvailableReaction, ApiAvailableReaction,
ApiChatMember,
} from '../../../api/types'; } from '../../../api/types';
import type { import type {
AnimationLevel, FocusDirection, IAlbum, ISettings, AnimationLevel, FocusDirection, IAlbum, ISettings,
@ -74,7 +75,9 @@ import {
areReactionsEmpty, areReactionsEmpty,
getMessageHtmlId, getMessageHtmlId,
isGeoLiveExpired, isGeoLiveExpired,
getMessageSingleCustomEmoji, hasMessageText, getMessageSingleCustomEmoji,
hasMessageText,
isChatGroup,
} from '../../../global/helpers'; } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useEnsureMessage from '../../../hooks/useEnsureMessage';
@ -183,6 +186,7 @@ type StateProps = {
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
isRepliesChat?: boolean; isRepliesChat?: boolean;
isChannel?: boolean; isChannel?: boolean;
isGroup?: boolean;
canReply?: boolean; canReply?: boolean;
lastSyncTime?: number; lastSyncTime?: number;
serverTimeOffset: number; serverTimeOffset: number;
@ -211,6 +215,7 @@ type StateProps = {
isTranscriptionError?: boolean; isTranscriptionError?: boolean;
isPremium: boolean; isPremium: boolean;
animationLevel: AnimationLevel; animationLevel: AnimationLevel;
senderAdminMember?: ApiChatMember;
}; };
type MetaPosition = type MetaPosition =
@ -275,6 +280,7 @@ const Message: FC<OwnProps & StateProps> = ({
isChatWithSelf, isChatWithSelf,
isRepliesChat, isRepliesChat,
isChannel, isChannel,
isGroup,
canReply, canReply,
lastSyncTime, lastSyncTime,
serverTimeOffset, serverTimeOffset,
@ -301,6 +307,7 @@ const Message: FC<OwnProps & StateProps> = ({
hasUnreadReaction, hasUnreadReaction,
memoFirstUnreadIdRef, memoFirstUnreadIdRef,
animationLevel, animationLevel,
senderAdminMember,
}) => { }) => {
const { const {
toggleMessageSelection, toggleMessageSelection,
@ -615,7 +622,9 @@ const Message: FC<OwnProps & StateProps> = ({
style = `width: ${calculatedWidth + extraPadding}px`; style = `width: ${calculatedWidth + extraPadding}px`;
} }
const signature = (isChannel && message.adminTitle) || (!asForwarded && forwardInfo?.adminTitle) || undefined; const signature = (isChannel && message.postAuthorTitle)
|| (!asForwarded && forwardInfo?.postAuthorTitle)
|| undefined;
const metaSafeAuthorWidth = useMemo(() => { const metaSafeAuthorWidth = useMemo(() => {
return signature ? calculateAuthorWidth(signature) : undefined; return signature ? calculateAuthorWidth(signature) : undefined;
}, [signature]); }, [signature]);
@ -984,13 +993,23 @@ const Message: FC<OwnProps & StateProps> = ({
)} )}
{forwardInfo?.isLinkedChannelPost ? ( {forwardInfo?.isLinkedChannelPost ? (
<span className="admin-title" dir="auto">{lang('DiscussChannel')}</span> <span className="admin-title" dir="auto">{lang('DiscussChannel')}</span>
) : message.adminTitle && !isChannel ? ( ) : message.forwardInfo?.postAuthorTitle && isGroup && asForwarded ? (
<span className="admin-title" dir="auto">{message.adminTitle}</span> <span className="admin-title" dir="auto">{message.forwardInfo?.postAuthorTitle}</span>
) : message.postAuthorTitle && isGroup && !asForwarded ? (
<span className="admin-title" dir="auto">{message.postAuthorTitle}</span>
) : senderAdminMember && !asForwarded ? (
<span className="admin-title" dir="auto">
{senderAdminMember.customTitle || lang(
senderAdminMember.isOwner ? 'GroupInfo.LabelOwner' : 'GroupInfo.LabelAdmin',
)}
</span>
) : undefined} ) : undefined}
</div> </div>
); );
} }
const forwardAuthor = isGroup && asForwarded ? message.postAuthorTitle : undefined;
return ( return (
<div <div
ref={ref} ref={ref}
@ -1040,7 +1059,10 @@ const Message: FC<OwnProps & StateProps> = ({
dir="auto" dir="auto"
> >
{asForwarded && !isInDocumentGroupNotFirst && ( {asForwarded && !isInDocumentGroupNotFirst && (
<div className="message-title">{lang('ForwardedMessage')}</div> <div className="message-title">
{lang('ForwardedMessage')}
{forwardAuthor && <span className="admin-title" dir="auto">{forwardAuthor}</span>}
</div>
)} )}
{renderContent()} {renderContent()}
{!isInDocumentGroupNotLast && metaPosition === 'standalone' && renderReactionsAndMeta()} {!isInDocumentGroupNotLast && metaPosition === 'standalone' && renderReactionsAndMeta()}
@ -1124,13 +1146,18 @@ export default memo(withGlobal<OwnProps>(
const isChatWithSelf = selectIsChatWithSelf(global, chatId); const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const isRepliesChat = isChatWithRepliesBot(chatId); const isRepliesChat = isChatWithRepliesBot(chatId);
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
const isGroup = chat && isChatGroup(chat);
const chatUsername = chat?.username; const chatUsername = chat?.username;
const isForwarding = forwardMessages.messageIds && forwardMessages.messageIds.includes(id);
const forceSenderName = !isChatWithSelf && isAnonymousOwnMessage(message); const forceSenderName = !isChatWithSelf && isAnonymousOwnMessage(message);
const canShowSender = withSenderName || withAvatar || forceSenderName; const canShowSender = withSenderName || withAvatar || forceSenderName;
const sender = selectSender(global, message); const sender = selectSender(global, message);
const originSender = selectForwardedSender(global, message); const originSender = selectForwardedSender(global, message);
const botSender = viaBotId ? selectUser(global, viaBotId) : undefined; const botSender = viaBotId ? selectUser(global, viaBotId) : undefined;
const senderAdminMember = sender?.id && isGroup
? chat.fullInfo?.adminMembersById?.[sender?.id]
: undefined;
const threadTopMessageId = threadId ? selectThreadTopMessageId(global, chatId, threadId) : undefined; const threadTopMessageId = threadId ? selectThreadTopMessageId(global, chatId, threadId) : undefined;
const isThreadTop = message.id === threadTopMessageId; const isThreadTop = message.id === threadTopMessageId;
@ -1152,8 +1179,6 @@ export default memo(withGlobal<OwnProps>(
direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer, direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer,
} = (isFocused && focusedMessage) || {}; } = (isFocused && focusedMessage) || {};
const isForwarding = forwardMessages.messageIds && forwardMessages.messageIds.includes(id);
const { query: highlight } = selectCurrentTextSearch(global) || {}; const { query: highlight } = selectCurrentTextSearch(global) || {};
const singleEmoji = getMessageSingleRegularEmoji(message); const singleEmoji = getMessageSingleRegularEmoji(message);
@ -1206,6 +1231,7 @@ export default memo(withGlobal<OwnProps>(
isChatWithSelf, isChatWithSelf,
isRepliesChat, isRepliesChat,
isChannel, isChannel,
isGroup,
canReply, canReply,
lastSyncTime, lastSyncTime,
serverTimeOffset, serverTimeOffset,
@ -1239,6 +1265,7 @@ export default memo(withGlobal<OwnProps>(
transcribedText: transcriptionId !== undefined ? global.transcriptions[transcriptionId]?.text : undefined, transcribedText: transcriptionId !== undefined ? global.transcriptions[transcriptionId]?.text : undefined,
isPremium: selectIsCurrentUserPremium(global), isPremium: selectIsCurrentUserPremium(global),
animationLevel: global.settings.byKey.animationLevel, animationLevel: global.settings.byKey.animationLevel,
senderAdminMember,
}; };
}, },
)(Message)); )(Message));

View File

@ -85,6 +85,7 @@ type StateProps = {
canAddMembers?: boolean; canAddMembers?: boolean;
canDeleteMembers?: boolean; canDeleteMembers?: boolean;
members?: ApiChatMember[]; members?: ApiChatMember[];
adminMembersById?: Record<string, ApiChatMember>;
commonChatIds?: string[]; commonChatIds?: string[];
chatsById: Record<string, ApiChat>; chatsById: Record<string, ApiChat>;
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
@ -126,6 +127,7 @@ const Profile: FC<OwnProps & StateProps> = ({
canDeleteMembers, canDeleteMembers,
commonChatIds, commonChatIds,
members, members,
adminMembersById,
usersById, usersById,
userStatusesById, userStatusesById,
chatsById, chatsById,
@ -416,7 +418,7 @@ const Profile: FC<OwnProps & StateProps> = ({
onClick={() => handleMemberClick(id)} onClick={() => handleMemberClick(id)}
contextActions={getMemberContextAction(id)} contextActions={getMemberContextAction(id)}
> >
<PrivateChatInfo userId={id} forceShowSelf /> <PrivateChatInfo userId={id} adminMember={adminMembersById?.[id]} forceShowSelf />
</ListItem> </ListItem>
)) ))
) : resultType === 'commonChats' ? ( ) : resultType === 'commonChats' ? (
@ -524,6 +526,7 @@ export default memo(withGlobal<OwnProps>(
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
const hasMembersTab = isGroup || (isChannel && isChatAdmin(chat!)); const hasMembersTab = isGroup || (isChannel && isChatAdmin(chat!));
const members = chat?.fullInfo?.members; const members = chat?.fullInfo?.members;
const adminMembersById = chat?.fullInfo?.adminMembersById;
const areMembersHidden = hasMembersTab && chat const areMembersHidden = hasMembersTab && chat
&& (chat.isForbidden || (chat.fullInfo && !chat.fullInfo.canViewMembers)); && (chat.isForbidden || (chat.fullInfo && !chat.fullInfo.canViewMembers));
const canAddMembers = hasMembersTab && chat const canAddMembers = hasMembersTab && chat
@ -562,7 +565,7 @@ export default memo(withGlobal<OwnProps>(
userStatusesById, userStatusesById,
chatsById, chatsById,
isChatProtected: chat?.isProtected, isChatProtected: chat?.isProtected,
...(hasMembersTab && members && { members }), ...(hasMembersTab && members && { members, adminMembersById }),
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }), ...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),
}; };
}, },

View File

@ -108,7 +108,7 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
} }
}, [progress]); }, [progress]);
const adminsCount = (chat?.fullInfo?.adminMembers?.length) || 0; const adminsCount = Object.keys(chat.fullInfo?.adminMembersById || {}).length;
const removedUsersCount = (chat?.fullInfo?.kickedMembers?.length) || 0; const removedUsersCount = (chat?.fullInfo?.kickedMembers?.length) || 0;
const handleClickEditType = useCallback(() => { const handleClickEditType = useCallback(() => {

View File

@ -49,11 +49,11 @@ const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
}, [onScreenSelect]); }, [onScreenSelect]);
const adminMembers = useMemo(() => { const adminMembers = useMemo(() => {
if (!chat.fullInfo || !chat.fullInfo.adminMembers) { if (!chat.fullInfo?.adminMembersById) {
return []; return [];
} }
return chat.fullInfo.adminMembers.sort((a, b) => { return Object.values(chat.fullInfo.adminMembersById).sort((a, b) => {
if (a.isOwner) { if (a.isOwner) {
return -1; return -1;
} else if (b.isOwner) { } else if (b.isOwner) {

View File

@ -236,7 +236,9 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
return totalCount; return totalCount;
}, [chat]); }, [chat]);
const adminsCount = (chat.fullInfo?.adminMembers?.length) || 0; const adminsCount = useMemo(() => {
return Object.keys(chat.fullInfo?.adminMembersById || {}).length;
}, [chat.fullInfo?.adminMembersById]);
const handleDeleteGroup = useCallback(() => { const handleDeleteGroup = useCallback(() => {
if (isBasicGroup) { if (isBasicGroup) {

View File

@ -70,7 +70,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
}); });
const selectedChatMember = useMemo(() => { const selectedChatMember = useMemo(() => {
const selectedAdminMember = chat.fullInfo?.adminMembers?.find(({ userId }) => userId === selectedUserId); const selectedAdminMember = selectedUserId ? chat.fullInfo?.adminMembersById?.[selectedUserId] : undefined;
// If `selectedAdminMember` variable is filled with a value, then we have already saved the administrator, // If `selectedAdminMember` variable is filled with a value, then we have already saved the administrator,
// so now we need to return to the list of administrators // so now we need to return to the list of administrators
@ -91,7 +91,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
} }
return selectedAdminMember; return selectedAdminMember;
}, [chat.fullInfo?.adminMembers, defaultRights, isNewAdmin, lang, selectedUserId]); }, [chat.fullInfo?.adminMembersById, defaultRights, isNewAdmin, lang, selectedUserId]);
useEffect(() => { useEffect(() => {
if (chat?.fullInfo && selectedUserId && !selectedChatMember) { if (chat?.fullInfo && selectedUserId && !selectedChatMember) {

View File

@ -37,7 +37,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
userStatusesById: Record<string, ApiUserStatus>; userStatusesById: Record<string, ApiUserStatus>;
members?: ApiChatMember[]; members?: ApiChatMember[];
adminMembers?: ApiChatMember[]; adminMembersById?: Record<string, ApiChatMember>;
isChannel?: boolean; isChannel?: boolean;
localContactIds?: string[]; localContactIds?: string[];
searchQuery?: string; searchQuery?: string;
@ -52,7 +52,7 @@ type StateProps = {
const ManageGroupMembers: FC<OwnProps & StateProps> = ({ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
noAdmins, noAdmins,
members, members,
adminMembers, adminMembersById,
userStatusesById, userStatusesById,
isChannel, isChannel,
isActive, isActive,
@ -78,8 +78,8 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
const [deletingUserId, setDeletingUserId] = useState<string | undefined>(); const [deletingUserId, setDeletingUserId] = useState<string | undefined>();
const adminIds = useMemo(() => { const adminIds = useMemo(() => {
return noAdmins ? adminMembers?.map(({ userId }) => userId) || [] : []; return noAdmins && adminMembersById ? Object.keys(adminMembersById) : [];
}, [adminMembers, noAdmins]); }, [adminMembersById, noAdmins]);
const memberIds = useMemo(() => { const memberIds = useMemo(() => {
// No need for expensive global updates on users, so we avoid them // No need for expensive global updates on users, so we avoid them
@ -233,7 +233,7 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const { statusesById: userStatusesById } = global.users; const { statusesById: userStatusesById } = global.users;
const members = chat?.fullInfo?.members; const members = chat?.fullInfo?.members;
const adminMembers = chat?.fullInfo?.adminMembers; const adminMembersById = chat?.fullInfo?.adminMembersById;
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
const { userIds: localContactIds } = global.contactList || {}; const { userIds: localContactIds } = global.contactList || {};
@ -248,7 +248,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
members, members,
adminMembers, adminMembersById,
userStatusesById, userStatusesById,
isChannel, isChannel,
localContactIds, localContactIds,

View File

@ -30,11 +30,11 @@ const ManageGroupRecentActions: FC<OwnProps & StateProps> = ({ chat, onClose, is
}); });
const adminMembers = useMemo(() => { const adminMembers = useMemo(() => {
if (!chat || !chat.fullInfo || !chat.fullInfo.adminMembers) { if (!chat?.fullInfo?.adminMembersById) {
return []; return [];
} }
return chat.fullInfo.adminMembers.sort((a, b) => { return Object.values(chat.fullInfo.adminMembersById).sort((a, b) => {
if (a.isOwner) { if (a.isOwner) {
return -1; return -1;
} else if (b.isOwner) { } else if (b.isOwner) {

View File

@ -165,6 +165,11 @@
overflow: hidden; overflow: hidden;
} }
.info-name-title {
display: flex;
align-items: center;
}
.info-row, .info-row,
.title, .title,
.subtitle { .subtitle {
@ -211,6 +216,17 @@
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
.custom-title {
padding-inline-start: 1rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-inline-start: auto;
flex-shrink: 0;
}
.status, .status,
.typing-status { .typing-status {
font-size: 0.875rem; font-size: 0.875rem;

View File

@ -3,7 +3,7 @@ import {
} from '../../index'; } from '../../index';
import type { import type {
ApiChat, ApiUser, ApiChatFolder, ApiError, ApiChat, ApiUser, ApiChatFolder, ApiError, ApiChatMember,
} from '../../../api/types'; } from '../../../api/types';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { NewChatMembersProgress, ChatCreationProgress, ManagementProgress } from '../../../types'; import { NewChatMembersProgress, ChatCreationProgress, ManagementProgress } from '../../../types';
@ -867,24 +867,31 @@ addActionHandler('updateChatAdmin', async (global, actions, payload) => {
return; return;
} }
const { adminMembers } = chatAfterUpdate.fullInfo; const { adminMembersById } = chatAfterUpdate.fullInfo;
const isDismissed = !Object.keys(adminRights).length; const isDismissed = !Object.keys(adminRights).length;
let newAdminMembersById: Record<string, ApiChatMember> | undefined;
if (adminMembersById) {
if (isDismissed) {
const { [userId]: remove, ...rest } = adminMembersById;
newAdminMembersById = rest;
} else {
newAdminMembersById = {
...adminMembersById,
[userId]: {
...adminMembersById[userId],
adminRights,
customTitle,
},
};
}
}
global = getGlobal(); global = getGlobal();
setGlobal(updateChat(global, chatId, { setGlobal(updateChat(global, chatId, {
fullInfo: { fullInfo: {
...chatAfterUpdate.fullInfo, ...chatAfterUpdate.fullInfo,
...(adminMembers && isDismissed && { ...(newAdminMembersById && { adminMembersById: newAdminMembersById }),
adminMembers: adminMembers.filter((m) => m.userId !== userId),
}),
...(adminMembers && !isDismissed && {
adminMembers: adminMembers.map((m) => (
m.userId === userId
? { ...m, adminRights, customTitle }
: m
)),
}),
}, },
})); }));
}); });

View File

@ -3,7 +3,7 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config'; import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config';
import { pick } from '../../../util/iteratees'; import { buildCollectionByKey, pick } from '../../../util/iteratees';
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications'; import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
import { import {
updateChat, updateChat,
@ -326,7 +326,7 @@ addActionHandler('apiUpdate', (global, actions, update) => {
fullInfo: { fullInfo: {
...targetChat.fullInfo, ...targetChat.fullInfo,
members, members,
adminMembers, adminMembersById: buildCollectionByKey(adminMembers, 'userId'),
}, },
}); });
} }

View File

@ -2,7 +2,7 @@ import type { GlobalState } from '../types';
import type { ApiChat, ApiChatMember, ApiPhoto } from '../../api/types'; import type { ApiChat, ApiChatMember, ApiPhoto } from '../../api/types';
import { ARCHIVED_FOLDER_ID } from '../../config'; import { ARCHIVED_FOLDER_ID } from '../../config';
import { areSortedArraysEqual, omit } from '../../util/iteratees'; import { areSortedArraysEqual, buildCollectionByKey, omit } from '../../util/iteratees';
import { selectChatListType } from '../selectors'; import { selectChatListType } from '../selectors';
export function replaceChatListIds( export function replaceChatListIds(
@ -242,6 +242,7 @@ export function addChatMembers(global: GlobalState, chat: ApiChat, membersToAdd:
fullInfo: { fullInfo: {
...chat.fullInfo, ...chat.fullInfo,
members: updatedMembers, members: updatedMembers,
adminMembersById: buildCollectionByKey(updatedMembers, 'userId'),
}, },
}); });
} }