Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,057 changes: 6,057 additions & 0 deletions web/package-lock.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions web/src/components/MemoActionMenu/MemoActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Edit3Icon,
FileTextIcon,
LinkIcon,
MessageSquarePlusIcon,
MoreVerticalIcon,
TrashIcon,
} from "lucide-react";
Expand Down Expand Up @@ -47,6 +48,7 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
handleCopyContent,
handleDeleteMemoClick,
confirmDeleteMemo,
handleCreateFollowUp
} = useMemoActionHandlers({
memo,
onEdit: props.onEdit,
Expand Down Expand Up @@ -77,6 +79,14 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
</>
)}

{/* Create Follow-up (non-archived) */}
{!isArchived && (
<DropdownMenuItem onClick={handleCreateFollowUp}>
<MessageSquarePlusIcon className="w-4 h-auto" />
{t("memo.create-followup")}
</DropdownMenuItem>
)}

{/* Copy submenu (non-archived) */}
{!isArchived && (
<DropdownMenuSub>
Expand Down
21 changes: 21 additions & 0 deletions web/src/components/MemoActionMenu/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,26 @@ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen }: Use
toast.success(t("message.succeed-copy-link"));
}, [memo.name, t, profile.instanceUrl]);


const handleCreateFollowUp = useCallback(() => {
let host = profile.instanceUrl;
if (host === "") {
host = window.location.origin;
}

// const memoLink = `${host}/${memo.name}`;

navigateTo("/", {
state: {
// followUpContent: `Following up on: ${memoLink}\n\n`,
followUpParent: memo.name,
},
});

window.scrollTo({ top: 0, behavior: "smooth" });

}, [memo.name, profile.instanceUrl, navigateTo]);

const handleCopyContent = useCallback(() => {
copy(memo.content);
toast.success(t("message.succeed-copy-content"));
Expand Down Expand Up @@ -122,5 +142,6 @@ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen }: Use
handleCopyContent,
handleDeleteMemoClick,
confirmDeleteMemo,
handleCreateFollowUp
};
};
17 changes: 14 additions & 3 deletions web/src/components/MemoEditor/hooks/useMemoInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ interface UseMemoInitOptions {
username: string;
autoFocus?: boolean;
defaultVisibility?: Visibility;
initialContent?: string;
}

export const useMemoInit = ({ editorRef, memo, cacheKey, username, autoFocus, defaultVisibility }: UseMemoInitOptions) => {
export const useMemoInit = ({ editorRef, memo, cacheKey, username, autoFocus, initialContent, defaultVisibility }: UseMemoInitOptions) => {
const { actions, dispatch } = useEditorContext();
const initializedRef = useRef(false);

Expand All @@ -25,7 +26,8 @@ export const useMemoInit = ({ editorRef, memo, cacheKey, username, autoFocus, de
dispatch(actions.initMemo(memoService.fromMemo(memo)));
} else {
const cachedContent = cacheService.load(cacheService.key(username, cacheKey));
if (cachedContent) {
const content = initialContent || cachedContent || "";
if (content) {
dispatch(actions.updateContent(cachedContent));
}
if (defaultVisibility !== undefined) {
Expand All @@ -36,5 +38,14 @@ export const useMemoInit = ({ editorRef, memo, cacheKey, username, autoFocus, de
if (autoFocus) {
setTimeout(() => editorRef.current?.focus(), 100);
}
}, [memo, cacheKey, username, autoFocus, defaultVisibility, actions, dispatch, editorRef]);
}, [memo, cacheKey, username, autoFocus, defaultVisibility, actions, dispatch, initialContent, editorRef]);


useEffect(() => {
if (initialContent) {
dispatch(actions.updateContent(initialContent));
}
}, [initialContent, actions, dispatch]);


};
3 changes: 2 additions & 1 deletion web/src/components/MemoEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
parentMemoName,
autoFocus,
placeholder,
initialContent,
onConfirm,
onCancel,
}) => {
Expand All @@ -45,7 +46,7 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
// Get default visibility from user settings
const defaultVisibility = userGeneralSetting?.memoVisibility ? convertVisibilityFromString(userGeneralSetting.memoVisibility) : undefined;

useMemoInit({ editorRef, memo, cacheKey, username: currentUser?.name ?? "", autoFocus, defaultVisibility });
useMemoInit({ editorRef, memo, cacheKey, initialContent, username: currentUser?.name ?? "", autoFocus, defaultVisibility });

// Auto-save content to localStorage
useAutoSave(state.content, currentUser?.name ?? "", cacheKey);
Expand Down
1 change: 1 addition & 0 deletions web/src/components/MemoEditor/types/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface MemoEditorProps {
/** Existing memo to edit. When provided, the editor initializes from it without fetching. */
memo?: Memo;
parentMemoName?: string;
initialContent?: string;
autoFocus?: boolean;
onConfirm?: (memoName: string) => void;
onCancel?: () => void;
Expand Down
36 changes: 34 additions & 2 deletions web/src/components/MemoView/MemoView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { memo, useMemo, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { Link, useLocation } from "react-router-dom";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useUser } from "@/hooks/useUserQueries";
import { cn } from "@/lib/utils";
Expand All @@ -12,6 +12,8 @@ import { MEMO_CARD_BASE_CLASSES } from "./constants";
import { useImagePreview, useMemoActions, useMemoHandlers } from "./hooks";
import { computeCommentAmount, MemoViewContext } from "./MemoViewContext";
import type { MemoViewProps } from "./types";
import { getTagColorClasses } from "@/utils/tag-colors";
import { ArrowRight } from "lucide-react";

const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
const { memo: memoData, className, parentPage: parentPageProp, compact, showCreator, showVisibility, showPinned } = props;
Expand Down Expand Up @@ -47,6 +49,13 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
const isInMemoDetailPage = location.pathname.startsWith(`/${memoData.name}`);
const showCommentPreview = !isInMemoDetailPage && computeCommentAmount(memoData) > 0;

const isFollowUp = Boolean(memoData.parent);
const parentMemoId = memoData.parent ? memoData.parent.split('/').pop() : null;

const tagColors = useMemo(() => {
return getTagColorClasses(memoData.tags || []);
}, [memoData.tags]);

const contextValue = useMemo(
() => ({
memo: memoData,
Expand Down Expand Up @@ -76,10 +85,33 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {

const article = (
<article
className={cn(MEMO_CARD_BASE_CLASSES, showCommentPreview ? "mb-0 rounded-b-none" : "mb-2", className)}
className={cn(MEMO_CARD_BASE_CLASSES,
showCommentPreview ? "mb-0 rounded-b-none" : "mb-2",
tagColors.background,
tagColors.border,
tagColors.hover,
className
)}
ref={cardRef}
tabIndex={readonly ? -1 : 0}
>


{isFollowUp && (
<div className="flex items-center gap-1.5 mb-2 text-xs text-muted-foreground">
<span>Following up</span>
<ArrowRight className="w-3.5 h-3.5" />
<Link
to={`/${memoData.parent}`}
className="font-mono text-primary hover:underline inline-flex items-center gap-0.5"
onClick={(e) => e.stopPropagation()}
>
#{parentMemoId}
</Link>
</div>
)}


<MemoHeader
showCreator={showCreator}
showVisibility={showVisibility}
Expand Down
66 changes: 64 additions & 2 deletions web/src/components/PagedMemoList/PagedMemoList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useQueryClient } from "@tanstack/react-query";
import { ArrowUpIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { matchPath } from "react-router-dom";
import { matchPath, useLocation } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { userServiceClient } from "@/connect";
import { useView } from "@/contexts/ViewContext";
Expand All @@ -18,6 +18,7 @@ import MasonryView from "../MasonryView";
import MemoEditor from "../MemoEditor";
import MemoFilters from "../MemoFilters";
import Skeleton from "../Skeleton";
import useNavigateTo from "@/hooks/useNavigateTo";

interface Props {
renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element;
Expand Down Expand Up @@ -82,13 +83,67 @@ function useAutoFetchWhenNotScrollable({
}

const PagedMemoList = (props: Props) => {

const navigate = useNavigateTo();


const t = useTranslate();
const { layout } = useView();
const queryClient = useQueryClient();

// Show memo editor only on the root route
const showMemoEditor = Boolean(matchPath(Routes.ROOT, window.location.pathname));

const location = useLocation();
// const followUpContent = (location.state as { followUpContent?: string })?.followUpContent;
const followUpState = location.state as {
followUpContent?: string;
followUpParent?: string;
} | null;

// Extract the values we need ONCE and store them in state
const [editorState, setEditorState] = useState<{
initialContent?: string;
parentMemoName?: string;
}>({
initialContent: undefined,
parentMemoName: undefined,
});

// Extract the values we need
// const initialContent = followUpState?.followUpContent;
// const parentMemoName = followUpState?.followUpParent;

// Clear the location state immediately after reading it
// useEffect(() => {
// if (followUpState?.followUpContent || followUpState?.followUpParent) {
// navigate(location.pathname, { replace: true, state: null });
// }
// }, [followUpState, navigate, location.pathname]);

// Update editor state when location state changes, then clear location state
useEffect(() => {
if (followUpState?.followUpContent || followUpState?.followUpParent) {
// Save the values to component state
setEditorState({
initialContent: followUpState.followUpContent,
parentMemoName: followUpState.followUpParent,
});

// Clear location state immediately after saving to component state
navigate(location.pathname, { replace: true, state: null });
}
}, [followUpState?.followUpContent, followUpState?.followUpParent, navigate, location.pathname]);

// Clear editor state after saving (when editor confirms)
const handleEditorConfirm = useCallback((memoName?: string) => {
// Clear the editor state so next memo doesn't inherit parent
setEditorState({
initialContent: undefined,
parentMemoName: undefined,
});
}, []);

const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteMemos(
{
state: props.state || State.NORMAL,
Expand Down Expand Up @@ -161,7 +216,14 @@ const PagedMemoList = (props: Props) => {
prefixElement={
<>
{showMemoEditor ? (
<MemoEditor className="mb-2" cacheKey="home-memo-editor" placeholder={t("editor.any-thoughts")} />
<MemoEditor
className="mb-2"
cacheKey="home-memo-editor"
placeholder={t("editor.any-thoughts")}
initialContent={editorState.initialContent}
parentMemoName={editorState.parentMemoName}
onConfirm={handleEditorConfirm}
/>
) : undefined}
<MemoFilters />
</>
Expand Down
1 change: 1 addition & 0 deletions web/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
},
"copy-content": "Copy Content",
"copy-link": "Copy Link",
"create-followup": "Create follow-up",
"count-memos-in-date": "{{count}} {{memos}} in {{date}}",
"delete-confirm": "Are you sure you want to delete this memo?",
"delete-confirm-description": "This action is irreversible. Attachments, links, and references will also be removed.",
Expand Down
Loading