From 4e77c4ba798d34fb3f341f09bbbc8e63cdbd5495 Mon Sep 17 00:00:00 2001 From: zerosoul Date: Tue, 8 Mar 2022 21:46:11 +0800 Subject: [PATCH] feat: message reply --- src/app/config.js | 1 + src/app/services/base.query.js | 82 +++++++++++-------- src/app/services/message.js | 7 +- src/app/slices/auth.data.js | 21 ++++- src/app/slices/message.handler.js | 5 ++ src/app/slices/message.pending.js | 14 ++++ src/common/component/Message/Commands.js | 12 ++- .../component/Message/DeleteMessageConfirm.js | 2 +- src/common/component/Message/EmojiPicker.js | 5 +- .../component/Message/PreviewMessage.js | 5 +- src/common/component/Message/index.js | 17 +++- src/common/component/Message/styled.js | 6 ++ src/common/component/NotificationHub/index.js | 6 +- .../NotificationHub/useMessageHandler.js | 18 ++++ src/common/component/Send/index.js | 40 +++++++-- src/common/component/Send/styled.js | 27 ++++++ src/routes/chat/ChannelChat/index.js | 2 + src/routes/chat/DMChat/index.js | 2 + src/routes/home/index.js | 2 +- src/routes/home/usePreload.js | 27 +----- 20 files changed, 221 insertions(+), 80 deletions(-) diff --git a/src/app/config.js b/src/app/config.js index ae212670..a27556bb 100644 --- a/src/app/config.js +++ b/src/app/config.js @@ -11,6 +11,7 @@ export const googleClientID = // "840319286941-6ds7lbvk55eq8mjortf68cb2ll65lprt.apps.googleusercontent.com"; export const tokenHeader = "X-API-Key"; export const KEY_TOKEN = "RUSTCHAT_TOKEN"; +export const KEY_EXPIRE = "RUSTCHAT_TOKEN_EXPIRE"; export const KEY_REFRESH_TOKEN = "RUSTCHAT_REFRESH_TOKEN"; export const KEY_UID = "RUSTCHAT_CURR_UID"; diff --git a/src/app/services/base.query.js b/src/app/services/base.query.js index bdfa9940..8eedca95 100644 --- a/src/app/services/base.query.js +++ b/src/app/services/base.query.js @@ -1,5 +1,6 @@ import { fetchBaseQuery } from "@reduxjs/toolkit/query"; import toast from "react-hot-toast"; +import dayjs from "dayjs"; import { updateToken, clearAuthData } from "../slices/auth.data"; import BASE_URL, { tokenHeader } from "../config"; const whiteList = ["login", "checkInviteTokenValid", "getServer", "getOpenid"]; @@ -8,18 +9,54 @@ const baseQuery = fetchBaseQuery({ prepareHeaders: (headers, { getState, endpoint }) => { console.log("req", endpoint); const { token } = getState().authData; - if (token && !whiteList.includes(endpoint)) { + const noHeaderList = [...whiteList, "renew"]; + if (token && !noHeaderList.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); +let waitingForRenew = null; +const baseQueryWithTokenCheck = async (args, api, extraOptions) => { + if (waitingForRenew) { + await waitingForRenew; + } + // 先检查token是否过期,过期则renew + const { + token, + refreshToken, + expireTime = new Date().getTime(), + } = api.getState().authData; + let result = null; + if ( + !whiteList.includes(api.endpoint) && + dayjs().isAfter(new Date(expireTime - 20 * 1000)) + ) { + // 快过期了,renew + waitingForRenew = baseQuery( + { + url: "/token/renew", + method: "POST", + body: { + token, + refresh_token: refreshToken, + }, + }, + api, + extraOptions + ); + result = await waitingForRenew; + waitingForRenew = null; + // console.log({ refreshResult }); + if (result.data) { + // store the new token + api.dispatch(updateToken(result.data)); + // retry the initial query + result = await baseQuery(args, api, extraOptions); + } + } else { + result = await baseQuery(args, api, extraOptions); + } if (result?.error) { console.log("api error", result.error.originalStatus, api.endpoint); switch (result.error.originalStatus || result.error.status) { @@ -40,35 +77,14 @@ const baseQueryWithReauth = async (args, api, extraOptions) => { api.dispatch(clearAuthData()); location.href = "/#/login"; return; - } - // try to get a new token with refreshToken - const { token, refreshToken } = api.getState().authData; - const refreshResult = await baseQuery( - { - url: "/token/renew", - method: "POST", - body: { - token, - refresh_token: refreshToken, - }, - }, - api, - extraOptions - ); - console.log({ refreshResult }); - if (refreshResult.data) { - // store the new token - api.dispatch(updateToken(refreshResult.data)); - // retry the initial query - result = await baseQuery(args, api, extraOptions); } else { - toast.error("token expired, please login again"); - api.dispatch(clearAuthData()); + toast.error("Not Authenticated"); } } - break; - + case 403: + toast.error("Request Not Allowed"); + break; default: break; } @@ -76,4 +92,4 @@ const baseQueryWithReauth = async (args, api, extraOptions) => { return result; }; -export default baseQueryWithReauth; +export default baseQueryWithTokenCheck; diff --git a/src/app/services/message.js b/src/app/services/message.js index 60b58a95..74e936ea 100644 --- a/src/app/services/message.js +++ b/src/app/services/message.js @@ -31,10 +31,13 @@ export const messageApi = createApi({ }), }), replyMessage: builder.mutation({ - query: (mid, data) => ({ + query: ({ mid, content, type = "text" }) => ({ + headers: { + "content-type": ContentTypes[type], + }, url: `/message/${mid}/reply`, method: "POST", - body: data, + body: content, }), }), }), diff --git a/src/app/slices/auth.data.js b/src/app/slices/auth.data.js index 837496c8..cd6b21c8 100644 --- a/src/app/slices/auth.data.js +++ b/src/app/slices/auth.data.js @@ -1,9 +1,15 @@ import { createSlice } from "@reduxjs/toolkit"; -import BASE_URL, { KEY_REFRESH_TOKEN, KEY_TOKEN, KEY_UID } from "../config"; +import BASE_URL, { + KEY_REFRESH_TOKEN, + KEY_TOKEN, + KEY_UID, + KEY_EXPIRE, +} from "../config"; import { getNonNullValues } from "../../common/utils"; const initialState = { user: null, token: localStorage.getItem(KEY_TOKEN), + expireTime: localStorage.getItem(KEY_EXPIRE) || new Date().getTime(), refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN), }; const authDataSlice = createSlice({ @@ -11,11 +17,16 @@ const authDataSlice = createSlice({ initialState, reducers: { setAuthData(state, action) { - const { user, token, refresh_token } = action.payload; + const { user, token, refresh_token, expired_in = 0 } = action.payload; state.user = user; state.token = token; state.refreshToken = refresh_token; + // 当前时间往后推expire时长 + console.log("expire", expired_in); + const expireTime = new Date().getTime() + Number(expired_in) * 1000; + state.expireTime = expireTime; // set local data + localStorage.setItem(KEY_EXPIRE, expireTime); localStorage.setItem(KEY_TOKEN, token); localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); localStorage.setItem(KEY_UID, user.uid); @@ -51,15 +62,19 @@ const authDataSlice = createSlice({ state.token = null; state.refreshToken = null; // remove local data + localStorage.removeItem(KEY_EXPIRE); localStorage.removeItem(KEY_TOKEN); localStorage.removeItem(KEY_REFRESH_TOKEN); localStorage.removeItem(KEY_UID); }, updateToken(state, action) { - const { token, refresh_token } = action.payload; + const { token, refresh_token, expired_in } = action.payload; console.log("refresh token"); state.token = token; + const et = new Date().getTime() + Number(expired_in) * 1000; + state.expireTime = et; state.refreshToken = refresh_token; + localStorage.setItem(KEY_EXPIRE, et); localStorage.setItem(KEY_TOKEN, token); localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); }, diff --git a/src/app/slices/message.handler.js b/src/app/slices/message.handler.js index 498e0b1a..93924a62 100644 --- a/src/app/slices/message.handler.js +++ b/src/app/slices/message.handler.js @@ -33,11 +33,16 @@ export const msgAdd = (state, payload) => { content, created_at, mid, + reply_mid = null, from_uid, content_type, unread = true, } = payload; const newMsg = { content, content_type, created_at, from_uid, unread }; + + if (reply_mid && state[id][reply_mid]) { + newMsg.reply = { mid: reply_mid, ...state[id][reply_mid] }; + } if (state[id]) { let replaceMsg = state[id][mid]; // 如果存在,并且新消息和缓存消息不一样,则替换掉,并且改为已读(可能有问题) diff --git a/src/app/slices/message.pending.js b/src/app/slices/message.pending.js index 65fee8f0..f61dcff8 100644 --- a/src/app/slices/message.pending.js +++ b/src/app/slices/message.pending.js @@ -1,6 +1,7 @@ import { createSlice } from "@reduxjs/toolkit"; const initialState = { + reply: {}, user: {}, channel: {}, }; @@ -18,6 +19,17 @@ const pendingMessageSlice = createSlice({ curr[mid] = { ...msg, pending: true }; state[type][id] = curr; }, + setReplyMessage(state, action) { + const { id, msg } = action.payload; + console.log("reply to ", id, msg); + state.reply[id] = msg; + }, + removeReplyMessage(state, action) { + const id = action.payload; + if (state.reply[id]) { + delete state.reply[id]; + } + }, removePendingMessage(state, action) { const { id, mid, type = "user" } = action.payload; console.log("remove msg", type, id, mid); @@ -29,5 +41,7 @@ export const { clearPendingMsg, addPendingMessage, removePendingMessage, + removeReplyMessage, + setReplyMessage, } = pendingMessageSlice.actions; export default pendingMessageSlice.reducer; diff --git a/src/common/component/Message/Commands.js b/src/common/component/Message/Commands.js index 731e4f7d..5701be9d 100644 --- a/src/common/component/Message/Commands.js +++ b/src/common/component/Message/Commands.js @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from "react-redux"; import styled from "styled-components"; import toast from "react-hot-toast"; import { useOutsideClick } from "rooks"; +import { setReplyMessage } from "../../../app/slices/message.pending"; import StyledMenu from "../StyledMenu"; import DeleteMessageConfirm from "./DeleteMessageConfirm"; import EmojiPicker from "./EmojiPicker"; @@ -39,6 +40,7 @@ const StyledCmds = styled.ul` } `; export default function Commands({ + contextId = 0, message = null, mid = 0, uid = 0, @@ -49,13 +51,17 @@ export default function Commands({ toggleEmojiPopover, toggleEditMessage, }) { + const dispatch = useDispatch(); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const currUid = useSelector((store) => store.authData.user.uid); const menuRef = useRef(null); - const handleClick = () => { - toast.success("cooming soon"); + const handleReply = () => { + if (contextId) { + dispatch(setReplyMessage({ id: contextId, msg: message })); + } + // toast.success("cooming soon"); }; useOutsideClick(menuRef, toggleMenu); @@ -86,7 +92,7 @@ export default function Commands({ /> ) : ( -
  • +
  • icon reply -