Signal drop!
Relay (operand.online) is unreachable.
Usually, a dropped signal means an upgrade is happening. Hold on!
Sorry, no connección.
Hang in there while we get back on track
gram: essay
> ./src/DocExplorer/hooks/useSelectedDocLink.tsx
Lenses
(coming soon!)
import {
AutomergeUrl,
DocumentId,
Repo,
isValidAutomergeUrl,
stringifyAutomergeUrl,
} from "@automerge/automerge-repo";
import { useEffect, useMemo, useState } from "react";
import {
DocLink,
DocLinkWithFolderPath,
FolderDoc,
} from "../../folders/datatype";
import { DatatypeId, datatypes } from "../../datatypes";
import { useCurrentUrl } from "../navigation";
import queryString from "query-string";
import { FolderDocWithMetadata } from "@/folders/useFolderDocWithChildren";
import { isEqual } from "lodash";
const docLinkToUrl = (docLink: DocLink): string => {
const documentId = docLink.url.split(":")[1];
const name = `${docLink.name.trim().replace(/\s/g, "-").toLowerCase()}-`;
return `${name}${documentId}?docType=${docLink.type}`;
};
const isDocType = (x: string): x is DatatypeId =>
Object.keys(datatypes).includes(x as DatatypeId);
// Parse older URL formats and map them into our newer format
const parseLegacyUrl = (
url: URL
): { url: AutomergeUrl; type: DatatypeId } | null => {
// First handle very old URLs that only had an Automerge URL:
// domain.com/automerge:12345
const possibleAutomergeUrl = url.pathname.slice(1);
if (isValidAutomergeUrl(possibleAutomergeUrl)) {
return {
url: possibleAutomergeUrl,
type: "essay",
};
}
// Now on to the main logic where we look for URLs of the form:
// domain.com/#?docUrl=automerge:12345&docType=essay
const { docUrl, docType } = queryString.parse(url.pathname.slice(1));
if (typeof docUrl !== "string" || typeof docType !== "string") {
return null;
}
if (typeof docUrl === "string" && !isValidAutomergeUrl(docUrl)) {
alert(`Invalid Automerge URL in URL: ${docUrl}`);
return null;
}
if (typeof docType === "string" && !isDocType(docType)) {
alert(`Invalid doc type in URL: ${docType}`);
return null;
}
return {
url: docUrl,
type: docType,
};
};
const parseUrl = (url: URL): Omit<DocLink, "name"> | null => {
const match = url.pathname.match(/^\/(?<name>.*-)?(?<docId>\w+)$/);
if (!match) {
return;
}
const { docId } = match.groups;
const docType = url.searchParams.get("docType");
const docUrl = stringifyAutomergeUrl(docId as DocumentId);
if (!isValidAutomergeUrl(docUrl)) {
alert(`Invalid doc id in URL: ${docUrl}`);
return null;
}
if (!isDocType(docType)) {
alert(`Invalid doc type in URL: ${docType}`);
return null;
}
// hack: allow to easily switch to patchwork by adding "&patchwork=1" to the url
// todo: remove once patchwork is migrated to new url schema
if (url.searchParams.get("patchwork")) {
window.location.assign(
`https://patchwork.tee.inkandswitch.com/#docType=${docType}&docUrl=${docUrl}`
);
}
return {
url: docUrl,
type: docType,
};
};
// This hook manages the currently selected doc shown in the interface.
// It may look more complex than necessary, but that's because we take some
// care with how we manage selection state and the URL hash:
//
// - Selection state is primarily stored in the URL hash and driven by URL changes.
// Even when we set selection within the app, we do so by updating the URL.
// This reduces the risk of the URL and the selected doc getting out of sync,
// which simplifies dataflow and also reduces risk of user sharing the wrong doc.
// - There is an exception to the above: while we can store the currently selected
// docURL in the URL hash, we can't store the folder path leading to it, because
// that could leak folders that aren't meant to be shared when sharing the URL.
// As a result, we store the folder path in React state, and then reassemble it
// together with the URL hash when determining the selected doc.
//
export const useSelectedDocLink = ({
folderDocWithMetadata,
repo,
}: {
folderDocWithMetadata: FolderDocWithMetadata | undefined;
repo: Repo;
}): {
selectedDocLink: DocLinkWithFolderPath | undefined;
// todo: should the folder path be optional?
selectDocLink: (docLink: DocLinkWithFolderPath) => void;
} => {
// parse the current URL
const currentUrl = useCurrentUrl();
const urlParams = useMemo(() => parseUrl(currentUrl), [currentUrl]);
// NOTE: this should not be externally exposed, it's just a way to store
// the folder path to the selection outside the URL.
const [
selectedDocLinkDangerouslyBypassingURL,
setSelectedDocLinkDangerouslyBypassingURL,
] = useState<DocLinkWithFolderPath | undefined>();
const selectedDocLink = useMemo(() => {
if (!urlParams || !urlParams.url) {
return undefined;
}
return folderDocWithMetadata?.flatDocLinks?.find((doc) => {
const urlMatches = doc.url === urlParams?.url;
const folderPathMatches =
// If we don't have a selected folder path, then just take the first link we find
!selectedDocLinkDangerouslyBypassingURL ||
isEqual(
doc.folderPath,
selectedDocLinkDangerouslyBypassingURL?.folderPath
);
return urlMatches && folderPathMatches;
});
}, [
urlParams,
folderDocWithMetadata?.flatDocLinks,
selectedDocLinkDangerouslyBypassingURL,
]);
const selectDocLink = (docLink: DocLinkWithFolderPath | null) => {
if (!docLink) {
setSelectedDocLinkDangerouslyBypassingURL(undefined);
window.location.hash = "";
return;
}
setSelectedDocLinkDangerouslyBypassingURL(docLink);
window.location.hash = docLinkToUrl(docLink);
};
// Add the doc to our collection if we don't have it
useEffect(() => {
if (
!folderDocWithMetadata.flatDocLinks ||
!urlParams?.url ||
!urlParams?.type
) {
return;
}
if (
!folderDocWithMetadata.flatDocLinks.find(
(doc) => doc.url === urlParams?.url
)
) {
repo.find<FolderDoc>(folderDocWithMetadata.rootFolderUrl).change((doc) =>
doc.docs.unshift({
type: urlParams?.type,
// The name will be synced in here once the doc loads
name: "Loading...",
url: urlParams?.url,
})
);
}
}, [folderDocWithMetadata, urlParams, repo]);
// Redirect legacy urls to our current URL format
useEffect(() => {
if (currentUrl && !urlParams) {
const legacyUrlParams = parseLegacyUrl(currentUrl);
if (!legacyUrlParams) {
return;
}
const { url, type } = legacyUrlParams;
if (url) {
window.location.hash = docLinkToUrl({ url, type, name: "" });
}
}
}, [currentUrl, urlParams]);
return {
selectedDocLink,
selectDocLink,
};
};