diff --git a/src/app/UserItemListPage.js b/src/app/UserItemListPage.js
index cb34bd0..f924728 100644
--- a/src/app/UserItemListPage.js
+++ b/src/app/UserItemListPage.js
@@ -9,13 +9,14 @@ import {
Wrap,
WrapItem,
} from "@chakra-ui/react";
+import { ChevronRightIcon } from "@chakra-ui/icons";
import { Heading1, MajorErrorMessage } from "./util";
import { gql, useQuery } from "@apollo/client";
import { Link, useParams } from "react-router-dom";
import { HashLink } from "react-router-hash-link";
import HangerSpinner from "./components/HangerSpinner";
-import { ChevronRightIcon } from "@chakra-ui/icons";
+import MarkdownAndSafeHTML from "./components/MarkdownAndSafeHTML";
import ItemCard from "./components/ItemCard";
import WIPCallout from "./components/WIPCallout";
@@ -28,6 +29,7 @@ function UserItemListPage() {
closetList(id: $listId) {
id
name
+ description
ownsOrWantsItems
creator {
id
@@ -43,7 +45,7 @@ function UserItemListPage() {
}
}
`,
- { variables: { listId } }
+ { variables: { listId }, context: { sendAuth: true } }
);
if (loading) {
@@ -102,49 +104,20 @@ function UserItemListPage() {
/>
- {/* TODO: Description */}
+ {closetList.description && (
+ {closetList.description}
+ )}
);
}
function ClosetListContents({ closetList }) {
- const isCurrentUser = false; // TODO
+ const sortedItems = [...closetList.items].sort((a, b) =>
+ a.name.localeCompare(b.name)
+ );
- // TODO: A lot of this is duplicated from UserItemsPage, find shared
- // abstractions!
- const hasYouWantThisBadge = (item) =>
- !isCurrentUser &&
- closetList.ownsOrWantsItems === "OWNS" &&
- item.currentUserWantsThis;
- const hasYouOwnThisBadge = (item) =>
- !isCurrentUser &&
- closetList.ownsOrWantsItems === "WANTS" &&
- item.currentUserOwnsThis;
- const hasAnyTradeBadge = (item) =>
- hasYouOwnThisBadge(item) || hasYouWantThisBadge(item);
-
- const sortedItems = [...closetList.items].sort((a, b) => {
- // This is a cute sort hack. We sort first by, bringing "You own/want
- // this!" to the top, and then sorting by name _within_ those two groups.
- const aName = `${hasAnyTradeBadge(a) ? "000" : "999"} ${a.name}`;
- const bName = `${hasAnyTradeBadge(b) ? "000" : "999"} ${b.name}`;
- return aName.localeCompare(bName);
- });
-
- let tradeMatchingMode;
- if (isCurrentUser) {
- // On your own item list, it's not helpful to show your own trade matches!
- tradeMatchingMode = "hide-all";
- } else if (closetList.ownsOrWantsItems === "OWNS") {
- tradeMatchingMode = "offering";
- } else if (closetList.ownsOrWantsItems === "WANTS") {
- tradeMatchingMode = "seeking";
- } else {
- throw new Error(
- `unexpected ownsOrWantsItems value: ${closetList.ownsOrWantsItems}`
- );
- }
+ const tradeMatchingMode = "hide-all"; // TODO
return (
diff --git a/src/app/UserItemsPage.js b/src/app/UserItemsPage.js
index ab8035b..037efc5 100644
--- a/src/app/UserItemsPage.js
+++ b/src/app/UserItemsPage.js
@@ -34,12 +34,11 @@ import {
import gql from "graphql-tag";
import { Link, useHistory, useParams } from "react-router-dom";
import { useQuery, useLazyQuery, useMutation } from "@apollo/client";
-import SimpleMarkdown from "simple-markdown";
-import DOMPurify from "dompurify";
import HangerSpinner from "./components/HangerSpinner";
import { Heading1, Heading2, Heading3 } from "./util";
import ItemCard from "./components/ItemCard";
+import MarkdownAndSafeHTML from "./components/MarkdownAndSafeHTML";
import SupportOnly from "./WardrobePage/support/SupportOnly";
import useSupport from "./WardrobePage/support/useSupport";
import useCurrentUser from "./components/useCurrentUser";
@@ -657,81 +656,6 @@ function buildClosetListPath(closetList) {
return `/user/${closetList.creator.id}/lists/${ownsOrWants}/${idString}`;
}
-const unsafeMarkdownRules = {
- autolink: SimpleMarkdown.defaultRules.autolink,
- br: SimpleMarkdown.defaultRules.br,
- em: SimpleMarkdown.defaultRules.em,
- escape: SimpleMarkdown.defaultRules.escape,
- link: SimpleMarkdown.defaultRules.link,
- list: SimpleMarkdown.defaultRules.list,
- newline: SimpleMarkdown.defaultRules.newline,
- paragraph: SimpleMarkdown.defaultRules.paragraph,
- strong: SimpleMarkdown.defaultRules.strong,
- u: SimpleMarkdown.defaultRules.u,
-
- // DANGER: We override Markdown's `text` rule to _not_ escape HTML. This is
- // intentional, to allow users to embed some limited HTML. DOMPurify is
- // responsible for sanitizing the HTML afterward. Do not use these rules
- // without sanitizing!!
- text: {
- ...SimpleMarkdown.defaultRules.text,
- html: (node) => node.content,
- },
-};
-const markdownParser = SimpleMarkdown.parserFor(unsafeMarkdownRules);
-const unsafeMarkdownOutput = SimpleMarkdown.htmlFor(
- SimpleMarkdown.ruleOutput(unsafeMarkdownRules, "html")
-);
-
-function MarkdownAndSafeHTML({ children }) {
- const htmlAndMarkdown = children;
-
- const unsafeHtml = unsafeMarkdownOutput(markdownParser(htmlAndMarkdown));
-
- const sanitizedHtml = DOMPurify.sanitize(unsafeHtml, {
- ALLOWED_TAGS: [
- "b",
- "i",
- "u",
- "strong",
- "em",
- "a",
- "p",
- "div",
- "br",
- "ol",
- "ul",
- "li",
- ],
- ALLOWED_ATTR: ["href", "class"],
- // URL must either start with an approved host (external link), or with a
- // slash or hash (internal link).
- ALLOWED_URI_REGEXP: /^https?:\/\/(impress\.openneo\.net|impress-2020\.openneo\.net|www\.neopets\.com|neopets\.com|items\.jellyneo\.net)\/|^[/#]/,
- });
-
- return (
-
- {({ css }) => (
-
- )}
-
- );
-}
-
function UserSupportMenu({ children, user }) {
const { supportSecret } = useSupport();
const toast = useToast();
diff --git a/src/app/components/MarkdownAndSafeHTML.js b/src/app/components/MarkdownAndSafeHTML.js
new file mode 100644
index 0000000..b97bdb7
--- /dev/null
+++ b/src/app/components/MarkdownAndSafeHTML.js
@@ -0,0 +1,89 @@
+import React from "react";
+import { ClassNames } from "@emotion/react";
+import { Box } from "@chakra-ui/react";
+import SimpleMarkdown from "simple-markdown";
+import DOMPurify from "dompurify";
+
+const unsafeMarkdownRules = {
+ autolink: SimpleMarkdown.defaultRules.autolink,
+ br: SimpleMarkdown.defaultRules.br,
+ em: SimpleMarkdown.defaultRules.em,
+ escape: SimpleMarkdown.defaultRules.escape,
+ link: SimpleMarkdown.defaultRules.link,
+ list: SimpleMarkdown.defaultRules.list,
+ newline: SimpleMarkdown.defaultRules.newline,
+ paragraph: SimpleMarkdown.defaultRules.paragraph,
+ strong: SimpleMarkdown.defaultRules.strong,
+ u: SimpleMarkdown.defaultRules.u,
+
+ // DANGER: We override Markdown's `text` rule to _not_ escape HTML. This is
+ // intentional, to allow users to embed some limited HTML. DOMPurify is
+ // responsible for sanitizing the HTML afterward. Do not use these rules
+ // without sanitizing!!
+ text: {
+ ...SimpleMarkdown.defaultRules.text,
+ html: (node) => node.content,
+ },
+};
+const markdownParser = SimpleMarkdown.parserFor(unsafeMarkdownRules);
+const unsafeMarkdownOutput = SimpleMarkdown.htmlFor(
+ SimpleMarkdown.ruleOutput(unsafeMarkdownRules, "html")
+);
+
+/**
+ * MarkdownAndSafeHTML renders its children as a Markdown string, with some
+ * safe inline HTML allowed.
+ *
+ * Rendering this component *should* be XSS-safe, it's designed to strip out
+ * bad things! Still, be careful when using it, and consider what you're doing!
+ */
+function MarkdownAndSafeHTML({ children }) {
+ const htmlAndMarkdown = children;
+
+ const unsafeHtml = unsafeMarkdownOutput(markdownParser(htmlAndMarkdown));
+
+ const sanitizedHtml = DOMPurify.sanitize(unsafeHtml, {
+ ALLOWED_TAGS: [
+ "b",
+ "i",
+ "u",
+ "strong",
+ "em",
+ "a",
+ "p",
+ "div",
+ "br",
+ "ol",
+ "ul",
+ "li",
+ ],
+ ALLOWED_ATTR: ["href", "class"],
+ // URL must either start with an approved host (external link), or with a
+ // slash or hash (internal link).
+ ALLOWED_URI_REGEXP: /^https?:\/\/(impress\.openneo\.net|impress-2020\.openneo\.net|www\.neopets\.com|neopets\.com|items\.jellyneo\.net)\/|^[/#]/,
+ });
+
+ return (
+
+ {({ css }) => (
+
+ )}
+
+ );
+}
+
+export default MarkdownAndSafeHTML;