feat: lots of updates
This commit is contained in:
+98
-95
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
// 如果存在,并且新消息和缓存消息不一样,则替换掉,并且改为已读(可能有问题)
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user