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
10 changes: 5 additions & 5 deletions src/components/layout/my-content-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,11 @@ interface ContentResponse {
}

export function MyContentPanel({ onClose }: { onClose: () => void }) {
const { pubKey, routeHint, isAdmin } = useUserStore()
const { pubKey, isAdmin } = useUserStore()
const schemas = useSchemaStore((s) => s.schemas)
const openModal = useModalStore((s) => s.open)
const setHoveredNode = useGraphStore((s) => s.setHoveredNode)
const setSidebarSelectedNode = useGraphStore((s) => s.setSidebarSelectedNode)
const userFullPubkey = pubKey && routeHint ? `${pubKey}_${routeHint}` : pubKey
const mocksEnabled = isMocksEnabled()
const [nodes, setNodes] = useState<GraphNode[]>([])
const [totalProcessing, setTotalProcessing] = useState(0)
Expand Down Expand Up @@ -194,7 +193,8 @@ export function MyContentPanel({ onClose }: { onClose: () => void }) {
) : (
<div className="py-1">
{nodes.map((node, i) => {
const canDelete = isAdmin || node.properties?.pubkey === userFullPubkey
// /v2/content is server-filtered to the caller's content, so every node
// here is the user's — always deletable, never self-boostable.
const isConfirming = deletingId === node.ref_id

const handleConfirmDelete = async () => {
Expand All @@ -212,10 +212,10 @@ export function MyContentPanel({ onClose }: { onClose: () => void }) {
onClick={() => { setSelectedNode(node); setSidebarSelectedNode(node) }}
onMouseEnter={() => setHoveredNode(node)}
onMouseLeave={() => setHoveredNode(null)}
hideBoost={isAdmin || node.properties?.pubkey === userFullPubkey}
hideBoost={true}
isAdmin={isAdmin}
/>
{canDelete && !isConfirming && (
{!isConfirming && (
<button
className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-destructive"
onClick={(e) => { e.stopPropagation(); setDeletingId(node.ref_id) }}
Expand Down
69 changes: 28 additions & 41 deletions src/lib/__tests__/my-content-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ describe("MyContentPanel", () => {
expect(screen.queryByText(/still processing/i)).not.toBeInTheDocument()
})

it("renders read-only boost amount when node has boost property", async () => {
it("never renders boost UI in MyContent (every node here is the user's own content)", async () => {
mockApiGet.mockResolvedValue({
nodes: [
{
Expand All @@ -128,9 +128,9 @@ describe("MyContentPanel", () => {
})
render(<MyContentPanel onClose={() => {}} />)
await waitFor(() => {
expect(screen.getByText("150")).toBeInTheDocument()
expect(screen.getByText("sats")).toBeInTheDocument()
expect(screen.getByText("Bitcoin is freedom")).toBeInTheDocument()
})
expect(screen.queryByText("sats")).not.toBeInTheDocument()
})

it("renders no boost display when boost is absent", async () => {
Expand Down Expand Up @@ -164,19 +164,23 @@ describe("MyContentPanel", () => {
)
})

it("hides boost sats display when node pubkey matches user pubKey (contributor)", async () => {
it("hides boost sats display when node has owner_reference_id (contributor)", async () => {
mockApiGet.mockResolvedValue({
nodes: [
{
node_type: "Tweet",
ref_id: "ref-1",
properties: { name: "Bitcoin is freedom", status: "complete", boost: 150, pubkey: "03abc123testkey" },
properties: {
name: "Bitcoin is freedom",
status: "complete",
boost: 150,
owner_reference_id: "lsat:11111111-1111-1111-1111-111111111111",
},
},
],
totalCount: 1,
totalProcessing: 0,
})
// pubKey matches node pubkey, no routeHint
myContentUserOverrides = { pubKey: "03abc123testkey", routeHint: "", isAdmin: false }
render(<MyContentPanel onClose={() => {}} />)
await waitFor(() => {
Expand All @@ -191,7 +195,12 @@ describe("MyContentPanel", () => {
{
node_type: "Tweet",
ref_id: "ref-1",
properties: { name: "Bitcoin is freedom", status: "complete", boost: 200, pubkey: "03someoneelse" },
properties: {
name: "Bitcoin is freedom",
status: "complete",
boost: 200,
owner_reference_id: "lsat:22222222-2222-2222-2222-222222222222",
},
},
],
totalCount: 1,
Expand Down Expand Up @@ -308,57 +317,35 @@ describe("MyContentPanel – delete button", () => {
mockDeleteNode.mockResolvedValue({})
})

const NODE_WITHOUT_UID = {
// /v2/content is server-filtered to the caller's own content, so every node here
// is implicitly owned — delete always renders, no client-side ownership check needed.
const NODE = {
nodes: [
{
node_type: "Tweet",
ref_id: "ref-orphan",
properties: { name: "Orphan Node", status: "error", pubkey: "03abc123testkey" },
},
],
totalCount: 1,
totalProcessing: 0,
}

const NODE_OTHER_PUBKEY = {
nodes: [
{
node_type: "Tweet",
ref_id: "ref-other",
properties: { name: "Someone Else Node", status: "complete", pubkey: "03someoneelse" },
properties: {
name: "Orphan Node",
status: "error",
owner_reference_id: "lsat:11111111-1111-1111-1111-111111111111",
},
},
],
totalCount: 1,
totalProcessing: 0,
}

it("trash button renders when isAdmin = true", async () => {
myContentUserOverrides = { pubKey: "03abc123testkey", routeHint: "", isAdmin: true }
mockApiGet.mockResolvedValue(NODE_OTHER_PUBKEY)
render(<MyContentPanel onClose={() => {}} />)
await waitFor(() => expect(screen.getByText("Someone Else Node")).toBeInTheDocument())
expect(screen.getByRole("button", { name: /delete node/i })).toBeInTheDocument()
})

it("trash button renders when node pubkey matches user pubKey", async () => {
it("trash button always renders for /v2/content nodes", async () => {
myContentUserOverrides = { pubKey: "03abc123testkey", routeHint: "", isAdmin: false }
mockApiGet.mockResolvedValue(NODE_WITHOUT_UID)
mockApiGet.mockResolvedValue(NODE)
render(<MyContentPanel onClose={() => {}} />)
await waitFor(() => expect(screen.getByText("Orphan Node")).toBeInTheDocument())
expect(screen.getByRole("button", { name: /delete node/i })).toBeInTheDocument()
})

it("trash button is absent when neither owner nor admin", async () => {
myContentUserOverrides = { pubKey: "03abc123testkey", routeHint: "", isAdmin: false }
mockApiGet.mockResolvedValue(NODE_OTHER_PUBKEY)
render(<MyContentPanel onClose={() => {}} />)
await waitFor(() => expect(screen.getByText("Someone Else Node")).toBeInTheDocument())
expect(screen.queryByRole("button", { name: /delete node/i })).toBeNull()
})

it("clicking trash shows inline confirmation; Cancel hides it with no API call", async () => {
myContentUserOverrides = { pubKey: "03abc123testkey", routeHint: "", isAdmin: false }
mockApiGet.mockResolvedValue(NODE_WITHOUT_UID)
mockApiGet.mockResolvedValue(NODE)
render(<MyContentPanel onClose={() => {}} />)
await waitFor(() => expect(screen.getByText("Orphan Node")).toBeInTheDocument())

Expand All @@ -373,7 +360,7 @@ describe("MyContentPanel – delete button", () => {

it("confirm calls deleteNode(ref_id) and removes only that node", async () => {
myContentUserOverrides = { pubKey: "03abc123testkey", routeHint: "", isAdmin: false }
mockApiGet.mockResolvedValue(NODE_WITHOUT_UID)
mockApiGet.mockResolvedValue(NODE)
render(<MyContentPanel onClose={() => {}} />)
await waitFor(() => expect(screen.getByText("Orphan Node")).toBeInTheDocument())

Expand Down
Loading