mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-29 11:12:12 +01:00
allow user to select people list for home feed
This commit is contained in:
parent
1740c813a9
commit
f6f465611d
5
.changeset/proud-pillows-promise.md
Normal file
5
.changeset/proud-pillows-promise.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Allow user to select people list for home feed
|
@ -4,12 +4,10 @@ import { Spinner } from "@chakra-ui/react";
|
||||
import { ErrorBoundary } from "./components/error-boundary";
|
||||
import Layout from "./components/layout";
|
||||
|
||||
import HomeView from "./views/home";
|
||||
import HomeView from "./views/home/index";
|
||||
import SettingsView from "./views/settings";
|
||||
import LoginView from "./views/login";
|
||||
import ProfileView from "./views/profile";
|
||||
import FollowingTab from "./views/home/following-tab";
|
||||
import GlobalTab from "./views/home/global-tab";
|
||||
import HashTagView from "./views/hashtag";
|
||||
import UserView from "./views/user";
|
||||
import UserNotesTab from "./views/user/notes";
|
||||
@ -137,11 +135,6 @@ const router = createHashRouter([
|
||||
{
|
||||
path: "",
|
||||
element: <HomeView />,
|
||||
children: [
|
||||
{ path: "", element: <FollowingTab /> },
|
||||
{ path: "following", element: <FollowingTab /> },
|
||||
{ path: "global", element: <GlobalTab /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -37,13 +37,15 @@ export class Subject<Value> implements Connectable<Value> {
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(listener: ListenerFn<Value>, ctx?: Object) {
|
||||
subscribe(listener: ListenerFn<Value>, ctx?: Object, initCall = true) {
|
||||
if (!this.findListener(listener, ctx)) {
|
||||
this.listeners.push([listener, ctx]);
|
||||
|
||||
if (this.value !== undefined) {
|
||||
if (ctx) listener.call(ctx, this.value);
|
||||
else listener(this.value);
|
||||
if (initCall) {
|
||||
if (this.value !== undefined) {
|
||||
if (ctx) listener.call(ctx, this.value);
|
||||
else listener(this.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
|
@ -1,46 +1,57 @@
|
||||
import { Select, SelectProps } from "@chakra-ui/react";
|
||||
import { usePeopleListContext } from "./people-list-provider";
|
||||
import useUserLists from "../../hooks/use-user-lists";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { getListName } from "../../helpers/nostr/lists";
|
||||
import { getEventCoordinate } from "../../helpers/nostr/events";
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuItemOption,
|
||||
MenuList,
|
||||
MenuOptionGroup,
|
||||
} from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
function UserListOptions() {
|
||||
const account = useCurrentAccount()!;
|
||||
const lists = useUserLists(account?.pubkey);
|
||||
|
||||
return (
|
||||
<>
|
||||
{lists.map((list) => (
|
||||
<option key={getEventCoordinate(list)} value={getEventCoordinate(list)}>
|
||||
{getListName(list)}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { usePeopleListContext } from "../../providers/people-list-provider";
|
||||
import useUserLists from "../../hooks/use-user-lists";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { PEOPLE_LIST_KIND, getListName } from "../../helpers/nostr/lists";
|
||||
import { getEventCoordinate } from "../../helpers/nostr/events";
|
||||
|
||||
export default function PeopleListSelection({
|
||||
hideGlobalOption = false,
|
||||
...props
|
||||
}: {
|
||||
hideGlobalOption?: boolean;
|
||||
} & Omit<SelectProps, "value" | "onChange" | "children">) {
|
||||
const account = useCurrentAccount()!;
|
||||
const { list, setList } = usePeopleListContext();
|
||||
} & Omit<ButtonProps, "children">) {
|
||||
const account = useCurrentAccount();
|
||||
const lists = useUserLists(account?.pubkey);
|
||||
const { list, setList, listEvent } = usePeopleListContext();
|
||||
|
||||
const handleSelect = (value: string | string[]) => {
|
||||
console.log(value);
|
||||
if (typeof value === "string") {
|
||||
setList(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={list}
|
||||
onChange={(e) => {
|
||||
setList(e.target.value === "global" ? undefined : e.target.value);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{account && <option value={`${Kind.Contacts}:${account.pubkey}`}>Following</option>}
|
||||
{!hideGlobalOption && <option value="global">Global</option>}
|
||||
{account && <UserListOptions />}
|
||||
</Select>
|
||||
<Menu>
|
||||
<MenuButton as={Button} {...props}>
|
||||
{listEvent ? getListName(listEvent) : list === "global" ? "Global" : "Following"}
|
||||
</MenuButton>
|
||||
<MenuList zIndex={100}>
|
||||
<MenuOptionGroup value={list} onChange={handleSelect} type="radio">
|
||||
{account && <MenuItemOption value={`${Kind.Contacts}:${account.pubkey}`}>Following</MenuItemOption>}
|
||||
{!hideGlobalOption && <MenuItemOption value="global">Global</MenuItemOption>}
|
||||
{account && <MenuDivider />}
|
||||
{lists
|
||||
.filter((l) => l.kind === PEOPLE_LIST_KIND)
|
||||
.map((list) => (
|
||||
<MenuItemOption key={getEventCoordinate(list)} value={getEventCoordinate(list)} isTruncated maxW="90vw">
|
||||
{getListName(list)}
|
||||
</MenuItemOption>
|
||||
))}
|
||||
</MenuOptionGroup>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
@ -1,15 +1,20 @@
|
||||
import { Button, ButtonProps } from "@chakra-ui/react";
|
||||
import { Button, ButtonProps, useDisclosure } from "@chakra-ui/react";
|
||||
import { RelayIcon } from "../icons";
|
||||
import { useRelaySelectionContext } from "../../providers/relay-selection-provider";
|
||||
import RelaySelectionModal from "./relay-selection-modal";
|
||||
|
||||
export default function RelaySelectionButton({ ...props }: ButtonProps) {
|
||||
const { openModal, relays } = useRelaySelectionContext();
|
||||
const relaysModal = useDisclosure();
|
||||
const { setSelected, relays } = useRelaySelectionContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button leftIcon={<RelayIcon />} onClick={openModal} {...props}>
|
||||
<Button leftIcon={<RelayIcon />} onClick={relaysModal.onOpen} {...props}>
|
||||
{relays.length} {relays.length === 1 ? "Relay" : "Relays"}
|
||||
</Button>
|
||||
{relaysModal.isOpen && (
|
||||
<RelaySelectionModal selected={relays} onSubmit={setSelected} onClose={relaysModal.onClose} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ function UsersLists({ pubkey }: { pubkey: string }) {
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const newListModal = useDisclosure();
|
||||
|
||||
const lists = useUserLists(pubkey);
|
||||
const lists = useUserLists(account.pubkey);
|
||||
|
||||
const inLists = lists.filter((list) => getPubkeysFromList(list).some((p) => p.pubkey === pubkey));
|
||||
|
||||
|
@ -7,12 +7,9 @@ function useSubject<Value extends unknown>(subject?: Subject<Value>): Value | un
|
||||
function useSubject<Value extends unknown>(subject?: Subject<Value>) {
|
||||
const [_, setValue] = useState(subject?.value);
|
||||
useEffect(() => {
|
||||
const handler = (value: Value) => setValue(value);
|
||||
setValue(subject?.value);
|
||||
subject?.subscribe(handler);
|
||||
|
||||
subject?.subscribe(setValue, undefined, false);
|
||||
return () => {
|
||||
subject?.unsubscribe(handler);
|
||||
subject?.unsubscribe(setValue, undefined);
|
||||
};
|
||||
}, [subject, setValue]);
|
||||
|
||||
|
@ -3,12 +3,17 @@ import { useReadRelayUrls } from "./use-client-relays";
|
||||
import useSubject from "./use-subject";
|
||||
import useTimelineLoader from "./use-timeline-loader";
|
||||
|
||||
export default function useUserLists(pubkey: string, additionalRelays: string[] = []) {
|
||||
export default function useUserLists(pubkey?: string, additionalRelays: string[] = []) {
|
||||
const readRelays = useReadRelayUrls(additionalRelays);
|
||||
const timeline = useTimelineLoader(`${pubkey}-lists`, readRelays, {
|
||||
authors: [pubkey],
|
||||
kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
|
||||
});
|
||||
const timeline = useTimelineLoader(
|
||||
`${pubkey}-lists`,
|
||||
readRelays,
|
||||
{
|
||||
authors: pubkey ? [pubkey] : [],
|
||||
kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
|
||||
},
|
||||
{ enabled: !!pubkey },
|
||||
);
|
||||
|
||||
return useSubject(timeline.timeline);
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { PropsWithChildren, createContext, useContext, useMemo, useState } from "react";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { getPubkeysFromList } from "../../helpers/nostr/lists";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import { getPubkeysFromList } from "../helpers/nostr/lists";
|
||||
import useReplaceableEvent from "../hooks/use-replaceable-event";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
|
||||
export type PeopleListContextType = {
|
||||
list?: string;
|
||||
listEvent?: NostrEvent;
|
||||
people: { pubkey: string; relay?: string }[] | undefined;
|
||||
setList: (list: string | undefined) => void;
|
||||
};
|
||||
@ -26,9 +28,10 @@ export default function PeopleListProvider({ children }: PropsWithChildren) {
|
||||
() => ({
|
||||
people,
|
||||
list: listCord,
|
||||
listEvent,
|
||||
setList,
|
||||
}),
|
||||
[listCord, setList, people],
|
||||
[listCord, setList, people, listEvent],
|
||||
);
|
||||
|
||||
return <PeopleListContext.Provider value={context}>{children}</PeopleListContext.Provider>;
|
@ -1,20 +1,17 @@
|
||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
|
||||
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import RelaySelectionModal from "../components/relay-selection/relay-selection-modal";
|
||||
import { unique } from "../helpers/array";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
||||
import { unique } from "../helpers/array";
|
||||
|
||||
type RelaySelectionContextType = {
|
||||
relays: string[];
|
||||
setSelected: (relays: string[]) => void;
|
||||
openModal: () => void;
|
||||
};
|
||||
|
||||
export const RelaySelectionContext = createContext<RelaySelectionContextType>({
|
||||
relays: [],
|
||||
setSelected: () => {},
|
||||
openModal: () => {},
|
||||
});
|
||||
|
||||
export function useRelaySelectionContext() {
|
||||
@ -34,7 +31,6 @@ export default function RelaySelectionProvider({
|
||||
overrideDefault,
|
||||
additionalDefaults,
|
||||
}: RelaySelectionProviderProps) {
|
||||
const relaysModal = useDisclosure();
|
||||
const { state } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -44,19 +40,22 @@ export default function RelaySelectionProvider({
|
||||
if (overrideDefault) return overrideDefault;
|
||||
if (additionalDefaults) return unique([...userReadRelays, ...additionalDefaults]);
|
||||
return userReadRelays;
|
||||
}, [state?.relays, overrideDefault, userReadRelays, additionalDefaults]);
|
||||
}, [state?.relays, overrideDefault, userReadRelays.join("|"), additionalDefaults]);
|
||||
|
||||
const setSelected = useCallback((relays: string[]) => {
|
||||
navigate(".", { state: { relays }, replace: true });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RelaySelectionContext.Provider value={{ relays, setSelected, openModal: relaysModal.onOpen }}>
|
||||
{children}
|
||||
|
||||
{relaysModal.isOpen && (
|
||||
<RelaySelectionModal selected={relays} onSubmit={setSelected} onClose={relaysModal.onClose} />
|
||||
)}
|
||||
</RelaySelectionContext.Provider>
|
||||
const setSelected = useCallback(
|
||||
(relays: string[]) => {
|
||||
navigate(".", { state: { relays }, replace: true });
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
relays,
|
||||
setSelected,
|
||||
}),
|
||||
[relays.join("|"), setSelected],
|
||||
);
|
||||
|
||||
return <RelaySelectionContext.Provider value={context}>{children}</RelaySelectionContext.Provider>;
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ export function TrustProvider({
|
||||
const parentTrust = useContext(TrustContext);
|
||||
|
||||
const account = useCurrentAccount();
|
||||
const contacts = useUserContactList(account?.pubkey)
|
||||
const following = contacts ? getPubkeysFromList(contacts).map(p => p.pubkey) : []
|
||||
const contactList = useUserContactList(account?.pubkey);
|
||||
const following = contactList ? getPubkeysFromList(contactList).map((p) => p.pubkey) : [];
|
||||
|
||||
const isEventTrusted = trust || (!!event && (event.pubkey === account?.pubkey || following.includes(event.pubkey)));
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
import db from "./db";
|
||||
import { fetchWithCorsFallback } from "../helpers/cors";
|
||||
import { isHex } from "../helpers/nip19";
|
||||
|
||||
export type RelayInformationDocument = {
|
||||
name: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
pubkey: string;
|
||||
pubkey?: string;
|
||||
contact: string;
|
||||
supported_nips?: number[];
|
||||
software: string;
|
||||
@ -13,6 +14,13 @@ export type RelayInformationDocument = {
|
||||
payments_url?: string;
|
||||
};
|
||||
|
||||
function sanitizeInfo(info: RelayInformationDocument) {
|
||||
if (info.pubkey && !isHex(info.pubkey)) {
|
||||
delete info.pubkey;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
async function fetchInfo(relay: string) {
|
||||
const url = new URL(relay);
|
||||
url.protocol = url.protocol === "ws:" ? "http" : "https";
|
||||
@ -21,6 +29,8 @@ async function fetchInfo(relay: string) {
|
||||
(res) => res.json() as Promise<RelayInformationDocument>,
|
||||
);
|
||||
|
||||
sanitizeInfo(infoDoc);
|
||||
|
||||
memoryCache.set(relay, infoDoc);
|
||||
await db.put("relayInfo", infoDoc, relay);
|
||||
|
||||
|
@ -1,65 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { Flex, FormControl, FormLabel, Switch } from "@chakra-ui/react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import { isReply, truncatedId } from "../../helpers/nostr/events";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
|
||||
import TimelineViewTypeButtons from "../../components/timeline-page/timeline-view-type";
|
||||
import useUserContactList from "../../hooks/use-user-contact-list";
|
||||
import { getPubkeysFromList } from "../../helpers/nostr/lists";
|
||||
|
||||
function FollowingTabBody() {
|
||||
const account = useCurrentAccount()!;
|
||||
const contacts = useUserContactList(account.pubkey);
|
||||
const [search, setSearch] = useSearchParams();
|
||||
const showReplies = search.has("replies");
|
||||
const onToggle = () => {
|
||||
showReplies ? setSearch({}) : setSearch({ replies: "show" });
|
||||
};
|
||||
|
||||
const timelinePageEventFilter = useTimelinePageEventFilter();
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
if (!showReplies && isReply(event)) return false;
|
||||
return timelinePageEventFilter(event);
|
||||
},
|
||||
[showReplies, timelinePageEventFilter],
|
||||
);
|
||||
|
||||
const following = contacts ? getPubkeysFromList(contacts).map((p) => p.pubkey) : [];
|
||||
const readRelays = useReadRelayUrls();
|
||||
const timeline = useTimelineLoader(
|
||||
`${truncatedId(account.pubkey)}-following`,
|
||||
readRelays,
|
||||
{ authors: following, kinds: [Kind.Text, Kind.Repost, 2] },
|
||||
{ enabled: following.length > 0, eventFilter },
|
||||
);
|
||||
|
||||
const header = (
|
||||
<Flex px="2">
|
||||
<FormControl display="flex" alignItems="center">
|
||||
<FormLabel htmlFor="show-replies" mb="0">
|
||||
Show Replies
|
||||
</FormLabel>
|
||||
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} />
|
||||
</FormControl>
|
||||
<TimelineViewTypeButtons />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return <TimelinePage timeline={timeline} header={header} pt="4" pb="8" />;
|
||||
}
|
||||
|
||||
export default function FollowingTab() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<FollowingTabBody />
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { Flex, FormControl, FormLabel, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import { isReply } from "../../helpers/nostr/events";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
|
||||
import useRelaysChanged from "../../hooks/use-relays-changed";
|
||||
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
|
||||
import TimelineViewTypeButtons from "../../components/timeline-page/timeline-view-type";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { safeUrl } from "../../helpers/parse";
|
||||
|
||||
function GlobalPage() {
|
||||
const readRelays = useRelaySelectionRelays();
|
||||
const { isOpen: showReplies, onToggle } = useDisclosure();
|
||||
|
||||
useAppTitle("global");
|
||||
|
||||
const timelineEventFilter = useTimelinePageEventFilter();
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
if (!showReplies && isReply(event)) return false;
|
||||
return timelineEventFilter(event);
|
||||
},
|
||||
[showReplies, timelineEventFilter],
|
||||
);
|
||||
const timeline = useTimelineLoader(`global`, readRelays, { kinds: [1] }, { eventFilter });
|
||||
useRelaysChanged(readRelays, () => timeline.reset());
|
||||
|
||||
const header = (
|
||||
<Flex gap="2" pr="2">
|
||||
<RelaySelectionButton />
|
||||
<FormControl display="flex" alignItems="center">
|
||||
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} mr="2" />
|
||||
<FormLabel htmlFor="show-replies" mb="0">
|
||||
Show Replies
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
<TimelineViewTypeButtons />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return <TimelinePage timeline={timeline} header={header} pt="4" pb="8" />;
|
||||
}
|
||||
|
||||
export default function GlobalTab() {
|
||||
const [params] = useSearchParams();
|
||||
|
||||
// wrap the global page with another relay selection so it dose not effect the rest of the app
|
||||
let relays = ["wss://welcome.nostr.wine"];
|
||||
|
||||
const setRelay = params.get("relay");
|
||||
if (setRelay) {
|
||||
const url = safeUrl(setRelay);
|
||||
relays = [setRelay];
|
||||
}
|
||||
|
||||
return (
|
||||
<RelaySelectionProvider overrideDefault={relays}>
|
||||
<GlobalPage />
|
||||
</RelaySelectionProvider>
|
||||
);
|
||||
}
|
@ -1,40 +1,55 @@
|
||||
import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
|
||||
import { Outlet, useMatches, useNavigate } from "react-router-dom";
|
||||
import { useCallback } from "react";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
const tabs = [
|
||||
{ label: "Following", path: "/following" },
|
||||
{ label: "Global", path: "/global" },
|
||||
];
|
||||
import { isReply, truncatedId } from "../../helpers/nostr/events";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
|
||||
import TimelineViewTypeButtons from "../../components/timeline-page/timeline-view-type";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
|
||||
|
||||
function HomePage() {
|
||||
const timelinePageEventFilter = useTimelinePageEventFilter();
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
if (isReply(event)) return false;
|
||||
return timelinePageEventFilter(event);
|
||||
},
|
||||
[timelinePageEventFilter],
|
||||
);
|
||||
|
||||
const { relays } = useRelaySelectionContext();
|
||||
const { people, list } = usePeopleListContext();
|
||||
|
||||
const kinds = [Kind.Text, Kind.Repost, 2];
|
||||
const query = people && people.length > 0 ? { authors: people.map((p) => p.pubkey), kinds } : { kinds };
|
||||
const timeline = useTimelineLoader(`${list}-home-feed`, relays, query, {
|
||||
enabled: !!people && people.length > 0,
|
||||
eventFilter,
|
||||
});
|
||||
|
||||
const header = (
|
||||
<Flex gap="2" wrap="wrap" px={["2", 0]}>
|
||||
<PeopleListSelection />
|
||||
<RelaySelectionButton ml="auto" />
|
||||
<TimelineViewTypeButtons />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return <TimelinePage timeline={timeline} header={header} pt="2" pb="8" />;
|
||||
}
|
||||
|
||||
export default function HomeView() {
|
||||
const navigate = useNavigate();
|
||||
const matches = useMatches();
|
||||
|
||||
const activeTab = tabs.indexOf(tabs.find((t) => matches[matches.length - 1].pathname === t.path) ?? tabs[0]);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
flexGrow="1"
|
||||
overflow="hidden"
|
||||
isLazy
|
||||
index={activeTab}
|
||||
onChange={(v) => navigate(tabs[v].path)}
|
||||
colorScheme="brand"
|
||||
>
|
||||
<TabList>
|
||||
{tabs.map(({ label }) => (
|
||||
<Tab key={label}>{label}</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanels overflow="hidden" h="full">
|
||||
{tabs.map(({ label }) => (
|
||||
<TabPanel key={label} p={0} overflow="hidden" h="full" display="flex" flexDirection="column">
|
||||
<Outlet />
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<PeopleListProvider>
|
||||
<RelaySelectionProvider>
|
||||
<HomePage />
|
||||
</RelaySelectionProvider>
|
||||
</PeopleListProvider>
|
||||
);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ export default function ListCard({ cord, event: maybeEvent }: { cord?: string; e
|
||||
<Text>{people.length} people</Text>
|
||||
<AvatarGroup overflow="hidden" mb="2">
|
||||
{people.map(({ pubkey, relay }) => (
|
||||
<UserAvatarLink pubkey={pubkey} relay={relay} />
|
||||
<UserAvatarLink key={pubkey} pubkey={pubkey} relay={relay} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
</>
|
||||
|
@ -15,7 +15,6 @@ import { DraftNostrEvent } from "../../types/nostr-event";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import { Message } from "./message";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { truncatedId } from "../../helpers/nostr/events";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
@ -31,7 +30,7 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
|
||||
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-${truncatedId(account.pubkey)}-messages`, readRelays, [
|
||||
const timeline = useTimelineLoader(`${pubkey}-${account.pubkey}-messages`, readRelays, [
|
||||
{
|
||||
kinds: [Kind.EncryptedDirectMessage],
|
||||
"#p": [account.pubkey],
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Badge } from "@chakra-ui/react";
|
||||
import { Badge, BadgeProps } from "@chakra-ui/react";
|
||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
|
||||
export default function StreamStatusBadge({ stream }: { stream: ParsedStream }) {
|
||||
export default function StreamStatusBadge({ stream, ...props }: { stream: ParsedStream } & Omit<BadgeProps, 'children'>) {
|
||||
switch (stream.status) {
|
||||
case "live":
|
||||
return <Badge colorScheme="green">live</Badge>;
|
||||
return <Badge colorScheme="green" {...props}>live</Badge>;
|
||||
case "ended":
|
||||
return <Badge colorScheme="red">ended</Badge>;
|
||||
return <Badge colorScheme="red" {...props}>ended</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1,32 +1,16 @@
|
||||
import { useRef } from "react";
|
||||
import { memo, useRef } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { Box, Card, CardBody, CardProps, Flex, Heading, LinkBox, LinkOverlay, Tag, Text } from "@chakra-ui/react";
|
||||
|
||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardProps,
|
||||
Divider,
|
||||
Flex,
|
||||
Heading,
|
||||
Image,
|
||||
LinkBox,
|
||||
LinkOverlay,
|
||||
Spacer,
|
||||
Tag,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { UserAvatar } from "../../../components/user-avatar";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
import dayjs from "dayjs";
|
||||
import StreamStatusBadge from "./status-badge";
|
||||
import { EventRelays } from "../../../components/note/note-relays";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
import useEventNaddr from "../../../hooks/use-event-naddr";
|
||||
import StreamDebugButton from "./stream-debug-button";
|
||||
|
||||
export default function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
|
||||
function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
|
||||
const { title, image } = stream;
|
||||
|
||||
// if there is a parent intersection observer, register this card
|
||||
@ -36,9 +20,17 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
|
||||
const naddr = useEventNaddr(stream.event, stream.relays);
|
||||
|
||||
return (
|
||||
<Card {...props} ref={ref}>
|
||||
<Card {...props} ref={ref} position="relative">
|
||||
<LinkBox as={CardBody} p="2" display="flex" flexDirection="column" gap="2">
|
||||
{image && <Image src={image} alt={title} borderRadius="lg" />}
|
||||
<StreamStatusBadge stream={stream} position="absolute" top="4" left="4" />
|
||||
<Box
|
||||
backgroundImage={image}
|
||||
backgroundPosition="center"
|
||||
backgroundRepeat="no-repeat"
|
||||
backgroundSize="cover"
|
||||
aspectRatio={16 / 9}
|
||||
borderRadius="lg"
|
||||
/>
|
||||
<Flex gap="2" alignItems="center">
|
||||
<UserAvatar pubkey={stream.host} size="sm" noProxy />
|
||||
<Heading size="sm">
|
||||
@ -59,13 +51,7 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
|
||||
)}
|
||||
{stream.starts && <Text>Started: {dayjs.unix(stream.starts).fromNow()}</Text>}
|
||||
</LinkBox>
|
||||
<Divider />
|
||||
<CardFooter p="2" display="flex" gap="2" alignItems="center">
|
||||
<StreamStatusBadge stream={stream} />
|
||||
<Spacer />
|
||||
<EventRelays event={stream.event} />
|
||||
<StreamDebugButton stream={stream} variant="ghost" size="sm" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
export default memo(StreamCard);
|
||||
|
@ -1,34 +1,20 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Code, Flex, Select, SimpleGrid } from "@chakra-ui/react";
|
||||
import { Divider, Flex, Heading, Select, SimpleGrid } from "@chakra-ui/react";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import StreamCard from "./components/stream-card";
|
||||
import { STREAM_KIND, parseStreamEvent } from "../../helpers/nostr/stream";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
|
||||
import useRelaysChanged from "../../hooks/use-relays-changed";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../components/people-list-selection/people-list-provider";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||
import useParsedStreams from "../../hooks/use-parsed-streams";
|
||||
|
||||
function StreamsPage() {
|
||||
const relays = useRelaySelectionRelays();
|
||||
const [filterStatus, setFilterStatus] = useState<string>("live");
|
||||
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
try {
|
||||
const parsed = parseStreamEvent(event);
|
||||
return parsed.status === filterStatus;
|
||||
} catch (e) {}
|
||||
return false;
|
||||
},
|
||||
[filterStatus],
|
||||
);
|
||||
|
||||
const { people, list } = usePeopleListContext();
|
||||
const query =
|
||||
@ -39,7 +25,7 @@ function StreamsPage() {
|
||||
]
|
||||
: { kinds: [STREAM_KIND] };
|
||||
|
||||
const timeline = useTimelineLoader(`${list}-streams`, relays, query, { eventFilter });
|
||||
const timeline = useTimelineLoader(`${list}-streams`, relays, query);
|
||||
|
||||
useRelaysChanged(relays, () => timeline.reset());
|
||||
|
||||
@ -48,19 +34,25 @@ function StreamsPage() {
|
||||
const events = useSubject(timeline.timeline);
|
||||
const streams = useParsedStreams(events);
|
||||
|
||||
const liveStreams = streams.filter((stream) => stream.status === "live");
|
||||
const endedStreams = streams.filter((stream) => stream.status === "ended");
|
||||
|
||||
return (
|
||||
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
||||
<Flex gap="2" wrap="wrap">
|
||||
<PeopleListSelection w={["full", "xs"]} />
|
||||
<Select w={["full", "xs"]} value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
|
||||
<option value="live">Live</option>
|
||||
<option value="ended">Ended</option>
|
||||
</Select>
|
||||
<RelaySelectionButton ml="auto" />
|
||||
</Flex>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2">
|
||||
{streams.map((stream) => (
|
||||
{liveStreams.map((stream) => (
|
||||
<StreamCard key={stream.event.id} stream={stream} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<Heading>Ended</Heading>
|
||||
<Divider />
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2">
|
||||
{endedStreams.map((stream) => (
|
||||
<StreamCard key={stream.event.id} stream={stream} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
import { ChatIcon, ClipboardIcon, CodeIcon, ExternalLinkIcon, RelayIcon, SpyIcon } from "../../../components/icons";
|
||||
@ -10,7 +11,6 @@ import { getUserDisplayName } from "../../../helpers/user-metadata";
|
||||
import { useUserRelays } from "../../../hooks/use-user-relays";
|
||||
import { RelayMode } from "../../../classes/relay";
|
||||
import UserDebugModal from "../../../components/debug-modals/user-debug-modal";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
|
||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||
import { truncatedId } from "../../../helpers/nostr/events";
|
||||
|
@ -29,7 +29,7 @@ export default function UserFollowersTab() {
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
const readRelays = useReadRelayUrls(contextRelays);
|
||||
|
||||
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-followers`, readRelays, {
|
||||
const timeline = useTimelineLoader(`${pubkey}-followers`, readRelays, {
|
||||
"#p": [pubkey],
|
||||
kinds: [Kind.Contacts],
|
||||
});
|
||||
|
@ -56,7 +56,7 @@ export default function UserLikesTab() {
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
const readRelays = useReadRelayUrls(contextRelays);
|
||||
|
||||
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-likes`, readRelays, { authors: [pubkey], kinds: [7] });
|
||||
const timeline = useTimelineLoader(`${pubkey}-likes`, readRelays, { authors: [pubkey], kinds: [7] });
|
||||
|
||||
const likes = useSubject(timeline.timeline);
|
||||
|
||||
|
@ -56,7 +56,7 @@ const UserRelaysTab = () => {
|
||||
const userRelays = useUserRelays(pubkey);
|
||||
|
||||
const readRelays = useReadRelayUrls(userRelays.map((r) => r.url));
|
||||
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-relay-reviews`, readRelays, {
|
||||
const timeline = useTimelineLoader(`${pubkey}-relay-reviews`, readRelays, {
|
||||
authors: [pubkey],
|
||||
kinds: [1985],
|
||||
"#l": ["review/relay"],
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Flex, Text } from "@chakra-ui/react";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
|
||||
import { NoteLink } from "../../components/note-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { filterTagsByContentRefs, truncatedId } from "../../helpers/nostr/events";
|
||||
import { filterTagsByContentRefs } from "../../helpers/nostr/events";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
@ -39,7 +40,7 @@ export default function UserReportsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
|
||||
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-reports`, contextRelays, {
|
||||
const timeline = useTimelineLoader(`${pubkey}-reports`, contextRelays, {
|
||||
authors: [pubkey],
|
||||
kinds: [1984],
|
||||
});
|
||||
|
@ -8,7 +8,6 @@ import { NoteLink } from "../../components/note-link";
|
||||
import { UserAvatarLink } from "../../components/user-avatar-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { readablizeSats } from "../../helpers/bolt11";
|
||||
import { truncatedId } from "../../helpers/nostr/events";
|
||||
import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/zaps";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
@ -90,12 +89,7 @@ const UserZapsTab = () => {
|
||||
[filter],
|
||||
);
|
||||
|
||||
const timeline = useTimelineLoader(
|
||||
`${truncatedId(pubkey)}-zaps`,
|
||||
relays,
|
||||
{ "#p": [pubkey], kinds: [9735] },
|
||||
{ eventFilter },
|
||||
);
|
||||
const timeline = useTimelineLoader(`${pubkey}-zaps`, relays, { "#p": [pubkey], kinds: [9735] }, { eventFilter });
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
const zaps = useMemo(() => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user