fix(web): sync group member nicknames
This commit is contained in:
@@ -48,10 +48,15 @@ export function AppShell() {
|
|||||||
// and NewFriendsView's list (['friend-requests','all']) both refetch on invalidation. Listening
|
// and NewFriendsView's list (['friend-requests','all']) both refetch on invalidation. Listening
|
||||||
// here rather than in those views keeps the badge live no matter which page the user is on.
|
// here rather than in those views keeps the badge live no matter which page the user is on.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return subscribeTopicInfo((_topicName, info) => {
|
return subscribeTopicInfo((topicName, info) => {
|
||||||
if (info?.event?.startsWith('friend.request.')) {
|
if (info?.event?.startsWith('friend.request.')) {
|
||||||
queryClient.invalidateQueries({ queryKey: ['friend-requests'] });
|
queryClient.invalidateQueries({ queryKey: ['friend-requests'] });
|
||||||
}
|
}
|
||||||
|
if (info?.event === 'group.member.profile.updated' && topicName.startsWith('grp')) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['group-members', topicName] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['group-members-preview', topicName] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['group-my-profile', topicName] });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [queryClient]);
|
}, [queryClient]);
|
||||||
// "sys" has its own authoritative unread source in entry_badges (bootstrap.ts) -- excluded here
|
// "sys" has its own authoritative unread source in entry_badges (bootstrap.ts) -- excluded here
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from '@/lib/api/groups';
|
} from '@/lib/api/groups';
|
||||||
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
|
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
|
||||||
import { useEscapeKey } from '@/lib/hooks';
|
import { useEscapeKey } from '@/lib/hooks';
|
||||||
|
import { groupMemberDisplayName } from '@/lib/messages/group-member-name';
|
||||||
import { requestConfirm } from '@/stores/confirmDialog';
|
import { requestConfirm } from '@/stores/confirmDialog';
|
||||||
import { requestActionToken } from '@/stores/securityChallenge';
|
import { requestActionToken } from '@/stores/securityChallenge';
|
||||||
import { useSessionStore } from '@/stores/session';
|
import { useSessionStore } from '@/stores/session';
|
||||||
@@ -288,9 +289,9 @@ function MemberPickerList({
|
|||||||
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left hover:bg-surface-rail disabled:opacity-50"
|
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left hover:bg-surface-rail disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<span className={`grid h-9 w-9 flex-none place-items-center rounded-lg text-sm font-medium text-white ${avatarBgClass(m.user_id)}`}>
|
<span className={`grid h-9 w-9 flex-none place-items-center rounded-lg text-sm font-medium text-white ${avatarBgClass(m.user_id)}`}>
|
||||||
{avatarLetter(m.display_name)}
|
{avatarLetter(groupMemberDisplayName(m))}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate text-sm text-text-body">{m.display_name || m.user_id}</span>
|
<span className="truncate text-sm text-text-body">{groupMemberDisplayName(m)}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -370,9 +371,9 @@ function AdminsSubView({
|
|||||||
{admins.map((a) => (
|
{admins.map((a) => (
|
||||||
<div key={a.user_id} className="flex items-center gap-3 px-3 py-2">
|
<div key={a.user_id} className="flex items-center gap-3 px-3 py-2">
|
||||||
<span className={`grid h-9 w-9 flex-none place-items-center rounded-lg text-sm font-medium text-white ${avatarBgClass(a.user_id)}`}>
|
<span className={`grid h-9 w-9 flex-none place-items-center rounded-lg text-sm font-medium text-white ${avatarBgClass(a.user_id)}`}>
|
||||||
{avatarLetter(a.display_name)}
|
{avatarLetter(groupMemberDisplayName(a))}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex-1 truncate text-sm text-text-body">{a.display_name || a.user_id}</span>
|
<span className="flex-1 truncate text-sm text-text-body">{groupMemberDisplayName(a)}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeMutation.mutate(a.user_id)}
|
onClick={() => removeMutation.mutate(a.user_id)}
|
||||||
@@ -445,7 +446,8 @@ function MutesSubView({ topicId, muted, onClose }: { topicId: string; muted: Gro
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{muted.map((m) => {
|
{muted.map((m) => {
|
||||||
const name = memberByUserId.get(m.user_id)?.display_name || m.user_id;
|
const member = memberByUserId.get(m.user_id);
|
||||||
|
const name = member ? groupMemberDisplayName(member) : m.user_id;
|
||||||
return (
|
return (
|
||||||
<div key={m.user_id} className="flex items-center gap-3 px-3 py-2">
|
<div key={m.user_id} className="flex items-center gap-3 px-3 py-2">
|
||||||
<span className={`grid h-9 w-9 flex-none place-items-center rounded-lg text-sm font-medium text-white ${avatarBgClass(m.user_id)}`}>
|
<span className={`grid h-9 w-9 flex-none place-items-center rounded-lg text-sm font-medium text-white ${avatarBgClass(m.user_id)}`}>
|
||||||
@@ -500,7 +502,7 @@ function TransferOwnerSubView({ topicId, groupVersion, onClose }: { topicId: str
|
|||||||
cacheKey="eligible-owner"
|
cacheKey="eligible-owner"
|
||||||
opts={{ eligibleRole: 'owner' }}
|
opts={{ eligibleRole: 'owner' }}
|
||||||
onPick={async (m) => {
|
onPick={async (m) => {
|
||||||
if (!(await requestConfirm({ message: `确定将群主转让给 ${m.display_name || m.user_id} 吗?此操作不可撤销。`, danger: true }))) return;
|
if (!(await requestConfirm({ message: `确定将群主转让给 ${groupMemberDisplayName(m)} 吗?此操作不可撤销。`, danger: true }))) return;
|
||||||
const actionToken = await requestActionToken({ scene: 'group_owner_transfer', actionDescription: '转让群主' });
|
const actionToken = await requestActionToken({ scene: 'group_owner_transfer', actionDescription: '转让群主' });
|
||||||
if (!actionToken) return;
|
if (!actionToken) return;
|
||||||
transferMutation.mutate({ userId: m.user_id, actionToken });
|
transferMutation.mutate({ userId: m.user_id, actionToken });
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import type { MediaType } from '@/lib/api/messages';
|
import type { MediaType } from '@/lib/api/messages';
|
||||||
import { uploadFile } from '@/lib/api/upload';
|
import { uploadFile } from '@/lib/api/upload';
|
||||||
import { MAX_TITLE_LENGTH } from '@/lib/config';
|
import { MAX_TITLE_LENGTH } from '@/lib/config';
|
||||||
|
import { groupMemberDisplayName } from '@/lib/messages/group-member-name';
|
||||||
import { useTinode } from '@/lib/tinode/hooks';
|
import { useTinode } from '@/lib/tinode/hooks';
|
||||||
import { forgetTopic } from '@/lib/tinode/topics';
|
import { forgetTopic } from '@/lib/tinode/topics';
|
||||||
import { requestConfirm } from '@/stores/confirmDialog';
|
import { requestConfirm } from '@/stores/confirmDialog';
|
||||||
@@ -119,8 +120,7 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
|
|||||||
// GroupAdminModal's join-request badge, so this is a cache hit more often than not).
|
// GroupAdminModal's join-request badge, so this is a cache hit more often than not).
|
||||||
const summaryQuery = useQuery({ queryKey: ['group-summary', topicId], queryFn: () => getGroupSummary(topicId) });
|
const summaryQuery = useQuery({ queryKey: ['group-summary', topicId], queryFn: () => getGroupSummary(topicId) });
|
||||||
const membersQuery = useQuery({ queryKey: ['group-members-preview', topicId], queryFn: () => getGroupMembers(topicId, {}) });
|
const membersQuery = useQuery({ queryKey: ['group-members-preview', topicId], queryFn: () => getGroupMembers(topicId, {}) });
|
||||||
// `GET .../members` never returns `group_nickname` on any item (confirmed live by the old app) --
|
// The nickname row needs the per-user profile endpoint. Uses the
|
||||||
// only this per-user profile endpoint does, so the nickname row needs its own fetch. Uses the
|
|
||||||
// dedicated `members/me/profile` route, not getGroupMemberProfile(topic, myUserId) -- see that
|
// dedicated `members/me/profile` route, not getGroupMemberProfile(topic, myUserId) -- see that
|
||||||
// function's own warning for why the generic by-id path 400s on self-lookups.
|
// function's own warning for why the generic by-id path 400s on self-lookups.
|
||||||
const myProfileQuery = useQuery({
|
const myProfileQuery = useQuery({
|
||||||
@@ -168,6 +168,8 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
|
|||||||
mutationFn: (nickname: string) => updateMyGroupNickname(topicId, nickname, myProfileQuery.data?.version ?? 1),
|
mutationFn: (nickname: string) => updateMyGroupNickname(topicId, nickname, myProfileQuery.data?.version ?? 1),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['group-my-profile', topicId, myUserId] });
|
queryClient.invalidateQueries({ queryKey: ['group-my-profile', topicId, myUserId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['group-members-preview', topicId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['group-members', topicId] });
|
||||||
setNicknameEditing(false);
|
setNicknameEditing(false);
|
||||||
},
|
},
|
||||||
onError: (err) => showError(err, '昵称保存失败'),
|
onError: (err) => showError(err, '昵称保存失败'),
|
||||||
@@ -430,12 +432,12 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
|
|||||||
desktop's 40px (unconditional here before this fix). */}
|
desktop's 40px (unconditional here before this fix). */}
|
||||||
<Avatar
|
<Avatar
|
||||||
id={m.user_id}
|
id={m.user_id}
|
||||||
name={m.display_name}
|
name={groupMemberDisplayName(m)}
|
||||||
avatarFileId={m.avatar_file_id}
|
avatarFileId={m.avatar_file_id}
|
||||||
className="h-11 w-11 rounded-lg text-sm md:h-10 md:w-10"
|
className="h-11 w-11 rounded-lg text-sm md:h-10 md:w-10"
|
||||||
/>
|
/>
|
||||||
<RoleCorner role={m.role} />
|
<RoleCorner role={m.role} />
|
||||||
<span className="w-full truncate text-center text-[11px] text-text-secondary">{m.display_name || m.user_id}</span>
|
<span className="w-full truncate text-center text-[11px] text-text-secondary">{groupMemberDisplayName(m)}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{canInvite ? (
|
{canInvite ? (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getFriends } from '@/lib/api/friends';
|
|||||||
import { canRemoveMember, getAllGroupMembers, inviteGroupMembers, removeGroupMember, type GroupInvitationResult, type GroupRole } from '@/lib/api/groups';
|
import { canRemoveMember, getAllGroupMembers, inviteGroupMembers, removeGroupMember, type GroupInvitationResult, type GroupRole } from '@/lib/api/groups';
|
||||||
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
|
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
|
||||||
import { useEscapeKey } from '@/lib/hooks';
|
import { useEscapeKey } from '@/lib/hooks';
|
||||||
|
import { groupMemberDisplayName } from '@/lib/messages/group-member-name';
|
||||||
import { requestConfirm } from '@/stores/confirmDialog';
|
import { requestConfirm } from '@/stores/confirmDialog';
|
||||||
import { openUserCard } from '@/stores/userCard';
|
import { openUserCard } from '@/stores/userCard';
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ export function GroupMembersModal({ topicId, canInvite, myUserId, myRole, initia
|
|||||||
|
|
||||||
const membersQuery = useQuery({ queryKey: ['group-members', topicId], queryFn: () => getAllGroupMembers(topicId) });
|
const membersQuery = useQuery({ queryKey: ['group-members', topicId], queryFn: () => getAllGroupMembers(topicId) });
|
||||||
const members = membersQuery.data || [];
|
const members = membersQuery.data || [];
|
||||||
const filtered = members.filter((m) => !query.trim() || (m.display_name || m.user_id).toLowerCase().includes(query.trim().toLowerCase()));
|
const filtered = members.filter((m) => !query.trim() || groupMemberDisplayName(m).toLowerCase().includes(query.trim().toLowerCase()));
|
||||||
|
|
||||||
const friendsQuery = useQuery({
|
const friendsQuery = useQuery({
|
||||||
queryKey: ['friends'],
|
queryKey: ['friends'],
|
||||||
@@ -198,9 +199,9 @@ export function GroupMembersModal({ topicId, canInvite, myUserId, myRole, initia
|
|||||||
<div key={m.user_id} className="flex w-full items-center gap-3 rounded-lg px-3 py-2">
|
<div key={m.user_id} className="flex w-full items-center gap-3 rounded-lg px-3 py-2">
|
||||||
<button type="button" onClick={() => openUserCard(m.user_id)} className="flex min-w-0 flex-1 items-center gap-3 text-left">
|
<button type="button" onClick={() => openUserCard(m.user_id)} className="flex min-w-0 flex-1 items-center gap-3 text-left">
|
||||||
<span className={`grid h-9 w-9 flex-none place-items-center rounded-lg text-sm font-medium text-white ${avatarBgClass(m.user_id)}`}>
|
<span className={`grid h-9 w-9 flex-none place-items-center rounded-lg text-sm font-medium text-white ${avatarBgClass(m.user_id)}`}>
|
||||||
{avatarLetter(m.display_name)}
|
{avatarLetter(groupMemberDisplayName(m))}
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1 truncate text-sm text-text-body">{m.display_name || m.user_id}</span>
|
<span className="min-w-0 flex-1 truncate text-sm text-text-body">{groupMemberDisplayName(m)}</span>
|
||||||
</button>
|
</button>
|
||||||
<RoleTag role={m.role} />
|
<RoleTag role={m.role} />
|
||||||
{canRemoveMember(myUserId, myRole, m) ? (
|
{canRemoveMember(myUserId, myRole, m) ? (
|
||||||
@@ -208,7 +209,7 @@ export function GroupMembersModal({ topicId, canInvite, myUserId, myRole, initia
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={removingIds.has(m.user_id)}
|
disabled={removingIds.has(m.user_id)}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const label = m.display_name || m.user_id;
|
const label = groupMemberDisplayName(m);
|
||||||
if (await requestConfirm({ message: `确定将"${label}"移出群聊吗?`, danger: true })) {
|
if (await requestConfirm({ message: `确定将"${label}"移出群聊吗?`, danger: true })) {
|
||||||
setRemovingIds((prev) => new Set(prev).add(m.user_id));
|
setRemovingIds((prev) => new Set(prev).add(m.user_id));
|
||||||
removeMutation.mutate(m.user_id, {
|
removeMutation.mutate(m.user_id, {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { flatTextContentOf, isRecalled } from '@/lib/messages/dispatch';
|
|||||||
import { buildMentionedContent, detectMentionTrigger, type PendingMention } from '@/lib/messages/mentions';
|
import { buildMentionedContent, detectMentionTrigger, type PendingMention } from '@/lib/messages/mentions';
|
||||||
import { buildReplyInfo, replySnapshotText } from '@/lib/messages/reply';
|
import { buildReplyInfo, replySnapshotText } from '@/lib/messages/reply';
|
||||||
import { groupSenderLabel } from '@/lib/messages/sender-name';
|
import { groupSenderLabel } from '@/lib/messages/sender-name';
|
||||||
|
import { groupMemberDisplayName } from '@/lib/messages/group-member-name';
|
||||||
import { sendFile, sendImage, sendVoice } from '@/lib/tinode/attachments';
|
import { sendFile, sendImage, sendVoice } from '@/lib/tinode/attachments';
|
||||||
import { isTopicGone } from '@/lib/tinode/errors';
|
import { isTopicGone } from '@/lib/tinode/errors';
|
||||||
import { useChatList, useTinode, useTopicMessages } from '@/lib/tinode/hooks';
|
import { useChatList, useTinode, useTopicMessages } from '@/lib/tinode/hooks';
|
||||||
@@ -602,7 +603,8 @@ export function ConversationView() {
|
|||||||
const quotedSenderLabel = (fromUserId: string | undefined) => {
|
const quotedSenderLabel = (fromUserId: string | undefined) => {
|
||||||
if (myUserId && fromUserId === myUserId) return '我';
|
if (myUserId && fromUserId === myUserId) return '我';
|
||||||
if (isGroup && fromUserId) {
|
if (isGroup && fromUserId) {
|
||||||
return groupSenderLabel(memberByUserId.get(fromUserId)?.display_name, staleSenderById.get(fromUserId)?.display_name);
|
const member = memberByUserId.get(fromUserId);
|
||||||
|
return groupSenderLabel(member && groupMemberDisplayName(member), staleSenderById.get(fromUserId)?.display_name);
|
||||||
}
|
}
|
||||||
return peerName;
|
return peerName;
|
||||||
};
|
};
|
||||||
@@ -1075,7 +1077,7 @@ export function ConversationView() {
|
|||||||
mine={mine}
|
mine={mine}
|
||||||
isGroup={isGroup}
|
isGroup={isGroup}
|
||||||
peerName={peerName}
|
peerName={peerName}
|
||||||
senderName={sender?.display_name || staleSender?.display_name}
|
senderName={sender ? groupMemberDisplayName(sender) : staleSender?.display_name}
|
||||||
senderRole={sender?.role}
|
senderRole={sender?.role}
|
||||||
avatarFileId={rowAvatarFileId}
|
avatarFileId={rowAvatarFileId}
|
||||||
selectMode={selectMode}
|
selectMode={selectMode}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useAuthedBlobUrl, useDebouncedValue, useEscapeKey } from '@/lib/hooks';
|
|||||||
import { contactCardContentOf, mobileAttachmentContentOf } from '@/lib/messages/dispatch';
|
import { contactCardContentOf, mobileAttachmentContentOf } from '@/lib/messages/dispatch';
|
||||||
import { renderDrafty } from '@/lib/messages/drafty';
|
import { renderDrafty } from '@/lib/messages/drafty';
|
||||||
import { groupSenderLabel } from '@/lib/messages/sender-name';
|
import { groupSenderLabel } from '@/lib/messages/sender-name';
|
||||||
|
import { groupMemberDisplayName } from '@/lib/messages/group-member-name';
|
||||||
import { formatEventTimestamp } from '@/lib/messages/time';
|
import { formatEventTimestamp } from '@/lib/messages/time';
|
||||||
import { requestConfirm } from '@/stores/confirmDialog';
|
import { requestConfirm } from '@/stores/confirmDialog';
|
||||||
import { notify } from '@/stores/globalToast';
|
import { notify } from '@/stores/globalToast';
|
||||||
@@ -199,7 +200,10 @@ export function HistorySearchModal({ topicId, initialTab = 'text', onClose, isGr
|
|||||||
const senderLabel = (from: string): string => {
|
const senderLabel = (from: string): string => {
|
||||||
const full = normalizeSender(from);
|
const full = normalizeSender(from);
|
||||||
if (myUserId && full === myUserId) return '我';
|
if (myUserId && full === myUserId) return '我';
|
||||||
if (isGroup) return groupSenderLabel(membersQuery.data?.find((m) => m.user_id === full)?.display_name);
|
if (isGroup) {
|
||||||
|
const member = membersQuery.data?.find((m) => m.user_id === full);
|
||||||
|
return groupSenderLabel(member && groupMemberDisplayName(member));
|
||||||
|
}
|
||||||
return peerName;
|
return peerName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function MentionPicker({ topicId, query, onPick }: MentionPickerProps) {
|
|||||||
// who have no idea what the sender's personal remark for this person means. `nickname`
|
// who have no idea what the sender's personal remark for this person means. `nickname`
|
||||||
// is that member's own real public name, unaffected by anyone's remark, so it stays
|
// is that member's own real public name, unaffected by anyone's remark, so it stays
|
||||||
// correct for whoever else ends up reading the mention.
|
// correct for whoever else ends up reading the mention.
|
||||||
const label = m.nickname || m.display_name || m.user_id;
|
const label = m.group_nickname || m.nickname || m.display_name || m.user_id;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={m.user_id}
|
key={m.user_id}
|
||||||
|
|||||||
@@ -159,6 +159,8 @@ export interface GroupMember {
|
|||||||
* OTHER member then saw with no idea who it meant. Use `nickname` (below) instead for that case.
|
* OTHER member then saw with no idea who it meant. Use `nickname` (below) instead for that case.
|
||||||
*/
|
*/
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
|
/** Name chosen by this member for this group. */
|
||||||
|
group_nickname?: string;
|
||||||
/** The member's own real public profile nickname -- same backend response, but from
|
/** The member's own real public profile nickname -- same backend response, but from
|
||||||
* copyUserNickname(), which reads straight off their public profile and is NEVER shadowed by the
|
* copyUserNickname(), which reads straight off their public profile and is NEVER shadowed by the
|
||||||
* viewer's own remark the way `display_name` above is. This is what stays correct for everyone
|
* viewer's own remark the way `display_name` above is. This is what stays correct for everyone
|
||||||
@@ -176,7 +178,6 @@ export interface GroupMember {
|
|||||||
acs?: { want?: string; given?: string; mode?: string };
|
acs?: { want?: string; given?: string; mode?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `GET .../members` never returns `group_nickname` on any item. */
|
|
||||||
export function getGroupMembers(
|
export function getGroupMembers(
|
||||||
topic: string,
|
topic: string,
|
||||||
opts: { query?: string; cursor?: string; role?: GroupRole; eligibleRole?: 'admin' | 'owner'; mentionable?: boolean; muteEligible?: boolean } = {},
|
opts: { query?: string; cursor?: string; role?: GroupRole; eligibleRole?: 'admin' | 'owner'; mentionable?: boolean; muteEligible?: boolean } = {},
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
type GroupMemberName = {
|
||||||
|
group_nickname?: string;
|
||||||
|
display_name?: string;
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function groupMemberDisplayName(member: GroupMemberName): string {
|
||||||
|
return member.group_nickname?.trim() || member.display_name?.trim() || member.user_id;
|
||||||
|
}
|
||||||
@@ -185,7 +185,7 @@ export function subscribeAnyTopicChange(callback: () => void): () => void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RawTopicInfo {
|
export interface RawTopicInfo {
|
||||||
what: 'read' | 'recv' | 'kp' | 'call' | 'im';
|
what: 'read' | 'recv' | 'kp' | 'call' | 'im' | 'group';
|
||||||
seq?: number;
|
seq?: number;
|
||||||
from?: string;
|
from?: string;
|
||||||
event?: string;
|
event?: string;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { groupMemberDisplayName } from '../src/lib/messages/group-member-name.ts';
|
||||||
|
|
||||||
|
test('group nickname wins over a viewer remark or public nickname', () => {
|
||||||
|
assert.equal(groupMemberDisplayName({ user_id: 'usrA', group_nickname: '群里小王', display_name: '好友备注' }), '群里小王');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty group nickname falls back to the usual member name', () => {
|
||||||
|
assert.equal(groupMemberDisplayName({ user_id: 'usrA', group_nickname: ' ', display_name: '公开昵称' }), '公开昵称');
|
||||||
|
assert.equal(groupMemberDisplayName({ user_id: 'usrA' }), 'usrA');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user