build: format the code with prettier

This commit is contained in:
Tristan Yang
2023-05-19 16:31:28 +08:00
parent 7afc132bbb
commit 5bd0183651
259 changed files with 7627 additions and 5607 deletions
+28 -18
View File
@@ -1,27 +1,29 @@
import { useState, useRef, FC } from "react";
import { FC, useRef, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import Tippy from "@tippyjs/react";
import clsx from "clsx";
import { hideAll } from "tippy.js";
import toast from "react-hot-toast";
import { updateSelectMessages } from "@/app/slices/ui";
import ContextMenu, { Item } from "../ContextMenu";
import Tooltip from "../Tooltip";
import { ChatContext } from "@/types/common";
import useFavMessage from "@/hooks/useFavMessage";
import useSendMessage from "@/hooks/useSendMessage";
import ReactionPicker from "./ReactionPicker";
import replyIcon from "@/assets/icons/reply.svg?url";
import reactIcon from "@/assets/icons/reaction.svg?url";
import editIcon from "@/assets/icons/edit.svg?url";
import IconBookmark from "@/assets/icons/bookmark.add.svg";
import IconPin from "@/assets/icons/pin.svg";
import IconForward from "@/assets/icons/forward.svg";
import IconSelect from "@/assets/icons/select.svg";
import IconDelete from "@/assets/icons/delete.svg";
import editIcon from "@/assets/icons/edit.svg?url";
import IconForward from "@/assets/icons/forward.svg";
import moreIcon from "@/assets/icons/more.svg?url";
import IconPin from "@/assets/icons/pin.svg";
import reactIcon from "@/assets/icons/reaction.svg?url";
import replyIcon from "@/assets/icons/reply.svg?url";
import IconSelect from "@/assets/icons/select.svg";
import ContextMenu, { Item } from "../ContextMenu";
import Tooltip from "../Tooltip";
import ReactionPicker from "./ReactionPicker";
import useMessageOperation from "./useMessageOperation";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import { ChatContext } from "@/types/common";
type Props = {
isSelf: boolean;
context: ChatContext;
@@ -29,7 +31,13 @@ type Props = {
mid: number;
toggleEditMessage: () => void;
};
const Commands: FC<Props> = ({ isSelf, context = "dm", contextId = 0, mid = 0, toggleEditMessage }) => {
const Commands: FC<Props> = ({
isSelf,
context = "dm",
contextId = 0,
mid = 0,
toggleEditMessage
}) => {
const { t } = useTranslation();
const {
canDelete,
@@ -89,10 +97,12 @@ const Commands: FC<Props> = ({ isSelf, context = "dm", contextId = 0, mid = 0, t
<>
<ul
ref={cmdsRef}
className={clsx(`bg-white dark:bg-gray-900 rounded-md z-[999] absolute top-0 -translate-y-1/2 flex items-center border border-solid border-black/10 invisible group-hover:visible`,
tippyVisible && '!visible',
className={clsx(
`bg-white dark:bg-gray-900 rounded-md z-[999] absolute top-0 -translate-y-1/2 flex items-center border border-solid border-black/10 invisible group-hover:visible`,
tippyVisible && "!visible",
isSelf ? "left-2.5" : "right-2.5"
)}>
)}
>
<Tippy
onShow={handleTippyVisible.bind(null, true)}
onHide={handleTippyVisible.bind(null, false)}
+10 -8
View File
@@ -1,19 +1,21 @@
import { FC, ReactElement } from "react";
import Tippy from "@tippyjs/react";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import ContextMenu, { Item } from "../ContextMenu";
import Tippy from "@tippyjs/react";
import { updateSelectMessages } from "@/app/slices/ui";
import { ChatContext } from "@/types/common";
import useSendMessage from "@/hooks/useSendMessage";
import IconCopy from "@/assets/icons/copy.svg";
import IconDelete from "@/assets/icons/delete.svg";
import IconEdit from "@/assets/icons/edit.svg";
import IconReply from "@/assets/icons/reply.svg";
import IconForward from "@/assets/icons/forward.svg";
import IconPin from "@/assets/icons/pin.svg";
import IconCopy from "@/assets/icons/copy.svg";
import IconReply from "@/assets/icons/reply.svg";
import IconSelect from "@/assets/icons/select.svg";
import { updateSelectMessages } from "@/app/slices/ui";
import useSendMessage from "@/hooks/useSendMessage";
import ContextMenu, { Item } from "../ContextMenu";
import useMessageOperation from "./useMessageOperation";
import { useTranslation } from "react-i18next";
import { ChatContext } from "@/types/common";
type Props = {
context: ChatContext;
contextId: number;
+11 -5
View File
@@ -1,8 +1,9 @@
import { useState, useRef, useEffect, ChangeEvent, KeyboardEvent, FC } from "react";
import { ChangeEvent, FC, KeyboardEvent, useEffect, useRef, useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import { useKey } from "rooks";
import { useEditMessageMutation } from "@/app/services/message";
import { ContentTypes } from "@/app/config";
import { useEditMessageMutation } from "@/app/services/message";
import { useAppSelector } from "@/app/store";
type Props = {
@@ -56,7 +57,6 @@ const EditMessage: FC<Props> = ({ mid, cancelEdit }) => {
};
if (!msg) return null;
return (
<div className="w-full">
<div className="bg-gray-200 rounded-lg p-4">
@@ -80,10 +80,16 @@ const EditMessage: FC<Props> = ({ mid, cancelEdit }) => {
</div>
<div className="flex items-center p-1 gap-4 text-xs">
<span>
esc to <button className="text-primary-500 cursor-pointer px-1" onClick={cancelEdit}>cancel</button>
esc to{" "}
<button className="text-primary-500 cursor-pointer px-1" onClick={cancelEdit}>
cancel
</button>
</span>
<span>
enter to <button className="text-primary-500 cursor-pointer px-1" onClick={handleSave}>{isEditing ? "saving" : `save`}</button>
enter to{" "}
<button className="text-primary-500 cursor-pointer px-1" onClick={handleSave}>
{isEditing ? "saving" : `save`}
</button>
</span>
</div>
</div>
+75 -75
View File
@@ -1,83 +1,83 @@
import dayjs from 'dayjs';
import { FC, useEffect, useState, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { removeMessage } from '../../app/slices/message';
import { removeChannelMsg } from '../../app/slices/message.channel';
import { removeUserMsg } from '../../app/slices/message.user';
import IconTimer from '@/assets/icons/timer.svg';
import { ChatContext } from '../../types/common';
import { FC, useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import dayjs from "dayjs";
import IconTimer from "@/assets/icons/timer.svg";
import { removeMessage } from "../../app/slices/message";
import { removeChannelMsg } from "../../app/slices/message.channel";
import { removeUserMsg } from "../../app/slices/message.user";
import { ChatContext } from "../../types/common";
type Props = {
context: ChatContext,
contextId: number,
mid: number;
expiresIn: number;
createAt: number;
context: ChatContext;
contextId: number;
mid: number;
expiresIn: number;
createAt: number;
};
const ExpireTimer: FC<Props> = ({ mid, createAt, expiresIn, context, contextId }) => {
const [countdown, setCountdown] = useState<number | undefined>();
const dispatch = useDispatch();
const clearMsgFromClient = useCallback(
() => {
if (context == "dm") {
dispatch(removeUserMsg({ mid, id: contextId }));
} else {
dispatch(removeChannelMsg({ mid, id: contextId }));
const [countdown, setCountdown] = useState<number | undefined>();
const dispatch = useDispatch();
const clearMsgFromClient = useCallback(() => {
if (context == "dm") {
dispatch(removeUserMsg({ mid, id: contextId }));
} else {
dispatch(removeChannelMsg({ mid, id: contextId }));
}
dispatch(removeMessage(mid));
}, [context, contextId, mid]);
}
dispatch(removeMessage(mid));
},
[context, contextId, mid],
);
useEffect(() => {
if (expiresIn > 0 && createAt > 0) {
const expire_time = createAt + expiresIn * 1000;
if (dayjs().isAfter(new Date(expire_time))) {
// 已过期,立即删除
clearMsgFromClient();
} else {
// 倒计时
setCountdown(Math.floor((expire_time - new Date().getTime()) / 1000));
}
}
}, [expiresIn, createAt]);
useEffect(() => {
let timer = 0;
if (typeof countdown !== "undefined") {
if (countdown > 0) {
timer = window.setTimeout(() => {
setCountdown(prev => {
const _prev = prev ?? 0;
return _prev - 1;
});
}, 1000);
} else {
// 倒计时结束
console.log("countdown over", mid, countdown);
clearMsgFromClient();
}
}
return () => {
if (typeof countdown !== "undefined") {
clearTimeout(timer);
}
};
}, [countdown, mid]);
if (!countdown) return null;
const duration = dayjs.duration(countdown * 1000);
const day = duration.days() !== 0 ? `${duration.days()} day` : "";
const hours = duration.hours() !== 0 ? `${duration.hours().toString().padStart(2, '0')}:` : "";
const minutes = duration.minutes() !== 0 ? `${duration.minutes().toString().padStart(2, '0')}:` : "";
const formatted_countdown = `${day} ${hours}${minutes}${duration.seconds().toString().padStart(2, '0')}`;
return (
<div className='absolute bottom-1 right-2 text-xs text-gray-400 flex items-center gap-1 font-mono'>
<IconTimer className="w-4 h-4 stroke-slate-400" />
{formatted_countdown}
</div>
);
useEffect(() => {
if (expiresIn > 0 && createAt > 0) {
const expire_time = createAt + expiresIn * 1000;
if (dayjs().isAfter(new Date(expire_time))) {
// 已过期,立即删除
clearMsgFromClient();
} else {
// 倒计时
setCountdown(Math.floor((expire_time - new Date().getTime()) / 1000));
}
}
}, [expiresIn, createAt]);
useEffect(() => {
let timer = 0;
if (typeof countdown !== "undefined") {
if (countdown > 0) {
timer = window.setTimeout(() => {
setCountdown((prev) => {
const _prev = prev ?? 0;
return _prev - 1;
});
}, 1000);
} else {
// 倒计时结束
console.log("countdown over", mid, countdown);
clearMsgFromClient();
}
}
return () => {
if (typeof countdown !== "undefined") {
clearTimeout(timer);
}
};
}, [countdown, mid]);
if (!countdown) return null;
const duration = dayjs.duration(countdown * 1000);
const day = duration.days() !== 0 ? `${duration.days()} day` : "";
const hours = duration.hours() !== 0 ? `${duration.hours().toString().padStart(2, "0")}:` : "";
const minutes =
duration.minutes() !== 0 ? `${duration.minutes().toString().padStart(2, "0")}:` : "";
const formatted_countdown = `${day} ${hours}${minutes}${duration
.seconds()
.toString()
.padStart(2, "0")}`;
return (
<div className="absolute bottom-1 right-2 text-xs text-gray-400 flex items-center gap-1 font-mono">
<IconTimer className="w-4 h-4 stroke-slate-400" />
{formatted_countdown}
</div>
);
};
export default ExpireTimer;
+21 -6
View File
@@ -1,7 +1,8 @@
import { FC, ReactElement, useEffect, useState } from "react";
import renderContent from "./renderContent";
import Avatar from "../Avatar";
import useFavMessage from "@/hooks/useFavMessage";
import Avatar from "../Avatar";
import renderContent from "./renderContent";
type Props = {
id?: string;
@@ -17,20 +18,34 @@ 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-50 dark:bg-slate-800">
<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 (
<div className="w-full relative flex items-start gap-3 px-2 py-1 my-2 rounded-lg md:dark:hover:bg-gray-800" key={idx}>
<div
className="w-full relative flex items-start gap-3 px-2 py-1 my-2 rounded-lg md:dark:hover:bg-gray-800"
key={idx}
>
{user && (
<div className="shrink-0 w-10 h-10 flex">
<Avatar width={40} height={40} className="rounded-full object-cover" src={user.avatar} name={user.name} />
<Avatar
width={40}
height={40}
className="rounded-full object-cover"
src={user.avatar}
name={user.name}
/>
</div>
)}
<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>
<span className="text-gray-600 dark:text-gray-400">
{user?.name || "Deleted User"}
</span>
</div>
<div className="select-text text-gray-800 break-all whitespace-pre-wrap dark:text-white">
{renderContent({
+14 -7
View File
@@ -1,10 +1,11 @@
import { useEffect, useState, FC, ReactElement } from "react";
import renderContent from "./renderContent";
import Avatar from "../Avatar";
import IconForward from "@/assets/icons/forward.svg";
import useNormalizeMessage from "@/hooks/useNormalizeMessage";
import { FC, ReactElement, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ChatContext } from "@/types/common";
import useNormalizeMessage from "@/hooks/useNormalizeMessage";
import IconForward from "@/assets/icons/forward.svg";
import Avatar from "../Avatar";
import renderContent from "./renderContent";
type Props = {
context: ChatContext;
@@ -22,7 +23,10 @@ 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 text-left rounded-lg bg-gray-200 dark:bg-gray-900">
<div
data-forwarded-mids={forward_mids.join(",")}
className="flex flex-col text-left rounded-lg bg-gray-200 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")}
@@ -31,7 +35,10 @@ const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
{messages.map((msg, idx) => {
const { user = {}, download, content, content_type, properties, thumbnail } = msg;
return (
<div className="w-full relative flex items-start gap-4 px-2 py-1 my-2 rounded-lg" 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="w-6 h-6 rounded-full flex shrink-0 overflow-hidden">
<Avatar width={24} height={24} src={user.avatar} name={user.name} />
+2 -1
View File
@@ -1,7 +1,8 @@
// import { FC, ReactNode } from "react";
import Tippy from "@tippyjs/react";
import Profile from "../Profile";
import { useAppSelector } from "@/app/store";
import Profile from "../Profile";
interface Props {
uid: number;
+6 -7
View File
@@ -1,13 +1,12 @@
import { FC } from "react";
import { useEffect } from "react";
import { FC, useEffect } from "react";
import toast from "react-hot-toast";
import usePinMessage from "@/hooks/usePinMessage";
import StyledModal from "../styled/Modal";
import Button from "../styled/Button";
import Modal from "../Modal";
import PreviewMessage from "./PreviewMessage";
import { useTranslation } from "react-i18next";
import usePinMessage from "@/hooks/usePinMessage";
import Modal from "../Modal";
import Button from "../styled/Button";
import StyledModal from "../styled/Modal";
import PreviewMessage from "./PreviewMessage";
interface Props {
closeModal: () => void;
+24 -6
View File
@@ -1,12 +1,13 @@
import { FC } from "react";
import clsx from "clsx";
import renderContent from "./renderContent";
import Avatar from "../Avatar";
import { useAppSelector } from "@/app/store";
import Avatar from "../Avatar";
import renderContent from "./renderContent";
interface Props {
mid?: number;
context?: "forward" | "pin"
context?: "forward" | "pin";
}
const PreviewMessage: FC<Props> = ({ mid = 0, context = "forward" }) => {
@@ -19,15 +20,32 @@ const PreviewMessage: FC<Props> = ({ mid = 0, context = "forward" }) => {
const pinMsg = context == "pin";
const forwardMsg = context == "forward";
return (
<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
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 className="w-10 h-10 flex shrink-0">
<Avatar width={40} height={40} className="rounded-full object-cover" src={avatar} name={name} />
<Avatar
width={40}
height={40}
className="rounded-full object-cover"
src={avatar}
name={name}
/>
</div>
<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={clsx(`select-text text-gray-600 text-sm break-all whitespace-pre-wrap dark:text-white`, forwardMsg && "max-h-72 overflow-y-scroll")}>
<div
className={clsx(
`select-text text-gray-600 text-sm break-all whitespace-pre-wrap dark:text-white`,
forwardMsg && "max-h-72 overflow-y-scroll"
)}
>
{renderContent({
content_type,
content,
+18 -10
View File
@@ -1,13 +1,13 @@
import { FC } from "react";
import Tippy from "@tippyjs/react";
import { hideAll } from "tippy.js";
import ReactionItem, { Emojis, ReactionMap } from "../ReactionItem";
import ReactionPicker from "./ReactionPicker";
import Tooltip from "../Tooltip";
import { useReactMessageMutation } from "@/app/services/message";
import IconAddEmoji from "@/assets/icons/add.emoji.svg";
import { useAppSelector } from "@/app/store";
import { useReactMessageMutation } from "@/app/services/message";
import { useAppSelector } from "@/app/store";
import IconAddEmoji from "@/assets/icons/add.emoji.svg";
import ReactionItem, { Emojis, ReactionMap } from "../ReactionItem";
import Tooltip from "../Tooltip";
import ReactionPicker from "./ReactionPicker";
const ReactionDetails = ({
uids = [],
@@ -28,7 +28,11 @@ const ReactionDetails = ({
? `${names.join(", ")} and ${names.length - 3} others reacted with`
: `${names.join(", ")} reacted with`;
return (
<div className={`relative bg-white rounded-lg shadow flex items-start gap-2 p-2 ${index == 0 ? "first" : ""}`}>
<div
className={`relative bg-white rounded-lg shadow flex items-start gap-2 p-2 ${
index == 0 ? "first" : ""
}`}
>
<div className="w-8 h-8">
<ReactionItem native={emoji} />
</div>
@@ -65,7 +69,9 @@ const Reaction: FC<Props> = ({ mid, reactions = null, readOnly = false }) => {
return uids.length > 0 ? (
<span
onClick={readOnly ? undefined : handleReact.bind(null, reaction)}
className={`cursor-pointer rounded-md relative flex items-center gap-1 p-1 md:hover:bg-cyan-100 ${reacted ? "shadow-[inset_0_0_0_1px_#06aed4] bg-cyan-200" : ""}`}
className={`cursor-pointer rounded-md relative flex items-center gap-1 p-1 md:hover:bg-cyan-100 ${
reacted ? "shadow-[inset_0_0_0_1px_#06aed4] bg-cyan-200" : ""
}`}
key={reaction}
>
<Tippy
@@ -81,7 +87,9 @@ const Reaction: FC<Props> = ({ mid, reactions = null, readOnly = false }) => {
</i>
</Tippy>
{uids.length > 1 ? <i className="text-primary-600 text-xs not-italic">{`${uids.length}`} </i> : null}
{uids.length > 1 ? (
<i className="text-primary-600 text-xs not-italic">{`${uids.length}`} </i>
) : null}
</span>
) : null;
})}
@@ -94,7 +102,7 @@ const Reaction: FC<Props> = ({ mid, reactions = null, readOnly = false }) => {
content={<ReactionPicker mid={mid} hidePicker={hideAll} />}
>
<button className="invisible group-hover:visible w-6 h-6 bg-cyan-100 md:hover:bg-cyan-200 rounded-md flex-center">
<IconAddEmoji className={'w-4 h-4'} />
<IconAddEmoji className={"w-4 h-4"} />
</button>
</Tippy>
</Tooltip>
+11 -5
View File
@@ -1,8 +1,9 @@
import { FC } from "react";
import { useReactMessageMutation } from "@/app/services/message";
import { Emojis } from "@/app/config";
import Emoji from "../ReactionItem";
import { useReactMessageMutation } from "@/app/services/message";
import { useAppSelector } from "@/app/store";
import Emoji from "../ReactionItem";
type Props = {
mid: number;
@@ -25,17 +26,22 @@ const ReactionPicker: FC<Props> = ({ mid, hidePicker }) => {
if (el) {
el.scrollIntoViewIfNeeded(false);
}
};
return (
<div className="z-[999]">
<ul className={`p-1 grid grid-cols-[repeat(4,_1fr)] gap-2 bg-white dark:bg-gray-900 drop-shadow-md rounded-xl ${isLoading ? "opacity-60" : ""}`}>
<ul
className={`p-1 grid grid-cols-[repeat(4,_1fr)] gap-2 bg-white dark:bg-gray-900 drop-shadow-md rounded-xl ${
isLoading ? "opacity-60" : ""
}`}
>
{Emojis.map((emoji) => {
let reacted =
reactionData[emoji] && reactionData[emoji].findIndex((id) => id == currUid) > -1;
return (
<li
className={`flex-center cursor-pointer rounded-lg p-4 md:hover:bg-gray-50 w-4 h-4 ${reacted ? "bg-gray-50" : ""}`}
className={`flex-center cursor-pointer rounded-lg p-4 md:hover:bg-gray-50 w-4 h-4 ${
reacted ? "bg-gray-50" : ""
}`}
key={emoji}
onClick={handleReact.bind(null, emoji)}
>
+24 -23
View File
@@ -1,31 +1,33 @@
import React, { MouseEvent, FC } from "react";
import clsx from "clsx";
import MarkdownRender from "../MarkdownRender";
import { ContentTypes } from "@/app/config";
import { getFileIcon, isImage } from "@/utils";
import LinkifyText from '../LinkifyText';
import Avatar from "../Avatar";
import { useAppSelector } from "@/app/store";
import { MessagePayload } from "@/app/slices/message";
import React, { FC, MouseEvent } from "react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import { ContentTypes } from "@/app/config";
import { MessagePayload } from "@/app/slices/message";
import { useAppSelector } from "@/app/store";
import { getFileIcon, isImage } from "@/utils";
import Avatar from "../Avatar";
import LinkifyText from "../LinkifyText";
import MarkdownRender from "../MarkdownRender";
const renderContent = (data: MessagePayload) => {
const { content_type, content, thumbnail, properties } = data;
let res = null;
switch (content_type) {
case ContentTypes.text:
res = (
<span className="max-w-lg md:truncate md:break-words md:break-all text-gray-800 dark:text-gray-100">
<LinkifyText text={content as string} url={false} mentionTextOnly={true} mentionPopOver={false} />
<LinkifyText
text={content as string}
url={false}
mentionTextOnly={true}
mentionPopOver={false}
/>
</span>
);
break;
case ContentTypes.audio:
res = (
<span className=" text-primary-400 text-sm">[Voice Message]</span>
);
res = <span className=" text-primary-400 text-sm">[Voice Message]</span>;
break;
case ContentTypes.markdown:
res = (
@@ -83,11 +85,12 @@ const Reply: FC<ReplyProps> = ({ mid, interactive = true }) => {
}
};
const defaultClass = `w-fit flex items-start flex-col md:flex-row p-2 bg-gray-200 dark:bg-gray-900 rounded-lg gap-2 mb-1`;
if (!data) return <div
key={mid}
data-mid={mid}
className={clsx(defaultClass, 'italic')}
>{t("reply_msg_del")}</div>;
if (!data)
return (
<div key={mid} data-mid={mid} className={clsx(defaultClass, "italic")}>
{t("reply_msg_del")}
</div>
);
const currUser = users[data.from_uid || 0];
if (!currUser) return null;
@@ -95,9 +98,7 @@ const Reply: FC<ReplyProps> = ({ mid, interactive = true }) => {
<div
key={mid}
data-mid={mid}
className={clsx(defaultClass,
interactive ? "cursor-pointer" : "!bg-transparent"
)}
className={clsx(defaultClass, interactive ? "cursor-pointer" : "!bg-transparent")}
onClick={interactive ? handleClick : undefined}
>
<div className="md:flex shrink-0 w-6 h-6 hidden ">
+16 -8
View File
@@ -1,15 +1,15 @@
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useLazyGetOGInfoQuery } from "@/app/services/message";
import { upsertOG } from "@/app/slices/footprint";
import { useAppSelector } from "@/app/store";
export default function URLPreview({ url = "" }) {
const dispatch = useDispatch();
const [favicon, setFavicon] = useState("");
const [getInfo, { isLoading }] = useLazyGetOGInfoQuery();
const ogData = useAppSelector(store => store.footprint.og[url]);
const ogData = useAppSelector((store) => store.footprint.og[url]);
const [data, setData] = useState<{ title: string; description: string; ogImage: string } | null>(
null
);
@@ -46,18 +46,26 @@ export default function URLPreview({ url = "" }) {
return ogImage ? (
// 简版
<a className={`${containerClass} flex-col !items-start p-3`} href={url} target="_blank" rel="noreferrer">
<a
className={`${containerClass} flex-col !items-start p-3`}
href={url}
target="_blank"
rel="noreferrer"
>
<h3 className={`text-primary-500 w-full truncate`}>{title}</h3>
<p className={`text-xs text-gray-400 mb-2 w-full truncate`}>{description}</p>
<div className="w-full h-[180px]">
<img className="w-full h-full object-cover" onError={handleOGImageError} src={ogImage} alt="og image" />
<img
className="w-full h-full object-cover"
onError={handleOGImageError}
src={ogImage}
alt="og image"
/>
</div>
</a>
) : (
// 带图详情
<a
className={`${containerClass} gap-2 px-2 py-3`}
href={url} target="_blank" rel="noreferrer">
<a className={`${containerClass} gap-2 px-2 py-3`} href={url} target="_blank" rel="noreferrer">
{favicon && (
<div className="flex rounded">
<img className="object-contain w-12 h-12" src={favicon} alt="favicon" />
+63 -43
View File
@@ -1,24 +1,24 @@
import React, { useRef, useState, useEffect, FC } from "react";
import dayjs from "dayjs";
import React, { FC, useEffect, useRef, useState } from "react";
import Tippy from "@tippyjs/react";
import clsx from "clsx";
import dayjs from "dayjs";
import useInView from "./useInView";
import Reaction from "./Reaction";
import Reply from "./Reply";
import Profile from "../Profile";
import Avatar from "../Avatar";
import Commands from "./Commands";
import EditMessage from "./EditMessage";
import renderContent from "./renderContent";
import Tooltip from "../Tooltip";
import ContextMenu from "./ContextMenu";
import { useAppSelector } from "@/app/store";
import { ChatContext } from "@/types/common";
import useContextMenu from "@/hooks/useContextMenu";
import usePinMessage from "@/hooks/usePinMessage";
import { useAppSelector } from "@/app/store";
import IconInfo from "@/assets/icons/info.svg";
import Avatar from "../Avatar";
import Profile from "../Profile";
import Tooltip from "../Tooltip";
import Commands from "./Commands";
import ContextMenu from "./ContextMenu";
import EditMessage from "./EditMessage";
import ExpireTimer from "./ExpireTimer";
import IconInfo from '@/assets/icons/info.svg';
import { ChatContext } from "@/types/common";
import Reaction from "./Reaction";
import renderContent from "./renderContent";
import Reply from "./Reply";
import useInView from "./useInView";
interface IProps {
readOnly?: boolean;
@@ -41,15 +41,17 @@ const Message: FC<IProps> = ({
const [edit, setEdit] = useState(false);
const avatarRef = useRef(null);
const { getPinInfo } = usePinMessage(context == "channel" ? contextId : 0);
const { message, reactionMessageData, usersData, loginUid, enableRightLayout } = useAppSelector((store) => {
return {
enableRightLayout: store.server.chat_layout_mode == "SelfRight",
reactionMessageData: store.reactionMessage,
message: store.message[mid],
usersData: store.users.byId,
loginUid: store.authData.user?.uid
};
});
const { message, reactionMessageData, usersData, loginUid, enableRightLayout } = useAppSelector(
(store) => {
return {
enableRightLayout: store.server.chat_layout_mode == "SelfRight",
reactionMessageData: store.reactionMessage,
message: store.message[mid],
usersData: store.users.byId,
loginUid: store.authData.user?.uid
};
}
);
const toggleEditMessage = () => {
setEdit((prev) => !prev);
@@ -81,10 +83,8 @@ const Message: FC<IProps> = ({
properties,
expires_in = 0,
failed = false
} = message;
const reactions = reactionMessageData[mid];
const currUser = usersData[fromUid || 0];
// if (!message) return null;
@@ -98,17 +98,17 @@ const Message: FC<IProps> = ({
const showExpire = (expires_in ?? 0) > 0;
const isSelf = fromUid == loginUid && enableRightLayout;
return (
<div
key={_key}
onContextMenu={readOnly ? undefined : handleContextMenuEvent}
data-msg-mid={mid}
ref={inViewRef}
className={clsx(`group w-full relative flex items-start gap-2 md:gap-4 p-1 md:p-2 my-2 rounded-lg md:dark:hover:bg-gray-800 md:hover:bg-gray-100`,
className={clsx(
`group w-full relative flex items-start gap-2 md:gap-4 p-1 md:p-2 my-2 rounded-lg md:dark:hover:bg-gray-800 md:hover:bg-gray-100`,
readOnly && "hover:bg-transparent",
showExpire && "bg-red-200 dark:bg-red-200/40",
pinInfo && "bg-cyan-50 dark:bg-cyan-800 pt-7",
isSelf && "flex-row-reverse",
isSelf && "flex-row-reverse"
)}
>
<Tippy
@@ -121,7 +121,13 @@ const Message: FC<IProps> = ({
content={<Profile uid={fromUid || 0} type="card" cid={context == "dm" ? 0 : contextId} />}
>
<div className="cursor-pointer w-10 h-10 shrink-0" data-uid={fromUid} ref={avatarRef}>
<Avatar className="w-10 h-10 rounded-full object-cover" width={40} height={40} src={currUser?.avatar} name={currUser?.name} />
<Avatar
className="w-10 h-10 rounded-full object-cover"
width={40}
height={40}
src={currUser?.avatar}
name={currUser?.name}
/>
</div>
</Tippy>
<ContextMenu
@@ -133,14 +139,23 @@ const Message: FC<IProps> = ({
hide={hideContextMenu}
>
<div
className={clsx("w-full flex flex-col gap-2", pinInfo && "relative", isSelf && "items-end")}
data-pin-tip={`pinned by ${pinInfo?.created_by ? usersData[pinInfo.created_by]?.name : ""
}`}
className={clsx(
"w-full flex flex-col gap-2",
pinInfo && "relative",
isSelf && "items-end"
)}
data-pin-tip={`pinned by ${
pinInfo?.created_by ? usersData[pinInfo.created_by]?.name : ""
}`}
>
{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={clsx(`flex items-center gap-2 font-semibold`, isSelf && "flex-row-reverse")}>
{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={clsx(`flex items-center gap-2 font-semibold`, isSelf && "flex-row-reverse")}
>
<span className="text-primary-500 text-sm">{currUser?.name || "Deleted User"}</span>
<Tooltip
delay={200}
@@ -154,13 +169,18 @@ const Message: FC<IProps> = ({
: dayjsTime.format("YYYY-MM-DD h:mm:ss A")}
</time>
</Tooltip>
{failed && <span className="text-red-500 text-xs flex items-center gap-1">
<IconInfo className="stroke-red-600 w-4 h-4" /> Send Failed
</span>}
{failed && (
<span className="text-red-500 text-xs flex items-center gap-1">
<IconInfo className="stroke-red-600 w-4 h-4" /> Send Failed
</span>
)}
</div>
<div className={clsx(`select-text text-gray-800 text-sm break-all whitespace-pre-wrap dark:!text-white`,
sending && "opacity-90",
)}>
<div
className={clsx(
`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} />
+12 -8
View File
@@ -1,14 +1,15 @@
// import React from "react";
import dayjs from "dayjs";
import i18n from "@/i18n";
import dayjs from "dayjs";
import { ContentTypes } from "@/app/config";
import ForwardedMessage from "./ForwardedMessage";
import MarkdownRender from "../MarkdownRender";
import FileMessage from "../FileMessage";
import { ContentType } from "@/types/message";
import LinkifyText from '../LinkifyText';
import VoiceMessage from "../VoiceMessage";
import { ChatContext } from "@/types/common";
import { ContentType } from "@/types/message";
import FileMessage from "../FileMessage";
import LinkifyText from "../LinkifyText";
import MarkdownRender from "../MarkdownRender";
import VoiceMessage from "../VoiceMessage";
import ForwardedMessage from "./ForwardedMessage";
type Props = {
context: ChatContext;
@@ -41,7 +42,10 @@ const renderContent = ({
<>
<LinkifyText text={content} cid={to} />
{edited && (
<span className="ml-1 text-gray-500 text-[10px]" 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>
)}
+11 -7
View File
@@ -1,13 +1,14 @@
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { ContentTypes } from "@/app/config";
import { useAppSelector } from "@/app/store";
import { ChatContext } from "@/types/common";
import useCopy from "@/hooks/useCopy";
import usePinMessage from "@/hooks/usePinMessage";
import DeleteMessageConfirm from "../DeleteMessageConfirm";
import ForwardModal from "../ForwardModal";
import PinMessageModal from "./PinMessageModal";
import { ContentTypes } from "@/app/config";
import useCopy from "@/hooks/useCopy";
import usePinMessage from "@/hooks/usePinMessage";
import { useAppSelector } from "@/app/store";
import { ChatContext } from "@/types/common";
interface Params {
mid: number;
@@ -74,7 +75,10 @@ export default function useMessageOperation({ mid, context, contextId }: Params)
properties?.content_type.startsWith("image");
const enableEdit =
loginUser?.uid == from_uid && [ContentTypes.text, ContentTypes.markdown].includes(content_type);
const canDelete = loginUser?.uid == from_uid || loginUser?.is_admin || (channel && channel.owner == loginUser?.uid);
const canDelete =
loginUser?.uid == from_uid ||
loginUser?.is_admin ||
(channel && channel.owner == loginUser?.uid);
const canCopy = [ContentTypes.text, ContentTypes.markdown].includes(content_type) || isImage;
return {
copyContent: isImage ? copyContent.bind(null, true) : copyContent.bind(null, false),