a18hvcryt: Delete the signed-in person's own post from its menu
This commit is contained in:
@@ -38,7 +38,13 @@ export function App() {
|
||||
);
|
||||
}
|
||||
if (state.kind === "out") {
|
||||
return <SignInForm onSignedIn={(person) => setState({ kind: "in", person })} />;
|
||||
return <SignInForm onSignedIn={(person) => {
|
||||
setState({ kind: "in", person });
|
||||
// POST /api/session keeps its shape; the feed user id comes from GET.
|
||||
void loadSession().then((full) => {
|
||||
if (full) setState({ kind: "in", person: full });
|
||||
});
|
||||
}} />;
|
||||
}
|
||||
return (
|
||||
<main className="shell">
|
||||
@@ -52,7 +58,7 @@ export function App() {
|
||||
{signingOut ? "Signing out..." : "Sign out"}
|
||||
</button>
|
||||
</header>
|
||||
<Feed onSignedOut={() => setState({ kind: "out" })} />
|
||||
<Feed feedUserId={state.person.feedUserId} onSignedOut={() => setState({ kind: "out" })} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
+34
-1
@@ -5,11 +5,12 @@ import { fetchFeedPage, FeedLoadError } from "@/lib/feed-client";
|
||||
import { applyLikeOutcome, LikeError, sendLike } from "@/lib/like-client";
|
||||
import { mergePosts, toCardPosts, type CardPost } from "@/lib/feed-model";
|
||||
import { PostError, publishAndConfirm } from "@/lib/post-client";
|
||||
import { DeleteError, sendDelete } from "@/lib/delete-client";
|
||||
import { FeedView, type FeedStatus } from "./FeedView";
|
||||
|
||||
const POLL_MS = 45_000;
|
||||
|
||||
export function Feed({ onSignedOut }: { onSignedOut: () => void }) {
|
||||
export function Feed({ onSignedOut, feedUserId }: { onSignedOut: () => void; feedUserId?: string | null }) {
|
||||
const [posts, setPosts] = useState<CardPost[]>([]);
|
||||
const [status, setStatus] = useState<FeedStatus>("loading");
|
||||
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
||||
@@ -18,6 +19,8 @@ export function Feed({ onSignedOut }: { onSignedOut: () => void }) {
|
||||
const [pending, setPending] = useState<CardPost[]>([]);
|
||||
const [likeBusy, setLikeBusy] = useState<ReadonlySet<string>>(new Set());
|
||||
const [likeErrors, setLikeErrors] = useState<ReadonlySet<string>>(new Set());
|
||||
const [deleteBusy, setDeleteBusy] = useState<ReadonlySet<string>>(new Set());
|
||||
const [deleteErrors, setDeleteErrors] = useState<ReadonlySet<string>>(new Set());
|
||||
const [draft, setDraft] = useState("");
|
||||
const [posting, setPosting] = useState(false);
|
||||
const [postFailed, setPostFailed] = useState(false);
|
||||
@@ -156,6 +159,32 @@ export function Feed({ onSignedOut }: { onSignedOut: () => void }) {
|
||||
[fail],
|
||||
);
|
||||
|
||||
const removePost = useCallback(
|
||||
async (post: CardPost) => {
|
||||
const mark = (set: ReadonlySet<string>, on: boolean) => {
|
||||
const next = new Set(set);
|
||||
if (on) next.add(post.key);
|
||||
else next.delete(post.key);
|
||||
return next;
|
||||
};
|
||||
setDeleteBusy((s) => mark(s, true));
|
||||
setDeleteErrors((s) => mark(s, false));
|
||||
try {
|
||||
// Removes the card only after the feed answered 200.
|
||||
if (!post.id) throw new DeleteError(false);
|
||||
await sendDelete(post.id);
|
||||
setPosts((cur) => cur.filter((p) => p.key !== post.key));
|
||||
setPending((cur) => cur.filter((p) => p.key !== post.key));
|
||||
} catch (e) {
|
||||
if (e instanceof DeleteError && e.signedOut) onSignedOut();
|
||||
setDeleteErrors((s) => mark(s, true));
|
||||
} finally {
|
||||
setDeleteBusy((s) => mark(s, false));
|
||||
}
|
||||
},
|
||||
[onSignedOut],
|
||||
);
|
||||
|
||||
const publish = useCallback(async () => {
|
||||
const text = draft.trim();
|
||||
if (!text || posting) return;
|
||||
@@ -199,6 +228,10 @@ export function Feed({ onSignedOut }: { onSignedOut: () => void }) {
|
||||
onToggleLike={(p) => void toggleLike(p)}
|
||||
likeBusy={likeBusy}
|
||||
likeErrors={likeErrors}
|
||||
feedUserId={feedUserId}
|
||||
onDelete={(p) => void removePost(p)}
|
||||
deleteBusy={deleteBusy}
|
||||
deleteErrors={deleteErrors}
|
||||
onSignedOut={onSignedOut}
|
||||
composer={{ value: draft, busy: posting, error: postFailed, onChange: changeDraft, onSubmit: () => void publish() }}
|
||||
sentinelRef={sentinel}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Ref } from "react";
|
||||
import type { CardPost } from "@/lib/feed-model";
|
||||
import { canDelete, type CardPost } from "@/lib/feed-model";
|
||||
import { Composer, type ComposerProps } from "./Composer";
|
||||
import { PostCard } from "./PostCard";
|
||||
|
||||
@@ -17,6 +17,11 @@ export interface FeedViewProps {
|
||||
likeBusy?: ReadonlySet<string>;
|
||||
likeErrors?: ReadonlySet<string>;
|
||||
onSignedOut?: () => void;
|
||||
/** The signed-in person's feed user id; delete is offered only on their own posts. */
|
||||
feedUserId?: string | null;
|
||||
onDelete?: (post: CardPost) => void;
|
||||
deleteBusy?: ReadonlySet<string>;
|
||||
deleteErrors?: ReadonlySet<string>;
|
||||
composer?: ComposerProps;
|
||||
sentinelRef?: Ref<HTMLDivElement>;
|
||||
now?: number;
|
||||
@@ -65,6 +70,10 @@ export function FeedView(p: FeedViewProps) {
|
||||
likeBusy={p.likeBusy?.has(post.key)}
|
||||
likeError={p.likeErrors?.has(post.key)}
|
||||
onSignedOut={p.onSignedOut}
|
||||
canDelete={canDelete(post, p.feedUserId)}
|
||||
onDelete={p.onDelete}
|
||||
deleteBusy={p.deleteBusy?.has(post.key)}
|
||||
deleteError={p.deleteErrors?.has(post.key)}
|
||||
/>)
|
||||
)}
|
||||
{p.loadMoreFailed ? (
|
||||
|
||||
@@ -12,6 +12,11 @@ export interface PostCardProps {
|
||||
likeBusy?: boolean;
|
||||
likeError?: boolean;
|
||||
onSignedOut?: () => void;
|
||||
/** True only on the signed-in person's own post. */
|
||||
canDelete?: boolean;
|
||||
onDelete?: (post: CardPost) => void;
|
||||
deleteBusy?: boolean;
|
||||
deleteError?: boolean;
|
||||
}
|
||||
|
||||
function Heart({ filled }: { filled: boolean }) {
|
||||
@@ -24,8 +29,10 @@ function Heart({ filled }: { filled: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function PostCard({ post, now, onToggleLike, likeBusy, likeError, onSignedOut }: PostCardProps) {
|
||||
export function PostCard({ post, now, onToggleLike, likeBusy, likeError, onSignedOut, canDelete, onDelete, deleteBusy, deleteError }: PostCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [menu, setMenu] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
return (
|
||||
<article className="post" data-testid="post-card">
|
||||
<header className="post-head">
|
||||
@@ -41,7 +48,43 @@ export function PostCard({ post, now, onToggleLike, likeBusy, likeError, onSigne
|
||||
<strong>{post.authorName}</strong>
|
||||
<time dateTime={post.publishedAt ?? undefined}>{relativeTime(post.publishedAt, now)}</time>
|
||||
</div>
|
||||
{canDelete && onDelete ? (
|
||||
<div className="post-menu">
|
||||
<button
|
||||
type="button"
|
||||
className="menu-btn"
|
||||
aria-label="More actions"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={menu}
|
||||
disabled={deleteBusy}
|
||||
onClick={() => { setMenu((m) => !m); setConfirming(false); }}
|
||||
>
|
||||
...
|
||||
</button>
|
||||
{menu ? (
|
||||
<div className="menu" role="menu">
|
||||
{confirming ? (
|
||||
<>
|
||||
<span className="menu-ask">Delete this post?</span>
|
||||
<button type="button" role="menuitem" className="menu-danger" disabled={deleteBusy}
|
||||
onClick={() => { setMenu(false); setConfirming(false); onDelete(post); }}>
|
||||
Confirm delete
|
||||
</button>
|
||||
<button type="button" role="menuitem" onClick={() => { setConfirming(false); setMenu(false); }}>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button type="button" role="menuitem" className="menu-danger" onClick={() => setConfirming(true)}>
|
||||
Delete post
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
{deleteError ? <p className="like-error" role="alert">Could not delete the post.</p> : null}
|
||||
{post.text ? <p className="post-text">{post.text}</p> : null}
|
||||
{post.media.map((m, i) =>
|
||||
m.kind === "video" && m.hlsUrl ? (
|
||||
|
||||
Reference in New Issue
Block a user