import React, { useState, useContext } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { destroyTweet, updateTweet, likeTweet, unlikeTweet, buildCSRFHeaders } from "../ash_rpc"; import { AuthCtx } from "../context"; import { timeAgo, userDisplayLabel } from "../utils"; import { Avatar, ContextMenu } from "./ui"; import { TweetMedia } from "./media"; import type { Tweet, ContextMenuItem } from "../types"; export function CommentIcon() { return ( ); } export function TweetCard({ tweet }: { tweet: Tweet }) { const { userId: currentUserId } = useContext(AuthCtx); const canModify = !!currentUserId && tweet.userId === currentUserId; const canLike = !!currentUserId; const [editing, setEditing] = useState(false); const [editText, setEditText] = useState(tweet.content); const [error, setError] = useState(null); const [confirmDelete, setConfirmDelete] = useState(false); const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null); const qc = useQueryClient(); const tweetUrl = `${window.location.origin}/feed/${tweet.id}`; const ctxItems: ContextMenuItem[] = canModify ? [ { type: "item", label: "Edit", onClick: () => { setEditText(tweet.content); setEditing(true); setConfirmDelete(false); }, }, { type: "separator" }, { type: "item", label: "Share", onClick: () => navigator.clipboard.writeText(tweetUrl), }, ] : [ { type: "item", label: "View", onClick: () => { window.location.href = tweetUrl; }, }, { type: "separator" }, { type: "item", label: "Share", onClick: () => navigator.clipboard.writeText(tweetUrl), }, ]; const deleteMutation = useMutation({ mutationFn: async () => { const res = await destroyTweet({ identity: tweet.id, headers: buildCSRFHeaders() }); if (!res.success) throw new Error(res.errors?.[0]?.message ?? "Failed to delete"); }, onSuccess: () => { qc.invalidateQueries({ queryKey: ["tweets"] }); qc.invalidateQueries({ queryKey: ["following_tweets"] }); }, onError: (e: Error) => setError(e.message), }); const updateMutation = useMutation({ mutationFn: async (content: string) => { const res = await updateTweet({ identity: tweet.id, input: { content }, fields: ["id", "content", "userId", "state"], headers: buildCSRFHeaders(), }); if (!res.success) throw new Error(res.errors?.[0]?.message ?? "Failed to update"); }, onSuccess: () => { qc.invalidateQueries({ queryKey: ["tweets"] }); qc.invalidateQueries({ queryKey: ["following_tweets"] }); setEditing(false); setError(null); }, onError: (e: Error) => setError(e.message), }); const likeMutation = useMutation({ mutationFn: async () => { const action = tweet.likedByMe ? unlikeTweet : likeTweet; const res = await action({ identity: tweet.id, fields: ["id", "likes", "likedByMe"], headers: buildCSRFHeaders(), }); if (!res.success) throw new Error(res.errors?.[0]?.message ?? "Failed to update like"); }, onSuccess: () => { qc.invalidateQueries({ queryKey: ["tweets"] }); qc.invalidateQueries({ queryKey: ["following_tweets"] }); setError(null); }, onError: (e: Error) => setError(e.message), }); function saveEdit() { const trimmed = editText.trim(); if (!trimmed) return; updateMutation.mutate(trimmed); } return (
{ window.location.href = `/feed/${tweet.id}`; }} onContextMenu={(e) => { e.preventDefault(); setCtxMenu({ x: e.clientX, y: e.clientY }); }} >
{userDisplayLabel({ displayName: tweet.userDisplayName, username: tweet.userUsername, email: tweet.userEmail })} {tweet.userUsername && ( @{tweet.userUsername} )} · {timeAgo(tweet.insertedAt)} {canModify && (
)}
{editing ? (