diff --git a/src/app.config.ts b/src/app.config.ts index 8893996..e461872 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -64,7 +64,7 @@ export const appConfig: AppConfig = { }, events: { name: 'events', - enabled: true, + enabled: false, lazy: false, config: { apiConfig: { diff --git a/src/modules/base/nostr/relay-hub.ts b/src/modules/base/nostr/relay-hub.ts index 486d3f2..7cdd00f 100644 --- a/src/modules/base/nostr/relay-hub.ts +++ b/src/modules/base/nostr/relay-hub.ts @@ -540,9 +540,13 @@ export class RelayHub extends BaseService { const successful = results.filter(result => result.status === 'fulfilled').length const total = results.length - this.emit('eventPublished', { eventId: event.id, success: successful, total }) + // Throw error if no relays accepted the event + if (successful === 0) { + throw new Error(`Failed to publish event - none of the ${total} relay(s) accepted it`) + } + return { success: successful, total } } diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue index 2d0aa4c..529b7d7 100644 --- a/src/modules/nostr-feed/components/NostrFeed.vue +++ b/src/modules/nostr-feed/components/NostrFeed.vue @@ -99,12 +99,22 @@ const { getDisplayName, fetchProfiles } = useProfiles() const { getEventReactions, subscribeToReactions, toggleLike } = useReactions() // Use scheduled events service -const { getEventsForSpecificDate, getCompletion, toggleComplete, allCompletions } = useScheduledEvents() +const { + getEventsForSpecificDate, + getCompletion, + getTaskStatus, + claimTask, + startTask, + completeEvent, + unclaimTask, + deleteTask, + allCompletions +} = useScheduledEvents() -// Selected date for viewing events (defaults to today) +// Selected date for viewing scheduled tasks (defaults to today) const selectedDate = ref(new Date().toISOString().split('T')[0]) -// Get scheduled events for the selected date (reactive) +// Get scheduled tasks for the selected date (reactive) const scheduledEventsForDate = computed(() => getEventsForSpecificDate(selectedDate.value)) // Navigate to previous day @@ -143,20 +153,20 @@ const dateDisplayText = computed(() => { const tomorrowStr = tomorrow.toISOString().split('T')[0] if (selectedDate.value === today) { - return "Today's Events" + return "Today's Tasks" } else if (selectedDate.value === yesterdayStr) { - return "Yesterday's Events" + return "Yesterday's Tasks" } else if (selectedDate.value === tomorrowStr) { - return "Tomorrow's Events" + return "Tomorrow's Tasks" } else { - // Format as "Events for Mon, Jan 15" + // Format as "Tasks for Mon, Jan 15" const date = new Date(selectedDate.value + 'T00:00:00') const formatted = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }) - return `Events for ${formatted}` + return `Tasks for ${formatted}` } }) @@ -255,14 +265,49 @@ async function onToggleLike(note: FeedPost) { } } -// Handle scheduled event completion toggle -async function onToggleComplete(event: ScheduledEvent, occurrence?: string) { - console.log('🎯 NostrFeed: onToggleComplete called for event:', event.title, 'occurrence:', occurrence) +// Task action handlers +async function onClaimTask(event: ScheduledEvent, occurrence?: string) { + console.log('👋 NostrFeed: Claiming task:', event.title) try { - await toggleComplete(event, occurrence) - console.log('✅ NostrFeed: toggleComplete succeeded') + await claimTask(event, '', occurrence) } catch (error) { - console.error('❌ NostrFeed: Failed to toggle event completion:', error) + console.error('❌ Failed to claim task:', error) + } +} + +async function onStartTask(event: ScheduledEvent, occurrence?: string) { + console.log('▶️ NostrFeed: Starting task:', event.title) + try { + await startTask(event, '', occurrence) + } catch (error) { + console.error('❌ Failed to start task:', error) + } +} + +async function onCompleteTask(event: ScheduledEvent, occurrence?: string) { + console.log('✅ NostrFeed: Completing task:', event.title) + try { + await completeEvent(event, occurrence, '') + } catch (error) { + console.error('❌ Failed to complete task:', error) + } +} + +async function onUnclaimTask(event: ScheduledEvent, occurrence?: string) { + console.log('🔙 NostrFeed: Unclaiming task:', event.title) + try { + await unclaimTask(event, occurrence) + } catch (error) { + console.error('❌ Failed to unclaim task:', error) + } +} + +async function onDeleteTask(event: ScheduledEvent) { + console.log('🗑️ NostrFeed: Deleting task:', event.title) + try { + await deleteTask(event) + } catch (error) { + console.error('❌ Failed to delete task:', error) } } @@ -466,7 +511,7 @@ function cancelDelete() {
- +
@@ -506,7 +551,7 @@ function cancelDelete() {
- +
diff --git a/src/modules/nostr-feed/components/ScheduledEventCard.vue b/src/modules/nostr-feed/components/ScheduledEventCard.vue index dfc48af..46c188e 100644 --- a/src/modules/nostr-feed/components/ScheduledEventCard.vue +++ b/src/modules/nostr-feed/components/ScheduledEventCard.vue @@ -2,6 +2,7 @@ import { computed, ref } from 'vue' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, @@ -15,18 +16,25 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' -import { Calendar, MapPin, Clock, CheckCircle } from 'lucide-vue-next' -import type { ScheduledEvent, EventCompletion } from '../services/ScheduledEventService' +import { Calendar, MapPin, Clock, CheckCircle, PlayCircle, Hand, Trash2 } from 'lucide-vue-next' +import type { ScheduledEvent, EventCompletion, TaskStatus } from '../services/ScheduledEventService' +import { injectService, SERVICE_TOKENS } from '@/core/di-container' +import type { AuthService } from '@/modules/base/auth/auth-service' interface Props { event: ScheduledEvent getDisplayName: (pubkey: string) => string getCompletion: (eventAddress: string, occurrence?: string) => EventCompletion | undefined + getTaskStatus: (eventAddress: string, occurrence?: string) => TaskStatus | null adminPubkeys?: string[] } interface Emits { - (e: 'toggle-complete', event: ScheduledEvent, occurrence?: string): void + (e: 'claim-task', event: ScheduledEvent, occurrence?: string): void + (e: 'start-task', event: ScheduledEvent, occurrence?: string): void + (e: 'complete-task', event: ScheduledEvent, occurrence?: string): void + (e: 'unclaim-task', event: ScheduledEvent, occurrence?: string): void + (e: 'delete-task', event: ScheduledEvent): void } const props = withDefaults(defineProps(), { @@ -35,8 +43,12 @@ const props = withDefaults(defineProps(), { const emit = defineEmits() +// Get auth service to check current user +const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) + // Confirmation dialog state const showConfirmDialog = ref(false) +const hasConfirmedCommunication = ref(false) // Event address for tracking completion const eventAddress = computed(() => `31922:${props.event.pubkey}:${props.event.dTag}`) @@ -53,12 +65,46 @@ const occurrence = computed(() => { // Check if this is an admin event const isAdminEvent = computed(() => props.adminPubkeys.includes(props.event.pubkey)) -// Check if event is completed - call function with occurrence for recurring events -const isCompleted = computed(() => props.getCompletion(eventAddress.value, occurrence.value)?.completed || false) +// Get current task status +const taskStatus = computed(() => props.getTaskStatus(eventAddress.value, occurrence.value)) // Check if event is completable (task type) const isCompletable = computed(() => props.event.eventType === 'task') +// Get completion data +const completion = computed(() => props.getCompletion(eventAddress.value, occurrence.value)) + +// Get current user's pubkey +const currentUserPubkey = computed(() => authService?.user.value?.pubkey) + +// Check if current user can unclaim +// Only show unclaim for "claimed" state, and only if current user is the one who claimed it +const canUnclaim = computed(() => { + if (!completion.value || !currentUserPubkey.value) return false + if (taskStatus.value !== 'claimed') return false + return completion.value.pubkey === currentUserPubkey.value +}) + +// Check if current user is the author of the task +const isAuthor = computed(() => { + if (!currentUserPubkey.value) return false + return props.event.pubkey === currentUserPubkey.value +}) + +// Status badges configuration +const statusConfig = computed(() => { + switch (taskStatus.value) { + case 'claimed': + return { label: 'Claimed', variant: 'secondary' as const, icon: Hand, color: 'text-blue-600' } + case 'in-progress': + return { label: 'In Progress', variant: 'default' as const, icon: PlayCircle, color: 'text-orange-600' } + case 'completed': + return { label: 'Completed', variant: 'secondary' as const, icon: CheckCircle, color: 'text-green-600' } + default: + return null + } +}) + // Format the date/time const formattedDate = computed(() => { try { @@ -110,28 +156,124 @@ const formattedTimeRange = computed(() => { } }) -// Handle mark complete button click - show confirmation dialog -function handleMarkComplete() { - console.log('🔘 Mark Complete button clicked for event:', props.event.title) +// Action type for confirmation dialog +const pendingAction = ref<'claim' | 'start' | 'complete' | 'unclaim' | 'delete' | null>(null) + +// Handle claim task +function handleClaimTask() { + pendingAction.value = 'claim' showConfirmDialog.value = true } -// Confirm and execute mark complete -function confirmMarkComplete() { - console.log('✅ Confirmed mark complete for event:', props.event.title, 'occurrence:', occurrence.value) - emit('toggle-complete', props.event, occurrence.value) - showConfirmDialog.value = false +// Handle start task +function handleStartTask() { + pendingAction.value = 'start' + showConfirmDialog.value = true } -// Cancel mark complete -function cancelMarkComplete() { - showConfirmDialog.value = false +// Handle complete task +function handleCompleteTask() { + pendingAction.value = 'complete' + showConfirmDialog.value = true } + +// Handle unclaim task +function handleUnclaimTask() { + pendingAction.value = 'unclaim' + showConfirmDialog.value = true +} + +// Handle delete task +function handleDeleteTask() { + pendingAction.value = 'delete' + showConfirmDialog.value = true +} + +// Confirm action +function confirmAction() { + if (!pendingAction.value) return + + // For unclaim action, require checkbox confirmation + if (pendingAction.value === 'unclaim' && !hasConfirmedCommunication.value) { + return + } + + switch (pendingAction.value) { + case 'claim': + emit('claim-task', props.event, occurrence.value) + break + case 'start': + emit('start-task', props.event, occurrence.value) + break + case 'complete': + emit('complete-task', props.event, occurrence.value) + break + case 'unclaim': + emit('unclaim-task', props.event, occurrence.value) + break + case 'delete': + emit('delete-task', props.event) + break + } + + showConfirmDialog.value = false + pendingAction.value = null + hasConfirmedCommunication.value = false +} + +// Cancel action +function cancelAction() { + showConfirmDialog.value = false + pendingAction.value = null + hasConfirmedCommunication.value = false +} + +// Get dialog content based on pending action +const dialogContent = computed(() => { + switch (pendingAction.value) { + case 'claim': + return { + title: 'Claim Task?', + description: `This will mark "${props.event.title}" as claimed by you. You can start working on it later.`, + confirmText: 'Claim Task' + } + case 'start': + return { + title: 'Start Task?', + description: `This will mark "${props.event.title}" as in-progress. Others will see you're actively working on it.`, + confirmText: 'Start Task' + } + case 'complete': + return { + title: 'Complete Task?', + description: `This will mark "${props.event.title}" as completed by you. Other users will be able to see that you completed this task.`, + confirmText: 'Mark Complete' + } + case 'unclaim': + return { + title: 'Unclaim Task?', + description: `This will remove your claim on "${props.event.title}" and make it available for others.\n\nHave you communicated to others that you are unclaiming this task?`, + confirmText: 'Unclaim Task' + } + case 'delete': + return { + title: 'Delete Task?', + description: `This will permanently delete "${props.event.title}". This action cannot be undone.`, + confirmText: 'Delete Task' + } + default: + return { + title: '', + description: '', + confirmText: '' + } + } +})