note translation sw & lru cache
Some checks reported errors
continuous-integration/drone/push Build was killed
Some checks reported errors
continuous-integration/drone/push Build was killed
This commit is contained in:
parent
dc99d2a653
commit
82d5b9fb64
@ -4,6 +4,7 @@ import React, { useCallback, useState } from "react";
|
|||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { LRUCache } from "typescript-lru-cache";
|
||||||
|
|
||||||
import NoteHeader from "@/Components/Event/Note/NoteHeader";
|
import NoteHeader from "@/Components/Event/Note/NoteHeader";
|
||||||
import { NoteText } from "@/Components/Event/Note/NoteText";
|
import { NoteText } from "@/Components/Event/Note/NoteText";
|
||||||
@ -30,6 +31,7 @@ const defaultOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls];
|
const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls];
|
||||||
|
const translationCache = new LRUCache<string, NoteTranslation>({ maxSize: 300 });
|
||||||
|
|
||||||
export function Note(props: NoteProps) {
|
export function Note(props: NoteProps) {
|
||||||
const { data: ev, highlight, options: opt, ignoreModeration = false, className, waitUntilInView } = props;
|
const { data: ev, highlight, options: opt, ignoreModeration = false, className, waitUntilInView } = props;
|
||||||
@ -37,7 +39,14 @@ export function Note(props: NoteProps) {
|
|||||||
const { isEventMuted } = useModeration();
|
const { isEventMuted } = useModeration();
|
||||||
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" });
|
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" });
|
||||||
const [showTranslation, setShowTranslation] = useState(true);
|
const [showTranslation, setShowTranslation] = useState(true);
|
||||||
const [translated, setTranslated] = useState<NoteTranslation>();
|
const [translated, setTranslated] = useState<NoteTranslation>(translationCache.get(ev.id));
|
||||||
|
const cachedSetTranslated = useCallback(
|
||||||
|
(translation: NoteTranslation) => {
|
||||||
|
translationCache.set(ev.id, translation);
|
||||||
|
setTranslated(translation);
|
||||||
|
},
|
||||||
|
[ev.id],
|
||||||
|
);
|
||||||
|
|
||||||
const optionsMerged = { ...defaultOptions, ...opt };
|
const optionsMerged = { ...defaultOptions, ...opt };
|
||||||
const goToEvent = useGoToEvent(props, optionsMerged);
|
const goToEvent = useGoToEvent(props, optionsMerged);
|
||||||
@ -50,7 +59,13 @@ export function Note(props: NoteProps) {
|
|||||||
if (waitUntilInView && !inView) return null;
|
if (waitUntilInView && !inView) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{optionsMerged.showHeader && <NoteHeader ev={ev} options={optionsMerged} setTranslated={setTranslated} />}
|
{optionsMerged.showHeader && (
|
||||||
|
<NoteHeader
|
||||||
|
ev={ev}
|
||||||
|
options={optionsMerged}
|
||||||
|
setTranslated={translated === null ? cachedSetTranslated : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="body" onClick={e => goToEvent(e, ev)}>
|
<div className="body" onClick={e => goToEvent(e, ev)}>
|
||||||
<NoteText {...props} translated={translated} showTranslation={showTranslation} />
|
<NoteText {...props} translated={translated} showTranslation={showTranslation} />
|
||||||
{translated && <TranslationInfo translated={translated} setShowTranslation={setShowTranslation} />}
|
{translated && <TranslationInfo translated={translated} setShowTranslation={setShowTranslation} />}
|
||||||
|
@ -18,6 +18,7 @@ export interface NoteTranslation {
|
|||||||
text: string;
|
text: string;
|
||||||
fromLanguage: string;
|
fromLanguage: string;
|
||||||
confidence: number;
|
confidence: number;
|
||||||
|
skipped?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NosteContextMenuProps {
|
interface NosteContextMenuProps {
|
||||||
@ -60,6 +61,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function translate() {
|
async function translate() {
|
||||||
|
if (!props.onTranslated) return;
|
||||||
const api = new SnortApi();
|
const api = new SnortApi();
|
||||||
const targetLang = lang.split("-")[0].toUpperCase();
|
const targetLang = lang.split("-")[0].toUpperCase();
|
||||||
const result = await api.translate({
|
const result = await api.translate({
|
||||||
@ -67,18 +69,23 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
|||||||
target_lang: targetLang,
|
target_lang: targetLang,
|
||||||
});
|
});
|
||||||
|
|
||||||
if ("translations" in result) {
|
if (
|
||||||
if (
|
"translations" in result &&
|
||||||
typeof props.onTranslated === "function" &&
|
result.translations.length > 0 &&
|
||||||
result.translations.length > 0 &&
|
targetLang != result.translations[0].detected_source_language
|
||||||
targetLang != result.translations[0].detected_source_language
|
) {
|
||||||
) {
|
props.onTranslated({
|
||||||
props.onTranslated({
|
text: result.translations[0].text,
|
||||||
text: result.translations[0].text,
|
fromLanguage: langNames.of(result.translations[0].detected_source_language),
|
||||||
fromLanguage: langNames.of(result.translations[0].detected_source_language),
|
confidence: 1,
|
||||||
confidence: 1,
|
} as NoteTranslation);
|
||||||
} as NoteTranslation);
|
} else {
|
||||||
}
|
props.onTranslated({
|
||||||
|
text: "",
|
||||||
|
fromLanguage: "",
|
||||||
|
confidence: 0,
|
||||||
|
skipped: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ import { setBookmarked, setPinned } from "@/Utils/Login";
|
|||||||
export default function NoteHeader(props: {
|
export default function NoteHeader(props: {
|
||||||
ev: TaggedNostrEvent;
|
ev: TaggedNostrEvent;
|
||||||
options: NotePropsOptions;
|
options: NotePropsOptions;
|
||||||
setTranslated: (t: NoteTranslation) => void;
|
setTranslated?: (t: NoteTranslation) => void;
|
||||||
context?: React.ReactNode;
|
context?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [showReactions, setShowReactions] = useState(false);
|
const [showReactions, setShowReactions] = useState(false);
|
||||||
@ -49,6 +49,8 @@ export default function NoteHeader(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onTranslated = setTranslated ? (t: NoteTranslation) => setTranslated(t) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header flex">
|
<div className="header flex">
|
||||||
<ProfileImage
|
<ProfileImage
|
||||||
@ -79,7 +81,7 @@ export default function NoteHeader(props: {
|
|||||||
<NoteContextMenu
|
<NoteContextMenu
|
||||||
ev={ev}
|
ev={ev}
|
||||||
react={async () => {}}
|
react={async () => {}}
|
||||||
onTranslated={t => setTranslated(t)}
|
onTranslated={onTranslated}
|
||||||
setShowReactions={setShowReactions}
|
setShowReactions={setShowReactions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -15,8 +15,8 @@ export const NoteText = function InnerContent(
|
|||||||
const { data: ev, options, translated, showTranslation } = props;
|
const { data: ev, options, translated, showTranslation } = props;
|
||||||
const appData = useLogin(s => s.appData);
|
const appData = useLogin(s => s.appData);
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
const body = translated && showTranslation ? translated.text : ev?.content ?? "";
|
const body = translated && !translated.skipped && showTranslation ? translated.text : ev?.content ?? "";
|
||||||
const id = translated && showTranslation ? `${ev.id}-translated` : ev.id;
|
const id = translated && !translated.skipped && showTranslation ? `${ev.id}-translated` : ev.id;
|
||||||
const shouldTruncate = options?.truncate && body.length > TEXT_TRUNCATE_LENGTH;
|
const shouldTruncate = options?.truncate && body.length > TEXT_TRUNCATE_LENGTH;
|
||||||
|
|
||||||
const ToggleShowMore = () => (
|
const ToggleShowMore = () => (
|
||||||
|
@ -23,7 +23,7 @@ export function TranslationInfo({ translated, setShowTranslation }: TranslationI
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else if (translated) {
|
} else if (translated && !translated.skipped) {
|
||||||
return (
|
return (
|
||||||
<p className="text-xs font-semibold text-gray-light">
|
<p className="text-xs font-semibold text-gray-light">
|
||||||
<FormattedMessage {...messages.TranslationFailed} />
|
<FormattedMessage {...messages.TranslationFailed} />
|
||||||
|
@ -89,9 +89,25 @@ registerRoute(
|
|||||||
|
|
||||||
registerRoute(
|
registerRoute(
|
||||||
({ url }) => url.origin === "https://api.snort.social" && url.pathname.startsWith("/api/v1/preview"),
|
({ url }) => url.origin === "https://api.snort.social" && url.pathname.startsWith("/api/v1/preview"),
|
||||||
new StaleWhileRevalidate({
|
new CacheFirst({
|
||||||
cacheName: "preview-cache",
|
cacheName: "preview-cache",
|
||||||
plugins: [new ExpirationPlugin({ maxAgeSeconds: 24 * 60 * 60 })],
|
plugins: [
|
||||||
|
new ExpirationPlugin({ maxAgeSeconds: 24 * 60 * 60 }),
|
||||||
|
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ url }) => url.origin === "https://api.snort.social" && url.pathname.startsWith("/api/v1/translate"),
|
||||||
|
new CacheFirst({
|
||||||
|
cacheName: "translate-cache",
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({ maxEntries: 1000 }),
|
||||||
|
new CacheableResponsePlugin({
|
||||||
|
statuses: [0, 200, 204],
|
||||||
|
}),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user