0.29.1原版

This commit is contained in:
anian
2026-07-02 19:14:14 +08:00
commit d94008f0fb
947 changed files with 174905 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { useInstance } from "@/contexts/InstanceContext";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useUser } from "@/hooks/useUserQueries";
import { findTagMetadata } from "@/lib/tag";
import { cn } from "@/lib/utils";
import { State } from "@/types/proto/api/v1/common_pb";
import { isSuperUser } from "@/utils/user";
import MemoShareImageDialog from "../MemoActionMenu/MemoShareImageDialog";
import MemoEditor from "../MemoEditor";
import PreviewImageDialog from "../PreviewImageDialog";
import { MemoBody, MemoCommentListView, MemoHeader } from "./components";
import { MEMO_CARD_BASE_CLASSES } from "./constants";
import { useImagePreview } from "./hooks";
import { computeCommentAmount, MemoViewContext } from "./MemoViewContext";
import type { MemoViewProps } from "./types";
const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
const { memo: memoData, className, parentPage: parentPageProp, compact, showCreator, showVisibility, showPinned } = props;
const cardRef = useRef<HTMLDivElement>(null);
const [showEditor, setShowEditor] = useState(false);
const [cardWidth, setCardWidth] = useState(0);
const currentUser = useCurrentUser();
const { tagsSetting } = useInstance();
const creator = useUser(memoData.creator).data;
const isArchived = memoData.state === State.ARCHIVED;
const readonly = memoData.creator !== currentUser?.name && !isSuperUser(currentUser);
const parentPage = parentPageProp || "/";
// Blur content when any tag has blur_content enabled in the instance tag settings.
const [showBlurredContent, setShowBlurredContent] = useState(false);
const blurred = memoData.tags?.some((tag) => findTagMetadata(tag, tagsSetting)?.blurContent) ?? false;
const toggleBlurVisibility = useCallback(() => setShowBlurredContent((prev) => !prev), []);
const { previewState, openPreview, setPreviewOpen } = useImagePreview();
const openEditor = useCallback(() => setShowEditor(true), []);
const closeEditor = useCallback(() => setShowEditor(false), []);
const location = useLocation();
const isInMemoDetailPage = location.pathname.startsWith(`/${memoData.name}`) || location.pathname.startsWith("/memos/shares/");
const showCommentPreview = !isInMemoDetailPage && computeCommentAmount(memoData) > 0;
useEffect(() => {
const card = cardRef.current;
if (!card) {
return;
}
const updateWidth = (nextWidth?: number) => {
const width = Math.round(nextWidth ?? card.getBoundingClientRect().width);
setCardWidth((prev) => (prev === width ? prev : width));
};
updateWidth();
if (typeof ResizeObserver === "undefined") {
const handleResize = () => updateWidth();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}
const resizeObserver = new ResizeObserver((entries) => {
updateWidth(entries[0]?.contentRect.width);
});
resizeObserver.observe(card);
return () => resizeObserver.disconnect();
}, []);
const contextValue = useMemo(
() => ({
memo: memoData,
creator,
currentUser,
parentPage,
cardWidth,
isArchived,
readonly,
showBlurredContent,
blurred,
openEditor,
toggleBlurVisibility,
openPreview,
}),
[
memoData,
creator,
currentUser,
parentPage,
cardWidth,
isArchived,
readonly,
showBlurredContent,
blurred,
openEditor,
toggleBlurVisibility,
openPreview,
],
);
if (showEditor) {
return (
<MemoEditor
autoFocus
className="mb-2"
cacheKey={`inline-memo-editor-${memoData.name}`}
memo={memoData}
parentMemoName={memoData.parent || undefined}
onConfirm={closeEditor}
onCancel={closeEditor}
/>
);
}
const article = (
<article
className={cn(MEMO_CARD_BASE_CLASSES, showCommentPreview ? "mb-0 rounded-b-none" : "mb-2", className)}
ref={cardRef}
tabIndex={readonly ? -1 : 0}
>
<MemoHeader showCreator={showCreator} showVisibility={showVisibility} showPinned={showPinned} />
<MemoBody compact={compact} />
<PreviewImageDialog
open={previewState.open}
onOpenChange={setPreviewOpen}
items={previewState.items}
initialIndex={previewState.index}
/>
{props.onShareImageDialogOpenChange && (
<MemoShareImageDialog open={Boolean(props.shareImageDialogOpen)} onOpenChange={props.onShareImageDialogOpenChange} />
)}
</article>
);
return (
<MemoViewContext.Provider value={contextValue}>
{showCommentPreview ? (
<div className="w-full mb-2">
{article}
<MemoCommentListView />
</div>
) : (
article
)}
</MemoViewContext.Provider>
);
};
export default memo(MemoView);
@@ -0,0 +1,66 @@
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { createContext, useContext } from "react";
import { useLocation } from "react-router-dom";
import { useView } from "@/contexts/ViewContext";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import type { User } from "@/types/proto/api/v1/user_service_pb";
import type { PreviewMediaItem } from "@/utils/media-item";
import { RELATIVE_TIME_THRESHOLD_MS } from "./constants";
export interface MemoViewContextValue {
memo: Memo;
creator: User | undefined;
currentUser: User | undefined;
parentPage: string;
cardWidth: number;
isArchived: boolean;
readonly: boolean;
showBlurredContent: boolean;
blurred: boolean;
openEditor: () => void;
toggleBlurVisibility: () => void;
openPreview: (items: string | string[] | PreviewMediaItem[], index?: number) => void;
}
export const MemoViewContext = createContext<MemoViewContextValue | null>(null);
export const useMemoViewContext = (): MemoViewContextValue => {
const context = useContext(MemoViewContext);
if (!context) {
throw new Error("useMemoViewContext must be used within MemoViewContext.Provider");
}
return context;
};
export const computeCommentAmount = (memo: Memo): number =>
memo.relations.filter((r) => r.type === MemoRelation_Type.COMMENT && r.relatedMemo?.name === memo.name).length;
export const useMemoViewDerived = () => {
const { memo, isArchived, readonly } = useMemoViewContext();
const { timeBasis } = useView();
const location = useLocation();
const isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`) || location.pathname.startsWith("/memos/shares/");
const commentAmount = computeCommentAmount(memo);
const createTime = memo.createTime ? timestampDate(memo.createTime) : undefined;
const updateTime = memo.updateTime ? timestampDate(memo.updateTime) : undefined;
const displayTime = timeBasis === "update_time" ? updateTime : createTime;
const isDisplayingUpdatedTime =
timeBasis === "update_time" && !!createTime && !!updateTime && updateTime.getTime() !== createTime.getTime();
const relativeTimeFormat: "datetime" | "auto" =
displayTime && Date.now() - displayTime.getTime() > RELATIVE_TIME_THRESHOLD_MS ? "datetime" : "auto";
return {
isArchived,
readonly,
isInMemoDetailPage,
commentAmount,
createTime,
updateTime,
displayTime,
isDisplayingUpdatedTime,
relativeTimeFormat,
};
};
@@ -0,0 +1,69 @@
import { useMemo } from "react";
import { AttachmentListView, LocationDisplayView, RelationListView } from "@/components/MemoMetadata";
import { cn } from "@/lib/utils";
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
import MemoContent from "../../MemoContent";
import { MemoReactionListView } from "../../MemoReactionListView";
import { useMemoHandlers } from "../hooks";
import { useMemoViewContext } from "../MemoViewContext";
import type { MemoBodyProps } from "../types";
const BlurOverlay: React.FC<{ onClick?: () => void }> = ({ onClick }) => {
const t = useTranslate();
return (
<div className="absolute inset-0 z-10 pt-4 flex items-center justify-center" onClick={onClick}>
<button
type="button"
className="rounded-lg border border-border bg-card px-2 py-1 text-xs text-muted-foreground transition-colors hover:border-accent hover:bg-accent hover:text-foreground"
>
{t("memo.click-to-show-sensitive-content")}
</button>
</div>
);
};
const getContentRevision = (content: string) => {
let hash = 2166136261;
for (let i = 0; i < content.length; i++) {
hash ^= content.charCodeAt(i);
hash = Math.imul(hash, 16777619);
}
return `${content.length}-${hash >>> 0}`;
};
const MemoBody: React.FC<MemoBodyProps> = ({ compact }) => {
const { memo, parentPage, showBlurredContent, blurred, readonly, openEditor, openPreview, toggleBlurVisibility } = useMemoViewContext();
const { handleMemoContentClick, handleMemoContentDoubleClick } = useMemoHandlers({ readonly, openEditor, openPreview });
const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);
const contentRevision = useMemo(() => getContentRevision(memo.content), [memo.content]);
return (
<>
<div
className={cn(
"w-full flex flex-col justify-start items-start gap-2",
blurred && !showBlurredContent && "blur-lg transition-all duration-200",
)}
>
<MemoContent
key={`${memo.name}-${contentRevision}`}
content={memo.content}
onClick={handleMemoContentClick}
onDoubleClick={handleMemoContentDoubleClick}
compact={memo.pinned ? false : compact} // Always show full content when pinned
/>
<AttachmentListView attachments={memo.attachments} onImagePreview={openPreview} />
<RelationListView relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />
{memo.location && <LocationDisplayView location={memo.location} />}
<MemoReactionListView memo={memo} reactions={memo.reactions} />
</div>
{blurred && !showBlurredContent && <BlurOverlay onClick={toggleBlurVisibility} />}
</>
);
};
export default MemoBody;
@@ -0,0 +1,58 @@
import { ArrowUpRightIcon } from "lucide-react";
import { Link } from "react-router-dom";
import { MemoPreview } from "@/components/MemoPreview";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { useMemoComments } from "@/hooks/useMemoQueries";
import { useUsersByNames } from "@/hooks/useUserQueries";
import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext";
const MemoCommentListView: React.FC = () => {
const { memo } = useMemoViewContext();
const { isInMemoDetailPage, commentAmount } = useMemoViewDerived();
const { data } = useMemoComments(memo.name, { enabled: !isInMemoDetailPage && commentAmount > 0, pageSize: 3 });
const comments = data?.memos ?? [];
const displayedComments = comments.slice(0, 3);
const { data: commentCreators } = useUsersByNames(displayedComments.map((comment) => comment.creator));
if (isInMemoDetailPage || commentAmount === 0) {
return null;
}
return (
<div className="border border-t-0 border-border rounded-b-lg px-4 pt-2 pb-3 flex flex-col gap-1">
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-muted-foreground">Comments{commentAmount > 1 ? ` (${commentAmount})` : ""}</span>
<Link
to={`/${memo.name}#comments`}
className="flex items-center gap-0.5 text-xs text-muted-foreground/80 hover:underline underline-offset-2 transition-colors"
>
View all
<ArrowUpRightIcon className="w-3 h-3" />
</Link>
</div>
{displayedComments.map((comment) => {
const uid = extractMemoIdFromName(comment.name);
const creator = commentCreators?.get(comment.creator);
return (
<Link
key={comment.name}
to={`/${memo.name}#${uid}`}
viewTransition
className="rounded-md bg-muted/40 px-2 py-1 transition-colors hover:bg-muted/60"
>
<MemoPreview
content={comment.snippet || comment.content}
attachments={comment.attachments}
creator={creator}
showCreator
truncate
/>
</Link>
);
})}
</div>
);
};
export default MemoCommentListView;
@@ -0,0 +1,170 @@
import { BookmarkIcon } from "lucide-react";
import { useCallback, useState } from "react";
import { Link } from "react-router-dom";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import useNavigateTo from "@/hooks/useNavigateTo";
import i18n from "@/i18n";
import { cn } from "@/lib/utils";
import { Visibility } from "@/types/proto/api/v1/memo_service_pb";
import type { User } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
import { convertVisibilityToString } from "@/utils/memo";
import MemoActionMenu from "../../MemoActionMenu";
import { ReactionSelector } from "../../MemoReactionListView";
import UserAvatar from "../../UserAvatar";
import VisibilityIcon from "../../VisibilityIcon";
import { useMemoActions } from "../hooks";
import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext";
import type { MemoHeaderProps } from "../types";
const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, showPinned }) => {
const t = useTranslate();
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
const { memo, creator, currentUser, parentPage, isArchived, readonly, openEditor } = useMemoViewContext();
const { createTime, updateTime, displayTime: memoDisplayTime, isDisplayingUpdatedTime, relativeTimeFormat } = useMemoViewDerived();
const navigateTo = useNavigateTo();
const handleGotoMemoDetailPage = useCallback(() => {
navigateTo(`/${memo.name}`, { state: { from: parentPage } });
}, [memo.name, parentPage, navigateTo]);
const { unpinMemo } = useMemoActions(memo);
const timeValue = isArchived ? (
memoDisplayTime?.toLocaleString(i18n.language)
) : (
<relative-time datetime={memoDisplayTime?.toISOString()} lang={i18n.language} format={relativeTimeFormat} no-title=""></relative-time>
);
const displayTime = isDisplayingUpdatedTime ? (
<>
{t("common.last-updated-at")} {timeValue}
</>
) : (
timeValue
);
const timeTooltip = {
createdAt: createTime ? `${t("common.created-at")}: ${createTime.toLocaleString(i18n.language)}` : undefined,
updatedAt: updateTime ? `${t("common.last-updated-at")}: ${updateTime.toLocaleString(i18n.language)}` : undefined,
};
return (
<div className="w-full flex flex-row justify-between items-center gap-2">
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
{showCreator && creator ? (
<CreatorDisplay creator={creator} displayTime={displayTime} timeTooltip={timeTooltip} onGotoDetail={handleGotoMemoDetailPage} />
) : (
<TimeDisplay displayTime={displayTime} timeTooltip={timeTooltip} onGotoDetail={handleGotoMemoDetailPage} />
)}
</div>
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
{currentUser && !isArchived && (
<ReactionSelector
className={cn("border-none w-auto h-auto", reactionSelectorOpen && "block!", "block sm:hidden sm:group-hover:block")}
memo={memo}
onOpenChange={setReactionSelectorOpen}
/>
)}
{showVisibility && memo.visibility !== Visibility.PRIVATE && (
<Tooltip>
<TooltipTrigger>
<span className="flex justify-center items-center rounded-md hover:opacity-80">
<VisibilityIcon visibility={memo.visibility} />
</span>
</TooltipTrigger>
<TooltipContent>
{t(`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}` as Parameters<typeof t>[0])}
</TooltipContent>
</Tooltip>
)}
{showPinned && memo.pinned && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-pointer">
<BookmarkIcon className="w-4 h-auto text-primary" onClick={unpinMemo} />
</span>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.unpin")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<MemoActionMenu memo={memo} readonly={readonly} onEdit={openEditor} />
</div>
</div>
);
};
interface CreatorDisplayProps {
creator: User;
displayTime: React.ReactNode;
timeTooltip: TimeTooltipContent;
onGotoDetail: () => void;
}
const CreatorDisplay: React.FC<CreatorDisplayProps> = ({ creator, displayTime, timeTooltip, onGotoDetail }) => (
<div className="w-full flex flex-row justify-start items-center">
<Link className="w-auto hover:opacity-80 rounded-md transition-colors" to={`/u/${encodeURIComponent(creator.username)}`} viewTransition>
<UserAvatar className="mr-2 shrink-0" avatarUrl={creator.avatarUrl} />
</Link>
<div className="w-full flex flex-col justify-center items-start">
<Link
className="block leading-tight hover:opacity-80 rounded-md transition-colors truncate text-muted-foreground"
to={`/u/${encodeURIComponent(creator.username)}`}
viewTransition
>
{creator.displayName || creator.username}
</Link>
<TimeTooltip content={timeTooltip}>
<button
type="button"
className="w-auto -mt-0.5 text-xs leading-tight text-muted-foreground select-none cursor-pointer hover:opacity-80 transition-colors text-left"
onClick={onGotoDetail}
>
{displayTime}
</button>
</TimeTooltip>
</div>
</div>
);
interface TimeTooltipContent {
createdAt?: string;
updatedAt?: string;
}
const TimeTooltip = ({ children, content }: { children: React.ReactElement; content: TimeTooltipContent }) => (
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent align="start" className="flex flex-col items-start gap-0.5 whitespace-nowrap text-left">
{content.createdAt && <span>{content.createdAt}</span>}
{content.updatedAt && <span>{content.updatedAt}</span>}
</TooltipContent>
</Tooltip>
);
interface TimeDisplayProps {
displayTime: React.ReactNode;
timeTooltip: TimeTooltipContent;
onGotoDetail: () => void;
}
const TimeDisplay: React.FC<TimeDisplayProps> = ({ displayTime, timeTooltip, onGotoDetail }) => (
<TimeTooltip content={timeTooltip}>
<button
type="button"
className="w-auto text-sm leading-tight text-muted-foreground select-none cursor-pointer hover:text-foreground transition-colors text-left"
onClick={onGotoDetail}
>
{displayTime}
</button>
</TimeTooltip>
);
export default MemoHeader;
@@ -0,0 +1,34 @@
import { Link } from "react-router-dom";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { cn } from "@/lib/utils";
interface MemoSnippetLinkProps {
name: string;
snippet: string;
to: string;
state?: object;
className?: string;
}
const MemoSnippetLink = ({ name, snippet, to, state, className }: MemoSnippetLinkProps) => {
const memoId = extractMemoIdFromName(name);
return (
<Link
className={cn(
"flex items-center gap-1 px-1 py-1 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-colors group",
className,
)}
to={to}
viewTransition
state={state}
>
<span className="text-[8px] font-mono px-1 py-0.5 rounded border border-border bg-muted/40 group-hover:bg-accent/30 transition-colors shrink-0">
{memoId.slice(0, 6)}
</span>
<span className="truncate">{snippet || <span className="italic opacity-60">No content</span>}</span>
</Link>
);
};
export default MemoSnippetLink;
@@ -0,0 +1,3 @@
export { default as MemoBody } from "./MemoBody";
export { default as MemoCommentListView } from "./MemoCommentListView";
export { default as MemoHeader } from "./MemoHeader";
+4
View File
@@ -0,0 +1,4 @@
export const MEMO_CARD_BASE_CLASSES =
"relative group flex flex-col justify-start items-start bg-card w-full px-4 py-3 mb-2 gap-2 text-card-foreground rounded-lg border border-border transition-colors";
export const RELATIVE_TIME_THRESHOLD_MS = 1000 * 60 * 60 * 24;
@@ -0,0 +1,3 @@
export { useImagePreview } from "./useImagePreview";
export { useMemoActions } from "./useMemoActions";
export { useMemoHandlers } from "./useMemoHandlers";
@@ -0,0 +1,55 @@
import { useCallback, useState } from "react";
import type { PreviewMediaItem } from "@/utils/media-item";
export interface ImagePreviewState {
open: boolean;
items: PreviewMediaItem[];
index: number;
}
export interface UseImagePreviewReturn {
previewState: ImagePreviewState;
openPreview: (items: string | string[] | PreviewMediaItem[], index?: number) => void;
setPreviewOpen: (open: boolean) => void;
}
export const useImagePreview = (): UseImagePreviewReturn => {
const [previewState, setPreviewState] = useState<ImagePreviewState>({ open: false, items: [], index: 0 });
const openPreview = useCallback((items: string | string[] | PreviewMediaItem[], index = 0) => {
const normalizedItems = normalizePreviewItems(items);
setPreviewState({ open: true, items: normalizedItems, index });
}, []);
const setPreviewOpen = useCallback((open: boolean) => {
setPreviewState((prev) => ({ ...prev, open }));
}, []);
return { previewState, openPreview, setPreviewOpen };
};
function normalizePreviewItems(items: string | string[] | PreviewMediaItem[]): PreviewMediaItem[] {
if (typeof items === "string") {
return [
{
id: items,
kind: "image",
sourceUrl: items,
posterUrl: items,
filename: "Image",
},
];
}
if (Array.isArray(items) && (items.length === 0 || typeof items[0] === "string")) {
return (items as string[]).map((url) => ({
id: url,
kind: "image",
sourceUrl: url,
posterUrl: url,
filename: "Image",
}));
}
return items as PreviewMediaItem[];
}
@@ -0,0 +1,13 @@
import { useUpdateMemo } from "@/hooks/useMemoQueries";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
export const useMemoActions = (memo: Memo) => {
const { mutateAsync: updateMemo } = useUpdateMemo();
const unpinMemo = async () => {
if (!memo.pinned) return;
await updateMemo({ update: { name: memo.name, pinned: false }, updateMask: ["pinned"] });
};
return { unpinMemo };
};
@@ -0,0 +1,40 @@
import { useCallback } from "react";
import { useInstance } from "@/contexts/InstanceContext";
import type { PreviewMediaItem } from "@/utils/media-item";
interface UseMemoHandlersOptions {
readonly: boolean;
openEditor: () => void;
openPreview: (items: string | string[] | PreviewMediaItem[], index?: number) => void;
}
export const useMemoHandlers = (options: UseMemoHandlersOptions) => {
const { readonly, openEditor, openPreview } = options;
const { memoRelatedSetting } = useInstance();
const handleMemoContentClick = useCallback(
(e: React.MouseEvent) => {
const targetEl = e.target as HTMLElement;
if (targetEl.tagName === "IMG") {
const linkElement = targetEl.closest("a");
if (linkElement) return; // If image is inside a link, don't show preview
const imgUrl = targetEl.getAttribute("src");
if (imgUrl) openPreview(imgUrl);
}
},
[openPreview],
);
const handleMemoContentDoubleClick = useCallback(
(e: React.MouseEvent) => {
if (readonly) return;
if (memoRelatedSetting.enableDoubleClickEdit) {
e.preventDefault();
openEditor();
}
},
[readonly, openEditor, memoRelatedSetting.enableDoubleClickEdit],
);
return { handleMemoContentClick, handleMemoContentDoubleClick };
};
+2
View File
@@ -0,0 +1,2 @@
export { default, default as MemoView } from "./MemoView";
export type { MemoViewProps } from "./types";
+23
View File
@@ -0,0 +1,23 @@
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
export interface MemoViewProps {
memo: Memo;
compact?: boolean;
showCreator?: boolean;
showVisibility?: boolean;
showPinned?: boolean;
className?: string;
parentPage?: string;
shareImageDialogOpen?: boolean;
onShareImageDialogOpenChange?: (open: boolean) => void;
}
export interface MemoHeaderProps {
showCreator?: boolean;
showVisibility?: boolean;
showPinned?: boolean;
}
export interface MemoBodyProps {
compact?: boolean;
}