refactor: og and msg history mark
This commit is contained in:
@@ -37,6 +37,17 @@ export default async function handler({ operation, data = {}, payload }: Params)
|
||||
await table?.setItem("muteChannels", data.muteChannels || {});
|
||||
}
|
||||
break;
|
||||
case "updateHistoryMark":
|
||||
{
|
||||
const { type } = payload;
|
||||
if (type == "channel") {
|
||||
await table?.setItem("historyChannels", data.historyChannels);
|
||||
|
||||
} else {
|
||||
await table?.setItem("historyUsers", data.historyUsers);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "updateReadChannels":
|
||||
{
|
||||
await table?.setItem("readChannels", data.readChannels);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||
import { ContentTypes } from "../config";
|
||||
import { updateReadChannels, updateReadUsers } from "../slices/footprint";
|
||||
import { updateReadChannels, updateReadUsers, upsertOG } from "../slices/footprint";
|
||||
import { fillFavorites, populateFavorite, addFavorite, deleteFavorite } from "../slices/favorites";
|
||||
import { onMessageSendStarted } from "./handlers";
|
||||
import { normalizeArchiveData } from "../../common/utils";
|
||||
@@ -64,8 +64,26 @@ export const messageApi = createApi({
|
||||
getOGInfo: builder.query<OG, string>({
|
||||
query: (url) => ({
|
||||
url: `/resource/open_graphic_parse?url=${encodeURIComponent(url)}`
|
||||
})
|
||||
}),
|
||||
async onQueryStarted(url, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
dispatch(upsertOG({ key: url, value: data }));
|
||||
} catch (err) {
|
||||
console.log("get og error", err);
|
||||
dispatch(upsertOG({
|
||||
key: url, value: {
|
||||
images: [],
|
||||
audios: [],
|
||||
videos: [],
|
||||
title: "",
|
||||
url,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
getArchiveMessage: builder.query<Archive, string>({
|
||||
query: (file_path) => ({
|
||||
url: `/resource/archive?file_path=${encodeURIComponent(file_path)}`
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { MuteDTO } from "../../types/message";
|
||||
import { OG } from "../../types/resource";
|
||||
import { AutoDeleteMessageSettingDTO, AutoDeleteMsgForGroup, AutoDeleteMsgForUser, AutoDeleteSettingForChannels, AutoDeleteSettingForUsers } from "../../types/sse";
|
||||
|
||||
export interface State {
|
||||
og: { [url: string]: OG }
|
||||
usersVersion: number;
|
||||
afterMid: number;
|
||||
historyUsers: { [uid: number]: string | "reached" };
|
||||
historyChannels: { [cid: number]: string | "reached" };
|
||||
readUsers: { [uid: number]: number };
|
||||
readChannels: { [gid: number]: number };
|
||||
readChannels: { [cid: number]: number };
|
||||
muteUsers: { [uid: number]: { expired_in?: number } | undefined };
|
||||
muteChannels: { [gid: number]: { expired_in?: number } | undefined };
|
||||
muteChannels: { [cid: number]: { expired_in?: number } | undefined };
|
||||
autoDeleteMsgUsers: AutoDeleteMsgForUser[];
|
||||
autoDeleteMsgChannels: AutoDeleteMsgForGroup[];
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
og: {},
|
||||
usersVersion: 0,
|
||||
afterMid: 0,
|
||||
historyUsers: {},
|
||||
historyChannels: {},
|
||||
readUsers: {},
|
||||
readChannels: {},
|
||||
muteUsers: {},
|
||||
@@ -33,8 +40,11 @@ const footprintSlice = createSlice({
|
||||
},
|
||||
fillFootprint(state, action) {
|
||||
const {
|
||||
og = {},
|
||||
usersVersion = 0,
|
||||
afterMid = 0,
|
||||
historyUsers = {},
|
||||
historyChannels = {},
|
||||
readUsers = {},
|
||||
readChannels = {},
|
||||
muteUsers = {},
|
||||
@@ -43,8 +53,11 @@ const footprintSlice = createSlice({
|
||||
autoDeleteMsgChannels = []
|
||||
} = action.payload;
|
||||
return {
|
||||
og,
|
||||
usersVersion,
|
||||
afterMid,
|
||||
historyUsers,
|
||||
historyChannels,
|
||||
readUsers,
|
||||
readChannels,
|
||||
muteUsers,
|
||||
@@ -139,6 +152,18 @@ const footprintSlice = createSlice({
|
||||
}
|
||||
});
|
||||
},
|
||||
upsertOG(state, action: PayloadAction<{ key: string, value: OG }>) {
|
||||
const { key, value } = action.payload;
|
||||
state.og[key] = value;
|
||||
},
|
||||
updateHistoryMark(state, action: PayloadAction<{ type: "channel" | "user", id: number, mid: string, }>) {
|
||||
const { type, id, mid } = action.payload;
|
||||
if (type == "channel") {
|
||||
state.historyChannels[id] = mid;
|
||||
} else {
|
||||
state.historyUsers[id] = mid;
|
||||
}
|
||||
},
|
||||
updateReadUsers(state, action: PayloadAction<{ uid: number; mid: number }[] | undefined>) {
|
||||
const reads = action.payload || [];
|
||||
if (reads.length == 0) return;
|
||||
@@ -164,7 +189,9 @@ export const {
|
||||
updateReadChannels,
|
||||
updateReadUsers,
|
||||
updateMute,
|
||||
updateAutoDeleteSetting
|
||||
updateAutoDeleteSetting,
|
||||
updateHistoryMark,
|
||||
upsertOG
|
||||
} = footprintSlice.actions;
|
||||
|
||||
export default footprintSlice.reducer;
|
||||
|
||||
@@ -1,68 +1,72 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useLazyGetOGInfoQuery } from "../../../app/services/message";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
|
||||
|
||||
export default function URLPreview({ url = "" }) {
|
||||
const [favicon, setFavicon] = useState("");
|
||||
const [getInfo] = useLazyGetOGInfoQuery();
|
||||
const [getInfo, { isLoading }] = useLazyGetOGInfoQuery();
|
||||
const ogData = useAppSelector(store => store.footprint.og[url]);
|
||||
const [data, setData] = useState<{ title: string; description: string; ogImage: string } | null>(
|
||||
null
|
||||
);
|
||||
useEffect(() => {
|
||||
const getMetaData = async (url: string) => {
|
||||
if (ogData) {
|
||||
let defaultFavIcon = "";
|
||||
try {
|
||||
defaultFavIcon = `${new URL(url).origin}/favicon.ico`;
|
||||
} catch {
|
||||
defaultFavIcon = `${location.origin}/favicon.ico`;
|
||||
}
|
||||
// todo
|
||||
const { data } = await getInfo(url);
|
||||
const title = data?.title || data?.site_name || "";
|
||||
const description = data?.description || "";
|
||||
const ogImage = data?.images.find((i) => !!i.url)?.url || "";
|
||||
const favicon = data?.favicon_url || defaultFavIcon;
|
||||
const title = ogData?.title || ogData?.site_name || "";
|
||||
const description = ogData?.description || "";
|
||||
const ogImage = ogData?.images.find((i) => !!i.url)?.url || "";
|
||||
const favicon = ogData?.favicon_url || defaultFavIcon;
|
||||
setFavicon(favicon);
|
||||
setData({ title, description, ogImage });
|
||||
// console.log("wtf url", data);
|
||||
};
|
||||
getMetaData(url);
|
||||
}, [url]);
|
||||
const handleFavError = () => {
|
||||
setFavicon("");
|
||||
};
|
||||
const handleOGImageError = () => {
|
||||
setData(prev => {
|
||||
if (!prev) return prev;
|
||||
return { ...prev, ogImage: "" };
|
||||
});
|
||||
};
|
||||
} else if (url) {
|
||||
// fetch first
|
||||
getInfo(url);
|
||||
}
|
||||
}, [url, ogData]);
|
||||
// const handleFavError = () => {
|
||||
// setFavicon("");
|
||||
// };
|
||||
// const handleOGImageError = () => {
|
||||
// setData(prev => {
|
||||
// if (!prev) return prev;
|
||||
// return { ...prev, ogImage: "" };
|
||||
// });
|
||||
// };
|
||||
if (isLoading) return <div className="h-28"></div>;
|
||||
if (!url || !data || !data.title) return null;
|
||||
const { title, description, ogImage } = data;
|
||||
|
||||
const containerClass = `flex items-center border border-solid border-gray-300 dark:border-gray-600 box-border rounded-md w-[80%] md:w-[380px]`;
|
||||
const dotsClass = `truncate`;
|
||||
|
||||
return ogImage ? (
|
||||
// 简版
|
||||
<a className={`${containerClass} flex-col !items-start p-3`} href={url} target="_blank" rel="noreferrer">
|
||||
<h3 className={`text-primary-500 w-full ${dotsClass}`}>{title}</h3>
|
||||
<p className={`text-xs text-gray-400 mb-2 w-full ${dotsClass}`}>{description}</p>
|
||||
<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" src={ogImage} alt="og image" />
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
// 带图详情
|
||||
<a
|
||||
className={`${containerClass} gap-2 px-2 py-3`}
|
||||
href={url} target="_blank" rel="noreferrer">
|
||||
{favicon && (
|
||||
<div className="flex rounded">
|
||||
<img onError={handleFavError} className="object-contain w-12 h-12" src={favicon} alt="favicon" />
|
||||
<img className="object-contain w-12 h-12" src={favicon} alt="favicon" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-sm text-gray-900 dark:text-gray-100">{title}</h3>
|
||||
<p className={`hidden md:block text-xs text-gray-500 dark:text-gray-400 w-[288px] ${dotsClass}`}>{description}</p>
|
||||
<span className={`text-[10px] text-gray-500 w-[288px] ${dotsClass}`}>{url}</span>
|
||||
<p className={`hidden md:block text-xs text-gray-500 dark:text-gray-400 w-[288px] truncate`}>{description}</p>
|
||||
<span className={`text-[10px] text-gray-500 w-[288px] truncate`}>{url}</span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
|
||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||
// import clsx from 'clsx';
|
||||
// import { useTranslation } from 'react-i18next';
|
||||
import { useDebounce, useLocalstorageState } from 'rooks';
|
||||
import { useDebounce } from 'rooks';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
import { useLazyLoadMoreMessagesQuery, useReadMessageMutation } from '../../../../app/services/message';
|
||||
@@ -10,6 +10,8 @@ import { renderMessageFragment } from '../../utils';
|
||||
import NewMessageBottomTip from "../NewMessageBottomTip";
|
||||
import CustomList from './CustomList';
|
||||
import CustomHeader from './CustomHeader';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateHistoryMark } from '../../../../app/slices/footprint';
|
||||
type Props = {
|
||||
context: "user" | "channel",
|
||||
id: number
|
||||
@@ -17,15 +19,16 @@ type Props = {
|
||||
// const firstMsgIndex = 10000;
|
||||
// let prevMids: number[] = [];
|
||||
const VirtualMessageFeed = ({ context, id }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
// const { t } = useTranslation("chat");
|
||||
// const [firstItemIndex, setFirstItemIndex] = useState(firstMsgIndex);
|
||||
const [atBottom, setAtBottom] = useState(false);
|
||||
const [loadMoreMessage, { isLoading: loadingMore, isSuccess, data: historyData }] = useLazyLoadMoreMessagesQuery();
|
||||
const [historyMid, setHistoryMid] = useLocalstorageState<'reached' | string>(`history_mid_${context}_${id}`, "");
|
||||
const vList = useRef<VirtuosoHandle | null>(null);
|
||||
const [updateReadIndex] = useReadMessageMutation();
|
||||
const updateReadDebounced = useDebounce(updateReadIndex, 300);
|
||||
const {
|
||||
historyMid = "",
|
||||
mids = [],
|
||||
selects,
|
||||
messageData,
|
||||
@@ -33,6 +36,7 @@ const VirtualMessageFeed = ({ context, id }: Props) => {
|
||||
footprint
|
||||
} = useAppSelector((store) => {
|
||||
return {
|
||||
historyMid: context == "user" ? store.footprint.historyUsers[id] : store.footprint.historyChannels[id],
|
||||
mids: context == "user" ? store.userMessage.byId[id] : store.channelMessage[id],
|
||||
selects: store.ui.selectMessages[`${context}_${id}`],
|
||||
footprint: store.footprint,
|
||||
@@ -45,14 +49,14 @@ const VirtualMessageFeed = ({ context, id }: Props) => {
|
||||
if (isSuccess && historyData) {
|
||||
if (historyData.length == 0) {
|
||||
// 到顶了
|
||||
setHistoryMid(`reached`);
|
||||
dispatch(updateHistoryMark({ type: context, id, mid: "reached" }));
|
||||
} else {
|
||||
// 记录最新的mid
|
||||
const [{ mid }] = historyData;
|
||||
setHistoryMid(`${mid}`);
|
||||
dispatch(updateHistoryMark({ type: context, id, mid: `${mid}` }));
|
||||
}
|
||||
}
|
||||
}, [isSuccess, historyData, mids]);
|
||||
}, [isSuccess, historyData, mids, context, id]);
|
||||
// useEffect(() => {
|
||||
// console.log("diff mids", prevMids, mids);
|
||||
// const newCount = mids.length - prevMids.length;
|
||||
|
||||
+23
-25
@@ -34,34 +34,32 @@ export interface UploadResponse {
|
||||
}
|
||||
// Open Graph
|
||||
export interface OG {
|
||||
type: string;
|
||||
type?: string;
|
||||
title: string;
|
||||
url: string;
|
||||
images: [
|
||||
{
|
||||
type?: string;
|
||||
url: string;
|
||||
secure_url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
alt?: string;
|
||||
}
|
||||
images: {
|
||||
type?: string;
|
||||
url: string;
|
||||
secure_url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
alt?: string;
|
||||
}[
|
||||
|
||||
];
|
||||
audios: [
|
||||
{
|
||||
type?: string;
|
||||
url: string;
|
||||
secure_url?: string;
|
||||
}
|
||||
];
|
||||
videos: [
|
||||
{
|
||||
type?: string;
|
||||
url: string;
|
||||
secure_url?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
audios: {
|
||||
type?: string;
|
||||
url: string;
|
||||
secure_url?: string;
|
||||
}[];
|
||||
videos: {
|
||||
type?: string;
|
||||
url: string;
|
||||
secure_url?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}[
|
||||
|
||||
];
|
||||
favicon_url?: string;
|
||||
description?: string;
|
||||
|
||||
Reference in New Issue
Block a user