diff --git a/src/app/cache/index.js b/src/app/cache/index.js new file mode 100644 index 00000000..621e7ba1 --- /dev/null +++ b/src/app/cache/index.js @@ -0,0 +1,56 @@ +import localforage from "localforage"; +import useRehydrate from "./useRehydrate"; +import { KEY_UID, CACHE_VERSION } 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: "server", + description: "store server data", + }, + // { + // storeName: "message", + // description: "store message with key-val full data", + // }, +]; +const initCache = () => { + const uid = localStorage.getItem(KEY_UID) || ""; + const name = `local_db_${uid}_v_${CACHE_VERSION.split(".").join("_")}`; + window.CACHE = {}; + tables.forEach(({ storeName, description }) => { + window.CACHE[storeName] = localforage.createInstance({ + name, + storeName, + description, + }); + }); +}; + +export { useRehydrate }; +export default initCache; diff --git a/src/app/cache.js b/src/app/cache/useRehydrate.js similarity index 57% rename from src/app/cache.js rename to src/app/cache/useRehydrate.js index 579f0e35..a6fa3aa1 100644 --- a/src/app/cache.js +++ b/src/app/cache/useRehydrate.js @@ -1,66 +1,15 @@ import { useState } from "react"; -import localforage from "localforage"; + import { useDispatch, batch } from "react-redux"; -import { fullfillReactionMessage } from "./slices/message.reaction"; -import { fullfillServer } from "./slices/server"; -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: "server", - description: "store server data", - }, - // { - // storeName: "message", - // description: "store message with key-val full data", - // }, -]; -const initCache = () => { - const uid = localStorage.getItem(KEY_UID) || ""; - const name = `local_db_${uid}`; - window.CACHE = {}; - tables.forEach(({ storeName, description }) => { - window.CACHE[storeName] = localforage.createInstance({ - name, - storeName, - description, - }); - }); -}; -export const useRehydrate = () => { +import { fullfillReactionMessage } from "../slices/message.reaction"; +import { fullfillServer } from "../slices/server"; +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"; +const useRehydrate = () => { const [iterated, setIterated] = useState(false); const dispatch = useDispatch(); const rehydrate = async () => { @@ -128,5 +77,4 @@ export const useRehydrate = () => { }; return { rehydrate, rehydrated: iterated }; }; - -export default initCache; +export default useRehydrate; diff --git a/src/app/config.js b/src/app/config.js index b99019ba..4add3edc 100644 --- a/src/app/config.js +++ b/src/app/config.js @@ -1,5 +1,6 @@ // const BASE_URL = `${location.origin}/api`; const BASE_URL = `https://dev.rustchat.com/api`; +export const CACHE_VERSION = `0.1`; // const BASE_URL = `https://rustchat.net/api`; export const ContentTypes = { text: "text/plain", diff --git a/src/app/listener.middleware/handler.channel.msg.js b/src/app/listener.middleware/handler.channel.msg.js index 083e2c0f..34674741 100644 --- a/src/app/listener.middleware/handler.channel.msg.js +++ b/src/app/listener.middleware/handler.channel.msg.js @@ -6,15 +6,6 @@ export default async function handler({ operation, data = {}, payload }) { 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": { @@ -23,6 +14,16 @@ export default async function handler({ operation, data = {}, payload }) { await table.setItem(id + "", data[id]); } break; + case "removeChannelSession": + { + const ids = Array.isArray(payload) ? payload : [payload]; + await Promise.all( + ids.map(async (id) => { + await table.removeItem(id + ""); + }) + ); + } + break; default: break; } diff --git a/src/app/listener.middleware/handler.channels.js b/src/app/listener.middleware/handler.channels.js index b55fab9e..986961c6 100644 --- a/src/app/listener.middleware/handler.channels.js +++ b/src/app/listener.middleware/handler.channels.js @@ -6,15 +6,16 @@ export default async function handler({ operation, data, payload }) { return; } switch (operation) { - // case "fullfillChannels": - // { - // await Promise.all( - // data.ids.map(async (cid) => { - // await table.setItem(cid + "", data.byId[cid]); - // }) - // ); - // } - // break; + case "fullfillChannels": + { + const chs = payload; + await Promise.all( + chs.map(({ gid, ...rest }) => { + return table.setItem(gid, { gid, ...rest }); + }) + ); + } + break; case "addChannel": { const { gid } = payload; diff --git a/src/app/listener.middleware/handler.contacts.js b/src/app/listener.middleware/handler.contacts.js index d4c924c0..5a5d83d3 100644 --- a/src/app/listener.middleware/handler.contacts.js +++ b/src/app/listener.middleware/handler.contacts.js @@ -6,15 +6,16 @@ export default async function handler({ operation, data, payload }) { return; } switch (operation) { - // case "fullfillContacts": - // { - // await Promise.all( - // data.ids.map(async (uid) => { - // await table.setItem(uid + "", data.byId[uid]); - // }) - // ); - // } - // break; + case "fullfillContacts": + { + const contacts = payload; + await Promise.all( + contacts.map(({ uid, ...rest }) => { + return table.setItem(uid, { uid, ...rest }); + }) + ); + } + break; case "updateUsersByLogs": { const changeLogs = payload; diff --git a/src/app/listener.middleware/handler.dm.msg.js b/src/app/listener.middleware/handler.dm.msg.js index 7a39258d..ca9d7492 100644 --- a/src/app/listener.middleware/handler.dm.msg.js +++ b/src/app/listener.middleware/handler.dm.msg.js @@ -6,15 +6,6 @@ export default async function handler({ operation, data = {}, payload }) { 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": { diff --git a/src/app/listener.middleware/handler.footprint.js b/src/app/listener.middleware/handler.footprint.js index 70ee83c0..d4a2b8a9 100644 --- a/src/app/listener.middleware/handler.footprint.js +++ b/src/app/listener.middleware/handler.footprint.js @@ -6,15 +6,6 @@ export default async function handler({ operation, data = {}, payload }) { 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; diff --git a/src/app/listener.middleware/handler.message.js b/src/app/listener.middleware/handler.message.js index 018902a4..0c973c09 100644 --- a/src/app/listener.middleware/handler.message.js +++ b/src/app/listener.middleware/handler.message.js @@ -6,15 +6,6 @@ export default async function handler({ operation, data = {}, payload }) { 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": { @@ -24,8 +15,12 @@ export default async function handler({ operation, data = {}, payload }) { break; case "removeMessage": { - const mid = payload; - await table.removeItem(mid + ""); + const mids = Array.isArray(payload) ? payload : [payload]; + await Promise.all( + mids.map(async (mid) => { + await table.removeItem(mid + ""); + }) + ); } break; case "readMessage": diff --git a/src/app/listener.middleware/handler.reaction.js b/src/app/listener.middleware/handler.reaction.js index 74630cd2..7165b5c3 100644 --- a/src/app/listener.middleware/handler.reaction.js +++ b/src/app/listener.middleware/handler.reaction.js @@ -6,21 +6,22 @@ export default async function handler({ operation, data = {}, payload }) { 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; + case "removeReactionMessage": + { + const mids = Array.isArray(payload) ? payload : [payload]; + await Promise.all( + mids.map(async (mid) => { + await table.removeItem(mid + ""); + }) + ); + } + break; default: break; } diff --git a/src/app/listener.middleware/handler.server.js b/src/app/listener.middleware/handler.server.js index d07c677b..accd4264 100644 --- a/src/app/listener.middleware/handler.server.js +++ b/src/app/listener.middleware/handler.server.js @@ -12,6 +12,16 @@ export default async function handler({ operation, payload }) { await table.setItem("inviteLink", data); } break; + case "updateInfo": + { + const data = payload; + await Promise.all( + Object.entries(data).map(([_key, _val]) => { + return table.setItem(_key, _val); + }) + ); + } + break; default: break; } diff --git a/src/app/listener.middleware/index.js b/src/app/listener.middleware/index.js index 1d9310c9..e717c9c6 100644 --- a/src/app/listener.middleware/index.js +++ b/src/app/listener.middleware/index.js @@ -17,6 +17,7 @@ const operations = [ "reactionMessage", "message", "footprint", + "server", ]; // Create the middleware instance and methods diff --git a/src/app/services/channel.js b/src/app/services/channel.js index 3b51bb73..0db7cf87 100644 --- a/src/app/services/channel.js +++ b/src/app/services/channel.js @@ -2,7 +2,11 @@ import { createApi } from "@reduxjs/toolkit/query/react"; // import toast from "react-hot-toast"; import baseQuery from "./base.query"; import { ContentTypes } from "../config"; -import { updateChannel } from "../slices/channels"; +import { updateChannel, removeChannel } from "../slices/channels"; +import { removeMessage } from "../slices/message"; +import { removeChannelSession } from "../slices/message.channel"; +import { removeReactionMessage } from "../slices/message.reaction"; +import { toggleChannelSetting } from "../slices/ui"; import { onMessageSendStarted } from "./handlers"; export const channelApi = createApi({ reducerPath: "channelApi", @@ -47,6 +51,28 @@ export const channelApi = createApi({ url: `group/${id}`, method: "DELETE", }), + async onQueryStarted(id, { dispatch, getState, queryFulfilled }) { + const { + ui: { channelSetting }, + channelMessage, + } = getState(); + try { + await queryFulfilled; + dispatch(removeChannel(id)); + if (id == channelSetting) { + dispatch(toggleChannelSetting()); + } + // 删掉该channel下的所有消息&reaction + const mids = channelMessage[id]; + if (mids) { + dispatch(removeChannelSession(id)); + dispatch(removeMessage(mids)); + dispatch(removeReactionMessage(mids)); + } + } catch { + console.log("remove channel error"); + } + }, }), sendChannelMsg: builder.mutation({ query: ({ id, content, type = "text" }) => ({ diff --git a/src/app/services/contact.js b/src/app/services/contact.js index 6238fec5..5eb1e494 100644 --- a/src/app/services/contact.js +++ b/src/app/services/contact.js @@ -1,6 +1,9 @@ import { createApi } from "@reduxjs/toolkit/query/react"; // import toast from "react-hot-toast"; +import { KEY_UID } from "../config"; import baseQuery from "./base.query"; +import { resetAuthData, setUid } from "../slices/auth.data"; +import { fullfillContacts } from "../slices/contacts"; import BASE_URL, { ContentTypes } from "../config"; import { onMessageSendStarted } from "./handlers"; export const contactApi = createApi({ @@ -19,6 +22,28 @@ export const contactApi = createApi({ return user; }); }, + async onQueryStarted(data, { dispatch, queryFulfilled }) { + const local_uid = localStorage.getItem(KEY_UID); + try { + const { data: contacts } = await queryFulfilled; + const matchedUser = contacts.find((c) => c.uid == local_uid); + console.log("wtf", contacts, matchedUser); + if (!matchedUser) { + // 用户已注销或被禁用 + console.log("no matched user, redirect to login"); + dispatch(resetAuthData()); + // navigate("/login"); + } else { + const markedContacts = contacts.map((u) => { + return u.uid == matchedUser.uid ? { ...u, online: true } : u; + }); + dispatch(setUid(matchedUser.uid)); + dispatch(fullfillContacts(markedContacts)); + } + } catch { + console.log("get contact list error"); + } + }, }), deleteContact: builder.query({ query: (uid) => ({ url: `/admin/user/${uid}`, method: "DELETE" }), diff --git a/src/app/services/server.js b/src/app/services/server.js index 10bcdc02..9fcc6a09 100644 --- a/src/app/services/server.js +++ b/src/app/services/server.js @@ -1,8 +1,8 @@ import { createApi } from "@reduxjs/toolkit/query/react"; import BASE_URL from "../config"; -import { updateInviteLink } from "../slices/server"; +import { updateInviteLink, updateInfo } from "../slices/server"; import baseQuery from "./base.query"; - +const defaultExpireDuration = 7 * 24 * 60 * 60; export const serverApi = createApi({ reducerPath: "serverApi", baseQuery, @@ -10,9 +10,17 @@ export const serverApi = createApi({ getServer: builder.query({ query: () => ({ url: `admin/system/organization` }), transformResponse: (data) => { - data.logo = `${BASE_URL}/resource/organization/logo`; + data.logo = `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`; return data; }, + async onQueryStarted(data, { dispatch, queryFulfilled }) { + try { + const { data: server } = await queryFulfilled; + dispatch(updateInfo(server)); + } catch { + console.log("get server info error"); + } + }, }), getMetrics: builder.query({ query: () => ({ url: `/admin/system/metrics` }), @@ -56,9 +64,21 @@ export const serverApi = createApi({ method: "POST", body: data, }), + async onQueryStarted(data, { dispatch, queryFulfilled }) { + try { + await queryFulfilled; + dispatch( + updateInfo({ + logo: `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`, + }) + ); + } catch { + console.log("update server logo error"); + } + }, }), createInviteLink: builder.query({ - query: (expired_in = 7 * 24 * 60 * 60) => ({ + query: (expired_in = defaultExpireDuration) => ({ headers: { "content-type": "text/plain", accept: "text/plain", @@ -66,17 +86,21 @@ export const serverApi = createApi({ url: `/admin/user/create_invite_link?expired_in=${expired_in}`, responseHandler: (response) => response.text(), }), - async onQueryStarted(expire, { dispatch, queryFulfilled, getState }) { - const { - expire: prevExp, - link: prevLink, - } = getState().server.inviteLink; + transformResponse: (link) => { + // 替换掉域名 + const invite = new URL(link); + return `${location.origin}${invite.pathname}${invite.search}${invite.hash}`; + }, + async onQueryStarted( + expire = defaultExpireDuration, + { dispatch, queryFulfilled } + ) { try { const { data: link } = await queryFulfilled; console.log("link", link); dispatch(updateInviteLink({ expire, link })); } catch { - dispatch(updateInviteLink({ expire: prevExp, link: prevLink })); + console.log("invite link error"); } }, }), @@ -86,6 +110,15 @@ export const serverApi = createApi({ method: "POST", body: data, }), + async onQueryStarted(data, { dispatch, queryFulfilled, getState }) { + const { name: prevName, description: prevDesc } = getState().server; + dispatch(updateInfo(data)); + try { + await queryFulfilled; + } catch { + dispatch(updateInfo({ name: prevName, description: prevDesc })); + } + }, }), }), }); diff --git a/src/app/services/streaming/chat.handler.js b/src/app/services/streaming/chat.handler.js deleted file mode 100644 index 0431935a..00000000 --- a/src/app/services/streaming/chat.handler.js +++ /dev/null @@ -1,131 +0,0 @@ -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, readUsers = {}, readChannels = {} } = 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; - const readIndex = (to == "user" ? readUsers[id] : readChannels[id]) || 0; - const read = self ? true : mid < readIndex ? true : false; - switch (type) { - case "normal": - { - batch(() => { - dispatch( - addMessage({ - mid, - // 如果是自己发的消息,就是已读 - read, - ...common, - }) - ); - // 未推送完 or 不是自己发的消息 - console.log("curr state", ready, loginUid, common.from_uid); - if (!ready || loginUid != common.from_uid) { - dispatch(appendMessage({ id, mid })); - } - }); - } - break; - case "reply": - { - batch(() => { - dispatch( - addMessage({ - mid, - reply_mid: detailMid, - // 如果是自己发的消息,就是已读 - read, - ...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 deleted file mode 100644 index bbbbd2ac..00000000 --- a/src/app/services/streaming/index.js +++ /dev/null @@ -1,196 +0,0 @@ -// 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, - updateReadChannels, - updateReadUsers, -} 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 }, - footprint: { readUsers, readChannels }, - } = 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 "user_settings": - case "user_settings_changed": - { - console.log("users settings"); - const { - read_index_users = [], - read_index_groups = [], - } = data; - dispatch(updateReadChannels(read_index_groups)); - dispatch(updateReadUsers(read_index_users)); - } - 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, - readUsers, - readChannels, - }); - } - break; - - default: - console.log("sse event data", data); - break; - } - }; - streaming.onopen = (wtf) => { - console.info("sse opened", wtf); - }; - 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: - streaming.close(); - 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/message.channel.js b/src/app/slices/message.channel.js index da749493..334b7b33 100644 --- a/src/app/slices/message.channel.js +++ b/src/app/slices/message.channel.js @@ -27,9 +27,18 @@ const channelMsgSlice = createSlice({ state[id].splice(idx, 1); } }, + removeChannelSession(state, action) { + const ids = Array.isArray(action.payload) + ? action.payload + : [action.payload]; + ids.forEach((id) => { + delete state[id]; + }); + }, }, }); export const { + removeChannelSession, resetChannelMsg, fullfillChannelMsg, addChannelMsg, diff --git a/src/app/slices/message.js b/src/app/slices/message.js index e91a134e..1cdb63be 100644 --- a/src/app/slices/message.js +++ b/src/app/slices/message.js @@ -1,5 +1,5 @@ import { createSlice } from "@reduxjs/toolkit"; - +import BASE_URL from "../config"; const initialState = { replying: {}, }; @@ -28,14 +28,29 @@ const messageSlice = createSlice({ }); }, addMessage(state, action) { - const { mid, sending } = action.payload; + const data = action.payload; + const { mid, sending, content_type, content } = data; // 如果是正发送,并且已存在,则不覆盖 if (sending && state[mid]) return; - state[mid] = action.payload; + const isImage = content_type.startsWith("image"); + if (!sending && isImage) { + data.image_id = content; + data.content = `${BASE_URL}/resource/image?id=${encodeURIComponent( + data.image_id + )}`; + data.thumbnail = `${BASE_URL}/resource/thumbnail?id=${encodeURIComponent( + data.image_id + )}`; + } + state[mid] = data; }, removeMessage(state, action) { - const mid = action.payload; - delete state[mid]; + const mids = Array.isArray(action.payload) + ? action.payload + : [action.payload]; + mids.forEach((id) => { + delete state[id]; + }); }, addReplyingMessage(state, action) { const { id, mid } = action.payload; diff --git a/src/app/slices/message.reaction.js b/src/app/slices/message.reaction.js index 7b304915..4181ae14 100644 --- a/src/app/slices/message.reaction.js +++ b/src/app/slices/message.reaction.js @@ -11,6 +11,14 @@ const reactionMessageSlice = createSlice({ fullfillReactionMessage(state, action) { return action.payload; }, + removeReactionMessage(state, action) { + const mids = Array.isArray(action.payload) + ? action.payload + : [action.payload]; + mids.forEach((id) => { + delete state[id]; + }); + }, toggleReactionMessage(state, action) { const { from_uid, mid, action: reaction } = action.payload; console.log("msg reaction", mid, from_uid, reaction); @@ -36,6 +44,7 @@ const reactionMessageSlice = createSlice({ }, }); export const { + removeReactionMessage, resetReactionMessage, fullfillReactionMessage, toggleReactionMessage, diff --git a/src/app/slices/server.js b/src/app/slices/server.js index 816f1353..d7224bc0 100644 --- a/src/app/slices/server.js +++ b/src/app/slices/server.js @@ -1,5 +1,8 @@ import { createSlice } from "@reduxjs/toolkit"; const initialState = { + name: "", + description: "", + logo: "", inviteLink: { link: "", expire: 0, @@ -18,8 +21,16 @@ const serverSlice = createSlice({ link: "", expire: 0, }, - } = action.payload; - return { inviteLink }; + name = "", + description = "", + } = action.payload || {}; + return { name, description, inviteLink }; + }, + updateInfo(state, action) { + const values = action.payload || {}; + Object.keys(values).forEach((_key) => { + state[_key] = values[_key]; + }); }, updateInviteLink(state, action) { const { link, expire = 7 * 24 * 60 * 60 } = action.payload; @@ -28,6 +39,7 @@ const serverSlice = createSlice({ }, }); export const { + updateInfo, resetServer, fullfillServer, updateInviteLink, diff --git a/src/app/store.js b/src/app/store.js index b439c371..123b200e 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -16,7 +16,6 @@ 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, @@ -34,7 +33,6 @@ const reducer = combineReducers({ [contactApi.reducerPath]: contactApi.reducer, [channelApi.reducerPath]: channelApi.reducer, [serverApi.reducerPath]: serverApi.reducer, - [streamingApi.reducerPath]: streamingApi.reducer, }); const store = configureStore({ @@ -46,8 +44,7 @@ const store = configureStore({ contactApi.middleware, channelApi.middleware, serverApi.middleware, - messageApi.middleware, - streamingApi.middleware + messageApi.middleware ) .prepend(listenerMiddleware.middleware), }); diff --git a/src/common/component/NotificationHub/index.js b/src/common/component/NotificationHub/index.js deleted file mode 100644 index a96efe73..00000000 --- a/src/common/component/NotificationHub/index.js +++ /dev/null @@ -1,200 +0,0 @@ -import React, { useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import toast from "react-hot-toast"; -import { useDispatch, useSelector } from "react-redux"; -import useNotification from "./useNotification"; -import BASE_URL, { - KEY_AFTER_MID, - KEY_USERS_VERSION, -} from "../../../app/config"; -import { useRenewMutation } from "../../../app/services/auth"; -import { - setChannels, - addChannel, - deleteChannel, -} from "../../../app/slices/channels"; -import { - updateUsersByLogs, - updateUsersStatus, -} from "../../../app/slices/contacts"; -import { - updateToken, - clearAuthData, - updateLoginedUserByLogs, -} from "../../../app/slices/auth.data"; -import { - updateUsersVersion, - updateAfterMid, -} from "../../../app/slices/visit.mark"; - -import { setReady } from "../../../app/slices/ui"; -import useMessageHandler from "./useMessageHandler"; -const getQueryString = (params = {}) => { - const sp = new URLSearchParams(); - Object.entries(params).forEach(([key, val]) => { - if (val) { - sp.append(key, val); - } - }); - return sp.toString(); -}; -const NotificationHub = () => { - const { enableNotification } = useNotification(); - const dispatch = useDispatch(); - const navigate = useNavigate(); - const { token, refreshToken, user: currUser } = useSelector( - (store) => store.authData - ); - const handleMessage = useMessageHandler(currUser); - const [ - renewToken, - { data, isSuccess: refreshTokenSuccess }, - ] = useRenewMutation(); - useEffect(() => { - enableNotification(); - }, []); - - useEffect(() => { - let sse = null; - if (token) { - const users_version = - localStorage.getItem(`${KEY_USERS_VERSION}_${currUser.uid}`) ?? 0; - const after_mid = - localStorage.getItem(`${KEY_AFTER_MID}_${currUser.uid}`) ?? 0; - sse = new EventSource( - `${BASE_URL}/user/events?${getQueryString({ - "api-key": token, - users_version, - after_mid, - })}` - ); - sse.onopen = () => { - console.info("sse opened"); - }; - sse.onerror = (err) => { - switch (err.eventPhase) { - case EventSource.CLOSED: - console.log("sse error closed error"); - break; - case EventSource.CONNECTING: - console.log("sse error connecting error"); - renewToken({ token, refreshToken }); - break; - - default: - console.error("sse error error", err); - // renewToken({ token, refreshToken }); - break; - } - }; - sse.onmessage = (evt) => { - handleSSEMessage(JSON.parse(evt.data)); - }; - } - return () => { - console.log("re-run sse init"); - if (sse) { - sse.close(); - } - }; - }, [token, refreshToken]); - useEffect(() => { - if (refreshTokenSuccess) { - const { token, refresh_token } = data; - dispatch(updateToken({ token, refresh_token })); - } - }, [refreshTokenSuccess, data]); - - const handleSSEMessage = (data) => { - const { type } = data; - - switch (type) { - case "heartbeat": - console.log("heartbeat"); - break; - case "ready": - console.log("sse 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; - const loginedUserLogs = logs.filter((l) => { - return l.uid == currUser.uid; - }); - if (loginedUserLogs.length) { - // 当前登录用户的变动,及时同步到auth data里 - dispatch(updateLoginedUserByLogs(logs)); - } - 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(clearAuthData()); - navigate("/login"); - toast("kicked from the other device"); - break; - case "delete_user": - dispatch(clearAuthData()); - navigate("/login"); - toast("sorry, your account has been deleted"); - break; - default: - break; - } - } - break; - case "related_groups": - console.log("related group list", data); - dispatch(setChannels(data.groups)); - break; - case "joined_group": - console.log("joined group list", data.group); - dispatch(addChannel(data.group)); - break; - case "kick_from_group": - console.log("kicked from group", data.gid); - dispatch(deleteChannel(data.gid)); - break; - case "chat": - { - handleMessage(data); - - // 更新after_mid - if (data.detail.type == "normal") { - dispatch(updateAfterMid({ mid: data.mid })); - } - } - break; - - default: - console.log("sse event data", data); - break; - } - }; - return null; -}; -function compareToken(prevHub, nextHub) { - return prevHub.token === nextHub.token; -} -export default React.memo(NotificationHub, compareToken); diff --git a/src/common/component/NotificationHub/useMessageHandler.js b/src/common/component/NotificationHub/useMessageHandler.js deleted file mode 100644 index 5b4fb0a9..00000000 --- a/src/common/component/NotificationHub/useMessageHandler.js +++ /dev/null @@ -1,134 +0,0 @@ -// import React from "react"; -import { - updateChannelMsg, - addChannelMsg, - deleteChannelMsg, - likeChannelMsg, -} from "../../../app/slices/message.channel"; -import { - updateUserMsg, - addUserMsg, - deleteUserMsg, - likeUserMsg, -} from "../../../app/slices/message.user"; -import useNotification from "./useNotification"; -import { useDispatch } from "react-redux"; -export default function useMessageHandler(currUser) { - const { showNotification } = useNotification(); - const dispatch = useDispatch(); - const dispatchReaction = ({ - to = "user", - id, - from_uid, - mid, - created_at, - detail = {}, - }) => { - const { type = "" } = detail; - const updateMsg = to == "user" ? updateUserMsg : updateChannelMsg; - const deleteMsg = to == "user" ? deleteUserMsg : deleteChannelMsg; - const likeMsg = to == "user" ? likeUserMsg : likeChannelMsg; - switch (type) { - case "edit": - { - const { content } = detail; - dispatch(updateMsg({ id, mid, content, time: created_at })); - } - break; - case "like": - dispatch(likeMsg({ id, from_uid, mid, action: detail.action })); - break; - case "delete": - dispatch(deleteMsg({ id, mid })); - break; - - default: - break; - } - }; - const dispatchAddMessage = ({ to = "user", id, self = false, common }) => { - const addMessage = to == "user" ? addUserMsg : addChannelMsg; - dispatch( - addMessage({ - id, // 自己发的 就不用标记未读 - read: !self, - ...common, - }) - ); - if (!self) { - showNotification({ - body: common.content, - data: { - path: `/chat/${to}/${id}`, - }, - }); - } - }; - const handleMessage = (data) => { - const { target } = data; - const { - created_at, - mid, - from_uid, - detail: { - mid: detailMid, - content, - content_type, - expires_in, - type, - detail = {}, - }, - } = data; - const to = typeof target.gid !== "undefined" ? "channel" : "user"; - const self = from_uid == currUser.uid; - const id = to == "user" ? (self ? target.uid : from_uid) : target.gid; - switch (type) { - case "normal": - { - dispatchAddMessage({ - to, - id, - self, - common: { - mid, - content, - content_type, - from_uid, - created_at, - expires_in, - }, - }); - } - break; - case "reply": - { - dispatchAddMessage({ - to, - id, - self, - common: { - mid, - reply_mid: detailMid, - content, - content_type, - from_uid, - created_at, - expires_in, - }, - }); - } - break; - case "reaction": { - dispatchReaction({ - to, - id, - from_uid, - created_at, - mid: detailMid, - detail, - }); - } - } - }; - return handleMessage; -} diff --git a/src/common/component/NotificationHub/useNotification.js b/src/common/component/NotificationHub/useNotification.js deleted file mode 100644 index efaffe10..00000000 --- a/src/common/component/NotificationHub/useNotification.js +++ /dev/null @@ -1,65 +0,0 @@ -import { useEffect, useState } from "react"; -// import { useNavigate } from "react-router-dom"; -const isSafariBrowser = () => - navigator.userAgent.indexOf("Safari") > -1 && - navigator.userAgent.indexOf("Chrome") <= -1; -export default function useNotification() { - // const navigate = useNavigate(); - // granted default denied / - const [status, setStatus] = useState(Notification.permission); - const [pageVisible, setPageVisible] = useState(true); - useEffect(() => { - const visibleChangeHandler = () => { - setPageVisible(document.visibilityState === "visible"); - }; - const notifyPermissionChangeHandler = (state) => { - setStatus(state); - }; - document.addEventListener("visibilitychange", visibleChangeHandler); - if (!isSafariBrowser) { - navigator.permissions - .query({ name: "notifications" }) - .then(function (permissionStatus) { - console.log( - "notifications permission status is ", - permissionStatus.state - ); - permissionStatus.onchange = notifyPermissionChangeHandler.bind( - null, - permissionStatus.state - ); - }); - } - return () => { - document.removeEventListener("visibilitychange", visibleChangeHandler); - }; - }, []); - const enableNotification = () => { - if (status !== "granted") { - Notification.requestPermission().then((permission) => { - console.log(permission); - setStatus(permission); - }); - } - }; - const showNotification = (payload = {}) => { - console.log("show notify", payload, pageVisible); - if (status !== "granted" || pageVisible) return; - const { - title = "New Message", - body = "You have one new message", - icon = "https://static.nicegoodthings.com/project/ext/webrowse.logo.png", - } = payload; - new Notification(title, { body, icon }); - // const n = new Notification(title, { body, icon }); - // n.onclick = (evt) => { - // const { data } = evt.target; - - // console.log("notify evt", evt); - // if (data && data.path) { - // navigate(data.path); - // } - // }; - }; - return { status, enableNotification, showNotification }; -} diff --git a/src/routes/home/usePreload.js b/src/routes/home/usePreload.js index 555b8608..4cdb3063 100644 --- a/src/routes/home/usePreload.js +++ b/src/routes/home/usePreload.js @@ -1,16 +1,13 @@ -import { useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { useNavigate } from "react-router-dom"; +import { useEffect } from "react"; +import { useSelector } from "react-redux"; +// import { useNavigate } from "react-router-dom"; import initCache, { useRehydrate } from "../../app/cache"; import { useLazyGetContactsQuery } from "../../app/services/contact"; // import { useLazyInitStreamingQuery } from "../../app/services/streaming"; -import { resetAuthData, setUid } from "../../app/slices/auth.data"; -import { fullfillContacts } from "../../app/slices/contacts"; // import { useGetChannelsQuery } from "../../app/services/channel"; import { useLazyGetServerQuery } from "../../app/services/server"; import useStreaming from "../../common/hook/useStreaming"; -import { KEY_UID } from "../../app/config"; // pollingInterval: 0, // const querySetting = { // refetchOnMountOrArgChange: true, @@ -19,9 +16,7 @@ let request = null; export default function usePreload() { const { rehydrate, rehydrated } = useRehydrate(); // const [initStreaming, { isLoading: streaming }] = useLazyInitStreamingQuery(); - const [checked, setChecked] = useState(false); - const dispatch = useDispatch(); - const navigate = useNavigate(); + // const navigate = useNavigate(); const store = useSelector((store) => store); const { startStreaming, streaming, initializing } = useStreaming(); const [ @@ -60,34 +55,14 @@ export default function usePreload() { } }, [rehydrated]); useEffect(() => { - if (checked && rehydrated && !initializing && !streaming) { + if (rehydrated && !initializing && !streaming) { request = startStreaming(store); } - }, [checked, rehydrated, store, streaming, initializing]); + }, [rehydrated, store, streaming, initializing]); - useEffect(() => { - const local_uid = localStorage.getItem(KEY_UID); - if (contacts) { - const matchedUser = contacts.find((c) => c.uid == local_uid); - console.log("wtf", contacts, matchedUser); - if (!matchedUser) { - // 用户已注销或被禁用 - console.log("no matched user, redirect to login"); - dispatch(resetAuthData()); - navigate("/login"); - } else { - const markedContacts = contacts.map((u) => { - return u.uid == matchedUser.uid ? { ...u, online: true } : u; - }); - dispatch(setUid(matchedUser.uid)); - dispatch(fullfillContacts(markedContacts)); - setChecked(true); - } - } - }, [contacts]); // console.log("loading", contactsLoading, serverLoading, !checked); return { - loading: contactsLoading || serverLoading || !checked || !rehydrated, + loading: contactsLoading || serverLoading || !rehydrated, error: contactsError && serverError, success: contactsSuccess && serverSuccess, data: {