Skip to content
Merged
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
62 changes: 62 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ import {
useBookmarkMutation,
} from "@/experiments";
import { useActions } from "@/hooks/useActions";
import { useAnnotations } from "@/hooks/useAnnotations";
import { useNavigation } from "@/hooks/useNavigation";
import { copyToClipboard } from "@/lib/clipboard";
import {
AnnotationEditorContent,
type AnnotationEditorResult,
} from "@/modals/AnnotationEditor";
import { BookmarkFolderSelectorContent } from "@/modals/BookmarkFolderSelector";
import { DeleteFolderConfirmContent } from "@/modals/DeleteFolderConfirmModal";
import { ExitConfirmationContent } from "@/modals/ExitConfirmationModal";
Expand Down Expand Up @@ -147,6 +152,10 @@ function AppContent({ client, user }: AppProps) {
currentFolderId: selectedBookmarkFolder?.id,
});

// Annotations hook for bookmark annotations
const { getAnnotation, hasAnnotation, setAnnotation, deleteAnnotation } =
useAnnotations();

// Splash screen state
const [showSplash, setShowSplash] = useState(true);
const [minTimeElapsed, setMinTimeElapsed] = useState(false);
Expand Down Expand Up @@ -391,6 +400,55 @@ function AppContent({ client, user }: AppProps) {
}
}, [client, selectedPost, dialog]);

// Open annotation editor for a tweet
const handleAnnotate = useCallback(
async (tweetId: string) => {
const existingAnnotation = getAnnotation(tweetId);

const result = await dialog.prompt<AnnotationEditorResult>({
content: (ctx) => (
<AnnotationEditorContent
initialText={existingAnnotation ?? ""}
hasExisting={!!existingAnnotation}
resolve={ctx.resolve}
dismiss={ctx.dismiss}
dialogId={ctx.dialogId}
/>
),
unstyled: true,
});

// undefined means dismissed
if (!result) return;

if (result.action === "delete") {
deleteAnnotation(tweetId);
toast.success("Annotation deleted");
} else if (result.action === "save" && result.text) {
setAnnotation(tweetId, result.text);
toast.success(
existingAnnotation ? "Annotation updated" : "Annotation saved"
);
}
},
[dialog, getAnnotation, setAnnotation, deleteAnnotation]
);

// Handler for annotating the currently selected post (from PostDetailScreen)
const handleAnnotateSelectedPost = useCallback(() => {
if (selectedPost) {
handleAnnotate(selectedPost.id);
}
}, [selectedPost, handleAnnotate]);

// Handler for annotating a post from BookmarksScreen list
const handleAnnotateFromList = useCallback(
(post: TweetData) => {
handleAnnotate(post.id);
},
[handleAnnotate]
);

// Open bookmark folder selector dialog
const handleBookmarkFolderSelectorOpen = useCallback(async () => {
const folder = await dialog.choice<BookmarkFolder | null>({
Expand Down Expand Up @@ -777,6 +835,8 @@ function AppContent({ client, user }: AppProps) {
onLike={() => toggleLike(selectedPost)}
onBookmark={() => toggleBookmark(selectedPost)}
onMoveToFolder={handleMoveToFolder}
onAnnotate={handleAnnotateSelectedPost}
annotationText={getAnnotation(selectedPost.id)}
isLiked={getState(selectedPost.id).liked}
isBookmarked={getState(selectedPost.id).bookmarked}
isJustLiked={getState(selectedPost.id).justLiked}
Expand Down Expand Up @@ -858,8 +918,10 @@ function AppContent({ client, user }: AppProps) {
onPostSelect={handlePostSelect}
onLike={toggleLike}
onBookmark={toggleBookmark}
onAnnotate={handleAnnotateFromList}
getActionState={getState}
initActionState={initState}
hasAnnotation={hasAnnotation}
onCreateFolder={handleCreateBookmarkFolder}
onEditFolder={handleEditBookmarkFolder}
onDeleteFolder={handleDeleteBookmarkFolder}
Expand Down
11 changes: 10 additions & 1 deletion src/components/PostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ interface PostCardProps {
isJustLiked?: boolean;
/** True briefly after bookmarking (for visual pulse feedback) */
isJustBookmarked?: boolean;
/** Whether the tweet has an annotation */
hasAnnotation?: boolean;
/** Parent post author username - if provided, strips leading @mention matching this user */
parentAuthorUsername?: string;
/** Main post author username (for nested reply mention stripping) */
Expand All @@ -66,6 +68,9 @@ const HEART_FILLED = "\u2665"; // ♥
const FLAG_EMPTY = "\u2690"; // ⚐
const FLAG_FILLED = "\u2691"; // ⚑

// Unicode symbol for annotation indicator
const NOTE_INDICATOR = "+"; // Simple plus sign for "has note"

export function PostCard({
post,
isSelected,
Expand All @@ -74,6 +79,7 @@ export function PostCard({
isBookmarked,
isJustLiked,
isJustBookmarked,
hasAnnotation,
parentAuthorUsername,
mainPostAuthorUsername,
onCardClick,
Expand Down Expand Up @@ -192,14 +198,17 @@ export function PostCard({
backgroundColor: isSelected ? colors.selectedBg : undefined,
}}
>
{/* Author line with selection indicator */}
{/* Author line with selection indicator and annotation marker */}
<box style={{ flexDirection: "row" }}>
<text fg={colors.primary}>{isSelected ? "> " : " "}</text>
<text>
<b fg={colors.primary}>{post.author.name}</b>
</text>
<text fg={colors.handle}> @{post.author.username}</text>
<text fg={colors.dim}>{timeAgo ? ` · ${timeAgo}` : ""}</text>
{hasAnnotation ? (
<text fg={colors.warning}> [{NOTE_INDICATOR}]</text>
) : null}
</box>

{/* Post text */}
Expand Down
11 changes: 10 additions & 1 deletion src/components/PostList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ interface PostListProps {
onLike?: (post: TweetData) => void;
/** Called when user presses 'b' to toggle bookmark on selected post */
onBookmark?: (post: TweetData) => void;
/** Called when user presses 'a' to annotate selected post */
onAnnotate?: (post: TweetData) => void;
/** Get current action state for a tweet */
getActionState?: (tweetId: string) => TweetActionState;
/** Initialize action state from API data */
Expand All @@ -31,6 +33,8 @@ interface PostListProps {
liked: boolean,
bookmarked: boolean
) => void;
/** Check if a tweet has an annotation */
hasAnnotation?: (tweetId: string) => boolean;
/** Called when user scrolls near the bottom to load more posts */
onLoadMore?: () => void;
/** Whether more posts are currently being loaded */
Expand All @@ -55,8 +59,10 @@ export function PostList({
onSelectedIndexChange,
onLike,
onBookmark,
onAnnotate,
getActionState,
initActionState,
hasAnnotation,
onLoadMore,
loadingMore = false,
hasMore = true,
Expand Down Expand Up @@ -121,7 +127,7 @@ export function PostList({
onSelectedIndexChange?.(selectedIndex);
}, [selectedIndex, onSelectedIndexChange]);

// Handle like/bookmark keyboard shortcuts
// Handle like/bookmark/annotate keyboard shortcuts
useKeyboard((key) => {
if (!focused || posts.length === 0) return;

Expand All @@ -132,6 +138,8 @@ export function PostList({
onLike?.(currentPost);
} else if (key.name === "b") {
onBookmark?.(currentPost);
} else if (key.name === "a") {
onAnnotate?.(currentPost);
}
});

Expand Down Expand Up @@ -237,6 +245,7 @@ export function PostList({
isBookmarked={actionState?.bookmarked}
isJustLiked={actionState?.justLiked}
isJustBookmarked={actionState?.justBookmarked}
hasAnnotation={hasAnnotation?.(post.id)}
onCardClick={() => onPostSelect?.(post)}
onLikeClick={() => onLike?.(post)}
onBookmarkClick={() => onBookmark?.(post)}
Expand Down
Loading
Loading