refactor: more TS code

This commit is contained in:
Tristan Yang
2022-07-08 19:13:49 +08:00
parent 678b8515ca
commit 9f1b115c73
55 changed files with 421 additions and 338 deletions
+16 -1
View File
@@ -26,7 +26,22 @@
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"cSpell.words": ["btns", "navs", "oidc", "reduxjs", "thirdparty", "tippyjs", "vocechat"], "cSpell.words": [
"appinstalled",
"beforeinstallprompt",
"btns",
"localstorage",
"magiclink",
"navs",
"oidc",
"reduxjs",
"Swiper",
"thirdparty",
"tippyjs",
"toastui",
"uiball",
"vocechat"
],
"reactSnippets.settings.prettierEnabled": true, "reactSnippets.settings.prettierEnabled": true,
"reactSnippets.settings.importReactOnTop": false "reactSnippets.settings.importReactOnTop": false
} }
+34 -28
View File
@@ -3,7 +3,13 @@ import { nanoid } from "@reduxjs/toolkit";
import baseQuery from "./base.query"; import baseQuery from "./base.query";
import { setAuthData, updateToken, resetAuthData, updateInitialized } from "../slices/auth.data"; import { setAuthData, updateToken, resetAuthData, updateInitialized } from "../slices/auth.data";
import BASE_URL, { KEY_DEVICE_KEY, KEY_LOCAL_MAGIC_TOKEN } from "../config"; import BASE_URL, { KEY_DEVICE_KEY, KEY_LOCAL_MAGIC_TOKEN } from "../config";
import { AuthData, LoginCredential } from "../../types/auth"; import {
AuthData,
CredentialResponse,
LoginCredential,
RenewTokenDTO,
RenewTokenResponse
} from "../../types/auth";
const getDeviceId = () => { const getDeviceId = () => {
let d = localStorage.getItem(KEY_DEVICE_KEY); let d = localStorage.getItem(KEY_DEVICE_KEY);
@@ -59,14 +65,11 @@ export const authApi = createApi({
}) })
}), }),
// 更新token // 更新token
renew: builder.mutation({ renew: builder.mutation<RenewTokenResponse, RenewTokenDTO>({
query: ({ token, refreshToken }) => ({ query: (data) => ({
url: "/token/renew", url: "/token/renew",
method: "POST", method: "POST",
body: { body: data
token,
refresh_token: refreshToken
}
}), }),
async onQueryStarted(params, { dispatch, queryFulfilled }) { async onQueryStarted(params, { dispatch, queryFulfilled }) {
try { try {
@@ -79,7 +82,7 @@ export const authApi = createApi({
} }
}), }),
// 更新 device token // 更新 device token
updateDeviceToken: builder.mutation({ updateDeviceToken: builder.mutation<void, { device_token: string }>({
query: (device_token) => ({ query: (device_token) => ({
url: "/token/device_token", url: "/token/device_token",
method: "PUT", method: "PUT",
@@ -89,66 +92,69 @@ export const authApi = createApi({
}) })
}), }),
// 获取openid // 获取openid
getOpenid: builder.mutation({ getOpenid: builder.mutation<{ url: string }, { issuer: string; redirect_uri: string }>({
query: ({ issuer, redirect_uri }) => ({ query: (data) => ({
url: "/token/openid/authorize", url: "/token/openid/authorize",
method: "POST", method: "POST",
body: { body: data
issuer,
redirect_uri
}
}) })
}), }),
checkMagicTokenValid: builder.mutation({ checkMagicTokenValid: builder.mutation<boolean, string>({
query: (token) => ({ query: (token) => ({
url: "user/check_magic_token", url: "user/check_magic_token",
method: "POST", method: "POST",
body: { magic_token: token } body: { magic_token: token }
}) })
}), }),
updatePassword: builder.mutation({ updatePassword: builder.mutation<void, { old_password: string; new_password: string }>({
query: ({ old_password, new_password }) => ({ query: (data) => ({
url: "user/change_password", url: "user/change_password",
method: "POST", method: "POST",
body: { old_password, new_password } body: data
}) })
}), }),
sendLoginMagicLink: builder.mutation({ sendLoginMagicLink: builder.mutation<string, string>({
query: (email) => ({ query: (email) => ({
headers: { headers: {
// "content-type": "text/plain",
accept: "text/plain" accept: "text/plain"
}, },
url: `user/send_login_magic_link?email=${encodeURIComponent(email)}`, url: `user/send_login_magic_link?email=${encodeURIComponent(email)}`,
method: "POST", method: "POST",
responseHandler: (response: Response) => response.text() responseHandler: (response: Response) => response.text()
// body: { email }
}) })
}), }),
sendRegMagicLink: builder.mutation({ sendRegMagicLink: builder.mutation<
{
new_magic_token: "string";
mail_is_sent: boolean;
},
{
magic_token: "string";
email: "string";
password: "string";
}
>({
query: (data) => ({ query: (data) => ({
url: `user/send_reg_magic_link`, url: `user/send_reg_magic_link`,
method: "POST", method: "POST",
body: data body: data
}) })
}), }),
getMetamaskNonce: builder.query({ getMetamaskNonce: builder.query<string, string>({
query: (address) => ({ query: (address) => ({
url: `/token/metamask/nonce?public_address=${address}` url: `/token/metamask/nonce?public_address=${address}`
}) })
}), }),
checkEmail: builder.query({ checkEmail: builder.query<boolean, string>({
query: (email) => ({ query: (email) => ({
url: `/user/check_email?email=${encodeURIComponent(email)}` url: `/user/check_email?email=${encodeURIComponent(email)}`
}) })
}), }),
// todo: check return type getCredentials: builder.query<CredentialResponse, void>({
getCredentials: builder.query<any, void>({
query: () => ({ url: "/token/credentials" }) query: () => ({ url: "/token/credentials" })
}), }),
// todo: check return type logout: builder.query<void, void>({
logout: builder.query<any, void>({
query: () => ({ url: "token/logout" }), query: () => ({ url: "token/logout" }),
async onQueryStarted(params, { dispatch, queryFulfilled }) { async onQueryStarted(params, { dispatch, queryFulfilled }) {
try { try {
+9 -8
View File
@@ -9,9 +9,10 @@ import { removeChannelSession } from "../slices/message.channel";
import { removeReactionMessage } from "../slices/message.reaction"; import { removeReactionMessage } from "../slices/message.reaction";
import { onMessageSendStarted } from "./handlers"; import { onMessageSendStarted } from "./handlers";
import handleChatMessage from "../../common/hook/useStreaming/chat.handler"; import handleChatMessage from "../../common/hook/useStreaming/chat.handler";
import { Channel, CreateChannelDTO } from "../../types/channel"; import { Channel, ChannelDTO, CreateChannelDTO } from "../../types/channel";
import { RootState } from "../store"; import { RootState } from "../store";
import { ContentTypeKey } from "../../types/message"; import { ContentTypeKey, ChatMessage } from "../../types/message";
export const channelApi = createApi({ export const channelApi = createApi({
reducerPath: "channelApi", reducerPath: "channelApi",
baseQuery, baseQuery,
@@ -41,7 +42,7 @@ export const channelApi = createApi({
body: data body: data
}) })
}), }),
updateChannel: builder.mutation({ updateChannel: builder.mutation<void, ChannelDTO>({
query: ({ id, ...data }) => ({ query: ({ id, ...data }) => ({
url: `group/${id}`, url: `group/${id}`,
method: "PUT", method: "PUT",
@@ -49,7 +50,7 @@ export const channelApi = createApi({
}), }),
async onQueryStarted({ id, name, description }, { dispatch, queryFulfilled }) { async onQueryStarted({ id, name, description }, { dispatch, queryFulfilled }) {
// id: who send to ,from_uid: who sent // id: who send to ,from_uid: who sent
const patchResult = dispatch(updateChannel({ id, name, description })); const patchResult = dispatch(updateChannel({ gid: id, name, description }));
try { try {
await queryFulfilled; await queryFulfilled;
} catch { } catch {
@@ -58,7 +59,7 @@ export const channelApi = createApi({
} }
} }
}), }),
getHistoryMessages: builder.query({ getHistoryMessages: builder.query<ChatMessage[], { id: number; mid?: number; limit: number }>({
query: ({ id, mid = null, limit = 100 }) => ({ query: ({ id, mid = null, limit = 100 }) => ({
url: mid url: mid
? `/group/${id}/history?before=${mid}&limit=${limit}` ? `/group/${id}/history?before=${mid}&limit=${limit}`
@@ -145,21 +146,21 @@ export const channelApi = createApi({
await onMessageSendStarted.call(this, param1, param2, "channel"); await onMessageSendStarted.call(this, param1, param2, "channel");
} }
}), }),
addMembers: builder.mutation({ addMembers: builder.mutation<void, { id: number; members: number[] }>({
query: ({ id, members }) => ({ query: ({ id, members }) => ({
url: `group/${id}/members/add`, url: `group/${id}/members/add`,
method: "POST", method: "POST",
body: members body: members
}) })
}), }),
removeMembers: builder.mutation({ removeMembers: builder.mutation<void, { id: number; members: number[] }>({
query: ({ id, members }) => ({ query: ({ id, members }) => ({
url: `group/${id}/members/remove`, url: `group/${id}/members/remove`,
method: "POST", method: "POST",
body: members body: members
}) })
}), }),
updateIcon: builder.mutation({ updateIcon: builder.mutation<void, { gid: number; image: File }>({
query: ({ gid, image }) => ({ query: ({ gid, image }) => ({
headers: { headers: {
"content-type": "image/png" "content-type": "image/png"
-12
View File
@@ -25,18 +25,6 @@ export const onMessageSendStarted = async (
console.log("handlers data", content, type, properties, ignoreLocal, id); console.log("handlers data", content, type, properties, ignoreLocal, id);
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();
// let imageData = null;
// if (type == "image") {
// if (typeof content == "string" && content.startsWith("data:image")) {
// // base64
// // const resp = await fetch(content);
// // const blob = await resp.blob();
// // imageData = new File([blob], "tmp.png", { type: "image/png" });
// imageData = content;
// } else {
// imageData = URL.createObjectURL(content);
// }
// }
const tmpMsg = { const tmpMsg = {
content: isImage ? content.path : content, content: isImage ? content.path : content,
content_type: ContentTypes[type], content_type: ContentTypes[type],
+15 -19
View File
@@ -4,23 +4,16 @@ import { updateReadChannels, updateReadUsers } from "../slices/footprint";
import { fillFavorites, populateFavorite, addFavorite, deleteFavorite } from "../slices/favorites"; import { fillFavorites, populateFavorite, addFavorite, deleteFavorite } from "../slices/favorites";
import { onMessageSendStarted } from "./handlers"; import { onMessageSendStarted } from "./handlers";
import { normalizeArchiveData } from "../../common/utils"; import { normalizeArchiveData } from "../../common/utils";
// import { updateMessage } from "../slices/message";
import baseQuery from "./base.query"; import baseQuery from "./base.query";
import { OG } from "../../types/common"; import { Archive, FavoriteArchive, OG } from "../../types/resource";
type UploadFileResponse = { import { ContentTypeKey, UploadFileResponse } from "../../types/message";
path: string; import { RootState } from "../store";
size: number;
hash: string;
image_properties: {
width: number;
height: number;
};
};
export const messageApi = createApi({ export const messageApi = createApi({
reducerPath: "messageApi", reducerPath: "messageApi",
baseQuery, baseQuery,
endpoints: (builder) => ({ endpoints: (builder) => ({
editMessage: builder.mutation({ editMessage: builder.mutation<number, { mid: number; content: string; type: ContentTypeKey }>({
query: ({ mid, content, type = "text" }) => ({ query: ({ mid, content, type = "text" }) => ({
headers: { headers: {
"content-type": ContentTypes[type] "content-type": ContentTypes[type]
@@ -50,7 +43,7 @@ export const messageApi = createApi({
body: meta body: meta
}) })
}), }),
createArchive: builder.mutation({ createArchive: builder.mutation<string, number[]>({
query: (mids = []) => ({ query: (mids = []) => ({
url: `/resource/archive`, url: `/resource/archive`,
method: "POST", method: "POST",
@@ -73,7 +66,7 @@ export const messageApi = createApi({
url: `/resource/open_graphic_parse?url=${encodeURIComponent(url)}` url: `/resource/open_graphic_parse?url=${encodeURIComponent(url)}`
}) })
}), }),
getArchiveMessage: builder.query({ getArchiveMessage: builder.query<Archive, string>({
query: (file_path) => ({ query: (file_path) => ({
url: `/resource/archive?file_path=${encodeURIComponent(file_path)}` url: `/resource/archive?file_path=${encodeURIComponent(file_path)}`
}) })
@@ -92,7 +85,7 @@ export const messageApi = createApi({
body: { mid } body: { mid }
}) })
}), }),
favoriteMessage: builder.mutation({ favoriteMessage: builder.mutation<FavoriteArchive, number[]>({
query: (mids) => ({ query: (mids) => ({
url: `/favorite`, url: `/favorite`,
method: "POST", method: "POST",
@@ -123,14 +116,14 @@ export const messageApi = createApi({
} }
} }
}), }),
getFavoriteDetails: builder.query({ getFavoriteDetails: builder.query<Archive, number>({
query: (id) => ({ query: (id) => ({
url: `/favorite/${id}` url: `/favorite/${id}`
}), }),
async onQueryStarted(id, { dispatch, queryFulfilled, getState }) { async onQueryStarted(id, { dispatch, queryFulfilled, getState }) {
try { try {
const { data } = await queryFulfilled; const { data } = await queryFulfilled;
const loginUid = getState().authData.user.uid; const loginUid = (getState() as RootState).authData.user.uid;
const messages = normalizeArchiveData(data, id, loginUid); const messages = normalizeArchiveData(data, id, loginUid);
dispatch(populateFavorite({ id, messages })); dispatch(populateFavorite({ id, messages }));
} catch (err) { } catch (err) {
@@ -138,7 +131,7 @@ export const messageApi = createApi({
} }
} }
}), }),
getFavorites: builder.query({ getFavorites: builder.query<FavoriteArchive[], void>({
query: () => ({ query: () => ({
url: `/favorite` url: `/favorite`
}), }),
@@ -155,7 +148,10 @@ export const messageApi = createApi({
} }
} }
}), }),
replyMessage: builder.mutation({ replyMessage: builder.mutation<
number,
{ reply_mid: number; content: string; type: ContentTypeKey }
>({
query: ({ reply_mid, content, type = "text" }) => ({ query: ({ reply_mid, content, type = "text" }) => ({
headers: { headers: {
"content-type": ContentTypes[type] "content-type": ContentTypes[type]
+2 -3
View File
@@ -3,7 +3,7 @@ import BASE_URL from "../config";
import { updateInfo } from "../slices/server"; import { updateInfo } from "../slices/server";
import baseQuery from "./base.query"; import baseQuery from "./base.query";
import { RootState } from "../store"; import { RootState } from "../store";
import { User } from "../../types/auth"; import { User } from "../../types/user";
import { import {
FirebaseConfig, FirebaseConfig,
GoogleAuthConfig, GoogleAuthConfig,
@@ -51,7 +51,6 @@ export const serverApi = createApi({
getServerVersion: builder.query<string, void>({ getServerVersion: builder.query<string, void>({
query: () => ({ query: () => ({
headers: { headers: {
// "content-type": "text/plain",
accept: "text/plain" accept: "text/plain"
}, },
url: `/admin/system/version`, url: `/admin/system/version`,
@@ -128,7 +127,7 @@ export const serverApi = createApi({
body: data body: data
}) })
}), }),
updateLogo: builder.mutation({ updateLogo: builder.mutation<void, File>({
query: (data) => ({ query: (data) => ({
headers: { headers: {
"content-type": "image/png" "content-type": "image/png"
+10 -6
View File
@@ -9,13 +9,14 @@ import BASE_URL, { ContentTypes } from "../config";
import { onMessageSendStarted } from "./handlers"; import { onMessageSendStarted } from "./handlers";
import handleChatMessage from "../../common/hook/useStreaming/chat.handler"; import handleChatMessage from "../../common/hook/useStreaming/chat.handler";
import { User, UserDTO, UserForAdmin, UserForAdminDTO } from "../../types/user"; import { User, UserDTO, UserForAdmin, UserForAdminDTO } from "../../types/user";
import { ContentType, MuteDTO } from "../../types/message"; import { ChatMessage, ContentTypeKey, MuteDTO } from "../../types/message";
import { RootState } from "../store";
export const userApi = createApi({ export const userApi = createApi({
reducerPath: "userApi", reducerPath: "userApi",
baseQuery, baseQuery,
endpoints: (builder) => ({ endpoints: (builder) => ({
getUsers: builder.query({ getUsers: builder.query<User[], void>({
query: () => ({ url: `user` }), query: () => ({ url: `user` }),
transformResponse: (data: User[]) => { transformResponse: (data: User[]) => {
return data.map((user) => { return data.map((user) => {
@@ -74,7 +75,7 @@ export const userApi = createApi({
} }
} }
}), }),
updateAvatar: builder.mutation({ updateAvatar: builder.mutation<void, File>({
query: (data) => ({ query: (data) => ({
headers: { headers: {
"content-type": "image/png" "content-type": "image/png"
@@ -92,7 +93,10 @@ export const userApi = createApi({
}) })
}), }),
sendMsg: builder.mutation({ sendMsg: builder.mutation<
number,
{ id: number; content: string; type: ContentTypeKey; properties?: object }
>({
query: ({ id, content, type = "text", properties = "" }) => ({ query: ({ id, content, type = "text", properties = "" }) => ({
headers: { headers: {
"content-type": ContentTypes[type], "content-type": ContentTypes[type],
@@ -108,7 +112,7 @@ export const userApi = createApi({
await onMessageSendStarted.call(this, param1, param2, "user"); await onMessageSendStarted.call(this, param1, param2, "user");
} }
}), }),
getHistoryMessages: builder.query({ getHistoryMessages: builder.query<ChatMessage[], { id: number; mid?: number; limit: number }>({
query: ({ id, mid = null, limit = 100 }) => ({ query: ({ id, mid = null, limit = 100 }) => ({
url: mid url: mid
? `/user/${id}/history?before=${mid}&limit=${limit}` ? `/user/${id}/history?before=${mid}&limit=${limit}`
@@ -118,7 +122,7 @@ export const userApi = createApi({
const { data: messages } = await queryFulfilled; const { data: messages } = await queryFulfilled;
if (messages?.length) { if (messages?.length) {
messages.forEach((msg) => { messages.forEach((msg) => {
handleChatMessage(msg, dispatch, getState()); handleChatMessage(msg, dispatch, getState() as RootState);
}); });
} }
} }
+2 -2
View File
@@ -7,7 +7,7 @@ import {
KEY_TOKEN, KEY_TOKEN,
KEY_UID KEY_UID
} from "../config"; } from "../config";
import { AuthData, AuthToken, User } from "../../types/auth"; import { AuthData, AuthToken, RenewTokenResponse, User } from "../../types/auth";
interface State { interface State {
initialized: boolean; initialized: boolean;
@@ -67,7 +67,7 @@ const authDataSlice = createSlice({
updateInitialized(state, action: PayloadAction<boolean>) { updateInitialized(state, action: PayloadAction<boolean>) {
state.initialized = action.payload; state.initialized = action.payload;
}, },
updateToken(state, action: PayloadAction<AuthToken>) { updateToken(state, action: PayloadAction<RenewTokenResponse>) {
const { token, refresh_token, expired_in } = action.payload; const { token, refresh_token, expired_in } = action.payload;
state.token = token; state.token = token;
const et = +new Date() + Number(expired_in) * 1000; const et = +new Date() + Number(expired_in) * 1000;
+1 -1
View File
@@ -53,7 +53,7 @@ const channelsSlice = createSlice({
}, },
updateChannel(state, action: PayloadAction<UpdateChannelDTO>) { updateChannel(state, action: PayloadAction<UpdateChannelDTO>) {
const ignoreInPublic = ["add_member", "remove_member"]; const ignoreInPublic = ["add_member", "remove_member"];
const { gid, operation, members = [], ...rest } = action.payload; const { gid, operation = "", members = [], ...rest } = action.payload;
const currChannel = state.byId[gid]; const currChannel = state.byId[gid];
if (!currChannel || (currChannel.is_public && ignoreInPublic.includes(operation))) return; if (!currChannel || (currChannel.is_public && ignoreInPublic.includes(operation))) return;
switch (operation) { switch (operation) {
+1 -1
View File
@@ -59,7 +59,7 @@ const uiSlice = createSlice({
}, },
updateRememberedNavs( updateRememberedNavs(
state, state,
action: PayloadAction<{ key?: "chat" | "user"; path: string | null } | undefined> action: PayloadAction<{ key?: "chat" | "user"; path?: string | null } | undefined>
) { ) {
const { key = "chat", path = null } = action.payload || {}; const { key = "chat", path = null } = action.payload || {};
state.rememberedNavs[key] = path; state.rememberedNavs[key] = path;
+1 -1
View File
@@ -1,7 +1,7 @@
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 BASE_URL from "../config";
import { User } from "../../types/auth"; import { User } from "../../types/user";
import { UserLog, UserState } from "../../types/sse"; import { UserLog, UserState } from "../../types/sse";
export interface StoredUser extends User { export interface StoredUser extends User {
+1 -1
View File
@@ -68,7 +68,7 @@ const Styled = styled.div`
`; `;
interface Props { interface Props {
type?: "chat"; type?: "chat" | "user";
} }
const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => { const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
-1
View File
@@ -32,7 +32,6 @@ const StyledWrapper = styled.div`
} }
} }
.name { .name {
/* user-select: text; */
display: flex; display: flex;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
+5 -6
View File
@@ -3,18 +3,17 @@ import styled from "styled-components";
import HashIcon from "../../assets/icons/channel.svg"; import HashIcon from "../../assets/icons/channel.svg";
import LockHashIcon from "../../assets/icons/channel.private.svg"; import LockHashIcon from "../../assets/icons/channel.private.svg";
interface Props {
personal?: boolean;
muted?: boolean;
className?: string;
}
const Styled = styled.div` const Styled = styled.div`
display: flex; display: flex;
&.muted path { &.muted path {
fill: #d0d5dd; fill: #d0d5dd;
} }
`; `;
interface Props {
personal?: boolean;
muted?: boolean;
className?: string;
}
const ChannelIcon: FC<Props> = ({ personal = false, muted = false, className = "" }) => { const ChannelIcon: FC<Props> = ({ personal = false, muted = false, className = "" }) => {
return ( return (
+5 -3
View File
@@ -4,6 +4,7 @@ import micIcon from "../../assets/icons/mic.on.svg?url";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import useConfig from "../hook/useConfig"; import useConfig from "../hook/useConfig";
import { useAppSelector } from "../../app/store"; import { useAppSelector } from "../../app/store";
import { FC } from "react";
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
background-color: #f4f4f5; background-color: #f4f4f5;
@@ -60,8 +61,8 @@ const StyledWrapper = styled.div`
} }
} }
`; `;
type Props = {};
export default function CurrentUser() { const CurrentUser: FC<Props> = () => {
const { values: agoraConfig } = useConfig("agora"); const { values: agoraConfig } = useConfig("agora");
const currUser = useAppSelector((store) => { const currUser = useAppSelector((store) => {
return store.authData.user; return store.authData.user;
@@ -86,4 +87,5 @@ export default function CurrentUser() {
)} )}
</StyledWrapper> </StyledWrapper>
); );
} };
export default CurrentUser;
-6
View File
@@ -11,8 +11,6 @@ interface Props {
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25)); filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 12px; border-radius: 12px;
/* height: 358px;
overflow: hidden; */
.emoji-mart { .emoji-mart {
border: none; border: none;
border-radius: 12px; border-radius: 12px;
@@ -43,10 +41,6 @@ const EmojiPicker: FC<Props> = ({ onSelect, ...rest }) => {
perLine={10} perLine={10}
emojiSize={24} emojiSize={24}
emojiTooltip={true} emojiTooltip={true}
// set="twitter"
// data={data}
// set="twitter"
// showPreview={false}
showSkinTones={false} showSkinTones={false}
onSelect={onSelect} onSelect={onSelect}
{...rest} {...rest}
+5 -3
View File
@@ -1,3 +1,4 @@
import { FC } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useGetServerVersionQuery } from "../../app/services/server"; import { useGetServerVersionQuery } from "../../app/services/server";
@@ -6,8 +7,8 @@ const Styled = styled.div`
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
`; `;
type Props = {};
export default function FAQ() { const FAQ: FC<Props> = () => {
const { data: serverVersion } = useGetServerVersionQuery(); const { data: serverVersion } = useGetServerVersionQuery();
return ( return (
<Styled> <Styled>
@@ -16,4 +17,5 @@ export default function FAQ() {
<div className="item">Build Timestamp: {process.env.REACT_APP_BUILD_TIME}</div> <div className="item">Build Timestamp: {process.env.REACT_APP_BUILD_TIME}</div>
</Styled> </Styled>
); );
} };
export default FAQ;
+2 -2
View File
@@ -25,7 +25,7 @@ interface Props {
context: "user" | "channel"; context: "user" | "channel";
to: number; to: number;
created_at: number; created_at: number;
from_uid?: number; from_uid: number;
content: string; content: string;
download: string; download: string;
thumbnail: string; thumbnail: string;
@@ -41,7 +41,7 @@ const FileMessage: FC<Props> = ({
context, context,
to, to,
created_at, created_at,
from_uid = null, from_uid,
content = "", content = "",
download = "", download = "",
thumbnail = "", thumbnail = "",
+8 -3
View File
@@ -1,4 +1,4 @@
import { useState, MouseEvent, ChangeEvent } from "react"; import { useState, MouseEvent, ChangeEvent, FC } from "react";
// import toast from "react-hot-toast"; // import toast from "react-hot-toast";
import Modal from "../Modal"; import Modal from "../Modal";
import Button from "../styled/Button"; import Button from "../styled/Button";
@@ -15,8 +15,12 @@ import useFilteredUsers from "../../hook/useFilteredUsers";
import CloseIcon from "../../../assets/icons/close.circle.svg"; import CloseIcon from "../../../assets/icons/close.circle.svg";
import StyledCheckbox from "../styled/Checkbox"; import StyledCheckbox from "../styled/Checkbox";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
interface IProps {
mids: number[];
closeModal: () => void;
}
export default function ForwardModal({ mids, closeModal }) { const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
const [appendText, setAppendText] = useState(""); const [appendText, setAppendText] = useState("");
const { sendMessages } = useSendMessage(); const { sendMessages } = useSendMessage();
const { forwardMessage, forwarding } = useForwardMessage(); const { forwardMessage, forwarding } = useForwardMessage();
@@ -176,4 +180,5 @@ export default function ForwardModal({ mids, closeModal }) {
</StyledWrapper> </StyledWrapper>
</Modal> </Modal>
); );
} };
export default ForwardModal;
+3 -1
View File
@@ -6,6 +6,7 @@ import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
import Button from "./styled/Button"; import Button from "./styled/Button";
import { useLoginMutation } from "../../app/services/auth"; import { useLoginMutation } from "../../app/services/auth";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
const StyledSocialButton = styled(Button)` const StyledSocialButton = styled(Button)`
width: 100%; width: 100%;
@@ -51,7 +52,8 @@ const GithubLoginButton: FC<Props> = ({ type = "login", client_id }) => {
}, [isSuccess]); }, [isSuccess]);
useEffect(() => { useEffect(() => {
if (error) { if (error) {
switch (error?.status) { // todo: why?
switch ((error as FetchBaseQueryError).status) {
case 410: case 410:
toast.error( toast.error(
"No associated account found, please user admin for an invitation link to join." "No associated account found, please user admin for an invitation link to join."
+2 -2
View File
@@ -66,13 +66,13 @@ const GoogleLoginInner: FC<Props> = ({ type = "login", loaded, loadError }) => {
}, [error]); }, [error]);
return ( return (
<StyledSocialButton disabled={!loaded || isLoading} onClick={null}> <StyledSocialButton disabled={!loaded || isLoading}>
<div className="invisible"> <div className="invisible">
<GoogleLogin <GoogleLogin
onSuccess={(res) => { onSuccess={(res) => {
login({ login({
magic_token, magic_token,
id_token: res.credential, id_token: res.credential || "",
type: "google" type: "google"
}); });
}} }}
+4 -4
View File
@@ -66,10 +66,10 @@ const StyledWrapper = styled.div`
export interface PreviewImageData { export interface PreviewImageData {
originUrl: string; originUrl: string;
thumbnail: string; thumbnail?: string;
downloadLink: string; downloadLink?: string;
name: string; name?: string;
type: string; type?: string;
} }
interface Props { interface Props {
+6 -3
View File
@@ -2,6 +2,7 @@ import styled from "styled-components";
import useInviteLink from "../hook/useInviteLink"; import useInviteLink from "../hook/useInviteLink";
import Input from "./styled/Input"; import Input from "./styled/Input";
import Button from "./styled/Button"; import Button from "./styled/Button";
import { FC } from "react";
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
display: flex; display: flex;
@@ -37,8 +38,8 @@ const StyledWrapper = styled.div`
margin-bottom: 20px; margin-bottom: 20px;
} }
`; `;
type Props = {};
export default function InviteLink() { const InviteLink: FC<Props> = () => {
const { generating, link, linkCopied, copyLink, generateNewLink } = useInviteLink(); const { generating, link, linkCopied, copyLink, generateNewLink } = useInviteLink();
const handleNewLink = () => { const handleNewLink = () => {
generateNewLink(); generateNewLink();
@@ -59,4 +60,6 @@ export default function InviteLink() {
</Button> </Button>
</StyledWrapper> </StyledWrapper>
); );
} };
export default InviteLink;
+12 -2
View File
@@ -135,7 +135,15 @@ const ManageMembers: FC<Props> = ({ cid }) => {
} }
}, [updateSuccess]); }, [updateSuccess]);
const handleToggleRole = ({ ignore = false, uid = null, isAdmin = true }) => { const handleToggleRole = ({
ignore = false,
uid,
isAdmin = true
}: {
ignore: boolean;
uid: number;
isAdmin: boolean;
}) => {
hideAll(); hideAll();
if (ignore) return; if (ignore) return;
updateUser({ id: uid, is_admin: isAdmin }); updateUser({ id: uid, is_admin: isAdmin });
@@ -154,7 +162,9 @@ const ManageMembers: FC<Props> = ({ cid }) => {
</div> </div>
<ul className="members"> <ul className="members">
{uids.map((uid) => { {uids.map((uid) => {
const { name, email, is_admin } = users.byId[uid]; const currUser = users.byId[uid];
if (!currUser) return null;
const { name, email, is_admin } = currUser;
const owner = channel && channel.owner == uid; const owner = channel && channel.owner == uid;
const switchRoleVisible = loginUser.is_admin && loginUser.uid !== uid; const switchRoleVisible = loginUser.is_admin && loginUser.uid !== uid;
const dotsVisible = email || loginUser?.is_admin; const dotsVisible = email || loginUser?.is_admin;
+8 -20
View File
@@ -1,13 +1,14 @@
import { useEffect, useState, useRef, SyntheticEvent } from "react"; import { useEffect, useState, useRef, FC } from "react";
import { BeforeInstallPromptEvent } from "../../../types/global";
import Prompt from "./Prompt"; import Prompt from "./Prompt";
import usePrompt from "./usePrompt"; import usePrompt from "./usePrompt";
interface IProps {}
export default function Manifest() { const Manifest: FC<IProps> = () => {
const { setCanceled: setCanceled, prompted } = usePrompt(); const { setCanceled: setCanceled, prompted } = usePrompt();
const deferredPromptRef = useRef(null); const deferredPromptRef = useRef<null | BeforeInstallPromptEvent>(null);
const [popup, setPopup] = useState(false); const [popup, setPopup] = useState(false);
useEffect(() => { useEffect(() => {
const handleInstallPromotion = (e: SyntheticEvent) => { const handleInstallPromotion = (e: BeforeInstallPromptEvent) => {
// Prevent the mini-infobar from appearing on mobile // Prevent the mini-infobar from appearing on mobile
e.preventDefault(); e.preventDefault();
// Stash the event so it can be triggered later. // Stash the event so it can be triggered later.
@@ -21,20 +22,6 @@ export default function Manifest() {
deferredPromptRef.current = null; deferredPromptRef.current = null;
setPopup(false); setPopup(false);
}; };
// if (isSuccess && data) {
// console.log("server", data);
// manifest.name = `${data.name}'s Chat`;
// // const stringManifest = JSON.stringify(manifest);
// // const blob = new Blob([stringManifest], { type: "application/json" });
// // const manifestURL = URL.createObjectURL(blob);
// let content = encodeURIComponent(JSON.stringify(manifest));
// let manifestURL = "data:application/manifest+json," + content;
// const manifestEle = document.querySelector("#my-manifest-placeholder");
// if (manifestEle) {
// manifestEle.setAttribute("href", manifestURL);
// }
// }
window.addEventListener("beforeinstallprompt", handleInstallPromotion, true); window.addEventListener("beforeinstallprompt", handleInstallPromotion, true);
window.addEventListener("appinstalled", handleInstalled); window.addEventListener("appinstalled", handleInstalled);
return () => { return () => {
@@ -61,4 +48,5 @@ export default function Manifest() {
}; };
if (!popup || prompted) return null; if (!popup || prompted) return null;
return <Prompt handleInstall={handleInstall} closePrompt={handleClose} />; return <Prompt handleInstall={handleInstall} closePrompt={handleClose} />;
} };
export default Manifest;
@@ -27,7 +27,7 @@ function MarkdownEditor({
const editorInstance = editor.getInstance(); const editorInstance = editor.getInstance();
editorInstance.removeHook("addImageBlobHook"); editorInstance.removeHook("addImageBlobHook");
editorInstance.addHook("addImageBlobHook", async (blob, callback) => { editorInstance.addHook("addImageBlobHook", async (blob, callback) => {
const { thumbnail = "" } = await uploadFile(blob); const { thumbnail = "" } = (await uploadFile(blob)) || {};
callback(thumbnail); callback(thumbnail);
}); });
setEditorInstance(editorInstance); setEditorInstance(editorInstance);
+26 -32
View File
@@ -1,14 +1,8 @@
import { useEffect, useState, useRef, FC, MouseEvent } from "react"; import { useEffect, useState, useRef, FC } from "react";
import "prismjs/themes/prism.css"; import "prismjs/themes/prism.css";
import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css"; import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css";
import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js"; import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js";
// import "prismjs/themes/prism.css";
// import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css";
// import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight";
// Import prismjs
// import Prism from "prismjs";
import { Viewer } from "@toast-ui/react-editor"; import { Viewer } from "@toast-ui/react-editor";
import styled from "styled-components"; import styled from "styled-components";
import ImagePreviewModal, { PreviewImageData } from "./ImagePreviewModal"; import ImagePreviewModal, { PreviewImageData } from "./ImagePreviewModal";
@@ -24,37 +18,37 @@ const Styled = styled.div`
} }
`; `;
interface Props { interface IProps {
content: string; content: string;
} }
const MarkdownRender: FC<IProps> = ({ content }) => {
const MarkdownRender: FC<Props> = ({ content }) => { const mdContainer = useRef<HTMLDivElement>();
const mdContainer = useRef<HTMLDivElement>(null);
const [previewImage, setPreviewImage] = useState<PreviewImageData | null>(null); const [previewImage, setPreviewImage] = useState<PreviewImageData | null>(null);
useEffect(() => { useEffect(() => {
const container = mdContainer?.current; const container = mdContainer?.current;
if (container) { if (!container) return;
// 点击查看大图 // 点击查看大图
container.addEventListener( // todo: 事件代理
"click", container.addEventListener(
(evt) => { "click",
console.log(evt); (evt) => {
evt.stopPropagation(); console.log(evt);
const { target } = evt; evt.stopPropagation();
// 图片 const target = evt.target as HTMLImageElement;
if (target.nodeName == "IMG") { if (!target) return;
const urlObj = new URL(target.src); // 图片
const originUrl = `${urlObj.origin}${ if (target.nodeName == "IMG") {
urlObj.pathname const urlObj = new URL(target.src);
}?file_path=${urlObj.searchParams.get("file_path")}`; const originUrl = `${urlObj.origin}${urlObj.pathname}?file_path=${urlObj.searchParams.get(
const data = { originUrl }; "file_path"
setPreviewImage(data); )}`;
} const data = { originUrl };
}, setPreviewImage(data);
true }
); },
} true
);
}, []); }, []);
const closePreviewModal = () => { const closePreviewModal = () => {
@@ -0,0 +1,66 @@
import { FC, ReactElement, useEffect, useState } from "react";
import styled from "styled-components";
import StyledMsg from "./styled";
import renderContent from "./renderContent";
import Avatar from "../Avatar";
import useFavMessage from "../../hook/useFavMessage";
const StyledFav = styled.div`
display: flex;
flex-direction: column;
border-radius: var(--br);
background-color: #f4f4f5;
`;
type Props = {
id?: number;
};
const FavoredMessage: FC<Props> = ({ id }) => {
const { favorites } = useFavMessage({});
const [msgs, setMsgs] = useState<ReactElement | null>(null);
useEffect(() => {
const current = favorites.find((f) => f.id == id);
const { messages } = current || {};
if (!messages) return;
const favorite_mids = messages.map(({ from_mid }) => +from_mid) || [];
setMsgs(
<StyledFav data-favorite-mids={favorite_mids.join(",")} className="favorite">
<div className="list">
{messages.map((msg, idx) => {
const { user = {}, download, content, content_type, properties, thumbnail } = msg;
return (
<StyledMsg className="archive" key={idx}>
{user && (
<div className="avatar">
<Avatar url={user.avatar} name={user.name} />
</div>
)}
<div className="details">
<div className="up">
<span className="name">{user?.name}</span>
</div>
<div className="down">
{renderContent({
download,
content,
content_type,
properties,
thumbnail
})}
</div>
</div>
</StyledMsg>
);
})}
</div>
</StyledFav>
);
}, [favorites, id]);
if (!id) return null;
return msgs;
};
export default FavoredMessage;
@@ -1,65 +0,0 @@
import { useEffect, useState } from "react";
import styled from "styled-components";
import StyledMsg from "./styled";
import renderContent from "./renderContent";
import Avatar from "../Avatar";
import useFavMessage from "../../hook/useFavMessage";
const StyledFav = styled.div`
display: flex;
flex-direction: column;
border-radius: var(--br);
background-color: #f4f4f5;
`;
const FavoritedMessage = ({ id }) => {
const { favorites } = useFavMessage({});
const [msgs, setMsgs] = useState(null);
useEffect(() => {
const current = favorites.find((f) => f.id == id);
const { messages } = current || {};
if (messages) {
const favorite_mids = messages.map(({ from_mid }) => from_mid) || [];
setMsgs(
<StyledFav data-favorite-mids={favorite_mids.join(",")} className="favorite">
<div className="list">
{messages.map((msg, idx) => {
const { user = {}, download, content, content_type, properties, thumbnail } = msg;
return (
<StyledMsg className="archive" key={idx}>
{user && (
<div className="avatar">
<Avatar url={user.avatar} name={user.name} />
</div>
)}
<div className="details">
<div className="up">
<span className="name">{user?.name}</span>
</div>
<div className="down">
{renderContent({
download,
content,
content_type,
properties,
thumbnail
})}
</div>
</div>
</StyledMsg>
);
})}
</div>
</StyledFav>
);
}
}, [favorites, id]);
if (!id) return null;
return msgs;
};
export default FavoritedMessage;
+5 -3
View File
@@ -1,8 +1,9 @@
import { FC } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import BASE_URL from "../../app/config"; import BASE_URL from "../../app/config";
import { useGetServerQuery } from "../../app/services/server"; import { useGetServerQuery } from "../../app/services/server";
type Props = {};
export default function Meta() { const Meta: FC<Props> = () => {
const { data, isSuccess } = useGetServerQuery(); const { data, isSuccess } = useGetServerQuery();
return ( return (
@@ -11,4 +12,5 @@ export default function Meta() {
{isSuccess && <title>{data.name} Web App</title>} {isSuccess && <title>{data.name} Web App</title>}
</Helmet> </Helmet>
); );
} };
export default Meta;
@@ -2,9 +2,8 @@ import { useState } from "react";
import { initializeApp } from "firebase/app"; import { initializeApp } from "firebase/app";
import { getToken, getMessaging } from "firebase/messaging"; import { getToken, getMessaging } from "firebase/messaging";
import { firebaseConfig } from "../../../app/config"; import { firebaseConfig } from "../../../app/config";
const useDeviceToken = (vapidKey: string) => {
const useDeviceToken = (vapidKey) => { const [token, setToken] = useState<string | null>(null);
const [token, setToken] = useState(null);
// https only // https only
if (navigator.serviceWorker) { if (navigator.serviceWorker) {
const messaging = getMessaging(initializeApp(firebaseConfig)); const messaging = getMessaging(initializeApp(firebaseConfig));
@@ -17,7 +16,7 @@ const useDeviceToken = (vapidKey) => {
console.log("current token for client: ", currentToken); console.log("current token for client: ", currentToken);
setToken(currentToken); setToken(currentToken);
// updateDeviceToken(currentToken) // updateDeviceToken(currentToken)
// Perform any other neccessary action with the token // Perform any other necessary action with the token
} else { } else {
// Show permission request UI // Show permission request UI
console.log("No registration token available. Request permission to generate one."); console.log("No registration token available. Request permission to generate one.");
+2 -2
View File
@@ -11,8 +11,8 @@ import useUserOperation from "../../hook/useUserOperation";
import { useAppSelector } from "../../../app/store"; import { useAppSelector } from "../../../app/store";
interface Props { interface Props {
uid?: number; uid: number;
type: string; type?: "embed" | "card";
cid?: number; cid?: number;
} }
+5 -3
View File
@@ -4,6 +4,7 @@ import searchIcon from "../../assets/icons/search.svg?url";
import addIcon from "../../assets/icons/add.svg?url"; import addIcon from "../../assets/icons/add.svg?url";
import AddEntriesMenu from "./AddEntriesMenu"; import AddEntriesMenu from "./AddEntriesMenu";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
import { FC } from "react";
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
position: relative; position: relative;
@@ -32,8 +33,8 @@ const StyledWrapper = styled.div`
cursor: pointer; cursor: pointer;
} }
`; `;
type Props = {};
export default function Search() { const Search: FC<Props> = () => {
return ( return (
<StyledWrapper> <StyledWrapper>
<div className="search"> <div className="search">
@@ -47,4 +48,5 @@ export default function Search() {
</Tooltip> </Tooltip>
</StyledWrapper> </StyledWrapper>
); );
} };
export default Search;
+10 -6
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState, FC } from "react";
import useSendMessage from "../../hook/useSendMessage"; import useSendMessage from "../../hook/useSendMessage";
import useAddLocalFileMessage from "../../hook/useAddLocalFileMessage"; import useAddLocalFileMessage from "../../hook/useAddLocalFileMessage";
import { updateInputMode } from "../../../app/slices/ui"; import { updateInputMode } from "../../../app/slices/ui";
@@ -20,11 +20,15 @@ const Modes = {
text: "text", text: "text",
markdown: "markdown" markdown: "markdown"
}; };
function Send({ interface IProps {
context?: "channel" | "user";
id: number;
}
const Send: FC<IProps> = ({
// 发给谁,或者是channel,或者是user // 发给谁,或者是channel,或者是user
context = "channel", context = "channel",
id = "" id
}) { }) => {
const { resetStageFiles } = useUploadFile({ context, id }); const { resetStageFiles } = useUploadFile({ context, id });
const { getDraft, getUpdateDraft } = useDraft({ context, id }); const { getDraft, getUpdateDraft } = useDraft({ context, id });
const editor = useMixedEditor(`${context}_${id}`); const editor = useMixedEditor(`${context}_${id}`);
@@ -111,7 +115,7 @@ function Send({
resetStageFiles(); resetStageFiles();
} }
}; };
const sendMarkdown = async (content) => { const sendMarkdown = async (content: string) => {
sendMessage({ sendMessage({
id, id,
reply_mid: replying_mid, reply_mid: replying_mid,
@@ -173,7 +177,7 @@ function Send({
</div> </div>
</StyledSend> </StyledSend>
); );
} };
export default Send; export default Send;
// export default memo(Send, (prevs, nexts) => { // export default memo(Send, (prevs, nexts) => {
+3 -3
View File
@@ -5,16 +5,16 @@ import { addChannelMsg } from "../../app/slices/message.channel";
import { addUserMsg } from "../../app/slices/message.user"; import { addUserMsg } from "../../app/slices/message.user";
interface IProps { interface IProps {
context: "channel" | "uesr"; context: "channel" | "user";
to: number; to: number;
} }
export default function useAddLocalFileMessage({ context, to }: IProps) { export default function useAddLocalFileMessage({ context, to }: IProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const addContextMessage = context == "channel" ? addChannelMsg : addUserMsg; const addContextMessage = context == "channel" ? addChannelMsg : addUserMsg;
const addLocalFileMesage = (data) => { const addLocalFileMessage = (data) => {
dispatch(addMessage(data)); dispatch(addMessage(data));
dispatch(addContextMessage({ id: to, mid: data.mid })); dispatch(addContextMessage({ id: to, mid: data.mid }));
}; };
return addLocalFileMesage; return addLocalFileMessage;
} }
+1 -1
View File
@@ -74,7 +74,7 @@ export default function useStreaming() {
isError isError
} = await renewToken({ } = await renewToken({
token, token,
refreshToken refresh_token: refreshToken
}); });
if (isError) return; if (isError) return;
api_token = newToken; api_token = newToken;
+11 -6
View File
@@ -1,4 +1,4 @@
import { useState, useRef } from "react"; import { useState, useRef, FC } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { updateUploadFiles } from "../../app/slices/ui"; import { updateUploadFiles } from "../../app/slices/ui";
import BASE_URL, { FILE_SLICE_SIZE } from "../../app/config"; import BASE_URL, { FILE_SLICE_SIZE } from "../../app/config";
@@ -6,8 +6,12 @@ import { usePrepareUploadFileMutation, useUploadFileMutation } from "../../app/s
import { useAppDispatch, useAppSelector } from "../../app/store"; import { useAppDispatch, useAppSelector } from "../../app/store";
// todo: check props type // todo: check props type
export default function useUploadFile(props: { context: string; id: string } | object = {}) { interface IProps {
const { context = "", id = "" } = props; context: "channel" | "user";
id: number;
}
const useUploadFile = (props: IProps) => {
const { context, id } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { stageFiles, replying } = useAppSelector((store) => { const { stageFiles, replying } = useAppSelector((store) => {
return { return {
@@ -112,7 +116,7 @@ export default function useUploadFile(props: { context: string; id: string } | o
canceledRef.current = true; canceledRef.current = true;
}; };
const removeStageFile = (idx) => { const removeStageFile = (idx: number) => {
dispatch(updateUploadFiles({ context, id, operation: "remove", index: idx })); dispatch(updateUploadFiles({ context, id, operation: "remove", index: idx }));
}; };
@@ -128,7 +132,7 @@ export default function useUploadFile(props: { context: string; id: string } | o
dispatch(updateUploadFiles({ context, id, operation: "reset" })); dispatch(updateUploadFiles({ context, id, operation: "reset" }));
}; };
const updateStageFile = (idx, data = {}) => { const updateStageFile = (idx: number, data = {}) => {
dispatch( dispatch(
updateUploadFiles({ updateUploadFiles({
context, context,
@@ -156,4 +160,5 @@ export default function useUploadFile(props: { context: string; id: string } | o
removeStageFile, removeStageFile,
updateStageFile updateStageFile
}; };
} };
export default useUploadFile;
+1 -1
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, FC } from "react"; import { useState, useEffect } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
// import { ContentTypes } from "../../../app/config"; // import { ContentTypes } from "../../../app/config";
import { useNavigate, useMatch } from "react-router-dom"; import { useNavigate, useMatch } from "react-router-dom";
+2 -2
View File
@@ -1,7 +1,7 @@
// import React from "react"; // import React from "react";
// import { useSelector } from "react-redux"; // import { useSelector } from "react-redux";
import styled from "styled-components"; import styled from "styled-components";
import FavoritedMessage from "../../common/component/Message/FavoritedMessage"; import FavoredMessage from "../../common/component/Message/FavoredMessage";
import IconSurprise from "../../assets/icons/emoji.suprise.svg"; import IconSurprise from "../../assets/icons/emoji.suprise.svg";
// import IconForward from "../../../assets/icons/forward.svg"; // import IconForward from "../../../assets/icons/forward.svg";
import IconRemove from "../../assets/icons/close.svg"; import IconRemove from "../../assets/icons/close.svg";
@@ -107,7 +107,7 @@ export default function FavList({ cid = null, uid = null }) {
console.log("favv", id); console.log("favv", id);
return ( return (
<li key={id} className="fav"> <li key={id} className="fav">
<FavoritedMessage id={id} /> <FavoredMessage id={id} />
<div className="opts"> <div className="opts">
{/* <button {/* <button
className="btn" className="btn"
+4 -4
View File
@@ -1,7 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Styled from "./styled"; import Styled from "./styled";
import { useSelector } from "react-redux"; import FavoredMessage from "../../common/component/Message/FavoredMessage";
import FavoritedMessage from "../../common/component/Message/FavoritedMessage";
import dayjs from "dayjs"; import dayjs from "dayjs";
import IconAudio from "../../assets/icons/file.audio.svg"; import IconAudio from "../../assets/icons/file.audio.svg";
import IconVideo from "../../assets/icons/file.video.svg"; import IconVideo from "../../assets/icons/file.video.svg";
@@ -10,6 +9,7 @@ import IconUnknown from "../../assets/icons/file.unknown.svg";
import IconImage from "../../assets/icons/file.image.svg"; import IconImage from "../../assets/icons/file.image.svg";
import IconChannel from "../../assets/icons/channel.svg"; import IconChannel from "../../assets/icons/channel.svg";
import { ContentTypes } from "../../app/config"; import { ContentTypes } from "../../app/config";
import { useAppSelector } from "../../app/store";
const Filters = [ const Filters = [
{ {
icon: <IconUnknown className="icon" />, icon: <IconUnknown className="icon" />,
@@ -40,7 +40,7 @@ const Filters = [
function FavsPage() { function FavsPage() {
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const [favs, setFavs] = useState([]); const [favs, setFavs] = useState([]);
const { favorites, channelData, userData } = useSelector((store) => { const { favorites, channelData, userData } = useAppSelector((store) => {
console.log("favs", store.favorites); console.log("favs", store.favorites);
return { return {
favorites: store.favorites, favorites: store.favorites,
@@ -153,7 +153,7 @@ function FavsPage() {
{tip} {tip}
{dayjs(created_at).format("YYYY-MM-DD")} {dayjs(created_at).format("YYYY-MM-DD")}
</h4> </h4>
<FavoritedMessage key={id} id={id} /> <FavoredMessage key={id} id={id} />
</div> </div>
); );
})} })}
+12 -8
View File
@@ -1,13 +1,14 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
import { useState } from "react"; import { FC, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import StyledModal from "../../common/component/styled/Modal"; import StyledModal from "../../common/component/styled/Modal";
import Modal from "../../common/component/Modal"; import Modal from "../../common/component/Modal";
import { StyledSocialButton } from "./styled"; import { StyledSocialButton } from "./styled";
import StyledButton from "../../common/component/styled/Button"; import StyledButton from "../../common/component/styled/Button";
import OidcLoginEntry from "./OidcLoginEntry"; import OidcLoginEntry from "./OidcLoginEntry";
import { OIDCConfig } from "../../types/auth";
const StyledOicdLoginModal = styled(StyledModal)` const StyledOidcLoginModal = styled(StyledModal)`
text-align: center; text-align: center;
padding: 32px 32px 16px; padding: 32px 32px 16px;
@@ -21,13 +22,15 @@ const StyledOicdLoginModal = styled(StyledModal)`
height: 24px; height: 24px;
} }
&Cancel { &.buttonCancel {
color: #8f8f8f; color: #8f8f8f;
} }
} }
`; `;
interface IProps {
export default function OidcLoginButton({ issuers }) { issuers?: OIDCConfig[];
}
const OidcLoginButton: FC<IProps> = ({ issuers }) => {
const [modal, setModal] = useState(false); const [modal, setModal] = useState(false);
if (!issuers) return null; if (!issuers) return null;
return ( return (
@@ -41,7 +44,7 @@ export default function OidcLoginButton({ issuers }) {
</StyledSocialButton> </StyledSocialButton>
{modal && ( {modal && (
<Modal id="modal-modal"> <Modal id="modal-modal">
<StyledOicdLoginModal title="Login with OIDC"> <StyledOidcLoginModal title="Login with OIDC">
{issuers {issuers
.filter((issuer) => issuer.enable) .filter((issuer) => issuer.enable)
.map((issuer, index) => ( .map((issuer, index) => (
@@ -55,9 +58,10 @@ export default function OidcLoginButton({ issuers }) {
> >
Close Close
</StyledButton> </StyledButton>
</StyledOicdLoginModal> </StyledOidcLoginModal>
</Modal> </Modal>
)} )}
</> </>
); );
} };
export default OidcLoginButton;
+7 -9
View File
@@ -1,27 +1,24 @@
/* eslint-disable no-undef */ import { useEffect, FC } from "react";
import { useEffect } from "react";
import { useGetOpenidMutation } from "../../app/services/auth"; import { useGetOpenidMutation } from "../../app/services/auth";
import { OIDCConfig } from "../../types/auth";
import { StyledSocialButton } from "./styled"; import { StyledSocialButton } from "./styled";
export default function OidcLoginEntry({ issuer }) { const OidcLoginEntry: FC<{ issuer: OIDCConfig }> = ({ issuer }) => {
const [getOpenId, { data, isLoading, isSuccess }] = useGetOpenidMutation(); const [getOpenId, { data, isLoading, isSuccess }] = useGetOpenidMutation();
const handleSolidLogin = () => { const handleSolidLogin = () => {
getOpenId({ getOpenId({
// issuer: "solidweb.org", issuer: issuer.domain,
issuer,
redirect_uri: `${location.origin}/#/login` redirect_uri: `${location.origin}/#/login`
}); });
}; };
useEffect(() => { useEffect(() => {
if (isSuccess) { if (isSuccess && data) {
console.log("wtf", data);
const { url } = data; const { url } = data;
location.href = url; location.href = url;
} }
}, [data, isSuccess]); }, [data, isSuccess]);
console.log(issuer);
return ( return (
<StyledSocialButton disabled={isLoading} onClick={handleSolidLogin}> <StyledSocialButton disabled={isLoading} onClick={handleSolidLogin}>
@@ -29,4 +26,5 @@ export default function OidcLoginEntry({ issuer }) {
Login with {issuer.domain} Login with {issuer.domain}
</StyledSocialButton> </StyledSocialButton>
); );
} };
export default OidcLoginEntry;
+7 -7
View File
@@ -1,5 +1,5 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
import { useState, useEffect } from "react"; import { useState, useEffect, FormEvent, ChangeEvent } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import BASE_URL from "../../app/config"; import BASE_URL from "../../app/config";
// import web3 from "web3"; // import web3 from "web3";
@@ -16,6 +16,7 @@ import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig"; import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
import GoogleLoginButton from "../../common/component/GoogleLoginButton"; import GoogleLoginButton from "../../common/component/GoogleLoginButton";
import GithubLoginButton from "../../common/component/GithubLoginButton"; import GithubLoginButton from "../../common/component/GithubLoginButton";
import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
export default function LoginPage() { export default function LoginPage() {
const { data: enableSMTP } = useGetSMTPStatusQuery(); const { data: enableSMTP } = useGetSMTPStatusQuery();
@@ -53,7 +54,6 @@ export default function LoginPage() {
} }
// magic link // magic link
if (magic_token && typeof exists !== "undefined") { if (magic_token && typeof exists !== "undefined") {
// console.log("tokken", token, exists);
const isLogin = exists == "true"; const isLogin = exists == "true";
if (isLogin) { if (isLogin) {
// login // login
@@ -71,7 +71,7 @@ export default function LoginPage() {
useEffect(() => { useEffect(() => {
if (error) { if (error) {
console.log("login err", error); console.log("login err", error);
switch (error.status) { switch ((error as FetchBaseQueryError).status) {
case 401: case 401:
toast.error("Username or Password incorrect"); toast.error("Username or Password incorrect");
break; break;
@@ -94,7 +94,7 @@ export default function LoginPage() {
} }
}, [isSuccess]); }, [isSuccess]);
const handleLogin = (evt) => { const handleLogin = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault(); evt.preventDefault();
console.log("wtf", input); console.log("wtf", input);
login({ login({
@@ -103,10 +103,10 @@ export default function LoginPage() {
}); });
}; };
const handleInput = (evt) => { const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
const { type } = evt.target.dataset; const { type } = evt.target.dataset as { type?: "email" | "password" };
const { value } = evt.target; const { value } = evt.target;
// console.log(type, value); if (!type) return;
setInput((prev) => { setInput((prev) => {
prev[type] = value; prev[type] = value;
return { ...prev }; return { ...prev };
+4 -4
View File
@@ -8,10 +8,10 @@ import toast from "react-hot-toast";
import { setAuthData } from "../../app/slices/auth.data"; import { setAuthData } from "../../app/slices/auth.data";
export default function OAuthPage() { export default function OAuthPage() {
const [login, { data, isSuccess, isError, error: loginError }] = useLoginMutation(); const [login, { data, isSuccess, isError }] = useLoginMutation();
const { token } = useParams(); const { token } = useParams();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState<string | null>(null);
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigateTo = useNavigate(); const navigateTo = useNavigate();
useEffect(() => { useEffect(() => {
@@ -28,9 +28,9 @@ export default function OAuthPage() {
}, [token]); }, [token]);
useEffect(() => { useEffect(() => {
if (isError) { if (isError) {
setError(loginError); setError("Something Error");
} }
}, [isError, loginError]); }, [isError]);
useEffect(() => { useEffect(() => {
if (isSuccess && data) { if (isSuccess && data) {
+1 -2
View File
@@ -20,7 +20,6 @@ const StyledConfirm = styled.div`
gap: 10px; gap: 10px;
width: 250px; width: 250px;
.tip { .tip {
/* word-break: break-all; */
color: orange; color: orange;
font-size: 12px; font-size: 12px;
line-height: 1.5; line-height: 1.5;
@@ -100,7 +99,7 @@ export default function APIConfig() {
</Tippy> </Tippy>
<div className="tip"> <div className="tip">
Tip: The security key agreed between the server and the third-party app is used to encrypt Tip: The security key agreed between the server and the third-party app is used to encrypt
the communication data.{" "} the communication data.
</div> </div>
</Styled> </Styled>
); );
+2 -3
View File
@@ -4,7 +4,7 @@ import StyledSettingContainer from "../../common/component/StyledSettingContaine
import useNavs from "./navs"; import useNavs from "./navs";
import LogoutConfirmModal from "./LogoutConfirmModal"; import LogoutConfirmModal from "./LogoutConfirmModal";
let from: string | null = null; let from: string = "";
export default function Setting() { export default function Setting() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -15,10 +15,9 @@ export default function Setting() {
const [logoutConfirm, setLogoutConfirm] = useState(false); const [logoutConfirm, setLogoutConfirm] = useState(false);
const navigateTo = useNavigate(); const navigateTo = useNavigate();
const close = () => { const close = () => {
// dispatch(toggleSetting());
// todo: check usage // todo: check usage
navigateTo(from!); navigateTo(from!);
from = null; from = "";
}; };
const toggleLogoutConfirm = () => { const toggleLogoutConfirm = () => {
+13 -13
View File
@@ -6,20 +6,20 @@ import DeleteConfirmModal from "./DeleteConfirmModal";
import useNavs from "./navs"; import useNavs from "./navs";
import { useAppSelector } from "../../app/store"; import { useAppSelector } from "../../app/store";
let from: string | null = null; let from: string = "";
export default function ChannelSetting() { export default function ChannelSetting() {
const { cid } = useParams(); const { cid = 0 } = useParams();
const { loginUser, channel } = useAppSelector((store) => { const { loginUser, channel } = useAppSelector((store) => {
return { return {
loginUser: store.authData.user, loginUser: store.authData.user,
channel: store.channels.byId[cid] channel: cid ? store.channels.byId[+cid] : undefined
}; };
}); });
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navs = useNavs(cid); const navs = useNavs(+cid);
const flatenNavs = navs const flattenNavs = navs
.map(({ items }) => { .map(({ items }) => {
return items; return items;
}) })
@@ -30,16 +30,16 @@ export default function ChannelSetting() {
const [leaveConfirm, setLeaveConfirm] = useState(false); const [leaveConfirm, setLeaveConfirm] = useState(false);
const close = () => { const close = () => {
navigate(from); navigate(from);
from = null; from = "";
}; };
const toggleDeleteConfrim = () => { const toggleDeleteConfirm = () => {
setDeleteConfirm((prev) => !prev); setDeleteConfirm((prev) => !prev);
}; };
const toggleLeaveConfrim = () => { const toggleLeaveConfirm = () => {
setLeaveConfirm((prev) => !prev); setLeaveConfirm((prev) => !prev);
}; };
if (!cid) return null; if (!cid) return null;
const currNav = flatenNavs.find((n) => n.name == navKey) || flatenNavs[0]; const currNav = flattenNavs.find((n) => n.name == navKey) || flattenNavs[0];
const canDelete = loginUser.isAdmin || channel?.owner == loginUser.uid; const canDelete = loginUser.isAdmin || channel?.owner == loginUser.uid;
const canLeave = !channel?.is_public; const canLeave = !channel?.is_public;
@@ -53,18 +53,18 @@ export default function ChannelSetting() {
dangers={[ dangers={[
canLeave && { canLeave && {
title: "Leave Channel", title: "Leave Channel",
handler: toggleLeaveConfrim handler: toggleLeaveConfirm
}, },
canDelete && { canDelete && {
title: "Delete Channel", title: "Delete Channel",
handler: toggleDeleteConfrim handler: toggleDeleteConfirm
} }
]} ]}
> >
{currNav.component} {currNav.component}
</StyledSettingContainer> </StyledSettingContainer>
{deleteConfirm && <DeleteConfirmModal closeModal={toggleDeleteConfrim} id={cid} />} {deleteConfirm && <DeleteConfirmModal closeModal={toggleDeleteConfirm} id={+cid} />}
{leaveConfirm && <LeaveChannel closeModal={toggleLeaveConfrim} id={cid} />} {leaveConfirm && <LeaveChannel closeModal={toggleLeaveConfirm} id={+cid} />}
</> </>
); );
} }
+5 -4
View File
@@ -1,6 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { NavLink, useParams, useLocation } from "react-router-dom"; import { NavLink, useParams, useLocation } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch } from "react-redux";
import { updateRememberedNavs } from "../../app/slices/ui"; import { updateRememberedNavs } from "../../app/slices/ui";
import Search from "../../common/component/Search"; import Search from "../../common/component/Search";
import User from "../../common/component/User"; import User from "../../common/component/User";
@@ -8,13 +8,14 @@ import Profile from "../../common/component/Profile";
import StyledWrapper from "./styled"; import StyledWrapper from "./styled";
import BlankPlaceholder from "../../common/component/BlankPlaceholder"; import BlankPlaceholder from "../../common/component/BlankPlaceholder";
import { useAppSelector } from "../../app/store";
export default function UsersPage() { export default function UsersPage() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { pathname } = useLocation(); const { pathname } = useLocation();
const { user_id } = useParams(); const { user_id } = useParams();
const userIds = useSelector((store) => store.users.ids); const userIds = useAppSelector((store) => store.users.ids);
useEffect(() => { useEffect(() => {
dispatch(updateRememberedNavs({ key: "user" })); dispatch(updateRememberedNavs({ key: "user" }));
return () => { return () => {
@@ -30,7 +31,7 @@ export default function UsersPage() {
<Search /> <Search />
<div className="list"> <div className="list">
<nav className="nav"> <nav className="nav">
{userIds.map((uid) => { {userIds.map((uid: number) => {
return ( return (
<NavLink key={uid} className="session" to={`/users/${uid}`}> <NavLink key={uid} className="session" to={`/users/${uid}`}>
<User uid={uid} enableContextMenu={true} /> <User uid={uid} enableContextMenu={true} />
@@ -43,7 +44,7 @@ export default function UsersPage() {
</div> </div>
{user_id ? ( {user_id ? (
<div className="right"> <div className="right">
<Profile uid={user_id} /> <Profile uid={+user_id} />
</div> </div>
) : ( ) : (
<div className="right placeholder"> <div className="right placeholder">
+15
View File
@@ -6,6 +6,9 @@ export interface AuthToken {
refresh_token: string; refresh_token: string;
expired_in: number; expired_in: number;
} }
export interface RenewTokenDTO extends Pick<AuthToken, "token" | "refresh_token"> {}
export interface RenewTokenResponse
extends Pick<AuthToken, "token" | "refresh_token" | "expired_in"> {}
export interface AuthData extends AuthToken { export interface AuthData extends AuthToken {
initialized?: boolean; initialized?: boolean;
@@ -57,3 +60,15 @@ export type LoginCredential =
| GoogleCredential | GoogleCredential
| ThirdPartyCredential | ThirdPartyCredential
| MagicLinkCredential; | MagicLinkCredential;
export type CredentialResponse = {
password: boolean;
google: string;
metamask: string;
oidc: string[];
};
export interface OIDCConfig {
enable: boolean;
favicon: string;
domain: string;
}
+5 -3
View File
@@ -34,8 +34,12 @@ export interface CreateChannelDTO {
is_public: boolean; is_public: boolean;
} }
export interface ChannelDTO extends Pick<Channel, "owner" | "description" | "name"> {
id: number;
}
export interface UpdateChannelDTO { export interface UpdateChannelDTO {
operation: "add_member" | "remove_member"; operation?: "add_member" | "remove_member";
members?: number[]; members?: number[];
gid: number; // todo check gid: number; // todo check
name?: string; name?: string;
@@ -43,8 +47,6 @@ export interface UpdateChannelDTO {
owner?: number; owner?: number;
avatar_updated_at?: number; avatar_updated_at?: number;
type?: string; type?: string;
// type = 'user_joined_group' | 'user_leaved_group'
// gid
uid?: number[]; uid?: number[];
icon?: string; icon?: string;
} }
+26
View File
@@ -1,3 +1,26 @@
interface BeforeInstallPromptEvent extends Event {
/**
* Returns an array of DOMString items containing the platforms on which the event was dispatched.
* This is provided for user agents that want to present a choice of versions to the user such as,
* for example, "web" or "play" which would allow the user to chose between a web version or
* an Android version.
*/
readonly platforms: Array<string>;
/**
* Returns a Promise that resolves to a DOMString containing either "accepted" or "dismissed".
*/
readonly userChoice: Promise<{
outcome: "accepted" | "dismissed";
platform: string;
}>;
/**
* Allows a developer to show the install prompt at a time of their own choosing.
* This method returns a Promise.
*/
prompt(): Promise<void>;
}
export declare global { export declare global {
import { PrecacheEntry } from "workbox-precaching/src/_types"; import { PrecacheEntry } from "workbox-precaching/src/_types";
import localforage from "localforage"; import localforage from "localforage";
@@ -7,4 +30,7 @@ export declare global {
skipWaiting: () => void; skipWaiting: () => void;
CACHE: { [key: string]: typeof localforage | undefined }; CACHE: { [key: string]: typeof localforage | undefined };
} }
interface WindowEventMap {
beforeinstallprompt: BeforeInstallPromptEvent;
}
} }
+12
View File
@@ -1,3 +1,5 @@
import { ChatEvent } from "./sse";
export type ContentType = "text/plain" | "text/markdown" | "vocechat/file" | "vocechat/archive"; export type ContentType = "text/plain" | "text/markdown" | "vocechat/file" | "vocechat/archive";
export type ContentTypeKey = "text" | "markdown" | "file" | "archive"; export type ContentTypeKey = "text" | "markdown" | "file" | "archive";
@@ -13,3 +15,13 @@ export interface MuteDTO {
remove_users?: number[]; remove_users?: number[];
remove_groups?: number[]; remove_groups?: number[];
} }
export interface UploadFileResponse {
path: string;
size: number;
hash: string;
image_properties: {
width: number;
height: number;
};
}
export interface ChatMessage extends Omit<ChatEvent, "type"> {}
+8 -4
View File
@@ -1,3 +1,8 @@
export interface Archive {
users: ArchiveUser[];
messages: ArchiveMessage[];
num_attachments: number;
}
export interface ArchiveUser { export interface ArchiveUser {
name: string; name: string;
avatar?: number; avatar?: number;
@@ -13,10 +18,9 @@ export interface ArchiveMessage {
file_id?: number; file_id?: number;
thumbnail_id?: number; thumbnail_id?: number;
} }
export interface Archive { export interface FavoriteArchive {
users: ArchiveUser[]; id: string;
messages: ArchiveMessage[]; created_at: number;
num_attachments: number;
} }
// 上传文件API响应 // 上传文件API响应
export interface UploadResponse { export interface UploadResponse {
+8 -3
View File
@@ -1,4 +1,4 @@
import { User } from "./auth"; import { User } from "./user";
import { Channel } from "./channel"; import { Channel } from "./channel";
import { ContentType } from "./message"; import { ContentType } from "./message";
@@ -110,13 +110,18 @@ export interface ReplyMessage {
content_type: ContentType; content_type: ContentType;
content: string; content: string;
} }
type MessageTargetUser = {
uid: number;
};
type MessageTargetChannel = {
gid: number;
};
export interface ChatEvent { export interface ChatEvent {
type: "chat"; type: "chat";
mid: number; mid: number;
from_uid: number; from_uid: number;
created_at: number; created_at: number;
target: { uid: number }; target: MessageTargetChannel | MessageTargetUser;
detail: NormalMessage | ReactionMessage | ReplyMessage; detail: NormalMessage | ReactionMessage | ReplyMessage;
} }
+4 -6
View File
@@ -2,7 +2,7 @@ export type Gender = 0 | 1;
export interface User { export interface User {
uid: number; uid: number;
email: string; email?: string;
name: string; name: string;
gender: Gender; gender: Gender;
language: string; language: string;
@@ -25,9 +25,7 @@ export interface UserForAdmin extends User {
status: UserStatus; status: UserStatus;
online_devices: UserDevice[]; online_devices: UserDevice[];
} }
export interface UserForAdminDTO export interface UserForAdminDTO extends Partial<UserForAdmin> {
extends Pick< id?: number;
UserForAdmin, }
"email" | "password" | "name" | "gender" | "is_admin" | "language" | "status"
> {}
export interface UserDTO extends Pick<User, "name" | "gender" | "language"> {} export interface UserDTO extends Pick<User, "name" | "gender" | "language"> {}