diff --git a/package-lock.json b/package-lock.json index b1a2cf4..3486759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "qr-scanner": "^1.4.2", "qrcode": "^1.5.4", "radix-vue": "^1.9.13", - "reka-ui": "^2.5.0", + "reka-ui": "^2.7.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "unique-names-generator": "^4.7.1", @@ -12172,9 +12172,9 @@ } }, "node_modules/reka-ui": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.5.0.tgz", - "integrity": "sha512-81aMAmJeVCy2k0E6x7n1kypDY6aM1ldLis5+zcdV1/JtoAlSDck5OBsyLRJU9CfgbrQp1ImnRnBSmC4fZ2fkZQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.7.0.tgz", + "integrity": "sha512-m+XmxQN2xtFzBP3OAdIafKq7C8OETo2fqfxcIIxYmNN2Ch3r5oAf6yEYCIJg5tL/yJU2mHqF70dCCekUkrAnXA==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.6.13", diff --git a/package.json b/package.json index 304528e..00b5554 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "qr-scanner": "^1.4.2", "qrcode": "^1.5.4", "radix-vue": "^1.9.13", - "reka-ui": "^2.5.0", + "reka-ui": "^2.7.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "unique-names-generator": "^4.7.1", diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index ba8df07..82ca06c 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon.ico b/public/favicon.ico index 84495b0..8d9e452 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/icon-192.png b/public/icon-192.png index 17abda4..4e6b3a3 100644 Binary files a/public/icon-192.png and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png index 3819dee..94f8c6b 100644 Binary files a/public/icon-512.png and b/public/icon-512.png differ diff --git a/public/icon-maskable-192.png b/public/icon-maskable-192.png index b280cd5..738f5f1 100644 Binary files a/public/icon-maskable-192.png and b/public/icon-maskable-192.png differ diff --git a/public/icon-maskable-512.png b/public/icon-maskable-512.png index bdd4754..3c734e6 100644 Binary files a/public/icon-maskable-512.png and b/public/icon-maskable-512.png differ diff --git a/src/App.vue b/src/App.vue index 127cdd6..559d0bf 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,8 +1,7 @@ + + diff --git a/src/components/layout/AppSidebar.vue b/src/components/layout/AppSidebar.vue new file mode 100644 index 0000000..351ef6a --- /dev/null +++ b/src/components/layout/AppSidebar.vue @@ -0,0 +1,135 @@ + + + diff --git a/src/components/layout/AppTopBar.vue b/src/components/layout/AppTopBar.vue new file mode 100644 index 0000000..3fae1d2 --- /dev/null +++ b/src/components/layout/AppTopBar.vue @@ -0,0 +1,242 @@ + + + diff --git a/src/components/layout/MobileDrawer.vue b/src/components/layout/MobileDrawer.vue new file mode 100644 index 0000000..20c4dad --- /dev/null +++ b/src/components/layout/MobileDrawer.vue @@ -0,0 +1,158 @@ + + + diff --git a/src/components/layout/Navbar.vue b/src/components/layout/Navbar.old.vue similarity index 97% rename from src/components/layout/Navbar.vue rename to src/components/layout/Navbar.old.vue index 07fbba1..6eb160f 100644 --- a/src/components/layout/Navbar.vue +++ b/src/components/layout/Navbar.old.vue @@ -1,3 +1,13 @@ + + + diff --git a/src/components/ui/sheet/SheetClose.vue b/src/components/ui/sheet/SheetClose.vue new file mode 100644 index 0000000..0295976 --- /dev/null +++ b/src/components/ui/sheet/SheetClose.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/ui/sheet/SheetContent.vue b/src/components/ui/sheet/SheetContent.vue new file mode 100644 index 0000000..50f3de6 --- /dev/null +++ b/src/components/ui/sheet/SheetContent.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/components/ui/sheet/SheetDescription.vue b/src/components/ui/sheet/SheetDescription.vue new file mode 100644 index 0000000..455c2f4 --- /dev/null +++ b/src/components/ui/sheet/SheetDescription.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/ui/sheet/SheetFooter.vue b/src/components/ui/sheet/SheetFooter.vue new file mode 100644 index 0000000..5f481e5 --- /dev/null +++ b/src/components/ui/sheet/SheetFooter.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/ui/sheet/SheetHeader.vue b/src/components/ui/sheet/SheetHeader.vue new file mode 100644 index 0000000..f97d24a --- /dev/null +++ b/src/components/ui/sheet/SheetHeader.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/components/ui/sheet/SheetTitle.vue b/src/components/ui/sheet/SheetTitle.vue new file mode 100644 index 0000000..5870787 --- /dev/null +++ b/src/components/ui/sheet/SheetTitle.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/ui/sheet/SheetTrigger.vue b/src/components/ui/sheet/SheetTrigger.vue new file mode 100644 index 0000000..a4fc3ee --- /dev/null +++ b/src/components/ui/sheet/SheetTrigger.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/ui/sheet/index.ts b/src/components/ui/sheet/index.ts new file mode 100644 index 0000000..a370633 --- /dev/null +++ b/src/components/ui/sheet/index.ts @@ -0,0 +1,32 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export { default as Sheet } from "./Sheet.vue" +export { default as SheetClose } from "./SheetClose.vue" +export { default as SheetContent } from "./SheetContent.vue" +export { default as SheetDescription } from "./SheetDescription.vue" +export { default as SheetFooter } from "./SheetFooter.vue" +export { default as SheetHeader } from "./SheetHeader.vue" +export { default as SheetTitle } from "./SheetTitle.vue" +export { default as SheetTrigger } from "./SheetTrigger.vue" + +export const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + }, +) + +export type SheetVariants = VariantProps diff --git a/src/core/di-container.ts b/src/core/di-container.ts index 32221f5..f9d65f9 100644 --- a/src/core/di-container.ts +++ b/src/core/di-container.ts @@ -137,6 +137,11 @@ export const SERVICE_TOKENS = { PROFILE_SERVICE: Symbol('profileService'), REACTION_SERVICE: Symbol('reactionService'), + // Link aggregator services + SUBMISSION_SERVICE: Symbol('submissionService'), + LINK_PREVIEW_SERVICE: Symbol('linkPreviewService'), + COMMUNITY_SERVICE: Symbol('communityService'), + // Nostr metadata services NOSTR_METADATA_SERVICE: Symbol('nostrMetadataService'), diff --git a/src/modules/nostr-feed/LINK_AGGREGATOR_PLAN.md b/src/modules/nostr-feed/LINK_AGGREGATOR_PLAN.md new file mode 100644 index 0000000..9f91aff --- /dev/null +++ b/src/modules/nostr-feed/LINK_AGGREGATOR_PLAN.md @@ -0,0 +1,176 @@ +# Link Aggregator Implementation Plan + +## Overview + +Transform the nostr-feed module into a Reddit-style link aggregator with support for: +- **Link posts** - External URLs with Open Graph previews +- **Media posts** - Images/videos with inline display +- **Self posts** - Text/markdown content + +## NIP Compliance + +| NIP | Purpose | Usage | +|-----|---------|-------| +| NIP-72 | Moderated Communities | Community definitions (kind 34550), approvals (kind 4550) | +| NIP-22 | Comments | Community posts (kind 1111) with scoped threading | +| NIP-92 | Media Attachments | `imeta` tags for media metadata | +| NIP-94 | File Metadata | Reference for media fields | +| NIP-25 | Reactions | Upvote (`+`) / Downvote (`-`) | +| NIP-10 | Reply Threading | Fallback for kind 1 compatibility | + +## Event Structure + +### Submission (kind 1111) + +```jsonc +{ + "kind": 1111, + "content": "", + "tags": [ + // Community scope (NIP-72 + NIP-22) + ["A", "34550::", ""], + ["a", "34550::", ""], + ["K", "34550"], + ["k", "34550"], + ["P", ""], + ["p", ""], + + // Submission metadata + ["title", ""], + ["post-type", "link|media|self"], + + // Link post fields + ["r", ""], + ["preview-title", ""], + ["preview-description", ""], + ["preview-image", ""], + ["preview-site-name", ""], + + // Media post fields (NIP-92) + ["imeta", "url ", "m ", "dim ", "blurhash ", "alt "], + + // Common + ["t", ""], + ["nsfw", "true|false"] + ] +} +``` + +### Comment on Submission (kind 1111) + +```jsonc +{ + "kind": 1111, + "content": "", + "tags": [ + // Root scope (the community) + ["A", "34550::", ""], + ["K", "34550"], + ["P", ""], + + // Parent (the submission or parent comment) + ["e", "", "", ""], + ["k", "1111"], + ["p", ""] + ] +} +``` + +## Implementation Phases + +### Phase 1: Core Data Model +- [x] Create feature branch +- [x] Document plan +- [x] Create `types/submission.ts` - Type definitions +- [x] Create `SubmissionService.ts` - Submission CRUD +- [x] Create `LinkPreviewService.ts` - OG tag fetching +- [x] Extend `FeedService.ts` - Handle kind 1111 + +### Phase 2: Post Creation +- [x] Create `SubmitComposer.vue` - Multi-type composer +- [x] Add link preview on URL paste +- [x] Add NSFW toggle +- [x] Add route `/submit` for composer +- [ ] Integrate with pictrs for media upload (Future) + +### Phase 3: Feed Display +- [x] Create `SubmissionRow.vue` - Link aggregator row (Reddit/Lemmy style) +- [x] Create `VoteControls.vue` - Up/down voting +- [x] Create `SortTabs.vue` - Sort tabs (hot, new, top, controversial) +- [x] Create `SubmissionList.vue` - Main feed container +- [x] Create `SubmissionThumbnail.vue` - Thumbnail display +- [x] Add feed sorting (hot, new, top, controversial) +- [x] Add score calculation +- [x] Create `LinkAggregatorTest.vue` - Test page with mock data & live mode + +### Phase 4: Detail View +- [x] Create `SubmissionDetail.vue` - Full post view with content display +- [x] Create `SubmissionComment.vue` - Recursive threaded comments +- [x] Create `SubmissionDetailPage.vue` - Route page wrapper +- [x] Add route `/submission/:id` for detail view +- [x] Add comment sorting (best, new, old, controversial) +- [x] Integrate comment submission via SubmissionService.createComment() + +### Phase 5: Communities (Future) +- [ ] Create `CommunityService.ts` +- [ ] Create community browser +- [ ] Add moderation queue + +## Ranking Algorithms + +### Hot Rank (Lemmy-style) +```typescript +function hotRank(score: number, createdAt: Date): number { + const order = Math.log10(Math.max(Math.abs(score), 1)) + const sign = score > 0 ? 1 : score < 0 ? -1 : 0 + const seconds = (createdAt.getTime() - EPOCH.getTime()) / 1000 + return sign * order + seconds / 45000 +} +``` + +### Controversy Rank +```typescript +function controversyRank(upvotes: number, downvotes: number): number { + const total = upvotes + downvotes + if (total === 0) return 0 + const magnitude = Math.pow(total, 0.8) + const balance = total > 0 ? Math.min(upvotes, downvotes) / Math.max(upvotes, downvotes) : 0 + return magnitude * balance +} +``` + +## File Structure + +``` +src/modules/nostr-feed/ +├── types/ +│ └── submission.ts # NEW +├── services/ +│ ├── FeedService.ts # MODIFY +│ ├── SubmissionService.ts # NEW +│ ├── LinkPreviewService.ts # NEW +│ ├── CommunityService.ts # NEW (Phase 5) +│ ├── ProfileService.ts # EXISTING +│ └── ReactionService.ts # EXISTING (enhance for up/down) +├── components/ +│ ├── SubmissionCard.vue # NEW (Phase 3) +│ ├── SubmitComposer.vue # NEW (Phase 2) +│ ├── SubmissionDetail.vue # NEW (Phase 4) +│ ├── VoteButtons.vue # NEW (Phase 3) +│ ├── ThreadedPost.vue # EXISTING (reuse) +│ ├── NostrFeed.vue # EXISTING (modify) +│ └── NoteComposer.vue # EXISTING +├── composables/ +│ ├── useSubmissions.ts # NEW +│ ├── useCommunities.ts # NEW (Phase 5) +│ ├── useFeed.ts # EXISTING +│ └── useReactions.ts # EXISTING +└── config/ + └── content-filters.ts # MODIFY +``` + +## Migration Strategy + +1. **Backwards compatible** - Continue supporting kind 1 notes +2. **Gradual adoption** - Add kind 1111 alongside existing +3. **Feature flag** - Toggle between classic feed and link aggregator view diff --git a/src/modules/nostr-feed/components/SortTabs.vue b/src/modules/nostr-feed/components/SortTabs.vue new file mode 100644 index 0000000..84a174f --- /dev/null +++ b/src/modules/nostr-feed/components/SortTabs.vue @@ -0,0 +1,107 @@ + + + diff --git a/src/modules/nostr-feed/components/SubmissionComment.vue b/src/modules/nostr-feed/components/SubmissionComment.vue new file mode 100644 index 0000000..ab52a9e --- /dev/null +++ b/src/modules/nostr-feed/components/SubmissionComment.vue @@ -0,0 +1,275 @@ + + +