feat: unify session UX

This commit is contained in:
zerosoul
2022-06-13 21:22:11 +08:00
parent 5d7ea864a5
commit b3f63dc8e2
8 changed files with 404 additions and 45 deletions
+103
View File
@@ -0,0 +1,103 @@
// import React from 'react'
import Tippy from "@tippyjs/react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useLocation, useMatch } from "react-router-dom";
import { useUpdateMuteSettingMutation } from "../../../app/services/contact";
import { useReadMessageMutation } from "../../../app/services/message";
import { removeUserSession } from "../../../app/slices/message.user";
import ContextMenu from "../../../common/component/ContextMenu";
export default function SessionContextMenu({
context,
id,
visible,
mid,
hide,
deleteChannel,
setInviteChannelId,
children
}) {
const [muteChannel] = useUpdateMuteSettingMutation();
const [updateReadIndex] = useReadMessageMutation();
const pathMatched = useMatch(`/chat/dm/${id}`);
const dispatch = useDispatch();
const navigateTo = useNavigate();
const { pathname } = useLocation();
const { channelMuted } = useSelector((store) => {
return {
channelMuted: context == "channel" ? store.footprint.muteChannels[id] : false
};
});
const handleChannelSetting = () => {
navigateTo(`/setting/channel/${id}?f=${pathname}`);
};
const handleReadAll = () => {
// console.log("last mid", mids, lastMid);
if (mid) {
const param =
context == "dm" ? { users: [{ uid: +id, mid }] } : { groups: [{ gid: +id, mid }] };
updateReadIndex(param);
}
};
const handleRemoveSession = () => {
dispatch(removeUserSession(id));
if (pathMatched) {
navigateTo("/chat");
}
};
const handleChannelMute = () => {
const data = channelMuted ? { remove_groups: [id] } : { add_groups: [{ gid: id }] };
muteChannel(data);
};
const items =
context == "dm"
? [
{
title: "Mark As Read",
handler: handleReadAll
},
{
title: "Hide Session",
danger: true,
handler: handleRemoveSession
}
]
: [
{
title: "Settings",
underline: true,
handler: handleChannelSetting
},
{
title: "Mark As Read",
// underline: true
handler: handleReadAll
},
{
title: channelMuted ? "Unmute" : "Mute",
handler: handleChannelMute
},
{
title: "Invite People",
handler: setInviteChannelId.bind(null, id)
},
{
title: "Delete Channel",
danger: true,
handler: deleteChannel.bind(null, id)
}
];
return (
<Tippy
interactive
placement="right-start"
popperOptions={{ strategy: "fixed" }}
followCursor={"initial"}
visible={visible}
onClickOutside={hide}
content={<ContextMenu hideMenu={hide} items={items} />}
>
{children}
</Tippy>
);
}
+108
View File
@@ -0,0 +1,108 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import dayjs from "dayjs";
import { useDrop } from "react-dnd";
import { NativeTypes } from "react-dnd-html5-backend";
import relativeTime from "dayjs/plugin/relativeTime";
import ContextMenu from "./ContextMenu";
import { renderPreviewMessage } from "../utils";
import Contact from "../../../common/component/Contact";
import iconChannel from "../../../assets/icons/channel.svg?url";
import IconLock from "../../../assets/icons/lock.svg";
import useContextMenu from "../../../common/hook/useContextMenu";
import { useNavigate, NavLink } from "react-router-dom";
dayjs.extend(relativeTime);
export default function Session({
type = "dm",
id,
mid,
setFiles,
setDeleteChannelId,
setInviteChannelId
}) {
const navigate = useNavigate();
const [{ isActive }, drop] = useDrop(() => ({
accept: [NativeTypes.FILE],
drop({ dataTransfer }) {
if (dataTransfer.files.length) {
// console.log(files, rest);
setFiles([...dataTransfer.files]);
navigate(type == "dm" ? `/chat/dm/${id}` : `/chat/channel/${id}`);
// 重置
setTimeout(() => {
setFiles([]);
}, 300);
}
},
collect: (monitor) => ({
isActive: monitor.canDrop() && monitor.isOver()
})
}));
const { visible: contextMenuVisible, handleContextMenuEvent, hideContextMenu } = useContextMenu();
const [data, setData] = useState(null);
const { messageData, contactData, channelData } = useSelector((store) => {
return {
messageData: store.message,
contactData: store.contacts.byId,
channelData: store.channels.byId
};
});
const handleImageError = (evt) => {
evt.target.classList.add("channel_default");
evt.target.src = iconChannel;
};
useEffect(() => {
const tmp = type == "dm" ? contactData[id] : channelData[id];
if (!tmp) return;
const { name, icon, avatar, is_public = true } = tmp;
const session =
type == "dm" ? { name, icon: avatar, mid, is_public } : { name, icon, mid, is_public };
setData(session);
}, [id, mid, type, contactData, channelData]);
if (!data) return null;
const previewMsg = messageData[mid] || {};
const { name, icon, is_public } = data;
return (
<li className="session">
<ContextMenu
visible={contextMenuVisible}
hide={hideContextMenu}
context={type}
id={id}
mid={mid}
setInviteChannelId={setInviteChannelId}
deleteChannel={setDeleteChannelId}
>
<NavLink
ref={drop}
className={`nav ${isActive ? "drop_over" : ""}`}
to={type == "dm" ? `/chat/dm/${id}` : `/chat/channel/${id}`}
onContextMenu={handleContextMenuEvent}
>
<div className="icon">
{type == "dm" ? (
<Contact avatarSize={40} compact interactive={false} className="avatar" uid={id} />
) : (
<img
className={`${icon ? "" : "channel_default"}`}
onError={handleImageError}
src={icon || iconChannel}
/>
)}
</div>
<div className="details">
<div className="up">
<span className="name">
{name} {!is_public && <IconLock />}
</span>
<span className="time">{dayjs(previewMsg.created_at).fromNow()}</span>
</div>
<div className="down">
<span className="msg">{renderPreviewMessage(previewMsg)}</span>
</div>
</div>
</NavLink>
</ContextMenu>
</li>
);
}
+84
View File
@@ -0,0 +1,84 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import Styled from "./styled";
import Session from "./Session";
import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal";
import InviteModal from "../../../common/component/InviteModal";
export default function SessionList() {
const [deleteId, setDeleteId] = useState(null);
const [inviteChannelId, setInviteChannelId] = useState(null);
const [sessions, setSessions] = useState([]);
const { channelIDs, DMs, readChannels, readUsers, channelMessage, userMessage, loginUid } =
useSelector((store) => {
return {
loginUid: store.authData.uid,
channelIDs: store.channels.ids,
DMs: store.userMessage.ids,
userMessage: store.userMessage.byId,
channelMessage: store.channelMessage,
readChannels: store.footprint.readChannels,
readUsers: store.footprint.readUsers
};
});
useEffect(() => {
const cSessions = channelIDs.map((id) => {
const mids = channelMessage[id];
if (!mids || mids.length == 0) {
return { mid: null, unreads: 0, id, type: "channel" };
}
const mid = [...mids].pop();
return { key: `channel_${id}`, id, mid, type: "channel" };
});
const uSessions = DMs.map((id) => {
const mids = userMessage[id];
if (!mids || mids.length == 0) {
return { mid: null, unreads: 0, id, type: "dm" };
}
const mid = [...mids].pop();
return { key: `dm_${id}`, type: "dm", id, mid };
});
setSessions(
[...cSessions, ...uSessions].sort((a, b) => {
return b.mid - a.mid;
})
);
}, [channelIDs, DMs, channelMessage, readChannels, readUsers, loginUid, userMessage]);
return (
<>
<Styled>
{sessions.map((s) => {
const { key, type, id, mid } = s;
return (
<Session
key={key}
type={type}
id={id}
mid={mid}
setInviteChannelId={setInviteChannelId}
setDeleteChannelId={setDeleteId}
className="session"
/>
);
})}
</Styled>
{deleteId && (
<DeleteChannelConfirmModal
id={deleteId}
closeModal={() => {
setDeleteId(null);
}}
/>
)}
{inviteChannelId && (
<InviteModal
type="channel"
cid={inviteChannelId}
closeModal={() => {
setInviteChannelId(null);
}}
/>
)}
</>
);
}
+78
View File
@@ -0,0 +1,78 @@
import styled from "styled-components";
const Styled = styled.ul`
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px;
height: calc(100vh - 56px - 56px - 16px);
overflow: auto;
> .session {
> a {
display: flex;
gap: 8px;
border-radius: 8px;
padding: 8px;
width: 100%;
&.active,
&:hover {
background: rgba(116, 127, 141, 0.2);
}
.icon {
flex: 1;
background-color: #eee;
border-radius: 50%;
img {
width: 40px;
height: 40px;
&.channel_default {
padding: 5px;
/* height: 35px; */
}
}
}
.details {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
.up {
display: flex;
align-items: center;
justify-content: space-between;
.name {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #52525b;
max-width: 112px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.time {
font-weight: 500;
font-size: 12px;
line-height: 18px;
color: #78787c;
}
}
.down {
display: flex;
align-items: center;
.msg {
font-weight: 400;
font-size: 12px;
line-height: 18px;
color: #78787c;
white-space: nowrap;
overflow: hidden;
width: 140px;
text-overflow: ellipsis;
}
}
}
}
}
`;
export default Styled;