diff --git a/package.json b/package.json index 24555e2a..a0e07b0b 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,12 @@ "dependencies": { "@ctrl/magnet-link": "^3.1.2", "@headlessui/react": "^1.7.17", - "@nostr-dev-kit/ndk": "^0.8.18", + "@nostr-dev-kit/ndk": "^0.8.19", "@nostr-fetch/adapter-ndk": "^0.12.2", "@radix-ui/react-alert-dialog": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-tooltip": "^1.0.6", "@tanstack/react-query": "^4.33.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ceb54c6..7d227c5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,11 +8,11 @@ dependencies: specifier: ^1.7.17 version: 1.7.17(react-dom@18.2.0)(react@18.2.0) '@nostr-dev-kit/ndk': - specifier: ^0.8.18 - version: 0.8.18(typescript@5.1.6) + specifier: ^0.8.19 + version: 0.8.19(typescript@5.1.6) '@nostr-fetch/adapter-ndk': specifier: ^0.12.2 - version: 0.12.2(@nostr-dev-kit/ndk@0.8.18)(nostr-fetch@0.12.2) + version: 0.12.2(@nostr-dev-kit/ndk@0.8.19)(nostr-fetch@0.12.2) '@radix-ui/react-alert-dialog': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) @@ -22,6 +22,9 @@ dependencies: '@radix-ui/react-dialog': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.0.5 + version: 2.0.5(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-popover': specifier: ^1.0.6 version: 1.0.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) @@ -965,8 +968,8 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 - /@nostr-dev-kit/ndk@0.8.18(typescript@5.1.6): - resolution: {integrity: sha512-LbEVEjJgFqLmKD0Y76bPxaa7YLlRuz3cPM+kg3XNqHqdN9cDnklYYxAcels58OQqgtFsZBxoqyT64JoYREdr+g==} + /@nostr-dev-kit/ndk@0.8.19(typescript@5.1.6): + resolution: {integrity: sha512-CyqZZiwhsWCCkfoT9JUAyDoQnfyFUVvb7vFhmsxg51eICqLV8SXNxKEyNaMFN1UJdqqi0GamFwisitC65c12jw==} dependencies: '@noble/hashes': 1.3.1 '@noble/secp256k1': 2.0.0 @@ -994,13 +997,13 @@ packages: - typescript dev: false - /@nostr-fetch/adapter-ndk@0.12.2(@nostr-dev-kit/ndk@0.8.18)(nostr-fetch@0.12.2): + /@nostr-fetch/adapter-ndk@0.12.2(@nostr-dev-kit/ndk@0.8.19)(nostr-fetch@0.12.2): resolution: {integrity: sha512-+7EVuxS5DDZvNo6qbfFp7xRHwIyjyi36hYkiQFDjbQ4gX5LKo9RIPB1P+1XGkOSDFshypTbovZCaFunscJ/zhQ==} peerDependencies: '@nostr-dev-kit/ndk': ^0.7.5 nostr-fetch: ^0.12.2 dependencies: - '@nostr-dev-kit/ndk': 0.8.18(typescript@5.1.6) + '@nostr-dev-kit/ndk': 0.8.19(typescript@5.1.6) '@nostr-fetch/kernel': 0.12.2 nostr-fetch: 0.12.2 dev: false @@ -1097,6 +1100,30 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.20)(react@18.2.0) + '@types/react': 18.2.20 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.20)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -1159,6 +1186,20 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.20)(react@18.2.0) dev: false + /@radix-ui/react-direction@1.0.1(@types/react@18.2.20)(react@18.2.0): + resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@types/react': 18.2.20 + react: 18.2.0 + dev: false + /@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} peerDependencies: @@ -1184,6 +1225,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dropdown-menu@2.0.5(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-menu': 2.0.5(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@types/react': 18.2.20 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.20)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: @@ -1236,6 +1304,44 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-menu@2.0.5(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Gw4f9pwdH+w5w+49k0gLjN0PfRDHvxmAgG16AbyJZ7zhwZ6PBHKtWohvnSwfusfnK3L68dpBREHpVkj8wEM7ZA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@types/react': 18.2.20 + '@types/react-dom': 18.2.7 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.20)(react@18.2.0) + dev: false + /@radix-ui/react-popover@1.0.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==} peerDependencies: @@ -1365,6 +1471,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.20)(react@18.2.0) + '@types/react': 18.2.20 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.20)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -2087,7 +2222,7 @@ packages: prosemirror-inputrules: 1.2.1 prosemirror-keymap: 1.2.2 prosemirror-markdown: 1.11.2 - prosemirror-menu: 1.2.3 + prosemirror-menu: 1.2.4 prosemirror-model: 1.19.3 prosemirror-schema-basic: 1.2.2 prosemirror-schema-list: 1.3.0 @@ -5671,8 +5806,8 @@ packages: prosemirror-model: 1.19.3 dev: false - /prosemirror-menu@1.2.3: - resolution: {integrity: sha512-13H9+XvdJiUt2vQVMqCveFbc7YfEKR3g70pUwuQdQLwuvNfVGTzMHr1y5dwdY5vOBQbzhmjgnWUnclKzMdnlJA==} + /prosemirror-menu@1.2.4: + resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==} dependencies: crelt: 1.0.6 prosemirror-commands: 1.5.2 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8b2be1d6..ef4b702b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -241,14 +241,14 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.5.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232344f51ca91edec473432f677c0f5ddf8deaa72f165d253ee19fb196f7d6f2" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand 2.0.0", + "fastrand 1.9.0", "futures-lite", "slab", ] @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", "libc", diff --git a/src/app/auth/onboarding/step-1.tsx b/src/app/auth/onboarding/step-1.tsx index 53e87369..774aefcc 100644 --- a/src/app/auth/onboarding/step-1.tsx +++ b/src/app/auth/onboarding/step-1.tsx @@ -76,7 +76,7 @@ export function OnboardStep1Screen() {

Choose account you want to follow

-
+
{status === 'loading' ? (
@@ -88,7 +88,7 @@ export function OnboardStep1Screen() { key={item.pubkey} type="button" onClick={() => toggleFollow(item.pubkey)} - className="inline-flex transform items-center justify-between bg-white/10 px-4 py-2 hover:bg-white/20" + className="inline-flex transform items-center justify-between px-4 py-2 hover:bg-white/20" > {follows.includes(item.pubkey) && ( diff --git a/src/app/auth/onboarding/step-2.tsx b/src/app/auth/onboarding/step-2.tsx index f530e509..14b43995 100644 --- a/src/app/auth/onboarding/step-2.tsx +++ b/src/app/auth/onboarding/step-2.tsx @@ -5,8 +5,8 @@ import { useStorage } from '@libs/storage/provider'; import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons'; -import { widgetKinds } from '@stores/constants'; import { useOnboarding } from '@stores/onboarding'; +import { WidgetKinds } from '@stores/widgets'; const data = [ { hashtag: '#bitcoin' }, @@ -52,7 +52,7 @@ export function OnboardStep2Screen() { setLoading(true); for (const tag of tags) { - await db.createWidget(widgetKinds.hashtag, tag, tag.replace('#', '')); + await db.createWidget(WidgetKinds.hashtag, tag, tag.replace('#', '')); } navigate('/auth/onboarding/step-3', { replace: true }); diff --git a/src/app/space/components/button.tsx b/src/app/space/components/button.tsx new file mode 100644 index 00000000..dee8532e --- /dev/null +++ b/src/app/space/components/button.tsx @@ -0,0 +1,163 @@ +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; + +import { useStorage } from '@libs/storage/provider'; + +import { + FeedIcon, + FileIcon, + HashtagIcon, + PlusIcon, + ThreadsIcon, + TrendingIcon, +} from '@shared/icons'; + +import { WidgetKinds, useWidgets } from '@stores/widgets'; + +export function AddWidgetButton() { + const { db } = useStorage(); + const setWidget = useWidgets((state) => state.setWidget); + + const setTrendingProfilesWidget = () => { + setWidget(db, { + kind: WidgetKinds.trendingProfiles, + title: 'Trending Profiles', + content: 'https://api.nostr.band/v0/trending/profiles', + }); + }; + + const setTrendingNotesWidget = () => { + setWidget(db, { + kind: WidgetKinds.trendingNotes, + title: 'Trending Notes', + content: 'https://api.nostr.band/v0/trending/notes', + }); + }; + + const setArticleWidget = () => { + setWidget(db, { + kind: WidgetKinds.article, + title: 'Blogs', + content: '', + }); + }; + + const setFileWidget = () => { + setWidget(db, { + kind: WidgetKinds.file, + title: 'Files', + content: '', + }); + }; + + const setHashtagWidget = () => { + setWidget(db, { + kind: WidgetKinds.xhashtag, + title: 'New hashtag', + content: '', + }); + }; + + const setGroupFeedWidget = () => { + setWidget(db, { + kind: WidgetKinds.xfeed, + title: 'New user group feed', + content: '', + }); + }; + + return ( + +
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/space/components/forms/feed.tsx b/src/app/space/components/forms/feed.tsx new file mode 100644 index 00000000..f70cc1fc --- /dev/null +++ b/src/app/space/components/forms/feed.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react'; + +import { User } from '@app/auth/components/user'; + +import { useStorage } from '@libs/storage/provider'; + +import { ArrowRightCircleIcon, CheckCircleIcon } from '@shared/icons'; + +import { WidgetKinds, useWidgets } from '@stores/widgets'; + +import { Widget } from '@utils/types'; + +export function FeedWidgetForm({ params }: { params: Widget }) { + const { db } = useStorage(); + + const [setWidget, removeWidget] = useWidgets((state) => [ + state.setWidget, + state.removeWidget, + ]); + const [title, setTitle] = useState(''); + const [groups, setGroups] = useState>([]); + + // toggle follow state + const toggleGroup = (pubkey: string) => { + const arr = groups.includes(pubkey) + ? groups.filter((i) => i !== pubkey) + : [...groups, pubkey]; + setGroups(arr); + }; + + const submit = async () => { + setWidget(db, { + kind: WidgetKinds.feed, + title: title || 'Group', + content: JSON.stringify(groups), + }); + // remove temp widget + removeWidget(db, params.id); + }; + + return ( +
+
+

+ Choose account you want to add to group feeds +

+
+
+ setTitle(e.target.value)} + placeholder="Title" + className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50" + /> +
+
+ {db.account.network.map((item: string) => ( + + ))} +
+
+ +
+
+
+
+ ); +} diff --git a/src/app/space/components/forms/hashtag.tsx b/src/app/space/components/forms/hashtag.tsx new file mode 100644 index 00000000..a61cae0a --- /dev/null +++ b/src/app/space/components/forms/hashtag.tsx @@ -0,0 +1,92 @@ +import { Resolver, useForm } from 'react-hook-form'; + +import { useStorage } from '@libs/storage/provider'; + +import { ArrowRightCircleIcon } from '@shared/icons'; + +import { WidgetKinds, useWidgets } from '@stores/widgets'; + +import { Widget } from '@utils/types'; + +type FormValues = { + hashtag: string; +}; + +const resolver: Resolver = async (values) => { + return { + values: values.hashtag ? values : {}, + errors: !values.hashtag + ? { + hashtag: { + type: 'required', + message: 'This is required.', + }, + } + : {}, + }; +}; + +export function HashTagWidgetForm({ params }: { params: Widget }) { + const [setWidget, removeWidget] = useWidgets((state) => [ + state.setWidget, + state.removeWidget, + ]); + + const { db } = useStorage(); + const { + register, + setError, + handleSubmit, + formState: { errors, isDirty, isValid }, + } = useForm({ resolver }); + + const onSubmit = async (data: FormValues) => { + try { + setWidget(db, { + kind: WidgetKinds.hashtag, + title: data.hashtag + ' in 24 hours ago', + content: data.hashtag.replace('#', ''), + }); + // remove temp widget + removeWidget(db, params.id); + } catch (e) { + setError('hashtag', { + type: 'custom', + message: e, + }); + } + }; + + return ( +
+
+

+ Enter hashtag you want to follow +

+
+
+ + + {errors.hashtag &&

{errors.hashtag.message}

} +
+
+
+ +
+
+
+
+ ); +} diff --git a/src/app/space/components/modals/feed.tsx b/src/app/space/components/modals/feed.tsx index e35dfc2e..0905d6a1 100644 --- a/src/app/space/components/modals/feed.tsx +++ b/src/app/space/components/modals/feed.tsx @@ -10,8 +10,7 @@ import { useStorage } from '@libs/storage/provider'; import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons'; -import { widgetKinds } from '@stores/constants'; -import { useWidgets } from '@stores/widgets'; +import { WidgetKinds, useWidgets } from '@stores/widgets'; export function FeedModal() { const setWidget = useWidgets((state) => state.setWidget); @@ -40,7 +39,7 @@ export function FeedModal() { // update state setWidget(db, { - kind: widgetKinds.feed, + kind: WidgetKinds.feed, title: data.title, content: JSON.stringify(selected), }); diff --git a/src/app/space/components/modals/hashtag.tsx b/src/app/space/components/modals/hashtag.tsx index 17e3a129..1dced02c 100644 --- a/src/app/space/components/modals/hashtag.tsx +++ b/src/app/space/components/modals/hashtag.tsx @@ -6,8 +6,7 @@ import { useStorage } from '@libs/storage/provider'; import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons'; -import { widgetKinds } from '@stores/constants'; -import { useWidgets } from '@stores/widgets'; +import { WidgetKinds, useWidgets } from '@stores/widgets'; export function HashtagModal() { const setWidget = useWidgets((state) => state.setWidget); @@ -28,7 +27,7 @@ export function HashtagModal() { // update state setWidget(db, { - kind: widgetKinds.hashtag, + kind: WidgetKinds.hashtag, title: data.hashtag, content: data.hashtag.replace('#', ''), }); diff --git a/src/app/space/components/modals/image.tsx b/src/app/space/components/modals/image.tsx deleted file mode 100644 index f6dc09c8..00000000 --- a/src/app/space/components/modals/image.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import * as Dialog from '@radix-ui/react-dialog'; -import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; - -import { useStorage } from '@libs/storage/provider'; - -import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons'; -import { Image } from '@shared/image'; - -import { widgetKinds } from '@stores/constants'; -import { useWidgets } from '@stores/widgets'; - -import { useImageUploader } from '@utils/hooks/useUploader'; - -export function ImageModal() { - const upload = useImageUploader(); - const setWidget = useWidgets((state) => state.setWidget); - - const [open, setOpen] = useState(false); - const [loading, setLoading] = useState(false); - const [image, setImage] = useState(''); - - const { db } = useStorage(); - const { - register, - handleSubmit, - reset, - setValue, - formState: { isDirty, isValid }, - } = useForm(); - - const uploadImage = async () => { - const image = await upload(null, true); - if (image.url) { - setImage(image.url); - } - }; - - const onSubmit = async (data: { kind: number; title: string; content: string }) => { - setLoading(true); - - // mutate - setWidget(db, { kind: widgetKinds.image, title: data.title, content: data.content }); - - setLoading(false); - // reset form - reset(); - // close modal - setOpen(false); - }; - - useEffect(() => { - setValue('content', image); - }, [setValue, image]); - - return ( - - - - - - - -
-
-
-
- - Create image block - - - - -
- - Pin your favorite image to Space then you can view every time that you - use Lume, your image will be broadcast to Nostr Relay as well - -
-
-
-
- -
- - -
-
- -
- content -
- -
-
-
- -
-
-
-
-
-
- ); -} diff --git a/src/app/space/components/widgets/article.tsx b/src/app/space/components/widgets/article.tsx new file mode 100644 index 00000000..53e0e1ec --- /dev/null +++ b/src/app/space/components/widgets/article.tsx @@ -0,0 +1,114 @@ +import { NDKEvent } from '@nostr-dev-kit/ndk'; +import { useQuery } from '@tanstack/react-query'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { useCallback, useRef } from 'react'; + +import { useNDK } from '@libs/ndk/provider'; + +import { NoteKind_1, NoteSkeleton, Repost } from '@shared/notes'; +import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport'; +import { TitleBar } from '@shared/titleBar'; + +import { nHoursAgo } from '@utils/date'; +import { Widget } from '@utils/types'; + +export function ArticleWidget({ params }: { params: Widget }) { + const { ndk } = useNDK(); + const { status, data } = useQuery(['article-widget', params.content], async () => { + const events = await ndk.fetchEvents({ + kinds: [30023], + '#t': [params.content], + since: nHoursAgo(48), + }); + return [...events] as unknown as NDKEvent[]; + }); + + const parentRef = useRef(null); + const virtualizer = useVirtualizer({ + count: data ? data.length : 0, + getScrollElement: () => parentRef.current, + estimateSize: () => 650, + overscan: 4, + }); + const items = virtualizer.getVirtualItems(); + + // render event match event kind + const renderItem = useCallback( + (index: string | number) => { + const event: NDKEvent = data[index]; + if (!event) return; + + switch (event.kind) { + case 1: + return ( +
+ +
+ ); + case 6: + return ( +
+ +
+ ); + default: + return ( +
+ +
+ ); + } + }, + [data] + ); + + return ( +
+ +
+ {status === 'loading' ? ( +
+
+ +
+
+ ) : items.length === 0 ? ( +
+
+
+

+ No new postrs about this hashtag in 24 hours ago +

+
+
+
+ ) : ( +
+
+ {items.map((item) => renderItem(item.index))} +
+
+ )} +
+
+ ); +} diff --git a/src/app/space/components/widgets/feed.tsx b/src/app/space/components/widgets/feed.tsx index e071615f..47b5e930 100644 --- a/src/app/space/components/widgets/feed.tsx +++ b/src/app/space/components/widgets/feed.tsx @@ -17,7 +17,7 @@ export function FeedWidget({ params }: { params: Widget }) { const { db } = useStorage(); const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ - queryKey: ['newsfeed', params.content], + queryKey: ['groupfeed-widget', params.content], queryFn: async ({ pageParam = 0 }) => { const authors = JSON.parse(params.content); return await db.getAllEventsByAuthors(authors, 20, pageParam); diff --git a/src/app/space/components/widgets/network.tsx b/src/app/space/components/widgets/network.tsx index 6fbc9de6..a981c9ea 100644 --- a/src/app/space/components/widgets/network.tsx +++ b/src/app/space/components/widgets/network.tsx @@ -1,6 +1,7 @@ import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useVirtualizer } from '@tanstack/react-virtual'; +import { destr } from 'destr'; import { useCallback, useEffect, useMemo, useRef } from 'react'; import { Link } from 'react-router-dom'; @@ -137,7 +138,7 @@ export function NetworkWidget() { } await db.createEvent( event.id, - JSON.stringify(event), + destr(event), event.pubkey, event.kind, root, diff --git a/src/app/space/components/widgets/trendingNotes.tsx b/src/app/space/components/widgets/trendingNotes.tsx index e2dfec6f..f0dfd48e 100644 --- a/src/app/space/components/widgets/trendingNotes.tsx +++ b/src/app/space/components/widgets/trendingNotes.tsx @@ -13,7 +13,7 @@ interface Response { export function TrendingNotesWidget({ params }: { params: Widget }) { const { status, data } = useQuery( - ['trending-notes'], + ['trending-notes-widget'], async () => { const res = await fetch(params.content); if (!res.ok) { @@ -33,7 +33,7 @@ export function TrendingNotesWidget({ params }: { params: Widget }) { return (
- +
{status === 'loading' ? (
diff --git a/src/app/space/components/widgets/trendingProfile.tsx b/src/app/space/components/widgets/trendingProfile.tsx index 5cd410e0..72e65abf 100644 --- a/src/app/space/components/widgets/trendingProfile.tsx +++ b/src/app/space/components/widgets/trendingProfile.tsx @@ -13,7 +13,7 @@ interface Response { export function TrendingProfilesWidget({ params }: { params: Widget }) { const { status, data } = useQuery( - ['trending-profiles'], + ['trending-profiles-widget'], async () => { const res = await fetch(params.content); if (!res.ok) { @@ -33,7 +33,7 @@ export function TrendingProfilesWidget({ params }: { params: Widget }) { return (
- +
{status === 'loading' ? (
diff --git a/src/app/space/index.tsx b/src/app/space/index.tsx index 0a74f8a6..7f56fc88 100644 --- a/src/app/space/index.tsx +++ b/src/app/space/index.tsx @@ -1,16 +1,21 @@ import { useCallback, useEffect } from 'react'; +import { AddWidgetButton } from '@app/space/components/button'; +import { FeedWidgetForm } from '@app/space/components/forms/feed'; +import { HashTagWidgetForm } from '@app/space/components/forms/hashtag'; import { FeedWidget } from '@app/space/components/widgets/feed'; import { HashtagWidget } from '@app/space/components/widgets/hashtag'; import { NetworkWidget } from '@app/space/components/widgets/network'; import { ThreadBlock } from '@app/space/components/widgets/thread'; +import { TrendingNotesWidget } from '@app/space/components/widgets/trendingNotes'; +import { TrendingProfilesWidget } from '@app/space/components/widgets/trendingProfile'; import { UserWidget } from '@app/space/components/widgets/user'; import { useStorage } from '@libs/storage/provider'; -import { LoaderIcon, PlusIcon } from '@shared/icons'; +import { LoaderIcon } from '@shared/icons'; -import { useWidgets } from '@stores/widgets'; +import { WidgetKinds, useWidgets } from '@stores/widgets'; import { Widget } from '@utils/types'; @@ -26,16 +31,24 @@ export function SpaceScreen() { (widget: Widget) => { if (!widget) return; switch (widget.kind) { - case 1: + case WidgetKinds.feed: return ; - case 2: + case WidgetKinds.thread: return ; - case 3: + case WidgetKinds.hashtag: return ; - case 5: + case WidgetKinds.user: return ; - case 9999: + case WidgetKinds.trendingProfiles: + return ; + case WidgetKinds.trendingNotes: + return ; + case WidgetKinds.network: return ; + case WidgetKinds.xhashtag: + return ; + case WidgetKinds.xfeed: + return ; default: break; } @@ -58,16 +71,7 @@ export function SpaceScreen() { ) : ( widgets.map((widget) => renderItem(widget)) )} -
-
- -
-
+
); } diff --git a/src/libs/storage/instance.ts b/src/libs/storage/instance.ts index 4683cbcb..9a248696 100644 --- a/src/libs/storage/instance.ts +++ b/src/libs/storage/instance.ts @@ -205,8 +205,8 @@ export class LumeStorage { }; const query: DBEvent[] = await this.db.select( - 'SELECT * FROM events WHERE author IN ($1) ORDER BY created_at DESC LIMIT $2 OFFSET $3;', - [authorsArr, limit, offset] + `SELECT * FROM events WHERE author IN (${authorsArr}) ORDER BY created_at DESC LIMIT $1 OFFSET $2;`, + [limit, offset] ); if (query && query.length > 0) { diff --git a/src/shared/icons/file.tsx b/src/shared/icons/file.tsx new file mode 100644 index 00000000..45872c2d --- /dev/null +++ b/src/shared/icons/file.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from 'react'; + +export function FileIcon(props: JSX.IntrinsicAttributes & SVGProps) { + return ( + + + + ); +} diff --git a/src/shared/icons/hashtag.tsx b/src/shared/icons/hashtag.tsx new file mode 100644 index 00000000..927489df --- /dev/null +++ b/src/shared/icons/hashtag.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from 'react'; + +export function HashtagIcon(props: JSX.IntrinsicAttributes & SVGProps) { + return ( + + + + ); +} diff --git a/src/shared/icons/index.tsx b/src/shared/icons/index.tsx index c0dd75a0..d36af3b3 100644 --- a/src/shared/icons/index.tsx +++ b/src/shared/icons/index.tsx @@ -1,4 +1,3 @@ -// @index('./*.tsx', f => `export * from '${f.path}'`) export * from './arrowLeft'; export * from './arrowRight'; export * from './bell'; @@ -49,4 +48,5 @@ export * from './strangers'; export * from './download'; export * from './horizontalDots'; export * from './arrowRightCircle'; -// @endindex +export * from './hashtag'; +export * from './file'; diff --git a/src/shared/lumeBar.tsx b/src/shared/lumeBar.tsx index 41c6893f..f807eed1 100644 --- a/src/shared/lumeBar.tsx +++ b/src/shared/lumeBar.tsx @@ -5,7 +5,6 @@ import { useStorage } from '@libs/storage/provider'; import { ActiveAccount } from '@shared/accounts/active'; import { SettingsIcon } from '@shared/icons'; import { Logout } from '@shared/logout'; -import { NotificationModal } from '@shared/notification/modal'; export function LumeBar() { const { db } = useStorage(); @@ -14,7 +13,6 @@ export function LumeBar() {
- setWidget(db, { - kind: widgetKinds.thread, + kind: WidgetKinds.thread, title: 'Thread', content: id, }) diff --git a/src/shared/notes/actions/more.tsx b/src/shared/notes/actions/more.tsx index ee729df0..0d2806fc 100644 --- a/src/shared/notes/actions/more.tsx +++ b/src/shared/notes/actions/more.tsx @@ -1,4 +1,4 @@ -import * as Popover from '@radix-ui/react-popover'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import * as Tooltip from '@radix-ui/react-tooltip'; import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import { nip19 } from 'nostr-tools'; @@ -25,17 +25,17 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) { }; return ( - + - + - + @@ -44,15 +44,17 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) { - - -
+ + + Open as new screen + + + + + + View profile -
-
-
-
+ + + + ); } diff --git a/src/shared/notes/mentions/hashtag.tsx b/src/shared/notes/mentions/hashtag.tsx index 3105a54d..17c80b98 100644 --- a/src/shared/notes/mentions/hashtag.tsx +++ b/src/shared/notes/mentions/hashtag.tsx @@ -1,7 +1,6 @@ import { useStorage } from '@libs/storage/provider'; -import { widgetKinds } from '@stores/constants'; -import { useWidgets } from '@stores/widgets'; +import { WidgetKinds, useWidgets } from '@stores/widgets'; export function Hashtag({ tag }: { tag: string }) { const { db } = useStorage(); @@ -12,7 +11,7 @@ export function Hashtag({ tag }: { tag: string }) { type="button" onClick={() => setWidget(db, { - kind: widgetKinds.hashtag, + kind: WidgetKinds.hashtag, title: tag, content: tag.replace('#', ''), }) diff --git a/src/shared/notes/mentions/note.tsx b/src/shared/notes/mentions/note.tsx index cbc2b78a..265173e1 100644 --- a/src/shared/notes/mentions/note.tsx +++ b/src/shared/notes/mentions/note.tsx @@ -8,28 +8,28 @@ import { Image } from '@shared/image'; import { MentionUser, NoteSkeleton } from '@shared/notes'; import { User } from '@shared/user'; -import { widgetKinds } from '@stores/constants'; -import { useWidgets } from '@stores/widgets'; +import { WidgetKinds, useWidgets } from '@stores/widgets'; import { useEvent } from '@utils/hooks/useEvent'; import { isImage } from '@utils/isImage'; export const MentionNote = memo(function MentionNote({ id }: { id: string }) { const { db } = useStorage(); - const { status, data } = useEvent(id); + const { status, data, error } = useEvent(id); const setWidget = useWidgets((state) => state.setWidget); const openThread = (event, thread: string) => { const selection = window.getSelection(); if (selection.toString().length === 0) { - setWidget(db, { kind: widgetKinds.thread, title: 'Thread', content: thread }); + setWidget(db, { kind: WidgetKinds.thread, title: 'Thread', content: thread }); } else { event.stopPropagation(); } }; const renderItem = useCallback(() => { + if (!data) return; switch (data.event.kind) { case 1: { return ( @@ -85,6 +85,14 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) { ); } + if (error) { + return ( +
+

Can't get event from relay

+
+ ); + } + return (
openThread(e, id)} @@ -93,7 +101,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) { tabIndex={0} className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3" > - +
{renderItem()}
); diff --git a/src/shared/notes/mentions/user.tsx b/src/shared/notes/mentions/user.tsx index 19dcf41a..114eee56 100644 --- a/src/shared/notes/mentions/user.tsx +++ b/src/shared/notes/mentions/user.tsx @@ -1,7 +1,6 @@ import { useStorage } from '@libs/storage/provider'; -import { widgetKinds } from '@stores/constants'; -import { useWidgets } from '@stores/widgets'; +import { WidgetKinds, useWidgets } from '@stores/widgets'; import { useProfile } from '@utils/hooks/useProfile'; import { displayNpub } from '@utils/shortenKey'; @@ -17,7 +16,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) { type="button" onClick={() => setWidget(db, { - kind: widgetKinds.user, + kind: WidgetKinds.user, title: user?.nip05 || user?.name || user?.display_name, content: pubkey, }) diff --git a/src/shared/notes/metadata.tsx b/src/shared/notes/metadata.tsx index d850236f..d424bac3 100644 --- a/src/shared/notes/metadata.tsx +++ b/src/shared/notes/metadata.tsx @@ -8,8 +8,7 @@ import { useStorage } from '@libs/storage/provider'; import { LoaderIcon } from '@shared/icons'; import { MiniUser } from '@shared/notes/users/mini'; -import { widgetKinds } from '@stores/constants'; -import { useWidgets } from '@stores/widgets'; +import { WidgetKinds, useWidgets } from '@stores/widgets'; import { compactNumber } from '@utils/number'; @@ -54,7 +53,11 @@ export function NoteMetadata({ id }: { id: string }) { return { replies, users, zap }; }, - { refetchOnWindowFocus: false, refetchOnReconnect: false, refetchOnMount: false } + { + enabled: !!ndk, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + } ); if (status === 'loading') { @@ -68,6 +71,12 @@ export function NoteMetadata({ id }: { id: string }) { ); } + if (status === 'error') { +
+
+
; + } + return (
{data.replies > 0 ? ( @@ -86,7 +95,7 @@ export function NoteMetadata({ id }: { id: string }) { type="button" onClick={() => setWidget(db, { - kind: widgetKinds.thread, + kind: WidgetKinds.thread, title: 'Thread', content: id, }) diff --git a/src/stores/constants.tsx b/src/stores/constants.tsx index d55e070f..d36ea84c 100644 --- a/src/stores/constants.tsx +++ b/src/stores/constants.tsx @@ -3,12 +3,3 @@ export const FULL_RELAYS = [ 'wss://relay.nostr.band/all', 'wss://nostr.mutinywallet.com', ]; - -export const widgetKinds = { - image: 0, - feed: 1, - thread: 2, - hashtag: 3, - exchange_rate: 4, - user: 5, -}; diff --git a/src/stores/widgets.tsx b/src/stores/widgets.tsx index b4fd37b6..74b29b38 100644 --- a/src/stores/widgets.tsx +++ b/src/stores/widgets.tsx @@ -12,6 +12,20 @@ interface WidgetState { removeWidget: (db: LumeStorage, id: string) => void; } +export const WidgetKinds = { + feed: 1, // NIP-01 + thread: 2, // NIP-01 + hashtag: 3, // NIP-01 + article: 4, // NIP-23 + user: 5, // NIP-01 + trendingProfiles: 6, + trendingNotes: 7, + file: 8, // NIP-94 + network: 9999, + xfeed: 10000, // x is temporary state for new feed widget form + xhashtag: 10001, // x is temporary state for new hashtag widget form +}; + export const useWidgets = create()( persist( (set) => ({ @@ -25,7 +39,7 @@ export const useWidgets = create()( id: '9999', title: 'Network', content: '', - kind: 9999, + kind: WidgetKinds.network, }); set({ widgets: dbWidgets }); diff --git a/src/utils/hooks/useEvent.tsx b/src/utils/hooks/useEvent.tsx index fbfef0c7..55ddaa59 100644 --- a/src/utils/hooks/useEvent.tsx +++ b/src/utils/hooks/useEvent.tsx @@ -30,12 +30,17 @@ export function useEvent(id: string, embed?: string) { } else { // get event from relay if event in db not present const event = await ndk.fetchEvent(id); - if (event.kind === 1) richContent = parser(event); + + if (!event) throw new Error(`Event not found: ${id}`); + if (event.kind === 1) { + richContent = parser(event); + } return { event: event, richContent: richContent }; } }, { + enabled: !!ndk, staleTime: Infinity, refetchOnMount: false, refetchOnWindowFocus: false, diff --git a/src/utils/hooks/useNostr.tsx b/src/utils/hooks/useNostr.tsx index 7dd6ac4b..0631f84e 100644 --- a/src/utils/hooks/useNostr.tsx +++ b/src/utils/hooks/useNostr.tsx @@ -109,7 +109,7 @@ export function useNostr() { const events = fetcher.allEventsIterator( relayUrls, { - kinds: [1, 6], + kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article], authors: db.account.network, }, { since: since } diff --git a/src/utils/hooks/useProfile.tsx b/src/utils/hooks/useProfile.tsx index f3c7cc95..62f511c7 100644 --- a/src/utils/hooks/useProfile.tsx +++ b/src/utils/hooks/useProfile.tsx @@ -20,7 +20,7 @@ export function useProfile(pubkey: string, embed?: string) { user.profile.display_name = user.profile.displayName; return user.profile; } else { - throw new Error('User not found'); + throw new Error(`User not found: ${pubkey}`); } } else { const profile: NDKUserProfile = JSON.parse(embed);