mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-29 21:13:37 +02:00
small improvements to map view
This commit is contained in:
@@ -52,14 +52,7 @@ const videoExt = [".mp4", ".mkv", ".webm", ".mov"];
|
||||
export function renderVideoUrl(match: URL) {
|
||||
if (!videoExt.some((ext) => match.pathname.endsWith(ext))) return null;
|
||||
|
||||
return (
|
||||
<video
|
||||
key={match.href}
|
||||
src={match.toString()}
|
||||
controls
|
||||
style={{ maxWidth: "30rem", maxHeight: "20rem", width: "100%" }}
|
||||
/>
|
||||
);
|
||||
return <video src={match.toString()} controls style={{ maxWidth: "30rem", maxHeight: "20rem", width: "100%" }} />;
|
||||
}
|
||||
|
||||
export function renderGenericUrl(match: URL) {
|
||||
|
@@ -51,7 +51,7 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
|
||||
return (
|
||||
<TrustProvider event={event}>
|
||||
<ExpandProvider>
|
||||
<Card variant={variant} ref={ref}>
|
||||
<Card variant={variant} ref={ref} data-event-id={event.id}>
|
||||
<CardHeader padding="2">
|
||||
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
|
||||
<UserAvatarLink pubkey={event.pubkey} size={["xs", "sm"]} />
|
||||
|
@@ -8,6 +8,7 @@ import { Text } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { STREAM_KIND } from "../../../helpers/nostr/stream";
|
||||
import StreamNote from "./stream-note";
|
||||
import { ErrorBoundary } from "../../error-boundary";
|
||||
|
||||
const RenderEvent = React.memo(({ event }: { event: NostrEvent }) => {
|
||||
switch (event.kind) {
|
||||
@@ -28,7 +29,9 @@ const GenericNoteTimeline = React.memo(({ timeline }: { timeline: TimelineLoader
|
||||
return (
|
||||
<>
|
||||
{notes.map((note) => (
|
||||
<RenderEvent key={note.id} event={note} />
|
||||
<ErrorBoundary key={note.id}>
|
||||
<RenderEvent event={note} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
17
src/helpers/function.ts
Normal file
17
src/helpers/function.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// copied from https://dev.to/bwca/create-a-debounce-function-from-scratch-in-typescript-560m
|
||||
export function debounce<A = unknown, R = void>(fn: (args: A) => R, ms: number): (args: A) => Promise<R> {
|
||||
let timer: number;
|
||||
|
||||
const debouncedFunc = (args: A): Promise<R> =>
|
||||
new Promise((resolve) => {
|
||||
if (timer) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
|
||||
timer = window.setTimeout(() => {
|
||||
resolve(fn(args));
|
||||
}, ms);
|
||||
});
|
||||
|
||||
return debouncedFunc;
|
||||
}
|
@@ -10,7 +10,7 @@ type Options = {
|
||||
cursor?: number;
|
||||
};
|
||||
|
||||
export function useTimelineLoader(key: string, relays: string[], query: NostrRequestFilter, opts?: Options) {
|
||||
export default function useTimelineLoader(key: string, relays: string[], query: NostrRequestFilter, opts?: Options) {
|
||||
const timeline = useMemo(() => timelineCacheService.createTimeline(key), [key]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@@ -17,7 +17,7 @@ import {
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
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 { CheckIcon, EditIcon } from "../../components/icons";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Flex, FormControl, FormLabel, Switch } from "@chakra-ui/react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
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 { useCallback } from "react";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
|
@@ -2,7 +2,7 @@ import { useCallback } from "react";
|
||||
import { Flex, FormControl, FormLabel, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import { isReply } from "../../helpers/nostr/event";
|
||||
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 RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
|
||||
|
@@ -1,20 +1,21 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Link as RouterLink, useSearchParams } from "react-router-dom";
|
||||
import { Box, Button, Flex } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
import ngeohash from "ngeohash";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import L from "leaflet";
|
||||
import "leaflet.locatecontrol/dist/L.Control.Locate.min.css";
|
||||
import "leaflet.locatecontrol";
|
||||
import ngeohash from "ngeohash";
|
||||
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
|
||||
import GenericNoteTimeline from "../../components/timeline-page/generic-note-timeline";
|
||||
import { debounce } from "../../helpers/function";
|
||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import MapTimeline from "./timeline";
|
||||
|
||||
function getPrecision(zoom: number) {
|
||||
if (zoom <= 4) return 1;
|
||||
@@ -36,10 +37,51 @@ function getEventGeohash(event: NostrEvent) {
|
||||
return hash || null;
|
||||
}
|
||||
|
||||
function useEventMarkers(events: NostrEvent[], map?: L.Map, onClick?: (event: NostrEvent) => void) {
|
||||
const markers = useRef<Record<string, L.Marker>>({});
|
||||
|
||||
// create markers
|
||||
useEffect(() => {
|
||||
for (const event of events) {
|
||||
const geohash = getEventGeohash(event);
|
||||
if (!geohash) continue;
|
||||
|
||||
const marker = markers.current[event.id] || L.marker([0, 0]);
|
||||
|
||||
const latLng = ngeohash.decode(geohash);
|
||||
marker.setLatLng([latLng.latitude, latLng.longitude]);
|
||||
|
||||
if (onClick) {
|
||||
marker.addEventListener("click", () => onClick(event));
|
||||
}
|
||||
|
||||
markers.current[event.id] = marker;
|
||||
}
|
||||
}, [events]);
|
||||
|
||||
// add makers to map
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const ids = events.map((e) => e.id);
|
||||
|
||||
for (const [id, marker] of Object.entries(markers.current)) {
|
||||
if (ids.includes(id)) marker?.addTo(map);
|
||||
else marker?.remove();
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const [id, marker] of Object.entries(markers.current)) {
|
||||
marker?.removeFrom(map);
|
||||
}
|
||||
};
|
||||
}, [map, events]);
|
||||
}
|
||||
|
||||
export default function MapView() {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [map, setMap] = useState<L.Map>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
@@ -52,11 +94,21 @@ export default function MapView() {
|
||||
|
||||
L.control.locate().addTo(map);
|
||||
|
||||
map.addEventListener(
|
||||
"move",
|
||||
debounce(() => {
|
||||
const center = map.getCenter();
|
||||
const hash = ngeohash.encode(center.lat, center.lng, 5);
|
||||
|
||||
setSearchParams({ hash }, { replace: true });
|
||||
}, 1000)
|
||||
);
|
||||
|
||||
setMap(map);
|
||||
|
||||
return () => {
|
||||
map.remove();
|
||||
setMap(undefined);
|
||||
map.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -84,25 +136,14 @@ export default function MapView() {
|
||||
setCells(hashes);
|
||||
}, [map]);
|
||||
|
||||
const [focused, setFocused] = useState<string>();
|
||||
const handleMarkerClick = useCallback((event: NostrEvent) => {
|
||||
document.querySelector(`[data-event-id="${event.id}"]`)?.scrollIntoView();
|
||||
setFocused(event.id);
|
||||
}, []);
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const markers: L.Marker[] = [];
|
||||
for (const event of events) {
|
||||
const geohash = getEventGeohash(event);
|
||||
if (!geohash) continue;
|
||||
const latLng = ngeohash.decode(geohash);
|
||||
const marker = L.marker([latLng.latitude, latLng.longitude]).addTo(map);
|
||||
markers.push(marker);
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const marker of markers) {
|
||||
marker.remove();
|
||||
}
|
||||
};
|
||||
}, [map, events]);
|
||||
useEventMarkers(events, map, handleMarkerClick);
|
||||
|
||||
return (
|
||||
<Flex overflow={{ lg: "hidden" }} h={{ lg: "full" }} direction={{ base: "column-reverse", lg: "row" }}>
|
||||
@@ -117,7 +158,7 @@ export default function MapView() {
|
||||
</Flex>
|
||||
|
||||
<Flex overflowY="auto" overflowX="hidden" gap="2" direction="column" h="full">
|
||||
<GenericNoteTimeline timeline={timeline} />
|
||||
<MapTimeline timeline={timeline} focused={focused} />
|
||||
{cells.length > 0 && <TimelineActionAndStatus timeline={timeline} />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
36
src/views/map/timeline.tsx
Normal file
36
src/views/map/timeline.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Kind } from "nostr-tools";
|
||||
import React from "react";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import StreamNote from "../../components/timeline-page/generic-note-timeline/stream-note";
|
||||
import { Note } from "../../components/note";
|
||||
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||
import { TimelineLoader } from "../../classes/timeline-loader";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
const RenderEvent = React.memo(({ event, focused }: { event: NostrEvent; focused?: boolean }) => {
|
||||
switch (event.kind) {
|
||||
case Kind.Text:
|
||||
return <Note event={event} variant={focused ? "elevated" : undefined} />;
|
||||
case STREAM_KIND:
|
||||
return <StreamNote event={event} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const MapTimeline = React.memo(({ timeline, focused }: { timeline: TimelineLoader; focused?: string }) => {
|
||||
const events = useSubject(timeline.timeline);
|
||||
|
||||
return (
|
||||
<>
|
||||
{events.map((event) => (
|
||||
<ErrorBoundary key={event.id}>
|
||||
<RenderEvent event={event} focused={focused === event.id} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default MapTimeline;
|
@@ -15,7 +15,7 @@ import clientRelaysService from "../../services/client-relays";
|
||||
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import { Message } from "./message";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { truncatedId } from "../../helpers/nostr/event";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
|
@@ -20,7 +20,7 @@ import { safeDecode } from "../../helpers/nip19";
|
||||
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 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";
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo, useState } from "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 { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
|
@@ -33,7 +33,7 @@ import { useForm } from "react-hook-form";
|
||||
import { useSigningContext } from "../../../../providers/signing-provider";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../../../hooks/use-timeline-cursor-intersection-callback";
|
||||
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 { css } from "@emotion/react";
|
||||
import TopZappers from "./top-zappers";
|
||||
|
@@ -5,7 +5,7 @@ import { Event, Kind } from "nostr-tools";
|
||||
import { UserCard, UserCardProps } from "./components/user-card";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
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 useSubject from "../../hooks/use-subject";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
|
@@ -3,7 +3,7 @@ import { useOutletContext } from "react-router-dom";
|
||||
import { Box, Flex, SkeletonText, Spacer, Text } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
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 { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
|
@@ -6,7 +6,7 @@ import { isReply, isRepost, truncatedId } from "../../helpers/nostr/event";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import { RelayIconStack } from "../../components/relay-icon-stack";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||
import TimelineViewType from "../../components/timeline-page/timeline-view-type";
|
||||
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
|
||||
|
@@ -3,7 +3,7 @@ import { useOutletContext } from "react-router-dom";
|
||||
import { NoteLink } from "../../components/note-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
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 { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||
|
@@ -6,7 +6,7 @@ import TimelineActionAndStatus from "../../components/timeline-page/timeline-act
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import GenericNoteTimeline from "../../components/timeline-page/generic-note-timeline";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||
|
||||
export default function UserStreamsTab() {
|
||||
|
@@ -10,7 +10,7 @@ import { UserLink } from "../../components/user-link";
|
||||
import { readablizeSats } from "../../helpers/bolt11";
|
||||
import { truncatedId } from "../../helpers/nostr/event";
|
||||
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 { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
|
Reference in New Issue
Block a user