refactor: finished tailwind
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 7.5L10 12.5L15 7.5" stroke="#344054" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" stroke="#344054" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 7.5L10 12.5L15 7.5" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 221 B After Width: | Height: | Size: 222 B |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 20 20" color="#78787c" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" style="color: rgb(120, 120, 124);"><path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd"></path><path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z"></path></svg>
|
||||
|
After Width: | Height: | Size: 668 B |
@@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 20 20" color="#78787c" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" style="color: rgb(120, 120, 124);"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 443 B |
@@ -1,3 +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 width="5" height="8" viewBox="0 0 5 8" fill="#616161" 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" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 570 B After Width: | Height: | Size: 559 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.0682 7.75765L16.2425 2.93189C14.9152 1.60462 12.6777 1.96772 11.8382 3.6466L9.40281 8.51748C9.31512 8.69287 9.16223 8.82694 8.97688 8.89096L4.81061 10.3302C3.93791 10.6317 3.682 11.7427 4.33487 12.3956L7.43936 15.5001L3.00008 19.9394L3 21.0001H4.06074L8.50002 16.5607L11.6045 19.6653C12.2574 20.3181 13.3684 20.0622 13.6699 19.1895L15.1092 15.0232C15.1732 14.8379 15.3073 14.685 15.4826 14.5973L20.3535 12.1619C22.0324 11.3224 22.3955 9.08491 21.0682 7.75765Z" fill="#70707B"/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#70707B" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.0682 7.75765L16.2425 2.93189C14.9152 1.60462 12.6777 1.96772 11.8382 3.6466L9.40281 8.51748C9.31512 8.69287 9.16223 8.82694 8.97688 8.89096L4.81061 10.3302C3.93791 10.6317 3.682 11.7427 4.33487 12.3956L7.43936 15.5001L3.00008 19.9394L3 21.0001H4.06074L8.50002 16.5607L11.6045 19.6653C12.2574 20.3181 13.3684 20.0622 13.6699 19.1895L15.1092 15.0232C15.1732 14.8379 15.3073 14.685 15.4826 14.5973L20.3535 12.1619C22.0324 11.3224 22.3955 9.08491 21.0682 7.75765Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 594 B After Width: | Height: | Size: 583 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.7042 16.2945C10.0945 16.6853 10.094 17.3184 9.70324 17.7087C9.31245 18.0989 8.67928 18.0985 8.28902 17.7077L3.29241 12.7043C2.90237 12.3137 2.90255 11.681 3.29282 11.2906L8.28943 6.29297C8.67992 5.9024 9.31308 5.90234 9.70365 6.29282C10.0942 6.6833 10.0943 7.31647 9.70379 7.70703L6.411 11H13C17.3349 11 20.8645 14.4478 20.9962 18.7508L21 19C21 19.5523 20.5523 20 20 20C19.4477 20 19 19.5523 19 19C19 15.7616 16.4344 13.1224 13.2249 13.0041L13 13H6.414L9.7042 16.2945Z" fill="#616161"/>
|
||||
<path d="M9.7042 16.2945C10.0945 16.6853 10.094 17.3184 9.70324 17.7087C9.31245 18.0989 8.67928 18.0985 8.28902 17.7077L3.29241 12.7043C2.90237 12.3137 2.90255 11.681 3.29282 11.2906L8.28943 6.29297C8.67992 5.9024 9.31308 5.90234 9.70365 6.29282C10.0942 6.6833 10.0943 7.31647 9.70379 7.70703L6.411 11H13C17.3349 11 20.8645 14.4478 20.9962 18.7508L21 19C21 19.5523 20.5523 20 20 20C19.4477 20 19 19.5523 19 19C19 15.7616 16.4344 13.1224 13.2249 13.0041L13 13H6.414L9.7042 16.2945Z" fill="#667085"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 602 B After Width: | Height: | Size: 602 B |
@@ -229,10 +229,70 @@ html.dark ::-webkit-scrollbar-thumb {
|
||||
---channel-text-color: #1c1c1e;
|
||||
---msg-text-color: #374151;
|
||||
}
|
||||
/* overrides */
|
||||
/* markdown overrides */
|
||||
|
||||
#MARKDOWN_CONTAINER .toastui-editor-contents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
[class^="toastui-editor-"] .toastui-editor-md-container {
|
||||
border-bottom: none;
|
||||
}
|
||||
[class^="toastui-editor-"] .toastui-editor-md-container .toastui-editor-md-preview {
|
||||
padding-right: 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
[class^="toastui-editor-"] .toastui-editor-md-container .toastui-editor-md-splitter {
|
||||
background-color: #d0d5dd;
|
||||
}
|
||||
[class^="toastui-editor-"] .toastui-editor-md-container .ProseMirror {
|
||||
height: 100%;
|
||||
}
|
||||
[class^="toastui-editor-"] .ProseMirror,
|
||||
[class^="toastui-editor-"] p,
|
||||
[class^="toastui-editor-"] .toastui-editor.md-mode {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #475467;
|
||||
margin: 0 0 16px;
|
||||
word-break: break-all;
|
||||
}
|
||||
[class^="toastui-editor-"] a {
|
||||
background-color: transparent;
|
||||
}
|
||||
[class^="toastui-editor-"] pre {
|
||||
width: 100%;
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
[class^="toastui-editor-"] img {
|
||||
cursor: pointer;
|
||||
max-width: 300px;
|
||||
display: block;
|
||||
}
|
||||
[class^="toastui-editor-"] ul {
|
||||
white-space: nowrap;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
[class^="toastui-editor-"] ul > li:before {
|
||||
margin-top: 10px !important;
|
||||
margin-left: -14px;
|
||||
background-color: #475467;
|
||||
}
|
||||
[class^="toastui-editor-"] ol li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
[class^="toastui-editor-"] li ol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
[class^="toastui-editor-"] ul,
|
||||
[class^="toastui-editor-"] ol {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, FC } from "react";
|
||||
import { Ping } from '@uiball/loaders';
|
||||
import { getDefaultSize } from "../../utils";
|
||||
// import { getDefaultSize } from "../../utils";
|
||||
|
||||
type Props = {
|
||||
uploading: boolean;
|
||||
@@ -20,7 +20,7 @@ const ImageMessage: FC<Props> = ({
|
||||
properties
|
||||
}) => {
|
||||
const [url, setUrl] = useState(thumbnail);
|
||||
const { width = 0, height = 0 } = getDefaultSize(properties);
|
||||
// const { width = 0, height = 0 } = getDefaultSize(properties);
|
||||
useEffect(() => {
|
||||
const newUrl = thumbnail;
|
||||
const img = new Image();
|
||||
@@ -36,21 +36,21 @@ const ImageMessage: FC<Props> = ({
|
||||
return (
|
||||
<div className="relative w-fit h-fit">
|
||||
{uploading && (
|
||||
<div className="absolute left-0 top-0 w-full h-full bg-white/40 flex flex-col justify-center items-center gap-1">
|
||||
<div className="absolute left-0 top-0 w-full h-full bg-white/50 flex flex-col justify-center items-center gap-1">
|
||||
<Ping
|
||||
size={45}
|
||||
speed={2}
|
||||
color="#666"
|
||||
color="#555"
|
||||
/>
|
||||
<span className="text-xs text-gray-600">{progress}%</span>
|
||||
<span className="text-xs text-gray-500">{progress}%</span>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
className="max-w-[480px] cursor-zoom-in object-cover preview"
|
||||
style={{
|
||||
width: width ? `${width}px` : "",
|
||||
height: height ? `${height}px` : ""
|
||||
}}
|
||||
className="w-full h-auto max-w-[480px] cursor-zoom-in object-cover preview"
|
||||
// style={{
|
||||
// width: width ? `${width}px` : "",
|
||||
// height: height ? `${height}px` : ""
|
||||
// }}
|
||||
data-meta={JSON.stringify(properties)}
|
||||
data-origin={content}
|
||||
data-download={download}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
|
||||
const MarkdownOverrides = createGlobalStyle`
|
||||
[class^='toastui-editor-'] {
|
||||
.toastui-editor-md-container {
|
||||
border-bottom: none;
|
||||
.toastui-editor-md-preview {
|
||||
padding-right: 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
.toastui-editor-md-splitter {
|
||||
background-color: #D0D5DD;
|
||||
}
|
||||
.ProseMirror {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.ProseMirror, p, .toastui-editor.md-mode {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #475467;
|
||||
margin: 0 0 16px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: none;
|
||||
display: flex;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
padding: 16px;
|
||||
color: #98a2b3;
|
||||
opacity: 0.8;
|
||||
box-shadow: inset 2px 0px 0px #a8b0bd;
|
||||
align-items: center;
|
||||
|
||||
> p {
|
||||
padding-right: 10px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> blockquote {
|
||||
padding: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
max-width: 300px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul{
|
||||
white-space: nowrap;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
> li:before {
|
||||
margin-top: 8px;
|
||||
margin-left: -14px;
|
||||
background-color: #475467;
|
||||
}
|
||||
}
|
||||
ol li{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
li ol{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3, [class*='heading'] {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-bottom: none;
|
||||
font-weight: 700;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
h1, [class*='heading1'] {
|
||||
margin-top: 0 !important;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
h2, [class*='heading2'] {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
h3, [class*='heading3'] {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default MarkdownOverrides;
|
||||
@@ -96,7 +96,7 @@ const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleE
|
||||
<>
|
||||
<ul
|
||||
ref={cmdsRef}
|
||||
className={clsx(`cmds bg-white dark:bg-gray-900 rounded-md z-[999] absolute right-2.5 top-0 -translate-y-1/2 flex items-center border border-solid border-black/10 invisible`, tippyVisible && '!visible')}>
|
||||
className={clsx(`bg-white dark:bg-gray-900 rounded-md z-[999] absolute right-2.5 top-0 -translate-y-1/2 flex items-center border border-solid border-black/10 invisible group-hover:visible`, tippyVisible && '!visible')}>
|
||||
<Tippy
|
||||
onShow={handleTippyVisible.bind(null, true)}
|
||||
onHide={handleTippyVisible.bind(null, false)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FC, ReactElement, useEffect, useState } from "react";
|
||||
import StyledMsg from "./styled";
|
||||
import renderContent from "./renderContent";
|
||||
import Avatar from "../Avatar";
|
||||
import useFavMessage from "../../hook/useFavMessage";
|
||||
@@ -18,22 +17,22 @@ const FavoredMessage: FC<Props> = ({ id = "" }) => {
|
||||
const favorite_mids = messages.map(({ from_mid }) => +from_mid) || [];
|
||||
|
||||
setMsgs(
|
||||
<div data-favorite-mids={favorite_mids.join(",")} className="favorite flex flex-col rounded-md bg-slate-200">
|
||||
<div data-favorite-mids={favorite_mids.join(",")} className="favorite flex flex-col rounded-md bg-slate-50 dark:bg-slate-800">
|
||||
<div className="list">
|
||||
{messages.map((msg, idx) => {
|
||||
const { user = {}, download, content, content_type, properties, thumbnail } = msg;
|
||||
return (
|
||||
<StyledMsg className="archive" key={idx}>
|
||||
<div className="w-full relative flex items-start gap-3 px-2 py-1 my-2 rounded-lg dark:hover:bg-gray-800" key={idx}>
|
||||
{user && (
|
||||
<div className="avatar">
|
||||
<div className="">
|
||||
<Avatar width={40} height={40} src={user.avatar} name={user.name} />
|
||||
</div>
|
||||
)}
|
||||
<div className="details">
|
||||
<div className="up">
|
||||
<span className="name">{user?.name || "Deleted User"}</span>
|
||||
<div className="w-full flex flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<span className="text-gray-600 dark:text-gray-400">{user?.name || "Deleted User"}</span>
|
||||
</div>
|
||||
<div className="down">
|
||||
<div className="select-text text-gray-800 break-all whitespace-pre-wrap dark:text-white">
|
||||
{renderContent({
|
||||
download,
|
||||
content,
|
||||
@@ -43,7 +42,7 @@ const FavoredMessage: FC<Props> = ({ id = "" }) => {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</StyledMsg>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
|
||||
const forward_mids = messages.map(({ from_mid }) => from_mid) || [];
|
||||
// console.log("fff", messages);
|
||||
setForwards(
|
||||
<div data-forwarded-mids={forward_mids.join(",")} className="flex flex-col rounded-lg bg-gray-100">
|
||||
<div data-forwarded-mids={forward_mids.join(",")} className="flex flex-col rounded-lg bg-gray-100 dark:bg-gray-900">
|
||||
<h4 className="p-2 pb-0 flex items-center gap-1 text-gray-500 text-xs">
|
||||
<IconForward className="w-4 h-4 fill-gray-500" />
|
||||
{t("action.forward")}
|
||||
@@ -35,17 +35,17 @@ const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
|
||||
{messages.map((msg, idx) => {
|
||||
const { user = {}, download, content, content_type, properties, thumbnail } = msg;
|
||||
return (
|
||||
<StyledMsg className="archive" key={idx}>
|
||||
<div className="w-full relative flex items-start gap-4 px-2 py-1 my-2 rounded-lg" key={idx}>
|
||||
{user && (
|
||||
<div className="avatar">
|
||||
<div >
|
||||
<Avatar width={24} height={24} src={user.avatar} name={user.name} />
|
||||
</div>
|
||||
)}
|
||||
<div className="details">
|
||||
<div className="up">
|
||||
<span className="name">{user?.name || "Deleted User"}</span>
|
||||
<div className="w-full flex flex-col">
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<span className="text-gray-500 text-sm">{user?.name || "Deleted User"}</span>
|
||||
</div>
|
||||
<div className="down">
|
||||
<div className="select-text text-gray-500 text-sm break-all whitespace-pre-wrap dark:text-white">
|
||||
{renderContent({
|
||||
download,
|
||||
context,
|
||||
@@ -58,7 +58,7 @@ const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</StyledMsg>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import clsx from "clsx";
|
||||
import dayjs from "dayjs";
|
||||
import renderContent from "./renderContent";
|
||||
import Avatar from "../Avatar";
|
||||
import StyledWrapper from "./styled";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
|
||||
interface Props {
|
||||
@@ -16,20 +14,19 @@ const PreviewMessage: FC<Props> = ({ mid = 0, context }) => {
|
||||
return { msg: store.message[mid], usersData: store.users.byId };
|
||||
});
|
||||
if (!msg) return null;
|
||||
const { from_uid = 0, created_at, content_type, content, thumbnail = "", properties } = msg;
|
||||
const { from_uid = 0, content_type, content, thumbnail = "", properties } = msg;
|
||||
const { name, avatar } = usersData[from_uid] ?? {};
|
||||
const pinMsg = context == "pin";
|
||||
return (
|
||||
<StyledWrapper className={clsx(`preview`, pinMsg && "max-h-64 !bg-transparent overflow-auto overflow-x-hidden border border-solid border-gray-200 dark:border-gray-400")}>
|
||||
<div className="avatar">
|
||||
<div className={clsx(`w-full relative flex items-start gap-3 p-2 my-2 rounded-lg`, pinMsg && "max-h-64 overflow-auto overflow-x-hidden border border-solid border-gray-200 dark:border-gray-400")}>
|
||||
<div>
|
||||
<Avatar width={40} height={40} src={avatar} name={name} />
|
||||
</div>
|
||||
<div className="details">
|
||||
<div className="up">
|
||||
<span className="name">{name}</span>
|
||||
<i className="time">{dayjs(created_at).format("YYYY-MM-DD h:mm:ss A")}</i>
|
||||
<div className="w-full flex flex-col items-start">
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<span className="text-gray-500 text-sm">{name}</span>
|
||||
</div>
|
||||
<div className={`down`}>
|
||||
<div className={`select-text text-gray-600 text-sm break-all whitespace-pre-wrap dark:text-white`}>
|
||||
{renderContent({
|
||||
content_type,
|
||||
content,
|
||||
@@ -39,7 +36,7 @@ const PreviewMessage: FC<Props> = ({ mid = 0, context }) => {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ const renderContent = (data: MessagePayload) => {
|
||||
switch (content_type) {
|
||||
case ContentTypes.text:
|
||||
res = (
|
||||
<span className="text-ellipsis overflow-hidden break-words break-all text-gray-800">
|
||||
<span className="text-ellipsis overflow-hidden break-words break-all text-gray-800 dark:text-gray-100">
|
||||
<LinkifyText text={content} url={false} mentionTextOnly={true} mentionPopOver={false} />
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case ContentTypes.markdown:
|
||||
res = (
|
||||
<div className="max-h-[152px] overflow-hidden">
|
||||
<div className="max-h-[152px] overflow-hidden dark:text-gray-100">
|
||||
<MarkdownRender content={content} />
|
||||
</div>
|
||||
);
|
||||
@@ -36,7 +36,7 @@ const renderContent = (data: MessagePayload) => {
|
||||
res = (
|
||||
<>
|
||||
{icon}
|
||||
<span className="ml-1 text-[10px] text-gray-500">{name}</span>
|
||||
<span className="ml-1 text-[10px] text-gray-500 dark:text-gray-100">{name}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -77,7 +77,7 @@ const Reply: FC<ReplyProps> = ({ mid, interactive = true }) => {
|
||||
<div
|
||||
key={mid}
|
||||
data-mid={mid}
|
||||
className={`flex items-start p-2 bg-gray-100 rounded-lg gap-2 mb-1 ${interactive ? "cursor-pointer" : "!bg-transparent"}`}
|
||||
className={`flex items-start p-2 bg-gray-100 dark:bg-gray-900 rounded-lg gap-2 mb-1 ${interactive ? "cursor-pointer" : "!bg-transparent"}`}
|
||||
onClick={interactive ? handleClick : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||
@@ -90,7 +90,7 @@ const Reply: FC<ReplyProps> = ({ mid, interactive = true }) => {
|
||||
/>
|
||||
<span className="text-sm text-primary-500">{currUser.name}</span>
|
||||
</div>
|
||||
<div className="text-sm flex items-center text-gray-400 overflow-hidden">{renderContent(data)}</div>
|
||||
<div className="text-sm flex items-center overflow-hidden">{renderContent(data)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useRef, useState, useEffect, FC } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import useInView from "./useInView";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import Reaction from "./Reaction";
|
||||
import Reply from "./Reply";
|
||||
import Profile from "../Profile";
|
||||
import Avatar from "../Avatar";
|
||||
import StyledWrapper from "./styled";
|
||||
import Commands from "./Commands";
|
||||
import EditMessage from "./EditMessage";
|
||||
import renderContent from "./renderContent";
|
||||
@@ -91,13 +91,16 @@ const Message: FC<IProps> = ({
|
||||
const _key = properties?.local_id || mid;
|
||||
const showExpire = (expires_in ?? 0) > 0;
|
||||
return (
|
||||
<StyledWrapper
|
||||
<div
|
||||
key={_key}
|
||||
onContextMenu={readOnly ? undefined : handleContextMenuEvent}
|
||||
data-msg-mid={mid}
|
||||
ref={inviewRef}
|
||||
className={`message dark:hover:!bg-gray-800 ${readOnly ? "readonly" : ""} ${showExpire ? "auto_delete" : ""} ${pinInfo ? "pinned" : ""} ${contextMenuVisible ? "contextVisible" : ""
|
||||
} `}
|
||||
className={clsx(`group w-full relative flex items-start gap-4 px-2 py-1 my-2 rounded-lg dark:hover:bg-gray-800 hover:bg-gray-50`,
|
||||
readOnly && "hover:bg-transparent",
|
||||
showExpire && "bg-red-200",
|
||||
pinInfo && "bg-cyan-50 dark:bg-cyan-800 pt-7"
|
||||
)}
|
||||
>
|
||||
<Tippy
|
||||
key={_key}
|
||||
@@ -108,7 +111,7 @@ const Message: FC<IProps> = ({
|
||||
trigger="click"
|
||||
content={<Profile uid={fromUid || 0} type="card" cid={context == "user" ? 0 : contextId} />}
|
||||
>
|
||||
<div className="avatar" data-uid={fromUid} ref={avatarRef}>
|
||||
<div className="cursor-pointer" data-uid={fromUid} ref={avatarRef}>
|
||||
<Avatar width={40} height={40} src={currUser?.avatar} name={currUser?.name} />
|
||||
</div>
|
||||
</Tippy>
|
||||
@@ -121,26 +124,29 @@ const Message: FC<IProps> = ({
|
||||
hide={hideContextMenu}
|
||||
>
|
||||
<div
|
||||
className="details"
|
||||
className={clsx("w-full flex flex-col gap-2", pinInfo && "relative")}
|
||||
data-pin-tip={`pinned by ${pinInfo?.created_by ? usersData[pinInfo.created_by]?.name : ""
|
||||
}`}
|
||||
>
|
||||
<div className="up">
|
||||
<span className="name">{currUser?.name || "Deleted User"}</span>
|
||||
{pinInfo && <span className="absolute left-0 -top-1 -translate-y-full text-xs text-gray-400">
|
||||
{`pinned by ${pinInfo.created_by ? usersData[pinInfo.created_by]?.name : ""}`}
|
||||
</span>}
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<span className="text-primary-500 text-sm">{currUser?.name || "Deleted User"}</span>
|
||||
<Tooltip
|
||||
delay={200}
|
||||
disabled={!timePrefix || readOnly}
|
||||
placement="top"
|
||||
tip={dayjsTime.format("YYYY-MM-DD h:mm:ss A")}
|
||||
>
|
||||
<time className="time">
|
||||
<time className="text-gray-400 text-xs">
|
||||
{timePrefix
|
||||
? `${timePrefix} ${dayjsTime.format("h:mm A")}`
|
||||
: dayjsTime.format("YYYY-MM-DD h:mm:ss A")}
|
||||
</time>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={`down dark:!text-white ${sending ? "sending" : ""}`}>
|
||||
<div className={`select-text text-gray-800 text-sm break-all whitespace-pre-wrap dark:!text-white ${sending ? "opacity-90" : ""}`}>
|
||||
{reply_mid && <Reply key={reply_mid} mid={reply_mid} />}
|
||||
{edit ? (
|
||||
<EditMessage mid={mid} cancelEdit={toggleEditMessage} />
|
||||
@@ -180,7 +186,7 @@ const Message: FC<IProps> = ({
|
||||
toggleEditMessage={toggleEditMessage}
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default React.memo(Message, (prevs, nexts) => {
|
||||
|
||||
@@ -39,7 +39,7 @@ const renderContent = ({
|
||||
<>
|
||||
<LinkifyText text={content} cid={to} />
|
||||
{edited && (
|
||||
<span className="edited" title={dayjs(+edited).format("YYYY-MM-DD h:mm:ss A")}>
|
||||
<span className="ml-1 text-gray-500 text-[10px]" title={dayjs(+edited).format("YYYY-MM-DD h:mm:ss A")}>
|
||||
({i18n.t("edited", { ns: "chat" })})
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledMsg = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 4px 8px;
|
||||
margin: 8px 0;
|
||||
border-radius: 8px;
|
||||
&[data-highlight="true"] {
|
||||
background: #f5f6f7;
|
||||
}
|
||||
&.auto_delete {
|
||||
background: #f1d1ca50;
|
||||
}
|
||||
&.pinned {
|
||||
background: #ecfdff;
|
||||
}
|
||||
&:hover,
|
||||
&.contextVisible,
|
||||
&.preview {
|
||||
background: #f5f6f7;
|
||||
.cmds {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
&.readonly:hover {
|
||||
background: none;
|
||||
}
|
||||
> .avatar {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
img {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
> .details {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
.up {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
.name {
|
||||
color: #06b6d4;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.time {
|
||||
color: #bfbfbf;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
.down {
|
||||
user-select: text;
|
||||
color: #374151;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
.edited {
|
||||
margin-left: 5px;
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
}
|
||||
&.sending {
|
||||
opacity: 0.9;
|
||||
}
|
||||
> .link {
|
||||
text-decoration: none;
|
||||
border-radius: 2px;
|
||||
/* background-color: #f5feff; */
|
||||
padding: 2px;
|
||||
color: #1fe1f9;
|
||||
}
|
||||
/* 下载图标颜色 */
|
||||
.download_icon.gray{
|
||||
path{
|
||||
fill: #616161;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.archive {
|
||||
gap: 8px;
|
||||
> .details {
|
||||
gap: 0;
|
||||
.up .name {
|
||||
font-weight: 600;
|
||||
color: #475467;
|
||||
}
|
||||
.down {
|
||||
color: #475467;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.pinned {
|
||||
padding-top: 26px;
|
||||
> .details {
|
||||
position: relative;
|
||||
&:before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -4px;
|
||||
transform: translateY(-100%);
|
||||
content: attr(data-pin-tip);
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #98a2b3;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledMsg;
|
||||
@@ -1,7 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
import renderContent from "./Message/renderContent";
|
||||
import Avatar from "./Avatar";
|
||||
import StyledWrapper from "./Message/styled";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { FC } from "react";
|
||||
import { PinnedMessage } from "../../types/channel";
|
||||
@@ -23,16 +22,16 @@ const PinnedMessageView: FC<Props> = ({ data }) => {
|
||||
const { created_at, content_type, content, properties, thumbnail = "" } = { ...data, ...normalized };
|
||||
const { name, avatar } = usersData[created_by] ?? {};
|
||||
return (
|
||||
<StyledWrapper className={`preview`}>
|
||||
<div className="avatar">
|
||||
<div className="w-full relative flex items-start gap-3 p-2 my-2 rounded-lg">
|
||||
<div>
|
||||
<Avatar width={40} height={40} src={avatar} name={name} />
|
||||
</div>
|
||||
<div className="details">
|
||||
<div className="up">
|
||||
<span className="name">{name}</span>
|
||||
<i className="time">{dayjs(created_at).format("YYYY-MM-DD h:mm:ss A")}</i>
|
||||
<div className="w-full flex flex-col items-start gap-1 text-sm">
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<span className="text-gray-500 dark:text-gray-400">{name}</span>
|
||||
<time className="text-xs text-gray-400">{dayjs(created_at).format("YYYY-MM-DD h:mm:ss A")}</time>
|
||||
</div>
|
||||
<div className={`down`}>
|
||||
<div className={`select-text text-gray-600 break-all whitespace-pre-wrap dark:text-white`}>
|
||||
{renderContent({
|
||||
content_type,
|
||||
content,
|
||||
@@ -42,7 +41,7 @@ const PinnedMessageView: FC<Props> = ({ data }) => {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
const isCard = type == 'card';
|
||||
const canRemoveFromServer = !isCard && canRemove;
|
||||
const hasMore = email || canRemoveFromChannel || canRemoveFromServer;
|
||||
const iconClass = `cursor-pointer flex flex-col items-center gap-1 rounded-lg w-32 text-primary-400 bg-slate-100 dark:bg-gray-700 text-sm pt-3.5 pb-3`;
|
||||
const containerClass = clsx(`flex-center flex-col w-[432px] gap-1 z-[99] mt-20 select-none`, isCard && "p-4 w-[280px] bg-white dark:bg-gray-800 drop-shadow rounded-md");
|
||||
const iconClass = `cursor-pointer flex flex-col items-center gap-1 rounded-lg w-32 text-primary-400 bg-gray-100 dark:bg-gray-800 text-sm pt-3.5 pb-3`;
|
||||
const containerClass = clsx(`flex-center flex-col gap-1 z-[99] mt-20 select-none`, isCard ? "p-4 w-[280px] bg-white dark:bg-gray-800 drop-shadow rounded-md" : "w-[432px]");
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<Avatar
|
||||
@@ -65,8 +65,6 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
<span>{t("send_msg")}</span>
|
||||
</li>
|
||||
</NavLink>
|
||||
{/* <NavLink to={`#`}> */}
|
||||
{/* </NavLink> */}
|
||||
<Tippy
|
||||
disabled={!hasMore}
|
||||
interactive
|
||||
|
||||
@@ -72,9 +72,9 @@ export default function Replying({
|
||||
const user = usersData[from_uid];
|
||||
|
||||
return (
|
||||
<div className="reply bg-gray-100 z-[999] flex items-start justify-start gap-4 rounded-t-lg w-full px-4 py-3 text-sm">
|
||||
<div className="reply bg-gray-100 dark:bg-gray-700 z-[999] flex items-start justify-start gap-4 rounded-t-lg w-full px-4 py-3 text-sm">
|
||||
<div className="whitespace-nowrap text-gray-400 ">
|
||||
Replying to <span className="font-bold text-gray-600">{user?.name}</span>
|
||||
Replying to <span className="font-bold text-gray-600 dark:text-gray-400">{user?.name}</span>
|
||||
</div>
|
||||
<div className="text-gray-500 overflow-hidden pr-7 ">{renderContent(msg)}</div>
|
||||
<button className="absolute top-4 right-4 cursor-pointer" onClick={removeReply}>
|
||||
|
||||
@@ -145,7 +145,7 @@ const Send: FC<IProps> = ({
|
||||
{replying_mid && <Replying context={context} mid={replying_mid} id={id} />}
|
||||
{mode == Modes.text && <UploadFileList context={context} id={id} />}
|
||||
|
||||
<div className={clsx(`flex justify-between items-start gap-4 px-4 py-3.5`, isMarkdownMode && `grid grid-cols-[1fr_1fr] grid-rows-[auto_auto] gap-0`)}>
|
||||
<div className={clsx(`flex justify-between items-center gap-4 px-4 py-3.5`, isMarkdownMode && `grid grid-cols-[1fr_1fr] grid-rows-[auto_auto] gap-0`)}>
|
||||
<EmojiPicker selectEmoji={insertEmoji} />
|
||||
{mode == Modes.text && (
|
||||
<MixedInput
|
||||
|
||||
@@ -36,7 +36,7 @@ const StyledSettingContainer: FC<PropsWithChildren<Props>> = ({
|
||||
<ul key={title} data-title={title} className="flex flex-col gap-0.5 mb-9 before:pl-3 before:content-[attr(data-title)] before:font-bold before:text-xs before:text-gray-400">
|
||||
{items.map(({ name, title }) => {
|
||||
return (
|
||||
<li key={name} className={clsx(`text-sm text-gray-700 whitespace-nowrap dark:text-gray-200 rounded hover:bg-[#e7e5e4] dark:hover:bg-slate-500/20`, name == nav?.name && "bg-[#e7e5e4] dark:bg-slate-500/20")}>
|
||||
<li key={name} className={clsx(`text-sm font-semibold text-gray-600 whitespace-nowrap dark:text-gray-200 rounded hover:bg-[#e7e5e4] dark:hover:bg-slate-500/20`, name == nav?.name && "bg-[#e7e5e4] dark:bg-slate-500/20")}>
|
||||
<NavLink to={`${pathname}?nav=${name}`} className="block px-3 py-1">{title}</NavLink>
|
||||
</li>
|
||||
);
|
||||
@@ -50,7 +50,7 @@ const StyledSettingContainer: FC<PropsWithChildren<Props>> = ({
|
||||
if (typeof d === "boolean" || !d) return null;
|
||||
const { title, handler } = d;
|
||||
return (
|
||||
<li key={title} onClick={handler} className="text-sm text-white dark:text-gray-200 rounded cursor-pointer py-1.5 px-3 bg-[#ef4444] hover:bg-red-600">
|
||||
<li key={title} onClick={handler} className="text-sm text-red-500 dark:text-red-400 rounded cursor-pointer py-1.5 px-3">
|
||||
{title}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -45,7 +45,7 @@ const User: FC<Props> = ({
|
||||
const online = curr.online || curr.uid == loginUid;
|
||||
const containerClass = clsx(`relative flex items-center justify-start gap-2 p-2 rounded-lg select-none `, interactive && "hover:bg-gray-500/10", compact && "p-0");
|
||||
const nameClass = clsx(`text-sm text-gray-500 max-w-[190px] overflow-hidden text-ellipsis font-semibold dark:text-white`);
|
||||
const statusClass = clsx(`absolute -bottom-0.5 -right-1.5 w-3 h-3 box-content rounded-full border-[2px] border-solid border-white`,
|
||||
const statusClass = clsx(`absolute -bottom-0.5 -right-1.5 w-3 h-3 box-content rounded-full border-[2px] border-solid border-white dark:border-gray-300`,
|
||||
online ? "bg-[#22c55e]" : "bg-[#a1a1aa]",
|
||||
compact && "w-3.5 h-3.5");
|
||||
if (!popover)
|
||||
|
||||
@@ -17,7 +17,7 @@ const StyledButton = ({ children, className = '', ...rest }: Props) => {
|
||||
isSmall && "!py-2",
|
||||
noBorder && "!shadow-none border-none",
|
||||
isMini && "!px-2.5 !py-1 !text-xs",
|
||||
isDanger && "bg-red-700 disabled:bg-gray-300 hover:bg-red-700/80",
|
||||
isDanger && "bg-red-500 disabled:bg-gray-300 hover:bg-red-500/80 active:bg-red-700",
|
||||
className
|
||||
)} {...rest}>
|
||||
{children}
|
||||
|
||||
@@ -1,80 +1,7 @@
|
||||
import clsx from "clsx";
|
||||
import { useState, FC, DetailedHTMLProps, InputHTMLAttributes } from "react";
|
||||
import { HiEye, HiEyeOff } from "react-icons/hi";
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0px 1px 2px rgba(31, 41, 55, 0.08);
|
||||
|
||||
.prefix {
|
||||
padding: 8px 16px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #9ca3af;
|
||||
background: #f3f4f6;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.view {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
// todo: check ime problem
|
||||
const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #333;
|
||||
padding: 8px;
|
||||
outline: none;
|
||||
|
||||
&:not(.inner) {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0px 1px 2px rgba(31, 41, 55, 0.08);
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
padding: 11px 8px;
|
||||
}
|
||||
|
||||
&.none {
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #78787c;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
&[type="password"] {
|
||||
padding-right: 30px;
|
||||
}
|
||||
`;
|
||||
|
||||
import IconEyeOpen from "../../../assets/icons/eye.open.svg";
|
||||
import IconEyeClose from "../../../assets/icons/eye.close.svg";
|
||||
interface Props
|
||||
extends DetailedHTMLProps<
|
||||
Pick<
|
||||
@@ -100,26 +27,36 @@ interface Props
|
||||
ref?: any;
|
||||
}
|
||||
|
||||
const Input: FC<Props> = ({ type = "text", prefix = "", className, ...rest }) => {
|
||||
const Input: FC<Props> = ({ type = "text", prefix = "", className = "", ...rest }) => {
|
||||
const [inputType, setInputType] = useState(type);
|
||||
const togglePasswordVisible = () => {
|
||||
setInputType((prev) => (prev == "password" ? "text" : "password"));
|
||||
};
|
||||
|
||||
const isLarge = className.includes("large");
|
||||
const isNone = className.includes("none");
|
||||
// const noInner=!className.includes("inner");
|
||||
const isPwd = type == "password";
|
||||
const inputClass = clsx(`w-full bg-white dark:bg-gray-800 text-sm text-gray-800 dark:text-gray-200 p-2 outline-none disabled:text-gray-500 disabled:bg-gray-100 dark:disabled:text-gray-500 dark:disabled:bg-gray-800/50 placeholder:text-gray-400`,
|
||||
// noInner && 'rounded border border-gray-200 shadow',
|
||||
isLarge && 'py-3',
|
||||
isNone && "border-none bg-transparent shadow-none",
|
||||
isPwd && "pr-[30px]"
|
||||
);
|
||||
return type == "password" ? (
|
||||
<StyledWrapper className={className}>
|
||||
<StyledInput type={inputType} autoComplete={inputType == "password" ? "current-password" : "on"} className={`inner ${className}`} {...rest} />
|
||||
<div className="view" onClick={togglePasswordVisible}>
|
||||
{inputType == "password" ? <HiEyeOff color="#78787c" /> : <HiEye color="#78787c" />}
|
||||
<div className={`w-full relative flex overflow-hidden rounded border border-gray-300 dark:border-gray-500 shadow ${className}`}>
|
||||
<input type={inputType} autoComplete={inputType == "password" ? "current-password" : "on"} className={`${inputClass} ${className}`} {...rest} />
|
||||
<div className="absolute top-1/2 right-2.5 -translate-y-1/2 cursor-pointer" onClick={togglePasswordVisible}>
|
||||
{inputType == "password" ? <IconEyeClose className="fill-gray-500" /> : <IconEyeOpen className="fill-gray-500" />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
) : prefix ? (
|
||||
<StyledWrapper className={className}>
|
||||
<span className="prefix">{prefix}</span>
|
||||
<StyledInput className={`inner ${className}`} type={inputType} {...rest} />
|
||||
</StyledWrapper>
|
||||
<div className={`w-full relative flex overflow-hidden rounded border border-gray-300 dark:border-gray-500 shadow ${className}`}>
|
||||
<span className="px-4 py-2 text-sm text-gray-500 dark:text-gray-300 bg-gray-200 dark:bg-gray-800 border dark:border-gray-600">{prefix}</span>
|
||||
<input className={`${inputClass} ${className}`} type={inputType} {...rest} />
|
||||
</div>
|
||||
) : (
|
||||
<StyledInput type={inputType} className={className} {...rest} />
|
||||
<input type={inputType} className={`${inputClass} rounded border border-gray-200 dark:border-gray-400 shadow ${className}`} {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ type Props = {
|
||||
values: (string | number)[];
|
||||
defaultValue?: string | number;
|
||||
onChange?: (param: any) => void;
|
||||
value: object | number | string;
|
||||
value: number | string;
|
||||
};
|
||||
|
||||
const VALUE_NOT_SET = "";
|
||||
@@ -43,12 +43,12 @@ const Radio: FC<Props> = ({
|
||||
}}
|
||||
id={`${id}-${index}`}
|
||||
/>
|
||||
<div className="drop-shadow px-2 py-3 border border-solid border-[#d0d5dd] rounded-lg w-full h-full bg-white peer-checked:bg-primary-400 text-sm text-gray-500 peer-checked:text-white">
|
||||
<div className="drop-shadow px-2 py-3 border border-gray-300 dark:border-gray-400 rounded-lg w-full h-full bg-white dark:bg-gray-800 peer-checked:bg-primary-400 text-sm text-gray-500 dark:text-gray-300 peer-checked:text-white">
|
||||
<label className="ml-6" htmlFor={`${id}-${index}`}>{item}</label>
|
||||
</div>
|
||||
<div className="absolute top-1/2 left-3 -translate-y-1/2 w-3.5 h-3.5 rounded-full border border-gray-400 peer-checked:hidden"></div>
|
||||
<div className="absolute top-1/2 left-3 -translate-y-1/2 w-3.5 h-3.5 rounded-full border border-white invisible peer-checked:visible flex-center">
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
<div className="absolute top-1/2 left-3 -translate-y-1/2 w-3.5 h-3.5 rounded-full border border-gray-300 peer-checked:hidden"></div>
|
||||
<div className="absolute top-1/2 left-3 -translate-y-1/2 w-3.5 h-3.5 rounded-full border border-solid border-white invisible peer-checked:visible flex-center">
|
||||
<div className="w-1 h-1 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { TextareaHTMLAttributes } from "react";
|
||||
|
||||
type Props = TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
const StyledTextarea = ({ className, ...rest }: Props) => {
|
||||
return <textarea className={`rounded text-sm p-2 bg-white text-gray-700 resize-none w-full shadow border border-solid border-gray-200 disabled:bg-[#f9fafb] disabled:text-gray-400 disabled:pointer-events-none placeholder:text-gray-400 ${className}`} {...rest}></textarea>;
|
||||
return <textarea className={`rounded text-sm p-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 resize-none w-full shadow border border-gray-200 dark:border-gray-400
|
||||
disabled:bg-gray-100 dark:disabled:bg-gray-800/50 disabled:text-gray-400 dark:disabled:text-gray-500 disabled:pointer-events-none placeholder:text-gray-400 ${className}`}
|
||||
{...rest}></textarea>;
|
||||
};
|
||||
export default StyledTextarea;
|
||||
|
||||
@@ -9,7 +9,6 @@ import "./assets/index.css";
|
||||
import "./common/DayjsSetting";
|
||||
import "./common/TippySetting";
|
||||
import { register } from "./serviceWorkerRegistration";
|
||||
import MarkdownStyleOverride from "./common/component/MarkdownStyleOverride";
|
||||
import MobileAppTip from "./common/component/MobileAppTip";
|
||||
import ReduxRoutes from "./routes";
|
||||
import NewVersion from "./common/component/NewVersion";
|
||||
@@ -33,7 +32,6 @@ root.render(
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ReduxRoutes />
|
||||
</DndProvider>
|
||||
<MarkdownStyleOverride />
|
||||
<MobileAppTip />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -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} />
|
||||
|
||||