feat: add invite call when start meeting from conversation

chore/devcontainer
moonrailgun 1 year ago
parent a1b56ef487
commit 8906b933d7

@ -57,7 +57,7 @@ export function openInNewWindow(
}
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 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<any>;
post: (actionName: string, data?: any, config?: any) => Promise<any>;
get: (
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;
@ -598,6 +632,8 @@ declare module '@capital/component' {
style?: React.CSSProperties;
}>;
export const UserListItem: any;
export const Markdown: any;
export const MarkdownEditor: any;

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

@ -2,8 +2,7 @@
* Fork <VideoConference /> 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<HTMLDivElement> {
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<VideoConferenceProps> = React.memo(
({ chatMessageFormatter, ...props }) => {
const [widgetState, setWidgetState] = React.useState<WidgetState>({
showChat: false,
});
const lastAutoFocusedScreenShareTrack =
React.useRef<TrackReferenceOrPlaceholder | null>(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<VideoConferenceProps> = 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<VideoConferenceProps> = 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<VideoConferenceProps> = React.memo(
return (
<div className="lk-video-conference" {...props}>
{isWeb() && (
<LayoutContextProvider
value={layoutContext}
// onPinChange={handleFocusStateChange}
onWidgetChange={widgetUpdate}
>
<LayoutContextProvider value={layoutContext}>
<div className="lk-video-conference-inner">
{!focusTrack ? (
<div className="lk-grid-layout-wrapper">
@ -146,6 +150,15 @@ export const VideoConference: React.FC<VideoConferenceProps> = React.memo(
</div>
)}
{Array.isArray(invitingUserIds) && invitingUserIds.length > 0 && (
<IsCallingContainer>
{Translate.isCalling}:
{invitingUserIds.map((userId) => (
<UserAvatar key={userId} userId={userId} />
))}
</IsCallingContainer>
)}
<ControlBar />
</div>
@ -165,3 +178,25 @@ export const VideoConference: React.FC<VideoConferenceProps> = 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);
}
}, []);
}

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

@ -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;
},
});

@ -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<void>;
inviteUserCompleted: (userId: string) => void;
}
export function createMeetingStateStore() {
export function createMeetingStateStore(meetingId: string) {
return createStore<MeetingState>()(
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),
}));
},
}))
);
}

@ -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',
}),
};

@ -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;

Loading…
Cancel
Save