refactor: og and msg history mark

This commit is contained in:
Tristan Yang
2023-03-14 22:01:18 +08:00
parent b7882fb890
commit 546898556e
6 changed files with 126 additions and 64 deletions
@@ -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);
+20 -2
View File
@@ -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)}`
+30 -3
View File
@@ -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;
+32 -28
View File
@@ -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
View File
@@ -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;