From cbce577daba911481b3b4448da702a6063cd06b2 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:29:01 +0200 Subject: [PATCH] Support member tags (#6749) --- src/api/gramjs/apiBuilders/chats.ts | 2 +- src/api/gramjs/apiBuilders/messages.ts | 1 + src/api/gramjs/methods/chats.ts | 6 +- src/api/gramjs/methods/messages.ts | 18 + src/api/gramjs/updates/mtpUpdateHandler.ts | 7 + src/api/types/chats.ts | 4 +- src/api/types/messages.ts | 1 + src/api/types/updates.ts | 10 +- src/assets/font-icons/user-tag.svg | 1 + src/assets/localization/fallback.strings | 19 +- src/bundles/extra.ts | 2 + .../calls/group/GroupCallParticipantVideo.tsx | 2 +- src/components/common/BadgeButton.module.scss | 11 +- src/components/common/BadgeButton.tsx | 16 +- .../common/PreviewBlock.module.scss | 132 ++++ src/components/common/PreviewBlock.tsx | 241 +++++++ src/components/common/PrivateChatInfo.tsx | 27 +- src/components/common/RankBadge.tsx | 58 ++ src/components/common/helpers/chatMember.ts | 5 + src/components/common/profile/ChatExtra.tsx | 2 +- .../main/PermissionCheckboxList.tsx | 11 + .../middle/MessageListAccountInfo.tsx | 1 + src/components/middle/message/Game.tsx | 2 +- src/components/middle/message/Invoice.tsx | 1 + .../middle/message/LastEditTimeMenuItem.tsx | 2 +- src/components/middle/message/Location.tsx | 2 +- src/components/middle/message/Message.tsx | 35 +- .../middle/message/MessageContextMenu.tsx | 4 +- .../middle/message/ReadTimeMenuItem.tsx | 2 +- .../middle/message/SimilarChannels.tsx | 2 +- .../middle/message/_message-content.scss | 27 +- src/components/modals/ModalContainer.tsx | 8 +- .../modals/rank/EditRankModal.async.tsx | 14 + .../modals/rank/EditRankModal.module.scss | 31 + src/components/modals/rank/EditRankModal.tsx | 177 +++++ .../modals/rank/RankModal.async.tsx | 14 + .../modals/rank/RankModal.module.scss | 102 +++ src/components/modals/rank/RankModal.tsx | 190 +++++ src/components/payment/Checkout.tsx | 1 + src/components/right/Profile.tsx | 65 +- .../management/ManageGroupAdminRights.tsx | 79 ++- src/components/story/Story.tsx | 2 +- src/components/ui/InputText.tsx | 2 +- src/components/ui/ListItem.scss | 1 + src/components/ui/placeholder/Skeleton.tsx | 4 +- src/global/actions/api/chats.ts | 32 +- src/global/actions/apiUpdaters/chats.ts | 5 + src/global/actions/ui/users.ts | 20 + src/global/reducers/chats.ts | 49 ++ src/global/selectors/chats.ts | 40 +- src/global/types/actions.ts | 23 +- src/global/types/tabState.ts | 14 + src/lib/gramjs/tl/apiTl.ts | 1 + src/lib/gramjs/tl/static/api.json | 1 + src/styles/icons.css | 649 +++++++++--------- src/styles/icons.scss | 643 ++++++++--------- src/styles/icons.woff | Bin 40900 -> 41128 bytes src/styles/icons.woff2 | Bin 34176 -> 34172 bytes src/types/icons/font.ts | 641 ++++++++--------- src/types/language.d.ts | 31 + 60 files changed, 2416 insertions(+), 1077 deletions(-) create mode 100644 src/assets/font-icons/user-tag.svg create mode 100644 src/components/common/PreviewBlock.module.scss create mode 100644 src/components/common/PreviewBlock.tsx create mode 100644 src/components/common/RankBadge.tsx create mode 100644 src/components/common/helpers/chatMember.ts create mode 100644 src/components/modals/rank/EditRankModal.async.tsx create mode 100644 src/components/modals/rank/EditRankModal.module.scss create mode 100644 src/components/modals/rank/EditRankModal.tsx create mode 100644 src/components/modals/rank/RankModal.async.tsx create mode 100644 src/components/modals/rank/RankModal.module.scss create mode 100644 src/components/modals/rank/RankModal.tsx diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 9201bc459..2b5c9d794 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -345,6 +345,7 @@ export function buildChatMember( return { userId, + rank: 'rank' in member ? member.rank : undefined, inviterId: 'inviterId' in member && member.inviterId ? buildApiPeerId(member.inviterId, 'user') : undefined, @@ -355,7 +356,6 @@ export function buildChatMember( : undefined, bannedRights: 'bannedRights' in member ? omitVirtualClassFields(member.bannedRights) : undefined, adminRights: 'adminRights' in member ? omitVirtualClassFields(member.adminRights) : undefined, - customTitle: 'rank' in member ? member.rank : undefined, isViaRequest: 'viaRequest' in member ? member.viaRequest : undefined, ...((member instanceof GramJs.ChannelParticipantAdmin || member instanceof GramJs.ChatParticipantAdmin) && { isAdmin: true, diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 6ca5ec8df..d0d3e578f 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -297,6 +297,7 @@ export function buildApiMessageWithChatId( paidMessageStars: toJSNumber(mtpMessage.paidMessageStars), restrictionReasons, summaryLanguageCode: mtpMessage.summaryFromLanguage, + fromRank: mtpMessage.fromRank, }; } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index a122f6331..21f8d7dee 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -1411,8 +1411,8 @@ export function updateChatMemberBannedRights({ } export function updateChatAdmin({ - chat, user, adminRights, customTitle = DEFAULT_PRIMITIVES.STRING, -}: { chat: ApiChat; user: ApiUser; adminRights: ApiChatAdminRights; customTitle?: string }) { + chat, user, adminRights, rank, +}: { chat: ApiChat; user: ApiUser; adminRights: ApiChatAdminRights; rank?: string }) { const channel = buildInputChannel(chat.id, chat.accessHash); const userId = buildInputUser(user.id, user.accessHash); @@ -1420,7 +1420,7 @@ export function updateChatAdmin({ channel, userId, adminRights: buildChatAdminRights(adminRights), - rank: customTitle, + rank, }), { shouldReturnTrue: true, }); diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 2c16174cd..58d272d93 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1112,6 +1112,24 @@ export async function deleteParticipantHistory({ } } +export function editChatParticipantRank({ + chat, peer, rank, +}: { + chat: ApiChat; + peer: ApiPeer; + rank: string; +}) { + const participant = buildInputPeer(peer.id, peer.accessHash); + + return invokeRequest(new GramJs.messages.EditChatParticipantRank({ + peer: buildInputPeer(chat.id, chat.accessHash), + participant, + rank, + }), { + shouldReturnTrue: true, + }); +} + export function deleteScheduledMessages({ chat, messageIds, }: { diff --git a/src/api/gramjs/updates/mtpUpdateHandler.ts b/src/api/gramjs/updates/mtpUpdateHandler.ts index 0f952b914..3f3c72448 100644 --- a/src/api/gramjs/updates/mtpUpdateHandler.ts +++ b/src/api/gramjs/updates/mtpUpdateHandler.ts @@ -654,6 +654,13 @@ export function updater(update: Update) { id: buildApiPeerId(update.chatId, 'chat'), deletedMemberId: buildApiPeerId(update.userId, 'user'), }); + } else if (update instanceof GramJs.UpdateChatParticipantRank) { + sendApiUpdate({ + '@type': 'updateChatParticipantRank', + id: buildApiPeerId(update.chatId, 'chat'), + userId: buildApiPeerId(update.userId, 'user'), + rank: update.rank, + }); } else if ( update instanceof GramJs.UpdatePinnedMessages || update instanceof GramJs.UpdatePinnedChannelMessages diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 4d55efa84..04f37a7d9 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -160,13 +160,13 @@ export interface ApiChatFullInfo { export interface ApiChatMember { userId: string; + rank?: string; inviterId?: string; joinedDate?: number; kickedByUserId?: string; promotedByUserId?: string; bannedRights?: ApiChatBannedRights; adminRights?: ApiChatAdminRights; - customTitle?: string; isAdmin?: true; isOwner?: true; isViaRequest?: true; @@ -188,6 +188,7 @@ export interface ApiChatAdminRights { editStories?: true; deleteStories?: true; manageDirectMessages?: true; + manageRanks?: true; } export interface ApiChatBannedRights { @@ -211,6 +212,7 @@ export interface ApiChatBannedRights { sendVoices?: true; sendDocs?: true; sendPlain?: true; + editRank?: true; untilDate?: number; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index c3732b16d..dc5e30e76 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -698,6 +698,7 @@ export interface ApiMessage { paidMessageStars?: number; restrictionReasons?: ApiRestrictionReason[]; summaryLanguageCode?: string; + fromRank?: string; isTypingDraft?: boolean; // Local field } diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 591723a37..68e826b21 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -179,6 +179,13 @@ export type ApiUpdateChatMembers = { deletedMemberId?: string; }; +export type ApiUpdateChatParticipantRank = { + '@type': 'updateChatParticipantRank'; + id: string; + userId: string; + rank: string; +}; + export type ApiUpdatePinnedChatIds = { '@type': 'updatePinnedChatIds'; ids?: string[]; @@ -924,7 +931,8 @@ export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | ApiUpdateChat | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds | - ApiUpdateChatMembers | ApiUpdateChatJoin | ApiUpdateChatLeave | ApiUpdateChatPinned | ApiUpdatePinnedMessageIds | + ApiUpdateChatMembers | ApiUpdateChatParticipantRank | ApiUpdateChatJoin | ApiUpdateChatLeave + | ApiUpdateChatPinned | ApiUpdatePinnedMessageIds | ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders | ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdatePasskeyOption | ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory | diff --git a/src/assets/font-icons/user-tag.svg b/src/assets/font-icons/user-tag.svg new file mode 100644 index 000000000..f169a5a08 --- /dev/null +++ b/src/assets/font-icons/user-tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 172201b03..6a57949bb 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -130,6 +130,7 @@ "UserRestrictionsNoChangeInfo" = "can't change Info"; "UserRestrictionsInviteUsers" = "Add Users"; "UserRestrictionsPinMessages" = "Pin Messages"; +"UserRestrictionsEditRank" = "Edit Own Tags"; "ChatPermissionNotAvailable" = "This permission is not available in public groups."; "StatsMessageInteractionsTitle" = "INTERACTIONS"; "StatsGroupGrowthTitle" = "GROWTH"; @@ -541,6 +542,7 @@ "PrivacyExceptions" = "Exceptions"; "AlwaysAllow" = "Always Allow"; "EditAdminAddUsers" = "Add Users"; +"EditAdminEditRank" = "Edit Member Tags"; "NeverAllow" = "Never Allow"; "AlwaysAllowPlaceholder" = "Always allow..."; "NeverAllowPlaceholder" = "Never allow..."; @@ -966,7 +968,7 @@ "StartVoipChatPermission" = "Manage Video Chats"; "EditAdminSendAnonymously" = "Remain Anonymous"; "ChannelEditAdminCannotEdit" = "You can't edit the rights of this admin."; -"EditAdminRank" = "Custom title"; +"EditAdminRank" = "Member tag"; "EditAdminRemoveAdmin" = "Dismiss Admin"; "EditAdminTransferChannelOwnership" = "Transfer Channel Ownership"; "EditAdminTransferGroupOwnership" = "Transfer Group Ownership"; @@ -2732,4 +2734,19 @@ "GiftPreviewToggleRegularModels" = "View Primary Models >"; "AriaGiftPreviewPlay" = "Play random previews"; "AriaGiftPreviewStop" = "Pause random previews"; +"RankModalEdit" = "Edit Tag"; +"RankModalEditMy" = "Edit My Tag"; +"MemberContextEditRank" = "Edit Tag"; +"RankMemberTag" = "Member Tag"; +"RankAdminTag" = "Admin Tag"; +"RankOwnerTag" = "Owner Tag"; +"RankModalMemberTagTitle" = "Member Tag"; +"RankModalAdminTagTitle" = "Admin Tag"; +"RankModalOwnerTagTitle" = "Owner Tag"; +"RankModalMemberText" = "This gray tag {tag} is **{author}**'s member tag in **{group}**."; +"RankModalAdminText" = "This green tag {tag} is **{author}**'s admin tag in **{group}**."; +"RankModalOwnerText" = "This purple tag {tag} is **{author}**'s owner tag in **{group}**."; +"RankEditSave" = "Edit Tag"; +"RankEditTextOwn" = "Share your role, title or how you're known in this group. Your tag is visible to all members."; +"RankEditText" = "Add a short tag next to {user}'s name."; "MenuAddCaption" = "Add Caption"; diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 4c608d2f5..37a501d03 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -105,6 +105,8 @@ export { default as OneTimeMediaModal } from '../components/modals/oneTimeMedia/ export { default as WebAppsCloseConfirmationModal } from '../components/main/WebAppsCloseConfirmationModal'; export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal'; export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal'; +export { default as EditRankModal } from '../components/modals/rank/EditRankModal'; +export { default as RankModal } from '../components/modals/rank/RankModal'; export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal'; export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal'; export { default as LeaveGroupModal } from '../components/modals/leaveGroup/LeaveGroupModal'; diff --git a/src/components/calls/group/GroupCallParticipantVideo.tsx b/src/components/calls/group/GroupCallParticipantVideo.tsx index 8e5cd179c..704c772b1 100644 --- a/src/components/calls/group/GroupCallParticipantVideo.tsx +++ b/src/components/calls/group/GroupCallParticipantVideo.tsx @@ -254,7 +254,7 @@ const GroupCallParticipantVideo: FC = ({ )} > {isLoading && ( - + )} {stream && (