feat: read index

This commit is contained in:
zerosoul
2022-03-20 17:41:09 +08:00
parent 8366be7182
commit b60160cd50
16 changed files with 179 additions and 138 deletions
@@ -11,7 +11,7 @@ export default async function handler({ operation, data, payload }) {
const chs = payload;
await Promise.all(
chs.map(({ gid, ...rest }) => {
return table.setItem(gid, { gid, ...rest });
return table.setItem(gid + "", { gid, ...rest });
})
);
}
@@ -11,7 +11,7 @@ export default async function handler({ operation, data, payload }) {
const contacts = payload;
await Promise.all(
contacts.map(({ uid, ...rest }) => {
return table.setItem(uid, { uid, ...rest });
return table.setItem(uid + "", { uid, ...rest });
})
);
}
+2 -1
View File
@@ -75,9 +75,10 @@ export const channelApi = createApi({
},
}),
sendChannelMsg: builder.mutation({
query: ({ id, content, type = "text" }) => ({
query: ({ id, content, type = "text", properties = "" }) => ({
headers: {
"content-type": ContentTypes[type],
"X-Properties": properties,
},
url: `group/${id}/send`,
method: "POST",
+2 -1
View File
@@ -73,9 +73,10 @@ export const contactApi = createApi({
}),
}),
sendMsg: builder.mutation({
query: ({ id, content, type = "text" }) => ({
query: ({ id, content, type = "text", properties = "" }) => ({
headers: {
"content-type": ContentTypes[type],
"X-Properties": properties,
},
url: `user/${id}/send`,
method: "POST",
+21 -17
View File
@@ -1,5 +1,5 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import { batch } from "react-redux";
// import { batch } from "react-redux";
import { ContentTypes } from "../config";
import { updateReadChannels, updateReadUsers } from "../slices/footprint";
@@ -57,27 +57,31 @@ export const messageApi = createApi({
method: "POST",
body: data,
}),
async onQueryStarted(data, { dispatch, getState, queryFulfilled }) {
const { users = [], groups = [] } = data;
const { readUsers, readChannels } = getState().footprint.readUsers;
const prevUsers = users.map(({ uid }) => {
return { uid, mid: readUsers[uid] };
});
const prevChannels = users.map(({ gid }) => {
return { gid, mid: readChannels[gid] };
});
batch(() => {
dispatch(updateReadChannels(groups));
async onQueryStarted(data, { dispatch, queryFulfilled }) {
const { users = null, groups = null } = data;
// const { readUsers, readChannels } = getState().footprint;
// const prevUsers = users.map(({ uid }) => {
// return { uid, mid: readUsers[uid] };
// });
// const prevChannels = users.map(({ gid }) => {
// return { gid, mid: readChannels[gid] };
// });
// batch(() => {
if (users) {
dispatch(updateReadUsers(users));
});
}
if (groups) {
dispatch(updateReadChannels(groups));
}
// });
try {
await queryFulfilled;
} catch {
// todo
batch(() => {
dispatch(updateReadChannels(prevChannels));
dispatch(updateReadUsers(prevUsers));
});
// batch(() => {
// dispatch(updateReadChannels(prevChannels));
// dispatch(updateReadUsers(prevUsers));
// });
}
},
}),
+4 -1
View File
@@ -50,7 +50,10 @@ const contactsSlice = createSlice({
case "create":
{
state.byId[uid] = { uid, ...rest };
state.ids.push(uid);
const idx = state.ids.findIndex((i) => i == uid);
if (idx == -1) {
state.ids.push(uid);
}
}
break;
case "delete":
+2 -4
View File
@@ -83,10 +83,8 @@ export default function Contact({
content={<Profile uid={uid} type="card" />}
>
<div className="avatar">
<Avatar url={curr?.avatar} name={curr?.name} alt="avatar" />
<div
className={`status ${curr?.online ? "online" : "offline"}`}
></div>
<Avatar url={curr.avatar} name={curr.name} alt="avatar" />
<div className={`status ${curr.online ? "online" : "offline"}`}></div>
</div>
</Tippy>
{!compact && <span className="name">{curr?.name}</span>}
+35 -33
View File
@@ -1,36 +1,41 @@
import React, { useRef, useState, useEffect } from "react";
import dayjs from "dayjs";
import { useSelector, useDispatch } from "react-redux";
import { useInViewRef } from "rooks";
import { useSelector } from "react-redux";
import { useInViewRef, useDebounce } from "rooks";
import Tippy from "@tippyjs/react";
import Reaction from "./Reaction";
import Reply from "./Reply";
import Profile from "../Profile";
import Avatar from "../Avatar";
import { readMessage } from "../../../app/slices/message";
import { useReadMessageMutation } from "../../../app/services/message";
import StyledWrapper from "./styled";
import Commands from "./Commands";
import EditMessage from "./EditMessage";
import renderContent from "./renderContent";
function Message({ contextId = 0, mid = "", read = true, context = "user" }) {
function Message({ contextId = 0, mid = "", context = "user" }) {
const [updateReadIndex] = useReadMessageMutation();
const updateReadDebounced = useDebounce(updateReadIndex, 300);
const [myRef, inView] = useInViewRef();
const [edit, setEdit] = useState(false);
const [emojiPopVisible, setEmojiPopVisible] = useState(false);
const [menuVisible, setMenuVisible] = useState(false);
const disptach = useDispatch();
const avatarRef = useRef(null);
const { message = {}, reactionMessageData, contactsData } = useSelector(
(store) => {
return {
reactionMessageData: store.reactionMessage,
message: store.message[mid],
contactsData: store.contacts.byId,
};
}
);
const {
footprint,
message = {},
reactionMessageData,
contactsData,
loginUid,
} = useSelector((store) => {
return {
footprint: store.footprint,
loginUid: store.authData.uid,
reactionMessageData: store.reactionMessage,
message: store.message[mid] || {},
contactsData: store.contacts.byId,
};
});
const toggleMenu = () => {
setMenuVisible((prev) => !prev);
};
@@ -40,38 +45,35 @@ function Message({ contextId = 0, mid = "", read = true, context = "user" }) {
const toggleEmojiPopover = () => {
setEmojiPopVisible((prev) => !prev);
};
// useEffect(() => {
// if (!read) {
// avatarRef.current?.scrollIntoView();
// }
// }, [read]);
// console.log("message", mid, messageData[mid]);
useEffect(() => {
if (inView && !read) {
disptach(readMessage(mid));
const data =
context == "user"
? { users: [{ uid: +contextId, mid }] }
: { groups: [{ gid: +contextId, mid }] };
updateReadIndex(data);
}
}, [mid, read, inView]);
if (!message) return null;
const {
reply_mid,
from_uid: fromUid,
created_at: time,
sending,
sending = false,
content,
thumbnail,
content_type = "text/plain",
edited,
properties,
} = message;
const readIndex =
context == "user"
? footprint.readUsers[contextId]
: footprint.readChannels[contextId];
useEffect(() => {
const read = fromUid == loginUid || mid <= readIndex;
if (inView && !read) {
const data =
context == "user"
? { users: [{ uid: +contextId, mid }] }
: { groups: [{ gid: +contextId, mid }] };
updateReadDebounced(data);
}
}, [mid, readIndex, inView, fromUid, loginUid]);
const reactions = reactionMessageData[mid];
const currUser = contactsData[fromUid] || {};
// if (!message) return null;
return (
<StyledWrapper
data-mid={mid}
+27 -27
View File
@@ -1,4 +1,4 @@
// import { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
// import toast from "react-hot-toast";
// import { useNavigate } from "react-router-dom";
@@ -23,37 +23,37 @@ export default function UploadModal({
{ isLoading: channelSending },
] = useSendChannelMsgMutation();
const [sendUserMsg, { isLoading: userSending }] = useSendMsgMutation();
// const [properties, setProperties] = useState([]);
// useEffect(() => {
// files.forEach((file, idx) => {
// const { name, size, type } = file;
// setProperties((prevs) => {
// prevs[idx] = { name, size, type };
// return prevs;
// });
// var fileReader = new FileReader();
// fileReader.onloadend = (e) => {
// let dataUrl = e.target.result;
// let tmp = new Image();
// tmp.src = dataUrl;
// tmp.onload = function () {
// console.log("image load", this.width, this.height);
// setProperties((prevs) => {
// prevs[idx].width = this.width;
// prevs[idx].height = this.height;
// return prevs;
// });
// };
// };
// fileReader.readAsDataURL(file);
// });
// }, [files]);
const [properties, setProperties] = useState([]);
useEffect(() => {
files.forEach((file, idx) => {
const { name, size, type } = file;
setProperties((prevs) => {
prevs[idx] = { name, size, type };
return prevs;
});
// var fileReader = new FileReader();
// fileReader.onloadend = (e) => {
// let dataUrl = e.target.result;
// let tmp = new Image();
// tmp.src = dataUrl;
// tmp.onload = function () {
// console.log("image load", this.width, this.height);
// setProperties((prevs) => {
// prevs[idx].width = this.width;
// prevs[idx].height = this.height;
// return prevs;
// });
// };
// };
// fileReader.readAsDataURL(file);
});
}, [files]);
const handleUpload = () => {
const uploadFn = type == "user" ? sendUserMsg : sendChannelMsg;
uploadFn({
id: sendTo,
content: files[0],
// properties: properties[0],
properties: btoa(JSON.stringify(properties[0])),
type: "image",
from_uid,
});
+14 -8
View File
@@ -2,6 +2,7 @@ import { NavLink, useNavigate } from "react-router-dom";
import { useDrop } from "react-dnd";
import { NativeTypes } from "react-dnd-html5-backend";
import { useDispatch, useSelector } from "react-redux";
// import { useDebounce} from "rooks";
import { toggleChannelSetting } from "../../../app/slices/ui";
import ChannelIcon from "../../../common/component/ChannelIcon";
import { getUnreadCount } from "../utils";
@@ -9,13 +10,18 @@ import { getUnreadCount } from "../utils";
const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { channel, mids, messageData } = useSelector((store) => {
return {
channel: store.channels.byId[id],
mids: store.channelMessage[id],
messageData: store.message,
};
});
// const getUnreadCountDebounced=useDebounce(getUnreadCount,300)
const { channel, mids, messageData, readIndex, loginUid } = useSelector(
(store) => {
return {
channel: store.channels.byId[id],
mids: store.channelMessage[id],
messageData: store.message,
loginUid: store.authData.uid,
readIndex: store.footprint.readChannels[id],
};
}
);
const handleChannelSetting = (evt) => {
evt.preventDefault();
evt.stopPropagation();
@@ -39,7 +45,7 @@ const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
}),
}));
const { is_public, name } = channel;
const unreads = getUnreadCount(mids, messageData);
const unreads = getUnreadCount({ mids, messageData, readIndex, loginUid });
return (
<NavLink
data-cid={id}
+1 -6
View File
@@ -13,12 +13,11 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
console.log("dm files", dropFiles);
const [dragFiles, setDragFiles] = useState([]);
// const [mids, setMids] = useState([]);
const { msgIds, currUser, messageData, readUsers } = useSelector((store) => {
const { msgIds, currUser, messageData } = useSelector((store) => {
return {
currUser: store.contacts.byId[uid],
msgIds: store.userMessage.byId[uid] || [],
messageData: store.message,
readUsers: store.footprint.readUser || {},
};
});
const ref = useChatScroll(msgIds);
@@ -33,7 +32,6 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
if (!currUser) return null;
// console.log("user msgs", msgs);
const readIndex = readUsers[uid] || 0;
return (
<Layout
setDragFiles={setDragFiles}
@@ -62,14 +60,11 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
})
.map((mid, idx) => {
const curr = messageData[mid];
const self = curr.from_uid == currUser.uid;
const read = self ? true : mid <= readIndex ? true : false;
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
return renderMessageFragment({
prev,
curr,
contextId: uid,
read,
context: "user",
});
})}
+27 -4
View File
@@ -5,7 +5,7 @@ import relativeTime from "dayjs/plugin/relativeTime";
import { useDrop } from "react-dnd";
import { NativeTypes } from "react-dnd-html5-backend";
import { useSelector } from "react-redux";
import { renderPreviewMessage } from "./utils";
import { renderPreviewMessage, getUnreadCount } from "./utils";
import Contact from "../../common/component/Contact";
dayjs.extend(relativeTime);
const NavItem = ({ uid, mid, unreads, setFiles }) => {
@@ -60,9 +60,32 @@ const NavItem = ({ uid, mid, unreads, setFiles }) => {
</NavLink>
);
};
export default function DMList({ sessions, setDropFiles }) {
return sessions.map(({ uid, lastMid, unreads } = {}) => {
if (!uid) return null;
// mids: ChannelMsgData[channel_id],
// messageData,
// readIndex: readChannels[channel_id],
// loginUid,
export default function DMList({ uids, setDropFiles }) {
const { userMessage, messageData, readUsers, loginUid } = useSelector(
(store) => {
return {
loginUid: store.authData.uid,
readUsers: store.footprint.readUsers,
contactData: store.contacts.byId,
userMessage: store.userMessage.byId,
messageData: store.message,
};
}
);
const sessions = uids.map((uid) => {
const mids = userMessage[uid];
const lastMid = [...mids].pop();
const readIndex = readUsers[uid];
const unreads = getUnreadCount({ mids, readIndex, messageData, loginUid });
return { lastMid, unreads, uid };
});
return sessions.map(({ lastMid, uid, unreads }) => {
return (
<NavItem
key={uid}
+5 -16
View File
@@ -16,16 +16,14 @@ import ChannelList from "./ChannelList";
import ContactsModal from "../../common/component/ContactsModal";
import ChannelModal from "../../common/component/ChannelModal";
import DMList from "./DMList";
import { getUnreadCount } from "./utils";
export default function ChatPage() {
const [channelDropFiles, setChannelDropFiles] = useState([]);
const [userDropFiles, setUserDropFiles] = useState([]);
const { contactsData, UserMsgData, messageData } = useSelector((store) => {
const { contactsData, sessionUids } = useSelector((store) => {
return {
contactsData: store.contacts.byId,
UserMsgData: store.userMessage,
messageData: store.message,
sessionUids: store.userMessage.ids,
};
});
const [channelModalVisible, setChannelModalVisible] = useState(false);
@@ -43,10 +41,6 @@ export default function ChatPage() {
listEle.classList.toggle("collapse");
};
const tmpSessionUser = contactsData[user_id];
const sessions = UserMsgData.ids.map((uid) => {
const unreads = getUnreadCount(UserMsgData.byId[uid], messageData);
return { uid, unreads, lastMid: [...UserMsgData.byId[uid]].pop() };
});
return (
<>
{channelModalVisible && (
@@ -95,8 +89,8 @@ export default function ChatPage() {
/>
</h3>
<nav className="nav">
<DMList sessions={sessions} setDropFiles={setUserDropFiles} />
{user_id && UserMsgData.ids.findIndex((i) => i == user_id) == -1 && (
<DMList uids={sessionUids} setDropFiles={setUserDropFiles} />
{user_id && sessionUids.findIndex((i) => i == user_id) == -1 && (
<NavLink className="session" to={`/chat/dm/${user_id}`}>
<Contact
compact
@@ -112,7 +106,6 @@ export default function ChatPage() {
<div className="down">
<div className="msg"></div>
{/* <i className="badge">3</i> */}
</div>
</div>
</NavLink>
@@ -123,11 +116,7 @@ export default function ChatPage() {
</div>
<div className="right">
{channel_id && (
<ChannelChat
unreads={getUnreadCount(channel_id)}
cid={channel_id}
dropFiles={channelDropFiles}
/>
<ChannelChat cid={channel_id} dropFiles={channelDropFiles} />
)}
{user_id && <DMChat uid={user_id} dropFiles={userDropFiles} />}
</div>
+31 -16
View File
@@ -3,16 +3,38 @@ import dayjs from "dayjs";
import { ContentTypes } from "../../app/config";
import Divider from "../../common/component/Divider";
import Message from "../../common/component/Message";
export const getUnreadCount = (mids, messageData) => {
if (!mids || !messageData) return 0;
let unreads = 0;
mids.forEach((id) => {
if (messageData[id] && !messageData[id].read) {
unreads++;
function debounce(callback, wait = 2000, immediate = false) {
let timeout = null;
return function () {
const callNow = immediate && !timeout;
const next = () => callback.apply(this, arguments);
clearTimeout(timeout);
timeout = setTimeout(next, wait);
if (callNow) {
next();
}
};
}
export function getUnreadCount({
mids = [],
messageData = {},
loginUid = 0,
readIndex = 0,
}) {
console.log({ mids, loginUid, readIndex });
// 先过滤掉from自己的
const others = mids.filter((mid) => {
const { from_uid } = messageData[mid];
return from_uid != loginUid;
});
return unreads;
};
if (others.length == 0) return 0;
if (readIndex == 0) return others.length;
// 再过滤掉大于read-index
const final = others.filter((mid) => {
return mid > readIndex;
});
return final.length;
}
export const renderPreviewMessage = (message = null) => {
if (!message) return null;
const { content_type, content } = message;
@@ -52,7 +74,6 @@ export const renderMessageFragment = ({
prev = null,
curr = null,
contextId = 0,
read = true,
context = "user",
}) => {
if (!curr) return null;
@@ -71,13 +92,7 @@ export const renderMessageFragment = ({
return (
<React.Fragment key={mid}>
{divider && <Divider content={divider}></Divider>}
<Message
read={read}
context={context}
mid={mid}
key={mid}
contextId={contextId}
/>
<Message context={context} mid={mid} key={mid} contextId={contextId} />
</React.Fragment>
);
};
+2
View File
@@ -10,6 +10,8 @@ const StyledWrapper = styled.div`
box-shadow: inset -1px 0px 0px rgba(0, 0, 0, 0.1);
.list {
margin: 12px 8px;
overflow: scroll;
padding-bottom: 50px;
> .nav {
display: flex;
flex-direction: column;
+4 -2
View File
@@ -54,11 +54,13 @@ export default function usePreload() {
getServer();
}
}, [rehydrated]);
const canStreaming =
contactsSuccess && rehydrated && !initializing && !streaming;
useEffect(() => {
if (rehydrated && !initializing && !streaming) {
if (canStreaming) {
request = startStreaming(store);
}
}, [rehydrated, store, streaming, initializing]);
}, [canStreaming]);
// console.log("loading", contactsLoading, serverLoading, !checked);
return {