feat: add invite call when start meeting from conversation

chore/devcontainer
moonrailgun 2 years ago
parent a1b56ef487
commit 8906b933d7

@ -57,7 +57,7 @@ export function openInNewWindow(
} }
class PanelWindowManager { class PanelWindowManager {
openedPanelWindows: Record<string, Window> = {}; private openedPanelWindows: Record<string, Window> = {};
/** /**
* *

@ -100,7 +100,20 @@ declare module '@capital/common' {
export const postMessageEvent: any; 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; export const getServiceUrl: () => string;
@ -224,8 +237,29 @@ declare module '@capital/common' {
}; };
export const createPluginRequest: (pluginName: string) => { export const createPluginRequest: (pluginName: string) => {
get: (actionName: string, config?: any) => Promise<any>; get: (
post: (actionName: string, data?: any, config?: any) => Promise<any>; actionName: string,
config?: any
) => Promise<{
data: any;
headers: Record<string, string>;
status: number;
statusText: string;
config: Record<string, any>;
request: any;
}>;
post: (
actionName: string,
data?: any,
config?: any
) => Promise<{
data: any;
headers: Record<string, string>;
status: number;
statusText: string;
config: Record<string, any>;
request: any;
}>;
}; };
export const postRequest: any; export const postRequest: any;
@ -598,6 +632,8 @@ declare module '@capital/component' {
style?: React.CSSProperties; style?: React.CSSProperties;
}>; }>;
export const UserListItem: any;
export const Markdown: any; export const Markdown: any;
export const MarkdownEditor: any; export const MarkdownEditor: any;

@ -72,7 +72,7 @@ export const ActiveRoom: React.FC<ActiveRoomProps> = React.memo((props) => {
audio={userChoices.audioEnabled} audio={userChoices.audioEnabled}
onDisconnected={onLeave} onDisconnected={onLeave}
> >
<MeetingContextProvider> <MeetingContextProvider meetingId={roomName}>
<VideoConference chatMessageFormatter={formatChatMessageLinks} /> <VideoConference chatMessageFormatter={formatChatMessageLinks} />
</MeetingContextProvider> </MeetingContextProvider>

@ -2,8 +2,7 @@
* Fork <VideoConference /> from "@livekit/components-react" * Fork <VideoConference /> from "@livekit/components-react"
*/ */
import React from 'react'; import React, { useEffect } from 'react';
import type { WidgetState } from '@livekit/components-core';
import { import {
isEqualTrackRef, isEqualTrackRef,
isTrackReference, isTrackReference,
@ -20,6 +19,7 @@ import {
MessageFormatter, MessageFormatter,
RoomAudioRenderer, RoomAudioRenderer,
useCreateLayoutContext, useCreateLayoutContext,
useRoomContext,
usePinnedTracks, usePinnedTracks,
useTracks, useTracks,
} from '@livekit/components-react'; } from '@livekit/components-react';
@ -30,15 +30,27 @@ import { Chat } from './Chat';
import { FocusLayout } from './FocusLayout'; import { FocusLayout } from './FocusLayout';
import { useMeetingContextState } from '../../context/MeetingContext'; import { useMeetingContextState } from '../../context/MeetingContext';
import { Member } from './Member'; import { Member } from './Member';
import { UserAvatar } from '@capital/component';
import { Translate } from '../../translate';
import styled from 'styled-components';
/**
* @public
*/
export interface VideoConferenceProps export interface VideoConferenceProps
extends React.HTMLAttributes<HTMLDivElement> { extends React.HTMLAttributes<HTMLDivElement> {
chatMessageFormatter?: MessageFormatter; 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. * 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. * It provides functionality like switching between participant grid view and focus view.
@ -57,12 +69,13 @@ export interface VideoConferenceProps
*/ */
export const VideoConference: React.FC<VideoConferenceProps> = React.memo( export const VideoConference: React.FC<VideoConferenceProps> = React.memo(
({ chatMessageFormatter, ...props }) => { ({ chatMessageFormatter, ...props }) => {
const [widgetState, setWidgetState] = React.useState<WidgetState>({
showChat: false,
});
const lastAutoFocusedScreenShareTrack = const lastAutoFocusedScreenShareTrack =
React.useRef<TrackReferenceOrPlaceholder | null>(null); React.useRef<TrackReferenceOrPlaceholder | null>(null);
const rightPanel = useMeetingContextState((state) => state.rightPanel); const rightPanel = useMeetingContextState((state) => state.rightPanel);
const invitingUserIds = useMeetingContextState(
(state) => state.invitingUserIds
);
useMeetingInit();
const tracks = useTracks( const tracks = useTracks(
[ [
@ -72,11 +85,6 @@ export const VideoConference: React.FC<VideoConferenceProps> = React.memo(
{ updateOnlyOn: [RoomEvent.ActiveSpeakersChanged] } { updateOnlyOn: [RoomEvent.ActiveSpeakersChanged] }
); );
const widgetUpdate = (state: WidgetState) => {
log.debug('updating widget state', state);
setWidgetState(state);
};
const layoutContext = useCreateLayoutContext(); const layoutContext = useCreateLayoutContext();
const screenShareTracks = tracks const screenShareTracks = tracks
@ -88,7 +96,7 @@ export const VideoConference: React.FC<VideoConferenceProps> = React.memo(
(track) => !isEqualTrackRef(track, focusTrack) (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 screen share tracks are published, and no pin is set explicitly, auto set the screen share.
if ( if (
screenShareTracks.length > 0 && screenShareTracks.length > 0 &&
@ -122,11 +130,7 @@ export const VideoConference: React.FC<VideoConferenceProps> = React.memo(
return ( return (
<div className="lk-video-conference" {...props}> <div className="lk-video-conference" {...props}>
{isWeb() && ( {isWeb() && (
<LayoutContextProvider <LayoutContextProvider value={layoutContext}>
value={layoutContext}
// onPinChange={handleFocusStateChange}
onWidgetChange={widgetUpdate}
>
<div className="lk-video-conference-inner"> <div className="lk-video-conference-inner">
{!focusTrack ? ( {!focusTrack ? (
<div className="lk-grid-layout-wrapper"> <div className="lk-grid-layout-wrapper">
@ -146,6 +150,15 @@ export const VideoConference: React.FC<VideoConferenceProps> = React.memo(
</div> </div>
)} )}
{Array.isArray(invitingUserIds) && invitingUserIds.length > 0 && (
<IsCallingContainer>
{Translate.isCalling}:
{invitingUserIds.map((userId) => (
<UserAvatar key={userId} userId={userId} />
))}
</IsCallingContainer>
)}
<ControlBar /> <ControlBar />
</div> </div>
@ -165,3 +178,25 @@ export const VideoConference: React.FC<VideoConferenceProps> = React.memo(
} }
); );
VideoConference.displayName = 'VideoConference'; 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);
}
}, []);
}

@ -8,17 +8,23 @@ import { useStore } from 'zustand';
const MeetingContext = React.createContext<MeetingStateStoreType>(null); const MeetingContext = React.createContext<MeetingStateStoreType>(null);
export const MeetingContextProvider: React.FC<PropsWithChildren> = React.memo( interface MeetingContextProviderProps extends PropsWithChildren {
(props) => { meetingId: string;
const store = useMemo(() => createMeetingStateStore(), []); }
export const MeetingContextProvider: React.FC<MeetingContextProviderProps> =
React.memo((props) => {
const store = useMemo(
() => createMeetingStateStore(props.meetingId),
[props.meetingId]
);
return ( return (
<MeetingContext.Provider value={store}> <MeetingContext.Provider value={store}>
{props.children} {props.children}
</MeetingContext.Provider> </MeetingContext.Provider>
); );
} });
);
MeetingContextProvider.displayName = 'MeetingContextProvider'; MeetingContextProvider.displayName = 'MeetingContextProvider';
export function useMeetingContextState<T>(selector: (s: MeetingState) => T) { export function useMeetingContextState<T>(selector: (s: MeetingState) => T) {

@ -5,6 +5,8 @@ import {
regPluginPanelAction, regPluginPanelAction,
regPluginPanelRoute, regPluginPanelRoute,
panelWindowManager, panelWindowManager,
regSocketEventListener,
getGlobalState,
} from '@capital/common'; } from '@capital/common';
import { Loadable } from '@capital/component'; import { Loadable } from '@capital/component';
import { useIconIsShow } from './navbar/useIconIsShow'; import { useIconIsShow } from './navbar/useIconIsShow';
@ -71,12 +73,18 @@ regPluginPanelAction({
position: 'dm', position: 'dm',
icon: 'mdi:video-box', icon: 'mdi:video-box',
onClick: ({ converseId }) => { 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}`, `/panel/plugin/${PLUGIN_ID}/meeting/${converseId}`,
{ {
width: 1280, width: 1280,
height: 768, height: 768,
} }
); );
(win.window as any).autoInviteIds = shouldInviteUserIds;
}, },
}); });

@ -1,15 +1,24 @@
import { createStore } from 'zustand'; import { createStore } from 'zustand';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
import { request } from '../request';
import { getCachedUserInfo, showErrorToasts } from '@capital/common';
import { Translate } from '../translate';
export interface MeetingState { export interface MeetingState {
meetingId: string;
rightPanel: 'chat' | 'member' | null; rightPanel: 'chat' | 'member' | null;
invitingUserIds: string[]; // 正在邀请的用户id
setRightPanel: (panel: 'chat' | 'member' | null) => void; setRightPanel: (panel: 'chat' | 'member' | null) => void;
inviteUsers: (userIds: string[]) => Promise<void>;
inviteUserCompleted: (userId: string) => void;
} }
export function createMeetingStateStore() { export function createMeetingStateStore(meetingId: string) {
return createStore<MeetingState>()( return createStore<MeetingState>()(
immer((set, get) => ({ immer((set, get) => ({
meetingId,
rightPanel: null, rightPanel: null,
invitingUserIds: [],
setRightPanel: (rightPanel) => { setRightPanel: (rightPanel) => {
if (get().rightPanel === rightPanel) { if (get().rightPanel === rightPanel) {
// toggle // toggle
@ -18,6 +27,39 @@ export function createMeetingStateStore() {
set({ rightPanel }); 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),
}));
},
})) }))
); );
} }

@ -77,4 +77,12 @@ export const Translate = {
'zh-CN': '发起/加入通话', 'zh-CN': '发起/加入通话',
'en-US': 'Start/Join Call', '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',
}),
}; };

@ -100,7 +100,20 @@ declare module '@capital/common' {
export const postMessageEvent: any; 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; export const getServiceUrl: () => string;
@ -598,6 +611,8 @@ declare module '@capital/component' {
style?: React.CSSProperties; style?: React.CSSProperties;
}>; }>;
export const UserListItem: any;
export const Markdown: any; export const Markdown: any;
export const MarkdownEditor: any; export const MarkdownEditor: any;

Loading…
Cancel
Save