feat: soft-delete particles within a stream (#149)
Lets a particle's creator delete their own message from the TopBar dropdown. Other viewers see a "This particle was deleted" tombstone in place and playback auto-advances after ~2s, keeping indices stable for concurrent watchers. - Add optional deleted_at / deleted_by_human_id to non-container particle variants and isParticleDeleted helper. - Add softDeleteParticle Firestore helper. - New DeleteParticleOverlay confirmation and DeletedParticleView tombstone. - Hide reactions (bar + 1-7 keybinding) on tombstoned particles. - Show "Deleted particle" + Trash2 icon in the stream list preview. Closes #146 https://claude.ai/code/session_01M2ShnZPvWfQzzvuu3Xm8b9 Co-authored-by: Claude <[email protected]>
This commit was merged in pull request #149.
This commit is contained in:
@@ -2,13 +2,14 @@ import { useState, useEffect, useEffectEvent, useCallback, useRef } from "react"
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { apiClient } from "@/api/client";
|
||||
import { type Particle, REACTION_EMOJIS } from "@/api/types";
|
||||
import { isParticleDeleted, type Particle, REACTION_EMOJIS } from "@/api/types";
|
||||
import { parseParticlePath, particlePath, toFirestoreDocPath, type ParticlePath } from "@/lib/particle-path";
|
||||
import { ComposeOverlay, type ComposeStep } from "@/features/compose/compose-overlay";
|
||||
import { PlaybackPageIndicator } from "@/features/particles/playback-page-indicator";
|
||||
import { MediaParticleView, type MediaParticleHandle } from "@/features/particles/media-particle-view";
|
||||
import { TextParticleView } from "@/features/particles/text-particle-view";
|
||||
import { FallbackParticleView } from "@/features/particles/fallback-particle-view";
|
||||
import { DeletedParticleView } from "@/features/particles/deleted-particle-view";
|
||||
import { VideoAudioToggle } from "@/components/video-audio-toggle";
|
||||
import { useMediaSettingsStore } from "@/stores/media-settings-store";
|
||||
import { KeybindingsOverlay, type KeybindingGroup } from "@/components/keybindings-overlay";
|
||||
@@ -25,6 +26,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useMount } from "react-use";
|
||||
|
||||
function getReactions(particle: Particle): Record<string, string[]> | undefined {
|
||||
if (isParticleDeleted(particle)) return undefined;
|
||||
if (particle.type === "media" || particle.type === "text") return particle.reactions;
|
||||
return undefined;
|
||||
}
|
||||
@@ -176,6 +178,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
|
||||
const handleToggleReaction = useCallback((emoji: string) => {
|
||||
if (!authedUser || !currentParticle) return;
|
||||
if (isParticleDeleted(currentParticle)) return;
|
||||
|
||||
|
||||
const currentParticleDocPath = currentParticle
|
||||
@@ -370,6 +373,17 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
|
||||
// Render particle content inline
|
||||
function renderParticle(particle: Particle) {
|
||||
if (isParticleDeleted(particle)) {
|
||||
return (
|
||||
<DeletedParticleView
|
||||
key={particle.id}
|
||||
particle={particle}
|
||||
networkId={networkId}
|
||||
paused={paused}
|
||||
onEnded={next}
|
||||
/>
|
||||
);
|
||||
}
|
||||
switch (particle.type) {
|
||||
case "media":
|
||||
return (
|
||||
@@ -432,7 +446,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
</div>
|
||||
|
||||
{/* Reaction bar — always visible */}
|
||||
{currentParticle && (
|
||||
{currentParticle && !isParticleDeleted(currentParticle) && (
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 z-10">
|
||||
<ReactionBar
|
||||
reactions={getReactions(currentParticle)}
|
||||
|
||||
Reference in New Issue
Block a user