- {replyRequestReason && (
- <>
-
-
handleVote(UPVOTE)}
- disabled={loading || ownVote === UPVOTE}
- >
- {positiveFeedbackCount}
-
-
-
-
-
{
- handleVote(DOWNVOTE);
- }}
- disabled={loading || ownVote === DOWNVOTE}
- >
- {negativeFeedbackCount}
-
-
-
-
-
-
-
{replyRequestReason}
- >
- )}
- {isArticleCreator &&
}
-
-
+
+
+ {replyRequestReason && (
+ <>
+
+
+
+
+ {replyRequestReason}
+
+
+ handleVote(UPVOTE)}
+ disabled={loading || ownVote === UPVOTE}
+ >
+
+ {positiveFeedbackCount}
+
+ handleVote(DOWNVOTE)}
+ disabled={loading || ownVote === DOWNVOTE}
+ >
+
+ {negativeFeedbackCount}
+
+
+ {/* isArticleCreator && (
+
+ ) */}
+
+
+ >
+ )}
+
+
);
}
diff --git a/components/ReplySearch/ReplySearch.js b/components/ReplySearch/ReplySearch.js
deleted file mode 100644
index a133dbf8..00000000
--- a/components/ReplySearch/ReplySearch.js
+++ /dev/null
@@ -1,172 +0,0 @@
-import { useState } from 'react';
-import { useLazyQuery } from '@apollo/react-hooks';
-import gql from 'graphql-tag';
-import { t } from 'ttag';
-
-import ButtonGroup from '@material-ui/core/ButtonGroup';
-import Button from '@material-ui/core/Button';
-import Badge from '@material-ui/core/Badge';
-import getDedupedArticleReplies from 'lib/getDedupedArticleReplies';
-import RelatedReplies from '../RelatedReplies';
-import SearchArticleItem from './SearchArticleItem.js';
-import { Typography } from '@material-ui/core';
-
-const SEARCH = gql`
- query SearchArticleAndReply($query: String!) {
- ListReplies(
- filter: { moreLikeThis: { like: $query, minimumShouldMatch: "0" } }
- orderBy: { _score: DESC }
- first: 25
- ) {
- edges {
- node {
- id
- articleReplies {
- ...RelatedArticleReplyData
- }
- }
- }
- }
- ListArticles(
- filter: {
- moreLikeThis: { like: $query, minimumShouldMatch: "0" }
- replyCount: { GT: 1 }
- }
- orderBy: { _score: DESC }
- first: 25
- ) {
- edges {
- node {
- ...SearchArticleData
- }
- }
- }
- }
- ${RelatedReplies.fragments.RelatedArticleReplyData}
- ${SearchArticleItem.fragments.SearchArticleData}
-`;
-
-const SearchArticles = ({ onConnect, searchArticles }) => {
- return (
-
- {searchArticles.map(({ node: article }) => {
- return (
-
- );
- })}
-
-
- );
-};
-
-/**
- * @param {function} props.onConnect - Connect reply handler. (replyId) => undefined
- * @param {boolean} props.disabled - Disable all connect buttons if true
- * @param {string[]} props.existingReplyIds
- */
-function ReplySearch({
- onConnect = () => {},
- disabled = false,
- existingReplyIds = [],
-}) {
- const [tab, setTab] = useState('reply'); // reply || article
- const [
- loadSearchResults,
- { loading, data, variables, called },
- ] = useLazyQuery(SEARCH);
-
- const handleSearch = e => {
- e.preventDefault();
- const query = e.target.elements.replySearch.value;
- loadSearchResults({ variables: { query } });
- };
-
- const articleReplies = getDedupedArticleReplies(
- data?.ListReplies,
- existingReplyIds
- );
-
- const articleCount = data?.ListArticles?.edges?.length || 0;
-
- let $result = null;
- if (loading) {
- $result = (
-
{t`Searching for messages and replies containing “${variables.query}”...`}
- );
- } else if (articleReplies.length > 0 || articleCount > 0) {
- $result = (
- <>
-
- setTab('reply')}>
-
- {t`Replies`}
-
-
- setTab('article')}
- >
-
- {t`Reported messages`}
-
-
-
- {tab === 'article' && (
-
- )}
-
- {tab === 'reply' && (
-
- )}
- >
- );
- } else if (called) {
- $result = (
-
{`- 找無${variables.query}相關的回覆與文章 -`}
- );
- }
-
- return (
- <>
-
-
- {$result}
-
-
- >
- );
-}
-
-export default ReplySearch;
diff --git a/components/ReplySearch/SearchArticleItem.js b/components/ReplySearch/SearchArticleItem.js
deleted file mode 100644
index 50ad4e2e..00000000
--- a/components/ReplySearch/SearchArticleItem.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import React, { PureComponent } from 'react';
-import Link from 'next/link';
-import { t } from 'ttag';
-import gql from 'graphql-tag';
-
-import DialogTitle from '@material-ui/core/DialogTitle';
-import Dialog from '@material-ui/core/Dialog';
-import ArticleReply from 'components/ArticleReply';
-
-import { format, formatDistanceToNow } from 'lib/dateWithLocale';
-import { nl2br, linkify } from 'lib/text';
-
-import ExpandableText from '../ExpandableText';
-import { sectionStyle } from '../ReplyConnection.styles';
-
-class SearchArticleItem extends PureComponent {
- static defaultProps = {
- disabled: false,
- article: null,
- onConnect() {},
- };
-
- state = {
- repliesModalOpen: false,
- };
-
- handleModalOpen = () => {
- this.setState({
- repliesModalOpen: true,
- });
- };
-
- handleModalClose = () => {
- this.setState({
- repliesModalOpen: false,
- });
- };
-
- handleOnConnect = articleReply => {
- this.props.onConnect(articleReply.replyId);
- this.handleModalClose();
- };
-
- render() {
- const { repliesModalOpen } = this.state;
- const { article, disabled } = this.props;
- const createdAt = new Date(article.createdAt);
-
- return (
-
-
- 查看{article.replyCount}則回覆
-
-
-
-
-
-
- {nl2br(linkify(article.text))}
-
-
- {t`Replies of the searched message`}
-
- {article.articleReplies.map(ar => (
-
- ))}
-
-
-
-
-
- );
- }
-}
-
-SearchArticleItem.fragments = {
- SearchArticleData: gql`
- fragment SearchArticleData on Article {
- id
- createdAt
- replyCount
- text
- articleReplies {
- ...ArticleReplyData
- }
- }
- ${ArticleReply.fragments.ArticleReplyData}
- `,
-};
-
-export default SearchArticleItem;
diff --git a/lib/useCurrentUser.js b/lib/useCurrentUser.js
index 36fb8f53..d726b97c 100644
--- a/lib/useCurrentUser.js
+++ b/lib/useCurrentUser.js
@@ -7,6 +7,7 @@ export const CurrentUser = gql`
fragment CurrentUser on User {
id
name
+ avatarUrl
}
`;
diff --git a/pages/article/[id].js b/pages/article/[id].js
index f78495e0..91d01d3e 100644
--- a/pages/article/[id].js
+++ b/pages/article/[id].js
@@ -1,6 +1,10 @@
import gql from 'graphql-tag';
-import { useEffect, useRef, useCallback } from 'react';
-import { t } from 'ttag';
+import Link from 'next/link';
+import { useEffect, useRef, useCallback, useState } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import { Box, Divider, Snackbar } from '@material-ui/core';
+import { ngettext, msgid, t } from 'ttag';
+
import { useRouter } from 'next/router';
import { useQuery, useLazyQuery } from '@apollo/react-hooks';
import Head from 'next/head';
@@ -10,22 +14,106 @@ import useCurrentUser from 'lib/useCurrentUser';
import { nl2br, linkify, ellipsis } from 'lib/text';
import { usePushToDataLayer } from 'lib/gtm';
+import { format, formatDistanceToNow } from 'lib/dateWithLocale';
+import isValid from 'date-fns/isValid';
+
import AppLayout from 'components/AppLayout';
import Hyperlinks from 'components/Hyperlinks';
-import ArticleInfo from 'components/ArticleInfo';
-import Trendline from 'components/Trendline';
import CurrentReplies from 'components/CurrentReplies';
import ReplyRequestReason from 'components/ReplyRequestReason';
-import CreateReplyRequestDialog from 'components/CreateReplyRequestDialog';
+import CreateReplyRequestForm from 'components/CreateReplyRequestForm';
import NewReplySection from 'components/NewReplySection';
import ArticleItem from 'components/ArticleItem';
+import ArticleInfo from 'components/ArticleInfo';
import ArticleCategories from 'components/ArticleCategories';
+import cx from 'clsx';
+import Trendline from 'components/Trendline';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ display: 'flex',
+ padding: '24px 0',
+ flexDirection: 'column',
+ [theme.breakpoints.up('md')]: {
+ flexDirection: 'row',
+ alignItems: 'baseline',
+ },
+ },
+ card: {
+ background: theme.palette.common.white,
+ borderRadius: 8,
+ },
+ main: {
+ flex: 1,
+ marginRight: 0,
+ [theme.breakpoints.up('md')]: {
+ flex: 3,
+ marginRight: 12,
+ },
+ },
+ aside: {
+ flex: 1,
+ background: 'transparent',
+ [theme.breakpoints.up('md')]: {
+ padding: '21px 19px',
+ background: theme.palette.common.white,
+ },
+ '& h4': {
+ [theme.breakpoints.up('md')]: {
+ paddingBottom: 10,
+ borderBottom: `1px solid ${theme.palette.secondary[500]}`,
+ },
+ },
+ },
+ newReplyContainer: {
+ position: 'fixed',
+ zIndex: theme.zIndex.modal,
+ height: '100%',
+ width: '100%',
+ top: 0,
+ left: 0,
+ background: theme.palette.common.white,
+ [theme.breakpoints.up('md')]: {
+ zIndex: 10,
+ position: 'relative',
+ padding: '28px 16px',
+ marginTop: 24,
+ borderRadius: 8,
+ },
+ },
+ similarMessageContainer: {
+ backgroundColor: theme.palette.common.white,
+ minWidth: '100%',
+ padding: '17px 19px',
+ marginRight: theme.spacing(2),
+ borderRadius: 8,
+ textDecoration: 'none',
+ color: 'inherit',
+ [theme.breakpoints.up('md')]: {
+ padding: '16px 0 0 0 ',
+ margin: 0,
+ width: 'auto',
+ borderBottom: `1px solid ${theme.palette.secondary[100]}`,
+ '&:last-child': {
+ borderBottom: 'none',
+ },
+ },
+ },
+ text: {
+ display: 'box',
+ overflow: 'hidden',
+ boxOrient: 'vertical',
+ textOverflow: 'ellipsis',
+ lineClamp: 5,
+ },
+}));
const LOAD_ARTICLE = gql`
query LoadArticlePage($id: String!) {
GetArticle(id: $id) {
id
text
+ requestedForReply
replyRequestCount
replyCount
createdAt
@@ -84,6 +172,8 @@ const LOAD_ARTICLE_FOR_USER = gql`
function ArticlePage() {
const { query } = useRouter();
+ const [showForm, setShowForm] = useState(false);
+ const [flashMessage, setFlashMessage] = useState(0);
const articleVars = { id: query.id };
const { data, loading } = useQuery(LOAD_ARTICLE, {
@@ -99,6 +189,7 @@ function ArticlePage() {
const currentUser = useCurrentUser();
const replySectionRef = useRef(null);
+ const classes = useStyles();
useEffect(() => {
if (!articleForUserCalled) {
@@ -111,8 +202,16 @@ function ArticlePage() {
const handleNewReplySubmit = useCallback(() => {
if (!replySectionRef.current) return;
replySectionRef.current.scrollIntoView({ behavior: 'smooth' });
+ setFlashMessage(t`Your reply has been submitted.`);
+ }, []);
+
+ const handleError = useCallback(error => {
+ console.error(error);
+ setFlashMessage(error.toString());
}, []);
+ const handleFormClose = () => setShowForm(false);
+
const article = data?.GetArticle;
usePushToDataLayer(!!article, { event: 'dataLoaded' });
@@ -139,6 +238,15 @@ function ArticlePage() {
);
}
+ const { replyRequestCount, text, hyperlinks } = article;
+ const similarArticles = article?.similarArticles?.edges || [];
+
+ const createdAt = article.createdAt
+ ? new Date(article.createdAt)
+ : new Date();
+
+ const timeAgoStr = formatDistanceToNow(createdAt);
+
return (
@@ -146,97 +254,131 @@ function ArticlePage() {
{ellipsis(article.text, { wordCount: 100 })} | {t`Cofacts`}
-
-
- {t`Reported Message`}
-
-
-
-
-
-
- {nl2br(
- linkify(article.text, {
- props: {
- target: '_blank',
- },
- })
- )}
-
-
-
- {article.replyRequests.map((replyRequest, idx) => (
-
+
+
+
+
+ {ngettext(
+ msgid`${replyRequestCount} person report this message`,
+ `${replyRequestCount} people report this message`,
+ replyRequestCount
+ )}
+
+ {isValid(createdAt) && (
+ {t`First reported ${timeAgoStr} ago`}
+ )}
+
+
+
+ {nl2br(
+ linkify(text, {
+ props: {
+ target: '_blank',
+ },
+ })
+ )}
+
+
+
- ))}
-
-
-
-
-
-
- {t`Replies to the message`}
-
-
-
-
- {t`Add a new reply`}
- replyId
+
+
+
+ {article.replyRequests.map((replyRequest, idx) => (
+
+ ))}
+ {
+ setShowForm(true);
+ }}
+ />
+
+
+
+ {showForm && (
+
+ replyId
+ )}
+ relatedArticles={article?.relatedArticles}
+ onSubmissionComplete={handleNewReplySubmit}
+ onError={handleError}
+ onClose={handleFormClose}
+ />
+
)}
- relatedArticles={article?.relatedArticles}
- onSubmissionComplete={handleNewReplySubmit}
- />
-
-
- {article?.similarArticles?.edges?.length > 0 && (
-
- {t`You may be interested in the following similar messages`}
-
- {article.similarArticles.edges.map(({ node }) => (
-
- ))}
-
-
- )}
-
-
+
+
+ {t`${article.articleReplies.length} replies to the message`}
+
+
+
+
+
+
+
{t`Similar messages`}
+ {similarArticles.length ? (
+
+ {similarArticles.map(({ node }) => (
+
+
+ {node.text}
+
+
+
+
+
+ ))}
+
+ ) : (
+
{t`No similar messages found`}
+ )}
+
+
+