feat: add friend nickname support in everywhere

pull/90/head
moonrailgun 2 years ago
parent 36608ed8f3
commit 20c16adeec

@ -1,8 +1,9 @@
import { isValidStr } from '..'; import { getReduxStore, isValidStr } from '..';
import { getCachedConverseInfo, getCachedUserInfo } from '../cache/cache'; import { getCachedConverseInfo, getCachedUserInfo } from '../cache/cache';
import { t } from '../i18n'; import { t } from '../i18n';
import type { ChatConverseInfo } from '../model/converse'; import type { ChatConverseInfo } from '../model/converse';
import { appendUserDMConverse } from '../model/user'; import { appendUserDMConverse } from '../model/user';
import type { FriendInfo } from '../redux/slices/user';
/** /**
* *
@ -26,6 +27,22 @@ export async function ensureDMConverse(
return converse; return converse;
} }
export function buildFriendNicknameMap(
friends: FriendInfo[]
): Record<string, string> {
const friendNicknameMap: Record<string, string> = friends.reduce(
(prev, curr) => {
return {
...prev,
[curr.id]: curr.nickname,
};
},
{}
);
return friendNicknameMap;
}
/** /**
* *
* @param userId ID() * @param userId ID()
@ -40,16 +57,26 @@ export async function getDMConverseName(
} }
const otherConverseMembers = converse.members.filter((m) => m !== userId); // 成员Id const otherConverseMembers = converse.members.filter((m) => m !== userId); // 成员Id
const len = otherConverseMembers.length;
const otherMembersInfo = await Promise.all( const otherMembersInfo = await Promise.all(
otherConverseMembers.map((memberId) => getCachedUserInfo(memberId)) otherConverseMembers.map((memberId) => getCachedUserInfo(memberId))
); );
const friends = getReduxStore().getState().user.friends;
const friendNicknameMap = buildFriendNicknameMap(friends);
const memberNicknames = otherMembersInfo.map((m) => {
if (friendNicknameMap[m._id]) {
return friendNicknameMap[m._id];
}
return m.nickname ?? '';
});
const len = memberNicknames.length;
if (len === 1) { if (len === 1) {
return otherMembersInfo[0]?.nickname ?? ''; return memberNicknames[0] ?? '';
} else if (len === 2) { } else if (len === 2) {
return `${otherMembersInfo[0]?.nickname}, ${otherMembersInfo[1]?.nickname}`; return `${memberNicknames[0]}, ${memberNicknames[1]}`;
} else { } else {
return `${otherMembersInfo[0]?.nickname}, ${otherMembersInfo[1]?.nickname} ...`; return `${memberNicknames[0]}, ${memberNicknames[1]} ...`;
} }
} }

@ -192,6 +192,7 @@ export { useDMConverseList } from './redux/hooks/useConverse';
export { useConverseAck } from './redux/hooks/useConverseAck'; export { useConverseAck } from './redux/hooks/useConverseAck';
export { useConverseMessage } from './redux/hooks/useConverseMessage'; export { useConverseMessage } from './redux/hooks/useConverseMessage';
export { useDMConverseName } from './redux/hooks/useDMConverseName'; export { useDMConverseName } from './redux/hooks/useDMConverseName';
export { useFriendNickname } from './redux/hooks/useFriendNickname';
export { export {
useGroupInfo, useGroupInfo,
useGroupMemberIds, useGroupMemberIds,
@ -219,7 +220,7 @@ export {
} from './redux/slices'; } from './redux/slices';
export type { ChatConverseState } from './redux/slices/chat'; export type { ChatConverseState } from './redux/slices/chat';
export { setupRedux } from './redux/setup'; export { setupRedux } from './redux/setup';
export { reduxStore, ReduxProvider } from './redux/store'; export { getReduxStore, ReduxProvider } from './redux/store';
export type { AppStore, AppState, AppDispatch } from './redux/store'; export type { AppStore, AppState, AppDispatch } from './redux/store';
// store // store

@ -1,17 +1,19 @@
import { getDMConverseName } from '../../helper/converse-helper'; import { getDMConverseName } from '../../helper/converse-helper';
import { isValidStr, useAsync } from '../../index'; import { isValidStr, useAppSelector, useAsync } from '../../index';
import type { ChatConverseState } from '../slices/chat'; import type { ChatConverseState } from '../slices/chat';
import { useUserId } from './useUserInfo'; import { useUserId } from './useUserInfo';
import type { FriendInfo } from '../slices/user';
export function useDMConverseName(converse: ChatConverseState) { export function useDMConverseName(converse: ChatConverseState) {
const userId = useUserId(); const userId = useUserId();
const friends: FriendInfo[] = useAppSelector((state) => state.user.friends);
const { value: name = '' } = useAsync(async () => { const { value: name = '' } = useAsync(async () => {
if (!isValidStr(userId)) { if (!isValidStr(userId)) {
return ''; return '';
} }
return getDMConverseName(userId, converse); return getDMConverseName(userId, converse);
}, [userId, converse.name, converse.members.join(',')]); }, [userId, converse.name, converse.members.join(','), friends]);
return name; return name;
} }

@ -0,0 +1,33 @@
import { useMemo } from 'react';
import { buildFriendNicknameMap } from '../../helper/converse-helper';
import { isValidStr } from '../../utils/string-helper';
import { useAppSelector } from './useAppSelector';
/**
*
*
*
* @param userId id
*/
export function useFriendNickname(userId: string): string | null {
const nickname = useAppSelector(
(state) => state.user.friends.find((f) => f.id === userId)?.nickname
);
if (isValidStr(nickname)) {
return nickname;
}
return null;
}
export function useFriendNicknameMap(): Record<string, string> {
const friends = useAppSelector((state) => state.user.friends);
const friendNicknameMap = useMemo(
() => buildFriendNicknameMap(friends),
[friends]
);
return friendNicknameMap;
}

@ -14,8 +14,11 @@ function createStore() {
return store; return store;
} }
export const reduxStore = createStore(); const reduxStore = createStore();
export function getReduxStore() {
return reduxStore;
}
export type AppStore = ReturnType<typeof createStore>; export type AppStore = ReturnType<typeof createStore>;
export type AppState = ReturnType<AppStore['getState']>; export type AppState = ReturnType<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch']; export type AppDispatch = AppStore['dispatch'];

@ -3,7 +3,7 @@ import { Avatar } from 'tailchat-design';
import _isEmpty from 'lodash/isEmpty'; import _isEmpty from 'lodash/isEmpty';
import { Popover, PopoverProps, Skeleton, Space } from 'antd'; import { Popover, PopoverProps, Skeleton, Space } from 'antd';
import { useCachedUserInfo, useCachedOnlineStatus } from 'tailchat-shared'; import { useCachedUserInfo, useCachedOnlineStatus } from 'tailchat-shared';
import clsx from 'clsx'; import { UserName } from './UserName';
interface UserListItemProps { interface UserListItemProps {
userId: string; userId: string;
@ -26,24 +26,24 @@ export const UserListItem: React.FC<UserListItemProps> = React.memo((props) => {
active={true} active={true}
> >
<div className="mr-2"> <div className="mr-2">
<Popover content={props.popover} placement="left" trigger="click"> {props.popover ? (
<Avatar <Popover content={props.popover} placement="left" trigger="click">
className={clsx({ <Avatar
'cursor-pointer': !!props.popover, className="cursor-pointer"
})} src={userInfo.avatar}
src={userInfo.avatar} name={userName}
name={userName} isOnline={isOnline}
isOnline={isOnline} />
/> </Popover>
</Popover> ) : (
<Avatar src={userInfo.avatar} name={userName} isOnline={isOnline} />
)}
</div> </div>
<div className="flex-1 text-gray-900 dark:text-white"> <div className="flex-1 text-gray-900 dark:text-white">
<span>{userName}</span> <UserName
{!hideDiscriminator && ( userId={props.userId}
<span className="text-gray-500 dark:text-gray-300 opacity-0 group-hover:opacity-100"> showDiscriminator={!hideDiscriminator}
#{userInfo.discriminator} />
</span>
)}
</div> </div>
<Space>{actions}</Space> <Space>{actions}</Space>
</Skeleton> </Skeleton>

@ -1,20 +1,66 @@
import React from 'react'; import React from 'react';
import { useCachedUserInfo } from 'tailchat-shared'; import { useCachedUserInfo, useFriendNickname } from 'tailchat-shared';
interface UserNameProps { interface UserNameProps {
userId: string; userId: string;
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
showDiscriminator?: boolean;
} }
/**
* UserName, redux
*/
export const UserNamePure: React.FC<UserNameProps> = React.memo((props) => {
const { userId, showDiscriminator, className, style } = props;
const cachedUserInfo = useCachedUserInfo(userId);
return (
<span className={className} style={style}>
{cachedUserInfo.nickname ?? <span>&nbsp;</span>}
{showDiscriminator && (
<UserNameDiscriminator discriminator={cachedUserInfo.discriminator} />
)}
</span>
);
});
UserNamePure.displayName = 'UserNamePure';
/**
* patch UserName
*/
export const UserName: React.FC<UserNameProps> = React.memo((props) => { export const UserName: React.FC<UserNameProps> = React.memo((props) => {
const { userId, className, style } = props; const { userId, showDiscriminator, className, style } = props;
const cachedUserInfo = useCachedUserInfo(userId); const cachedUserInfo = useCachedUserInfo(userId);
const friendNickname = useFriendNickname(userId);
return ( return (
<span className={className} style={style}> <span className={className} style={style}>
{cachedUserInfo.nickname} {friendNickname ? (
<>
{friendNickname}
<span className="opacity-60">({cachedUserInfo.nickname})</span>
</>
) : (
cachedUserInfo.nickname ?? <span>&nbsp;</span>
)}
{showDiscriminator && (
<UserNameDiscriminator discriminator={cachedUserInfo.discriminator} />
)}
</span> </span>
); );
}); });
UserName.displayName = 'UserName'; UserName.displayName = 'UserName';
const UserNameDiscriminator: React.FC<{ discriminator: string }> = React.memo(
({ discriminator }) => {
return (
<span className="text-gray-500 dark:text-gray-300 opacity-0 group-hover:opacity-100">
#{discriminator}
</span>
);
}
);
UserNameDiscriminator.displayName = 'UserNameDiscriminator';

@ -1,7 +1,7 @@
import { fetchImagePrimaryColor } from '@/utils/image-helper'; import { fetchImagePrimaryColor } from '@/utils/image-helper';
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import { AvatarWithPreview, getTextColorHex } from 'tailchat-design'; import { AvatarWithPreview, getTextColorHex } from 'tailchat-design';
import { parseUrlStr, useAsync, UserBaseInfo } from 'tailchat-shared'; import { useAsync, UserBaseInfo } from 'tailchat-shared';
/** /**
* *

@ -1,3 +1,4 @@
import { UserName } from '@/components/UserName';
import { fetchImagePrimaryColor } from '@/utils/image-helper'; import { fetchImagePrimaryColor } from '@/utils/image-helper';
import { Space, Tag } from 'antd'; import { Space, Tag } from 'antd';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
@ -33,7 +34,9 @@ export const GroupUserPopover: React.FC<{
<div className="w-80 -mx-4 -my-3 bg-inherit"> <div className="w-80 -mx-4 -my-3 bg-inherit">
<UserProfileContainer userInfo={userInfo}> <UserProfileContainer userInfo={userInfo}>
<div className="text-xl"> <div className="text-xl">
<span className="font-semibold">{userInfo.nickname}</span> <span className="font-semibold">
<UserName userId={userInfo._id} />
</span>
{!hideGroupMemberDiscriminator && ( {!hideGroupMemberDiscriminator && (
<span className="opacity-60 ml-1">#{userInfo.discriminator}</span> <span className="opacity-60 ml-1">#{userInfo.discriminator}</span>
)} )}

@ -1,3 +1,4 @@
import { UserName } from '@/components/UserName';
import { fetchImagePrimaryColor } from '@/utils/image-helper'; import { fetchImagePrimaryColor } from '@/utils/image-helper';
import { Space, Tag } from 'antd'; import { Space, Tag } from 'antd';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
@ -24,7 +25,9 @@ export const PersonalUserPopover: React.FC<{
<div className="w-80 -mx-4 -my-3 bg-inherit"> <div className="w-80 -mx-4 -my-3 bg-inherit">
<UserProfileContainer userInfo={userInfo}> <UserProfileContainer userInfo={userInfo}>
<div className="text-xl"> <div className="text-xl">
<span className="font-semibold">{userInfo.nickname}</span> <span className="font-semibold">
<UserName userId={userInfo._id} />
</span>
<span className="opacity-60 ml-1">#{userInfo.discriminator}</span> <span className="opacity-60 ml-1">#{userInfo.discriminator}</span>
</div> </div>

@ -15,6 +15,7 @@ import {
useSearch, useSearch,
} from 'tailchat-shared'; } from 'tailchat-shared';
import _compact from 'lodash/compact'; import _compact from 'lodash/compact';
import { useFriendNicknameMap } from 'tailchat-shared/redux/hooks/useFriendNickname';
/** /**
* *
@ -23,6 +24,7 @@ export function useGroupMemberAction(groupId: string) {
const groupInfo = useGroupInfo(groupId); const groupInfo = useGroupInfo(groupId);
const members = groupInfo?.members ?? []; const members = groupInfo?.members ?? [];
const userInfos = useGroupMemberInfos(groupId); const userInfos = useGroupMemberInfos(groupId);
const friendNicknameMap = useFriendNicknameMap();
const { handleMuteMember, handleUnmuteMember } = useMemberMuteAction( const { handleMuteMember, handleUnmuteMember } = useMemberMuteAction(
groupId, groupId,
@ -31,7 +33,19 @@ export function useGroupMemberAction(groupId: string) {
const { searchText, setSearchText, isSearching, searchResult } = useSearch({ const { searchText, setSearchText, isSearching, searchResult } = useSearch({
dataSource: userInfos, dataSource: userInfos,
filterFn: (item, searchText) => item.nickname.includes(searchText), filterFn: (item, searchText) => {
if (friendNicknameMap[item._id]) {
if (friendNicknameMap[item._id].includes(searchText)) {
return true;
}
}
if (item.nickname.includes(searchText)) {
return true;
}
return false;
},
}); });
/** /**

@ -1,7 +1,7 @@
import { Avatar } from 'tailchat-design'; import { Avatar } from 'tailchat-design';
import { InviteCodeExpiredAt } from '@/components/InviteCodeExpiredAt'; import { InviteCodeExpiredAt } from '@/components/InviteCodeExpiredAt';
import { LoadingSpinner } from '@/components/LoadingSpinner'; import { LoadingSpinner } from '@/components/LoadingSpinner';
import { UserName } from '@/components/UserName'; import { UserNamePure } from '@/components/UserName';
import { Divider } from 'antd'; import { Divider } from 'antd';
import React from 'react'; import React from 'react';
import { import {
@ -60,7 +60,7 @@ export const InviteInfo: React.FC<Props> = React.memo((props) => {
/> />
</div> </div>
<div> <div>
<UserName className="font-bold" userId={inviteInfo.creator} />{' '} <UserNamePure className="font-bold" userId={inviteInfo.creator} />{' '}
{t('邀请您加入群组')} {t('邀请您加入群组')}
</div> </div>
<div className="text-xl my-2 font-bold">{inviteInfo.group.name}</div> <div className="text-xl my-2 font-bold">{inviteInfo.group.name}</div>

@ -79,6 +79,7 @@ export const PersonalSidebar: React.FC = React.memo(() => {
> >
{t('私信')} {t('私信')}
</SidebarSection> </SidebarSection>
{converseList.map((converse) => { {converseList.map((converse) => {
return <SidebarDMItem key={converse._id} converse={converse} />; return <SidebarDMItem key={converse._id} converse={converse} />;
})} })}

@ -6,7 +6,7 @@ import {
t, t,
ReduxProvider, ReduxProvider,
UserLoginInfo, UserLoginInfo,
reduxStore, getReduxStore,
} from 'tailchat-shared'; } from 'tailchat-shared';
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import { LoadingSpinner } from '@/components/LoadingSpinner'; import { LoadingSpinner } from '@/components/LoadingSpinner';
@ -41,7 +41,7 @@ function useAppState() {
// 到这里 userLoginInfo 必定存在 // 到这里 userLoginInfo 必定存在
// 创建Redux store // 创建Redux store
const store = reduxStore; const store = getReduxStore();
store.dispatch(userActions.setUserInfo(userLoginInfo)); store.dispatch(userActions.setUserInfo(userLoginInfo));
setGlobalStore(store); setGlobalStore(store);

Loading…
Cancel
Save