feat: unify session UX
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg width="5" height="8" viewBox="0 0 5 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.1875 3.16699V2.33366C4.1875 1.40033 3.375 0.666992 2.5 0.666992C1.625 0.666992 0.833333 1.40033 0.833333 2.33366V3.16699C0.373083 3.16699 0 3.54009 0 4.00032V6.50032C0 6.96056 0.373083 7.33366 0.833333 7.33366H2.5H4.16667C4.62692 7.33366 5 6.96056 5 6.50032V3.97949C5 3.53076 4.63625 3.16699 4.1875 3.16699ZM3.33333 3.16699H1.66667V2.33366C1.66667 1.85747 2.05558 1.50033 2.5 1.50033C2.94442 1.50033 3.33333 1.85747 3.33333 2.33366V3.16699Z" fill="#616161"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 570 B |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -3,22 +3,17 @@ import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { AiOutlineCaretDown } from "react-icons/ai";
|
||||
|
||||
import StyledWrapper from "./styled";
|
||||
import AddIcon from "../../assets/icons/add.svg";
|
||||
import BlankPlaceholder from "../../common/component/BlankPlaceholder";
|
||||
import Server from "../../common/component/Server";
|
||||
import Tooltip from "../../common/component/Tooltip";
|
||||
// import Tooltip from "../../common/component/Tooltip";
|
||||
// import Contact from "../../common/component/Contact";
|
||||
import CurrentUser from "../../common/component/CurrentUser";
|
||||
import ChannelChat from "./ChannelChat";
|
||||
import DMChat from "./DMChat";
|
||||
import ChannelList from "./ChannelList";
|
||||
import ContactsModal from "../../common/component/ContactsModal";
|
||||
import ChannelModal from "../../common/component/ChannelModal";
|
||||
import DMList from "./DMList";
|
||||
|
||||
import SessionList from "./SessionList";
|
||||
export default function ChatPage() {
|
||||
const [channelDropFiles, setChannelDropFiles] = useState([]);
|
||||
const [userDropFiles, setUserDropFiles] = useState([]);
|
||||
@@ -36,11 +31,6 @@ export default function ChatPage() {
|
||||
const toggleChannelModalVisible = () => {
|
||||
setChannelModalVisible((prev) => !prev);
|
||||
};
|
||||
const handleToggleExpand = (evt) => {
|
||||
const { currentTarget } = evt;
|
||||
const listEle = currentTarget.parentElement.parentElement;
|
||||
listEle.classList.toggle("collapse");
|
||||
};
|
||||
const tmpUid = sessionUids.findIndex((i) => i == user_id) > -1 ? null : user_id;
|
||||
// console.log("temp uid", tmpUid);
|
||||
const placeholderVisible = !channel_id && !user_id;
|
||||
@@ -53,37 +43,7 @@ export default function ChatPage() {
|
||||
<StyledWrapper>
|
||||
<div className="left">
|
||||
<Server />
|
||||
<div className="list channels">
|
||||
<h3 className="title">
|
||||
<span className="txt" onClick={handleToggleExpand}>
|
||||
<AiOutlineCaretDown className="icon" size={18} color="#78787C" />
|
||||
CHANNELS
|
||||
</span>
|
||||
<Tooltip tip="New Channel" placement="bottom">
|
||||
<AddIcon className="add_icon" onClick={toggleChannelModalVisible} />
|
||||
</Tooltip>
|
||||
</h3>
|
||||
<nav className="nav">
|
||||
<ChannelList setDropFiles={setChannelDropFiles} />
|
||||
</nav>
|
||||
</div>
|
||||
<div className="list dms">
|
||||
<h3 className="title">
|
||||
<span className="txt" onClick={handleToggleExpand}>
|
||||
<AiOutlineCaretDown className="icon" size={18} color="#78787C" />
|
||||
DIRECT MESSAGE
|
||||
</span>
|
||||
<Tooltip tip="New DM" placement="bottom">
|
||||
<AddIcon className="add_icon" onClick={toggleContactsModalVisible} />
|
||||
</Tooltip>
|
||||
</h3>
|
||||
<nav className="nav">
|
||||
<DMList
|
||||
uids={tmpUid ? [...sessionUids, tmpUid] : sessionUids}
|
||||
setDropFiles={setUserDropFiles}
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
<SessionList />
|
||||
<CurrentUser />
|
||||
</div>
|
||||
<div className={`right ${placeholderVisible ? "placeholder" : ""}`}>
|
||||
|
||||
@@ -107,8 +107,6 @@ const StyledWrapper = styled.div`
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
> .badge {
|
||||
/* letter-spacing: -1px; */
|
||||
/* padding: 2px; */
|
||||
color: #fff;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
// import { useUpdateChannelMutation } from "../../app/services/channel";
|
||||
import { useUpdateMuteSettingMutation } from "../../app/services/contact";
|
||||
import { useReadMessageMutation } from "../../app/services/message";
|
||||
|
||||
export default function useSession({ type, id }) {
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||
const [muteChannel] = useUpdateMuteSettingMutation();
|
||||
const [updateReadIndex] = useReadMessageMutation();
|
||||
const { messageData, contactData, channelData } = useSelector((store) => {
|
||||
return {
|
||||
messageData: store.message,
|
||||
contactData: store.contacts.byId,
|
||||
channelData: store.channels.byId
|
||||
};
|
||||
});
|
||||
const toggleInviteModal = () => {
|
||||
setInviteModalVisible((prev) => !prev);
|
||||
};
|
||||
return {
|
||||
inviteModalVisible,
|
||||
toggleInviteModal
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user