feat: lots of updates

This commit is contained in:
zerosoul
2022-02-10 23:35:25 +08:00
parent e8b6c2b0f1
commit d05c5ff8f2
33 changed files with 1072 additions and 331 deletions
+98 -95
View File
@@ -1,97 +1,100 @@
{
"name": "rustchat-web",
"version": "0.1.0",
"private": true,
"homepage": "http://privoce.rustchat.com",
"dependencies": {
"@babel/core": "^7.17.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@reduxjs/toolkit": "^1.7.2",
"@rtk-query/codegen-openapi": "^1.0.0-alpha.1",
"@svgr/webpack": "^6.2.1",
"axios": "^0.25.0",
"babel-loader": "^8.2.3",
"babel-plugin-named-asset-import": "^0.3.8",
"babel-preset-react-app": "^10.0.1",
"bfj": "^7.0.2",
"browserslist": "^4.19.1",
"camelcase": "^6.3.0",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"css-loader": "^6.6.0",
"css-minimizer-webpack-plugin": "^3.4.1",
"dayjs": "^1.10.7",
"dotenv": "^16.0.0",
"dotenv-expand": "^8.0.1",
"emoji-mart": "^3.0.1",
"eslint": "^8.8.0",
"event-source-polyfill": "^1.0.25",
"file-loader": "^6.2.0",
"fs-extra": "^10.0.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.5.3",
"react": "^17.0.2",
"react-dev-utils": "^12.0.0",
"react-dom": "^17.0.2",
"react-hot-toast": "^2.2.0",
"react-icons": "^4.3.1",
"react-redux": "^7.2.6",
"react-refresh": "^0.11.0",
"react-router-dom": "6",
"react-textarea-autosize": "^8.3.3",
"redux-persist": "^6.0.0",
"resolve": "^1.22.0",
"rooks": "^5.10.0",
"semver": "^7.3.5",
"source-map-loader": "^3.0.1",
"style-loader": "^3.3.1",
"styled-components": "^5.3.3",
"styled-reset": "^4.3.4",
"terser-webpack-plugin": "^5.3.1",
"webpack": "^5.68.0",
"webpack-dev-server": "^4.7.4",
"webpack-manifest-plugin": "^4.1.1",
"workbox-webpack-plugin": "^6.4.2"
},
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"deploy": "yarn build && gh-pages -d build",
"lint": "lint-staged",
"prepare": "husky install"
},
"browserslist": {
"production": [
"last 5 chrome version",
"last 5 firefox version",
"last 5 safari version"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"babel": {
"presets": [
"react-app"
]
},
"devDependencies": {
"@commitlint/cli": "^16.1.0",
"@commitlint/config-conventional": "^16.0.0",
"autoprefixer": "^10.4.2",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"gh-pages": "^3.2.3",
"husky": "^7.0.4",
"lint-staged": "^12.3.3",
"postcss": "^8.4.6",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^6.2.1",
"postcss-preset-env": "^7.3.1",
"prettier": "^2.5.1"
}
"name": "rustchat-web",
"version": "0.1.0",
"private": true,
"homepage": "http://privoce.rustchat.com",
"dependencies": {
"@babel/core": "^7.17.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@reduxjs/toolkit": "^1.7.2",
"@rtk-query/codegen-openapi": "^1.0.0-alpha.1",
"@svgr/webpack": "^6.2.1",
"animate.css": "^4.1.1",
"axios": "^0.25.0",
"babel-loader": "^8.2.3",
"babel-plugin-named-asset-import": "^0.3.8",
"babel-preset-react-app": "^10.0.1",
"bfj": "^7.0.2",
"browserslist": "^4.19.1",
"camelcase": "^6.3.0",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"css-loader": "^6.6.0",
"css-minimizer-webpack-plugin": "^3.4.1",
"dayjs": "^1.10.7",
"dotenv": "^16.0.0",
"dotenv-expand": "^8.0.1",
"emoji-mart": "^3.0.1",
"eslint": "^8.8.0",
"event-source-polyfill": "^1.0.25",
"file-loader": "^6.2.0",
"fs-extra": "^10.0.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.5.3",
"react": "^17.0.2",
"react-dev-utils": "^12.0.0",
"react-dnd": "^15.1.1",
"react-dnd-html5-backend": "^15.1.2",
"react-dom": "^17.0.2",
"react-hot-toast": "^2.2.0",
"react-icons": "^4.3.1",
"react-redux": "^7.2.6",
"react-refresh": "^0.11.0",
"react-router-dom": "6",
"react-textarea-autosize": "^8.3.3",
"redux-persist": "^6.0.0",
"resolve": "^1.22.0",
"rooks": "^5.10.0",
"semver": "^7.3.5",
"source-map-loader": "^3.0.1",
"style-loader": "^3.3.1",
"styled-components": "^5.3.3",
"styled-reset": "^4.3.4",
"terser-webpack-plugin": "^5.3.1",
"webpack": "^5.68.0",
"webpack-dev-server": "^4.7.4",
"webpack-manifest-plugin": "^4.1.1",
"workbox-webpack-plugin": "^6.4.2"
},
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"deploy": "yarn build && gh-pages -d build",
"lint": "lint-staged",
"prepare": "husky install"
},
"browserslist": {
"production": [
"last 5 chrome version",
"last 5 firefox version",
"last 5 safari version"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"babel": {
"presets": [
"react-app"
]
},
"devDependencies": {
"@commitlint/cli": "^16.1.0",
"@commitlint/config-conventional": "^16.0.0",
"autoprefixer": "^10.4.2",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"gh-pages": "^3.2.3",
"husky": "^7.0.4",
"lint-staged": "^12.3.3",
"postcss": "^8.4.6",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^6.2.1",
"postcss-preset-env": "^7.3.1",
"prettier": "^2.5.1"
}
}
+6 -1
View File
@@ -1,5 +1,10 @@
// const BASE_URL = `${location.origin}/api`;
const BASE_URL = `http://rustchat.com:3000/api`;
export const ContentTypes = {
text: "text/plain",
image: "image/png",
json: "application/json",
};
export const tokenHeader = "X-API-Key";
export default BASE_URL;
+1 -1
View File
@@ -8,7 +8,7 @@ export const authApi = createApi({
query: (credentials) => ({
url: "token/login",
method: "POST",
body: credentials,
body: { credential: credentials, device: "web", device_token: "test" },
}),
// transformResponse: (resp) => {
// console.log("resp", resp);
+38 -11
View File
@@ -1,4 +1,5 @@
import { fetchBaseQuery } from "@reduxjs/toolkit/query";
import toast from "react-hot-toast";
import BASE_URL, { tokenHeader } from "../config";
const whiteList = ["login"];
const baseQuery = fetchBaseQuery({
@@ -9,22 +10,48 @@ const baseQuery = fetchBaseQuery({
if (token && !whiteList.includes(endpoint)) {
headers.set(tokenHeader, token);
}
// 发送channel msg (临时举措)
// if (endpoint == "sendChannelMsg") {
// headers.set("Content-Type", "text/plain");
// }
return headers;
},
});
const baseQueryWithReauth = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions);
if (result.error && result.error.status === 401) {
// try to get a new token with refreshToken
// const refreshResult = await baseQuery('/refreshToken', api, extraOptions);
// if (refreshResult.data) {
// // store the new token
// api.dispatch(tokenReceived(refreshResult.data));
// // retry the initial query
// result = await baseQuery(args, api, extraOptions);
// } else {
// api.dispatch(loggedOut());
// }
if (result.error) {
console.log("api error", result.error);
switch (result.error.originalStatus) {
case 404:
{
toast.error("Request Not Found");
}
break;
case 500:
{
toast.error(result.error.data || "server error");
}
break;
case 401:
{
toast.error("token expired, please login again");
// try to get a new token with refreshToken
// const refreshResult = await baseQuery('/refreshToken', api, extraOptions);
// if (refreshResult.data) {
// // store the new token
// api.dispatch(tokenReceived(refreshResult.data));
// // retry the initial query
// result = await baseQuery(args, api, extraOptions);
// } else {
// api.dispatch(loggedOut());
// }
}
break;
default:
break;
}
}
return result;
};
+8 -3
View File
@@ -1,6 +1,8 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import baseQuery from "./base.query";
import { REHYDRATE } from "redux-persist";
import { ContentTypes } from "../config";
export const channelApi = createApi({
reducerPath: "channel",
baseQuery,
@@ -22,10 +24,13 @@ export const channelApi = createApi({
}),
}),
sendChannelMsg: builder.mutation({
query: ({ gid, message }) => ({
url: `group/${gid}/send`,
query: ({ id, content, type = "text" }) => ({
headers: {
"content-type": ContentTypes[type],
},
url: `group/${id}/send`,
method: "POST",
body: message,
body: content,
}),
}),
}),
+8 -4
View File
@@ -1,7 +1,8 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import baseQuery from "./base.query";
import BASE_URL from "../config";
import BASE_URL, { ContentTypes } from "../config";
import { REHYDRATE } from "redux-persist";
export const contactApi = createApi({
reducerPath: "contact",
baseQuery,
@@ -22,10 +23,13 @@ export const contactApi = createApi({
},
}),
sendMsg: builder.mutation({
query: ({ uid, message }) => ({
url: `user/${uid}/send`,
query: ({ id, content, type = "text" }) => ({
headers: {
"content-type": ContentTypes[type],
},
url: `user/${id}/send`,
method: "POST",
body: message,
body: content,
}),
}),
}),
+11 -1
View File
@@ -2,7 +2,8 @@ import { createSlice } from "@reduxjs/toolkit";
const initialState = {
user: null,
usersVersion: 0,
usersVersion: null,
afterMid: null,
token: null,
refreshToken: null,
};
@@ -21,16 +22,25 @@ const authDataSlice = createSlice({
state.user = null;
state.token = null;
state.refreshToken = null;
// 清掉本地缓存auth data
// localStorage.removeItem("AUTH_DATA");
// state.afterMid = null;
// state.usersVersion = null;
},
setUsersVersion(state, action) {
const { version } = action.payload;
state.usersVersion = version;
},
setAfterMid(state, action) {
const { mid } = action.payload;
state.afterMid = mid;
},
},
});
export const {
setAuthData,
clearAuthData,
setUsersVersion,
setAfterMid,
} = authDataSlice.actions;
export default authDataSlice.reducer;
+9 -9
View File
@@ -1,18 +1,18 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {};
const channelsSlice = createSlice({
name: "channels",
name: `channels`,
initialState,
reducers: {
setChannels(state, action) {
// console.log("set channels store", action);
const chs = action.payload;
chs.forEach((c) => {
const { gid, ...rest } = c;
console.log("wtf", gid, rest);
state[gid] = rest;
});
console.log("set channels store", state);
const chs = action.payload || [];
return Object.fromEntries(
chs.map((c) => {
const { gid, ...rest } = c;
return [gid, rest];
})
);
},
addChannel(state, action) {
// console.log("set channels store", action);
+3 -1
View File
@@ -5,6 +5,7 @@ const initialState = {
// accessLogs: {},
pendingMsgs: [],
};
const channelMsgSlice = createSlice({
name: "channelMessage",
initialState,
@@ -16,9 +17,10 @@ const channelMsgSlice = createSlice({
created_at,
mid,
from_uid,
content_type,
unread = true,
} = action.payload;
const newMsg = { content, created_at, from_uid, unread };
const newMsg = { content, content_type, created_at, from_uid, unread };
if (state[id]) {
let replaceMsg = state[id][mid];
// 如果存在,并且新消息和缓存消息不一样,则替换掉,并且改为已读(可能有问题)
+2 -1
View File
@@ -10,12 +10,13 @@ const userMsgSlice = createSlice({
const {
id,
content,
content_type,
created_at,
mid,
from_uid,
unread = true,
} = action.payload;
const newMsg = { content, created_at, from_uid, unread };
const newMsg = { content, content_type, created_at, from_uid, unread };
if (state[id]) {
let replaceMsg = state[id][mid];
// 如果存在,并且新消息和缓存消息不一样,则替换掉
+18 -36
View File
@@ -1,15 +1,5 @@
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import storage from "redux-persist/lib/storage";
import {
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from "redux-persist";
import authDataReducer from "./slices/auth.data";
import uiReducer from "./slices/ui";
import channelsReducer from "./slices/channels";
@@ -19,14 +9,9 @@ import { authApi } from "./services/auth";
import { contactApi } from "./services/contact";
import { channelApi } from "./services/channel";
import { serverApi } from "./services/server";
const persistConfig = {
key: "root",
version: 1,
storage,
};
const persistedReducer = persistReducer(
persistConfig,
combineReducers({
const getStore = () => {
const reducer = combineReducers({
ui: uiReducer,
channels: channelsReducer,
userMsg: userMsgReducer,
@@ -36,21 +21,18 @@ const persistedReducer = persistReducer(
[contactApi.reducerPath]: contactApi.reducer,
[channelApi.reducerPath]: channelApi.reducer,
[serverApi.reducerPath]: serverApi.reducer,
})
);
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(
authApi.middleware,
contactApi.middleware,
channelApi.middleware,
serverApi.middleware
),
});
setupListeners(store.dispatch);
export default store;
});
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
authApi.middleware,
contactApi.middleware,
channelApi.middleware,
serverApi.middleware
),
});
setupListeners(store.dispatch);
return store;
};
export default getStore;
+56
View File
@@ -0,0 +1,56 @@
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import storage from "redux-persist/lib/storage";
import {
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from "redux-persist";
import authDataReducer from "./slices/auth.data";
import uiReducer from "./slices/ui";
import channelsReducer from "./slices/channels";
import channelMsgReducer from "./slices/message.channel";
import userMsgReducer from "./slices/message.user";
import { authApi } from "./services/auth";
import { contactApi } from "./services/contact";
import { channelApi } from "./services/channel";
import { serverApi } from "./services/server";
const persistConfig = {
key: "root",
version: 1,
storage,
};
const persistedReducer = persistReducer(
persistConfig,
combineReducers({
ui: uiReducer,
channels: channelsReducer,
userMsg: userMsgReducer,
channelMsg: channelMsgReducer,
authData: authDataReducer,
[authApi.reducerPath]: authApi.reducer,
[contactApi.reducerPath]: contactApi.reducer,
[channelApi.reducerPath]: channelApi.reducer,
[serverApi.reducerPath]: serverApi.reducer,
})
);
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(
authApi.middleware,
contactApi.middleware,
channelApi.middleware,
serverApi.middleware
),
});
setupListeners(store.dispatch);
export default store;
+5 -1
View File
@@ -1,16 +1,19 @@
import { useState, useEffect } from "react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useSelector, useDispatch } from "react-redux";
import Modal from "../Modal";
import ChannelIcon from "../ChannelIcon";
import Contact from "../Contact";
import StyledWrapper from "./styled";
import useFilteredUsers from "../../hook/useFilteredUsers";
import { addChannel } from "../../../app/slices/channels";
import { useCreateChannelMutation } from "../../../app/services/channel";
export default function ChannelModal({ personal = false, closeModal }) {
const navigateTo = useNavigate();
const dispatch = useDispatch();
const [data, setData] = useState({
name: "",
dsecription: "",
@@ -47,6 +50,7 @@ export default function ChannelModal({ personal = false, closeModal }) {
if (isSuccess) {
toast.success("create new channel success");
closeModal();
dispatch(addChannel(newChannel));
navigateTo(`/chat/channel/${newChannel.gid}`);
}
}, [isSuccess, newChannel]);
+42 -10
View File
@@ -1,9 +1,10 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import styled from "styled-components";
import dayjs from "dayjs";
import { useDispatch } from "react-redux";
import { useInViewRef } from "rooks";
import Avatar from "./Avatar";
import BASE_URL from "../../app/config";
import { useGetContactsQuery } from "../../app/services/contact";
import { setChannelMsgRead } from "../../app/slices/message.channel";
import { setUserMsgRead } from "../../app/slices/message.user";
@@ -50,9 +51,32 @@ const StyledMsg = styled.div`
&.pending {
opacity: 0.5;
}
.img {
max-width: 400px;
}
}
}
`;
const renderContent = (type, content) => {
let ctn = null;
switch (type) {
case "text/plain":
ctn = content;
break;
case "image/jpeg":
ctn = (
<img
className="img"
src={`${BASE_URL}/resource/image?id=${encodeURIComponent(content)}`}
/>
);
break;
default:
break;
}
return ctn;
};
export default function Message({
gid = "",
mid = "",
@@ -60,28 +84,34 @@ export default function Message({
fromUid,
time,
content,
content_type = "text/plain",
unread = false,
pending,
}) {
const [myRef, inView] = useInViewRef();
const disptach = useDispatch();
// const wrapperRef = useRef(null);
const avatarRef = useRef(null);
const { data: contacts } = useGetContactsQuery();
useEffect(() => {
// if (wrapperRef) {
// wrapperRef.current.scrollIntoView();
if (inView && unread) {
const setMsgRead = gid ? setChannelMsgRead : setUserMsgRead;
disptach(setMsgRead({ id: gid || uid, mid }));
if (!unread) {
avatarRef.current?.scrollIntoView(false);
}
}, [unread]);
useEffect(() => {
if (inView) {
if (unread) {
const setMsgRead = gid ? setChannelMsgRead : setUserMsgRead;
disptach(setMsgRead({ id: gid || uid, mid }));
}
}
// }
}, [gid, mid, uid, unread, inView]);
if (!contacts) return null;
const currUser = contacts.find((c) => c.uid == fromUid) || {};
return (
<StyledMsg ref={myRef}>
<div className="avatar" data-uid={uid}>
<div className="avatar" data-uid={uid} ref={avatarRef}>
<Avatar url={currUser.avatar} id={fromUid} name={currUser.name} />
</div>
<div className="details">
@@ -89,7 +119,9 @@ export default function Message({
<span className="name">{currUser.name}</span>
<i className="time">{dayjs(time).format("YYYY-MM-DD h:mm:ss A")}</i>
</div>
<div className={`down ${pending ? "pending" : ""}`}>{content}</div>
<div className={`down ${pending ? "pending" : ""}`}>
{renderContent(content_type, content)}
</div>
</div>
</StyledMsg>
);
+25 -5
View File
@@ -10,19 +10,35 @@ import {
addChannel,
deleteChannel,
} from "../../app/slices/channels";
import { clearAuthData, setUsersVersion } from "../../app/slices/auth.data";
import {
clearAuthData,
setUsersVersion,
setAfterMid,
} from "../../app/slices/auth.data";
import { addChannelMsg } from "../../app/slices/message.channel";
import { addUserMsg } from "../../app/slices/message.user";
const NotificationHub = ({ token, usersVersion = 0 }) => {
const getQueryString = (params = {}) => {
const sp = new URLSearchParams();
Object.entries(params).forEach(([key, val]) => {
if (val) {
sp.append(key, val);
}
});
return sp.toString();
};
const NotificationHub = ({ token, usersVersion = 0, afterMid = 0 }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
let sse = null;
if (token) {
sse = new EventSource(
`${BASE_URL}/user/events?api-key=${token}&users_version=${usersVersion}`
`${BASE_URL}/user/events?${getQueryString({
"api-key": token,
users_version: usersVersion,
after_mid: afterMid,
})}`
);
sse.onopen = () => {
console.info("sse opened");
@@ -40,7 +56,7 @@ const NotificationHub = ({ token, usersVersion = 0 }) => {
sse.close();
}
};
}, [token]);
}, [token, usersVersion, afterMid]);
const handleSSEMessage = (data) => {
const { type } = data;
@@ -89,11 +105,15 @@ const NotificationHub = ({ token, usersVersion = 0 }) => {
case "chat":
// console.log("chat data", data);
if (data.gid) {
// channel msg
const { gid, ...rest } = data;
dispatch(addChannelMsg({ id: gid, ...rest }));
} else {
// user msg
dispatch(addUserMsg({ id: data.from_uid, ...data }));
}
// 更新after_mid
dispatch(setAfterMid({ mid: data.mid }));
break;
default:
@@ -0,0 +1,119 @@
import { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
// import toast from "react-hot-toast";
// import { useNavigate } from "react-router-dom";
// import { useSelector, useDispatch } from "react-redux";
import { useSendChannelMsgMutation } from "../../../../app/services/channel";
import { useSendMsgMutation } from "../../../../app/services/contact";
import { addChannelMsg } from "../../../../app/slices/message.channel";
import { addUserMsg } from "../../../../app/slices/message.user";
import Modal from "../../Modal";
import StyledWrapper from "./styled";
export default function UploadModal({
type = "user",
sendTo = 0,
files = [],
closeModal,
}) {
const dispatch = useDispatch();
const [
sendChannelMsg,
{
error: sendChannelError,
isLoading: channelSending,
isSuccess: sendChannelSuccess,
data: sendChannelData,
},
] = useSendChannelMsgMutation();
const [
sendUserMsg,
{
error: sendUserError,
isLoading: userSending,
isSuccess: sendUserSuccess,
data: sendUserData,
},
] = useSendMsgMutation();
const [blobs, setBlobs] = useState([]);
useEffect(() => {
files.forEach((file) => {
var fileReader = new FileReader();
fileReader.onloadend = (e) => {
const { name, size, type } = file;
const obj = { name, size, type };
let dataUrl = e.target.result;
console.log({ dataUrl }, e.target);
// let blob = new Blob([arrayBuffer]);
obj.data = dataUrl;
setBlobs((prevs) => {
return [...prevs, obj];
});
};
fileReader.readAsDataURL(file);
});
}, [files]);
const handleUpload = () => {
const uploadFn = type == "user" ? sendUserMsg : sendChannelMsg;
uploadFn({ id: sendTo, content: files[0], type: "image" });
};
useEffect(() => {
if (sendUserSuccess) {
dispatch(addUserMsg({ id: sendTo, ...sendUserData, unread: false }));
closeModal();
}
}, [sendUserSuccess, sendUserData]);
useEffect(() => {
if (sendChannelSuccess) {
const { gid, ...rest } = sendChannelData;
dispatch(addChannelMsg({ id: gid, ...rest, unread: false }));
closeModal();
}
}, [sendChannelSuccess, sendChannelData]);
if (!sendTo) return null;
return (
<Modal>
<StyledWrapper className="animate__animated animate__fadeInDown animate__faster">
<h3 className="head">Upload a file</h3>
<p className="intro">Photos accept jpg, png, max size limit to 10M.</p>
<ul className="list">
{blobs.map((b, idx) => {
console.log({ b });
return (
<li key={idx} className="item">
<img
src={b.data}
// src={URL.createObjectURL(b.data)}
alt="thumb"
className="thumb"
/>
<div className="right">
<div className="name">
<span contentEditable className="input">
{b.name}
</span>
<i className="tip">(click title to change name)</i>
</div>
<i className="size">{`${Math.floor(b.size / 1000)}KB`}</i>
</div>
</li>
);
})}
</ul>
<div className="btns">
<button className="btn cancel" onClick={closeModal}>
Cancel
</button>
<button
className="btn upload"
disabled={channelSending || userSending}
onClick={handleUpload}
>
{channelSending || userSending ? "Uploading" : `Upload`}
</button>
</div>
</StyledWrapper>
</Modal>
);
}
@@ -0,0 +1,96 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
background-color: #fff;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 8px;
.head {
font-style: normal;
font-weight: 600;
font-size: 20px;
line-height: 30px;
margin-bottom: 8px;
}
.intro {
font-style: normal;
font-weight: normal;
font-size: 14px;
line-height: 20px;
color: #6b7280;
}
.list {
padding-top: 32px;
display: flex;
flex-direction: column;
gap: 8px;
.item {
padding: 16px;
border: 1px solid rgba(116, 127, 141, 0.2);
border-radius: 6px;
display: flex;
align-items: center;
gap: 16px;
min-width: 473px;
.thumb {
object-fit: cover;
width: 64px;
height: 64px;
border-radius: 6px;
}
.right {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
.name {
font-style: normal;
.input {
user-select: text;
line-height: 20px;
font-size: 14px;
font-weight: 600;
margin-right: 8px;
}
.tip {
line-height: 18px;
font-size: 12px;
color: #78787c;
}
}
.size {
font-size: 14px;
line-height: 20px;
color: #616161;
}
}
}
}
.btns {
padding-top: 32px;
padding-bottom: 16px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
width: 100%;
.btn {
color: #fff;
padding: 8px 16px;
background: #1fe1f9;
/* shadow-base */
box-shadow: 0px 1px 2px rgba(31, 41, 55, 0.08);
border-radius: 4px;
&.cancel {
color: #333;
border: 1px solid #e5e7eb;
background-color: #fff;
}
}
}
`;
export default StyledWrapper;
+71 -33
View File
@@ -10,18 +10,32 @@ import { useSendMsgMutation } from "../../../app/services/contact";
import { addChannelMsg } from "../../../app/slices/message.channel";
import { addUserMsg } from "../../../app/slices/message.user";
import StyledSend from "./styled";
import UploadModal from "./UploadModal";
const Types = {
channel: "#",
user: "@",
};
export default function Send({ name, type = "channel", id = "" }) {
export default function Send({
name,
type = "channel",
id = "",
dragFiles = [],
}) {
const [files, setFiles] = useState([]);
const inputRef = useRef();
const [emojiPicker, setEmojiPicker] = useState(false);
const [shift, setShift] = useState(false);
const [enter, setEnter] = useState(false);
const [msg, setMsg] = useState("");
const dispatch = useDispatch();
console.log("send drag files", dragFiles);
useEffect(() => {
if (dragFiles.length) {
setFiles((prev) => [...prev, ...dragFiles]);
}
}, [dragFiles]);
const toggleEmojiPicker = () => {
setEmojiPicker((prev) => !prev);
};
@@ -79,15 +93,20 @@ export default function Send({ name, type = "channel", id = "" }) {
useEffect(() => {
inputRef.current.focus();
}, [msg]);
const handleUpload = (evt) => {
setFiles([...evt.target.files]);
};
const resetFiles = () => {
setFiles([]);
};
const handleSendMessage = () => {
if (!msg || !type || !id) return;
switch (type) {
case "channel":
sendChannelMsg({ gid: id, message: msg });
sendChannelMsg({ id, content: msg });
break;
case "user":
sendMsg({ uid: id, message: msg });
sendMsg({ id, content: msg });
break;
default:
@@ -95,35 +114,54 @@ export default function Send({ name, type = "channel", id = "" }) {
}
};
return (
<StyledSend className="send">
<MdAdd className="addon" size={20} color="#78787C" />
<div className="input">
<TextareaAutosize
// autoFocus
ref={inputRef}
className="content"
maxRows={8}
minRows={1}
onKeyDown={handleInputKeydown}
onChange={handleMsgChange}
value={msg}
placeholder={`${Types[type]}${name} 发消息`}
<>
<StyledSend className="send">
<div className="addon">
<MdAdd size={20} color="#78787C" />
<input
multiple={true}
onChange={handleUpload}
type="file"
name="file"
id="file"
/>
</div>
<div className="input">
<TextareaAutosize
// autoFocus
ref={inputRef}
className="content"
maxRows={8}
minRows={1}
onKeyDown={handleInputKeydown}
onChange={handleMsgChange}
value={msg}
placeholder={`${Types[type]}${name} 发消息`}
/>
</div>
<div className="emoji">
<button className="toggle" onClick={toggleEmojiPicker}>
😄
</button>
{emojiPicker && (
<div className="picker">
<Picker
onSelect={handleEmojiSelect}
showPreview={false}
showSkinTones={false}
/>
</div>
)}
</div>
</StyledSend>
{files.length !== 0 && (
<UploadModal
type={type}
files={files}
sendTo={id}
closeModal={resetFiles}
/>
</div>
<div className="emoji">
<button className="toggle" onClick={toggleEmojiPicker}>
😄
</button>
{emojiPicker && (
<div className="picker">
<Picker
onSelect={handleEmojiSelect}
showPreview={false}
showSkinTones={false}
/>
</div>
)}
</div>
</StyledSend>
)}
</>
);
}
+10
View File
@@ -16,6 +16,16 @@ const StyledSend = styled.div`
/* margin: 0 16px; */
.addon {
cursor: pointer;
position: relative;
input {
opacity: 0;
cursor: pointer;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
.input {
width: 100%;
+9 -44
View File
@@ -1,56 +1,21 @@
// import React from 'react';
import ReactDOM from "react-dom";
import { Toaster } from "react-hot-toast";
import { Route, Routes, HashRouter } from "react-router-dom";
import { Reset } from "styled-reset";
import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";
import { Provider } from "react-redux";
import store from "./app/store";
import RequireAuth from "./common/component/RequireAuth";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import "animate.css";
import ReduxRoutes from "./routes";
// import Welcome from './routes/Welcome'
import NotFoundPage from "./routes/404";
import LoginPage from "./routes/login";
import HomePage from "./routes/home";
import ChatPage from "./routes/chat";
import ContactsPage from "./routes/contacts";
let persistor = persistStore(store);
ReactDOM.render(
<>
<Toaster />
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<HashRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/"
element={
<RequireAuth>
<HomePage />
</RequireAuth>
}
>
<Route index element={<ChatPage />} />
<Route path="chat">
<Route index element={<ChatPage />} />
<Route path="channel/:channel_id" element={<ChatPage />} />
<Route path="dm/:user_id" element={<ChatPage />} />
</Route>
<Route path="contacts">
<Route index element={<ContactsPage />} />
<Route path=":user_id" element={<ContactsPage />} />
</Route>
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</HashRouter>
</PersistGate>
</Provider>
<Reset />
<Toaster />
<DndProvider backend={HTML5Backend}>
<ReduxRoutes />
</DndProvider>
</>,
document.getElementById("root")
);
+25 -4
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import dayjs from "dayjs";
@@ -20,7 +20,14 @@ import {
StyledHeader,
} from "./styled";
export default function ChannelChat({ cid = "", unreads = 0, data = {} }) {
export default function ChannelChat({
cid = "",
unreads = 0,
data = {},
dropFiles = [],
}) {
// const containerRef = useRef(null);
const [dragFiles, setDragFiles] = useState([]);
const dispatch = useDispatch();
const msgs = useSelector((store) => {
return store.channelMsg[cid] || {};
@@ -29,6 +36,11 @@ export default function ChannelChat({ cid = "", unreads = 0, data = {} }) {
const handleClearUnreads = () => {
dispatch(clearChannelMsgUnread(cid));
};
useEffect(() => {
if (dropFiles.length) {
setDragFiles(dropFiles);
}
}, [dropFiles]);
useEffect(() => {
console.log({ cid });
return () => {
@@ -45,6 +57,8 @@ export default function ChannelChat({ cid = "", unreads = 0, data = {} }) {
console.log("channel message list", msgs);
return (
<Layout
setDragFiles={setDragFiles}
// ref={containerRef}
header={
<StyledHeader>
<div className="txt">
@@ -73,9 +87,16 @@ export default function ChannelChat({ cid = "", unreads = 0, data = {} }) {
<div className="chat">
{Object.entries(msgs).map(([mid, msg]) => {
if (!msg) return null;
const { from_uid, content, created_at, unread } = msg;
const {
from_uid,
content,
content_type,
created_at,
unread,
} = msg;
return (
<Message
content_type={content_type}
unread={unread}
gid={cid}
mid={mid}
@@ -89,7 +110,7 @@ export default function ChannelChat({ cid = "", unreads = 0, data = {} }) {
</div>
</div>
<Send id={cid} type="channel" name={name} />
<Send dragFiles={dragFiles} id={cid} type="channel" name={name} />
<div className="placeholder"></div>
</StyledChannelChat>
{unreads != 0 && (
+52
View File
@@ -0,0 +1,52 @@
// import React from 'react'
import { NavLink, useNavigate } from "react-router-dom";
import { useDrop } from "react-dnd";
import { NativeTypes } from "react-dnd-html5-backend";
import ChannelIcon from "../../common/component/ChannelIcon";
const NavItem = ({ data, setFiles }) => {
const navigate = useNavigate();
const [{ isActive }, drop] = useDrop(() => ({
accept: [NativeTypes.FILE],
drop({ dataTransfer }) {
if (dataTransfer.files.length) {
// console.log(files, rest);
setFiles([...dataTransfer.files]);
navigate(`/chat/channel/${data.id}`);
// 重置
setTimeout(() => {
setFiles([]);
}, 300);
}
},
collect: (monitor) => ({
isActive: monitor.canDrop() && monitor.isOver(),
}),
}));
const { id, is_public, name, description, unreads } = data;
return (
<NavLink
ref={drop}
title={description}
key={id}
className={`link ${isActive ? "drop_over" : ""}`}
to={`/chat/channel/${id}`}
>
<span className="txt">
<ChannelIcon personal={!is_public} />
{name}
</span>
{unreads > 0 && <i className="badge">{unreads}</i>}
</NavLink>
);
};
export default function ChannelList({ channels, setDropFiles }) {
return channels.map(({ id, is_public, name, description, unreads }) => {
return (
<NavItem
key={id}
data={{ id, is_public, name, description, unreads }}
setFiles={setDropFiles}
/>
);
});
}
+18 -3
View File
@@ -8,7 +8,9 @@ import Layout from "../Layout";
import { StyledHeader, StyledDMChat } from "./styled";
export default function DMChat({ uid = "" }) {
export default function DMChat({ uid = "", dropFiles = [] }) {
console.log("dm files", dropFiles);
const [dragFiles, setDragFiles] = useState([]);
const msgs = useSelector((store) => {
return store.userMsg[uid] || {};
});
@@ -20,10 +22,17 @@ export default function DMChat({ uid = "" }) {
setCurrUser(contacts.find((c) => c.uid == uid));
}
}, [uid, contacts]);
useEffect(() => {
if (dropFiles.length) {
setDragFiles(dropFiles);
}
}, [dropFiles]);
if (!currUser) return null;
console.log("user msgs", msgs);
return (
<Layout
setDragFiles={setDragFiles}
header={
<StyledHeader>
<Contact interactive={false} uid={currUser.uid} />
@@ -35,9 +44,10 @@ export default function DMChat({ uid = "" }) {
{Object.entries(msgs).map(([mid, msg]) => {
if (!msg) return null;
console.log("user msg", msg);
const { from_uid, content, created_at, unread } = msg;
const { from_uid, content, content_type, created_at, unread } = msg;
return (
<Message
content_type={content_type}
unread={unread}
fromUid={from_uid}
mid={mid}
@@ -51,7 +61,12 @@ export default function DMChat({ uid = "" }) {
</div>
</StyledDMChat>
<div className="placeholder"></div>
<Send type="user" name={currUser?.name} id={currUser?.uid} />
<Send
dragFiles={dragFiles}
type="user"
name={currUser?.name}
id={currUser?.uid}
/>
</Layout>
);
}
+60
View File
@@ -0,0 +1,60 @@
// import React from "react";
import { NavLink, useNavigate } from "react-router-dom";
import dayjs from "dayjs";
import { useDrop } from "react-dnd";
import { NativeTypes } from "react-dnd-html5-backend";
import Avatar from "../../common/component/Avatar";
const NavItem = ({ data, setFiles }) => {
const navigate = useNavigate();
const [{ isActive }, drop] = useDrop(() => ({
accept: [NativeTypes.FILE],
drop({ dataTransfer }) {
if (dataTransfer.files.length) {
// console.log(files, rest);
setFiles([...dataTransfer.files]);
navigate(`/chat/dm/${data.uid}`);
// 重置
setTimeout(() => {
setFiles([]);
}, 300);
}
},
collect: (monitor) => ({
isActive: monitor.canDrop() && monitor.isOver(),
}),
}));
const { uid, user, lastMsg, unreads } = data;
return (
<NavLink
ref={drop}
key={uid}
className={`session ${isActive ? "drop_over" : ""}`}
to={`/chat/dm/${uid}`}
>
<Avatar className="avatar" url={user.avatar} id={uid} />
<div className="details">
<div className="up">
<span className="name">{user.name}</span>
<time>{dayjs(lastMsg.created_at).format("YYYY-MM-DD")}</time>
</div>
<div className="down">
<div className="msg">{lastMsg.content}</div>
{unreads > 0 && <i className="badge">{unreads}</i>}
</div>
</div>
</NavLink>
);
};
export default function DMList({ sessions, setDropFiles }) {
return sessions.map(({ uid, user, lastMsg, unreads } = {}) => {
if (!user) return null;
return (
<NavItem
key={uid}
data={{ uid, user, lastMsg, unreads }}
setFiles={setDropFiles}
/>
);
});
}
+82 -4
View File
@@ -1,11 +1,13 @@
import React from "react";
// import { useState } from "react";
import { useDrop } from "react-dnd";
import { NativeTypes } from "react-dnd-html5-backend";
import styled from "styled-components";
const StyledWrapper = styled.article`
position: relative;
width: 100%;
background: #fff;
height: 100vh;
.head {
> .head {
height: 56px;
padding: 0 20px;
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1);
@@ -21,16 +23,92 @@ const StyledWrapper = styled.article`
border-top: 1px solid transparent;
}
}
.drag_tip {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
visibility: hidden;
/* pointer-events: none; */
&.visible {
visibility: visible;
}
.box {
padding: 16px;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 8px;
background: #52edff;
.inner {
padding: 16px;
padding-top: 64px;
border: 2px dashed #a5f3fc;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
color: #fff;
.head {
font-weight: 600;
font-size: 20px;
line-height: 30px;
}
.intro {
font-weight: normal;
font-size: 14px;
line-height: 20px;
}
}
}
}
`;
export default function Layout({ children, header, contacts = null }) {
export default function Layout({
children,
header,
contacts = null,
setDragFiles,
}) {
const [{ isActive }, drop] = useDrop(() => ({
accept: [NativeTypes.FILE],
drop({ files }) {
if (files.length) {
setDragFiles([...files]);
}
},
collect: (monitor) => ({
isActive: monitor.canDrop() && monitor.isOver(),
}),
}));
return (
<StyledWrapper>
<StyledWrapper className="animate__animated animate__fadeIn" ref={drop}>
<header className="head">{header}</header>
<main className="main">
{children}
{contacts && <div className="members">{contacts}</div>}
</main>
<div
className={`drag_tip ${
isActive ? "visible animate__animated animate__fadeIn" : ""
}`}
>
<div
className={`box ${
isActive ? "animate__animated animate__bounceIn" : ""
}`}
>
<div className="inner">
<h4 className="head">Upload to #Channel</h4>
<span className="intro">
Photos accept jpg, png, max size limit to 10M.
</span>
</div>
</div>
</div>
</StyledWrapper>
);
}
+27 -51
View File
@@ -2,7 +2,7 @@
import { useState } from "react";
import { NavLink, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import dayjs from "dayjs";
import { MdAdd } from "react-icons/md";
import { AiOutlineCaretDown } from "react-icons/ai";
@@ -10,13 +10,16 @@ import { useGetContactsQuery } from "../../app/services/contact";
import StyledWrapper from "./styled";
import Search from "../../common/component/Search";
import Avatar from "../../common/component/Avatar";
import ChannelIcon from "../../common/component/ChannelIcon";
import ChannelChat from "./ChannelChat";
import DMChat from "./DMChat";
import ChannelList from "./ChannelList";
import ContactsModal from "../../common/component/ContactsModal";
import ChannelModal from "../../common/component/ChannelModal";
import DMList from "./DMList";
export default function ChatPage() {
const [channelDropFiles, setChannelDropFiles] = useState([]);
const [userDropFiles, setUserDropFiles] = useState([]);
const { channels, UserMsgData, ChannelMsgData } = useSelector((store) => {
return {
channels: store.channels,
@@ -38,11 +41,23 @@ export default function ChatPage() {
return Object.values(ChannelMsgData[gid] || {}).filter((m) => m.unread)
.length;
};
if (!contacts) return null;
const Sessions = Object.keys(UserMsgData);
const tmpSessionUser = contacts.find((c) => c.uid == user_id);
const transformedChannels = Object.entries(channels).map(([key, obj]) => {
return { id: key, ...obj };
const unreads = Object.values(ChannelMsgData[key] || {}).filter(
(m) => m.unread
).length;
return { id: key, ...obj, unreads };
});
const sessions = Object.keys(UserMsgData).map((uid) => {
let currUser = contacts.find((c) => c.uid == uid);
if (!currUser) return undefined;
let lastMid = Object.keys(UserMsgData[uid]).sort().pop();
let unreads = Object.values(UserMsgData[uid] || {}).filter((m) => m.unread)
.length;
let lastMsg = UserMsgData[uid][lastMid];
return { user: currUser, unreads, uid, lastMsg };
});
return (
<>
@@ -68,25 +83,10 @@ export default function ChatPage() {
/>
</h3>
<nav className="nav">
{transformedChannels.map(
({ id, is_public, name, description }) => {
let unreads = getUnreadCount(id);
return (
<NavLink
title={description}
key={id}
className="link"
to={`/chat/channel/${id}`}
>
<span className="txt">
<ChannelIcon personal={!is_public} />
{name}
</span>
{unreads > 0 && <i className="badge">{unreads}</i>}
</NavLink>
);
}
)}
<ChannelList
channels={transformedChannels}
setDropFiles={setChannelDropFiles}
/>
</nav>
</div>
<div className="list dms">
@@ -102,33 +102,8 @@ export default function ChatPage() {
/>
</h3>
<nav className="nav">
{Sessions.map((uid) => {
let currUser = contacts.find((c) => c.uid == uid);
let latestMid = Object.keys(UserMsgData[uid]).sort().pop();
let unreads = Object.values(UserMsgData[uid] || {}).filter(
(m) => m.unread
).length;
let latestMsg = UserMsgData[uid][latestMid];
return (
<NavLink key={uid} className="session" to={`/chat/dm/${uid}`}>
<Avatar className="avatar" url={currUser.avatar} id={uid} />
<div className="details">
<div className="up">
<span className="name">{currUser.name}</span>
<time>
{dayjs(latestMsg.created_at).format("YYYY-MM-DD")}
</time>
</div>
<div className="down">
<div className="msg">{latestMsg.content}</div>
{unreads > 0 && <i className="badge">{unreads}</i>}
</div>
</div>
</NavLink>
);
})}
{user_id && !Sessions.includes(user_id) && (
<DMList sessions={sessions} setDropFiles={setUserDropFiles} />
{user_id && !Object.keys(UserMsgData).includes(user_id) && (
<NavLink className="session" to={`/chat/dm/${user_id}`}>
<Avatar
className="avatar"
@@ -157,9 +132,10 @@ export default function ChatPage() {
unreads={getUnreadCount(channel_id)}
cid={channel_id}
data={channels[channel_id]}
dropFiles={channelDropFiles}
/>
)}
{user_id && <DMChat uid={user_id} />}
{user_id && <DMChat uid={user_id} dropFiles={userDropFiles} />}
</div>
</StyledWrapper>
</>
+5
View File
@@ -83,6 +83,7 @@ const StyledWrapper = styled.div`
&.active {
background: rgba(116, 127, 141, 0.1);
}
.avatar {
width: 32px;
height: 32px;
@@ -145,6 +146,10 @@ const StyledWrapper = styled.div`
}
}
}
/* drop files effect */
.drop_over {
outline: 2px solid #52edff;
}
}
}
}
-1
View File
@@ -6,7 +6,6 @@ import { useSelector, useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { clearAuthData } from "../../app/slices/auth.data";
import { useLazyLogoutQuery } from "../../app/services/auth";
const StyledWrapper = styled.div`
+23 -6
View File
@@ -1,8 +1,9 @@
// import React from 'react';
// import { useState } from "react";
// import { useEffect } from "react";
import { Outlet, NavLink } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { toggleMenuExpand } from "../../app/slices/ui";
// import { setAuthData } from "../../app/slices/auth.data";
import StyledWrapper from "./styled";
import ServerDropList from "./ServerDropList";
import Tools from "./Tools";
@@ -16,21 +17,37 @@ import NotificationHub from "../../common/component/NotificationHub";
export default function HomePage() {
const dispatch = useDispatch();
const { menuExpand, token, usersVersion } = useSelector((store) => {
const {
menuExpand,
authData: { token, usersVersion, afterMid },
} = useSelector((store) => {
return {
token: store.authData.token,
usersVersion: store.authData.usersVersion,
authData: store.authData,
menuExpand: store.ui.menuExpand,
};
});
const { data, error, success } = usePreload();
const { data, loading, error, success } = usePreload();
// useEffect(() => {
// if (authData) {
// dispatch(setAuthData(data));
// }
// }, [authData]);
const toggleExpand = () => {
dispatch(toggleMenuExpand());
};
console.log({ data, error, success });
if (loading) {
return "loading";
}
return (
<>
<NotificationHub token={token} usersVersion={usersVersion} />
<NotificationHub
token={token}
usersVersion={usersVersion}
afterMid={afterMid}
/>
<StyledWrapper>
<div className={`col left ${menuExpand ? "expand" : ""}`}>
<ServerDropList
+22 -2
View File
@@ -1,5 +1,9 @@
// import React from 'react';
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { useGetContactsQuery } from "../../app/services/contact";
import { clearAuthData } from "../../app/slices/auth.data";
// import { useGetChannelsQuery } from "../../app/services/channel";
import { useGetServerQuery } from "../../app/services/server";
// pollingInterval: 0,
@@ -7,6 +11,12 @@ const querySetting = {
refetchOnMountOrArgChange: true,
};
export default function usePreload() {
const [checked, setChecked] = useState(false);
const loginedUser = useSelector((store) => {
return store.authData.user;
});
const dispatch = useDispatch();
const navigate = useNavigate();
const {
isLoading: contactsLoading,
isSuccess: contactsSuccess,
@@ -25,9 +35,19 @@ export default function usePreload() {
// isError: groupsError,
// data: groups,
// } = useGetChannelsQuery(undefined, querySetting);
useEffect(() => {
if (contacts) {
const matchedUser = contacts.find((c) => c.uid == loginedUser.uid);
if (!matchedUser) {
dispatch(clearAuthData());
navigate("/login");
}
setChecked(true);
}
}, [contacts]);
return {
loading: contactsLoading && serverLoading,
loading: contactsLoading || serverLoading || !checked,
error: contactsError && serverError,
success: contactsSuccess && serverSuccess,
data: {
+70
View File
@@ -0,0 +1,70 @@
// import { useState } from "react";
import { Route, Routes, HashRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";
// import Welcome from './Welcome'
import NotFoundPage from "./404";
import LoginPage from "./login";
import HomePage from "./home";
import ChatPage from "./chat";
import ContactsPage from "./contacts";
import RequireAuth from "../common/component/RequireAuth";
import store from "../app/store.with.persist";
// import getStore from "../app/store";
const PageRoutes = () => {
return (
<HashRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/"
element={
<RequireAuth>
<HomePage />
</RequireAuth>
}
>
<Route index element={<ChatPage />} />
<Route path="chat">
<Route index element={<ChatPage />} />
<Route path="channel/:channel_id" element={<ChatPage />} />
<Route path="dm/:user_id" element={<ChatPage />} />
</Route>
<Route path="contacts">
<Route index element={<ContactsPage />} />
<Route path=":user_id" element={<ContactsPage />} />
</Route>
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</HashRouter>
);
};
// const local_key = "AUTH_DATA";
export default function ReduxRoutes() {
// const [authData, setAuthData] = useState(
// JSON.parse(localStorage.getItem(local_key))
// );
// const updateAuthData = (data) => {
// localStorage.setItem(local_key, JSON.stringify(data));
// setAuthData(data);
// };
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistStore(store)}>
<PageRoutes />
</PersistGate>
{/* {authData ? (
<PersistGate loading={null} persistor={persistStore(getPersistStore())}>
<PageRoutes authData={authData} updateAuthData={updateAuthData} />
</PersistGate>
) : (
<PageRoutes authData={authData} updateAuthData={updateAuthData} />
)} */}
</Provider>
);
}
+5 -3
View File
@@ -8,6 +8,7 @@ import { useLoginMutation } from "../../app/services/auth";
import { setAuthData } from "../../app/slices/auth.data";
export default function LoginPage() {
// const { token } = useSelector((store) => store.authData);
const navigateTo = useNavigate();
const dispatch = useDispatch();
const [login] = useLoginMutation();
@@ -21,8 +22,7 @@ export default function LoginPage() {
console.log("wtf", input);
const { data, error } = await login({
...input,
device: "web",
device_token: "test",
type: "password",
});
if (error) {
console.log(error);
@@ -37,9 +37,11 @@ export default function LoginPage() {
return;
}
if (data) {
dispatch(setAuthData(data));
// 更新本地认证信息
toast.success("login success");
dispatch(setAuthData(data));
navigateTo("/");
// location.reload(true);
}
};
const handleInput = (evt) => {
+48 -1
View File
@@ -1437,6 +1437,21 @@
schema-utils "^3.0.0"
source-map "^0.7.3"
"@react-dnd/asap@4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651"
integrity sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==
"@react-dnd/invariant@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-3.0.0.tgz#ea55db612b8be3284e87b67f1a1567595cd4c386"
integrity sha512-keberJRIqPX15IK3SWS/iO1t/kGETiL1oczKrDitAaMnQ+kpHf81l3MrRmFjvfqcnApE+izEvwM6GsyoIcpsVA==
"@react-dnd/shallowequal@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-3.0.0.tgz#ccfe1d6eead7591b4cebf6d7fec7150eb4de1962"
integrity sha512-1ELWQdJB2UrCXTKK5cCD9uGLLIwECLIEdttKA255owdpchtXohIjZBTlFJszwYi2ZKe2Do+QvUzsGyGCMNwbdw==
"@reduxjs/toolkit@^1.7.2":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.7.2.tgz#b428aaef92582379464f9de698dbb71957eafb02"
@@ -2075,6 +2090,11 @@ ajv@^8.0.0, ajv@^8.6.0, ajv@^8.8.0:
require-from-string "^2.0.2"
uri-js "^4.2.2"
animate.css@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/animate.css/-/animate.css-4.1.1.tgz#614ec5a81131d7e4dc362a58143f7406abd68075"
integrity sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==
ansi-escapes@^4.3.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@@ -3140,6 +3160,15 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
dnd-core@15.1.1:
version "15.1.1"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-15.1.1.tgz#b4dce2d892be2a7c9ca32ffdd545350be8d52f4f"
integrity sha512-Mtj/Sltcx7stVXzeDg4g7roTe/AmzRuIf/FYOxX6F8gULbY54w066BlErBOzQfn9RIJ3gAYLGX7wvVvoBSq7ig==
dependencies:
"@react-dnd/asap" "4.0.0"
"@react-dnd/invariant" "3.0.0"
redux "^4.1.1"
dns-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
@@ -6223,6 +6252,24 @@ react-dev-utils@^12.0.0:
strip-ansi "^6.0.1"
text-table "^0.2.0"
react-dnd-html5-backend@^15.1.2:
version "15.1.2"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.2.tgz#85e2c5ad57e87190495756f68f44fd89299062fb"
integrity sha512-mem9QbutUF+aA2YC1y47G3ECjnYV/sCYKSnu5Jd7cbg3fLMPAwbnTf/JayYdnCH5l3eg9akD9dQt+cD0UdF8QQ==
dependencies:
dnd-core "15.1.1"
react-dnd@^15.1.1:
version "15.1.1"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-15.1.1.tgz#feb233200cb372ea6382b511baac6cc52a0068dc"
integrity sha512-QLrHtPU08U4c5zop0ANeqrHXaQw2EWLMn8DQoN6/e4eSN/UbB84P49/80Qg0MEF29VLB5vikSoiFh9N8ASNmpQ==
dependencies:
"@react-dnd/invariant" "3.0.0"
"@react-dnd/shallowequal" "3.0.0"
dnd-core "15.1.1"
fast-deep-equal "^3.1.3"
hoist-non-react-statics "^3.3.2"
react-dom@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
@@ -6381,7 +6428,7 @@ redux-thunk@^2.4.1:
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714"
integrity sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==
redux@^4.0.0, redux@^4.1.2:
redux@^4.0.0, redux@^4.1.1, redux@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==