diff --git a/.dockerignore b/.dockerignore index 70197281..65b1caa8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,3 +14,4 @@ !public/* !pages/* !typegen/* +!tsconfig.json diff --git a/components/ListPageControls/ArticleStatusFilter.js b/components/ListPageControls/ArticleStatusFilter.js deleted file mode 100644 index e90148a8..00000000 --- a/components/ListPageControls/ArticleStatusFilter.js +++ /dev/null @@ -1,63 +0,0 @@ -import { memo } from 'react'; -import { useRouter } from 'next/router'; -import { t } from 'ttag'; -import BaseFilter from './BaseFilter'; -import useCurrentUser from 'lib/useCurrentUser'; -import { goToUrlQueryAndResetPagination } from 'lib/listPage'; - -import * as FILTERS from 'constants/articleFilters'; - -/** - * URL param name to read from and write to - */ -const PARAM_NAME = 'filters'; - -const OPTIONS = [ - { value: FILTERS.REPLIED_BY_ME, label: t`Replied by me` }, - { value: FILTERS.NO_USEFUL_REPLY_YET, label: t`No useful reply yet` }, - { value: FILTERS.ASKED_MANY_TIMES, label: t`Asked many times` }, - { value: FILTERS.REPLIED_MANY_TIMES, label: t`Replied many times` }, -]; - -const LOGIN_ONLY_OPTIONS = [FILTERS.REPLIED_BY_ME]; - -/** - * @param {object} query - query from router - * @returns {Arary} list of selected filter values; see constants/articleFilters for all possible values - */ -function getValues(query) { - return query[PARAM_NAME] ? query[PARAM_NAME].split(',') : []; -} - -function ArticleStatusFilter() { - const { query } = useRouter(); - const user = useCurrentUser(); - const selectedValues = getValues(query); - - // Disable login-only options when not logged in - const options = user - ? OPTIONS - : OPTIONS.map(option => ({ - ...option, - disabled: LOGIN_ONLY_OPTIONS.includes(option.value), - })); - - return ( - - goToUrlQueryAndResetPagination({ - ...query, - [PARAM_NAME]: values.join(','), - }) - } - data-ga="Filter(filter)" - /> - ); -} - -const MemoizedArticleStatusFilter = memo(ArticleStatusFilter); -MemoizedArticleStatusFilter.getValues = getValues; -export default MemoizedArticleStatusFilter; diff --git a/components/ListPageControls/ArticleStatusFilter.tsx b/components/ListPageControls/ArticleStatusFilter.tsx new file mode 100644 index 00000000..86dd451f --- /dev/null +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -0,0 +1,164 @@ +import { memo } from 'react'; +import { useRouter } from 'next/router'; +import { t } from 'ttag'; +import { ParsedUrlQuery } from 'querystring'; + +import { ListArticleFilter } from 'typegen/graphql'; +import BaseFilter from './BaseFilter'; +import useCurrentUser from 'lib/useCurrentUser'; +import { goToUrlQueryAndResetPagination } from 'lib/listPage'; + +import * as FILTERS from 'constants/articleFilters'; + +/** + * URL param name to read from and write to + */ +const PARAM_NAME = 'filters'; + +const OPTIONS = [ + { value: FILTERS.ASKED_ONCE, label: t`Asked only once` }, + { value: FILTERS.ASKED_MANY_TIMES, label: t`Asked many times` }, + { value: FILTERS.NO_REPLY, label: t`Zero replies` }, + { value: FILTERS.REPLIED_MANY_TIMES, label: t`Replied many times` }, + { value: FILTERS.NO_USEFUL_REPLY_YET, label: t`No useful reply yet` }, + { value: FILTERS.HAS_USEFUL_REPLY, label: t`Has useful replies` }, + { value: FILTERS.REPLIED_BY_ME, label: t`Replied by me` }, + { value: FILTERS.NOT_REPLIED_BY_ME, label: t`Not replied by me` }, +]; + +/** Filters that is only enabled when user is logged in */ +const LOGIN_ONLY_OPTIONS = [FILTERS.REPLIED_BY_ME, FILTERS.NOT_REPLIED_BY_ME]; + +const MUTUALLY_EXCLUSIVE_FILTERS: ReadonlyArray< + ReadonlyArray +> = [ + // Sets of filters that are mutually exclusive (cannot be selected together) + [FILTERS.ASKED_ONCE, FILTERS.ASKED_MANY_TIMES], + [FILTERS.NO_REPLY, FILTERS.REPLIED_MANY_TIMES], + [FILTERS.NO_USEFUL_REPLY_YET, FILTERS.HAS_USEFUL_REPLY], + [FILTERS.REPLIED_BY_ME, FILTERS.NOT_REPLIED_BY_ME], +]; + +/** + * @param query - query from router + * @returns list of selected filter values; see constants/articleFilters for all possible values + */ +export function getValues(query: ParsedUrlQuery): Array { + return query[PARAM_NAME] + ? query[PARAM_NAME].toString() + .split(',') + .filter((param): param is keyof typeof FILTERS => param in FILTERS) + : []; +} + +/** + * @param query - query from router + * @param userId - currently logged in user ID. Can be undefined if not logged in. + * @returns a ListArticleFilter with filter fields from router query + */ +export function getFilter( + query: ParsedUrlQuery, + userId?: string +): ListArticleFilter { + const filterObj: ListArticleFilter = {}; + + for (const filter of getValues(query)) { + // Skip login only filters when user is not logged in + if (!userId && LOGIN_ONLY_OPTIONS.includes(filter)) break; + + switch (filter) { + case FILTERS.REPLIED_BY_ME: + filterObj.articleRepliesFrom = { + userId: userId, + exists: true, + }; + break; + case FILTERS.NOT_REPLIED_BY_ME: + filterObj.articleRepliesFrom = { + userId: userId, + exists: false, + }; + break; + case FILTERS.NO_USEFUL_REPLY_YET: + filterObj.hasArticleReplyWithMorePositiveFeedback = false; + break; + case FILTERS.HAS_USEFUL_REPLY: + filterObj.hasArticleReplyWithMorePositiveFeedback = true; + break; + case FILTERS.ASKED_ONCE: + filterObj.replyRequestCount = { EQ: 1 }; + break; + case FILTERS.ASKED_MANY_TIMES: + filterObj.replyRequestCount = { GTE: 2 }; + break; + case FILTERS.NO_REPLY: + filterObj.replyCount = { EQ: 0 }; + break; + case FILTERS.REPLIED_MANY_TIMES: + filterObj.replyCount = { GTE: 3 }; + break; + default: { + const exhaustiveCheck: never = filter; + throw new Error(`Unhandled case: ${exhaustiveCheck}`); + } + } + } + + return filterObj; +} + +type Props = { + /** setting FILTERS.XXX false means that XXX option should be hidden in ArticleStatusFilter. */ + filterMap?: Partial>; +}; + +function ArticleStatusFilter({ filterMap = {} }: Props) { + const { query } = useRouter(); + const user = useCurrentUser(); + const selectedValues = getValues(query); + + // Disable login-only options when not logged in + let options = OPTIONS.filter(f => filterMap[f.value] !== false); + + if (!user) { + options = options.map(option => ({ + ...option, + disabled: LOGIN_ONLY_OPTIONS.includes(option.value), + })); + } + + return ( + { + MUTUALLY_EXCLUSIVE_FILTERS.forEach(mutuallyExclusiveFilters => { + for (const filter of mutuallyExclusiveFilters) { + if ( + !selectedValues.includes(filter) && + newValues.includes(filter) + ) { + // This filter is being toggled on; + // remove others in the same mutually exclusive filters set + newValues = newValues.filter(v => + mutuallyExclusiveFilters.includes(v) ? v === filter : true + ); + + // Found the toggled filter, can skip the rest. + break; + } + } + }); + + goToUrlQueryAndResetPagination({ + ...query, + [PARAM_NAME]: newValues.join(','), + }); + }} + data-ga="Filter(filter)" + /> + ); +} + +export default memo(ArticleStatusFilter); diff --git a/components/ListPageControls/BaseFilter.js b/components/ListPageControls/BaseFilter.tsx similarity index 92% rename from components/ListPageControls/BaseFilter.js rename to components/ListPageControls/BaseFilter.tsx index 6ebf4581..19e22e31 100644 --- a/components/ListPageControls/BaseFilter.js +++ b/components/ListPageControls/BaseFilter.tsx @@ -4,6 +4,8 @@ import { makeStyles } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import cx from 'clsx'; + +import { Theme } from 'lib/theme'; import BaseFilterOption from './BaseFilterOption'; const useStyles = makeStyles(theme => ({ @@ -86,20 +88,32 @@ const useStyles = makeStyles(theme => ({ * @param {Array} props.options * @param {(selected: string[]) => void} props.onChange */ -function BaseFilter({ + +type Props = { + title: string; + expandable?: boolean; + placeholder?: string; + selected: ReadonlyArray; + options: ReadonlyArray< + React.ComponentPropsWithoutRef + >; + onChange: (selected: V[]) => void; +}; + +function BaseFilter({ title, onChange = () => null, placeholder, expandable, selected = [], options = [], -}) { +}: Props) { const classes = useStyles(); const [expandEl, setExpandEl] = useState(null); // Note: this is implemented using JS, don't use it on places // that is going to cause flicker on page load! - const isDesktop = useMediaQuery(theme => theme.breakpoints.up('md')); + const isDesktop = useMediaQuery(theme => theme.breakpoints.up('md')); const isValueSelected = Object.fromEntries( selected.map(value => [value, true]) diff --git a/components/ListPageControls/BaseFilterOption.js b/components/ListPageControls/BaseFilterOption.tsx similarity index 67% rename from components/ListPageControls/BaseFilterOption.js rename to components/ListPageControls/BaseFilterOption.tsx index cebff7a6..5556a2ed 100644 --- a/components/ListPageControls/BaseFilterOption.js +++ b/components/ListPageControls/BaseFilterOption.tsx @@ -1,17 +1,29 @@ import { makeStyles } from '@material-ui/core/styles'; import Chip from '@material-ui/core/Chip'; +type Props = { + /** toggle chip-like border on display */ + chip?: boolean; + label: string; + value: V; + disabled?: boolean; + selected?: boolean; + onClick?: (value: string) => void; +}; + +type StyleProps = Pick, 'selected' | 'chip'>; + const useStyles = makeStyles(theme => ({ root: { height: 'auto' /* Override material-ui */, padding: '3px 9px' /* consider 1px border */, // Add background color on selected - backgroundColor: ({ selected }) => + backgroundColor: ({ selected }: StyleProps) => selected ? theme.palette.secondary[50] : '', // Hide border color when not chip and not selected - borderColor: ({ chip, selected }) => + borderColor: ({ chip, selected }: StyleProps) => !chip && !selected ? `transparent` : '', }, /* Chip label */ @@ -26,22 +38,14 @@ const useStyles = makeStyles(theme => ({ }, })); -/** - * @param {boolean} props.chip - toggle chip-like border on display - * @param {string} props.label - * @param {string} props.value - * @param {boolean} props.disabled - * @param {boolean} props.selected - * @param {(value: string) => void} props.onClick - */ -function BaseFilterOption({ +function BaseFilterOption({ chip, selected, label, value, disabled, - onClick = () => {}, -}) { + onClick = () => undefined, +}: Props) { const classes = useStyles({ chip, selected }); const handleClick = () => { onClick(value); diff --git a/components/ListPageControls/index.js b/components/ListPageControls/index.ts similarity index 72% rename from components/ListPageControls/index.js rename to components/ListPageControls/index.ts index e311e7a5..8d1a8a52 100644 --- a/components/ListPageControls/index.js +++ b/components/ListPageControls/index.ts @@ -1,7 +1,10 @@ import Tools from './Tools'; import Filters from './Filters'; import BaseFilter from './BaseFilter'; -import ArticleStatusFilter from './ArticleStatusFilter'; +import ArticleStatusFilter, { + getFilter as getArticleStatusFilter, + getValues as getArticleStatusFilterValues, +} from './ArticleStatusFilter'; import CategoryFilter from './CategoryFilter'; import ArticleTypeFilter from './ArticleTypeFilter'; import ReplyTypeFilter from './ReplyTypeFilter'; @@ -14,6 +17,8 @@ export { Filters, BaseFilter, ArticleStatusFilter, + getArticleStatusFilter, + getArticleStatusFilterValues, CategoryFilter, ArticleTypeFilter, ReplyTypeFilter, diff --git a/constants/articleFilters.js b/constants/articleFilters.ts similarity index 53% rename from constants/articleFilters.js rename to constants/articleFilters.ts index 6f4c04bf..539cde6a 100644 --- a/constants/articleFilters.js +++ b/constants/articleFilters.ts @@ -1,4 +1,8 @@ export const REPLIED_BY_ME = 'REPLIED_BY_ME'; -export const NO_USEFUL_REPLY_YET = 'NO_USEFUL_REPLY_YET'; +export const NOT_REPLIED_BY_ME = 'NOT_REPLIED_BY_ME'; +export const ASKED_ONCE = 'ASKED_ONCE'; export const ASKED_MANY_TIMES = 'ASKED_MANY_TIMES'; +export const NO_REPLY = 'NO_REPLY'; export const REPLIED_MANY_TIMES = 'REPLIED_MANY_TIMES'; +export const NO_USEFUL_REPLY_YET = 'NO_USEFUL_REPLY_YET'; +export const HAS_USEFUL_REPLY = 'HAS_USEFUL_REPLY'; diff --git a/i18n/zh_TW.po b/i18n/zh_TW.po index 406b4d43..46cd1ee3 100644 --- a/i18n/zh_TW.po +++ b/i18n/zh_TW.po @@ -12,9 +12,9 @@ msgid "About" msgstr "關於" #: components/ReportPage/SectionEcosystem.js:175 -#: pages/article/[id].js:354 -#: pages/reply/[id].js:207 -#: pages/reply/[id].js:236 +#: pages/article/[id].js:367 +#: pages/reply/[id].js:210 +#: pages/reply/[id].js:239 msgid "Cofacts" msgstr "Cofacts 真的假的" @@ -52,24 +52,24 @@ msgid "Sort by" msgstr "排序方式" #: components/ProfilePage/RepliedArticleTab.js:36 -#: pages/articles.js:166 -#: pages/replies.js:143 +#: pages/articles.js:143 +#: pages/replies.js:128 msgid "Most recently asked" msgstr "最近被查詢" #: components/ProfilePage/RepliedArticleTab.js:33 -#: pages/articles.js:168 -#: pages/replies.js:142 +#: pages/articles.js:145 +#: pages/replies.js:127 msgid "Most recently replied" msgstr "最近被回應" #: components/ProfilePage/RepliedArticleTab.js:37 -#: pages/articles.js:167 -#: pages/replies.js:144 +#: pages/articles.js:144 +#: pages/replies.js:129 msgid "Most asked" msgstr "最多人詢問" -#: pages/api/articles/[feed].js:165 +#: pages/api/articles/[feed].js:164 #. https://stackoverflow.com/a/54905457/1582110 msgid "Reported Message" msgstr "使用者回報訊息" @@ -82,7 +82,7 @@ msgstr "理由" msgid "Details" msgstr "詳細解釋" -#: pages/api/articles/[feed].js:178 +#: pages/api/articles/[feed].js:177 #. https://stackoverflow.com/a/54905457/1582110 msgid "Reference" msgstr "出處" @@ -129,7 +129,7 @@ msgid "Delete" msgstr "刪除" #: components/ArticleCategories/DownVoteDialog.js:99 -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:132 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:136 #: components/NewReplySection/ReplyForm/Submit.js:28 #: components/ProfilePage/EditAvatarDialog.js:241 #: components/ProfilePage/EditProfileDialog.js:98 @@ -141,7 +141,7 @@ msgstr "送出" msgid "Thank you for the feedback." msgstr "感謝您的意見。" -#: pages/article/[id].js:263 +#: pages/article/[id].js:269 msgid "Your reply has been submitted." msgstr "已經送出回應。" @@ -150,7 +150,7 @@ msgstr "已經送出回應。" msgid "Please login first." msgstr "請先登入。" -#: pages/search.js:302 +#: pages/search.js:306 msgid "Search" msgstr "搜尋" @@ -159,7 +159,7 @@ msgstr "搜尋" msgid "Searching for messages and replies containing “${ variables.query }”..." msgstr "正在搜尋含有「${ variables.query }」的訊息與回應⋯⋯" -#: components/AppLayout/LoginModal.js:109 +#: components/AppLayout/LoginModal.js:108 msgid "Login / Signup" msgstr "登入/註冊" @@ -182,7 +182,7 @@ msgstr "轉傳訊息或網路文章含有個人感想、假說猜測、陰謀論 msgid "This message has some of its content proved to be false." msgstr "轉傳訊息或網路文章有一部分含有不實資訊。" -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:279 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:297 msgid "Facebook" msgstr "Facebook" @@ -191,13 +191,13 @@ msgstr "Facebook" msgid "Tutorial" msgstr "使用教學" -#: pages/reply/[id].js:213 -#: pages/reply/[id].js:242 +#: pages/reply/[id].js:216 +#: pages/reply/[id].js:245 msgid "This reply" msgstr "本篇回應" -#: pages/api/articles/[feed].js:174 -#: pages/reply/[id].js:272 +#: pages/api/articles/[feed].js:173 +#: pages/reply/[id].js:275 msgid "someone" msgstr "有人" @@ -235,37 +235,38 @@ msgid "" msgstr "一起來幫網路上的可疑訊息寫查核回應、共同教導聊天機器人回話,將多元的闢謠資訊傳遞給在 LINE 上收到謠言的人。" #: components/ProfilePage/ProfilePage.js:103 -#: pages/article/[id].js:300 -#: pages/article/[id].js:304 -#: pages/reply/[id].js:167 -#: pages/reply/[id].js:171 +#: pages/article/[id].js:309 +#: pages/article/[id].js:313 +#: pages/reply/[id].js:170 +#: pages/reply/[id].js:174 +#: pages/terms.js:112 msgid "Loading" msgstr "載入中" -#: pages/article/[id].js:315 -#: pages/reply/[id].js:182 +#: pages/article/[id].js:324 +#: pages/reply/[id].js:185 msgid "Not found" msgstr "找不到此頁面" -#: pages/reply/[id].js:186 +#: pages/reply/[id].js:189 msgid "Reply does not exist" msgstr "此回應不存在" -#: pages/article/[id].js:319 +#: pages/article/[id].js:328 msgid "Message does not exist" msgstr "此訊息不存在" -#: pages/reply/[id].js:288 +#: pages/reply/[id].js:291 #, javascript-format msgid "Added by ${ editorElem }" msgstr "${editorElem} 加的" -#: pages/api/articles/[feed].js:133 +#: pages/api/articles/[feed].js:132 #. Use & for XML meta tags msgid "Cofacts reported messages" msgstr "Cofacts 真的假的回報訊息" -#: pages/api/articles/[feed].js:135 +#: pages/api/articles/[feed].js:134 #. Use & for XML meta tags msgid "List of messages reported by Cofacts users" msgstr "真的假的使用者所回報的訊息" @@ -274,7 +275,7 @@ msgstr "真的假的使用者所回報的訊息" msgid "Via Feedrabbit" msgstr "透過 Feedrabbit" -#: pages/api/articles/[feed].js:172 +#: pages/api/articles/[feed].js:171 #. https://stackoverflow.com/a/54905457/1582110 msgid "Latest reply" msgstr "最新回應" @@ -283,7 +284,7 @@ msgstr "最新回應" msgid "Fact Check" msgstr "闢謠" -#: components/AppLayout/AppFooter.js:142 +#: components/AppLayout/AppFooter.js:141 #: components/AppLayout/AppSidebar.js:136 msgid "Contact Us" msgstr "聯絡我們" @@ -293,11 +294,12 @@ msgstr "聯絡我們" msgid "My Profile" msgstr "我的主頁" -#: components/AppLayout/AppFooter.js:155 +#: components/AppLayout/AppFooter.js:154 msgid "Facebook forum" msgstr "Facebook 社團" -#: components/AppLayout/AppFooter.js:168 +#: components/AppLayout/AppFooter.js:171 +#: components/ReportPage/SectionSponsor.js:246 msgid "Donate to Cofacts" msgstr "捐款給 Cofacts" @@ -321,7 +323,7 @@ msgstr "含有正確訊息" msgid "contains misinformation" msgstr "含有錯誤訊息" -#: pages/replies.js:275 +#: pages/replies.js:260 msgid "Bust Hoaxes" msgstr "查核闢謠" @@ -331,26 +333,24 @@ msgstr "查核闢謠" msgid "Messages" msgstr "可疑訊息" -#: components/ListPageControls/ArticleStatusFilter.js:16 -#: pages/search.js:316 +#: components/ListPageControls/ArticleStatusFilter.tsx:25 msgid "Replied by me" msgstr "我查核過" -#: components/ListPageControls/ArticleStatusFilter.js:17 +#: components/ListPageControls/ArticleStatusFilter.tsx:23 msgid "No useful reply yet" -msgstr "還未有有效查核" +msgstr "尚無有效查核" -#: components/ListPageControls/ArticleStatusFilter.js:18 +#: components/ListPageControls/ArticleStatusFilter.tsx:20 msgid "Asked many times" msgstr "熱門回報" -#: components/ListPageControls/ArticleStatusFilter.js:19 +#: components/ListPageControls/ArticleStatusFilter.tsx:22 msgid "Replied many times" -msgstr "熱門討論" +msgstr "多份查核" -#: components/ListPageControls/ArticleStatusFilter.js:47 +#: components/ListPageControls/ArticleStatusFilter.tsx:127 #: components/ListPageControls/Filters.js:70 -#: pages/search.js:314 msgid "Filter" msgstr "篩選" @@ -371,8 +371,8 @@ msgstr "查看更多" msgid "Hoax for you" msgstr "等你來答" -#: pages/replies.js:226 -#: pages/replies.js:229 +#: pages/replies.js:211 +#: pages/replies.js:214 msgid "Latest replies" msgstr "最新查核" @@ -438,8 +438,8 @@ msgstr "在 可疑訊息" msgid "in Replies" msgstr "在 最新查核" -#: pages/articles.js:155 -#: pages/articles.js:157 +#: pages/articles.js:132 +#: pages/articles.js:134 msgid "Dubious Messages" msgstr "可疑訊息" @@ -454,11 +454,11 @@ msgstr "您認為沒有幫助的理由是什麼呢?" #: components/LandingPage/SectionArticles.js:153 #: components/ProfilePage/CommentTab.js:151 #: components/ProfilePage/RepliedArticleTab.js:251 -#: pages/articles.js:181 +#: pages/articles.js:158 #: pages/hoax-for-you.js:133 -#: pages/replies.js:246 -#: pages/search.js:181 -#: pages/search.js:258 +#: pages/replies.js:231 +#: pages/search.js:177 +#: pages/search.js:256 msgid "Loading..." msgstr "載入中⋯" @@ -492,17 +492,17 @@ msgstr[0] "被回報 ${ replyRequestCount } 次" msgid "Searching" msgstr "正在搜尋" -#: pages/article/[id].js:362 +#: pages/article/[id].js:377 #, javascript-format msgid "${ replyRequestCount } person report this message" msgid_plural "${ replyRequestCount } people report this message" msgstr[0] "有 ${ replyRequestCount } 人想知道以下訊息的真實性" -#: pages/article/[id].js:519 +#: pages/article/[id].js:542 msgid "Similar messages" msgstr "相似可疑訊息" -#: pages/article/[id].js:545 +#: pages/article/[id].js:568 msgid "No similar messages found" msgstr "沒有相似的可疑訊息" @@ -591,22 +591,22 @@ msgstr "此查核回應尚被用於以下的可疑訊息" msgid "Marked as" msgstr "被認為" -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:234 -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:292 -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:305 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:241 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:332 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:345 msgid "Reply to this message" msgstr "我要查核闢謠" -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:245 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:252 msgid "Update comment" msgstr "更新補充" -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:245 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:252 msgid "Comment" msgstr "我想補充" #: components/ArticleReply/ReplyShare.js:74 -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:253 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:260 msgid "Share" msgstr "分享" @@ -712,7 +712,7 @@ msgstr "${ totalVisits } 次瀏覽" #: components/ListPageDisplays/ReplySearchItem.js:103 #: components/ProfilePage/CommentTab.js:169 #: components/ProfilePage/RepliedArticleTab.js:269 -#: pages/replies.js:256 +#: pages/replies.js:241 #, javascript-format msgid "${ article.replyRequestCount } occurrence" msgid_plural "${ article.replyRequestCount } occurrences" @@ -725,25 +725,25 @@ msgid_plural "${ otherCount } other replies" msgstr[0] "" msgstr[1] "" -#: pages/reply/[id].js:248 +#: pages/reply/[id].js:251 msgid "The reply is used in the following messages" msgstr "此查核回應尚被用於以下的可疑訊息" -#: pages/reply/[id].js:262 +#: pages/reply/[id].js:265 msgid "Deleted by its author" msgstr "此回應已被原作者刪除。" -#: pages/reply/[id].js:307 +#: pages/reply/[id].js:310 msgid "Similar replies" msgstr "相似回應" -#: pages/reply/[id].js:320 +#: pages/reply/[id].js:323 #, javascript-format msgid "Used in ${ node.articleReplies.length } message" msgid_plural "Used in ${ node.articleReplies.length } messages" msgstr[0] "此查核回應曾經被用於 ${ node.articleReplies.length } 個可疑訊息" -#: pages/reply/[id].js:332 +#: pages/reply/[id].js:335 msgid "No similar replies found" msgstr "沒有相似的回應" @@ -767,15 +767,15 @@ msgstr "目標網站有 HTTPS 錯誤" msgid "Unknown error" msgstr "未知錯誤" -#: pages/article/[id].js:488 +#: pages/article/[id].js:511 #, javascript-format msgid "There is ${ replyCount } fact-checking reply to the message" msgid_plural "There are ${ replyCount } fact-checking replies to the message" msgstr[0] "本訊息有 ${ replyCount } 則查核回應" msgstr[1] "本訊息有 ${ replyCount } 則查核回應" -#: pages/article/[id].js:512 -#: pages/article/[id].js:555 +#: pages/article/[id].js:535 +#: pages/article/[id].js:578 msgid "Add Cofacts as friend in LINE" msgstr "加 LINE 查謠言" @@ -783,17 +783,17 @@ msgstr "加 LINE 查謠言" msgid "Past 31 days" msgstr "近 31 日" -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:132 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:136 msgid "Please provide more info" msgstr "字數不足" -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:198 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:205 msgid "" "Did you find anything suspicious about the message after you search " "Facebook & Google?" msgstr "搜尋臉書或 Google 後,你發現了什麼可疑的地方,想給其他編輯參考呢?" -#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:210 +#: components/CreateReplyRequestForm/CreateReplyRequestForm.js:217 msgid "" "Please provide paragraphs you find controversial, or related news, image & " "video material you have found." @@ -872,21 +872,21 @@ msgstr "我可以闢謠" msgid "I can help with coding" msgstr "我會寫 Code" -#: components/AppLayout/AppFooter.js:120 +#: components/AppLayout/AppFooter.js:118 msgid "Introduction" msgstr "專案介紹" -#: components/AppLayout/AppFooter.js:123 +#: components/AppLayout/AppFooter.js:121 #: components/ReportPage/SectionTower.js:107 msgid "Source Code" msgstr "原始碼" -#: components/AppLayout/AppFooter.js:130 +#: components/AppLayout/AppFooter.js:129 msgid "Contact" msgstr "聯繫" -#: pages/article/[id].js:503 -#: pages/reply/[id].js:303 +#: pages/article/[id].js:526 +#: pages/reply/[id].js:306 msgid "The content above" msgstr "以上內容" @@ -1044,12 +1044,13 @@ msgstr "CC BY-SA 4.0" msgid "Creative Commons Attribution-ShareAlike 4.0" msgstr "CC授權 姓名標示-相同方式分享 4.0" -#: components/AppLayout/AppFooter.js:117 -#: components/AppLayout/LoginModal.js:94 +#: components/AppLayout/AppFooter.js:116 +#: components/AppLayout/LoginModal.js:92 +#: pages/terms.js:109 msgid "User Agreement" msgstr "使用者條款" -#: components/AppLayout/LoginModal.js:152 +#: components/AppLayout/LoginModal.js:151 #, javascript-format msgid "" "By logging in you agree to ${ termsLink }, and your contribution will be " @@ -1366,10 +1367,6 @@ msgstr "" msgid "Dcard" msgstr "" -#: components/ReportPage/SectionSponsor.js:246 -msgid "Donate to Cofacts" -msgstr "捐款給 Cofacts" - #: components/ReportPage/SectionSponsor.js:255 msgid "Share to Facebook" msgstr "分享到 Facebook" @@ -2311,8 +2308,8 @@ msgstr "目前已經存有4萬5千筆以上查證過的資訊,透過投入資 #: components/ListPageDisplays/ReplySearchItem.js:112 #: components/ProfilePage/CommentTab.js:178 #: components/ProfilePage/RepliedArticleTab.js:278 -#: pages/article/[id].js:370 -#: pages/replies.js:263 +#: pages/article/[id].js:385 +#: pages/replies.js:248 msgid "First reported ${ timeAgo }" msgstr "首次回報於 ${ timeAgo }" @@ -2343,7 +2340,7 @@ msgid "In linked text" msgstr "所附網址的網頁內容" #: components/ArticleReply/ArticleReply.js:106 -#: pages/article/[id].js:285 +#: pages/article/[id].js:291 #. t: terms subject msgid "This info" msgstr "此資訊" @@ -2396,7 +2393,7 @@ msgid "" "the thumb-down button below each reply if you find it not good enough." msgstr "注意:這裏不是討論區!請針對上面的「可疑訊息」撰寫回覆,不要去回覆下面的查核回應。若只是想表達現有的查核回應不夠好,請使用該篇回應下方的「倒讚」按鈕。" -#: pages/article/[id].js:438 +#: pages/article/[id].js:460 msgid "Comments from people reporting this message" msgstr "網友回報補充" @@ -2414,11 +2411,11 @@ msgstr[0] "${ totalCount } 則符合篩選條件的訊息" msgid "Report abuse" msgstr "檢舉違規內容" -#: pages/article/[id].js:399 +#: pages/article/[id].js:418 msgid "Log in to view video content" msgstr "登入以檢視影片內容" -#: pages/article/[id].js:409 +#: pages/article/[id].js:428 msgid "Log in to view audio content" msgstr "登入以檢視錄音內容" @@ -2427,11 +2424,11 @@ msgid "A video" msgstr "影片一則" #: components/Thumbnail.js:37 -#: components/Thumbnail.js:49 +#: components/Thumbnail.js:50 msgid "Preview not supported yet" msgstr "尚未支援預覽" -#: components/Thumbnail.js:49 +#: components/Thumbnail.js:50 msgid "An audio" msgstr "語音訊息一則" @@ -2480,6 +2477,35 @@ msgid "${ article.replyCount } reply" msgid_plural "${ article.replyCount } replies" msgstr[0] "${ article.replyCount } 則回應" +#: pages/terms.js:103 +msgid "Github" +msgstr "" + +#: pages/terms.js:114 +#, javascript-format +msgid "See ${ revisionLink } for other revisions of the user agreement." +msgstr "" + +#: pages/article/[id].js:391 +msgid "Log in to view content" +msgstr "" + +#: components/ListPageControls/ArticleStatusFilter.tsx:19 +msgid "Asked only once" +msgstr "僅一人回報" + +#: components/ListPageControls/ArticleStatusFilter.tsx:21 +msgid "Zero replies" +msgstr "無人查核" + +#: components/ListPageControls/ArticleStatusFilter.tsx:24 +msgid "Has useful replies" +msgstr "查核有效" + +#: components/ListPageControls/ArticleStatusFilter.tsx:26 +msgid "Not replied by me" +msgstr "我沒查過" + #: pages/index.js:29 msgctxt "site title" msgid "Cofacts" diff --git a/lib/theme.js b/lib/theme.tsx similarity index 81% rename from lib/theme.js rename to lib/theme.tsx index ab784ce3..fb12f28f 100644 --- a/lib/theme.js +++ b/lib/theme.tsx @@ -14,7 +14,7 @@ export const cofactsColors = { blue2: '#2daef7', blue3: '#5fd8ff', purple: '#966dee', -}; +} as const; const baseThemeOption = { palette: { @@ -60,7 +60,25 @@ const baseThemeOption = { shape: { borderRadius: 8, }, -}; +} as const; + +// https://v4.mui.com/guides/typescript/#customization-of-theme +declare module '@material-ui/core/styles/createPalette' { + interface CommonColors { + red1: string; + red2: string; + orange1: string; + orange2: string; + yellow: string; + green1: string; + green2: string; + green3: string; + blue1: string; + blue2: string; + blue3: string; + purple: string; + } +} // Create a theme instance. export const lightTheme = createTheme(baseThemeOption); @@ -77,6 +95,8 @@ export const darkTheme = createTheme({ }, }); +export type Theme = typeof lightTheme; + export function withDarkTheme(WrappedComponent) { function Component(props) { return ( diff --git a/pages/articles.js b/pages/articles.js index e4c04fc4..e120de77 100644 --- a/pages/articles.js +++ b/pages/articles.js @@ -5,7 +5,6 @@ import { t } from 'ttag'; import { useQuery } from '@apollo/react-hooks'; import useCurrentUser from 'lib/useCurrentUser'; -import * as FILTERS from 'constants/articleFilters'; import { ListPageCards, ArticleCard, @@ -15,6 +14,7 @@ import { Tools, Filters, ArticleStatusFilter, + getArticleStatusFilter, CategoryFilter, ArticleTypeFilter, ReplyTypeFilter, @@ -63,34 +63,11 @@ const LIST_STAT = gql` * @returns {object} ListArticleFilter */ function urlQuery2Filter({ userId, ...query } = {}) { - const filterObj = {}; + const filterObj = getArticleStatusFilter(query, userId); const selectedCategoryIds = CategoryFilter.getValues(query); if (selectedCategoryIds.length) filterObj.categoryIds = selectedCategoryIds; - const selectedFilters = ArticleStatusFilter.getValues(query); - selectedFilters.forEach(filter => { - switch (filter) { - case FILTERS.REPLIED_BY_ME: - if (!userId) break; - filterObj.articleRepliesFrom = { - userId: userId, - exists: true, - }; - break; - case FILTERS.NO_USEFUL_REPLY_YET: - filterObj.hasArticleReplyWithMorePositiveFeedback = false; - break; - case FILTERS.ASKED_MANY_TIMES: - filterObj.replyRequestCount = { GTE: 2 }; - break; - case FILTERS.REPLIED_MANY_TIMES: - filterObj.replyCount = { GTE: 3 }; - break; - default: - } - }); - const [start, end] = TimeRange.getValues(query); if (start) { diff --git a/pages/replies.js b/pages/replies.js index 4a1a2b17..10f05dc2 100644 --- a/pages/replies.js +++ b/pages/replies.js @@ -21,6 +21,7 @@ import { Tools, Filters, ArticleStatusFilter, + getArticleStatusFilter, CategoryFilter, ArticleTypeFilter, ReplyTypeFilter, @@ -79,42 +80,26 @@ const LIST_STAT = gql` } `; +/** Toggle options in ArticleStatusFilter */ +const ARTICLE_STATUS_FILTER_MAP = { + [FILTERS.NO_REPLY]: false, +}; + /** * @param {object} urlQuery - URL query object and urserId * @returns {object} ListArticleFilter */ function urlQuery2Filter({ userId, ...query } = {}) { - const filterObj = { - // Default filters - replyCount: { GTE: 1 }, - }; + const filterObj = getArticleStatusFilter(query, userId); + + // Default filters + if (!filterObj.replyCount) { + filterObj.replyCount = { GTE: 1 }; + } const selectedCategoryIds = CategoryFilter.getValues(query); if (selectedCategoryIds.length) filterObj.categoryIds = selectedCategoryIds; - const selectedFilters = ArticleStatusFilter.getValues(query); - selectedFilters.forEach(filter => { - switch (filter) { - case FILTERS.REPLIED_BY_ME: - if (!userId) break; - filterObj.articleRepliesFrom = { - userId: userId, - exists: true, - }; - break; - case FILTERS.NO_USEFUL_REPLY_YET: - filterObj.hasArticleReplyWithMorePositiveFeedback = false; - break; - case FILTERS.ASKED_MANY_TIMES: - filterObj.replyRequestCount = { GTE: 2 }; - break; - case FILTERS.REPLIED_MANY_TIMES: - filterObj.replyCount = { GTE: 3 }; - break; - default: - } - }); - const [start, end] = TimeRange.getValues(query); if (start) { @@ -236,7 +221,7 @@ function ReplyListPage() { - + diff --git a/pages/search.js b/pages/search.js index 51b4e309..60d31622 100644 --- a/pages/search.js +++ b/pages/search.js @@ -1,5 +1,4 @@ import gql from 'graphql-tag'; -import { useState } from 'react'; import { useRouter } from 'next/router'; import Head from 'next/head'; import { t } from 'ttag'; @@ -7,6 +6,8 @@ import { useQuery } from '@apollo/react-hooks'; import Box from '@material-ui/core/Box'; import { Container } from '@material-ui/core'; + +import * as FILTERS from 'constants/articleFilters'; import { ListPageCards, ArticleCard, @@ -15,7 +16,9 @@ import { import { Tools, Filters, - BaseFilter, + getArticleStatusFilter, + getArticleStatusFilterValues, + ArticleStatusFilter, CategoryFilter, ReplyTypeFilter, TimeRange, @@ -137,23 +140,16 @@ function urlQuery2Filter(query = {}) { * Search for matching articles and list the result * * @param {object} props.query - URL param object - * @param {string?} props.userId - If given, list only articles replied by this user ID + * @param {string?} props.userId - Currently logged in userId */ function MessageSearchResult({ query, userId }) { const replyTypes = ReplyTypeFilter.getValues(query); const listQueryVars = { filter: { ...urlQuery2Filter(query), + ...getArticleStatusFilter(query, userId), categoryIds: CategoryFilter.getValues(query), ...(replyTypes.length ? { replyTypes } : {}), - ...(userId - ? { - articleRepliesFrom: { - userId, - exists: true, - }, - } - : {}), }, }; @@ -222,15 +218,17 @@ function MessageSearchResult({ query, userId }) { * Search for matching replies and list the result * * @param {object} props.query - URL param object - * @param {boolean} props.selfOnly - If true, list only replies written by current user + * @param {boolean} props.isLoggedIn */ -function ReplySearchResult({ query, selfOnly }) { +function ReplySearchResult({ query, isLoggedIn }) { const types = ReplyTypeFilter.getValues(query); + const articleStatusValues = getArticleStatusFilterValues(query); const listQueryVars = { filter: { ...urlQuery2Filter(query), ...(types.length ? { types } : {}), - selfOnly, + selfOnly: + isLoggedIn && articleStatusValues.includes(FILTERS.REPLIED_BY_ME), }, }; @@ -291,10 +289,16 @@ function ReplySearchResult({ query, selfOnly }) { ); } +// Only "reply by me" filter for filter search +const ARTICLE_STATUS_FILTER_MAP_FOR_REPLIES = { + ...Object.fromEntries(Object.keys(FILTERS).map(key => [key, false])), + [FILTERS.REPLIED_BY_ME]: true, + [FILTERS.NOT_REPLIED_BY_ME]: true, +}; + function SearchPage() { const { query } = useRouter(); const user = useCurrentUser(); - const [selfOnly, setSelfOnly] = useState(false); return ( @@ -310,26 +314,21 @@ function SearchPage() { - setSelfOnly(values.includes('self'))} + {query.type === 'messages' && } - {query.type === 'messages' && ( - + )} {query.type === 'replies' && ( - + )} diff --git a/tsconfig.json b/tsconfig.json index 35d51eac..68e7ac64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "es5", + "baseUrl": ".", "lib": [ "dom", "dom.iterable",