diff --git a/package.json b/package.json index d83d3878..55bb7cb9 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,15 @@ "@svgr/webpack": "^6.2.1", "@tippyjs/react": "^4.2.6", "animate.css": "^4.1.1", - "axios": "^0.26.0", + "axios": "^0.26.1", "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.3", + "browserslist": "^4.20.0", "camelcase": "^6.3.0", "case-sensitive-paths-webpack-plugin": "^2.4.0", - "css-loader": "^6.6.0", + "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^3.4.1", "dayjs": "^1.10.8", "dotenv": "^16.0.0", @@ -29,12 +29,12 @@ "eslint": "^8.10.0", "event-source-polyfill": "^1.0.25", "file-loader": "^6.2.0", - "firebase": "^9.6.7", + "firebase": "^9.6.8", "fs-extra": "^10.0.1", "html-webpack-plugin": "^5.5.0", "linkify-it": "^3.0.3", "localforage": "^1.10.0", - "mini-css-extract-plugin": "^2.5.3", + "mini-css-extract-plugin": "^2.6.0", "react": "^17.0.2", "react-dev-utils": "^12.0.0", "react-dnd": "^15.1.1", @@ -52,7 +52,7 @@ "react-window": "^1.8.6", "redux-persist": "^6.0.0", "resolve": "^1.22.0", - "rooks": "^5.10.0", + "rooks": "^5.10.2", "semver": "^7.3.5", "source-map-loader": "^3.0.1", "style-loader": "^3.3.1", @@ -60,10 +60,10 @@ "styled-reset": "^4.3.4", "terser-webpack-plugin": "^5.3.1", "tippy.js": "^6.3.7", - "webpack": "^5.69.1", + "webpack": "^5.70.0", "webpack-dev-server": "^4.7.4", - "webpack-manifest-plugin": "^4.1.1", - "workbox-webpack-plugin": "^6.5.0" + "webpack-manifest-plugin": "^5.0.0", + "workbox-webpack-plugin": "^6.5.1" }, "scripts": { "start": "node scripts/start.js", @@ -93,18 +93,18 @@ "@commitlint/cli": "^16.2.1", "@commitlint/config-conventional": "^16.2.1", "autoprefixer": "^10.4.2", - "eslint-config-prettier": "^8.4.0", + "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.25.4", "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.2", + "eslint-plugin-react": "^7.29.3", "eslint-plugin-react-hooks": "^4.3.0", "gh-pages": "^3.2.3", "husky": "^7.0.4", - "lint-staged": "^12.3.4", - "postcss": "^8.4.7", + "lint-staged": "^12.3.5", + "postcss": "^8.4.8", "postcss-flexbugs-fixes": "^5.0.2", "postcss-loader": "^6.2.1", - "postcss-preset-env": "^7.4.1", + "postcss-preset-env": "^7.4.2", "prettier": "^2.5.1" } } diff --git a/src/app/cache.js b/src/app/cache.js index 74b05081..b60288f9 100644 --- a/src/app/cache.js +++ b/src/app/cache.js @@ -1,70 +1,122 @@ import { useState } from "react"; import localforage from "localforage"; -import { useDispatch } from "react-redux"; -import { initChannelMsg } from "./slices/message.channel"; -import { initUserMsg } from "./slices/message.user"; -import { setChannels } from "./slices/channels"; -import { setMark } from "./slices/visit.mark"; +import { useDispatch, batch } from "react-redux"; +import { fullfillReactionMessage } from "./slices/message.reaction"; +import { fullfillMessage } from "./slices/message"; +import { fullfillChannelMsg } from "./slices/message.channel"; +import { fullfillUserMsg } from "./slices/message.user"; +import { fullfillChannels } from "./slices/channels"; +import { fullfillContacts } from "./slices/contacts"; +import { fullfillFootprint } from "./slices/footprint"; import { KEY_UID } from "./config"; +const tables = [ + { + storeName: "channels", + description: "store channel list", + }, + { + storeName: "contacts", + description: "store contact list", + }, + { + storeName: "messageDM", + description: "store DM message with IDs", + }, + { + storeName: "messageChannel", + description: "store channel message with IDs", + }, + { + storeName: "message", + description: "store message with key-val full data", + }, + { + storeName: "messageReaction", + description: "store message reaction with key-val full data", + }, + { + storeName: "footprint", + description: "store user visit data", + }, + // { + // storeName: "message", + // description: "store message with key-val full data", + // }, +]; const initCache = () => { const uid = localStorage.getItem(KEY_UID) || ""; - const name = `cache_db_${uid}`; - // const channels = localforage.createInstance({ - // name, - // storeName: "channels", - // description: "channel list", - // }); - // const contacts = localforage.createInstance({ - // name, - // storeName: "contacts", - // description: "contact list", - // }); - // const msgs_user = localforage.createInstance({ - // name, - // storeName: "msgs_dm", - // description: "DM message list", - // }); - // const msgs_channel = localforage.createInstance({ - // name, - // storeName: "msgs_channel", - // description: "channel message list", - // }); - window.CACHE = localforage.createInstance({ - name, - storeName: "data", - description: "rustchat cache data by uid", + const name = `local_db_${uid}`; + window.CACHE = {}; + tables.forEach(({ storeName, description }) => { + window.CACHE[storeName] = localforage.createInstance({ + name, + storeName, + description, + }); }); }; export const useRehydrate = () => { const [iterated, setIterated] = useState(false); const dispatch = useDispatch(); const rehydrate = async () => { - const res = await window.CACHE.iterate((data, key) => { - switch (key) { - case "visitMark": - dispatch(setMark(data)); - break; - case "channels": - dispatch(setChannels(data)); - break; - // case "contacts": - // dispatch(initChannelMsg(data)); - // break; - case "channelMessage": - dispatch(initChannelMsg(data)); - break; - case "userMessage": - dispatch(initUserMsg(data)); - break; + const rehydrateData = { + channels: [], + contacts: [], + channelMessage: {}, + userMessage: {}, + reactionMessage: {}, + message: { replying: {} }, + footprint: {}, + }; + const tables = Object.keys(window.CACHE); + const results = await Promise.all( + tables.map((_key) => { + return window.CACHE[_key].iterate((data, key) => { + // console.log("iterated", key); + switch (_key) { + case "channels": + rehydrateData.channels.push(data); + break; + case "contacts": + rehydrateData.contacts.push(data); + break; + case "footprint": + rehydrateData.footprint[key] = data; + break; + case "messageChannel": + rehydrateData.channelMessage[key] = data; + break; + case "messageDM": + rehydrateData.userMessage[key] = data; + break; + case "messageReaction": + rehydrateData.reactionMessage[key] = data; + break; + case "message": + rehydrateData.message[key] = data; + break; - default: - break; - } + default: + break; + } + }); + }) + ); + batch(() => { + dispatch(fullfillContacts(rehydrateData.contacts)); + console.log("fullfill channels from indexedDB"); + dispatch(fullfillChannels(rehydrateData.channels)); + dispatch(fullfillChannelMsg(rehydrateData.channelMessage)); + dispatch(fullfillUserMsg(rehydrateData.userMessage)); + dispatch(fullfillMessage(rehydrateData.message)); + dispatch(fullfillFootprint(rehydrateData.footprint)); + dispatch(fullfillReactionMessage(rehydrateData.reactionMessage)); }); + setIterated(true); - console.log("iterate", res); + // console.log("iterate results", rehydrateData, results); }; - return { rehydrate, cacheFirst: iterated }; + return { rehydrate, rehydrated: iterated }; }; export default initCache; diff --git a/src/app/config.js b/src/app/config.js index 4e85535a..0fe152fd 100644 --- a/src/app/config.js +++ b/src/app/config.js @@ -3,6 +3,7 @@ const BASE_URL = `https://dev.rustchat.com/api`; // const BASE_URL = `https://rustchat.net/api`; export const ContentTypes = { text: "text/plain", + markdown: "text/markdown", image: "image/png", json: "application/json", }; diff --git a/src/app/listener.middleware.js b/src/app/listener.middleware.js deleted file mode 100644 index 688e7535..00000000 --- a/src/app/listener.middleware.js +++ /dev/null @@ -1,39 +0,0 @@ -import { createListenerMiddleware } from "@reduxjs/toolkit"; -const getCacheKey = (action = "") => { - const [type] = action.split("/"); - const keys = [ - "visitMark", - "channels", - "contacts", - "userMessage", - "channelMessage", - // "pendingMessage", - ]; - const [tmp = ""] = keys.filter((k) => k == type); - return tmp; -}; - -// Create the middleware instance and methods -const listenerMiddleware = createListenerMiddleware(); - -// Add one or more listener entries that look for specific actions. -// They may contain any sync or async logic, similar to thunks. -listenerMiddleware.startListening({ - predicate: (action, currentState, previousState) => { - const { type = "" } = action; - - return !!getCacheKey(type); - // console.log("listener predicate", action, currentState, previousState); - // return true; - }, - effect: async (action, listenerApi) => { - const { type = "" } = action; - const key = getCacheKey(type); - // console.log("listener effect", key, listenerApi.getState(), window.CACHE); - if (key && window.CACHE) { - await window.CACHE.setItem(key, listenerApi.getState()[key]); - } - }, -}); - -export default listenerMiddleware; diff --git a/src/app/listener.middleware/clear.handler.js b/src/app/listener.middleware/clear.handler.js new file mode 100644 index 00000000..347dc9f8 --- /dev/null +++ b/src/app/listener.middleware/clear.handler.js @@ -0,0 +1,9 @@ +const clearTable = async (table) => { + const t = window.CACHE[table]; + if (!t) return; + + await t.iterate((data, key) => { + t.removeItem(key); + }); +}; +export default clearTable; diff --git a/src/app/listener.middleware/handler.channel.msg.js b/src/app/listener.middleware/handler.channel.msg.js new file mode 100644 index 00000000..083e2c0f --- /dev/null +++ b/src/app/listener.middleware/handler.channel.msg.js @@ -0,0 +1,29 @@ +import clearTable from "./clear.handler"; +export default async function handler({ operation, data = {}, payload }) { + const table = window.CACHE["messageChannel"]; + if (operation.startsWith("reset")) { + clearTable("messageChannel"); + return; + } + switch (operation) { + // case "fullfillChannelMsgs": + // { + // await Promise.all( + // Object.entries(data).map(async ([cid, arr]) => { + // await table.setItem(cid + "", arr); + // }) + // ); + // } + // break; + case "addChannelMsg": + case "removeChannelMsg": + { + const { id } = payload; + + await table.setItem(id + "", data[id]); + } + break; + default: + break; + } +} diff --git a/src/app/listener.middleware/handler.channels.js b/src/app/listener.middleware/handler.channels.js new file mode 100644 index 00000000..b55fab9e --- /dev/null +++ b/src/app/listener.middleware/handler.channels.js @@ -0,0 +1,40 @@ +import clearTable from "./clear.handler"; +export default async function handler({ operation, data, payload }) { + const table = window.CACHE["channels"]; + if (operation.startsWith("reset")) { + clearTable("channels"); + return; + } + switch (operation) { + // case "fullfillChannels": + // { + // await Promise.all( + // data.ids.map(async (cid) => { + // await table.setItem(cid + "", data.byId[cid]); + // }) + // ); + // } + // break; + case "addChannel": + { + const { gid } = payload; + await table.setItem(gid + "", payload); + } + break; + case "removeChannel": + { + const id = payload; + await table.removeItem(id + ""); + } + break; + case "updateChannel": + { + const { id } = payload; + await table.setItem(id + "", data.byId[id]); + } + break; + + default: + break; + } +} diff --git a/src/app/listener.middleware/handler.contacts.js b/src/app/listener.middleware/handler.contacts.js new file mode 100644 index 00000000..d4c924c0 --- /dev/null +++ b/src/app/listener.middleware/handler.contacts.js @@ -0,0 +1,55 @@ +import clearTable from "./clear.handler"; +export default async function handler({ operation, data, payload }) { + const table = window.CACHE["contacts"]; + if (operation.startsWith("reset")) { + clearTable("contacts"); + return; + } + switch (operation) { + // case "fullfillContacts": + // { + // await Promise.all( + // data.ids.map(async (uid) => { + // await table.setItem(uid + "", data.byId[uid]); + // }) + // ); + // } + // break; + case "updateUsersByLogs": + { + const changeLogs = payload; + await Promise.all( + changeLogs.map(async ({ action, uid }) => { + switch (action) { + case "update": + case "create": + await table.setItem(uid + "", data.byId[uid]); + break; + case "delete": + await table.removeItem(uid + ""); + break; + + default: + break; + } + }) + ); + } + break; + case "addContact": + { + const { uid } = payload; + await table.setItem(uid + "", payload); + } + break; + case "removeContact": + { + const id = payload; + await table.removeItem(id + ""); + } + break; + + default: + break; + } +} diff --git a/src/app/listener.middleware/handler.dm.msg.js b/src/app/listener.middleware/handler.dm.msg.js new file mode 100644 index 00000000..7a39258d --- /dev/null +++ b/src/app/listener.middleware/handler.dm.msg.js @@ -0,0 +1,28 @@ +import clearTable from "./clear.handler"; +export default async function handler({ operation, data = {}, payload }) { + const table = window.CACHE["messageDM"]; + if (operation.startsWith("reset")) { + clearTable("messageDM"); + return; + } + switch (operation) { + // case "fullfillUserMsgs": + // { + // await Promise.all( + // Object.entries(data).map(async ([uid, arr]) => { + // await table.setItem(uid + "", arr); + // }) + // ); + // } + // break; + case "addUserMsg": + case "removeUserMsg": + { + const { id } = payload; + await table.setItem(id + "", data.byId[id]); + } + break; + default: + break; + } +} diff --git a/src/app/listener.middleware/handler.footprint.js b/src/app/listener.middleware/handler.footprint.js new file mode 100644 index 00000000..7fdb686f --- /dev/null +++ b/src/app/listener.middleware/handler.footprint.js @@ -0,0 +1,33 @@ +import clearTable from "./clear.handler"; +export default async function handler({ operation, data = {}, payload }) { + const table = window.CACHE["footprint"]; + if (operation.startsWith("reset")) { + clearTable("footprint"); + return; + } + switch (operation) { + // case "fullfillFootprint": + // { + // await Promise.all( + // Object.entries(data).map(async ([key, value]) => { + // await table.setItem(key, value); + // }) + // ); + // } + // break; + case "updateUsersVersion": + { + const usersVersion = payload; + await table.setItem("usersVersion", usersVersion); + } + break; + case "updateAfterMid": + { + const afterMid = payload; + await table.setItem("afterMid", afterMid); + } + break; + default: + break; + } +} diff --git a/src/app/listener.middleware/handler.message.js b/src/app/listener.middleware/handler.message.js new file mode 100644 index 00000000..018902a4 --- /dev/null +++ b/src/app/listener.middleware/handler.message.js @@ -0,0 +1,44 @@ +import clearTable from "./clear.handler"; +export default async function handler({ operation, data = {}, payload }) { + const table = window.CACHE["message"]; + if (operation.startsWith("reset")) { + clearTable("message"); + return; + } + switch (operation) { + // case "fullfillChannelMsgs": + // { + // await Promise.all( + // Object.entries(data).map(async ([mid, obj]) => { + // await table.setItem(mid + "", obj); + // }) + // ); + // } + // break; + case "addMessage": + case "updateMessage": + { + const { mid } = payload; + await table.setItem(mid + "", data[mid]); + } + break; + case "removeMessage": + { + const mid = payload; + await table.removeItem(mid + ""); + } + break; + case "readMessage": + { + const mids = Array.isArray(payload) ? payload : [payload]; + await Promise.all( + mids.map(async (mid) => { + await table.setItem(mid + "", data[mid]); + }) + ); + } + break; + default: + break; + } +} diff --git a/src/app/listener.middleware/handler.reaction.js b/src/app/listener.middleware/handler.reaction.js new file mode 100644 index 00000000..74630cd2 --- /dev/null +++ b/src/app/listener.middleware/handler.reaction.js @@ -0,0 +1,27 @@ +import clearTable from "./clear.handler"; +export default async function handler({ operation, data = {}, payload }) { + const table = window.CACHE["messageReaction"]; + if (operation.startsWith("reset")) { + clearTable("messageReaction"); + return; + } + switch (operation) { + // case "fullfillReactionMessage": + // { + // await Promise.all( + // Object.entries(data).map(async ([mid, data]) => { + // await table.setItem(mid + "", data); + // }) + // ); + // } + // break; + case "toggleReactionMessage": + { + const { mid } = payload; + await table.setItem(mid + "", data[mid]); + } + break; + default: + break; + } +} diff --git a/src/app/listener.middleware/index.js b/src/app/listener.middleware/index.js new file mode 100644 index 00000000..73316075 --- /dev/null +++ b/src/app/listener.middleware/index.js @@ -0,0 +1,114 @@ +import { createListenerMiddleware } from "@reduxjs/toolkit"; +import channelsHandler from "./handler.channels"; +import contactsHandler from "./handler.contacts"; +import channelMsgHandler from "./handler.channel.msg"; +import dmMsgHandler from "./handler.dm.msg"; +import messageHandler from "./handler.message"; +import reactionHandler from "./handler.reaction"; +import footprintHandler from "./handler.footprint"; +const operations = [ + "channels", + "channelMessage", + "contacts", + "userMessage", + "reactionMessage", + "message", + "footprint", +]; + +// Create the middleware instance and methods +const listenerMiddleware = createListenerMiddleware(); + +// Add one or more listener entries that look for specific actions. +// They may contain any sync or async logic, similar to thunks. +listenerMiddleware.startListening({ + predicate: (action, currentState, previousState) => { + const { type = "" } = action; + console.log("operation", type); + const [prefix] = type.split("/"); + return operations.includes(prefix); + // console.log("listener predicate", action, currentState, previousState); + // return true; + }, + effect: async (action, listenerApi) => { + if (!window.CACHE) return; + const { type = "", payload } = action; + // const key = getCacheKey(type); + // // console.log("listener effect", key, listenerApi.getState(), window.CACHE); + // if (key && window.CACHE) { + // await window.CACHE.setItem(key, listenerApi.getState()[key]); + // } + const [prefix, operation] = type.split("/"); + const state = listenerApi.getState()[prefix]; + switch (prefix) { + case "channels": + { + await channelsHandler({ + operation, + payload, + data: state, + }); + } + break; + case "contacts": + { + await contactsHandler({ + operation, + payload, + data: state, + }); + } + break; + case "channelMessage": + { + await channelMsgHandler({ + operation, + payload, + data: state, + }); + } + break; + case "userMessage": + { + await dmMsgHandler({ + operation, + payload, + data: state, + }); + } + break; + case "message": + { + await messageHandler({ + operation, + payload, + data: state, + }); + } + break; + case "reactionMessage": + { + await reactionHandler({ + operation, + payload, + data: state, + }); + } + break; + case "footprint": + { + await footprintHandler({ + operation, + payload, + data: state, + }); + } + break; + + default: + break; + } + }, +}); + +export default listenerMiddleware; diff --git a/src/app/services/auth.js b/src/app/services/auth.js index 3134fbfe..74779be5 100644 --- a/src/app/services/auth.js +++ b/src/app/services/auth.js @@ -2,7 +2,7 @@ import { createApi } from "@reduxjs/toolkit/query/react"; import baseQuery from "./base.query"; import BASE_URL from "../config"; export const authApi = createApi({ - reducerPath: "auth", + reducerPath: "authApi", baseQuery, endpoints: (builder) => ({ login: builder.mutation({ diff --git a/src/app/services/base.query.js b/src/app/services/base.query.js index 8eedca95..dfdebacd 100644 --- a/src/app/services/base.query.js +++ b/src/app/services/base.query.js @@ -1,9 +1,15 @@ import { fetchBaseQuery } from "@reduxjs/toolkit/query"; import toast from "react-hot-toast"; import dayjs from "dayjs"; -import { updateToken, clearAuthData } from "../slices/auth.data"; +import { updateToken, resetAuthData } from "../slices/auth.data"; import BASE_URL, { tokenHeader } from "../config"; -const whiteList = ["login", "checkInviteTokenValid", "getServer", "getOpenid"]; +const whiteList = [ + "login", + "checkInviteTokenValid", + "getServer", + "getOpenid", + "getMetamaskNonce", +]; const baseQuery = fetchBaseQuery({ baseUrl: BASE_URL, prepareHeaders: (headers, { getState, endpoint }) => { @@ -72,14 +78,14 @@ const baseQueryWithTokenCheck = async (args, api, extraOptions) => { break; case 401: { - if (api.endpoint === "renew") { - // toast.error("token expired, please login again"); - api.dispatch(clearAuthData()); - location.href = "/#/login"; - return; - } else { - toast.error("Not Authenticated"); - } + // if (api.endpoint === "renew") { + // toast.error("token expired, please login again"); + api.dispatch(resetAuthData()); + location.href = "/#/login"; + // } else { + toast.error("API Not Authenticated"); + return; + // } } break; case 403: diff --git a/src/app/services/channel.js b/src/app/services/channel.js index a3260e26..5342b7ac 100644 --- a/src/app/services/channel.js +++ b/src/app/services/channel.js @@ -5,7 +5,7 @@ import { ContentTypes } from "../config"; import { updateChannel } from "../slices/channels"; import { onMessageSendStarted } from "./handlers"; export const channelApi = createApi({ - reducerPath: "channel", + reducerPath: "channelApi", baseQuery, refetchOnFocus: true, endpoints: (builder) => ({ diff --git a/src/app/services/contact.js b/src/app/services/contact.js index 8001617d..6238fec5 100644 --- a/src/app/services/contact.js +++ b/src/app/services/contact.js @@ -2,10 +2,9 @@ import { createApi } from "@reduxjs/toolkit/query/react"; // import toast from "react-hot-toast"; import baseQuery from "./base.query"; import BASE_URL, { ContentTypes } from "../config"; -import { removeContact } from "../slices/contacts"; import { onMessageSendStarted } from "./handlers"; export const contactApi = createApi({ - reducerPath: "contact", + reducerPath: "contactApi", baseQuery, endpoints: (builder) => ({ getContacts: builder.query({ @@ -23,16 +22,6 @@ export const contactApi = createApi({ }), deleteContact: builder.query({ query: (uid) => ({ url: `/admin/user/${uid}`, method: "DELETE" }), - async onQueryStarted(uid, { dispatch, queryFulfilled }) { - // id: who send to ,from_uid: who sent - const patchResult = dispatch(removeContact(uid)); - try { - await queryFulfilled; - } catch { - console.log("channel update failed"); - patchResult.undo(); - } - }, }), updateAvatar: builder.mutation({ query: (data) => ({ diff --git a/src/app/services/handlers.js b/src/app/services/handlers.js index 6c99e676..0754e103 100644 --- a/src/app/services/handlers.js +++ b/src/app/services/handlers.js @@ -1,56 +1,49 @@ import toast from "react-hot-toast"; +import { batch } from "react-redux"; import { ContentTypes } from "../config"; -import { - // addChannelMsg, - addChannelPendingMsg, - removeChannelPendingMsg, - replaceChannelPendingMsg, -} from "../slices/message.channel"; -import { - addUserPendingMsg, - removeUserPendingMsg, - replaceUserPendingMsg, -} from "../slices/message.user"; - -// import { -// addPendingMessage, -// removePendingMessage, -// } from "../slices/message.pending"; +import { addChannelMsg, removeChannelMsg } from "../slices/message.channel"; +import { addUserMsg, removeUserMsg } from "../slices/message.user"; +import { addMessage, removeMessage } from "../slices/message"; export const onMessageSendStarted = async ( - { id, content, type, from_uid }, + { id, content, type = "text", from_uid, reply_mid = null }, { dispatch, queryFulfilled }, from = "channel" ) => { // id: who send to ,from_uid: who sent const ts = new Date().getTime(); const tmpMsg = { - id, content: type == "image" ? URL.createObjectURL(content) : content, content_type: ContentTypes[type], created_at: ts, - local_mid: ts, from_uid, - // unread: false, + reply_mid, + // 已读 + read: true, + sending: true, }; - const addPendingMessage = - from == "channel" ? addChannelPendingMsg : addUserPendingMsg; - const replacePendingMessage = - from == "channel" ? replaceChannelPendingMsg : replaceUserPendingMsg; - const removePendingMessage = - from == "channel" ? removeChannelPendingMsg : removeUserPendingMsg; - // dispatch(addPendingMessage({ type: from, msg: tmpMsg })); - dispatch(addPendingMessage({ ...tmpMsg })); + const addContextMessage = from == "channel" ? addChannelMsg : addUserMsg; + const removeContextMessage = + from == "channel" ? removeChannelMsg : removeUserMsg; + batch(() => { + dispatch(addMessage({ mid: ts, ...tmpMsg })); + dispatch(addContextMessage({ id, mid: ts })); + }); + try { const { data: server_mid } = await queryFulfilled; console.log("message server mid", server_mid); - // 此处的id,是指给谁发的 - // const addMessage = from == "channel" ? addChannelMsg : addUserMsg; - dispatch(replacePendingMessage({ id, local_mid: ts, server_mid })); + batch(() => { + dispatch(removeContextMessage({ id, mid: ts })); + dispatch(removeMessage(ts)); + dispatch(addMessage({ mid: server_mid, ...tmpMsg })); + dispatch(addContextMessage({ id, mid: server_mid })); + }); // dispatch(removePendingMessage({ id, mid:ts, type: from })); } catch { console.log("message send failed"); toast.error("Send Message Failed"); - dispatch(removePendingMessage({ id, mid: ts, type: from })); + dispatch(removeContextMessage({ id, mid: ts })); + dispatch(removeMessage(ts)); // patchResult.undo(); } }; diff --git a/src/app/services/message.js b/src/app/services/message.js index 74e936ea..d8a204da 100644 --- a/src/app/services/message.js +++ b/src/app/services/message.js @@ -1,10 +1,12 @@ import { createApi } from "@reduxjs/toolkit/query/react"; import { ContentTypes } from "../config"; +import { onMessageSendStarted } from "./handlers"; +// import { updateMessage } from "../slices/message"; import baseQuery from "./base.query"; export const messageApi = createApi({ - reducerPath: "message", + reducerPath: "messageApi", baseQuery, endpoints: (builder) => ({ editMessage: builder.mutation({ @@ -16,8 +18,11 @@ export const messageApi = createApi({ method: "PUT", body: content, }), + // async onQueryStarted({mid,content},{dispatch}){ + // dispatch() + // } }), - likeMessage: builder.mutation({ + reactMessage: builder.mutation({ query: ({ mid, action }) => ({ url: `/message/${mid}/like`, method: "PUT", @@ -31,21 +36,24 @@ export const messageApi = createApi({ }), }), replyMessage: builder.mutation({ - query: ({ mid, content, type = "text" }) => ({ + query: ({ reply_mid, content, type = "text" }) => ({ headers: { "content-type": ContentTypes[type], }, - url: `/message/${mid}/reply`, + url: `/message/${reply_mid}/reply`, method: "POST", body: content, }), + async onQueryStarted(param1, param2) { + await onMessageSendStarted.call(this, param1, param2, param1.context); + }, }), }), }); export const { useEditMessageMutation, - useLikeMessageMutation, + useReactMessageMutation, useReplyMessageMutation, useLazyDeleteMessageQuery, } = messageApi; diff --git a/src/app/services/server.js b/src/app/services/server.js index 166be7b4..437001f1 100644 --- a/src/app/services/server.js +++ b/src/app/services/server.js @@ -4,7 +4,7 @@ import BASE_URL from "../config"; import baseQuery from "./base.query"; export const serverApi = createApi({ - reducerPath: "server", + reducerPath: "serverApi", baseQuery, endpoints: (builder) => ({ getServer: builder.query({ diff --git a/src/app/services/streaming/chat.handler.js b/src/app/services/streaming/chat.handler.js new file mode 100644 index 00000000..5d5e0550 --- /dev/null +++ b/src/app/services/streaming/chat.handler.js @@ -0,0 +1,130 @@ +import { batch } from "react-redux"; +import { addChannelMsg, removeChannelMsg } from "../../slices/message.channel"; +import { addMessage, removeMessage, updateMessage } from "../../slices/message"; +import { toggleReactionMessage } from "../../slices/message.reaction"; +import { addUserMsg, removeUserMsg } from "../../slices/message.user"; +import { updateAfterMid } from "../../slices/footprint"; +const handler = (data, dispatch, currState) => { + const { + mid, + from_uid, + created_at, + target, + detail: { + mid: detailMid, + content, + content_type, + type, + properties, + expires_in, + detail: innerDetail, + }, + } = data; + const common = { + from_uid, + created_at, + content, + content_type, + properties, + expires_in, + }; + switch (type) { + case "normal": + case "reply": + // 更新after_mid + dispatch(updateAfterMid(mid)); + break; + } + const { ready, loginUid } = currState; + const to = typeof target.gid !== "undefined" ? "channel" : "user"; + const appendMessage = to == "user" ? addUserMsg : addChannelMsg; + const self = from_uid == loginUid; + // 此处有点绕 + const id = to == "user" ? (self ? target.uid : from_uid) : target.gid; + + switch (type) { + case "normal": + { + batch(() => { + dispatch( + addMessage({ + mid, + // 如果是自己发的消息,就是已读 + read: self, + ...common, + }) + ); + // 未推送完 or 不是自己发的消息 + console.log("curr state", ready, loginUid, common.from_uid); + if (!ready || loginUid != common.from_uid) { + dispatch(appendMessage({ id, mid })); + } + }); + } + break; + case "reply": + { + batch(() => { + dispatch( + addMessage({ + mid, + reply_mid: detailMid, + // 如果是自己发的消息,就是已读 + read: self, + ...common, + }) + ); + // 未推送完 or 不是自己发的消息 + if (!ready || loginUid != common.from_uid) { + dispatch(appendMessage({ id, mid })); + } + }); + } + break; + case "reaction": + { + const removeContextMessage = + to == "user" ? removeUserMsg : removeChannelMsg; + const { type, action, content, content_type, properties } = innerDetail; + switch (type) { + case "like": + { + dispatch( + toggleReactionMessage({ from_uid, mid: detailMid, action }) + ); + } + + break; + case "delete": + { + batch(() => { + dispatch(removeContextMessage({ id, mid: detailMid })); + dispatch(removeMessage(detailMid)); + }); + } + break; + case "edit": + { + dispatch( + updateMessage({ + mid: detailMid, + content, + content_type, + properties, + edited: true, + }) + ); + } + break; + + default: + break; + } + } + break; + + default: + break; + } +}; +export default handler; diff --git a/src/app/services/streaming/index.js b/src/app/services/streaming/index.js new file mode 100644 index 00000000..c0654376 --- /dev/null +++ b/src/app/services/streaming/index.js @@ -0,0 +1,173 @@ +// for SSE + +import { createApi } from "@reduxjs/toolkit/query/react"; +import toast from "react-hot-toast"; +import BASE_URL from "../../config"; +import { setReady } from "../../slices/ui"; +import { + fullfillChannels, + addChannel, + removeChannel, +} from "../../slices/channels"; +import { updateUsersVersion } from "../../slices/footprint"; +import { updateUsersByLogs, updateUsersStatus } from "../../slices/contacts"; +import { resetAuthData } from "../../slices/auth.data"; +import baseQuery from "../base.query"; +import chatMessageHandler from "./chat.handler"; +const getQueryString = (params = {}) => { + const sp = new URLSearchParams(); + Object.entries(params).forEach(([key, val]) => { + if (val) { + sp.append(key, val); + } + }); + return sp.toString(); +}; +export const streamingApi = createApi({ + reducerPath: "streamingApi", + baseQuery, + refetchOnMountOrArgChange: true, + tagTypes: ["streaming"], + endpoints: (builder) => ({ + initStreaming: builder.query({ + query: () => ({ url: `user/me?ts=${new Date().getTime()}` }), + async onCacheEntryAdded( + arg, + { dispatch, getState, cacheDataLoaded, cacheEntryRemoved } + ) { + // todo + const { + authData: { token }, + footprint: { usersVersion, afterMid }, + } = getState(); + // create a websocket connection when the cache subscription starts + const streaming = new EventSource( + `${BASE_URL}/user/events?${getQueryString({ + "api-key": token, + users_version: usersVersion, + after_mid: afterMid, + })}` + ); + console.log("cache loaded 1"); + try { + // wait for the initial query to resolve before proceeding + await cacheDataLoaded; + console.log("cache loaded 2"); + + streaming.onmessage = (evt) => { + const { + ui: { ready }, + authData: { uid: loginUid }, + } = getState(); + const data = JSON.parse(evt.data); + const { type } = data; + switch (type) { + case "heartbeat": + console.log("heartbeat"); + break; + case "ready": + console.log("streaming ready"); + dispatch(setReady()); + break; + case "users_snapshot": + { + console.log("users snapshot"); + const { version } = data; + dispatch(updateUsersVersion(version)); + } + break; + case "users_log": + { + console.log("users change logs"); + const { logs } = data; + dispatch(updateUsersByLogs(logs)); + } + break; + case "users_state": + case "users_state_changed": + { + let { type, ...rest } = data; + const onlines = + type == "users_state_changed" ? [rest] : rest.users; + dispatch(updateUsersStatus(onlines)); + } + break; + case "kick": + { + console.log("kicked"); + switch (data.reason) { + case "login_from_other_device": + dispatch(resetAuthData()); + toast("kicked from the other device"); + break; + case "delete_user": + dispatch(resetAuthData()); + toast("sorry, your account has been deleted"); + break; + default: + break; + } + } + break; + case "related_groups": + console.log("fullfill channels from streaming", data); + dispatch(fullfillChannels(data.groups)); + break; + case "joined_group": + console.log("joined group", data.group); + dispatch(addChannel(data.group)); + break; + case "kick_from_group": + console.log("kicked from group", data.gid); + dispatch(removeChannel(data.gid)); + break; + case "chat": + { + chatMessageHandler(data, dispatch, { ready, loginUid }); + } + break; + + default: + console.log("sse event data", data); + break; + } + }; + streaming.onopen = () => { + console.info("sse opened"); + }; + streaming.onerror = (err) => { + switch (err.eventPhase) { + case EventSource.CLOSED: + console.log("sse error closed error"); + break; + case EventSource.CONNECTING: + console.log("sse error connecting error"); + // streamingApi.util.resetApiState({ type: "initStreaming" }); + streamingApi.util.invalidateTags("streaming"); + streamingApi.util.updateQueryData("initStreaming"); + break; + + default: + console.error("sse error error", err); + // renewToken({ token, refreshToken }); + break; + } + }; + } catch { + // no-op in case `cacheEntryRemoved` resolves before `cacheDataLoaded`, + // in which case `cacheDataLoaded` will throw + } + // cacheEntryRemoved will resolve when the cache subscription is no longer active + await cacheEntryRemoved; + // perform cleanup steps once the `cacheEntryRemoved` promise resolves + console.log("close streaming"); + streaming.close(); + }, + }), + }), +}); + +export const { + useInitStreamingQuery, + useLazyInitStreamingQuery, +} = streamingApi; diff --git a/src/app/slices/auth.data.js b/src/app/slices/auth.data.js index cd6b21c8..485a4757 100644 --- a/src/app/slices/auth.data.js +++ b/src/app/slices/auth.data.js @@ -1,13 +1,7 @@ import { createSlice } from "@reduxjs/toolkit"; -import BASE_URL, { - KEY_REFRESH_TOKEN, - KEY_TOKEN, - KEY_UID, - KEY_EXPIRE, -} from "../config"; -import { getNonNullValues } from "../../common/utils"; +import { KEY_REFRESH_TOKEN, KEY_TOKEN, KEY_UID, KEY_EXPIRE } from "../config"; const initialState = { - user: null, + uid: null, token: localStorage.getItem(KEY_TOKEN), expireTime: localStorage.getItem(KEY_EXPIRE) || new Date().getTime(), refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN), @@ -17,8 +11,13 @@ const authDataSlice = createSlice({ initialState, reducers: { setAuthData(state, action) { - const { user, token, refresh_token, expired_in = 0 } = action.payload; - state.user = user; + const { + user: { uid }, + token, + refresh_token, + expired_in = 0, + } = action.payload; + state.uid = uid; state.token = token; state.refreshToken = refresh_token; // 当前时间往后推expire时长 @@ -29,43 +28,20 @@ const authDataSlice = createSlice({ localStorage.setItem(KEY_EXPIRE, expireTime); localStorage.setItem(KEY_TOKEN, token); localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); - localStorage.setItem(KEY_UID, user.uid); + localStorage.setItem(KEY_UID, uid); }, - setUserData(state, action) { - const user = action.payload; - state.user = user; - }, - updateLoginedUserByLogs(state, action) { - const logs = action.payload; - logs.forEach(({ action, uid, ...rest }) => { - switch (action) { - case "update": - { - const vals = getNonNullValues(rest); - console.log("update vals", vals); - if (Object.keys(vals).includes("avatar_updated_at")) { - vals.avatar = `${BASE_URL}/resource/avatar?uid=${uid}&t=${vals.avatar_updated_at}`; - } - state.user = { ...state.user, ...vals }; - } - - break; - - default: - break; - } - }); - }, - clearAuthData(state) { + resetAuthData() { console.log("clear auth data"); - state.user = null; - 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); + return initialState; + }, + setUid(state, action) { + const uid = action.payload; + state.uid = uid; }, updateToken(state, action) { const { token, refresh_token, expired_in } = action.payload; @@ -81,10 +57,9 @@ const authDataSlice = createSlice({ }, }); export const { - updateToken, setAuthData, - setUserData, - clearAuthData, - updateLoginedUserByLogs, + resetAuthData, + setUid, + updateToken, } = authDataSlice.actions; export default authDataSlice.reducer; diff --git a/src/app/slices/channels.js b/src/app/slices/channels.js index 5df92004..8490c549 100644 --- a/src/app/slices/channels.js +++ b/src/app/slices/channels.js @@ -1,55 +1,53 @@ import { createSlice } from "@reduxjs/toolkit"; -const initialState = {}; +const initialState = { + ids: [], + byId: {}, +}; const channelsSlice = createSlice({ name: `channels`, initialState, reducers: { - clearChannels() { + resetChannels() { return initialState; }, - setChannels(state, action) { + fullfillChannels(state, action) { console.log("set channels store", state); const chs = action.payload || []; - return Array.isArray(chs) - ? Object.fromEntries( - chs.map((c) => { - const { gid, ...rest } = c; - return [gid, rest]; - }) - ) - : chs; - }, - - updateChannel(state, action) { - // console.log("set channels store", action); - const { id, name, description } = action.payload; - const oObj = state[id]; - const newObj = { ...oObj, name, description }; - state[id] = newObj; + state.ids = chs.map(({ gid }) => gid); + state.byId = Object.fromEntries( + chs.map((c) => { + const { gid } = c; + return [gid, c]; + }) + ); }, addChannel(state, action) { // console.log("set channels store", action); const ch = action.payload; const { gid, ...rest } = ch; - state[gid] = rest; + state.ids.push(gid); + state.byId[gid] = rest; }, - deleteChannel(state, action) { + updateChannel(state, action) { + // console.log("set channels store", action); + const { id, ...rest } = action.payload; + state.byId[id] = { ...state.byId[id], ...rest }; + }, + removeChannel(state, action) { const gid = action.payload; - delete state[gid]; + const idx = state.ids.findIndex((i) => i == gid); + if (idx > -1) { + state.ids.splice(idx, 1); + delete state.byId[gid]; + } }, - // clearAuthData(state) { - // console.log("clear auth data"); - // state.user = null; - // state.token = null; - // state.refreshToken = null; - // }, }, }); export const { - clearChannels, - setChannels, + resetChannels, + fullfillChannels, addChannel, - deleteChannel, updateChannel, + removeChannel, } = channelsSlice.actions; export default channelsSlice.reducer; diff --git a/src/app/slices/contacts.js b/src/app/slices/contacts.js index 97c96238..bb9e89a6 100644 --- a/src/app/slices/contacts.js +++ b/src/app/slices/contacts.js @@ -1,21 +1,31 @@ import { createSlice } from "@reduxjs/toolkit"; import { getNonNullValues } from "../../common/utils"; -const initialState = []; +const initialState = { + ids: [], + byId: {}, +}; const contactsSlice = createSlice({ name: `contacts`, initialState, reducers: { - clearContacts() { + resetContacts() { return initialState; }, - setContacts(state, action) { - console.log("set Contacts store", state); + fullfillContacts(state, action) { + console.log("set Contacts store", action); const contacts = action.payload || []; - return contacts; + state.ids = contacts.map(({ uid }) => uid); + state.byId = Object.fromEntries( + contacts.map((c) => { + const { uid } = c; + return [uid, c]; + }) + ); }, removeContact(state, action) { const uid = action.payload; - return state.filter((c) => c.uid != uid); + state.ids = state.ids.filter((i) => i != uid); + delete state.byId[uid]; }, updateUsersByLogs(state, action) { const changeLogs = action.payload; @@ -24,34 +34,25 @@ const contactsSlice = createSlice({ case "update": { const vals = getNonNullValues(rest); - const curr = state.find(({ uid: id }) => id == uid); - console.log("update vals", vals, curr); - if (curr) { + if (state.byId[uid]) { Object.keys(vals).forEach((k) => { - curr[k] = vals[k]; + state.byId[uid][k] = vals[k]; }); } } break; case "create": { - const idx = state.findIndex((o) => { - return o.uid == uid; - }); - if (idx > -1) { - state.splice(idx, 1, { uid, ...rest }); - } else { - state.push({ uid, ...rest }); - } + state.byId[uid] = { uid, ...rest }; + state.ids.push(uid); } break; case "delete": { - const idx = state.findIndex((o) => { - return o.uid == uid; - }); + const idx = state.ids.findIndex((i) => i == uid); if (idx > -1) { - state.splice(idx, 1); + state.ids.splice(idx, 1); + delete state.byId[uid]; } } break; @@ -63,21 +64,19 @@ const contactsSlice = createSlice({ }, updateUsersStatus(state, action) { const onlines = action.payload; - onlines.forEach((item) => { - const { uid, online = false } = item; - const curr = state.find(({ uid: id }) => id == uid); + onlines.forEach((data) => { + const { uid, online = false } = data; // console.log("update user status", curr, online); - if (curr) { - curr.online = online; + if (state.byId[uid]) { + state.byId[uid].online = online; } }); }, }, }); export const { - clearContacts, - setContacts, - removeContact, + resetContacts, + fullfillContacts, updateUsersByLogs, updateUsersStatus, } = contactsSlice.actions; diff --git a/src/app/slices/footprint.js b/src/app/slices/footprint.js new file mode 100644 index 00000000..51d576dd --- /dev/null +++ b/src/app/slices/footprint.js @@ -0,0 +1,32 @@ +import { createSlice } from "@reduxjs/toolkit"; +const initialState = { + usersVersion: 0, + afterMid: 0, +}; +const footprintSlice = createSlice({ + name: "footprint", + initialState, + reducers: { + resetFootprint() { + return initialState; + }, + fullfillFootprint(state, action) { + return action.payload; + }, + updateUsersVersion(state, action) { + const usersVersion = action.payload; + state.usersVersion = usersVersion; + }, + updateAfterMid(state, action) { + const afterMid = action.payload; + state.afterMid = afterMid; + }, + }, +}); +export const { + resetFootprint, + fullfillFootprint, + updateAfterMid, + updateUsersVersion, +} = footprintSlice.actions; +export default footprintSlice.reducer; diff --git a/src/app/slices/message.channel.js b/src/app/slices/message.channel.js index 3ed05c01..24ac1695 100644 --- a/src/app/slices/message.channel.js +++ b/src/app/slices/message.channel.js @@ -1,67 +1,38 @@ import { createSlice } from "@reduxjs/toolkit"; -import { - msgReaction, - msgAdd, - msgSetRead, - msgClearUnread, - msgUpdate, - msgDelete, - msgAddPending, - msgRemovePending, - msgReplacePending, -} from "./message.handler"; const initialState = {}; const channelMsgSlice = createSlice({ name: "channelMessage", initialState, reducers: { - clearChannelMsg() { + resetChannelMsg() { return initialState; }, - initChannelMsg(state, action) { + fullfillChannelMsg(state, action) { return action.payload; }, addChannelMsg(state, action) { - msgAdd(state, action.payload); + const { id, mid } = action.payload; + if (state[id]) { + if (state[id].findIndex((id) => id == mid) > -1) return; + state[id].push(mid); + } else { + state[id] = [mid]; + } }, - deleteChannelMsg(state, action) { - msgDelete(state, action.payload); - }, - updateChannelMsg(state, action) { - msgUpdate(state, action.payload); - }, - likeChannelMsg(state, action) { - msgReaction(state, action.payload); - }, - setChannelMsgRead(state, action) { - msgSetRead(state, action.payload); - }, - clearChannelMsgUnread(state, action) { - msgClearUnread(state, action.payload); - }, - addChannelPendingMsg(state, action) { - msgAddPending(state, action.payload); - }, - replaceChannelPendingMsg(state, action) { - msgReplacePending(state, action.payload); - }, - removeChannelPendingMsg(state, action) { - msgRemovePending(state, action.payload); + removeChannelMsg(state, action) { + const { id, mid } = action.payload; + if (state[id]) { + const idx = state[id].findIndex((i) => i == mid); + state[id].splice(idx, 1); + } }, }, }); export const { - updateChannelMsg, - deleteChannelMsg, - likeChannelMsg, - clearChannelMsg, - initChannelMsg, - clearChannelMsgUnread, - setChannelMsgRead, + resetChannelMsg, + fullfillChannelMsg, addChannelMsg, - addChannelPendingMsg, - replaceChannelPendingMsg, - removeChannelPendingMsg, + removeChannelMsg, } = channelMsgSlice.actions; export default channelMsgSlice.reducer; diff --git a/src/app/slices/message.handler.js b/src/app/slices/message.handler.js deleted file mode 100644 index f45c4cf1..00000000 --- a/src/app/slices/message.handler.js +++ /dev/null @@ -1,130 +0,0 @@ -import { isObjectEqual } from "../../common/utils"; - -export const msgReaction = (state, payload) => { - const { id, from_uid, mid, action: reaction } = payload; - console.log("msg reaction: likes", id, mid, from_uid, reaction); - if (state[id] && state[id][mid]) { - if (!state[id][mid].likes) { - console.log("msg reaction: initial "); - state[id][mid].likes = {}; - } - const currLikes = state[id][mid].likes; - // state[id][mid].likes = currLikes ? [...currLikes, reaction] : [reaction]; - if (currLikes[reaction]) { - if (currLikes[reaction].includes(from_uid)) { - const idx = currLikes[reaction].findIndex((id) => { - return id == from_uid; - }); - console.log("remove reaction", currLikes[reaction], idx, from_uid); - currLikes[reaction].splice(idx, 1); - } else { - currLikes[reaction].push(from_uid); - } - } else { - currLikes[reaction] = [from_uid]; - } - // state[id][mid].likes = currLikes; - } -}; - -export const msgAdd = (state, payload) => { - const { - id, - 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]; - // 如果存在,并且新消息和缓存消息不一样,则替换掉,并且改为已读(可能有问题) - if (replaceMsg) { - const copyMsg = { ...replaceMsg }; - if (!isObjectEqual(copyMsg, newMsg)) { - state[id][mid] = { ...newMsg, unread: false }; - } - } else { - state[id][mid] = newMsg; - } - } else { - state[id] = { [mid]: newMsg }; - } -}; -export const msgAddPending = (state, payload) => { - const { - id, - content, - created_at, - local_mid, - reply_mid = null, - from_uid, - content_type, - unread = false, - } = 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]) { - state[id][local_mid] = newMsg; - } else { - state[id] = { [local_mid]: newMsg }; - } -}; -export const msgReplacePending = (state, payload) => { - const { id, local_mid, server_mid } = payload; - if (state[id] && state[id][local_mid]) { - // 先赋值,再去delete - state[id] = Object.fromEntries( - Object.entries(state[id]).map(([key, obj]) => { - return key == local_mid ? [server_mid, obj] : [key, obj]; - }) - ); - // state[id][server_mid] = { ...state[id][local_mid] }; - // delete state[id][local_mid]; - } -}; -export const msgRemovePending = (state, payload) => { - const { id, local_mid } = payload; - if (state[id] && state[id][local_mid]) { - delete state[id][local_mid]; - } -}; -export const msgSetRead = (state, payload) => { - const { id, mid } = payload; - console.log("set read", id, mid); - if (state[id] && state[id][mid]) { - state[id][mid].unread = false; - } -}; -export const msgDelete = (state, payload) => { - const { id, mid } = payload; - console.log("delete message", id, mid); - if (state[id][mid]) { - // state[id][mid].removed = true; - delete state[id][mid]; - } -}; -export const msgClearUnread = (state, id) => { - console.log("set all unread", id); - Object.entries(state[id]).forEach(([, obj]) => { - obj.unread = false; - }); -}; -export const msgUpdate = (state, payload) => { - const { id, mid, content, time } = payload; - console.log("update channel message", id, mid); - if (state[id][mid]) { - state[id][mid].content = content; - state[id][mid].edited = time; - } -}; diff --git a/src/app/slices/message.js b/src/app/slices/message.js new file mode 100644 index 00000000..e91a134e --- /dev/null +++ b/src/app/slices/message.js @@ -0,0 +1,64 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = { + replying: {}, +}; +const messageSlice = createSlice({ + name: "message", + initialState, + reducers: { + resetMessage() { + return initialState; + }, + fullfillMessage(state, action) { + return action.payload; + }, + updateMessage(state, action) { + const { mid, ...rest } = action.payload; + state[mid] = { ...state[mid], ...rest }; + }, + readMessage(state, action) { + const mids = Array.isArray(action.payload) + ? action.payload + : [action.payload]; + mids.forEach((id) => { + if (state[id]) { + state[id].read = true; + } + }); + }, + addMessage(state, action) { + const { mid, sending } = action.payload; + // 如果是正发送,并且已存在,则不覆盖 + if (sending && state[mid]) return; + state[mid] = action.payload; + }, + removeMessage(state, action) { + const mid = action.payload; + delete state[mid]; + }, + addReplyingMessage(state, action) { + const { id, mid } = action.payload; + console.log("to ", id, mid); + state.replying[id] = mid; + }, + removeReplyingMessage(state, action) { + const id = action.payload; + if (state.replying[id]) { + delete state.replying[id]; + } + }, + }, +}); +export const { + resetMessage, + fullfillMessage, + setMessage, + updateMessage, + readMessage, + addMessage, + removeMessage, + addReplyingMessage, + removeReplyingMessage, +} = messageSlice.actions; +export default messageSlice.reducer; diff --git a/src/app/slices/message.pending.js b/src/app/slices/message.pending.js deleted file mode 100644 index f61dcff8..00000000 --- a/src/app/slices/message.pending.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createSlice } from "@reduxjs/toolkit"; - -const initialState = { - reply: {}, - user: {}, - channel: {}, -}; -const pendingMessageSlice = createSlice({ - name: "pendingMessage", - initialState, - reducers: { - clearPendingMsg() { - return initialState; - }, - addPendingMessage(state, action) { - const { type = "user", msg } = action.payload; - const { id, mid } = msg; - const curr = state[type][id] || {}; - 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); - delete state[type][id][mid]; - }, - }, -}); -export const { - clearPendingMsg, - addPendingMessage, - removePendingMessage, - removeReplyMessage, - setReplyMessage, -} = pendingMessageSlice.actions; -export default pendingMessageSlice.reducer; diff --git a/src/app/slices/message.reaction.js b/src/app/slices/message.reaction.js new file mode 100644 index 00000000..431e00d1 --- /dev/null +++ b/src/app/slices/message.reaction.js @@ -0,0 +1,39 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = {}; +const reactionMessageSlice = createSlice({ + name: "reactionMessage", + initialState, + reducers: { + resetReactionMessage() { + return initialState; + }, + fullfillReactionMessage(state, action) { + return action.payload; + }, + toggleReactionMessage(state, action) { + const { from_uid, mid, action: reaction } = action.payload; + console.log("msg reaction", mid, from_uid, reaction); + if (!state[mid]) { + state[mid] = {}; + } + if (state[mid][reaction]) { + const reactionUids = state[mid][reaction]; + const idx = reactionUids.findIndex((id) => id == from_uid); + if (idx > -1) { + reactionUids.splice(idx, 1); + } else { + reactionUids.push(from_uid); + } + } else { + state[mid][reaction] = [from_uid]; + } + }, + }, +}); +export const { + resetReactionMessage, + fullfillReactionMessage, + toggleReactionMessage, +} = reactionMessageSlice.actions; +export default reactionMessageSlice.reducer; diff --git a/src/app/slices/message.user.js b/src/app/slices/message.user.js index 0de3fee3..200fab27 100644 --- a/src/app/slices/message.user.js +++ b/src/app/slices/message.user.js @@ -1,61 +1,42 @@ import { createSlice } from "@reduxjs/toolkit"; -import { - msgReaction, - msgAdd, - msgSetRead, - msgUpdate, - msgDelete, - msgAddPending, - msgRemovePending, - msgReplacePending, -} from "./message.handler"; -const initialState = {}; +const initialState = { + ids: [], + byId: {}, +}; const userMsgSlice = createSlice({ name: "userMessage", initialState, reducers: { - clearUserMsg() { + resetUserMsg() { return initialState; }, - initUserMsg(state, action) { - return action.payload; + fullfillUserMsg(state, action) { + state.ids = Object.keys(action.payload); + state.byId = action.payload; }, addUserMsg(state, action) { - msgAdd(state, action.payload); + const { id, mid } = action.payload; + if (state.byId[id]) { + if (state.byId[id].findIndex((id) => id == mid) > -1) return; + state.byId[id].push(mid); + } else { + state.byId[id] = [mid]; + state.ids.push(id); + } }, - likeUserMsg(state, action) { - msgReaction(state, action.payload); - }, - updateUserMsg(state, action) { - msgUpdate(state, action.payload); - }, - deleteUserMsg(state, action) { - msgDelete(state, action.payload); - }, - setUserMsgRead(state, action) { - msgSetRead(state, action.payload); - }, - addUserPendingMsg(state, action) { - msgAddPending(state, action.payload); - }, - replaceUserPendingMsg(state, action) { - msgReplacePending(state, action.payload); - }, - removeUserPendingMsg(state, action) { - msgRemovePending(state, action.payload); + removeUserMsg(state, action) { + const { id, mid } = action.payload; + if (state.byId[id]) { + const idx = state.byId[id].findIndex((i) => i == mid); + state.byId[id].splice(idx, 1); + } }, }, }); export const { - updateUserMsg, - likeUserMsg, - deleteUserMsg, - clearUserMsg, - initUserMsg, + resetUserMsg, + fullfillUserMsg, addUserMsg, - setUserMsgRead, - addUserPendingMsg, - replaceUserPendingMsg, - removeUserPendingMsg, + removeUserMsg, } = userMsgSlice.actions; export default userMsgSlice.reducer; diff --git a/src/app/slices/readme.md b/src/app/slices/readme.md new file mode 100644 index 00000000..096881d3 --- /dev/null +++ b/src/app/slices/readme.md @@ -0,0 +1,7 @@ +reset +fullfill +set +add +update +remove +toggle \ No newline at end of file diff --git a/src/app/slices/visit.mark.js b/src/app/slices/visit.mark.js deleted file mode 100644 index 3f643a72..00000000 --- a/src/app/slices/visit.mark.js +++ /dev/null @@ -1,38 +0,0 @@ -import { createSlice } from "@reduxjs/toolkit"; -import { KEY_AFTER_MID, KEY_USERS_VERSION, KEY_UID } from "../config"; -const initialState = { - usersVersion: null, - afterMid: null, -}; -const visitMarkSlice = createSlice({ - name: "visitMark", - initialState, - reducers: { - clearMark() { - return initialState; - }, - setMark(state, action) { - const mark = action.payload; - return mark; - }, - setUsersVersion(state, action) { - const { version } = action.payload; - state.usersVersion = version; - let currUid = localStorage.getItem(KEY_UID); - localStorage.setItem(`${KEY_USERS_VERSION}_${currUid}`, version); - }, - setAfterMid(state, action) { - const { mid } = action.payload; - state.afterMid = mid; - let currUid = localStorage.getItem(KEY_UID); - localStorage.setItem(`${KEY_AFTER_MID}_${currUid}`, mid); - }, - }, -}); -export const { - setMark, - setUsersVersion, - setAfterMid, - clearMark, -} = visitMarkSlice.actions; -export default visitMarkSlice.reducer; diff --git a/src/app/store.js b/src/app/store.js index e78e38dc..7fff0bdf 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -2,33 +2,37 @@ import { configureStore, combineReducers } from "@reduxjs/toolkit"; import { setupListeners } from "@reduxjs/toolkit/query"; import listenerMiddleware from "./listener.middleware"; import authDataReducer from "./slices/auth.data"; -import visitMarkReducer from "./slices/visit.mark"; +import footprintReducer from "./slices/footprint"; import uiReducer from "./slices/ui"; import channelsReducer from "./slices/channels"; import contactsReducer from "./slices/contacts"; -import pendingMsgReducer from "./slices/message.pending"; +import reactionMsgReducer from "./slices/message.reaction"; import channelMsgReducer from "./slices/message.channel"; import userMsgReducer from "./slices/message.user"; +import messageReducer from "./slices/message"; import { authApi } from "./services/auth"; import { contactApi } from "./services/contact"; import { channelApi } from "./services/channel"; import { messageApi } from "./services/message"; import { serverApi } from "./services/server"; +import { streamingApi } from "./services/streaming"; const reducer = combineReducers({ + authData: authDataReducer, ui: uiReducer, - visitMark: visitMarkReducer, + footprint: footprintReducer, contacts: contactsReducer, channels: channelsReducer, - pendingMessage: pendingMsgReducer, + reactionMessage: reactionMsgReducer, userMessage: userMsgReducer, channelMessage: channelMsgReducer, - authData: authDataReducer, + message: messageReducer, [authApi.reducerPath]: authApi.reducer, [messageApi.reducerPath]: messageApi.reducer, [contactApi.reducerPath]: contactApi.reducer, [channelApi.reducerPath]: channelApi.reducer, [serverApi.reducerPath]: serverApi.reducer, + [streamingApi.reducerPath]: streamingApi.reducer, }); const store = configureStore({ @@ -40,7 +44,8 @@ const store = configureStore({ contactApi.middleware, channelApi.middleware, serverApi.middleware, - messageApi.middleware + messageApi.middleware, + streamingApi.middleware ) .prepend(listenerMiddleware.middleware), }); diff --git a/src/common/component/Avatar.js b/src/common/component/Avatar.js index 6a1bcd50..3f04aa06 100644 --- a/src/common/component/Avatar.js +++ b/src/common/component/Avatar.js @@ -1,7 +1,8 @@ import { useState, useEffect } from "react"; import { getInitials, getInitialsAvatar } from "../utils"; export default function Avatar({ url, name = "unkonw name", ...rest }) { - const [src, setSrc] = useState(url); + // console.log("avatar url", url); + const [src, setSrc] = useState(""); const handleError = () => { const tmp = getInitialsAvatar({ initials: getInitials(name), @@ -14,6 +15,8 @@ export default function Avatar({ url, name = "unkonw name", ...rest }) { initials: getInitials(name), }); setSrc(tmp); + } else { + setSrc(url); } }, [url, name]); diff --git a/src/common/component/ChannelModal/index.js b/src/common/component/ChannelModal/index.js index 1870a1e7..6e3c04df 100644 --- a/src/common/component/ChannelModal/index.js +++ b/src/common/component/ChannelModal/index.js @@ -15,6 +15,9 @@ import { addChannel } from "../../../app/slices/channels"; import { useCreateChannelMutation } from "../../../app/services/channel"; export default function ChannelModal({ personal = false, closeModal }) { + const { conactsData, loginUid } = useSelector((store) => { + return { conactsData: store.contacts.byId, loginUid: store.authData.uid }; + }); const navigateTo = useNavigate(); const dispatch = useDispatch(); const [data, setData] = useState({ @@ -28,9 +31,7 @@ export default function ChannelModal({ personal = false, closeModal }) { createChannel, { isSuccess, isError, isLoading, data: newChannel }, ] = useCreateChannelMutation(); - const currentUser = useSelector((state) => { - return state.authData.user; - }); + const handleToggle = () => { const { is_public } = data; setData((prev) => { @@ -48,6 +49,7 @@ export default function ChannelModal({ personal = false, closeModal }) { } createChannel(data); }; + useEffect(() => { if (isError) { toast.error("create new channel failed"); @@ -83,7 +85,8 @@ export default function ChannelModal({ personal = false, closeModal }) { console.log({ data }); }; console.log("contacts", contacts); - if (!currentUser) return null; + const loginUser = conactsData[loginUid]; + if (!loginUser) return null; const { name, members, is_public } = data; return ( @@ -142,17 +145,14 @@ export default function ChannelModal({ personal = false, closeModal }) { - {/* admin or */} - { -
- Private Channel - -
- } +
+ Private Channel + +
- {expand && ( -
- mic icon - sound icon -
- )} + {/* {expand && ( */} +
+ mic icon + sound icon +
+ {/* )} */} ); } diff --git a/src/common/component/ManageMembers.js b/src/common/component/ManageMembers.js index 19651d80..3b3f5373 100644 --- a/src/common/component/ManageMembers.js +++ b/src/common/component/ManageMembers.js @@ -1,6 +1,7 @@ import { useState, useRef, useEffect } from "react"; import styled from "styled-components"; import { useOutsideClick } from "rooks"; +import { useSelector } from "react-redux"; import toast from "react-hot-toast"; import useCopy from "../hook/useCopy"; import { useLazyDeleteContactQuery } from "../../app/services/contact"; @@ -85,6 +86,7 @@ const StyledWrapper = styled.section` } `; export default function ManageMembers({ members = [] }) { + const contacts = useSelector((store) => store.contacts); const [copied, copy] = useCopy(); const [remove, { isSuccess: removeSuccess }] = useLazyDeleteContactQuery(); const wrapperRef = useRef(null); @@ -111,6 +113,7 @@ export default function ManageMembers({ members = [] }) { const handleCopy = (str) => { copy(str); }; + const uids = !members || members.length == 0 ? contacts.ids : members; return (
@@ -121,12 +124,12 @@ export default function ManageMembers({ members = [] }) {

    - {members.map((m) => { - const { name, email, uid, is_admin } = m; + {uids.map((uid) => { + const { name, email, is_admin } = contacts.byId[uid]; return (
  • - +
    {name} {email} diff --git a/src/common/component/Message/Commands.js b/src/common/component/Message/Commands.js index 5701be9d..13241b00 100644 --- a/src/common/component/Message/Commands.js +++ b/src/common/component/Message/Commands.js @@ -1,9 +1,9 @@ import { useState, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; import styled from "styled-components"; -import toast from "react-hot-toast"; +// import toast from "react-hot-toast"; import { useOutsideClick } from "rooks"; -import { setReplyMessage } from "../../../app/slices/message.pending"; +import { addReplyingMessage } from "../../../app/slices/message"; import StyledMenu from "../StyledMenu"; import DeleteMessageConfirm from "./DeleteMessageConfirm"; import EmojiPicker from "./EmojiPicker"; @@ -41,10 +41,8 @@ const StyledCmds = styled.ul` `; export default function Commands({ contextId = 0, - message = null, mid = 0, - uid = 0, - reactions = [], + from_uid = 0, menuVisible, toggleMenu, emojiPopVisible, @@ -54,12 +52,12 @@ export default function Commands({ const dispatch = useDispatch(); const [deleteModalVisible, setDeleteModalVisible] = useState(false); - const currUid = useSelector((store) => store.authData.user.uid); + const currUid = useSelector((store) => store.authData.uid); const menuRef = useRef(null); const handleReply = () => { if (contextId) { - dispatch(setReplyMessage({ id: contextId, msg: message })); + dispatch(addReplyingMessage({ id: contextId, mid })); } // toast.success("cooming soon"); }; @@ -78,13 +76,9 @@ export default function Commands({ />
  • {emojiPopVisible && ( - + )} - {currUid == uid ? ( + {currUid == from_uid ? (
  • Edit Message
  • */}
  • Pin Message
  • Reply
  • - {currUid == uid && ( + {currUid == from_uid && (
  • Delete Message
  • @@ -118,10 +112,7 @@ export default function Commands({ )} {deleteModalVisible && ( - + )} ); diff --git a/src/common/component/Message/DeleteMessageConfirm.js b/src/common/component/Message/DeleteMessageConfirm.js index ebd874a3..60392b38 100644 --- a/src/common/component/Message/DeleteMessageConfirm.js +++ b/src/common/component/Message/DeleteMessageConfirm.js @@ -7,7 +7,7 @@ import StyledModal from "../styled/Modal"; import Button from "../styled/Button"; import Modal from "../Modal"; import PreviewMessage from "./PreviewMessage"; -export default function LogoutConfirmModal({ closeModal, message = null }) { +export default function DeleteMessageConfirmModal({ closeModal, mid = 0 }) { // const dispatch = useDispatch(); const [deleteMessage, { isLoading, isSuccess }] = useLazyDeleteMessageQuery(); const handleDelete = (evt) => { @@ -21,8 +21,7 @@ export default function LogoutConfirmModal({ closeModal, message = null }) { } }, [isSuccess]); - if (!message) return; - const { mid, ...previewContent } = message; + if (!mid) return null; return ( - + ); diff --git a/src/common/component/Message/EmojiPicker.js b/src/common/component/Message/EmojiPicker.js index f6efd216..07143b90 100644 --- a/src/common/component/Message/EmojiPicker.js +++ b/src/common/component/Message/EmojiPicker.js @@ -3,7 +3,8 @@ import { useRef } from "react"; import styled from "styled-components"; import { useOutsideClick } from "rooks"; -import { useLikeMessageMutation } from "../../../app/services/message"; +import { useSelector } from "react-redux"; +import { useReactMessageMutation } from "../../../app/services/message"; const StyledPicker = styled.div` border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 6px; @@ -32,13 +33,19 @@ const StyledPicker = styled.div` } `; const emojis = { - thumb_up: "👍", - ok: "👌", - like: "❤️", + ["U+1F44D"]: "👍", + ["U+1F44C"]: "👌", + ["U+2764"]: "❤️", }; -export default function EmojiPicker({ mid, reactions = [], hidePicker }) { +export default function EmojiPicker({ mid, hidePicker }) { const wrapperRef = useRef(null); - const [reactMessage, { isLoading }] = useLikeMessageMutation(); + const [reactMessage, { isLoading }] = useReactMessageMutation(); + const { reactionData, currUid } = useSelector((store) => { + return { + reactionData: store.reactionMessage[mid], + currUid: store.authData.uid, + }; + }); useOutsideClick(wrapperRef, hidePicker); const handleReact = (action) => { console.log("react", action); @@ -48,9 +55,14 @@ export default function EmojiPicker({ mid, reactions = [], hidePicker }) {
      {Object.entries(emojis).map(([key, emoji]) => { + let reacted = + reactionData && + reactionData[key] && + reactionData[key].includes(currUid); + return (
    • diff --git a/src/common/component/Message/PreviewMessage.js b/src/common/component/Message/PreviewMessage.js index d91bc163..dd093a78 100644 --- a/src/common/component/Message/PreviewMessage.js +++ b/src/common/component/Message/PreviewMessage.js @@ -1,12 +1,16 @@ // import { useEffect, useRef, useState } from "react"; import dayjs from "dayjs"; -// import { useSelector } from "react-redux"; import renderContent from "./renderContent"; import Avatar from "../Avatar"; import StyledWrapper from "./styled"; -export default function PreviewMessage({ data = null }) { - if (!data) return null; - const { avatar, name, time, content_type, content } = data; +import { useSelector } from "react-redux"; +export default function PreviewMessage({ mid = 0 }) { + const { msg, contactsData } = useSelector((store) => { + return { msg: store.message[mid], contactsData: store.contacts.byId }; + }); + if (!msg) return null; + const { from_uid, created_at, content_type, content } = msg; + const { name, avatar } = contactsData[from_uid]; return (
      @@ -15,7 +19,9 @@ export default function PreviewMessage({ data = null }) {
      {name} - {dayjs(time).format("YYYY-MM-DD h:mm:ss A")} + + {dayjs(created_at).format("YYYY-MM-DD h:mm:ss A")} +
      {renderContent(content_type, content)}
      diff --git a/src/common/component/Message/Reaction.js b/src/common/component/Message/Reaction.js new file mode 100644 index 00000000..8c69ad71 --- /dev/null +++ b/src/common/component/Message/Reaction.js @@ -0,0 +1,54 @@ +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; +import styled from "styled-components"; +import { emojis } from "./EmojiPicker"; +const StyledWrapper = styled.span` + display: flex; + gap: 8px; + font-size: 16px; + /* align-items: center; */ + .reaction { + position: relative; + display: flex; + align-items: center; + gap: 6px; + em { + font-size: 12px; + color: #999; + } + } +`; +export default function Reaction({ reactions = null }) { + // const { + // messageData, + // reactionMessageData, + // contactsData, + // loginedUser, + // } = useSelector((store) => { + // return { + // reactionMessageData: store.reactionMessage, + // messageData: store.message, + // contactsData: store.contacts.byId, + // loginedUser: store.authData.user, + // }; + // }); + + if (!reactions) return null; + return ( + + {Object.entries(reactions).map(([reaction, uids]) => { + return uids.length > 0 ? ( + 1 ? count : ""} + key={reaction} + > + {emojis[reaction]} + + {uids.length > 1 ? {`+${uids.length}`} : null} + + ) : null; + })} + + ); +} diff --git a/src/common/component/Message/Removed.js b/src/common/component/Message/Removed.js deleted file mode 100644 index 244c4876..00000000 --- a/src/common/component/Message/Removed.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -const Styled = styled.div` - display: flex; -`; -export default function Removed() { - return Removed; -} diff --git a/src/common/component/Message/Reply.js b/src/common/component/Message/Reply.js new file mode 100644 index 00000000..40ba8306 --- /dev/null +++ b/src/common/component/Message/Reply.js @@ -0,0 +1,15 @@ +import React from "react"; +import styled from "styled-components"; +import { useSelector } from "react-redux"; + +const Styled = styled.div` + color: #aaa; + font-size: 12px; + margin-bottom: -10px; + /* padding-left: 10px; */ +`; +export default function Reply({ mid }) { + const data = useSelector((store) => store.message[mid]); + if (!data) return null; + return {data.content}; +} diff --git a/src/common/component/Message/index.js b/src/common/component/Message/index.js index 2afeba94..694c14e9 100644 --- a/src/common/component/Message/index.js +++ b/src/common/component/Message/index.js @@ -1,44 +1,34 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useRef, useState, useEffect } from "react"; import dayjs from "dayjs"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; import { useInViewRef } from "rooks"; import Tippy from "@tippyjs/react"; - +import Reaction from "./Reaction"; +import Reply from "./Reply"; import Profile from "../Profile"; import Avatar from "../Avatar"; -import { setChannelMsgRead } from "../../../app/slices/message.channel"; -import { setUserMsgRead } from "../../../app/slices/message.user"; +import { readMessage } from "../../../app/slices/message"; import StyledWrapper from "./styled"; import Commands from "./Commands"; -import { emojis } from "./EmojiPicker"; + import EditMessage from "./EditMessage"; import renderContent from "./renderContent"; -function Message({ - reply = null, - gid = "", - mid = "", - uid, - fromUid, - time, - content, - content_type = "text/plain", - unread = false, - pending, - edited = false, - likes = {}, -}) { +function Message({ contextId = 0, mid = "" }) { const [myRef, inView] = useInViewRef(); const [edit, setEdit] = useState(false); const [emojiPopVisible, setEmojiPopVisible] = useState(false); const [menuVisible, setMenuVisible] = useState(false); const disptach = useDispatch(); const avatarRef = useRef(null); - const { contacts, loginedUser } = useSelector((store) => { - return { - contacts: store.contacts, - loginedUser: store.authData.user, - }; - }); + const { message = {}, reactionMessageData, contactsData } = useSelector( + (store) => { + return { + reactionMessageData: store.reactionMessage, + message: store.message[mid], + contactsData: store.contacts.byId, + }; + } + ); const toggleMenu = () => { setMenuVisible((prev) => !prev); }; @@ -49,20 +39,30 @@ function Message({ setEmojiPopVisible((prev) => !prev); }; // useEffect(() => { - // if (!unread) { + // if (!read) { // avatarRef.current?.scrollIntoView(); // } - // }, [unread]); + // }, [read]); + + // console.log("message", mid, messageData[mid]); useEffect(() => { - if (inView && unread) { - const setMsgRead = gid ? setChannelMsgRead : setUserMsgRead; - disptach(setMsgRead({ id: gid || uid, mid })); + if (inView && !message.read) { + disptach(readMessage(mid)); } - }, [gid, mid, uid, unread, inView]); - - if (!contacts) return null; - const currUser = contacts.find((c) => c.uid == fromUid) || {}; + }, [mid, message, inView]); + if (!message) return null; + const { + reply_mid, + from_uid: fromUid, + created_at: time, + sending, + content, + content_type = "text/plain", + edited, + } = message; + const reactions = reactionMessageData[mid]; + const currUser = contactsData[fromUid] || {}; return ( } + content={} > -
      +
      - {reply &&
      {reply.content}
      } + {reply_mid && }
      {currUser.name} {dayjs(time).format("YYYY-MM-DD h:mm:ss A")} - {likes && ( - - {Object.entries(likes).map(([reaction, uids]) => { - return uids.length > 0 ? ( - 1 ? count : ""} - key={reaction} - > - {emojis[reaction]} - - {uids.length > 1 ? {`+${uids.length}`} : null} - - ) : null; - })} - - )} + {reactions && }
      -
      +
      {edit ? ( {!edit && ( uids.includes(loginedUser.uid)) - .map(([reaction]) => { - return reaction; - })} + contextId={contextId} mid={mid} - uid={fromUid} + from_uid={fromUid} toggleMenu={toggleMenu} menuVisible={menuVisible} emojiPopVisible={emojiPopVisible} diff --git a/src/common/component/Message/styled.js b/src/common/component/Message/styled.js index 19d733b3..2695db3d 100644 --- a/src/common/component/Message/styled.js +++ b/src/common/component/Message/styled.js @@ -35,12 +35,7 @@ const StyledMsg = styled.div` display: flex; flex-direction: column; gap: 8px; - .reply { - color: #aaa; - font-size: 12px; - margin-bottom: -10px; - /* padding-left: 10px; */ - } + .up { display: flex; align-items: center; @@ -57,30 +52,6 @@ const StyledMsg = styled.div` font-size: 12px; line-height: 18px; } - .likes { - display: flex; - gap: 8px; - font-size: 16px; - /* align-items: center; */ - .like { - position: relative; - display: flex; - align-items: center; - gap: 6px; - em { - font-size: 12px; - color: #999; - } - /* &:after { - content: attr(data-count); - position: absolute; - top: -4px; - right: -8px; - font-size: 12px; - color: #999; - } */ - } - } } .down { user-select: text; @@ -95,7 +66,7 @@ const StyledMsg = styled.div` color: #999; font-size: 10px; } - &.pending { + &.sending { opacity: 0.5; } .img { diff --git a/src/common/component/NotificationHub/index.js b/src/common/component/NotificationHub/index.js index 55e672de..a96efe73 100644 --- a/src/common/component/NotificationHub/index.js +++ b/src/common/component/NotificationHub/index.js @@ -22,7 +22,10 @@ import { clearAuthData, updateLoginedUserByLogs, } from "../../../app/slices/auth.data"; -import { setUsersVersion, setAfterMid } from "../../../app/slices/visit.mark"; +import { + updateUsersVersion, + updateAfterMid, +} from "../../../app/slices/visit.mark"; import { setReady } from "../../../app/slices/ui"; import useMessageHandler from "./useMessageHandler"; @@ -117,7 +120,7 @@ const NotificationHub = () => { { console.log("users snapshot"); const { version } = data; - dispatch(setUsersVersion({ version })); + dispatch(updateUsersVersion({ version })); } break; case "users_log": @@ -179,7 +182,7 @@ const NotificationHub = () => { // 更新after_mid if (data.detail.type == "normal") { - dispatch(setAfterMid({ mid: data.mid })); + dispatch(updateAfterMid({ mid: data.mid })); } } break; diff --git a/src/common/component/NotificationHub/useMessageHandler.js b/src/common/component/NotificationHub/useMessageHandler.js index b500544a..5b4fb0a9 100644 --- a/src/common/component/NotificationHub/useMessageHandler.js +++ b/src/common/component/NotificationHub/useMessageHandler.js @@ -51,7 +51,7 @@ export default function useMessageHandler(currUser) { dispatch( addMessage({ id, // 自己发的 就不用标记未读 - unread: !self, + read: !self, ...common, }) ); diff --git a/src/common/component/Profile.js b/src/common/component/Profile.js index e1af5eff..96c370a1 100644 --- a/src/common/component/Profile.js +++ b/src/common/component/Profile.js @@ -1,7 +1,7 @@ // import React from "react"; // import toast from "react-hot-toast"; // import { useState, useEffect } from "react"; - +import { useSelector } from "react-redux"; import { NavLink } from "react-router-dom"; import { BsChatText } from "react-icons/bs"; import { RiUserAddLine } from "react-icons/ri"; @@ -62,9 +62,11 @@ const StyledWrapper = styled.div` } } `; -export default function Profile({ data = null, type = "embed" }) { +export default function Profile({ uid = null, type = "embed" }) { + const data = useSelector((store) => store.contacts.byId[uid]); if (!data) return null; - const { uid, name, email, avatar } = data; + // console.log("profile", data); + const { name, email, avatar } = data; return ( diff --git a/src/common/component/Search.js b/src/common/component/Search.js index 8e3bcb4c..a1843ae1 100644 --- a/src/common/component/Search.js +++ b/src/common/component/Search.js @@ -34,6 +34,7 @@ const StyledWrapper = styled.div` cursor: pointer; } .popup { + z-index: 999; user-select: none; box-shadow: 0px 20px 25px rgba(31, 41, 55, 0.1), 0px 10px 10px rgba(31, 41, 55, 0.04); @@ -63,7 +64,9 @@ const StyledWrapper = styled.div` } `; export default function Search() { - const currentUser = useSelector((store) => store.authData.user); + const currentUser = useSelector( + (store) => store.contacts.byId[store.authData.uid] + ); const [popupVisible, setPopupVisible] = useState(false); const [channelModalVisible, setChannelModalVisible] = useState(false); const [contactsModalVisible, setContactsModalVisible] = useState(false); @@ -85,6 +88,7 @@ export default function Search() { const handleCloseModal = () => { setChannelModalVisible(false); }; + console.log("searching"); return ( {channelModalVisible && ( diff --git a/src/common/component/Send/Replying.js b/src/common/component/Send/Replying.js new file mode 100644 index 00000000..cad99bbe --- /dev/null +++ b/src/common/component/Send/Replying.js @@ -0,0 +1,28 @@ +// import React from 'react' +import { useDispatch, useSelector } from "react-redux"; +import { MdClose } from "react-icons/md"; +import { removeReplyingMessage } from "../../../app/slices/message"; + +export default function Replying({ id, mid }) { + const { msg, contactsData } = useSelector((store) => { + return { contactsData: store.contacts.byId, msg: store.message[mid] }; + }); + const dispatch = useDispatch(); + const removeReply = () => { + dispatch(removeReplyingMessage(id)); + }; + if (!msg) return null; + const { from_uid } = msg; + const user = contactsData[from_uid]; + return ( +
      + + Replying to + {user?.name} + + +
      + ); +} diff --git a/src/common/component/Send/UploadModal/index.js b/src/common/component/Send/UploadModal/index.js index 31588967..5deae794 100644 --- a/src/common/component/Send/UploadModal/index.js +++ b/src/common/component/Send/UploadModal/index.js @@ -2,11 +2,8 @@ import { useState, useEffect } from "react"; import { useSelector } 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 Button from "../../styled/Button"; @@ -19,7 +16,7 @@ export default function UploadModal({ closeModal, }) { // const dispatch = useDispatch(); - const from_uid = useSelector((store) => store.authData.user.uid); + const from_uid = useSelector((store) => store.authData.uid); const [ sendChannelMsg, { isLoading: channelSending }, diff --git a/src/common/component/Send/index.js b/src/common/component/Send/index.js index 620f47cc..2a3e885a 100644 --- a/src/common/component/Send/index.js +++ b/src/common/component/Send/index.js @@ -1,140 +1,148 @@ -import { useState, useEffect, useRef } from 'react'; -import { MdAdd, MdClose } from 'react-icons/md'; -import TextareaAutosize from 'react-textarea-autosize'; -import { useDispatch, useSelector } from 'react-redux'; -import { useKey } from 'rooks'; +import { useState, useEffect, useRef } from "react"; +import { MdAdd } from "react-icons/md"; +import TextareaAutosize from "react-textarea-autosize"; +import { useDispatch, useSelector } from "react-redux"; +import { useKey } from "rooks"; -import { removeReplyMessage } from '../../../app/slices/message.pending'; -import { useSendChannelMsgMutation } from '../../../app/services/channel'; -import { useSendMsgMutation } from '../../../app/services/contact'; -import { useReplyMessageMutation } from '../../../app/services/message'; -import StyledSend from './styled'; -import useFiles from './useFiles'; -import UploadModal from './UploadModal'; -import EmojiPicker from './EmojiPicker'; +import { removeReplyingMessage } from "../../../app/slices/message"; +import { useSendChannelMsgMutation } from "../../../app/services/channel"; +import { useSendMsgMutation } from "../../../app/services/contact"; +import { useReplyMessageMutation } from "../../../app/services/message"; +import StyledSend from "./styled"; +import useFiles from "./useFiles"; +import UploadModal from "./UploadModal"; +import EmojiPicker from "./EmojiPicker"; +import Replying from "./Replying"; const Types = { - channel: '#', - user: '@' + channel: "#", + user: "@", }; export default function Send({ - name, - type = 'channel', - // 发给谁,或者是channel,或者是user - id = '', - dragFiles = [] + name, + type = "channel", + // 发给谁,或者是channel,或者是user + id = "", + dragFiles = [], }) { - const [replyMessage] = useReplyMessageMutation(); - const { files, setFiles, resetFiles } = useFiles([]); - const inputRef = useRef(); - const [shift, setShift] = useState(false); - const [enter, setEnter] = useState(false); - const [msg, setMsg] = useState(''); - const dispatch = useDispatch(); - // 谁发的 - const { - from_uid, - reply = null, - contacts - } = useSelector((store) => { - return { - contacts: store.contacts, - from_uid: store.authData.user.uid, - reply: store.pendingMessage.reply[id] - }; - }); - useEffect(() => { - if (dragFiles.length) { - setFiles((prev) => [...prev, ...dragFiles]); - } - }, [dragFiles]); + const [replyMessage] = useReplyMessageMutation(); + const { files, setFiles, resetFiles } = useFiles([]); + const inputRef = useRef(); + const [shift, setShift] = useState(false); + const [enter, setEnter] = useState(false); + const [msg, setMsg] = useState(""); + const dispatch = useDispatch(); + // 谁发的 + const { from_uid, replying_mid = null } = useSelector((store) => { + return { + from_uid: store.authData.uid, + replying_mid: store.message.replying[id], + }; + }); + useEffect(() => { + if (dragFiles.length) { + setFiles((prev) => [...prev, ...dragFiles]); + } + }, [dragFiles]); - const [sendMsg, { isLoading: userSending }] = useSendMsgMutation(); - const [sendChannelMsg, { isLoading: channelSending }] = useSendChannelMsgMutation(); - const sendMessage = type == 'channel' ? sendChannelMsg : sendMsg; - const sendingMessage = userSending || channelSending; - useKey( - 'Shift', - (e) => { - console.log('shift', e.type); - setShift(e.type == 'keydown'); - }, - { eventTypes: ['keydown', 'keyup'], target: inputRef } - ); - const handleMsgChange = (evt) => { - if (enter && !shift) { - handleSendMessage(); - } else { - setMsg(evt.target.value); - } - }; - const handleInputKeydown = (e) => { - console.log('keydown event', e); - setEnter(e.key === 'Enter'); - }; - const selectEmoji = (emoji) => { - setMsg((prev) => `${prev}${emoji}`); - }; - useEffect(() => { - inputRef.current.focus(); - }, [msg, reply]); - const handleUpload = (evt) => { - setFiles([...evt.target.files]); - }; - const handleSendMessage = () => { - if (!msg || !id || sendingMessage) return; - if (reply) { - replyMessage({ mid: reply.mid, content: msg }); - dispatch(removeReplyMessage(id)); - } else { - sendMessage({ id, content: msg, from_uid }); - } - setMsg(''); - }; - const removeReply = () => { - dispatch(removeReplyMessage(id)); - }; - return ( - <> - - {reply && ( -
      - - Replying to - {contacts.find((c) => c.uid == reply.from_uid)?.name} - - -
      - )} -
      - - -
      -
      - - e.currentTarget.setSelectionRange(e.currentTarget.value.length, e.currentTarget.value.length) - } - ref={inputRef} - className="content" - maxRows={8} - minRows={1} - onKeyDown={handleInputKeydown} - onChange={handleMsgChange} - value={msg} - placeholder={`给 ${Types[type]}${name} 发消息`} - /> -
      -
      - -
      -
      - {files.length !== 0 && ( - - )} - - ); + const [sendMsg, { isLoading: userSending }] = useSendMsgMutation(); + const [ + sendChannelMsg, + { isLoading: channelSending }, + ] = useSendChannelMsgMutation(); + const sendMessage = type == "channel" ? sendChannelMsg : sendMsg; + const sendingMessage = userSending || channelSending; + useKey( + "Shift", + (e) => { + console.log("shift", e.type); + setShift(e.type == "keydown"); + }, + { eventTypes: ["keydown", "keyup"], target: inputRef } + ); + const handleMsgChange = (evt) => { + if (enter && !shift) { + handleSendMessage(); + } else { + setMsg(evt.target.value); + } + }; + const handleInputKeydown = (e) => { + console.log("keydown event", e); + setEnter(e.key === "Enter"); + }; + const selectEmoji = (emoji) => { + setMsg((prev) => `${prev}${emoji}`); + }; + useEffect(() => { + inputRef.current.focus(); + }, [msg, replying_mid]); + const handleUpload = (evt) => { + setFiles([...evt.target.files]); + }; + const handleSendMessage = () => { + if (!msg || !id || sendingMessage) return; + if (replying_mid) { + console.log("replying", replying_mid); + replyMessage({ + id, + reply_mid: replying_mid, + content: msg, + context: type, + from_uid, + }); + dispatch(removeReplyingMessage(id)); + } else { + sendMessage({ id, content: msg, from_uid }); + } + setMsg(""); + }; + + return ( + <> + + {replying_mid && } +
      + + +
      +
      + + e.currentTarget.setSelectionRange( + e.currentTarget.value.length, + e.currentTarget.value.length + ) + } + ref={inputRef} + className="content" + maxRows={8} + minRows={1} + onKeyDown={handleInputKeydown} + onChange={handleMsgChange} + value={msg} + placeholder={`给 ${Types[type]}${name} 发消息`} + /> +
      +
      + +
      +
      + {files.length !== 0 && ( + + )} + + ); } diff --git a/src/common/component/Setting/MyAccount.js b/src/common/component/Setting/MyAccount.js index f8035645..5fb74e64 100644 --- a/src/common/component/Setting/MyAccount.js +++ b/src/common/component/Setting/MyAccount.js @@ -114,7 +114,9 @@ export default function MyAccount() { uploadAvatar, { isSuccess: uploadSuccess }, ] = useUpdateAvatarMutation(); - const { user } = useSelector((store) => store.authData); + const loginUser = useSelector((store) => { + return store.contacts.byId[store.authData.uid]; + }); useEffect(() => { if (uploadSuccess) { toast.success("update avatar successfully!"); @@ -127,8 +129,8 @@ export default function MyAccount() { const closeBasicEditModal = () => { setEditModal(null); }; - if (!user) return null; - const { uid, avatar, name, email } = user; + if (!loginUser) return null; + const { uid, avatar, name, email } = loginUser; return ( <> diff --git a/src/common/component/Setting/Overview.js b/src/common/component/Setting/Overview.js index 8c506e6a..7028dd53 100644 --- a/src/common/component/Setting/Overview.js +++ b/src/common/component/Setting/Overview.js @@ -66,7 +66,9 @@ const StyledWrapper = styled.div` } `; export default function Overview() { - const currUser = useSelector((store) => store.authData.user); + const loginUser = useSelector((store) => { + return store.contacts.byId[store.authData.uid]; + }); const [changed, setChanged] = useState(false); const [values, setValues] = useState(null); const { data, refetch } = useGetServerQuery(); @@ -119,7 +121,7 @@ export default function Overview() { if (!values) return null; const { name, description, logo } = values; - const isAdmin = currUser?.is_admin; + const isAdmin = loginUser?.is_admin; return (
      diff --git a/src/common/component/Setting/config/Agora.js b/src/common/component/Setting/config/Agora.js index 6191b695..833f9cd3 100644 --- a/src/common/component/Setting/config/Agora.js +++ b/src/common/component/Setting/config/Agora.js @@ -1,6 +1,5 @@ // import { useState, useEffect } from "react"; import StyledContainer from "./StyledContainer"; -// import { useSelector } from "react-redux"; import Input from "../../styled/Input"; import Textarea from "../../styled/Textarea"; import Label from "../../styled/Label"; diff --git a/src/common/component/Setting/config/Firebase.js b/src/common/component/Setting/config/Firebase.js index 9f91eb75..1cdb15df 100644 --- a/src/common/component/Setting/config/Firebase.js +++ b/src/common/component/Setting/config/Firebase.js @@ -1,6 +1,5 @@ // import { useState, useEffect } from "react"; import StyledContainer from "./StyledContainer"; -// import { useSelector } from "react-redux"; import Input from "../../styled/Input"; import Textarea from "../../styled/Textarea"; import Toggle from "../../styled/Toggle"; diff --git a/src/common/component/Setting/navs.js b/src/common/component/Setting/navs.js index c6039f8f..4762c544 100644 --- a/src/common/component/Setting/navs.js +++ b/src/common/component/Setting/navs.js @@ -4,16 +4,8 @@ import ConfigFirebase from "./config/Firebase"; import ConfigSMTP from "./config/SMTP"; import Notifications from "./Notifications"; import ManageMembers from "../ManageMembers"; -import { useSelector } from "react-redux"; import ConfigAgora from "./config/Agora"; const useNavs = () => { - const { contacts } = useSelector((store) => { - return { - currUser: store.authData.user, - channels: store.channels, - contacts: store.contacts, - }; - }); const navs = [ { title: "General", @@ -54,7 +46,7 @@ const useNavs = () => { { name: "members", title: "Members", - component: , + component: , }, ], }, diff --git a/src/common/hook/useChatScroll.js b/src/common/hook/useChatScroll.js new file mode 100644 index 00000000..0cea8f98 --- /dev/null +++ b/src/common/hook/useChatScroll.js @@ -0,0 +1,32 @@ +import { useRef, useEffect } from "react"; +// import { useDebounce } from "rooks"; +function useChatScroll(dep) { + const ref = useRef(); + // const updateScrollTop = useDebounce(() => { + // console.log("chat scroll", ref); + // if (ref.current) { + // setTimeout(() => { + // ref.current.scrollTop = ref.current.scrollHeight; + // }, 50); + // } + // }, 100); + // const updateScrollTop = () => { + // console.log("chat scroll", ref); + // if (ref.current) { + // setTimeout(() => { + // ref.current.scrollTop = ref.current.scrollHeight; + // }, 100); + // } + // }; + useEffect(() => { + console.log("chat scroll", ref); + if (ref.current) { + setTimeout(() => { + ref.current.scrollTop = ref.current.scrollHeight; + }, 20); + } + }, [dep]); + return ref; +} + +export default useChatScroll; diff --git a/src/common/hook/useFilteredUsers.js b/src/common/hook/useFilteredUsers.js index 4b62a946..93ee777a 100644 --- a/src/common/hook/useFilteredUsers.js +++ b/src/common/hook/useFilteredUsers.js @@ -2,8 +2,8 @@ import { useState, useEffect } from "react"; import { useSelector } from "react-redux"; export default function useFilteredUsers() { const [input, setInput] = useState(""); - const contacts = useSelector((store) => store.contacts); - const [filteredUsers, setFilteredUsers] = useState(contacts); + const contacts = useSelector((store) => Object.values(store.contacts.byId)); + const [filteredUsers, setFilteredUsers] = useState([]); useEffect(() => { if (!input) { setFilteredUsers(contacts); @@ -16,7 +16,7 @@ export default function useFilteredUsers() { }) ); } - }, [input, contacts]); + }, [input]); const updateInput = (val) => { setInput(val); }; diff --git a/src/common/hook/useLogout.js b/src/common/hook/useLogout.js index 1d916e1f..6f5f958e 100644 --- a/src/common/hook/useLogout.js +++ b/src/common/hook/useLogout.js @@ -1,30 +1,34 @@ import { useEffect } from "react"; -import { useDispatch } from "react-redux"; +import { useDispatch, batch } from "react-redux"; import { useNavigate } from "react-router-dom"; -import { clearAuthData } from "../../app/slices/auth.data"; -import { clearMark } from "../../app/slices/visit.mark"; -import { clearChannels } from "../../app/slices/channels"; -import { clearContacts } from "../../app/slices/contacts"; -import { clearChannelMsg } from "../../app/slices/message.channel"; -import { clearUserMsg } from "../../app/slices/message.user"; -import { clearPendingMsg } from "../../app/slices/message.pending"; +import { resetAuthData } from "../../app/slices/auth.data"; +import { resetFootprint } from "../../app/slices/footprint"; +import { resetChannels } from "../../app/slices/channels"; +import { resetContacts } from "../../app/slices/contacts"; +import { resetChannelMsg } from "../../app/slices/message.channel"; +import { resetUserMsg } from "../../app/slices/message.user"; +import { resetReactionMessage } from "../../app/slices/message.reaction"; +import { resetMessage } from "../../app/slices/message"; import { useLazyLogoutQuery } from "../../app/services/auth"; export default function useLogout() { const dispatch = useDispatch(); const navigate = useNavigate(); const [logout, { isLoading, isSuccess }] = useLazyLogoutQuery(); const clearLocalData = () => { - dispatch(clearMark()); - dispatch(clearChannelMsg()); - dispatch(clearUserMsg()); - dispatch(clearChannels()); - dispatch(clearContacts()); - dispatch(clearPendingMsg()); + batch(() => { + dispatch(resetFootprint()); + dispatch(resetChannelMsg()); + dispatch(resetUserMsg()); + dispatch(resetChannels()); + dispatch(resetContacts()); + dispatch(resetMessage()); + dispatch(resetReactionMessage()); + }); }; useEffect(() => { if (isSuccess) { - dispatch(clearAuthData()); + dispatch(resetAuthData()); navigate("/login"); } }, [isSuccess]); diff --git a/src/routes/chat/ChannelChat/index.js b/src/routes/chat/ChannelChat/index.js index 6ebd63c3..d611a7c4 100644 --- a/src/routes/chat/ChannelChat/index.js +++ b/src/routes/chat/ChannelChat/index.js @@ -1,12 +1,13 @@ import { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; -import dayjs from "dayjs"; +// import dayjs from "dayjs"; +import useChatScroll from "../../../common/hook/useChatScroll"; import Message from "../../../common/component/Message"; import ChannelIcon from "../../../common/component/ChannelIcon"; import Send from "../../../common/component/Send"; -import { clearChannelMsgUnread } from "../../../app/slices/message.channel"; +// import { readMessage } from "../../../app/slices/message"; import Contact from "../../../common/component/Contact"; import Layout from "../Layout"; import { @@ -16,37 +17,29 @@ import { StyledHeader, } from "./styled"; -export default function ChannelChat({ - cid = "", - unreads = 0, - data = {}, - dropFiles = [], -}) { +export default function ChannelChat({ cid = "", dropFiles = [] }) { // const containerRef = useRef(null); const [dragFiles, setDragFiles] = useState([]); - const dispatch = useDispatch(); - const { msgs, users } = useSelector((store) => { + // const dispatch = useDispatch(); + const { msgIds, userIds, data } = useSelector((store) => { return { - msgs: store.channelMessage[cid] || {}, - users: store.contacts, + msgIds: store.channelMessage[cid] || [], + userIds: store.contacts.ids, + data: store.channels.byId[cid], }; }); - const handleClearUnreads = () => { - dispatch(clearChannelMsgUnread(cid)); - }; + const ref = useChatScroll(msgIds); + // const handleClearUnreads = () => { + // dispatch(readMessage(msgIds)); + // }; useEffect(() => { if (dropFiles.length) { setDragFiles(dropFiles); } }, [dropFiles]); const { name, description, is_public, members = [] } = data; - const filteredUsers = - members.length == 0 - ? users - : users.filter((u) => { - return members.includes(u.uid); - }); - console.log("channel message list", msgs); + const memberIds = members.length == 0 ? userIds : members; + console.log("channel message list", msgIds); return ( - {filteredUsers.map(({ name, uid }) => { - return ; + {memberIds.map((uid) => { + return ; })} } > -
      +

      Welcome to #{name} !

      This is the start of the #{name} channel.

      {/* */}
      - {Object.entries(msgs) - .sort(([, msg1], [, msg2]) => { - return msg1.created_at - msg2.created_at; - }) - .map(([mid, msg], idx) => { - if (!msg) return null; - const { - likes = {}, - pending = false, - from_uid, - content, - content_type, - created_at, - unread, - edited, - reply, - } = msg; - return ( - - ); - })} + {msgIds.map((mid, idx) => { + // if (!msg) return null; + return ; + })}
      - {unreads != 0 && ( + {/* {unreads != 0 && (
      {unreads} new messages @@ -148,7 +111,7 @@ export default function ChannelChat({ Mark As Read - )} + )} */} ); } diff --git a/src/routes/chat/ChannelList.js b/src/routes/chat/ChannelList.js index 41988d21..866915ae 100644 --- a/src/routes/chat/ChannelList.js +++ b/src/routes/chat/ChannelList.js @@ -2,19 +2,26 @@ import { NavLink, useNavigate } from "react-router-dom"; import { useDrop } from "react-dnd"; import { NativeTypes } from "react-dnd-html5-backend"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import useContextMenu from "../../common/hook/useContextMenu"; import ContextMenu from "../../common/component/ContextMenu"; import { toggleChannelSetting } from "../../app/slices/ui"; import ChannelIcon from "../../common/component/ChannelIcon"; -const NavItem = ({ data, setFiles, contextMenuEventHandler }) => { +import getUnreadCount from "./getUnreadCount"; +const NavItem = ({ id, setFiles, contextMenuEventHandler }) => { const dispatch = useDispatch(); const navigate = useNavigate(); - + const { channel, mids, messageData } = useSelector((store) => { + return { + channel: store.channels.byId[id], + mids: store.channelMessage[id], + messageData: store.message, + }; + }); const handleChannelSetting = (evt) => { evt.preventDefault(); evt.stopPropagation(); - dispatch(toggleChannelSetting(data.id)); + dispatch(toggleChannelSetting(id)); }; const [{ isActive }, drop] = useDrop(() => ({ accept: [NativeTypes.FILE], @@ -22,7 +29,7 @@ const NavItem = ({ data, setFiles, contextMenuEventHandler }) => { if (dataTransfer.files.length) { // console.log(files, rest); setFiles([...dataTransfer.files]); - navigate(`/chat/channel/${data.id}`); + navigate(`/chat/channel/${id}`); // 重置 setTimeout(() => { setFiles([]); @@ -33,7 +40,8 @@ const NavItem = ({ data, setFiles, contextMenuEventHandler }) => { isActive: monitor.canDrop() && monitor.isOver(), }), })); - const { id, is_public, name, unreads } = data; + const { is_public, name } = channel; + const unreads = getUnreadCount(mids, messageData); return ( { ); }; -export default function ChannelList({ channels, setDropFiles }) { +export default function ChannelList({ setDropFiles }) { + const channelIds = useSelector((store) => store.channels.ids); const { visible: contextMenuVisible, posX, @@ -67,12 +76,12 @@ export default function ChannelList({ channels, setDropFiles }) { } = useContextMenu(); return ( <> - {channels.map(({ id, is_public, name, description, unreads }) => { + {channelIds.map((cid) => { return ( ); diff --git a/src/routes/chat/DMChat/index.js b/src/routes/chat/DMChat/index.js index 8c0a4363..b7e1d144 100644 --- a/src/routes/chat/DMChat/index.js +++ b/src/routes/chat/DMChat/index.js @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { useSelector } from "react-redux"; +import useChatScroll from "../../../common/hook/useChatScroll"; import Message from "../../../common/component/Message"; import Send from "../../../common/component/Send"; import Contact from "../../../common/component/Contact"; @@ -10,19 +11,17 @@ import { StyledHeader, StyledDMChat } from "./styled"; export default function DMChat({ uid = "", dropFiles = [] }) { console.log("dm files", dropFiles); const [dragFiles, setDragFiles] = useState([]); - const contacts = useSelector((store) => store.contacts); - const { msgs } = useSelector((store) => { + // const [mids, setMids] = useState([]); + const { msgIds, currUser } = useSelector((store) => { return { - msgs: store.userMessage[uid] || {}, + currUser: store.contacts.byId[uid], + msgIds: store.userMessage.byId[uid] || [], }; }); - const [currUser, setCurrUser] = useState(null); - useEffect(() => { - console.log({ uid }); - if (uid && contacts) { - setCurrUser(contacts.find((c) => c.uid == uid)); - } - }, [uid, contacts]); + const ref = useChatScroll(msgIds); + // useEffect(() => { + // setMids(msgIds); + // }, [msgIds]); useEffect(() => { if (dropFiles.length) { setDragFiles(dropFiles); @@ -67,42 +66,12 @@ export default function DMChat({ uid = "", dropFiles = [] }) { } > -
      - {Object.entries(msgs) - .sort(([, msg1], [, msg2]) => { - return msg1.created_at - msg2.created_at; - }) - .map(([mid, msg], idx) => { - if (!msg) return null; - // console.log("user msg", msg); - const { - likes = {}, - from_uid, - content, - content_type, - created_at, - unread, - pending = false, - edited, - reply, - } = msg; - return ( - - ); - })} +
      + {msgIds.map((mid) => { + // if (!msg) return null; + // console.log("user msg", msg); + return ; + })}
      diff --git a/src/routes/chat/DMChat/styled.js b/src/routes/chat/DMChat/styled.js index 5d31f49e..73a878ed 100644 --- a/src/routes/chat/DMChat/styled.js +++ b/src/routes/chat/DMChat/styled.js @@ -45,13 +45,12 @@ export const StyledHeader = styled.header` export const StyledDMChat = styled.article` position: relative; width: 100%; - padding-top: 25px; - /* margin-bottom: 120px; */ + padding-top: 20px; > .chat { display: flex; flex-direction: column; padding: 0 16px; - padding-top: 10px; + padding-top: 15px; padding-bottom: 25px; height: calc(100vh - 56px - 80px); overflow-y: scroll; diff --git a/src/routes/chat/DMList.js b/src/routes/chat/DMList.js index f46878d1..667be285 100644 --- a/src/routes/chat/DMList.js +++ b/src/routes/chat/DMList.js @@ -4,9 +4,16 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { useDrop } from "react-dnd"; import { NativeTypes } from "react-dnd-html5-backend"; +import { useSelector } from "react-redux"; import Contact from "../../common/component/Contact"; dayjs.extend(relativeTime); -const NavItem = ({ data, setFiles }) => { +const NavItem = ({ uid, mid, unreads, setFiles }) => { + const { currMsg, currUser } = useSelector((store) => { + return { + currUser: store.contacts.byId[uid], + currMsg: store.message[mid], + }; + }); const navigate = useNavigate(); const [{ isActive }, drop] = useDrop(() => ({ accept: [NativeTypes.FILE], @@ -14,7 +21,7 @@ const NavItem = ({ data, setFiles }) => { if (dataTransfer.files.length) { // console.log(files, rest); setFiles([...dataTransfer.files]); - navigate(`/chat/dm/${data.uid}`); + navigate(`/chat/dm/${uid}`); // 重置 setTimeout(() => { setFiles([]); @@ -25,7 +32,7 @@ const NavItem = ({ data, setFiles }) => { isActive: monitor.canDrop() && monitor.isOver(), }), })); - const { uid, user, lastMsg, unreads } = data; + if (!currUser || !currMsg) return null; return ( { className={`session ${isActive ? "drop_over" : ""}`} to={`/chat/dm/${uid}`} > - +
      - {user.name} - + {currUser.name} + {currMsg && }
      -
      {lastMsg.content}
      + {currMsg &&
      {currMsg.content}
      } {unreads > 0 && {unreads}}
      @@ -49,12 +56,14 @@ const NavItem = ({ data, setFiles }) => { ); }; export default function DMList({ sessions, setDropFiles }) { - return sessions.map(({ uid, user, lastMsg, unreads } = {}) => { - if (!user) return null; + return sessions.map(({ uid, lastMid, unreads } = {}) => { + if (!uid) return null; return ( ); diff --git a/src/routes/chat/getUnreadCount.js b/src/routes/chat/getUnreadCount.js new file mode 100644 index 00000000..618687fa --- /dev/null +++ b/src/routes/chat/getUnreadCount.js @@ -0,0 +1,12 @@ +const getUnreadCount = (mids, messageData) => { + if (!mids || !messageData) return 0; + let unreads = 0; + mids.forEach((id) => { + if (!messageData[id].read) { + unreads++; + } + }); + return unreads; +}; + +export default getUnreadCount; diff --git a/src/routes/chat/index.js b/src/routes/chat/index.js index b029f58a..7043ada8 100644 --- a/src/routes/chat/index.js +++ b/src/routes/chat/index.js @@ -16,20 +16,18 @@ import ChannelList from "./ChannelList"; import ContactsModal from "../../common/component/ContactsModal"; import ChannelModal from "../../common/component/ChannelModal"; import DMList from "./DMList"; +import getUnreadCount from "./getUnreadCount"; export default function ChatPage() { const [channelDropFiles, setChannelDropFiles] = useState([]); const [userDropFiles, setUserDropFiles] = useState([]); - const { contacts, channels, UserMsgData, ChannelMsgData } = useSelector( - (store) => { - return { - contacts: store.contacts, - channels: store.channels, - UserMsgData: store.userMessage, - ChannelMsgData: store.channelMessage, - }; - } - ); + const { contactsData, UserMsgData, messageData } = useSelector((store) => { + return { + contactsData: store.contacts.byId, + UserMsgData: store.userMessage, + messageData: store.message, + }; + }); const [channelModalVisible, setChannelModalVisible] = useState(false); const [contactsModalVisible, setContactsModalVisible] = useState(false); const { channel_id, user_id } = useParams(); @@ -39,31 +37,19 @@ export default function ChatPage() { const toggleChannelModalVisible = () => { setChannelModalVisible((prev) => !prev); }; - const getUnreadCount = (gid) => { - return Object.values(ChannelMsgData[gid] || {}).filter((m) => m.unread) - .length; - }; + // const getUnreadCount = (gid) => { + // return Object.values(ChannelMsgData[gid] || {}).filter((m) => m.read) + // .length; + // }; const handleToggleExpand = (evt) => { const { currentTarget } = evt; const listEle = currentTarget.parentElement.parentElement; listEle.classList.toggle("collapse"); }; - if (!contacts) return null; - const tmpSessionUser = contacts.find((c) => c.uid == user_id); - const transformedChannels = Object.entries(channels).map(([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 }; + const tmpSessionUser = contactsData[user_id]; + const sessions = UserMsgData.ids.map((uid) => { + const unreads = getUnreadCount(UserMsgData.byId[uid], messageData); + return { uid, unreads, lastMid: [...UserMsgData.byId[uid]].pop() }; }); return ( <> @@ -93,10 +79,7 @@ export default function ChatPage() { />
      @@ -117,7 +100,7 @@ export default function ChatPage() {