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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user