diff --git a/src/app.tsx b/src/app.tsx
index bd9bb5efb..28526bdd0 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -123,6 +123,7 @@ const WikiHomeView = lazy(() => import("./views/wiki"));
const WikiPageView = lazy(() => import("./views/wiki/page"));
const WikiTopicView = lazy(() => import("./views/wiki/topic"));
const WikiSearchView = lazy(() => import("./views/wiki/search"));
+const WikiCompareView = lazy(() => import("./views/wiki/compare"));
const overrideReactTextareaAutocompleteStyles = css`
.rta__autocomplete {
@@ -318,6 +319,7 @@ const router = createHashRouter([
{ path: "search", element: },
{ path: "topic/:topic", element: },
{ path: "page/:naddr", element: },
+ { path: "compare/:topic/:a/:b", element: },
{ path: "", element: },
],
},
diff --git a/src/components/diff/diff-viewer.tsx b/src/components/diff/diff-viewer.tsx
index 9ce21b96f..8703a7a0b 100644
--- a/src/components/diff/diff-viewer.tsx
+++ b/src/components/diff/diff-viewer.tsx
@@ -1,5 +1,5 @@
import { ColorModeContext, useColorMode } from "@chakra-ui/react";
-import ReactDiffViewer from "react-diff-viewer-continued";
+import ReactDiffViewer, { DiffMethod } from "react-diff-viewer-continued";
import computeStyles, { ReactDiffViewerStylesOverride } from "react-diff-viewer-continued/lib/src/styles";
const fixedStyles: ReactDiffViewerStylesOverride = {
@@ -35,7 +35,17 @@ class FixedReactDiffViewer extends ReactDiffViewer {
}
}
-export default function DiffViewer({ oldValue, newValue }: { oldValue: string; newValue: string }) {
+export default function DiffViewer({
+ oldValue,
+ newValue,
+ method = DiffMethod.WORDS,
+ splitView = false,
+}: {
+ oldValue: string;
+ newValue: string;
+ method?: DiffMethod;
+ splitView?: boolean;
+}) {
const { colorMode } = useColorMode();
return (
@@ -44,9 +54,8 @@ export default function DiffViewer({ oldValue, newValue }: { oldValue: string; n
newValue={newValue}
useDarkTheme={colorMode === "dark"}
hideLineNumbers
- splitView={false}
- //@ts-expect-error
- compareMethod="diffWords"
+ splitView={splitView}
+ compareMethod={method}
/>
);
}
diff --git a/src/const.ts b/src/const.ts
index 7b7165092..a3c247e19 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -7,4 +7,5 @@ export const SEARCH_RELAYS = safeRelayUrls([
// TODO: requires NIP-42 auth
// "wss://filter.nostr.wine",
]);
+export const WIKI_RELAYS = safeRelayUrls(["wss://relay.wikifreedia.xyz/"]);
export const COMMON_CONTACT_RELAY = safeRelayUrl("wss://purplepag.es") as string;
diff --git a/src/helpers/nostr/wiki.ts b/src/helpers/nostr/wiki.ts
index 0642b8d96..798c64b86 100644
--- a/src/helpers/nostr/wiki.ts
+++ b/src/helpers/nostr/wiki.ts
@@ -1,9 +1,10 @@
-import { NostrEvent } from "nostr-tools";
+import { NostrEvent, nip19 } from "nostr-tools";
+import { parseCoordinate } from "./event";
export const WIKI_PAGE_KIND = 30818;
export function getPageTitle(page: NostrEvent) {
- return page.tags.find((t) => t[0] === "title")?.[1];
+ return page.tags.find((t) => t[0] === "title")?.[1] || page.tags.find((t) => t[0] === "d")?.[1];
}
export function getPageTopic(page: NostrEvent) {
@@ -16,3 +17,26 @@ export function getPageSummary(page: NostrEvent) {
const summary = page.tags.find((t) => t[0] === "summary")?.[1];
return summary || page.content.split("\n")[0];
}
+
+export function getPageForks(page: NostrEvent) {
+ const addressFork = page.tags.find((t) => t[0] === "a" && t[1] && t[3]);
+ const eventFork = page.tags.find((t) => t[0] === "a" && t[1] && t[3]);
+
+ const address = addressFork ? parseCoordinate(addressFork[1], true) ?? undefined : undefined;
+ const event: nip19.EventPointer | undefined = eventFork ? { id: eventFork[1] } : undefined;
+
+ return { event, address };
+}
+
+export function isPageFork(page: NostrEvent) {
+ return page.tags.some((t) => (t[0] === "a" || t[0] === "e") && t[3] === "fork");
+}
+
+export function validatePage(page: NostrEvent) {
+ try {
+ getPageTopic(page);
+ return true;
+ } catch (error) {
+ return false;
+ }
+}
diff --git a/src/views/wiki/compare.tsx b/src/views/wiki/compare.tsx
new file mode 100644
index 000000000..1c7e2ffd4
--- /dev/null
+++ b/src/views/wiki/compare.tsx
@@ -0,0 +1,74 @@
+import { NostrEvent } from "nostr-tools";
+import { Alert, AlertIcon, Box, ButtonGroup, Divider, Flex, Heading, Spinner, Text } from "@chakra-ui/react";
+import { Navigate, useParams } from "react-router-dom";
+
+import useReplaceableEvent from "../../hooks/use-replaceable-event";
+import VerticalPageLayout from "../../components/vertical-page-layout";
+import { WIKI_PAGE_KIND, getPageTitle } from "../../helpers/nostr/wiki";
+import Timestamp from "../../components/timestamp";
+import DebugEventButton from "../../components/debug-modal/debug-event-button";
+import WikiPageHeader from "./components/wiki-page-header";
+import DiffViewer from "../../components/diff/diff-viewer";
+import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
+import MarkdownContent from "./components/markdown";
+import { WIKI_RELAYS } from "../../const";
+import UserName from "../../components/user/user-name";
+
+function WikiComparePage({ base, diff }: { base: NostrEvent; diff: NostrEvent }) {
+ const vertical = useBreakpointValue({ base: true, lg: false }) ?? false;
+ const identical = base.content.trim() === diff.content.trim();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {getPageTitle(base)} -
+
+
+
+
+
+
+
+
+
+
+ {getPageTitle(diff)} -
+
+
+
+
+ {identical ? (
+ <>
+
+
+ Both versions are identical
+
+
+ >
+ ) : (
+
+ )}
+
+ );
+}
+
+export default function WikiCompareView() {
+ const { topic, a, b } = useParams();
+ if (!topic || !a || !b) return ;
+
+ const base = useReplaceableEvent({ kind: WIKI_PAGE_KIND, identifier: topic, pubkey: a }, WIKI_RELAYS);
+ const diff = useReplaceableEvent({ kind: WIKI_PAGE_KIND, identifier: topic, pubkey: b }, WIKI_RELAYS);
+
+ if (!base || !diff) return ;
+ return ;
+}
diff --git a/src/views/wiki/components/wiki-page-header.tsx b/src/views/wiki/components/wiki-page-header.tsx
new file mode 100644
index 000000000..a8f991aab
--- /dev/null
+++ b/src/views/wiki/components/wiki-page-header.tsx
@@ -0,0 +1,17 @@
+import { Flex, Heading, Link } from "@chakra-ui/react";
+import { Link as RouterLink } from "react-router-dom";
+
+import WikiSearchForm from "./wiki-search-form";
+
+export default function WikiPageHeader() {
+ return (
+
+
+
+ Wikifreedia
+
+
+
+
+ );
+}
diff --git a/src/views/wiki/components/wiki-page-result.tsx b/src/views/wiki/components/wiki-page-result.tsx
index ef1bbe343..f72b11271 100644
--- a/src/views/wiki/components/wiki-page-result.tsx
+++ b/src/views/wiki/components/wiki-page-result.tsx
@@ -1,27 +1,62 @@
-import { Heading, LinkBox, Text } from "@chakra-ui/react";
-import { NostrEvent } from "nostr-tools";
+import { useMemo } from "react";
+import { Box, Button, ButtonGroup, Flex, Heading, LinkBox, Text } from "@chakra-ui/react";
+import { NostrEvent, nip19 } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import { getSharableEventAddress } from "../../../helpers/nip19";
-import { getPageSummary, getPageTitle } from "../../../helpers/nostr/wiki";
+import { getPageForks, getPageSummary, getPageTitle, getPageTopic } from "../../../helpers/nostr/wiki";
import UserLink from "../../../components/user/user-link";
import Timestamp from "../../../components/timestamp";
+import FileSearch01 from "../../../components/icons/file-search-01";
+import GitBranch01 from "../../../components/icons/git-branch-01";
+import UserName from "../../../components/user/user-name";
+
+export default function WikiPageResult({ page, compare }: { page: NostrEvent; compare?: NostrEvent }) {
+ const topic = getPageTopic(page);
+
+ const { address } = useMemo(() => getPageForks(page), [page]);
-export default function WikiPageResult({ page }: { page: NostrEvent }) {
return (
-
-
-
- {getPageTitle(page)}
-
-
-
- by -
-
-
- {getPageSummary(page)}
-
-
+
+
+
+
+
+ {getPageTitle(page)}
+
+
+
+ by -
+
+
+ {getPageSummary(page)}
+
+
+
+
+ {address && (
+ }
+ >
+
+
+ )}
+ {compare && (
+ }
+ ml="auto"
+ >
+ Compare
+
+ )}
+
+
);
}
diff --git a/src/views/wiki/hooks/use-wiki-topic-timeline.tsx b/src/views/wiki/hooks/use-wiki-topic-timeline.tsx
index 25643e26f..b5c1bf7f5 100644
--- a/src/views/wiki/hooks/use-wiki-topic-timeline.tsx
+++ b/src/views/wiki/hooks/use-wiki-topic-timeline.tsx
@@ -1,19 +1,22 @@
import { NostrEvent } from "nostr-tools";
-import { WIKI_PAGE_KIND } from "../../../helpers/nostr/wiki";
+
+import { WIKI_PAGE_KIND, validatePage } from "../../../helpers/nostr/wiki";
import { useReadRelays } from "../../../hooks/use-client-relays";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
+import { WIKI_RELAYS } from "../../../const";
-function noEmptyEvent(event: NostrEvent) {
+function eventFilter(event: NostrEvent) {
+ if (!validatePage(event)) return false;
return event.content.length > 0;
}
export default function useWikiTopicTimeline(topic: string) {
- const relays = useReadRelays(["wss://relay.wikifreedia.xyz/"]);
+ const relays = useReadRelays(WIKI_RELAYS);
return useTimelineLoader(
`wiki-${topic.toLocaleLowerCase()}-pages`,
relays,
[{ kinds: [WIKI_PAGE_KIND], "#d": [topic.toLocaleLowerCase()] }],
- { eventFilter: noEmptyEvent },
+ { eventFilter: eventFilter },
);
}
diff --git a/src/views/wiki/index.tsx b/src/views/wiki/index.tsx
index f022b605b..86a275f37 100644
--- a/src/views/wiki/index.tsx
+++ b/src/views/wiki/index.tsx
@@ -1,20 +1,28 @@
import { Box, Flex, Heading, SimpleGrid } from "@chakra-ui/react";
import { Link } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
+import { NostrEvent } from "nostr-tools";
import VerticalPageLayout from "../../components/vertical-page-layout";
import WikiSearchForm from "./components/wiki-search-form";
-import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki";
+import { WIKI_PAGE_KIND, validatePage } from "../../helpers/nostr/wiki";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelays } from "../../hooks/use-client-relays";
import useSubject from "../../hooks/use-subject";
import { getWebOfTrust } from "../../services/web-of-trust";
import WikiPageResult from "./components/wiki-page-result";
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
+import { ErrorBoundary } from "../../components/error-boundary";
+import { WIKI_RELAYS } from "../../const";
+
+function eventFilter(event: NostrEvent) {
+ if (!validatePage(event)) return false;
+ return event.content.length > 0;
+}
export default function WikiHomeView() {
- const relays = useReadRelays(["wss://relay.wikifreedia.xyz/"]);
- const timeline = useTimelineLoader(`wiki-recent-pages`, relays, [{ kinds: [WIKI_PAGE_KIND] }]);
+ const relays = useReadRelays(WIKI_RELAYS);
+ const timeline = useTimelineLoader(`wiki-recent-pages`, relays, [{ kinds: [WIKI_PAGE_KIND] }], { eventFilter });
const pages = useSubject(timeline.timeline).filter((p) => p.content.length > 0);
const sorted = getWebOfTrust().sortByDistanceAndConnections(pages, (p) => p.pubkey);
@@ -35,7 +43,9 @@ export default function WikiHomeView() {
{sorted.map((page) => (
-
+
+
+
))}
diff --git a/src/views/wiki/page.tsx b/src/views/wiki/page.tsx
index f28ae0d0d..229f9ef21 100644
--- a/src/views/wiki/page.tsx
+++ b/src/views/wiki/page.tsx
@@ -1,12 +1,22 @@
-import { NostrEvent } from "nostr-tools";
-import { Box, ButtonGroup, Divider, Flex, Heading, Link, Spinner, Text } from "@chakra-ui/react";
+import { NostrEvent, nip19 } from "nostr-tools";
+import {
+ Alert,
+ AlertIcon,
+ Box,
+ Button,
+ ButtonGroup,
+ Divider,
+ Heading,
+ SimpleGrid,
+ Spinner,
+ Text,
+} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import useParamsAddressPointer from "../../hooks/use-params-address-pointer";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import VerticalPageLayout from "../../components/vertical-page-layout";
-import { getPageTitle, getPageTopic } from "../../helpers/nostr/wiki";
-import WikiSearchForm from "./components/wiki-search-form";
+import { getPageForks, getPageTitle, getPageTopic } from "../../helpers/nostr/wiki";
import MarkdownContent from "./components/markdown";
import UserLink from "../../components/user/user-link";
import { getWebOfTrust } from "../../services/web-of-trust";
@@ -15,45 +25,94 @@ import useWikiTopicTimeline from "./hooks/use-wiki-topic-timeline";
import WikiPageResult from "./components/wiki-page-result";
import Timestamp from "../../components/timestamp";
import DebugEventButton from "../../components/debug-modal/debug-event-button";
+import WikiPageHeader from "./components/wiki-page-header";
+import { WIKI_RELAYS } from "../../const";
+import GitBranch01 from "../../components/icons/git-branch-01";
+import { ExternalLinkIcon } from "../../components/icons";
+import FileSearch01 from "../../components/icons/file-search-01";
+import NoteZapButton from "../../components/note/note-zap-button";
+import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbles";
+import QuoteRepostButton from "../../components/note/quote-repost-button";
function WikiPagePage({ page }: { page: NostrEvent }) {
const topic = getPageTopic(page);
const timeline = useWikiTopicTimeline(topic);
const pages = useSubject(timeline.timeline).filter((p) => p.pubkey !== page.pubkey);
- const sorted = getWebOfTrust().sortByDistanceAndConnections(pages, (p) => p.pubkey);
+ const { address } = getPageForks(page);
+
+ const forks = getWebOfTrust().sortByDistanceAndConnections(
+ pages.filter((p) => getPageForks(p).address?.pubkey === page.pubkey),
+ (p) => p.pubkey,
+ );
+ const other = getWebOfTrust().sortByDistanceAndConnections(
+ pages.filter((p) => !forks.includes(p)),
+ (p) => p.pubkey,
+ );
return (
-
-
-
- Wikifreedia
-
-
-
-
+
+
+
{getPageTitle(page)}
by -
+ {address && (
+
+
+
+
+
+ This page was forked from version
+
+
+ } as={RouterLink} to={`/wiki/page/${nip19.naddrEncode(address)}`}>
+ Original
+
+ }
+ as={RouterLink}
+ to={`/wiki/compare/${topic}/${address.pubkey}/${page.pubkey}`}
+ >
+ Compare
+
+
+
+ )}
+
- {sorted.length > 0 && (
+ {forks.length > 0 && (
+ <>
+
+ Forks:
+
+
+ {forks.map((p) => (
+
+ ))}
+
+ >
+ )}
+ {other.length > 0 && (
<>
Other Versions:
- {sorted.slice(0, 6).map((page) => (
-
- ))}
+
+ {other.map((p) => (
+
+ ))}
+
>
)}
@@ -62,7 +121,7 @@ function WikiPagePage({ page }: { page: NostrEvent }) {
export default function WikiPageView() {
const pointer = useParamsAddressPointer("naddr");
- const event = useReplaceableEvent(pointer, ["wss://relay.wikifreedia.xyz/"]);
+ const event = useReplaceableEvent(pointer, WIKI_RELAYS);
if (!event) return ;
return ;