Update package dependencies and enhance application structure

- Added several new dependencies related to the Applesauce library, including 'applesauce-accounts', 'applesauce-common', 'applesauce-core', 'applesauce-loaders', 'applesauce-relay', and 'applesauce-signers', all at version 5.1.0.
- Updated the development script in package.json to specify a port for Vite and added new seed scripts for profiles and activity.
- Removed outdated image files from the public directory to clean up unused assets.
- Enhanced the App.vue structure by integrating shared components like AppHeader and AuthModal for improved user experience.
- Refactored ContentDetailModal and MobileNav components to support new features and improve usability.

These changes improve the overall functionality and maintainability of the application while ensuring it utilizes the latest libraries for better performance.
This commit is contained in:
Dorian
2026-02-12 12:24:58 +00:00
parent c970f5b29f
commit 725896673c
42 changed files with 3767 additions and 1329 deletions

View File

@@ -56,19 +56,31 @@
</button>
<!-- Like Button -->
<button @click="handleLike" class="action-btn" :class="{ 'action-btn-active': userReaction === '+' }">
<button
@click="handleLike"
:disabled="hasVoted || !isNostrLoggedIn"
class="action-btn"
:class="{ 'action-btn-active': userReaction === '+' }"
:style="{ opacity: userReaction === '+' ? 1 : hasVoted ? 0.4 : 1 }"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5" />
</svg>
<span v-if="reactionCounts.positive > 0" class="text-xs">{{ reactionCounts.positive }}</span>
<span class="text-xs">{{ reactionCounts.positive }}</span>
</button>
<!-- Dislike Button -->
<button @click="handleDislike" class="action-btn" :class="{ 'action-btn-active': userReaction === '-' }">
<button
@click="handleDislike"
:disabled="hasVoted || !isNostrLoggedIn"
class="action-btn"
:class="{ 'action-btn-active': userReaction === '-' }"
:style="{ opacity: userReaction === '-' ? 1 : hasVoted ? 0.4 : 1 }"
>
<svg class="w-5 h-5 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5" />
</svg>
<span v-if="reactionCounts.negative > 0" class="text-xs">{{ reactionCounts.negative }}</span>
<span class="text-xs">{{ reactionCounts.negative }}</span>
</button>
<!-- Share Button -->
@@ -113,16 +125,18 @@
<div class="comments-section">
<h3 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
Comments
<span class="text-sm font-normal text-white/50">({{ comments.length }})</span>
<span v-if="isDev" class="text-xs bg-white/10 text-white/40 px-2 py-0.5 rounded-full ml-auto">Demo Mode</span>
<span class="text-sm font-normal text-white/50">({{ commentCount }})</span>
<span v-if="!relayConnected" class="text-xs bg-orange-500/20 text-orange-300/60 px-2 py-0.5 rounded-full ml-auto">Relay Offline</span>
</h3>
<!-- Comment Input -->
<div v-if="isAuthenticated" class="comment-input-wrap mb-6">
<div v-if="isNostrLoggedIn" class="comment-input-wrap mb-6">
<div class="flex gap-3">
<div class="profile-avatar flex-shrink-0">
<span>{{ userInitials }}</span>
</div>
<img
:src="currentUserAvatar"
class="w-8 h-8 rounded-full flex-shrink-0 object-cover"
alt="You"
/>
<div class="flex-1">
<textarea
v-model="newComment"
@@ -135,11 +149,11 @@
<div class="flex justify-end mt-2">
<button
@click="submitComment"
:disabled="!newComment.trim()"
:disabled="!newComment.trim() || isPostingComment"
class="submit-comment-btn"
:class="{ 'opacity-40 cursor-not-allowed': !newComment.trim() }"
:class="{ 'opacity-40 cursor-not-allowed': !newComment.trim() || isPostingComment }"
>
Post
{{ isPostingComment ? 'Posting...' : 'Post' }}
</button>
</div>
</div>
@@ -159,41 +173,22 @@
<div class="text-white/40 text-sm">Loading comments...</div>
</div>
<div v-else-if="comments.length === 0" class="text-center py-8">
<div v-else-if="commentTree.length === 0" class="text-center py-8">
<div class="text-white/40 text-sm">No comments yet. Be the first!</div>
</div>
<div v-else class="space-y-4">
<div
v-for="comment in comments"
:key="comment.id"
class="comment-item"
>
<div class="flex gap-3">
<!-- Author Avatar -->
<img
v-if="getProfile(comment.pubkey)?.picture"
:src="getProfile(comment.pubkey).picture"
:alt="getProfile(comment.pubkey)?.name || 'User'"
class="w-8 h-8 rounded-full flex-shrink-0 object-cover"
/>
<div v-else class="w-8 h-8 rounded-full flex-shrink-0 bg-gradient-to-br from-orange-500 to-pink-500 flex items-center justify-center text-xs font-bold text-white">
{{ (getProfile(comment.pubkey)?.name || 'A')[0].toUpperCase() }}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="text-white text-sm font-medium truncate">
{{ getProfile(comment.pubkey)?.name || 'Anonymous' }}
</span>
<span class="text-white/30 text-xs flex-shrink-0">
{{ formatTimeAgo(comment.created_at) }}
</span>
</div>
<p class="text-white/70 text-sm leading-relaxed">{{ comment.content }}</p>
</div>
</div>
</div>
<!-- Threaded comments -->
<div v-else class="space-y-1">
<template v-for="node in commentTree" :key="node.event.id">
<CommentNode
:node="node"
:depth="0"
:nostr="nostr"
:is-logged-in="isNostrLoggedIn"
:current-user-avatar="currentUserAvatar"
@get-profile="getProfile"
/>
</template>
</div>
</div>
</div>
@@ -227,11 +222,13 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { useAuth } from '../composables/useAuth'
import { useAccounts } from '../composables/useAccounts'
import { useNostr } from '../composables/useNostr'
import type { Content } from '../types/content'
import VideoPlayer from './VideoPlayer.vue'
import SubscriptionModal from './SubscriptionModal.vue'
import RentalModal from './RentalModal.vue'
import CommentNode from './CommentNode.vue'
interface Props {
isOpen: boolean
@@ -246,51 +243,52 @@ interface Emits {
const props = defineProps<Props>()
defineEmits<Emits>()
const { isAuthenticated, hasActiveSubscription, user } = useAuth()
const { isAuthenticated, hasActiveSubscription } = useAuth()
const { isLoggedIn: isNostrLoggedIn, activePubkey } = useAccounts()
const isDev = import.meta.env.DEV
const newComment = ref('')
const isPostingComment = ref(false)
const isInMyList = ref(false)
const userReaction = ref<string | null>(null)
const showVideoPlayer = ref(false)
const showSubscriptionModal = ref(false)
const showRentalModal = ref(false)
const relayConnected = ref(true)
// Nostr social data -- initialized per content
// Nostr social data -- subscribes to relay in real time
const nostr = useNostr()
const comments = computed(() => nostr.comments.value)
const commentTree = computed(() => nostr.commentTree.value)
const reactionCounts = computed(() => nostr.reactionCounts.value)
const isLoadingComments = computed(() => nostr.isLoading.value)
const commentCount = computed(() => nostr.commentCount.value)
const userInitials = computed(() => {
if (!user.value?.legalName) return 'U'
const names = user.value.legalName.split(' ')
return names.length > 1
? `${names[0][0]}${names[names.length - 1][0]}`
: names[0][0]
// User's existing reaction read from relay (not local state)
const userReaction = computed(() => nostr.userContentReaction.value)
const hasVoted = computed(() => nostr.hasVotedOnContent.value)
// Current user avatar (robohash)
const currentUserAvatar = computed(() => {
if (activePubkey.value) {
return `https://robohash.org/${activePubkey.value}.png`
}
return 'https://robohash.org/anonymous.png'
})
// Fetch social data when content changes
watch(() => props.content?.id, async (newId) => {
// Subscribe to Nostr data when content changes
watch(() => props.content?.id, (newId) => {
if (newId && props.isOpen) {
await loadSocialData(newId)
loadSocialData(newId)
}
})
watch(() => props.isOpen, async (open) => {
watch(() => props.isOpen, (open) => {
if (open && props.content?.id) {
await loadSocialData(props.content.id)
loadSocialData(props.content.id)
}
})
async function loadSocialData(contentId: string) {
userReaction.value = null
await Promise.all([
nostr.fetchComments(contentId),
nostr.fetchReactions(contentId),
])
nostr.subscribeToComments(contentId)
nostr.subscribeToReactions(contentId)
function loadSocialData(contentId: string) {
nostr.cleanup()
nostr.subscribeToContent(contentId)
}
function getProfile(pubkey: string) {
@@ -298,17 +296,13 @@ function getProfile(pubkey: string) {
}
function handlePlay() {
if (!isAuthenticated.value) {
// Will be caught by parent via openAuth emit
return
}
if (!isAuthenticated.value) return
if (hasActiveSubscription.value) {
showVideoPlayer.value = true
return
}
// No subscription -- show rental modal
showRentalModal.value = true
}
@@ -317,8 +311,7 @@ function toggleMyList() {
}
async function handleLike() {
if (!isAuthenticated.value) return
userReaction.value = userReaction.value === '+' ? null : '+'
if (!isNostrLoggedIn.value || hasVoted.value) return
if (props.content?.id) {
try {
await nostr.postReaction(true, props.content.id)
@@ -329,8 +322,7 @@ async function handleLike() {
}
async function handleDislike() {
if (!isAuthenticated.value) return
userReaction.value = userReaction.value === '-' ? null : '-'
if (!isNostrLoggedIn.value || hasVoted.value) return
if (props.content?.id) {
try {
await nostr.postReaction(false, props.content.id)
@@ -356,13 +348,16 @@ function handleShare() {
}
async function submitComment() {
if (!newComment.value.trim() || !props.content?.id) return
if (!newComment.value.trim() || !props.content?.id || isPostingComment.value) return
isPostingComment.value = true
try {
await nostr.postComment(newComment.value.trim(), props.content.id)
newComment.value = ''
} catch (err) {
console.error('Failed to post comment:', err)
} finally {
isPostingComment.value = false
}
}
@@ -379,17 +374,6 @@ function openSubscriptionFromRental() {
showRentalModal.value = false
showSubscriptionModal.value = true
}
function formatTimeAgo(timestamp: number): string {
const now = Math.floor(Date.now() / 1000)
const diff = now - timestamp
if (diff < 60) return 'just now'
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`
return new Date(timestamp * 1000).toLocaleDateString()
}
</script>
<style scoped>
@@ -589,15 +573,6 @@ function formatTimeAgo(timestamp: number): string {
transform: translateY(-1px);
}
.comment-item {
padding: 12px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.comment-item:last-child {
border-bottom: none;
}
/* Transitions */
.modal-fade-enter-active {
transition: opacity 0.3s ease;