mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-28 18:53:47 +01:00
Merge branch 'next'
This commit is contained in:
commit
4934b5ecf2
5
.changeset/eighty-phones-switch.md
Normal file
5
.changeset/eighty-phones-switch.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Rebuild search view to use NIP-50
|
5
.changeset/shaggy-toys-explode.md
Normal file
5
.changeset/shaggy-toys-explode.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for playing back stream recordings
|
5
.changeset/smart-students-peel.md
Normal file
5
.changeset/smart-students-peel.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Correctly handle replaceable events in timeline loader
|
60
src/classes/event-store.ts
Normal file
60
src/classes/event-store.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { getEventUID } from "../helpers/nostr/event";
|
||||||
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
import Subject from "./subject";
|
||||||
|
|
||||||
|
type EventFilter = (event: NostrEvent) => boolean;
|
||||||
|
export default class EventStore {
|
||||||
|
name?: string;
|
||||||
|
events = new Map<string, NostrEvent>();
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortedEvents() {
|
||||||
|
return Array.from(this.events.values()).sort((a, b) => b.created_at - a.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
onEvent = new Subject<NostrEvent>();
|
||||||
|
onClear = new Subject();
|
||||||
|
|
||||||
|
addEvent(event: NostrEvent) {
|
||||||
|
const id = getEventUID(event);
|
||||||
|
const existing = this.events.get(id);
|
||||||
|
if (!existing || event.created_at > existing.created_at) {
|
||||||
|
this.events.set(id, event);
|
||||||
|
this.onEvent.next(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.events.clear();
|
||||||
|
this.onClear.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(other: EventStore) {
|
||||||
|
other.onEvent.subscribe(this.addEvent, this);
|
||||||
|
}
|
||||||
|
disconnect(other: EventStore) {
|
||||||
|
other.onEvent.unsubscribe(this.addEvent, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstEvent(nth = 0, filter?: EventFilter) {
|
||||||
|
const events = this.getSortedEvents();
|
||||||
|
const filteredEvents = filter ? events.filter(filter) : events;
|
||||||
|
for (let i = 0; i <= nth; i++) {
|
||||||
|
const event = filteredEvents[i];
|
||||||
|
if (event) return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getLastEvent(nth = 0, filter?: EventFilter) {
|
||||||
|
const events = this.getSortedEvents();
|
||||||
|
const filteredEvents = filter ? events.filter(filter) : events;
|
||||||
|
for (let i = nth; i >= 0; i--) {
|
||||||
|
const event = filteredEvents[filteredEvents.length - 1 - i];
|
||||||
|
if (event) return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { getReferences } from "../helpers/nostr-event";
|
import { getReferences } from "../helpers/nostr/event";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
import { NostrRequest } from "./nostr-request";
|
import { NostrRequest } from "./nostr-request";
|
||||||
import { NostrMultiSubscription } from "./nostr-multi-subscription";
|
import { NostrMultiSubscription } from "./nostr-multi-subscription";
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { utils } from "nostr-tools";
|
import { utils } from "nostr-tools";
|
||||||
import debug, { Debug, Debugger } from "debug";
|
import { Debugger } from "debug";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
import { NostrQuery, NostrRequestFilter } from "../types/nostr-query";
|
import { NostrQuery, NostrRequestFilter } from "../types/nostr-query";
|
||||||
import { NostrRequest } from "./nostr-request";
|
import { NostrRequest } from "./nostr-request";
|
||||||
import { NostrMultiSubscription } from "./nostr-multi-subscription";
|
import { NostrMultiSubscription } from "./nostr-multi-subscription";
|
||||||
import Subject, { PersistentSubject } from "./subject";
|
import Subject, { PersistentSubject } from "./subject";
|
||||||
import { logger } from "../helpers/debug";
|
import { logger } from "../helpers/debug";
|
||||||
|
import EventStore from "./event-store";
|
||||||
|
|
||||||
function addToQuery(filter: NostrRequestFilter, query: NostrQuery) {
|
function addToQuery(filter: NostrRequestFilter, query: NostrQuery) {
|
||||||
if (Array.isArray(filter)) {
|
if (Array.isArray(filter)) {
|
||||||
@ -28,11 +29,10 @@ class RelayTimelineLoader {
|
|||||||
private log: Debugger;
|
private log: Debugger;
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
events: NostrEvent[] = [];
|
events: EventStore;
|
||||||
/** set to true when the next block produces 0 events */
|
/** set to true when the next block produces 0 events */
|
||||||
complete = false;
|
complete = false;
|
||||||
|
|
||||||
onEvent = new Subject<NostrEvent>();
|
|
||||||
onBlockFinish = new Subject<void>();
|
onBlockFinish = new Subject<void>();
|
||||||
|
|
||||||
constructor(relay: string, query: NostrRequestFilter, name: string, log?: Debugger) {
|
constructor(relay: string, query: NostrRequestFilter, name: string, log?: Debugger) {
|
||||||
@ -41,19 +41,24 @@ class RelayTimelineLoader {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
|
|
||||||
this.log = log || logger.extend(name);
|
this.log = log || logger.extend(name);
|
||||||
|
this.events = new EventStore(relay);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadNextBlock() {
|
loadNextBlock() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
let query: NostrRequestFilter = addToQuery(this.query, { limit: this.blockSize });
|
let query: NostrRequestFilter = addToQuery(this.query, { limit: this.blockSize });
|
||||||
if (this.events[this.events.length - 1]) {
|
let oldestEvent = this.getLastEvent();
|
||||||
query = addToQuery(query, { until: this.events[this.events.length - 1].created_at - 1 });
|
if (oldestEvent) {
|
||||||
|
query = addToQuery(query, { until: oldestEvent.created_at - 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new NostrRequest([this.relay], undefined, this.name + "-" + this.requestId++);
|
const request = new NostrRequest([this.relay], undefined, this.name + "-" + this.requestId++);
|
||||||
|
|
||||||
let gotEvents = 0;
|
let gotEvents = 0;
|
||||||
request.onEvent.subscribe((e) => {
|
request.onEvent.subscribe((e) => {
|
||||||
|
// if(oldestEvent && e.created_at<oldestEvent.created_at){
|
||||||
|
// this.log('Got event older than oldest')
|
||||||
|
// }
|
||||||
if (this.handleEvent(e)) {
|
if (this.handleEvent(e)) {
|
||||||
gotEvents++;
|
gotEvents++;
|
||||||
}
|
}
|
||||||
@ -68,23 +73,12 @@ class RelayTimelineLoader {
|
|||||||
request.start(query);
|
request.start(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
private seenEvents = new Set<string>();
|
|
||||||
private handleEvent(event: NostrEvent) {
|
private handleEvent(event: NostrEvent) {
|
||||||
if (!this.seenEvents.has(event.id)) {
|
return this.events.addEvent(event);
|
||||||
this.seenEvents.add(event.id);
|
|
||||||
this.events = utils.insertEventIntoDescendingList(Array.from(this.events), event);
|
|
||||||
this.onEvent.next(event);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getLastEvent(nth = 0, filter?: EventFilter) {
|
getLastEvent(nth = 0, filter?: EventFilter) {
|
||||||
const events = filter ? this.events.filter(filter) : this.events;
|
return this.events.getLastEvent(nth, filter);
|
||||||
for (let i = nth; i >= 0; i--) {
|
|
||||||
const event = events[events.length - 1 - i];
|
|
||||||
if (event) return event;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +87,7 @@ export class TimelineLoader {
|
|||||||
query?: NostrRequestFilter;
|
query?: NostrRequestFilter;
|
||||||
relays: string[] = [];
|
relays: string[] = [];
|
||||||
|
|
||||||
events = new PersistentSubject<NostrEvent[]>([]);
|
events: EventStore;
|
||||||
timeline = new PersistentSubject<NostrEvent[]>([]);
|
timeline = new PersistentSubject<NostrEvent[]>([]);
|
||||||
loading = new PersistentSubject(false);
|
loading = new PersistentSubject(false);
|
||||||
complete = new PersistentSubject(false);
|
complete = new PersistentSubject(false);
|
||||||
@ -110,20 +104,23 @@ export class TimelineLoader {
|
|||||||
constructor(name: string) {
|
constructor(name: string) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.log = logger.extend("TimelineLoader:" + name);
|
this.log = logger.extend("TimelineLoader:" + name);
|
||||||
|
this.events = new EventStore(name);
|
||||||
|
|
||||||
this.subscription = new NostrMultiSubscription([], undefined, name);
|
this.subscription = new NostrMultiSubscription([], undefined, name);
|
||||||
this.subscription.onEvent.subscribe(this.handleEvent, this);
|
this.subscription.onEvent.subscribe(this.handleEvent, this);
|
||||||
|
|
||||||
|
// update the timeline when there are new events
|
||||||
|
this.events.onEvent.subscribe(this.updateTimeline, this);
|
||||||
|
this.events.onClear.subscribe(this.updateTimeline, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private seenEvents = new Set<string>();
|
private updateTimeline() {
|
||||||
|
if (this.eventFilter) {
|
||||||
|
this.timeline.next(this.events.getSortedEvents().filter(this.eventFilter));
|
||||||
|
} else this.timeline.next(this.events.getSortedEvents());
|
||||||
|
}
|
||||||
private handleEvent(event: NostrEvent) {
|
private handleEvent(event: NostrEvent) {
|
||||||
if (!this.seenEvents.has(event.id)) {
|
this.events.addEvent(event);
|
||||||
this.seenEvents.add(event.id);
|
|
||||||
this.events.next(utils.insertEventIntoDescendingList(Array.from(this.events.value), event));
|
|
||||||
|
|
||||||
if (!this.eventFilter || this.eventFilter(event)) {
|
|
||||||
this.timeline.next(utils.insertEventIntoDescendingList(Array.from(this.timeline.value), event));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createLoaders() {
|
private createLoaders() {
|
||||||
@ -133,7 +130,7 @@ export class TimelineLoader {
|
|||||||
if (!this.relayTimelineLoaders.has(relay)) {
|
if (!this.relayTimelineLoaders.has(relay)) {
|
||||||
const loader = new RelayTimelineLoader(relay, this.query, this.name, this.log.extend(relay));
|
const loader = new RelayTimelineLoader(relay, this.query, this.name, this.log.extend(relay));
|
||||||
this.relayTimelineLoaders.set(relay, loader);
|
this.relayTimelineLoaders.set(relay, loader);
|
||||||
loader.onEvent.subscribe(this.handleEvent, this);
|
this.events.connect(loader.events);
|
||||||
loader.onBlockFinish.subscribe(this.updateLoading, this);
|
loader.onBlockFinish.subscribe(this.updateLoading, this);
|
||||||
loader.onBlockFinish.subscribe(this.updateComplete, this);
|
loader.onBlockFinish.subscribe(this.updateComplete, this);
|
||||||
}
|
}
|
||||||
@ -142,9 +139,9 @@ export class TimelineLoader {
|
|||||||
private removeLoaders(filter?: (loader: RelayTimelineLoader) => boolean) {
|
private removeLoaders(filter?: (loader: RelayTimelineLoader) => boolean) {
|
||||||
for (const [relay, loader] of this.relayTimelineLoaders) {
|
for (const [relay, loader] of this.relayTimelineLoaders) {
|
||||||
if (!filter || filter(loader)) {
|
if (!filter || filter(loader)) {
|
||||||
loader?.onEvent.unsubscribe(this.handleEvent, this);
|
this.events.disconnect(loader.events);
|
||||||
loader?.onBlockFinish.unsubscribe(this.updateLoading, this);
|
loader.onBlockFinish.unsubscribe(this.updateLoading, this);
|
||||||
loader?.onBlockFinish.unsubscribe(this.updateComplete, this);
|
loader.onBlockFinish.unsubscribe(this.updateComplete, this);
|
||||||
this.relayTimelineLoaders.delete(relay);
|
this.relayTimelineLoaders.delete(relay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,9 +165,8 @@ export class TimelineLoader {
|
|||||||
this.removeLoaders();
|
this.removeLoaders();
|
||||||
|
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.events.next([]);
|
this.events.clear();
|
||||||
this.timeline.next([]);
|
this.timeline.next([]);
|
||||||
this.seenEvents.clear();
|
|
||||||
|
|
||||||
this.createLoaders();
|
this.createLoaders();
|
||||||
this.updateComplete();
|
this.updateComplete();
|
||||||
@ -181,9 +177,7 @@ export class TimelineLoader {
|
|||||||
}
|
}
|
||||||
setFilter(filter?: (event: NostrEvent) => boolean) {
|
setFilter(filter?: (event: NostrEvent) => boolean) {
|
||||||
this.eventFilter = filter;
|
this.eventFilter = filter;
|
||||||
if (this.eventFilter) {
|
this.updateTimeline();
|
||||||
this.timeline.next(this.events.value.filter(this.eventFilter));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCursor(cursor: number) {
|
setCursor(cursor: number) {
|
||||||
@ -250,9 +244,8 @@ export class TimelineLoader {
|
|||||||
// TODO: this is only needed because the current logic dose not remove events when the relay they where fetched from is removed
|
// TODO: this is only needed because the current logic dose not remove events when the relay they where fetched from is removed
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
forgetEvents() {
|
forgetEvents() {
|
||||||
this.events.next([]);
|
this.events.clear();
|
||||||
this.timeline.next([]);
|
this.timeline.next([]);
|
||||||
this.seenEvents.clear();
|
|
||||||
this.subscription.forgetEvents();
|
this.subscription.forgetEvents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Flex } from "@chakra-ui/react";
|
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Flex } from "@chakra-ui/react";
|
||||||
import { ModalProps } from "@chakra-ui/react";
|
import { ModalProps } from "@chakra-ui/react";
|
||||||
import { Bech32Prefix, hexToBech32 } from "../../helpers/nip19";
|
import { Bech32Prefix, hexToBech32 } from "../../helpers/nip19";
|
||||||
import { getReferences } from "../../helpers/nostr-event";
|
import { getReferences } from "../../helpers/nostr/event";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import RawJson from "./raw-json";
|
import RawJson from "./raw-json";
|
||||||
import RawValue from "./raw-value";
|
import RawValue from "./raw-value";
|
||||||
|
@ -3,7 +3,7 @@ import { Link as RouterLink, useLocation } from "react-router-dom";
|
|||||||
import { UserAvatar } from "../user-avatar";
|
import { UserAvatar } from "../user-avatar";
|
||||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
|
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
import { truncatedId } from "../../helpers/nostr/event";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
|
|
||||||
function ProfileButton() {
|
function ProfileButton() {
|
||||||
|
@ -44,15 +44,6 @@ export function LiveVideoPlayer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justifyContent="center" alignItems="center" {...props} position="relative">
|
<Flex justifyContent="center" alignItems="center" {...props} position="relative">
|
||||||
<Badge
|
|
||||||
position="absolute"
|
|
||||||
top="4"
|
|
||||||
left="4"
|
|
||||||
fontSize="1.2rem"
|
|
||||||
colorScheme={status === VideoStatus.Offline ? "red" : undefined}
|
|
||||||
>
|
|
||||||
{status}
|
|
||||||
</Badge>
|
|
||||||
<video
|
<video
|
||||||
ref={video}
|
ref={video}
|
||||||
playsInline={true}
|
playsInline={true}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Link, LinkProps } from "@chakra-ui/react";
|
import { Link, LinkProps } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { truncatedId } from "../helpers/nostr-event";
|
import { truncatedId } from "../helpers/nostr/event";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { getSharableNoteId } from "../helpers/nip19";
|
import { getSharableNoteId } from "../helpers/nip19";
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { IconButton } from "@chakra-ui/react";
|
|||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { QuoteRepostIcon } from "../../icons";
|
import { QuoteRepostIcon } from "../../icons";
|
||||||
import { PostModalContext } from "../../../providers/post-modal-provider";
|
import { PostModalContext } from "../../../providers/post-modal-provider";
|
||||||
import { buildQuoteRepost } from "../../../helpers/nostr-event";
|
import { buildQuoteRepost } from "../../../helpers/nostr/event";
|
||||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||||
|
|
||||||
export function QuoteRepostButton({ event }: { event: NostrEvent }) {
|
export function QuoteRepostButton({ event }: { event: NostrEvent }) {
|
||||||
|
@ -3,7 +3,7 @@ import { IconButton } from "@chakra-ui/react";
|
|||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { ReplyIcon } from "../../icons";
|
import { ReplyIcon } from "../../icons";
|
||||||
import { PostModalContext } from "../../../providers/post-modal-provider";
|
import { PostModalContext } from "../../../providers/post-modal-provider";
|
||||||
import { buildReply } from "../../../helpers/nostr-event";
|
import { buildReply } from "../../../helpers/nostr/event";
|
||||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||||
|
|
||||||
export function ReplyButton({ event }: { event: NostrEvent }) {
|
export function ReplyButton({ event }: { event: NostrEvent }) {
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { RepostIcon } from "../../icons";
|
import { RepostIcon } from "../../icons";
|
||||||
import { buildRepost } from "../../../helpers/nostr-event";
|
import { buildRepost } from "../../../helpers/nostr/event";
|
||||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||||
import { nostrPostAction } from "../../../classes/nostr-post-action";
|
import { nostrPostAction } from "../../../classes/nostr-post-action";
|
||||||
import clientRelaysService from "../../../services/client-relays";
|
import clientRelaysService from "../../../services/client-relays";
|
||||||
|
@ -17,7 +17,7 @@ import { NostrEvent } from "../../types/nostr-event";
|
|||||||
import { UserAvatarLink } from "../user-avatar-link";
|
import { UserAvatarLink } from "../user-avatar-link";
|
||||||
|
|
||||||
import { NoteMenu } from "./note-menu";
|
import { NoteMenu } from "./note-menu";
|
||||||
import { NoteRelays } from "./note-relays";
|
import { EventRelays } from "./note-relays";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
import { UserLink } from "../user-link";
|
import { UserLink } from "../user-link";
|
||||||
import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
||||||
@ -90,7 +90,7 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<NoteRelays event={event} />
|
<EventRelays event={event} />
|
||||||
<NoteMenu event={event} size="sm" variant="link" aria-label="More Options" />
|
<NoteMenu event={event} size="sm" variant="link" aria-label="More Options" />
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -24,7 +24,7 @@ import NoteDebugModal from "../debug-modals/note-debug-modal";
|
|||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import QuoteNote from "./quote-note";
|
import QuoteNote from "./quote-note";
|
||||||
import { buildDeleteEvent } from "../../helpers/nostr-event";
|
import { buildDeleteEvent } from "../../helpers/nostr/event";
|
||||||
import signingService from "../../services/signing";
|
import signingService from "../../services/signing";
|
||||||
import { nostrPostAction } from "../../classes/nostr-post-action";
|
import { nostrPostAction } from "../../classes/nostr-post-action";
|
||||||
import clientRelaysService from "../../services/client-relays";
|
import clientRelaysService from "../../services/client-relays";
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { IconButtonProps } from "@chakra-ui/react";
|
|
||||||
import { getEventRelays } from "../../services/event-relays";
|
import { getEventRelays } from "../../services/event-relays";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { RelayIconStack } from "../relay-icon-stack";
|
import { RelayIconStack } from "../relay-icon-stack";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
|
import { getEventUID } from "../../helpers/nostr/event";
|
||||||
|
|
||||||
export type NoteRelaysProps = {
|
export type NoteRelaysProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoteRelays = memo(({ event }: NoteRelaysProps) => {
|
export const EventRelays = memo(({ event }: NoteRelaysProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const eventRelays = useSubject(getEventRelays(event.id));
|
const eventRelays = useSubject(getEventRelays(getEventUID(event)));
|
||||||
|
|
||||||
return <RelayIconStack relays={eventRelays} direction="row-reverse" maxRelays={isMobile ? 4 : undefined} />;
|
return <RelayIconStack relays={eventRelays} direction="row-reverse" maxRelays={isMobile ? 4 : undefined} />;
|
||||||
});
|
});
|
||||||
|
@ -17,7 +17,7 @@ import React, { useRef, useState } from "react";
|
|||||||
import { useList } from "react-use";
|
import { useList } from "react-use";
|
||||||
import { nostrPostAction, PostResult } from "../../classes/nostr-post-action";
|
import { nostrPostAction, PostResult } from "../../classes/nostr-post-action";
|
||||||
import { normalizeToHex } from "../../helpers/nip19";
|
import { normalizeToHex } from "../../helpers/nip19";
|
||||||
import { getReferences } from "../../helpers/nostr-event";
|
import { getReferences } from "../../helpers/nostr/event";
|
||||||
import { matchHashtag, mentionNpubOrNote } from "../../helpers/regexp";
|
import { matchHashtag, mentionNpubOrNote } from "../../helpers/regexp";
|
||||||
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
|
@ -23,7 +23,7 @@ import { useRegisterIntersectionEntity } from "../../../providers/intersection-o
|
|||||||
import { UserAvatar } from "../../user-avatar";
|
import { UserAvatar } from "../../user-avatar";
|
||||||
import { UserLink } from "../../user-link";
|
import { UserLink } from "../../user-link";
|
||||||
import StreamStatusBadge from "../../../views/streams/components/status-badge";
|
import StreamStatusBadge from "../../../views/streams/components/status-badge";
|
||||||
import { NoteRelays } from "../../note/note-relays";
|
import { EventRelays } from "../../note/note-relays";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
|
|
||||||
export default function StreamNote({ event, ...props }: CardProps & { event: NostrEvent }) {
|
export default function StreamNote({ event, ...props }: CardProps & { event: NostrEvent }) {
|
||||||
@ -69,7 +69,7 @@ export default function StreamNote({ event, ...props }: CardProps & { event: Nos
|
|||||||
<CardFooter p="2" display="flex" gap="2" alignItems="center">
|
<CardFooter p="2" display="flex" gap="2" alignItems="center">
|
||||||
<StreamStatusBadge stream={stream} />
|
<StreamStatusBadge stream={stream} />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<NoteRelays event={stream.event} />
|
<EventRelays event={stream.event} />
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { getEventRelays } from "../services/event-relays";
|
import { getEventRelays } from "../../services/event-relays";
|
||||||
import { DraftNostrEvent, isETag, isPTag, NostrEvent, RTag, Tag } from "../types/nostr-event";
|
import { DraftNostrEvent, isETag, isPTag, NostrEvent, RTag, Tag } from "../../types/nostr-event";
|
||||||
import { RelayConfig, RelayMode } from "../classes/relay";
|
import { RelayConfig, RelayMode } from "../../classes/relay";
|
||||||
import accountService from "../services/account";
|
import accountService from "../../services/account";
|
||||||
import { Kind, nip19 } from "nostr-tools";
|
import { Kind, nip19 } from "nostr-tools";
|
||||||
import { matchNostrLink } from "./regexp";
|
import { matchNostrLink } from "../regexp";
|
||||||
import { getSharableNoteId } from "./nip19";
|
import { getSharableNoteId } from "../nip19";
|
||||||
import relayScoreboardService from "../services/relay-scoreboard";
|
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||||
|
import { getAddr } from "../../services/replaceable-event-requester";
|
||||||
|
|
||||||
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
||||||
return event.kind === 1 && !!getReferences(event).replyId;
|
return event.kind === 1 && !!getReferences(event).replyId;
|
||||||
@ -22,6 +23,14 @@ export function truncatedId(str: string, keep = 6) {
|
|||||||
return str.substring(0, keep) + "..." + str.substring(str.length - keep);
|
return str.substring(0, keep) + "..." + str.substring(str.length - keep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// used to get a unique Id for each event, should take into account replaceable events
|
||||||
|
export function getEventUID(event: NostrEvent) {
|
||||||
|
if (event.kind >= 30000 && event.kind < 40000) {
|
||||||
|
return getAddr(event.kind, event.pubkey, event.tags.find((t) => t[0] === "d" && t[1])?.[1]);
|
||||||
|
}
|
||||||
|
return event.id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns an array of tag indexes that are referenced in the content
|
* returns an array of tag indexes that are referenced in the content
|
||||||
* either with the legacy #[0] syntax or nostr:xxxxx links
|
* either with the legacy #[0] syntax or nostr:xxxxx links
|
@ -1,6 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event";
|
import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event";
|
||||||
import { unique } from "../array";
|
import { unique } from "../array";
|
||||||
|
import { getAddr } from "../../services/replaceable-event-requester";
|
||||||
|
|
||||||
export const STREAM_KIND = 30311;
|
export const STREAM_KIND = 30311;
|
||||||
export const STREAM_CHAT_MESSAGE_KIND = 1311;
|
export const STREAM_CHAT_MESSAGE_KIND = 1311;
|
||||||
@ -18,7 +19,9 @@ export type ParsedStream = {
|
|||||||
ends?: number;
|
ends?: number;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
streaming: string;
|
streaming?: string;
|
||||||
|
recording?: string;
|
||||||
|
relays?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseStreamEvent(stream: NostrEvent): ParsedStream {
|
export function parseStreamEvent(stream: NostrEvent): ParsedStream {
|
||||||
@ -28,16 +31,23 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream {
|
|||||||
const starts = stream.tags.find((t) => t[0] === "starts")?.[1];
|
const starts = stream.tags.find((t) => t[0] === "starts")?.[1];
|
||||||
const endsTag = stream.tags.find((t) => t[0] === "ends")?.[1];
|
const endsTag = stream.tags.find((t) => t[0] === "ends")?.[1];
|
||||||
const streaming = stream.tags.find((t) => t[0] === "streaming")?.[1];
|
const streaming = stream.tags.find((t) => t[0] === "streaming")?.[1];
|
||||||
|
const recording = stream.tags.find((t) => t[0] === "recording")?.[1];
|
||||||
const identifier = stream.tags.find((t) => t[0] === "d")?.[1];
|
const identifier = stream.tags.find((t) => t[0] === "d")?.[1];
|
||||||
|
|
||||||
|
let relays = stream.tags.find((t) => t[0] === "relays");
|
||||||
|
// remove the first "relays" element
|
||||||
|
if (relays) {
|
||||||
|
relays = Array.from(relays);
|
||||||
|
relays.shift();
|
||||||
|
}
|
||||||
|
|
||||||
const startTime = starts ? parseInt(starts) : stream.created_at;
|
const startTime = starts ? parseInt(starts) : stream.created_at;
|
||||||
const endTime = endsTag ? parseInt(endsTag) : dayjs(startTime).add(4, "hour").unix();
|
const endTime = endsTag ? parseInt(endsTag) : undefined;
|
||||||
|
|
||||||
if (!identifier) throw new Error("missing identifier");
|
if (!identifier) throw new Error("missing identifier");
|
||||||
if (!streaming) throw new Error("missing streaming");
|
|
||||||
|
|
||||||
let status = stream.tags.find((t) => t[0] === "status")?.[1] || "ended";
|
let status = stream.tags.find((t) => t[0] === "status")?.[1] || "ended";
|
||||||
if (endTime > dayjs().unix()) {
|
if (endTime && endTime > dayjs().unix()) {
|
||||||
status = "ended";
|
status = "ended";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +65,7 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream {
|
|||||||
event: stream,
|
event: stream,
|
||||||
updated: stream.created_at,
|
updated: stream.created_at,
|
||||||
streaming,
|
streaming,
|
||||||
|
recording,
|
||||||
tags,
|
tags,
|
||||||
title,
|
title,
|
||||||
summary,
|
summary,
|
||||||
@ -63,11 +74,12 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream {
|
|||||||
starts: startTime,
|
starts: startTime,
|
||||||
ends: endTime,
|
ends: endTime,
|
||||||
identifier,
|
identifier,
|
||||||
|
relays,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getATag(stream: ParsedStream) {
|
export function getATag(stream: ParsedStream) {
|
||||||
return `${stream.event.kind}:${stream.author}:${stream.identifier}`;
|
return getAddr(stream.event.kind, stream.author, stream.identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildChatMessage(stream: ParsedStream, content: string) {
|
export function buildChatMessage(stream: ParsedStream, content: string) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
import { EventReferences, getReferences } from "./nostr-event";
|
import { EventReferences, getReferences } from "./nostr/event";
|
||||||
|
|
||||||
export function countReplies(thread: ThreadItem): number {
|
export function countReplies(thread: ThreadItem): number {
|
||||||
return thread.replies.reduce((c, item) => c + countReplies(item), 0) + thread.replies.length;
|
return thread.replies.reduce((c, item) => c + countReplies(item), 0) + thread.replies.length;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
import { Bech32Prefix, normalizeToBech32 } from "./nip19";
|
import { Bech32Prefix, normalizeToBech32 } from "./nip19";
|
||||||
import { truncatedId } from "./nostr-event";
|
import { truncatedId } from "./nostr/event";
|
||||||
|
|
||||||
export type Kind0ParsedContent = {
|
export type Kind0ParsedContent = {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
@ -4,10 +4,10 @@ import { nip19 } from "nostr-tools";
|
|||||||
import { getEventRelays } from "../services/event-relays";
|
import { getEventRelays } from "../services/event-relays";
|
||||||
import relayScoreboardService from "../services/relay-scoreboard";
|
import relayScoreboardService from "../services/relay-scoreboard";
|
||||||
|
|
||||||
export default function useEventNaddr(event: NostrEvent) {
|
export default function useEventNaddr(event: NostrEvent, overrideRelays?: string[]) {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const identifier = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
|
const identifier = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
|
||||||
const relays = getEventRelays(event.id).value;
|
const relays = overrideRelays || getEventRelays(event.id).value;
|
||||||
const ranked = relayScoreboardService.getRankedRelays(relays);
|
const ranked = relayScoreboardService.getRankedRelays(relays);
|
||||||
const onlyTwo = ranked.slice(0, 2);
|
const onlyTwo = ranked.slice(0, 2);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { PropsWithChildren, createContext, useContext, useEffect, useMemo } from "react";
|
import { PropsWithChildren, createContext, useContext, useEffect, useMemo } from "react";
|
||||||
import { truncatedId } from "../helpers/nostr-event";
|
import { truncatedId } from "../helpers/nostr/event";
|
||||||
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
||||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||||
import { TimelineLoader } from "../classes/timeline-loader";
|
import { TimelineLoader } from "../classes/timeline-loader";
|
||||||
|
@ -2,7 +2,7 @@ import { Kind } from "nostr-tools";
|
|||||||
import { NostrRequest } from "../classes/nostr-request";
|
import { NostrRequest } from "../classes/nostr-request";
|
||||||
import Subject from "../classes/subject";
|
import Subject from "../classes/subject";
|
||||||
import { SuperMap } from "../classes/super-map";
|
import { SuperMap } from "../classes/super-map";
|
||||||
import { getReferences } from "../helpers/nostr-event";
|
import { getReferences } from "../helpers/nostr/event";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
|
||||||
type eventId = string;
|
type eventId = string;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Relay } from "../classes/relay";
|
import { Relay } from "../classes/relay";
|
||||||
import { PersistentSubject } from "../classes/subject";
|
import { PersistentSubject } from "../classes/subject";
|
||||||
|
import { getEventUID } from "../helpers/nostr/event";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
import relayPoolService from "./relay-pool";
|
import relayPoolService from "./relay-pool";
|
||||||
|
|
||||||
@ -14,14 +15,21 @@ export function getEventRelays(id: string) {
|
|||||||
return relays;
|
return relays;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleEventFromRelay(relay: Relay, event: NostrEvent) {
|
function addRelay(id: string, relay: string) {
|
||||||
const relays = getEventRelays(event.id);
|
const relays = getEventRelays(id);
|
||||||
|
|
||||||
if (!relays.value.includes(relay.url)) {
|
if (!relays.value.includes(relay)) {
|
||||||
relays.next(relays.value.concat(relay.url));
|
relays.next(relays.value.concat(relay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleEventFromRelay(relay: Relay, event: NostrEvent) {
|
||||||
|
const uid = getEventUID(event);
|
||||||
|
|
||||||
|
addRelay(uid, relay.url);
|
||||||
|
if (event.id !== uid) addRelay(event.id, relay.url);
|
||||||
|
}
|
||||||
|
|
||||||
relayPoolService.onRelayCreated.subscribe((relay) => {
|
relayPoolService.onRelayCreated.subscribe((relay) => {
|
||||||
relay.onEvent.subscribe(({ body: event }) => {
|
relay.onEvent.subscribe(({ body: event }) => {
|
||||||
handleEventFromRelay(relay, event);
|
handleEventFromRelay(relay, event);
|
||||||
|
@ -2,7 +2,7 @@ import { Kind } from "nostr-tools";
|
|||||||
import { NostrRequest } from "../classes/nostr-request";
|
import { NostrRequest } from "../classes/nostr-request";
|
||||||
import Subject from "../classes/subject";
|
import Subject from "../classes/subject";
|
||||||
import { SuperMap } from "../classes/super-map";
|
import { SuperMap } from "../classes/super-map";
|
||||||
import { getReferences } from "../helpers/nostr-event";
|
import { getReferences } from "../helpers/nostr/event";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
|
||||||
type eventId = string;
|
type eventId = string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { isRTag, NostrEvent } from "../types/nostr-event";
|
import { isRTag, NostrEvent } from "../types/nostr-event";
|
||||||
import { RelayConfig } from "../classes/relay";
|
import { RelayConfig } from "../classes/relay";
|
||||||
import { parseRTag } from "../helpers/nostr-event";
|
import { parseRTag } from "../helpers/nostr/event";
|
||||||
import { SuperMap } from "../classes/super-map";
|
import { SuperMap } from "../classes/super-map";
|
||||||
import Subject from "../classes/subject";
|
import Subject from "../classes/subject";
|
||||||
import { normalizeRelayConfigs } from "../helpers/relay";
|
import { normalizeRelayConfigs } from "../helpers/relay";
|
||||||
|
@ -18,6 +18,7 @@ export type NostrQuery = {
|
|||||||
since?: number;
|
since?: number;
|
||||||
until?: number;
|
until?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NostrRequestFilter = NostrQuery | NostrQuery[];
|
export type NostrRequestFilter = NostrQuery | NostrQuery[];
|
||||||
|
@ -18,7 +18,7 @@ import { CloseIcon } from "@chakra-ui/icons";
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useAppTitle } from "../../hooks/use-app-title";
|
import { useAppTitle } from "../../hooks/use-app-title";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { isReply } from "../../helpers/nostr-event";
|
import { isReply } from "../../helpers/nostr/event";
|
||||||
import { CheckIcon, EditIcon } from "../../components/icons";
|
import { CheckIcon, EditIcon } from "../../components/icons";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Flex, FormControl, FormLabel, Switch } from "@chakra-ui/react";
|
import { Flex, FormControl, FormLabel, Switch } from "@chakra-ui/react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { isReply, truncatedId } from "../../helpers/nostr-event";
|
import { isReply, truncatedId } from "../../helpers/nostr/event";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Flex, FormControl, FormLabel, Switch, useDisclosure } from "@chakra-ui/react";
|
import { Flex, FormControl, FormLabel, Switch, useDisclosure } from "@chakra-ui/react";
|
||||||
import { isReply } from "../../helpers/nostr-event";
|
import { isReply } from "../../helpers/nostr/event";
|
||||||
import { useAppTitle } from "../../hooks/use-app-title";
|
import { useAppTitle } from "../../hooks/use-app-title";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
@ -16,7 +16,7 @@ import { DraftNostrEvent } from "../../types/nostr-event";
|
|||||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
import { Message } from "./message";
|
import { Message } from "./message";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
import { truncatedId } from "../../helpers/nostr/event";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
|
@ -15,7 +15,7 @@ import { useNotificationTimeline } from "../../providers/notification-timeline";
|
|||||||
import { Kind, getEventHash } from "nostr-tools";
|
import { Kind, getEventHash } from "nostr-tools";
|
||||||
import { parseZapEvent } from "../../helpers/zaps";
|
import { parseZapEvent } from "../../helpers/zaps";
|
||||||
import { readablizeSats } from "../../helpers/bolt11";
|
import { readablizeSats } from "../../helpers/bolt11";
|
||||||
import { getReferences } from "../../helpers/nostr-event";
|
import { getReferences } from "../../helpers/nostr/event";
|
||||||
|
|
||||||
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
|
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
|
||||||
<Card size="sm" variant="outline">
|
<Card size="sm" variant="outline">
|
||||||
|
@ -1,100 +1,129 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
Flex,
|
Flex,
|
||||||
Heading,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Text,
|
Link,
|
||||||
|
SimpleGrid,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { useSearchParams, Link as RouterLink, useNavigate } from "react-router-dom";
|
import { useSearchParams, Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
import { useAsync } from "react-use";
|
import { ClipboardIcon, QrCodeIcon } from "../../components/icons";
|
||||||
import { ClipboardIcon, LightningIcon, QrCodeIcon } from "../../components/icons";
|
|
||||||
import { UserAvatarLink } from "../../components/user-avatar-link";
|
|
||||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
|
||||||
import ZapModal from "../../components/zap-modal";
|
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
|
||||||
import QrScannerModal from "../../components/qr-scanner-modal";
|
import QrScannerModal from "../../components/qr-scanner-modal";
|
||||||
import { safeDecode } from "../../helpers/nip19";
|
import { safeDecode } from "../../helpers/nip19";
|
||||||
import { useInvoiceModalContext } from "../../providers/invoice-modal";
|
|
||||||
import { matchHashtag } from "../../helpers/regexp";
|
import { matchHashtag } from "../../helpers/regexp";
|
||||||
|
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||||
|
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
|
||||||
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
|
import { Kind, nip19 } from "nostr-tools";
|
||||||
|
import useSubject from "../../hooks/use-subject";
|
||||||
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import { getUserDisplayName, parseKind0Event } from "../../helpers/user-metadata";
|
||||||
|
import { UserAvatar } from "../../components/user-avatar";
|
||||||
|
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||||
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
|
import { EventRelays } from "../../components/note/note-relays";
|
||||||
|
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||||
|
import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types";
|
||||||
|
import { getEventRelays } from "../../services/event-relays";
|
||||||
|
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||||
|
|
||||||
type relay = string;
|
function buildDescriptionContent(description: string) {
|
||||||
type NostrBandSearchResults = {
|
let content: EmbedableContent = [description.trim()];
|
||||||
query: string;
|
|
||||||
page: number;
|
content = embedNostrLinks(content);
|
||||||
page_size: number;
|
content = embedUrls(content, [renderGenericUrl]);
|
||||||
nip05_count: number;
|
|
||||||
timeline: any[];
|
return content;
|
||||||
page_count: number;
|
|
||||||
result_count: number;
|
|
||||||
serp: any[];
|
|
||||||
people_count: number;
|
|
||||||
people: [
|
|
||||||
{
|
|
||||||
i: number;
|
|
||||||
pubkey: string;
|
|
||||||
name: string;
|
|
||||||
about: string;
|
|
||||||
picture: string;
|
|
||||||
nip05: string;
|
|
||||||
nip05_verified: boolean;
|
|
||||||
website: string;
|
|
||||||
display_name: string;
|
|
||||||
lud06: string;
|
|
||||||
lud16: string;
|
|
||||||
lud06_url: string;
|
|
||||||
first_tm: number;
|
|
||||||
last_tm: number;
|
|
||||||
last_tm_str: string;
|
|
||||||
followed_count: number;
|
|
||||||
following_count: number;
|
|
||||||
zappers: number;
|
|
||||||
zap_amount: number;
|
|
||||||
zapped_pubkeys: number;
|
|
||||||
zap_amount_sent: number;
|
|
||||||
zap_amount_processed: number;
|
|
||||||
zapped_pubkeys_processed: number;
|
|
||||||
zappers_processed: number;
|
|
||||||
twitter?: {
|
|
||||||
verified: boolean;
|
|
||||||
verify_event: string;
|
|
||||||
handle: string;
|
|
||||||
name: string;
|
|
||||||
bio: string;
|
|
||||||
picture: string;
|
|
||||||
followers: number;
|
|
||||||
tweet: string;
|
|
||||||
};
|
|
||||||
relays: number[];
|
|
||||||
}
|
}
|
||||||
];
|
|
||||||
relays: Record<number | string, relay>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SearchView() {
|
function ProfileResult({ event }: { event: NostrEvent }) {
|
||||||
|
const metadata = parseKind0Event(event);
|
||||||
|
|
||||||
|
const aboutContent = metadata.about && buildDescriptionContent(metadata.about);
|
||||||
|
const nprofile = useMemo(() => {
|
||||||
|
const relays = getEventRelays(event.id).value;
|
||||||
|
const ranked = relayScoreboardService.getRankedRelays(relays).slice(2);
|
||||||
|
return nip19.nprofileEncode({ pubkey: event.pubkey, relays: ranked });
|
||||||
|
}, [event.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card overflow="hidden" variant="outline" size="sm">
|
||||||
|
<CardHeader display="flex" gap="4" alignItems="flex-start">
|
||||||
|
<UserAvatar pubkey={event.pubkey} noProxy />
|
||||||
|
<Flex alignItems="center" gap="2" overflow="hidden">
|
||||||
|
<Link as={RouterLink} to={`/u/${nprofile}`} whiteSpace="nowrap" fontWeight="bold" fontSize="xl" isTruncated>
|
||||||
|
{getUserDisplayName(metadata, event.pubkey)}
|
||||||
|
</Link>
|
||||||
|
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||||
|
</Flex>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody py={0} overflow="hidden" maxH="20rem">
|
||||||
|
{aboutContent && (
|
||||||
|
<Box whiteSpace="pre" isTruncated>
|
||||||
|
{aboutContent}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
<CardFooter>
|
||||||
|
<EventRelays event={event} />
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchResults({ search }: { search: string }) {
|
||||||
|
const searchRelays = useRelaySelectionRelays();
|
||||||
|
|
||||||
|
const timeline = useTimelineLoader(
|
||||||
|
`search`,
|
||||||
|
searchRelays,
|
||||||
|
{ search: search || "", kinds: [Kind.Metadata] },
|
||||||
|
{ enabled: !!search }
|
||||||
|
);
|
||||||
|
|
||||||
|
const events = useSubject(timeline?.timeline) ?? [];
|
||||||
|
|
||||||
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntersectionObserverProvider callback={callback}>
|
||||||
|
<SimpleGrid minChildWidth="30rem" spacing="2">
|
||||||
|
{events.map((event) => (
|
||||||
|
<ProfileResult key={event.id} event={event} />
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<TimelineActionAndStatus timeline={timeline} />
|
||||||
|
</IntersectionObserverProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isOpen: donateOpen, onOpen: openDonate, onClose: closeDonate } = useDisclosure();
|
const qrScannerModal = useDisclosure();
|
||||||
const { isOpen: qrScannerOpen, onOpen: openScanner, onClose: closeScanner } = useDisclosure();
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [search, setSearch] = useState(searchParams.get("q") ?? "");
|
const [searchInput, setSearchInput] = useState(searchParams.get("q") ?? "");
|
||||||
const { requestPay } = useInvoiceModalContext();
|
|
||||||
|
const search = searchParams.get("q");
|
||||||
|
|
||||||
// update the input value when search changes
|
// update the input value when search changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSearch(searchParams.get("q") ?? "");
|
setSearchInput(searchParams.get("q") ?? "");
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
const handleSearchText = (text: string) => {
|
const handleSearchText = (text: string) => {
|
||||||
const cleanText = text.trim();
|
const cleanText = text.trim();
|
||||||
|
|
||||||
if (cleanText.startsWith("nostr:") || cleanText.startsWith("web+nostr:") || safeDecode(search)) {
|
if (cleanText.startsWith("nostr:") || cleanText.startsWith("web+nostr:") || safeDecode(text)) {
|
||||||
navigate({ pathname: "/l/" + encodeURIComponent(text) }, { replace: true });
|
navigate({ pathname: "/l/" + encodeURIComponent(text) }, { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -115,92 +144,41 @@ export default function SearchView() {
|
|||||||
// set the search when the form is submitted
|
// set the search when the form is submitted
|
||||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSearchText(search);
|
handleSearchText(searchInput);
|
||||||
};
|
};
|
||||||
|
|
||||||
// fetch search data from nostr.band
|
|
||||||
const { value: searchResults, loading } = useAsync(async () => {
|
|
||||||
if (!searchParams.has("q")) return;
|
|
||||||
return await fetch(`https://nostr.realsearch.cc/nostr?method=search&count=10&q=${searchParams.get("q")}`).then(
|
|
||||||
(res) => res.json() as Promise<NostrBandSearchResults>
|
|
||||||
);
|
|
||||||
}, [searchParams.get("q")]);
|
|
||||||
|
|
||||||
// handle data from qr code scanner
|
|
||||||
const handleQrCodeData = handleSearchText;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" overflowX="hidden" overflowY="auto" height="100%" p="2" gap="2">
|
<Flex direction="column" py="2" gap="2">
|
||||||
<QrScannerModal isOpen={qrScannerOpen} onClose={closeScanner} onData={handleQrCodeData} />
|
<QrScannerModal isOpen={qrScannerModal.isOpen} onClose={qrScannerModal.onClose} onData={handleSearchText} />
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<IconButton onClick={openScanner} icon={<QrCodeIcon />} aria-label="Qr Scanner" />
|
<IconButton onClick={qrScannerModal.onOpen} icon={<QrCodeIcon />} aria-label="Qr Scanner" />
|
||||||
{!!navigator.clipboard.readText && (
|
{!!navigator.clipboard.readText && (
|
||||||
<IconButton onClick={readClipboard} icon={<ClipboardIcon />} aria-label="Read clipboard" />
|
<IconButton onClick={readClipboard} icon={<ClipboardIcon />} aria-label="Read clipboard" />
|
||||||
)}
|
)}
|
||||||
<Input type="search" value={search} onChange={(e) => setSearch(e.target.value)} />
|
<Input type="search" value={searchInput} onChange={(e) => setSearchInput(e.target.value)} />
|
||||||
<Button type="submit" isLoading={loading}>
|
<Button type="submit">Search</Button>
|
||||||
Search
|
<RelaySelectionButton />
|
||||||
</Button>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{searchResults && (
|
{search && <SearchResults search={search} />}
|
||||||
<Flex gap="2" alignItems="center" justifyContent="center">
|
|
||||||
<Text>Find what you where looking for?</Text>
|
|
||||||
<Button leftIcon={<LightningIcon color="yellow.400" />} size="sm" onClick={openDonate} flexShrink={0}>
|
|
||||||
Support Creator
|
|
||||||
</Button>
|
|
||||||
{donateOpen && (
|
|
||||||
<ZapModal
|
|
||||||
isOpen={donateOpen}
|
|
||||||
pubkey="3356de61b39647931ce8b2140b2bab837e0810c0ef515bbe92de0248040b8bdd"
|
|
||||||
initialAmount={500}
|
|
||||||
initialComment="Thanks for creating nostr.band"
|
|
||||||
onClose={closeDonate}
|
|
||||||
onInvoice={async (invoice) => {
|
|
||||||
closeDonate();
|
|
||||||
await requestPay(invoice);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Flex direction="column" gap="2">
|
|
||||||
{searchResults?.people.map((person) => (
|
|
||||||
<Card key={person.pubkey} overflow="hidden" variant="outline" size="sm">
|
|
||||||
<CardHeader display="flex" gap="4" alignItems="flex-start">
|
|
||||||
<UserAvatarLink pubkey={person.pubkey} />
|
|
||||||
<Flex alignItems="center" gap="2">
|
|
||||||
<Heading size="md" overflow="hidden">
|
|
||||||
{person.name || truncatedId(person.pubkey)}
|
|
||||||
</Heading>
|
|
||||||
<UserDnsIdentityIcon pubkey={person.pubkey} onlyIcon />
|
|
||||||
</Flex>
|
|
||||||
<Button
|
|
||||||
as={RouterLink}
|
|
||||||
variant="solid"
|
|
||||||
colorScheme="blue"
|
|
||||||
to={`/u/${person.pubkey}`}
|
|
||||||
size="sm"
|
|
||||||
ml="auto"
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
View Profile
|
|
||||||
</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody py={0}>
|
|
||||||
<Text>{person.about}</Text>
|
|
||||||
</CardBody>
|
|
||||||
<CardFooter display="flex" gap="2">
|
|
||||||
<Text>{person.followed_count} Followers</Text>
|
|
||||||
<Text>Created: {dayjs.unix(person.first_tm).toString()}</Text>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove this when there is a good way to allow the user to select from a list of filtered relays that support NIP-50
|
||||||
|
const searchRelays = ["wss://relay.nostr.band", "wss://search.nos.today"];
|
||||||
|
export default function SearchView() {
|
||||||
|
// const { value: searchRelays = ["wss://relay.nostr.band"] } = useAsync(async () => {
|
||||||
|
// const relays: string[] = await fetch("https://api.nostr.watch/v1/nip/50").then((res) => res.json());
|
||||||
|
// return relays;
|
||||||
|
// });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RelaySelectionProvider overrideDefault={searchRelays}>
|
||||||
|
<SearchPage />
|
||||||
|
</RelaySelectionProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@ -9,44 +9,32 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
Heading,
|
Heading,
|
||||||
IconButton,
|
|
||||||
Image,
|
Image,
|
||||||
LinkBox,
|
LinkBox,
|
||||||
LinkOverlay,
|
LinkOverlay,
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
Spacer,
|
Spacer,
|
||||||
Text,
|
Text,
|
||||||
useDisclosure,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { UserAvatar } from "../../../components/user-avatar";
|
import { UserAvatar } from "../../../components/user-avatar";
|
||||||
import { UserLink } from "../../../components/user-link";
|
import { UserLink } from "../../../components/user-link";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import StreamStatusBadge from "./status-badge";
|
import StreamStatusBadge from "./status-badge";
|
||||||
import { CodeIcon } from "../../../components/icons";
|
import { EventRelays } from "../../../components/note/note-relays";
|
||||||
import RawValue from "../../../components/debug-modals/raw-value";
|
|
||||||
import RawJson from "../../../components/debug-modals/raw-json";
|
|
||||||
import { NoteRelays } from "../../../components/note/note-relays";
|
|
||||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
import useEventNaddr from "../../../hooks/use-event-naddr";
|
import useEventNaddr from "../../../hooks/use-event-naddr";
|
||||||
|
import StreamDebugButton from "./stream-debug-button";
|
||||||
|
|
||||||
export default function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
|
export default function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
|
||||||
const { title, identifier, image } = stream;
|
const { title, image } = stream;
|
||||||
const devModal = useDisclosure();
|
|
||||||
|
|
||||||
// if there is a parent intersection observer, register this card
|
// if there is a parent intersection observer, register this card
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useRegisterIntersectionEntity(ref, stream.event.id);
|
useRegisterIntersectionEntity(ref, stream.event.id);
|
||||||
|
|
||||||
const naddr = useEventNaddr(stream.event);
|
const naddr = useEventNaddr(stream.event, stream.relays);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Card {...props} ref={ref}>
|
<Card {...props} ref={ref}>
|
||||||
<LinkBox as={CardBody} p="2" display="flex" flexDirection="column" gap="2">
|
<LinkBox as={CardBody} p="2" display="flex" flexDirection="column" gap="2">
|
||||||
{image && <Image src={image} alt={title} borderRadius="lg" />}
|
{image && <Image src={image} alt={title} borderRadius="lg" />}
|
||||||
@ -74,32 +62,9 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
|
|||||||
<CardFooter p="2" display="flex" gap="2" alignItems="center">
|
<CardFooter p="2" display="flex" gap="2" alignItems="center">
|
||||||
<StreamStatusBadge stream={stream} />
|
<StreamStatusBadge stream={stream} />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<NoteRelays event={stream.event} />
|
<EventRelays event={stream.event} />
|
||||||
<IconButton
|
<StreamDebugButton stream={stream} variant="ghost" size="sm" />
|
||||||
icon={<CodeIcon />}
|
|
||||||
aria-label="show raw event"
|
|
||||||
onClick={devModal.onOpen}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Modal isOpen={devModal.isOpen} onClose={devModal.onClose} size="6xl">
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>Raw event</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody p="4">
|
|
||||||
<Flex gap="2" direction="column">
|
|
||||||
<RawValue heading="Event Id" value={stream.event.id} />
|
|
||||||
<RawValue heading="naddr" value={naddr} />
|
|
||||||
<RawJson heading="Parsed" json={{ ...stream, event: "Omitted, see JSON below" }} />
|
|
||||||
<RawJson heading="JSON" json={stream.event} />
|
|
||||||
</Flex>
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
47
src/views/streams/components/stream-debug-button.tsx
Normal file
47
src/views/streams/components/stream-debug-button.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
IconButtonProps,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
useDisclosure,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { CodeIcon } from "../../../components/icons";
|
||||||
|
import RawValue from "../../../components/debug-modals/raw-value";
|
||||||
|
import RawJson from "../../../components/debug-modals/raw-json";
|
||||||
|
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||||
|
import useEventNaddr from "../../../hooks/use-event-naddr";
|
||||||
|
|
||||||
|
export default function StreamDebugButton({
|
||||||
|
stream,
|
||||||
|
...props
|
||||||
|
}: { stream: ParsedStream } & Omit<IconButtonProps, "icon" | "aria-label">) {
|
||||||
|
const debugModal = useDisclosure();
|
||||||
|
const naddr = useEventNaddr(stream.event);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton icon={<CodeIcon />} aria-label="Show raw event" onClick={debugModal.onOpen} {...props} />
|
||||||
|
|
||||||
|
<Modal isOpen={debugModal.isOpen} onClose={debugModal.onClose} size="6xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Raw event</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody p="4">
|
||||||
|
<Flex gap="2" direction="column">
|
||||||
|
<RawValue heading="Event Id" value={stream.event.id} />
|
||||||
|
<RawValue heading="naddr" value={naddr} />
|
||||||
|
<RawJson heading="Parsed" json={{ ...stream, event: "Omitted, see JSON below" }} />
|
||||||
|
<RawJson heading="JSON" json={stream.event} />
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Flex, Select } from "@chakra-ui/react";
|
import { Flex, Select, SimpleGrid } from "@chakra-ui/react";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import StreamCard from "./components/stream-card";
|
import StreamCard from "./components/stream-card";
|
||||||
import { ParsedStream, STREAM_KIND, getATag, parseStreamEvent } from "../../helpers/nostr/stream";
|
import { ParsedStream, STREAM_KIND, parseStreamEvent } from "../../helpers/nostr/stream";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||||
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
|
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
|
||||||
@ -15,8 +15,7 @@ import PeopleListProvider, { usePeopleListContext } from "../../components/peopl
|
|||||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
|
|
||||||
function StreamsPage() {
|
function StreamsPage() {
|
||||||
// hard code damus and snort relays for finding streams
|
const relays = useRelaySelectionRelays();
|
||||||
const readRelays = useRelaySelectionRelays(); //useReadRelayUrls(["wss://relay.damus.io", "wss://relay.snort.social"]);
|
|
||||||
const [filterStatus, setFilterStatus] = useState<string>("live");
|
const [filterStatus, setFilterStatus] = useState<string>("live");
|
||||||
|
|
||||||
const eventFilter = useCallback(
|
const eventFilter = useCallback(
|
||||||
@ -38,25 +37,22 @@ function StreamsPage() {
|
|||||||
{ "#p": people, kinds: [STREAM_KIND] },
|
{ "#p": people, kinds: [STREAM_KIND] },
|
||||||
]
|
]
|
||||||
: { kinds: [STREAM_KIND] };
|
: { kinds: [STREAM_KIND] };
|
||||||
const timeline = useTimelineLoader(`streams`, readRelays, query, { eventFilter });
|
const timeline = useTimelineLoader(`streams`, relays, query, { eventFilter });
|
||||||
|
|
||||||
useRelaysChanged(readRelays, () => timeline.reset());
|
useRelaysChanged(relays, () => timeline.reset());
|
||||||
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
const events = useSubject(timeline.timeline);
|
const events = useSubject(timeline.timeline);
|
||||||
const streams = useMemo(() => {
|
const streams = useMemo(() => {
|
||||||
const parsedStreams: Record<string, ParsedStream> = {};
|
const parsedStreams: ParsedStream[] = [];
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
try {
|
try {
|
||||||
const parsed = parseStreamEvent(event);
|
const parsed = parseStreamEvent(event);
|
||||||
const aTag = getATag(parsed);
|
parsedStreams.push(parsed);
|
||||||
if (!parsedStreams[aTag] || parsed.event.created_at > parsedStreams[aTag].event.created_at) {
|
|
||||||
parsedStreams[aTag] = parsed;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
return Array.from(Object.values(parsedStreams)).sort((a, b) => (b.starts ?? 0) - (a.starts ?? 0));
|
return parsedStreams.sort((a, b) => (b.starts ?? 0) - (a.starts ?? 0));
|
||||||
}, [events]);
|
}, [events]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -70,12 +66,12 @@ function StreamsPage() {
|
|||||||
<RelaySelectionButton ml="auto" />
|
<RelaySelectionButton ml="auto" />
|
||||||
</Flex>
|
</Flex>
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<Flex gap="2" wrap="wrap">
|
<SimpleGrid minChildWidth="25rem" spacing="2">
|
||||||
{streams.map((stream) => (
|
{streams.map((stream) => (
|
||||||
<StreamCard key={stream.event.id} stream={stream} w="sm" />
|
<StreamCard key={stream.event.id} stream={stream} />
|
||||||
))}
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
<TimelineActionAndStatus timeline={timeline} />
|
<TimelineActionAndStatus timeline={timeline} />
|
||||||
</Flex>
|
|
||||||
</IntersectionObserverProvider>
|
</IntersectionObserverProvider>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useScroll } from "react-use";
|
import { useScroll } from "react-use";
|
||||||
import { Box, Button, ButtonGroup, Flex, Heading, Spacer, Spinner, Text } from "@chakra-ui/react";
|
import { Box, Button, ButtonGroup, Flex, Heading, Spacer, Spinner, Text } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink, useParams, Navigate, useSearchParams } from "react-router-dom";
|
import { Link as RouterLink, useParams, Navigate, useSearchParams } from "react-router-dom";
|
||||||
@ -6,7 +6,6 @@ import { nip19 } from "nostr-tools";
|
|||||||
import { Global, css } from "@emotion/react";
|
import { Global, css } from "@emotion/react";
|
||||||
|
|
||||||
import { ParsedStream, STREAM_KIND, parseStreamEvent } from "../../../helpers/nostr/stream";
|
import { ParsedStream, STREAM_KIND, parseStreamEvent } from "../../../helpers/nostr/stream";
|
||||||
import { NostrRequest } from "../../../classes/nostr-request";
|
|
||||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||||
import { unique } from "../../../helpers/array";
|
import { unique } from "../../../helpers/array";
|
||||||
import { LiveVideoPlayer } from "../../../components/live-video-player";
|
import { LiveVideoPlayer } from "../../../components/live-video-player";
|
||||||
@ -14,12 +13,15 @@ import StreamChat, { ChatDisplayMode } from "./stream-chat";
|
|||||||
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||||
import { UserLink } from "../../../components/user-link";
|
import { UserLink } from "../../../components/user-link";
|
||||||
import { useIsMobile } from "../../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../../hooks/use-is-mobile";
|
||||||
import { AdditionalRelayProvider } from "../../../providers/additional-relay-context";
|
|
||||||
import StreamSummaryContent from "../components/stream-summary-content";
|
import StreamSummaryContent from "../components/stream-summary-content";
|
||||||
import { ArrowDownSIcon, ArrowUpSIcon, ExternalLinkIcon } from "../../../components/icons";
|
import { ArrowDownSIcon, ArrowUpSIcon, ExternalLinkIcon } from "../../../components/icons";
|
||||||
import useSetColorMode from "../../../hooks/use-set-color-mode";
|
import useSetColorMode from "../../../hooks/use-set-color-mode";
|
||||||
import { CopyIconButton } from "../../../components/copy-icon-button";
|
import { CopyIconButton } from "../../../components/copy-icon-button";
|
||||||
import { NoteRelays } from "../../../components/note/note-relays";
|
import StreamDebugButton from "../components/stream-debug-button";
|
||||||
|
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
|
||||||
|
import useSubject from "../../../hooks/use-subject";
|
||||||
|
import RelaySelectionButton from "../../../components/relay-selection/relay-selection-button";
|
||||||
|
import RelaySelectionProvider from "../../../providers/relay-selection-provider";
|
||||||
|
|
||||||
function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) {
|
function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
@ -91,7 +93,12 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
|
|||||||
)}
|
)}
|
||||||
{!displayMode && (
|
{!displayMode && (
|
||||||
<Flex gap={isMobile ? "2" : "4"} direction="column" flexGrow={isMobile ? 0 : 1}>
|
<Flex gap={isMobile ? "2" : "4"} direction="column" flexGrow={isMobile ? 0 : 1}>
|
||||||
<LiveVideoPlayer stream={stream.streaming} autoPlay poster={stream.image} maxH="100vh" />
|
<LiveVideoPlayer
|
||||||
|
stream={stream.streaming || stream.recording}
|
||||||
|
autoPlay={!!stream.streaming}
|
||||||
|
poster={stream.image}
|
||||||
|
maxH="100vh"
|
||||||
|
/>
|
||||||
<Flex gap={isMobile ? "2" : "4"} alignItems="center" p={isMobile ? "2" : 0}>
|
<Flex gap={isMobile ? "2" : "4"} alignItems="center" p={isMobile ? "2" : 0}>
|
||||||
<UserAvatarLink pubkey={stream.host} noProxy />
|
<UserAvatarLink pubkey={stream.host} noProxy />
|
||||||
<Box>
|
<Box>
|
||||||
@ -101,7 +108,8 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
|
|||||||
<Text>{stream.title}</Text>
|
<Text>{stream.title}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<NoteRelays event={stream.event} />
|
<StreamDebugButton stream={stream} variant="ghost" />
|
||||||
|
<RelaySelectionButton />
|
||||||
<Button as={RouterLink} to="/streams">
|
<Button as={RouterLink} to="/streams">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
@ -131,33 +139,40 @@ export default function StreamView() {
|
|||||||
if (!naddr) return <Navigate replace to="/streams" />;
|
if (!naddr) return <Navigate replace to="/streams" />;
|
||||||
|
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
const [stream, setStream] = useState<ParsedStream>();
|
const [streamRelays, setStreamRelays] = useState<string[]>([]);
|
||||||
const [relays, setRelays] = useState<string[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const subject = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
const parsed = nip19.decode(naddr);
|
const parsed = nip19.decode(naddr);
|
||||||
if (parsed.type !== "naddr") throw new Error("Invalid stream address");
|
if (parsed.type !== "naddr") throw new Error("Invalid stream address");
|
||||||
if (parsed.data.kind !== STREAM_KIND) throw new Error("Invalid stream kind");
|
if (parsed.data.kind !== STREAM_KIND) throw new Error("Invalid stream kind");
|
||||||
|
|
||||||
const request = new NostrRequest(unique([...readRelays, ...(parsed.data.relays ?? [])]));
|
const addrRelays = parsed.data.relays ?? [];
|
||||||
request.onEvent.subscribe((event) => {
|
return replaceableEventLoaderService.requestEvent(
|
||||||
setStream(parseStreamEvent(event));
|
unique([...readRelays, ...streamRelays, ...addrRelays]),
|
||||||
if (parsed.data.relays) setRelays(parsed.data.relays);
|
parsed.data.kind,
|
||||||
});
|
parsed.data.pubkey,
|
||||||
request.start({ kinds: [parsed.data.kind], "#d": [parsed.data.identifier], authors: [parsed.data.pubkey] });
|
parsed.data.identifier,
|
||||||
|
true
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
}, [naddr]);
|
}, [naddr, streamRelays.join("|")]);
|
||||||
|
|
||||||
|
const streamEvent = useSubject(subject);
|
||||||
|
const stream = useMemo(() => streamEvent && parseStreamEvent(streamEvent), [streamEvent]);
|
||||||
|
|
||||||
|
// refetch the stream from the correct relays when its loaded to ensure we have the latest
|
||||||
|
useEffect(() => {
|
||||||
|
if (stream?.relays) setStreamRelays(stream.relays);
|
||||||
|
}, [stream?.relays]);
|
||||||
|
|
||||||
if (!stream) return <Spinner />;
|
if (!stream) return <Spinner />;
|
||||||
return (
|
return (
|
||||||
// add snort and damus relays so zap.stream will always see zaps
|
// add snort and damus relays so zap.stream will always see zaps
|
||||||
<AdditionalRelayProvider
|
<RelaySelectionProvider additionalDefaults={streamRelays}>
|
||||||
relays={unique([...relays, "wss://relay.snort.social", "wss://relay.damus.io", "wss://nos.lol"])}
|
|
||||||
>
|
|
||||||
<StreamPage stream={stream} displayMode={(params.get("displayMode") as ChatDisplayMode) ?? undefined} />
|
<StreamPage stream={stream} displayMode={(params.get("displayMode") as ChatDisplayMode) ?? undefined} />
|
||||||
</AdditionalRelayProvider>
|
</RelaySelectionProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -34,11 +34,12 @@ import { useSigningContext } from "../../../../providers/signing-provider";
|
|||||||
import { useTimelineCurserIntersectionCallback } from "../../../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import useSubject from "../../../../hooks/use-subject";
|
import useSubject from "../../../../hooks/use-subject";
|
||||||
import { useTimelineLoader } from "../../../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../../../hooks/use-timeline-loader";
|
||||||
import { truncatedId } from "../../../../helpers/nostr-event";
|
import { truncatedId } from "../../../../helpers/nostr/event";
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
import TopZappers from "./top-zappers";
|
import TopZappers from "./top-zappers";
|
||||||
import { parseZapEvent } from "../../../../helpers/zaps";
|
import { parseZapEvent } from "../../../../helpers/zaps";
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
|
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
|
||||||
|
|
||||||
const hideScrollbar = css`
|
const hideScrollbar = css`
|
||||||
scrollbar-width: 0;
|
scrollbar-width: 0;
|
||||||
@ -57,13 +58,14 @@ export default function StreamChat({
|
|||||||
...props
|
...props
|
||||||
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode; displayMode?: ChatDisplayMode }) {
|
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode; displayMode?: ChatDisplayMode }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const contextRelays = useAdditionalRelayContext();
|
const streamRelays = useRelaySelectionRelays();
|
||||||
const readRelays = useReadRelayUrls(contextRelays);
|
|
||||||
const hostReadRelays = useUserRelays(stream.host)
|
const hostReadRelays = useUserRelays(stream.host)
|
||||||
.filter((r) => r.mode & RelayMode.READ)
|
.filter((r) => r.mode & RelayMode.READ)
|
||||||
.map((r) => r.url);
|
.map((r) => r.url);
|
||||||
|
|
||||||
const timeline = useTimelineLoader(`${truncatedId(stream.event.id)}-chat`, readRelays, {
|
const relays = useMemo(() => unique([...streamRelays, ...hostReadRelays]), [hostReadRelays, streamRelays]);
|
||||||
|
|
||||||
|
const timeline = useTimelineLoader(`${truncatedId(stream.identifier)}-chat`, streamRelays, {
|
||||||
"#a": [getATag(stream)],
|
"#a": [getATag(stream)],
|
||||||
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
|
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
|
||||||
});
|
});
|
||||||
@ -92,7 +94,7 @@ export default function StreamChat({
|
|||||||
const draft = buildChatMessage(stream, values.content);
|
const draft = buildChatMessage(stream, values.content);
|
||||||
const signed = await requestSignature(draft);
|
const signed = await requestSignature(draft);
|
||||||
if (!signed) throw new Error("Failed to sign");
|
if (!signed) throw new Error("Failed to sign");
|
||||||
nostrPostAction(unique([...contextRelays, ...hostReadRelays]), signed);
|
nostrPostAction(relays, signed);
|
||||||
reset();
|
reset();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
@ -181,7 +183,7 @@ export default function StreamChat({
|
|||||||
}}
|
}}
|
||||||
onClose={zapModal.onClose}
|
onClose={zapModal.onClose}
|
||||||
initialComment={getValues().content}
|
initialComment={getValues().content}
|
||||||
additionalRelays={contextRelays}
|
additionalRelays={relays}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -30,7 +30,7 @@ import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
|||||||
import { ArrowDownSIcon, ArrowUpSIcon, AtIcon, ExternalLinkIcon, KeyIcon, LightningIcon } from "../../components/icons";
|
import { ArrowDownSIcon, ArrowUpSIcon, AtIcon, ExternalLinkIcon, KeyIcon, LightningIcon } from "../../components/icons";
|
||||||
import { normalizeToBech32 } from "../../helpers/nip19";
|
import { normalizeToBech32 } from "../../helpers/nip19";
|
||||||
import { Bech32Prefix } from "../../helpers/nip19";
|
import { Bech32Prefix } from "../../helpers/nip19";
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
import { truncatedId } from "../../helpers/nostr/event";
|
||||||
import { CopyIconButton } from "../../components/copy-icon-button";
|
import { CopyIconButton } from "../../components/copy-icon-button";
|
||||||
import { QrIconButton } from "./components/share-qr-button";
|
import { QrIconButton } from "./components/share-qr-button";
|
||||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||||
@ -100,8 +100,8 @@ export default function UserAboutTab() {
|
|||||||
alignItems={isMobile ? "flex-start" : "flex-end"}
|
alignItems={isMobile ? "flex-start" : "flex-end"}
|
||||||
>
|
>
|
||||||
<UserAvatar pubkey={pubkey} size={isMobile ? "lg" : "xl"} noProxy />
|
<UserAvatar pubkey={pubkey} size={isMobile ? "lg" : "xl"} noProxy />
|
||||||
<Box>
|
<Box overflow="hidden">
|
||||||
<Heading>{getUserDisplayName(metadata, pubkey)}</Heading>
|
<Heading isTruncated>{getUserDisplayName(metadata, pubkey)}</Heading>
|
||||||
<UserDnsIdentityIcon pubkey={pubkey} />
|
<UserDnsIdentityIcon pubkey={pubkey} />
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -115,19 +115,7 @@ export default function UserAboutTab() {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{aboutContent && (
|
{aboutContent && <Box whiteSpace="pre">{aboutContent}</Box>}
|
||||||
<Text whiteSpace="pre-wrap" px="2">
|
|
||||||
{aboutContent.map((part, i) =>
|
|
||||||
typeof part === "string" ? (
|
|
||||||
<Text as="span" key={"part-" + i}>
|
|
||||||
{part}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
React.cloneElement(part, { key: "part-" + i })
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Flex gap="2" px="2" direction="column">
|
<Flex gap="2" px="2" direction="column">
|
||||||
{metadata?.lud16 && (
|
{metadata?.lud16 && (
|
||||||
|
@ -31,7 +31,9 @@ export default function Header({
|
|||||||
<Flex direction="column" gap="2" px="2" pt="2">
|
<Flex direction="column" gap="2" px="2" pt="2">
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap="2" alignItems="center">
|
||||||
<UserAvatar pubkey={pubkey} size="sm" noProxy mr="2" />
|
<UserAvatar pubkey={pubkey} size="sm" noProxy mr="2" />
|
||||||
<Heading size="md">{getUserDisplayName(metadata, pubkey)}</Heading>
|
<Heading size="md" isTruncated>
|
||||||
|
{getUserDisplayName(metadata, pubkey)}
|
||||||
|
</Heading>
|
||||||
<UserDnsIdentityIcon pubkey={pubkey} onlyIcon={isMobile} />
|
<UserDnsIdentityIcon pubkey={pubkey} onlyIcon={isMobile} />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{isSelf && (
|
{isSelf && (
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Box, Flex, FlexProps, Heading, Input, Link } from "@chakra-ui/react";
|
import { Flex, FlexProps, Heading, Link } from "@chakra-ui/react";
|
||||||
import { Link as ReactRouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
||||||
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
||||||
@ -28,7 +28,7 @@ export const UserCard = ({ pubkey, relay, ...props }: UserCardProps) => {
|
|||||||
>
|
>
|
||||||
<UserAvatar pubkey={pubkey} />
|
<UserAvatar pubkey={pubkey} />
|
||||||
<Flex direction="column" flex={1} overflow="hidden">
|
<Flex direction="column" flex={1} overflow="hidden">
|
||||||
<Link as={ReactRouterLink} to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
|
<Link as={RouterLink} to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
|
||||||
<Heading size="sm" whiteSpace="nowrap" isTruncated>
|
<Heading size="sm" whiteSpace="nowrap" isTruncated>
|
||||||
{getUserDisplayName(metadata, pubkey)}
|
{getUserDisplayName(metadata, pubkey)}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
@ -10,7 +10,7 @@ import { RelayMode } from "../../../classes/relay";
|
|||||||
import UserDebugModal from "../../../components/debug-modals/user-debug-modal";
|
import UserDebugModal from "../../../components/debug-modals/user-debug-modal";
|
||||||
import { useCopyToClipboard } from "react-use";
|
import { useCopyToClipboard } from "react-use";
|
||||||
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
|
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
|
||||||
import { truncatedId } from "../../../helpers/nostr-event";
|
import { truncatedId } from "../../../helpers/nostr/event";
|
||||||
|
|
||||||
export const UserProfileMenu = ({
|
export const UserProfileMenu = ({
|
||||||
pubkey,
|
pubkey,
|
||||||
|
@ -6,7 +6,7 @@ import { UserCard, UserCardProps } from "./components/user-card";
|
|||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
import { truncatedId } from "../../helpers/nostr/event";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||||
|
@ -2,7 +2,7 @@ import { useRef } from "react";
|
|||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { Box, Flex, SkeletonText, Spacer, Text } from "@chakra-ui/react";
|
import { Box, Flex, SkeletonText, Spacer, Text } from "@chakra-ui/react";
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
import { getReferences, truncatedId } from "../../helpers/nostr-event";
|
import { getReferences, truncatedId } from "../../helpers/nostr/event";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
|
@ -2,7 +2,7 @@ import { useCallback } from "react";
|
|||||||
import { Flex, FormControl, FormLabel, Spacer, Switch, useDisclosure } from "@chakra-ui/react";
|
import { Flex, FormControl, FormLabel, Spacer, Switch, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
import { isReply, isRepost, truncatedId } from "../../helpers/nostr-event";
|
import { isReply, isRepost, truncatedId } from "../../helpers/nostr/event";
|
||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
import { RelayIconStack } from "../../components/relay-icon-stack";
|
import { RelayIconStack } from "../../components/relay-icon-stack";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
@ -2,7 +2,7 @@ import { Flex, Text } from "@chakra-ui/react";
|
|||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { NoteLink } from "../../components/note-link";
|
import { NoteLink } from "../../components/note-link";
|
||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
import { filterTagsByContentRefs, truncatedId } from "../../helpers/nostr-event";
|
import { filterTagsByContentRefs, truncatedId } from "../../helpers/nostr/event";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
|
import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
|
||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Flex } from "@chakra-ui/react";
|
import { Flex } from "@chakra-ui/react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
import { truncatedId } from "../../helpers/nostr/event";
|
||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
|
@ -8,7 +8,7 @@ import { NoteLink } from "../../components/note-link";
|
|||||||
import { UserAvatarLink } from "../../components/user-avatar-link";
|
import { UserAvatarLink } from "../../components/user-avatar-link";
|
||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
import { readablizeSats } from "../../helpers/bolt11";
|
import { readablizeSats } from "../../helpers/bolt11";
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
import { truncatedId } from "../../helpers/nostr/event";
|
||||||
import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/zaps";
|
import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/zaps";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user