feat: auto delete message logic
This commit is contained in:
@@ -12,6 +12,7 @@ export interface MessagePayload {
|
|||||||
content: string;
|
content: string;
|
||||||
expires_in?: number | null;
|
expires_in?: number | null;
|
||||||
properties?: {
|
properties?: {
|
||||||
|
local_id?: number;
|
||||||
content_type: string;
|
content_type: string;
|
||||||
size: number;
|
size: number;
|
||||||
};
|
};
|
||||||
@@ -75,7 +76,7 @@ const messageSlice = createSlice({
|
|||||||
data.thumbnail = content;
|
data.thumbnail = content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state[mid] = data;
|
state[mid] = { ...state[mid], ...data };
|
||||||
},
|
},
|
||||||
removeMessage(state, action: PayloadAction<number | number[]>) {
|
removeMessage(state, action: PayloadAction<number | number[]>) {
|
||||||
const mids = Array.isArray(action.payload) ? action.payload : [action.payload];
|
const mids = Array.isArray(action.payload) ? action.payload : [action.payload];
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" stroke="#101828" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 6V12L16 14M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 325 B |
@@ -1,11 +1,13 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
import "dayjs/locale/ja";
|
import "dayjs/locale/ja";
|
||||||
|
import duration from "dayjs/plugin/duration";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import isToday from "dayjs/plugin/isToday";
|
import isToday from "dayjs/plugin/isToday";
|
||||||
import isYesterday from "dayjs/plugin/isYesterday";
|
import isYesterday from "dayjs/plugin/isYesterday";
|
||||||
// import i18n from '../i18n';
|
// import i18n from '../i18n';
|
||||||
|
|
||||||
|
dayjs.extend(duration);
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
dayjs.extend(isToday);
|
dayjs.extend(isToday);
|
||||||
dayjs.extend(isYesterday);
|
dayjs.extend(isYesterday);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { removeMessage } from '../../../app/slices/message';
|
||||||
|
import IconTimer from '../../../assets/icons/timer.svg';
|
||||||
|
type Props = {
|
||||||
|
mid: number;
|
||||||
|
expires_in: number;
|
||||||
|
create_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExpireTimer: FC<Props> = ({ mid, create_at, expires_in }) => {
|
||||||
|
const [countdown, setCountdown] = useState<number | undefined>();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
useEffect(() => {
|
||||||
|
if (expires_in > 0 && create_at > 0) {
|
||||||
|
const expire_time = create_at + expires_in * 1000;
|
||||||
|
if (dayjs().isAfter(new Date(expire_time))) {
|
||||||
|
// 已过期,立即删除
|
||||||
|
dispatch(removeMessage(mid));
|
||||||
|
// dispatch()
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 倒计时
|
||||||
|
setCountdown(Math.floor((expire_time - new Date().getTime()) / 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [expires_in, create_at, mid]);
|
||||||
|
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);
|
||||||
|
|
||||||
|
dispatch(removeMessage(mid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ import ContextMenu from "./ContextMenu";
|
|||||||
import useContextMenu from "../../hook/useContextMenu";
|
import useContextMenu from "../../hook/useContextMenu";
|
||||||
import usePinMessage from "../../hook/usePinMessage";
|
import usePinMessage from "../../hook/usePinMessage";
|
||||||
import { useAppSelector } from "../../../app/store";
|
import { useAppSelector } from "../../../app/store";
|
||||||
|
import ExpireTimer from "./ExpireTimer";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -50,6 +51,19 @@ const Message: FC<IProps> = ({
|
|||||||
setEdit((prev) => !prev);
|
setEdit((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!read) {
|
||||||
|
// 标记已读
|
||||||
|
const data =
|
||||||
|
context == "user"
|
||||||
|
? { users: [{ uid: +contextId, mid }] }
|
||||||
|
: { groups: [{ gid: +contextId, mid }] };
|
||||||
|
if (updateReadIndex) {
|
||||||
|
updateReadIndex(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mid, read]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
reply_mid,
|
reply_mid,
|
||||||
from_uid: fromUid,
|
from_uid: fromUid,
|
||||||
@@ -60,20 +74,10 @@ const Message: FC<IProps> = ({
|
|||||||
download,
|
download,
|
||||||
content_type = "text/plain",
|
content_type = "text/plain",
|
||||||
edited,
|
edited,
|
||||||
properties
|
properties,
|
||||||
|
expires_in = 0
|
||||||
} = message;
|
} = message;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!read) {
|
|
||||||
const data =
|
|
||||||
context == "user"
|
|
||||||
? { users: [{ uid: +contextId, mid }] }
|
|
||||||
: { groups: [{ gid: +contextId, mid }] };
|
|
||||||
if (updateReadIndex) {
|
|
||||||
updateReadIndex(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [mid, read]);
|
|
||||||
|
|
||||||
const reactions = reactionMessageData[mid];
|
const reactions = reactionMessageData[mid];
|
||||||
const currUser = usersData[fromUid || 0];
|
const currUser = usersData[fromUid || 0];
|
||||||
@@ -85,14 +89,14 @@ const Message: FC<IProps> = ({
|
|||||||
const pinInfo = getPinInfo(mid);
|
const pinInfo = getPinInfo(mid);
|
||||||
// return null;
|
// return null;
|
||||||
const _key = properties?.local_id || mid;
|
const _key = properties?.local_id || mid;
|
||||||
|
const showExpire = (expires_in ?? 0) > 0;
|
||||||
return (
|
return (
|
||||||
<StyledWrapper
|
<StyledWrapper
|
||||||
key={_key}
|
key={_key}
|
||||||
onContextMenu={readOnly ? undefined : handleContextMenuEvent}
|
onContextMenu={readOnly ? undefined : handleContextMenuEvent}
|
||||||
data-msg-mid={mid}
|
data-msg-mid={mid}
|
||||||
ref={inviewRef}
|
ref={inviewRef}
|
||||||
className={`message ${readOnly ? "readonly" : ""} ${pinInfo ? "pinned" : ""} ${
|
className={`message ${readOnly ? "readonly" : ""} ${showExpire ? "auto_delete" : ""} ${pinInfo ? "pinned" : ""} ${contextMenuVisible ? "contextVisible" : ""
|
||||||
contextMenuVisible ? "contextVisible" : ""
|
|
||||||
} `}
|
} `}
|
||||||
>
|
>
|
||||||
<Tippy
|
<Tippy
|
||||||
@@ -118,8 +122,7 @@ const Message: FC<IProps> = ({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="details"
|
className="details"
|
||||||
data-pin-tip={`pinned by ${
|
data-pin-tip={`pinned by ${pinInfo?.created_by ? usersData[pinInfo.created_by]?.name : ""
|
||||||
pinInfo?.created_by ? usersData[pinInfo.created_by]?.name : ""
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="up">
|
<div className="up">
|
||||||
@@ -160,6 +163,13 @@ const Message: FC<IProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|
||||||
|
{showExpire && (
|
||||||
|
<ExpireTimer
|
||||||
|
mid={message.mid}
|
||||||
|
expires_in={expires_in ?? 0}
|
||||||
|
create_at={time ?? 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!edit && !readOnly && (
|
{!edit && !readOnly && (
|
||||||
<Commands
|
<Commands
|
||||||
context={context}
|
context={context}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ const StyledMsg = styled.div`
|
|||||||
&[data-highlight="true"] {
|
&[data-highlight="true"] {
|
||||||
background: #f5f6f7;
|
background: #f5f6f7;
|
||||||
}
|
}
|
||||||
|
&.auto_delete {
|
||||||
|
background: #f1d1ca50;
|
||||||
|
}
|
||||||
&.pinned {
|
&.pinned {
|
||||||
background: #ecfdff;
|
background: #ecfdff;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user