diff --git a/browser/src/components/ContributionCard.tsx b/browser/src/components/ContributionCard.tsx index 7922d24..6f95a8a 100644 --- a/browser/src/components/ContributionCard.tsx +++ b/browser/src/components/ContributionCard.tsx @@ -1,6 +1,6 @@ import { Author, - Contribution, + ClientContribution, PatternToDisplay, } from "src/types/common/server-api"; import dayjs from "dayjs"; @@ -14,11 +14,12 @@ import { import { getPatternPlaceholder } from "src/types"; import { getDisplayForAuthor } from "./SignatureContent"; -import sanitizeHtml from "sanitize-html"; import parse from 'html-react-parser'; +import sanitizeHtml from "sanitize-html"; +import ContributionsCarousel from "./ContributionsCarousel"; interface Props { - contribution: Contribution; + contribution: ClientContribution; hideHeader?: boolean; isCompact?: boolean; className?: string; @@ -28,7 +29,7 @@ export function getFullContributionResponse({ response, prompt, pattern, -}: Contribution) { +}: ClientContribution) { return ( PromptDescriptions[prompt].replace( `{${Placeholder}}`, @@ -45,13 +46,28 @@ export function ContributionCard({ isCompact = false, className = "", }: Props) { - const { author, response, prompt, pattern, createdAt } = contribution; + const { author, response, responseHtml, prompt, pattern, createdAt } = contribution; const authorDisplay = getDisplayForAuthor(author, true); const date = dayjs(createdAt, { utc: true }); const dateDisplay = date.format("MMM, YYYY"); - - const responseHtml = parse(sanitizeHtml(response)); + + const renderHtml = (resp: string): string | JSX.Element | JSX.Element[] => { + // Remove first p tag to prevent first text going to next line, sanitize html string + // and then convert to JSX element + return parse( + sanitizeHtml( + resp.replace( + /]*>|<\/p[^>]*>/, + "" + ) + ) + ) + }; + + const input = renderHtml( + responseHtml || response + ); return (
{getPatternPlaceholder(pattern, prompt)}, })}{" "} - {responseHtml} + {input}

(null); const [displayLinkModal, setDisplayLinkModal] = useState(false); - - const sanitize = (inputHtml: string): string => { - // Remove first p tag to prevent text going to next line - return inputHtml.replace( - /]*>|<\/p[^>]*>/, - "" - ); - }; + const [isInvalidInput, setIsInvalidInput] = useState(false); const openModal = () => { setDisplayLinkModal(true) toggleBackgroundScrollingOnModal(false); } + const closeModal = () => { + setDisplayLinkModal(false); + setLinkInput(null); + toggleBackgroundScrollingOnModal(true) + } + // Set Cmd/Ctrl-k shortcut const CustomLink = Link.extend({ addKeyboardShortcuts() { @@ -76,27 +80,37 @@ export function Editor({ ], onUpdate: ({ editor }) => { onChange( - sanitize( - editor.getHTML() - ) + sanitizeHtml(editor.getHTML()) ); + // Okay so what we want is ContributionCard should render the + // html directly to be fast, but we should store markdown + // so we should keep onChange the same so that contribution card is + // recieve the html, but we need to find a way to persist the + // markdown, but only to the request and not the contribution card + // maybe a set markdown field? or an optional value on the AddContributionRequest setResponseLength(editor.storage.characterCount.characters()); }, }) - const setLink = (cancel: boolean) => { - if (cancel) { - // Previous url - return editor.getAttributes('link').href; + const setLink = (save: boolean) => { + setIsInvalidInput(false); + if (!save) { + closeModal(); } - var url = linkInput; - // TODO: Add Link validation - - if (url === "" || url === null || url === undefined) { - //editor.chain().focus().extendMarkRange('link').unsetLink().run() - editor.chain().focus().unsetLink().run() - return + var url = null; + // If link is not null, check if it's valid and display error message otherwise. + if (!(linkInput === "" || linkInput === null || linkInput === undefined)) { + if (isURL(linkInput)) { + url = linkInput; + } else { + setIsInvalidInput(true); + return; + } + } else { + editor.chain().focus().unsetLink().run(); + closeModal(); + return; } // Add so href doesn't point to pluriverse.world/{url} @@ -107,22 +121,9 @@ export function Editor({ url = "http://" + url; } - // Update link - editor.chain().focus().setLink({ href: url }).run() - }; - - const onClickAddLink = () => { - setLink(false); - setDisplayLinkModal(false); - setLinkInput(null); - toggleBackgroundScrollingOnModal(true) - }; - - const onCloseModal = () => { - setLink(true); - setDisplayLinkModal(false); - setLinkInput(null); - toggleBackgroundScrollingOnModal(true); + // Set link. + editor.chain().focus().setLink({ href: url }).run(); + closeModal(); }; const getPreviousLink = () => { @@ -168,30 +169,30 @@ export function Editor({ }} className={`menuItem linkIcon ${displayLinkModal ? 'shimmer' : 'white'}`} > - 🔗 + 🔗
} - onCloseModal()} shouldCloseOnOverlayClick={true} >

- Add Link + Add Link

- { - setLinkInput(e.target.value) + setLinkInput((e.target.value) || "") }} onKeyPress={handleKeyPress} autoFocus @@ -201,24 +202,24 @@ export function Editor({
- ) diff --git a/browser/src/helpers/api.ts b/browser/src/helpers/api.ts index 6056684..952abfe 100644 --- a/browser/src/helpers/api.ts +++ b/browser/src/helpers/api.ts @@ -3,6 +3,7 @@ import { AddContributionResponse, AddContributionRequest, GetContributionRequest, + ClientContribution, Contribution, GetContributionsRequest, AddUserRequest, @@ -13,6 +14,8 @@ import { VerifyTwitterRequest, } from "../types/common/server-api"; +import { Converter } from "showdown"; + export function withQueryParams( url: string, params: Record @@ -70,23 +73,28 @@ export async function addContribution( body: request, method: "POST", }); - console.log(`Added ${response} contribution`); return response as AddContributionResponse; } export async function getContribution({ id, -}: GetContributionRequest): Promise { +}: GetContributionRequest): Promise { const response = await makeRequest(`${ApiUrl}/contributions/${id}`, { method: "GET", }); - return response as Contribution; + + const mdToHtmlConverter = new Converter(); + response.responseHtml = mdToHtmlConverter.makeHtml( + response.response + ); + + return response as ClientContribution; } export async function getContributions({ offset, contributionId, -}: GetContributionsRequest): Promise { +}: GetContributionsRequest): Promise { const response = await makeRequest( withQueryParams(`${ApiUrl}/contributions`, { offset: offset ? String(offset) : offset, @@ -96,7 +104,16 @@ export async function getContributions({ method: "GET", } ); - return response as Contribution[]; + + const mdToHtmlConverter = new Converter(); + const responseWithHtml = response.map((res) => { + res.responseHtml = mdToHtmlConverter.makeHtml( + res.response + ); + return res; + }); + + return response as ClientContribution[]; } export async function getUser({ diff --git a/browser/src/types/common/server-api/index.ts b/browser/src/types/common/server-api/index.ts index 01872e4..e3a7b97 100644 --- a/browser/src/types/common/server-api/index.ts +++ b/browser/src/types/common/server-api/index.ts @@ -59,10 +59,15 @@ export interface Contribution { createdAt: Date; } +export interface ClientContribution extends Contribution { + responseHtml?: string; +} + export interface AddContributionRequest { walletId: string; // This should be the full text response, formatted as markdown. response: string; + // Full text response formatted as markdown as HTML. prompt: Prompt; pattern: Pattern; } diff --git a/browser/yarn.lock b/browser/yarn.lock index 054837c..99c6dc0 100644 --- a/browser/yarn.lock +++ b/browser/yarn.lock @@ -5566,6 +5566,11 @@ entities@^3.0.1: resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + err-code@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" @@ -7154,7 +7159,7 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: inherits "^2.0.3" minimalistic-assert "^1.0.1" -he@1.2.0, he@^1.2.0: +he@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -8918,6 +8923,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" + linkifyjs@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-3.0.5.tgz#99e51a3a0c0e232fcb63ebb89eea3ff923378f34" @@ -9106,6 +9118,17 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-it@^12.0.0: + version "12.3.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -9125,6 +9148,11 @@ mdn-data@2.0.4: resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + media-typer@0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" @@ -9466,14 +9494,6 @@ node-gyp-build@^4.2.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -node-html-parser@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.2.0.tgz#6f29fd00d79f65334e7e20200964644207925607" - integrity sha512-fmiwLfQu+J2A0zjwSEkztSHexAf5qq/WoiL/Hgo1K7JpfEP+OGWY5maG0kGaM+IFVdixF/1QbyXaQ3h4cGfeLw== - dependencies: - css-select "^4.1.3" - he "1.2.0" - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" @@ -11973,6 +11993,13 @@ shell-quote@^1.7.3: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz" integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== +showdown@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/showdown/-/showdown-1.9.1.tgz#134e148e75cd4623e09c21b0511977d79b5ad0ef" + integrity sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA== + dependencies: + yargs "^14.2" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" @@ -12966,6 +12993,11 @@ typescript@^4.4.2: resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz" integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + uint8arrays@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" @@ -13162,6 +13194,11 @@ v8-to-istanbul@^8.1.0: convert-source-map "^1.6.0" source-map "^0.7.3" +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== + varint@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" @@ -13839,6 +13876,14 @@ yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^15.0.1: + version "15.0.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.3.tgz#316e263d5febe8b38eef61ac092b33dfcc9b1115" + integrity sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" @@ -13860,6 +13905,23 @@ yargs@^13.2.4: y18n "^4.0.0" yargs-parser "^13.1.2" +yargs@^14.2: + version "14.2.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" + integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== + dependencies: + cliui "^5.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^15.0.1" + yargs@^16.2.0: version "16.2.0" resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz"