mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:17:19 +08:00
Merge branch 'master' into docker-plugins
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
|
||||
A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
Talk is currently in Beta! [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
|
||||
@@ -0,0 +1,622 @@
|
||||
$minHeight: 200px;
|
||||
$fullscreenZIndex: 10;
|
||||
|
||||
.wrapper {
|
||||
/* Fix blockquote styling of MDL: https://github.com/google/material-design-lite/issues/2037 */
|
||||
blockquote {
|
||||
> p:first-child {
|
||||
padding-top: 16px;
|
||||
}
|
||||
> p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&::after {
|
||||
margin-left: -0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.iconBold {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'format_bold';
|
||||
}
|
||||
}
|
||||
|
||||
.iconItalic {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'format_italic';
|
||||
}
|
||||
}
|
||||
|
||||
.iconTitle {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'title';
|
||||
}
|
||||
}
|
||||
|
||||
.iconQuote {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'format_quote';
|
||||
}
|
||||
}
|
||||
|
||||
.iconUnorderedList {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'format_list_bulleted';
|
||||
}
|
||||
}
|
||||
|
||||
.iconOrderedList {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'format_list_numbered';
|
||||
}
|
||||
}
|
||||
|
||||
.iconLink {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'link';
|
||||
}
|
||||
}
|
||||
|
||||
.iconImage {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'insert_photo';
|
||||
}
|
||||
}
|
||||
|
||||
.iconPreview {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'remove_red_eye';
|
||||
}
|
||||
}
|
||||
|
||||
.iconSideBySide {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'chrome_reader_mode';
|
||||
}
|
||||
}
|
||||
|
||||
.iconFullscreen {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'fullscreen';
|
||||
}
|
||||
}
|
||||
|
||||
.iconGuide {
|
||||
composes: material-icons from global;
|
||||
&::before {
|
||||
content: 'help_outline';
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* These are modified styles taken from https://github.com/NextStepWebs/simplemde-markdown-editor and
|
||||
* put through http://sebastianpontow.de/css2compass/.
|
||||
*/
|
||||
|
||||
/*
|
||||
* simplemde v1.11.2
|
||||
* Copyright Next Step Webs, Inc.
|
||||
* @link https://github.com/NextStepWebs/simplemde-markdown-editor
|
||||
* @license MIT
|
||||
*/
|
||||
:global {
|
||||
.CodeMirror {
|
||||
font-family: monospace;
|
||||
color: black;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
height: auto;
|
||||
min-height: $minHeight;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
padding: 10px;
|
||||
font: inherit;
|
||||
z-index: 1;
|
||||
pre {
|
||||
padding: 0 4px;
|
||||
border-radius: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
span {
|
||||
/*TODO: vertical-align: text-bottom;*/
|
||||
}
|
||||
.CodeMirror-code {
|
||||
.cm-tag {
|
||||
color: #63a35c;
|
||||
}
|
||||
.cm-attribute {
|
||||
color: #795da3;
|
||||
}
|
||||
.cm-string {
|
||||
color: #183691;
|
||||
}
|
||||
.cm-header-1 {
|
||||
font-size: 200%;
|
||||
line-height: 200%;
|
||||
}
|
||||
.cm-header-2 {
|
||||
font-size: 160%;
|
||||
line-height: 160%;
|
||||
}
|
||||
.cm-header-3 {
|
||||
font-size: 125%;
|
||||
line-height: 125%;
|
||||
}
|
||||
.cm-header-4 {
|
||||
font-size: 110%;
|
||||
line-height: 110%;
|
||||
}
|
||||
.cm-comment {
|
||||
background: rgba(0, 0, 0, .05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.cm-link {
|
||||
color: #7f8c8d;
|
||||
}
|
||||
.cm-url {
|
||||
color: #aab2b3;
|
||||
}
|
||||
.cm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.cm-tab {
|
||||
display: inline-block;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
.CodeMirror-ruler {
|
||||
border-left: 1px solid #ccc;
|
||||
position: absolute;
|
||||
}
|
||||
.cm-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.cm-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.cm-em {
|
||||
font-style: italic;
|
||||
}
|
||||
.cm-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.cm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.cm-invalidchar {
|
||||
color: #f00;
|
||||
}
|
||||
}
|
||||
.CodeMirror-selected {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
.CodeMirror-placeholder {
|
||||
opacity: .5;
|
||||
}
|
||||
div.CodeMirror-secondarycursor {
|
||||
border-left: 1px solid silver;
|
||||
}
|
||||
.cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word) {
|
||||
background: rgba(255, 0, 0, .15);
|
||||
}
|
||||
}
|
||||
.CodeMirror-lines {
|
||||
padding: 4px 0;
|
||||
cursor: text;
|
||||
min-height: 1px;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.CodeMirror-gutter-filler {
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #f7f7f7;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-height: 100%;
|
||||
z-index: 3;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.CodeMirror-guttermarker {
|
||||
color: black;
|
||||
}
|
||||
.CodeMirror-guttermarker-subtle {
|
||||
color: #999;
|
||||
}
|
||||
.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.CodeMirror-cursor {
|
||||
border-left: 1px solid black;
|
||||
border-right: none;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
}
|
||||
@keyframes blink {
|
||||
0% {
|
||||
}
|
||||
50% {
|
||||
background-color: transparent;
|
||||
}
|
||||
100% {
|
||||
}
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
overflow: scroll !important;
|
||||
margin-bottom: -30px;
|
||||
margin-right: -30px;
|
||||
padding-bottom: 30px;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
position: relative;
|
||||
min-height: $minHeight;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.CodeMirror-sizer {
|
||||
position: relative;
|
||||
border-right: 30px solid transparent;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
right: 0;
|
||||
top: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.CodeMirror-gutter {
|
||||
white-space: normal;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: -30px;
|
||||
*zoom: 1;
|
||||
*display: inline;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
user-select: none;
|
||||
}
|
||||
.CodeMirror-gutter-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-elt {
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-code {
|
||||
outline: none;
|
||||
}
|
||||
.CodeMirror-measure {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
pre {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
.CodeMirror-focused {
|
||||
.CodeMirror-selected {
|
||||
background: #d7d4f0;
|
||||
}
|
||||
div.CodeMirror-cursors {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.CodeMirror-selected {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
.CodeMirror-line::selection {
|
||||
background: #d7d4f0;
|
||||
}
|
||||
.CodeMirror-line {
|
||||
> span::selection {
|
||||
background: #d7d4f0;
|
||||
}
|
||||
> span {
|
||||
> span::selection {
|
||||
background: #d7d4f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
.CodeMirror div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
.CodeMirror-fullscreen {
|
||||
background: #fff;
|
||||
position: fixed !important;
|
||||
top: 50px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: auto;
|
||||
z-index: $fullscreenZIndex;
|
||||
}
|
||||
.CodeMirror-sided {
|
||||
width: 50% !important;
|
||||
}
|
||||
.editor-toolbar {
|
||||
position: relative;
|
||||
opacity: .6;
|
||||
user-select: none;
|
||||
padding: 0 10px;
|
||||
border-top: 1px solid #bbb;
|
||||
border-left: 1px solid #bbb;
|
||||
border-right: 1px solid #bbb;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
&:after {
|
||||
display: block;
|
||||
content: ' ';
|
||||
height: 1px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
&:before {
|
||||
display: block;
|
||||
content: ' ';
|
||||
height: 1px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
}
|
||||
&.fullscreen {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
z-index: $fullscreenZIndex;
|
||||
}
|
||||
&.fullscreen::before {
|
||||
width: 20px;
|
||||
height: 50px;
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 1) 0, rgba(255, 255, 255, 0) 100%);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
&.fullscreen::after {
|
||||
width: 20px;
|
||||
height: 50px;
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none!important;
|
||||
color: #2c3e50!important;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
margin-right: 2px;
|
||||
&.active {
|
||||
background: #fcfcfc;
|
||||
border-color: #95a5a6;
|
||||
}
|
||||
&:hover {
|
||||
background: #fcfcfc;
|
||||
border-color: #95a5a6;
|
||||
}
|
||||
&:active {
|
||||
background: #eee;
|
||||
}
|
||||
&:before {
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
i.separator {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
border-right: 1px solid #fff;
|
||||
color: transparent;
|
||||
text-indent: -10px;
|
||||
margin: 0 6px;
|
||||
}
|
||||
&.disabled-for-preview a:not(.no-disable) {
|
||||
pointer-events: none;
|
||||
background: #fff;
|
||||
border-color: transparent;
|
||||
text-shadow: inherit;
|
||||
}
|
||||
}
|
||||
@media only screen and(max-width: 700px) {
|
||||
.editor-toolbar a.no-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.editor-statusbar {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
color: #959694;
|
||||
text-align: right;
|
||||
span {
|
||||
display: inline-block;
|
||||
min-width: 4em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
.lines:before {
|
||||
content: 'lines: ';
|
||||
}
|
||||
.words:before {
|
||||
content: 'words: ';
|
||||
}
|
||||
.characters:before {
|
||||
content: 'characters: ';
|
||||
}
|
||||
}
|
||||
.editor-preview {
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: #fafafa;
|
||||
z-index: 7;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
> p {
|
||||
margin-top: 0;
|
||||
}
|
||||
pre {
|
||||
background: #eee;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
table {
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.editor-preview-side {
|
||||
padding: 10px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
top: 50px;
|
||||
right: 0;
|
||||
background: #fafafa;
|
||||
z-index: $fullscreenZIndex;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ddd;
|
||||
> p {
|
||||
margin-top: 0;
|
||||
}
|
||||
pre {
|
||||
background: #eee;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
table {
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.editor-preview-active-side {
|
||||
display: block;
|
||||
}
|
||||
.editor-preview-active {
|
||||
display: block;
|
||||
}
|
||||
.CodeMirror-overwrite .CodeMirror-cursor {
|
||||
}
|
||||
.CodeMirror-wrap pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
}
|
||||
.cm-tab-wrap-hack:after {
|
||||
content: '';
|
||||
}
|
||||
span.CodeMirror-selectedtext {
|
||||
background: none;
|
||||
}
|
||||
.editor-wrapper input.title {
|
||||
&:focus {
|
||||
opacity: .8;
|
||||
}
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import SimpleMDE from 'simplemde';
|
||||
import cn from 'classnames';
|
||||
import noop from 'lodash/noop';
|
||||
import styles from './MarkdownEditor.css';
|
||||
|
||||
const config = {
|
||||
status: false,
|
||||
|
||||
// Do not download fontAwesome icons as we replace them with
|
||||
// material icons.
|
||||
autoDownloadFontAwesome: false,
|
||||
|
||||
// Disable built-in spell checker as it is very rudimentary.
|
||||
spellChecker: false,
|
||||
|
||||
toolbar: [
|
||||
{
|
||||
name: 'bold',
|
||||
action: SimpleMDE.toggleBold,
|
||||
className: styles.iconBold,
|
||||
title: 'Bold',
|
||||
},
|
||||
{
|
||||
name: 'italic',
|
||||
action: SimpleMDE.toggleItalic,
|
||||
className: styles.iconItalic,
|
||||
title: 'Italic',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
action: SimpleMDE.toggleHeadingSmaller,
|
||||
className: styles.iconTitle,
|
||||
title: 'Title, Subtitle, Heading',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'quote',
|
||||
action: SimpleMDE.toggleBlockquote,
|
||||
className: styles.iconQuote,
|
||||
title: 'Quote',
|
||||
},
|
||||
{
|
||||
name: 'unordered-list',
|
||||
action: SimpleMDE.toggleUnorderedList,
|
||||
className: styles.iconUnorderedList,
|
||||
title: 'Generic List',
|
||||
},
|
||||
{
|
||||
name: 'ordered-list',
|
||||
action: SimpleMDE.toggleOrderedList,
|
||||
className: styles.iconOrderedList,
|
||||
title: 'Numbered List',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'link',
|
||||
action: SimpleMDE.drawLink,
|
||||
className: styles.iconLink,
|
||||
title: 'Create Link',
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
action: SimpleMDE.drawImage,
|
||||
className: styles.iconImage,
|
||||
title: 'Insert Image',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'preview',
|
||||
action: SimpleMDE.togglePreview,
|
||||
className: cn(styles.iconPreview, 'no-disable'),
|
||||
title: 'Toggle Preview',
|
||||
},
|
||||
{
|
||||
name: 'side-by-side',
|
||||
action: SimpleMDE.toggleSideBySide,
|
||||
className: cn(styles.iconSideBySide, 'no-disable'),
|
||||
title: 'Toggle Side by Side',
|
||||
},
|
||||
{
|
||||
name: 'fullscreen',
|
||||
action: SimpleMDE.toggleFullScreen,
|
||||
className: cn(styles.iconFullscreen, 'no-disable'),
|
||||
title: 'Toggle Fullscreen',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'guide',
|
||||
action: 'https://simplemde.com/markdown-guide',
|
||||
className: styles.iconGuide,
|
||||
title: 'Markdown Guide',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default class MarkdownEditor extends Component {
|
||||
|
||||
textarea = null
|
||||
editor = null
|
||||
|
||||
onRef = (ref) => this.textarea = ref
|
||||
|
||||
componentDidMount() {
|
||||
this.editor = new SimpleMDE({
|
||||
...config,
|
||||
element: this.textarea,
|
||||
});
|
||||
this.editor.codemirror.on('change', this.onChange);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.value !== nextProps.value && nextProps.value !== this.editor.value()) {
|
||||
this.editor.value(nextProps.value);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
||||
// Workaround empty render issue.
|
||||
// https://github.com/NextStepWebs/simplemde-markdown-editor/issues/313
|
||||
this.editor.codemirror.refresh();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.editor.toTextArea();
|
||||
}
|
||||
|
||||
onChange = () => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this.editor.value());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<textarea ref={this.onRef} {...this.props} onChange={noop} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownEditor.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
@@ -73,7 +73,6 @@
|
||||
.configSettingInfoBox {
|
||||
min-height: 100px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
height: auto;
|
||||
text-align: left;
|
||||
@@ -180,7 +179,9 @@
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
padding-left: 30px;
|
||||
padding: 0px 30px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,12 +193,11 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 550px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.descriptionBox {
|
||||
margin-top: 15px;
|
||||
max-width: 550px;
|
||||
|
||||
input {
|
||||
height: 150px;
|
||||
|
||||
@@ -5,6 +5,7 @@ import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {Checkbox, Textfield} from 'react-mdl';
|
||||
import {Card, Icon, TextArea} from 'coral-ui';
|
||||
import MarkdownEditor from 'coral-admin/src/components/MarkdownEditor';
|
||||
|
||||
const TIMESTAMPS = {
|
||||
weeks: 60 * 60 * 24 * 7,
|
||||
@@ -32,8 +33,8 @@ const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
|
||||
updateSettings({infoBoxEnable});
|
||||
};
|
||||
|
||||
const updateInfoBoxContent = (updateSettings) => (event) => {
|
||||
const infoBoxContent = event.target.value;
|
||||
const updateInfoBoxContent = (updateSettings) => (value) => {
|
||||
const infoBoxContent = value;
|
||||
updateSettings({infoBoxContent});
|
||||
};
|
||||
|
||||
@@ -112,19 +113,17 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
|
||||
{lang.t('configure.include-comment-stream-desc')}
|
||||
</p>
|
||||
<div className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<div>
|
||||
<TextArea
|
||||
className={styles.descriptionBox}
|
||||
onChange={updateInfoBoxContent(updateSettings)}
|
||||
value={settings.infoBoxContent}
|
||||
/>
|
||||
</div>
|
||||
<MarkdownEditor
|
||||
className={styles.descriptionBox}
|
||||
onChange={updateInfoBoxContent(updateSettings)}
|
||||
value={settings.infoBoxContent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox}`}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.closed-stream-settings')}</div>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.closed-stream-settings')}</div>
|
||||
<p>{lang.t('configure.closed-comments-desc')}</p>
|
||||
<div>
|
||||
<TextArea className={styles.descriptionBox}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
.Comment {
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pendingComment {
|
||||
|
||||
@@ -117,7 +117,6 @@ class Comment extends React.Component {
|
||||
const flag = getActionSummary('FlagActionSummary', comment);
|
||||
const dontagree = getActionSummary('DontAgreeActionSummary', comment);
|
||||
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
|
||||
commentClass += highlighted === comment.id ? ' highlighted-comment' : '';
|
||||
commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : '';
|
||||
|
||||
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
|
||||
@@ -147,18 +146,19 @@ class Comment extends React.Component {
|
||||
id={`c_${comment.id}`}
|
||||
style={{marginLeft: depth * 30}}>
|
||||
<hr aria-hidden={true} />
|
||||
<AuthorName
|
||||
author={comment.user}/>
|
||||
{ isStaff(comment.tags)
|
||||
? <TagLabel>Staff</TagLabel>
|
||||
<div className={highlighted === comment.id ? 'highlighted-comment' : ''}>
|
||||
<AuthorName
|
||||
author={comment.user}/>
|
||||
{ isStaff(comment.tags)
|
||||
? <TagLabel>Staff</TagLabel>
|
||||
: null }
|
||||
|
||||
{ commentIsBest(comment)
|
||||
? <TagLabel><BestIndicator /></TagLabel>
|
||||
{ commentIsBest(comment)
|
||||
? <TagLabel><BestIndicator /></TagLabel>
|
||||
: null }
|
||||
<PubDate created_at={comment.created_at} />
|
||||
<PubDate created_at={comment.created_at} />
|
||||
|
||||
<Content body={comment.body} />
|
||||
<Content body={comment.body} />
|
||||
<div className="commentActionsLeft comment__action-container">
|
||||
<ActionButton>
|
||||
<LikeButton
|
||||
@@ -188,21 +188,22 @@ class Comment extends React.Component {
|
||||
</IfUserCanModifyBest>
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="commentActionsRight comment__action-container">
|
||||
<ActionButton>
|
||||
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<FlagComment
|
||||
flag={flag && flag.current_user ? flag : dontagree}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
</ActionButton>
|
||||
<div className="commentActionsRight comment__action-container">
|
||||
<ActionButton>
|
||||
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<FlagComment
|
||||
flag={flag && flag.current_user ? flag : dontagree}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
activeReplyBox === comment.id
|
||||
|
||||
@@ -6,16 +6,17 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-framework/translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
import {TabBar, Tab, TabContent, Spinner} from 'coral-ui';
|
||||
import {TabBar, Tab, TabContent, Spinner, Button} from 'coral-ui';
|
||||
|
||||
const {logout, showSignInDialog, requestConfirmEmail} = authActions;
|
||||
const {addNotification, clearNotification} = notificationActions;
|
||||
const {fetchAssetSuccess} = assetActions;
|
||||
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
|
||||
|
||||
import {queryStream} from 'coral-framework/graphql/queries';
|
||||
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations';
|
||||
import {editName} from 'coral-framework/actions/user';
|
||||
import {updateCountCache} from 'coral-framework/actions/asset';
|
||||
import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset';
|
||||
import {notificationActions, authActions, assetActions, pym} from 'coral-framework';
|
||||
|
||||
import Stream from './Stream';
|
||||
@@ -31,7 +32,7 @@ import ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUserna
|
||||
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
|
||||
import RestrictedContent from 'coral-framework/components/RestrictedContent';
|
||||
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
|
||||
import Comment from './Comment';
|
||||
import HighlightedComment from './Comment';
|
||||
import LoadMore from './LoadMore';
|
||||
import NewCount from './NewCount';
|
||||
|
||||
@@ -68,10 +69,30 @@ class Embed extends Component {
|
||||
pym.sendMessage('childReady');
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.state.countPoll);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const {loadAsset} = this.props;
|
||||
if(!isEqual(nextProps.data.asset, this.props.data.asset)) {
|
||||
loadAsset(nextProps.data.asset);
|
||||
|
||||
const {getCounts, updateCountCache} = this.props;
|
||||
const {asset} = nextProps.data;
|
||||
|
||||
updateCountCache(asset.id, asset.commentCount);
|
||||
|
||||
this.setState({
|
||||
countPoll: setInterval(() => {
|
||||
const {asset} = this.props.data;
|
||||
getCounts({
|
||||
asset_id: asset.id,
|
||||
limit: asset.comments.length,
|
||||
sort: 'REVERSE_CHRONOLOGICAL'
|
||||
});
|
||||
}, NEW_COMMENT_COUNT_POLL_INTERVAL)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +100,7 @@ class Embed extends Component {
|
||||
if(!isEqual(prevProps.data.comment, this.props.data.comment)) {
|
||||
|
||||
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
|
||||
setTimeout(()=>pym.scrollParentToChildEl(`c_${this.props.data.comment.id}`), 0);
|
||||
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +118,8 @@ class Embed extends Component {
|
||||
const {closedAt, countCache = {}} = this.props.asset;
|
||||
const {loading, asset, refetch, comment} = this.props.data;
|
||||
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
|
||||
|
||||
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
|
||||
const highlightedComment = comment && comment.parent ? comment.parent : comment;
|
||||
|
||||
const openStream = closedAt === null;
|
||||
@@ -124,6 +147,16 @@ class Embed extends Component {
|
||||
<Tab>{lang.t('MY_COMMENTS')}</Tab>
|
||||
<Tab restricted={!isAdmin}>Configure Stream</Tab>
|
||||
</TabBar>
|
||||
{
|
||||
highlightedComment &&
|
||||
<Button
|
||||
cStyle='darkGrey'
|
||||
style={{float: 'right'}}
|
||||
onClick={() => {
|
||||
this.props.viewAllComments();
|
||||
this.props.data.refetch();
|
||||
}}>{lang.t('showAllComments')}</Button>
|
||||
}
|
||||
{loggedIn && <UserBox user={user} logout={() => this.props.logout().then(refetch)} changeTab={this.changeTab}/>}
|
||||
<TabContent show={activeTab === 0}>
|
||||
{
|
||||
@@ -170,9 +203,11 @@ class Embed extends Component {
|
||||
offset={signInOffset}/>}
|
||||
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
|
||||
{loggedIn && <ModerationLink assetId={asset.id} isAdmin={isAdmin} />}
|
||||
|
||||
{/* the highlightedComment is isolated after the user followed a permalink */}
|
||||
{
|
||||
highlightedComment &&
|
||||
<Comment
|
||||
highlightedComment
|
||||
? <HighlightedComment
|
||||
refetch={refetch}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.state.activeReplyBox}
|
||||
@@ -191,42 +226,42 @@ class Embed extends Component {
|
||||
key={highlightedComment.id}
|
||||
reactKey={highlightedComment.id}
|
||||
comment={highlightedComment} />
|
||||
: <div>
|
||||
<NewCount
|
||||
commentCount={asset.commentCount}
|
||||
countCache={countCache[asset.id]}
|
||||
loadMore={this.props.loadMore}
|
||||
firstCommentDate={firstCommentDate}
|
||||
assetId={asset.id}
|
||||
updateCountCache={this.props.updateCountCache}
|
||||
/>
|
||||
<div className="embed__stream">
|
||||
<Stream
|
||||
open={openStream}
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.state.activeReplyBox}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postLike={this.props.postLike}
|
||||
postFlag={this.props.postFlag}
|
||||
postDontAgree={this.props.postDontAgree}
|
||||
addCommentTag={this.props.addCommentTag}
|
||||
removeCommentTag={this.props.removeCommentTag}
|
||||
loadMore={this.props.loadMore}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
comments={asset.comments} />
|
||||
</div>
|
||||
<LoadMore
|
||||
topLevel={true}
|
||||
assetId={asset.id}
|
||||
comments={asset.comments}
|
||||
moreComments={countCache[asset.id] > asset.comments.length}
|
||||
loadMore={this.props.loadMore} />
|
||||
</div>
|
||||
}
|
||||
<NewCount
|
||||
commentCount={asset.commentCount}
|
||||
countCache={countCache[asset.id]}
|
||||
loadMore={this.props.loadMore}
|
||||
firstCommentDate={firstCommentDate}
|
||||
assetId={asset.id}
|
||||
updateCountCache={this.props.updateCountCache}
|
||||
/>
|
||||
<div className="embed__stream">
|
||||
<Stream
|
||||
open={openStream}
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.state.activeReplyBox}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postLike={this.props.postLike}
|
||||
postFlag={this.props.postFlag}
|
||||
postDontAgree={this.props.postDontAgree}
|
||||
getCounts={this.props.getCounts}
|
||||
addCommentTag={this.props.addCommentTag}
|
||||
removeCommentTag={this.props.removeCommentTag}
|
||||
updateCountCache={this.props.updateCountCache}
|
||||
loadMore={this.props.loadMore}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
comments={asset.comments} />
|
||||
</div>
|
||||
<LoadMore
|
||||
topLevel={true}
|
||||
assetId={asset.id}
|
||||
comments={asset.comments}
|
||||
moreComments={countCache[asset.id] > asset.comments.length}
|
||||
loadMore={this.props.loadMore}/>
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 1}>
|
||||
<ProfileContainer
|
||||
@@ -263,6 +298,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
editName: (username) => dispatch(editName(username)),
|
||||
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
|
||||
updateCountCache: (id, count) => dispatch(updateCountCache(id, count)),
|
||||
viewAllComments: () => dispatch(viewAllComments()),
|
||||
logout: () => dispatch(logout()),
|
||||
dispatch: d => dispatch(d)
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {ADDTL_COMMENTS_ON_LOAD_MORE} from 'coral-framework/constants/comments';
|
||||
import {Button} from 'coral-ui';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const loadMoreComments = (assetId, comments, loadMore, parentId) => {
|
||||
const loadMoreComments = (assetId, comments, loadMore, parentId, replyCount) => {
|
||||
|
||||
let cursor = null;
|
||||
if (comments.length) {
|
||||
@@ -15,7 +15,7 @@ const loadMoreComments = (assetId, comments, loadMore, parentId) => {
|
||||
}
|
||||
|
||||
loadMore({
|
||||
limit: ADDTL_COMMENTS_ON_LOAD_MORE,
|
||||
limit: parentId ? replyCount : ADDTL_COMMENTS_ON_LOAD_MORE,
|
||||
cursor,
|
||||
asset_id: assetId,
|
||||
parent_id: parentId,
|
||||
@@ -48,7 +48,7 @@ class LoadMore extends React.Component {
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.initialState = false;
|
||||
loadMoreComments(assetId, comments, loadMore, parentId);
|
||||
loadMoreComments(assetId, comments, loadMore, parentId, replyCount);
|
||||
}}>
|
||||
{topLevel ? lang.t('viewMoreComments') : this.replyCountFormat(replyCount)}
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Comment from './Comment';
|
||||
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
|
||||
|
||||
class Stream extends React.Component {
|
||||
|
||||
@@ -27,26 +26,6 @@ class Stream extends React.Component {
|
||||
this.state = {activeReplyBox: '', countPoll: null};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {asset, getCounts, updateCountCache} = this.props;
|
||||
|
||||
updateCountCache(asset.id, asset.commentCount);
|
||||
|
||||
// Note: Apollo's built-in polling doesn't work with fetchMore queries, so a
|
||||
// setInterval is being used instead.
|
||||
this.setState({
|
||||
countPoll: setInterval(() => getCounts({
|
||||
asset_id: asset.id,
|
||||
limit: asset.comments.length,
|
||||
sort: 'REVERSE_CHRONOLOGICAL'
|
||||
}), NEW_COMMENT_COUNT_POLL_INTERVAL),
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.state.countPoll);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
comments,
|
||||
|
||||
@@ -16,6 +16,7 @@ body {
|
||||
font-size: 14px;
|
||||
margin: 0px;
|
||||
padding: 0px 0px 50px 0px;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.expandForSignin {
|
||||
@@ -306,10 +307,7 @@ button.comment__action-button[disabled],
|
||||
display: none;
|
||||
background-color: white;
|
||||
border: 1px solid black;
|
||||
width: calc(100% - 15px);
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,15 @@ function configurePymParent(pymParent, asset_url) {
|
||||
snackbar.style.opacity = 0;
|
||||
});
|
||||
|
||||
// remove the permalink comment id from the hash
|
||||
pymParent.onMessage('coral-view-all-comments', function () {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
location.origin + location.pathname + location.search
|
||||
);
|
||||
});
|
||||
|
||||
pymParent.onMessage('coral-alert', function (message) {
|
||||
const [type, text] = message.split('|');
|
||||
snackbar.style.transform = 'translate(-50%, 20px)';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as actions from '../constants/asset';
|
||||
import coralApi from '../helpers/response';
|
||||
import {addNotification} from '../actions/notification';
|
||||
import {pym} from 'coral-framework';
|
||||
|
||||
import I18n from '../../coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
@@ -49,3 +50,37 @@ export const updateOpenStatus = status => dispatch => {
|
||||
dispatch(updateOpenStream({closedAt: new Date().getTime()}));
|
||||
}
|
||||
};
|
||||
|
||||
function removeParam(key, sourceURL) {
|
||||
let rtn = sourceURL.split('?')[0];
|
||||
let param;
|
||||
let params_arr = [];
|
||||
let queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : '';
|
||||
if (queryString !== '') {
|
||||
params_arr = queryString.split('&');
|
||||
for (let i = params_arr.length - 1; i >= 0; i -= 1) {
|
||||
param = params_arr[i].split('=')[0];
|
||||
if (param === key) {
|
||||
params_arr.splice(i, 1);
|
||||
}
|
||||
}
|
||||
rtn = `${rtn}?${params_arr.join('&')}`;
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
|
||||
export const viewAllComments = () => {
|
||||
|
||||
// remove the comment_id url param
|
||||
const modifiedUrl = removeParam('comment_id', location.href);
|
||||
try {
|
||||
|
||||
// "window" here refers to the embedded iframe
|
||||
window.history.replaceState({}, document.title, modifiedUrl);
|
||||
|
||||
// also change the parent url
|
||||
pym.sendMessage('coral-view-all-comments');
|
||||
} catch (e) { /* not sure if we're worried about old browsers */ }
|
||||
|
||||
return {type: actions.VIEW_ALL_COMMENTS};
|
||||
};
|
||||
|
||||
@@ -9,3 +9,5 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
|
||||
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
|
||||
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
|
||||
export const UPDATE_COUNT_CACHE = 'UPDATE_COUNT_CACHE';
|
||||
|
||||
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
|
||||
|
||||
@@ -5,6 +5,7 @@ import GET_COUNTS from './getCounts.graphql';
|
||||
import MY_COMMENT_HISTORY from './myCommentHistory.graphql';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import isNil from 'lodash/isNil';
|
||||
|
||||
function getQueryVariable(variable) {
|
||||
let query = window.location.search.substring(1);
|
||||
@@ -20,6 +21,7 @@ function getQueryVariable(variable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get the counts of the top-level comments
|
||||
export const getCounts = (data) => ({asset_id, limit, sort}) => {
|
||||
return data.fetchMore({
|
||||
query: GET_COUNTS,
|
||||
@@ -41,23 +43,48 @@ export const getCounts = (data) => ({asset_id, limit, sort}) => {
|
||||
});
|
||||
};
|
||||
|
||||
// handle paginated requests for more Comments pertaining to the Asset
|
||||
export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => {
|
||||
return data.fetchMore({
|
||||
query: LOAD_MORE,
|
||||
variables: {
|
||||
limit,
|
||||
cursor,
|
||||
parent_id,
|
||||
asset_id,
|
||||
sort
|
||||
limit, // how many comments are we returning
|
||||
cursor, // the date of the first/last comment depending on the sort order
|
||||
parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment
|
||||
asset_id, // the id of the asset we're currently on
|
||||
sort // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL
|
||||
},
|
||||
updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) => {
|
||||
|
||||
let updatedAsset;
|
||||
|
||||
if (parent_id) {
|
||||
if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment
|
||||
|
||||
let comment = {};
|
||||
if (oldData.comment && oldData.comment.parent) {
|
||||
|
||||
// put comments (replies) onto the oldData.comment.parent object
|
||||
// the initial comment permalinked was a reply
|
||||
const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.parent.replies], 'id');
|
||||
comment.parent = {...oldData.comment.parent, replies: sortBy(uniqReplies, 'created_at')};
|
||||
} else if (oldData.comment) {
|
||||
|
||||
// put the comments (replies) directly onto oldData.comment
|
||||
// the initial comment permalinked was a top-level comment
|
||||
const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.replies], 'id');
|
||||
comment.replies = sortBy(uniqReplies, 'created_at');
|
||||
}
|
||||
|
||||
updatedAsset = {
|
||||
...oldData,
|
||||
comment: {
|
||||
...oldData.comment,
|
||||
...comment
|
||||
}
|
||||
};
|
||||
|
||||
} else if (parent_id) { // If loading more replies
|
||||
|
||||
// If loading more replies
|
||||
updatedAsset = {
|
||||
...oldData,
|
||||
asset: {
|
||||
@@ -76,9 +103,8 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s
|
||||
})
|
||||
}
|
||||
};
|
||||
} else {
|
||||
} else { // If loading more top-level comments
|
||||
|
||||
// If loading more top-level comments
|
||||
updatedAsset = {
|
||||
...oldData,
|
||||
asset: {
|
||||
@@ -94,8 +120,11 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s
|
||||
});
|
||||
};
|
||||
|
||||
// load the comment stream.
|
||||
export const queryStream = graphql(STREAM_QUERY, {
|
||||
options: () => {
|
||||
|
||||
// where the query string is from the embeded iframe url
|
||||
let comment_id = getQueryVariable('comment_id');
|
||||
let has_comment = comment_id != null;
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comment: Boolean!) {
|
||||
# the comment here is for loading one comment and it's children, probably after following a permalink
|
||||
# $has_comment is derived from the comment_id query param in the iframe url,
|
||||
# which is in turn pulled from the host page url
|
||||
comment(id: $comment_id) @include(if: $has_comment) {
|
||||
...commentView
|
||||
replyCount
|
||||
replies {
|
||||
...commentView
|
||||
}
|
||||
parent {
|
||||
...commentView
|
||||
replyCount
|
||||
replies {
|
||||
...commentView
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"error": "Usernames can contain letters, numbers and _ only"
|
||||
},
|
||||
"viewMoreComments": "view more comments",
|
||||
"showAllComments": "Show all comments",
|
||||
"viewReply": "view reply",
|
||||
"viewAllRepliesInitial": "view all {0} replies",
|
||||
"viewAllReplies": "view {0} replies",
|
||||
@@ -56,6 +57,7 @@
|
||||
"newCount": "Ver {0} {1} más",
|
||||
"comment": "commentario",
|
||||
"comments": "commentarios",
|
||||
"showAllComments": "Mostrar todos los comentarios",
|
||||
"error": {
|
||||
"emailNotVerified": "E-mail {0} no verificado.",
|
||||
"email": "No es un e-mail válido",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import Markdown from './Markdown';
|
||||
|
||||
const packagename = 'coral-plugin-infobox';
|
||||
|
||||
const InfoBox = ({enable, content}) =>
|
||||
<div
|
||||
className={`${packagename}-info ${enable ? null : ', hidden'}` }>
|
||||
{content}
|
||||
className={`${packagename}-info ${enable ? '' : 'hidden'}` }>
|
||||
<Markdown content={content} />
|
||||
</div>;
|
||||
|
||||
export default InfoBox;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, {PureComponent, PropTypes} from 'react';
|
||||
import marked from 'marked';
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
// Set link target to `_parent` to work properly in an embed.
|
||||
renderer.link = ( href, title, text ) =>
|
||||
`<a target="_parent" href="${href}" title="${title}">${text}</a>`;
|
||||
|
||||
marked.setOptions({renderer});
|
||||
|
||||
export default class Markdown extends PureComponent {
|
||||
render() {
|
||||
const {content, ...rest} = this.props;
|
||||
const __html = marked(content);
|
||||
return <div {...rest} dangerouslySetInnerHTML={{__html}} />;
|
||||
}
|
||||
}
|
||||
|
||||
Markdown.propTypes = {
|
||||
content: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -3,24 +3,27 @@ import {shallow} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import InfoBox from '../InfoBox';
|
||||
|
||||
const render = props => shallow(<InfoBox {...props} />);
|
||||
|
||||
describe('InfoBox', () => {
|
||||
let comment;
|
||||
let render;
|
||||
beforeEach(() => {
|
||||
comment = {};
|
||||
const postItem = (item) => {
|
||||
comment.posted = item;
|
||||
return Promise.resolve(4);
|
||||
};
|
||||
render = shallow(<InfoBox
|
||||
postItem={postItem}
|
||||
updateItem={(e) => comment.text = e.target.value}
|
||||
item_id={'1'}
|
||||
comments={['1', '2', '3']}/>);
|
||||
it('should render hidden InfoBox', () => {
|
||||
const wrapper = render();
|
||||
const className = wrapper.prop('className');
|
||||
expect(className).to.include('-info');
|
||||
expect(className).to.include('hidden');
|
||||
});
|
||||
|
||||
it('should render the InfoBox appropriately', () => {
|
||||
expect(render.contains('<div class="InfoBox"')).to.be.truthy;
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.truthy;
|
||||
it('should render enabled InfoBox', () => {
|
||||
const wrapper = render({enable: true});
|
||||
const className = wrapper.prop('className');
|
||||
expect(className).to.include('-info');
|
||||
expect(className).to.not.include('hidden');
|
||||
});
|
||||
|
||||
it('should render Markdown', () => {
|
||||
const wrapper = render({content: 'x'});
|
||||
const Markddown = wrapper.find('Markdown');
|
||||
expect(Markddown).to.have.length(1);
|
||||
expect(Markddown.prop('content')).to.equal('x');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import Markdown from '../Markdown';
|
||||
|
||||
const render = props => shallow(<Markdown {...props} />);
|
||||
|
||||
describe('Markdown', () => {
|
||||
it('should convert Markdown to html', () => {
|
||||
const wrapper = render({content: '*test*'});
|
||||
const html = wrapper.html();
|
||||
expect(html).to.contain('<em>');
|
||||
});
|
||||
|
||||
it('should set target="_parent" for links', () => {
|
||||
const wrapper = render({content: '[link](https://coralproject.net)'});
|
||||
const html = wrapper.html();
|
||||
expect(html).to.contain('target="_parent"');
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,10 @@ class PermalinkButton extends React.Component {
|
||||
}
|
||||
|
||||
toggle () {
|
||||
|
||||
// I wish I could position this with a stylesheet, but top-level comments with
|
||||
// nested replies throws everything off, as well as very long comments
|
||||
this.popover.style.top = `${this.linkButton.offsetTop - 80}px`;
|
||||
this.setState({popoverOpen: !this.state.popoverOpen});
|
||||
}
|
||||
|
||||
@@ -48,11 +52,16 @@ class PermalinkButton extends React.Component {
|
||||
const {copySuccessful, copyFailure} = this.state;
|
||||
return (
|
||||
<div className={`${name}-container`}>
|
||||
<button onClick={this.toggle} className={`${name}-button`}>
|
||||
<button
|
||||
ref={ref => this.linkButton = ref}
|
||||
onClick={this.toggle}
|
||||
className={`${name}-button`}>
|
||||
{lang.t('permalink.permalink')}
|
||||
<i className={`${name}-icon material-icons`} aria-hidden={true}>link</i>
|
||||
</button>
|
||||
<div className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}>
|
||||
<div
|
||||
ref={ref => this.popover = ref}
|
||||
className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}>
|
||||
<input
|
||||
className={`${name}-copy-field`}
|
||||
type='text'
|
||||
|
||||
@@ -4,22 +4,15 @@
|
||||
box-sizing: border-box;
|
||||
/* box-shadow: 3px 3px 5px 0 rgba(0, 0, 0, 0.3); */
|
||||
border: solid 1px rgba(153, 153, 153, 0.33);
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
left: 0;
|
||||
margin: 0 5px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
margin-top: -13px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: white;
|
||||
position: absolute;
|
||||
right: 8.69em;
|
||||
right: 7em;
|
||||
bottom: -20px;
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -29,7 +22,7 @@
|
||||
border: 10px solid transparent;
|
||||
border-top-color: rgba(153, 153, 153, 0.33);
|
||||
position: absolute;
|
||||
right: 8.69em;
|
||||
right: 7em;
|
||||
bottom: -21px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
min-height: 200px;
|
||||
overflow: hidden;
|
||||
width: 330px;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
|
||||
+5
-3
@@ -76,6 +76,7 @@
|
||||
"kue": "^0.11.5",
|
||||
"linkify-it": "^2.0.3",
|
||||
"lodash": "^4.16.6",
|
||||
"marked": "^0.3.6",
|
||||
"metascraper": "^1.0.6",
|
||||
"minimist": "^1.2.0",
|
||||
"mongoose": "^4.9.1",
|
||||
@@ -92,6 +93,7 @@
|
||||
"react-recaptcha": "^2.2.6",
|
||||
"redis": "^2.7.1",
|
||||
"resolve": "^1.3.2",
|
||||
"simplemde": "^1.11.2",
|
||||
"uuid": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -152,9 +154,9 @@
|
||||
"pre-git": "^3.10.0",
|
||||
"precss": "^1.4.0",
|
||||
"pym.js": "^1.1.1",
|
||||
"react": "15.3.2",
|
||||
"react-addons-test-utils": "15.3.2",
|
||||
"react-dom": "15.3.2",
|
||||
"react": "^15.4.2",
|
||||
"react-addons-test-utils": "^15.4.2",
|
||||
"react-dom": "^15.4.2",
|
||||
"react-highlight-words": "^0.6.0",
|
||||
"react-linkify": "^0.1.3",
|
||||
"react-mdl": "^1.7.2",
|
||||
|
||||
+35
-10
@@ -17,16 +17,41 @@ if (smtpRequiredProps.some(prop => !process.env[prop])) {
|
||||
}
|
||||
|
||||
// load all the templates as strings
|
||||
const templateStrings = {};
|
||||
fs.readdir(path.join(__dirname, 'email'), (err, files) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
const templates = {
|
||||
data: {}
|
||||
};
|
||||
|
||||
// load the temlates per request during development
|
||||
templates.render = (name, format = 'txt', context) => new Promise((resolve, reject) => {
|
||||
|
||||
// If we are in production mode, check the view cache.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (name in templates.data && format in templates.data[name]) {
|
||||
let view = templates.data[name][format];
|
||||
|
||||
return resolve(view(context));
|
||||
}
|
||||
}
|
||||
|
||||
files.forEach(file => {
|
||||
fs.readFile(path.join(__dirname, 'email', file), 'utf8', (err, data) => {
|
||||
templateStrings[file] = _.template(data);
|
||||
});
|
||||
const filename = path.join(__dirname, 'email', [name, format, 'ejs'].join('.'));
|
||||
|
||||
fs.readFile(filename, (err, file) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let view = _.template(file);
|
||||
|
||||
// If we are in production mode, fill the view cache.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (!(name in templates.data)) {
|
||||
templates.data[name] = {};
|
||||
}
|
||||
|
||||
templates.data[name][format] = view;
|
||||
}
|
||||
|
||||
return resolve(view(context));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,10 +95,10 @@ const mailer = module.exports = {
|
||||
return Promise.all([
|
||||
|
||||
// Render the HTML version of the email.
|
||||
templateStrings[`${template}.ejs`](locals),
|
||||
templates.render(template, 'html', locals),
|
||||
|
||||
// Render the TEXT version of the email.
|
||||
templateStrings[`${template}.txt.ejs`](locals)
|
||||
templates.render(template, 'txt', locals)
|
||||
])
|
||||
.then(([html, text]) => {
|
||||
|
||||
|
||||
@@ -1595,6 +1595,16 @@ code-point-at@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
|
||||
codemirror-spell-checker@*:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz#1c660f9089483ccb5113b9ba9ca19c3f4993371e"
|
||||
dependencies:
|
||||
typo-js "*"
|
||||
|
||||
codemirror@*:
|
||||
version "5.25.0"
|
||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.25.0.tgz#78e06939c7bb41f65707b8aa9c5328111948b756"
|
||||
|
||||
color-convert@^1.3.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
|
||||
@@ -2842,7 +2852,7 @@ fastparse@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
|
||||
|
||||
fbjs@^0.8.4:
|
||||
fbjs@^0.8.1, fbjs@^0.8.4:
|
||||
version "0.8.8"
|
||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.8.tgz#02f1b6e0ea0d46c24e0b51a2d24df069563a5ad6"
|
||||
dependencies:
|
||||
@@ -4797,7 +4807,7 @@ map-stream@~0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
|
||||
|
||||
marked@^0.3.5:
|
||||
marked@*, marked@^0.3.5, marked@^0.3.6:
|
||||
version "0.3.6"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7"
|
||||
|
||||
@@ -6480,9 +6490,12 @@ rc@^1.0.1, rc@~1.1.6:
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~1.0.4"
|
||||
|
||||
react-addons-test-utils@15.3.2:
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.3.2.tgz#c09a44f583425a4a9c1b38444d7a6c3e6f0f41f6"
|
||||
react-addons-test-utils@^15.4.2:
|
||||
version "15.4.2"
|
||||
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.4.2.tgz#93bcaa718fcae7360d42e8fb1c09756cc36302a2"
|
||||
dependencies:
|
||||
fbjs "^0.8.4"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react-apollo@^0.10.0:
|
||||
version "0.10.1"
|
||||
@@ -6499,10 +6512,18 @@ react-apollo@^0.10.0:
|
||||
optionalDependencies:
|
||||
react-dom "0.14.x || 15.* || ^15.0.0"
|
||||
|
||||
"react-dom@0.14.x || 15.* || ^15.0.0", react-dom@15.3.2, react-dom@^15.3.1:
|
||||
"react-dom@0.14.x || 15.* || ^15.0.0", react-dom@^15.3.1:
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f"
|
||||
|
||||
react-dom@^15.4.2:
|
||||
version "15.4.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.4.2.tgz#015363f05b0a1fd52ae9efdd3a0060d90695208f"
|
||||
dependencies:
|
||||
fbjs "^0.8.1"
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react-highlight-words@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.6.0.tgz#e12e9fedda4333e410ea408cdedffc77122020aa"
|
||||
@@ -6565,7 +6586,7 @@ react-tagsinput@^3.14.0:
|
||||
version "3.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.14.0.tgz#6729f8d5b313ed8fbb35496205ec2a97654af6bc"
|
||||
|
||||
react@15.3.2, react@^15.3.1:
|
||||
react@^15.3.1:
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-15.3.2.tgz#a7bccd2fee8af126b0317e222c28d1d54528d09e"
|
||||
dependencies:
|
||||
@@ -6573,6 +6594,14 @@ react@15.3.2, react@^15.3.1:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react@^15.4.2:
|
||||
version "15.4.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef"
|
||||
dependencies:
|
||||
fbjs "^0.8.4"
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
read-all-stream@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa"
|
||||
@@ -7132,6 +7161,14 @@ simple-is@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/simple-is/-/simple-is-0.2.0.tgz#2abb75aade39deb5cc815ce10e6191164850baf0"
|
||||
|
||||
simplemde@^1.11.2:
|
||||
version "1.11.2"
|
||||
resolved "https://registry.yarnpkg.com/simplemde/-/simplemde-1.11.2.tgz#a23a35d978d2c40ef07dec008c92f070d8e080e3"
|
||||
dependencies:
|
||||
codemirror "*"
|
||||
codemirror-spell-checker "*"
|
||||
marked "*"
|
||||
|
||||
slash@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
|
||||
@@ -7717,6 +7754,10 @@ typedarray@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
|
||||
typo-js@*:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.0.3.tgz#54d8ebc7949f1a7810908b6002c6841526c99d5a"
|
||||
|
||||
ua-parser-js@^0.7.9:
|
||||
version "0.7.12"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb"
|
||||
|
||||
Reference in New Issue
Block a user