refactor: app store and cache
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user