mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 17:50:42 +08:00
[CORL-1108] Media Embeds (#3010)
* Create Twitter and YouTube embed components Uses the `/api/oembed` endpoint to proxy the oembed requests that the embed components drop into an iframe. CORL-1012 * Create preliminary embed link parsing/storage CORL-1012 * Create preliminary embed section on comments CORL-1012 * Preliminarily add admin embed config options CORL-1012 * Show a "missing" message when embed is unavailable CORL-1012 * Simplify naming of embeds in schema embedLinks -> embed CORL-1012 * Rename oEmbedHandler to oembedHandler CORL-1012 * add backend services for giphy * search gifs on frontend * display selected gif * show gif previews * display giphy attribution and no results text * save a gif to a comment * use embeds feature for gif embeds * clean up gif/video/tweet display * style and configure post comment form * preview and confirm twitter and youtube embeds * moderate embeds on server * update reply and edit forms * update snaps * fix some of the tests * fix tests and types * fix tests * fix types * show gifs in moderate cards * correctly attach embeds to comments * make gif rating configurable * make gif rating configurable * configure giphy api key * refactor comment form * only allow embeds if settings enabled * scale youtube * resize embeds if necessary * make tweets and videos responsive * set maxwidth on tweet embeds * update copy for embed config * force gif search results to fit container * prevent double posting of gifs * undo hiding html if empmty because now it doesn't contain random break tags * use downsampled preview images * update fixtures and snapshots * remove unused css * add i18n string * remove console logs * Fix styles on logged-out comment form * click to pause gif in moderation * style youtube and twitter embeds in mod stream" * use mp4s for stream gifs * use mp4 for moderation gifs * clean up commentform * fix dom tests * update rte * import oembed module with correct casing * bump rte * add correct return type for setInterval * add migration for embeds config * catch errors from gif search * return early from iframe container size calculation if width and height are set * remove unused classnames * make giphy api key protected * reorganize tenant embed settings schema * update schema on backend to support single comment embed instead of array * move findEmbedLinks to common * wrap error * return function for linkify instead of ternary * remove unused url param * clean up oembed service * remove conditional in repeat post check * use joi to validate giphy responses * fix types for embeds * fix optimistic responses * move attachEmbeds function * update snapshots * fix: improved repeatPost checking * force case change on oembed * force rename file name * feat: Rename Embed -> Media * fix: cleanup of service functions * fix: moved types * fix: fixed logic bug * fix: fixed translation * show embeds on history comments * fix: fixed iframe csp and query param bug * correct validation for twitter oembed * feat: save youtube still * fix: typeerror * fix: fixed errors related to final form * fix: fixed issue with types * fix: added docs to the schema * fix: linting + tests Co-authored-by: nick-funk <nick.funk@outlook.com> Co-authored-by: Wyatt Johnson <me@wyattjoh.ca>
This commit is contained in:
@@ -92,6 +92,8 @@ const typescriptOverrides = {
|
||||
"react/prop-types": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"no-empty-function": "off",
|
||||
// (tessalt) disabled because video elements are only used to display gifs, which have no audio
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
Generated
+90
-99
@@ -5087,9 +5087,9 @@
|
||||
}
|
||||
},
|
||||
"@coralproject/rte": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@coralproject/rte/-/rte-1.0.0.tgz",
|
||||
"integrity": "sha512-Z+NMytOSdz2qqphcevI9+s3CUzsW/LPzXPxVHAd9qKSlgJ+lr4pvht/gzgcvDvKy1hT88CompJwkqlIBxk0y9w==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@coralproject/rte/-/rte-1.1.1.tgz",
|
||||
"integrity": "sha512-r70xg7arHttiJr4pXCZTTWA86bjT9M+XHDeaYjNpMQrW5rIL+kw7sjoRuiyPLzi88xINMbxffurdKep2Z68bcg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"classnames": "^2.2.6",
|
||||
@@ -22068,6 +22068,13 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
|
||||
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"resolved": false,
|
||||
@@ -22127,12 +22134,28 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"fs-minipass": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
|
||||
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"minipass": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
|
||||
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"version": "2.7.4",
|
||||
@@ -22245,14 +22268,42 @@
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
|
||||
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
|
||||
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
@@ -22534,6 +22585,22 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.8",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz",
|
||||
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.3.4",
|
||||
"minizlib": "^1.1.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": false,
|
||||
@@ -22557,6 +22624,13 @@
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
|
||||
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -29139,7 +29213,6 @@
|
||||
"integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==",
|
||||
"requires": {
|
||||
"chownr": "^1.0.1",
|
||||
"minizlib": "^1.1.0",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.2"
|
||||
@@ -38530,35 +38603,6 @@
|
||||
"minipass": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
|
||||
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
|
||||
"requires": {
|
||||
"minipass": "^2.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"minipass": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
|
||||
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mississippi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
|
||||
@@ -49223,9 +49267,9 @@
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"squire-rte": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/squire-rte/-/squire-rte-1.10.1.tgz",
|
||||
"integrity": "sha512-BGTSZmwY3BFFvLaGSz6qFwzdCbRc0EhTW9rw3xNwlEIVHKBFzImDdzT1aM4dWZYyvNvAHknUuPvr/elNU7Nt1g==",
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/squire-rte/-/squire-rte-1.10.2.tgz",
|
||||
"integrity": "sha512-/IUZbc3+pdZUL+kP6wsUI8brDKtiP+EiIlw6Hj6q1n384NzUqQNa8iMLkeYZKC4o96ToyteY7fhv/DeS1GkWRw==",
|
||||
"dev": true
|
||||
},
|
||||
"sshpk": {
|
||||
@@ -50805,59 +50849,6 @@
|
||||
"integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==",
|
||||
"dev": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.13",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
|
||||
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.8.6",
|
||||
"minizlib": "^1.2.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-minipass": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
|
||||
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
|
||||
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"tar-stream": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.0.tgz",
|
||||
|
||||
+1
-1
@@ -155,7 +155,7 @@
|
||||
"@babel/preset-typescript": "^7.10.1",
|
||||
"@babel/runtime-corejs3": "^7.10.3",
|
||||
"@coralproject/npm-run-all": "^4.1.5",
|
||||
"@coralproject/rte": "^1.0.0",
|
||||
"@coralproject/rte": "^1.1.1",
|
||||
"@fluent/react": "^0.11.1",
|
||||
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
|
||||
"@types/archiver": "^3.1.0",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
|
||||
import { BaseButton, Flex, Icon } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./Media.css";
|
||||
|
||||
interface Props {
|
||||
still: string | null;
|
||||
title: string | null;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
video: string | null;
|
||||
}
|
||||
|
||||
const GiphyMedia: FunctionComponent<Props> = ({
|
||||
still,
|
||||
title,
|
||||
width,
|
||||
height,
|
||||
video,
|
||||
}) => {
|
||||
const [showAnimated, setShowAnimated] = useState(false);
|
||||
const toggleImage = useCallback(() => {
|
||||
setShowAnimated(!showAnimated);
|
||||
}, [showAnimated]);
|
||||
return (
|
||||
<div className={styles.embed}>
|
||||
{!showAnimated && still && (
|
||||
<BaseButton onClick={toggleImage} className={styles.toggle}>
|
||||
<img src={still} className={styles.image} alt={title || ""} />
|
||||
<Flex
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
className={styles.toggleTrigger}
|
||||
>
|
||||
<Icon size="xl" className={styles.playIcon}>
|
||||
play_circle_outline
|
||||
</Icon>
|
||||
<Localized id="moderate-comment-play-gif">
|
||||
<p className={styles.playText}>Play GIF</p>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</BaseButton>
|
||||
)}
|
||||
{showAnimated && video && (
|
||||
<BaseButton onClick={toggleImage}>
|
||||
<video
|
||||
width={width || undefined}
|
||||
height={height || undefined}
|
||||
autoPlay
|
||||
loop
|
||||
>
|
||||
<source src={video} type="video/mp4" />
|
||||
</video>
|
||||
</BaseButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GiphyMedia;
|
||||
@@ -0,0 +1,33 @@
|
||||
.embed {
|
||||
margin: var(--v2-spacing-2) 0;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggleTrigger {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(101, 105, 107, 0.6);
|
||||
}
|
||||
|
||||
.playIcon {
|
||||
color: var(--v2-colors-pure-white);
|
||||
}
|
||||
|
||||
.playText {
|
||||
color: var(--v2-colors-pure-white);
|
||||
font-size: var(--v2-font-size-2);
|
||||
font-weight: var(--v2-font-weight-primary-bold);
|
||||
font-family: var(--v2-font-family-primary);
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {} from "coral-ui/components/v2";
|
||||
|
||||
import { MediaContainer_comment } from "coral-admin/__generated__/MediaContainer_comment.graphql";
|
||||
|
||||
import GiphyMedia from "./GiphyMedia";
|
||||
import TwitterMedia from "./TwitterMedia";
|
||||
import YouTubeMedia from "./YouTubeMedia";
|
||||
|
||||
interface Props {
|
||||
comment: MediaContainer_comment;
|
||||
}
|
||||
|
||||
const MediaContainer: FunctionComponent<Props> = ({ comment }) => {
|
||||
if (!comment || !comment.revision || !comment.revision.media) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{comment.revision.media.__typename === "GiphyMedia" && (
|
||||
<GiphyMedia
|
||||
still={comment.revision.media.still}
|
||||
video={comment.revision.media.video}
|
||||
title={comment.revision.media.title}
|
||||
width={comment.revision.media.width}
|
||||
height={comment.revision.media.height}
|
||||
/>
|
||||
)}
|
||||
{comment.revision.media.__typename === "TwitterMedia" && (
|
||||
<TwitterMedia
|
||||
url={comment.revision.media.url}
|
||||
width={comment.revision.media.width}
|
||||
siteID={comment.site.id}
|
||||
/>
|
||||
)}
|
||||
{comment.revision.media.__typename === "YouTubeMedia" && (
|
||||
<YouTubeMedia
|
||||
url={comment.revision.media.url}
|
||||
width={comment.revision.media.width}
|
||||
height={comment.revision.media.height}
|
||||
siteID={comment.site.id}
|
||||
still={comment.revision.media.still}
|
||||
title={comment.revision.media.title}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment MediaContainer_comment on Comment {
|
||||
site {
|
||||
id
|
||||
}
|
||||
revision {
|
||||
media {
|
||||
__typename
|
||||
... on GiphyMedia {
|
||||
url
|
||||
title
|
||||
width
|
||||
height
|
||||
still
|
||||
video
|
||||
}
|
||||
... on TwitterMedia {
|
||||
url
|
||||
width
|
||||
}
|
||||
... on YouTubeMedia {
|
||||
url
|
||||
still
|
||||
title
|
||||
width
|
||||
height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(MediaContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -29,6 +29,7 @@ import ApproveButton from "./ApproveButton";
|
||||
import CommentAuthorContainer from "./CommentAuthorContainer";
|
||||
import FeatureButton from "./FeatureButton";
|
||||
import MarkersContainer from "./MarkersContainer";
|
||||
import MediaContainer from "./MediaContainer";
|
||||
import RejectButton from "./RejectButton";
|
||||
|
||||
import styles from "./ModerateCard.css";
|
||||
@@ -44,7 +45,8 @@ interface Props {
|
||||
username: string | null;
|
||||
} | null;
|
||||
comment: PropTypesOf<typeof MarkersContainer>["comment"] &
|
||||
PropTypesOf<typeof CommentAuthorContainer>["comment"];
|
||||
PropTypesOf<typeof CommentAuthorContainer>["comment"] &
|
||||
PropTypesOf<typeof MediaContainer>["comment"];
|
||||
settings: PropTypesOf<typeof MarkersContainer>["settings"];
|
||||
status: "approved" | "rejected" | "undecided";
|
||||
featured: boolean;
|
||||
@@ -238,13 +240,12 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.contentArea}>
|
||||
<CommentContent
|
||||
highlight={highlight}
|
||||
phrases={phrases}
|
||||
className={styles.content}
|
||||
>
|
||||
{commentBody}
|
||||
</CommentContent>
|
||||
<div className={styles.content}>
|
||||
<CommentContent highlight={highlight} phrases={phrases}>
|
||||
{commentBody}
|
||||
</CommentContent>
|
||||
<MediaContainer comment={comment} />
|
||||
</div>
|
||||
{onConversationClick && (
|
||||
<div className={styles.viewContext}>
|
||||
<Button iconLeft variant="text" onClick={onConversationClick}>
|
||||
|
||||
@@ -404,6 +404,7 @@ const enhanced = withFragmentContainer<Props>({
|
||||
...MarkersContainer_comment
|
||||
...ModeratedByContainer_comment
|
||||
...CommentAuthorContainer_comment
|
||||
...MediaContainer_comment
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./Media.css";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
width: number | null;
|
||||
siteID: string;
|
||||
}
|
||||
|
||||
const TwitterMedia: FunctionComponent<Props> = ({ url, width, siteID }) => {
|
||||
const cleanUrl = encodeURIComponent(url);
|
||||
return (
|
||||
<div className={styles.embed}>
|
||||
<iframe
|
||||
frameBorder="0"
|
||||
width={width || 450}
|
||||
allowFullScreen
|
||||
title="oEmbed"
|
||||
src={`/api/oembed?type=twitter&url=${cleanUrl}&siteID=${siteID}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwitterMedia;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
|
||||
import { BaseButton, Flex, Icon } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./Media.css";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
still: string;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
title: string | null;
|
||||
siteID: string;
|
||||
}
|
||||
|
||||
const YouTubeMedia: FunctionComponent<Props> = ({
|
||||
url,
|
||||
still,
|
||||
title,
|
||||
width,
|
||||
height,
|
||||
siteID,
|
||||
}) => {
|
||||
const [showAnimated, setShowAnimated] = useState(false);
|
||||
const toggleImage = useCallback(() => {
|
||||
setShowAnimated(!showAnimated);
|
||||
}, [showAnimated]);
|
||||
const cleanUrl = encodeURIComponent(url);
|
||||
return (
|
||||
<div className={styles.embed}>
|
||||
{!showAnimated && still && (
|
||||
<BaseButton onClick={toggleImage} className={styles.toggle}>
|
||||
<img src={still} className={styles.image} alt={title || ""} />
|
||||
<Flex
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
className={styles.toggleTrigger}
|
||||
>
|
||||
<Icon size="xl" className={styles.playIcon}>
|
||||
play_circle_outline
|
||||
</Icon>
|
||||
<Localized id="moderate-comment-load-video">
|
||||
<p className={styles.playText}>Load Video</p>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</BaseButton>
|
||||
)}
|
||||
{showAnimated && (
|
||||
<iframe
|
||||
frameBorder="0"
|
||||
width={width || 450}
|
||||
allowFullScreen
|
||||
title="oEmbed"
|
||||
src={`/api/oembed?type=youtube&url=${cleanUrl}&siteID=${siteID}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default YouTubeMedia;
|
||||
+167
-125
@@ -42,25 +42,31 @@ exports[`renders approved correctly 1`] = `
|
||||
<div
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<CommentContent
|
||||
<div
|
||||
className="ModerateCard-content"
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<CommentContent
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<Relay(MediaContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -179,25 +185,31 @@ exports[`renders correctly 1`] = `
|
||||
<div
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<CommentContent
|
||||
<div
|
||||
className="ModerateCard-content"
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<CommentContent
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<Relay(MediaContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -316,25 +328,31 @@ exports[`renders dangling correctly 1`] = `
|
||||
<div
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<CommentContent
|
||||
<div
|
||||
className="ModerateCard-content"
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<CommentContent
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<Relay(MediaContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -453,25 +471,31 @@ exports[`renders rejected correctly 1`] = `
|
||||
<div
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<CommentContent
|
||||
<div
|
||||
className="ModerateCard-content"
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<CommentContent
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<Relay(MediaContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -599,25 +623,31 @@ exports[`renders reply correctly 1`] = `
|
||||
<div
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<CommentContent
|
||||
<div
|
||||
className="ModerateCard-content"
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<CommentContent
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<Relay(MediaContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -736,25 +766,31 @@ exports[`renders story info 1`] = `
|
||||
<div
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<CommentContent
|
||||
<div
|
||||
className="ModerateCard-content"
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<CommentContent
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
content
|
||||
</CommentContent>
|
||||
<Relay(MediaContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -911,33 +947,39 @@ exports[`renders tombstoned when comment is deleted 1`] = `
|
||||
<div
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<CommentContent
|
||||
<div
|
||||
className="ModerateCard-content"
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Localized
|
||||
id="moderate-comment-deleted-body"
|
||||
<CommentContent
|
||||
highlight={false}
|
||||
phrases={
|
||||
Object {
|
||||
"locale": "en-US",
|
||||
"wordList": Object {
|
||||
"banned": Array [
|
||||
"banned",
|
||||
],
|
||||
"suspect": Array [
|
||||
"suspect",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-deleted"
|
||||
<Localized
|
||||
id="moderate-comment-deleted-body"
|
||||
>
|
||||
This comment is no longer available. The commenter has deleted their account.
|
||||
</div>
|
||||
</Localized>
|
||||
</CommentContent>
|
||||
<div
|
||||
className="ModerateCard-deleted"
|
||||
>
|
||||
This comment is no longer available. The commenter has deleted their account.
|
||||
</div>
|
||||
</Localized>
|
||||
</CommentContent>
|
||||
<Relay(MediaContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
|
||||
@@ -17,6 +17,7 @@ import CommentEditingConfig from "./CommentEditingConfig";
|
||||
import CommentLengthConfig from "./CommentLengthConfig";
|
||||
import GuidelinesConfig from "./GuidelinesConfig";
|
||||
import LocaleConfig from "./LocaleConfig";
|
||||
import MediaLinksConfig from "./MediaLinksConfig";
|
||||
import ReactionConfigContainer from "./ReactionConfigContainer";
|
||||
import RTEConfig from "./RTEConfig";
|
||||
import SitewideCommentingConfig from "./SitewideCommentingConfig";
|
||||
@@ -52,6 +53,7 @@ const GeneralConfigContainer: React.FunctionComponent<Props> = ({
|
||||
<ClosedStreamMessageConfig disabled={submitting} />
|
||||
<ReactionConfigContainer disabled={submitting} settings={settings} />
|
||||
<StaffConfig disabled={submitting} />
|
||||
<MediaLinksConfig disabled={submitting} />
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
@@ -70,7 +72,7 @@ const enhanced = withFragmentContainer<Props>({
|
||||
...ReactionConfig_formValues @relay(mask: false)
|
||||
...StaffConfig_formValues @relay(mask: false)
|
||||
...RTEConfig_formValues @relay(mask: false)
|
||||
|
||||
...MediaLinksConfig_formValues @relay(mask: false)
|
||||
...ReactionConfigContainer_settings
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.ratingDesc {
|
||||
font-size: var(--v2-font-size-2);
|
||||
font-weight: var(--v2-font-weight-primary-regular);
|
||||
font-family: var(--v2-font-family-primary);
|
||||
color: var(--v2-colors-grey-500);
|
||||
padding-left: 26px;
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field, FormSpy } from "react-final-form";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { ExternalLink } from "coral-framework/lib/i18n/components";
|
||||
import {
|
||||
Condition,
|
||||
required,
|
||||
validateWhen,
|
||||
} from "coral-framework/lib/validation";
|
||||
import {
|
||||
FieldSet,
|
||||
FormField,
|
||||
FormFieldDescription,
|
||||
HelperText,
|
||||
Label,
|
||||
RadioButton,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import ConfigBox from "../../ConfigBox";
|
||||
import Header from "../../Header";
|
||||
import OnOffField from "../../OnOffField";
|
||||
import Subheader from "../../Subheader";
|
||||
import APIKeyField from "../Moderation/APIKeyField";
|
||||
|
||||
import styles from "./MediaLinksConfig.css";
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const giphyIsEnabled: Condition = (value, values) =>
|
||||
Boolean(values.media && values.media.giphy.enabled);
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
graphql`
|
||||
fragment MediaLinksConfig_formValues on Settings {
|
||||
media {
|
||||
twitter {
|
||||
enabled
|
||||
}
|
||||
youtube {
|
||||
enabled
|
||||
}
|
||||
giphy {
|
||||
enabled
|
||||
maxRating
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MediaLinksConfig: FunctionComponent<Props> = ({ disabled }) => {
|
||||
return (
|
||||
<ConfigBox
|
||||
title={
|
||||
<Localized id="configure-general-embedLinks-title">
|
||||
<Header container={<legend />}>Embedded media</Header>
|
||||
</Localized>
|
||||
}
|
||||
container={<FieldSet />}
|
||||
>
|
||||
<Localized id="configure-general-embedLinks-desc">
|
||||
<FormFieldDescription>
|
||||
Allow commenters to add a YouTube video, Tweet or GIF from GIPHY's
|
||||
library to the end of their comment
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<FormField>
|
||||
<Localized id="configure-general-embedLinks-enableTwitterEmbeds">
|
||||
<Label component="legend">Allow Twitter media</Label>
|
||||
</Localized>
|
||||
<OnOffField
|
||||
name="media.twitter.enabled"
|
||||
disabled={disabled}
|
||||
onLabel={
|
||||
<Localized id="configure-general-embedLinks-On">
|
||||
<span>Yes</span>
|
||||
</Localized>
|
||||
}
|
||||
offLabel={
|
||||
<Localized id="configure-general-embedLinks-Off">
|
||||
<span>No</span>
|
||||
</Localized>
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Localized id="configure-general-embedLinks-enableYouTubeEmbeds">
|
||||
<Label component="legend">Enable YouTube media</Label>
|
||||
</Localized>
|
||||
<OnOffField
|
||||
name="media.youtube.enabled"
|
||||
disabled={disabled}
|
||||
onLabel={
|
||||
<Localized id="configure-general-embedLinks-On">
|
||||
<span>Yes</span>
|
||||
</Localized>
|
||||
}
|
||||
offLabel={
|
||||
<Localized id="configure-general-embedLinks-Off">
|
||||
<span>No</span>
|
||||
</Localized>
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Localized id="configure-general-embedLinks-enableGiphyEmbeds">
|
||||
<Label component="legend">Allow GIFs from GIPHY</Label>
|
||||
</Localized>
|
||||
<OnOffField
|
||||
name="media.giphy.enabled"
|
||||
disabled={disabled}
|
||||
onLabel={
|
||||
<Localized id="configure-general-embedLinks-On">
|
||||
<span>Yes</span>
|
||||
</Localized>
|
||||
}
|
||||
offLabel={
|
||||
<Localized id="configure-general-embedLinks-Off">
|
||||
<span>No</span>
|
||||
</Localized>
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormSpy subscription={{ values: true }}>
|
||||
{(props) => {
|
||||
const giphyDisabled =
|
||||
!props.values.media ||
|
||||
!props.values.media.giphy ||
|
||||
!props.values.media.giphy.enabled;
|
||||
return (
|
||||
<>
|
||||
<FormField>
|
||||
<Localized id="configure-general-embedLinks-giphyMaxRating">
|
||||
<Label component="legend">GIF content rating</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-general-embedLinks-giphyMaxRating-desc">
|
||||
<HelperText>
|
||||
Select the maximum content rating for the GIFs that will
|
||||
appear in commenters’ search results
|
||||
</HelperText>
|
||||
</Localized>
|
||||
<Field name="media.giphy.maxRating" type="radio" value="g">
|
||||
{({ input }) => (
|
||||
<>
|
||||
<Localized id="configure-general-embedLinks-giphyMaxRating-g">
|
||||
<RadioButton
|
||||
{...input}
|
||||
id="G"
|
||||
disabled={giphyDisabled || disabled}
|
||||
>
|
||||
G
|
||||
</RadioButton>
|
||||
</Localized>
|
||||
|
||||
<Localized id="configure-general-embedLinks-giphyMaxRating-g-desc">
|
||||
<div className={styles.ratingDesc}>
|
||||
Content that is appropriate for all ages
|
||||
</div>
|
||||
</Localized>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="media.giphy.maxRating" type="radio" value="pg">
|
||||
{({ input }) => (
|
||||
<>
|
||||
<Localized id="configure-general-embedLinks-giphyMaxRating-pg">
|
||||
<RadioButton
|
||||
{...input}
|
||||
id="PG"
|
||||
disabled={giphyDisabled || disabled}
|
||||
>
|
||||
PG
|
||||
</RadioButton>
|
||||
</Localized>
|
||||
<Localized id="configure-general-embedLinks-giphyMaxRating-pg-desc">
|
||||
<div className={styles.ratingDesc}>
|
||||
Content that is generally safe for everyone, but
|
||||
parental guidance for children is advised.
|
||||
</div>
|
||||
</Localized>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="media.giphy.maxRating" type="radio" value="pg13">
|
||||
{({ input }) => (
|
||||
<>
|
||||
<Localized id="configure-general-embedLinks-giphyMaxRating-pg13">
|
||||
<RadioButton
|
||||
{...input}
|
||||
id="PG13"
|
||||
disabled={giphyDisabled || disabled}
|
||||
>
|
||||
PG-13
|
||||
</RadioButton>
|
||||
</Localized>
|
||||
<Localized id="configure-general-embedLinks-giphyMaxRating-pg13-desc">
|
||||
<div className={styles.ratingDesc}>
|
||||
Mild sexual innuendos, mild substance use, mild
|
||||
profanity, or threatening images. May include images
|
||||
of semi-naked people, but DOES NOT show real human
|
||||
genitalia or nudity.
|
||||
</div>
|
||||
</Localized>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="media.giphy.maxRating" type="radio" value="r">
|
||||
{({ input }) => (
|
||||
<>
|
||||
<Localized id="configure-general-embedLinks-giphyMaxRating-r">
|
||||
<RadioButton
|
||||
{...input}
|
||||
id="r"
|
||||
disabled={giphyDisabled || disabled}
|
||||
>
|
||||
R
|
||||
</RadioButton>
|
||||
</Localized>
|
||||
<Localized id="configure-general-embedLinks-giphyMaxRating-r-desc">
|
||||
<div className={styles.ratingDesc}>
|
||||
Strong language, strong sexual innuendo, violence, and
|
||||
illegal drug use; not suitable for teens or younger.
|
||||
No nudity.
|
||||
</div>
|
||||
</Localized>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</FormField>
|
||||
<Localized id="configure-general-embedLinks-configuration">
|
||||
<Subheader>Configuration</Subheader>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-general-embedLinks-configuration-desc"
|
||||
externalLink={
|
||||
<ExternalLink
|
||||
href={"https://developers.giphy.com/docs/api"}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HelperText>
|
||||
For additional information on GIPHY’s API please visit:
|
||||
https://developers.giphy.com/docs/api
|
||||
</HelperText>
|
||||
</Localized>
|
||||
<FormField>
|
||||
<Localized id="configure-general-embedLinks-giphyAPIKey">
|
||||
<Label>GIPHY API Key</Label>
|
||||
</Localized>
|
||||
<Field name="media.giphy.key">
|
||||
{({ input, meta }) => (
|
||||
<APIKeyField
|
||||
{...input}
|
||||
disabled={giphyDisabled || disabled}
|
||||
validate={validateWhen(giphyIsEnabled, required)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</FormSpy>
|
||||
</ConfigBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaLinksConfig;
|
||||
@@ -1502,6 +1502,411 @@ appears on the comment stream and in the admin interface.
|
||||
</fieldset>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
className="FieldSet-root Box-root ConfigBox-root"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root ConfigBox-title Flex-flex Flex-justifySpaceBetween"
|
||||
>
|
||||
<div>
|
||||
<legend
|
||||
className="Header-root"
|
||||
>
|
||||
Embeded media
|
||||
</legend>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
className="ConfigBox-content"
|
||||
>
|
||||
<fieldset
|
||||
className="FieldSet-root Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
|
||||
>
|
||||
<p
|
||||
className="FormFieldDescription-root"
|
||||
>
|
||||
Allow commenters to add a YouTube video, Tweet or GIF from GIPHY's library to the end of their comment
|
||||
</p>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-spacing-2"
|
||||
>
|
||||
<legend
|
||||
className="Label-root"
|
||||
>
|
||||
Allow Twitter embeds
|
||||
</legend>
|
||||
<div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="RadioButton-input"
|
||||
disabled={false}
|
||||
id="media.twitter.enabled-true"
|
||||
name="media.twitter.enabled"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="true"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label"
|
||||
htmlFor="media.twitter.enabled-true"
|
||||
>
|
||||
<span>
|
||||
Yes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={true}
|
||||
className="RadioButton-input"
|
||||
disabled={false}
|
||||
id="media.twitter.enabled-false"
|
||||
name="media.twitter.enabled"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="false"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label RadioButton-labelChecked"
|
||||
htmlFor="media.twitter.enabled-false"
|
||||
>
|
||||
<span>
|
||||
No
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-spacing-2"
|
||||
>
|
||||
<legend
|
||||
className="Label-root"
|
||||
>
|
||||
Allow YouTube embeds
|
||||
</legend>
|
||||
<div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="RadioButton-input"
|
||||
disabled={false}
|
||||
id="media.youtube.enabled-true"
|
||||
name="media.youtube.enabled"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="true"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label"
|
||||
htmlFor="media.youtube.enabled-true"
|
||||
>
|
||||
<span>
|
||||
Yes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={true}
|
||||
className="RadioButton-input"
|
||||
disabled={false}
|
||||
id="media.youtube.enabled-false"
|
||||
name="media.youtube.enabled"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="false"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label RadioButton-labelChecked"
|
||||
htmlFor="media.youtube.enabled-false"
|
||||
>
|
||||
<span>
|
||||
No
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-spacing-2"
|
||||
>
|
||||
<legend
|
||||
className="Label-root"
|
||||
>
|
||||
Allow GIFs from GIPHY
|
||||
</legend>
|
||||
<div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="RadioButton-input"
|
||||
disabled={false}
|
||||
id="media.giphy.enabled-true"
|
||||
name="media.giphy.enabled"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="true"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label"
|
||||
htmlFor="media.giphy.enabled-true"
|
||||
>
|
||||
<span>
|
||||
Yes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={true}
|
||||
className="RadioButton-input"
|
||||
disabled={false}
|
||||
id="media.giphy.enabled-false"
|
||||
name="media.giphy.enabled"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="false"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label RadioButton-labelChecked"
|
||||
htmlFor="media.giphy.enabled-false"
|
||||
>
|
||||
<span>
|
||||
No
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-spacing-2"
|
||||
>
|
||||
<legend
|
||||
className="Label-root"
|
||||
>
|
||||
GIF content rating
|
||||
</legend>
|
||||
<p
|
||||
className="HelperText-root"
|
||||
>
|
||||
Select the maximum content rating for the GIFs that will appear in commenters’ search results
|
||||
</p>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="RadioButton-input"
|
||||
disabled={true}
|
||||
id="G"
|
||||
name="media.giphy.maxRating"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="g"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label"
|
||||
htmlFor="G"
|
||||
>
|
||||
G
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="MediaLinksConfig-ratingDesc"
|
||||
>
|
||||
Content that is appropriate for all ages
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="RadioButton-input"
|
||||
disabled={true}
|
||||
id="PG"
|
||||
name="media.giphy.maxRating"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="pg"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label"
|
||||
htmlFor="PG"
|
||||
>
|
||||
PG
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="MediaLinksConfig-ratingDesc"
|
||||
>
|
||||
Content that is generally safe for everyone, but parental guidance for children is advised.
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="RadioButton-input"
|
||||
disabled={true}
|
||||
id="PG13"
|
||||
name="media.giphy.maxRating"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="pg13"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label"
|
||||
htmlFor="PG13"
|
||||
>
|
||||
PG-13
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="MediaLinksConfig-ratingDesc"
|
||||
>
|
||||
Mild sexual innuendos, mild substance use, mild profanity, or threatening images. May include images of semi-naked people, but DOES NOT show real human genitalia or nudity.
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="RadioButton-input"
|
||||
disabled={true}
|
||||
id="r"
|
||||
name="media.giphy.maxRating"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="r"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label"
|
||||
htmlFor="r"
|
||||
>
|
||||
R
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="MediaLinksConfig-ratingDesc"
|
||||
>
|
||||
Strong language, strong sexual innuendo, violence, and illegal drug use; not suitable for teens or younger. No nudity.
|
||||
</div>
|
||||
</div>
|
||||
<h3
|
||||
className="Subheader-root"
|
||||
>
|
||||
Configuration
|
||||
</h3>
|
||||
<p
|
||||
className="HelperText-root"
|
||||
>
|
||||
For additional information on GIPHY’s API please visit:
|
||||
<a
|
||||
className="ExternalLink-root"
|
||||
href="https://developers.giphy.com/docs/api"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
https://developers.giphy.com/docs/api
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-spacing-2"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
>
|
||||
GIPHY API key
|
||||
</label>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-spacing-2"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
htmlFor="configure-moderation-media.giphy.key"
|
||||
>
|
||||
API key
|
||||
</label>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
autoCapitalize="off"
|
||||
autoComplete="new-password"
|
||||
autoCorrect="off"
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
data-testid="password-field"
|
||||
disabled={true}
|
||||
id="configure-moderation-media.giphy.key"
|
||||
name="media.giphy.key"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
placeholder=""
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show API Key"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,13 +88,17 @@ exports[`approves comment in reported queue: dangling 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -354,13 +358,17 @@ exports[`rejects comment in reported queue: dangling 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -617,13 +625,17 @@ exports[`renders reported queue with comments 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -867,13 +879,17 @@ exports[`renders reported queue with comments 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Don't fool with me",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Don't fool with me",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -1133,13 +1149,17 @@ exports[`renders reported queue with comments 2`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -1383,13 +1403,17 @@ exports[`renders reported queue with comments 2`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Don't fool with me",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Don't fool with me",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -1639,13 +1663,17 @@ exports[`renders reported queue with comments and load more 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "I think I deserve better",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "I think I deserve better",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
|
||||
@@ -88,13 +88,17 @@ exports[`approves comment in rejected queue: dangling 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -351,13 +355,17 @@ exports[`renders rejected queue with comments 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -622,13 +630,17 @@ exports[`renders rejected queue with comments 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Don't fool with me",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Don't fool with me",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -899,13 +911,17 @@ exports[`renders rejected queue with comments and load more 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "I think I deserve better",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "I think I deserve better",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
|
||||
@@ -75,13 +75,17 @@ exports[`approves single comment 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -298,13 +302,17 @@ exports[`rejects single comment 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
@@ -556,13 +564,17 @@ exports[`renders single comment view 1`] = `
|
||||
className="ModerateCard-contentArea"
|
||||
>
|
||||
<div
|
||||
className="ModerateCard-content CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
className="ModerateCard-content"
|
||||
>
|
||||
<div
|
||||
className="CommentContent-root"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ModerateCard-viewContext"
|
||||
>
|
||||
|
||||
@@ -95,7 +95,7 @@ Your email address will be used to:
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -482,7 +482,7 @@ Your email address will be used to:
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -512,7 +512,7 @@ Your email address will be used to:
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
@@ -876,7 +876,7 @@ GraphQL request:4:3
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -906,7 +906,7 @@ GraphQL request:4:3
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
|
||||
@@ -132,7 +132,7 @@ exports[`renders createUsername view 1`] = `
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="username"
|
||||
name="username"
|
||||
@@ -321,7 +321,7 @@ GraphQL request:4:3
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="username"
|
||||
name="username"
|
||||
@@ -385,7 +385,7 @@ exports[`successfully sets username 1`] = `
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="username"
|
||||
name="username"
|
||||
|
||||
@@ -70,7 +70,7 @@ reset your password.
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
|
||||
@@ -25,7 +25,7 @@ exports[`auth configuration renders all auth enabled 1`] = `
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -395,7 +395,7 @@ exports[`renders sign in view 1`] = `
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
|
||||
@@ -25,7 +25,7 @@ exports[`auth configuration renders all auth enabled 1`] = `
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -60,7 +60,7 @@ exports[`auth configuration renders all auth enabled 1`] = `
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="username"
|
||||
name="username"
|
||||
@@ -413,7 +413,7 @@ exports[`renders sign up form 1`] = `
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -448,7 +448,7 @@ exports[`renders sign up form 1`] = `
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorStreamBlue"
|
||||
className="TextField-input"
|
||||
disabled={false}
|
||||
id="username"
|
||||
name="username"
|
||||
|
||||
@@ -287,3 +287,15 @@ export function validateWhen<T = any, V = any>(
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export function validateWhenOtherwise<T = any, V = any>(
|
||||
condition: Condition<T, V>,
|
||||
truthy: Validator<T, V>,
|
||||
falsy: Validator<T, V>
|
||||
): Validator<T, V> {
|
||||
return (value, values) => {
|
||||
return condition(value, values)
|
||||
? truthy(value, values)
|
||||
: falsy(value, values);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -175,6 +175,10 @@ const CLASSES = {
|
||||
* dismissButton is the button to dismiss the in review message.
|
||||
*/
|
||||
dismissButton: "coral coral-createComment-dismissButton",
|
||||
/**
|
||||
* cancel is the button for cancelling the post.
|
||||
*/
|
||||
cancel: "coral coral-createComment-cancel",
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.root {
|
||||
composes: root from "~coral-stream/shared/htmlContent.css";
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
video?: string | null;
|
||||
title?: string | null;
|
||||
}
|
||||
|
||||
const GiphyMedia: FunctionComponent<Props> = ({
|
||||
url,
|
||||
title,
|
||||
video,
|
||||
width,
|
||||
height,
|
||||
}) => {
|
||||
return video ? (
|
||||
<video
|
||||
width={width || undefined}
|
||||
height={height || undefined}
|
||||
autoPlay
|
||||
loop
|
||||
>
|
||||
<source src={video} type="video/mp4" />
|
||||
</video>
|
||||
) : (
|
||||
<img src={url} alt={title || ""} />
|
||||
);
|
||||
};
|
||||
|
||||
export default GiphyMedia;
|
||||
@@ -0,0 +1,23 @@
|
||||
.root {
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-left: var(--v2-spacing-2);
|
||||
color: var(--palette-primary-main);
|
||||
}
|
||||
|
||||
.frame {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
HTMLProps,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import styles from "./OEmbed.css";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
type: string;
|
||||
loadTimeout?: number;
|
||||
showLink?: boolean;
|
||||
className?: string;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
siteID: string;
|
||||
}
|
||||
|
||||
function calculateBottomPadding(width: string, height: string) {
|
||||
return `${(parseInt(height, 10) / parseInt(width, 10)) * 100}%`;
|
||||
}
|
||||
|
||||
const oEmbed: FunctionComponent<Props> = ({
|
||||
url,
|
||||
type,
|
||||
className,
|
||||
width,
|
||||
height,
|
||||
siteID,
|
||||
}) => {
|
||||
const iframeRef = React.createRef<HTMLIFrameElement>();
|
||||
const containerRef = React.createRef<HTMLDivElement>();
|
||||
const cleanUrl = encodeURIComponent(url);
|
||||
const [maxWidth, setMaxWidth] = useState<number | null>(null);
|
||||
const attrs: HTMLProps<HTMLIFrameElement> = {};
|
||||
if (width) {
|
||||
attrs.width = width;
|
||||
}
|
||||
if (height) {
|
||||
attrs.height = height;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
setMaxWidth(containerRef.current.offsetWidth);
|
||||
}
|
||||
}, [containerRef.current, maxWidth]);
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
if (width && height && containerRef && containerRef.current) {
|
||||
containerRef.current.style.paddingBottom = `${(height / width) * 100}%`;
|
||||
return;
|
||||
}
|
||||
let resizeInterval: number | null = null;
|
||||
|
||||
let iterations = 0;
|
||||
resizeInterval = window.setInterval(() => {
|
||||
if (iterations > 10 && resizeInterval) {
|
||||
clearInterval(resizeInterval);
|
||||
}
|
||||
if (!iframeRef.current || !iframeRef.current.contentWindow) {
|
||||
return;
|
||||
}
|
||||
let calculatedWidth = `${width}`;
|
||||
let calculatedHeight = `${height}`;
|
||||
if (!width) {
|
||||
calculatedWidth = `${iframeRef.current.contentWindow.document.body.scrollWidth}`;
|
||||
iframeRef.current.width = `${calculatedWidth}px`;
|
||||
}
|
||||
if (!height) {
|
||||
calculatedHeight = `${iframeRef.current.contentWindow.document.body.scrollHeight}`;
|
||||
iframeRef.current.height = `${calculatedHeight}px`;
|
||||
}
|
||||
if (
|
||||
containerRef &&
|
||||
containerRef.current &&
|
||||
calculatedWidth &&
|
||||
calculatedHeight
|
||||
) {
|
||||
containerRef.current.style.paddingBottom = calculateBottomPadding(
|
||||
calculatedWidth,
|
||||
calculatedHeight
|
||||
);
|
||||
}
|
||||
iterations = iterations + 1;
|
||||
}, 100);
|
||||
}, [iframeRef, iframeRef.current, containerRef.current, width, height]);
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={containerRef}>
|
||||
{maxWidth && (
|
||||
<iframe
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
scrolling="no"
|
||||
ref={iframeRef}
|
||||
title="oEmbed"
|
||||
src={`/api/oembed?type=${type}&url=${cleanUrl}&maxWidth=${maxWidth}&siteID=${siteID}`}
|
||||
onLoad={onLoad}
|
||||
className={styles.frame}
|
||||
{...attrs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default oEmbed;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import OEmbed from "./OEmbed";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
width?: number | null;
|
||||
siteID: string;
|
||||
}
|
||||
|
||||
const TwitterMedia: FunctionComponent<Props> = ({ url, width, siteID }) => {
|
||||
return <OEmbed url={url} type="twitter" siteID={siteID} />;
|
||||
};
|
||||
|
||||
export default TwitterMedia;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import OEmbed from "./OEmbed";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
siteID: string;
|
||||
}
|
||||
|
||||
const YouTubeMedia: FunctionComponent<Props> = ({
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
siteID,
|
||||
}) => {
|
||||
return (
|
||||
<OEmbed
|
||||
url={url}
|
||||
width={width}
|
||||
height={height}
|
||||
type="youtube"
|
||||
siteID={siteID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default YouTubeMedia;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as YouTubeMedia } from "./YouTubeMedia";
|
||||
export { default as TwitterMedia } from "./TwitterMedia";
|
||||
export { default as GiphyMedia } from "./GiphyMedia";
|
||||
@@ -24,6 +24,7 @@ export interface CommentProps {
|
||||
parentAuthorName?: string | null;
|
||||
userTags?: React.ReactNode;
|
||||
collapsed?: boolean;
|
||||
media?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Comment: FunctionComponent<CommentProps> = (props) => {
|
||||
@@ -68,6 +69,7 @@ const Comment: FunctionComponent<CommentProps> = (props) => {
|
||||
<HTMLContent className={CLASSES.comment.content}>
|
||||
{props.body || ""}
|
||||
</HTMLContent>
|
||||
{props.media}
|
||||
{props.footer}
|
||||
</HorizontalGutter>
|
||||
</HorizontalGutter>
|
||||
|
||||
@@ -37,6 +37,7 @@ import ButtonsBar from "./ButtonsBar";
|
||||
import EditCommentFormContainer from "./EditCommentForm";
|
||||
import FeaturedTag from "./FeaturedTag";
|
||||
import IndentedComment from "./IndentedComment";
|
||||
import MediaSectionContainer from "./MediaSection/MediaSectionContainer";
|
||||
import CaretContainer, {
|
||||
RejectedTombstoneContainer,
|
||||
} from "./ModerationDropdown";
|
||||
@@ -400,6 +401,9 @@ export class CommentContainer extends Component<Props, State> {
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
media={
|
||||
<MediaSectionContainer comment={comment} settings={settings} />
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<Flex
|
||||
@@ -594,6 +598,8 @@ const enhanced = withContext(({ eventEmitter }) => ({ eventEmitter }))(
|
||||
...UserTagsContainer_comment
|
||||
...UsernameWithPopoverContainer_comment
|
||||
...UsernameContainer_comment
|
||||
...MediaSectionContainer_comment
|
||||
...UsernameContainer_comment
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
@@ -605,6 +611,7 @@ const enhanced = withContext(({ eventEmitter }) => ({ eventEmitter }))(
|
||||
...ReplyCommentFormContainer_settings
|
||||
...EditCommentFormContainer_settings
|
||||
...UserTagsContainer_settings
|
||||
...MediaSectionContainer_settings
|
||||
}
|
||||
`,
|
||||
})(CommentContainer)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
$commentsBorder: var(--v2-colors-grey-300);
|
||||
|
||||
.commentFormBox {
|
||||
border: 1px solid $commentsBorder;
|
||||
border-radius: var(--v2-round-corners);
|
||||
}
|
||||
@@ -1,38 +1,18 @@
|
||||
import { CoralRTE } from "@coralproject/rte";
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import cn from "classnames";
|
||||
import React, { EventHandler, FunctionComponent, MouseEvent, Ref } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { OnSubmit } from "coral-framework/lib/form";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import Timestamp from "coral-stream/common/Timestamp";
|
||||
import ValidationMessage from "coral-stream/common/ValidationMessage";
|
||||
import {
|
||||
AriaInfo,
|
||||
Button,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
MatchMedia,
|
||||
Message,
|
||||
MessageIcon,
|
||||
RelativeTime,
|
||||
} from "coral-ui/components/v2";
|
||||
import { AriaInfo } from "coral-ui/components/v2";
|
||||
import { PropTypesOf } from "coral-ui/types";
|
||||
|
||||
import {
|
||||
getCommentBodyValidators,
|
||||
getHTMLCharacterLength,
|
||||
} from "../../helpers";
|
||||
import RemainingCharactersContainer from "../../RemainingCharacters";
|
||||
import RTEContainer from "../../RTE";
|
||||
import CommentForm from "../../Stream/CommentForm";
|
||||
import TopBarLeft from "../TopBarLeft";
|
||||
import Username from "../Username";
|
||||
|
||||
interface FormProps {
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface EditCommentFormProps {
|
||||
id: string;
|
||||
className?: string;
|
||||
@@ -44,167 +24,65 @@ export interface EditCommentFormProps {
|
||||
onSubmit: OnSubmit<any>;
|
||||
onCancel?: EventHandler<MouseEvent<any>>;
|
||||
onClose?: EventHandler<MouseEvent<any>>;
|
||||
initialValues?: FormProps;
|
||||
initialValues?: any;
|
||||
rteRef?: Ref<CoralRTE>;
|
||||
expired?: boolean;
|
||||
min: number | null;
|
||||
max: number | null;
|
||||
rteConfig: PropTypesOf<typeof RTEContainer>["config"];
|
||||
rteConfig: PropTypesOf<typeof CommentForm>["rteConfig"];
|
||||
mediaConfig: PropTypesOf<typeof CommentForm>["mediaConfig"];
|
||||
siteID: string;
|
||||
}
|
||||
|
||||
const EditCommentForm: FunctionComponent<EditCommentFormProps> = (props) => {
|
||||
const inputID = `comments-editCommentForm-rte-${props.id}`;
|
||||
|
||||
return (
|
||||
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
|
||||
{({ handleSubmit, submitting, pristine, submitError }) => (
|
||||
<form
|
||||
className={cn(props.className, CLASSES.editComment.$root)}
|
||||
autoComplete="off"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<HorizontalGutter>
|
||||
<div>
|
||||
<div>
|
||||
<TopBarLeft>
|
||||
{props.author && props.author.username && (
|
||||
<div>
|
||||
<TopBarLeft>
|
||||
{props.author && props.author.username && (
|
||||
<div>
|
||||
<Username>{props.author.username}</Username>
|
||||
</div>
|
||||
)}
|
||||
<Timestamp>{props.createdAt}</Timestamp>
|
||||
</TopBarLeft>
|
||||
<Username>{props.author.username}</Username>
|
||||
</div>
|
||||
<Field
|
||||
name="body"
|
||||
validate={getCommentBodyValidators(props.min, props.max)}
|
||||
>
|
||||
{/* FIXME: (wyattjoh) reorganize this */}
|
||||
{({ input, meta }) => (
|
||||
<>
|
||||
<HorizontalGutter size="half">
|
||||
<Localized id="comments-editCommentForm-rteLabel">
|
||||
<AriaInfo component="label" htmlFor={inputID}>
|
||||
Edit comment
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="comments-editCommentForm-rte"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<RTEContainer
|
||||
config={props.rteConfig}
|
||||
inputID={inputID}
|
||||
onChange={(html) => input.onChange(html)}
|
||||
value={input.value}
|
||||
placeholder="Edit comment"
|
||||
ref={props.rteRef}
|
||||
disabled={submitting || props.expired}
|
||||
/>
|
||||
</Localized>
|
||||
{props.expired ? (
|
||||
<Localized id="comments-editCommentForm-editTimeExpired">
|
||||
<ValidationMessage
|
||||
className={CLASSES.editComment.expiredTime}
|
||||
>
|
||||
Edit time has expired. You can no longer edit this
|
||||
comment. Why not post another one?
|
||||
</ValidationMessage>
|
||||
</Localized>
|
||||
) : (
|
||||
<>
|
||||
<Message
|
||||
className={CLASSES.editComment.remainingTime}
|
||||
fullWidth
|
||||
>
|
||||
<MessageIcon>alarm</MessageIcon>
|
||||
<Localized
|
||||
id="comments-editCommentForm-editRemainingTime"
|
||||
time={
|
||||
<RelativeTime date={props.editableUntil} live />
|
||||
}
|
||||
>
|
||||
<span>{"Edit: <time></time> remaining"}</span>
|
||||
</Localized>
|
||||
</Message>
|
||||
{meta.touched &&
|
||||
(meta.error ||
|
||||
(meta.submitError &&
|
||||
!meta.dirtySinceLastSubmit)) && (
|
||||
<ValidationMessage>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
{submitError && (
|
||||
<ValidationMessage>{submitError}</ValidationMessage>
|
||||
)}
|
||||
{props.max && (
|
||||
<RemainingCharactersContainer
|
||||
value={input.value}
|
||||
max={props.max}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
<Flex
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
itemGutter="half"
|
||||
>
|
||||
{props.expired ? (
|
||||
<Localized id="comments-editCommentForm-close">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={submitting}
|
||||
onClick={props.onClose}
|
||||
className={CLASSES.editComment.close}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Localized>
|
||||
) : (
|
||||
<MatchMedia ltWidth="sm">
|
||||
{(matches) => (
|
||||
<>
|
||||
<Localized id="comments-editCommentForm-cancel">
|
||||
<Button
|
||||
color="mono"
|
||||
variant="outline"
|
||||
disabled={submitting}
|
||||
onClick={props.onCancel}
|
||||
fullWidth={matches}
|
||||
className={CLASSES.editComment.cancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="comments-editCommentForm-saveChanges">
|
||||
<Button
|
||||
color="stream"
|
||||
variant="regular"
|
||||
disabled={
|
||||
submitting ||
|
||||
getHTMLCharacterLength(input.value) === 0 ||
|
||||
pristine
|
||||
}
|
||||
type="submit"
|
||||
fullWidth={matches}
|
||||
className={CLASSES.editComment.submit}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</Localized>
|
||||
</>
|
||||
)}
|
||||
</MatchMedia>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
<Timestamp>{props.createdAt}</Timestamp>
|
||||
</TopBarLeft>
|
||||
</div>
|
||||
|
||||
<CommentForm
|
||||
siteID={props.siteID}
|
||||
onSubmit={props.onSubmit}
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
disabled={props.expired}
|
||||
bodyInputID={inputID}
|
||||
initialValues={props.initialValues}
|
||||
onCancel={props.onCancel}
|
||||
editableUntil={props.editableUntil}
|
||||
classNameRoot="editComment"
|
||||
mediaConfig={props.mediaConfig}
|
||||
expired={props.expired}
|
||||
placeholder="Edit comment"
|
||||
placeHolderId="comments-editCommentForm-rte"
|
||||
disabledMessage={
|
||||
<Localized id="comments-editCommentForm-editTimeExpired">
|
||||
<ValidationMessage className={CLASSES.editComment.expiredTime}>
|
||||
Edit time has expired. You can no longer edit this comment. Why
|
||||
not post another one?
|
||||
</ValidationMessage>
|
||||
</Localized>
|
||||
}
|
||||
bodyLabel={
|
||||
<Localized id="comments-editCommentForm-rteLabel">
|
||||
<AriaInfo component="label" htmlFor={inputID}>
|
||||
Edit comment
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
}
|
||||
rteConfig={props.rteConfig}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+45
-1
@@ -48,7 +48,10 @@ interface State {
|
||||
|
||||
export class EditCommentFormContainer extends Component<Props, State> {
|
||||
private expiredTimer: any;
|
||||
private intitialValues = { body: this.props.comment.body || "" };
|
||||
private intitialValues = {
|
||||
body: this.props.comment.body || "",
|
||||
media: this.props.comment.revision && this.props.comment.revision.media,
|
||||
};
|
||||
|
||||
public state: State = {
|
||||
initialized: false,
|
||||
@@ -98,6 +101,7 @@ export class EditCommentFormContainer extends Component<Props, State> {
|
||||
await this.props.editComment({
|
||||
commentID: this.props.comment.id,
|
||||
body: input.body,
|
||||
media: input.media,
|
||||
})
|
||||
);
|
||||
if (submitStatus !== "RETRY") {
|
||||
@@ -133,6 +137,7 @@ export class EditCommentFormContainer extends Component<Props, State> {
|
||||
}
|
||||
return (
|
||||
<EditCommentForm
|
||||
siteID={this.props.comment.site.id}
|
||||
id={this.props.comment.id}
|
||||
rteConfig={this.props.settings.rte}
|
||||
onSubmit={this.handleOnSubmit}
|
||||
@@ -144,6 +149,7 @@ export class EditCommentFormContainer extends Component<Props, State> {
|
||||
createdAt={this.props.comment.createdAt}
|
||||
editableUntil={this.props.comment.editing.editableUntil!}
|
||||
expired={this.state.expired}
|
||||
mediaConfig={this.props.settings.media}
|
||||
min={
|
||||
(this.props.settings.charCount.enabled &&
|
||||
this.props.settings.charCount.min) ||
|
||||
@@ -158,6 +164,7 @@ export class EditCommentFormContainer extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
|
||||
// Disable autofocus on ios and enable for the rest.
|
||||
autofocus: !browserInfo.ios,
|
||||
@@ -170,12 +177,38 @@ const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
|
||||
id
|
||||
body
|
||||
createdAt
|
||||
revision {
|
||||
id
|
||||
media {
|
||||
__typename
|
||||
... on GiphyMedia {
|
||||
url
|
||||
title
|
||||
width
|
||||
height
|
||||
still
|
||||
video
|
||||
}
|
||||
... on TwitterMedia {
|
||||
url
|
||||
width
|
||||
}
|
||||
... on YouTubeMedia {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
}
|
||||
}
|
||||
author {
|
||||
username
|
||||
}
|
||||
editing {
|
||||
editableUntil
|
||||
}
|
||||
site {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
story: graphql`
|
||||
@@ -190,6 +223,17 @@ const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
|
||||
min
|
||||
max
|
||||
}
|
||||
media {
|
||||
twitter {
|
||||
enabled
|
||||
}
|
||||
youtube {
|
||||
enabled
|
||||
}
|
||||
giphy {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
rte {
|
||||
...RTEContainer_config
|
||||
}
|
||||
|
||||
+22
-1
@@ -26,6 +26,26 @@ const mutation = graphql`
|
||||
status
|
||||
revision {
|
||||
id
|
||||
media {
|
||||
__typename
|
||||
... on GiphyMedia {
|
||||
url
|
||||
title
|
||||
width
|
||||
height
|
||||
still
|
||||
video
|
||||
}
|
||||
... on TwitterMedia {
|
||||
url
|
||||
width
|
||||
}
|
||||
... on YouTubeMedia {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
}
|
||||
}
|
||||
editing {
|
||||
edited
|
||||
@@ -54,7 +74,7 @@ async function commit(
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, ["commentID", "body"]),
|
||||
...pick(input, ["commentID", "body", "media"]),
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
@@ -66,6 +86,7 @@ async function commit(
|
||||
status: lookup<GQLComment>(environment, input.commentID)!.status,
|
||||
revision: {
|
||||
id: uuidGenerator(),
|
||||
media: null,
|
||||
},
|
||||
editing: {
|
||||
edited: true,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
.root {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-size: var(--v2-font-size-2);
|
||||
color: var(--v2-colors-mono-500);
|
||||
border-top: 1px solid var(--v2-colors-grey-200);
|
||||
margin: 0 var(--v2-spacing-2);
|
||||
padding: var(--v2-spacing-2) 0;
|
||||
@media (min-width: $breakpoints-xs) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.prompt {
|
||||
margin: 0;
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@media (max-width: $breakpoints-xs) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.promptButton {
|
||||
@media (max-width: $breakpoints-xs) {
|
||||
flex: 1;
|
||||
}
|
||||
@media (min-width: $breakpoints-xs) {
|
||||
padding-top: var(--v2-spacing-1) !important;
|
||||
padding-bottom: var(--v2-spacing-1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.promptContainer {
|
||||
}
|
||||
|
||||
.xsFlex {
|
||||
@media (max-width: $breakpoints-xs) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
}
|
||||
|
||||
.break {
|
||||
}
|
||||
|
||||
.icon {
|
||||
@media (max-width: $breakpoints-xs) {
|
||||
}
|
||||
margin-right: var(--v2-spacing-2);
|
||||
}
|
||||
|
||||
.url {
|
||||
color: var(--v2-colors-mono-100);
|
||||
margin: 0;
|
||||
|
||||
@media (min-width: $breakpoints-xs) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { MediaLink } from "coral-common/helpers/findMediaLinks";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
MatchMedia,
|
||||
} from "coral-ui/components/v2";
|
||||
import MediaConfirmationIcon from "./MediaConfirmationIcon";
|
||||
import styles from "./MediaConfirmPrompt.css";
|
||||
|
||||
interface Props {
|
||||
media: MediaLink;
|
||||
onConfirm: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
const MediaConfirmPrompt: FunctionComponent<Props> = ({
|
||||
media,
|
||||
onConfirm,
|
||||
onRemove,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<MatchMedia gtWidth="xs">
|
||||
<div className={styles.icon}>
|
||||
<MediaConfirmationIcon media={media} />
|
||||
</div>
|
||||
</MatchMedia>
|
||||
<HorizontalGutter spacing={3}>
|
||||
<div className={styles.xsFlex}>
|
||||
<MatchMedia lteWidth="xs">
|
||||
<div className={styles.icon}>
|
||||
<MediaConfirmationIcon media={media} />
|
||||
</div>
|
||||
</MatchMedia>
|
||||
<div className={styles.promptContainer}>
|
||||
{media.type === "youtube" && (
|
||||
<p className={styles.prompt}>
|
||||
<Localized id="comments-postComment-confirmMedia-youtube">
|
||||
Add this YouTube video to the end of your comment?
|
||||
</Localized>
|
||||
</p>
|
||||
)}
|
||||
{media.type === "twitter" && (
|
||||
<Localized id="comments-postComment-confirmMedia-twitter">
|
||||
<p className={styles.prompt}>
|
||||
Add this tweet to the end of your comment?
|
||||
</p>
|
||||
</Localized>
|
||||
)}
|
||||
<p className={styles.url}>{media.url}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Flex spacing={2} className={styles.buttons}>
|
||||
<Localized id="comments-postComment-confirmMedia-cancel">
|
||||
<Button
|
||||
color="mono"
|
||||
variant="outline"
|
||||
className={styles.promptButton}
|
||||
onClick={onRemove}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
{media.type === "twitter" && (
|
||||
<Localized id="comments-postComment-confirmMedia-add-tweet">
|
||||
<Button
|
||||
color="mono"
|
||||
onClick={onConfirm}
|
||||
className={styles.promptButton}
|
||||
>
|
||||
Add tweet
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
{media.type === "youtube" && (
|
||||
<Localized id="comments-postComment-confirmMedia-add-video">
|
||||
<Button
|
||||
color="mono"
|
||||
onClick={onConfirm}
|
||||
className={styles.promptButton}
|
||||
>
|
||||
Add video
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaConfirmPrompt;
|
||||
@@ -0,0 +1,23 @@
|
||||
.root {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-size: var(--v2-font-size-2);
|
||||
color: var(--v2-colors-mono-500);
|
||||
border-top: 1px solid var(--v2-colors-grey-200);
|
||||
margin: 0 var(--v2-spacing-2);
|
||||
padding: var(--v2-spacing-2) 0;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
margin: 0;
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
}
|
||||
|
||||
.url {
|
||||
color: var(--v2-colors-mono-100);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.boldURL {
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
color: var(--v2-colors-mono-500);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { MediaLink } from "coral-common/helpers/findMediaLinks";
|
||||
import MediaConfirmPrompt from "./MediaConfirmPrompt";
|
||||
import MediaPreview from "./MediaPreview";
|
||||
|
||||
import styles from "./MediaConfirmation.css";
|
||||
|
||||
interface MediaConfig {
|
||||
giphy: {
|
||||
enabled: boolean;
|
||||
};
|
||||
twitter: {
|
||||
enabled: boolean;
|
||||
};
|
||||
youtube: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
media: MediaLink & {
|
||||
confirmed: boolean;
|
||||
};
|
||||
onConfirm: () => void;
|
||||
onRemove: () => void;
|
||||
config: MediaConfig;
|
||||
siteID: string;
|
||||
}
|
||||
|
||||
const MediaConfirmation: FunctionComponent<Props> = ({
|
||||
media,
|
||||
onConfirm,
|
||||
config,
|
||||
onRemove,
|
||||
siteID,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{!media.confirmed && (
|
||||
<MediaConfirmPrompt
|
||||
media={media}
|
||||
onConfirm={onConfirm}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
)}
|
||||
{media.confirmed && (
|
||||
<MediaPreview
|
||||
media={media}
|
||||
onRemove={onRemove}
|
||||
config={config}
|
||||
siteID={siteID}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaConfirmation;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
.twitterIcon {
|
||||
width: 14px;
|
||||
height: 12px;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Icon } from "coral-ui/components/v2";
|
||||
|
||||
import { MediaLink } from "coral-common/helpers/findMediaLinks";
|
||||
import styles from "./MediaConfirmationIcon.css";
|
||||
import twitterImg from "./twitter.png";
|
||||
|
||||
interface Props {
|
||||
media: MediaLink;
|
||||
}
|
||||
|
||||
const MediaConfirmationIcon: FunctionComponent<Props> = ({ media }) => {
|
||||
return (
|
||||
<>
|
||||
{media.type === "youtube" && <Icon>ondemand_video</Icon>}
|
||||
{media.type === "twitter" && (
|
||||
<img className={styles.twitterIcon} src={twitterImg} alt="twitter" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaConfirmationIcon;
|
||||
@@ -0,0 +1,25 @@
|
||||
.root {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-size: var(--v2-font-size-2);
|
||||
color: var(--v2-colors-mono-500);
|
||||
border-top: 1px solid var(--v2-colors-grey-200);
|
||||
margin: 0 var(--v2-spacing-2);
|
||||
padding: var(--v2-spacing-2) 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url {
|
||||
color: var(--v2-colors-stream-blue-500);
|
||||
margin: 0;
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
flex: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: var(--v2-spacing-1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { MediaLink } from "coral-common/helpers/findMediaLinks";
|
||||
import { TwitterMedia, YouTubeMedia } from "coral-stream/common/Media";
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
MatchMedia,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import MediaConfirmationIcon from "./MediaConfirmationIcon";
|
||||
|
||||
import styles from "./MediaPreview.css";
|
||||
interface MediaConfig {
|
||||
giphy: {
|
||||
enabled: boolean;
|
||||
};
|
||||
twitter: {
|
||||
enabled: boolean;
|
||||
};
|
||||
youtube: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
media: MediaLink;
|
||||
onRemove: () => void;
|
||||
config: MediaConfig | null;
|
||||
siteID: string;
|
||||
}
|
||||
|
||||
const MediaPreview: FunctionComponent<Props> = ({
|
||||
media,
|
||||
onRemove,
|
||||
config,
|
||||
siteID,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<HorizontalGutter spacing={3} className={styles.root}>
|
||||
<div>
|
||||
<Flex justifyContent="space-between">
|
||||
<Flex spacing={2}>
|
||||
<div className={styles.icon}>
|
||||
<MediaConfirmationIcon media={media} />
|
||||
</div>
|
||||
<a
|
||||
href={media.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.url}
|
||||
>
|
||||
{media.url}
|
||||
</a>
|
||||
</Flex>
|
||||
<MatchMedia gteWidth="xs">
|
||||
<Localized id="comments-postComment-confirmMedia-remove">
|
||||
<Button onClick={onRemove} color="mono" variant="text" iconLeft>
|
||||
<ButtonIcon>close</ButtonIcon>
|
||||
Remove
|
||||
</Button>
|
||||
</Localized>
|
||||
</MatchMedia>
|
||||
</Flex>
|
||||
</div>
|
||||
{media.type === "twitter" && (
|
||||
<TwitterMedia url={media.url} siteID={siteID} />
|
||||
)}
|
||||
{media.type === "youtube" && (
|
||||
<YouTubeMedia url={media.url} siteID={siteID} />
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
<MatchMedia ltWidth="xs">
|
||||
<Localized id="comments-postComment-confirmMedia-remove">
|
||||
<Button
|
||||
onClick={onRemove}
|
||||
color="mono"
|
||||
variant="text"
|
||||
iconLeft
|
||||
size="large"
|
||||
className={styles.removeButton}
|
||||
>
|
||||
<ButtonIcon>close</ButtonIcon>
|
||||
Remove
|
||||
</Button>
|
||||
</Localized>
|
||||
</MatchMedia>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaPreview;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default, default as MediaConfirmation } from "./MediaConfirmation";
|
||||
export { default as MediaConfirmPrompt } from "./MediaConfirmPrompt";
|
||||
export { default as MediaPreview } from "./MediaPreview";
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 752 B |
@@ -0,0 +1,3 @@
|
||||
.button {
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
GiphyMedia,
|
||||
TwitterMedia,
|
||||
YouTubeMedia,
|
||||
} from "coral-stream/common/Media";
|
||||
import { Button, ButtonIcon, HorizontalGutter } from "coral-ui/components/v2";
|
||||
|
||||
import { MediaSectionContainer_comment } from "coral-stream/__generated__/MediaSectionContainer_comment.graphql";
|
||||
import { MediaSectionContainer_settings } from "coral-stream/__generated__/MediaSectionContainer_settings.graphql";
|
||||
|
||||
import styles from "./MediaSectionContainer.css";
|
||||
|
||||
interface Props {
|
||||
comment: MediaSectionContainer_comment;
|
||||
settings: MediaSectionContainer_settings;
|
||||
}
|
||||
|
||||
const MediaSectionContainer: FunctionComponent<Props> = ({
|
||||
comment,
|
||||
settings,
|
||||
}) => {
|
||||
const { revision } = comment;
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const onToggleExpand = useCallback(() => {
|
||||
setExpanded((v) => !v);
|
||||
}, []);
|
||||
|
||||
if (!revision || !revision.media || !settings.media) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { media } = revision;
|
||||
|
||||
if (
|
||||
!settings.media ||
|
||||
(media.__typename === "TwitterMedia" &&
|
||||
(!settings.media.twitter || !settings.media.twitter.enabled)) ||
|
||||
(media.__typename === "YouTubeMedia" &&
|
||||
(!settings.media.youtube || !settings.media.youtube.enabled)) ||
|
||||
(media.__typename === "GiphyMedia" &&
|
||||
(!settings.media.giphy || !settings.media.giphy.enabled))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!expanded) {
|
||||
return (
|
||||
<Button
|
||||
iconLeft
|
||||
variant="outline"
|
||||
color="stream"
|
||||
onClick={onToggleExpand}
|
||||
size="small"
|
||||
className={styles.button}
|
||||
>
|
||||
<ButtonIcon>add</ButtonIcon>
|
||||
{media.__typename === "TwitterMedia" && (
|
||||
<Localized id="comments-embedLinks-show-twitter">
|
||||
Show Tweet
|
||||
</Localized>
|
||||
)}
|
||||
{media.__typename === "YouTubeMedia" && (
|
||||
<Localized id="comments-embedLinks-show-youtube">
|
||||
Show video
|
||||
</Localized>
|
||||
)}
|
||||
{media.__typename === "GiphyMedia" && (
|
||||
<Localized id="comments-embedLinks-show-giphy">Show GIF</Localized>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HorizontalGutter>
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="stream"
|
||||
onClick={onToggleExpand}
|
||||
size="small"
|
||||
iconLeft
|
||||
className={styles.button}
|
||||
>
|
||||
<ButtonIcon>remove</ButtonIcon>
|
||||
{media.__typename === "TwitterMedia" && (
|
||||
<Localized id="comments-embedLinks-hide-twitter">
|
||||
Hide Tweet
|
||||
</Localized>
|
||||
)}
|
||||
{media.__typename === "GiphyMedia" && (
|
||||
<Localized id="comments-embedLinks-hide-giphy">Hide GIF</Localized>
|
||||
)}
|
||||
{media.__typename === "YouTubeMedia" && (
|
||||
<Localized id="comments-embedLinks-hide-youtube">
|
||||
Hide video
|
||||
</Localized>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{media.__typename === "TwitterMedia" && (
|
||||
<TwitterMedia
|
||||
url={media.url}
|
||||
width={media.width}
|
||||
siteID={comment.site.id}
|
||||
/>
|
||||
)}
|
||||
{media.__typename === "YouTubeMedia" && (
|
||||
<YouTubeMedia
|
||||
url={media.url}
|
||||
width={media.width}
|
||||
height={media.height}
|
||||
siteID={comment.site.id}
|
||||
/>
|
||||
)}
|
||||
{media.__typename === "GiphyMedia" && (
|
||||
<GiphyMedia
|
||||
url={media.url}
|
||||
width={media.width}
|
||||
height={media.height}
|
||||
title={media.title}
|
||||
video={media.video}
|
||||
/>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment MediaSectionContainer_comment on Comment {
|
||||
site {
|
||||
id
|
||||
}
|
||||
revision {
|
||||
media {
|
||||
__typename
|
||||
... on GiphyMedia {
|
||||
url
|
||||
title
|
||||
width
|
||||
height
|
||||
still
|
||||
video
|
||||
}
|
||||
... on TwitterMedia {
|
||||
url
|
||||
width
|
||||
}
|
||||
... on YouTubeMedia {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment MediaSectionContainer_settings on Settings {
|
||||
media {
|
||||
twitter {
|
||||
enabled
|
||||
}
|
||||
youtube {
|
||||
enabled
|
||||
}
|
||||
giphy {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(MediaSectionContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
default as MediaSectionContainer,
|
||||
} from "./MediaSectionContainer";
|
||||
+5
@@ -198,6 +198,7 @@ async function commit(
|
||||
body: input.body,
|
||||
nudge: input.nudge,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
media: input.media,
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
@@ -218,6 +219,7 @@ async function commit(
|
||||
body: input.body,
|
||||
revision: {
|
||||
id: uuidGenerator(),
|
||||
media: null,
|
||||
},
|
||||
parent: {
|
||||
id: parentComment.id,
|
||||
@@ -251,6 +253,9 @@ async function commit(
|
||||
},
|
||||
},
|
||||
},
|
||||
site: {
|
||||
id: uuidGenerator(),
|
||||
},
|
||||
replies: {
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
|
||||
@@ -2,3 +2,10 @@
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
|
||||
$commentsBorder: var(--v2-colors-grey-300);
|
||||
|
||||
.commentFormBox {
|
||||
border: 1px solid $commentsBorder;
|
||||
border-radius: var(--v2-round-corners);
|
||||
}
|
||||
|
||||
+37
-145
@@ -1,6 +1,5 @@
|
||||
import { CoralRTE } from "@coralproject/rte";
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import cn from "classnames";
|
||||
import { FormApi, FormState } from "final-form";
|
||||
import React, {
|
||||
EventHandler,
|
||||
@@ -9,50 +8,33 @@ import React, {
|
||||
Ref,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { Field, Form, FormSpy } from "react-final-form";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { OnSubmit } from "coral-framework/lib/form";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import ValidationMessage from "coral-stream/common/ValidationMessage";
|
||||
import { ReplyCommentFocusEvent } from "coral-stream/events";
|
||||
import {
|
||||
AriaInfo,
|
||||
Button,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
MatchMedia,
|
||||
} from "coral-ui/components/v2";
|
||||
import { AriaInfo } from "coral-ui/components/v2";
|
||||
import { PropTypesOf } from "coral-ui/types";
|
||||
|
||||
import {
|
||||
getCommentBodyValidators,
|
||||
getHTMLCharacterLength,
|
||||
} from "../../helpers";
|
||||
import RemainingCharactersContainer from "../../RemainingCharacters";
|
||||
import RTEContainer from "../../RTE";
|
||||
import CommentForm from "../../Stream/CommentForm";
|
||||
import ReplyTo from "./ReplyTo";
|
||||
|
||||
import styles from "./ReplyCommentForm.css";
|
||||
|
||||
interface FormProps {
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ReplyCommentFormProps {
|
||||
id: string;
|
||||
className?: string;
|
||||
onSubmit: OnSubmit<any>;
|
||||
onCancel?: EventHandler<MouseEvent<any>>;
|
||||
onChange?: (state: FormState<any>, form: FormApi) => void;
|
||||
initialValues?: FormProps;
|
||||
initialValues?: any;
|
||||
rteRef?: Ref<CoralRTE>;
|
||||
parentUsername: string | null;
|
||||
min: number | null;
|
||||
max: number | null;
|
||||
disabled?: boolean;
|
||||
siteID: string;
|
||||
disabledMessage?: React.ReactNode;
|
||||
rteConfig: PropTypesOf<typeof RTEContainer>["config"];
|
||||
mediaConfig: PropTypesOf<typeof CommentForm>["mediaConfig"];
|
||||
}
|
||||
|
||||
const ReplyCommentForm: FunctionComponent<ReplyCommentFormProps> = (props) => {
|
||||
@@ -61,129 +43,39 @@ const ReplyCommentForm: FunctionComponent<ReplyCommentFormProps> = (props) => {
|
||||
const onFocus = useCallback(() => {
|
||||
emitFocusEvent();
|
||||
}, [emitFocusEvent]);
|
||||
return (
|
||||
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
|
||||
{({ handleSubmit, submitting, form, submitError }) => (
|
||||
<form
|
||||
className={cn(props.className, CLASSES.createReplyComment.$root)}
|
||||
autoComplete="off"
|
||||
onSubmit={handleSubmit}
|
||||
id={`comments-replyCommentForm-form-${props.id}`}
|
||||
>
|
||||
<FormSpy
|
||||
onChange={(state) => props.onChange && props.onChange(state, form)}
|
||||
/>
|
||||
<HorizontalGutter>
|
||||
<Field
|
||||
name="body"
|
||||
validate={getCommentBodyValidators(props.min, props.max)}
|
||||
>
|
||||
{/* FIXME: (wyattjoh) reorganize this */}
|
||||
{({ input, meta }) => (
|
||||
<>
|
||||
<HorizontalGutter size="half">
|
||||
<div>
|
||||
<Localized id="comments-replyCommentForm-rteLabel">
|
||||
<AriaInfo component="label" htmlFor={inputID}>
|
||||
Write a reply
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
{props.parentUsername && (
|
||||
<ReplyTo username={props.parentUsername} />
|
||||
)}
|
||||
<Localized
|
||||
id="comments-replyCommentForm-rte"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<RTEContainer
|
||||
config={props.rteConfig}
|
||||
inputID={inputID}
|
||||
onFocus={onFocus}
|
||||
onChange={(html) => input.onChange(html)}
|
||||
value={input.value}
|
||||
placeholder="Write a reply"
|
||||
ref={props.rteRef}
|
||||
disabled={submitting || props.disabled}
|
||||
contentClassName={styles.rteContent}
|
||||
/>
|
||||
</Localized>
|
||||
</div>
|
||||
{props.disabled ? (
|
||||
<>
|
||||
{props.disabledMessage && (
|
||||
<ValidationMessage>
|
||||
{props.disabledMessage}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{meta.touched &&
|
||||
(meta.error ||
|
||||
(meta.submitError &&
|
||||
!meta.dirtySinceLastSubmit)) && (
|
||||
<ValidationMessage>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
{submitError && (
|
||||
<ValidationMessage>{submitError}</ValidationMessage>
|
||||
)}
|
||||
{props.max && (
|
||||
<RemainingCharactersContainer
|
||||
value={input.value}
|
||||
max={props.max}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
|
||||
<MatchMedia ltWidth="sm">
|
||||
{(matches) => (
|
||||
<Flex
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
itemGutter="half"
|
||||
>
|
||||
<Localized id="comments-replyCommentForm-cancel">
|
||||
<Button
|
||||
color="mono"
|
||||
variant="outline"
|
||||
disabled={submitting}
|
||||
onClick={props.onCancel}
|
||||
className={CLASSES.createReplyComment.cancel}
|
||||
fullWidth={matches}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="comments-replyCommentForm-submit">
|
||||
<Button
|
||||
color="stream"
|
||||
variant="regular"
|
||||
disabled={
|
||||
submitting ||
|
||||
getHTMLCharacterLength(input.value) === 0 ||
|
||||
props.disabled
|
||||
}
|
||||
type="submit"
|
||||
className={CLASSES.createReplyComment.submit}
|
||||
fullWidth={matches}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
)}
|
||||
</MatchMedia>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
return (
|
||||
<div>
|
||||
<CommentForm
|
||||
siteID={props.siteID}
|
||||
onSubmit={props.onSubmit}
|
||||
initialValues={props.initialValues}
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
disabled={props.disabled}
|
||||
classNameRoot="createReplyComment"
|
||||
disabledMessage={props.disabledMessage}
|
||||
onFocus={onFocus}
|
||||
onCancel={props.onCancel}
|
||||
mediaConfig={props.mediaConfig}
|
||||
placeHolderId="comments-replyCommentForm-rte"
|
||||
placeholder="Write a reply"
|
||||
bodyInputID={inputID}
|
||||
bodyLabel={
|
||||
<>
|
||||
<Localized id="comments-replyCommentForm-rteLabel">
|
||||
<AriaInfo component="label" htmlFor={inputID}>
|
||||
Write a reply
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
{props.parentUsername && (
|
||||
<ReplyTo username={props.parentUsername} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
rteConfig={props.rteConfig}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+9
@@ -39,6 +39,9 @@ function createDefaultProps(add: DeepPartial<Props> = {}): Props {
|
||||
revision: {
|
||||
id: "revision-id",
|
||||
},
|
||||
site: {
|
||||
id: "site-id",
|
||||
},
|
||||
},
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
autofocus: false,
|
||||
@@ -48,6 +51,11 @@ function createDefaultProps(add: DeepPartial<Props> = {}): Props {
|
||||
min: 3,
|
||||
max: 100,
|
||||
},
|
||||
media: {
|
||||
giphy: { enabled: false },
|
||||
twitter: { enabled: false },
|
||||
youtube: { enabled: false },
|
||||
},
|
||||
closeCommenting: {
|
||||
message: "closed",
|
||||
},
|
||||
@@ -133,6 +141,7 @@ it("creates a comment", async () => {
|
||||
parentRevisionID: "revision-id",
|
||||
nudge: true,
|
||||
local: undefined,
|
||||
media: undefined,
|
||||
...input,
|
||||
})
|
||||
).toBeTruthy();
|
||||
|
||||
+17
@@ -116,6 +116,7 @@ export class ReplyCommentFormContainer extends Component<Props, State> {
|
||||
local: this.props.localReply,
|
||||
nudge: this.state.nudge,
|
||||
body: input.body,
|
||||
media: input.media,
|
||||
})
|
||||
);
|
||||
if (submitStatus !== "RETRY") {
|
||||
@@ -179,10 +180,12 @@ export class ReplyCommentFormContainer extends Component<Props, State> {
|
||||
}
|
||||
return (
|
||||
<ReplyCommentForm
|
||||
siteID={this.props.comment.site.id}
|
||||
id={this.props.comment.id}
|
||||
rteConfig={this.props.settings.rte}
|
||||
onSubmit={this.handleOnSubmit}
|
||||
onChange={this.handleOnChange}
|
||||
mediaConfig={this.props.settings.media}
|
||||
initialValues={this.state.initialValues}
|
||||
onCancel={this.handleOnCancelOrDismiss}
|
||||
rteRef={this.handleRTERef}
|
||||
@@ -234,6 +237,17 @@ const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
|
||||
closeCommenting {
|
||||
message
|
||||
}
|
||||
media {
|
||||
twitter {
|
||||
enabled
|
||||
}
|
||||
youtube {
|
||||
enabled
|
||||
}
|
||||
giphy {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
rte {
|
||||
...RTEContainer_config
|
||||
}
|
||||
@@ -248,6 +262,9 @@ const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
|
||||
comment: graphql`
|
||||
fragment ReplyCommentFormContainer_comment on Comment {
|
||||
id
|
||||
site {
|
||||
id
|
||||
}
|
||||
author {
|
||||
username
|
||||
}
|
||||
|
||||
+56
@@ -6,6 +6,19 @@ exports[`renders correctly 1`] = `
|
||||
disabledMessage="closed"
|
||||
id="comment-id"
|
||||
max={100}
|
||||
mediaConfig={
|
||||
Object {
|
||||
"giphy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"twitter": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"youtube": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
min={3}
|
||||
onCancel={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -13,6 +26,7 @@ exports[`renders correctly 1`] = `
|
||||
parentUsername="Joe"
|
||||
rteConfig={Object {}}
|
||||
rteRef={[Function]}
|
||||
siteID="site-id"
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -22,6 +36,19 @@ exports[`renders when commenting has been disabled 1`] = `
|
||||
disabledMessage="commenting disabled"
|
||||
id="comment-id"
|
||||
max={100}
|
||||
mediaConfig={
|
||||
Object {
|
||||
"giphy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"twitter": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"youtube": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
min={3}
|
||||
onCancel={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -29,6 +56,7 @@ exports[`renders when commenting has been disabled 1`] = `
|
||||
parentUsername="Joe"
|
||||
rteConfig={Object {}}
|
||||
rteRef={[Function]}
|
||||
siteID="site-id"
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -38,6 +66,19 @@ exports[`renders when story has been closed 1`] = `
|
||||
disabledMessage="story closed"
|
||||
id="comment-id"
|
||||
max={100}
|
||||
mediaConfig={
|
||||
Object {
|
||||
"giphy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"twitter": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"youtube": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
min={3}
|
||||
onCancel={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -45,6 +86,7 @@ exports[`renders when story has been closed 1`] = `
|
||||
parentUsername="Joe"
|
||||
rteConfig={Object {}}
|
||||
rteRef={[Function]}
|
||||
siteID="site-id"
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -59,6 +101,19 @@ exports[`renders with initialValues 1`] = `
|
||||
}
|
||||
}
|
||||
max={100}
|
||||
mediaConfig={
|
||||
Object {
|
||||
"giphy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"twitter": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"youtube": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
min={3}
|
||||
onCancel={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -66,5 +121,6 @@ exports[`renders with initialValues 1`] = `
|
||||
parentUsername="Joe"
|
||||
rteConfig={Object {}}
|
||||
rteRef={[Function]}
|
||||
siteID="site-id"
|
||||
/>
|
||||
`;
|
||||
|
||||
+298
@@ -121,6 +121,48 @@ exports[`hide reply button 1`] = `
|
||||
</React.Fragment>
|
||||
}
|
||||
indentLevel={1}
|
||||
media={
|
||||
<Relay(MediaSectionContainer)
|
||||
comment={
|
||||
Object {
|
||||
"actionCounts": Object {
|
||||
"reaction": Object {
|
||||
"total": 0,
|
||||
},
|
||||
},
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"deleted": false,
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
"viewerActionPresence": Object {
|
||||
"dontAgree": false,
|
||||
"flag": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
parentAuthorName={null}
|
||||
showEditedMarker={false}
|
||||
staticTopBarRight={<React.Fragment />}
|
||||
@@ -513,6 +555,48 @@ exports[`renders body only 1`] = `
|
||||
</React.Fragment>
|
||||
}
|
||||
indentLevel={1}
|
||||
media={
|
||||
<Relay(MediaSectionContainer)
|
||||
comment={
|
||||
Object {
|
||||
"actionCounts": Object {
|
||||
"reaction": Object {
|
||||
"total": 0,
|
||||
},
|
||||
},
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": null,
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"deleted": false,
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
"viewerActionPresence": Object {
|
||||
"dontAgree": false,
|
||||
"flag": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
parentAuthorName={null}
|
||||
showEditedMarker={false}
|
||||
staticTopBarRight={<React.Fragment />}
|
||||
@@ -905,6 +989,48 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
|
||||
</React.Fragment>
|
||||
}
|
||||
indentLevel={1}
|
||||
media={
|
||||
<Relay(MediaSectionContainer)
|
||||
comment={
|
||||
Object {
|
||||
"actionCounts": Object {
|
||||
"reaction": Object {
|
||||
"total": 0,
|
||||
},
|
||||
},
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"deleted": false,
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
"viewerActionPresence": Object {
|
||||
"dontAgree": false,
|
||||
"flag": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"disableCommenting": Object {
|
||||
"enabled": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
parentAuthorName={null}
|
||||
showEditedMarker={false}
|
||||
staticTopBarRight={<React.Fragment />}
|
||||
@@ -1297,6 +1423,48 @@ exports[`renders disabled reply when story is closed 1`] = `
|
||||
</React.Fragment>
|
||||
}
|
||||
indentLevel={1}
|
||||
media={
|
||||
<Relay(MediaSectionContainer)
|
||||
comment={
|
||||
Object {
|
||||
"actionCounts": Object {
|
||||
"reaction": Object {
|
||||
"total": 0,
|
||||
},
|
||||
},
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"deleted": false,
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
"viewerActionPresence": Object {
|
||||
"dontAgree": false,
|
||||
"flag": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
parentAuthorName={null}
|
||||
showEditedMarker={false}
|
||||
staticTopBarRight={<React.Fragment />}
|
||||
@@ -1697,6 +1865,52 @@ exports[`renders in reply to 1`] = `
|
||||
</React.Fragment>
|
||||
}
|
||||
indentLevel={1}
|
||||
media={
|
||||
<Relay(MediaSectionContainer)
|
||||
comment={
|
||||
Object {
|
||||
"actionCounts": Object {
|
||||
"reaction": Object {
|
||||
"total": 0,
|
||||
},
|
||||
},
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"deleted": false,
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": Object {
|
||||
"author": Object {
|
||||
"username": "ParentAuthor",
|
||||
},
|
||||
},
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
"viewerActionPresence": Object {
|
||||
"dontAgree": false,
|
||||
"flag": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
parentAuthorName="ParentAuthor"
|
||||
showEditedMarker={false}
|
||||
staticTopBarRight={<React.Fragment />}
|
||||
@@ -2113,6 +2327,48 @@ exports[`renders username and body 1`] = `
|
||||
</React.Fragment>
|
||||
}
|
||||
indentLevel={1}
|
||||
media={
|
||||
<Relay(MediaSectionContainer)
|
||||
comment={
|
||||
Object {
|
||||
"actionCounts": Object {
|
||||
"reaction": Object {
|
||||
"total": 0,
|
||||
},
|
||||
},
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"deleted": false,
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
"viewerActionPresence": Object {
|
||||
"dontAgree": false,
|
||||
"flag": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
parentAuthorName={null}
|
||||
showEditedMarker={false}
|
||||
staticTopBarRight={<React.Fragment />}
|
||||
@@ -2520,6 +2776,48 @@ exports[`shows conversation link 1`] = `
|
||||
</React.Fragment>
|
||||
}
|
||||
indentLevel={1}
|
||||
media={
|
||||
<Relay(MediaSectionContainer)
|
||||
comment={
|
||||
Object {
|
||||
"actionCounts": Object {
|
||||
"reaction": Object {
|
||||
"total": 0,
|
||||
},
|
||||
},
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"deleted": false,
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
"viewerActionPresence": Object {
|
||||
"dontAgree": false,
|
||||
"flag": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
parentAuthorName={null}
|
||||
showEditedMarker={false}
|
||||
staticTopBarRight={<React.Fragment />}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.root {
|
||||
padding: var(--v2-spacing-2);
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
padding-top: var(--v2-spacing-2);
|
||||
border-top: 1px solid var(--v2-colors-grey-200);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 8px;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Button, ButtonIcon } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./GifPreview.css";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
title: string;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
const GifPreview: FunctionComponent<Props> = ({ onRemove, url, title }) => {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.imageWrapper}>
|
||||
<Button
|
||||
onClick={onRemove}
|
||||
iconLeft
|
||||
color="mono"
|
||||
className={styles.button}
|
||||
>
|
||||
<ButtonIcon>cancel</ButtonIcon>
|
||||
<Localized id="comments-commentForm-gifPreview-remove">
|
||||
Remove
|
||||
</Localized>
|
||||
</Button>
|
||||
<img src={url} alt={title} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GifPreview;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { GiphyGifSearchResponse } from "coral-common/rest/external/giphy";
|
||||
import { createFetch, FetchVariables } from "coral-framework/lib/relay";
|
||||
|
||||
export const GIF_RESULTS_LIMIT = 10;
|
||||
|
||||
interface QueryTypes {
|
||||
variables: {
|
||||
query: string;
|
||||
page: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const GifSearchFetch = createFetch(
|
||||
"gifSearch",
|
||||
async (
|
||||
environment: Environment,
|
||||
variables: FetchVariables<QueryTypes>,
|
||||
{ rest }
|
||||
) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("query", variables.query);
|
||||
params.set("offset", `${variables.page * GIF_RESULTS_LIMIT}`);
|
||||
const url = `/remote-media/gifs?${params.toString()}`;
|
||||
return rest.fetch<GiphyGifSearchResponse>(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
.root {
|
||||
padding: var(--v2-spacing-2);
|
||||
}
|
||||
|
||||
.input {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-size: var(--v2-font-size-3);
|
||||
color: var(--v2-colors-mono-100);
|
||||
}
|
||||
|
||||
.loading {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-size: var(--v2-font-size-3);
|
||||
color: var(--v2-colors-mono-100);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-size: var(--v2-font-size-3);
|
||||
color: var(--v2-colors-red-500);
|
||||
}
|
||||
|
||||
.results {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result {
|
||||
/* flex: 1; */
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.resultImg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { GiphyGif } from "coral-common/rest/external/giphy";
|
||||
import { useFetch } from "coral-framework/lib/relay";
|
||||
import {
|
||||
BaseButton,
|
||||
Button,
|
||||
ButtonIcon,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
InputLabel,
|
||||
TextField,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { GIF_RESULTS_LIMIT, GifSearchFetch } from "./GifSearchFetch";
|
||||
import GiphyAttribution from "./GiphyAttribution";
|
||||
|
||||
import styles from "./GifSelector.css";
|
||||
|
||||
interface Props {
|
||||
onGifSelect: (gif: GiphyGif) => void;
|
||||
}
|
||||
|
||||
const GifSelector: FunctionComponent<Props> = (props) => {
|
||||
const gifSearchFetch = useFetch(GifSearchFetch);
|
||||
const [results, setResults] = useState<GiphyGif[]>([]);
|
||||
const [searchError, setSearchError] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(0);
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [hasNextPage, setHasNextPage] = useState(false);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onSearchFieldChange = useCallback(
|
||||
(evt: ChangeEvent<HTMLInputElement>) => {
|
||||
setQuery(evt.target.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const searchInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: why doesn't this work?
|
||||
if (searchInput && searchInput.current) {
|
||||
searchInput.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function search() {
|
||||
try {
|
||||
const res = await gifSearchFetch({ query, page });
|
||||
const { pagination, data } = res;
|
||||
if (pagination.total_count > pagination.offset * GIF_RESULTS_LIMIT) {
|
||||
setHasNextPage(true);
|
||||
} else {
|
||||
setHasNextPage(false);
|
||||
}
|
||||
setSearchError(null);
|
||||
setResults(data);
|
||||
} catch (error) {
|
||||
setSearchError(error.message);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
let timeout: any | null = null;
|
||||
|
||||
if (query && query.length > 1) {
|
||||
setIsLoading(true);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
void search();
|
||||
}, 200);
|
||||
} else {
|
||||
setPage(0);
|
||||
setResults([]);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
}, [query, page, setResults, setIsLoading, setPage]);
|
||||
const nextPage = useCallback(() => {
|
||||
setPage(page + 1);
|
||||
}, [page]);
|
||||
const prevPage = useCallback(() => {
|
||||
setPage(page - 1);
|
||||
}, [page]);
|
||||
const onGifSelect = useCallback((gif: GiphyGif) => {
|
||||
setResults([]);
|
||||
setPage(0);
|
||||
setHasNextPage(false);
|
||||
setQuery("");
|
||||
props.onGifSelect(gif);
|
||||
}, []);
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<HorizontalGutter>
|
||||
<HorizontalGutter>
|
||||
<Localized id="comments-postComment-gifSearch">
|
||||
<InputLabel>Search for a GIF</InputLabel>
|
||||
</Localized>
|
||||
<TextField
|
||||
className={styles.input}
|
||||
value={query}
|
||||
onChange={onSearchFieldChange}
|
||||
fullWidth
|
||||
variant="seamlessAdornment"
|
||||
color="streamBlue"
|
||||
adornment={
|
||||
<Button color="stream" className={styles.searchButton}>
|
||||
<ButtonIcon>search</ButtonIcon>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</HorizontalGutter>
|
||||
{isLoading && (
|
||||
<Localized id="comments-postComment-gifSearch-loading">
|
||||
<p className={styles.loading}>Loading...</p>
|
||||
</Localized>
|
||||
)}
|
||||
{results.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<Flex className={styles.results} justifyContent="space-evenly">
|
||||
{results.slice(0, results.length / 2).map((result) => (
|
||||
<BaseButton
|
||||
key={result.id}
|
||||
onClick={() => onGifSelect(result)}
|
||||
className={styles.result}
|
||||
>
|
||||
<img
|
||||
src={result.images.fixed_height_downsampled.url}
|
||||
alt={result.title}
|
||||
className={styles.resultImg}
|
||||
/>
|
||||
</BaseButton>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex className={styles.results} justifyContent="space-evenly">
|
||||
{results
|
||||
.slice(results.length / 2, results.length)
|
||||
.map((result) => (
|
||||
<BaseButton
|
||||
className={styles.result}
|
||||
key={result.id}
|
||||
onClick={() => onGifSelect(result)}
|
||||
>
|
||||
<img
|
||||
src={result.images.fixed_height_downsampled.url}
|
||||
alt={result.title}
|
||||
className={styles.resultImg}
|
||||
/>
|
||||
</BaseButton>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
<GiphyAttribution />
|
||||
</>
|
||||
)}
|
||||
{results.length > 0 && (
|
||||
<Flex
|
||||
justifyContent={
|
||||
results.length > 0 && hasNextPage && page > 0
|
||||
? "space-between"
|
||||
: "flex-end"
|
||||
}
|
||||
>
|
||||
{results.length > 0 && page > 0 && (
|
||||
<Button
|
||||
onClick={prevPage}
|
||||
variant="outline"
|
||||
color="stream"
|
||||
iconLeft
|
||||
>
|
||||
<ButtonIcon>keyboard_arrow_left</ButtonIcon>
|
||||
Previous
|
||||
</Button>
|
||||
)}
|
||||
{results.length > 0 && hasNextPage && (
|
||||
<Button
|
||||
onClick={nextPage}
|
||||
variant="outline"
|
||||
color="stream"
|
||||
iconRight
|
||||
>
|
||||
Next
|
||||
<ButtonIcon>keyboard_arrow_right</ButtonIcon>
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{searchError && <p className={styles.error}>{searchError}</p>}
|
||||
{!isLoading &&
|
||||
!searchError &&
|
||||
results.length === 0 &&
|
||||
query.length > 1 && (
|
||||
<Localized
|
||||
id="comments-postComment-gifSearch-no-results"
|
||||
$query={query}
|
||||
>
|
||||
<p className={styles.noResults}>
|
||||
No results found for "{query}"{" "}
|
||||
</p>
|
||||
</Localized>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GifSelector;
|
||||
@@ -0,0 +1,3 @@
|
||||
.img {
|
||||
width: 100px;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Flex } from "coral-ui/components/v2";
|
||||
|
||||
import giphyAttributionImg from "./giphyAttribution.png";
|
||||
|
||||
import styles from "./GiphyAttribution.css";
|
||||
|
||||
const GiphyAttribution: FunctionComponent = () => {
|
||||
return (
|
||||
<Flex justifyContent="flex-end">
|
||||
<Localized
|
||||
id="comments-postComment-gifSearch-powered-by-giphy"
|
||||
attrs={{ alt: true }}
|
||||
>
|
||||
<img
|
||||
className={styles.img}
|
||||
src={giphyAttributionImg}
|
||||
alt="powered by giphy"
|
||||
/>
|
||||
</Localized>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default GiphyAttribution;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,2 @@
|
||||
export { default, default as GifSelector } from "./GifSelector";
|
||||
export { default as GifPreview } from "./GifPreview";
|
||||
@@ -2,13 +2,13 @@ $commentsRTEBorder: var(--v2-colors-grey-300);
|
||||
$commentsRTEContentBackground: var(--v2-colors-pure-white);
|
||||
$commentsRTEPlaceholder: var(--v2-colors-grey-400);
|
||||
|
||||
.container {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.content {
|
||||
composes: root from "~coral-stream/shared/htmlContent.css";
|
||||
|
||||
border: 1px solid $commentsRTEBorder;
|
||||
border-bottom-width: 0px;
|
||||
border-top-left-radius: var(--v2-round-corners);
|
||||
border-top-right-radius: var(--v2-round-corners);
|
||||
border: 0;
|
||||
|
||||
& :global(.coral-rte-spoiler) {
|
||||
background-color: black;
|
||||
@@ -20,11 +20,7 @@ $commentsRTEPlaceholder: var(--v2-colors-grey-400);
|
||||
|
||||
.toolbar {
|
||||
background-color: $commentsRTEContentBackground;
|
||||
border: 1px solid $commentsRTEBorder;
|
||||
|
||||
border-top-width: 0px;
|
||||
border-bottom-left-radius: var(--v2-round-corners);
|
||||
border-bottom-right-radius: var(--v2-round-corners);
|
||||
border: 0;
|
||||
}
|
||||
.toolbarHidden {
|
||||
display: none;
|
||||
|
||||
@@ -76,6 +76,12 @@ const Localized = React.forwardRef<any, PropTypesOf<typeof LocalizedOriginal>>(
|
||||
}
|
||||
);
|
||||
|
||||
export interface PasteEvent {
|
||||
fragment: DocumentFragment;
|
||||
preventDefault: () => void;
|
||||
defaultPrevented: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
inputID?: string;
|
||||
/**
|
||||
@@ -131,6 +137,10 @@ interface Props {
|
||||
forwardRef?: Ref<CoralRTE>;
|
||||
|
||||
features?: RTEFeatures;
|
||||
|
||||
toolbarButtons?: React.ReactElement | null;
|
||||
|
||||
onWillPaste?: (event: PasteEvent) => void;
|
||||
}
|
||||
|
||||
const RTE: FunctionComponent<Props> = (props) => {
|
||||
@@ -151,6 +161,7 @@ const RTE: FunctionComponent<Props> = (props) => {
|
||||
onFocus,
|
||||
onBlur,
|
||||
features,
|
||||
onWillPaste,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@@ -228,6 +239,9 @@ const RTE: FunctionComponent<Props> = (props) => {
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
if (props.toolbarButtons) {
|
||||
x.push(props.toolbarButtons);
|
||||
}
|
||||
return x;
|
||||
}, [features]);
|
||||
|
||||
@@ -255,6 +269,7 @@ const RTE: FunctionComponent<Props> = (props) => {
|
||||
}
|
||||
)}
|
||||
contentContainerClassName={cn(
|
||||
styles.container,
|
||||
CLASSES.rte.container,
|
||||
containerClassName
|
||||
)}
|
||||
@@ -267,6 +282,7 @@ const RTE: FunctionComponent<Props> = (props) => {
|
||||
toolbarPosition="bottom"
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
onWillPaste={onWillPaste}
|
||||
sanitizeToDOMFragment={sanitizeToDOMFragment}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import RTE from "./RTE";
|
||||
interface Props extends Omit<PropTypesOf<typeof RTE>, "ref"> {
|
||||
forwardRef: Ref<CoralRTE>;
|
||||
config: RTEContainer_config;
|
||||
toolbarButtons?: React.ReactElement | null;
|
||||
}
|
||||
|
||||
const RTEContainer: React.FunctionComponent<Props> = ({ config, ...rest }) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ exports[`renders correctly 1`] = `
|
||||
classNameDisabled=""
|
||||
contentClassName="coral coral-rte-content RTE-content"
|
||||
contentClassNameDisabled=""
|
||||
contentContainerClassName="coral coral-rte-container"
|
||||
contentContainerClassName="RTE-container coral coral-rte-container"
|
||||
contentContainerClassNameDisabled=""
|
||||
features={Array []}
|
||||
placeholder="Post a comment"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
$commentsBorder: var(--v2-colors-grey-300);
|
||||
|
||||
.messageBox {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.commentFormBox {
|
||||
border: 1px solid $commentsBorder;
|
||||
border-radius: var(--v2-round-corners);
|
||||
}
|
||||
|
||||
.poweredBy {
|
||||
margin-top: calc(-0.5 * var(--mini-unit));
|
||||
}
|
||||
|
||||
.rteBorderless {
|
||||
border-top-width: 0;
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
import { CoralRTE } from "@coralproject/rte";
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { FormApi, FormState } from "final-form";
|
||||
import React, {
|
||||
EventHandler,
|
||||
FunctionComponent,
|
||||
MouseEvent,
|
||||
Ref,
|
||||
useCallback,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Field, Form, FormSpy } from "react-final-form";
|
||||
|
||||
import { findMediaLinks, MediaLink } from "coral-common/helpers/findMediaLinks";
|
||||
import { useToggleState } from "coral-framework/hooks";
|
||||
import { FormError, OnSubmit } from "coral-framework/lib/form";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import ValidationMessage from "coral-stream/common/ValidationMessage";
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
MatchMedia,
|
||||
Message,
|
||||
MessageIcon,
|
||||
RelativeTime,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { getCommentBodyValidators } from "../../helpers";
|
||||
import RemainingCharactersContainer from "../../RemainingCharacters";
|
||||
import RTEContainer from "../../RTE";
|
||||
import MediaField from "./MediaField";
|
||||
|
||||
import styles from "./CommentForm.css";
|
||||
|
||||
export interface PasteEvent {
|
||||
fragment: DocumentFragment;
|
||||
preventDefault: () => void;
|
||||
defaultPrevented: boolean;
|
||||
}
|
||||
|
||||
interface MediaProps {
|
||||
type: "giphy" | "twitter" | "youtube";
|
||||
url: string;
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
body: string;
|
||||
media?: MediaProps;
|
||||
}
|
||||
|
||||
interface FormSubmitProps extends FormProps, FormError {}
|
||||
|
||||
interface Props {
|
||||
onSubmit: OnSubmit<FormSubmitProps>;
|
||||
onChange?: (state: FormState<any>, form: FormApi) => void;
|
||||
initialValues?: FormProps;
|
||||
min: number | null;
|
||||
max: number | null;
|
||||
disabled?: boolean;
|
||||
disabledMessage?: React.ReactNode;
|
||||
bodyLabel: React.ReactNode;
|
||||
rteConfig: PropTypesOf<typeof RTEContainer>["config"];
|
||||
onFocus?: () => void;
|
||||
rteRef?: Ref<CoralRTE>;
|
||||
onCancel?: EventHandler<MouseEvent<any>>;
|
||||
editableUntil?: string;
|
||||
expired?: boolean;
|
||||
submitStatus?: React.ReactNode;
|
||||
classNameRoot: "createComment" | "editComment" | "createReplyComment";
|
||||
mediaConfig: PropTypesOf<typeof MediaField>["config"] | null;
|
||||
placeholder: string;
|
||||
placeHolderId: string;
|
||||
bodyInputID: string;
|
||||
siteID: string;
|
||||
}
|
||||
|
||||
const CommentForm: FunctionComponent<Props> = (props) => {
|
||||
const [showGifSelector, , toggleGIFSelector] = useToggleState();
|
||||
const [media, setMedia] = useState<MediaLink | null>(null);
|
||||
const onSubmit = useCallback(
|
||||
(values: FormSubmitProps, form: FormApi) => {
|
||||
// Unset the media.
|
||||
setMedia(null);
|
||||
|
||||
// Submit the form.
|
||||
return props.onSubmit(values, form);
|
||||
},
|
||||
[props.onSubmit, setMedia]
|
||||
);
|
||||
|
||||
const onPaste = useCallback(
|
||||
(event: PasteEvent) => {
|
||||
const children = event.fragment.children;
|
||||
let link = null;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const item = children.item(i);
|
||||
if (item && item.textContent) {
|
||||
const links = findMediaLinks(item.textContent);
|
||||
if (links.length > 0) {
|
||||
link = links[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
link &&
|
||||
props.mediaConfig &&
|
||||
((link.type === "twitter" && props.mediaConfig.twitter.enabled) ||
|
||||
(link.type === "youtube" && props.mediaConfig.youtube.enabled))
|
||||
) {
|
||||
setMedia({ ...link });
|
||||
}
|
||||
},
|
||||
[setMedia, props.mediaConfig]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={CLASSES[props.classNameRoot].$root}>
|
||||
<Form onSubmit={onSubmit} initialValues={props.initialValues}>
|
||||
{({
|
||||
handleSubmit,
|
||||
submitting,
|
||||
submitError,
|
||||
hasValidationErrors,
|
||||
form,
|
||||
pristine,
|
||||
}) => (
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={handleSubmit}
|
||||
id="comments-postCommentForm-form"
|
||||
>
|
||||
<HorizontalGutter>
|
||||
<FormSpy
|
||||
onChange={(state) => {
|
||||
return props.onChange && props.onChange(state, form);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.commentFormBox}>
|
||||
<Field
|
||||
name="body"
|
||||
validate={getCommentBodyValidators(props.min, props.max)}
|
||||
>
|
||||
{({ input }) => (
|
||||
<>
|
||||
<HorizontalGutter size="half">
|
||||
{props.bodyLabel}
|
||||
<div>
|
||||
<Localized
|
||||
id={props.placeHolderId}
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<RTEContainer
|
||||
inputID={props.bodyInputID}
|
||||
config={props.rteConfig}
|
||||
onFocus={props.onFocus}
|
||||
onWillPaste={onPaste}
|
||||
onChange={(html: string) => {
|
||||
input.onChange(html);
|
||||
}}
|
||||
value={input.value}
|
||||
placeholder={props.placeholder}
|
||||
disabled={submitting || props.disabled}
|
||||
ref={props.rteRef || null}
|
||||
toolbarButtons={
|
||||
props.mediaConfig &&
|
||||
props.mediaConfig.giphy.enabled ? (
|
||||
<>
|
||||
<Button
|
||||
color="mono"
|
||||
variant={
|
||||
showGifSelector ? "regular" : "flat"
|
||||
}
|
||||
onClick={toggleGIFSelector}
|
||||
iconLeft
|
||||
>
|
||||
<ButtonIcon>add</ButtonIcon>
|
||||
GIF
|
||||
</Button>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</Localized>
|
||||
</div>
|
||||
</HorizontalGutter>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
{props.mediaConfig && (
|
||||
<MediaField
|
||||
config={props.mediaConfig}
|
||||
siteID={props.siteID}
|
||||
media={media}
|
||||
setMedia={setMedia}
|
||||
showGIFSelector={showGifSelector}
|
||||
toggleGIFSelector={toggleGIFSelector}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!props.expired && props.editableUntil && (
|
||||
<Message
|
||||
className={CLASSES.editComment.remainingTime}
|
||||
fullWidth
|
||||
>
|
||||
<MessageIcon>alarm</MessageIcon>
|
||||
<Localized
|
||||
id="comments-editCommentForm-editRemainingTime"
|
||||
time={<RelativeTime date={props.editableUntil} live />}
|
||||
>
|
||||
<span>{"Edit: <time></time> remaining"}</span>
|
||||
</Localized>
|
||||
</Message>
|
||||
)}
|
||||
{props.disabled ? (
|
||||
<>
|
||||
{props.disabledMessage && (
|
||||
<ValidationMessage>
|
||||
{props.disabledMessage}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Field
|
||||
name="body"
|
||||
subscription={{
|
||||
touched: true,
|
||||
error: true,
|
||||
submitError: true,
|
||||
value: true,
|
||||
dirtySinceLastSubmit: true,
|
||||
}}
|
||||
>
|
||||
{({
|
||||
input: { value },
|
||||
meta: {
|
||||
touched,
|
||||
error,
|
||||
submitError: localSubmitError,
|
||||
dirtySinceLastSubmit,
|
||||
},
|
||||
}) => (
|
||||
<>
|
||||
{touched &&
|
||||
(error ||
|
||||
(localSubmitError && !dirtySinceLastSubmit)) && (
|
||||
<ValidationMessage>
|
||||
{error || localSubmitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
{props.max && (
|
||||
<RemainingCharactersContainer
|
||||
value={value}
|
||||
max={props.max}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
{submitError && (
|
||||
<ValidationMessage>{submitError}</ValidationMessage>
|
||||
)}
|
||||
<Flex justifyContent="flex-end" spacing={1}>
|
||||
<MatchMedia ltWidth="sm">
|
||||
{(matches) => (
|
||||
<>
|
||||
{props.onCancel && (
|
||||
<Localized id="comments-commentForm-cancel">
|
||||
<Button
|
||||
color="mono"
|
||||
variant="outline"
|
||||
disabled={submitting}
|
||||
onClick={props.onCancel}
|
||||
fullWidth={matches}
|
||||
className={CLASSES[props.classNameRoot].cancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
<Localized
|
||||
id={
|
||||
props.editableUntil
|
||||
? "comments-commentForm-saveChanges"
|
||||
: "comments-commentForm-submit"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
color="stream"
|
||||
variant="regular"
|
||||
disabled={
|
||||
hasValidationErrors ||
|
||||
submitting ||
|
||||
props.disabled ||
|
||||
(!!props.editableUntil && pristine)
|
||||
}
|
||||
type="submit"
|
||||
fullWidth={matches}
|
||||
className={CLASSES[props.classNameRoot].submit}
|
||||
>
|
||||
{props.editableUntil ? "Save changes" : "Submit"}
|
||||
</Button>
|
||||
</Localized>
|
||||
</>
|
||||
)}
|
||||
</MatchMedia>
|
||||
</Flex>
|
||||
{props.submitStatus}
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentForm;
|
||||
@@ -0,0 +1,101 @@
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { useField } from "react-final-form";
|
||||
|
||||
import { isMediaLink, MediaLink } from "coral-common/helpers/findMediaLinks";
|
||||
import { GiphyGif } from "coral-common/rest/external/giphy";
|
||||
|
||||
import {
|
||||
MediaConfirmPrompt,
|
||||
MediaPreview,
|
||||
} from "../../Comment/MediaConfirmation";
|
||||
import GifSelector, { GifPreview } from "../../GifSelector";
|
||||
|
||||
interface Props {
|
||||
showGIFSelector: boolean;
|
||||
toggleGIFSelector: () => void;
|
||||
siteID: string;
|
||||
config: MediaConfig;
|
||||
media: MediaLink | null;
|
||||
setMedia: (media: MediaLink | null) => void;
|
||||
}
|
||||
|
||||
interface Media {
|
||||
type: "giphy" | "twitter" | "youtube";
|
||||
url: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface MediaConfig {
|
||||
giphy: {
|
||||
enabled: boolean;
|
||||
};
|
||||
twitter: {
|
||||
enabled: boolean;
|
||||
};
|
||||
youtube: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const MediaField: FunctionComponent<Props> = (props) => {
|
||||
const field = useField<Media | undefined>("media", {
|
||||
initialValue: undefined,
|
||||
});
|
||||
|
||||
const onGIFSelect = useCallback(
|
||||
(gif: GiphyGif) => {
|
||||
field.input.onChange({
|
||||
type: "giphy",
|
||||
id: gif.id,
|
||||
url: gif.images.original.url,
|
||||
});
|
||||
|
||||
props.toggleGIFSelector();
|
||||
},
|
||||
[props.toggleGIFSelector]
|
||||
);
|
||||
|
||||
const onGIFRemove = useCallback(() => {
|
||||
field.input.onChange(undefined);
|
||||
}, [field.input]);
|
||||
|
||||
const onConfirmMedia = useCallback(() => {
|
||||
field.input.onChange(props.media!);
|
||||
props.setMedia(null);
|
||||
}, [props.media, props.setMedia, field.input]);
|
||||
|
||||
const onRemoveMedia = useCallback(() => {
|
||||
field.input.onChange(null);
|
||||
props.setMedia(null);
|
||||
}, [props.media, props.setMedia, field.input]);
|
||||
|
||||
// Grab the reference to the media object.
|
||||
const media = field.input.value;
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.showGIFSelector && <GifSelector onGifSelect={onGIFSelect} />}
|
||||
{props.media && (
|
||||
<MediaConfirmPrompt
|
||||
media={props.media}
|
||||
onConfirm={onConfirmMedia}
|
||||
onRemove={onRemoveMedia}
|
||||
/>
|
||||
)}
|
||||
{media &&
|
||||
media.url &&
|
||||
(isMediaLink(media) ? (
|
||||
<MediaPreview
|
||||
config={props.config}
|
||||
media={media}
|
||||
siteID={props.siteID}
|
||||
onRemove={onRemoveMedia}
|
||||
/>
|
||||
) : (
|
||||
<GifPreview url={media.url} onRemove={onGIFRemove} title="" />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaField;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./CommentForm";
|
||||
+13
-5
@@ -15,7 +15,7 @@ import {
|
||||
SetCommentIDMutation,
|
||||
withSetCommentIDMutation,
|
||||
} from "coral-stream/mutations";
|
||||
import { Box, Flex, Icon } from "coral-ui/components/v2";
|
||||
import { Box, Flex, HorizontalGutter, Icon } from "coral-ui/components/v2";
|
||||
import { Button } from "coral-ui/components/v3";
|
||||
|
||||
import { FeaturedCommentContainer_comment as CommentData } from "coral-stream/__generated__/FeaturedCommentContainer_comment.graphql";
|
||||
@@ -24,6 +24,7 @@ import { FeaturedCommentContainer_story as StoryData } from "coral-stream/__gene
|
||||
import { FeaturedCommentContainer_viewer as ViewerData } from "coral-stream/__generated__/FeaturedCommentContainer_viewer.graphql";
|
||||
|
||||
import { UserTagsContainer } from "../../Comment";
|
||||
import MediaSectionContainer from "../../Comment/MediaSection";
|
||||
import ReactionButtonContainer from "../../Comment/ReactionButton";
|
||||
import { UsernameWithPopoverContainer } from "../../Comment/Username";
|
||||
|
||||
@@ -61,13 +62,18 @@ const FeaturedCommentContainer: FunctionComponent<Props> = (props) => {
|
||||
className={cn(CLASSES.featuredComment.$root, styles.root)}
|
||||
data-testid={`featuredComment-${comment.id}`}
|
||||
>
|
||||
<HTMLContent className={cn(styles.body, CLASSES.featuredComment.content)}>
|
||||
{comment.body || ""}
|
||||
</HTMLContent>
|
||||
<HorizontalGutter>
|
||||
<HTMLContent
|
||||
className={cn(styles.body, CLASSES.featuredComment.content)}
|
||||
>
|
||||
{comment.body || ""}
|
||||
</HTMLContent>
|
||||
<MediaSectionContainer comment={comment} settings={settings} />
|
||||
</HorizontalGutter>
|
||||
<Flex
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mt={4}
|
||||
mt={3}
|
||||
className={CLASSES.featuredComment.authorBar.$root}
|
||||
>
|
||||
{comment.author && (
|
||||
@@ -188,6 +194,7 @@ const enhanced = withSetCommentIDMutation(
|
||||
replyCount
|
||||
...UsernameWithPopoverContainer_comment
|
||||
...ReactionButtonContainer_comment
|
||||
...MediaSectionContainer_comment
|
||||
...UserTagsContainer_comment
|
||||
}
|
||||
`,
|
||||
@@ -195,6 +202,7 @@ const enhanced = withSetCommentIDMutation(
|
||||
fragment FeaturedCommentContainer_settings on Settings {
|
||||
...ReactionButtonContainer_settings
|
||||
...UserTagsContainer_settings
|
||||
...MediaSectionContainer_settings
|
||||
}
|
||||
`,
|
||||
})(FeaturedCommentContainer)
|
||||
|
||||
+8
-3
@@ -182,8 +182,9 @@ async function commit(
|
||||
variables: {
|
||||
input: {
|
||||
storyID: input.storyID,
|
||||
body: input.body,
|
||||
body: input.body || "",
|
||||
nudge: input.nudge,
|
||||
media: input.media,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
@@ -202,11 +203,15 @@ async function commit(
|
||||
badges: viewer.badges,
|
||||
ignoreable: false,
|
||||
},
|
||||
revision: {
|
||||
site: {
|
||||
id: uuidGenerator(),
|
||||
},
|
||||
revision: {
|
||||
id: uuidGenerator(),
|
||||
media: null,
|
||||
},
|
||||
parent: null,
|
||||
body: input.body,
|
||||
body: input.body || "",
|
||||
editing: {
|
||||
editableUntil: new Date(Date.now() + 10000).toISOString(),
|
||||
edited: false,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
$commentsBorder: var(--v2-colors-grey-300);
|
||||
|
||||
.messageBox {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.commentFormBox {
|
||||
border: 1px solid $commentsBorder;
|
||||
border-radius: var(--v2-round-corners);
|
||||
}
|
||||
|
||||
.poweredBy {
|
||||
margin-top: calc(-0.5 * var(--mini-unit));
|
||||
}
|
||||
|
||||
@@ -2,35 +2,30 @@ import { Localized } from "@fluent/react/compat";
|
||||
import cn from "classnames";
|
||||
import { FormApi, FormState } from "final-form";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { Field, Form, FormSpy } from "react-final-form";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { FormError, OnSubmit } from "coral-framework/lib/form";
|
||||
import { GQLSTORY_MODE } from "coral-framework/schema";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import ValidationMessage from "coral-stream/common/ValidationMessage";
|
||||
import { CreateCommentFocusEvent } from "coral-stream/events";
|
||||
import {
|
||||
AriaInfo,
|
||||
Button,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
} from "coral-ui/components/v2";
|
||||
import { AriaInfo } from "coral-ui/components/v2";
|
||||
|
||||
import {
|
||||
getCommentBodyValidators,
|
||||
getHTMLCharacterLength,
|
||||
} from "../../helpers";
|
||||
import RemainingCharactersContainer from "../../RemainingCharacters";
|
||||
import RTEContainer from "../../RTE";
|
||||
import CommentForm from "../CommentForm";
|
||||
import MessageBoxContainer from "../MessageBoxContainer";
|
||||
import PostCommentSubmitStatusContainer from "./PostCommentSubmitStatusContainer";
|
||||
|
||||
import styles from "./PostCommentForm.css";
|
||||
|
||||
interface MediaProps {
|
||||
type: "twitter" | "youtube" | "giphy";
|
||||
url: string;
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
body: string;
|
||||
media?: MediaProps;
|
||||
}
|
||||
|
||||
interface FormSubmitProps extends FormProps, FormError {}
|
||||
@@ -44,24 +39,26 @@ interface StorySettings {
|
||||
interface Props {
|
||||
onSubmit: OnSubmit<FormSubmitProps>;
|
||||
onChange?: (state: FormState<any>, form: FormApi) => void;
|
||||
initialValues?: FormProps;
|
||||
initialValues?: any;
|
||||
min: number | null;
|
||||
max: number | null;
|
||||
disabled?: boolean;
|
||||
disabledMessage?: React.ReactNode;
|
||||
submitStatus: PropTypesOf<PostCommentSubmitStatusContainer>["status"];
|
||||
showMessageBox?: boolean;
|
||||
siteID: string;
|
||||
story: PropTypesOf<typeof MessageBoxContainer>["story"] & StorySettings;
|
||||
rteConfig: PropTypesOf<typeof RTEContainer>["config"];
|
||||
rteConfig: PropTypesOf<typeof CommentForm>["rteConfig"];
|
||||
mediaConfig: PropTypesOf<typeof CommentForm>["mediaConfig"];
|
||||
}
|
||||
|
||||
const PostCommentForm: FunctionComponent<Props> = (props) => {
|
||||
const isQA =
|
||||
props.story.settings && props.story.settings.mode === GQLSTORY_MODE.QA;
|
||||
const emitFocusEvent = useViewerEvent(CreateCommentFocusEvent);
|
||||
const onFocus = useCallback(() => {
|
||||
emitFocusEvent();
|
||||
}, [emitFocusEvent]);
|
||||
const isQA =
|
||||
props.story.settings && props.story.settings.mode === GQLSTORY_MODE.QA;
|
||||
return (
|
||||
<div className={CLASSES.createComment.$root}>
|
||||
{props.showMessageBox && (
|
||||
@@ -70,125 +67,47 @@ const PostCommentForm: FunctionComponent<Props> = (props) => {
|
||||
className={cn(CLASSES.createComment.message, styles.messageBox)}
|
||||
/>
|
||||
)}
|
||||
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
|
||||
{({ handleSubmit, submitting, submitError, form }) => (
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={handleSubmit}
|
||||
id="comments-postCommentForm-form"
|
||||
>
|
||||
<FormSpy
|
||||
onChange={(state) =>
|
||||
props.onChange && props.onChange(state, form)
|
||||
}
|
||||
/>
|
||||
<HorizontalGutter>
|
||||
<Field
|
||||
name="body"
|
||||
validate={getCommentBodyValidators(props.min, props.max)}
|
||||
<CommentForm
|
||||
siteID={props.siteID}
|
||||
onSubmit={props.onSubmit}
|
||||
onChange={props.onChange}
|
||||
min={props.min}
|
||||
initialValues={props.initialValues}
|
||||
max={props.max}
|
||||
disabled={props.disabled}
|
||||
disabledMessage={props.disabledMessage}
|
||||
rteConfig={props.rteConfig}
|
||||
placeHolderId="comments-postCommentForm-rte"
|
||||
placeholder="Post a comment"
|
||||
mediaConfig={props.mediaConfig}
|
||||
onFocus={onFocus}
|
||||
classNameRoot="createComment"
|
||||
bodyInputID="comments-postCommentForm-field"
|
||||
bodyLabel={
|
||||
isQA ? (
|
||||
<Localized id="qa-postQuestionForm-rteLabel">
|
||||
<AriaInfo
|
||||
component="label"
|
||||
htmlFor="comments-postCommentForm-field"
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<>
|
||||
<HorizontalGutter size="half">
|
||||
{isQA ? (
|
||||
<Localized id="qa-postQuestionForm-rteLabel">
|
||||
<AriaInfo
|
||||
component="label"
|
||||
htmlFor="comments-postCommentForm-field"
|
||||
>
|
||||
Post a question
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="comments-postCommentForm-rteLabel">
|
||||
<AriaInfo
|
||||
component="label"
|
||||
htmlFor="comments-postCommentForm-field"
|
||||
>
|
||||
Post a comment
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
)}
|
||||
<Localized
|
||||
id={
|
||||
isQA
|
||||
? "qa-postQuestionForm-rte"
|
||||
: "comments-postCommentForm-rte"
|
||||
}
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<RTEContainer
|
||||
inputID="comments-postCommentForm-field"
|
||||
config={props.rteConfig}
|
||||
onFocus={onFocus}
|
||||
onChange={(html) => input.onChange(html)}
|
||||
contentClassName={
|
||||
props.showMessageBox
|
||||
? styles.rteBorderless
|
||||
: undefined
|
||||
}
|
||||
value={input.value}
|
||||
placeholder="Post a comment"
|
||||
disabled={submitting || props.disabled}
|
||||
/>
|
||||
</Localized>
|
||||
{props.disabled ? (
|
||||
<>
|
||||
{props.disabledMessage && (
|
||||
<ValidationMessage>
|
||||
{props.disabledMessage}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{meta.touched &&
|
||||
(meta.error ||
|
||||
(meta.submitError &&
|
||||
!meta.dirtySinceLastSubmit)) && (
|
||||
<ValidationMessage>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
{submitError && (
|
||||
<ValidationMessage>{submitError}</ValidationMessage>
|
||||
)}
|
||||
<PostCommentSubmitStatusContainer
|
||||
status={props.submitStatus}
|
||||
/>
|
||||
{props.max && (
|
||||
<RemainingCharactersContainer
|
||||
value={input.value}
|
||||
max={props.max}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
<Flex direction="column" alignItems="flex-end">
|
||||
<Localized id="comments-postCommentForm-submit">
|
||||
<Button
|
||||
color="stream"
|
||||
variant="regular"
|
||||
className={CLASSES.createComment.submit}
|
||||
disabled={
|
||||
submitting ||
|
||||
getHTMLCharacterLength(input.value) === 0 ||
|
||||
props.disabled
|
||||
}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
Post a question
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="comments-postCommentForm-rteLabel">
|
||||
<AriaInfo
|
||||
component="label"
|
||||
htmlFor="comments-postCommentForm-field"
|
||||
>
|
||||
Post a comment
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
)
|
||||
}
|
||||
submitStatus={
|
||||
<PostCommentSubmitStatusContainer status={props.submitStatus} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+10
-1
@@ -27,6 +27,9 @@ function createDefaultProps(add: DeepPartial<Props> = {}): Props {
|
||||
story: {
|
||||
id: "story-id",
|
||||
isClosed: false,
|
||||
site: {
|
||||
id: "site-id",
|
||||
},
|
||||
settings: {
|
||||
messageBox: {
|
||||
enabled: false,
|
||||
@@ -41,6 +44,11 @@ function createDefaultProps(add: DeepPartial<Props> = {}): Props {
|
||||
min: 3,
|
||||
max: 100,
|
||||
},
|
||||
media: {
|
||||
giphy: { enabled: false },
|
||||
twitter: { enabled: false },
|
||||
youtube: { enabled: false },
|
||||
},
|
||||
closeCommenting: {
|
||||
message: "closed",
|
||||
},
|
||||
@@ -156,10 +164,11 @@ it("creates a comment", async () => {
|
||||
await wait(() =>
|
||||
expect(
|
||||
createCommentStub.calledWith({
|
||||
...input,
|
||||
storyID,
|
||||
nudge: true,
|
||||
commentsOrderBy: "CREATED_AT_ASC",
|
||||
...input,
|
||||
media: undefined,
|
||||
})
|
||||
).toBeTruthy()
|
||||
);
|
||||
|
||||
+24
-5
@@ -35,6 +35,7 @@ import {
|
||||
} from "../../helpers";
|
||||
import RefreshSettingsFetch from "../../RefreshSettingsFetch";
|
||||
import { RTE_RESET_VALUE } from "../../RTE/RTE";
|
||||
import CommentForm from "../CommentForm";
|
||||
import {
|
||||
CreateCommentMutation,
|
||||
withCreateCommentMutation,
|
||||
@@ -60,7 +61,7 @@ interface Props {
|
||||
interface State {
|
||||
/** nudge will turn on the nudging behavior on the server */
|
||||
nudge: boolean;
|
||||
initialValues?: PropTypesOf<typeof PostCommentForm>["initialValues"];
|
||||
initialValues?: PropTypesOf<typeof CommentForm>["initialValues"];
|
||||
initialized: boolean;
|
||||
keepFormWhenClosed: boolean;
|
||||
submitStatus: SubmitStatus | null;
|
||||
@@ -118,9 +119,10 @@ export class PostCommentFormContainer extends Component<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
private handleOnSubmit: PropTypesOf<
|
||||
typeof PostCommentForm
|
||||
>["onSubmit"] = async (input, form) => {
|
||||
private handleOnSubmit: PropTypesOf<typeof CommentForm>["onSubmit"] = async (
|
||||
input,
|
||||
form
|
||||
) => {
|
||||
try {
|
||||
if (this.props.tab === "FEATURED_COMMENTS") {
|
||||
this.props.onChangeTab("ALL_COMMENTS");
|
||||
@@ -132,6 +134,7 @@ export class PostCommentFormContainer extends Component<Props, State> {
|
||||
nudge: this.state.nudge,
|
||||
commentsOrderBy: this.props.commentsOrderBy,
|
||||
body: input.body,
|
||||
media: input.media,
|
||||
})
|
||||
);
|
||||
if (submitStatus !== "RETRY") {
|
||||
@@ -160,7 +163,7 @@ export class PostCommentFormContainer extends Component<Props, State> {
|
||||
return;
|
||||
};
|
||||
|
||||
private handleOnChange: PropTypesOf<typeof PostCommentForm>["onChange"] = (
|
||||
private handleOnChange: PropTypesOf<typeof CommentForm>["onChange"] = (
|
||||
state,
|
||||
form
|
||||
) => {
|
||||
@@ -229,10 +232,12 @@ export class PostCommentFormContainer extends Component<Props, State> {
|
||||
|
||||
return (
|
||||
<PostCommentForm
|
||||
siteID={this.props.story.site.id}
|
||||
story={this.props.story}
|
||||
onSubmit={this.handleOnSubmit}
|
||||
onChange={this.handleOnChange}
|
||||
initialValues={this.state.initialValues}
|
||||
mediaConfig={this.props.settings.media}
|
||||
rteConfig={this.props.settings.rte}
|
||||
min={
|
||||
(this.props.settings.charCount.enabled &&
|
||||
@@ -288,6 +293,17 @@ const enhanced = withContext(({ sessionStorage }) => ({
|
||||
closeCommenting {
|
||||
message
|
||||
}
|
||||
media {
|
||||
twitter {
|
||||
enabled
|
||||
}
|
||||
youtube {
|
||||
enabled
|
||||
}
|
||||
giphy {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
rte {
|
||||
...RTEContainer_config
|
||||
}
|
||||
@@ -298,6 +314,9 @@ const enhanced = withContext(({ sessionStorage }) => ({
|
||||
id
|
||||
isClosed
|
||||
...MessageBoxContainer_story
|
||||
site {
|
||||
id
|
||||
}
|
||||
settings {
|
||||
messageBox {
|
||||
enabled
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
$commentsBorder: var(--v2-colors-grey-300);
|
||||
.root {
|
||||
}
|
||||
|
||||
.rteContainer {
|
||||
border: 1px solid $commentsBorder;
|
||||
border-radius: var(--v2-round-corners);
|
||||
}
|
||||
|
||||
.messageBox {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ const PostCommentFormFake: FunctionComponent<Props> = (props) => {
|
||||
/>
|
||||
)}
|
||||
<HorizontalGutter className={styles.root}>
|
||||
<div>
|
||||
<div className={styles.rteContainer}>
|
||||
<Localized
|
||||
id={
|
||||
isQA
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
$commentsRTEBorder: var(--v2-colors-grey-300);
|
||||
$commentsRTEContentBackground: var(--v2-colors-pure-white);
|
||||
|
||||
.root {
|
||||
border: 1px solid $commentsRTEBorder;
|
||||
border-radius: var(--v2-round-corners);
|
||||
}
|
||||
+91
@@ -10,6 +10,19 @@ exports[`renders correctly 1`] = `
|
||||
}
|
||||
}
|
||||
max={100}
|
||||
mediaConfig={
|
||||
Object {
|
||||
"giphy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"twitter": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"youtube": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
min={3}
|
||||
onChange={[Function]}
|
||||
onSubmit={[Function]}
|
||||
@@ -21,6 +34,7 @@ exports[`renders correctly 1`] = `
|
||||
}
|
||||
}
|
||||
showMessageBox={false}
|
||||
siteID="site-id"
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
@@ -31,6 +45,9 @@ exports[`renders correctly 1`] = `
|
||||
},
|
||||
"mode": "COMMENTS",
|
||||
},
|
||||
"site": Object {
|
||||
"id": "site-id",
|
||||
},
|
||||
}
|
||||
}
|
||||
submitStatus={null}
|
||||
@@ -51,6 +68,9 @@ exports[`renders when commenting has been disabled (collapsing) 1`] = `
|
||||
},
|
||||
"mode": "COMMENTS",
|
||||
},
|
||||
"site": Object {
|
||||
"id": "site-id",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -66,6 +86,19 @@ exports[`renders when commenting has been disabled (non-collapsing) 1`] = `
|
||||
}
|
||||
}
|
||||
max={100}
|
||||
mediaConfig={
|
||||
Object {
|
||||
"giphy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"twitter": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"youtube": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
min={3}
|
||||
onChange={[Function]}
|
||||
onSubmit={[Function]}
|
||||
@@ -77,6 +110,7 @@ exports[`renders when commenting has been disabled (non-collapsing) 1`] = `
|
||||
}
|
||||
}
|
||||
showMessageBox={false}
|
||||
siteID="site-id"
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
@@ -87,6 +121,9 @@ exports[`renders when commenting has been disabled (non-collapsing) 1`] = `
|
||||
},
|
||||
"mode": "COMMENTS",
|
||||
},
|
||||
"site": Object {
|
||||
"id": "site-id",
|
||||
},
|
||||
}
|
||||
}
|
||||
submitStatus={null}
|
||||
@@ -107,6 +144,9 @@ exports[`renders when story has been closed (collapsing) 1`] = `
|
||||
},
|
||||
"mode": "COMMENTS",
|
||||
},
|
||||
"site": Object {
|
||||
"id": "site-id",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -122,6 +162,19 @@ exports[`renders when story has been closed (non-collapsing) 1`] = `
|
||||
}
|
||||
}
|
||||
max={100}
|
||||
mediaConfig={
|
||||
Object {
|
||||
"giphy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"twitter": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"youtube": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
min={3}
|
||||
onChange={[Function]}
|
||||
onSubmit={[Function]}
|
||||
@@ -133,6 +186,7 @@ exports[`renders when story has been closed (non-collapsing) 1`] = `
|
||||
}
|
||||
}
|
||||
showMessageBox={false}
|
||||
siteID="site-id"
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
@@ -143,6 +197,9 @@ exports[`renders when story has been closed (non-collapsing) 1`] = `
|
||||
},
|
||||
"mode": "COMMENTS",
|
||||
},
|
||||
"site": Object {
|
||||
"id": "site-id",
|
||||
},
|
||||
}
|
||||
}
|
||||
submitStatus={null}
|
||||
@@ -165,6 +222,19 @@ exports[`renders when user is scheduled to be deleted 1`] = `
|
||||
}
|
||||
}
|
||||
max={100}
|
||||
mediaConfig={
|
||||
Object {
|
||||
"giphy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"twitter": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"youtube": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
min={3}
|
||||
onChange={[Function]}
|
||||
onSubmit={[Function]}
|
||||
@@ -176,6 +246,7 @@ exports[`renders when user is scheduled to be deleted 1`] = `
|
||||
}
|
||||
}
|
||||
showMessageBox={false}
|
||||
siteID="site-id"
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
@@ -186,6 +257,9 @@ exports[`renders when user is scheduled to be deleted 1`] = `
|
||||
},
|
||||
"mode": "COMMENTS",
|
||||
},
|
||||
"site": Object {
|
||||
"id": "site-id",
|
||||
},
|
||||
}
|
||||
}
|
||||
submitStatus={null}
|
||||
@@ -202,6 +276,19 @@ exports[`renders with initialValues 1`] = `
|
||||
}
|
||||
}
|
||||
max={100}
|
||||
mediaConfig={
|
||||
Object {
|
||||
"giphy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"twitter": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"youtube": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
min={3}
|
||||
onChange={[Function]}
|
||||
onSubmit={[Function]}
|
||||
@@ -213,6 +300,7 @@ exports[`renders with initialValues 1`] = `
|
||||
}
|
||||
}
|
||||
showMessageBox={false}
|
||||
siteID="site-id"
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
@@ -223,6 +311,9 @@ exports[`renders with initialValues 1`] = `
|
||||
},
|
||||
"mode": "COMMENTS",
|
||||
},
|
||||
"site": Object {
|
||||
"id": "site-id",
|
||||
},
|
||||
}
|
||||
}
|
||||
submitStatus={null}
|
||||
|
||||
+3
-1
@@ -11,7 +11,9 @@ exports[`renders correctly 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
className="PostCommentFormFake-root"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="PostCommentFormFake-rteContainer"
|
||||
>
|
||||
<Localized
|
||||
attrs={
|
||||
Object {
|
||||
|
||||
@@ -1,28 +1,44 @@
|
||||
import {
|
||||
composeValidators,
|
||||
Condition,
|
||||
required,
|
||||
validateMaxLength,
|
||||
validateMinLength,
|
||||
validateWhenOtherwise,
|
||||
Validator,
|
||||
} from "coral-framework/lib/validation";
|
||||
|
||||
import getHTMLCharacterLength from "./getHTMLCharacterLength";
|
||||
|
||||
const hasGIFAttached: Condition = (value, values) =>
|
||||
!!values.media && values.media.type === "giphy" && !!values.media.url;
|
||||
|
||||
function getLengthValidators(min: number | null, max: number | null) {
|
||||
const validators: Validator[] = [];
|
||||
if (min) {
|
||||
validators.push(required, validateMinLength(min, getHTMLCharacterLength));
|
||||
}
|
||||
|
||||
if (max) {
|
||||
validators.push(validateMaxLength(max, getHTMLCharacterLength));
|
||||
}
|
||||
|
||||
return composeValidators(...validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* getBodyValidators will return validators based on given min & max parameters.
|
||||
*
|
||||
* @param min minimum length or null
|
||||
* @param max maximum length or null
|
||||
*/
|
||||
export default function getBodyValidators(
|
||||
export default function getCommentBodyValdiators(
|
||||
min: number | null,
|
||||
max: number | null
|
||||
) {
|
||||
const validators = [required];
|
||||
if (min) {
|
||||
validators.push(validateMinLength(min, getHTMLCharacterLength));
|
||||
}
|
||||
if (max) {
|
||||
validators.push(validateMaxLength(max, getHTMLCharacterLength));
|
||||
}
|
||||
return composeValidators(...validators);
|
||||
return validateWhenOtherwise(
|
||||
hasGIFAttached,
|
||||
getLengthValidators(null, max),
|
||||
getLengthValidators(min || 1, max)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
margin-top: var(--v2-spacing-2);
|
||||
}
|
||||
|
||||
.content {}
|
||||
|
||||
.title {
|
||||
margin-bottom: var(--v2-spacing-2);
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const DownloadCommentsContainer: FunctionComponent<Props> = ({ viewer }) => {
|
||||
return (
|
||||
<div className={cn(styles.root, CLASSES.downloadCommentHistory.$root)}>
|
||||
<Flex justifyContent="space-between" alignItems="flex-start">
|
||||
<div className={styles.content}>
|
||||
<div>
|
||||
<Localized id="profile-account-download-comments-title">
|
||||
<div className={styles.title}>Download my comment history</div>
|
||||
</Localized>
|
||||
|
||||
@@ -20,6 +20,7 @@ it("renders correctly", () => {
|
||||
label: "reaction",
|
||||
icon: "icon",
|
||||
},
|
||||
media: null,
|
||||
story: {
|
||||
metadata: {
|
||||
title: "Story Title",
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface HistoryCommentProps {
|
||||
};
|
||||
conversationURL: string;
|
||||
onGotoConversation: (e: React.MouseEvent) => void;
|
||||
media: React.ReactNode;
|
||||
}
|
||||
|
||||
const HistoryComment: FunctionComponent<HistoryCommentProps> = (props) => {
|
||||
@@ -53,13 +54,16 @@ const HistoryComment: FunctionComponent<HistoryCommentProps> = (props) => {
|
||||
<InReplyTo username={props.parentAuthorName} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
{props.body && (
|
||||
<HTMLContent className={CLASSES.myComment.content}>
|
||||
{props.body}
|
||||
</HTMLContent>
|
||||
)}
|
||||
</div>
|
||||
<HorizontalGutter>
|
||||
<div className={styles.content}>
|
||||
{props.body && (
|
||||
<HTMLContent className={CLASSES.myComment.content}>
|
||||
{props.body}
|
||||
</HTMLContent>
|
||||
)}
|
||||
</div>
|
||||
{props.media}
|
||||
</HorizontalGutter>
|
||||
</div>
|
||||
<Flex
|
||||
direction="row"
|
||||
|
||||
@@ -14,6 +14,7 @@ import { HistoryCommentContainer_comment as CommentData } from "coral-stream/__g
|
||||
import { HistoryCommentContainer_settings as SettingsData } from "coral-stream/__generated__/HistoryCommentContainer_settings.graphql";
|
||||
import { HistoryCommentContainer_story as StoryData } from "coral-stream/__generated__/HistoryCommentContainer_story.graphql";
|
||||
|
||||
import MediaSectionContainer from "../../Comments/Comment/MediaSection";
|
||||
import HistoryComment from "./HistoryComment";
|
||||
|
||||
interface Props {
|
||||
@@ -54,6 +55,12 @@ const HistoryCommentContainer: FunctionComponent<Props> = (props) => {
|
||||
props.comment.id
|
||||
)}
|
||||
onGotoConversation={handleGotoConversation}
|
||||
media={
|
||||
<MediaSectionContainer
|
||||
comment={props.comment}
|
||||
settings={props.settings}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -71,6 +78,7 @@ const enhanced = withSetCommentIDMutation(
|
||||
label
|
||||
icon
|
||||
}
|
||||
...MediaSectionContainer_settings
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
@@ -79,6 +87,7 @@ const enhanced = withSetCommentIDMutation(
|
||||
body
|
||||
createdAt
|
||||
replyCount
|
||||
...MediaSectionContainer_comment
|
||||
parent {
|
||||
author {
|
||||
username
|
||||
|
||||
+10
-8
@@ -29,15 +29,17 @@ exports[`renders correctly 1`] = `
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</TimeStamp>
|
||||
<div
|
||||
className="HistoryComment-content"
|
||||
>
|
||||
<HTMLContent
|
||||
className="coral coral-myComment-content"
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
className="HistoryComment-content"
|
||||
>
|
||||
Hello World
|
||||
</HTMLContent>
|
||||
</div>
|
||||
<HTMLContent
|
||||
className="coral coral-myComment-content"
|
||||
>
|
||||
Hello World
|
||||
</HTMLContent>
|
||||
</div>
|
||||
</ForwardRef(forwardRef)>
|
||||
</div>
|
||||
<ForwardRef(forwardRef)
|
||||
alignItems="center"
|
||||
|
||||
+26
-16
@@ -114,7 +114,9 @@ exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="PostCommentFormFake-rteContainer"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar container"
|
||||
@@ -122,7 +124,7 @@ exports[`renders comment stream 1`] = `
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE.module-contentEditableContainer coral coral-rte-container"
|
||||
className="RTE.module-contentEditableContainer RTE-container coral coral-rte-container"
|
||||
>
|
||||
<div
|
||||
aria-placeholder="Post a comment"
|
||||
@@ -405,15 +407,19 @@ exports[`renders comment stream 1`] = `
|
||||
data-testid="featuredComment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="HTMLContent-root FeaturedCommentContainer-body coral coral-content coral-featuredComment-content"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Joining Too",
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="HTMLContent-root FeaturedCommentContainer-body coral coral-content coral-featuredComment-content"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Joining Too",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root coral coral-featuredComment-authorBar Flex-flex Flex-alignCenter Flex-directionRow Box-mt-4"
|
||||
className="Box-root Flex-root coral coral-featuredComment-authorBar Flex-flex Flex-alignCenter Flex-directionRow Box-mt-3"
|
||||
>
|
||||
<div
|
||||
className="Popover-root"
|
||||
@@ -541,15 +547,19 @@ exports[`renders comment stream 1`] = `
|
||||
data-testid="featuredComment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="HTMLContent-root FeaturedCommentContainer-body coral coral-content coral-featuredComment-content"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "What's up?",
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="HTMLContent-root FeaturedCommentContainer-body coral coral-content coral-featuredComment-content"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "What's up?",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root coral coral-featuredComment-authorBar Flex-flex Flex-alignCenter Flex-directionRow Box-mt-4"
|
||||
className="Box-root Flex-root coral coral-featuredComment-authorBar Flex-flex Flex-alignCenter Flex-directionRow Box-mt-3"
|
||||
>
|
||||
<div
|
||||
className="Popover-root"
|
||||
|
||||
+715
-639
File diff suppressed because it is too large
Load Diff
+154
-145
@@ -296,168 +296,177 @@ exports[`post a reply: open reply form 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
autoComplete="off"
|
||||
className="coral coral-createReplyComment"
|
||||
id="comments-replyCommentForm-form-comment-with-deepest-replies-3"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
className="coral coral-createReplyComment"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-half"
|
||||
<form
|
||||
autoComplete="off"
|
||||
id="comments-postCommentForm-form"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="AriaInfo-root"
|
||||
htmlFor="comments-replyCommentForm-rte-comment-with-deepest-replies-3"
|
||||
>
|
||||
Write a reply
|
||||
</label>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root ReplyTo-root coral coral-createReplyComment-replyTo Flex-flex Flex-alignCenter"
|
||||
className="CommentForm-commentFormBox"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
reply
|
||||
</i>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="ReplyTo-text coral coral-createReplyComment-replyToText"
|
||||
>
|
||||
Replying to
|
||||
<span
|
||||
className="ReplyTo-username coral coral-createReplyComment-replyToUsername"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar container"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-half"
|
||||
>
|
||||
<div
|
||||
className="RTE.module-contentEditableContainer coral coral-rte-container"
|
||||
<label
|
||||
className="AriaInfo-root"
|
||||
htmlFor="comments-replyCommentForm-rte-comment-with-deepest-replies-3"
|
||||
>
|
||||
<div
|
||||
aria-placeholder="Write a reply"
|
||||
className="RTE.module-contentEditable coral coral-rte-content ReplyCommentForm-rteContent RTE-content"
|
||||
id="comments-replyCommentForm-rte-comment-with-deepest-replies-3"
|
||||
/>
|
||||
<div
|
||||
Write a reply
|
||||
</label>
|
||||
<div
|
||||
className="Box-root Flex-root ReplyTo-root coral coral-createReplyComment-replyTo Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="RTE.module-placeholder coral coral-rte-placeholder RTE-placeholder"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
Write a reply
|
||||
</div>
|
||||
reply
|
||||
</i>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="ReplyTo-text coral coral-createReplyComment-replyToText"
|
||||
>
|
||||
Replying to
|
||||
<span
|
||||
className="ReplyTo-username coral coral-createReplyComment-replyToUsername"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="coral coral-rte-toolbar RTE-toolbar RTE.module-toolbarBottom Toolbar.module-toolbar"
|
||||
>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bold"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar container"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
format_bold
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Italic"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_italic
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Blockquote"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_quote
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bulleted List"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_list_bulleted
|
||||
</i>
|
||||
</button>
|
||||
<div
|
||||
className="RTE.module-contentEditableContainer RTE-container coral coral-rte-container"
|
||||
>
|
||||
<div
|
||||
aria-placeholder="Write a reply"
|
||||
className="RTE.module-contentEditable coral coral-rte-content RTE-content"
|
||||
id="comments-replyCommentForm-rte-comment-with-deepest-replies-3"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="RTE.module-placeholder coral coral-rte-placeholder RTE-placeholder"
|
||||
>
|
||||
Write a reply
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="coral coral-rte-toolbar RTE-toolbar RTE.module-toolbarBottom Toolbar.module-toolbar"
|
||||
>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bold"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_bold
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Italic"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_italic
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Blockquote"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_quote
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bulleted List"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_list_bulleted
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd gutter Flex-spacing-1"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantOutline Button-uppercase coral coral-createReplyComment-cancel"
|
||||
data-color="mono"
|
||||
data-variant="outline"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorStream Button-variantRegular Button-uppercase Button-disabled coral coral-createReplyComment-submit"
|
||||
data-color="stream"
|
||||
data-variant="regular"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-halfItemGutter Flex-justifyFlexEnd Flex-directionRow gutter"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantOutline Button-uppercase coral coral-createReplyComment-cancel"
|
||||
data-color="mono"
|
||||
data-variant="outline"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorStream Button-variantRegular Button-uppercase Button-disabled coral coral-createReplyComment-submit"
|
||||
data-color="stream"
|
||||
data-variant="regular"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
+154
-145
@@ -252,168 +252,177 @@ exports[`post a reply: open reply form 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
autoComplete="off"
|
||||
className="coral coral-createReplyComment"
|
||||
id="comments-replyCommentForm-form-comment-0"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
className="coral coral-createReplyComment"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-half"
|
||||
<form
|
||||
autoComplete="off"
|
||||
id="comments-postCommentForm-form"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="AriaInfo-root"
|
||||
htmlFor="comments-replyCommentForm-rte-comment-0"
|
||||
>
|
||||
Write a reply
|
||||
</label>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root ReplyTo-root coral coral-createReplyComment-replyTo Flex-flex Flex-alignCenter"
|
||||
className="CommentForm-commentFormBox"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
reply
|
||||
</i>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="ReplyTo-text coral coral-createReplyComment-replyToText"
|
||||
>
|
||||
Replying to
|
||||
<span
|
||||
className="ReplyTo-username coral coral-createReplyComment-replyToUsername"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar container"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-half"
|
||||
>
|
||||
<div
|
||||
className="RTE.module-contentEditableContainer coral coral-rte-container"
|
||||
<label
|
||||
className="AriaInfo-root"
|
||||
htmlFor="comments-replyCommentForm-rte-comment-0"
|
||||
>
|
||||
<div
|
||||
aria-placeholder="Write a reply"
|
||||
className="RTE.module-contentEditable coral coral-rte-content ReplyCommentForm-rteContent RTE-content"
|
||||
id="comments-replyCommentForm-rte-comment-0"
|
||||
/>
|
||||
<div
|
||||
Write a reply
|
||||
</label>
|
||||
<div
|
||||
className="Box-root Flex-root ReplyTo-root coral coral-createReplyComment-replyTo Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="RTE.module-placeholder coral coral-rte-placeholder RTE-placeholder"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
Write a reply
|
||||
</div>
|
||||
reply
|
||||
</i>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="ReplyTo-text coral coral-createReplyComment-replyToText"
|
||||
>
|
||||
Replying to
|
||||
<span
|
||||
className="ReplyTo-username coral coral-createReplyComment-replyToUsername"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="coral coral-rte-toolbar RTE-toolbar RTE.module-toolbarBottom Toolbar.module-toolbar"
|
||||
>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bold"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar container"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
format_bold
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Italic"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_italic
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Blockquote"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_quote
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bulleted List"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_list_bulleted
|
||||
</i>
|
||||
</button>
|
||||
<div
|
||||
className="RTE.module-contentEditableContainer RTE-container coral coral-rte-container"
|
||||
>
|
||||
<div
|
||||
aria-placeholder="Write a reply"
|
||||
className="RTE.module-contentEditable coral coral-rte-content RTE-content"
|
||||
id="comments-replyCommentForm-rte-comment-0"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="RTE.module-placeholder coral coral-rte-placeholder RTE-placeholder"
|
||||
>
|
||||
Write a reply
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="coral coral-rte-toolbar RTE-toolbar RTE.module-toolbarBottom Toolbar.module-toolbar"
|
||||
>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bold"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_bold
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Italic"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_italic
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Blockquote"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_quote
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bulleted List"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_list_bulleted
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd gutter Flex-spacing-1"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantOutline Button-uppercase coral coral-createReplyComment-cancel"
|
||||
data-color="mono"
|
||||
data-variant="outline"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorStream Button-variantRegular Button-uppercase Button-disabled coral coral-createReplyComment-submit"
|
||||
data-color="stream"
|
||||
data-variant="regular"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-halfItemGutter Flex-justifyFlexEnd Flex-directionRow gutter"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantOutline Button-uppercase coral coral-createReplyComment-cancel"
|
||||
data-color="mono"
|
||||
data-variant="outline"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorStream Button-variantRegular Button-uppercase Button-disabled coral coral-createReplyComment-submit"
|
||||
data-color="stream"
|
||||
data-variant="regular"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
+4
-2
@@ -76,7 +76,9 @@ exports[`renders comment stream with community guidelines 1`] = `
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="PostCommentFormFake-rteContainer"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar container"
|
||||
@@ -84,7 +86,7 @@ exports[`renders comment stream with community guidelines 1`] = `
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE.module-contentEditableContainer coral coral-rte-container"
|
||||
className="RTE.module-contentEditableContainer RTE-container coral coral-rte-container"
|
||||
>
|
||||
<div
|
||||
aria-placeholder="Post a comment"
|
||||
|
||||
+121
-109
@@ -330,127 +330,137 @@ exports[`renders message box when logged in 1`] = `
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
autoComplete="off"
|
||||
id="comments-postCommentForm-form"
|
||||
onSubmit={[Function]}
|
||||
<div
|
||||
className="coral coral-createComment"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
<form
|
||||
autoComplete="off"
|
||||
id="comments-postCommentForm-form"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-half"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<label
|
||||
className="AriaInfo-root"
|
||||
htmlFor="comments-postCommentForm-field"
|
||||
<div
|
||||
className="CommentForm-commentFormBox"
|
||||
>
|
||||
Post a comment
|
||||
</label>
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar container"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-half"
|
||||
>
|
||||
<div
|
||||
className="RTE.module-contentEditableContainer coral coral-rte-container"
|
||||
<label
|
||||
className="AriaInfo-root"
|
||||
htmlFor="comments-postCommentForm-field"
|
||||
>
|
||||
<div
|
||||
aria-placeholder="Post a comment"
|
||||
className="RTE.module-contentEditable coral coral-rte-content PostCommentForm-rteBorderless RTE-content"
|
||||
id="comments-postCommentForm-field"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="RTE.module-placeholder coral coral-rte-placeholder RTE-placeholder"
|
||||
>
|
||||
Post a comment
|
||||
Post a comment
|
||||
</label>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar container"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE.module-contentEditableContainer RTE-container coral coral-rte-container"
|
||||
>
|
||||
<div
|
||||
aria-placeholder="Post a comment"
|
||||
className="RTE.module-contentEditable coral coral-rte-content RTE-content"
|
||||
id="comments-postCommentForm-field"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="RTE.module-placeholder coral coral-rte-placeholder RTE-placeholder"
|
||||
>
|
||||
Post a comment
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="coral coral-rte-toolbar RTE-toolbar RTE.module-toolbarBottom Toolbar.module-toolbar"
|
||||
>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bold"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_bold
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Italic"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_italic
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Blockquote"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_quote
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bulleted List"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_list_bulleted
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="coral coral-rte-toolbar RTE-toolbar RTE.module-toolbarBottom Toolbar.module-toolbar"
|
||||
>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bold"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_bold
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Italic"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_italic
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Blockquote"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_quote
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
className="Button.module-button"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Bulleted List"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_list_bulleted
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignFlexEnd Flex-directionColumn"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorStream Button-variantRegular Button-uppercase Button-disabled coral coral-createComment-submit"
|
||||
data-color="stream"
|
||||
data-variant="regular"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd gutter Flex-spacing-1"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorStream Button-variantRegular Button-uppercase Button-disabled coral coral-createComment-submit"
|
||||
data-color="stream"
|
||||
data-variant="regular"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-4"
|
||||
@@ -666,7 +676,9 @@ exports[`renders message box when not logged in 1`] = `
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="PostCommentFormFake-rteContainer"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar container"
|
||||
@@ -674,7 +686,7 @@ exports[`renders message box when not logged in 1`] = `
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE.module-contentEditableContainer coral coral-rte-container"
|
||||
className="RTE.module-contentEditableContainer RTE-container coral coral-rte-container"
|
||||
>
|
||||
<div
|
||||
aria-placeholder="Post a comment"
|
||||
|
||||
+4
-2
@@ -114,7 +114,9 @@ exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="PostCommentFormFake-rteContainer"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar container"
|
||||
@@ -122,7 +124,7 @@ exports[`renders comment stream 1`] = `
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE.module-contentEditableContainer coral coral-rte-container"
|
||||
className="RTE.module-contentEditableContainer RTE-container coral coral-rte-container"
|
||||
>
|
||||
<div
|
||||
aria-placeholder="Post a comment"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user