style: format ts file with prettier

This commit is contained in:
Tristan Yang
2023-05-19 17:28:15 +08:00
parent 5bd0183651
commit 54f995803a
70 changed files with 1021 additions and 777 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
import * as localforage from "localforage"; import * as localforage from "localforage";
import { extendPrototype } from 'localforage-setitems'; import { extendPrototype } from "localforage-setitems";
import { CACHE_VERSION, KEY_UID } from "../config";
import useRehydrate from "./useRehydrate"; import useRehydrate from "./useRehydrate";
import { KEY_UID, CACHE_VERSION } from "../config";
extendPrototype(localforage); extendPrototype(localforage);
const tables = [ const tables = [
+7 -6
View File
@@ -1,15 +1,16 @@
import { useState } from "react"; import { useState } from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { fillReactionMessage } from "../slices/message.reaction";
import { fillServer } from "../slices/server"; import { fillChannels } from "../slices/channels";
import { fillFootprint } from "../slices/footprint";
import { fillMessage } from "../slices/message"; import { fillMessage } from "../slices/message";
import { fillChannelMsg } from "../slices/message.channel"; import { fillChannelMsg } from "../slices/message.channel";
import { fillUserMsg } from "../slices/message.user";
import { fillChannels } from "../slices/channels";
import { fillUsers } from "../slices/users";
import { fillFootprint } from "../slices/footprint";
import { fillFileMessage } from "../slices/message.file"; import { fillFileMessage } from "../slices/message.file";
import { fillReactionMessage } from "../slices/message.reaction";
import { fillUserMsg } from "../slices/message.user";
import { fillServer } from "../slices/server";
import { fillUI } from "../slices/ui"; import { fillUI } from "../slices/ui";
import { fillUsers } from "../slices/users";
const useRehydrate = () => { const useRehydrate = () => {
const [iterated, setIterated] = useState(false); const [iterated, setIterated] = useState(false);
@@ -1,4 +1,5 @@
import clearTable from "./clear.handler"; import clearTable from "./clear.handler";
interface Params { interface Params {
payload: any; payload: any;
operation: string; operation: string;
@@ -1,6 +1,8 @@
import localforage from "localforage"; import localforage from "localforage";
import { Channel } from "@/types/channel"; import { Channel } from "@/types/channel";
import clearTable from "./clear.handler"; import clearTable from "./clear.handler";
interface Params { interface Params {
data: any; data: any;
payload: any; payload: any;
@@ -1,4 +1,5 @@
import clearTable from "./clear.handler"; import clearTable from "./clear.handler";
interface Params { interface Params {
data: any; data: any;
payload: any; payload: any;
@@ -1,4 +1,5 @@
import clearTable from "./clear.handler"; import clearTable from "./clear.handler";
interface Params { interface Params {
data: any; data: any;
operation: string; operation: string;
@@ -1,5 +1,7 @@
import localforage from "localforage"; import localforage from "localforage";
import clearTable from "./clear.handler"; import clearTable from "./clear.handler";
interface Params { interface Params {
payload: any; payload: any;
data: any; data: any;
@@ -7,7 +9,7 @@ interface Params {
} }
const ignores = ["voice_fullscreen", "voice"]; const ignores = ["voice_fullscreen", "voice"];
export default async function handler({ operation, data = {}, payload }: Params) { export default async function handler({ operation, data = {}, payload }: Params) {
const table = window.CACHE["footprint"] as typeof localforage;; const table = window.CACHE["footprint"] as typeof localforage;
if (operation.startsWith("reset")) { if (operation.startsWith("reset")) {
clearTable("footprint"); clearTable("footprint");
return; return;
@@ -34,7 +36,6 @@ export default async function handler({ operation, data = {}, payload }: Params)
table?.setItem("afterMid", afterMid); table?.setItem("afterMid", afterMid);
} }
}); });
} }
break; break;
case "updateMute": case "updateMute":
@@ -48,7 +49,6 @@ export default async function handler({ operation, data = {}, payload }: Params)
const { type } = payload; const { type } = payload;
if (type == "channel") { if (type == "channel") {
await table?.setItem("historyChannels", data.historyChannels); await table?.setItem("historyChannels", data.historyChannels);
} else { } else {
await table?.setItem("historyUsers", data.historyUsers); await table?.setItem("historyUsers", data.historyUsers);
} }
@@ -1,4 +1,5 @@
import localforage from "localforage"; import localforage from "localforage";
import clearTable from "./clear.handler"; import clearTable from "./clear.handler";
interface Params { interface Params {
+4 -1
View File
@@ -1,4 +1,5 @@
import localforage from "localforage"; import localforage from "localforage";
import { ContactStatus, User } from "@/types/user"; import { ContactStatus, User } from "@/types/user";
import clearTable from "./clear.handler"; import clearTable from "./clear.handler";
@@ -20,7 +21,9 @@ export default async function handler({ operation, data, payload }) {
break; break;
case "updateContactStatus": case "updateContactStatus":
{ {
const tmp = payload as { uid: number, status: ContactStatus } | { uid: number, status: ContactStatus }[]; const tmp = payload as
| { uid: number; status: ContactStatus }
| { uid: number; status: ContactStatus }[];
const arr = Array.isArray(tmp) ? tmp : [tmp]; const arr = Array.isArray(tmp) ? tmp : [tmp];
const opts = arr.map(({ uid, status }) => { const opts = arr.map(({ uid, status }) => {
return { key: uid + "", value: { ...data.byId[uid], status } }; return { key: uid + "", value: { ...data.byId[uid], status } };
+13 -12
View File
@@ -1,17 +1,18 @@
import { createListenerMiddleware } from "@reduxjs/toolkit"; import { createListenerMiddleware } from "@reduxjs/toolkit";
import rtkqHandler from "./handler.rtkq";
import channelsHandler from "./handler.channels";
import usersHandler from "./handler.users";
import channelMsgHandler from "./handler.channel.msg";
import dmMsgHandler from "./handler.dm.msg";
import serverHandler from "./handler.server";
import messageHandler from "./handler.message";
import fileMessageHandler from "./handler.file.msg";
import archiveMessageHandler from "./handler.archive.msg";
import reactionHandler from "./handler.reaction";
import UIHandler from "./handler.ui";
import footprintHandler from "./handler.footprint";
import { RootState } from "../store"; import { RootState } from "../store";
import archiveMessageHandler from "./handler.archive.msg";
import channelMsgHandler from "./handler.channel.msg";
import channelsHandler from "./handler.channels";
import dmMsgHandler from "./handler.dm.msg";
import fileMessageHandler from "./handler.file.msg";
import footprintHandler from "./handler.footprint";
import messageHandler from "./handler.message";
import reactionHandler from "./handler.reaction";
import rtkqHandler from "./handler.rtkq";
import serverHandler from "./handler.server";
import UIHandler from "./handler.ui";
import usersHandler from "./handler.users";
const operations = [ const operations = [
"__rtkq", "__rtkq",
+5 -4
View File
@@ -1,8 +1,6 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import { nanoid } from "@reduxjs/toolkit"; import { nanoid } from "@reduxjs/toolkit";
import baseQuery from "./base.query"; import { createApi } from "@reduxjs/toolkit/query/react";
import { setAuthData, updateToken, resetAuthData, updateInitialized } from "../slices/auth.data";
import BASE_URL, { KEY_DEVICE_ID, KEY_DEVICE_TOKEN, KEY_LOCAL_MAGIC_TOKEN } from "../config";
import { import {
AuthData, AuthData,
CredentialResponse, CredentialResponse,
@@ -11,6 +9,9 @@ import {
RenewTokenResponse RenewTokenResponse
} from "@/types/auth"; } from "@/types/auth";
import { UserRegDTO, UserRegResponse } from "@/types/user"; import { UserRegDTO, UserRegResponse } from "@/types/user";
import BASE_URL, { KEY_DEVICE_ID, KEY_DEVICE_TOKEN, KEY_LOCAL_MAGIC_TOKEN } from "../config";
import { resetAuthData, setAuthData, updateInitialized, updateToken } from "../slices/auth.data";
import baseQuery from "./base.query";
const getDeviceId = () => { const getDeviceId = () => {
let d = localStorage.getItem(KEY_DEVICE_ID); let d = localStorage.getItem(KEY_DEVICE_ID);
+11 -9
View File
@@ -1,10 +1,11 @@
import { fetchBaseQuery } from "@reduxjs/toolkit/query";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { fetchBaseQuery } from "@reduxjs/toolkit/query";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { updateToken, resetAuthData } from "../slices/auth.data";
import BASE_URL, { tokenHeader } from "../config";
import { RootState } from "../store";
import { getLocalAuthData } from "@/utils"; import { getLocalAuthData } from "@/utils";
import BASE_URL, { tokenHeader } from "../config";
import { resetAuthData, updateToken } from "../slices/auth.data";
import { RootState } from "../store";
const whiteList = [ const whiteList = [
"guestLogin", "guestLogin",
@@ -102,12 +103,13 @@ const baseQueryWithTokenCheck = async (args: any, api: any, extraOptions: any) =
} }
} }
break; break;
case 403: { case 403:
const whiteList403 = ["sendMsg"]; {
if (!whiteList403.includes(api.endpoint)) { const whiteList403 = ["sendMsg"];
toast.error("Request Not Allowed"); if (!whiteList403.includes(api.endpoint)) {
toast.error("Request Not Allowed");
}
} }
}
break; break;
case 404: case 404:
+22 -18
View File
@@ -1,16 +1,17 @@
import { createApi } from "@reduxjs/toolkit/query/react"; import { createApi } from "@reduxjs/toolkit/query/react";
// import toast from "react-hot-toast";
import baseQuery from "./base.query"; import { Channel, ChannelDTO, CreateChannelDTO } from "@/types/channel";
import { ContentTypeKey } from "@/types/message";
import BASE_URL, { ContentTypes } from "../config"; import BASE_URL, { ContentTypes } from "../config";
import { updateChannel, removeChannel } from "../slices/channels"; import { removeChannel, updateChannel } from "../slices/channels";
import { updateRememberedNavs } from "../slices/ui";
import { removeMessage } from "../slices/message"; import { removeMessage } from "../slices/message";
import { removeChannelSession } from "../slices/message.channel"; import { removeChannelSession } from "../slices/message.channel";
import { removeReactionMessage } from "../slices/message.reaction"; import { removeReactionMessage } from "../slices/message.reaction";
import { onMessageSendStarted } from "./handlers"; import { updateRememberedNavs } from "../slices/ui";
import { Channel, ChannelDTO, CreateChannelDTO } from "@/types/channel";
import { RootState } from "../store"; import { RootState } from "../store";
import { ContentTypeKey } from "@/types/message"; // import toast from "react-hot-toast";
import baseQuery from "./base.query";
import { onMessageSendStarted } from "./handlers";
export const channelApi = createApi({ export const channelApi = createApi({
reducerPath: "channelApi", reducerPath: "channelApi",
@@ -34,14 +35,19 @@ export const channelApi = createApi({
} }
} }
}), }),
createChannel: builder.mutation<{ gid: number, created_at: number } | number, CreateChannelDTO>({ createChannel: builder.mutation<{ gid: number; created_at: number } | number, CreateChannelDTO>(
query: (data) => ({ {
url: "group", query: (data) => ({
method: "POST", url: "group",
body: data method: "POST",
}) body: data
}), })
changeChannelType: builder.mutation<number, { is_public: boolean, id: number, members?: number[] }>({ }
),
changeChannelType: builder.mutation<
number,
{ is_public: boolean; id: number; members?: number[] }
>({
query: ({ id, is_public, members }) => ({ query: ({ id, is_public, members }) => ({
url: `/group/${id}/change_type`, url: `/group/${id}/change_type`,
method: "POST", method: "POST",
@@ -101,9 +107,7 @@ export const channelApi = createApi({
method: "DELETE" method: "DELETE"
}), }),
async onQueryStarted(id, { dispatch, getState, queryFulfilled }) { async onQueryStarted(id, { dispatch, getState, queryFulfilled }) {
const { const { channelMessage } = getState() as RootState;
channelMessage,
} = getState() as RootState;
try { try {
await queryFulfilled; await queryFulfilled;
// 删掉该channel下的所有消息&reaction // 删掉该channel下的所有消息&reaction
+5 -3
View File
@@ -2,9 +2,9 @@ import toast from "react-hot-toast";
import { batch } from "react-redux"; import { batch } from "react-redux";
import { ContentTypes } from "../config"; import { ContentTypes } from "../config";
import { addMessage, removeMessage, updateMessage } from "../slices/message";
import { addChannelMsg, removeChannelMsg } from "../slices/message.channel"; import { addChannelMsg, removeChannelMsg } from "../slices/message.channel";
import { addUserMsg, removeUserMsg } from "../slices/message.user"; import { addUserMsg, removeUserMsg } from "../slices/message.user";
import { addMessage, removeMessage, updateMessage } from "../slices/message";
export const onMessageSendStarted = async ( export const onMessageSendStarted = async (
{ {
@@ -23,7 +23,9 @@ export const onMessageSendStarted = async (
if (type == "archive") return; if (type == "archive") return;
// id: who send to ,from_uid: who sent // id: who send to ,from_uid: who sent
// console.log("handlers data", content, type, properties, ignoreLocal, id); // console.log("handlers data", content, type, properties, ignoreLocal, id);
const isMedia = properties.content_type ? ["image", "video", "audio"].includes(properties.content_type.split('/')[0]) : false; const isMedia = properties.content_type
? ["image", "video", "audio"].includes(properties.content_type.split("/")[0])
: false;
// const isImage = properties.content_type?.startsWith("image"); // const isImage = properties.content_type?.startsWith("image");
const ts = properties.local_id || +new Date(); const ts = properties.local_id || +new Date();
const tmpMsg = { const tmpMsg = {
@@ -63,7 +65,7 @@ export const onMessageSendStarted = async (
// toast.error(`Failed to send, blocked maybe.`,); // toast.error(`Failed to send, blocked maybe.`,);
dispatch(updateMessage({ mid: ts, failed: true })); dispatch(updateMessage({ mid: ts, failed: true }));
} else { } else {
toast.error(`Send Message Failed ${JSON.stringify(error)}`,); toast.error(`Send Message Failed ${JSON.stringify(error)}`);
dispatch(removeContextMessage({ id, mid: ts })); dispatch(removeContextMessage({ id, mid: ts }));
dispatch(removeMessage(ts)); dispatch(removeMessage(ts));
} }
+38 -25
View File
@@ -1,16 +1,17 @@
import { createApi } from "@reduxjs/toolkit/query/react"; import { createApi } from "@reduxjs/toolkit/query/react";
import { ContentTypes } from "../config";
import { updateReadChannels, updateReadUsers, upsertOG } from "../slices/footprint";
import { fillFavorites, populateFavorite, addFavorite, deleteFavorite } from "../slices/favorites";
import { onMessageSendStarted } from "./handlers";
import { normalizeArchiveData } from "@/utils";
import baseQuery from "./base.query";
import { Archive, FavoriteArchive, OG } from "@/types/resource";
import { ChatMessage, ContentTypeKey, UploadFileResponse } from "@/types/message";
import { ChatContext } from "@/types/common"; import { ChatContext } from "@/types/common";
import { ChatMessage, ContentTypeKey, UploadFileResponse } from "@/types/message";
import { Archive, FavoriteArchive, OG } from "@/types/resource";
import handleChatMessage from "@/hooks/useStreaming/chat.handler"; import handleChatMessage from "@/hooks/useStreaming/chat.handler";
import { RootState } from "../store"; import { normalizeArchiveData } from "@/utils";
import { ContentTypes } from "../config";
import { addFavorite, deleteFavorite, fillFavorites, populateFavorite } from "../slices/favorites";
import { updateReadChannels, updateReadUsers, upsertOG } from "../slices/footprint";
import { upsertArchiveMessage } from "../slices/message.archive"; import { upsertArchiveMessage } from "../slices/message.archive";
import { RootState } from "../store";
import baseQuery from "./base.query";
import { onMessageSendStarted } from "./handlers";
export const messageApi = createApi({ export const messageApi = createApi({
reducerPath: "messageApi", reducerPath: "messageApi",
@@ -73,15 +74,18 @@ export const messageApi = createApi({
dispatch(upsertOG({ key: url, value: data })); dispatch(upsertOG({ key: url, value: data }));
} catch (err) { } catch (err) {
console.log("get og error", err); console.log("get og error", err);
dispatch(upsertOG({ dispatch(
key: url, value: { upsertOG({
images: [], key: url,
audios: [], value: {
videos: [], images: [],
title: "", audios: [],
url, videos: [],
} title: "",
})); url
}
})
);
} }
} }
}), }),
@@ -176,12 +180,15 @@ export const messageApi = createApi({
} }
} }
}), }),
loadMoreMessages: builder.query<ChatMessage[], { context?: ChatContext, id: number; mid?: number; limit?: number }>({ loadMoreMessages: builder.query<
ChatMessage[],
{ context?: ChatContext; id: number; mid?: number; limit?: number }
>({
query: ({ context = "channel", id, mid = "", limit = 100 }) => { query: ({ context = "channel", id, mid = "", limit = 100 }) => {
const url = context == "channel" ? const url =
`/group/${id}/history?limit=${limit}${mid ? `&before=${mid}` : ""}` context == "channel"
: ? `/group/${id}/history?limit=${limit}${mid ? `&before=${mid}` : ""}`
`/user/${id}/history?limit=${limit}${mid ? `&before=${mid}` : ""}`; : `/user/${id}/history?limit=${limit}${mid ? `&before=${mid}` : ""}`;
return { return {
url url
}; };
@@ -198,7 +205,13 @@ export const messageApi = createApi({
}), }),
replyMessage: builder.mutation< replyMessage: builder.mutation<
number, number,
{ from_uid: number, reply_mid: number; content: string; type: ContentTypeKey, properties?: {} } {
from_uid: number;
reply_mid: number;
content: string;
type: ContentTypeKey;
properties?: {};
}
>({ >({
query: ({ reply_mid, content, type = "text", properties }) => ({ query: ({ reply_mid, content, type = "text", properties }) => ({
headers: { headers: {
@@ -245,7 +258,7 @@ export const messageApi = createApi({
method: "HEAD", method: "HEAD",
responseHandler: "text" responseHandler: "text"
}) })
}), })
}) })
}); });
+61 -46
View File
@@ -1,30 +1,31 @@
import { createApi } from "@reduxjs/toolkit/query/react"; import { createApi } from "@reduxjs/toolkit/query/react";
import BASE_URL, { ContentTypes, IS_OFFICIAL_DEMO, PAYMENT_URL_PREFIX } from "../config";
import { updateInfo } from "../slices/server";
import baseQuery from "./base.query";
import { RootState } from "../store";
import { User } from "@/types/user";
import {
FirebaseConfig,
GoogleAuthConfig,
LoginConfig,
Server,
TestEmailDTO,
CreateAdminDTO,
SMTPConfig,
AgoraConfig,
GithubAuthConfig,
LicenseResponse,
RenewLicense,
RenewLicenseResponse,
AgoraTokenResponse,
AgoraVoicingListResponse,
SystemCommon,
AgoraChannelUsersResponse
} from "@/types/server";
import { Channel } from "@/types/channel"; import { Channel } from "@/types/channel";
import { ContentTypeKey } from "@/types/message"; import { ContentTypeKey } from "@/types/message";
import {
AgoraChannelUsersResponse,
AgoraConfig,
AgoraTokenResponse,
AgoraVoicingListResponse,
CreateAdminDTO,
FirebaseConfig,
GithubAuthConfig,
GoogleAuthConfig,
LicenseResponse,
LoginConfig,
RenewLicense,
RenewLicenseResponse,
Server,
SMTPConfig,
SystemCommon,
TestEmailDTO
} from "@/types/server";
import { User } from "@/types/user";
import BASE_URL, { ContentTypes, IS_OFFICIAL_DEMO, PAYMENT_URL_PREFIX } from "../config";
import { updateInfo } from "../slices/server";
import { updateCallInfo, upsertVoiceList } from "../slices/voice"; import { updateCallInfo, upsertVoiceList } from "../slices/voice";
import { RootState } from "../store";
import baseQuery from "./base.query";
const defaultExpireDuration = 2 * 24 * 60 * 60; const defaultExpireDuration = 2 * 24 * 60 * 60;
@@ -69,9 +70,7 @@ export const serverApi = createApi({
async onQueryStarted(data, { dispatch, queryFulfilled }) { async onQueryStarted(data, { dispatch, queryFulfilled }) {
try { try {
const resp = await queryFulfilled; const resp = await queryFulfilled;
dispatch( dispatch(updateInfo({ version: resp.data }));
updateInfo({ version: resp.data })
);
} catch { } catch {
console.error("get server version error"); console.error("get server version error");
} }
@@ -117,18 +116,26 @@ export const serverApi = createApi({
getAgoraConfig: builder.query<AgoraConfig, void>({ getAgoraConfig: builder.query<AgoraConfig, void>({
query: () => ({ url: `/admin/agora/config` }) query: () => ({ url: `/admin/agora/config` })
}), }),
getAgoraChannels: builder.query<AgoraVoicingListResponse, { page_no: number, page_size: number }>({ getAgoraChannels: builder.query<
query: (param = { page_no: 0, page_size: 100 }) => ({ url: `/admin/agora/channel/${param.page_no}/${param.page_size}` }), AgoraVoicingListResponse,
{ page_no: number; page_size: number }
>({
query: (param = { page_no: 0, page_size: 100 }) => ({
url: `/admin/agora/channel/${param.page_no}/${param.page_size}`
}),
async onQueryStarted(data, { dispatch, queryFulfilled, getState }) { async onQueryStarted(data, { dispatch, queryFulfilled, getState }) {
try { try {
const { voice: { callingFrom }, authData } = getState() as RootState; const {
voice: { callingFrom },
authData
} = getState() as RootState;
const { data: resp } = await queryFulfilled; const { data: resp } = await queryFulfilled;
const { success } = resp; const { success } = resp;
if (success) { if (success) {
const arr = resp.data.channels.map(data => { const arr = resp.data.channels.map((data) => {
const [type, id] = data.channel_name.split(":").slice(-2); const [type, id] = data.channel_name.split(":").slice(-2);
const count = data.user_count; const count = data.user_count;
const context = type === "group" ? "channel" as const : "dm" as const; const context = type === "group" ? ("channel" as const) : ("dm" as const);
return { return {
id: +id, id: +id,
context, context,
@@ -137,7 +144,9 @@ export const serverApi = createApi({
}; };
}); });
dispatch(upsertVoiceList(arr)); dispatch(upsertVoiceList(arr));
const hasMyself = arr.some(data => data.context === "dm" && data.id == authData?.user?.uid); const hasMyself = arr.some(
(data) => data.context === "dm" && data.id == authData?.user?.uid
);
const sendByMe = callingFrom && callingFrom === authData?.user?.uid; const sendByMe = callingFrom && callingFrom === authData?.user?.uid;
// reset dm call setting // reset dm call setting
if (callingFrom && !sendByMe && !hasMyself) { if (callingFrom && !sendByMe && !hasMyself) {
@@ -187,9 +196,7 @@ export const serverApi = createApi({
async onQueryStarted(data, { dispatch, queryFulfilled }) { async onQueryStarted(data, { dispatch, queryFulfilled }) {
try { try {
const resp = await queryFulfilled; const resp = await queryFulfilled;
dispatch( dispatch(updateInfo(resp.data));
updateInfo(resp.data)
);
} catch { } catch {
console.error("get server common error"); console.error("get server common error");
} }
@@ -204,9 +211,7 @@ export const serverApi = createApi({
async onQueryStarted(data, { dispatch, queryFulfilled }) { async onQueryStarted(data, { dispatch, queryFulfilled }) {
try { try {
await queryFulfilled; await queryFulfilled;
dispatch( dispatch(updateInfo(data));
updateInfo(data)
);
} catch { } catch {
console.error("update server common error"); console.error("update server common error");
} }
@@ -309,7 +314,7 @@ export const serverApi = createApi({
url: `/admin/system/update_frontend_url`, url: `/admin/system/update_frontend_url`,
method: "POST", method: "POST",
headers: { headers: {
"content-type": 'text/plain', "content-type": "text/plain"
}, },
body: url body: url
}) })
@@ -324,16 +329,16 @@ export const serverApi = createApi({
const rootStore = getState() as RootState; const rootStore = getState() as RootState;
const { upgraded: prevValue } = rootStore.server; const { upgraded: prevValue } = rootStore.server;
try { try {
const { data: { user_limit } } = await queryFulfilled; const {
data: { user_limit }
} = await queryFulfilled;
const currValue = user_limit > 20; const currValue = user_limit > 20;
if (prevValue !== currValue) { if (prevValue !== currValue) {
dispatch(updateInfo({ upgraded: currValue })); dispatch(updateInfo({ upgraded: currValue }));
} }
} catch { } catch {
console.error("update license upgraded status failed "); console.error("update license upgraded status failed ");
} }
} }
}), }),
@@ -363,15 +368,25 @@ export const serverApi = createApi({
body: { license } body: { license }
}) })
}), }),
getBotRelatedChannels: builder.query<Channel[], { api_key: string, public_only?: boolean }>({ getBotRelatedChannels: builder.query<Channel[], { api_key: string; public_only?: boolean }>({
query: ({ api_key, public_only = false }) => ({ query: ({ api_key, public_only = false }) => ({
url: public_only ? `/bot?public_only=${public_only}` : `/bot`, url: public_only ? `/bot?public_only=${public_only}` : `/bot`,
headers: { headers: {
"x-api-key": api_key "x-api-key": api_key
}, }
}) })
}), }),
sendMessageByBot: builder.mutation<number, { uid?: number, cid?: number, api_key: string, content: string, type?: ContentTypeKey, properties?: object }>({ sendMessageByBot: builder.mutation<
number,
{
uid?: number;
cid?: number;
api_key: string;
content: string;
type?: ContentTypeKey;
properties?: object;
}
>({
query: ({ uid, cid, api_key, type = "text", properties, content }) => ({ query: ({ uid, cid, api_key, type = "text", properties, content }) => ({
headers: { headers: {
"x-api-key": api_key, "x-api-key": api_key,
@@ -384,7 +399,7 @@ export const serverApi = createApi({
method: "POST", method: "POST",
body: content body: content
}) })
}), })
}) })
}); });
+44 -28
View File
@@ -1,13 +1,25 @@
import { createApi } from "@reduxjs/toolkit/query/react"; import { createApi } from "@reduxjs/toolkit/query/react";
// import toast from "react-hot-toast";
import baseQuery from "./base.query"; import { ContentTypeKey, MuteDTO } from "@/types/message";
import {
AutoDeleteMsgDTO,
BotAPIKey,
ContactAction,
ContactResponse,
ContactStatus,
User,
UserCreateDTO,
UserDTO,
UserForAdmin,
UserForAdminDTO
} from "@/types/user";
import BASE_URL, { ContentTypes } from "../config";
import { updateAutoDeleteSetting, updateMute } from "../slices/footprint"; import { updateAutoDeleteSetting, updateMute } from "../slices/footprint";
import { fillUsers, updateContactStatus as updateStatus } from "../slices/users"; import { fillUsers, updateContactStatus as updateStatus } from "../slices/users";
import BASE_URL, { ContentTypes } from "../config";
import { onMessageSendStarted } from "./handlers";
import { AutoDeleteMsgDTO, BotAPIKey, ContactAction, ContactResponse, ContactStatus, User, UserCreateDTO, UserDTO, UserForAdmin, UserForAdminDTO } from "@/types/user";
import { ContentTypeKey, MuteDTO } from "@/types/message";
import { RootState } from "../store"; import { RootState } from "../store";
// import toast from "react-hot-toast";
import baseQuery from "./base.query";
import { onMessageSendStarted } from "./handlers";
export const userApi = createApi({ export const userApi = createApi({
reducerPath: "userApi", reducerPath: "userApi",
@@ -29,14 +41,20 @@ export const userApi = createApi({
async onQueryStarted(data, { dispatch, queryFulfilled, getState }) { async onQueryStarted(data, { dispatch, queryFulfilled, getState }) {
try { try {
const { data: users } = await queryFulfilled; const { data: users } = await queryFulfilled;
const { authData: { user: loginUser } } = getState() as RootState; const {
dispatch(fillUsers(users.map((u) => { authData: { user: loginUser }
const status = loginUser?.uid == u.uid ? "added" : ""; } = getState() as RootState;
return { dispatch(
...u, fillUsers(
status users.map((u) => {
}; const status = loginUser?.uid == u.uid ? "added" : "";
}))); return {
...u,
status
};
})
)
);
} catch { } catch {
console.log("get user list error"); console.log("get user list error");
} }
@@ -71,7 +89,7 @@ export const userApi = createApi({
method: "POST" method: "POST"
}) })
}), }),
searchUser: builder.mutation<User, { search_type: "id" | "email", keyword: string }>({ searchUser: builder.mutation<User, { search_type: "id" | "email"; keyword: string }>({
query: (input) => ({ query: (input) => ({
url: `/user/search`, url: `/user/search`,
body: input, body: input,
@@ -123,7 +141,7 @@ export const userApi = createApi({
} }
}), }),
updateContactStatus: builder.mutation<void, { action: ContactAction, target_uid: number }>({ updateContactStatus: builder.mutation<void, { action: ContactAction; target_uid: number }>({
query: (payload) => ({ query: (payload) => ({
url: `/user/update_contact_status`, url: `/user/update_contact_status`,
method: "POST", method: "POST",
@@ -131,10 +149,10 @@ export const userApi = createApi({
}), }),
async onQueryStarted(data, { dispatch, queryFulfilled }) { async onQueryStarted(data, { dispatch, queryFulfilled }) {
const map = { const map = {
"add": "added", add: "added",
"block": "blocked", block: "blocked",
"remove": "", remove: "",
"unblock": "", unblock: ""
}; };
try { try {
await queryFulfilled; await queryFulfilled;
@@ -170,7 +188,7 @@ export const userApi = createApi({
body: data body: data
}) })
}), }),
updateAvatarByAdmin: builder.mutation<void, { uid: number, file: File }>({ updateAvatarByAdmin: builder.mutation<void, { uid: number; file: File }>({
query: ({ uid, file }) => ({ query: ({ uid, file }) => ({
headers: { headers: {
"content-type": "image/png" "content-type": "image/png"
@@ -184,18 +202,17 @@ export const userApi = createApi({
query: (uid) => ({ url: `/admin/user/${uid}` }) query: (uid) => ({ url: `/admin/user/${uid}` })
}), }),
// bot operations // bot operations
createBotAPIKey: builder.mutation<void, { uid: number, name: string }>({ createBotAPIKey: builder.mutation<void, { uid: number; name: string }>({
query: ({ uid, name }) => ({ query: ({ uid, name }) => ({
url: `/admin/user/bot-api-key/${uid}`, url: `/admin/user/bot-api-key/${uid}`,
method: "POST", method: "POST",
body: { name }, body: { name }
}), })
}), }),
getBotAPIKeys: builder.query<BotAPIKey[], number>({ getBotAPIKeys: builder.query<BotAPIKey[], number>({
query: (uid) => ({ url: `/admin/user/bot-api-key/${uid}` }) query: (uid) => ({ url: `/admin/user/bot-api-key/${uid}` })
}), }),
deleteBotAPIKey: builder.query<void, { uid: number, kid: number }>({ deleteBotAPIKey: builder.query<void, { uid: number; kid: number }>({
query: ({ uid, kid }) => ({ url: `/admin/user/bot-api-key/${uid}/${kid}`, method: "DELETE" }) query: ({ uid, kid }) => ({ url: `/admin/user/bot-api-key/${uid}/${kid}`, method: "DELETE" })
}), }),
// bot operations end // bot operations end
@@ -225,8 +242,7 @@ export const userApi = createApi({
// @ts-ignore // @ts-ignore
await onMessageSendStarted.call(this, param1, param2, "user"); await onMessageSendStarted.call(this, param1, param2, "user");
} }
}), })
}) })
}); });
+14 -5
View File
@@ -1,15 +1,17 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AuthData, RenewTokenResponse } from "@/types/auth";
// import { isNull, omitBy } from "lodash"; // import { isNull, omitBy } from "lodash";
import { import {
KEY_EXPIRE, KEY_EXPIRE,
KEY_PWA_INSTALLED,
KEY_LOGIN_USER, KEY_LOGIN_USER,
KEY_PWA_INSTALLED,
KEY_REFRESH_TOKEN, KEY_REFRESH_TOKEN,
KEY_TOKEN, KEY_TOKEN,
KEY_UID KEY_UID
} from "../config"; } from "../config";
import { AuthData, RenewTokenResponse } from "@/types/auth";
import { StoredUser } from "./users"; import { StoredUser } from "./users";
// import { updateUsersByLogs } from './users'; // import { updateUsersByLogs } from './users';
interface State { interface State {
@@ -71,7 +73,7 @@ const authDataSlice = createSlice({
if (!state.user) return; if (!state.user) return;
const obj = { ...state.user, ...payload }; const obj = { ...state.user, ...payload };
Object.keys(obj).forEach(key => { Object.keys(obj).forEach((key) => {
// @ts-ignore // @ts-ignore
if (obj[key] === undefined) { if (obj[key] === undefined) {
// @ts-ignore // @ts-ignore
@@ -108,7 +110,7 @@ const authDataSlice = createSlice({
localStorage.setItem(KEY_TOKEN, token); localStorage.setItem(KEY_TOKEN, token);
localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token);
} }
}, }
// extraReducers: (builder) => { // extraReducers: (builder) => {
// builder.addCase(updateUsersByLogs, (state, action) => { // builder.addCase(updateUsersByLogs, (state, action) => {
// const changeLogs = action.payload; // const changeLogs = action.payload;
@@ -131,5 +133,12 @@ const authDataSlice = createSlice({
// } // }
}); });
export const { updateInitialized, updateLoginUser, setAuthData, resetAuthData, updateToken, updateRoleChanged } = authDataSlice.actions; export const {
updateInitialized,
updateLoginUser,
setAuthData,
resetAuthData,
updateToken,
updateRoleChanged
} = authDataSlice.actions;
export default authDataSlice.reducer; export default authDataSlice.reducer;
+7 -6
View File
@@ -1,7 +1,9 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { isNull, omitBy } from "lodash"; import { isNull, omitBy } from "lodash";
import BASE_URL from "../config";
import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from "@/types/channel"; import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from "@/types/channel";
import BASE_URL from "../config";
// import { updateVoicingInfo } from "./voice"; // import { updateVoicingInfo } from "./voice";
interface StoredChannel extends Channel { interface StoredChannel extends Channel {
@@ -35,7 +37,7 @@ const channelsSlice = createSlice({
icon: icon:
c.avatar_updated_at == 0 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}`
}; };
}); });
}, },
@@ -50,7 +52,7 @@ const channelsSlice = createSlice({
icon: icon:
avatar_updated_at == 0 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}`
}; };
}, },
updateChannel(state, action: PayloadAction<UpdateChannelDTO>) { updateChannel(state, action: PayloadAction<UpdateChannelDTO>) {
@@ -109,8 +111,7 @@ const channelsSlice = createSlice({
delete state.byId[gid]; delete state.byId[gid];
} }
} }
}, }
}); });
export const { export const {
@@ -119,7 +120,7 @@ export const {
fillChannels, fillChannels,
addChannel, addChannel,
updateChannel, updateChannel,
removeChannel, removeChannel
} = channelsSlice.actions; } = channelsSlice.actions;
export default channelsSlice.reducer; export default channelsSlice.reducer;
+1
View File
@@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
// import BASE_URL from "../config"; // import BASE_URL from "../config";
export interface Favorite { export interface Favorite {
+27 -16
View File
@@ -1,14 +1,23 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ChatContext } from "@/types/common";
import { MuteDTO } from "@/types/message"; import { MuteDTO } from "@/types/message";
import { OG } from "@/types/resource"; import { OG } from "@/types/resource";
import { AutoDeleteMessageSettingDTO, AutoDeleteMsgForGroup, AutoDeleteMsgForUser, AutoDeleteSettingForChannels, AutoDeleteSettingForUsers, PinChat, PinChatTarget } from "@/types/sse"; import {
AutoDeleteMessageSettingDTO,
AutoDeleteMsgForGroup,
AutoDeleteMsgForUser,
AutoDeleteSettingForChannels,
AutoDeleteSettingForUsers,
PinChat,
PinChatTarget
} from "@/types/sse";
import { resetAuthData } from "./auth.data"; import { resetAuthData } from "./auth.data";
import { ChatContext } from "@/types/common";
type ChannelAside = "members" | "voice" | "voice_fullscreen" | null; type ChannelAside = "members" | "voice" | "voice_fullscreen" | null;
type DMAside = "voice" | "voice_fullscreen" | null; type DMAside = "voice" | "voice_fullscreen" | null;
export interface State { export interface State {
og: { [url: string]: OG } og: { [url: string]: OG };
usersVersion: number; usersVersion: number;
afterMid: number; afterMid: number;
historyUsers: { [uid: number]: string | "reached" }; historyUsers: { [uid: number]: string | "reached" };
@@ -24,7 +33,6 @@ export interface State {
pinChats: PinChat[]; pinChats: PinChat[];
} }
const initialState: State = { const initialState: State = {
og: {}, og: {},
usersVersion: 0, usersVersion: 0,
@@ -99,9 +107,9 @@ const footprintSlice = createSlice({
switch (key) { switch (key) {
case "burn_after_reading_users": { case "burn_after_reading_users": {
const updates = (payload as AutoDeleteSettingForUsers).burn_after_reading_users; const updates = (payload as AutoDeleteSettingForUsers).burn_after_reading_users;
updates.map(item => { updates.map((item) => {
const { uid } = item; const { uid } = item;
const idx = state.autoDeleteMsgUsers.findIndex(tmp => tmp.uid == uid); const idx = state.autoDeleteMsgUsers.findIndex((tmp) => tmp.uid == uid);
if (idx !== -1) { if (idx !== -1) {
// update // update
state.autoDeleteMsgUsers[idx] = item; state.autoDeleteMsgUsers[idx] = item;
@@ -114,9 +122,9 @@ const footprintSlice = createSlice({
} }
case "burn_after_reading_groups": { case "burn_after_reading_groups": {
const updates = (payload as AutoDeleteSettingForChannels).burn_after_reading_groups; const updates = (payload as AutoDeleteSettingForChannels).burn_after_reading_groups;
updates.map(item => { updates.map((item) => {
const { gid } = item; const { gid } = item;
const idx = state.autoDeleteMsgChannels.findIndex(tmp => tmp.gid == gid); const idx = state.autoDeleteMsgChannels.findIndex((tmp) => tmp.gid == gid);
if (idx !== -1) { if (idx !== -1) {
// update // update
state.autoDeleteMsgChannels[idx] = item; state.autoDeleteMsgChannels[idx] = item;
@@ -169,7 +177,7 @@ const footprintSlice = createSlice({
} }
}); });
}, },
upsertPinChats(state, action: PayloadAction<{ pins: PinChat[], override?: boolean }>) { upsertPinChats(state, action: PayloadAction<{ pins: PinChat[]; override?: boolean }>) {
const { pins, override = false } = action.payload; const { pins, override = false } = action.payload;
if (override) { if (override) {
state.pinChats = pins; state.pinChats = pins;
@@ -179,17 +187,20 @@ const footprintSlice = createSlice({
}, },
removePinChats(state, action: PayloadAction<PinChatTarget[]>) { removePinChats(state, action: PayloadAction<PinChatTarget[]>) {
const pins = action.payload; const pins = action.payload;
state.pinChats = state.pinChats.filter(pin => { state.pinChats = state.pinChats.filter((pin) => {
const key = "uid" in pin.target ? "uid" : "gid"; const key = "uid" in pin.target ? "uid" : "gid";
// @ts-ignore // @ts-ignore
return !pins.some(p => p[key] == pin.target[key]); return !pins.some((p) => p[key] == pin.target[key]);
}); });
}, },
upsertOG(state, action: PayloadAction<{ key: string, value: OG }>) { upsertOG(state, action: PayloadAction<{ key: string; value: OG }>) {
const { key, value } = action.payload; const { key, value } = action.payload;
state.og[key] = value; state.og[key] = value;
}, },
updateHistoryMark(state, action: PayloadAction<{ type: ChatContext, id: number, mid: string, }>) { updateHistoryMark(
state,
action: PayloadAction<{ type: ChatContext; id: number; mid: string }>
) {
const { type, id, mid } = action.payload; const { type, id, mid } = action.payload;
if (type == "channel") { if (type == "channel") {
state.historyChannels[id] = mid; state.historyChannels[id] = mid;
@@ -211,14 +222,14 @@ const footprintSlice = createSlice({
state.readChannels[gid] = mid; state.readChannels[gid] = mid;
}); });
}, },
updateChannelVisibleAside(state, action: PayloadAction<{ id: number, aside: ChannelAside }>) { updateChannelVisibleAside(state, action: PayloadAction<{ id: number; aside: ChannelAside }>) {
const { id, aside } = action.payload; const { id, aside } = action.payload;
state.channelAsides[id] = aside; state.channelAsides[id] = aside;
}, },
updateDMVisibleAside(state, action: PayloadAction<{ id: number, aside: DMAside }>) { updateDMVisibleAside(state, action: PayloadAction<{ id: number; aside: DMAside }>) {
const { id, aside } = action.payload; const { id, aside } = action.payload;
state.dmAsides[id] = aside; state.dmAsides[id] = aside;
}, }
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(resetAuthData, (state) => { builder.addCase(resetAuthData, (state) => {
+7 -9
View File
@@ -1,13 +1,14 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
// import { ContentTypes } from "../config"; // import { ContentTypes } from "../config";
// import { normalizeFileMessage } from "../../common/utils"; // import { normalizeFileMessage } from "../../common/utils";
// import { ContentType } from "@/types/message"; // import { ContentType } from "@/types/message";
import { Archive } from "@/types/resource"; import { Archive } from "@/types/resource";
export interface State { export interface State {
[key: string]: Archive; [key: string]: Archive;
} }
const initialState: State = { const initialState: State = {};
};
const messageArchiveSlice = createSlice({ const messageArchiveSlice = createSlice({
name: "archiveMessage", name: "archiveMessage",
@@ -19,17 +20,14 @@ const messageArchiveSlice = createSlice({
fillArchiveMessage(state, action) { fillArchiveMessage(state, action) {
return Object.assign({ ...initialState }, action.payload); return Object.assign({ ...initialState }, action.payload);
}, },
upsertArchiveMessage(state, action: PayloadAction<{ filePath: string, data: Archive }>) { upsertArchiveMessage(state, action: PayloadAction<{ filePath: string; data: Archive }>) {
const { filePath, data } = action.payload; const { filePath, data } = action.payload;
state[filePath] = data; state[filePath] = data;
}, }
} }
}); });
export const { export const { resetArchiveMessage, fillArchiveMessage, upsertArchiveMessage } =
resetArchiveMessage, messageArchiveSlice.actions;
fillArchiveMessage,
upsertArchiveMessage
} = messageArchiveSlice.actions;
export default messageArchiveSlice.reducer; export default messageArchiveSlice.reducer;
+1 -1
View File
@@ -27,7 +27,7 @@ const channelMsgSlice = createSlice({
const localMsgExisted = state[id]!.findIndex((id) => id == local_id) > -1; const localMsgExisted = state[id]!.findIndex((id) => id == local_id) > -1;
if (midExisted || localMsgExisted) return; if (midExisted || localMsgExisted) return;
// 每次入库,都排序 // 每次入库,都排序
const newArr = [...state[id] as number[], +mid].sort((a, b) => a - b); const newArr = [...(state[id] as number[]), +mid].sort((a, b) => a - b);
state[id] = newArr; state[id] = newArr;
} else { } else {
state[id] = [+mid]; state[id] = [+mid];
+4 -2
View File
@@ -1,7 +1,9 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ContentType } from "@/types/message";
// import { ContentTypes } from "../config"; // import { ContentTypes } from "../config";
import { normalizeFileMessage } from "@/utils"; import { normalizeFileMessage } from "@/utils";
import { ContentType } from "@/types/message";
export interface MessagePayload { export interface MessagePayload {
mid: number; mid: number;
from_uid?: number; from_uid?: number;
@@ -45,7 +47,7 @@ const messageSlice = createSlice({
fillMessage(state, action) { fillMessage(state, action) {
return Object.assign({ ...initialState }, action.payload); return Object.assign({ ...initialState }, action.payload);
}, },
updateMessage(state, action: PayloadAction<{ mid: number;[key: string | number]: any }>) { updateMessage(state, action: PayloadAction<{ mid: number; [key: string | number]: any }>) {
const { mid, ...rest } = action.payload; const { mid, ...rest } = action.payload;
state[mid] = { ...state[mid], ...rest }; state[mid] = { ...state[mid], ...rest };
}, },
+1 -1
View File
@@ -40,7 +40,7 @@ const userMsgSlice = createSlice({
state.ids.push(+id); state.ids.push(+id);
} }
}, },
removeUserMsg(state, action: PayloadAction<{ id: number, mid: number }>) { removeUserMsg(state, action: PayloadAction<{ id: number; mid: number }>) {
const { id, mid } = action.payload; const { id, mid } = action.payload;
if (state.byId[id]) { if (state.byId[id]) {
const idx = state.byId[id].findIndex((i: number) => i == mid); const idx = state.byId[id].findIndex((i: number) => i == mid);
+15 -3
View File
@@ -1,9 +1,10 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Server } from "@/types/server"; import { Server } from "@/types/server";
export interface StoredServer extends Server { export interface StoredServer extends Server {
version: string, version: string;
upgraded: boolean, upgraded: boolean;
logo: string; logo: string;
inviteLink?: { inviteLink?: {
link: string; link: string;
@@ -49,7 +50,18 @@ const serverSlice = createSlice({
contact_verification_enable = false, contact_verification_enable = false,
chat_layout_mode = "Left" chat_layout_mode = "Left"
} = action.payload || {}; } = action.payload || {};
return { version, upgraded, name, logo, description, inviteLink, show_user_online_status, webclient_auto_update, contact_verification_enable, chat_layout_mode }; return {
version,
upgraded,
name,
logo,
description,
inviteLink,
show_user_online_status,
webclient_auto_update,
contact_verification_enable,
chat_layout_mode
};
}, },
updateInfo(state, action: PayloadAction<Partial<StoredServer>>) { updateInfo(state, action: PayloadAction<Partial<StoredServer>>) {
const values = action.payload || {}; const values = action.payload || {};
+1 -1
View File
@@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export type ListView = "item" | "grid" export type ListView = "item" | "grid";
export interface State { export interface State {
online: boolean; online: boolean;
ready: boolean; ready: boolean;
+12 -5
View File
@@ -1,15 +1,16 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { isNull, omitBy } from "lodash"; import { isNull, omitBy } from "lodash";
import BASE_URL from "../config";
import { Contact, ContactStatus } from "@/types/user";
import { UserLog, UserState } from "@/types/sse"; import { UserLog, UserState } from "@/types/sse";
import { Contact, ContactStatus } from "@/types/user";
import BASE_URL from "../config";
type DMAside = "voice" | null; type DMAside = "voice" | null;
export interface StoredUser extends Contact { export interface StoredUser extends Contact {
online?: boolean; online?: boolean;
voice?: boolean; voice?: boolean;
avatar?: string; avatar?: string;
visibleAside?: DMAside visibleAside?: DMAside;
} }
export interface State { export interface State {
@@ -99,7 +100,12 @@ const usersSlice = createSlice({
} }
}); });
}, },
updateContactStatus(state, action: PayloadAction<{ uid: number, status: ContactStatus } | { uid: number, status: ContactStatus }[]>) { updateContactStatus(
state,
action: PayloadAction<
{ uid: number; status: ContactStatus } | { uid: number; status: ContactStatus }[]
>
) {
const arr = Array.isArray(action.payload) ? action.payload : [action.payload]; const arr = Array.isArray(action.payload) ? action.payload : [action.payload];
arr.forEach((data) => { arr.forEach((data) => {
if (state.byId[data.uid]) { if (state.byId[data.uid]) {
@@ -110,5 +116,6 @@ const usersSlice = createSlice({
} }
}); });
export const { updateContactStatus, resetUsers, fillUsers, updateUsersByLogs, updateUsersStatus } = usersSlice.actions; export const { updateContactStatus, resetUsers, fillUsers, updateUsersByLogs, updateUsersStatus } =
usersSlice.actions;
export default usersSlice.reducer; export default usersSlice.reducer;
+56 -46
View File
@@ -1,53 +1,55 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { KEY_UID } from "../config";
import { ConnectionState } from "agora-rtc-sdk-ng"; import { ConnectionState } from "agora-rtc-sdk-ng";
import { resetAuthData } from "./auth.data";
import { ChatContext } from "@/types/common"; import { ChatContext } from "@/types/common";
import { KEY_UID } from "../config";
import { resetAuthData } from "./auth.data";
export type VoiceBasicInfo = { export type VoiceBasicInfo = {
context: ChatContext, context: ChatContext;
from?: number, from?: number;
id: number,// means to in dm context id: number; // means to in dm context
} };
export type VoicingInfo = { export type VoicingInfo = {
downlinkNetworkQuality?: number, downlinkNetworkQuality?: number;
joining?: boolean, joining?: boolean;
connectionState?: ConnectionState connectionState?: ConnectionState;
} & VoiceBasicInfo & VoicingMemberInfo } & VoiceBasicInfo &
VoicingMemberInfo;
export type VoicingMemberInfo = { export type VoicingMemberInfo = {
speakingVolume?: number, speakingVolume?: number;
muted?: boolean, muted?: boolean;
deafen?: boolean, deafen?: boolean;
video?: boolean, video?: boolean;
shareScreen?: boolean shareScreen?: boolean;
} };
export type VoicingMembers = { export type VoicingMembers = {
ids: number[], ids: number[];
byId: { byId: {
[key: number]: VoicingMemberInfo [key: number]: VoicingMemberInfo;
}, };
pin?: number pin?: number;
} };
export type VoiceInfo = { export type VoiceInfo = {
channelName: string, channelName: string;
memberCount: number memberCount: number;
} & VoiceBasicInfo } & VoiceBasicInfo;
export type DeviceInfo = Pick<MediaDeviceInfo, "deviceId" | "groupId" | "kind" | "label">; export type DeviceInfo = Pick<MediaDeviceInfo, "deviceId" | "groupId" | "kind" | "label">;
export type MediaDeviceKind = "audioinput" | "audiooutput" | "videoinput" export type MediaDeviceKind = "audioinput" | "audiooutput" | "videoinput";
interface State { interface State {
callingFrom: number, callingFrom: number;
callingTo: number, callingTo: number;
calling: boolean, calling: boolean;
voicing: VoicingInfo | null, voicing: VoicingInfo | null;
voicingMembers: VoicingMembers, voicingMembers: VoicingMembers;
list: VoiceInfo[], list: VoiceInfo[];
devices: DeviceInfo[], devices: DeviceInfo[];
audioInputDeviceId: string, audioInputDeviceId: string;
audioOutputDeviceId: string, audioOutputDeviceId: string;
videoInputDeviceId: string, videoInputDeviceId: string;
} }
const initialState: State = { const initialState: State = {
calling: false, calling: false,
@@ -68,7 +70,10 @@ const voiceSlice = createSlice({
name: "voice", name: "voice",
initialState, initialState,
reducers: { reducers: {
updateCallInfo(state, { payload }: PayloadAction<{ from: number, to?: number, calling?: boolean }>) { updateCallInfo(
state,
{ payload }: PayloadAction<{ from: number; to?: number; calling?: boolean }>
) {
const { from, to = 0, calling } = payload; const { from, to = 0, calling } = payload;
state.callingFrom = from; state.callingFrom = from;
state.callingTo = to; state.callingTo = to;
@@ -80,9 +85,9 @@ const voiceSlice = createSlice({
state.devices = payload; state.devices = payload;
if (payload.length > 0) { if (payload.length > 0) {
// 默认选择第一个 // 默认选择第一个
const audioInputDevice = payload.find(v => v.kind == "audioinput"); const audioInputDevice = payload.find((v) => v.kind == "audioinput");
const audioOutputDevice = payload.find(v => v.kind == "audiooutput"); const audioOutputDevice = payload.find((v) => v.kind == "audiooutput");
const videoInputDevice = payload.find(v => v.kind == "videoinput"); const videoInputDevice = payload.find((v) => v.kind == "videoinput");
if (audioInputDevice) { if (audioInputDevice) {
state.audioInputDeviceId = audioInputDevice.deviceId; state.audioInputDeviceId = audioInputDevice.deviceId;
} }
@@ -97,7 +102,10 @@ const voiceSlice = createSlice({
updateCalling(state, { payload }: PayloadAction<boolean>) { updateCalling(state, { payload }: PayloadAction<boolean>) {
state.calling = payload; state.calling = payload;
}, },
updateSelectDeviceId(state, { payload }: PayloadAction<{ kind: MediaDeviceKind, value: string }>) { updateSelectDeviceId(
state,
{ payload }: PayloadAction<{ kind: MediaDeviceKind; value: string }>
) {
const { kind, value } = payload; const { kind, value } = payload;
switch (kind) { switch (kind) {
case "audioinput": case "audioinput":
@@ -152,7 +160,6 @@ const voiceSlice = createSlice({
} }
}); });
} }
} else { } else {
// reset // reset
state.voicing = payload; state.voicing = payload;
@@ -177,7 +184,7 @@ const voiceSlice = createSlice({
state.list = payload; state.list = payload;
} else { } else {
const { id, context } = payload; const { id, context } = payload;
const idx = state.list.findIndex(v => v.id == id && v.context == context); const idx = state.list.findIndex((v) => v.id == id && v.context == context);
if (idx > -1) { if (idx > -1) {
state.list.splice(idx, 1, payload); state.list.splice(idx, 1, payload);
} else { } else {
@@ -203,14 +210,17 @@ const voiceSlice = createSlice({
delete state.voicingMembers.byId[payload]; delete state.voicingMembers.byId[payload];
} }
}, },
updateVoicingMember(state, { payload }: PayloadAction<{ uid: number, info: VoicingMemberInfo }>) { updateVoicingMember(
state,
{ payload }: PayloadAction<{ uid: number; info: VoicingMemberInfo }>
) {
const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload.uid); const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload.uid);
if (idx > -1) { if (idx > -1) {
const { uid, info } = payload; const { uid, info } = payload;
state.voicingMembers.byId[uid] = { ...state.voicingMembers.byId[uid], ...info }; state.voicingMembers.byId[uid] = { ...state.voicingMembers.byId[uid], ...info };
} }
}, },
updatePin(state, { payload }: PayloadAction<{ uid: number, action: "pin" | "unpin" }>) { updatePin(state, { payload }: PayloadAction<{ uid: number; action: "pin" | "unpin" }>) {
const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload.uid); const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload.uid);
if (idx > -1) { if (idx > -1) {
state.voicingMembers.pin = payload.action == "pin" ? payload.uid : undefined; state.voicingMembers.pin = payload.action == "pin" ? payload.uid : undefined;
@@ -223,13 +233,13 @@ const voiceSlice = createSlice({
if (window.VOICE_CLIENT) { if (window.VOICE_CLIENT) {
window.VOICE_CLIENT.leave(); window.VOICE_CLIENT.leave();
Object.entries(window.VOICE_TRACK_MAP).forEach(([uid, track]) => { Object.entries(window.VOICE_TRACK_MAP).forEach(([uid, track]) => {
if (track && 'close' in track) { if (track && "close" in track) {
track.close(); track.close();
} }
delete window.VOICE_TRACK_MAP[+uid]; delete window.VOICE_TRACK_MAP[+uid];
}); });
Object.entries(window.VIDEO_TRACK_MAP).forEach(([uid, track]) => { Object.entries(window.VIDEO_TRACK_MAP).forEach(([uid, track]) => {
if (track && 'close' in track) { if (track && "close" in track) {
track?.close(); track?.close();
} }
delete window.VOICE_TRACK_MAP[+uid]; delete window.VOICE_TRACK_MAP[+uid];
+17 -16
View File
@@ -1,26 +1,27 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { combineReducers, configureStore } from "@reduxjs/toolkit"; import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query"; import { setupListeners } from "@reduxjs/toolkit/query";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import listenerMiddleware from "./listener.middleware"; 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";
import channelsReducer from "./slices/channels";
import usersReducer from "./slices/users";
import reactionMsgReducer from "./slices/message.reaction";
import channelMsgReducer from "./slices/message.channel";
import userMsgReducer from "./slices/message.user";
import favoritesReducer from "./slices/favorites";
import fileMsgReducer from "./slices/message.file";
import archiveMsgReducer from "./slices/message.archive";
import messageReducer from "./slices/message";
import { authApi } from "./services/auth"; import { authApi } from "./services/auth";
import { userApi } from "./services/user";
import { channelApi } from "./services/channel"; import { channelApi } from "./services/channel";
import { messageApi } from "./services/message"; import { messageApi } from "./services/message";
import { serverApi } from "./services/server"; import { serverApi } from "./services/server";
import { userApi } from "./services/user";
import authDataReducer from "./slices/auth.data";
import channelsReducer from "./slices/channels";
import favoritesReducer from "./slices/favorites";
import footprintReducer from "./slices/footprint";
import messageReducer from "./slices/message";
import archiveMsgReducer from "./slices/message.archive";
import channelMsgReducer from "./slices/message.channel";
import fileMsgReducer from "./slices/message.file";
import reactionMsgReducer from "./slices/message.reaction";
import userMsgReducer from "./slices/message.user";
import serverReducer from "./slices/server";
import uiReducer from "./slices/ui";
import usersReducer from "./slices/users";
import voiceReducer from "./slices/voice";
const reducer = combineReducers({ const reducer = combineReducers({
authData: authDataReducer, authData: authDataReducer,
+1 -1
View File
@@ -1,4 +1,4 @@
import { useRef, useEffect } from "react"; import { useEffect, useRef } from "react";
export default function useInView<T extends HTMLElement>() { export default function useInView<T extends HTMLElement>() {
const ref = useRef<T>(null); const ref = useRef<T>(null);
+1
View File
@@ -4,6 +4,7 @@ import {
createParagraphPlugin, createParagraphPlugin,
createSelectOnBackspacePlugin createSelectOnBackspacePlugin
} from "@udecode/plate"; } from "@udecode/plate";
import { CONFIG } from "./config"; import { CONFIG } from "./config";
const basicElements = [ const basicElements = [
@@ -1,7 +1,9 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { initializeApp } from "firebase/app"; import { initializeApp } from "firebase/app";
import { getToken, getMessaging } from "firebase/messaging"; import { getMessaging, getToken } from "firebase/messaging";
import { firebaseConfig, KEY_DEVICE_TOKEN } from "@/app/config"; import { firebaseConfig, KEY_DEVICE_TOKEN } from "@/app/config";
let requesting = false; let requesting = false;
let error = false; let error = false;
const useDeviceToken = (vapidKey: string) => { const useDeviceToken = (vapidKey: string) => {
+308 -274
View File
@@ -1,287 +1,321 @@
import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng'; import { useDispatch } from "react-redux";
import { useDispatch } from 'react-redux'; import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from "agora-rtc-sdk-ng";
import { ShareScreenTrack } from '../../types/global';
import AudioJoin from '@/assets/join.wav';
import { useGenerateAgoraTokenMutation } from '../../app/services/server';
import { updateChannelVisibleAside, updateDMVisibleAside } from '../../app/slices/footprint';
import { addVoiceMember, updatePin, updateVoicingInfo, upsertVoiceList } from '../../app/slices/voice';
import { useAppSelector } from '../../app/store';
import { playAgoraVideo } from '../../utils';
import { ChatContext } from '../../types/common';
import AudioJoin from "@/assets/join.wav";
import { useGenerateAgoraTokenMutation } from "../../app/services/server";
import { updateChannelVisibleAside, updateDMVisibleAside } from "../../app/slices/footprint";
import {
addVoiceMember,
updatePin,
updateVoicingInfo,
upsertVoiceList
} from "../../app/slices/voice";
import { useAppSelector } from "../../app/store";
import { ChatContext } from "../../types/common";
import { ShareScreenTrack } from "../../types/global";
import { playAgoraVideo } from "../../utils";
type VoiceProps = { type VoiceProps = {
id: number, id: number;
context?: ChatContext context?: ChatContext;
} };
const audioJoin = new Audio(AudioJoin); const audioJoin = new Audio(AudioJoin);
const useVoice = ({ id, context = "channel" }: VoiceProps) => { const useVoice = ({ id, context = "channel" }: VoiceProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { voicingInfo, loginUid, audioInputDevices, audioOutputDevices, videoInputDevices, audioInputDeviceId, audioOutputDeviceId, videoInputDeviceId } = useAppSelector(store => { const {
return { voicingInfo,
loginUid: store.authData.user?.uid ?? 0, loginUid,
voicingInfo: store.voice.voicing, audioInputDevices,
audioInputDevices: store.voice.devices.filter(d => d.kind == "audioinput") ?? [], audioOutputDevices,
audioOutputDevices: store.voice.devices.filter(d => d.kind == "audiooutput") ?? [], videoInputDevices,
videoInputDevices: store.voice.devices.filter(d => d.kind == "videoinput") ?? [], audioInputDeviceId,
audioInputDeviceId: store.voice.audioInputDeviceId, audioOutputDeviceId,
audioOutputDeviceId: store.voice.audioOutputDeviceId, videoInputDeviceId
videoInputDeviceId: store.voice.videoInputDeviceId, } = useAppSelector((store) => {
}; return {
}); loginUid: store.authData.user?.uid ?? 0,
const [generateToken] = useGenerateAgoraTokenMutation(); voicingInfo: store.voice.voicing,
// const [joining, setJoining] = useState(false); audioInputDevices: store.voice.devices.filter((d) => d.kind == "audioinput") ?? [],
const joinVoice = async () => { audioOutputDevices: store.voice.devices.filter((d) => d.kind == "audiooutput") ?? [],
// setJoining(true); videoInputDevices: store.voice.devices.filter((d) => d.kind == "videoinput") ?? [],
dispatch(updateVoicingInfo({ audioInputDeviceId: store.voice.audioInputDeviceId,
id, audioOutputDeviceId: store.voice.audioOutputDeviceId,
context, videoInputDeviceId: store.voice.videoInputDeviceId
joining: true
}));
const resp = await generateToken(context == "channel" ? { gid: id } : { uid: id });
if ('error' in resp) {
console.error("generate agora token error");
dispatch(updateVoicingInfo({
joining: false,
id,
context,
}));
} else {
console.table(resp.data);
const { channel_name, app_id, agora_token, uid } = resp.data;
if (window.VOICE_CLIENT) {
await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid);
// Create a local audio track from the microphone audio.
const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack({ microphoneId: audioInputDeviceId });
// Publish the local audio track in the channel.
await window.VOICE_CLIENT.publish(localAudioTrack);
// play the join audio
try {
await audioJoin.play();
} catch (error) {
console.warn("play join sound failed!", error);
}
console.log("Publish success!,joined the channel");
dispatch(updateVoicingInfo({
deafen: false,
muted: false,
joining: false,
id,
context,
}));
// 把自己加进去
dispatch(addVoiceMember(uid));
// 放到全局变量里
window.VOICE_TRACK_MAP[loginUid] = localAudioTrack;
}
}
// setJoining(false);
}; };
const openCamera = async () => { });
const localVideoTrack = await AgoraRTC.createCameraVideoTrack({ cameraId: videoInputDeviceId }); const [generateToken] = useGenerateAgoraTokenMutation();
// 取消正在进行的桌面共享 // const [joining, setJoining] = useState(false);
await stopShareScreen(); const joinVoice = async () => {
await window.VOICE_CLIENT?.publish(localVideoTrack); // setJoining(true);
dispatch(updateVoicingInfo({ dispatch(
video: true, updateVoicingInfo({
shareScreen: false, id,
id, context,
context, joining: true
})); })
// 放到全局变量里 );
window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack; const resp = await generateToken(context == "channel" ? { gid: id } : { uid: id });
playAgoraVideo(loginUid); if ("error" in resp) {
}; console.error("generate agora token error");
const closeCamera = async () => { dispatch(
const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack; updateVoicingInfo({
if (localVideoTrack) { joining: false,
await window.VOICE_CLIENT?.unpublish(localVideoTrack); id,
localVideoTrack.close(); context
window.VIDEO_TRACK_MAP[loginUid] = null; })
// 关闭视频后,需要把视频的高度设置回去 );
const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; } else {
playerEle.classList.remove("h-[120px]"); console.table(resp.data);
dispatch(updateVoicingInfo({ const { channel_name, app_id, agora_token, uid } = resp.data;
video: false, if (window.VOICE_CLIENT) {
shareScreen: false, await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid);
id, // Create a local audio track from the microphone audio.
context, const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack({
})); microphoneId: audioInputDeviceId
} });
}; // Publish the local audio track in the channel.
const stopShareScreen = async () => { await window.VOICE_CLIENT.publish(localAudioTrack);
let localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ShareScreenTrack | null; // play the join audio
if (!localVideoTrack) return;
await window.VOICE_CLIENT?.unpublish(localVideoTrack);
if ("close" in localVideoTrack) {
localVideoTrack.close();
} else {
localVideoTrack[0].close();
// localVideoTrack[0]=null
}
// localVideoTrack.close();
window.VIDEO_TRACK_MAP[loginUid] = null;
// 关闭视频后,需要把视频的高度设置回去
const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement;
playerEle.classList.remove("h-[120px]");
dispatch(updateVoicingInfo({
video: false,
shareScreen: false,
id,
context,
}));
};
const startShareScreen = async () => {
try { try {
const localVideoTrack = await AgoraRTC.createScreenVideoTrack({ await audioJoin.play();
// electronScreenSourceId: "share_screen",
selfBrowserSurface: "exclude",
// 配置屏幕共享编码参数,详情请查看 API 文档。
encoderConfig: "1080p_1",
// 设置视频传输优化策略为清晰优先或流畅优先。
optimizationMode: "detail",
});
// 取消正在进行的视频
await closeCamera();
await window.VOICE_CLIENT?.publish(localVideoTrack);
dispatch(updateVoicingInfo({
video: false,
shareScreen: true,
id,
context,
}));
// 放到全局变量里
window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack;
playAgoraVideo(loginUid);
// 进入全屏并Pin自己
if (context == "channel") {
dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" }));
} else {
dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" }));
}
dispatch(updatePin({ uid: loginUid, action: "pin" }));
// 监听屏幕共享结束事件
if ("close" in localVideoTrack) {
localVideoTrack.getMediaStreamTrack().onended = () => {
stopShareScreen();
};
}
} catch (error) { } catch (error) {
console.log("start share screen error", error); console.warn("play join sound failed!", error);
} }
}; console.log("Publish success!,joined the channel");
dispatch(
const leave = async () => { updateVoicingInfo({
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; deafen: false,
const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack; muted: false,
if (window.VOICE_CLIENT) { joining: false,
await window.VOICE_CLIENT.leave();
dispatch(updateVoicingInfo(null));
if (localAudioTrack) {
localAudioTrack.close();
window.VOICE_TRACK_MAP[loginUid] = null;
}
if (localVideoTrack) {
localVideoTrack.close();
window.VIDEO_TRACK_MAP[loginUid] = null;
}
const updateAside = context == "channel" ? updateChannelVisibleAside : updateDMVisibleAside;
dispatch(updateAside({ id, aside: null }));
// 即时更新对应的活跃列表信息
dispatch(upsertVoiceList({
id,
context,
memberCount: 0,
// will fix it
channelName: `vocechat:${context}:${id}`,
}));
}
};
const setMute = (mute: boolean) => {
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack;
if (!localAudioTrack) return;
localAudioTrack.setMuted(mute);
if (mute == false && voicingInfo?.deafen) {
// 远端音频,恢复原音
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => {
audioTrack?.setVolume(100);
});
dispatch(updateVoicingInfo({
muted: false,
id,
context
}));
}
dispatch(updateVoicingInfo({
muted: mute,
id, id,
context context
})); })
}; );
const setDeafen = (deafen: boolean) => { // 把自己加进去
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; dispatch(addVoiceMember(uid));
if (!localAudioTrack) return; // 放到全局变量里
if (deafen) { window.VOICE_TRACK_MAP[loginUid] = localAudioTrack;
localAudioTrack.setMuted(true); }
// 远端音频,全部静音 }
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { // setJoining(false);
audioTrack?.setVolume(0); };
}); const openCamera = async () => {
} else { const localVideoTrack = await AgoraRTC.createCameraVideoTrack({ cameraId: videoInputDeviceId });
localAudioTrack.setMuted(false); // 取消正在进行的桌面共享
// 远端音频,恢复原音 await stopShareScreen();
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { await window.VOICE_CLIENT?.publish(localVideoTrack);
audioTrack?.setVolume(100); dispatch(
}); updateVoicingInfo({
} video: true,
dispatch(updateVoicingInfo({ deafen, id, context })); shareScreen: false,
}; id,
const enterFullscreen = (uid?: number) => { context
if (context == "channel") { })
dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); );
} else { // 放到全局变量里
dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" })); window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack;
} playAgoraVideo(loginUid);
if (uid) { };
dispatch(updatePin({ uid, action: "pin" })); const closeCamera = async () => {
} const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack;
}; if (localVideoTrack) {
const exitFullscreen = () => { await window.VOICE_CLIENT?.unpublish(localVideoTrack);
if (context == "channel") { localVideoTrack.close();
dispatch(updateChannelVisibleAside({ id, aside: "voice" })); window.VIDEO_TRACK_MAP[loginUid] = null;
} else { // 关闭视频后,需要把视频的高度设置回去
dispatch(updateDMVisibleAside({ id, aside: null })); const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement;
} playerEle.classList.remove("h-[120px]");
}; dispatch(
const joinedAtThisContext = voicingInfo ? (voicingInfo.id == id && voicingInfo.context == context) : false; updateVoicingInfo({
return { video: false,
setMute, shareScreen: false,
setDeafen, id,
leave, context
// canVoice, })
voicingInfo, );
joining: voicingInfo ? voicingInfo.joining : undefined, }
joinedAtThisContext, };
joined: !!voicingInfo, const stopShareScreen = async () => {
joinVoice, let localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ShareScreenTrack | null;
openCamera, if (!localVideoTrack) return;
closeCamera, await window.VOICE_CLIENT?.unpublish(localVideoTrack);
startShareScreen, if ("close" in localVideoTrack) {
stopShareScreen, localVideoTrack.close();
enterFullscreen, } else {
exitFullscreen, localVideoTrack[0].close();
audioInputDevices, // localVideoTrack[0]=null
audioOutputDevices, }
videoInputDevices, // localVideoTrack.close();
audioInputDeviceId, window.VIDEO_TRACK_MAP[loginUid] = null;
audioOutputDeviceId, // 关闭视频后,需要把视频的高度设置回去
videoInputDeviceId const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement;
}; playerEle.classList.remove("h-[120px]");
dispatch(
updateVoicingInfo({
video: false,
shareScreen: false,
id,
context
})
);
};
const startShareScreen = async () => {
try {
const localVideoTrack = await AgoraRTC.createScreenVideoTrack({
// electronScreenSourceId: "share_screen",
selfBrowserSurface: "exclude",
// 配置屏幕共享编码参数,详情请查看 API 文档。
encoderConfig: "1080p_1",
// 设置视频传输优化策略为清晰优先或流畅优先。
optimizationMode: "detail"
});
// 取消正在进行的视频
await closeCamera();
await window.VOICE_CLIENT?.publish(localVideoTrack);
dispatch(
updateVoicingInfo({
video: false,
shareScreen: true,
id,
context
})
);
// 放到全局变量里
window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack;
playAgoraVideo(loginUid);
// 进入全屏并Pin自己
if (context == "channel") {
dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" }));
} else {
dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" }));
}
dispatch(updatePin({ uid: loginUid, action: "pin" }));
// 监听屏幕共享结束事件
if ("close" in localVideoTrack) {
localVideoTrack.getMediaStreamTrack().onended = () => {
stopShareScreen();
};
}
} catch (error) {
console.log("start share screen error", error);
}
};
const leave = async () => {
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack;
const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack;
if (window.VOICE_CLIENT) {
await window.VOICE_CLIENT.leave();
dispatch(updateVoicingInfo(null));
if (localAudioTrack) {
localAudioTrack.close();
window.VOICE_TRACK_MAP[loginUid] = null;
}
if (localVideoTrack) {
localVideoTrack.close();
window.VIDEO_TRACK_MAP[loginUid] = null;
}
const updateAside = context == "channel" ? updateChannelVisibleAside : updateDMVisibleAside;
dispatch(updateAside({ id, aside: null }));
// 即时更新对应的活跃列表信息
dispatch(
upsertVoiceList({
id,
context,
memberCount: 0,
// will fix it
channelName: `vocechat:${context}:${id}`
})
);
}
};
const setMute = (mute: boolean) => {
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack;
if (!localAudioTrack) return;
localAudioTrack.setMuted(mute);
if (mute == false && voicingInfo?.deafen) {
// 远端音频,恢复原音
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => {
audioTrack?.setVolume(100);
});
dispatch(
updateVoicingInfo({
muted: false,
id,
context
})
);
}
dispatch(
updateVoicingInfo({
muted: mute,
id,
context
})
);
};
const setDeafen = (deafen: boolean) => {
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack;
if (!localAudioTrack) return;
if (deafen) {
localAudioTrack.setMuted(true);
// 远端音频,全部静音
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => {
audioTrack?.setVolume(0);
});
} else {
localAudioTrack.setMuted(false);
// 远端音频,恢复原音
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => {
audioTrack?.setVolume(100);
});
}
dispatch(updateVoicingInfo({ deafen, id, context }));
};
const enterFullscreen = (uid?: number) => {
if (context == "channel") {
dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" }));
} else {
dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" }));
}
if (uid) {
dispatch(updatePin({ uid, action: "pin" }));
}
};
const exitFullscreen = () => {
if (context == "channel") {
dispatch(updateChannelVisibleAside({ id, aside: "voice" }));
} else {
dispatch(updateDMVisibleAside({ id, aside: null }));
}
};
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,
openCamera,
closeCamera,
startShareScreen,
stopShareScreen,
enterFullscreen,
exitFullscreen,
audioInputDevices,
audioOutputDevices,
videoInputDevices,
audioInputDeviceId,
audioOutputDeviceId,
videoInputDeviceId
};
}; };
export default useVoice; export default useVoice;
+1
View File
@@ -1,4 +1,5 @@
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { addMessage, MessagePayload } from "@/app/slices/message"; import { addMessage, MessagePayload } from "@/app/slices/message";
import { addChannelMsg } from "@/app/slices/message.channel"; import { addChannelMsg } from "@/app/slices/message.channel";
import { addUserMsg } from "@/app/slices/message.user"; import { addUserMsg } from "@/app/slices/message.user";
+2 -1
View File
@@ -1,4 +1,5 @@
import { useRef, useEffect } from "react"; import { useEffect, useRef } from "react";
// import { useDebounce } from "rooks"; // import { useDebounce } from "rooks";
function useChatScroll<T extends HTMLElement>() { function useChatScroll<T extends HTMLElement>() {
+17 -11
View File
@@ -1,18 +1,20 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { import {
useUpdateLoginConfigMutation, useLazyGetAgoraConfigQuery,
useUpdateSMTPConfigMutation, useLazyGetFirebaseConfigQuery,
useLazyGetLoginConfigQuery,
useLazyGetSMTPConfigQuery,
useUpdateAgoraConfigMutation, useUpdateAgoraConfigMutation,
useUpdateFirebaseConfigMutation, useUpdateFirebaseConfigMutation,
useLazyGetFirebaseConfigQuery, useUpdateLoginConfigMutation,
useLazyGetAgoraConfigQuery, useUpdateSMTPConfigMutation
useLazyGetSMTPConfigQuery,
useLazyGetLoginConfigQuery
} from "@/app/services/server"; } from "@/app/services/server";
import { AgoraConfig, FirebaseConfig, LoginConfig, SMTPConfig } from "@/types/server"; import { AgoraConfig, FirebaseConfig, LoginConfig, SMTPConfig } from "@/types/server";
import { useTranslation } from "react-i18next";
// config: smtp agora login firebase // config: smtp agora login firebase
type ConfigType = "smtp" | "agora" | "login" | "firebase"; type ConfigType = "smtp" | "agora" | "login" | "firebase";
type ConfigMap = Record<ConfigType, AgoraConfig | FirebaseConfig | LoginConfig | SMTPConfig>; type ConfigMap = Record<ConfigType, AgoraConfig | FirebaseConfig | LoginConfig | SMTPConfig>;
@@ -23,10 +25,14 @@ export default function useConfig(config: keyof ConfigMap = "smtp") {
const { t: ct } = useTranslation(); const { t: ct } = useTranslation();
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
const [values, setValues] = useState<valuesOf<ConfigMap> | undefined>(undefined); const [values, setValues] = useState<valuesOf<ConfigMap> | undefined>(undefined);
const [updateLoginConfig, { isSuccess: LoginUpdated, isLoading: LoginUpdating }] = useUpdateLoginConfigMutation(); const [updateLoginConfig, { isSuccess: LoginUpdated, isLoading: LoginUpdating }] =
const [updateSMTPConfig, { isSuccess: SMTPUpdated, isLoading: SMTPUpdating }] = useUpdateSMTPConfigMutation(); useUpdateLoginConfigMutation();
const [updateAgoraConfig, { isSuccess: AgoraUpdated, isLoading: AgoraUpdating }] = useUpdateAgoraConfigMutation(); const [updateSMTPConfig, { isSuccess: SMTPUpdated, isLoading: SMTPUpdating }] =
const [updateFirebaseConfig, { isSuccess: FirebaseUpdated, isLoading: FirebaseUpdating }] = useUpdateFirebaseConfigMutation(); useUpdateSMTPConfigMutation();
const [updateAgoraConfig, { isSuccess: AgoraUpdated, isLoading: AgoraUpdating }] =
useUpdateAgoraConfigMutation();
const [updateFirebaseConfig, { isSuccess: FirebaseUpdated, isLoading: FirebaseUpdating }] =
useUpdateFirebaseConfigMutation();
const [getAgoraConfig, { data: agoraConfig }] = useLazyGetAgoraConfigQuery(); const [getAgoraConfig, { data: agoraConfig }] = useLazyGetAgoraConfigQuery();
const [getLoginConfig, { data: loginConfig }] = useLazyGetLoginConfigQuery(); const [getLoginConfig, { data: loginConfig }] = useLazyGetLoginConfigQuery();
const [getSMTPConfig, { data: smtpConfig }] = useLazyGetSMTPConfigQuery(); const [getSMTPConfig, { data: smtpConfig }] = useLazyGetSMTPConfigQuery();
+2 -2
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { copyImageToClipboard } from "copy-image-clipboard";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { copyImageToClipboard } from "copy-image-clipboard";
const useCopy = (config: { enableToast: boolean } | void) => { const useCopy = (config: { enableToast: boolean } | void) => {
const { enableToast = true } = config || {}; const { enableToast = true } = config || {};
+2 -1
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import { Channel } from "@/types/channel"; import { Channel } from "@/types/channel";
+2 -1
View File
@@ -1,7 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import { useSendChannelMsgMutation } from "@/app/services/channel"; import { useSendChannelMsgMutation } from "@/app/services/channel";
import { useSendMsgMutation } from "@/app/services/user";
import { useCreateArchiveMutation } from "@/app/services/message"; import { useCreateArchiveMutation } from "@/app/services/message";
import { useSendMsgMutation } from "@/app/services/user";
export default function useForwardMessage() { export default function useForwardMessage() {
const [forwarding, setForwarding] = useState(false); const [forwarding, setForwarding] = useState(false);
+2 -1
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { import {
useGetGoogleAuthConfigQuery, useGetGoogleAuthConfigQuery,
useUpdateGoogleAuthConfigMutation useUpdateGoogleAuthConfigMutation
+1 -1
View File
@@ -1,4 +1,4 @@
import { useUpdateChannelMutation, useLazyLeaveChannelQuery } from "@/app/services/channel"; import { useLazyLeaveChannelQuery, useUpdateChannelMutation } from "@/app/services/channel";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
export default function useLeaveChannel(cid: number) { export default function useLeaveChannel(cid: number) {
+14 -3
View File
@@ -1,23 +1,34 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { import {
useCheckLicenseMutation, useCheckLicenseMutation,
useGetLicenseQuery, useGetLicenseQuery,
useUpsertLicenseMutation useUpsertLicenseMutation
} from "@/app/services/server"; } from "@/app/services/server";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
// type Props = { // type Props = {
// refetchOnMountOrArgChange?: boolean // refetchOnMountOrArgChange?: boolean
// } | undefined // } | undefined
const useLicense = (refetchOnMountOrArgChange = false) => { const useLicense = (refetchOnMountOrArgChange = false) => {
const { userCount, isGuest, upgraded } = useAppSelector((store) => { const { userCount, isGuest, upgraded } = useAppSelector((store) => {
return { userCount: store.users.ids.length, isGuest: store.authData.guest, upgraded: store.server.upgraded }; return {
userCount: store.users.ids.length,
isGuest: store.authData.guest,
upgraded: store.server.upgraded
};
}); });
const { data: license, refetch: refetchLicense, isLoading } = useGetLicenseQuery(undefined, { const {
data: license,
refetch: refetchLicense,
isLoading
} = useGetLicenseQuery(undefined, {
refetchOnMountOrArgChange, refetchOnMountOrArgChange,
skip: isGuest skip: isGuest
}); });
const [check, { isLoading: isChecking, isSuccess: checked }] = useCheckLicenseMutation(); const [check, { isLoading: isChecking, isSuccess: checked }] = useCheckLicenseMutation();
const [upsert, { isSuccess: upserted, isLoading: upserting, reset: resetUpsert }] = useUpsertLicenseMutation(); const [upsert, { isSuccess: upserted, isLoading: upserting, reset: resetUpsert }] =
useUpsertLicenseMutation();
const checkLicense = (l: string) => { const checkLicense = (l: string) => {
check(l); check(l);
}; };
+10 -8
View File
@@ -1,13 +1,15 @@
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { resetFootprint } from "@/app/slices/footprint";
import { resetChannels } from "@/app/slices/channels";
import { resetUsers } from "@/app/slices/users";
import { resetChannelMsg } from "@/app/slices/message.channel";
import { resetUserMsg } from "@/app/slices/message.user";
import { resetReactionMessage } from "@/app/slices/message.reaction";
import { resetFileMessage } from "@/app/slices/message.file";
import { resetMessage } from "@/app/slices/message";
import { useLazyLogoutQuery } from "@/app/services/auth"; import { useLazyLogoutQuery } from "@/app/services/auth";
import { resetChannels } from "@/app/slices/channels";
import { resetFootprint } from "@/app/slices/footprint";
import { resetMessage } from "@/app/slices/message";
import { resetChannelMsg } from "@/app/slices/message.channel";
import { resetFileMessage } from "@/app/slices/message.file";
import { resetReactionMessage } from "@/app/slices/message.reaction";
import { resetUserMsg } from "@/app/slices/message.user";
import { resetUsers } from "@/app/slices/users";
export default function useLogout() { export default function useLogout() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [logout, { isLoading, isSuccess }] = useLazyLogoutQuery(); const [logout, { isLoading, isSuccess }] = useLazyLogoutQuery();
+6 -6
View File
@@ -1,14 +1,14 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { normalizeArchiveData } from "../utils";
import { useLazyGetArchiveMessageQuery } from "@/app/services/message"; import { useLazyGetArchiveMessageQuery } from "@/app/services/message";
import { ArchiveMessage } from "@/types/resource";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import { ArchiveMessage } from "@/types/resource";
import { normalizeArchiveData } from "../utils";
export default function useNormalizeMessage(filePath: string | null) { export default function useNormalizeMessage(filePath: string | null) {
const archiveData = useAppSelector(store => store.archiveMessage[filePath ?? ""]); const archiveData = useAppSelector((store) => store.archiveMessage[filePath ?? ""]);
const [normalizedMessages, setNormalizedMessages] = useState<ArchiveMessage[] | null>(null); const [normalizedMessages, setNormalizedMessages] = useState<ArchiveMessage[] | null>(null);
const [getArchiveMessage, { isError, isLoading, isSuccess }] = const [getArchiveMessage, { isError, isLoading, isSuccess }] = useLazyGetArchiveMessageQuery();
useLazyGetArchiveMessageQuery();
useEffect(() => { useEffect(() => {
if (archiveData) { if (archiveData) {
const msgs = normalizeArchiveData(archiveData, filePath); const msgs = normalizeArchiveData(archiveData, filePath);
+2 -1
View File
@@ -1,4 +1,5 @@
import { useRef, useEffect } from "react"; import { useEffect, useRef } from "react";
import { KEY_PWA_INSTALLED } from "@/app/config"; import { KEY_PWA_INSTALLED } from "@/app/config";
import { BeforeInstallPromptEvent } from "@/types/global"; import { BeforeInstallPromptEvent } from "@/types/global";
+2 -1
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { usePinMessageMutation, useUnpinMessageMutation } from "@/app/services/message"; import { usePinMessageMutation, useUnpinMessageMutation } from "@/app/services/message";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import { PinnedMessage } from "@/types/channel"; import { PinnedMessage } from "@/types/channel";
+34 -12
View File
@@ -1,14 +1,19 @@
import { useEffect } from "react"; import { useEffect } from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import initCache, { useRehydrate } from "@/app/cache"; import initCache, { useRehydrate } from "@/app/cache";
import { useLazyGetFavoritesQuery } from "@/app/services/message"; import { useLazyGetFavoritesQuery, useLazyLoadMoreMessagesQuery } from "@/app/services/message";
import {
useLazyGetServerQuery,
useLazyGetServerVersionQuery,
useLazyGetSystemCommonQuery
} from "@/app/services/server";
import { useLazyGetContactsQuery, useLazyGetUsersQuery } from "@/app/services/user"; import { useLazyGetContactsQuery, useLazyGetUsersQuery } from "@/app/services/user";
import { useLazyGetSystemCommonQuery, useLazyGetServerQuery, useLazyGetServerVersionQuery } from "@/app/services/server";
import useStreaming from "./useStreaming";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import { useLazyLoadMoreMessagesQuery } from "@/app/services/message";
import useLicense from "./useLicense";
import { compareVersion } from "../utils"; import { compareVersion } from "../utils";
import useLicense from "./useLicense";
import useStreaming from "./useStreaming";
// type Props={ // type Props={
// guest?:boolean // guest?:boolean
// } // }
@@ -44,16 +49,27 @@ export default function usePreload() {
data: favorites data: favorites
} }
] = useLazyGetFavoritesQuery(); ] = useLazyGetFavoritesQuery();
const [getUsers, { isLoading: usersLoading, isSuccess: usersSuccess, isError: usersError, data: users }] = useLazyGetUsersQuery(); const [
getUsers,
{ isLoading: usersLoading, isSuccess: usersSuccess, isError: usersError, data: users }
] = useLazyGetUsersQuery();
const [ const [
getContacts, getContacts,
{ isLoading: contactsLoading, isSuccess: contactsSuccess, isError: contactsError, data: contacts } {
isLoading: contactsLoading,
isSuccess: contactsSuccess,
isError: contactsError,
data: contacts
}
] = useLazyGetContactsQuery(); ] = useLazyGetContactsQuery();
const [ const [
getServer, getServer,
{ isLoading: serverLoading, isSuccess: serverSuccess, isError: serverError, data: server } { isLoading: serverLoading, isSuccess: serverSuccess, isError: serverError, data: server }
] = useLazyGetServerQuery(); ] = useLazyGetServerQuery();
const [getServerVersion, { data: serverVersion, isSuccess: serverVersionSuccess, isLoading: loadingServerVersion }] = useLazyGetServerVersionQuery(); const [
getServerVersion,
{ data: serverVersion, isSuccess: serverVersionSuccess, isLoading: loadingServerVersion }
] = useLazyGetServerVersionQuery();
const [getSystemCommon] = useLazyGetSystemCommonQuery(); const [getSystemCommon] = useLazyGetSystemCommonQuery();
useEffect(() => { useEffect(() => {
initCache(); initCache();
@@ -79,14 +95,14 @@ export default function usePreload() {
useEffect(() => { useEffect(() => {
if (rehydrated && serverVersion) { if (rehydrated && serverVersion) {
getUsers().then(() => { getUsers().then(() => {
if (compareVersion(serverVersion, '0.3.7') >= 0 && !isGuest) { if (compareVersion(serverVersion, "0.3.7") >= 0 && !isGuest) {
// 根据版本号判断是否需要初始化contact // 根据版本号判断是否需要初始化contact
getContacts(); getContacts();
} }
}); });
getServer(); getServer();
getFavorites(); getFavorites();
if (compareVersion(serverVersion, '0.3.4') >= 0) { if (compareVersion(serverVersion, "0.3.4") >= 0) {
// 根据版本号判断是否需要调用system common api // 根据版本号判断是否需要调用system common api
getSystemCommon(); getSystemCommon();
} }
@@ -99,9 +115,15 @@ export default function usePreload() {
useEffect(() => { useEffect(() => {
setStreamingReady(canStreaming); setStreamingReady(canStreaming);
}, [canStreaming]); }, [canStreaming]);
const enableContact = serverVersion ? compareVersion(serverVersion, '0.3.7') >= 0 : false; const enableContact = serverVersion ? compareVersion(serverVersion, "0.3.7") >= 0 : false;
return { return {
loading: usersLoading || serverLoading || favoritesLoading || !rehydrated || loadingLicense || loadingServerVersion, loading:
usersLoading ||
serverLoading ||
favoritesLoading ||
!rehydrated ||
loadingLicense ||
loadingServerVersion,
error: usersError && serverError && favoritesError, error: usersError && serverError && favoritesError,
success: usersSuccess && serverSuccess && favoritesSuccess && serverVersionSuccess, success: usersSuccess && serverSuccess && favoritesSuccess && serverVersionSuccess,
data: { data: {
+7 -4
View File
@@ -1,11 +1,12 @@
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { removeReplyingMessage, addReplyingMessage, MessagePayload } from "@/app/slices/message";
import { useSendChannelMsgMutation } from "@/app/services/channel"; import { useSendChannelMsgMutation } from "@/app/services/channel";
import { useSendMsgMutation } from "@/app/services/user";
import { useReplyMessageMutation } from "@/app/services/message"; import { useReplyMessageMutation } from "@/app/services/message";
import { useSendMsgMutation } from "@/app/services/user";
import { addReplyingMessage, MessagePayload, removeReplyingMessage } from "@/app/slices/message";
import { useAppDispatch, useAppSelector } from "@/app/store"; import { useAppDispatch, useAppSelector } from "@/app/store";
import { ContentTypeKey } from "@/types/message";
import { ChatContext } from "@/types/common"; import { ChatContext } from "@/types/common";
import { ContentTypeKey } from "@/types/message";
interface Props { interface Props {
context: ChatContext; context: ChatContext;
@@ -20,7 +21,9 @@ interface SendMessagesDTO {
channels: number[]; channels: number[];
} }
type SendMessageDTO = { type: ContentTypeKey } & Partial<MessagePayload> & { ignoreLocal?: boolean } type SendMessageDTO = { type: ContentTypeKey } & Partial<MessagePayload> & {
ignoreLocal?: boolean;
};
const useSendMessage = (props?: Props) => { const useSendMessage = (props?: Props) => {
const { context = "dm", from = 0, to = 0 } = props || {}; const { context = "dm", from = 0, to = 0 } = props || {};
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
+15 -9
View File
@@ -1,16 +1,17 @@
import { batch } from "react-redux"; import { batch } from "react-redux";
import { addChannelMsg, removeChannelMsg } from "@/app/slices/message.channel";
import { addMessage, removeMessage, updateMessage } from "@/app/slices/message";
import { toggleReactionMessage } from "@/app/slices/message.reaction";
import { addFileMessage, removeFileMessage } from "@/app/slices/message.file";
import { addUserMsg, removeUserMsg } from "@/app/slices/message.user";
import { updateAfterMid } from "@/app/slices/footprint";
import { ContentTypes } from "@/app/config"; import { ContentTypes } from "@/app/config";
import { ChatEvent } from "@/types/sse"; import { updateAfterMid } from "@/app/slices/footprint";
import { addMessage, removeMessage, updateMessage } from "@/app/slices/message";
import { addChannelMsg, removeChannelMsg } from "@/app/slices/message.channel";
import { addFileMessage, removeFileMessage } from "@/app/slices/message.file";
import { toggleReactionMessage } from "@/app/slices/message.reaction";
import { addUserMsg, removeUserMsg } from "@/app/slices/message.user";
import { AppDispatch } from "@/app/store"; import { AppDispatch } from "@/app/store";
import { ChatEvent } from "@/types/sse";
type CurrentState = { type CurrentState = {
afterMid: number, afterMid: number;
ready: boolean; ready: boolean;
loginUid: number; loginUid: number;
readUsers: { readUsers: {
@@ -20,7 +21,12 @@ type CurrentState = {
[key: number]: number; [key: number]: number;
}; };
}; };
const handler = (data: ChatEvent, dispatch: AppDispatch, currState: CurrentState, fromHistory = false) => { const handler = (
data: ChatEvent,
dispatch: AppDispatch,
currState: CurrentState,
fromHistory = false
) => {
const { const {
mid, mid,
from_uid, from_uid,
+26 -25
View File
@@ -1,32 +1,33 @@
import { useState, useEffect } from 'react'; import { useEffect, useState } from "react";
import { BroadcastChannel } from 'broadcast-channel'; import { BroadcastChannel } from "broadcast-channel";
// import useStreaming from './useStreaming'; // import useStreaming from './useStreaming';
type ChannelData = { type ChannelData = {
type: "NEW_TAB", type: "NEW_TAB";
message: string message: string;
} };
const useTabBroadcast = () => { const useTabBroadcast = () => {
const [tabActive, setTabActive] = useState(true); const [tabActive, setTabActive] = useState(true);
useEffect(() => { useEffect(() => {
const channel = new BroadcastChannel('VOCECHAT_CHANNEL'); const channel = new BroadcastChannel("VOCECHAT_CHANNEL");
channel.postMessage({ type: "NEW_TAB", message: "new tab opened" } as ChannelData); channel.postMessage({ type: "NEW_TAB", message: "new tab opened" } as ChannelData);
const handler = (data: ChannelData) => { const handler = (data: ChannelData) => {
console.log("channel data", data); console.log("channel data", data);
if (data.type == "NEW_TAB") { if (data.type == "NEW_TAB") {
setTabActive(false); setTabActive(false);
} }
};
channel.addEventListener('message', handler);
return () => {
channel.removeEventListener("message", handler);
channel.close();
};
}, []);
return {
tabActive
}; };
channel.addEventListener("message", handler);
return () => {
channel.removeEventListener("message", handler);
channel.close();
};
}, []);
return {
tabActive
};
}; };
export default useTabBroadcast; export default useTabBroadcast;
+6 -4
View File
@@ -1,12 +1,13 @@
import { useState, useRef } from "react"; import { useRef, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { updateUploadFiles } from "@/app/slices/ui";
import BASE_URL, { FILE_SLICE_SIZE } from "@/app/config"; import BASE_URL, { FILE_SLICE_SIZE } from "@/app/config";
import { usePrepareUploadFileMutation, useUploadFileMutation } from "@/app/services/message"; import { usePrepareUploadFileMutation, useUploadFileMutation } from "@/app/services/message";
import { updateUploadFiles } from "@/app/slices/ui";
import { useAppDispatch, useAppSelector } from "@/app/store"; import { useAppDispatch, useAppSelector } from "@/app/store";
import { Message } from "@/types/channel"; import { Message } from "@/types/channel";
import { UploadFileResponse } from "@/types/message";
import { ChatContext } from "@/types/common"; import { ChatContext } from "@/types/common";
import { UploadFileResponse } from "@/types/message";
interface IProps { interface IProps {
context: ChatContext; context: ChatContext;
@@ -25,7 +26,8 @@ const useUploadFile = (props?: IProps) => {
const canceledRef = useRef(false); const canceledRef = useRef(false);
const sliceUploadedCountRef = useRef(0); const sliceUploadedCountRef = useRef(0);
const totalSliceCountRef = useRef(1); const totalSliceCountRef = useRef(1);
const [prepareUploadFile, { isLoading: isPreparing, isError: prepareFileError }] = usePrepareUploadFileMutation(); const [prepareUploadFile, { isLoading: isPreparing, isError: prepareFileError }] =
usePrepareUploadFileMutation();
const [uploadFileFn, { isLoading: isUploading, isError: uploadFileError }] = const [uploadFileFn, { isLoading: isUploading, isError: uploadFileError }] =
useUploadFileMutation(); useUploadFileMutation();
+15 -9
View File
@@ -1,24 +1,30 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
// import { ContentTypes } from "@/app/config";
import { useNavigate, useMatch } from "react-router-dom";
import { hideAll } from "tippy.js";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
// import { ContentTypes } from "@/app/config";
import { useMatch, useNavigate } from "react-router-dom";
import { hideAll } from "tippy.js";
import { useRemoveMembersMutation } from "@/app/services/channel"; import { useRemoveMembersMutation } from "@/app/services/channel";
import { useLazyDeleteUserQuery, useUpdateContactStatusMutation } from "@/app/services/user"; import { useLazyDeleteUserQuery, useUpdateContactStatusMutation } from "@/app/services/user";
import useCopy from "./useCopy"; import { updateDMVisibleAside } from "@/app/slices/footprint";
import { updateCallInfo } from "@/app/slices/voice";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import { useVoice } from "@/components/Voice"; import { useVoice } from "@/components/Voice";
import { updateCallInfo } from "@/app/slices/voice"; import useCopy from "./useCopy";
import { updateDMVisibleAside } from "@/app/slices/footprint";
import { useDispatch } from "react-redux";
interface IProps { interface IProps {
uid?: number; uid?: number;
cid?: number; cid?: number;
} }
const useUserOperation = ({ uid, cid }: IProps) => { const useUserOperation = ({ uid, cid }: IProps) => {
const { joinVoice, joined, joining = false, joinedAtThisContext } = useVoice({ id: uid ?? 0, context: "dm" }); const {
joinVoice,
joined,
joining = false,
joinedAtThisContext
} = useVoice({ id: uid ?? 0, context: "dm" });
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t: ct } = useTranslation(); const { t: ct } = useTranslation();
const [passedUid, setPassedUid] = useState<number | undefined>(undefined); const [passedUid, setPassedUid] = useState<number | undefined>(undefined);
+12 -10
View File
@@ -1,18 +1,20 @@
import i18n from "i18next";
import dayjs from "dayjs";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import Backend, { HttpBackendOptions } from "i18next-http-backend"; import dayjs from "dayjs";
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import common from "../public/locales/en/common.json"; import Backend, { HttpBackendOptions } from "i18next-http-backend";
import pkg from "../package.json";
import auth from "../public/locales/en/auth.json"; import auth from "../public/locales/en/auth.json";
import member from "../public/locales/en/member.json";
import chat from "../public/locales/en/chat.json"; import chat from "../public/locales/en/chat.json";
import common from "../public/locales/en/common.json";
import fav from "../public/locales/en/fav.json"; import fav from "../public/locales/en/fav.json";
import widget from "../public/locales/en/widget.json";
import welcome from "../public/locales/en/welcome.json";
import setting from "../public/locales/en/setting.json";
import file from "../public/locales/en/file.json"; import file from "../public/locales/en/file.json";
import pkg from '../package.json'; import member from "../public/locales/en/member.json";
import setting from "../public/locales/en/setting.json";
import welcome from "../public/locales/en/welcome.json";
import widget from "../public/locales/en/widget.json";
// don't want to use this? // don't want to use this?
// have a look at the Quick start guide // have a look at the Quick start guide
// for passing in lng and translations on init // for passing in lng and translations on init
@@ -64,7 +66,7 @@ i18n
returnNull: false, returnNull: false,
// for backend middleware // for backend middleware
backend: { backend: {
queryStringParams: { v: pkg.version }, queryStringParams: { v: pkg.version }
} }
}); });
+9 -8
View File
@@ -1,9 +1,10 @@
import { resources, defaultNS } from "./i18n"; import { defaultNS, resources } from "./i18n";
declare module "i18next" { declare module "i18next" {
interface CustomTypeOptions { interface CustomTypeOptions {
allowObjectInHTMLChildren: true, allowObjectInHTMLChildren: true;
returnNull: false; returnNull: false;
defaultNS: typeof defaultNS; defaultNS: typeof defaultNS;
resources: typeof resources["en"]; resources: (typeof resources)["en"];
} }
} }
+3 -1
View File
@@ -1,10 +1,12 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import "dayjs/locale/ja"; import "dayjs/locale/ja";
import duration from "dayjs/plugin/duration"; import duration from "dayjs/plugin/duration";
import relativeTime from "dayjs/plugin/relativeTime";
import isToday from "dayjs/plugin/isToday"; import isToday from "dayjs/plugin/isToday";
import isYesterday from "dayjs/plugin/isYesterday"; import isYesterday from "dayjs/plugin/isYesterday";
import relativeTime from "dayjs/plugin/relativeTime";
// import i18n from '../i18n'; // import i18n from '../i18n';
dayjs.extend(duration); dayjs.extend(duration);
+1 -1
View File
@@ -66,7 +66,7 @@ declare module "*.module.css" {
const classes: { readonly [key: string]: string }; const classes: { readonly [key: string]: string };
export default classes; export default classes;
} }
declare module '@emoji-mart/react'; declare module "@emoji-mart/react";
interface Window { interface Window {
ethereum: any; ethereum: any;
+2 -2
View File
@@ -1,5 +1,6 @@
// import { useState } from "react"; // import { useState } from "react";
import { t } from 'i18next'; import { t } from "i18next";
// `name` for in-code usage, `label` for display // `name` for in-code usage, `label` for display
export interface Step { export interface Step {
name: string; name: string;
@@ -36,5 +37,4 @@ const steps: Step[] = [
} }
]; ];
export default steps; export default steps;
+6 -2
View File
@@ -9,7 +9,7 @@
import { clientsClaim } from "workbox-core"; import { clientsClaim } from "workbox-core";
import { ExpirationPlugin } from "workbox-expiration"; import { ExpirationPlugin } from "workbox-expiration";
import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching"; import { createHandlerBoundToURL, precacheAndRoute } from "workbox-precaching";
import { registerRoute } from "workbox-routing"; import { registerRoute } from "workbox-routing";
import { StaleWhileRevalidate } from "workbox-strategies"; import { StaleWhileRevalidate } from "workbox-strategies";
@@ -34,7 +34,11 @@ registerRoute(
} // If this is a URL that starts with /_, skip. } // If this is a URL that starts with /_, skip.
const urlPath = url.pathname; const urlPath = url.pathname;
// 忽略api开头的path 本地语言文件 挂件地址 // 忽略api开头的path 本地语言文件 挂件地址
if (urlPath.startsWith("/api") || urlPath.startsWith("/locales/") || urlPath.startsWith("/widget")) { if (
urlPath.startsWith("/api") ||
urlPath.startsWith("/locales/") ||
urlPath.startsWith("/widget")
) {
return false; return false;
} }
if (urlPath.startsWith("/_")) { if (urlPath.startsWith("/_")) {
+6 -6
View File
@@ -17,10 +17,10 @@ interface Config {
const isLocalhost = Boolean( const isLocalhost = Boolean(
window.location.hostname === "localhost" || window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address. // [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" || window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4. // 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
); );
export function register(config: Config) { export function register(config: Config) {
@@ -46,7 +46,7 @@ export function register(config: Config) {
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
"This web app is being served cache-first by a service " + "This web app is being served cache-first by a service " +
"worker. To learn more, visit https://cra.link/PWA" "worker. To learn more, visit https://cra.link/PWA"
); );
}); });
} else { } else {
@@ -75,7 +75,7 @@ function registerValidSW(swUrl: string, config: Config) {
// content until all client tabs are closed. // content until all client tabs are closed.
console.log( console.log(
"New content is available and will be used when all " + "New content is available and will be used when all " +
"tabs for this page are closed. See https://cra.link/PWA." "tabs for this page are closed. See https://cra.link/PWA."
); );
// Execute callback // Execute callback
+1
View File
@@ -1,4 +1,5 @@
import { User } from "./user"; import { User } from "./user";
export interface AuthToken { export interface AuthToken {
// common // common
server_id: string; server_id: string;
+4 -3
View File
@@ -1,8 +1,9 @@
//call `group` in backend //call `group` in backend
import { ContentType } from "./message"; import { ContentType } from "./message";
export interface ChannelMember { }
export interface Message { } export interface ChannelMember {}
export interface Message {}
export interface PinnedMessage { export interface PinnedMessage {
mid: number; mid: number;
@@ -13,7 +14,7 @@ export interface PinnedMessage {
properties: { properties: {
local_id?: number; local_id?: number;
content_type?: string; content_type?: string;
size?: number size?: number;
}; };
} }
+8 -8
View File
@@ -7,11 +7,11 @@ export type AuthType = "register" | "login";
export type PriceType = "subscription" | "payment" | "booking"; export type PriceType = "subscription" | "payment" | "booking";
export type PriceSubscriptionDuration = "month" | "quarter" | "year"; export type PriceSubscriptionDuration = "month" | "quarter" | "year";
export type Price = { export type Price = {
title?: string, title?: string;
price?: string, price?: string;
limit?: number, limit?: number;
pid?: string, pid?: string;
desc?: string, desc?: string;
type: PriceType, type: PriceType;
sub_dur?: PriceSubscriptionDuration sub_dur?: PriceSubscriptionDuration;
} };
+7 -2
View File
@@ -1,6 +1,11 @@
import { ChatEvent } from "./sse"; import { ChatEvent } from "./sse";
export type ContentType = "text/plain" | "text/markdown" | "vocechat/file" | "vocechat/audio" | "vocechat/archive"; export type ContentType =
| "text/plain"
| "text/markdown"
| "vocechat/file"
| "vocechat/audio"
| "vocechat/archive";
export type ContentTypeKey = "text" | "markdown" | "file" | "audio" | "archive"; export type ContentTypeKey = "text" | "markdown" | "file" | "audio" | "archive";
export interface MuteDTO { export interface MuteDTO {
@@ -24,4 +29,4 @@ export interface UploadFileResponse {
height: number; height: number;
}; };
} }
export interface ChatMessage extends Omit<ChatEvent, "type"> { } export interface ChatMessage extends Omit<ChatEvent, "type"> {}
+1 -3
View File
@@ -56,9 +56,7 @@ export interface OG {
secure_url?: string; secure_url?: string;
width: number; width: number;
height: number; height: number;
}[ }[];
];
favicon_url?: string; favicon_url?: string;
description?: string; description?: string;
locale?: string; locale?: string;
+21 -22
View File
@@ -6,14 +6,13 @@ export interface Server extends SystemCommon {
name: string; name: string;
description: string; description: string;
} }
export type ChatLayout = "SelfRight" | "Left" export type ChatLayout = "SelfRight" | "Left";
export interface SystemCommon { export interface SystemCommon {
show_user_online_status?: boolean, show_user_online_status?: boolean;
webclient_auto_update?: boolean, webclient_auto_update?: boolean;
contact_verification_enable?: boolean, contact_verification_enable?: boolean;
chat_layout_mode?: ChatLayout, chat_layout_mode?: ChatLayout;
max_file_expiry_mode?: "Off" | "Day1" | "Day7" | "Day30" | "Day90" | "Day180" max_file_expiry_mode?: "Off" | "Day1" | "Day7" | "Day30" | "Day90" | "Day180";
} }
export interface GithubAuthConfig { export interface GithubAuthConfig {
client_id?: string; client_id?: string;
@@ -37,26 +36,26 @@ export interface AgoraConfig {
customer_secret: string; customer_secret: string;
} }
export interface AgoraVoicingListResponse { export interface AgoraVoicingListResponse {
success: boolean, success: boolean;
data: { data: {
channels: { channel_name: string, user_count: number }[], channels: { channel_name: string; user_count: number }[];
total_size: number total_size: number;
} };
} }
export interface AgoraChannelUsersResponse { export interface AgoraChannelUsersResponse {
success: boolean, success: boolean;
data: { data: {
channel_exist: boolean, channel_exist: boolean;
mode?: number, mode?: number;
total?: number, total?: number;
users?: number[] users?: number[];
} };
} }
export interface AgoraTokenResponse { export interface AgoraTokenResponse {
agora_token: string, agora_token: string;
uid: number, uid: number;
channel_name: string, channel_name: string;
expired_in: number, expired_in: number;
app_id: string; app_id: string;
} }
export interface SMTPConfig { export interface SMTPConfig {
@@ -107,7 +106,7 @@ export interface LicenseMetadata {
domain: string | string[]; domain: string | string[];
} }
export interface RenewLicense { export interface RenewLicense {
type: PriceType, type: PriceType;
priceId: string; priceId: string;
metadata: LicenseMetadata; metadata: LicenseMetadata;
cancel_url: string; cancel_url: string;
+17 -15
View File
@@ -1,6 +1,6 @@
import { ContactInfo, User } from "./user";
import { Channel } from "./channel"; import { Channel } from "./channel";
import { ContentType } from "./message"; import { ContentType } from "./message";
import { ContactInfo, User } from "./user";
export interface ReadyEvent { export interface ReadyEvent {
type: "ready"; type: "ready";
@@ -47,11 +47,13 @@ export interface MuteChannel {
expired_at: number; expired_at: number;
} }
export type AutoDeleteMsgForUser = { export type AutoDeleteMsgForUser = {
uid: number; expires_in: number | null uid: number;
} expires_in: number | null;
};
export type AutoDeleteMsgForGroup = { export type AutoDeleteMsgForGroup = {
gid: number; expires_in: number | null gid: number;
} expires_in: number | null;
};
export type UserSettingsEvent = { export type UserSettingsEvent = {
type: "user_settings"; type: "user_settings";
mute_users?: MuteUser[]; mute_users?: MuteUser[];
@@ -60,21 +62,21 @@ export type UserSettingsEvent = {
read_index_groups?: { gid: number; mid: number }[]; read_index_groups?: { gid: number; mid: number }[];
burn_after_reading_users?: AutoDeleteMsgForUser[]; burn_after_reading_users?: AutoDeleteMsgForUser[];
burn_after_reading_groups?: AutoDeleteMsgForGroup[]; burn_after_reading_groups?: AutoDeleteMsgForGroup[];
} };
export type AutoDeleteSettingForUsers = { export type AutoDeleteSettingForUsers = {
burn_after_reading_users: AutoDeleteMsgForUser[] burn_after_reading_users: AutoDeleteMsgForUser[];
} };
export type AutoDeleteSettingForChannels = { export type AutoDeleteSettingForChannels = {
burn_after_reading_groups: AutoDeleteMsgForGroup[] burn_after_reading_groups: AutoDeleteMsgForGroup[];
} };
export type AutoDeleteMessageSettingDTO = AutoDeleteSettingForUsers | AutoDeleteSettingForChannels export type AutoDeleteMessageSettingDTO = AutoDeleteSettingForUsers | AutoDeleteSettingForChannels;
export type UserSettingsChangedEvent = { export type UserSettingsChangedEvent = {
type: "user_settings_changed"; type: "user_settings_changed";
from_device?: string; from_device?: string;
add_mute_users?: MuteUser[]; add_mute_users?: MuteUser[];
remove_mute_users?: number[]; remove_mute_users?: number[];
add_mute_groups?: MuteChannel[]; add_mute_groups?: MuteChannel[];
add_contacts?: { target_uid: number, info: ContactInfo }[]; add_contacts?: { target_uid: number; info: ContactInfo }[];
remove_contacts?: number[]; remove_contacts?: number[];
remove_mute_groups?: number[]; remove_mute_groups?: number[];
add_pin_chats?: PinChat[]; add_pin_chats?: PinChat[];
@@ -83,7 +85,7 @@ export type UserSettingsChangedEvent = {
read_index_groups?: { gid: number; mid: number }[]; read_index_groups?: { gid: number; mid: number }[];
burn_after_reading_users?: AutoDeleteMsgForUser[]; burn_after_reading_users?: AutoDeleteMsgForUser[];
burn_after_reading_groups?: AutoDeleteMsgForGroup[]; burn_after_reading_groups?: AutoDeleteMsgForGroup[];
} };
export interface RelatedGroupsEvent { export interface RelatedGroupsEvent {
type: "related_groups"; type: "related_groups";
@@ -96,7 +98,7 @@ export type PinChatTargetChannel = {
gid: number; gid: number;
}; };
export type PinChatTarget = PinChatTargetUser | PinChatTargetChannel; export type PinChatTarget = PinChatTargetUser | PinChatTargetChannel;
export type PinChat = { target: PinChatTarget[], updated_at: number } export type PinChat = { target: PinChatTarget[]; updated_at: number };
export interface PinnedChatsEvent { export interface PinnedChatsEvent {
type: "pinned_chats"; type: "pinned_chats";
chats: PinChat[]; chats: PinChat[];
@@ -214,7 +216,7 @@ interface GroupClearEvent {
interface UserCallEvent { interface UserCallEvent {
type: "user_calling"; type: "user_calling";
target: number; target: number;
uid: number uid: number;
} }
export type ServerEvent = export type ServerEvent =
+23 -16
View File
@@ -3,8 +3,8 @@ import { AuthToken } from "./auth";
export type Gender = 0 | 1; export type Gender = 0 | 1;
export interface AutoDeleteMsgDTO { export interface AutoDeleteMsgDTO {
users?: { uid: number, expires_in: number }[], users?: { uid: number; expires_in: number }[];
groups?: { gid: number, expires_in: number }[], groups?: { gid: number; expires_in: number }[];
} }
export interface User { export interface User {
uid: number; uid: number;
@@ -25,14 +25,14 @@ export type ContactInfo = {
status: ContactStatus; status: ContactStatus;
created_at: number; created_at: number;
updated_at: number; updated_at: number;
} };
export interface ContactResponse { export interface ContactResponse {
target_uid: number; target_uid: number;
target_info: User; target_info: User;
contact_info: ContactInfo contact_info: ContactInfo;
} }
export interface Contact extends User { export interface Contact extends User {
status: ContactStatus status: ContactStatus;
} }
export type UserStatus = "normal" | "frozen"; export type UserStatus = "normal" | "frozen";
export type UserDevice = { export type UserDevice = {
@@ -41,11 +41,11 @@ export type UserDevice = {
is_online: boolean; is_online: boolean;
}; };
export type BotAPIKey = { export type BotAPIKey = {
id: number, id: number;
name: string, name: string;
key: string, key: string;
created_at: number, created_at: number;
last_used: number last_used: number;
}; };
export interface UserForAdmin extends User { export interface UserForAdmin extends User {
@@ -58,16 +58,23 @@ export interface UserForAdmin extends User {
export interface UserForAdminDTO extends Partial<UserForAdmin> { export interface UserForAdminDTO extends Partial<UserForAdmin> {
id?: number; id?: number;
} }
export interface UserDTO extends Partial<Pick<User, "name" | "gender" | "language" | "email" | "webhook_url">> { export interface UserDTO
password?: string extends Partial<Pick<User, "name" | "gender" | "language" | "email" | "webhook_url">> {
password?: string;
} }
export interface UserCreateDTO extends Pick<User, "name" | "gender" | "language" | "email" | "webhook_url" | "is_bot" | "is_admin"> { export interface UserCreateDTO
extends Pick<
User,
"name" | "gender" | "language" | "email" | "webhook_url" | "is_bot" | "is_admin"
> {
password: string; password: string;
} }
export interface UserRegDTO extends Partial<Pick<User, "name" | "gender" | "language" | "email">>, Partial<Pick<UserDevice, "device" | "device_token">> { export interface UserRegDTO
extends Partial<Pick<User, "name" | "gender" | "language" | "email">>,
Partial<Pick<UserDevice, "device" | "device_token">> {
password?: string; password?: string;
magic_token?: string magic_token?: string;
} }
export interface UserRegResponse extends AuthToken { export interface UserRegResponse extends AuthToken {
user: User user: User;
} }
+12 -10
View File
@@ -1,13 +1,15 @@
import { useEffect } from "react"; import { useEffect } from "react";
import initCache, { useRehydrate } from "../app/cache";
export default function useCache() {
const { rehydrate, rehydrated } = useRehydrate();
useEffect(() => {
initCache();
rehydrate();
}, []);
return { import initCache, { useRehydrate } from "../app/cache";
rehydrated
}; export default function useCache() {
const { rehydrate, rehydrated } = useRehydrate();
useEffect(() => {
initCache();
rehydrate();
}, []);
return {
rehydrated
};
} }
+24 -23
View File
@@ -1,32 +1,33 @@
import { useEffect } from "react"; import { useEffect } from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useAppSelector } from "../app/store";
import useStreaming from "@/hooks/useStreaming"; import useStreaming from "@/hooks/useStreaming";
import { useAppSelector } from "../app/store";
export default function useSSE() { export default function useSSE() {
const { const {
loginUid, loginUid,
token, token,
expireTime = +new Date(), expireTime = +new Date()
} = useAppSelector((store) => { } = useAppSelector((store) => {
return { return {
loginUid: store.authData.user?.uid, loginUid: store.authData.user?.uid,
token: store.authData.token, token: store.authData.token,
expireTime: store.authData.expireTime expireTime: store.authData.expireTime
}; };
}); });
const { setStreamingReady } = useStreaming(); const { setStreamingReady } = useStreaming();
const tokenAlmostExpire = dayjs().isAfter(new Date(expireTime - 20 * 1000)); const tokenAlmostExpire = dayjs().isAfter(new Date(expireTime - 20 * 1000));
const canStreaming = !!loginUid && !!token && !tokenAlmostExpire; const canStreaming = !!loginUid && !!token && !tokenAlmostExpire;
// console.log("ttt", loginUid, rehydrated, token); // console.log("ttt", loginUid, rehydrated, token);
useEffect(() => { useEffect(() => {
setStreamingReady(canStreaming); setStreamingReady(canStreaming);
return () => { return () => {
setStreamingReady(false); setStreamingReady(false);
}; };
}, [canStreaming]); }, [canStreaming]);
return null; return null;
} }