diff --git a/client/shared/model/group.ts b/client/shared/model/group.ts index faf134a0..6325c20f 100644 --- a/client/shared/model/group.ts +++ b/client/shared/model/group.ts @@ -34,11 +34,11 @@ export type GroupPanelFeature = export interface GroupPanel { /** - * 在群组中唯一 + * 在群组中唯一, 是个objectId */ id: string; /** - * 用于显示的面板名 + * 用于显示的面板名,形如: `com.msgbyte.xxx/panel` */ name: string; parentId?: string; diff --git a/client/web/src/plugin/common/reg.ts b/client/web/src/plugin/common/reg.ts index cd96570b..f1e7fb45 100644 --- a/client/web/src/plugin/common/reg.ts +++ b/client/web/src/plugin/common/reg.ts @@ -7,7 +7,6 @@ import { regSocketEventListener, PermissionItemType, GroupPanelFeature, - InboxItem, buildRegMap, BasicInboxItem, } from 'tailchat-shared'; @@ -255,7 +254,8 @@ export const [pluginPermission, regPluginPermission] = */ export const [pluginGroupPanelBadges, regGroupPanelBadge] = buildRegList<{ name: string; - render: (groupId: string, panelId: string) => React.ReactNode; + panelType: string; + render: React.ComponentType<{ groupId: string; panelId: string }>; }>(); /** diff --git a/client/web/src/routes/Main/Content/Group/AckPanelItem.tsx b/client/web/src/routes/Main/Content/Group/AckPanelItem.tsx index 63fb91ac..338781a7 100644 --- a/client/web/src/routes/Main/Content/Group/AckPanelItem.tsx +++ b/client/web/src/routes/Main/Content/Group/AckPanelItem.tsx @@ -21,7 +21,11 @@ export const GroupAckPanelItem: React.FC = React.memo( const { groupId, panel } = props; const panelId = panel.id; const hasUnread = useGroupTextPanelUnread(panelId); - const extraBadge = useGroupPanelExtraBadge(groupId, panelId); + const extraBadge = useGroupPanelExtraBadge( + groupId, + panelId, + panel.pluginPanelName ?? '' + ); const { checkIsMuted } = useUserNotifyMute(); const isMuted = checkIsMuted(panelId, groupId); diff --git a/client/web/src/routes/Main/Content/Group/SidebarItem.tsx b/client/web/src/routes/Main/Content/Group/SidebarItem.tsx index 5cc3435b..37121be9 100644 --- a/client/web/src/routes/Main/Content/Group/SidebarItem.tsx +++ b/client/web/src/routes/Main/Content/Group/SidebarItem.tsx @@ -38,7 +38,11 @@ export const SidebarItem: React.FC<{ const dispatch = useAppDispatch(); const { markConverseAllAck } = useConverseAck(panelId); const extraMenuItems = useExtraMenuItems(panel); - const extraBadge = useGroupPanelExtraBadge(groupId, panelId); + const extraBadge = useGroupPanelExtraBadge( + groupId, + panelId, + panel.pluginPanelName ?? '' + ); const { checkIsMuted, toggleMute } = useUserNotifyMute(); if (!groupInfo) { diff --git a/client/web/src/routes/Main/Content/Group/utils.tsx b/client/web/src/routes/Main/Content/Group/utils.tsx index 7147c506..8010c406 100644 --- a/client/web/src/routes/Main/Content/Group/utils.tsx +++ b/client/web/src/routes/Main/Content/Group/utils.tsx @@ -66,7 +66,8 @@ export function useExtraMenuItems(panel: GroupPanel): ItemType[] { */ export function useGroupPanelExtraBadge( groupId: string, - panelId: string + panelId: string, + panelType: string ): React.ReactNode[] { const update = useUpdate(); @@ -74,11 +75,17 @@ export function useGroupPanelExtraBadge( update(); }); - const extraBadge = pluginGroupPanelBadges.map((item) => ( - - {item.render(groupId, panelId)} - - )); + const extraBadge = pluginGroupPanelBadges + .filter((item) => item.panelType === panelType) + .map((item) => { + const Component = item.render; + + return ( + + + + ); + }); return extraBadge; } diff --git a/client/web/src/utils/group-helper.ts b/client/web/src/utils/group-helper.ts index 18494313..24b89433 100644 --- a/client/web/src/utils/group-helper.ts +++ b/client/web/src/utils/group-helper.ts @@ -9,7 +9,7 @@ export function isGroupAckPanel(panel: GroupPanel) { return true; } - if (panel.type === GroupPanelType.GROUP) { + if (panel.type === GroupPanelType.PLUGIN) { const pluginPanelInfo = findPluginPanelInfoByName(panel.name); return pluginPanelInfo?.feature?.includes('ack') ?? false; } diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ParticipantAvatars.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ParticipantAvatars.tsx index dec225fd..589e3b0e 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ParticipantAvatars.tsx +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ParticipantAvatars.tsx @@ -1,24 +1,21 @@ import React, { useEffect, useRef } from 'react'; -import { Avatar, Tooltip, UserAvatar, UserName } from '@capital/component'; -import { useAsyncFn, useEvent } from '@capital/common'; -import { request } from '../request'; +import { + Avatar, + LoadingSpinner, + Tooltip, + UserAvatar, + UserName, +} from '@capital/component'; import { Translate } from '../translate'; +import { useRoomParticipants } from '../utils/useRoomParticipants'; interface Props { roomName: string; } export const ParticipantAvatars: React.FC = React.memo((props) => { const containerEl = useRef(null); - const [{ value: participants = [] }, _handleFetchParticipants] = - useAsyncFn(async () => { - const { data } = await request.post('roomMembers', { - roomName: props.roomName, - }); - - return data ?? []; - }, [props.roomName]); - - const handleFetchParticipants = useEvent(_handleFetchParticipants); + const { participants, fetchParticipants, isFirstLoading } = + useRoomParticipants(props.roomName); useEffect(() => { let timer: number; @@ -26,13 +23,13 @@ export const ParticipantAvatars: React.FC = React.memo((props) => { const fn = async () => { if (containerEl.current && containerEl.current.offsetWidth !== 0) { // 该元素可见 - await handleFetchParticipants(); + await fetchParticipants(); } timer = window.setTimeout(fn, 3000); }; - timer = window.setTimeout(fn, 3000); + fn(); return () => { if (timer) { @@ -43,36 +40,33 @@ export const ParticipantAvatars: React.FC = React.memo((props) => { let inner: React.ReactNode; - if (participants.length === 0) { - inner = Translate.nobodyInMeeting; + if (isFirstLoading) { + inner = ; } else { - inner = ( - <> -
{Translate.peopleInMeeting}
- - {[ - ...participants, - ...participants, - ...participants, - ...participants, - ...participants, - ...participants, - ].map((info, i) => ( - } - placement="top" - > - - - ))} - - - ); + if (participants.length === 0) { + inner = Translate.nobodyInMeeting; + } else { + inner = ( + <> +
{Translate.peopleInMeeting}
+ + {participants.map((info, i) => ( + } + placement="top" + > + + + ))} + + + ); + } } return ( diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/group/LivekitPanelBadge.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/group/LivekitPanelBadge.tsx new file mode 100644 index 00000000..e7f3ec51 --- /dev/null +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/group/LivekitPanelBadge.tsx @@ -0,0 +1,37 @@ +import { Avatar, Tooltip, UserAvatar, UserName } from '@capital/component'; +import React, { useEffect } from 'react'; +import { useRoomParticipants } from '../utils/useRoomParticipants'; + +export const LivekitPanelBadge: React.FC<{ + groupId: string; + panelId: string; +}> = React.memo((props) => { + const roomName = `${props.groupId}#${props.panelId}`; + const { participants, fetchParticipants } = useRoomParticipants(roomName); + + useEffect(() => { + fetchParticipants(); + }, []); + + return ( + + {participants.map((info, i) => ( + } + placement="top" + > + + + ))} + + ); +}); +LivekitPanelBadge.displayName = 'LivekitPanelBadge'; + +export default LivekitPanelBadge; diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/index.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/index.tsx index f283b3df..3ea9fc4e 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/index.tsx +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/index.tsx @@ -1,4 +1,8 @@ -import { regCustomPanel, regGroupPanel } from '@capital/common'; +import { + regCustomPanel, + regGroupPanel, + regGroupPanelBadge, +} from '@capital/common'; import { Loadable } from '@capital/component'; import { useIconIsShow } from './navbar/useIconIsShow'; import { Translate } from './translate'; @@ -16,6 +20,15 @@ regGroupPanel({ }), }); +regGroupPanelBadge({ + name: `${PLUGIN_ID}/livekitPanelBadge`, + panelType: `${PLUGIN_ID}/livekitPanel`, + render: Loadable(() => import('./group/LivekitPanelBadge'), { + componentName: `${PLUGIN_ID}:LivekitPanelBadge`, + fallback: null, + }), +}); + regCustomPanel({ position: 'navbar-more', icon: 'mingcute:voice-line', diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/utils/useRoomParticipants.ts b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/utils/useRoomParticipants.ts new file mode 100644 index 00000000..f1e21fdd --- /dev/null +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/utils/useRoomParticipants.ts @@ -0,0 +1,34 @@ +import { useAsyncFn, useEvent } from '@capital/common'; +import { useMemo, useRef } from 'react'; +import { request } from '../request'; + +export function useRoomParticipants(roomName: string) { + const [{ value: participants = [], loading }, _handleFetchParticipants] = + useAsyncFn(async () => { + const { data } = await request.post('roomMembers', { + roomName, + }); + + return data ?? []; + }, [roomName]); + + const fetchParticipants = useEvent(_handleFetchParticipants); + + const lockRef = useRef(false); + + const isFirstLoading = useMemo(() => { + if (loading && lockRef.current === false) { + lockRef.current = true; + return true; + } + + return false; + }, [loading]); + + return { + loading, + isFirstLoading, + participants, + fetchParticipants, + }; +}