From 8906b933d7e0e50e487770da16f296f86636b8de Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sun, 19 Nov 2023 18:12:20 +0800 Subject: [PATCH] feat: add invite call when start meeting from conversation --- client/web/src/utils/window-helper.ts | 2 +- client/web/tailchat.d.ts | 42 ++++++++++- .../src/components/ActiveRoom.tsx | 2 +- .../src/components/lib/VideoConference.tsx | 73 ++++++++++++++----- .../src/context/MeetingContext.tsx | 16 ++-- .../plugins/com.msgbyte.livekit/src/index.tsx | 10 ++- .../com.msgbyte.livekit/src/store/meeting.ts | 44 ++++++++++- .../com.msgbyte.livekit/src/translate.ts | 8 ++ .../com.msgbyte.livekit/types/tailchat.d.ts | 17 ++++- 9 files changed, 182 insertions(+), 32 deletions(-) diff --git a/client/web/src/utils/window-helper.ts b/client/web/src/utils/window-helper.ts index 98623f65..ef631754 100644 --- a/client/web/src/utils/window-helper.ts +++ b/client/web/src/utils/window-helper.ts @@ -57,7 +57,7 @@ export function openInNewWindow( } class PanelWindowManager { - openedPanelWindows: Record = {}; + private openedPanelWindows: Record = {}; /** * 打开一个独立窗口 diff --git a/client/web/tailchat.d.ts b/client/web/tailchat.d.ts index abeca866..19a2d1bf 100644 --- a/client/web/tailchat.d.ts +++ b/client/web/tailchat.d.ts @@ -100,7 +100,20 @@ declare module '@capital/common' { export const postMessageEvent: any; - export const panelWindowManager: any; + export const panelWindowManager: { + open: ( + url: string, + options?: { + width?: number; + height?: number; + top?: number; + left?: number; + onClose?: () => void; + } + ) => Window; + close: (url: string) => void; + has: (url: string) => boolean; + }; export const getServiceUrl: () => string; @@ -224,8 +237,29 @@ declare module '@capital/common' { }; export const createPluginRequest: (pluginName: string) => { - get: (actionName: string, config?: any) => Promise; - post: (actionName: string, data?: any, config?: any) => Promise; + get: ( + actionName: string, + config?: any + ) => Promise<{ + data: any; + headers: Record; + status: number; + statusText: string; + config: Record; + request: any; + }>; + post: ( + actionName: string, + data?: any, + config?: any + ) => Promise<{ + data: any; + headers: Record; + status: number; + statusText: string; + config: Record; + request: any; + }>; }; export const postRequest: any; @@ -598,6 +632,8 @@ declare module '@capital/component' { style?: React.CSSProperties; }>; + export const UserListItem: any; + export const Markdown: any; export const MarkdownEditor: any; diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ActiveRoom.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ActiveRoom.tsx index 00d17c8a..e49ece5d 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ActiveRoom.tsx +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ActiveRoom.tsx @@ -72,7 +72,7 @@ export const ActiveRoom: React.FC = React.memo((props) => { audio={userChoices.audioEnabled} onDisconnected={onLeave} > - + diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/lib/VideoConference.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/lib/VideoConference.tsx index 87d77540..bdec4c56 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/lib/VideoConference.tsx +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/lib/VideoConference.tsx @@ -2,8 +2,7 @@ * Fork from "@livekit/components-react" */ -import React from 'react'; -import type { WidgetState } from '@livekit/components-core'; +import React, { useEffect } from 'react'; import { isEqualTrackRef, isTrackReference, @@ -20,6 +19,7 @@ import { MessageFormatter, RoomAudioRenderer, useCreateLayoutContext, + useRoomContext, usePinnedTracks, useTracks, } from '@livekit/components-react'; @@ -30,15 +30,27 @@ import { Chat } from './Chat'; import { FocusLayout } from './FocusLayout'; import { useMeetingContextState } from '../../context/MeetingContext'; import { Member } from './Member'; +import { UserAvatar } from '@capital/component'; +import { Translate } from '../../translate'; +import styled from 'styled-components'; -/** - * @public - */ export interface VideoConferenceProps extends React.HTMLAttributes { chatMessageFormatter?: MessageFormatter; } +const IsCallingContainer = styled.div` + display: flex; + gap: 8px; + align-items: center; + padding: 4px 8px; + background: rgba(0, 0, 0, 0.25); + border-radius: 10px; + position: absolute; + right: 10px; + bottom: 120px; +`; + /** * This component is the default setup of a classic LiveKit video conferencing app. * It provides functionality like switching between participant grid view and focus view. @@ -57,12 +69,13 @@ export interface VideoConferenceProps */ export const VideoConference: React.FC = React.memo( ({ chatMessageFormatter, ...props }) => { - const [widgetState, setWidgetState] = React.useState({ - showChat: false, - }); const lastAutoFocusedScreenShareTrack = React.useRef(null); const rightPanel = useMeetingContextState((state) => state.rightPanel); + const invitingUserIds = useMeetingContextState( + (state) => state.invitingUserIds + ); + useMeetingInit(); const tracks = useTracks( [ @@ -72,11 +85,6 @@ export const VideoConference: React.FC = React.memo( { updateOnlyOn: [RoomEvent.ActiveSpeakersChanged] } ); - const widgetUpdate = (state: WidgetState) => { - log.debug('updating widget state', state); - setWidgetState(state); - }; - const layoutContext = useCreateLayoutContext(); const screenShareTracks = tracks @@ -88,7 +96,7 @@ export const VideoConference: React.FC = React.memo( (track) => !isEqualTrackRef(track, focusTrack) ); - React.useEffect(() => { + useEffect(() => { // If screen share tracks are published, and no pin is set explicitly, auto set the screen share. if ( screenShareTracks.length > 0 && @@ -122,11 +130,7 @@ export const VideoConference: React.FC = React.memo( return (
{isWeb() && ( - +
{!focusTrack ? (
@@ -146,6 +150,15 @@ export const VideoConference: React.FC = React.memo(
)} + {Array.isArray(invitingUserIds) && invitingUserIds.length > 0 && ( + + {Translate.isCalling}: + {invitingUserIds.map((userId) => ( + + ))} + + )} +
@@ -165,3 +178,25 @@ export const VideoConference: React.FC = React.memo( } ); VideoConference.displayName = 'VideoConference'; + +function useMeetingInit() { + const inviteUsers = useMeetingContextState((state) => state.inviteUsers); + const inviteUserCompleted = useMeetingContextState( + (state) => state.inviteUserCompleted + ); + const room = useRoomContext(); + + useEffect(() => { + room.addListener('participantConnected', (p) => { + inviteUserCompleted(p.identity); + }); + }, []); + + useEffect(() => { + // Auto invite user on start + const autoInviteIds = (window as any).autoInviteIds as string[]; + if (Array.isArray(autoInviteIds) && autoInviteIds.length > 0) { + inviteUsers(autoInviteIds); + } + }, []); +} diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/context/MeetingContext.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/context/MeetingContext.tsx index 60235df8..15d0980a 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/context/MeetingContext.tsx +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/context/MeetingContext.tsx @@ -8,17 +8,23 @@ import { useStore } from 'zustand'; const MeetingContext = React.createContext(null); -export const MeetingContextProvider: React.FC = React.memo( - (props) => { - const store = useMemo(() => createMeetingStateStore(), []); +interface MeetingContextProviderProps extends PropsWithChildren { + meetingId: string; +} + +export const MeetingContextProvider: React.FC = + React.memo((props) => { + const store = useMemo( + () => createMeetingStateStore(props.meetingId), + [props.meetingId] + ); return ( {props.children} ); - } -); + }); MeetingContextProvider.displayName = 'MeetingContextProvider'; export function useMeetingContextState(selector: (s: MeetingState) => T) { 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 759cc518..25de6960 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 @@ -5,6 +5,8 @@ import { regPluginPanelAction, regPluginPanelRoute, panelWindowManager, + regSocketEventListener, + getGlobalState, } from '@capital/common'; import { Loadable } from '@capital/component'; import { useIconIsShow } from './navbar/useIconIsShow'; @@ -71,12 +73,18 @@ regPluginPanelAction({ position: 'dm', icon: 'mdi:video-box', onClick: ({ converseId }) => { - panelWindowManager.open( + const state = getGlobalState() ?? {}; + const currentUserId = state.user?.info?._id ?? ''; + const members: string[] = + state.chat?.converses?.[converseId]?.members ?? []; + const shouldInviteUserIds = members.filter((m) => m !== currentUserId); + const win = panelWindowManager.open( `/panel/plugin/${PLUGIN_ID}/meeting/${converseId}`, { width: 1280, height: 768, } ); + (win.window as any).autoInviteIds = shouldInviteUserIds; }, }); diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/store/meeting.ts b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/store/meeting.ts index bffcb89e..710b6ed1 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/store/meeting.ts +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/store/meeting.ts @@ -1,15 +1,24 @@ import { createStore } from 'zustand'; import { immer } from 'zustand/middleware/immer'; +import { request } from '../request'; +import { getCachedUserInfo, showErrorToasts } from '@capital/common'; +import { Translate } from '../translate'; export interface MeetingState { + meetingId: string; rightPanel: 'chat' | 'member' | null; + invitingUserIds: string[]; // 正在邀请的用户id setRightPanel: (panel: 'chat' | 'member' | null) => void; + inviteUsers: (userIds: string[]) => Promise; + inviteUserCompleted: (userId: string) => void; } -export function createMeetingStateStore() { +export function createMeetingStateStore(meetingId: string) { return createStore()( immer((set, get) => ({ + meetingId, rightPanel: null, + invitingUserIds: [], setRightPanel: (rightPanel) => { if (get().rightPanel === rightPanel) { // toggle @@ -18,6 +27,39 @@ export function createMeetingStateStore() { set({ rightPanel }); } }, + async inviteUsers(userIds: string[]) { + const { meetingId, inviteUserCompleted } = get(); + const res = await request.post('inviteCall', { + roomName: meetingId, + targetUserIds: userIds, + }); + + const { online, offline } = res.data; + + if (Array.isArray(offline) && offline.length > 0) { + // 部分通知失败,对方不在线 + Promise.all(offline.map((userId) => getCachedUserInfo(userId))).then( + (users) => { + const names = users.map((u) => u.nickname); + + showErrorToasts(Translate.callFailed + ' : ' + names.join(', ')); + + users.forEach((user) => inviteUserCompleted(user._id)); + } + ); + } + + set((state) => ({ + invitingUserIds: Array.from( + new Set([...state.invitingUserIds, ...online]) + ), + })); + }, + inviteUserCompleted(userId: string) { + set((state) => ({ + invitingUserIds: state.invitingUserIds.filter((id) => id !== userId), + })); + }, })) ); } diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/translate.ts b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/translate.ts index c082d4b9..aebe7c24 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/translate.ts +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/translate.ts @@ -77,4 +77,12 @@ export const Translate = { 'zh-CN': '发起/加入通话', 'en-US': 'Start/Join Call', }), + isCalling: localTrans({ + 'zh-CN': '正在呼叫', + 'en-US': 'Is calling', + }), + callFailed: localTrans({ + 'zh-CN': '用户呼叫失败,该用户离线', + 'en-US': 'The user call failed because of offline', + }), }; diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/types/tailchat.d.ts b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/types/tailchat.d.ts index abeca866..4fc5fdf9 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/types/tailchat.d.ts +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/types/tailchat.d.ts @@ -100,7 +100,20 @@ declare module '@capital/common' { export const postMessageEvent: any; - export const panelWindowManager: any; + export const panelWindowManager: { + open: ( + url: string, + options?: { + width?: number; + height?: number; + top?: number; + left?: number; + onClose?: () => void; + } + ) => Window; + close: (url: string) => void; + has: (url: string) => boolean; + }; export const getServiceUrl: () => string; @@ -598,6 +611,8 @@ declare module '@capital/component' { style?: React.CSSProperties; }>; + export const UserListItem: any; + export const Markdown: any; export const MarkdownEditor: any;