Merge branch 'feat/voice'

This commit is contained in:
Tristan Yang
2023-03-31 21:37:00 +08:00
39 changed files with 1123 additions and 107 deletions
+9 -8
View File
@@ -14,11 +14,12 @@
"@toast-ui/editor": "^3.2.2",
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
"@toast-ui/react-editor": "^3.2.3",
"@types/node": "^18.15.10",
"@types/react": "^18.0.30",
"@types/node": "^18.15.11",
"@types/react": "^18.0.31",
"@types/react-dom": "^18.0.11",
"@udecode/plate": "16.8",
"@uiball/loaders": "^1.2.6",
"agora-rtc-sdk-ng": "^4.17.0",
"bfj": "^7.0.2",
"broadcast-channel": "^5.0.3",
"browserslist": "^4.21.5",
@@ -30,7 +31,7 @@
"dotenv": "^16.0.3",
"dotenv-expand": "^10.0.0",
"emoji-mart": "5.5.2",
"eslint": "^8.36.0",
"eslint": "^8.37.0",
"file-loader": "^6.2.0",
"firebase": "^9.18.0",
"fs-extra": "^11.1.1",
@@ -58,15 +59,15 @@
"react-i18next": "^12.2.0",
"react-redux": "^8.0.5",
"react-refresh": "0.14.0",
"react-router-dom": "^6.9.0",
"react-router-dom": "^6.10.0",
"react-scripts": "^5.0.1",
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.4.1",
"react-use-wizard": "^2.2.1",
"react-viewport-list": "^7.0.0",
"react-virtuoso": "^4.1.0",
"react-virtuoso": "^4.1.1",
"resolve": "^1.22.1",
"rooks": "^7.9.0",
"rooks": "^7.10.0",
"slate": "^0.91.4",
"slate-history": "^0.86.0",
"slate-react": "^0.92.0",
@@ -76,7 +77,7 @@
"terser-webpack-plugin": "^5.3.7",
"tippy.js": "^6.3.7",
"typescript": "^5.0.2",
"webpack": "^5.76.3",
"webpack": "^5.77.0",
"webpack-dev-server": "^4.13.1",
"webpack-manifest-plugin": "^5.0.0",
"workbox-background-sync": "^6.5.4",
@@ -145,6 +146,6 @@
"postcss-loader": "^7.1.0",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.7",
"tailwindcss": "^3.2.7"
"tailwindcss": "^3.3.0"
}
}
-16
View File
@@ -115,13 +115,6 @@
</style>
<style>
/* reset */
* {
border: none;
outline: none;
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
width: 100%;
@@ -149,15 +142,6 @@
body.iframe .guest-container .right-container {
border-radius: 0;
}
a {
text-decoration: none;
color: inherit;
}
button {
cursor: pointer;
}
</style>
<body>
+7 -1
View File
@@ -45,5 +45,11 @@
"file": "file",
"image": "image",
"forward": "forward"
"forward": "forward",
"voice": "Voice Chat",
"deafen": "Deafen",
"undeafen": "Undeafen",
"mute": "Mute",
"unmute": "Unmute",
"leave_voice": "Leave Voice Chat"
}
+7 -1
View File
@@ -46,5 +46,11 @@
"file": "文件",
"image": "图片",
"forward": "转发"
"forward": "转发",
"voice": "音频通话",
"deafen": "静默",
"undeafen": "取消静默",
"mute": "静音",
"unmute": "取消静音",
"leave_voice": "离开"
}
+2 -1
View File
@@ -27,7 +27,8 @@ const whiteList = [
"createAdmin",
"getBotRelatedChannels",
"sendMessageByBot",
"replyWithChatGPT"
"replyWithChatGPT",
"getAgoraVoicingList"
];
const baseQuery = fetchBaseQuery({
+42 -2
View File
@@ -16,10 +16,13 @@ import {
GithubAuthConfig,
LicenseResponse,
RenewLicense,
RenewLicenseResponse
RenewLicenseResponse,
AgoraTokenResponse,
AgoraVoicingListResponse
} from "../../types/server";
import { Channel } from "../../types/channel";
import { ContentTypeKey } from "../../types/message";
import { upsertVoiceList } from "../slices/voice";
const defaultExpireDuration = 2 * 24 * 60 * 60;
@@ -109,6 +112,40 @@ export const serverApi = createApi({
body: data
})
}),
getAgoraToken: builder.query<AgoraTokenResponse, number>({
query: (id) => ({
url: `group/${id}/agora_token`,
})
}),
// tmp API
getAgoraVoicingList: builder.query<AgoraVoicingListResponse, { appid: string, key: string, secret: string }>({
query: ({ appid, key, secret }) => ({
headers: {
Authorization: `Basic ${btoa(`${key}:${secret}`)}`
},
url: `https://api.agora.io/dev/v1/channel/${appid}`,
}),
async onQueryStarted(data, { dispatch, queryFulfilled }) {
try {
const { data: resp } = await queryFulfilled;
const { success } = resp;
if (success) {
const arr = resp.data.channels.map(data => {
const [id] = data.channel_name.split(":").slice(-1);
const count = data.user_count;
return {
id: +id,
context: "channel" as const,
memberCount: count
};
});
dispatch(upsertVoiceList(arr));
}
} catch {
console.error("get voice list error");
}
}
}),
getSMTPConfig: builder.query<SMTPConfig, void>({
query: () => ({ url: `admin/smtp/config` })
}),
@@ -320,5 +357,8 @@ export const {
useLazyGetBotRelatedChannelsQuery,
useSendMessageByBotMutation,
useUpdateFrontendUrlMutation,
useGetFrontendUrlQuery
useGetFrontendUrlQuery,
useLazyGetAgoraTokenQuery,
useGetAgoraConfigQuery,
useGetAgoraVoicingListQuery
} = serverApi;
+8 -5
View File
@@ -9,17 +9,18 @@ import {
KEY_UID
} from "../config";
import { AuthData, RenewTokenResponse } from "../../types/auth";
import { User } from "../../types/user";
import { StoredUser } from "./users";
// import { updateUsersByLogs } from './users';
interface State {
initialized: boolean;
guest: boolean;
user: User | undefined;
user: StoredUser | undefined;
token: string;
expireTime: number;
refreshToken: string;
roleChanged: boolean;
voice: boolean;
}
const loginUser = localStorage.getItem(KEY_LOGIN_USER) || "";
const initialState: State = {
@@ -29,7 +30,8 @@ const initialState: State = {
token: localStorage.getItem(KEY_TOKEN) || "",
expireTime: Number(localStorage.getItem(KEY_EXPIRE) || +new Date()),
refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN) || "",
roleChanged: false
roleChanged: false,
voice: false
};
const emptyState: State = {
@@ -39,7 +41,8 @@ const emptyState: State = {
token: "",
expireTime: +new Date(),
refreshToken: "",
roleChanged: false
roleChanged: false,
voice: false
};
const authDataSlice = createSlice({
@@ -64,7 +67,7 @@ const authDataSlice = createSlice({
localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token);
localStorage.setItem(KEY_UID, `${uid}`);
},
updateLoginUser(state, { payload }: PayloadAction<Partial<User>>) {
updateLoginUser(state, { payload }: PayloadAction<Partial<StoredUser>>) {
if (!state.user) return;
const obj = { ...state.user, ...payload };
+14 -3
View File
@@ -3,8 +3,10 @@ import { isNull, omitBy } from "lodash";
import BASE_URL from "../config";
import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from "../../types/channel";
type ChannelAside = "members" | "voice" | null;
interface StoredChannel extends Channel {
icon?: string;
visibleAside: ChannelAside
}
interface State {
@@ -33,7 +35,8 @@ const channelsSlice = createSlice({
icon:
c.avatar_updated_at == 0
? ""
: `${BASE_URL}/resource/group_avatar?gid=${c.gid}&t=${c.avatar_updated_at}`
: `${BASE_URL}/resource/group_avatar?gid=${c.gid}&t=${c.avatar_updated_at}`,
visibleAside: state.byId[c.gid]?.visibleAside ?? null
};
});
},
@@ -48,7 +51,8 @@ const channelsSlice = createSlice({
icon:
avatar_updated_at == 0
? ""
: `${BASE_URL}/resource/group_avatar?gid=${gid}&t=${avatar_updated_at}`
: `${BASE_URL}/resource/group_avatar?gid=${gid}&t=${avatar_updated_at}`,
visibleAside: "members"
};
},
updateChannel(state, action: PayloadAction<UpdateChannelDTO>) {
@@ -99,6 +103,12 @@ const channelsSlice = createSlice({
}
}
},
updateChannelVisibleAside(state, action: PayloadAction<{ id: number, aside: ChannelAside }>) {
const { id, aside } = action.payload;
if (state.byId[id]) {
state.byId[id].visibleAside = aside;
}
},
removeChannel(state, action: PayloadAction<number>) {
const gid = action.payload;
const idx = state.ids.findIndex((i) => i == gid);
@@ -116,7 +126,8 @@ export const {
fillChannels,
addChannel,
updateChannel,
removeChannel
removeChannel,
updateChannelVisibleAside
} = channelsSlice.actions;
export default channelsSlice.reducer;
+3
View File
@@ -4,9 +4,12 @@ import BASE_URL from "../config";
import { User } from "../../types/user";
import { UserLog, UserState } from "../../types/sse";
type DMAside = "voice" | null;
export interface StoredUser extends User {
online?: boolean;
voice?: boolean;
avatar?: string;
visibleAside?: DMAside
}
export interface State {
+138
View File
@@ -0,0 +1,138 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { KEY_UID } from "../config";
export type VoiceBasicInfo = {
context: "channel" | "dm"
id: number,
}
export type VoicingInfo = {
downlinkNetworkQuality?: number,
muted?: boolean,
deafen?: boolean,
joining?: boolean
} & VoiceBasicInfo
export type VoicingMemberInfo = {
speakingVolume?: number,
muted?: boolean,
deafen?: boolean
}
export type VoicingMembers = {
ids: number[],
byId: {
[key: number]: VoicingMemberInfo
}
}
export type VoiceInfo = {
memberCount: number
} & VoiceBasicInfo
interface State {
voicing: VoicingInfo | null,
voicingMembers: VoicingMembers,
list: VoiceInfo[]
}
// const initialInfo = {
// context: "channel" as const,
// id: 0,
// members: []
// };
const initialState: State = {
voicing: null,
voicingMembers: {
ids: [],
byId: {}
},
list: []
};
const voiceSlice = createSlice({
name: "voice",
initialState,
reducers: {
updateVoicingInfo(state, { payload }: PayloadAction<VoicingInfo | null>) {
if (payload) {
state.voicing = { ...(state.voicing ?? {}), ...payload };
} else {
// reset
state.voicing = payload;
state.voicingMembers = {
ids: [],
byId: {}
};
}
},
updateMuteStatus(state, { payload }: PayloadAction<boolean>) {
if (state.voicing) {
state.voicing.muted = payload;
// 更新登录用户在member list的状态
const loginUid = localStorage.getItem(KEY_UID) ?? 0;
const idx = state.voicingMembers.ids.findIndex((uid) => uid == loginUid);
if (idx > -1) {
state.voicingMembers.byId[+loginUid].muted = payload;
}
}
},
updateDeafenStatus(state, { payload }: PayloadAction<boolean>) {
if (state.voicing) {
state.voicing.deafen = payload;
state.voicing.muted = payload;
// 更新登录用户在member list的状态
const loginUid = localStorage.getItem(KEY_UID) ?? 0;
const idx = state.voicingMembers.ids.findIndex((uid) => uid == loginUid);
if (idx > -1) {
state.voicingMembers.byId[+loginUid].muted = payload;
}
}
},
updateVoicingNetworkQuality(state, { payload }: PayloadAction<number>) {
if (state.voicing) {
state.voicing.downlinkNetworkQuality = payload;
}
},
upsertVoiceList(state, { payload }: PayloadAction<VoiceInfo[] | VoiceInfo>) {
if (Array.isArray(payload)) {
state.list = payload;
} else {
const { id, context } = payload;
const idx = state.list.findIndex(v => v.id == id && v.context == context);
if (idx > -1) {
state.list.splice(idx, 1, payload);
} else {
state.list.push(payload);
}
}
},
addVoiceMember(state, { payload }: PayloadAction<number>) {
const notExisted = !state.voicingMembers.ids.includes(payload);
console.log("add voice member", payload, notExisted, state.voicingMembers.ids);
if (notExisted) {
state.voicingMembers.ids = [...state.voicingMembers.ids, payload];
state.voicingMembers.byId[payload] = {
speakingVolume: 0,
muted: false
};
}
},
removeVoiceMember(state, { payload }: PayloadAction<number>) {
const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload);
if (idx > -1) {
state.voicingMembers.ids.splice(idx, 1);
delete state.voicingMembers.byId[payload];
}
},
updateVoicingMember(state, { payload }: PayloadAction<{ uid: number, info: VoicingMemberInfo }>) {
const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload.uid);
if (idx > -1) {
const { uid, info } = payload;
state.voicingMembers.byId[uid] = { ...state.voicingMembers.byId[uid], ...info };
}
}
},
});
export const { addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember, updateDeafenStatus } = voiceSlice.actions;
export default voiceSlice.reducer;
+2
View File
@@ -3,6 +3,7 @@ import { setupListeners } from "@reduxjs/toolkit/query";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import listenerMiddleware from "./listener.middleware";
import authDataReducer from "./slices/auth.data";
import voiceReducer from "./slices/voice";
import footprintReducer from "./slices/footprint";
import serverReducer from "./slices/server";
import uiReducer from "./slices/ui";
@@ -23,6 +24,7 @@ import { serverApi } from "./services/server";
const reducer = combineReducers({
authData: authDataReducer,
voice: voiceReducer,
ui: uiReducer,
footprint: footprintReducer,
server: serverReducer,
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
<path d="M21.7806 2.21967C22.0735 2.51256 22.0735 2.98744 21.7806 3.28033L18.5609 6.5L21.7806 9.71967C22.0735 10.0126 22.0735 10.4874 21.7806 10.7803C21.4877 11.0732 21.0128 11.0732 20.7199 10.7803L17.5002 7.56066L14.2806 10.7803C13.9877 11.0732 13.5128 11.0732 13.2199 10.7803C12.927 10.4874 12.927 10.0126 13.2199 9.71967L16.4396 6.5L13.2199 3.28033C12.927 2.98744 12.927 2.51256 13.2199 2.21967C13.5128 1.92678 13.9877 1.92678 14.2806 2.21967L17.5002 5.43934L20.7199 2.21967C21.0128 1.92678 21.4877 1.92678 21.7806 2.21967ZM9.36737 3.31232L10.2271 5.33967C10.6018 6.22312 10.3939 7.26203 9.71313 7.90815L7.81881 9.70616C7.93569 10.7816 8.2972 11.8406 8.90334 12.8832C9.50948 13.9257 10.2665 14.7905 11.1744 15.4776L13.4496 14.7189C14.312 14.4313 15.2512 14.7618 15.7802 15.539L17.0125 17.3495C17.6275 18.2529 17.5169 19.4993 16.7538 20.2653L15.9361 21.0862C15.1222 21.9033 13.9597 22.1997 12.8843 21.8643C10.3454 21.0723 8.01109 18.7211 5.88132 14.8107C3.74845 10.8945 2.9957 7.57189 3.62307 4.84289C3.88707 3.69457 4.70458 2.78009 5.77209 2.43899L6.84868 2.09498C7.8575 1.77263 8.93535 2.29358 9.36737 3.31232Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#70707B" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C17.5228 2 22 6.47715 22 12V19C22 20.6569 20.6569 22 19 22H16C15.4477 22 15 21.5523 15 21V15C15 14.4477 15.4477 14 16 14H20.5V12C20.5 7.30558 16.6944 3.5 12 3.5C7.30558 3.5 3.5 7.30558 3.5 12V14H8C8.55228 14 9 14.4477 9 15V21C9 21.5523 8.55228 22 8 22H5C3.34315 22 2 20.6569 2 19V12C2 6.47715 6.47715 2 12 2Z"/>
<path d="M2.88667 2.21966C3.17956 1.92677 3.65444 1.92678 3.94733 2.21968L22.4471 20.7198C22.74 21.0127 22.74 21.4876 22.4471 21.7805C22.1542 22.0734 21.6793 22.0734 21.3864 21.7805L2.88666 3.28032C2.59377 2.98743 2.59377 2.51255 2.88667 2.21966Z" fill="#D92D20"/>
</svg>

After

Width:  |  Height:  |  Size: 697 B

+2 -2
View File
@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C17.5228 2 22 6.47715 22 12V19C22 20.6569 20.6569 22 19 22H16C15.4477 22 15 21.5523 15 21V15C15 14.4477 15.4477 14 16 14H20.5V12C20.5 7.30558 16.6944 3.5 12 3.5C7.30558 3.5 3.5 7.30558 3.5 12V14H8C8.55228 14 9 14.4477 9 15V21C9 21.5523 8.55228 22 8 22H5C3.34315 22 2 20.6569 2 19V12C2 6.47715 6.47715 2 12 2Z" fill="#70707B"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#70707B" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C17.5228 2 22 6.47715 22 12V19C22 20.6569 20.6569 22 19 22H16C15.4477 22 15 21.5523 15 21V15C15 14.4477 15.4477 14 16 14H20.5V12C20.5 7.30558 16.6944 3.5 12 3.5C7.30558 3.5 3.5 7.30558 3.5 12V14H8C8.55228 14 9 14.4477 9 15V21C9 21.5523 8.55228 22 8 22H5C3.34315 22 2 20.6569 2 19V12C2 6.47715 6.47715 2 12 2Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

After

Width:  |  Height:  |  Size: 432 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#78787C" xmlns="http://www.w3.org/2000/svg">
<path d="M3.94733 2.21968C3.65444 1.92678 3.17956 1.92677 2.88667 2.21966C2.59377 2.51255 2.59377 2.98743 2.88666 3.28032L8.66699 9.06078V12C8.66699 14.2091 10.4579 16 12.667 16C13.5005 16 14.2744 15.7451 14.9151 15.309L16.061 16.4549C15.1846 17.1112 14.0962 17.5 12.917 17.5H12.417L12.2006 17.4956C9.40144 17.3821 7.16699 15.077 7.16699 12.25V11.75L7.16015 11.6482C7.11048 11.2822 6.79669 11 6.41699 11C6.00278 11 5.66699 11.3358 5.66699 11.75V12.25L5.67105 12.4863C5.78983 15.938 8.50022 18.7316 11.917 18.9818L11.917 21.25L11.9238 21.3518C11.9735 21.7178 12.2873 22 12.667 22C13.0812 22 13.417 21.6642 13.417 21.25L13.418 18.9817C14.8169 18.8791 16.0975 18.35 17.1301 17.5241L21.3864 21.7805C21.6793 22.0734 22.1542 22.0734 22.4471 21.7805C22.74 21.4876 22.74 21.0127 22.4471 20.7198L3.94733 2.21968ZM17.8631 14.0144L19.0091 15.1604C19.4308 14.2791 19.667 13.2921 19.667 12.25V11.75L19.6601 11.6482C19.6105 11.2822 19.2967 11 18.917 11C18.5028 11 18.167 11.3358 18.167 11.75V12.25L18.1626 12.4664C18.1407 13.0075 18.0368 13.5276 17.8631 14.0144ZM8.80467 4.95575L16.5971 12.7483C16.643 12.5059 16.667 12.2558 16.667 12V6C16.667 3.79086 14.8761 2 12.667 2C10.8191 2 9.26391 3.25302 8.80467 4.95575Z" />
<path d="M2.88667 2.21966C3.17956 1.92677 3.65444 1.92678 3.94733 2.21968L22.4471 20.7198C22.74 21.0127 22.74 21.4876 22.4471 21.7805C22.1542 22.0734 21.6793 22.0734 21.3864 21.7805L2.88666 3.28032C2.59377 2.98743 2.59377 2.51255 2.88667 2.21966Z" fill="#D92D20"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+2 -3
View File
@@ -1,4 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.25 11C18.6297 11 18.9435 11.2822 18.9932 11.6482L19 11.75V12.25C19 15.8094 16.245 18.7254 12.751 18.9817L12.75 21.25C12.75 21.6642 12.4142 22 12 22C11.6203 22 11.3065 21.7178 11.2568 21.3518L11.25 21.25L11.25 18.9818C7.83323 18.7316 5.12283 15.938 5.00406 12.4863L5 12.25V11.75C5 11.3358 5.33579 11 5.75 11C6.1297 11 6.44349 11.2822 6.49315 11.6482L6.5 11.75V12.25C6.5 15.077 8.73445 17.3821 11.5336 17.4956L11.75 17.5H12.25C15.077 17.5 17.3821 15.2656 17.4956 12.4664L17.5 12.25V11.75C17.5 11.3358 17.8358 11 18.25 11ZM12 2C14.2091 2 16 3.79086 16 6V12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12V6C8 3.79086 9.79086 2 12 2Z" fill="#78787C"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#78787C" xmlns="http://www.w3.org/2000/svg">
<path d="M18.25 11C18.6297 11 18.9435 11.2822 18.9932 11.6482L19 11.75V12.25C19 15.8094 16.245 18.7254 12.751 18.9817L12.75 21.25C12.75 21.6642 12.4142 22 12 22C11.6203 22 11.3065 21.7178 11.2568 21.3518L11.25 21.25L11.25 18.9818C7.83323 18.7316 5.12283 15.938 5.00406 12.4863L5 12.25V11.75C5 11.3358 5.33579 11 5.75 11C6.1297 11 6.44349 11.2822 6.49315 11.6482L6.5 11.75V12.25C6.5 15.077 8.73445 17.3821 11.5336 17.4956L11.75 17.5H12.25C15.077 17.5 17.3821 15.2656 17.4956 12.4664L17.5 12.25V11.75C17.5 11.3358 17.8358 11 18.25 11ZM12 2C14.2091 2 16 3.79086 16 6V12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12V6C8 3.79086 9.79086 2 12 2Z" />
</svg>

Before

Width:  |  Height:  |  Size: 771 B

After

Width:  |  Height:  |  Size: 759 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#039855" xmlns="http://www.w3.org/2000/svg">
<path d="M20 5C20.5523 5 21 5.44735 21 5.99917V19.0008C21 19.5527 20.5523 20 20 20C19.4477 20 19 19.5527 19 19.0008V5.99917C19 5.44735 19.4477 5 20 5ZM16 8C16.5523 8 17 8.44567 17 8.99543V19.0046C17 19.5543 16.5523 20 16 20C15.4477 20 15 19.5543 15 19.0046V8.99543C15 8.44567 15.4477 8 16 8ZM12 11C12.5523 11 13 11.4477 13 12V19C13 19.5523 12.5523 20 12 20C11.4477 20 11 19.5523 11 19V12C11 11.4477 11.4477 11 12 11ZM8 14C8.55228 14 9 14.4451 9 14.9942V19.0058C9 19.5549 8.55228 20 8 20C7.44772 20 7 19.5549 7 19.0058V14.9942C7 14.4451 7.44772 14 8 14ZM4 17C4.55228 17 5 17.4403 5 17.9836V19.0164C5 19.5597 4.55228 20 4 20C3.44772 20 3 19.5597 3 19.0164V17.9836C3 17.4403 3.44772 17 4 17Z" />
</svg>

After

Width:  |  Height:  |  Size: 799 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#78787C" xmlns="http://www.w3.org/2000/svg">
<path d="M15 4.25V19.7456C15 20.8243 13.7255 21.3965 12.9194 20.6797L8.42793 16.686C8.29063 16.5639 8.11329 16.4965 7.92956 16.4965H4.25C3.00736 16.4965 2 15.4891 2 14.2465V9.74859C2 8.50595 3.00736 7.49859 4.25 7.49859H7.92961C8.11333 7.49859 8.29065 7.43116 8.42794 7.30909L12.9195 3.31583C13.7255 2.59915 15 3.17138 15 4.25ZM18.9916 5.89733C19.3244 5.65079 19.7941 5.72077 20.0407 6.05362C21.2717 7.7157 22 9.7739 22 12C22 14.2261 21.2717 16.2843 20.0407 17.9464C19.7941 18.2793 19.3244 18.3492 18.9916 18.1027C18.6587 17.8562 18.5888 17.3865 18.8353 17.0536C19.8815 15.6411 20.5 13.8939 20.5 12C20.5 10.1062 19.8815 8.35896 18.8353 6.94641C18.5888 6.61356 18.6587 6.14387 18.9916 5.89733ZM17.143 8.36933C17.5072 8.17214 17.9624 8.30757 18.1596 8.67184C18.6958 9.66245 19 10.7968 19 12C19 13.2032 18.6958 14.3376 18.1596 15.3282C17.9624 15.6924 17.5072 15.8279 17.143 15.6307C16.7787 15.4335 16.6432 14.9783 16.8404 14.6141C17.2609 13.8373 17.5 12.9477 17.5 12C17.5 11.0523 17.2609 10.1627 16.8404 9.38593C16.6432 9.02167 16.7787 8.56652 17.143 8.36933Z" />
<path d="M2.88667 2.21966C3.17956 1.92677 3.65444 1.92678 3.94733 2.21968L22.4471 20.7198C22.74 21.0127 22.74 21.4876 22.4471 21.7805C22.1542 22.0734 21.6793 22.0734 21.3864 21.7805L2.88666 3.28032C2.59377 2.98743 2.59377 2.51255 2.88667 2.21966Z" fill="#D92D20"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+2 -2
View File
@@ -1,4 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 4.25V19.7456C15 20.8243 13.7255 21.3965 12.9194 20.6797L8.42793 16.686C8.29063 16.5639 8.11329 16.4965 7.92956 16.4965H4.25C3.00736 16.4965 2 15.4891 2 14.2465V9.74859C2 8.50595 3.00736 7.49859 4.25 7.49859H7.92961C8.11333 7.49859 8.29065 7.43116 8.42794 7.30909L12.9195 3.31583C13.7255 2.59915 15 3.17138 15 4.25ZM18.9916 5.89733C19.3244 5.65079 19.7941 5.72077 20.0407 6.05362C21.2717 7.7157 22 9.7739 22 12C22 14.2261 21.2717 16.2843 20.0407 17.9464C19.7941 18.2793 19.3244 18.3492 18.9916 18.1027C18.6587 17.8562 18.5888 17.3865 18.8353 17.0536C19.8815 15.6411 20.5 13.8939 20.5 12C20.5 10.1062 19.8815 8.35896 18.8353 6.94641C18.5888 6.61356 18.6587 6.14387 18.9916 5.89733ZM17.143 8.36933C17.5072 8.17214 17.9624 8.30757 18.1596 8.67184C18.6958 9.66245 19 10.7968 19 12C19 13.2032 18.6958 14.3376 18.1596 15.3282C17.9624 15.6924 17.5072 15.8279 17.143 15.6307C16.7787 15.4335 16.6432 14.9783 16.8404 14.6141C17.2609 13.8373 17.5 12.9477 17.5 12C17.5 11.0523 17.2609 10.1627 16.8404 9.38593C16.6432 9.02167 16.7787 8.56652 17.143 8.36933Z" fill="#78787C"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#78787C" xmlns="http://www.w3.org/2000/svg">
<path d="M15 4.25V19.7456C15 20.8243 13.7255 21.3965 12.9194 20.6797L8.42793 16.686C8.29063 16.5639 8.11329 16.4965 7.92956 16.4965H4.25C3.00736 16.4965 2 15.4891 2 14.2465V9.74859C2 8.50595 3.00736 7.49859 4.25 7.49859H7.92961C8.11333 7.49859 8.29065 7.43116 8.42794 7.30909L12.9195 3.31583C13.7255 2.59915 15 3.17138 15 4.25ZM18.9916 5.89733C19.3244 5.65079 19.7941 5.72077 20.0407 6.05362C21.2717 7.7157 22 9.7739 22 12C22 14.2261 21.2717 16.2843 20.0407 17.9464C19.7941 18.2793 19.3244 18.3492 18.9916 18.1027C18.6587 17.8562 18.5888 17.3865 18.8353 17.0536C19.8815 15.6411 20.5 13.8939 20.5 12C20.5 10.1062 19.8815 8.35896 18.8353 6.94641C18.5888 6.61356 18.6587 6.14387 18.9916 5.89733ZM17.143 8.36933C17.5072 8.17214 17.9624 8.30757 18.1596 8.67184C18.6958 9.66245 19 10.7968 19 12C19 13.2032 18.6958 14.3376 18.1596 15.3282C17.9624 15.6924 17.5072 15.8279 17.143 15.6307C16.7787 15.4335 16.6432 14.9783 16.8404 14.6141C17.2609 13.8373 17.5 12.9477 17.5 12C17.5 11.0523 17.2609 10.1627 16.8404 9.38593C16.6432 9.02167 16.7787 8.56652 17.143 8.36933Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="24" height="24" rx="12" fill="#12B76A"/>
<path d="M14.9995 8.50001C14.9995 8.29898 14.8791 8.11749 14.6939 8.03934C14.5086 7.96119 14.2946 8.00157 14.1506 8.14185L11.2239 10.9927H9.49951C8.67108 10.9927 7.99951 11.6643 7.99951 12.4927V15.4817C7.99951 16.3101 8.67108 16.9817 9.49951 16.9817H11.2227L14.1491 19.8567C14.2928 19.9978 14.5071 20.039 14.6929 19.9611C14.8786 19.8832 14.9995 19.7014 14.9995 19.5V8.50001ZM16.1108 11.1887C16.2827 10.9726 16.5972 10.9368 16.8133 11.1087L16.8142 11.1094L16.8151 11.1102L16.8173 11.1119L16.8226 11.1163L16.8376 11.1288C16.8494 11.1388 16.8647 11.1522 16.883 11.169C16.9196 11.2026 16.9684 11.2499 17.0252 11.3112C17.1387 11.4336 17.2852 11.613 17.4302 11.8514C17.7217 12.331 18.0038 13.0444 18.0038 13.9986C18.0038 14.9527 17.7217 15.6669 17.4304 16.1471C17.2855 16.3859 17.1392 16.5657 17.0258 16.6884C16.9691 16.7498 16.9203 16.7973 16.8837 16.8309C16.8641 16.849 16.8439 16.8666 16.8235 16.8838L16.815 16.8907L16.8142 16.8914C16.8142 16.8914 16.3679 17.1337 16.1115 16.8129C15.9399 16.5983 15.9738 16.2858 16.1866 16.1128L16.1882 16.1115L16.1874 16.1122L16.1891 16.1108L16.1882 16.1115C16.1911 16.109 16.1974 16.1036 16.2065 16.0952C16.2247 16.0784 16.2542 16.05 16.2913 16.0098C16.3658 15.9292 16.4699 15.8025 16.5754 15.6285C16.785 15.283 17.0038 14.7461 17.0038 13.9986C17.0038 13.2511 16.785 12.7152 16.5757 12.3709C16.4702 12.1974 16.3662 12.0712 16.2919 11.9911C16.2548 11.951 16.2254 11.9228 16.2072 11.9061C16.1981 11.8978 16.1919 11.8924 16.189 11.89L16.19 11.8907C15.9743 11.7187 15.939 11.4047 16.1108 11.1887ZM17.8126 9.10886C17.5965 8.93686 17.282 8.97255 17.11 9.18858C16.938 9.40451 16.9742 9.71932 17.1899 9.89138L17.201 9.9006C17.2118 9.90975 17.2293 9.92484 17.2526 9.94582C17.2993 9.98781 17.3688 10.0532 17.4537 10.1413C17.6238 10.3179 17.8536 10.5841 18.084 10.9351C18.5445 11.6364 19.0028 12.6685 19.0028 14.004C19.0028 15.3395 18.5445 16.3694 18.0843 17.0685C17.854 17.4184 17.6243 17.6835 17.4544 17.8593C17.3696 17.947 17.3001 18.0121 17.2535 18.0538C17.2302 18.0747 17.2127 18.0897 17.2019 18.0988L17.1903 18.1083L17.1894 18.1091C16.9741 18.2808 16.9381 18.5945 17.1092 18.8105C17.2807 19.0269 17.5959 19.0628 17.8124 18.8913L17.8453 18.8642C17.8639 18.8487 17.8893 18.8268 17.9208 18.7986C17.9837 18.7423 18.0705 18.6607 18.1734 18.5543C18.3788 18.3418 18.6495 18.0286 18.9196 17.6183C19.4603 16.797 20.0028 15.5788 20.0028 14.004C20.0028 12.4293 19.4603 11.2094 18.9199 10.3863C18.6499 9.97503 18.3793 9.66089 18.1741 9.4477C18.0713 9.34097 17.9844 9.25908 17.9216 9.20255C17.8902 9.17426 17.8647 9.15228 17.8462 9.13665L17.8238 9.11798L17.8167 9.11222L17.8143 9.11024L17.8126 9.10886ZM16.189 11.89L16.1876 11.8887L16.189 11.89Z" fill="white"/>
<rect x="2" y="2" width="24" height="24" rx="12" stroke="white" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

+6
View File
@@ -294,3 +294,9 @@ html.dark .slate-Combobox {
html.dark .slate-Combobox li:hover {
background: #333 !important;
}
/* remove focus border */
svg:focus {
border: none;
outline: none;
}
+32
View File
@@ -0,0 +1,32 @@
import clsx from 'clsx';
import React from 'react';
type Props = {
strength?: number
}
// 0:质量未知。
// 1:质量极好。
// 2:用户主观感觉和极好差不多,但码率可能略低于极好。
// 3:用户主观感受有瑕疵但不影响沟通。
// 4:勉强能沟通但不顺畅。
// 5:网络质量非常差,基本不能沟通。
// 6: 网络连接断开,完全无法沟通。
const Signal = ({ strength = 0 }: Props) => {
const finalStrength = 6 - strength;
const color = finalStrength <= 2 ? `bg-red-700` : (finalStrength == 3 ? `bg-yellow-700` : `bg-green-700`);
let bgColor = ` bg-gray-500`;
return (
<div className='w-6 h-6 flex-center'>
<div className="w-[18px] h-[15px] flex items-end gap-[2px]">
<span className={clsx('h-1/5 w-[2px]', finalStrength == 0 ? bgColor : color)}></span>
<span className={clsx('h-2/5 w-[2px]', finalStrength <= 1 ? bgColor : color)}></span>
<span className={clsx('h-3/5 w-[2px]', finalStrength <= 2 ? bgColor : color)}></span>
<span className={clsx('h-4/5 w-[2px]', finalStrength <= 3 ? bgColor : color)}></span>
<span className={clsx('h-full w-[2px]', finalStrength <= 4 ? bgColor : color)}></span>
</div>
</div>
);
};
export default Signal;
+236
View File
@@ -0,0 +1,236 @@
import AgoraRTC, { IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng';
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useGetAgoraConfigQuery, useGetAgoraVoicingListQuery, useLazyGetAgoraTokenQuery } from '../../app/services/server';
import { updateChannelVisibleAside } from '../../app/slices/channels';
import { addVoiceMember, removeVoiceMember, updateDeafenStatus, updateMuteStatus, updateVoicingInfo, updateVoicingMember, updateVoicingNetworkQuality, upsertVoiceList } from '../../app/slices/voice';
import { useAppSelector } from '../../app/store';
// type Props = {}
window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {};
// let tmpUids: number[] = [];
const Voice = () => {
const { isAdmin } = useAppSelector(store => {
return {
isAdmin: !!store.authData.user?.is_admin,
// joined: !!store.voice.voicing
};
});
const { data: agoraConfig } = useGetAgoraConfigQuery(undefined, {
skip: !isAdmin
});
useGetAgoraVoicingListQuery({
appid: agoraConfig?.app_id ?? "",
key: agoraConfig?.rtm_key ?? "",
secret: agoraConfig?.rtm_secret ?? "",
}, {
skip: !isAdmin || !agoraConfig,
pollingInterval: 5000
});
const dispatch = useDispatch();
useEffect(() => {
const initializeAgoraClient = async () => {
// 创建agora客户端实例
const agoraEngine = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
// 无论频道内是否有人说话,都会每两秒返回提示音量
agoraEngine.enableAudioVolumeIndicator();
// Listen for the "user-published" event to retrieve an AgoraRTCRemoteUser object.
agoraEngine.on("user-published", async (user, mediaType) => {
// Subscribe to the remote user when the SDK triggers the "user-published" event.
await agoraEngine.subscribe(user, mediaType);
console.log(user, " has published at the channel");
if (mediaType == "audio") {
// 播放远端音频
user.audioTrack?.play();
// const level = user.audioTrack?.getVolumeLevel();
// if (level === 0) {
// // 远端静音
// dispatch(updateVoicingMember({ uid: +user.uid, info: { muted: true } }));
// }
window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack;
}
agoraEngine.on("user-unpublished", (user) => {
// 远端用户取消了音频(muted)
dispatch(updateVoicingMember({ uid: +user.uid, info: { muted: true } }));
});
//remote user leave
agoraEngine.on("user-left", (user, reason) => {
switch (reason) {
case "Quit":
case "ServerTimeOut": {
dispatch(removeVoiceMember(+user.uid as number));
console.log(user, "has left the channel");
}
break;
default:
break;
}
});
// 报告频道内正在说话的远端用户及其音量的回调。
agoraEngine.on("volume-indicator", (vols) => {
vols.forEach((vol, index) => {
console.log(`${index} UID ${vol.uid} Level ${vol.level}`);
const { uid, level } = vol;
dispatch(updateVoicingMember({ uid: +uid, info: { speakingVolume: level } }));
});
});
// 信号强度
agoraEngine.on("network-quality", (qlt) => {
const { downlinkNetworkQuality } = qlt;
dispatch(updateVoicingNetworkQuality(downlinkNetworkQuality));
});
// 用户状态变化
agoraEngine.on("user-info-updated", (uid, msg) => {
console.log("user-info-updated", uid, msg);
switch (msg) {
case "mute-audio":
// todo
dispatch(updateVoicingMember({ uid: +uid, info: { muted: true } }));
break;
case "unmute-audio":
// todo
dispatch(updateVoicingMember({ uid: +uid, info: { muted: false } }));
break;
default:
break;
}
// const { downlinkNetworkQuality } = qlt;
// dispatch(updateVoicingNetworkQuality(downlinkNetworkQuality));
});
});
// 有新用户加入
agoraEngine.on("user-joined", async (user) => {
console.log(user.uid, !!localTrack, agoraEngine.channelName, " has joined the channel");
dispatch(addVoiceMember(+user.uid));
});
window.VOICE_CLIENT = agoraEngine;
};
if (!window.VOICE_CLIENT) {
initializeAgoraClient();
}
return () => {
if (window.VOICE_CLIENT && localTrack) {
localTrack.close();
localTrack = null;
window.VOICE_CLIENT.leave();
}
// window.VOICE_CLIENT=null
};
}, []);
return null;
};
let localTrack: IMicrophoneAudioTrack | null = null;
type VoiceProps = {
id: number,
context?: "channel" | "dm"
}
const useVoice = ({ id, context = "channel" }: VoiceProps) => {
const dispatch = useDispatch();
const { voicingInfo } = useAppSelector(store => {
return {
// loginUid: store.authData.user?.uid,
voicingInfo: store.voice.voicing
};
});
const [generateToken] = useLazyGetAgoraTokenQuery();
// const [joining, setJoining] = useState(false);
const joinVoice = async () => {
// setJoining(true);
dispatch(updateVoicingInfo({
id,
context,
joining: true
}));
const { isError, data } = await generateToken(id);
if (!isError && data) {
const { channel_name, app_id, agora_token, uid } = data;
if (window.VOICE_CLIENT) {
await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid);
console.table(data);
// Create a local audio track from the microphone audio.
localTrack = await AgoraRTC.createMicrophoneAudioTrack();
// Publish the local audio track in the channel.
await window.VOICE_CLIENT.publish(localTrack);
console.log("Publish success!,joined the channel");
dispatch(updateVoicingInfo({
deafen: false,
muted: false,
joining: false,
id,
context,
}));
// 把自己加进去
dispatch(addVoiceMember(uid));
}
} else {
console.error("generate agora token error");
dispatch(updateVoicingInfo({
joining: false,
id,
context,
}));
}
// setJoining(false);
};
const leave = async () => {
if (window.VOICE_CLIENT && localTrack) {
localTrack.close();
localTrack = null;
await window.VOICE_CLIENT.leave();
dispatch(updateVoicingInfo(null));
if (context == "channel") {
dispatch(updateChannelVisibleAside({
id, aside: null
}));
dispatch(upsertVoiceList({
id,
context,
memberCount: 0
}));
}
}
};
const setMute = (mute: boolean) => {
if (localTrack) {
localTrack.setMuted(mute);
dispatch(updateMuteStatus(mute));
}
};
const setDeafen = (deafen: boolean) => {
if (localTrack) {
if (deafen) {
localTrack.setMuted(true);
// 远端音频,全部静音
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => {
audioTrack?.setVolume(0);
});
} else {
localTrack.setMuted(false);
// 远端音频,恢复原音
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => {
audioTrack?.setVolume(100);
});
}
dispatch(updateDeafenStatus(deafen));
}
};
const joinedAtThisContext = voicingInfo ? (voicingInfo.id == id && voicingInfo.context == context) : false;
return {
setMute,
setDeafen,
leave,
// canVoice,
voicingInfo,
joining: voicingInfo ? voicingInfo.joining : undefined,
joinedAtThisContext,
joined: !!voicingInfo,
joinVoice
};
};
export { useVoice };
export default Voice;
+1
View File
@@ -20,6 +20,7 @@ interface Props
| "pattern"
| "disabled"
| "minLength"
| "spellCheck"
>,
HTMLInputElement
> {
+15 -5
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, memo } from "react";
import { useEffect, memo } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import Tippy from "@tippyjs/react";
import { useTranslation } from "react-i18next";
@@ -16,6 +16,9 @@ import IconPin from "../../../assets/icons/pin.svg";
import { useAppSelector } from "../../../app/store";
import GoBackNav from "../../../common/component/GoBackNav";
import Members from "./Members";
import VoiceChat from "../VoiceChat";
import { updateChannelVisibleAside } from "../../../app/slices/channels";
import Dashboard from "../VoiceChat/Dashboard";
type Props = {
cid?: number;
dropFiles?: File[];
@@ -25,7 +28,6 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
const { pathname } = useLocation();
const navigate = useNavigate();
const dispatch = useDispatch();
const [membersVisible, setMembersVisible] = useState(true);
const {
userIds,
@@ -52,7 +54,10 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
}, [pathname]);
const toggleMembersVisible = () => {
setMembersVisible((prev) => !prev);
dispatch(updateChannelVisibleAside({
id: cid,
aside: data.visibleAside !== "members" ? "members" : null
}));
};
@@ -83,6 +88,8 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
</li>
</Tippy>
</Tooltip>
{/* 音频 暂时只有admin有入口 */}
{loginUser?.is_admin && <VoiceChat context={`channel`} id={cid} />}
<Tooltip tip={t("fav")} placement="left">
<Tippy
placement="left-start"
@@ -102,7 +109,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
onClick={toggleMembersVisible}
>
<Tooltip tip={t("channel_members")} placement="left">
<IconPeople className={membersVisible ? "fill-gray-600" : ""} />
<IconPeople className={data.visibleAside == "members" ? "fill-gray-600" : ""} />
</Tooltip>
</li>
</ul>
@@ -118,7 +125,10 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
</header>
}
users={
<Members uids={memberIds} addVisible={addVisible} cid={cid} ownerId={owner} membersVisible={membersVisible} />
<Members uids={memberIds} addVisible={addVisible} cid={cid} ownerId={owner} membersVisible={data.visibleAside == "members"} />
}
voice={
<Dashboard visible={data.visibleAside == "voice"} id={cid} context="channel" />
}
/>;
}
+3
View File
@@ -23,6 +23,7 @@ interface Props {
header: ReactElement;
aside?: ReactElement | null;
users?: ReactElement | null;
voice?: ReactElement | null;
dropFiles?: File[];
context: "channel" | "user";
to: number;
@@ -33,6 +34,7 @@ const Layout: FC<Props> = ({
header,
aside = null,
users = null,
voice = null,
dropFiles = [],
context = "channel",
to
@@ -116,6 +118,7 @@ const Layout: FC<Props> = ({
</main>
{aside && <div className={clsx("z-50 p-3 absolute right-0 top-0 translate-x-full flex-col hidden md:flex")}>{aside}</div>}
{users && <div className="hidden md:block">{users}</div>}
{voice && <div className="hidden md:block">{voice}</div>}
{!readonly && inputMode == "text" && isActive && (
<DnDTip context={context} name={name} />
)}
+74
View File
@@ -0,0 +1,74 @@
// import React from 'react';
import { useAppSelector } from '../../app/store';
import User from '../../common/component/User';
import IconMic from '../../assets/icons/mic.on.svg';
import IconMicOff from '../../assets/icons/mic.off.svg';
import IconCallOff from '../../assets/icons/headphone.svg';
import IconSoundOn from '../../assets/icons/sound.on.svg';
import IconSoundOff from '../../assets/icons/sound.off.svg';
import { useVoice } from '../../common/component/Voice';
import Signal from '../../common/component/Signal';
import Tooltip from '../../common/component/Tooltip';
import { useTranslation } from 'react-i18next';
type Props = {
id: number,
context?: "channel" | "dm"
}
const RTCWidget = ({ id, context = "channel" }: Props) => {
const { t } = useTranslation("chat");
const { leave, voicingInfo, setMute, setDeafen, joining = true } = useVoice({ context, id });
const { loginUser, channelData, userData } = useAppSelector(store => {
return {
userData: store.users.byId,
channelData: store.channels.byId,
loginUser: store.authData.user,
};
});
if (!loginUser || !voicingInfo || joining) return null;
const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name;
if (!name) return null;
return (
<div className='bg-gray-100 dark:bg-gray-900 flex flex-col p-2 rounded-3xl m-3 text-sm'>
{/* {voicingInfo && */}
<div className="flex justify-between items-center border-b border-b-gray-200 dark:border-b-gray-800 pb-2">
<div className="flex flex-1 items-center gap-1">
<Signal strength={voicingInfo.downlinkNetworkQuality} />
<div className="flex flex-col">
<span className='text-green-800 font-bold'>Voice Connected</span>
<span className='text-gray-600 dark:text-gray-400 text-xs truncate max-w-[170px]' >{voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id].name : userData[voicingInfo.id].name}</span>
</div>
</div>
<Tooltip tip={t("leave_voice")} placement="top">
<IconCallOff onClick={leave} role="button" className="fill-red-600" />
</Tooltip>
</div>
{/* } */}
<div className="flex justify-between items-center pt-2">
<div className="flex items-center gap-3">
<User uid={loginUser.uid} compact />
<div className="flex flex-col">
<span className='dark:text-white text-sm font-bold'>{loginUser.name}</span>
<span className='text-gray-400 text-xs'>#{loginUser.uid}</span>
</div>
</div>
{/* {voicingInfo && */}
<div className="flex gap-2 px-1">
<Tooltip tip={voicingInfo.muted ? t("undeafen") : t("deafen")} placement="top">
{voicingInfo.deafen ? <IconSoundOff role="button" onClick={setDeafen.bind(null, false)} /> : <IconSoundOn role="button" onClick={setDeafen.bind(null, true)} />}
</Tooltip>
<Tooltip tip={voicingInfo.muted ? t("unmute") : t("mute")} placement="top">
{voicingInfo.muted ?
<IconMicOff onClick={setMute.bind(null, false)} role="button" />
:
<IconMic onClick={setMute.bind(null, true)} role="button" />}
</Tooltip>
</div>
{/* } */}
</div>
</div>
);
};
export default RTCWidget;
+15 -11
View File
@@ -9,26 +9,27 @@ import getUnreadCount, { renderPreviewMessage } from "../utils";
import User from "../../../common/component/User";
import Avatar from "../../../common/component/Avatar";
import IconLock from "../../../assets/icons/lock.svg";
import IconVoicing from "../../../assets/icons/voicing.svg";
import useContextMenu from "../../../common/hook/useContextMenu";
import useUploadFile from "../../../common/hook/useUploadFile";
import { useAppSelector } from "../../../app/store";
import { fromNowTime } from "../../../common/utils";
interface IProps {
type?: "user" | "channel";
type?: "dm" | "channel";
id: number;
mid: number;
setDeleteChannelId: (param: number) => void;
setInviteChannelId: (param: number) => void;
}
const Session: FC<IProps> = ({
type = "user",
type = "dm",
id,
mid,
setDeleteChannelId,
setInviteChannelId
}) => {
const navPath = type == "user" ? `/chat/dm/${id}` : `/chat/channel/${id}`;
const navPath = type == "dm" ? `/chat/dm/${id}` : `/chat/channel/${id}`;
// const { pathname } = useLocation();
const isCurrentPath = useMatch(navPath);
const navigate = useNavigate();
@@ -45,7 +46,7 @@ const Session: FC<IProps> = ({
return { size, type, name, url };
});
addStageFile(filesData);
navigate(type == "user" ? `/chat/dm/${id}` : `/chat/channel/${id}`);
navigate(type == "dm" ? `/chat/dm/${id}` : `/chat/channel/${id}`);
}
},
collect: (monitor) => ({
@@ -61,23 +62,24 @@ const Session: FC<IProps> = ({
mid: number;
is_public: boolean;
}>();
const { messageData, userData, channelData, readIndex, loginUid, mids, muted } = useAppSelector(
const { messageData, userData, channelData, readIndex, loginUid, mids, muted, voiceList } = useAppSelector(
(store) => {
return {
mids: type == "user" ? store.userMessage.byId[id] : store.channelMessage[id],
voiceList: store.voice.list,
mids: type == "dm" ? store.userMessage.byId[id] : store.channelMessage[id],
loginUid: store.authData.user?.uid || 0,
readIndex:
type == "user" ? store.footprint.readUsers[id] : store.footprint.readChannels[id],
type == "dm" ? store.footprint.readUsers[id] : store.footprint.readChannels[id],
messageData: store.message,
userData: store.users.byId,
channelData: store.channels.byId,
muted: type == "user" ? store.footprint.muteUsers[id] : store.footprint.muteChannels[id]
muted: type == "dm" ? store.footprint.muteUsers[id] : store.footprint.muteChannels[id]
};
}
);
useEffect(() => {
const tmp = type == "user" ? userData[id] : channelData[id];
const tmp = type == "dm" ? userData[id] : channelData[id];
if (!tmp) return;
if ("avatar" in tmp) {
// user
@@ -98,6 +100,7 @@ const Session: FC<IProps> = ({
messageData,
loginUid
});
const isVoicing = type == "channel" && voiceList.findIndex(item => item.id == id) > -1;
return (
<li className="session">
<ContextMenu
@@ -115,8 +118,8 @@ const Session: FC<IProps> = ({
to={navPath}
onContextMenu={handleContextMenuEvent}
>
<div className="flex shrink-0">
{type == "user" ? (
<div className="flex shrink-0 relative">
{type == "dm" ? (
<User avatarSize={40} compact interactive={false} uid={id} />
) : (
<Avatar
@@ -128,6 +131,7 @@ const Session: FC<IProps> = ({
src={icon}
/>
)}
{isVoicing && <IconVoicing className="-top-0.5 -right-0.5 absolute w-[18px] h-[18px]" />}
</div>
<div className="w-full flex flex-col justify-between overflow-hidden">
<div className="flex items-center justify-between ">
+4 -4
View File
@@ -6,7 +6,7 @@ import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal";
import InviteModal from "../../../common/component/InviteModal";
import { useAppSelector } from "../../../app/store";
export interface ChatSession {
type: "user" | "channel";
type: "dm" | "channel";
id: number;
mid: number;
unread: number;
@@ -46,11 +46,11 @@ const SessionList: FC<Props> = ({ tempSession }) => {
// console.log("adddd", id);
const mids = userMessage[id];
if (!mids || mids.length == 0) {
return { unreads: 0, id, type: "user" };
return { unreads: 0, id, type: "dm" };
}
// 先转换成数字,再排序
const mid = [...mids].sort((a, b) => +a - +b).pop();
return { type: "user", id, mid };
return { type: "dm", id, mid };
});
const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort((a, b) => {
const { mid: aMid = 0 } = a;
@@ -74,7 +74,7 @@ const SessionList: FC<Props> = ({ tempSession }) => {
return (
<>
<ul ref={ref} className="flex flex-col gap-0.5 p-2 overflow-auto">
<ul ref={ref} className="flex flex-1 flex-col gap-0.5 p-2 overflow-auto">
<ViewportList
initialPrerender={10}
viewportRef={ref}
+22
View File
@@ -0,0 +1,22 @@
// import { useEffect } from 'react';
// import { useDispatch } from 'react-redux';
import VoiceManagement from './VoiceManagement';
import { useVoice } from '../../../common/component/Voice';
type Props = {
visible: boolean,
context?: "channel" | "dm",
id: number,
}
const Dashboard = ({ context = "channel", id, visible }: Props) => {
const { voicingInfo, setMute, setDeafen, leave } = useVoice({ id, context });
// const dispatch = useDispatch();
return <div className={`h-full flex-col gap-1 w-[226px] overflow-y-scroll overflow-x-hidden p-2 shadow-[inset_1px_0px_0px_rgba(0,_0,_0,_0.1)] ${visible ? "flex" : "hidden"}`}>
<VoiceManagement info={voicingInfo} setMute={setMute} setDeafen={setDeafen} leave={leave} />
</div>;
};
export default Dashboard;
@@ -0,0 +1,90 @@
import clsx from 'clsx';
import React from 'react';
import { VoicingInfo } from '../../../app/slices/voice';
import { useAppSelector } from '../../../app/store';
import Avatar from '../../../common/component/Avatar';
import IconHeadphone from '../../../assets/icons/sound.on.svg';
import IconHeadphoneOff from '../../../assets/icons/sound.off.svg';
import IconMic from '../../../assets/icons/mic.on.svg';
import IconMicOff from '../../../assets/icons/mic.off.svg';
import StyledButton from '../../../common/component/styled/Button';
import IconCallOff from '../../../assets/icons/call.off.svg';
// import User from '../../../common/component/User';
type Props = {
info: VoicingInfo | null,
setMute: (param: boolean) => void,
setDeafen: (param: boolean) => void,
leave: () => void
}
const VoiceManagement = ({ info, setMute, setDeafen, leave }: Props) => {
const { userData, voicingMembers } = useAppSelector(store => {
return {
userData: store.users.byId,
voicingMembers: store.voice.voicingMembers
};
});
if (!info) return null;
const { deafen, muted } = info;
const nameClass = clsx(`text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white`);
const members = voicingMembers.ids;
const membersData = voicingMembers.byId;
if (info.joining) {
return <div className='w-full h-full flex-center p-1 text-sm text-gray-600 dark:text-gray-400'>
Connecting to voice channel...
</div>;
}
return (
<div className='w-full h-full py-2 flex flex-col'>
<ul className='flex grow flex-col'>
{members.map((uid) => {
const curr = userData[uid];
if (!curr) return null;
const { muted, speakingVolume = 0 } = membersData[uid];
const speaking = speakingVolume > 50;
return <li key={uid} className="flex items-center justify-between gap-6 pb-4 ">
<div className="flex items-center gap-2 transition-opacity" style={{ opacity: `${speaking ? 0.4 : 1}` }}>
<div className="w-8 h-8 flex shrink-0">
<Avatar
width={32}
height={32}
className="w-full h-full rounded-full object-cover"
src={curr.avatar}
name={curr.name}
alt="avatar"
/>
</div>
<span className={nameClass} title={curr?.name}>
{curr?.name}
</span>
</div>
<div className="flex items-center gap-2">
{/* {deafen ? <IconHeadphoneOff className="w-4" /> : <IconHeadphone className="w-4" />} */}
{muted ? <IconMicOff className="w-4 fill-gray-500" /> : <IconMic className="w-4 fill-gray-500" />}
</div>
{/* <User uid={uid} interactive={false} /> */}
{/* {userData[uid]?.name} */}
</li>;
})}
</ul>
<div className="flex flex-col gap-2">
<ul className='flex justify-between'>
<li role={"button"} onClick={setDeafen.bind(null, !deafen)} className="py-2 px-3 rounded bg-gray-100 dark:bg-gray-900">
{deafen ? <IconHeadphoneOff className="fill-gray-700 dark:fill-gray-300" /> : <IconHeadphone className="fill-gray-700 dark:fill-gray-300" />}
</li>
<li role={"button"} onClick={setMute.bind(null, !muted)} className="py-2 px-3 rounded bg-gray-100 dark:bg-gray-900">
{muted ? <IconMicOff className="fill-gray-700 dark:fill-gray-300" /> : <IconMic className="fill-gray-700 dark:fill-gray-300" />}
</li>
</ul>
<StyledButton onClick={leave} className='bg-red-600 hover:!bg-red-700 text-center'>
<IconCallOff className="m-auto" />
</StyledButton>
</div>
</div>
);
};
export default VoiceManagement;
+66
View File
@@ -0,0 +1,66 @@
// import React from 'react';
// import Tippy from '@tippyjs/react';
// import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { updateChannelVisibleAside } from '../../../app/slices/channels';
import { useAppSelector } from '../../../app/store';
import IconHeadphone from '../../../assets/icons/headphone.svg';
import Tooltip from '../../../common/component/Tooltip';
import { useVoice } from '../../../common/component/Voice';
type Props = {
context?: "channel" | "dm"
id: number,
}
const VoiceChat = ({ id, context = "channel" }: Props) => {
const { joinVoice, joined, joining = false, joinedAtThisContext } = useVoice({ id, context });
const dispatch = useDispatch();
const { loginUser, contextData, voiceList } = useAppSelector(store => {
return {
loginUser: store.authData.user,
contextData: context == "channel" ? store.channels.byId[id] : store.users.byId[id],
voiceList: store.voice.list
};
});
const { t } = useTranslation("chat");
const toggleDashboard = () => {
if (context == "channel") {
dispatch(updateChannelVisibleAside({
id,
aside: contextData.visibleAside == "voice" ? null : "voice"
}));
}
// todo DM
};
const handleJoin = () => {
if (joining || joined) {
alert("You have joined another channel, please leave first!");
return;
}
joinVoice();
if (context == "channel") {
dispatch(updateChannelVisibleAside({
id,
aside: "voice"
}));
}
};
if (!loginUser) return null;
const visible = contextData.visibleAside == "voice";
const memberCount = voiceList.find((v) => v.context == context && v.id == id)?.memberCount ?? 0;
const badgeClass = `absolute -top-2 -right-2 w-4 h-4 rounded-full bg-primary-400 text-white `;
return (
<Tooltip disabled={visible} tip={t("voice")} placement="left">
<li className={`relative group`} >
<IconHeadphone className={visible ? "fill-gray-600" : "fill-gray-500"} role="button" onClick={joinedAtThisContext ? toggleDashboard : handleJoin} />
{visible ? null : memberCount > 0 ? <span className={`${badgeClass} flex-center font-bold text-[10px]`}>{memberCount}</span> : <span className={`${badgeClass} hidden text-xs group-hover:flex-center`}>
<em className='not-italic'>+</em>
</span>}
</li>
</Tooltip>
);
};
export default VoiceChat;
+7 -3
View File
@@ -14,6 +14,7 @@ import GuestBlankPlaceholder from "./GuestBlankPlaceholder";
import GuestChannelChat from "./GuestChannelChat";
import GuestSessionList from "./GuestSessionList";
import ErrorCatcher from "../../common/component/ErrorCatcher";
import RTCWidget from "./RTCWidget";
function ChatPage() {
const isHomePath = useMatch(`/`);
@@ -43,12 +44,14 @@ function ChatPage() {
mid: 0,
unread: 0,
id: +user_id,
type: "user" as const
type: "dm" as const
}
: undefined;
// console.log("temp uid", tmpUid);
const placeholderVisible = channel_id == 0 && user_id == 0;
const isMainPath = isHomePath || isChatHomePath;
const context = channel_id !== 0 ? "channel" : "dm";
const contextId = (channel_id || user_id) ?? 0;
return (
<ErrorCatcher>
{channelModalVisible && (
@@ -62,9 +65,10 @@ function ChatPage() {
)}>
<Server readonly={isGuest} />
{isGuest ? <GuestSessionList /> : <SessionList tempSession={tmpSession} />}
{isGuest && <footer className="hidden md:block py-1 text-xs text-gray-300 dark:text-gray-700 text-center">
{isGuest ? <footer className="hidden md:block py-1 text-xs text-gray-300 dark:text-gray-700 text-center">
Host your own <a href="https://voce.chat" target="_blank" rel="noopener noreferrer" className="text-gray-400 dark:text-gray-600">voce.chat</a>
</footer>}
</footer> : <RTCWidget id={+contextId} context={context} />}
</div>
<div className={clsx(`right-container md:rounded-r-2xl w-full bg-white dark:!bg-gray-700`, placeholderVisible && "h-full flex-center", isMainPath && "hidden md:flex")}>
{placeholderVisible && (isGuest ? <GuestBlankPlaceholder /> : <BlankPlaceholder />)}
+4 -2
View File
@@ -19,6 +19,7 @@ import MobileNavs from "./MobileNavs";
import { updateRememberedNavs } from "../../app/slices/ui";
import UnreadTabTip from "../../common/component/UnreadTabTip";
import ReLoginModal from "../../common/component/ReLoginModal";
import Voice from "../../common/component/Voice";
function HomePage() {
@@ -29,7 +30,7 @@ function HomePage() {
const { pathname } = useLocation();
const {
roleChanged,
loginUid,
loginUser: { uid: loginUid, is_admin: isAdmin },
guest,
ui: {
ready,
@@ -38,7 +39,7 @@ function HomePage() {
} = useAppSelector((store) => {
return {
ui: store.ui,
loginUid: store.authData.user?.uid,
loginUser: store.authData.user ?? { uid: 0, is_admin: false },
guest: store.authData.guest,
roleChanged: store.authData.roleChanged
};
@@ -67,6 +68,7 @@ function HomePage() {
<>
{roleChanged && <ReLoginModal />}
{!guest && <UnreadTabTip />}
{!guest && isAdmin && <Voice />}
<Manifest />
{!guest && <Notification />}
<div className={`vocechat-container flex w-screen h-screen bg-gray-200 dark:bg-gray-900`}>
+5 -1
View File
@@ -54,8 +54,8 @@ export default function ConfigAgora() {
<div className="input">
<Label htmlFor="project_id">Project ID</Label>
<Input
spellCheck={false}
disabled={!enabled}
// type={"number"}
data-type="project_id"
onChange={handleChange}
value={project_id}
@@ -66,6 +66,7 @@ export default function ConfigAgora() {
<div className="input">
<Label htmlFor="app_id">App ID</Label>
<Input
spellCheck={false}
disabled={!enabled}
data-type="app_id"
onChange={handleChange}
@@ -77,6 +78,7 @@ export default function ConfigAgora() {
<div className="input">
<Label htmlFor="app_certificate">APP Certificate</Label>
<Input
spellCheck={false}
disabled={!enabled}
data-type="app_certificate"
onChange={handleChange}
@@ -88,6 +90,7 @@ export default function ConfigAgora() {
<div className="input">
<Label htmlFor="rtm_key">RTM Key</Label>
<Textarea
spellCheck={false}
disabled={!enabled}
data-type="rtm_key"
onChange={handleChange}
@@ -99,6 +102,7 @@ export default function ConfigAgora() {
<div className="input">
<Label htmlFor="rtm_secret">RTM Secret</Label>
<Textarea
spellCheck={false}
disabled={!enabled}
data-type="rtm_secret"
onChange={handleChange}
+5 -5
View File
@@ -11,7 +11,7 @@ import BotConfig from "./BotConfig";
import APIDocument from "./APIDocument";
import ManageMembers from "../../common/component/ManageMembers";
import Version from "../../common/component/Version";
// import ConfigAgora from "./config/Agora";
import ConfigAgora from "./config/Agora";
import { useAppSelector } from "../../app/store";
import ServerVersionChecker from "../../common/component/ServerVersionChecker";
@@ -46,10 +46,10 @@ const navs = [
name: "firebase",
component: <ConfigFirebase />
},
// {
// name: "agora",
// component: <ConfigAgora />
// },
{
name: "agora",
component: <ConfigAgora />
},
{
name: "smtp",
component: <ConfigSMTP />
+6
View File
@@ -1,3 +1,5 @@
import { IAgoraRTCClient, IMicrophoneAudioTrack, IRemoteAudioTrack } from "agora-rtc-sdk-ng";
interface BeforeInstallPromptEvent extends Event {
/**
* Returns an array of DOMString items containing the platforms on which the event was dispatched.
@@ -29,6 +31,10 @@ export declare global {
__WB_MANIFEST: Array<PrecacheEntry | string>;
skipWaiting: () => void;
CACHE: { [key: string]: typeof localforage | undefined };
VOICE_CLIENT?: IAgoraRTCClient;
VOICE_TRACK_MAP: {
[key: number]: IRemoteAudioTrack | IMicrophoneAudioTrack | undefined | null
}
}
interface WindowEventMap {
beforeinstallprompt: BeforeInstallPromptEvent;
+14
View File
@@ -27,6 +27,20 @@ export interface AgoraConfig {
rtm_key: string;
rtm_secret: string;
}
export interface AgoraVoicingListResponse {
success: boolean,
data: {
channels: { channel_name: string, user_count: number }[],
total_size: number
}
}
export interface AgoraTokenResponse {
agora_token: string,
uid: number,
channel_name: string,
expired_in: number,
app_id: string;
}
export interface SMTPConfig {
enabled: boolean;
host: string;
+256 -32
View File
@@ -1242,11 +1242,31 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/eslintrc@^2.0.2":
version "2.0.2"
resolved "https://mirrors.cloud.tencent.com/npm/@eslint/eslintrc/-/eslintrc-2.0.2.tgz#01575e38707add677cf73ca1589abba8da899a02"
integrity sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^9.5.1"
globals "^13.19.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.0"
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.36.0":
version "8.36.0"
resolved "https://mirrors.cloud.tencent.com/npm/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe"
integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==
"@eslint/js@8.37.0":
version "8.37.0"
resolved "https://mirrors.cloud.tencent.com/npm/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d"
integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==
"@firebase/analytics-compat@0.2.4":
version "0.2.4"
resolved "https://mirrors.cloud.tencent.com/npm/@firebase/analytics-compat/-/analytics-compat-0.2.4.tgz#3887676286ead7b30f9880581e0144f43bc71f16"
@@ -2158,10 +2178,10 @@
redux-thunk "^2.4.2"
reselect "^4.1.7"
"@remix-run/router@1.4.0":
version "1.4.0"
resolved "https://mirrors.cloud.tencent.com/npm/@remix-run/router/-/router-1.4.0.tgz#74935d538e4df8893e47831a7aea362f295bcd39"
integrity sha512-BJ9SxXux8zAg991UmT8slpwpsd31K1dHHbD3Ba4VzD+liLQ4WAMSxQp2d2ZPRPfN0jN2NPRowcSSoM7lCaF08Q==
"@remix-run/router@1.5.0":
version "1.5.0"
resolved "https://mirrors.cloud.tencent.com/npm/@remix-run/router/-/router-1.5.0.tgz#57618e57942a5f0131374a9fdb0167e25a117fdc"
integrity sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==
"@rollup/plugin-babel@^5.2.0":
version "5.3.1"
@@ -2726,11 +2746,16 @@
resolved "https://mirrors.cloud.tencent.com/npm/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^18.15.10":
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
version "18.15.10"
resolved "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-18.15.10.tgz#4ee2171c3306a185d1208dad5f44dae3dee4cfe3"
integrity sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==
"@types/node@^18.15.11":
version "18.15.11"
resolved "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"
integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://mirrors.cloud.tencent.com/npm/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -2791,10 +2816,10 @@
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@^18.0.30":
version "18.0.30"
resolved "https://mirrors.cloud.tencent.com/npm/@types/react/-/react-18.0.30.tgz#83944e679fc7aeab3f042b76d63c4d755b56b7c4"
integrity sha512-AnME2cHDH11Pxt/yYX6r0w448BfTwQOLEhQEjCdwB7QskEI7EKtxhGUsExTQe/MsY3D9D5rMtu62WRocw9A8FA==
"@types/react@^18.0.31":
version "18.0.31"
resolved "https://mirrors.cloud.tencent.com/npm/@types/react/-/react-18.0.31.tgz#a69ef8dd7bfa849734d258c793a8fe343a338205"
integrity sha512-EEG67of7DsvRDU6BLLI0p+k1GojDLz9+lZsnCpCRTa/lOokvyPBvp8S5x+A24hME3yyQuIipcP70KJ6H7Qupww==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -3875,6 +3900,18 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"
agora-rtc-sdk-ng@^4.17.0:
version "4.17.0"
resolved "https://mirrors.cloud.tencent.com/npm/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.17.0.tgz#5824fcd2613a82ab9e946d451007d0f35b0b91a2"
integrity sha512-TmqnvWr7J9cVm62KzE3jhBjVcQpcvxj0GoSMHUylce5GWZrN6rkDLgcPNvhNmPPK5Slqwsx/d70FrWM7T3ngQw==
dependencies:
agora-rte-extension "^1.2.3"
agora-rte-extension@^1.2.3:
version "1.2.3"
resolved "https://mirrors.cloud.tencent.com/npm/agora-rte-extension/-/agora-rte-extension-1.2.3.tgz#979b96df0146300296f9f37212ffa67656c698ef"
integrity sha512-k3yNrYVyzJRoQJjaJUktKUI1XRtf8J1XsW8OzYKFqGlS8WQRMsES1+Phj2rfuEriiLObfuyuCimG6KHQCt5tiw==
ajv-formats@^2.1.1:
version "2.1.1"
resolved "https://mirrors.cloud.tencent.com/npm/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
@@ -3960,6 +3997,11 @@ ansi-styles@^6.0.0:
resolved "https://mirrors.cloud.tencent.com/npm/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
any-promise@^1.0.0:
version "1.3.0"
resolved "https://mirrors.cloud.tencent.com/npm/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
anymatch@^3.0.3, anymatch@~3.1.2:
version "3.1.3"
resolved "https://mirrors.cloud.tencent.com/npm/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
@@ -4731,6 +4773,11 @@ commander@^2.18.0, commander@^2.20.0:
resolved "https://mirrors.cloud.tencent.com/npm/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^4.0.0:
version "4.1.1"
resolved "https://mirrors.cloud.tencent.com/npm/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
commander@^7.2.0:
version "7.2.0"
resolved "https://mirrors.cloud.tencent.com/npm/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
@@ -5978,6 +6025,11 @@ eslint-visitor-keys@^3.3.0:
resolved "https://mirrors.cloud.tencent.com/npm/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint-visitor-keys@^3.4.0:
version "3.4.0"
resolved "https://mirrors.cloud.tencent.com/npm/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc"
integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==
eslint-webpack-plugin@^3.1.1:
version "3.2.0"
resolved "https://mirrors.cloud.tencent.com/npm/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz#1978cdb9edc461e4b0195a20da950cf57988347c"
@@ -5989,7 +6041,7 @@ eslint-webpack-plugin@^3.1.1:
normalize-path "^3.0.0"
schema-utils "^4.0.0"
eslint@^8.3.0, eslint@^8.36.0:
eslint@^8.3.0:
version "8.36.0"
resolved "https://mirrors.cloud.tencent.com/npm/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf"
integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==
@@ -6035,6 +6087,52 @@ eslint@^8.3.0, eslint@^8.36.0:
strip-json-comments "^3.1.0"
text-table "^0.2.0"
eslint@^8.37.0:
version "8.37.0"
resolved "https://mirrors.cloud.tencent.com/npm/eslint/-/eslint-8.37.0.tgz#1f660ef2ce49a0bfdec0b0d698e0b8b627287412"
integrity sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.4.0"
"@eslint/eslintrc" "^2.0.2"
"@eslint/js" "8.37.0"
"@humanwhocodes/config-array" "^0.11.8"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
eslint-scope "^7.1.1"
eslint-visitor-keys "^3.4.0"
espree "^9.5.1"
esquery "^1.4.2"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
find-up "^5.0.0"
glob-parent "^6.0.2"
globals "^13.19.0"
grapheme-splitter "^1.0.4"
ignore "^5.2.0"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
js-sdsl "^4.1.4"
js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash.merge "^4.6.2"
minimatch "^3.1.2"
natural-compare "^1.4.0"
optionator "^0.9.1"
strip-ansi "^6.0.1"
strip-json-comments "^3.1.0"
text-table "^0.2.0"
espree@^9.5.0:
version "9.5.0"
resolved "https://mirrors.cloud.tencent.com/npm/espree/-/espree-9.5.0.tgz#3646d4e3f58907464edba852fa047e6a27bdf113"
@@ -6044,6 +6142,15 @@ espree@^9.5.0:
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.3.0"
espree@^9.5.1:
version "9.5.1"
resolved "https://mirrors.cloud.tencent.com/npm/espree/-/espree-9.5.1.tgz#4f26a4d5f18905bf4f2e0bd99002aab807e96dd4"
integrity sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==
dependencies:
acorn "^8.8.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.4.0"
esprima@^4.0.0, esprima@^4.0.1:
version "4.0.1"
resolved "https://mirrors.cloud.tencent.com/npm/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
@@ -6627,6 +6734,18 @@ glob-to-regexp@^0.4.1:
resolved "https://mirrors.cloud.tencent.com/npm/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@7.1.6:
version "7.1.6"
resolved "https://mirrors.cloud.tencent.com/npm/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.2.3"
resolved "https://mirrors.cloud.tencent.com/npm/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -7989,6 +8108,11 @@ jest@^27.4.3:
import-local "^3.0.2"
jest-cli "^27.5.1"
jiti@^1.17.2:
version "1.18.2"
resolved "https://mirrors.cloud.tencent.com/npm/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd"
integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==
jotai@^1.7.2:
version "1.13.1"
resolved "https://mirrors.cloud.tencent.com/npm/jotai/-/jotai-1.13.1.tgz#20cc46454cbb39096b12fddfa635b873b3668236"
@@ -8618,6 +8742,15 @@ multicast-dns@^7.2.5:
dns-packet "^5.2.2"
thunky "^1.0.2"
mz@^2.7.0:
version "2.7.0"
resolved "https://mirrors.cloud.tencent.com/npm/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
dependencies:
any-promise "^1.0.0"
object-assign "^4.0.1"
thenify-all "^1.0.0"
nano-css@^5.3.1:
version "5.3.5"
resolved "https://mirrors.cloud.tencent.com/npm/nano-css/-/nano-css-5.3.5.tgz#3075ea29ffdeb0c7cb6d25edb21d8f7fa8e8fe8e"
@@ -9168,7 +9301,7 @@ pinkie@^2.0.0:
resolved "https://mirrors.cloud.tencent.com/npm/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
pirates@^4.0.4:
pirates@^4.0.1, pirates@^4.0.4:
version "4.0.5"
resolved "https://mirrors.cloud.tencent.com/npm/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
@@ -10387,20 +10520,20 @@ react-refresh@^0.11.0:
resolved "https://mirrors.cloud.tencent.com/npm/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
react-router-dom@^6.9.0:
version "6.9.0"
resolved "https://mirrors.cloud.tencent.com/npm/react-router-dom/-/react-router-dom-6.9.0.tgz#dd8b4e461453bd4cad2e6404493d1a5b4bfea758"
integrity sha512-/seUAPY01VAuwkGyVBPCn1OXfVbaWGGu4QN9uj0kCPcTyNYgL1ldZpxZUpRU7BLheKQI4Twtl/OW2nHRF1u26Q==
react-router-dom@^6.10.0:
version "6.10.0"
resolved "https://mirrors.cloud.tencent.com/npm/react-router-dom/-/react-router-dom-6.10.0.tgz#090ddc5c84dc41b583ce08468c4007c84245f61f"
integrity sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==
dependencies:
"@remix-run/router" "1.4.0"
react-router "6.9.0"
"@remix-run/router" "1.5.0"
react-router "6.10.0"
react-router@6.9.0:
version "6.9.0"
resolved "https://mirrors.cloud.tencent.com/npm/react-router/-/react-router-6.9.0.tgz#0f503d9becbc62d9e4ddc0f9bd4026e0fd29fbf5"
integrity sha512-51lKevGNUHrt6kLuX3e/ihrXoXCa9ixY/nVWRLlob4r/l0f45x3SzBvYJe3ctleLUQQ5fVa4RGgJOTH7D9Umhw==
react-router@6.10.0:
version "6.10.0"
resolved "https://mirrors.cloud.tencent.com/npm/react-router/-/react-router-6.10.0.tgz#230f824fde9dd0270781b5cb497912de32c0a971"
integrity sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==
dependencies:
"@remix-run/router" "1.4.0"
"@remix-run/router" "1.5.0"
react-scripts@^5.0.1:
version "5.0.1"
@@ -10525,10 +10658,10 @@ react-viewport-list@^7.0.0:
resolved "https://mirrors.cloud.tencent.com/npm/react-viewport-list/-/react-viewport-list-7.0.0.tgz#4d662dec3fcacd04dd99d0110c2145782ca6037d"
integrity sha512-iI1R4yqqdAfDmov2BEDiFaHTCijfGYyquhdhMeTis4ke84pSW9Ccfbna6I9q+/7VAtV81atpIAIWQjU47dnh2Q==
react-virtuoso@^4.1.0:
version "4.1.0"
resolved "https://mirrors.cloud.tencent.com/npm/react-virtuoso/-/react-virtuoso-4.1.0.tgz#44138385d87bddc7597867778f2cbcd9d4dfea79"
integrity sha512-Vcq5WXn18PvPT55kdeGQ8BN3K95XyPe7hum8zG6Tx7g1CtUYVsQKN7fouMxBSy+XymEDB5ynGy8JWhuqyLLtPw==
react-virtuoso@^4.1.1:
version "4.1.1"
resolved "https://mirrors.cloud.tencent.com/npm/react-virtuoso/-/react-virtuoso-4.1.1.tgz#df42fe867377068562890752dc9282fde71499c7"
integrity sha512-G2lyifG5UIRZI9vgcBt+6OLDL6CO5/O6YiGS+O9z+VNRjVSTioSwfBC7sVSLspYq7iyL60aIXlVqSKVJiSVhlg==
react@^18.2.0:
version "18.2.0"
@@ -10819,10 +10952,10 @@ rollup@^2.43.1:
optionalDependencies:
fsevents "~2.3.2"
rooks@^7.9.0:
version "7.9.0"
resolved "https://mirrors.cloud.tencent.com/npm/rooks/-/rooks-7.9.0.tgz#ae919d75c41d7ed6b44094dfaa1ab5de6b1f1113"
integrity sha512-NcpK2uXCExldl6W/e4xvh+p3DsAlRJ44iJ9FwWKJRkLwPTH/onTLrfj8uKwhc6Q2QV/9HcHWHJwgCybsh/U/Wg==
rooks@^7.10.0:
version "7.10.0"
resolved "https://mirrors.cloud.tencent.com/npm/rooks/-/rooks-7.10.0.tgz#2f457d176391621e2f51d8248d899aca2a136382"
integrity sha512-aCeYfl7G86bKdBdOva/dzA9uWRYUgo5aOjGsCWBPjFp82SfdMUmQWUPWhCz3+BT/xuZJ5GqEsApMmoTPpupPLg==
dependencies:
fast-deep-equal "^3.1.3"
lodash.debounce "^4.0.8"
@@ -11554,6 +11687,18 @@ stylis@^4.0.6:
resolved "https://mirrors.cloud.tencent.com/npm/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7"
integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==
sucrase@^3.29.0:
version "3.31.0"
resolved "https://mirrors.cloud.tencent.com/npm/sucrase/-/sucrase-3.31.0.tgz#daae4fd458167c5d4ba1cce6aef57b988b417b33"
integrity sha512-6QsHnkqyVEzYcaiHsOKkzOtOgdJcb8i54x6AV2hDwyZcY9ZyykGZVw6L/YN98xC0evwTP6utsWWrKRaa8QlfEQ==
dependencies:
commander "^4.0.0"
glob "7.1.6"
lines-and-columns "^1.1.6"
mz "^2.7.0"
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://mirrors.cloud.tencent.com/npm/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -11642,7 +11787,7 @@ symbol-tree@^3.2.4:
resolved "https://mirrors.cloud.tencent.com/npm/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
tailwindcss@^3.0.2, tailwindcss@^3.2.7:
tailwindcss@^3.0.2:
version "3.2.7"
resolved "https://mirrors.cloud.tencent.com/npm/tailwindcss/-/tailwindcss-3.2.7.tgz#5936dd08c250b05180f0944500c01dce19188c07"
integrity sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==
@@ -11671,6 +11816,36 @@ tailwindcss@^3.0.2, tailwindcss@^3.2.7:
quick-lru "^5.1.1"
resolve "^1.22.1"
tailwindcss@^3.3.0:
version "3.3.0"
resolved "https://mirrors.cloud.tencent.com/npm/tailwindcss/-/tailwindcss-3.3.0.tgz#8cab40e5a10a10648118c0859ba8bfbc744a761e"
integrity sha512-hOXlFx+YcklJ8kXiCAfk/FMyr4Pm9ck477G0m/us2344Vuj355IpoEDB5UmGAsSpTBmr+4ZhjzW04JuFXkb/fw==
dependencies:
arg "^5.0.2"
chokidar "^3.5.3"
color-name "^1.1.4"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.12"
glob-parent "^6.0.2"
is-glob "^4.0.3"
jiti "^1.17.2"
lilconfig "^2.0.6"
micromatch "^4.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
postcss "^8.0.9"
postcss-import "^14.1.0"
postcss-js "^4.0.0"
postcss-load-config "^3.1.4"
postcss-nested "6.0.0"
postcss-selector-parser "^6.0.11"
postcss-value-parser "^4.2.0"
quick-lru "^5.1.1"
resolve "^1.22.1"
sucrase "^3.29.0"
tapable@^1.0.0:
version "1.1.3"
resolved "https://mirrors.cloud.tencent.com/npm/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
@@ -11739,6 +11914,20 @@ text-table@^0.2.0:
resolved "https://mirrors.cloud.tencent.com/npm/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://mirrors.cloud.tencent.com/npm/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=
dependencies:
thenify ">= 3.1.0 < 4"
"thenify@>= 3.1.0 < 4":
version "3.3.1"
resolved "https://mirrors.cloud.tencent.com/npm/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
dependencies:
any-promise "^1.0.0"
throat@^6.0.1:
version "6.0.2"
resolved "https://mirrors.cloud.tencent.com/npm/throat/-/throat-6.0.2.tgz#51a3fbb5e11ae72e2cf74861ed5c8020f89f29fe"
@@ -11861,6 +12050,11 @@ ts-easing@^0.2.0:
resolved "https://mirrors.cloud.tencent.com/npm/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://mirrors.cloud.tencent.com/npm/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
tsconfig-paths@^3.14.1:
version "3.14.2"
resolved "https://mirrors.cloud.tencent.com/npm/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
@@ -12319,7 +12513,7 @@ webpack-sources@^3.2.3:
resolved "https://mirrors.cloud.tencent.com/npm/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.64.4, webpack@^5.76.3:
webpack@^5.64.4:
version "5.76.3"
resolved "https://mirrors.cloud.tencent.com/npm/webpack/-/webpack-5.76.3.tgz#dffdc72c8950e5b032fddad9c4452e7787d2f489"
integrity sha512-18Qv7uGPU8b2vqGeEEObnfICyw2g39CHlDEK4I7NK13LOur1d0HGmGNKGT58Eluwddpn3oEejwvBPoP4M7/KSA==
@@ -12349,6 +12543,36 @@ webpack@^5.64.4, webpack@^5.76.3:
watchpack "^2.4.0"
webpack-sources "^3.2.3"
webpack@^5.77.0:
version "5.77.0"
resolved "https://mirrors.cloud.tencent.com/npm/webpack/-/webpack-5.77.0.tgz#dea3ad16d7ea6b84aa55fa42f4eac9f30e7eb9b4"
integrity sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/wasm-edit" "1.11.1"
"@webassemblyjs/wasm-parser" "1.11.1"
acorn "^8.7.1"
acorn-import-assertions "^1.7.6"
browserslist "^4.14.5"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.10.0"
es-module-lexer "^0.9.0"
eslint-scope "5.1.1"
events "^3.2.0"
glob-to-regexp "^0.4.1"
graceful-fs "^4.2.9"
json-parse-even-better-errors "^2.3.1"
loader-runner "^4.2.0"
mime-types "^2.1.27"
neo-async "^2.6.2"
schema-utils "^3.1.0"
tapable "^2.1.1"
terser-webpack-plugin "^5.1.3"
watchpack "^2.4.0"
webpack-sources "^3.2.3"
websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
version "0.7.4"
resolved "https://mirrors.cloud.tencent.com/npm/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"