diff --git a/src/components/embed-types/common.tsx b/src/components/embed-types/common.tsx
index 5b4849d14..32804bc27 100644
--- a/src/components/embed-types/common.tsx
+++ b/src/components/embed-types/common.tsx
@@ -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 (
-
- );
+ return ;
}
export function renderGenericUrl(match: URL) {
diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx
index 376d003be..a876baef1 100644
--- a/src/components/note/index.tsx
+++ b/src/components/note/index.tsx
@@ -51,7 +51,7 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
return (
-
+
diff --git a/src/components/timeline-page/generic-note-timeline/index.tsx b/src/components/timeline-page/generic-note-timeline/index.tsx
index 55a01a4f5..490316e8c 100644
--- a/src/components/timeline-page/generic-note-timeline/index.tsx
+++ b/src/components/timeline-page/generic-note-timeline/index.tsx
@@ -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) => (
-
+
+
+
))}
>
);
diff --git a/src/helpers/function.ts b/src/helpers/function.ts
new file mode 100644
index 000000000..715fd95cd
--- /dev/null
+++ b/src/helpers/function.ts
@@ -0,0 +1,17 @@
+// copied from https://dev.to/bwca/create-a-debounce-function-from-scratch-in-typescript-560m
+export function debounce(fn: (args: A) => R, ms: number): (args: A) => Promise {
+ let timer: number;
+
+ const debouncedFunc = (args: A): Promise =>
+ new Promise((resolve) => {
+ if (timer) {
+ window.clearTimeout(timer);
+ }
+
+ timer = window.setTimeout(() => {
+ resolve(fn(args));
+ }, ms);
+ });
+
+ return debouncedFunc;
+}
diff --git a/src/hooks/use-timeline-loader.ts b/src/hooks/use-timeline-loader.ts
index 4b9875788..44c0c8eb2 100644
--- a/src/hooks/use-timeline-loader.ts
+++ b/src/hooks/use-timeline-loader.ts
@@ -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(() => {
diff --git a/src/views/hashtag/index.tsx b/src/views/hashtag/index.tsx
index fa19a8364..bff14b25a 100644
--- a/src/views/hashtag/index.tsx
+++ b/src/views/hashtag/index.tsx
@@ -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";
diff --git a/src/views/home/following-tab.tsx b/src/views/home/following-tab.tsx
index d6c496e2f..401482f5f 100644
--- a/src/views/home/following-tab.tsx
+++ b/src/views/home/following-tab.tsx
@@ -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";
diff --git a/src/views/home/global-tab.tsx b/src/views/home/global-tab.tsx
index 4c53697bc..9367667bf 100644
--- a/src/views/home/global-tab.tsx
+++ b/src/views/home/global-tab.tsx
@@ -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";
diff --git a/src/views/map/index.tsx b/src/views/map/index.tsx
index 5bb05717f..ef292f9fb 100644
--- a/src/views/map/index.tsx
+++ b/src/views/map/index.tsx
@@ -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>({});
+
+ // 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(null);
-
const [map, setMap] = useState();
+ 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();
+ 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 (
@@ -117,7 +158,7 @@ export default function MapView() {
-
+
{cells.length > 0 && }
diff --git a/src/views/map/timeline.tsx b/src/views/map/timeline.tsx
new file mode 100644
index 000000000..c7a105303
--- /dev/null
+++ b/src/views/map/timeline.tsx
@@ -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 ;
+ case STREAM_KIND:
+ return ;
+ default:
+ return null;
+ }
+});
+
+const MapTimeline = React.memo(({ timeline, focused }: { timeline: TimelineLoader; focused?: string }) => {
+ const events = useSubject(timeline.timeline);
+
+ return (
+ <>
+ {events.map((event) => (
+
+
+
+ ))}
+ >
+ );
+});
+
+export default MapTimeline;
diff --git a/src/views/messages/chat.tsx b/src/views/messages/chat.tsx
index c7466d2ba..9231a4799 100644
--- a/src/views/messages/chat.tsx
+++ b/src/views/messages/chat.tsx
@@ -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";
diff --git a/src/views/search/index.tsx b/src/views/search/index.tsx
index cd3da4738..ef9a655bc 100644
--- a/src/views/search/index.tsx
+++ b/src/views/search/index.tsx
@@ -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";
diff --git a/src/views/streams/index.tsx b/src/views/streams/index.tsx
index ee47d55c9..545931d08 100644
--- a/src/views/streams/index.tsx
+++ b/src/views/streams/index.tsx
@@ -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";
diff --git a/src/views/streams/stream/stream-chat/index.tsx b/src/views/streams/stream/stream-chat/index.tsx
index 75d1916d2..878a0002c 100644
--- a/src/views/streams/stream/stream-chat/index.tsx
+++ b/src/views/streams/stream/stream-chat/index.tsx
@@ -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";
diff --git a/src/views/user/followers.tsx b/src/views/user/followers.tsx
index 52bfbf05f..4dd3016f2 100644
--- a/src/views/user/followers.tsx
+++ b/src/views/user/followers.tsx
@@ -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";
diff --git a/src/views/user/likes.tsx b/src/views/user/likes.tsx
index f9abb1b56..4e2a36316 100644
--- a/src/views/user/likes.tsx
+++ b/src/views/user/likes.tsx
@@ -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";
diff --git a/src/views/user/notes.tsx b/src/views/user/notes.tsx
index 9fa2a37d6..7d3057c43 100644
--- a/src/views/user/notes.tsx
+++ b/src/views/user/notes.tsx
@@ -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";
diff --git a/src/views/user/reports.tsx b/src/views/user/reports.tsx
index 6a1f6e371..de0785dda 100644
--- a/src/views/user/reports.tsx
+++ b/src/views/user/reports.tsx
@@ -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";
diff --git a/src/views/user/streams.tsx b/src/views/user/streams.tsx
index 496673817..8871e8c22 100644
--- a/src/views/user/streams.tsx
+++ b/src/views/user/streams.tsx
@@ -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() {
diff --git a/src/views/user/zaps.tsx b/src/views/user/zaps.tsx
index 21e48a3b4..f5e59614d 100644
--- a/src/views/user/zaps.tsx
+++ b/src/views/user/zaps.tsx
@@ -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";