webapp/src/modules/nostr-feed/composables/useScheduledEvents.ts
padreug 3b8c82514a Add delete task functionality for task authors
Added ability for task authors to delete their own tasks from the expanded
view in the task feed.

**Features:**
- Delete button visible only to task author in expanded task view
- Confirmation dialog with destructive styling
- Publishes NIP-09 deletion event (kind 5) with 'a' tag referencing the
  task's event address (kind:pubkey:d-tag format)
- Real-time deletion handling via FeedService routing
- Optimistic local state update for immediate UI feedback

**Implementation:**
- Added deleteTask() method to ScheduledEventService
- Added handleTaskDeletion() for processing incoming deletion events
- Updated FeedService to route kind 31922 deletions to ScheduledEventService
- Added delete button and dialog flow to ScheduledEventCard component
- Integrated with existing confirmation dialog pattern

**Permissions:**
- Only task authors can delete tasks (enforced by isAuthor check)
- NIP-09 validation: relays only accept deletion from event author
- Pubkey verification in handleTaskDeletion()

**Testing:**
- Created tasks and verified delete button appears for author only
- Confirmed deletion removes task from UI immediately
- Verified deletion persists after refresh
- Tested with multiple users - others cannot delete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 22:39:38 +01:00

261 lines
8.1 KiB
TypeScript

import { computed } from 'vue'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { ScheduledEventService, ScheduledEvent, EventCompletion, TaskStatus } from '../services/ScheduledEventService'
import type { AuthService } from '@/modules/base/auth/auth-service'
import { useToast } from '@/core/composables/useToast'
/**
* Composable for managing scheduled events in the feed
*/
export function useScheduledEvents() {
const scheduledEventService = injectService<ScheduledEventService>(SERVICE_TOKENS.SCHEDULED_EVENT_SERVICE)
const authService = injectService<AuthService>(SERVICE_TOKENS.AUTH_SERVICE)
const toast = useToast()
// Get current user's pubkey
const currentUserPubkey = computed(() => authService?.user.value?.pubkey)
/**
* Get all scheduled events
*/
const getScheduledEvents = (): ScheduledEvent[] => {
if (!scheduledEventService) return []
return scheduledEventService.getScheduledEvents()
}
/**
* Get events for a specific date (YYYY-MM-DD)
*/
const getEventsForDate = (date: string): ScheduledEvent[] => {
if (!scheduledEventService) return []
return scheduledEventService.getEventsForDate(date)
}
/**
* Get events for a specific date (filtered by current user participation)
* @param date - ISO date string (YYYY-MM-DD). Defaults to today.
*/
const getEventsForSpecificDate = (date?: string): ScheduledEvent[] => {
if (!scheduledEventService) return []
return scheduledEventService.getEventsForSpecificDate(date, currentUserPubkey.value)
}
/**
* Get today's scheduled events (filtered by current user participation)
*/
const getTodaysEvents = (): ScheduledEvent[] => {
if (!scheduledEventService) return []
return scheduledEventService.getTodaysEvents(currentUserPubkey.value)
}
/**
* Get completion status for an event
*/
const getCompletion = (eventAddress: string): EventCompletion | undefined => {
if (!scheduledEventService) return undefined
return scheduledEventService.getCompletion(eventAddress)
}
/**
* Check if an event is completed
*/
const isCompleted = (eventAddress: string): boolean => {
if (!scheduledEventService) return false
return scheduledEventService.isCompleted(eventAddress)
}
/**
* Get task status for an event
*/
const getTaskStatus = (eventAddress: string, occurrence?: string): TaskStatus | null => {
if (!scheduledEventService) return null
return scheduledEventService.getTaskStatus(eventAddress, occurrence)
}
/**
* Claim a task
*/
const claimTask = async (event: ScheduledEvent, notes: string = '', occurrence?: string): Promise<void> => {
if (!scheduledEventService) {
toast.error('Scheduled event service not available')
return
}
try {
await scheduledEventService.claimTask(event, notes, occurrence)
toast.success('Task claimed!')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to claim task'
if (message.includes('authenticated')) {
toast.error('Please sign in to claim tasks')
} else {
toast.error(message)
}
console.error('Failed to claim task:', error)
}
}
/**
* Start a task (mark as in-progress)
*/
const startTask = async (event: ScheduledEvent, notes: string = '', occurrence?: string): Promise<void> => {
if (!scheduledEventService) {
toast.error('Scheduled event service not available')
return
}
try {
await scheduledEventService.startTask(event, notes, occurrence)
toast.success('Task started!')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to start task'
toast.error(message)
console.error('Failed to start task:', error)
}
}
/**
* Unclaim a task (remove task status)
*/
const unclaimTask = async (event: ScheduledEvent, occurrence?: string): Promise<void> => {
if (!scheduledEventService) {
toast.error('Scheduled event service not available')
return
}
try {
await scheduledEventService.unclaimTask(event, occurrence)
toast.success('Task unclaimed')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to unclaim task'
toast.error(message)
console.error('Failed to unclaim task:', error)
}
}
/**
* Toggle completion status of an event (optionally for a specific occurrence)
* DEPRECATED: Use claimTask, startTask, completeEvent, or unclaimTask instead for more granular control
*/
const toggleComplete = async (event: ScheduledEvent, occurrence?: string, notes: string = ''): Promise<void> => {
console.log('🔧 useScheduledEvents: toggleComplete called for event:', event.title, 'occurrence:', occurrence)
if (!scheduledEventService) {
console.error('❌ useScheduledEvents: Scheduled event service not available')
toast.error('Scheduled event service not available')
return
}
try {
const eventAddress = `31922:${event.pubkey}:${event.dTag}`
const currentlyCompleted = scheduledEventService.isCompleted(eventAddress, occurrence)
console.log('📊 useScheduledEvents: Current completion status:', currentlyCompleted)
if (currentlyCompleted) {
console.log('⬇️ useScheduledEvents: Unclaiming task...')
await scheduledEventService.unclaimTask(event, occurrence)
toast.success('Task unclaimed')
} else {
console.log('⬆️ useScheduledEvents: Marking as complete...')
await scheduledEventService.completeEvent(event, notes, occurrence)
toast.success('Task completed!')
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to toggle completion'
if (message.includes('authenticated')) {
toast.error('Please sign in to complete tasks')
} else if (message.includes('Not connected')) {
toast.error('Not connected to relays')
} else {
toast.error(message)
}
console.error('❌ useScheduledEvents: Failed to toggle completion:', error)
}
}
/**
* Complete an event with optional notes
*/
const completeEvent = async (event: ScheduledEvent, occurrence?: string, notes: string = ''): Promise<void> => {
if (!scheduledEventService) {
toast.error('Scheduled event service not available')
return
}
try {
await scheduledEventService.completeEvent(event, notes, occurrence)
toast.success('Task completed!')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to complete task'
toast.error(message)
console.error('Failed to complete task:', error)
}
}
/**
* Get loading state
*/
const isLoading = computed(() => {
return scheduledEventService?.isLoading ?? false
})
/**
* Get all scheduled events (reactive)
*/
const allScheduledEvents = computed(() => {
return scheduledEventService?.scheduledEvents ?? new Map()
})
/**
* Delete a task (only author can delete)
*/
const deleteTask = async (event: ScheduledEvent): Promise<void> => {
if (!scheduledEventService) {
toast.error('Scheduled event service not available')
return
}
try {
await scheduledEventService.deleteTask(event)
toast.success('Task deleted!')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to delete task'
toast.error(message)
console.error('Failed to delete task:', error)
}
}
/**
* Get all completions (reactive) - returns array for better reactivity
*/
const allCompletions = computed(() => {
if (!scheduledEventService?.completions) return []
return Array.from(scheduledEventService.completions.values())
})
return {
// Methods - Getters
getScheduledEvents,
getEventsForDate,
getEventsForSpecificDate,
getTodaysEvents,
getCompletion,
isCompleted,
getTaskStatus,
// Methods - Actions
claimTask,
startTask,
completeEvent,
unclaimTask,
deleteTask,
toggleComplete, // DEPRECATED: Use specific actions instead
// State
isLoading,
allScheduledEvents,
allCompletions
}
}