feat: add discover newsfeeds and interests

This commit is contained in:
reya 2024-10-31 13:34:25 +07:00
parent 043cabfd4e
commit c201b5816c
42 changed files with 579 additions and 41 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -5,7 +5,7 @@ use specta::Type;
use std::{str::FromStr, time::Duration};
use tauri::{Emitter, Manager, State};
use crate::{common::process_event, Nostr, RichEvent};
use crate::{common::process_event, Nostr, RichEvent, FETCH_LIMIT};
#[derive(Clone, Serialize, Deserialize, Type)]
pub struct Mention {
@ -259,6 +259,35 @@ pub async fn get_all_newsfeeds(
Ok(alt_events)
}
#[tauri::command]
#[specta::specta]
pub async fn get_all_local_newsfeeds(
until: Option<String>,
state: State<'_, Nostr>,
) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(&until).unwrap_or(Timestamp::now()),
None => Timestamp::now(),
};
let filter = Filter::new()
.kind(Kind::FollowSet)
.limit(FETCH_LIMIT)
.until(as_of);
let events = client
.database()
.query(vec![filter])
.await
.map_err(|err| err.to_string())?;
let alt_events = process_event(client, events, false).await;
Ok(alt_events)
}
#[tauri::command]
#[specta::specta]
pub async fn set_interest(
@ -361,6 +390,35 @@ pub async fn get_all_interests(
Ok(alt_events)
}
#[tauri::command]
#[specta::specta]
pub async fn get_all_local_interests(
until: Option<String>,
state: State<'_, Nostr>,
) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(&until).unwrap_or(Timestamp::now()),
None => Timestamp::now(),
};
let filter = Filter::new()
.kinds(vec![Kind::Interests, Kind::InterestSet])
.limit(FETCH_LIMIT)
.until(as_of);
let events = client
.database()
.query(vec![filter])
.await
.map_err(|err| err.to_string())?;
let alt_events = process_event(client, events, false).await;
Ok(alt_events)
}
#[tauri::command]
#[specta::specta]
pub async fn get_all_profiles(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {

View File

@ -97,9 +97,11 @@ fn main() {
set_group,
get_group,
get_all_newsfeeds,
get_all_local_newsfeeds,
set_interest,
get_interest,
get_all_interests,
get_all_local_interests,
set_wallet,
load_wallet,
remove_wallet,

View File

@ -208,6 +208,14 @@ async getAllNewsfeeds(id: string) : Promise<Result<RichEvent[], string>> {
else return { status: "error", error: e as any };
}
},
async getAllLocalNewsfeeds(until: string | null) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_all_local_newsfeeds", { until }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async setInterest(title: string, description: string | null, image: string | null, hashtags: string[]) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("set_interest", { title, description, image, hashtags }) };
@ -232,6 +240,14 @@ async getAllInterests(id: string) : Promise<Result<RichEvent[], string>> {
else return { status: "error", error: e as any };
}
},
async getAllLocalInterests(until: string | null) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_all_local_interests", { until }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async setWallet(uri: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("set_wallet", { uri }) };

View File

@ -51,6 +51,12 @@ const ColumnsLayoutSearchLazyImport = createFileRoute(
const ColumnsLayoutOnboardingLazyImport = createFileRoute(
'/columns/_layout/onboarding',
)()
const ColumnsLayoutDiscoverNewsfeedsLazyImport = createFileRoute(
'/columns/_layout/discover-newsfeeds',
)()
const ColumnsLayoutDiscoverInterestsLazyImport = createFileRoute(
'/columns/_layout/discover-interests',
)()
const ColumnsLayoutUsersIdLazyImport = createFileRoute(
'/columns/_layout/users/$id',
)()
@ -196,6 +202,28 @@ const ColumnsLayoutOnboardingLazyRoute =
import('./routes/columns/_layout/onboarding.lazy').then((d) => d.Route),
)
const ColumnsLayoutDiscoverNewsfeedsLazyRoute =
ColumnsLayoutDiscoverNewsfeedsLazyImport.update({
id: '/discover-newsfeeds',
path: '/discover-newsfeeds',
getParentRoute: () => ColumnsLayoutRoute,
} as any).lazy(() =>
import('./routes/columns/_layout/discover-newsfeeds.lazy').then(
(d) => d.Route,
),
)
const ColumnsLayoutDiscoverInterestsLazyRoute =
ColumnsLayoutDiscoverInterestsLazyImport.update({
id: '/discover-interests',
path: '/discover-interests',
getParentRoute: () => ColumnsLayoutRoute,
} as any).lazy(() =>
import('./routes/columns/_layout/discover-interests.lazy').then(
(d) => d.Route,
),
)
const SettingsIdWalletRoute = SettingsIdWalletImport.update({
id: '/wallet',
path: '/wallet',
@ -469,6 +497,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsIdWalletImport
parentRoute: typeof SettingsIdLazyImport
}
'/columns/_layout/discover-interests': {
id: '/columns/_layout/discover-interests'
path: '/discover-interests'
fullPath: '/columns/discover-interests'
preLoaderRoute: typeof ColumnsLayoutDiscoverInterestsLazyImport
parentRoute: typeof ColumnsLayoutImport
}
'/columns/_layout/discover-newsfeeds': {
id: '/columns/_layout/discover-newsfeeds'
path: '/discover-newsfeeds'
fullPath: '/columns/discover-newsfeeds'
preLoaderRoute: typeof ColumnsLayoutDiscoverNewsfeedsLazyImport
parentRoute: typeof ColumnsLayoutImport
}
'/columns/_layout/onboarding': {
id: '/columns/_layout/onboarding'
path: '/onboarding'
@ -602,6 +644,8 @@ const ColumnsLayoutCreateNewsfeedRouteWithChildren =
interface ColumnsLayoutRouteChildren {
ColumnsLayoutCreateNewsfeedRoute: typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
ColumnsLayoutGlobalRoute: typeof ColumnsLayoutGlobalRoute
ColumnsLayoutDiscoverInterestsLazyRoute: typeof ColumnsLayoutDiscoverInterestsLazyRoute
ColumnsLayoutDiscoverNewsfeedsLazyRoute: typeof ColumnsLayoutDiscoverNewsfeedsLazyRoute
ColumnsLayoutOnboardingLazyRoute: typeof ColumnsLayoutOnboardingLazyRoute
ColumnsLayoutSearchLazyRoute: typeof ColumnsLayoutSearchLazyRoute
ColumnsLayoutTrendingLazyRoute: typeof ColumnsLayoutTrendingLazyRoute
@ -620,6 +664,10 @@ const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = {
ColumnsLayoutCreateNewsfeedRoute:
ColumnsLayoutCreateNewsfeedRouteWithChildren,
ColumnsLayoutGlobalRoute: ColumnsLayoutGlobalRoute,
ColumnsLayoutDiscoverInterestsLazyRoute:
ColumnsLayoutDiscoverInterestsLazyRoute,
ColumnsLayoutDiscoverNewsfeedsLazyRoute:
ColumnsLayoutDiscoverNewsfeedsLazyRoute,
ColumnsLayoutOnboardingLazyRoute: ColumnsLayoutOnboardingLazyRoute,
ColumnsLayoutSearchLazyRoute: ColumnsLayoutSearchLazyRoute,
ColumnsLayoutTrendingLazyRoute: ColumnsLayoutTrendingLazyRoute,
@ -685,6 +733,8 @@ export interface FileRoutesByFullPath {
'/settings/$id/general': typeof SettingsIdGeneralRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute
'/settings/$id/wallet': typeof SettingsIdWalletRoute
'/columns/discover-interests': typeof ColumnsLayoutDiscoverInterestsLazyRoute
'/columns/discover-newsfeeds': typeof ColumnsLayoutDiscoverNewsfeedsLazyRoute
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
@ -720,6 +770,8 @@ export interface FileRoutesByTo {
'/settings/$id/general': typeof SettingsIdGeneralRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute
'/settings/$id/wallet': typeof SettingsIdWalletRoute
'/columns/discover-interests': typeof ColumnsLayoutDiscoverInterestsLazyRoute
'/columns/discover-newsfeeds': typeof ColumnsLayoutDiscoverNewsfeedsLazyRoute
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
@ -758,6 +810,8 @@ export interface FileRoutesById {
'/settings/$id/general': typeof SettingsIdGeneralRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute
'/settings/$id/wallet': typeof SettingsIdWalletRoute
'/columns/_layout/discover-interests': typeof ColumnsLayoutDiscoverInterestsLazyRoute
'/columns/_layout/discover-newsfeeds': typeof ColumnsLayoutDiscoverNewsfeedsLazyRoute
'/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
'/columns/_layout/search': typeof ColumnsLayoutSearchLazyRoute
'/columns/_layout/trending': typeof ColumnsLayoutTrendingLazyRoute
@ -796,6 +850,8 @@ export interface FileRouteTypes {
| '/settings/$id/general'
| '/settings/$id/relay'
| '/settings/$id/wallet'
| '/columns/discover-interests'
| '/columns/discover-newsfeeds'
| '/columns/onboarding'
| '/columns/search'
| '/columns/trending'
@ -830,6 +886,8 @@ export interface FileRouteTypes {
| '/settings/$id/general'
| '/settings/$id/relay'
| '/settings/$id/wallet'
| '/columns/discover-interests'
| '/columns/discover-newsfeeds'
| '/columns/onboarding'
| '/columns/search'
| '/columns/trending'
@ -866,6 +924,8 @@ export interface FileRouteTypes {
| '/settings/$id/general'
| '/settings/$id/relay'
| '/settings/$id/wallet'
| '/columns/_layout/discover-interests'
| '/columns/_layout/discover-newsfeeds'
| '/columns/_layout/onboarding'
| '/columns/_layout/search'
| '/columns/_layout/trending'
@ -975,6 +1035,8 @@ export const routeTree = rootRoute
"children": [
"/columns/_layout/create-newsfeed",
"/columns/_layout/global",
"/columns/_layout/discover-interests",
"/columns/_layout/discover-newsfeeds",
"/columns/_layout/onboarding",
"/columns/_layout/search",
"/columns/_layout/trending",
@ -1040,6 +1102,14 @@ export const routeTree = rootRoute
"filePath": "settings.$id/wallet.tsx",
"parent": "/settings/$id"
},
"/columns/_layout/discover-interests": {
"filePath": "columns/_layout/discover-interests.lazy.tsx",
"parent": "/columns/_layout"
},
"/columns/_layout/discover-newsfeeds": {
"filePath": "columns/_layout/discover-newsfeeds.lazy.tsx",
"parent": "/columns/_layout"
},
"/columns/_layout/onboarding": {
"filePath": "columns/_layout/onboarding.lazy.tsx",
"parent": "/columns/_layout"

View File

@ -0,0 +1,182 @@
import { commands } from "@/commands.gen";
import { toLumeEvents } from "@/commons";
import { Spinner, User } from "@/components";
import { LumeWindow } from "@/system";
import { ArrowDown } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { nanoid } from "nanoid";
import type { NostrEvent } from "nostr-tools";
import { type RefObject, useCallback, useRef } from "react";
import { Virtualizer } from "virtua";
export const Route = createLazyFileRoute("/columns/_layout/discover-interests")(
{
component: Screen,
},
);
function Screen() {
const {
isLoading,
isError,
error,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
data,
} = useInfiniteQuery({
queryKey: ["local-interests"],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const until = pageParam > 0 ? pageParam.toString() : null;
const res = await commands.getAllLocalInterests(until);
if (res.status === "ok") {
const data = toLumeEvents(res.data);
return data;
} else {
throw new Error(res.error);
}
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (lastEvent) {
return lastEvent.created_at - 1;
}
},
select: (data) =>
data?.pages
.flat()
.filter(
(item) => item.tags.filter((tag) => tag[0] === "p")?.length > 0,
),
refetchOnWindowFocus: false,
});
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback(
(item: NostrEvent) => {
const name =
item.tags.find((tag) => tag[0] === "title")?.[1] || "Unnamed";
const label =
item.tags.find((tag) => tag[0] === "label")?.[1] || nanoid();
return (
<div
key={item.id}
className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800"
>
<div className="px-2 pt-2">
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full"
>
<ScrollArea.Viewport className="p-3 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
<div className="flex flex-wrap items-center justify-center gap-2">
{item.tags
.filter((tag) => tag[0] === "t")
.map((tag) => (
<div key={tag[1]} className="text-sm font-medium">
{tag[1].includes("#") ? tag[1] : `#${tag[1]}`}
</div>
))}
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div>
<div className="p-3 flex items-center justify-between">
<div className="inline-flex items-center gap-2">
<User.Provider pubkey={item.pubkey}>
<User.Root>
<User.Avatar className="size-7 rounded-full" />
</User.Root>
</User.Provider>
<h5 className="text-xs font-medium">{name}</h5>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
label,
name,
url: `/columns/interests/${item.id}`,
})
}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-100 group-hover:bg-blue-600 dark:group-hover:bg-blue-400 group-hover:text-white"
>
Add
</button>
</div>
</div>
</div>
);
},
[data],
);
return (
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full"
>
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3">
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
{isLoading ? (
<div className="inline-flex items-center gap-1.5">
<Spinner className="size-4" />
Loading...
</div>
) : isError ? (
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">{error?.message ?? "Error"}</p>
</div>
) : !data?.length ? (
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">Empty.</p>
</div>
) : (
data?.map((item) => renderItem(item))
)}
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="h-11 w-full px-3 flex items-center justify-center gap-1.5 bg-neutral-200/50 hover:bg-neutral-200 rounded-full text-sm font-medium text-blue-600 dark:hover:bg-neutral-800 dark:bg-neutral-800/50 dark:text-blue-400"
>
{isFetchingNextPage ? (
<Spinner className="size-4" />
) : (
<>
<ArrowDown className="size-4" />
Load more
</>
)}
</button>
) : null}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
);
}

View File

@ -0,0 +1,184 @@
import { commands } from "@/commands.gen";
import { toLumeEvents } from "@/commons";
import { Spinner, User } from "@/components";
import { LumeWindow } from "@/system";
import { ArrowDown } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { nanoid } from "nanoid";
import type { NostrEvent } from "nostr-tools";
import { type RefObject, useCallback, useRef } from "react";
import { Virtualizer } from "virtua";
export const Route = createLazyFileRoute("/columns/_layout/discover-newsfeeds")(
{
component: Screen,
},
);
function Screen() {
const {
isLoading,
isError,
error,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
data,
} = useInfiniteQuery({
queryKey: ["local-newsfeeds"],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const until = pageParam > 0 ? pageParam.toString() : null;
const res = await commands.getAllLocalNewsfeeds(until);
if (res.status === "ok") {
const data = toLumeEvents(res.data);
return data;
} else {
throw new Error(res.error);
}
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (lastEvent) {
return lastEvent.created_at - 1;
}
},
select: (data) =>
data?.pages
.flat()
.filter(
(item) => item.tags.filter((tag) => tag[0] === "p")?.length > 0,
),
refetchOnWindowFocus: false,
});
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback(
(item: NostrEvent) => {
const name =
item.tags.find((tag) => tag[0] === "title")?.[1] || "Unnamed";
const label = item.tags.find((tag) => tag[0] === "d")?.[1] || nanoid();
return (
<div
key={item.id}
className="mb-3 group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800"
>
<div className="px-2 pt-2">
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full"
>
<ScrollArea.Viewport className="p-3 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
<div className="flex flex-wrap items-center justify-center gap-2">
{item.tags
.filter((tag) => tag[0] === "p")
.map((tag) => (
<User.Provider key={tag[1]} pubkey={tag[1]}>
<User.Root>
<User.Avatar className="size-8 rounded-full" />
</User.Root>
</User.Provider>
))}
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div>
<div className="p-2 flex items-center justify-between">
<div className="inline-flex items-center gap-2">
<User.Provider pubkey={item.pubkey}>
<User.Root>
<User.Avatar className="size-7 rounded-full" />
</User.Root>
</User.Provider>
<h5 className="text-xs font-medium">{name}</h5>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
label,
name,
url: `/columns/groups/${item.id}`,
})
}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-100 group-hover:bg-blue-600 dark:group-hover:bg-blue-400 group-hover:text-white"
>
Add
</button>
</div>
</div>
</div>
);
},
[data],
);
return (
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full"
>
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3">
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
{isLoading ? (
<div className="inline-flex items-center gap-1.5">
<Spinner className="size-4" />
Loading...
</div>
) : isError ? (
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">{error?.message ?? "Error"}</p>
</div>
) : !data?.length ? (
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">Empty.</p>
</div>
) : (
data?.map((item) => renderItem(item))
)}
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="h-11 w-full px-3 flex items-center justify-center gap-1.5 bg-neutral-200/50 hover:bg-neutral-200 rounded-full text-sm font-medium text-blue-600 dark:hover:bg-neutral-800 dark:bg-neutral-800/50 dark:text-blue-400"
>
{isFetchingNextPage ? (
<Spinner className="size-4" />
) : (
<>
<ArrowDown className="size-4" />
Load more
</>
)}
</button>
) : null}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
);
}

View File

@ -182,10 +182,20 @@ function Newsfeeds() {
) : (
data?.map((item) => renderItem(item))
)}
<div className="h-12 px-3 flex items-center justify-between items-betwe bg-neutral-200/50 rounded-xl text-blue-600 dark:text-blue-400">
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
name: "Newsfeeds",
url: "/columns/discover-newsfeeds",
label: "discover_newsfeeds",
})
}
className="h-12 px-3 flex items-center justify-between bg-neutral-200/50 rounded-xl text-blue-600 dark:text-blue-400"
>
<span className="text-sm font-medium">Discover newsfeeds</span>
<ArrowRight className="size-4" weight="bold" />
</div>
</button>
</div>
</div>
);
@ -326,10 +336,20 @@ function Interests() {
) : (
data?.map((item) => renderItem(item))
)}
<div className="h-12 px-3 flex items-center justify-between items-betwe bg-neutral-200/50 rounded-xl text-blue-600 dark:text-blue-400">
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
name: "Interests",
url: "/columns/discover-interests",
label: "discover_interests",
})
}
className="h-12 px-3 flex items-center justify-between bg-neutral-200/50 rounded-xl text-blue-600 dark:text-blue-400"
>
<span className="text-sm font-medium">Discover interests</span>
<ArrowRight className="size-4" weight="bold" />
</div>
</button>
</div>
</div>
);

View File

@ -2,55 +2,61 @@ import { commands } from "@/commands.gen";
import type { NostrEvent } from "@/types";
import { useQuery } from "@tanstack/react-query";
import { nip19 } from "nostr-tools";
import { useMemo } from "react";
import { LumeEvent } from "./event";
export function useEvent(id: string, repost?: string) {
const hex = useMemo(() => {
try {
const normalized = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
const decoded = nip19.decode(normalized);
switch (decoded.type) {
case "note":
return decoded.data;
case "nevent":
return decoded.data.id;
default:
return normalized;
}
} catch {
return id;
}
}, [id]);
const { isLoading, isError, error, data } = useQuery({
queryKey: ["ids", "event", id],
queryFn: async () => {
try {
if (repost?.length) {
const nostrEvent: NostrEvent = JSON.parse(repost);
const res = await commands.getMetaFromEvent(nostrEvent.content);
if (res.status === "ok") {
nostrEvent.meta = res.data;
}
return new LumeEvent(nostrEvent);
}
let normalizedId = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
if (normalizedId.startsWith("nevent")) {
const decoded = nip19.decode(normalizedId);
if (decoded.type === "nevent") {
normalizedId = decoded.data.id;
}
}
const res = await commands.getEvent(normalizedId);
if (repost?.length) {
const nostrEvent: NostrEvent = JSON.parse(repost);
const res = await commands.getMetaFromEvent(nostrEvent.content);
if (res.status === "ok") {
const data = res.data;
const raw: NostrEvent = JSON.parse(data.raw);
if (data.parsed) {
raw.meta = data.parsed;
}
return new LumeEvent(raw);
} else {
throw new Error(res.error);
nostrEvent.meta = res.data;
}
} catch (e) {
throw new Error(String(e));
return new LumeEvent(nostrEvent);
}
const res = await commands.getEvent(hex);
if (res.status === "ok") {
const data = res.data;
const raw: NostrEvent = JSON.parse(data.raw);
if (data.parsed) {
raw.meta = data.parsed;
}
return new LumeEvent(raw);
} else {
throw new Error(res.error);
}
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
enabled: !!hex,
});
return { isLoading, isError, error, data };

View File

@ -19,7 +19,7 @@ export function useProfile(pubkey: string, data?: string) {
case "naddr":
return decoded.data.pubkey;
default:
return pubkey;
return normalized;
}
} catch {
return pubkey;