build: format the code with prettier
This commit is contained in:
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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 ">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user