Merge remote-tracking branch 'github-desktop-hdsuperman/refactor/typescript'
This commit is contained in:
@@ -51,7 +51,7 @@ export const authApi = createApi({
|
||||
}
|
||||
}
|
||||
}),
|
||||
register: builder.mutation({
|
||||
register: builder.mutation<any, any>({
|
||||
query: (data) => ({
|
||||
url: `user/register`,
|
||||
method: "POST",
|
||||
@@ -143,10 +143,12 @@ export const authApi = createApi({
|
||||
url: `/user/check_email?email=${encodeURIComponent(email)}`
|
||||
})
|
||||
}),
|
||||
getCredentials: builder.query({
|
||||
// todo: check return type
|
||||
getCredentials: builder.query<any, void>({
|
||||
query: () => ({ url: "/token/credentials" })
|
||||
}),
|
||||
logout: builder.query({
|
||||
// todo: check return type
|
||||
logout: builder.query<any, void>({
|
||||
query: () => ({ url: "token/logout" }),
|
||||
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { fullfillContacts } from "../slices/contacts";
|
||||
import BASE_URL, { ContentTypes } from "../config";
|
||||
import { onMessageSendStarted } from "./handlers";
|
||||
import handleChatMessage from "../../common/hook/useStreaming/chat.handler";
|
||||
import { User } from "../../types/auth";
|
||||
|
||||
export const contactApi = createApi({
|
||||
reducerPath: "contactApi",
|
||||
@@ -15,14 +16,15 @@ export const contactApi = createApi({
|
||||
endpoints: (builder) => ({
|
||||
getContacts: builder.query({
|
||||
query: () => ({ url: `user` }),
|
||||
transformResponse: (data) => {
|
||||
transformResponse: (data: User[]) => {
|
||||
return data.map((user) => {
|
||||
const avatar =
|
||||
user.avatar_updated_at == 0
|
||||
? ""
|
||||
: `${BASE_URL}/resource/avatar?uid=${user.uid}&t=${user.avatar_updated_at}`;
|
||||
user.avatar = avatar;
|
||||
return user;
|
||||
return {
|
||||
...user,
|
||||
avatar:
|
||||
user.avatar_updated_at == 0
|
||||
? ""
|
||||
: `${BASE_URL}/resource/avatar?uid=${user.uid}&t=${user.avatar_updated_at}`
|
||||
};
|
||||
});
|
||||
},
|
||||
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
||||
|
||||
@@ -10,11 +10,12 @@ import {
|
||||
LoginConfig,
|
||||
Server,
|
||||
TestEmailDTO,
|
||||
NewAdminDTO,
|
||||
CreateAdminDTO,
|
||||
SMTPConfig,
|
||||
AgoraConfig,
|
||||
GithubAuthConfig
|
||||
} from "../../types/server";
|
||||
|
||||
const defaultExpireDuration = 7 * 24 * 60 * 60;
|
||||
|
||||
export const serverApi = createApi({
|
||||
@@ -166,7 +167,7 @@ export const serverApi = createApi({
|
||||
}),
|
||||
updateServer: builder.mutation<void, Server>({
|
||||
query: (data) => ({
|
||||
url: `admin/system/organization`,
|
||||
url: "admin/system/organization",
|
||||
method: "POST",
|
||||
body: data
|
||||
}),
|
||||
@@ -181,9 +182,9 @@ export const serverApi = createApi({
|
||||
}
|
||||
}
|
||||
}),
|
||||
createAdmin: builder.mutation<User, NewAdminDTO>({
|
||||
createAdmin: builder.mutation<User, CreateAdminDTO>({
|
||||
query: (data) => ({
|
||||
url: `/admin/system/create_admin`,
|
||||
url: "/admin/system/create_admin",
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { isNull, omitBy } from "lodash";
|
||||
import BASE_URL from "../config";
|
||||
import { getNonNullValues } from "../../common/utils";
|
||||
import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from "../../types/channel";
|
||||
|
||||
interface StoredChannel extends Channel {
|
||||
@@ -70,7 +70,8 @@ const channelsSlice = createSlice({
|
||||
break;
|
||||
}
|
||||
default:
|
||||
state.byId[gid] = { ...state.byId[gid]!, ...getNonNullValues(rest) };
|
||||
// old code: state.byId[gid] = { ...state.byId[gid]!, ...getNonNullValues(rest) };
|
||||
state.byId[gid] = { ...state.byId[gid]!, ...omitBy(rest, isNull) };
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
+29
-31
@@ -1,8 +1,8 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { getNonNullValues } from '../../common/utils';
|
||||
import BASE_URL from '../config';
|
||||
import { User } from '../../types/auth';
|
||||
import { UserLog, UserState } from '../../types/sse';
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { isNull, omitBy } from "lodash";
|
||||
import BASE_URL from "../config";
|
||||
import { User } from "../../types/auth";
|
||||
import { UserLog, UserState } from "../../types/sse";
|
||||
|
||||
export interface StoredUser extends User {
|
||||
online?: boolean;
|
||||
@@ -20,31 +20,21 @@ const initialState: State = {
|
||||
};
|
||||
|
||||
const contactsSlice = createSlice({
|
||||
name: 'contacts',
|
||||
name: "contacts",
|
||||
initialState,
|
||||
reducers: {
|
||||
resetContacts() {
|
||||
return initialState;
|
||||
},
|
||||
fullfillContacts(state, action: PayloadAction<User[]>) {
|
||||
console.log('set Contacts store', action);
|
||||
fullfillContacts(state, action: PayloadAction<StoredUser[]>) {
|
||||
const contacts = action.payload || [];
|
||||
state.ids = contacts.map(({ uid }) => uid);
|
||||
// old code
|
||||
// state.byId = Object.fromEntries(
|
||||
// contacts.map((c) => {
|
||||
// const { uid } = c;
|
||||
// return [uid, c];
|
||||
// })
|
||||
// );
|
||||
|
||||
contacts.forEach(u => {
|
||||
state.byId[u.uid] = {
|
||||
...u,
|
||||
// todo: add avatar field
|
||||
avatar: ''
|
||||
};
|
||||
});
|
||||
state.byId = Object.fromEntries(
|
||||
contacts.map((c) => {
|
||||
const { uid } = c;
|
||||
return [uid, c];
|
||||
})
|
||||
);
|
||||
},
|
||||
removeContact(state, action: PayloadAction<number>) {
|
||||
const uid = action.payload;
|
||||
@@ -55,28 +45,36 @@ const contactsSlice = createSlice({
|
||||
const changeLogs = action.payload;
|
||||
changeLogs.forEach(({ action, uid, ...rest }) => {
|
||||
switch (action) {
|
||||
case 'update': {
|
||||
const vals = getNonNullValues(rest);
|
||||
case "update": {
|
||||
const vals = omitBy(rest, isNull);
|
||||
if (state.byId[uid]) {
|
||||
Object.keys(vals).forEach((k) => {
|
||||
state.byId[uid][k] = vals[k];
|
||||
if (k == 'avatar_updated_at') {
|
||||
// @ts-ignore
|
||||
state.byId[uid]![k] = vals[k];
|
||||
if (k == "avatar_updated_at") {
|
||||
state.byId[uid]!.avatar = `${BASE_URL}/resource/avatar?uid=${uid}&t=${vals[k]}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'create': {
|
||||
// todo: missing properties avatar, create_by
|
||||
state.byId[uid] = { uid, ...rest };
|
||||
case "create": {
|
||||
state.byId[uid] = {
|
||||
uid,
|
||||
avatar:
|
||||
rest.avatar_updated_at === 0
|
||||
? ""
|
||||
: `${BASE_URL}/resource/avatar?uid=${uid}&t=${rest.avatar_updated_at}`,
|
||||
create_by: "", // todo: missing properties create_by
|
||||
...rest
|
||||
};
|
||||
const idx = state.ids.findIndex((i) => i == uid);
|
||||
if (idx == -1) {
|
||||
state.ids.push(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
case "delete": {
|
||||
const idx = state.ids.findIndex((i) => i == uid);
|
||||
if (idx > -1) {
|
||||
state.ids.splice(idx, 1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { ChannelMessage } from '../../types/channel';
|
||||
import { EntityId } from '../../types/common';
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { EntityId } from "../../types/common";
|
||||
import { ChatEvent } from "../../types/sse";
|
||||
|
||||
export interface State {
|
||||
[gid: number]: number[] | undefined;
|
||||
@@ -18,7 +18,7 @@ const channelMsgSlice = createSlice({
|
||||
fullfillChannelMsg(state, action: PayloadAction<State>) {
|
||||
return action.payload;
|
||||
},
|
||||
addChannelMsg(state, action) {
|
||||
addChannelMsg(state, action: PayloadAction<ChatEvent>) {
|
||||
const { id, mid, local_id = null } = action.payload;
|
||||
if (state[id]) {
|
||||
const midExsited = state[id]!.findIndex((id) => id == mid) > -1;
|
||||
|
||||
@@ -64,9 +64,9 @@ const StyledWrapper = styled.div`
|
||||
interface Props {
|
||||
url?: string;
|
||||
name?: string;
|
||||
type?: "user";
|
||||
type?: "user" | "channel";
|
||||
disabled?: boolean;
|
||||
uploadImage: (file: File) => Promise<any>;
|
||||
uploadImage: (file: File) => void;
|
||||
}
|
||||
|
||||
const AvatarUploader: FC<Props> = ({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import Avatar from "./Avatar";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
@@ -46,14 +47,21 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Channel({ interactive = true, id = "", compact = false, avatarSize = 32 }) {
|
||||
interface Props {
|
||||
interactive?: boolean;
|
||||
id: number;
|
||||
compact?: boolean;
|
||||
avatarSize?: number;
|
||||
}
|
||||
|
||||
const Channel: FC<Props> = ({ interactive = true, id, compact = false, avatarSize = 32 }) => {
|
||||
const { channel, totalMemberCount } = useAppSelector((store) => {
|
||||
return {
|
||||
channel: store.channels.byId[id],
|
||||
totalMemberCount: store.contacts.ids.length
|
||||
};
|
||||
});
|
||||
console.log("channel item", id, channel);
|
||||
|
||||
if (!channel) return null;
|
||||
const { name, members = [], is_public, avatar } = channel;
|
||||
return (
|
||||
@@ -71,4 +79,6 @@ export default function Channel({ interactive = true, id = "", compact = false,
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Channel;
|
||||
|
||||
@@ -6,7 +6,7 @@ import LockHashIcon from "../../assets/icons/channel.private.svg";
|
||||
interface Props {
|
||||
personal?: boolean;
|
||||
muted?: boolean;
|
||||
className: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Styled = styled.div`
|
||||
@@ -16,7 +16,7 @@ const Styled = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const ChannelIcon: FC<Props> = ({ personal = false, muted = false, className }) => {
|
||||
const ChannelIcon: FC<Props> = ({ personal = false, muted = false, className = "" }) => {
|
||||
return (
|
||||
<Styled className={`${muted ? "muted" : ""} ${className}`}>
|
||||
{personal ? <LockHashIcon /> : <HashIcon />}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, FC, MouseEvent, ChangeEvent } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Modal from "../Modal";
|
||||
@@ -6,24 +6,29 @@ import Button from "../styled/Button";
|
||||
import ChannelIcon from "../ChannelIcon";
|
||||
import Contact from "../Contact";
|
||||
import StyledWrapper from "./styled";
|
||||
// import StyledToggle from "../../component/styled/Toggle";
|
||||
import StyledCheckbox from "../styled/Checkbox";
|
||||
import useFilteredUsers from "../../hook/useFilteredUsers";
|
||||
|
||||
import { useCreateChannelMutation } from "../../../app/services/channel";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { CreateChannelDTO } from "../../../types/channel";
|
||||
|
||||
export default function ChannelModal({ personal = false, closeModal }) {
|
||||
interface Props {
|
||||
personal?: boolean;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
|
||||
const { contactsData, loginUid } = useAppSelector((store) => {
|
||||
return { contactsData: store.contacts.byId, loginUid: store.authData.user?.uid };
|
||||
});
|
||||
const navigateTo = useNavigate();
|
||||
const [data, setData] = useState({
|
||||
const [data, setData] = useState<CreateChannelDTO>({
|
||||
name: "",
|
||||
description: "",
|
||||
members: [loginUid],
|
||||
members: loginUid ? [Number(loginUid)] : [],
|
||||
is_public: !personal
|
||||
});
|
||||
|
||||
const { contacts, input, updateInput } = useFilteredUsers();
|
||||
const [createChannel, { isSuccess, isError, isLoading, data: newChannelId }] =
|
||||
useCreateChannelMutation();
|
||||
@@ -35,6 +40,7 @@ export default function ChannelModal({ personal = false, closeModal }) {
|
||||
// });
|
||||
// };
|
||||
const handleCreate = () => {
|
||||
// todo: add field validation (maxLength, text format, trim)
|
||||
if (!data.name) {
|
||||
toast("please input channel name");
|
||||
return;
|
||||
@@ -46,11 +52,13 @@ export default function ChannelModal({ personal = false, closeModal }) {
|
||||
createChannel(data);
|
||||
};
|
||||
|
||||
// todo: delete the following code and use common error handler instead
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
toast.error("create new channel failed");
|
||||
}
|
||||
}, [isError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
toast.success("create new channel success");
|
||||
@@ -59,26 +67,27 @@ export default function ChannelModal({ personal = false, closeModal }) {
|
||||
}
|
||||
}, [isSuccess, newChannelId]);
|
||||
|
||||
const handleNameInput = (evt) => {
|
||||
setData((prev) => {
|
||||
return { ...prev, name: evt.target.value };
|
||||
});
|
||||
};
|
||||
const handleInputChange = (evt) => {
|
||||
updateInput(evt.target.value);
|
||||
};
|
||||
const toggleCheckMember = ({ currentTarget }) => {
|
||||
const { members } = data;
|
||||
const { uid } = currentTarget.dataset;
|
||||
let tmp = members.includes(+uid) ? members.filter((m) => m != uid) : [...members, +uid];
|
||||
setData((prev) => {
|
||||
return { ...prev, members: tmp };
|
||||
});
|
||||
const handleNameInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
setData((prev) => ({ ...prev, name: evt.target.value }));
|
||||
};
|
||||
|
||||
const loginUser = contactsData[loginUid];
|
||||
const handleInputChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
updateInput(evt.target.value);
|
||||
};
|
||||
|
||||
const toggleCheckMember = ({ currentTarget }: MouseEvent<HTMLLIElement>) => {
|
||||
const members = data.members ?? [];
|
||||
const { uid } = currentTarget.dataset;
|
||||
const uidNum = Number(uid);
|
||||
let tmp = members.includes(uidNum) ? members.filter((m) => m != uidNum) : [...members, uidNum];
|
||||
setData((prev) => ({ ...prev, members: tmp }));
|
||||
};
|
||||
|
||||
if (!loginUid) return null;
|
||||
const loginUser = contactsData[Number(loginUid)];
|
||||
if (!loginUser) return null;
|
||||
const { name, members, is_public } = data;
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<StyledWrapper>
|
||||
@@ -95,13 +104,13 @@ export default function ChannelModal({ personal = false, closeModal }) {
|
||||
<ul className="users">
|
||||
{contacts.map((u) => {
|
||||
const { uid } = u;
|
||||
const checked = members.includes(uid);
|
||||
const checked = members ? members.includes(uid) : false;
|
||||
return (
|
||||
<li
|
||||
key={uid}
|
||||
data-uid={uid}
|
||||
className="user"
|
||||
onClick={loginUid == uid ? null : toggleCheckMember}
|
||||
onClick={loginUid == uid ? undefined : toggleCheckMember}
|
||||
>
|
||||
<StyledCheckbox
|
||||
disabled={loginUid == uid}
|
||||
@@ -152,4 +161,6 @@ export default function ChannelModal({ personal = false, closeModal }) {
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ChannelModal;
|
||||
|
||||
@@ -6,7 +6,7 @@ import ContextMenu from "../ContextMenu";
|
||||
interface Props {
|
||||
enable?: boolean;
|
||||
uid: number;
|
||||
cid: number;
|
||||
cid?: number;
|
||||
visible: boolean;
|
||||
hide: () => void;
|
||||
children: ReactElement;
|
||||
|
||||
@@ -10,8 +10,8 @@ import useContextMenu from "../../hook/useContextMenu";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
|
||||
interface Props {
|
||||
cid: number;
|
||||
uid: number;
|
||||
cid?: number;
|
||||
owner?: number;
|
||||
dm?: boolean;
|
||||
interactive?: boolean;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import styled from "styled-components";
|
||||
import { useSelector } from "react-redux";
|
||||
import soundIcon from "../../assets/icons/sound.on.svg?url";
|
||||
import micIcon from "../../assets/icons/mic.on.svg?url";
|
||||
import Avatar from "./Avatar";
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { useSelector } from "react-redux";
|
||||
import Styled from "./styled";
|
||||
import ImageMessage from "./ImageMessage";
|
||||
import useRemoveLocalMessage from "../../hook/useRemoveLocalMessage";
|
||||
import useUploadFile from "../../hook/useUploadFile";
|
||||
import useSendMessage from "../../hook/useSendMessage";
|
||||
import Progress from "./Progress";
|
||||
import { getFileIcon, formatBytes, isImage, getImageSize } from "../../utils";
|
||||
import { getFileIcon, formatBytes, isImage, getImageSize, ImageSize } from "../../utils";
|
||||
// import { ReactComponent as IconDownload } from "../../../assets/icons/download.svg";
|
||||
// import { ReactComponent as IconClose } from "../../../assets/icons/close.circle.svg";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import IconDownload from "../../../assets/icons/download.svg";
|
||||
import IconClose from "../../../assets/icons/close.circle.svg";
|
||||
|
||||
@@ -21,17 +21,33 @@ const isLocalFile = (content: string) => {
|
||||
return content.startsWith("blob:");
|
||||
};
|
||||
|
||||
export default function FileMessage({
|
||||
context = "",
|
||||
to = null,
|
||||
interface Props {
|
||||
context: "user" | "channel";
|
||||
to: number;
|
||||
created_at: number;
|
||||
from_uid?: number;
|
||||
content: string;
|
||||
download: string;
|
||||
thumbnail: string;
|
||||
properties: {
|
||||
local_id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
content_type: string;
|
||||
};
|
||||
}
|
||||
|
||||
const FileMessage: FC<Props> = ({
|
||||
context,
|
||||
to,
|
||||
created_at,
|
||||
from_uid = null,
|
||||
content = "",
|
||||
download = "",
|
||||
thumbnail = "",
|
||||
properties = { local_id: 0, name: "", size: 0, content_type: "" }
|
||||
}) {
|
||||
const [imageSize, setImageSize] = useState(null);
|
||||
}) => {
|
||||
const [imageSize, setImageSize] = useState<ImageSize | null>(null);
|
||||
const [uploadingFile, setUploadingFile] = useState(false);
|
||||
const removeLocalMessage = useRemoveLocalMessage({ context, id: to });
|
||||
const {
|
||||
@@ -44,10 +60,18 @@ export default function FileMessage({
|
||||
to
|
||||
});
|
||||
const { stopUploading, data, uploadFile, progress, isSuccess: uploadSuccess } = useUploadFile();
|
||||
const fromUser = useSelector((store) => store.contacts.byId[from_uid]);
|
||||
const fromUser = useAppSelector((store) => store.contacts.byId[from_uid]);
|
||||
const { size, name, content_type } = properties ?? {};
|
||||
useEffect(() => {
|
||||
const handleUpSend = async ({ url, name, type }) => {
|
||||
const handleUpSend = async ({
|
||||
url,
|
||||
name,
|
||||
type
|
||||
}: {
|
||||
url: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}) => {
|
||||
try {
|
||||
setUploadingFile(true);
|
||||
if (type.startsWith("image")) {
|
||||
@@ -102,7 +126,6 @@ export default function FileMessage({
|
||||
|
||||
if (!content || !name) return null;
|
||||
|
||||
console.log("file content", content, name, content_type, size);
|
||||
const sending = uploadingFile || isSending;
|
||||
|
||||
if (isImage(content_type, size))
|
||||
@@ -152,4 +175,6 @@ export default function FileMessage({
|
||||
</div>
|
||||
</Styled>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default FileMessage;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { useGoogleLogin, GoogleOAuthProvider, GoogleOAuthProviderProps } from "@react-oauth/google";
|
||||
import { useGoogleLogin, GoogleOAuthProvider } from "@react-oauth/google";
|
||||
import toast from "react-hot-toast";
|
||||
import styled from "styled-components";
|
||||
import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
|
||||
@@ -36,12 +36,16 @@ const GoogleLogin: FC<Props> = ({ type = "login", loaded, loadError }) => {
|
||||
const magic_token = localStorage.getItem(KEY_LOCAL_MAGIC_TOKEN);
|
||||
const googleLogin = useGoogleLogin({
|
||||
// flow: "auth-code",
|
||||
onSuccess: ({ access_token }) => {
|
||||
login({
|
||||
magic_token,
|
||||
id_token: access_token,
|
||||
type: "google"
|
||||
});
|
||||
onSuccess: (res) => {
|
||||
if ("code" in res) {
|
||||
console.error(`google login failed: ${res.code}`);
|
||||
} else {
|
||||
login({
|
||||
magic_token,
|
||||
id_token: res.access_token,
|
||||
type: "google"
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
@@ -51,7 +55,7 @@ const GoogleLogin: FC<Props> = ({ type = "login", loaded, loadError }) => {
|
||||
}
|
||||
}, [isSuccess]);
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
if (error && "status" in error) {
|
||||
switch (error.status) {
|
||||
case 410:
|
||||
toast.error(
|
||||
@@ -68,10 +72,10 @@ const GoogleLogin: FC<Props> = ({ type = "login", loaded, loadError }) => {
|
||||
const handleGoogleLogin = () => {
|
||||
googleLogin();
|
||||
};
|
||||
// console.log("google login ", loaded);
|
||||
|
||||
return (
|
||||
<StyledSocialButton disabled={!loaded || isLoading} onClick={handleGoogleLogin}>
|
||||
<IconGoogle className="icon" alt="google icon" />
|
||||
<IconGoogle className="icon" />
|
||||
{loadError
|
||||
? "Script Load Error!"
|
||||
: loaded
|
||||
@@ -80,6 +84,7 @@ const GoogleLogin: FC<Props> = ({ type = "login", loaded, loadError }) => {
|
||||
</StyledSocialButton>
|
||||
);
|
||||
};
|
||||
|
||||
const GoogleLoginButton: FC<Props> = ({ type = "login", clientId }) => {
|
||||
const [scriptLoaded, setScriptLoaded] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
@@ -97,4 +102,5 @@ const GoogleLoginButton: FC<Props> = ({ type = "login", clientId }) => {
|
||||
</GoogleOAuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleLoginButton;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, FC, MouseEvent, ChangeEvent } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useSelector } from "react-redux";
|
||||
import toast from "react-hot-toast";
|
||||
import Button from "../styled/Button";
|
||||
import Input from "../styled/Input";
|
||||
@@ -9,6 +8,7 @@ import StyledCheckbox from "../styled/Checkbox";
|
||||
import { useAddMembersMutation } from "../../../app/services/channel";
|
||||
import CloseIcon from "../../../assets/icons/close.svg";
|
||||
import useFilteredUsers from "../../hook/useFilteredUsers";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
|
||||
const Styled = styled.div`
|
||||
padding-top: 16px;
|
||||
@@ -21,7 +21,7 @@ const Styled = styled.div`
|
||||
margin-bottom: 12px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0px 1px 2px rgba(31, 41, 55, 0.08);
|
||||
box-shadow: 0 1px 2px rgba(31, 41, 55, 0.08);
|
||||
border-radius: 4px;
|
||||
.selects {
|
||||
display: flex;
|
||||
@@ -96,10 +96,16 @@ const Styled = styled.div`
|
||||
line-height: 20px;
|
||||
}
|
||||
`;
|
||||
export default function AddMembers({ cid = null, closeModal }) {
|
||||
|
||||
interface Props {
|
||||
cid: null | number;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const AddMembers: FC<Props> = ({ cid = null, closeModal }) => {
|
||||
const [addMembers, { isLoading: isAdding, isSuccess }] = useAddMembersMutation();
|
||||
const [selects, setSelects] = useState([]);
|
||||
const { channel, contactData } = useSelector((store) => {
|
||||
const { channel, contactData } = useAppSelector((store) => {
|
||||
return {
|
||||
channel: store.channels.byId[cid],
|
||||
contactData: store.contacts.byId
|
||||
@@ -116,7 +122,8 @@ export default function AddMembers({ cid = null, closeModal }) {
|
||||
addMembers({ id: cid, members: selects });
|
||||
};
|
||||
const { input, updateInput, contacts = [] } = useFilteredUsers();
|
||||
const toggleCheckMember = ({ currentTarget }) => {
|
||||
|
||||
const toggleCheckMember = ({ currentTarget }: MouseEvent<SVGSVGElement | HTMLLIElement>) => {
|
||||
const { uid } = currentTarget.dataset;
|
||||
if (selects.includes(+uid)) {
|
||||
setSelects((prevs) => {
|
||||
@@ -126,13 +133,15 @@ export default function AddMembers({ cid = null, closeModal }) {
|
||||
setSelects([...selects, +uid]);
|
||||
}
|
||||
};
|
||||
const handleFilterInput = (evt) => {
|
||||
|
||||
const handleFilterInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
updateInput(evt.target.value);
|
||||
};
|
||||
|
||||
if (!channel) return null;
|
||||
const { members: uids } = channel;
|
||||
const contactIds = contacts.map(({ uid }) => uid);
|
||||
console.log("selects", selects);
|
||||
|
||||
return (
|
||||
<Styled>
|
||||
<div className="filter">
|
||||
@@ -164,7 +173,7 @@ export default function AddMembers({ cid = null, closeModal }) {
|
||||
key={uid}
|
||||
data-uid={uid}
|
||||
className="user"
|
||||
onClick={added ? null : toggleCheckMember}
|
||||
onClick={added ? undefined : toggleCheckMember}
|
||||
>
|
||||
<StyledCheckbox
|
||||
disabled={added}
|
||||
@@ -183,4 +192,6 @@ export default function AddMembers({ cid = null, closeModal }) {
|
||||
</Button>
|
||||
</Styled>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AddMembers;
|
||||
|
||||
@@ -4,12 +4,13 @@ import AddMembers from "./AddMembers";
|
||||
import CloseIcon from "../../../assets/icons/close.svg";
|
||||
import Modal from "../Modal";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { FC } from "react";
|
||||
|
||||
const Styled = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
box-shadow: 0px 25px 50px rgba(31, 41, 55, 0.25);
|
||||
box-shadow: 0 25px 50px rgba(31, 41, 55, 0.25);
|
||||
border-radius: var(--br);
|
||||
padding: 16px;
|
||||
min-width: 408px;
|
||||
@@ -29,7 +30,15 @@ const Styled = styled.div`
|
||||
`;
|
||||
|
||||
// type: server,channel
|
||||
export default function InviteModal({ type = "server", cid = null, title = "", closeModal }) {
|
||||
|
||||
interface Props {
|
||||
type?: "server" | "channel";
|
||||
cid?: null | number;
|
||||
title?: string;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const InviteModal: FC<Props> = ({ type = "server", cid = null, title = "", closeModal }) => {
|
||||
const { channel, server } = useAppSelector((store) => {
|
||||
return {
|
||||
channel: store.channels.byId[cid],
|
||||
@@ -49,4 +58,6 @@ export default function InviteModal({ type = "server", cid = null, title = "", c
|
||||
</Styled>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default InviteModal;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useRef, FC } from "react";
|
||||
import { useEffect, useState, useRef, FC, MouseEvent } from "react";
|
||||
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/dist/toastui-editor-plugin-code-syntax-highlight-all.js";
|
||||
@@ -11,7 +11,7 @@ import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/d
|
||||
|
||||
import { Viewer } from "@toast-ui/react-editor";
|
||||
import styled from "styled-components";
|
||||
import ImagePreviewModal from "./ImagePreviewModal";
|
||||
import ImagePreviewModal, { PreviewImageData } from "./ImagePreviewModal";
|
||||
|
||||
const Styled = styled.div`
|
||||
* {
|
||||
@@ -28,9 +28,9 @@ interface Props {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const MrakdownRender: FC<Props> = ({ content }) => {
|
||||
const MarkdownRender: FC<Props> = ({ content }) => {
|
||||
const mdContainer = useRef<HTMLDivElement>(null);
|
||||
const [previewImage, setPreviewImage] = useState(null);
|
||||
const [previewImage, setPreviewImage] = useState<PreviewImageData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = mdContainer?.current;
|
||||
@@ -77,4 +77,4 @@ const MrakdownRender: FC<Props> = ({ content }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MrakdownRender;
|
||||
export default MarkdownRender;
|
||||
@@ -3,8 +3,13 @@ import renderContent from "./renderContent";
|
||||
import Avatar from "../Avatar";
|
||||
import StyledWrapper from "./styled";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { FC } from "react";
|
||||
|
||||
export default function PreviewMessage({ mid = 0 }) {
|
||||
interface Props {
|
||||
mid?: number;
|
||||
}
|
||||
|
||||
const PreviewMessage: FC<Props> = ({ mid = 0 }) => {
|
||||
const { msg, contactsData } = useAppSelector((store) => {
|
||||
return { msg: store.message[mid], contactsData: store.contacts.byId };
|
||||
});
|
||||
@@ -33,4 +38,6 @@ export default function PreviewMessage({ mid = 0 }) {
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default PreviewMessage;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { MouseEvent, FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import MrakdownRender from "../MrakdownRender";
|
||||
import MarkdownRender from "../MarkdownRender";
|
||||
import Mention from "./Mention";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import { getFileIcon, isImage } from "../../utils";
|
||||
@@ -122,7 +122,7 @@ const renderContent = (data) => {
|
||||
case ContentTypes.markdown:
|
||||
res = (
|
||||
<div className="md">
|
||||
<MrakdownRender content={content} />
|
||||
<MarkdownRender content={content} />
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -5,7 +5,7 @@ import reactStringReplace from "react-string-replace";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import Mention from "./Mention";
|
||||
import ForwardedMessage from "./ForwardedMessage";
|
||||
import MrakdownRender from "../MrakdownRender";
|
||||
import MarkdownRender from "../MarkdownRender";
|
||||
import FileMessage from "../FileMessage";
|
||||
import URLPreview from "./URLPreview";
|
||||
|
||||
@@ -54,7 +54,7 @@ const renderContent = ({
|
||||
break;
|
||||
case ContentTypes.markdown:
|
||||
{
|
||||
ctn = <MrakdownRender content={content} />;
|
||||
ctn = <MarkdownRender content={content} />;
|
||||
}
|
||||
break;
|
||||
case ContentTypes.file:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// import React from 'react'
|
||||
import { Helmet } from "react-helmet";
|
||||
import BASE_URL from "../../app/config";
|
||||
import { useGetServerQuery } from "../../app/services/server";
|
||||
@@ -7,11 +6,9 @@ export default function Meta() {
|
||||
const { data, isSuccess } = useGetServerQuery();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<link rel="icon" href={`${BASE_URL}/resource/organization/logo`} />
|
||||
{isSuccess && <title>{data.name} Web App</title>}
|
||||
</Helmet>
|
||||
</>
|
||||
<Helmet>
|
||||
<link rel="icon" href={`${BASE_URL}/resource/organization/logo`} />
|
||||
{isSuccess && <title>{data.name} Web App</title>}
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useAppSelector } from "../../../app/store";
|
||||
interface Props {
|
||||
uid: number;
|
||||
type: string;
|
||||
cid: number;
|
||||
cid?: number;
|
||||
}
|
||||
|
||||
const Profile: FC<Props> = ({ uid = null, type = "embed", cid = null }) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import reactStringReplace from "react-string-replace";
|
||||
import styled from "styled-components";
|
||||
import Mention from "../Message/Mention";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import MrakdownRender from "../MrakdownRender";
|
||||
import MarkdownRender from "../MarkdownRender";
|
||||
import closeIcon from "../../../assets/icons/close.circle.svg?url";
|
||||
import pictureIcon from "../../../assets/icons/picture.svg?url";
|
||||
import { getFileIcon, isImage } from "../../utils";
|
||||
@@ -91,7 +91,7 @@ const renderContent = (data) => {
|
||||
case ContentTypes.markdown:
|
||||
res = (
|
||||
<div className="md">
|
||||
<MrakdownRender content={content} />
|
||||
<MarkdownRender content={content} />
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -51,19 +51,21 @@ export default function Toolbar({
|
||||
to,
|
||||
context
|
||||
}) {
|
||||
// todo: check code logic
|
||||
const { addStageFile } = useUploadFile({ context, id: to });
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const handleUpload = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = [...evt.target.files];
|
||||
console.log("files", files);
|
||||
if (!evt.target.files) return;
|
||||
const files = Array.from(evt.target.files);
|
||||
const filesData = files.map((file) => {
|
||||
const { size, type, name } = file;
|
||||
const url = URL.createObjectURL(file);
|
||||
return { size, type, name, url };
|
||||
});
|
||||
addStageFile(filesData);
|
||||
// todo: check code logic
|
||||
// @ts-ignore
|
||||
fileInputRef.current.value = null;
|
||||
// @ts-ignore
|
||||
fileInputRef.current.value = "";
|
||||
// setFiles([...evt.target.files]);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { FC, ReactElement } from "react";
|
||||
import { FC, ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useLocation, NavLink } from "react-router-dom";
|
||||
import backIcon from "../../assets/icons/arrow.left.svg?url";
|
||||
import { Nav } from "../../routes/settingChannel/navs";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
|
||||
> .left {
|
||||
max-height: 100vh;
|
||||
overflow: scroll;
|
||||
padding: 32px 16px;
|
||||
min-width: 212px;
|
||||
background-color: #f5f6f7;
|
||||
|
||||
> .title {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
@@ -21,16 +24,16 @@ const StyledWrapper = styled.div`
|
||||
color: #1c1c1e;
|
||||
margin-bottom: 32px;
|
||||
padding-left: 24px;
|
||||
background: url(${backIcon});
|
||||
background-size: 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: left;
|
||||
background: url(${backIcon}) no-repeat left;
|
||||
}
|
||||
|
||||
> .items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 36px;
|
||||
|
||||
&:before {
|
||||
padding-left: 12px;
|
||||
content: attr(data-title);
|
||||
@@ -40,31 +43,37 @@ const StyledWrapper = styled.div`
|
||||
color: #6b7280;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #44494f;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover,
|
||||
&.curr {
|
||||
background: #e7e5e4;
|
||||
}
|
||||
|
||||
> a {
|
||||
display: block;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&.danger .item {
|
||||
cursor: pointer;
|
||||
padding: 4px 12px;
|
||||
color: #ef4444;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .right {
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
@@ -72,6 +81,7 @@ const StyledWrapper = styled.div`
|
||||
/* max-height: -webkit-fill-available; */
|
||||
overflow: auto;
|
||||
padding: 32px;
|
||||
|
||||
> .title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
@@ -82,21 +92,18 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
interface Nav {
|
||||
name?: string;
|
||||
export interface Danger {
|
||||
title: string;
|
||||
// todo
|
||||
items: [];
|
||||
component: ReactElement;
|
||||
handler: () => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
closeModal: () => void;
|
||||
title?: string;
|
||||
navs: Nav[];
|
||||
dangers: [];
|
||||
nav: Nav;
|
||||
children: ReactElement;
|
||||
dangers: Danger[];
|
||||
nav: { title: string; name?: string; component?: ReactNode };
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const StyledSettingContainer: FC<Props> = ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import IconSelect from "../../../assets/icons/check.sign.svg";
|
||||
@@ -26,21 +26,34 @@ const Styled = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const CURRENT_NOT_SET = {};
|
||||
export interface Option {
|
||||
icon?: string;
|
||||
title: string;
|
||||
value: string;
|
||||
selected: boolean;
|
||||
underline?: boolean;
|
||||
}
|
||||
|
||||
export default function Select({ options = [], updateSelect = null, current = CURRENT_NOT_SET }) {
|
||||
interface Props {
|
||||
options: Option[];
|
||||
updateSelect: null | ((option: Partial<Option>) => void);
|
||||
current: null | Partial<Option>;
|
||||
}
|
||||
|
||||
const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }) => {
|
||||
const [optionsVisible, setOptionsVisible] = useState(false);
|
||||
const [curr, setCurr] = useState(undefined);
|
||||
const [curr, setCurr] = useState<Partial<Option> | null>(null);
|
||||
const toggleVisible = () => {
|
||||
setOptionsVisible((prev) => !prev);
|
||||
};
|
||||
const handleSelect = (data) => {
|
||||
const handleSelect = (data: Partial<Option>) => {
|
||||
setCurr(data);
|
||||
toggleVisible();
|
||||
if (updateSelect) {
|
||||
updateSelect(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tippy
|
||||
visible={optionsVisible}
|
||||
@@ -52,7 +65,7 @@ export default function Select({ options = [], updateSelect = null, current = CU
|
||||
{options.map(({ title, value, selected, underline }) => {
|
||||
return (
|
||||
<li
|
||||
onClick={selected ? null : handleSelect.bind(null, { title, value })}
|
||||
onClick={selected ? undefined : handleSelect.bind(null, { title, value })}
|
||||
className={`item sb ${underline ? "underline" : ""}`}
|
||||
data-disabled={selected}
|
||||
key={value}
|
||||
@@ -66,11 +79,11 @@ export default function Select({ options = [], updateSelect = null, current = CU
|
||||
}
|
||||
>
|
||||
<Styled onClick={toggleVisible}>
|
||||
<span className="txt">
|
||||
{(current !== CURRENT_NOT_SET ? current : curr)?.title || `Select`}
|
||||
</span>
|
||||
<span className="txt">{(current !== null ? current : curr)?.title || "Select"}</span>
|
||||
<IconArrow className="icon" />
|
||||
</Styled>
|
||||
</Tippy>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Select;
|
||||
|
||||
@@ -17,7 +17,7 @@ let originalValue: null | object = null;
|
||||
|
||||
export default function useConfig(config = "smtp") {
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [values, setValues] = useState({});
|
||||
const [values, setValues] = useState<object>({});
|
||||
const [updateLoginConfig, { isSuccess: LoginUpdated }] = useUpdateLoginConfigMutation();
|
||||
const [updateSMTPConfig, { isSuccess: SMTPUpdated }] = useUpdateSMTPConfigMutation();
|
||||
const [getAgoraConfig, { data: agoraConfig }] = useLazyGetAgoraConfigQuery();
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
useUpdateGithubAuthConfigMutation
|
||||
} from "../../app/services/server";
|
||||
|
||||
interface GithubAuthConfig {
|
||||
client_id: string;
|
||||
}
|
||||
|
||||
export default function useGithubAuthConfig() {
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [config, setConfig] = useState({});
|
||||
|
||||
@@ -19,7 +19,6 @@ export default function useInviteLink(cid: string | null = "") {
|
||||
|
||||
useEffect(() => {
|
||||
const _link = channelInviteLink;
|
||||
console.log("fetching", channelInviteLink);
|
||||
if (_link && smtpStatusFetchSuccess) {
|
||||
// const tmpURL = new URL(_link);
|
||||
// tmpURL.searchParams.set("code", SMTPEnabled);
|
||||
@@ -29,6 +28,7 @@ export default function useInviteLink(cid: string | null = "") {
|
||||
const genServerLink = () => {
|
||||
generateChannelInviteLink();
|
||||
};
|
||||
|
||||
return {
|
||||
enableSMTP: SMTPEnabled,
|
||||
generating: generatingChannelLink,
|
||||
|
||||
@@ -11,6 +11,10 @@ import { useLazyLogoutQuery } from "../../app/services/auth";
|
||||
export default function useLogout() {
|
||||
const dispatch = useDispatch();
|
||||
const [logout, { isLoading, isSuccess }] = useLazyLogoutQuery();
|
||||
// todo: remove batch
|
||||
// If you're using React 18, you do not need to use the batch API. React 18 automatically
|
||||
// batches all state updates, no matter where they're queued.
|
||||
// ref: https://react-redux.js.org/api/batch
|
||||
const clearLocalData = () => {
|
||||
batch(() => {
|
||||
dispatch(resetFootprint());
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { normalizeArchiveData } from "../utils";
|
||||
import { normalizeArchiveData, ArchiveMessage } from "../utils";
|
||||
import { useLazyGetArchiveMessageQuery } from "../../app/services/message";
|
||||
|
||||
export default function useNormalizeMessage() {
|
||||
const [filePath, setFilePath] = useState<string | null>(null);
|
||||
const [normalizedMessages, setNormalizedMessages] = useState(null);
|
||||
const [normalizedMessages, setNormalizedMessages] = useState<ArchiveMessage[] | null>(null);
|
||||
const [getArchiveMessage, { data, isError, isLoading, isSuccess }] =
|
||||
useLazyGetArchiveMessageQuery();
|
||||
useEffect(() => {
|
||||
|
||||
@@ -6,8 +6,10 @@ import { addFileMessage, removeFileMessage } from "../../../app/slices/message.f
|
||||
import { addUserMsg, removeUserMsg } from "../../../app/slices/message.user";
|
||||
import { updateAfterMid } from "../../../app/slices/footprint";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import { ChatEvent } from "../../../types/sse";
|
||||
import { AppDispatch, RootState } from "../../../app/store";
|
||||
|
||||
const handler = (data, dispatch, currState) => {
|
||||
const handler = (data: ChatEvent, dispatch: AppDispatch, currState: RootState) => {
|
||||
const {
|
||||
mid,
|
||||
from_uid,
|
||||
@@ -45,37 +47,30 @@ const handler = (data, dispatch, currState) => {
|
||||
// 此处有点绕
|
||||
const id = to == "user" ? (self ? target.uid : from_uid) : target.gid;
|
||||
const readIndex = (to == "user" ? readUsers[id] : readChannels[id]) || 0;
|
||||
const read = self ? true : mid < readIndex ? true : false;
|
||||
const read = self ? true : mid < readIndex;
|
||||
switch (type) {
|
||||
case "normal":
|
||||
{
|
||||
batch(() => {
|
||||
dispatch(
|
||||
addMessage({
|
||||
mid,
|
||||
// 如果是自己发的消息,就是已读
|
||||
read,
|
||||
...common
|
||||
})
|
||||
);
|
||||
// 未推送完 or 不是自己发的消息
|
||||
console.log("curr state", ready, loginUid, common.from_uid);
|
||||
// if (!ready || loginUid != common.from_uid) {
|
||||
dispatch(
|
||||
appendMessage({
|
||||
id,
|
||||
mid,
|
||||
local_id: properties ? properties.local_id : null
|
||||
})
|
||||
);
|
||||
// 加到file message 列表
|
||||
if (content_type == ContentTypes.file) {
|
||||
dispatch(addFileMessage(mid));
|
||||
}
|
||||
// }
|
||||
});
|
||||
}
|
||||
case "normal": {
|
||||
batch(() => {
|
||||
// 如果是自己发的消息,就是已读
|
||||
dispatch(addMessage({ mid, read, ...common }));
|
||||
// 未推送完 or 不是自己发的消息
|
||||
console.log("curr state", ready, loginUid, common.from_uid);
|
||||
// if (!ready || loginUid != common.from_uid) {
|
||||
dispatch(
|
||||
appendMessage({
|
||||
id,
|
||||
mid,
|
||||
local_id: properties ? properties.local_id : null
|
||||
})
|
||||
);
|
||||
// 加到file message 列表
|
||||
if (content_type == ContentTypes.file) {
|
||||
dispatch(addFileMessage(mid));
|
||||
}
|
||||
// }
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "reply":
|
||||
{
|
||||
batch(() => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { updateUsersByLogs, updateUsersStatus } from "../../../app/slices/contac
|
||||
import { resetAuthData } from "../../../app/slices/auth.data";
|
||||
import chatMessageHandler from "./chat.handler";
|
||||
import store, { useAppDispatch, useAppSelector } from "../../../app/store";
|
||||
import { ServerEvent } from "../../../types/sse";
|
||||
|
||||
class RetriableError extends Error {}
|
||||
|
||||
@@ -115,7 +116,7 @@ export default function useStreaming() {
|
||||
console.log("sse debug: error message fatal");
|
||||
throw new FatalError(evt.data);
|
||||
}
|
||||
const data = JSON.parse(evt.data);
|
||||
const data: ServerEvent = JSON.parse(evt.data);
|
||||
const { type } = data;
|
||||
switch (type) {
|
||||
case "heartbeat":
|
||||
|
||||
@@ -5,7 +5,8 @@ import BASE_URL, { FILE_SLICE_SIZE } from "../../app/config";
|
||||
import { usePrepareUploadFileMutation, useUploadFileMutation } from "../../app/services/message";
|
||||
import { useAppDispatch, useAppSelector } from "../../app/store";
|
||||
|
||||
export default function useUploadFile(props = {}) {
|
||||
// todo: check props type
|
||||
export default function useUploadFile(props: { context: string; id: string } | object = {}) {
|
||||
const { context = "", id = "" } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { stageFiles, replying } = useAppSelector((store) => {
|
||||
@@ -16,7 +17,7 @@ export default function useUploadFile(props = {}) {
|
||||
});
|
||||
const [data, setData] = useState(null);
|
||||
// const [uploadingFile, setUploadingFile] = useState(false);
|
||||
const canneledRef = useRef(false);
|
||||
const canceledRef = useRef(false);
|
||||
const sliceUploadedCountRef = useRef(0);
|
||||
const totalSliceCountRef = useRef(1);
|
||||
const [prepareUploadFile, { isLoading: isPreparing, isSuccess: isPrepared }] =
|
||||
@@ -26,16 +27,18 @@ export default function useUploadFile(props = {}) {
|
||||
{ isLoading: isUploading, isSuccess: isUploaded, isError: uploadFileError }
|
||||
] = useUploadFileMutation();
|
||||
|
||||
const uploadChunk = (data) => {
|
||||
const uploadChunk = (data: { file_id: string; chunk: File; is_last: boolean }) => {
|
||||
const { file_id, chunk, is_last } = data;
|
||||
const formData = new FormData();
|
||||
formData.append("file_id", file_id);
|
||||
formData.append("chunk_data", chunk);
|
||||
formData.append("chunk_is_last", is_last);
|
||||
formData.append("chunk_is_last", `${is_last}`);
|
||||
// old code
|
||||
// formData.append("chunk_is_last", is_last);
|
||||
return uploadFileFn(formData);
|
||||
};
|
||||
|
||||
const uploadFile = async (file) => {
|
||||
const uploadFile = async (file?: File) => {
|
||||
if (!file) return;
|
||||
setData(null);
|
||||
const {
|
||||
@@ -51,7 +54,7 @@ export default function useUploadFile(props = {}) {
|
||||
console.log("file id", file_id);
|
||||
|
||||
let uploadResult = null;
|
||||
canneledRef.current = false;
|
||||
canceledRef.current = false;
|
||||
totalSliceCountRef.current = 1;
|
||||
sliceUploadedCountRef.current = 0;
|
||||
// setUploadingFile(true);
|
||||
@@ -69,7 +72,7 @@ export default function useUploadFile(props = {}) {
|
||||
|
||||
for await (const [idx] of _arr.entries()) {
|
||||
// 退出循环
|
||||
if (canneledRef.current) break;
|
||||
if (canceledRef.current) break;
|
||||
try {
|
||||
const chunk = file.slice(FILE_SLICE_SIZE * idx, FILE_SLICE_SIZE * (idx + 1), file_type);
|
||||
|
||||
@@ -109,7 +112,7 @@ export default function useUploadFile(props = {}) {
|
||||
};
|
||||
|
||||
const stopUploading = () => {
|
||||
canneledRef.current = true;
|
||||
canceledRef.current = true;
|
||||
};
|
||||
|
||||
const removeStageFile = (idx) => {
|
||||
|
||||
+7
-18
@@ -2,7 +2,7 @@ import BASE_URL, { FILE_IMAGE_SIZE, ContentTypes } from "../app/config";
|
||||
import IconPdf from "../assets/icons/file.pdf.svg";
|
||||
import IconAudio from "../assets/icons/file.audio.svg";
|
||||
import IconVideo from "../assets/icons/file.video.svg";
|
||||
import IconUnkown from "../assets/icons/file.unkown.svg";
|
||||
import IconUnknown from "../assets/icons/file.unkown.svg";
|
||||
import IconDoc from "../assets/icons/file.doc.svg";
|
||||
import IconCode from "../assets/icons/file.code.svg";
|
||||
import IconImage from "../assets/icons/file.image.svg";
|
||||
@@ -11,26 +11,14 @@ export const isImage = (file_type = "", size = 0) => {
|
||||
return file_type.startsWith("image") && size <= FILE_IMAGE_SIZE;
|
||||
};
|
||||
|
||||
export const isTreatAsImage = (file) => {
|
||||
let isImage = false;
|
||||
if (!file) return isImage;
|
||||
export const isTreatAsImage = (file: File) => {
|
||||
const { type, size } = file;
|
||||
if (type.startsWith("image")) {
|
||||
// 10MB
|
||||
return size < 1000 * 1000;
|
||||
return size < 1024 * 1024; // 10MB
|
||||
}
|
||||
return isImage;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getNonNullValues = (obj, whiteList = ["log_id"]) => {
|
||||
const tmp = {};
|
||||
Object.keys(obj).forEach((k) => {
|
||||
if (!whiteList.includes(k) && obj[k] !== null) {
|
||||
tmp[k] = obj[k];
|
||||
}
|
||||
});
|
||||
return tmp;
|
||||
};
|
||||
export function getDefaultSize(size = null, min = 480) {
|
||||
if (!size) return { width: 0, height: 0 };
|
||||
const { width: oWidth, height: oHeight } = size;
|
||||
@@ -38,7 +26,7 @@ export function getDefaultSize(size = null, min = 480) {
|
||||
const tmp = min > oWidth ? oWidth : min;
|
||||
return { width: tmp, height: tmp };
|
||||
}
|
||||
const isVertical = oWidth > oHeight ? false : true;
|
||||
const isVertical = oWidth <= oHeight;
|
||||
let dWidth = 0;
|
||||
let dHeight = 0;
|
||||
if (isVertical) {
|
||||
@@ -50,6 +38,7 @@ export function getDefaultSize(size = null, min = 480) {
|
||||
}
|
||||
return { width: dWidth, height: dHeight };
|
||||
}
|
||||
|
||||
export function formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
@@ -175,7 +164,7 @@ export const getFileIcon = (type, name = "") => {
|
||||
break;
|
||||
|
||||
default:
|
||||
icon = <IconUnkown className="icon" />;
|
||||
icon = <IconUnknown className="icon" />;
|
||||
break;
|
||||
}
|
||||
return icon;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Suspense, useEffect, lazy } from "react";
|
||||
import { Route, Routes, HashRouter } from "react-router-dom";
|
||||
import { Provider, useSelector } from "react-redux";
|
||||
import { Provider } from "react-redux";
|
||||
import toast from "react-hot-toast";
|
||||
import NotFoundPage from "./404";
|
||||
// import Welcome from './Welcome'
|
||||
@@ -25,16 +25,19 @@ import Meta from "../common/component/Meta";
|
||||
import HomePage from "./home";
|
||||
import ChatPage from "./chat";
|
||||
import Loading from "../common/component/Loading";
|
||||
import store from "../app/store";
|
||||
import store, { useAppSelector } from "../app/store";
|
||||
|
||||
const PageRoutes = () => {
|
||||
const { ui: { online }, fileMessages } = useSelector((store) => {
|
||||
const {
|
||||
ui: { online },
|
||||
fileMessages
|
||||
} = useAppSelector((store) => {
|
||||
return { ui: store.ui, fileMessages: store.fileMessage };
|
||||
});
|
||||
|
||||
// 掉线检测
|
||||
useEffect(() => {
|
||||
let toastId = '0';
|
||||
let toastId = "0";
|
||||
if (!online) {
|
||||
toast.error("Network Offline!", { duration: Infinity });
|
||||
} else {
|
||||
|
||||
+33
-24
@@ -1,77 +1,82 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, FormEvent, ChangeEvent, FC } from "react";
|
||||
import StyledWrapper from "./styled";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import toast from "react-hot-toast";
|
||||
import BASE_URL from "../../app/config";
|
||||
import { useRegisterMutation } from "../../app/services/auth";
|
||||
import { useCheckMagicTokenValidMutation } from "../../app/services/auth";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
|
||||
export default function InvitePage() {
|
||||
const { token: loginToken } = useSelector((store) => store.authData);
|
||||
interface AuthForm {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const InvitePage: FC = () => {
|
||||
const { token: loginToken } = useAppSelector((store) => store.authData);
|
||||
const [secondPwd, setSecondPwd] = useState("");
|
||||
const [samePwd, setSamePwd] = useState(true);
|
||||
const [token, setToken] = useState("");
|
||||
const [token, setToken] = useState<string | null>("");
|
||||
const [valid, setValid] = useState(false);
|
||||
const [register, { data, isLoading, isSuccess, isError, error }] = useRegisterMutation();
|
||||
const [checkToken, { data: isValid, isLoading: checkLoading, isSuccess: checkSuccess }] =
|
||||
useCheckMagicTokenValidMutation();
|
||||
|
||||
useEffect(() => {
|
||||
// console.log(search);
|
||||
const query = new URLSearchParams(location.search);
|
||||
setToken(query.get("token"));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
checkToken(token);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (checkSuccess) {
|
||||
console.log({ isValid });
|
||||
setValid(isValid);
|
||||
} else {
|
||||
setValid(false);
|
||||
}
|
||||
}, [checkSuccess, isValid]);
|
||||
|
||||
const [input, setInput] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
password: ""
|
||||
});
|
||||
const [input, setInput] = useState<AuthForm>({ name: "", email: "", password: "" });
|
||||
|
||||
const handleReg = (evt) => {
|
||||
const handleReg = (evt: FormEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
if (!samePwd) {
|
||||
toast.error("two passwords not same");
|
||||
return;
|
||||
}
|
||||
console.log("wtf", input);
|
||||
register({
|
||||
...input,
|
||||
magic_token: token,
|
||||
gender: 1
|
||||
});
|
||||
};
|
||||
const handleInput = (evt) => {
|
||||
const { type } = evt.target.dataset;
|
||||
|
||||
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const { type } = evt.target.dataset as { type: keyof AuthForm };
|
||||
const { value } = evt.target;
|
||||
console.log(type, value);
|
||||
setInput((prev) => {
|
||||
prev[type] = value;
|
||||
return { ...prev };
|
||||
});
|
||||
};
|
||||
const handleSecondPwdInput = (evt) => {
|
||||
|
||||
const handleSecondPwdInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = evt.target;
|
||||
setSecondPwd(value);
|
||||
};
|
||||
|
||||
const handlePwdCheck = () => {
|
||||
if (secondPwd) {
|
||||
setSamePwd(secondPwd == input.password);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!samePwd) {
|
||||
toast.error("two passwords not same");
|
||||
@@ -86,8 +91,7 @@ export default function InvitePage() {
|
||||
location.href = `/#/login`;
|
||||
// navigateTo("/login",);
|
||||
}, 500);
|
||||
} else if (isError) {
|
||||
console.log("register failed", error);
|
||||
} else if (isError && error && "data" in error) {
|
||||
switch (error.status) {
|
||||
case 400:
|
||||
toast.error("Register Failed: please check inputs");
|
||||
@@ -100,6 +104,8 @@ export default function InvitePage() {
|
||||
email_conflict: "email conflict",
|
||||
name_conflict: "name conflict"
|
||||
};
|
||||
// todo
|
||||
// @ts-ignore
|
||||
toast.error(`Register Failed: ${tips[error.data?.reason]}`);
|
||||
break;
|
||||
}
|
||||
@@ -112,9 +118,10 @@ export default function InvitePage() {
|
||||
|
||||
const { email, password, name } = input;
|
||||
if (loginToken) return <Navigate replace to="/" />;
|
||||
if (!token) return "token not found";
|
||||
if (checkLoading) return "checking token valid";
|
||||
if (!valid) return "invite token expires or invalid";
|
||||
if (!token) return <>token not found</>;
|
||||
if (checkLoading) return <>checking token valid</>;
|
||||
if (!valid) return <>invite token expires or invalid</>;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="form animate__animated animate__fadeInDown animate__faster">
|
||||
@@ -166,4 +173,6 @@ export default function InvitePage() {
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default InvitePage;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import "swiper/css";
|
||||
@@ -11,9 +11,14 @@ import DonePage from "./steps/donePage";
|
||||
import useServerSetup, { steps } from "./useServerSetup";
|
||||
import StyledOnboardingPage from "./styled";
|
||||
|
||||
function Navigator({ step, setStep }) {
|
||||
interface Props {
|
||||
step: string;
|
||||
setStep: (step: string) => void;
|
||||
}
|
||||
|
||||
const Navigator: FC<Props> = ({ step, setStep }) => {
|
||||
const index = steps.map((value) => value.name).indexOf(step);
|
||||
const canJumpTo = steps.find((value) => value.name === step).canJumpTo || [];
|
||||
const canJumpTo = steps.find((value) => value.name === step)?.canJumpTo || [];
|
||||
|
||||
return (
|
||||
<div className="navigator">
|
||||
@@ -41,7 +46,7 @@ function Navigator({ step, setStep }) {
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const serverSetup = useServerSetup();
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Swiper } from "swiper/types";
|
||||
|
||||
// `name` for in-code usage, `label` for display
|
||||
const steps = [
|
||||
export interface Step {
|
||||
name: string;
|
||||
label: string;
|
||||
canJumpTo?: string[];
|
||||
}
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
name: "welcomePage",
|
||||
label: "Welcome Page"
|
||||
@@ -31,12 +38,12 @@ const steps = [
|
||||
];
|
||||
|
||||
export default function useServerSetup() {
|
||||
const [swiper, setSwiper] = useState(null);
|
||||
const [swiper, setSwiper] = useState<Swiper | null>(null);
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
const [index, setIndex] = useState<number>(0);
|
||||
const step = steps[index].name;
|
||||
const setStep = useCallback(
|
||||
(name) => {
|
||||
(name: string) => {
|
||||
const newIndex = steps.map((step) => step.name).indexOf(name);
|
||||
setIndex(newIndex);
|
||||
if (swiper !== null) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Styled = styled.div`
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
/* eslint-disable no-undef */
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, FC, FormEvent, ChangeEvent } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
import toast from "react-hot-toast";
|
||||
import { setAuthData } from "../../app/slices/auth.data";
|
||||
|
||||
import Input from "../../common/component/styled/Input";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
import { useLoginMutation, useCheckMagicTokenValidMutation } from "../../app/services/auth";
|
||||
import toast from "react-hot-toast";
|
||||
import ExpiredTip from "./ExpiredTip";
|
||||
import { useRegisterMutation } from "../../app/services/auth";
|
||||
|
||||
export default function RegWithUsername() {
|
||||
const RegWithUsername: FC = () => {
|
||||
const [checkTokenInvalid, { data: isTokenValid, isLoading: checkingToken }] =
|
||||
useCheckMagicTokenValidMutation();
|
||||
const [
|
||||
@@ -28,7 +26,8 @@ export default function RegWithUsername() {
|
||||
const [username, setUsername] = useState("");
|
||||
const query = new URLSearchParams(location.search);
|
||||
// const githubCode = query.get("gcode");
|
||||
const token = query.get("magic_token");
|
||||
// todo: check if query param exists
|
||||
const token = query.get("magic_token") as string;
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
checkTokenInvalid(token);
|
||||
@@ -36,25 +35,29 @@ export default function RegWithUsername() {
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
switch (loginError?.status) {
|
||||
case 401:
|
||||
toast.error("Invalided Token");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
if (loginError && "status" in loginError) {
|
||||
switch (loginError.status) {
|
||||
case 401:
|
||||
toast.error("Invalided Token");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [loginError]);
|
||||
useEffect(() => {
|
||||
switch (regError?.status) {
|
||||
case 409:
|
||||
toast.error("Something Conflicted!");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
useEffect(() => {
|
||||
if (regError && "status" in regError) {
|
||||
switch (regError.status) {
|
||||
case 409:
|
||||
toast.error("Something Conflicted!");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [regError]);
|
||||
|
||||
useEffect(() => {
|
||||
const isSuccess = loginSuccess || regSuccess;
|
||||
const data = loginData || regData;
|
||||
@@ -67,7 +70,7 @@ export default function RegWithUsername() {
|
||||
}
|
||||
}, [loginSuccess, regSuccess, loginData, regData]);
|
||||
|
||||
const handleAuth = (evt) => {
|
||||
const handleAuth = (evt: FormEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
if (from == "reg") {
|
||||
register({
|
||||
@@ -83,13 +86,12 @@ export default function RegWithUsername() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (evt) => {
|
||||
const { value } = evt.target;
|
||||
setUsername(value);
|
||||
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
setUsername(evt.target.value);
|
||||
};
|
||||
console.log("ffff", from);
|
||||
if (!token) return "No Token";
|
||||
if (checkingToken) return "Checking Magic Link...";
|
||||
|
||||
if (!token) return <>"No Token"</>;
|
||||
if (checkingToken) return <>"Checking Magic Link..."</>;
|
||||
if (!isTokenValid) return <ExpiredTip />;
|
||||
const isLoading = loginLoading || regLoading;
|
||||
const isSuccess = loginSuccess || regSuccess;
|
||||
@@ -112,9 +114,12 @@ export default function RegWithUsername() {
|
||||
onChange={handleInput}
|
||||
/>
|
||||
<Button type="submit" disabled={isLoading || !username || isSuccess}>
|
||||
{/* todo typo */}
|
||||
{isLoading ? "Logining" : `Continue`}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RegWithUsername;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, ChangeEvent, FormEvent } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
// import { useNavigate } from "react-router-dom";
|
||||
import BASE_URL, { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
|
||||
import Input from "../../common/component/styled/Input";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
@@ -13,13 +12,19 @@ import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
|
||||
import GoogleLoginButton from "../../common/component/GoogleLoginButton";
|
||||
import GithubLoginButton from "../../common/component/GithubLoginButton";
|
||||
|
||||
interface AuthForm {
|
||||
email: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export default function Reg() {
|
||||
const [sendRegMagicLink, { isLoading: signingUp, data, isSuccess }] =
|
||||
useSendRegMagicLinkMutation();
|
||||
const [checkEmail, { isLoading: checkingEmail }] = useLazyCheckEmailQuery();
|
||||
// const navigateTo = useNavigate();
|
||||
const [magicToken, setMagicToken] = useState("");
|
||||
const [input, setInput] = useState({
|
||||
const [input, setInput] = useState<AuthForm>({
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: ""
|
||||
@@ -47,7 +52,7 @@ export default function Reg() {
|
||||
}
|
||||
}, [isSuccess, data]);
|
||||
|
||||
const handleReg = async (evt) => {
|
||||
const handleReg = async (evt: FormEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
const { email, password, confirmPassword } = input;
|
||||
if (password !== confirmPassword) {
|
||||
@@ -55,7 +60,6 @@ export default function Reg() {
|
||||
return;
|
||||
}
|
||||
const { data: canReg } = await checkEmail(email);
|
||||
console.log("can reg", canReg);
|
||||
if (canReg) {
|
||||
sendRegMagicLink({
|
||||
magic_token: magicToken,
|
||||
@@ -67,21 +71,23 @@ export default function Reg() {
|
||||
}
|
||||
// sendMagicLink(email);
|
||||
};
|
||||
|
||||
const handleCompare = () => {
|
||||
const { password, confirmPassword } = input;
|
||||
if (password !== confirmPassword) {
|
||||
toast.error("Not Same Password!");
|
||||
}
|
||||
};
|
||||
const handleInput = (evt) => {
|
||||
const { type } = evt.target.dataset;
|
||||
|
||||
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const { type } = evt.target.dataset as { type: keyof AuthForm };
|
||||
const { value } = evt.target;
|
||||
// console.log(type, value);
|
||||
setInput((prev) => {
|
||||
prev[type] = value;
|
||||
return { ...prev };
|
||||
});
|
||||
};
|
||||
|
||||
const { clientId } = useGoogleAuthConfig();
|
||||
const { config: githubAuthConfig } = useGithubAuthConfig();
|
||||
const { data: loginConfig, isSuccess: loginConfigSuccess } = useGetLoginConfigQuery();
|
||||
@@ -94,10 +100,11 @@ export default function Reg() {
|
||||
const googleLogin = enableGoogleLogin && clientId;
|
||||
// magic token 没有并且没有开放注册
|
||||
if (whoCanSignUp !== "EveryOne" && !magicToken)
|
||||
return `Sign up method is updated to Invitation Link Only`;
|
||||
return <>Sign up method is updated to Invitation Link Only</>;
|
||||
const { email, password, confirmPassword } = input;
|
||||
if (data?.mail_is_sent) return <EmailNextTip />;
|
||||
const isLoading = signingUp || checkingEmail;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tips">
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Outlet } from "react-router-dom";
|
||||
import StyledWrapper from "./styled";
|
||||
|
||||
export default function RegContainer() {
|
||||
// const isRegHome = useMatch(`/register`);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="form">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ChangeEvent, FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import iconSearch from "../../assets/icons/search.svg?url";
|
||||
|
||||
@@ -24,8 +25,14 @@ const Styled = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Search({ value = "", updateSearchValue = null, embed = false }) {
|
||||
const handleChange = (evt) => {
|
||||
interface Props {
|
||||
value?: string;
|
||||
updateSearchValue?: (value: string) => void;
|
||||
embed?: boolean;
|
||||
}
|
||||
|
||||
const Search: FC<Props> = ({ value = "", updateSearchValue = null, embed = false }) => {
|
||||
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
if (updateSearchValue) {
|
||||
updateSearchValue(evt.target.value);
|
||||
}
|
||||
@@ -36,4 +43,6 @@ export default function Search({ value = "", updateSearchValue = null, embed = f
|
||||
<input value={value} onChange={handleChange} className="search" placeholder="Search..." />
|
||||
</Styled>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Search;
|
||||
|
||||
@@ -22,8 +22,7 @@ export default function SendMagicLinkPage() {
|
||||
}, [isSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
if (error && "status" in error) {
|
||||
switch (error.status) {
|
||||
case "PARSING_ERROR":
|
||||
toast.error(error.data);
|
||||
@@ -32,7 +31,7 @@ export default function SendMagicLinkPage() {
|
||||
toast.error("Username or Password Incorrect");
|
||||
break;
|
||||
case 404:
|
||||
toast.error("Account not exsit");
|
||||
toast.error("Account not exist");
|
||||
break;
|
||||
default:
|
||||
toast.error("Something Error");
|
||||
@@ -53,7 +52,6 @@ export default function SendMagicLinkPage() {
|
||||
|
||||
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = evt.target;
|
||||
console.log(value);
|
||||
setEmail(value);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -8,7 +9,7 @@ const StyledWrapper = styled.div`
|
||||
max-width: 440px;
|
||||
padding: 32px 40px 32px 40px;
|
||||
/* border: 1px solid #eee; */
|
||||
box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
|
||||
box-shadow: 0 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
|
||||
border-radius: 12px;
|
||||
.tips {
|
||||
display: flex;
|
||||
|
||||
@@ -86,10 +86,10 @@ export default function APIConfig() {
|
||||
Are you sure to update API secret? Previous secret will be invalided
|
||||
</div>
|
||||
<div className="btns">
|
||||
<Button onClick={hideAll} className="cancel small">
|
||||
<Button onClick={() => hideAll()} className="cancel small">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isLoading} className="small danger" onClick={updateSecret}>
|
||||
<Button disabled={isLoading} className="small danger" onClick={() => updateSecret()}>
|
||||
Yes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ChangeEvent, FC, useEffect, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
// import { useNavigate } from "react-router-dom";
|
||||
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../../common/component/Modal";
|
||||
import StyledModal from "../../common/component/styled/Modal";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
import Checkbox from "../../common/component/styled/Checkbox";
|
||||
import useLogout from "../../common/hook/useLogout";
|
||||
|
||||
const StyledConfirm = styled(StyledModal)`
|
||||
.clear {
|
||||
font-weight: normal;
|
||||
@@ -26,21 +26,25 @@ const StyledConfirm = styled(StyledModal)`
|
||||
}
|
||||
}
|
||||
`;
|
||||
import Modal from "../../common/component/Modal";
|
||||
import toast from "react-hot-toast";
|
||||
export default function LogoutConfirmModal({ closeModal }) {
|
||||
|
||||
interface Props {
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
|
||||
const [clearLocal, setClearLocal] = useState(false);
|
||||
const { logout, exited, exiting, clearLocalData } = useLogout();
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
};
|
||||
const handleCheck = (evt) => {
|
||||
|
||||
const handleCheck = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
setClearLocal(evt.target.checked);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (exited) {
|
||||
if (clearLocal) {
|
||||
console.log("clear all store");
|
||||
clearLocalData();
|
||||
}
|
||||
toast.success("Logout Successfully");
|
||||
@@ -50,6 +54,7 @@ export default function LogoutConfirmModal({ closeModal }) {
|
||||
// location.reload();
|
||||
}
|
||||
}, [exited, clearLocal]);
|
||||
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledConfirm
|
||||
@@ -59,7 +64,7 @@ export default function LogoutConfirmModal({ closeModal }) {
|
||||
<>
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button onClick={handleLogout} className="danger">
|
||||
{exiting ? "Logging out" : `Log Out`}
|
||||
{exiting ? "Logging out" : "Log Out"}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -74,4 +79,6 @@ export default function LogoutConfirmModal({ closeModal }) {
|
||||
</StyledConfirm>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default LogoutConfirmModal;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ChangeEvent, FC, useEffect, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import toast from "react-hot-toast";
|
||||
import Input from "../../common/component/styled/Input";
|
||||
@@ -24,18 +23,27 @@ const StyledEdit = styled(StyledModal)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default function ProfileBasicEditModal({
|
||||
interface Props {
|
||||
label?: string;
|
||||
valueKey?: string;
|
||||
value?: string;
|
||||
title?: string;
|
||||
intro?: string;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const ProfileBasicEditModal: FC<Props> = ({
|
||||
label = "Username",
|
||||
valueKey = "name",
|
||||
value = "",
|
||||
title = "Change your username",
|
||||
intro = "Enter a new username and your existing password.",
|
||||
closeModal
|
||||
}) {
|
||||
}) => {
|
||||
const [input, setInput] = useState(value);
|
||||
// const dispatch = useDispatch();
|
||||
const [update, { isLoading, isSuccess }] = useUpdateInfoMutation();
|
||||
const handleChange = (evt) => {
|
||||
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
setInput(evt.target.value);
|
||||
};
|
||||
const handleUpdate = () => {
|
||||
@@ -69,4 +77,6 @@ export default function ProfileBasicEditModal({
|
||||
</StyledEdit>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ProfileBasicEditModal;
|
||||
|
||||
@@ -27,9 +27,15 @@ interface Props {
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
interface BaseForm {
|
||||
current: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
|
||||
const { data } = useGetCredentialsQuery();
|
||||
const [input, setInput] = useState({
|
||||
const [input, setInput] = useState<BaseForm>({
|
||||
current: "",
|
||||
newPassword: "",
|
||||
confirmPassword: ""
|
||||
@@ -37,7 +43,7 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
|
||||
// const dispatch = useDispatch();
|
||||
const [updatePassword, { isLoading, isSuccess }] = useUpdatePasswordMutation();
|
||||
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const { type } = evt.target.dataset;
|
||||
const { type } = evt.target.dataset as { type: keyof BaseForm };
|
||||
setInput((prev) => {
|
||||
return { ...prev, [type]: evt.target.value };
|
||||
});
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import Select from "../../../../common/component/styled/Select";
|
||||
import { ChangeEvent, FC, useState } from "react";
|
||||
import Select, { Option } from "../../../../common/component/styled/Select";
|
||||
import Button from "../../../../common/component/styled/Button";
|
||||
import Input from "../../../../common/component/styled/Input";
|
||||
import Toggle from "../../../../common/component/styled/Toggle";
|
||||
import options from "./items.json";
|
||||
import Styled from "./styled";
|
||||
// import IconPlus from "../../../../assets/icons/plus.circle.svg";
|
||||
import IconMinus from "../../../../assets/icons/minus.circle.svg";
|
||||
|
||||
export default function IssuerList({ issuers = [], onChange }) {
|
||||
const [select, setSelect] = useState({});
|
||||
interface Issuer {
|
||||
domain: string;
|
||||
enable: boolean;
|
||||
favicon: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
issuers: Issuer[];
|
||||
onChange: (issuers: Issuer[]) => void;
|
||||
}
|
||||
|
||||
const IssuerList: FC<Props> = ({ issuers = [], onChange }) => {
|
||||
const [select, setSelect] = useState<Partial<Option> | null>(null);
|
||||
const [newDomain, setNewDomain] = useState("");
|
||||
|
||||
const handleNewDomain = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -87,7 +97,9 @@ export default function IssuerList({ issuers = [], onChange }) {
|
||||
<Button
|
||||
disabled={disableBtn}
|
||||
onClick={() => {
|
||||
const { icon, value } = options.find((option) => option.value === select.value);
|
||||
const found = options.find((option) => option.value === select?.value);
|
||||
if (!found) return;
|
||||
const { icon, value } = found;
|
||||
onChange(
|
||||
issuers.concat({
|
||||
enable: true,
|
||||
@@ -95,7 +107,7 @@ export default function IssuerList({ issuers = [], onChange }) {
|
||||
domain: value || newDomain
|
||||
})
|
||||
);
|
||||
setSelect({});
|
||||
setSelect(null);
|
||||
setNewDomain("");
|
||||
}}
|
||||
>
|
||||
@@ -107,4 +119,6 @@ export default function IssuerList({ issuers = [], onChange }) {
|
||||
{/* <IconPlus className="add" /> */}
|
||||
</Styled>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default IssuerList;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { roundArrow } from "tippy.js";
|
||||
import "tippy.js/dist/svg-arrow.css";
|
||||
// import React from 'react'
|
||||
import styled from "styled-components";
|
||||
import IconQuestion from "../../../assets/icons/question.svg";
|
||||
|
||||
const StyledContent = styled.div`
|
||||
padding: 8px 12px;
|
||||
background: #101828;
|
||||
@@ -15,7 +16,7 @@ const StyledContent = styled.div`
|
||||
color: #55c7ec;
|
||||
}
|
||||
`;
|
||||
import IconQuestion from "../../../assets/icons/question.svg";
|
||||
|
||||
export default function Tooltip({ link = "#" }) {
|
||||
return (
|
||||
<Tippy
|
||||
|
||||
@@ -9,24 +9,23 @@ let from: string | null = null;
|
||||
export default function Setting() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navs = useNavs();
|
||||
const flatenNavs = navs
|
||||
.map(({ items }) => {
|
||||
return items;
|
||||
})
|
||||
.flat();
|
||||
const flattenNaves = navs.map(({ items }) => items).flat();
|
||||
const navKey = searchParams.get("nav");
|
||||
from = from ?? (searchParams.get("f") || "/");
|
||||
const [logoutConfirm, setLogoutConfirm] = useState(false);
|
||||
const navgateTo = useNavigate();
|
||||
const navigateTo = useNavigate();
|
||||
const close = () => {
|
||||
// dispatch(toggleSetting());
|
||||
navgateTo(from);
|
||||
// todo: check usage
|
||||
navigateTo(from!);
|
||||
from = null;
|
||||
};
|
||||
const toggleLogoutConfrim = () => {
|
||||
|
||||
const toggleLogoutConfirm = () => {
|
||||
setLogoutConfirm((prev) => !prev);
|
||||
};
|
||||
const currNav = flatenNavs.find((n) => n.name == navKey) || flatenNavs[0];
|
||||
|
||||
const currNav = flattenNaves.find((n) => n.name == navKey) || flattenNaves[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -35,11 +34,11 @@ export default function Setting() {
|
||||
closeModal={close}
|
||||
title="Settings"
|
||||
navs={navs}
|
||||
dangers={[{ title: "Log Out", handler: toggleLogoutConfrim }]}
|
||||
dangers={[{ title: "Log Out", handler: toggleLogoutConfirm }]}
|
||||
>
|
||||
{currNav.component}
|
||||
</StyledSettingContainer>
|
||||
{logoutConfirm && <LogoutConfirmModal closeModal={toggleLogoutConfrim} />}
|
||||
{logoutConfirm && <LogoutConfirmModal closeModal={toggleLogoutConfirm} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import Overview from "./Overview";
|
||||
import ManageMembers from "../../common/component/ManageMembers";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const useNavs = (channelId: number) => {
|
||||
export interface NavItem {
|
||||
name: string;
|
||||
title: string;
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
export interface Nav {
|
||||
name?: string;
|
||||
title: string;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
const useNavs = (cid: number): Nav[] => {
|
||||
return [
|
||||
{
|
||||
title: "General",
|
||||
@@ -9,12 +22,12 @@ const useNavs = (channelId: number) => {
|
||||
{
|
||||
name: "overview",
|
||||
title: "Overview",
|
||||
component: <Overview id={channelId} />
|
||||
component: <Overview id={cid} />
|
||||
},
|
||||
{
|
||||
name: "members",
|
||||
title: "Members",
|
||||
component: <ManageMembers cid={channelId} />
|
||||
component: <ManageMembers cid={cid} />
|
||||
}
|
||||
// {
|
||||
// name: "permissions",
|
||||
|
||||
@@ -27,6 +27,13 @@ export interface Channel {
|
||||
pinned_messages: PinnedMessage[];
|
||||
}
|
||||
|
||||
export interface CreateChannelDTO {
|
||||
name: string;
|
||||
description: string;
|
||||
members?: number[];
|
||||
is_public: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateChannelDTO {
|
||||
operation: "add_member" | "remove_member";
|
||||
members?: number[];
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
import { User } from "./auth";
|
||||
|
||||
export interface Server {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
export interface GithubAuthConfig {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
client_secret: string; // todo: check security problem!
|
||||
}
|
||||
export interface FirebaseConfig {
|
||||
enabled: boolean;
|
||||
@@ -56,6 +57,6 @@ export interface LoginConfig {
|
||||
metamask: boolean;
|
||||
third_party: boolean;
|
||||
}
|
||||
export interface NewAdminDTO extends Pick<User, "email" | "name" | "gender"> {
|
||||
export interface CreateAdminDTO extends Pick<User, "email" | "name" | "gender"> {
|
||||
password: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user