refactor: app store and cache
This commit is contained in:
Vendored
+56
@@ -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;
|
||||||
+11
-63
@@ -1,66 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import localforage from "localforage";
|
|
||||||
import { useDispatch, batch } from "react-redux";
|
import { useDispatch, batch } from "react-redux";
|
||||||
import { fullfillReactionMessage } from "./slices/message.reaction";
|
import { fullfillReactionMessage } from "../slices/message.reaction";
|
||||||
import { fullfillServer } from "./slices/server";
|
import { fullfillServer } from "../slices/server";
|
||||||
import { fullfillMessage } from "./slices/message";
|
import { fullfillMessage } from "../slices/message";
|
||||||
import { fullfillChannelMsg } from "./slices/message.channel";
|
import { fullfillChannelMsg } from "../slices/message.channel";
|
||||||
import { fullfillUserMsg } from "./slices/message.user";
|
import { fullfillUserMsg } from "../slices/message.user";
|
||||||
import { fullfillChannels } from "./slices/channels";
|
import { fullfillChannels } from "../slices/channels";
|
||||||
import { fullfillContacts } from "./slices/contacts";
|
import { fullfillContacts } from "../slices/contacts";
|
||||||
import { fullfillFootprint } from "./slices/footprint";
|
import { fullfillFootprint } from "../slices/footprint";
|
||||||
import { KEY_UID } from "./config";
|
const useRehydrate = () => {
|
||||||
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 = () => {
|
|
||||||
const [iterated, setIterated] = useState(false);
|
const [iterated, setIterated] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const rehydrate = async () => {
|
const rehydrate = async () => {
|
||||||
@@ -128,5 +77,4 @@ export const useRehydrate = () => {
|
|||||||
};
|
};
|
||||||
return { rehydrate, rehydrated: iterated };
|
return { rehydrate, rehydrated: iterated };
|
||||||
};
|
};
|
||||||
|
export default useRehydrate;
|
||||||
export default initCache;
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// const BASE_URL = `${location.origin}/api`;
|
// const BASE_URL = `${location.origin}/api`;
|
||||||
const BASE_URL = `https://dev.rustchat.com/api`;
|
const BASE_URL = `https://dev.rustchat.com/api`;
|
||||||
|
export const CACHE_VERSION = `0.1`;
|
||||||
// const BASE_URL = `https://rustchat.net/api`;
|
// const BASE_URL = `https://rustchat.net/api`;
|
||||||
export const ContentTypes = {
|
export const ContentTypes = {
|
||||||
text: "text/plain",
|
text: "text/plain",
|
||||||
|
|||||||
@@ -6,15 +6,6 @@ export default async function handler({ operation, data = {}, payload }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
// case "fullfillChannelMsgs":
|
|
||||||
// {
|
|
||||||
// await Promise.all(
|
|
||||||
// Object.entries(data).map(async ([cid, arr]) => {
|
|
||||||
// await table.setItem(cid + "", arr);
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// break;
|
|
||||||
case "addChannelMsg":
|
case "addChannelMsg":
|
||||||
case "removeChannelMsg":
|
case "removeChannelMsg":
|
||||||
{
|
{
|
||||||
@@ -23,6 +14,16 @@ export default async function handler({ operation, data = {}, payload }) {
|
|||||||
await table.setItem(id + "", data[id]);
|
await table.setItem(id + "", data[id]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "removeChannelSession":
|
||||||
|
{
|
||||||
|
const ids = Array.isArray(payload) ? payload : [payload];
|
||||||
|
await Promise.all(
|
||||||
|
ids.map(async (id) => {
|
||||||
|
await table.removeItem(id + "");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ export default async function handler({ operation, data, payload }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
// case "fullfillChannels":
|
case "fullfillChannels":
|
||||||
// {
|
{
|
||||||
// await Promise.all(
|
const chs = payload;
|
||||||
// data.ids.map(async (cid) => {
|
await Promise.all(
|
||||||
// await table.setItem(cid + "", data.byId[cid]);
|
chs.map(({ gid, ...rest }) => {
|
||||||
// })
|
return table.setItem(gid, { gid, ...rest });
|
||||||
// );
|
})
|
||||||
// }
|
);
|
||||||
// break;
|
}
|
||||||
|
break;
|
||||||
case "addChannel":
|
case "addChannel":
|
||||||
{
|
{
|
||||||
const { gid } = payload;
|
const { gid } = payload;
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ export default async function handler({ operation, data, payload }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
// case "fullfillContacts":
|
case "fullfillContacts":
|
||||||
// {
|
{
|
||||||
// await Promise.all(
|
const contacts = payload;
|
||||||
// data.ids.map(async (uid) => {
|
await Promise.all(
|
||||||
// await table.setItem(uid + "", data.byId[uid]);
|
contacts.map(({ uid, ...rest }) => {
|
||||||
// })
|
return table.setItem(uid, { uid, ...rest });
|
||||||
// );
|
})
|
||||||
// }
|
);
|
||||||
// break;
|
}
|
||||||
|
break;
|
||||||
case "updateUsersByLogs":
|
case "updateUsersByLogs":
|
||||||
{
|
{
|
||||||
const changeLogs = payload;
|
const changeLogs = payload;
|
||||||
|
|||||||
@@ -6,15 +6,6 @@ export default async function handler({ operation, data = {}, payload }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
// case "fullfillUserMsgs":
|
|
||||||
// {
|
|
||||||
// await Promise.all(
|
|
||||||
// Object.entries(data).map(async ([uid, arr]) => {
|
|
||||||
// await table.setItem(uid + "", arr);
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// break;
|
|
||||||
case "addUserMsg":
|
case "addUserMsg":
|
||||||
case "removeUserMsg":
|
case "removeUserMsg":
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,15 +6,6 @@ export default async function handler({ operation, data = {}, payload }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
// case "fullfillFootprint":
|
|
||||||
// {
|
|
||||||
// await Promise.all(
|
|
||||||
// Object.entries(data).map(async ([key, value]) => {
|
|
||||||
// await table.setItem(key, value);
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// break;
|
|
||||||
case "updateUsersVersion":
|
case "updateUsersVersion":
|
||||||
{
|
{
|
||||||
const usersVersion = payload;
|
const usersVersion = payload;
|
||||||
|
|||||||
@@ -6,15 +6,6 @@ export default async function handler({ operation, data = {}, payload }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
// case "fullfillChannelMsgs":
|
|
||||||
// {
|
|
||||||
// await Promise.all(
|
|
||||||
// Object.entries(data).map(async ([mid, obj]) => {
|
|
||||||
// await table.setItem(mid + "", obj);
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// break;
|
|
||||||
case "addMessage":
|
case "addMessage":
|
||||||
case "updateMessage":
|
case "updateMessage":
|
||||||
{
|
{
|
||||||
@@ -24,8 +15,12 @@ export default async function handler({ operation, data = {}, payload }) {
|
|||||||
break;
|
break;
|
||||||
case "removeMessage":
|
case "removeMessage":
|
||||||
{
|
{
|
||||||
const mid = payload;
|
const mids = Array.isArray(payload) ? payload : [payload];
|
||||||
|
await Promise.all(
|
||||||
|
mids.map(async (mid) => {
|
||||||
await table.removeItem(mid + "");
|
await table.removeItem(mid + "");
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "readMessage":
|
case "readMessage":
|
||||||
|
|||||||
@@ -6,21 +6,22 @@ export default async function handler({ operation, data = {}, payload }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
// case "fullfillReactionMessage":
|
|
||||||
// {
|
|
||||||
// await Promise.all(
|
|
||||||
// Object.entries(data).map(async ([mid, data]) => {
|
|
||||||
// await table.setItem(mid + "", data);
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// break;
|
|
||||||
case "toggleReactionMessage":
|
case "toggleReactionMessage":
|
||||||
{
|
{
|
||||||
const { mid } = payload;
|
const { mid } = payload;
|
||||||
await table.setItem(mid + "", data[mid]);
|
await table.setItem(mid + "", data[mid]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "removeReactionMessage":
|
||||||
|
{
|
||||||
|
const mids = Array.isArray(payload) ? payload : [payload];
|
||||||
|
await Promise.all(
|
||||||
|
mids.map(async (mid) => {
|
||||||
|
await table.removeItem(mid + "");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ export default async function handler({ operation, payload }) {
|
|||||||
await table.setItem("inviteLink", data);
|
await table.setItem("inviteLink", data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "updateInfo":
|
||||||
|
{
|
||||||
|
const data = payload;
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(data).map(([_key, _val]) => {
|
||||||
|
return table.setItem(_key, _val);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const operations = [
|
|||||||
"reactionMessage",
|
"reactionMessage",
|
||||||
"message",
|
"message",
|
||||||
"footprint",
|
"footprint",
|
||||||
|
"server",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Create the middleware instance and methods
|
// Create the middleware instance and methods
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { createApi } from "@reduxjs/toolkit/query/react";
|
|||||||
// import toast from "react-hot-toast";
|
// import toast from "react-hot-toast";
|
||||||
import baseQuery from "./base.query";
|
import baseQuery from "./base.query";
|
||||||
import { ContentTypes } from "../config";
|
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";
|
import { onMessageSendStarted } from "./handlers";
|
||||||
export const channelApi = createApi({
|
export const channelApi = createApi({
|
||||||
reducerPath: "channelApi",
|
reducerPath: "channelApi",
|
||||||
@@ -47,6 +51,28 @@ export const channelApi = createApi({
|
|||||||
url: `group/${id}`,
|
url: `group/${id}`,
|
||||||
method: "DELETE",
|
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({
|
sendChannelMsg: builder.mutation({
|
||||||
query: ({ id, content, type = "text" }) => ({
|
query: ({ id, content, type = "text" }) => ({
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||||
// import toast from "react-hot-toast";
|
// import toast from "react-hot-toast";
|
||||||
|
import { KEY_UID } from "../config";
|
||||||
import baseQuery from "./base.query";
|
import baseQuery from "./base.query";
|
||||||
|
import { resetAuthData, setUid } from "../slices/auth.data";
|
||||||
|
import { fullfillContacts } from "../slices/contacts";
|
||||||
import BASE_URL, { ContentTypes } from "../config";
|
import BASE_URL, { ContentTypes } from "../config";
|
||||||
import { onMessageSendStarted } from "./handlers";
|
import { onMessageSendStarted } from "./handlers";
|
||||||
export const contactApi = createApi({
|
export const contactApi = createApi({
|
||||||
@@ -19,6 +22,28 @@ export const contactApi = createApi({
|
|||||||
return user;
|
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({
|
deleteContact: builder.query({
|
||||||
query: (uid) => ({ url: `/admin/user/${uid}`, method: "DELETE" }),
|
query: (uid) => ({ url: `/admin/user/${uid}`, method: "DELETE" }),
|
||||||
|
|||||||
+43
-10
@@ -1,8 +1,8 @@
|
|||||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||||
import BASE_URL from "../config";
|
import BASE_URL from "../config";
|
||||||
import { updateInviteLink } from "../slices/server";
|
import { updateInviteLink, updateInfo } from "../slices/server";
|
||||||
import baseQuery from "./base.query";
|
import baseQuery from "./base.query";
|
||||||
|
const defaultExpireDuration = 7 * 24 * 60 * 60;
|
||||||
export const serverApi = createApi({
|
export const serverApi = createApi({
|
||||||
reducerPath: "serverApi",
|
reducerPath: "serverApi",
|
||||||
baseQuery,
|
baseQuery,
|
||||||
@@ -10,9 +10,17 @@ export const serverApi = createApi({
|
|||||||
getServer: builder.query({
|
getServer: builder.query({
|
||||||
query: () => ({ url: `admin/system/organization` }),
|
query: () => ({ url: `admin/system/organization` }),
|
||||||
transformResponse: (data) => {
|
transformResponse: (data) => {
|
||||||
data.logo = `${BASE_URL}/resource/organization/logo`;
|
data.logo = `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`;
|
||||||
return data;
|
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({
|
getMetrics: builder.query({
|
||||||
query: () => ({ url: `/admin/system/metrics` }),
|
query: () => ({ url: `/admin/system/metrics` }),
|
||||||
@@ -56,9 +64,21 @@ export const serverApi = createApi({
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: data,
|
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({
|
createInviteLink: builder.query({
|
||||||
query: (expired_in = 7 * 24 * 60 * 60) => ({
|
query: (expired_in = defaultExpireDuration) => ({
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "text/plain",
|
"content-type": "text/plain",
|
||||||
accept: "text/plain",
|
accept: "text/plain",
|
||||||
@@ -66,17 +86,21 @@ export const serverApi = createApi({
|
|||||||
url: `/admin/user/create_invite_link?expired_in=${expired_in}`,
|
url: `/admin/user/create_invite_link?expired_in=${expired_in}`,
|
||||||
responseHandler: (response) => response.text(),
|
responseHandler: (response) => response.text(),
|
||||||
}),
|
}),
|
||||||
async onQueryStarted(expire, { dispatch, queryFulfilled, getState }) {
|
transformResponse: (link) => {
|
||||||
const {
|
// 替换掉域名
|
||||||
expire: prevExp,
|
const invite = new URL(link);
|
||||||
link: prevLink,
|
return `${location.origin}${invite.pathname}${invite.search}${invite.hash}`;
|
||||||
} = getState().server.inviteLink;
|
},
|
||||||
|
async onQueryStarted(
|
||||||
|
expire = defaultExpireDuration,
|
||||||
|
{ dispatch, queryFulfilled }
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { data: link } = await queryFulfilled;
|
const { data: link } = await queryFulfilled;
|
||||||
console.log("link", link);
|
console.log("link", link);
|
||||||
dispatch(updateInviteLink({ expire, link }));
|
dispatch(updateInviteLink({ expire, link }));
|
||||||
} catch {
|
} catch {
|
||||||
dispatch(updateInviteLink({ expire: prevExp, link: prevLink }));
|
console.log("invite link error");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -86,6 +110,15 @@ export const serverApi = createApi({
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: data,
|
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 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -27,9 +27,18 @@ const channelMsgSlice = createSlice({
|
|||||||
state[id].splice(idx, 1);
|
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 {
|
export const {
|
||||||
|
removeChannelSession,
|
||||||
resetChannelMsg,
|
resetChannelMsg,
|
||||||
fullfillChannelMsg,
|
fullfillChannelMsg,
|
||||||
addChannelMsg,
|
addChannelMsg,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
import BASE_URL from "../config";
|
||||||
const initialState = {
|
const initialState = {
|
||||||
replying: {},
|
replying: {},
|
||||||
};
|
};
|
||||||
@@ -28,14 +28,29 @@ const messageSlice = createSlice({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
addMessage(state, action) {
|
addMessage(state, action) {
|
||||||
const { mid, sending } = action.payload;
|
const data = action.payload;
|
||||||
|
const { mid, sending, content_type, content } = data;
|
||||||
// 如果是正发送,并且已存在,则不覆盖
|
// 如果是正发送,并且已存在,则不覆盖
|
||||||
if (sending && state[mid]) return;
|
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) {
|
removeMessage(state, action) {
|
||||||
const mid = action.payload;
|
const mids = Array.isArray(action.payload)
|
||||||
delete state[mid];
|
? action.payload
|
||||||
|
: [action.payload];
|
||||||
|
mids.forEach((id) => {
|
||||||
|
delete state[id];
|
||||||
|
});
|
||||||
},
|
},
|
||||||
addReplyingMessage(state, action) {
|
addReplyingMessage(state, action) {
|
||||||
const { id, mid } = action.payload;
|
const { id, mid } = action.payload;
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ const reactionMessageSlice = createSlice({
|
|||||||
fullfillReactionMessage(state, action) {
|
fullfillReactionMessage(state, action) {
|
||||||
return action.payload;
|
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) {
|
toggleReactionMessage(state, action) {
|
||||||
const { from_uid, mid, action: reaction } = action.payload;
|
const { from_uid, mid, action: reaction } = action.payload;
|
||||||
console.log("msg reaction", mid, from_uid, reaction);
|
console.log("msg reaction", mid, from_uid, reaction);
|
||||||
@@ -36,6 +44,7 @@ const reactionMessageSlice = createSlice({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const {
|
export const {
|
||||||
|
removeReactionMessage,
|
||||||
resetReactionMessage,
|
resetReactionMessage,
|
||||||
fullfillReactionMessage,
|
fullfillReactionMessage,
|
||||||
toggleReactionMessage,
|
toggleReactionMessage,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
logo: "",
|
||||||
inviteLink: {
|
inviteLink: {
|
||||||
link: "",
|
link: "",
|
||||||
expire: 0,
|
expire: 0,
|
||||||
@@ -18,8 +21,16 @@ const serverSlice = createSlice({
|
|||||||
link: "",
|
link: "",
|
||||||
expire: 0,
|
expire: 0,
|
||||||
},
|
},
|
||||||
} = action.payload;
|
name = "",
|
||||||
return { inviteLink };
|
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) {
|
updateInviteLink(state, action) {
|
||||||
const { link, expire = 7 * 24 * 60 * 60 } = action.payload;
|
const { link, expire = 7 * 24 * 60 * 60 } = action.payload;
|
||||||
@@ -28,6 +39,7 @@ const serverSlice = createSlice({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const {
|
export const {
|
||||||
|
updateInfo,
|
||||||
resetServer,
|
resetServer,
|
||||||
fullfillServer,
|
fullfillServer,
|
||||||
updateInviteLink,
|
updateInviteLink,
|
||||||
|
|||||||
+1
-4
@@ -16,7 +16,6 @@ import { contactApi } from "./services/contact";
|
|||||||
import { channelApi } from "./services/channel";
|
import { channelApi } from "./services/channel";
|
||||||
import { messageApi } from "./services/message";
|
import { messageApi } from "./services/message";
|
||||||
import { serverApi } from "./services/server";
|
import { serverApi } from "./services/server";
|
||||||
import { streamingApi } from "./services/streaming";
|
|
||||||
|
|
||||||
const reducer = combineReducers({
|
const reducer = combineReducers({
|
||||||
authData: authDataReducer,
|
authData: authDataReducer,
|
||||||
@@ -34,7 +33,6 @@ const reducer = combineReducers({
|
|||||||
[contactApi.reducerPath]: contactApi.reducer,
|
[contactApi.reducerPath]: contactApi.reducer,
|
||||||
[channelApi.reducerPath]: channelApi.reducer,
|
[channelApi.reducerPath]: channelApi.reducer,
|
||||||
[serverApi.reducerPath]: serverApi.reducer,
|
[serverApi.reducerPath]: serverApi.reducer,
|
||||||
[streamingApi.reducerPath]: streamingApi.reducer,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
@@ -46,8 +44,7 @@ const store = configureStore({
|
|||||||
contactApi.middleware,
|
contactApi.middleware,
|
||||||
channelApi.middleware,
|
channelApi.middleware,
|
||||||
serverApi.middleware,
|
serverApi.middleware,
|
||||||
messageApi.middleware,
|
messageApi.middleware
|
||||||
streamingApi.middleware
|
|
||||||
)
|
)
|
||||||
.prepend(listenerMiddleware.middleware),
|
.prepend(listenerMiddleware.middleware),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useNavigate } from "react-router-dom";
|
// import { useNavigate } from "react-router-dom";
|
||||||
import initCache, { useRehydrate } from "../../app/cache";
|
import initCache, { useRehydrate } from "../../app/cache";
|
||||||
import { useLazyGetContactsQuery } from "../../app/services/contact";
|
import { useLazyGetContactsQuery } from "../../app/services/contact";
|
||||||
// import { useLazyInitStreamingQuery } from "../../app/services/streaming";
|
// 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 { useGetChannelsQuery } from "../../app/services/channel";
|
||||||
import { useLazyGetServerQuery } from "../../app/services/server";
|
import { useLazyGetServerQuery } from "../../app/services/server";
|
||||||
import useStreaming from "../../common/hook/useStreaming";
|
import useStreaming from "../../common/hook/useStreaming";
|
||||||
import { KEY_UID } from "../../app/config";
|
|
||||||
// pollingInterval: 0,
|
// pollingInterval: 0,
|
||||||
// const querySetting = {
|
// const querySetting = {
|
||||||
// refetchOnMountOrArgChange: true,
|
// refetchOnMountOrArgChange: true,
|
||||||
@@ -19,9 +16,7 @@ let request = null;
|
|||||||
export default function usePreload() {
|
export default function usePreload() {
|
||||||
const { rehydrate, rehydrated } = useRehydrate();
|
const { rehydrate, rehydrated } = useRehydrate();
|
||||||
// const [initStreaming, { isLoading: streaming }] = useLazyInitStreamingQuery();
|
// const [initStreaming, { isLoading: streaming }] = useLazyInitStreamingQuery();
|
||||||
const [checked, setChecked] = useState(false);
|
// const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const store = useSelector((store) => store);
|
const store = useSelector((store) => store);
|
||||||
const { startStreaming, streaming, initializing } = useStreaming();
|
const { startStreaming, streaming, initializing } = useStreaming();
|
||||||
const [
|
const [
|
||||||
@@ -60,34 +55,14 @@ export default function usePreload() {
|
|||||||
}
|
}
|
||||||
}, [rehydrated]);
|
}, [rehydrated]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (checked && rehydrated && !initializing && !streaming) {
|
if (rehydrated && !initializing && !streaming) {
|
||||||
request = startStreaming(store);
|
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);
|
// console.log("loading", contactsLoading, serverLoading, !checked);
|
||||||
return {
|
return {
|
||||||
loading: contactsLoading || serverLoading || !checked || !rehydrated,
|
loading: contactsLoading || serverLoading || !rehydrated,
|
||||||
error: contactsError && serverError,
|
error: contactsError && serverError,
|
||||||
success: contactsSuccess && serverSuccess,
|
success: contactsSuccess && serverSuccess,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
Reference in New Issue
Block a user