refactor: finished tailwind
This commit is contained in:
@@ -1,19 +1,10 @@
|
||||
import { FC, FormEvent } from "react";
|
||||
import styled from "styled-components";
|
||||
import usePinMessage from "../../../common/hook/usePinMessage";
|
||||
import IconSurprise from "../../../assets/icons/emoji.surprise.svg";
|
||||
import IconClose from "../../../assets/icons/close.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PinnedMessage from "../../../common/component/PinnedMessage";
|
||||
const Styled = styled.div`
|
||||
.preview {
|
||||
background: none;
|
||||
.down img {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
id: number;
|
||||
};
|
||||
@@ -27,23 +18,23 @@ const PinList: FC<Props> = ({ id }: Props) => {
|
||||
};
|
||||
const noPins = pins.length == 0;
|
||||
return (
|
||||
<Styled className="p-4 drop-shadow-md overflow-y-scroll max-h-[90vh] min-w-[486px] rounded-xl bg-gray-50">
|
||||
<h4 className=" text-gray-600 mb-4 font-semibold">{t("pinned_msg")}({pins.length})</h4>
|
||||
<div className="p-4 drop-shadow-md overflow-y-scroll max-h-[90vh] min-w-[486px] rounded-xl bg-gray-50 dark:bg-gray-800">
|
||||
<h4 className=" text-gray-600 dark:text-gray-400 mb-4 font-semibold">{t("pinned_msg")}({pins.length})</h4>
|
||||
{noPins ? (
|
||||
<div className="flex flex-col items-center gap-2 w-full p-4">
|
||||
<IconSurprise />
|
||||
<div className="w-60 font-semibold text-gray-500 text-center">{t("pin_empty_tip")}</div>
|
||||
<div className="w-60 font-semibold text-gray-500 dark:text-gray-300 text-center">{t("pin_empty_tip")}</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{pins.map((data) => {
|
||||
return (
|
||||
<li key={data.mid} className="group relative border border-solid border-slate-100 rounded-md ">
|
||||
<li key={data.mid} className="group relative border border-solid border-slate-100 dark:border-slate-600 rounded-md ">
|
||||
<PinnedMessage data={data} />
|
||||
<div className="invisible group-hover:visible flex items-center gap-1 absolute top-1 right-1 p-1 border border-solid border-black/10 rounded-md">
|
||||
<div className="invisible group-hover:visible flex items-center gap-1 absolute top-1 right-1 p-1 border border-solid border-black/10 dark:border-gray-500 rounded-md">
|
||||
{canPin && (
|
||||
<button className="flex bg-none border-none" data-mid={data.mid} onClick={handleUnpin}>
|
||||
<IconClose className="fill-slate-900" />
|
||||
<IconClose className="fill-slate-900 dark:fill-slate-300" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -52,7 +43,7 @@ const PinList: FC<Props> = ({ id }: Props) => {
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</Styled>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default PinList;
|
||||
|
||||
@@ -8,7 +8,6 @@ import FavList from "../FavList";
|
||||
import { useReadMessageMutation } from "../../../app/services/message";
|
||||
import { updateRememberedNavs } from "../../../app/slices/ui";
|
||||
import useMessageFeed from "../../../common/hook/useMessageFeed";
|
||||
import useConfig from "../../../common/hook/useConfig";
|
||||
import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||
import Tooltip from "../../../common/component/Tooltip";
|
||||
import User from "../../../common/component/User";
|
||||
@@ -18,13 +17,11 @@ import EditIcon from "../../../assets/icons/edit.svg";
|
||||
import IconFav from "../../../assets/icons/bookmark.svg";
|
||||
import IconPeople from "../../../assets/icons/people.svg";
|
||||
import IconPin from "../../../assets/icons/pin.svg";
|
||||
import IconHeadphone from "../../../assets/icons/headphone.svg";
|
||||
|
||||
import addIcon from "../../../assets/icons/add.svg?url";
|
||||
import IconAdd from "../../../assets/icons/add.svg";
|
||||
import InviteModal from "../../../common/component/InviteModal";
|
||||
import LoadMore from "../LoadMore";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { AgoraConfig } from "../../../types/server";
|
||||
import { useTranslation } from "react-i18next";
|
||||
type Props = {
|
||||
cid?: number;
|
||||
@@ -32,7 +29,6 @@ type Props = {
|
||||
};
|
||||
function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
const { t } = useTranslation("chat");
|
||||
const { values: agoraConfig } = useConfig("agora");
|
||||
const {
|
||||
pulling,
|
||||
list: msgIds,
|
||||
@@ -86,6 +82,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
const readIndex = footprint.readChannels[cid];
|
||||
const pinCount = data?.pinned_messages?.length || 0;
|
||||
const feeds = [...msgIds, ...appends];
|
||||
const toolClass = `relative cursor-pointer`;
|
||||
return (
|
||||
<>
|
||||
{addMemberModalVisible && <InviteModal cid={cid} closeModal={toggleAddVisible} />}
|
||||
@@ -94,14 +91,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
context="channel"
|
||||
dropFiles={dropFiles}
|
||||
aside={
|
||||
<ul className="tools">
|
||||
{(agoraConfig as AgoraConfig)?.enabled && (
|
||||
<li className="tool">
|
||||
<Tooltip tip="Voice/Video Chat" placement="left">
|
||||
<IconHeadphone />
|
||||
</Tooltip>
|
||||
</li>
|
||||
)}
|
||||
<ul className="flex flex-col gap-6">
|
||||
<Tooltip tip={t("pin")} placement="left">
|
||||
<Tippy
|
||||
placement="left-start"
|
||||
@@ -111,8 +101,9 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
trigger="click"
|
||||
content={<PinList id={cid} />}
|
||||
>
|
||||
<li className={`tool ${pinCount > 0 ? "badge" : ""}`} data-count={pinCount}>
|
||||
<IconPin />
|
||||
<li className={`${toolClass}`}>
|
||||
{pinCount > 0 ? <span className="absolute -top-2 -right-2 flex-center w-4 h-4 rounded-full bg-primary-400 text-white font-bold text-[10px]">{pinCount}</span> : null}
|
||||
<IconPin className="fill-gray-500" />
|
||||
</li>
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
@@ -125,23 +116,23 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
trigger="click"
|
||||
content={<FavList cid={cid} />}
|
||||
>
|
||||
<li className={`tool fav`} data-count={pinCount}>
|
||||
<IconFav />
|
||||
<li className={`${toolClass}`}>
|
||||
<IconFav className="fill-gray-500" />
|
||||
</li>
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
<li
|
||||
className={`tool ${membersVisible ? "active" : ""}`}
|
||||
className={`${toolClass}`}
|
||||
onClick={toggleMembersVisible}
|
||||
>
|
||||
<Tooltip tip={t("channel_members")} placement="left">
|
||||
<IconPeople />
|
||||
<IconPeople className={membersVisible ? "fill-gray-600" : ""} />
|
||||
</Tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
header={
|
||||
<header className="head flex items-center h-full justify-center md:justify-between">
|
||||
<header className="box-border h-14 px-5 border-solid border-b border-b-black/10 flex items-center justify-center md:justify-between">
|
||||
<div className="flex items-center gap-1 text-base">
|
||||
<ChannelIcon personal={!is_public} />
|
||||
<span className="text-gray-800 dark:text-white">{name}</span>
|
||||
@@ -153,7 +144,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
<div className={`flex-col gap-1 w-[226px] h-[calc(100vh_-_56px_-_22px)] overflow-y-scroll p-2 shadow-[inset_1px_0px_0px_rgba(0,_0,_0,_0.1)] ${membersVisible ? "flex" : "hidden"}`}>
|
||||
{addVisible && (
|
||||
<div className="cursor-pointer flex items-center justify-start gap-1 select-none rounded-lg p-2.5 hover:bg-gray-500/10" onClick={toggleAddVisible}>
|
||||
<img className="w-6 h-6" src={addIcon} />
|
||||
<IconAdd className="w-6 h-6 dark:fill-slate-300" />
|
||||
<div className="font-semibold text-sm text-gray-600 dark:text-gray-50">{t("add_channel_members")}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
import { useState, FC, MouseEvent } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { NativeTypes } from "react-dnd-html5-backend";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import useContextMenu from "../../../common/hook/useContextMenu";
|
||||
import ContextMenu from "../../../common/component/ContextMenu";
|
||||
import InviteModal from "../../../common/component/InviteModal";
|
||||
import Tooltip from "../../../common/component/Tooltip";
|
||||
import IconSetting from "../../../assets/icons/setting.svg";
|
||||
import IconInvite from "../../../assets/icons/invite.from.channel.svg";
|
||||
import { useReadMessageMutation } from "../../../app/services/message";
|
||||
import { useUpdateMuteSettingMutation } from "../../../app/services/user";
|
||||
import StyledLink from "./styled";
|
||||
import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||
import { getUnreadCount } from "../utils";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
interface IProps {
|
||||
id: number;
|
||||
setFiles: (files: File[]) => void;
|
||||
toggleRemoveConfirm: (id: number) => void;
|
||||
}
|
||||
const NavItem: FC<IProps> = ({ id, setFiles, toggleRemoveConfirm }) => {
|
||||
const { t } = useTranslation();
|
||||
const { pathname } = useLocation();
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [muteChannel] = useUpdateMuteSettingMutation();
|
||||
const [updateReadIndex] = useReadMessageMutation();
|
||||
|
||||
const {
|
||||
visible: contextMenuVisible,
|
||||
offset,
|
||||
handleContextMenuEvent,
|
||||
hideContextMenu
|
||||
} = useContextMenu();
|
||||
const {
|
||||
channel,
|
||||
mids = [],
|
||||
messageData,
|
||||
readIndex,
|
||||
muted,
|
||||
loginUid = 0
|
||||
} = useAppSelector((store) => {
|
||||
return {
|
||||
channel: store.channels.byId[id],
|
||||
mids: store.channelMessage[id],
|
||||
messageData: store.message,
|
||||
loginUid: store.authData.user?.uid,
|
||||
readIndex: store.footprint.readChannels[id],
|
||||
muted: store.footprint.muteChannels[id]
|
||||
};
|
||||
});
|
||||
const handleChannelSetting = (evt: MouseEvent<SVGElement>) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const { id } = evt.currentTarget.dataset;
|
||||
if (id) {
|
||||
navigate(`/setting/channel/${id}?f=${pathname}`);
|
||||
}
|
||||
};
|
||||
|
||||
// todo: type check
|
||||
const [{ isActive }, drop] = useDrop<any, unknown, { isActive: boolean }>(() => ({
|
||||
accept: [NativeTypes.FILE],
|
||||
drop({ dataTransfer }) {
|
||||
if (dataTransfer.files.length) {
|
||||
// console.log(files, rest);
|
||||
setFiles([...dataTransfer.files]);
|
||||
navigate(`/chat/channel/${id}`);
|
||||
// 重置
|
||||
setTimeout(() => {
|
||||
setFiles([]);
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isActive: monitor.canDrop() && monitor.isOver()
|
||||
})
|
||||
}));
|
||||
const handleReadAll = () => {
|
||||
const lastMid = mids[mids.length - 1];
|
||||
if (lastMid) {
|
||||
const param = { groups: [{ gid: id, mid: lastMid }] };
|
||||
updateReadIndex(param);
|
||||
}
|
||||
};
|
||||
const toggleInviteModalVisible = (evt?: Event) => {
|
||||
if (evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
setInviteModalVisible((prev) => !prev);
|
||||
};
|
||||
const handleMute = () => {
|
||||
const data = muted ? { remove_groups: [id] } : { add_groups: [{ gid: id }] };
|
||||
muteChannel(data);
|
||||
};
|
||||
if (!channel) return null;
|
||||
const { is_public, name, owner } = channel;
|
||||
const { unreads = 0, mentions = [] } = getUnreadCount({
|
||||
mids,
|
||||
messageData,
|
||||
readIndex,
|
||||
loginUid
|
||||
});
|
||||
const isMentions = mentions.length !== 0;
|
||||
const inviteIconVisible = is_public || owner == loginUid;
|
||||
return (
|
||||
<>
|
||||
<Tippy
|
||||
interactive
|
||||
placement="right-start"
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
offset={[offset.y, offset.x]}
|
||||
visible={contextMenuVisible}
|
||||
onClickOutside={hideContextMenu}
|
||||
key={id}
|
||||
content={
|
||||
<ContextMenu
|
||||
hideMenu={hideContextMenu}
|
||||
items={[
|
||||
{
|
||||
title: t("action.mark_read"),
|
||||
underline: true,
|
||||
handler: handleReadAll
|
||||
},
|
||||
{
|
||||
title: muted ? t("action.unmute") : t("action.mute"),
|
||||
handler: handleMute
|
||||
},
|
||||
{
|
||||
title: t("action.invite_people"),
|
||||
handler: toggleInviteModalVisible
|
||||
},
|
||||
{
|
||||
title: t("channel.delete", { ns: "setting" }),
|
||||
danger: true,
|
||||
handler: toggleRemoveConfirm.bind(null, id)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StyledLink
|
||||
data-cid={id}
|
||||
onContextMenu={handleContextMenuEvent}
|
||||
ref={drop}
|
||||
key={id}
|
||||
className={`link ${isActive ? "drop_over" : ""} ${muted ? "muted" : ""}`}
|
||||
activeclassname="link_active"
|
||||
to={`/chat/channel/${id}`}
|
||||
>
|
||||
<div className={`name`} title={name}>
|
||||
<ChannelIcon personal={!is_public} muted={!!muted} />
|
||||
<span className={`txt ${unreads == 0 ? "read" : ""}`}>{name}</span>
|
||||
</div>
|
||||
<div className="icons">
|
||||
{inviteIconVisible && (
|
||||
<Tooltip placement="bottom" tip="Add Member">
|
||||
<IconInvite
|
||||
className="icon invite"
|
||||
data-id={id}
|
||||
onClick={toggleInviteModalVisible}
|
||||
></IconInvite>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip placement="bottom" tip="Channel Setting">
|
||||
<IconSetting
|
||||
className="icon setting"
|
||||
data-id={id}
|
||||
onClick={handleChannelSetting}
|
||||
></IconSetting>
|
||||
</Tooltip>
|
||||
{unreads > 0 && (
|
||||
<i className={`badge ${isMentions ? "mention" : ""}`}>
|
||||
{isMentions ? mentions.length : unreads}
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
</StyledLink>
|
||||
</Tippy>
|
||||
{inviteModalVisible && (
|
||||
<InviteModal
|
||||
type="channel"
|
||||
cid={id}
|
||||
title={channel?.name}
|
||||
closeModal={toggleInviteModalVisible}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavItem;
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import DeleteConfirmModal from "../../settingChannel/DeleteConfirmModal";
|
||||
import NavItem from "./NavItem";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
|
||||
export default function ChannelList({ setDropFiles }: { setDropFiles: (files: File[]) => void }) {
|
||||
const [currId, setCurrId] = useState<number>();
|
||||
const { channelIds } = useAppSelector((store) => {
|
||||
return { channelIds: store.channels.ids };
|
||||
});
|
||||
|
||||
const setRemoveChannel = (cid?: number) => {
|
||||
setCurrId(cid);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{channelIds.map((cid) => {
|
||||
return (
|
||||
<NavItem
|
||||
key={cid}
|
||||
toggleRemoveConfirm={setRemoveChannel}
|
||||
id={cid}
|
||||
setFiles={setDropFiles}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{typeof currId !== "undefined" && (
|
||||
<DeleteConfirmModal id={currId} closeModal={setRemoveChannel} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
import { NavLink } from "react-router-dom";
|
||||
|
||||
const Styled = styled(NavLink)`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
&:hover,
|
||||
&.active {
|
||||
background: rgba(116, 127, 141, 0.1);
|
||||
}
|
||||
> .name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.txt {
|
||||
color: #1c1c1e;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
width: 150px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
&.read {
|
||||
color: #616161;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
> .icon {
|
||||
display: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
&:hover path {
|
||||
fill: #667085;
|
||||
}
|
||||
}
|
||||
> .badge {
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
border-radius: 50%;
|
||||
background: #22ccee;
|
||||
font-weight: 900;
|
||||
font-size: 10px;
|
||||
line-height: 10px;
|
||||
&.mention {
|
||||
background: #f97066;
|
||||
}
|
||||
&.dot {
|
||||
min-width: unset;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.muted {
|
||||
.name .txt {
|
||||
color: #d0d5dd;
|
||||
}
|
||||
.icons .badge {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
}
|
||||
&:hover > .icons {
|
||||
> .badge {
|
||||
display: none;
|
||||
}
|
||||
> .icon {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Styled;
|
||||
@@ -46,7 +46,7 @@ const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
|
||||
context="user"
|
||||
dropFiles={dropFiles}
|
||||
aside={
|
||||
<ul className="tools">
|
||||
<ul className="flex flex-col gap-6">
|
||||
<Tooltip tip="Saved Items" placement="left">
|
||||
<Tippy
|
||||
placement="left-start"
|
||||
@@ -56,15 +56,15 @@ const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
|
||||
trigger="click"
|
||||
content={<FavList uid={uid} />}
|
||||
>
|
||||
<li className={`tool fav`}>
|
||||
<FavIcon />
|
||||
<li className={`relative cursor-pointer fav`}>
|
||||
<FavIcon className="fill-gray-500" />
|
||||
</li>
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
}
|
||||
header={
|
||||
<header className="head h-full flex items-center justify-between">
|
||||
<header className="box-border h-14 px-5 border-b border-b-black/10 flex items-center justify-between">
|
||||
<User interactive={false} uid={currUser.uid} />
|
||||
</header>
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { useEffect, useState, FC } from "react";
|
||||
import { NavLink, useNavigate, useMatch } from "react-router-dom";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { NativeTypes } from "react-dnd-html5-backend";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { useReadMessageMutation } from "../../../app/services/message";
|
||||
import { removeUserSession } from "../../../app/slices/message.user";
|
||||
import useNormalizeMessage from "../../../common/hook/useNormalizeMessage";
|
||||
import useContextMenu from "../../../common/hook/useContextMenu";
|
||||
import ContextMenu from "../../../common/component/ContextMenu";
|
||||
import { renderPreviewMessage } from "../utils";
|
||||
import User from "../../../common/component/User";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { ArchiveMessage } from "../../../types/resource";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface IProps {
|
||||
uid: number;
|
||||
mid?: number;
|
||||
unreads: number;
|
||||
setFiles: (files: File[]) => void;
|
||||
}
|
||||
const NavItem: FC<IProps> = ({ uid, mid = 0, unreads, setFiles }) => {
|
||||
const { t } = useTranslation();
|
||||
const [previewMsg, setPreviewMsg] = useState<ArchiveMessage>();
|
||||
const { messages: normalizedMessages, normalizeMessage } = useNormalizeMessage();
|
||||
const dispatch = useDispatch();
|
||||
const pathMatched = useMatch(`/chat/dm/${uid}`);
|
||||
const [updateReadIndex] = useReadMessageMutation();
|
||||
const { currMsg, currUser } = useAppSelector((store) => {
|
||||
return {
|
||||
currUser: store.users.byId[uid],
|
||||
currMsg: store.message[mid]
|
||||
};
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const { visible: contextMenuVisible, handleContextMenuEvent, hideContextMenu } = useContextMenu();
|
||||
const [{ isActive }, drop] = useDrop(() => ({
|
||||
accept: [NativeTypes.FILE],
|
||||
drop({ dataTransfer }) {
|
||||
if (dataTransfer.files.length) {
|
||||
setFiles([...dataTransfer.files]);
|
||||
navigate(`/chat/dm/${uid}`);
|
||||
// 重置
|
||||
setTimeout(() => {
|
||||
setFiles([]);
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isActive: monitor.canDrop() && monitor.isOver()
|
||||
})
|
||||
}));
|
||||
useEffect(() => {
|
||||
if (currMsg) {
|
||||
if (currMsg.content_type == ContentTypes.archive) {
|
||||
normalizeMessage(currMsg.content);
|
||||
} else {
|
||||
setPreviewMsg(currMsg);
|
||||
}
|
||||
}
|
||||
}, [currMsg]);
|
||||
useEffect(() => {
|
||||
if (normalizedMessages) {
|
||||
setPreviewMsg(normalizedMessages?.pop());
|
||||
}
|
||||
}, [normalizedMessages]);
|
||||
|
||||
const handleReadAll = () => {
|
||||
const param = { users: [{ uid, mid }] };
|
||||
updateReadIndex(param);
|
||||
};
|
||||
const handleRemoveSession = () => {
|
||||
dispatch(removeUserSession(uid));
|
||||
if (pathMatched) {
|
||||
navigate("/chat");
|
||||
}
|
||||
};
|
||||
if (!currUser) return null;
|
||||
return (
|
||||
<Tippy
|
||||
interactive
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
placement="right-start"
|
||||
visible={contextMenuVisible}
|
||||
followCursor={"initial"}
|
||||
onClickOutside={hideContextMenu}
|
||||
key={uid}
|
||||
content={
|
||||
<ContextMenu
|
||||
hideMenu={hideContextMenu}
|
||||
items={[
|
||||
{
|
||||
title: t("action.mark_read"),
|
||||
handler: handleReadAll
|
||||
},
|
||||
{
|
||||
title: t("action.hide_session"),
|
||||
danger: true,
|
||||
handler: handleRemoveSession
|
||||
}
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NavLink
|
||||
ref={drop}
|
||||
key={uid}
|
||||
className={`link session ${isActive ? "drop_over" : ""}`}
|
||||
to={`/chat/dm/${uid}`}
|
||||
onContextMenu={handleContextMenuEvent}
|
||||
>
|
||||
<User compact interactive={false} uid={uid} />
|
||||
<div className="details">
|
||||
<div className="up">
|
||||
<span className="name">{currUser.name}</span>
|
||||
{previewMsg && <time>{dayjs(previewMsg.created_at).fromNow()}</time>}
|
||||
</div>
|
||||
|
||||
<div className="down">
|
||||
<div className="msg">{renderPreviewMessage(previewMsg)}</div>
|
||||
{unreads > 0 && (
|
||||
<i className={`badge ${unreads > 99 ? "dot" : ""}`}>
|
||||
{unreads > 99 ? null : unreads}
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
</Tippy>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavItem;
|
||||
@@ -1,59 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { getUnreadCount } from "../utils";
|
||||
import NavItem from "./NavItem";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
|
||||
interface Props {
|
||||
uids: number[];
|
||||
setDropFiles: (files: File[]) => void;
|
||||
}
|
||||
|
||||
const DMList: FC<Props> = ({ uids, setDropFiles }) => {
|
||||
const {
|
||||
userMessage,
|
||||
messageData,
|
||||
readUsers,
|
||||
loginUid = 0
|
||||
} = useAppSelector((store) => {
|
||||
return {
|
||||
loginUid: store.authData.user?.uid,
|
||||
readUsers: store.footprint.readUsers,
|
||||
userData: store.users.byId,
|
||||
userMessage: store.userMessage.byId,
|
||||
messageData: store.message
|
||||
};
|
||||
});
|
||||
const sessions = uids.map((uid) => {
|
||||
const mids = userMessage[uid] || [];
|
||||
if (mids.length == 0) {
|
||||
return { lastMid: null, unreads: 0, uid };
|
||||
}
|
||||
const lastMid = [...mids].sort().pop();
|
||||
const readIndex = readUsers[uid];
|
||||
const { unreads = 0 } = getUnreadCount({
|
||||
mids,
|
||||
readIndex,
|
||||
messageData,
|
||||
loginUid
|
||||
});
|
||||
|
||||
return { lastMid, unreads, uid };
|
||||
});
|
||||
// console.log("temp uids", sessions);
|
||||
return (
|
||||
<>
|
||||
{sessions
|
||||
.sort((s1, s2) => {
|
||||
if (!s1.lastMid) return s2.lastMid - Infinity;
|
||||
return s2.lastMid - s1.lastMid;
|
||||
})
|
||||
.map(({ lastMid, uid, unreads }) => {
|
||||
return (
|
||||
<NavItem key={uid} uid={uid} mid={lastMid} unreads={unreads} setFiles={setDropFiles} />
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DMList;
|
||||
@@ -1,23 +1,10 @@
|
||||
import { MouseEvent, FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FavoredMessage from "../../common/component/Message/FavoredMessage";
|
||||
import IconSurprise from "../../assets/icons/emoji.surprise.svg";
|
||||
import IconRemove from "../../assets/icons/close.svg";
|
||||
import useFavMessage from "../../common/hook/useFavMessage";
|
||||
import { useTranslation } from "react-i18next";
|
||||
// 覆盖样式
|
||||
const Styled = styled.div`
|
||||
.favorite {
|
||||
background: none;
|
||||
&:hover{
|
||||
background: #f5f6f7;
|
||||
}
|
||||
.down img {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = { cid?: number; uid?: number };
|
||||
const FavList: FC<Props> = ({ cid = null, uid = null }) => {
|
||||
const { t } = useTranslation("chat");
|
||||
@@ -29,22 +16,22 @@ const FavList: FC<Props> = ({ cid = null, uid = null }) => {
|
||||
};
|
||||
const noFavs = favorites.length == 0;
|
||||
return (
|
||||
<Styled className="p-4 bg-slate-50 rounded-xl min-w-[500px] max-h-[500px] overflow-auto drop-shadow-[0px_25px_50px_rgba(31,_41,_55,_0.25)]">
|
||||
<h4 className="font-bold text-base text-gray-600 mb-4">{t('fav_msg')}({favorites.length})</h4>
|
||||
<div className="p-4 bg-slate-50 dark:bg-slate-800 rounded-xl min-w-[500px] max-h-[500px] overflow-auto drop-shadow-[0px_25px_50px_rgba(31,_41,_55,_0.25)]">
|
||||
<h4 className="font-bold text-base text-gray-600 dark:text-gray-400 mb-4">{t('fav_msg')}({favorites.length})</h4>
|
||||
{noFavs ? (
|
||||
<div className="flex flex-col gap-2 w-full items-center p-4">
|
||||
<IconSurprise />
|
||||
<div className="w-60 text-base text-gray-600 text-center font-bold">{t("fav_empty_tip")}</div>
|
||||
<div className="w-60 text-base text-gray-600 dark:text-gray-400 text-center font-bold">{t("fav_empty_tip")}</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{favorites.map(({ id }) => {
|
||||
return (
|
||||
<li key={id} className="relative border border-solid border-slate-200 rounded-md group">
|
||||
<li key={id} className="relative border border-solid border-slate-200 dark:border-gray-600 rounded-md group">
|
||||
<FavoredMessage id={id} />
|
||||
<div className="flex items-center absolute top-2 right-2 border border-solid border-gray-300 rounded-md overflow-hidden invisible group-hover:visible">
|
||||
<div className="flex items-center absolute top-2 right-2 border border-solid border-gray-300 dark:border-gray-600 rounded-md overflow-hidden invisible group-hover:visible">
|
||||
<button className="flex-center w-6 h-6 p-1" data-id={id} onClick={handleRemove}>
|
||||
<IconRemove className="fill-slate-900" />
|
||||
<IconRemove className="fill-slate-900 dark:fill-slate-500" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
@@ -52,7 +39,7 @@ const FavList: FC<Props> = ({ cid = null, uid = null }) => {
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</Styled>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default FavList;
|
||||
|
||||
@@ -21,7 +21,7 @@ const GuestBlankPlaceholder = () => {
|
||||
};
|
||||
return (
|
||||
<section className="flex flex-col items-center bg-transparent">
|
||||
<h2 className="text-3xl text-gray-600 dark:text-gray-50 font-bold ">{t("welcome", { name: serverName })}</h2>
|
||||
<h2 className="text-3xl text-gray-600 dark:text-gray-50 font-bold text-center">{t("welcome", { name: serverName })}</h2>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-400 dark:text-gray-200 my-3 text-sm">{t("guest_login_tip")}</span>
|
||||
<Button onClick={handleSignIn} className="small">{t("sign_in")}</Button>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useRef, useEffect, FC, ReactElement } from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { NativeTypes } from "react-dnd-html5-backend";
|
||||
import clsx from "clsx";
|
||||
import ImagePreviewModal from "../../../common/component/ImagePreviewModal";
|
||||
import Send from "../../../common/component/Send";
|
||||
import Styled from "./styled";
|
||||
import Operations from "./Operations";
|
||||
import useUploadFile from "../../../common/hook/useUploadFile";
|
||||
|
||||
@@ -13,7 +13,6 @@ import useLicense from "../../../common/hook/useLicense";
|
||||
import LicenseUpgradeTip from "./LicenseOutdatedTip";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
import DnDTip from "./DnDTip";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface Props {
|
||||
readonly?: boolean;
|
||||
@@ -104,14 +103,13 @@ const Layout: FC<Props> = ({
|
||||
);
|
||||
}, []);
|
||||
const name = context == "channel" ? channelsData[to]?.name : usersData[to]?.name;
|
||||
|
||||
return (
|
||||
<>
|
||||
{previewImage && <ImagePreviewModal data={previewImage} closeModal={closePreviewModal} />}
|
||||
<Styled ref={drop} className={`${readonly ? "readonly" : ""}`}>
|
||||
<article ref={drop} className={`relative w-full rounded-r-2xl`}>
|
||||
{header}
|
||||
<main className="main" ref={messagesContainer}>
|
||||
<div className="chat">
|
||||
<main className="h-full w-full flex items-start justify-between relative" ref={messagesContainer}>
|
||||
<div className="rounded-br-2xl w-full flex flex-col h-[calc(100vh_-_56px_-_18px)]">
|
||||
{children}
|
||||
<div className={`p-4 pt-0 ${selects ? "selecting" : ""}`}>
|
||||
{readonly ? (
|
||||
@@ -126,13 +124,13 @@ const Layout: FC<Props> = ({
|
||||
{selects && <Operations context={context} id={to} />}
|
||||
</div>
|
||||
</div>
|
||||
{users && <div className="members hidden md:block">{users}</div>}
|
||||
{aside && <div className="aside !hidden md:!block">{aside}</div>}
|
||||
{users && <div className="shadow-[inset_0px_10px_2px_-10px_rgba(0,_0,_0,_0.1)] hidden md:block">{users}</div>}
|
||||
{aside && <div className={clsx("p-3 absolute right-0 -top-14 translate-x-full flex-col hidden md:flex")}>{aside}</div>}
|
||||
</main>
|
||||
{!readonly && isActive && (
|
||||
<DnDTip context={context} name={name} />
|
||||
)}
|
||||
</Styled>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
const Styled = styled.article`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-top-right-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
> .head {
|
||||
box-sizing: border-box;
|
||||
height: 56px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
> .main {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
> .chat {
|
||||
border-bottom-right-radius: 16px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
height: calc(100vh - 56px - 18px);
|
||||
}
|
||||
.members {
|
||||
box-shadow: inset 0px 10px 2px -10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
> .aside {
|
||||
padding: 12px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -56px;
|
||||
transform: translateX(100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.divider {
|
||||
border: none;
|
||||
background-color: #d4d4d4;
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
margin: 18px auto;
|
||||
}
|
||||
.tools {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.tools {
|
||||
gap: 24px;
|
||||
.tool {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
&.fav svg path {
|
||||
fill: #70707b;
|
||||
}
|
||||
&.active svg path {
|
||||
fill: #3f3f46;
|
||||
}
|
||||
&:not(.active):hover svg path {
|
||||
fill: #51525c;
|
||||
}
|
||||
&.badge:after {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
content: attr(data-count);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
border-radius: 50%;
|
||||
background-color: #22ccee;
|
||||
color: #fff;
|
||||
font-weight: 900;
|
||||
font-size: 10px;
|
||||
line-height: 10px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
export default Styled;
|
||||
@@ -129,15 +129,15 @@ const Session: FC<IProps> = ({
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-between">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={clsx(`flex items-center gap-2 font-semibold text-sm text-gray-500 dark:text-white whitespace-nowrap overflow-hidden text-ellipsis`, previewMsg.created_at && "max-w-[190px]")}>
|
||||
{name} {!is_public && <IconLock />}
|
||||
<span className={clsx(`flex items-center gap-1 font-semibold text-sm text-gray-500 dark:text-white whitespace-nowrap overflow-hidden text-ellipsis`, previewMsg.created_at && "max-w-[190px]")}>
|
||||
{name} {!is_public && <IconLock className="dark:fill-gray-400" />}
|
||||
</span>
|
||||
<span className="text-xs text-gray-600 whitespace-nowrap overflow-hidden max-w-[80px] text-ellipsis">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap overflow-hidden max-w-[80px] text-ellipsis">
|
||||
{previewMsg.created_at ? dayjs(previewMsg.created_at).fromNow() : null}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap overflow-hidden w-36 text-ellipsis">{renderPreviewMessage(previewMsg)}</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap overflow-hidden w-36 text-ellipsis">{renderPreviewMessage(previewMsg)}</span>
|
||||
{unreads > 0 && (
|
||||
<strong className={clsx(`text-white h-5 min-w-[20px] bg-primary-400 font-bold text-[10px] rounded-[10px] flex-center`, unreads > 99 && 'w-1.5 h-1.5 p-0 min-w-[unset]', muted && "bg-gray-500")}>
|
||||
{unreads > 99 ? null : unreads}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import clsx from "clsx";
|
||||
import StyledWrapper from "./styled";
|
||||
|
||||
import BlankPlaceholder from "../../common/component/BlankPlaceholder";
|
||||
import Server from "../../common/component/Server";
|
||||
import ChannelChat from "./ChannelChat";
|
||||
@@ -14,7 +14,7 @@ import GuestBlankPlaceholder from "./GuestBlankPlaceholder";
|
||||
import GuestChannelChat from "./GuestChannelChat";
|
||||
import GuestSessionList from "./GuestSessionList";
|
||||
import IconList from '../../assets/icons/list.svg';
|
||||
// import IconClose from '../../assets/icons/close.svg';
|
||||
|
||||
function ChatPage() {
|
||||
const [sessionListVisible, setSessionListVisible] = useState(false);
|
||||
const [channelModalVisible, setChannelModalVisible] = useState(false);
|
||||
@@ -46,22 +46,23 @@ function ChatPage() {
|
||||
};
|
||||
// console.log("temp uid", tmpUid);
|
||||
const placeholderVisible = channel_id == 0 && user_id == 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{channelModalVisible && (
|
||||
<ChannelModal closeModal={toggleChannelModalVisible} personal={true} />
|
||||
)}
|
||||
{usersModalVisible && <UsersModal closeModal={toggleUsersModalVisible} />}
|
||||
<StyledWrapper className={`${isGuest ? "!pr-1 !pt-0" : ""} md:!pr-12 `}>
|
||||
<div className={`flex h-full pt-2 pb-2.5 pr-1 ${isGuest ? "!pr-1 !pt-0" : ""} md:!pr-12 `}>
|
||||
{sessionListVisible && <div onClick={toggleSessionList} className="z-30 fixed top-0 left-4 w-screen h-screen bg-black/50 transition-all backdrop-blur-sm"></div>}
|
||||
<div className={clsx("left fixed md:relative top-0 left-0 z-40 transition-all md:overflow-auto bg-white dark:!bg-[#1F2A37]", sessionListVisible ? "max-md:translate-x-0" : "max-md:-translate-x-full")}>
|
||||
<div className={clsx("flex-col rounded-l-2xl min-w-[268px] h-full border border-solid border-r-black/5 dark:border-none box-border fixed md:relative top-0 left-0 z-40 transition-all md:overflow-auto bg-white dark:!bg-[#1F2A37]", sessionListVisible ? "max-md:translate-x-0" : "max-md:-translate-x-full")}>
|
||||
<Server readonly={isGuest} />
|
||||
{isGuest ? <GuestSessionList /> : <SessionList tempSession={tmpSession} />}
|
||||
{sessionListVisible ? null : <button className="absolute top-2 -right-[52px] z-50 p-2 bg-none md:hidden" onClick={toggleSessionList}>
|
||||
<IconList className="dark:stroke-gray-300" />
|
||||
</button>}
|
||||
</div>
|
||||
<div className={`right ${placeholderVisible ? "placeholder" : ""} bg-white dark:!bg-[#384250]`}>
|
||||
<div className={`rounded-r-2xl w-full ${placeholderVisible ? "h-full flex-center" : ""} bg-white dark:!bg-[#384250]`}>
|
||||
{placeholderVisible && (isGuest ? <GuestBlankPlaceholder /> : <BlankPlaceholder />)}
|
||||
{channel_id !== 0 &&
|
||||
(isGuest ? (
|
||||
@@ -71,7 +72,7 @@ function ChatPage() {
|
||||
))}
|
||||
{user_id !== 0 && <DMChat uid={+user_id} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 10px;
|
||||
padding-right: 4px;
|
||||
> .left {
|
||||
flex-direction: column;
|
||||
min-width: 268px;
|
||||
box-shadow: inset -1px 0px 0px rgba(0, 0, 0, 0.05);
|
||||
height: 100%;
|
||||
border-radius: 16px 0 0 16px;
|
||||
.list {
|
||||
margin: 12px 8px;
|
||||
&.dms {
|
||||
flex: 1;
|
||||
}
|
||||
.title {
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
> .txt {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #78787c;
|
||||
}
|
||||
.icon {
|
||||
transition: transform 0.5s ease;
|
||||
transform-origin: center;
|
||||
}
|
||||
.add_icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
> .nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.session {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
&:hover,
|
||||
&.active {
|
||||
background: rgba(116, 127, 141, 0.1);
|
||||
}
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
.up {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #52525b;
|
||||
max-width: 112px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
time {
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #78787c;
|
||||
}
|
||||
}
|
||||
.down {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.msg {
|
||||
min-height: 18px;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #78787c;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 140px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
> .badge {
|
||||
color: #fff;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background: #1fe1f9;
|
||||
font-weight: 900;
|
||||
font-size: 10px;
|
||||
line-height: 10px;
|
||||
&.dot {
|
||||
min-width: unset;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
padding: 0;
|
||||
}
|
||||
&.mute {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* drop files effect */
|
||||
.drop_over {
|
||||
box-shadow: inset 0 0 0 2px #52edff;
|
||||
}
|
||||
}
|
||||
&.collapse {
|
||||
.title .icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
> .nav > .link:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .right {
|
||||
border-radius: 0 16px 16px 0;
|
||||
width: 100%;
|
||||
&.placeholder {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -145,7 +145,7 @@ function FavsPage() {
|
||||
] = messages;
|
||||
const tip = <span className={clsx("inline-flex items-center gap-1 mr-2")}>
|
||||
{gid ? <><IconChannel className="w-3 h-3" /> {channelData[gid]?.name}</> : <>
|
||||
From <strong className="font-bold text-gray-800">{userData[uid]?.name}</strong>
|
||||
From <strong className="font-bold text-gray-800 dark:text-gray-100">{userData[uid]?.name}</strong>
|
||||
</>}
|
||||
</span>;
|
||||
return (
|
||||
@@ -156,8 +156,8 @@ function FavsPage() {
|
||||
</h4>
|
||||
<div className="relative group">
|
||||
<FavoredMessage key={id} id={id} />
|
||||
<button className="absolute top-2 right-2 flex-center w-6 h-6 p-1 border border-solid border-slate-200 rounded invisible group-hover:visible" data-id={id} onClick={handleRemove} >
|
||||
<IconRemove className="fill-slate-900" />
|
||||
<button className="absolute top-2 right-2 flex-center w-6 h-6 p-1 border border-solid border-slate-200 dark:border-slate-700 rounded invisible group-hover:visible" data-id={id} onClick={handleRemove} >
|
||||
<IconRemove className="fill-slate-900 dark:fill-slate-100" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,30 +51,30 @@ function HomePage() {
|
||||
// 有点绕
|
||||
const chatNav = isChatHomePath ? "/chat" : chatPath || "/chat";
|
||||
const userNav = userPath || "/users";
|
||||
const linkClass = `flex items-center gap-2.5 px-3 py-2 font-semibold text-sm text-gray-600 rounded-lg hover:bg-black/10`;
|
||||
const linkClass = `flex items-center gap-2.5 px-3 py-2 font-semibold text-sm text-gray-600 rounded-lg hover:bg-gray-800/10`;
|
||||
return (
|
||||
<>
|
||||
<Manifest />
|
||||
{!guest && <Notification />}
|
||||
<div className={`flex w-screen h-screen dark:bg-[#121926]`}>
|
||||
<div className={`flex w-screen h-screen bg-[#e5e7eb] dark:bg-[#121926]`}>
|
||||
{!guest && (
|
||||
<div className={`h-full flex flex-col items-center relative w-16 transition-all`}>
|
||||
<div className={`h-full flex flex-col items-center relative w-16 bg-transparent transition-all`}>
|
||||
{loginUid && <User uid={loginUid} />}
|
||||
<nav className="flex flex-col gap-1 px-3 py-6">
|
||||
<NavLink
|
||||
className={() => {
|
||||
return `${linkClass} ${isChattingPage ? "bg-primary-400" : ""}`;
|
||||
return `${linkClass} ${isChattingPage ? "bg-primary-400 hover:bg-primary-400" : ""}`;
|
||||
}}
|
||||
to={chatNav}
|
||||
>
|
||||
{({ isActive }) => {
|
||||
return <Tooltip tip={t("chat")}>
|
||||
<ChatIcon className={isActive ? "fill-white" : ""} />
|
||||
<ChatIcon className={(isActive || isChattingPage) ? "fill-white" : ""} />
|
||||
</Tooltip>;
|
||||
}}
|
||||
|
||||
</NavLink>
|
||||
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400' : ""}`} to={userNav}>
|
||||
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400 hover:bg-primary-400' : ""}`} to={userNav}>
|
||||
{({ isActive }) => {
|
||||
return <Tooltip tip={t("members")}>
|
||||
<UserIcon className={isActive ? "fill-white" : ""} />
|
||||
@@ -82,7 +82,7 @@ function HomePage() {
|
||||
}}
|
||||
|
||||
</NavLink>
|
||||
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400' : ""}`} to={"/favs"}>
|
||||
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400 hover:bg-primary-400' : ""}`} to={"/favs"}>
|
||||
{({ isActive }) => {
|
||||
return <Tooltip tip={t("favs")}>
|
||||
<FavIcon className={isActive ? "fill-white" : ""} />
|
||||
@@ -91,7 +91,7 @@ function HomePage() {
|
||||
|
||||
|
||||
</NavLink>
|
||||
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400' : ""}`} to={"/files"}>
|
||||
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400 hover:bg-primary-400' : ""}`} to={"/files"}>
|
||||
{({ isActive }) => {
|
||||
return <Tooltip tip={t("files")}>
|
||||
<FolderIcon className={isActive ? "fill-white" : ""} />
|
||||
|
||||
@@ -16,7 +16,7 @@ const Channel: FC<Props> = ({ select = 0, updateFilter }) => {
|
||||
|
||||
return (
|
||||
<div className="rounded-lg p-1 pt-0 bg-white dark:bg-gray-800 overflow-auto max-h-[400px] flex flex-col items-start relative drop-shadow">
|
||||
<div className=" bg-white dark:bg-gray-800 sticky top-0 z-10">
|
||||
<div className=" bg-white dark:bg-transparent sticky top-0 z-10">
|
||||
<Search embed={true} value={input} updateSearchValue={updateInput} />
|
||||
</div>
|
||||
<ul className="w-full flex flex-col gap-4 p-2">
|
||||
|
||||
@@ -30,15 +30,15 @@ const DateFilter: FC<Props> = ({ select = "", updateFilter }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-3 bg-white min-w-[200px] overflow-auto rounded-lg flex flex-col items-start relative drop-shadow">
|
||||
<div className="p-3 bg-white dark:bg-gray-800 min-w-[200px] overflow-auto rounded-lg flex flex-col items-start relative drop-shadow">
|
||||
<ul className="w-full flex flex-col gap-4">
|
||||
<li className="relative cursor-pointer flex items-center gap-4 text-gray-500 font-semibold text-sm" onClick={handleClick.bind(null, undefined)}>
|
||||
<li className="relative cursor-pointer flex items-center gap-4 text-gray-500 dark:text-gray-300 font-semibold text-sm" onClick={handleClick.bind(null, undefined)}>
|
||||
Any Time
|
||||
{!select && <CheckSign className="absolute right-0 top-1/2 -translate-y-1/2" />}
|
||||
</li>
|
||||
{Object.entries(Dates).map(([_key, { title }]) => {
|
||||
return (
|
||||
<li key={title} className="relative cursor-pointer flex items-center gap-4 text-gray-500 font-semibold text-sm" onClick={handleClick.bind(null, _key)}>
|
||||
<li key={title} className="relative cursor-pointer flex items-center gap-4 text-gray-500 dark:text-gray-300 font-semibold text-sm" onClick={handleClick.bind(null, _key)}>
|
||||
{title}
|
||||
{select == _key && <CheckSign className="absolute right-0 -top-1/2 -translate-y-1/2" />}
|
||||
</li>
|
||||
|
||||
@@ -16,7 +16,7 @@ const From: FC<Props> = ({ select = "", updateFilter }) => {
|
||||
|
||||
return (
|
||||
<div className="rounded-lg p-1 pt-0 bg-white dark:bg-gray-800 overflow-auto max-h-[300px] flex flex-col items-start relative drop-shadow">
|
||||
<div className="bg-white sticky top-0 z-10">
|
||||
<div className="bg-white dark:bg-transparent sticky top-0 z-10">
|
||||
<Search embed={true} value={input} updateSearchValue={updateInput} />
|
||||
</div>
|
||||
<ul className="w-full flex flex-col">
|
||||
|
||||
@@ -48,15 +48,15 @@ const Type: FC<Props> = ({ select = "", updateFilter }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-3 bg-white min-w-[180px] overflow-auto shadow-md rounded-lg flex flex-col items-start relative">
|
||||
<div className="p-3 bg-white dark:bg-gray-800 min-w-[180px] overflow-auto shadow-md rounded-lg flex flex-col items-start relative">
|
||||
<ul className="w-full flex flex-col gap-4">
|
||||
<li className="relative cursor-pointer flex items-center gap-4 text-gray-500 font-semibold text-sm" onClick={handleClick.bind(null, undefined)}>
|
||||
<li className="relative cursor-pointer flex items-center gap-4 text-gray-500 dark:text-gray-300 font-semibold text-sm" onClick={handleClick.bind(null, undefined)}>
|
||||
Any Type
|
||||
{!select && <CheckSign className="absolute right-0 top-1/2 -translate-y-1/2" />}
|
||||
</li>
|
||||
{Object.entries(FileTypes).map(([type, { title, icon }]) => {
|
||||
return (
|
||||
<li key={title} className="relative cursor-pointer flex items-center gap-2 text-sm text-gray-500 font-semibold" onClick={handleClick.bind(null, type)}>
|
||||
<li key={title} className="relative cursor-pointer flex items-center gap-2 text-sm text-gray-500 dark:text-gray-300 font-semibold" onClick={handleClick.bind(null, type)}>
|
||||
{icon} {title}
|
||||
{select == type && <CheckSign className="absolute right-0 top-1/2 -translate-y-1/2" />}
|
||||
</li>
|
||||
|
||||
@@ -11,8 +11,7 @@ import { useTranslation } from "react-i18next";
|
||||
import clsx from "clsx";
|
||||
|
||||
const getClass = (selected: boolean) => {
|
||||
|
||||
return clsx(`cursor-pointer flex items-center gap-2 border border-solid box-border shadow rounded-lg py-2 px-3 text-xs`, selected && 'text-white bg-primary-400');
|
||||
return clsx(`cursor-pointer flex items-center gap-2 border border-solid dark:border-gray-400 box-border shadow rounded-lg py-2 px-3 text-xs text-gray-900 dark:text-gray-200`, selected && 'text-white bg-primary-400');
|
||||
};
|
||||
export default function Filter({ filter, updateFilter }) {
|
||||
const { t } = useTranslation("file");
|
||||
@@ -75,7 +74,7 @@ export default function Filter({ filter, updateFilter }) {
|
||||
<span className="txt">
|
||||
{t("from")} {from && userMap[from].name}
|
||||
</span>
|
||||
<ArrowDown className="arrow" />
|
||||
<ArrowDown className="dark:stroke-gray-400" />
|
||||
</div>
|
||||
</Tippy>
|
||||
<Tippy
|
||||
@@ -90,7 +89,7 @@ export default function Filter({ filter, updateFilter }) {
|
||||
onClick={toggleFilterVisible.bind(null, { channel: true })}
|
||||
>
|
||||
<span className="txt">{channel ? `In ${channelMap[channel].name}` : t("channel")}</span>
|
||||
<ArrowDown className="arrow" />
|
||||
<ArrowDown className="dark:stroke-gray-400" />
|
||||
</div>
|
||||
</Tippy>
|
||||
<Tippy
|
||||
@@ -105,7 +104,7 @@ export default function Filter({ filter, updateFilter }) {
|
||||
onClick={toggleFilterVisible.bind(null, { type: true })}
|
||||
>
|
||||
<span className="txt">{type ? FileTypes[type].title : t("type")}</span>
|
||||
<ArrowDown className="arrow" />
|
||||
<ArrowDown className="dark:stroke-gray-400" />
|
||||
</div>
|
||||
</Tippy>
|
||||
<Tippy
|
||||
@@ -120,7 +119,7 @@ export default function Filter({ filter, updateFilter }) {
|
||||
onClick={toggleFilterVisible.bind(null, { date: true })}
|
||||
>
|
||||
<span className="txt">{date ? Dates[date].title : t("date")}</span>
|
||||
<ArrowDown className="arrow" />
|
||||
<ArrowDown className="dark:stroke-gray-400" />
|
||||
</div>
|
||||
</Tippy>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ const Search: FC<Props> = ({ value = "", updateSearchValue = null, embed = false
|
||||
return (
|
||||
<div className={clsx(`relative w-full py-1.5 px-4 shadow`, embed && "py-2 shadow-none")}>
|
||||
<IconSearch className="absolute left-6 top-1/2 -translate-y-1/2" />
|
||||
<input value={value} onChange={handleChange} className="bg-black/5 rounded-full text-sm text-gray-500 py-2.5 pl-9" placeholder={`${t("action.search")}...`} />
|
||||
<input value={value} onChange={handleChange} className="bg-black/5 dark:bg-black/20 rounded-full text-sm text-gray-500 py-2.5 pl-9" placeholder={`${t("action.search")}...`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,12 +18,12 @@ export default function View({ view = "item" }: Props) {
|
||||
};
|
||||
const isGrid = view == "grid";
|
||||
return (
|
||||
<ul className={`flex border border-solid shadow rounded-lg box-border`}>
|
||||
<ul className={`flex border border-solid dark:border-gray-400 shadow rounded-lg box-border`}>
|
||||
<li className={getClass(!isGrid)} data-view={"item"} onClick={handleChangeView}>
|
||||
<IconList className={`${!isGrid ? "fill-primary-400" : ""} dark:fill-white`} />
|
||||
<IconList className={`${!isGrid ? "fill-primary-400" : ""} dark:fill-gray-400`} />
|
||||
</li>
|
||||
<li className={getClass(isGrid)} data-view={"grid"} onClick={handleChangeView}>
|
||||
<IconGrid className={`${isGrid ? "fill-primary-400" : ""} dark:fill-white`} />
|
||||
<IconGrid className={`${isGrid ? "fill-primary-400" : ""} dark:fill-gray-400`} />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function MyAccount() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-start gap-8">
|
||||
<div className="p-6 flex flex-col items-center w-[512px] bg-slate-100 dark:bg-gray-800 rounded-2xl">
|
||||
<div className="p-6 flex flex-col items-center w-[512px] bg-gray-100 dark:bg-gray-800 rounded-2xl">
|
||||
<AvatarUploader url={avatar} name={name} uploadImage={uploadAvatar} />
|
||||
<div className="mt-2 mb-16 font-bold text-lg text-gray-800 dark:text-white">
|
||||
{name} <span className="font-normal text-gray-500">#{uid}</span>
|
||||
|
||||
@@ -15,7 +15,7 @@ const Search: FC<Props> = ({ input, updateInput }) => {
|
||||
updateInput(evt.target.value);
|
||||
};
|
||||
return (
|
||||
<div className="relative min-h-[56px] px-2 py-3 flex items-center justify-between gap-2 border-solid border-b dark:border-b-gray-500">
|
||||
<div className="relative min-h-[56px] px-2 py-3 flex items-center justify-between gap-2 shadow-[rgb(0_0_0_/_10%)_0px_1px_0px] dark:border-b-gray-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<img src={searchIcon} alt="search icon" />
|
||||
<input value={input} placeholder={`${t("action.search")}...`} className="w-full text-sm bg-transparent" onChange={handleInput} />
|
||||
|
||||
Reference in New Issue
Block a user