refactor: app store and cache

This commit is contained in:
zerosoul
2022-03-18 16:30:38 +08:00
parent ad994f1eaf
commit 7bb8e9a914
26 changed files with 280 additions and 908 deletions
@@ -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 };
}